diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dfb3ba6..d0596cf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,8 @@ endif () if (SLIC3R_GUI) add_subdirectory(libvgcode) + add_subdirectory(slic3r-arrange) + add_subdirectory(slic3r-arrange-wrapper) if(WIN32) message(STATUS "WXWIN environment set to: $ENV{WXWIN}") @@ -109,7 +111,7 @@ if (NOT WIN32 AND NOT APPLE) set_target_properties(QIDISlicer PROPERTIES OUTPUT_NAME "qidi-slicer") endif () -target_link_libraries(QIDISlicer libslic3r libcereal) +target_link_libraries(QIDISlicer libslic3r libcereal slic3r-arrange-wrapper) if (APPLE) # add_compile_options(-stdlib=libc++) diff --git a/src/QIDISlicer.cpp b/src/QIDISlicer.cpp index a0e4f0b..9fa95d2 100644 --- a/src/QIDISlicer.cpp +++ b/src/QIDISlicer.cpp @@ -50,7 +50,7 @@ #include "libslic3r/GCode/PostProcessor.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/CutUtils.hpp" -#include "libslic3r/ModelArrange.hpp" +#include #include "libslic3r/Platform.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/SLAPrint.hpp" @@ -60,12 +60,14 @@ #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/Format/SL1.hpp" +#include "libslic3r/miniz_extension.hpp" +#include "libslic3r/PNGReadWrite.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Thread.hpp" #include "libslic3r/BlacklistedLibraryCheck.hpp" #include "libslic3r/ProfilesSharingUtils.hpp" #include "libslic3r/Utils/DirectoriesUtils.hpp" - +#include "libslic3r/MultipleBeds.hpp" #include "QIDISlicer.hpp" @@ -94,6 +96,10 @@ int CLI::run(int argc, char **argv) // startup if gtk3 is used. This env var has to be set explicitly to // instruct the window manager to fall back to X server mode. ::setenv("GDK_BACKEND", "x11", /* replace */ true); + + // https://github.com/prusa3d/PrusaSlicer/issues/12969 + ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false); + ::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", /* replace */ false); #endif // Switch boost::filesystem to utf8. @@ -387,7 +393,9 @@ int CLI::run(int argc, char **argv) // Loop through transform options. bool user_center_specified = false; - arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(m_print_config)); + + const Vec2crd gap{s_multiple_beds.get_bed_gap()}; + arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(m_print_config), gap); arr2::ArrangeSettings arrange_cfg; arrange_cfg.set_distance_from_objects(min_object_distance(m_print_config)); @@ -609,9 +617,6 @@ int CLI::run(int argc, char **argv) model.add_default_instances(); if (! this->export_models(IO::OBJ)) return 1; - } else if (opt_key == "export_amf") { - if (! this->export_models(IO::AMF)) - return 1; } else if (opt_key == "export_3mf") { if (! this->export_models(IO::TMF)) return 1; @@ -641,8 +646,10 @@ int CLI::run(int argc, char **argv) sla_print.set_status_callback( [](const PrintBase::SlicingStatus& s) { - if(s.percent >= 0) // FIXME: is this sufficient? + if(s.percent >= 0) { // FIXME: is this sufficient? printf("%3d%s %s\n", s.percent, "% =>", s.text.c_str()); + std::fflush(stdout); + } }); PrintBase *print = (printer_technology == ptFFF) ? static_cast(&fff_print) : static_cast(&sla_print); @@ -670,8 +677,66 @@ int CLI::run(int argc, char **argv) std::string outfile_final; print->process(); if (printer_technology == ptFFF) { + + + + + + std::function thumbnail_generator_cli; + if (!fff_print.model().objects.empty() && boost::iends_with(fff_print.model().objects.front()->input_file, ".3mf")) { + std::string filename = fff_print.model().objects.front()->input_file; + thumbnail_generator_cli = [filename](const ThumbnailsParams&) { + ThumbnailsList list_out; + + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, filename)) + return list_out; + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + mz_zip_archive_file_stat stat; + + int index = mz_zip_reader_locate_file(&archive, "Metadata/thumbnail.png", nullptr, 0); + if (index < 0 || !mz_zip_reader_file_stat(&archive, index, &stat)) + return list_out; + std::string buffer; + buffer.resize(int(stat.m_uncomp_size)); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) + return list_out; + close_zip_reader(&archive); + + std::vector data; + unsigned width = 0; + unsigned height = 0; + png::decode_png(buffer, data, width, height); + + { + // Flip the image vertically so it matches the convention in Thumbnails generator. + const int row_size = width * 4; // Each pixel is 4 bytes (RGBA) + std::vector temp_row(row_size); + for (int i = 0; i < height / 2; ++i) { + unsigned char* top_row = &data[i * row_size]; + unsigned char* bottom_row = &data[(height - i - 1) * row_size]; + std::copy(bottom_row, bottom_row + row_size, temp_row.begin()); + std::copy(top_row, top_row + row_size, bottom_row); + std::copy(temp_row.begin(), temp_row.end(), top_row); + } + } + + ThumbnailData th; + th.set(width, height); + th.pixels = data; + list_out.push_back(th); + return list_out; + }; + } + + + + // The outfile is processed by a PlaceholderParser. - outfile = fff_print.export_gcode(outfile, nullptr, nullptr); + outfile = fff_print.export_gcode(outfile, nullptr, thumbnail_generator_cli); outfile_final = fff_print.print_statistics().finalize_output_path(outfile); } else { outfile = sla_print.output_filepath(outfile); @@ -754,6 +819,11 @@ int CLI::run(int argc, char **argv) params.load_configs = load_configs; params.extra_config = std::move(m_extra_config); params.input_files = std::move(m_input_files); + if (has_config_from_profiles && params.input_files.empty()) { + params.selected_presets = Slic3r::GUI::CLISelectedProfiles{ m_config.opt_string("print-profile"), + m_config.opt_string("printer-profile") , + m_config.option("material-profile")->values }; + } params.start_as_gcodeviewer = start_as_gcodeviewer; params.start_downloader = start_downloader; params.download_url = download_url; @@ -953,7 +1023,6 @@ bool CLI::export_models(IO::ExportFormat format) const std::string path = this->output_filepath(model, format); bool success = false; switch (format) { - case IO::AMF: success = Slic3r::store_amf(path.c_str(), &model, nullptr, false); break; case IO::OBJ: success = Slic3r::store_obj(path.c_str(), &model); break; case IO::STL: success = Slic3r::store_stl(path.c_str(), &model, true); break; case IO::TMF: success = Slic3r::store_3mf(path.c_str(), &model, nullptr, false); break; @@ -973,7 +1042,6 @@ std::string CLI::output_filepath(const Model &model, IO::ExportFormat format) co { std::string ext; switch (format) { - case IO::AMF: ext = ".zip.amf"; break; case IO::OBJ: ext = ".obj"; break; case IO::STL: ext = ".stl"; break; case IO::TMF: ext = ".3mf"; break; diff --git a/src/QIDISlicer.hpp b/src/QIDISlicer.hpp index e6138fa..2df4ae4 100644 --- a/src/QIDISlicer.hpp +++ b/src/QIDISlicer.hpp @@ -8,7 +8,6 @@ namespace Slic3r { namespace IO { enum ExportFormat : int { - AMF, OBJ, STL, // SVG, diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 2377882..894b892 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -388,11 +388,13 @@ BuildVolume::ObjectState BuildVolume::volume_state_bbox(const BoundingBoxf3 volu // B66 BuildVolume::ObjectState BuildVolume::check_outside(Polygon hull) const { + //y21 + const Vec3d bed_offset = s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()); if (m_exclude_bed_shape.size() > 0) { for (int i = 1; i < m_exclude_bed_shape.size(); i += 7) { std::vector tem_exclude_bed_shap; for (int j = 1; j < 6; j++) - tem_exclude_bed_shap.push_back(m_exclude_bed_shape[i + j]); + tem_exclude_bed_shap.push_back(Vec2d{ m_exclude_bed_shape[i + j].x()+ bed_offset.x(), m_exclude_bed_shape[i + j].y()+ bed_offset.y()}); BoundingBoxf tem_bboxf = get_extents(tem_exclude_bed_shap); auto tem_exclude_bboxf = BoundingBoxf3{to_3d(tem_bboxf.min, 0.), to_3d(tem_bboxf.max, m_max_print_height)}; Slic3r::Polygon p = tem_exclude_bboxf.polygon(true); // instance convex hull is scaled, so we need to scale here diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index dd80818..dfc5521 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -20,6 +20,10 @@ set(SLIC3R_GUI_SOURCES GUI/UserAccount.hpp GUI/WebViewDialog.cpp GUI/WebViewDialog.hpp + GUI/WebViewPanel.cpp + GUI/WebViewPanel.hpp + GUI/ConnectRequestHandler.cpp + GUI/ConnectRequestHandler.hpp GUI/WebView.cpp GUI/WebView.hpp GUI/WebViewPlatformUtils.hpp @@ -70,6 +74,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSlaSupports.hpp GUI/Gizmos/GLGizmoFdmSupports.cpp GUI/Gizmos/GLGizmoFdmSupports.hpp + GUI/Gizmos/GLGizmoFuzzySkin.cpp + GUI/Gizmos/GLGizmoFuzzySkin.hpp GUI/Gizmos/GLGizmoFlatten.cpp GUI/Gizmos/GLGizmoFlatten.hpp GUI/Gizmos/GLGizmoCut.cpp @@ -138,6 +144,8 @@ set(SLIC3R_GUI_SOURCES GUI/EditGCodeDialog.cpp GUI/SavePresetDialog.hpp GUI/SavePresetDialog.cpp + GUI/BulkExportDialog.hpp + GUI/BulkExportDialog.cpp GUI/PhysicalPrinterDialog.hpp GUI/PhysicalPrinterDialog.cpp GUI/GUI_Factories.cpp @@ -368,6 +376,8 @@ set(SLIC3R_GUI_SOURCES Utils/Udp.hpp Utils/PresetUpdater.cpp Utils/PresetUpdater.hpp + Utils/PresetUpdaterWrapper.cpp + Utils/PresetUpdaterWrapper.hpp Utils/Process.cpp Utils/Process.hpp Utils/RaycastManager.cpp @@ -395,6 +405,9 @@ set(SLIC3R_GUI_SOURCES ) find_package(NanoSVG REQUIRED) +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(OpenSSL REQUIRED) +endif() if(QDT_RELEASE_TO_PUBLIC) list(APPEND SLIC3R_GUI_SOURCES @@ -447,6 +460,7 @@ target_link_libraries( libslic3r_gui PUBLIC libslic3r + slic3r-arrange-wrapper avrdude libcereal imgui @@ -460,12 +474,17 @@ target_link_libraries( NanoSVG::nanosvgrast stb_dxt fastfloat + boost_headeronly ) if (MSVC) target_link_libraries(libslic3r_gui PUBLIC Setupapi.lib) elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(libslic3r_gui PUBLIC ${DBUS_LIBRARIES}) + target_link_libraries(libslic3r_gui PUBLIC + ${DBus1_LIBRARIES} + OpenSSL::SSL + OpenSSL::Crypto + ) elseif (APPLE) target_link_libraries(libslic3r_gui PUBLIC ${DISKARBITRATION_LIBRARY} ${COREWLAN_LIBRARY}) endif() diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp index 47ea15a..d9e5c8d 100644 --- a/src/slic3r/Config/Version.cpp +++ b/src/slic3r/Config/Version.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "libslic3r/libslic3r.h" diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index d961ef1..2470606 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Geometry/Circle.hpp" #include "libslic3r/Tesselate.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/MultipleBeds.hpp" #include "GUI_App.hpp" #include "GLCanvas3D.hpp" @@ -20,11 +21,14 @@ #include #include +#include + static const float GROUND_Z = -0.02f; static const Slic3r::ColorRGBA DEFAULT_MODEL_COLOR = Slic3r::ColorRGBA::DARK_GRAY(); static const Slic3r::ColorRGBA PICKING_MODEL_COLOR = Slic3r::ColorRGBA::BLACK(); static const Slic3r::ColorRGBA DEFAULT_SOLID_GRID_COLOR = { 0.9f, 0.9f, 0.9f, 1.0f }; static const Slic3r::ColorRGBA DEFAULT_TRANSPARENT_GRID_COLOR = { 0.9f, 0.9f, 0.9f, 0.6f }; +static const Slic3r::ColorRGBA DISABLED_MODEL_COLOR = { 0.6f, 0.6f, 0.6f, 0.75f }; namespace Slic3r { namespace GUI { @@ -90,41 +94,133 @@ bool Bed3D::set_shape(const Pointfs& bed_shape, const double max_print_height, c m_texture.reset(); m_model.reset(); + // unregister from picking + wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); + + init_internal_model_from_file(); + init_triangles(); + + s_multiple_beds.update_build_volume(m_build_volume.bounding_volume2d()); + + m_models_overlap = false; + if (! m_model_filename.empty()) { + // Calculate bb of the bed model and figure out if the models would overlap when rendered next to each other. + const BoundingBoxf3& mdl_bb3 = m_model.model.get_bounding_box(); + const BoundingBoxf model_bb(Vec2d(mdl_bb3.min.x(), mdl_bb3.min.y()), Vec2d(mdl_bb3.max.x(), mdl_bb3.max.y())); + BoundingBoxf bed_bb = m_build_volume.bounding_volume2d(); + bed_bb.translate(-m_model_offset.x(), -m_model_offset.y()); + Vec2d gap = unscale(s_multiple_beds.get_bed_gap()); + m_models_overlap = (model_bb.size().x() - bed_bb.size().x() > 2 * gap.x() || model_bb.size().y() - bed_bb.size().y() > 2 * gap.y()); + } + // Set the origin and size for rendering the coordinate system axes. m_axes.set_origin({ 0.0, 0.0, static_cast(GROUND_Z) }); m_axes.set_stem_length(0.1f * static_cast(m_build_volume.bounding_volume().max_size())); - // unregister from picking - wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); - // Let the calee to update the UI. return true; } void Bed3D::render(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, float scale_factor, bool show_texture) { - render_internal(canvas, view_matrix, projection_matrix, bottom, scale_factor, show_texture, false); + bool is_thumbnail = s_multiple_beds.get_thumbnail_bed_idx() != -1; + bool is_preview = wxGetApp().plater()->is_preview_shown(); + int bed_to_highlight = s_multiple_beds.get_active_bed(); + + static std::vector beds_to_render; + beds_to_render.clear(); + if (is_thumbnail) + beds_to_render.push_back(s_multiple_beds.get_thumbnail_bed_idx()); + else if (is_preview) + beds_to_render.push_back(s_multiple_beds.get_active_bed()); + else { + beds_to_render.resize(s_multiple_beds.get_number_of_beds() + int(s_multiple_beds.should_show_next_bed())); + std::iota(beds_to_render.begin(), beds_to_render.end(), 0); + } + + for (int i : beds_to_render) { + Transform3d mat = view_matrix; + mat.translate(s_multiple_beds.get_bed_translation(i)); + render_internal(canvas, mat, projection_matrix, bottom, scale_factor, show_texture, false, is_thumbnail || i == bed_to_highlight); + } + + if (m_digits_models.empty()) { + for (size_t i = 0; i < 10; ++i) { + GLModel::Geometry g; + g.format.vertex_layout = GLModel::Geometry::EVertexLayout::P3T2; + const double digit_part = 94./1024.; + g.add_vertex(Vec3f(0, 0, 0), Vec2f(digit_part * i, 1.)); + g.add_vertex(Vec3f(1, 0, 0), Vec2f(digit_part * (i+1), 1.)); + g.add_vertex(Vec3f(1, 1, 0), Vec2f(digit_part * (i+1), 0)); + g.add_vertex(Vec3f(0, 1, 0), Vec2f(digit_part * i, 0)); + g.add_triangle(0, 1, 3); + g.add_triangle(3, 1, 2); + m_digits_models.emplace_back(std::make_unique()); + m_digits_models.back()->init_from(std::move(g)); + m_digits_models.back()->set_color(ColorRGBA(0.5f, 0.5f, 0.5f, 0.5f)); + } + m_digits_texture = std::make_unique(); + m_digits_texture->load_from_file(resources_dir() + "/icons/numbers.png", true, GLTexture::ECompressionType::None, false); + m_digits_texture->send_compressed_data_to_gpu(); + } + if (!is_thumbnail && s_multiple_beds.get_number_of_beds() > 1) { + GLShaderProgram* shader = wxGetApp().get_shader("flat_texture"); + shader->start_using(); + shader->set_uniform("projection_matrix", projection_matrix); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_digits_texture->get_id())); + + const BoundingBoxf bb = this->build_volume().bounding_volume2d(); + + for (int i : beds_to_render) { + if (i + 1 >= m_digits_models.size()) + break; + + double size_x = std::max(10., std::min(bb.size().x(), bb.size().y()) * 0.11); + double aspect = 1.2; + Transform3d mat = view_matrix; + mat.translate(Vec3d(bb.min.x(), bb.min.y(), 0.)); + 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.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)); + glsafe(::glDisable(GL_DEPTH_TEST)); + shader->stop_using(); + } } void Bed3D::render_for_picking(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, float scale_factor) { - render_internal(canvas, view_matrix, projection_matrix, bottom, scale_factor, false, true); + render_internal(canvas, view_matrix, projection_matrix, bottom, scale_factor, false, true, false); } void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, float scale_factor, - bool show_texture, bool picking) + bool show_texture, bool picking, bool active) { m_scale_factor = scale_factor; glsafe(::glEnable(GL_DEPTH_TEST)); m_model.model.set_color(picking ? PICKING_MODEL_COLOR : DEFAULT_MODEL_COLOR); + m_triangles.set_color(picking ? PICKING_MODEL_COLOR : DEFAULT_MODEL_COLOR); + if (!picking && !active) { + m_model.model.set_color(DISABLED_MODEL_COLOR); + m_triangles.set_color(DISABLED_MODEL_COLOR); + } switch (m_type) { - case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom, show_texture); break; } + case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom, show_texture, active); break; } default: - case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom, show_texture, picking); break; } + case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom, show_texture, picking, active); break; } } glsafe(::glDisable(GL_DEPTH_TEST)); @@ -201,7 +297,6 @@ void Bed3D::init_triangles() register_raycasters_for_picking(init_data, Transform3d::Identity()); m_triangles.init_from(std::move(init_data)); - m_triangles.set_color(DEFAULT_MODEL_COLOR); } void Bed3D::init_gridlines() @@ -308,18 +403,23 @@ void Bed3D::render_axes() m_axes.render(Transform3d::Identity(), 0.25f); } -void Bed3D::render_system(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture) +void Bed3D::render_system(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool is_active) { + if (m_models_overlap && s_multiple_beds.get_number_of_beds() + int(s_multiple_beds.should_show_next_bed()) > 1) { + render_default(bottom, false, show_texture, view_matrix, projection_matrix); + return; + } + if (!bottom) render_model(view_matrix, projection_matrix); if (show_texture) - render_texture(bottom, canvas, view_matrix, projection_matrix); + render_texture(bottom, canvas, view_matrix, projection_matrix, is_active); else if (bottom) render_contour(view_matrix, projection_matrix); } -void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix) +void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool is_active) { if (m_texture_filename.empty()) { m_texture.reset(); @@ -372,6 +472,7 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& v else if (m_texture.unsent_compressed_data_available()) { // sends to gpu the already available compressed levels of the main texture m_texture.send_compressed_data_to_gpu(); + wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_REGENERATE_BED_THUMBNAILS)); // the temporary texture is not needed anymore, reset it if (m_temp_texture.get_id() != 0) @@ -387,7 +488,7 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& v shader->start_using(); shader->set_uniform("view_model_matrix", view_matrix); shader->set_uniform("projection_matrix", projection_matrix); - shader->set_uniform("transparent_background", bottom); + shader->set_uniform("transparent_background", bottom || ! is_active); shader->set_uniform("svg_source", boost::algorithm::iends_with(m_texture.get_source(), ".svg")); glsafe(::glEnable(GL_DEPTH_TEST)); @@ -420,7 +521,7 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& v } } -void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix) +void Bed3D::init_internal_model_from_file() { if (m_model_filename.empty()) return; @@ -445,6 +546,14 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj // update extended bounding box m_extended_bounding_box = this->calc_extended_bounding_box(); } +} + +void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix) +{ + if (m_model_filename.empty()) + return; + + init_internal_model_from_file(); if (!m_model.model.get_filename().empty()) { GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); @@ -462,9 +571,10 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj } } -void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool picking) +void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool picking, bool is_active) { - if (m_texture_filename.empty() && m_model_filename.empty()) { + if ((m_texture_filename.empty() && m_model_filename.empty()) + || (m_models_overlap && s_multiple_beds.get_number_of_beds() + int(s_multiple_beds.should_show_next_bed()) > 1)) { render_default(bottom, picking, show_texture, view_matrix, projection_matrix); return; } @@ -473,7 +583,7 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co render_model(view_matrix, projection_matrix); if (show_texture) - render_texture(bottom, canvas, view_matrix, projection_matrix); + render_texture(bottom, canvas, view_matrix, projection_matrix, is_active); else if (bottom) render_contour(view_matrix, projection_matrix); } @@ -496,7 +606,7 @@ void Bed3D::render_default(bool bottom, bool picking, bool show_texture, const T glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - const bool has_model = !m_model.model.get_filename().empty(); + const bool has_model = !m_model.model.get_filename().empty() && ! m_models_overlap; if (!has_model && !bottom) { // draw background glsafe(::glDepthMask(GL_FALSE)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 87b3c94..64b36be 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -33,6 +33,7 @@ private: Type m_type{ Type::Custom }; std::string m_texture_filename; std::string m_model_filename; + bool m_models_overlap; // Print volume bounding box exteded with axes and model. BoundingBoxf3 m_extended_bounding_box; // Print bed polygon @@ -49,6 +50,9 @@ private: float m_scale_factor{ 1.0f }; + std::vector> m_digits_models; + std::unique_ptr m_digits_texture; + public: Bed3D() = default; ~Bed3D() = default; @@ -81,13 +85,14 @@ private: void init_triangles(); void init_gridlines(); void init_contourlines(); + void init_internal_model_from_file(); static std::tuple detect_type(const Pointfs& shape); void render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, float scale_factor, - bool show_texture, bool picking); - void render_system(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture); - void render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix); + bool show_texture, bool picking, bool active); + void render_system(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool is_active); + void render_texture(bool bottom, GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool is_active); void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool picking); + void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_texture, bool picking, bool is_active); void render_default(bool bottom, bool picking, bool show_texture, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_contour(const Transform3d& view_matrix, const Transform3d& projection_matrix); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 12ecbdc..dfdd2f6 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -23,6 +23,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Tesselate.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/MultipleBeds.hpp" #include #include @@ -238,7 +239,6 @@ GLVolume::GLVolume(float r, float g, float b, float a) , is_outside(false) , hover(HS_None) , is_modifier(false) - , is_wipe_tower(false) , is_extrusion_path(false) , force_native_color(false) , force_neutral_color(false) @@ -489,13 +489,13 @@ int GLVolumeCollection::load_object_volume( } #if SLIC3R_OPENGL_ES -int GLVolumeCollection::load_wipe_tower_preview( +GLVolume* GLVolumeCollection::load_wipe_tower_preview( float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, - float rotation_angle, bool size_unknown, float brim_width, TriangleMesh* out_mesh) + float rotation_angle, bool size_unknown, float brim_width, size_t idx, TriangleMesh* out_mesh) #else -int GLVolumeCollection::load_wipe_tower_preview( +GLVolume* GLVolumeCollection::load_wipe_tower_preview( float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, - float rotation_angle, bool size_unknown, float brim_width) + float rotation_angle, bool size_unknown, float brim_width, size_t idx) #endif // SLIC3R_OPENGL_ES { if (height == 0.0f) @@ -580,9 +580,8 @@ int GLVolumeCollection::load_wipe_tower_preview( mesh.merge(cone_mesh); } - - volumes.emplace_back(new GLVolume(color)); - GLVolume& v = *volumes.back(); + GLVolume* result{new GLVolume(color)}; + GLVolume& v = *result; #if SLIC3R_OPENGL_ES if (out_mesh != nullptr) *out_mesh = mesh; @@ -593,12 +592,13 @@ int GLVolumeCollection::load_wipe_tower_preview( v.set_convex_hull(mesh.convex_hull_3d()); v.set_volume_offset(Vec3d(pos_x, pos_y, 0.0)); v.set_volume_rotation(Vec3d(0., 0., (M_PI / 180.) * rotation_angle)); - v.composite_id = GLVolume::CompositeID(INT_MAX, 0, 0); + v.composite_id = GLVolume::CompositeID(INT_MAX - idx, 0, 0); v.geometry_id.first = 0; - v.geometry_id.second = wipe_tower_instance_id().id; - v.is_wipe_tower = true; + v.geometry_id.second = wipe_tower_instance_id(idx).id; + v.wipe_tower_bed_index = idx; v.shader_outside_printer_detection_enabled = !size_unknown; - return int(volumes.size() - 1); + + return result; } // Load SLA auxiliary GLVolumes (for support trees or pad). @@ -646,6 +646,7 @@ void GLVolumeCollection::load_object_auxiliary( std::shared_ptr preview_mesh_ptr = print_object->get_mesh_to_print(); if (preview_mesh_ptr != nullptr) backend_mesh = TriangleMesh(*preview_mesh_ptr); + backend_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); if (!backend_mesh.empty()) { backend_mesh.transform(mesh_trafo_inv); TriangleMesh convex_hull = backend_mesh.convex_hull_3d(); @@ -660,6 +661,7 @@ void GLVolumeCollection::load_object_auxiliary( // Get the support mesh. if (milestone == SLAPrintObjectStep::slaposSupportTree) { TriangleMesh supports_mesh = print_object->support_mesh(); + supports_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); if (!supports_mesh.empty()) { supports_mesh.transform(mesh_trafo_inv); TriangleMesh convex_hull = supports_mesh.convex_hull_3d(); @@ -673,6 +675,7 @@ void GLVolumeCollection::load_object_auxiliary( // Get the pad mesh. if (milestone == SLAPrintObjectStep::slaposPad) { TriangleMesh pad_mesh = print_object->pad_mesh(); + pad_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); if (!pad_mesh.empty()) { pad_mesh.transform(mesh_trafo_inv); TriangleMesh convex_hull = pad_mesh.convex_hull_3d(); @@ -792,7 +795,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab const int obj_idx = volume.first->object_idx(); const int vol_idx = volume.first->volume_idx(); const bool render_as_mmu_painted = is_render_as_mmu_painted_enabled && !volume.first->selected && - !volume.first->is_outside && volume.first->hover == GLVolume::HS_None && !volume.first->is_wipe_tower && obj_idx >= 0 && vol_idx >= 0 && + !volume.first->is_outside && volume.first->hover == GLVolume::HS_None && !volume.first->is_wipe_tower() && obj_idx >= 0 && vol_idx >= 0 && !model_objects[obj_idx]->volumes[vol_idx]->mm_segmentation_facets.empty() && type != GLVolumeCollection::ERenderType::Transparent; // to filter out shells (not very nice) volume.first->set_render_color(true); @@ -871,7 +874,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab shader->set_uniform("print_volume.xy_data", m_print_volume.data); shader->set_uniform("print_volume.z_data", m_print_volume.zs); shader->set_uniform("volume_world_matrix", world_matrix); - shader->set_uniform("slope.actived", m_slope.active && !volume.first->is_modifier && !volume.first->is_wipe_tower); + shader->set_uniform("slope.actived", m_slope.active && !volume.first->is_modifier && !volume.first->is_wipe_tower()); shader->set_uniform("slope.volume_world_normal_matrix", static_cast(world_matrix_inv_transp.cast())); shader->set_uniform("slope.normal_z", m_slope.normal_z); @@ -1001,7 +1004,7 @@ void GLVolumeCollection::update_colors_by_extruder(const DynamicPrintConfig* con } for (GLVolume* volume : volumes) { - if (volume == nullptr || volume->is_modifier || volume->is_wipe_tower || volume->is_sla_pad() || volume->is_sla_support()) + if (volume == nullptr || volume->is_modifier || volume->is_wipe_tower() || volume->is_sla_pad() || volume->is_sla_support()) continue; int extruder_id = volume->extruder_id - 1; diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 1348618..45a66c6 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -178,8 +178,6 @@ public: bool is_outside : 1; // Wheter or not this volume has been generated from a modifier bool is_modifier : 1; - // Wheter or not this volume has been generated from the wipe tower - bool is_wipe_tower : 1; // Wheter or not this volume has been generated from an extrusion path bool is_extrusion_path : 1; // Whether or not always use the volume's own color (not using SELECTED/HOVER/DISABLED/OUTSIDE) @@ -205,6 +203,13 @@ public: // Offset into qverts & tverts, or offsets into indices stored into an OpenGL name_index_buffer. std::vector offsets; + std::optional wipe_tower_bed_index; + + // Wheter or not this volume has been generated from the wipe tower + bool is_wipe_tower() const { + return bool{wipe_tower_bed_index}; + } + // Bounding box of this volume, in unscaled coordinates. BoundingBoxf3 bounding_box() const { return this->model.get_bounding_box(); @@ -414,11 +419,11 @@ public: int instance_idx); #if SLIC3R_OPENGL_ES - int load_wipe_tower_preview( - float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, float rotation_angle, bool size_unknown, float brim_width, TriangleMesh* out_mesh = nullptr); + GLVolume* load_wipe_tower_preview( + float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, float rotation_angle, bool size_unknown, float brim_width, size_t idx, TriangleMesh* out_mesh = nullptr); #else - int load_wipe_tower_preview( - float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, float rotation_angle, bool size_unknown, float brim_width); + GLVolume* load_wipe_tower_preview( + float pos_x, float pos_y, float width, float depth, const std::vector>& z_and_depth_pairs, float height, float cone_angle, float rotation_angle, bool size_unknown, float brim_width, size_t idx); #endif // SLIC3R_OPENGL_ES // Load SLA auxiliary GLVolumes (for support trees or pad). diff --git a/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp b/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp index a0d33e2..45102ae 100644 --- a/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp +++ b/src/slic3r/GUI/ArrangeSettingsDialogImgui.cpp @@ -31,7 +31,7 @@ ArrangeSettingsDialogImgui::ArrangeSettingsDialogImgui( : m_imgui{imgui}, m_db{std::move(db)} {} -void ArrangeSettingsDialogImgui::render(float pos_x, float pos_y) +void ArrangeSettingsDialogImgui::render(float pos_x, float pos_y, bool current_bed) { assert(m_imgui && m_db); @@ -128,9 +128,12 @@ void ArrangeSettingsDialogImgui::render(float pos_x, float pos_y) ImGui::SameLine(); - if (ImGuiPureWrap::button(_u8L("Arrange")) && m_on_arrange_btn) { + if (!current_bed && ImGuiPureWrap::button(_u8L("Arrange")) && m_on_arrange_btn) { m_on_arrange_btn(); } + if (current_bed && ImGuiPureWrap::button(_u8L("Arrange bed")) && m_on_arrange_bed_btn) { + m_on_arrange_bed_btn(); + } ImGuiPureWrap::end(); } diff --git a/src/slic3r/GUI/ArrangeSettingsDialogImgui.hpp b/src/slic3r/GUI/ArrangeSettingsDialogImgui.hpp index c18adf9..c9b9270 100644 --- a/src/slic3r/GUI/ArrangeSettingsDialogImgui.hpp +++ b/src/slic3r/GUI/ArrangeSettingsDialogImgui.hpp @@ -2,7 +2,7 @@ #ifndef ARRANGESETTINGSDIALOGIMGUI_HPP #define ARRANGESETTINGSDIALOGIMGUI_HPP -#include "libslic3r/Arrange/ArrangeSettingsView.hpp" +#include #include "ImGuiWrapper.hpp" #include "libslic3r/AnyPtr.hpp" @@ -14,6 +14,7 @@ class ArrangeSettingsDialogImgui: public arr2::ArrangeSettingsView { AnyPtr m_db; std::function m_on_arrange_btn; + std::function m_on_arrange_bed_btn; std::function m_on_reset_btn; std::function m_show_xl_combo_predicate = [] { return true; }; @@ -21,7 +22,7 @@ class ArrangeSettingsDialogImgui: public arr2::ArrangeSettingsView { public: ArrangeSettingsDialogImgui(ImGuiWrapper *imgui, AnyPtr db); - void render(float pos_x, float pos_y); + void render(float pos_x, float pos_y, bool current_bed); void show_xl_align_combo(std::function pred) { @@ -33,6 +34,11 @@ public: m_on_arrange_btn = on_arrangefn; } + void on_arrange_bed_btn(std::function on_arrangefn) + { + m_on_arrange_bed_btn = on_arrangefn; + } + void on_reset_btn(std::function on_resetfn) { m_on_reset_btn = on_resetfn; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index bd79f4a..3d93a4e 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -96,10 +96,10 @@ std::pair SlicingProcessCompletedEvent::format_error_message( return std::make_pair(std::move(error), monospace); } -BackgroundSlicingProcess::BackgroundSlicingProcess() +void BackgroundSlicingProcess::set_temp_output_path(int bed_idx) { boost::filesystem::path temp_path(wxStandardPaths::Get().GetTempDir().utf8_str().data()); - temp_path /= (boost::format(".%1%.gcode") % get_current_pid()).str(); + temp_path /= (boost::format(".%1%_%2%.gcode") % get_current_pid() % bed_idx).str(); m_temp_output_path = temp_path.string(); } @@ -107,7 +107,19 @@ BackgroundSlicingProcess::~BackgroundSlicingProcess() { this->stop(); this->join_background_thread(); - boost::nowide::remove(m_temp_output_path.c_str()); + + // Current m_temp_output_path corresponds to the last selected bed. Remove everything + // in the same directory that starts the same (see set_temp_output_path). + const auto temp_dir = boost::filesystem::path(m_temp_output_path).parent_path(); + std::string prefix = boost::filesystem::path(m_temp_output_path).filename().string(); + prefix = prefix.substr(0, prefix.find('_')); + for (const auto& entry : boost::filesystem::directory_iterator(temp_dir)) { + if (entry.is_regular_file()) { + const std::string filename = entry.path().filename().string(); + if (boost::starts_with(filename, prefix) && boost::ends_with(filename, ".gcode")) + boost::filesystem::remove(entry); + } + } } bool BackgroundSlicingProcess::select_technology(PrinterTechnology tech) @@ -123,6 +135,8 @@ bool BackgroundSlicingProcess::select_technology(PrinterTechnology tech) } changed = true; } + if (tech == ptFFF) + m_print = m_fff_print; assert(m_print != nullptr); return changed; } @@ -155,10 +169,10 @@ void BackgroundSlicingProcess::process_fff() if (this->set_step_started(bspsGCodeFinalize)) { if (! m_export_path.empty()) { wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); - finalize_gcode(); + finalize_gcode(m_export_path, m_export_path_on_removable_media); } else if (! m_upload_job.empty()) { wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); - prepare_upload(); + prepare_upload(m_upload_job); } else { m_print->set_status(100, _u8L("Slicing complete")); } @@ -196,7 +210,7 @@ void BackgroundSlicingProcess::process_sla() m_print->set_status(100, GUI::format(_L("Masked SLA file exported to %1%"), export_path)); } else if (! m_upload_job.empty()) { wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); - prepare_upload(); + prepare_upload(m_upload_job); } else { m_print->set_status(100, _u8L("Slicing complete")); } @@ -660,12 +674,12 @@ bool BackgroundSlicingProcess::invalidate_all_steps() // G-code is generated in m_temp_output_path. // Optionally run a post-processing script on a copy of m_temp_output_path. // Copy the final G-code to target location (possibly a SD card, if it is a removable media, then verify that the file was written without an error). -void BackgroundSlicingProcess::finalize_gcode() +void BackgroundSlicingProcess::finalize_gcode(const std::string &path, const bool path_on_removable_media) { m_print->set_status(95, _u8L("Running post-processing scripts")); // Perform the final post-processing of the export path by applying the print statistics over the file name. - std::string export_path = m_fff_print->print_statistics().finalize_output_path(m_export_path); + std::string export_path = m_fff_print->print_statistics().finalize_output_path(path); std::string output_path = m_temp_output_path; // Both output_path and export_path ar in-out parameters. // If post processed, output_path will differ from m_temp_output_path as run_post_process_scripts() will make a copy of the G-code to not @@ -687,7 +701,7 @@ void BackgroundSlicingProcess::finalize_gcode() int copy_ret_val = CopyFileResult::SUCCESS; try { - copy_ret_val = copy_file(output_path, export_path, error_message, m_export_path_on_removable_media); + copy_ret_val = copy_file(output_path, export_path, error_message, path_on_removable_media); remove_post_processed_temp_file(); } catch (...) @@ -722,7 +736,7 @@ void BackgroundSlicingProcess::finalize_gcode() } // A print host upload job has been scheduled, enqueue it to the printhost job queue -void BackgroundSlicingProcess::prepare_upload() +void BackgroundSlicingProcess::prepare_upload(PrintHostJob &upload_job) { // Generate a unique temp path to which the gcode/zip file is copied/exported boost::filesystem::path source_path = boost::filesystem::temp_directory_path() @@ -733,15 +747,15 @@ void BackgroundSlicingProcess::prepare_upload() std::string error_message; if (copy_file(m_temp_output_path, source_path.string(), error_message) != SUCCESS) throw Slic3r::RuntimeError("Copying of the temporary G-code to the output G-code failed"); - m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); + upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(upload_job.upload_data.upload_path.string()); // Make a copy of the source path, as run_post_process_scripts() is allowed to change it when making a copy of the source file // (not here, but when the final target is a file). std::string source_path_str = source_path.string(); - std::string output_name_str = m_upload_job.upload_data.upload_path.string(); - if (run_post_process_scripts(source_path_str, false, m_upload_job.printhost->get_name(), output_name_str, m_fff_print->full_print_config())) - m_upload_job.upload_data.upload_path = output_name_str; + std::string output_name_str = upload_job.upload_data.upload_path.string(); + if (run_post_process_scripts(source_path_str, false, upload_job.printhost->get_name(), output_name_str, m_fff_print->full_print_config())) + upload_job.upload_data.upload_path = output_name_str; } else { - m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); + upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(upload_job.upload_data.upload_path.string()); auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(current_print()->full_print_config()); @@ -758,14 +772,14 @@ void BackgroundSlicingProcess::prepare_upload() sizes.emplace_back(size); } ThumbnailsList thumbnails = this->render_thumbnails(ThumbnailsParams{ sizes, true, true, true, true }); - m_sla_print->export_print(source_path.string(),thumbnails, m_upload_job.upload_data.upload_path.filename().string()); + m_sla_print->export_print(source_path.string(),thumbnails, upload_job.upload_data.upload_path.filename().string()); } - m_print->set_status(100, GUI::format(_L("Scheduling upload to `%1%`. See Window -> Print Host Upload Queue"), m_upload_job.printhost->get_host())); + m_print->set_status(100, GUI::format(_L("Scheduling upload to `%1%`. See Window -> Print Host Upload Queue"), upload_job.printhost->get_host())); - m_upload_job.upload_data.source_path = std::move(source_path); + upload_job.upload_data.source_path = std::move(source_path); - GUI::wxGetApp().printhost_job_queue().enqueue(std::move(m_upload_job)); + GUI::wxGetApp().printhost_job_queue().enqueue(std::move(upload_job)); } // Executed by the background thread, to start a task on the UI thread. diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index 2b049a4..8ae1d07 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -9,8 +9,10 @@ #include +#include "libslic3r/Print.hpp" #include "libslic3r/PrintBase.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" +#include "libslic3r/SLAPrint.hpp" #include "slic3r/Utils/PrintHost.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -78,15 +80,17 @@ enum BackgroundSlicingProcessStep { class BackgroundSlicingProcess { public: - BackgroundSlicingProcess(); // Stop the background processing and finalize the bacgkround processing thread, remove temp files. ~BackgroundSlicingProcess(); - void set_fff_print(Print *print) { m_fff_print = print; } - void set_sla_print(SLAPrint *print) { m_sla_print = print; } + void set_temp_output_path(int bed_idx); + void set_fff_print(Print* print) { if (m_fff_print != print) stop(); m_fff_print = print; } + void set_sla_print(SLAPrint *print) { if (m_sla_print != print) stop(); m_sla_print = print; } void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; } void set_gcode_result(GCodeProcessorResult* result) { m_gcode_result = result; } + GCodeProcessorResult *get_gcode_result() { return m_gcode_result; } + // The following wxCommandEvent will be sent to the UI thread / Plater window, when the slicing is finished // and the background processing will transition into G-code export. // The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed. @@ -170,7 +174,9 @@ public: // This "finished" flag does not account for the final export of the output file (.gcode or zipped PNGs), // and it does not account for the OctoPrint scheduling. bool finished() const { return m_print->finished(); } - + void finalize_gcode(const std::string &path, const bool path_on_removable_media); + void prepare_upload(PrintHostJob &upload_job); + private: void thread_proc(); // Calls thread_proc(), catches all C++ exceptions and shows them using wxApp::OnUnhandledException(). @@ -261,8 +267,6 @@ private: bool invalidate_all_steps(); // If the background processing stop was requested, throw CanceledException. void throw_if_canceled() const { if (m_print->canceled()) throw CanceledException(); } - void finalize_gcode(); - void prepare_upload(); // To be executed at the background thread. ThumbnailsList render_thumbnails(const ThumbnailsParams ¶ms); // Execute task from background thread on the UI thread synchronously. Returns true if processed, false if cancelled before executing the task. diff --git a/src/slic3r/GUI/BulkExportDialog.cpp b/src/slic3r/GUI/BulkExportDialog.cpp new file mode 100644 index 0000000..e117f32 --- /dev/null +++ b/src/slic3r/GUI/BulkExportDialog.cpp @@ -0,0 +1,319 @@ +#include "BulkExportDialog.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "libslic3r/PresetBundle.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "format.hpp" +#include "MsgDialog.hpp" + +namespace fs = boost::filesystem; + +namespace Slic3r { +namespace GUI { + +constexpr auto BORDER_W = 10; + +void BulkExportDialog::Item::init_input_name_ctrl(wxFlexGridSizer* row_sizer, const std::string &path) +{ +#ifdef _WIN32 + const long style = wxBORDER_SIMPLE; +#else + const long style = 0L; +#endif + m_text_ctrl = new wxTextCtrl(m_parent, wxID_ANY, from_u8(path), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), style); + wxGetApp().UpdateDarkUI(m_text_ctrl); + m_text_ctrl->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update(); }); + m_text_ctrl->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& event) { event.Enable(selected); }); + + row_sizer->Add(m_text_ctrl, 1, wxEXPAND); +} + +void BulkExportDialog::Item::init_selection_ctrl(wxFlexGridSizer* row_sizer, int bed_index) +{ + m_checkbox = new ::CheckBox(m_parent, std::to_string(bed_index + 1)); + m_checkbox->SetFont(wxGetApp().bold_font()); + wxGetApp().UpdateDarkUI(m_checkbox); + m_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { + this->selected = event.IsChecked(); + }); + + row_sizer->Add(m_checkbox, 0, wxALIGN_CENTER_VERTICAL); + m_checkbox->SetValue(this->selected); +} + +BulkExportDialog::Item::Item( + wxWindow *parent, + wxFlexGridSizer*sizer, + const std::optional& path_opt, + const int bed_index, + Validator validator +): + bed_index(bed_index), + m_parent(parent), + m_valid_bmp(new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("tick_mark"))), + m_validator(std::move(validator)) +{ + if (path_opt) { + path = *path_opt; + m_directory = path.parent_path(); + } + + init_selection_ctrl(sizer, bed_index); + init_input_name_ctrl(sizer, path.filename().string()); + sizer->Add(m_valid_bmp, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, BORDER_W); + + if (!path_opt) { + m_checkbox->Enable(false); + m_checkbox->SetValue(false); + selected = false; + } + + m_valid_bmp->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& event) { event.Show(selected); }); + update(); +} + +#ifdef __WXMSW__ +constexpr int max_path_length = MAX_PATH; +#else +constexpr int max_path_length = 255; +#endif + +struct PathValidator { + std::reference_wrapper>> items; + using ItemStatus = BulkExportDialog::ItemStatus; + + bool is_duplicate(const fs::path &path) { + const int64_t count{std::count_if( + items.get().begin(), + items.get().end(), + [&](const auto &item){ + return item->path == path; + } + )}; + + return count >= 2; + } + + std::pair operator()( + const fs::path &path, + const std::string &filename + ) { + const char* unusable_symbols = "<>[]:/\\|?*\""; + for (size_t i = 0; i < std::strlen(unusable_symbols); i++) { + if (filename.find_first_of(unusable_symbols[i]) != std::string::npos) { + return { + ItemStatus::NoValid, + _L("The following characters are not allowed in the name") + ": " + unusable_symbols + }; + } + } + + if (filename.empty()) { + return { + ItemStatus::NoValid, + _L("The name cannot be empty.") + }; + } + + if (path.string().length() >= max_path_length) { + return { + ItemStatus::NoValid, + _L("The name is too long.") + }; + } + + if (filename.find_first_of(' ') == 0) { + return { + ItemStatus::NoValid, + _L("The name cannot start with space character.") + }; + } + + if (filename.find_last_of(' ') == filename.length()-1) { + return { + ItemStatus::NoValid, + _L("The name cannot end with space character.") + }; + } + + if (is_duplicate(path)) { + return { + ItemStatus::NoValid, + _L("This name is already used, use another.") + }; + } + + if (fs::exists(path)) { + return { + ItemStatus::Warning, + _L("The file already exists!") + }; + } + + return {ItemStatus::Valid, ""}; + } +}; + +void BulkExportDialog::Item::update() +{ + std::string filename{into_u8(m_text_ctrl->GetValue())}; + path = m_directory / filename; + + // Validator needs to be called after path is set! + // It has has a reference to all items and searches + // for duplicates; + const auto [status, info_line]{m_validator(path, filename)}; + + m_valid_bmp->SetToolTip(info_line); + + m_status = status; + + update_valid_bmp(); +} + +std::string get_bmp_name(const BulkExportDialog::ItemStatus status) { + using ItemStatus = BulkExportDialog::ItemStatus; + switch(status) { + case ItemStatus::Warning: return "exclamation_manifold"; + case ItemStatus::NoValid: return "exclamation"; + case ItemStatus::Valid: return "tick_mark"; + } + return ""; // unreachable +} + +void BulkExportDialog::Item::update_valid_bmp() +{ + m_valid_bmp->SetBitmap(*get_bmp_bundle(get_bmp_name(m_status))); +} + +BulkExportDialog::BulkExportDialog(const std::vector>> &paths): + DPIDialog( + nullptr, + wxID_ANY, + _L("Export beds"), + wxDefaultPosition, + wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), + wxDEFAULT_DIALOG_STYLE | wxICON_WARNING + ) +{ + this->SetFont(wxGetApp().normal_font()); + +#ifndef __WXMSW__ + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif // __WXMSW__ + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + m_sizer = new wxFlexGridSizer(paths.size(), 3, wxSize(0.5*BORDER_W, BORDER_W)); + + for (const auto&[bed_index, path] : paths) { + AddItem(path, bed_index); + } + + // Add dialog's buttons + wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxButton* btnOK = static_cast(this->FindWindowById(wxID_OK, this)); + btnOK->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); }); + btnOK->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(enable_ok_btn()); }); + + topSizer->Add(m_sizer, 0, wxEXPAND | wxALL, BORDER_W); + + topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); + + this->CenterOnScreen(); + +#ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); +#endif +} + +void BulkExportDialog::AddItem(const std::optional& path, int bed_index) +{ + m_items.push_back(std::make_unique(this, m_sizer, path, bed_index, PathValidator{m_items})); +} + +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); + if (dialog.ShowModal() == wxID_NO) + return; + } + + EndModal(wxID_OK); +} + +bool BulkExportDialog::enable_ok_btn() const +{ + for (const auto &item : m_items) + if (item->selected && !item->is_valid()) { + return false; + } + + bool all_unselected{ true }; + for (const auto& item : m_items) + if (item->selected) { + all_unselected = false; + break; + } + + return !all_unselected; +} + +std::vector>> BulkExportDialog::get_paths() const { + std::vector>> result; + std::transform( + m_items.begin(), + m_items.end(), + std::back_inserter(result), + [](const auto &item) -> std::pair> { + if (!item->selected) { + return {item->bed_index, std::nullopt}; + } + return {item->bed_index, item->path}; + } + ); + return result; +} + +bool BulkExportDialog::has_warnings() const +{ + for (const auto& item : m_items) + if (item->selected && item->is_warning()) { + return true; + } + return false; +} + +void BulkExportDialog::on_dpi_changed(const wxRect&) +{ + const int& em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); + + for (auto &item : m_items) + item->update_valid_bmp(); + + const wxSize& size = wxSize(65 * em, 35 * em); + SetMinSize(size); + + Fit(); + Refresh(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/BulkExportDialog.hpp b/src/slic3r/GUI/BulkExportDialog.hpp new file mode 100644 index 0000000..c4c9a8c --- /dev/null +++ b/src/slic3r/GUI/BulkExportDialog.hpp @@ -0,0 +1,96 @@ +#pragma once + +//#include + +#include +#include "wxExtensions.hpp" +#include "GUI_Utils.hpp" +#include + +#include "Widgets/CheckBox.hpp" + +class wxString; +class wxStaticText; +class wxTextCtrl; +class wxStaticBitmap; +class wxFlexGridSizer; + +namespace Slic3r { +class Print; +struct GCodeProcessorResult; + +namespace GUI { + +class BulkExportDialog : public DPIDialog +{ +public: + enum class ItemStatus { Valid, NoValid, Warning }; + + struct Item + { + using Validator = std::function< + std::pair( + boost::filesystem::path, + std::string + ) + >; + Item( + wxWindow *parent, + wxFlexGridSizer *sizer, + const std::optional& path, + const int bed_index, + Validator validator + ); + Item(const Item &) = delete; + Item& operator=(const Item &) = delete; + Item(Item &&) = delete; + Item& operator=(Item &&) = delete; + + // Item cannot have copy or move constructors, because a wx event binds + // directly to its address. + + void update_valid_bmp(); + bool is_valid() const { return m_status != ItemStatus::NoValid; } + bool is_warning() const { return m_status == ItemStatus::Warning; } + + boost::filesystem::path path; + int bed_index{}; + bool selected{true}; + + private: + ItemStatus m_status{ItemStatus::NoValid}; + wxWindow *m_parent{nullptr}; + wxStaticBitmap *m_valid_bmp{nullptr}; + wxTextCtrl *m_text_ctrl{nullptr}; + ::CheckBox *m_checkbox{nullptr}; + Validator m_validator; + boost::filesystem::path m_directory{}; + + void init_input_name_ctrl(wxFlexGridSizer*row_sizer, const std::string &path); + void init_selection_ctrl(wxFlexGridSizer*row_sizer, int bed_index); + void update(); + }; + +private: + // This must be a unique ptr, because Item does not have copy nor move constructors. + std::vector> m_items; + wxFlexGridSizer*m_sizer{nullptr}; + +public: + + BulkExportDialog(const std::vector>> &paths); + std::vector>> get_paths() const; + bool has_warnings() const; + +protected: + void on_dpi_changed(const wxRect &) override; + void on_sys_color_changed() override {} + +private: + void AddItem(const std::optional& path, int bed_index); + void accept(); + bool enable_ok_btn() const; +}; + +} // namespace GUI +} diff --git a/src/slic3r/GUI/Camera.cpp b/src/slic3r/GUI/Camera.cpp index 72a3be6..4e2b8f8 100644 --- a/src/slic3r/GUI/Camera.cpp +++ b/src/slic3r/GUI/Camera.cpp @@ -20,6 +20,7 @@ double Camera::FrustrumMinZRange = 50.0; double Camera::FrustrumMinNearZ = 100.0; double Camera::FrustrumZMargin = 10.0; double Camera::MaxFovDeg = 60.0; +double Camera::SceneBoxScaleFactor = 2.5; std::string Camera::get_type_as_string() const { @@ -56,10 +57,23 @@ void Camera::set_target(const Vec3d& target) const Vec3d new_displacement = new_target - m_target; if (!new_displacement.isApprox(Vec3d::Zero())) { m_target = new_target; - m_view_matrix.translate(-new_displacement); + Transform3d inv_view_matrix = m_view_matrix.inverse(); + inv_view_matrix.translation() = m_target - m_distance * get_dir_forward(); + m_view_matrix = inv_view_matrix.inverse(); } } +BoundingBoxf3 Camera::get_target_validation_box() const +{ + const Vec3d center = m_scene_box.center(); + Vec3d size = m_scene_box.size(); + size.x() *= m_scene_box_scale_factor; + size.y() *= m_scene_box_scale_factor; + size.z() *= 1.5; + const Vec3d half_size = 0.5 * size; + return BoundingBoxf3(center - half_size, center + half_size); +} + void Camera::set_zoom(double zoom) { // Don't allow to zoom too far outside the scene. @@ -256,6 +270,8 @@ void Camera::apply_projection(double left, double right, double bottom, double t void Camera::zoom_to_box(const BoundingBoxf3& box, double margin_factor) { + m_distance = DefaultDistance; + // Calculate the zoom factor needed to adjust the view around the given box. const double zoom = calc_zoom_to_bounding_box_factor(box, margin_factor); if (zoom > 0.0) { @@ -267,6 +283,8 @@ void Camera::zoom_to_box(const BoundingBoxf3& box, double margin_factor) void Camera::zoom_to_volumes(const GLVolumePtrs& volumes, double margin_factor) { + m_distance = DefaultDistance; + Vec3d center; const double zoom = calc_zoom_to_volumes_factor(volumes, center, margin_factor); if (zoom > 0.0) { @@ -280,7 +298,7 @@ void Camera::zoom_to_volumes(const GLVolumePtrs& volumes, double margin_factor) void Camera::debug_render() const { ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Camera statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::Begin(std::string("Camera statistics").c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); std::string type = get_type_as_string(); if (wxGetApp().plater()->get_mouse3d_controller().connected() || (wxGetApp().app_config->get_bool("use_free_camera"))) @@ -325,7 +343,7 @@ void Camera::debug_render() const ImGui::InputInt4("Viewport", viewport.data(), ImGuiInputTextFlags_ReadOnly); ImGui::Separator(); ImGui::InputFloat("GUI scale", &gui_scale, 0.0f, 0.0f, "%.6f", ImGuiInputTextFlags_ReadOnly); - imgui.end(); + ImGui::End(); } #endif // ENABLE_CAMERA_STATISTICS @@ -347,7 +365,7 @@ void Camera::rotate_on_sphere(double delta_azimut_rad, double delta_zenit_rad, b const auto rot_z = Eigen::AngleAxisd(delta_azimut_rad, Vec3d::UnitZ()); m_view_rotation *= rot_z * Eigen::AngleAxisd(delta_zenit_rad, rot_z.inverse() * get_dir_right()); m_view_rotation.normalize(); - m_view_matrix.fromPositionOrientationScale(m_view_rotation * (- m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); + m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); } // Virtual trackball, rotate around an axis, where the eucledian norm of the axis gives the rotation angle in radians. @@ -359,9 +377,9 @@ void Camera::rotate_local_around_target(const Vec3d& rotation_rad) const Vec3d axis = m_view_rotation.conjugate() * rotation_rad.normalized(); m_view_rotation *= Eigen::Quaterniond(Eigen::AngleAxisd(angle, axis)); m_view_rotation.normalize(); - m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); - update_zenit(); - } + m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); + update_zenit(); + } } std::pair Camera::calc_tight_frustrum_zs_around(const BoundingBoxf3& box) @@ -369,8 +387,6 @@ std::pair Camera::calc_tight_frustrum_zs_around(const BoundingBo std::pair ret; auto& [near_z, far_z] = ret; - set_distance(DefaultDistance); - // box in eye space const BoundingBoxf3 eye_box = box.transformed(m_view_matrix); near_z = -eye_box.max.z(); @@ -573,16 +589,10 @@ void Camera::set_default_orientation() Vec3d Camera::validate_target(const Vec3d& target) const { - BoundingBoxf3 test_box = m_scene_box; - test_box.translate(-m_scene_box.center()); - // We may let this factor be customizable - static const double ScaleFactor = 1.5; - test_box.scale(ScaleFactor); - test_box.translate(m_scene_box.center()); - - return { std::clamp(target(0), test_box.min(0), test_box.max(0)), - std::clamp(target(1), test_box.min(1), test_box.max(1)), - std::clamp(target(2), test_box.min(2), test_box.max(2)) }; + const BoundingBoxf3 test_box = get_target_validation_box(); + return { std::clamp(target.x(), test_box.min.x(), test_box.max.x()), + std::clamp(target.y(), test_box.min.y(), test_box.max.y()), + std::clamp(target.z(), test_box.min.z(), test_box.max.z())}; } void Camera::update_zenit() diff --git a/src/slic3r/GUI/Camera.hpp b/src/slic3r/GUI/Camera.hpp index abca87b..2bc8819 100644 --- a/src/slic3r/GUI/Camera.hpp +++ b/src/slic3r/GUI/Camera.hpp @@ -13,6 +13,7 @@ struct Camera static const double DefaultDistance; static const double DefaultZoomToBoxMarginFactor; static const double DefaultZoomToVolumesMarginFactor; + static double SceneBoxScaleFactor; static double FrustrumMinZRange; static double FrustrumMinNearZ; static double FrustrumZMargin; @@ -44,6 +45,7 @@ private: Eigen::Quaterniond m_view_rotation{ 1.0, 0.0, 0.0, 0.0 }; Transform3d m_projection_matrix{ Transform3d::Identity() }; std::pair m_frustrum_zs; + double m_scene_box_scale_factor{ SceneBoxScaleFactor }; BoundingBoxf3 m_scene_box; @@ -62,6 +64,8 @@ public: const Vec3d& get_target() const { return m_target; } void set_target(const Vec3d& target); + BoundingBoxf3 get_target_validation_box() const; + double get_distance() const { return (get_position() - m_target).norm(); } double get_gui_scale() const { return m_gui_scale; } @@ -135,7 +139,9 @@ public: void look_at(const Vec3d& position, const Vec3d& target, const Vec3d& up); double max_zoom() const { return 250.0; } - double min_zoom() const { return 0.7 * calc_zoom_to_bounding_box_factor(m_scene_box); } + double min_zoom() const { return 0.25 * calc_zoom_to_bounding_box_factor(m_scene_box); } + + void set_distance(double distance); private: // returns tight values for nearZ and farZ plane around the given bounding box @@ -143,7 +149,6 @@ private: std::pair calc_tight_frustrum_zs_around(const BoundingBoxf3& box); double calc_zoom_to_bounding_box_factor(const BoundingBoxf3& box, double margin_factor = DefaultZoomToBoxMarginFactor) const; double calc_zoom_to_volumes_factor(const GLVolumePtrs& volumes, Vec3d& center, double margin_factor = DefaultZoomToVolumesMarginFactor) const; - void set_distance(double distance); void set_default_orientation(); Vec3d validate_target(const Vec3d& target) const; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 1912966..46363c9 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -34,6 +34,64 @@ void ConfigManipulation::toggle_field(const std::string& opt_key, const bool tog cb_toggle_field(opt_key, toggle, opt_index); } +std::optional handle_automatic_extrusion_widths(const DynamicPrintConfig &config, const bool is_global_config, wxWindow *msg_dlg_parent) +{ + const std::vector extrusion_width_parameters = {"extrusion_width", "external_perimeter_extrusion_width", "first_layer_extrusion_width", + "infill_extrusion_width", "perimeter_extrusion_width", "solid_infill_extrusion_width", + "support_material_extrusion_width", "top_infill_extrusion_width"}; + + auto is_zero_width = [](const ConfigOptionFloatOrPercent &opt) -> bool { + return opt.value == 0. && !opt.percent; + }; + + auto is_parameters_adjustment_needed = [&is_zero_width, &config, &extrusion_width_parameters]() -> bool { + if (!config.opt_bool("automatic_extrusion_widths")) { + return false; + } + + for (const std::string &extrusion_width_parameter : extrusion_width_parameters) { + if (!is_zero_width(*config.option(extrusion_width_parameter))) { + return true; + } + } + + return false; + }; + + if (is_parameters_adjustment_needed()) { + wxString msg_text = _(L("The automatic extrusion widths calculation requires:\n" + "- Default extrusion width: 0\n" + "- First layer extrusion width: 0\n" + "- Perimeter extrusion width: 0\n" + "- External perimeter extrusion width: 0\n" + "- Infill extrusion width: 0\n" + "- Solid infill extrusion width: 0\n" + "- Top infill extrusion width: 0\n" + "- Support material extrusion width: 0")); + + if (is_global_config) { + msg_text += "\n\n" + _(L("Shall I adjust those settings in order to enable automatic extrusion widths calculation?")); + } + + MessageDialog dialog(msg_dlg_parent, msg_text, _(L("Automatic extrusion widths calculation")), + wxICON_WARNING | (is_global_config ? wxYES | wxNO : wxOK)); + + const int answer = dialog.ShowModal(); + DynamicPrintConfig new_conf = config; + if (!is_global_config || answer == wxID_YES) { + for (const std::string &extrusion_width_parameter : extrusion_width_parameters) { + new_conf.set_key_value(extrusion_width_parameter, new ConfigOptionFloatOrPercent(0., false)); + } + } else { + new_conf.set_key_value("automatic_extrusion_widths", new ConfigOptionBool(false)); + } + + return new_conf; + } + + return std::nullopt; +} + void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, const bool is_global_config) { // #ys_FIXME_to_delete @@ -72,7 +130,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con double fill_density = config->option("fill_density")->value; if (config->opt_bool("spiral_vase") && - ! (config->opt_int("perimeters") == 1 && + ! (config->opt_int("perimeters") == 1 && config->opt_int("top_solid_layers") == 0 && fill_density == 0 && ! config->opt_bool("support_material") && @@ -98,7 +156,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con new_conf.set_key_value("fill_density", new ConfigOptionPercent(0)); new_conf.set_key_value("support_material", new ConfigOptionBool(false)); new_conf.set_key_value("support_material_enforce_layers", new ConfigOptionInt(0)); - new_conf.set_key_value("thin_walls", new ConfigOptionBool(false)); + new_conf.set_key_value("thin_walls", new ConfigOptionBool(false)); fill_density = 0; support = false; } @@ -238,6 +296,13 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con } } } + + if (config->opt_bool("automatic_extrusion_widths")) { + std::optional new_config = handle_automatic_extrusion_widths(*config, is_global_config, m_msg_dlg_parent); + if (new_config.has_value()) { + apply(config, &(*new_config)); + } + } } void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) @@ -258,11 +323,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("min_resonance_avoidance_speed", resonance_avoidance); toggle_field("max_resonance_avoidance_speed", resonance_avoidance); - bool have_infill = config->option("fill_density")->value > 0; + const bool have_infill = config->option("fill_density")->value > 0; + const bool has_automatic_infill_combination = config->option("automatic_infill_combination")->value; // infill_extruder uses the same logic as in Print::extruders() - for (auto el : { "fill_pattern", "infill_every_layers", "infill_only_where_needed", - "solid_infill_every_layers", "solid_infill_below_area", "infill_extruder", "infill_anchor_max" }) + for (auto el : { "fill_pattern","solid_infill_every_layers", "solid_infill_below_area", "infill_extruder", + "infill_anchor_max", "automatic_infill_combination" }) { toggle_field(el, have_infill); + } + + toggle_field("infill_every_layers", have_infill && !has_automatic_infill_combination); + toggle_field("automatic_infill_combination_max_layer_height", have_infill && has_automatic_infill_combination); + // Only allow configuration of open anchors if the anchoring is enabled. bool has_infill_anchors = have_infill && config->option("infill_anchor_max")->value > 0; toggle_field("infill_anchor", has_infill_anchors); @@ -280,8 +351,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "infill_speed", "bridge_speed" }) toggle_field(el, have_infill || has_solid_infill); - toggle_field("top_solid_min_thickness", ! has_spiral_vase && has_top_solid_infill); - toggle_field("bottom_solid_min_thickness", ! has_spiral_vase && has_bottom_solid_infill); + const bool has_ensure_vertical_shell_thickness = config->opt_enum("ensure_vertical_shell_thickness") != EnsureVerticalShellThickness::Disabled; + toggle_field("top_solid_min_thickness", !has_spiral_vase && has_top_solid_infill && has_ensure_vertical_shell_thickness); + toggle_field("bottom_solid_min_thickness", !has_spiral_vase && has_bottom_solid_infill && has_ensure_vertical_shell_thickness); // Gap fill is newly allowed in between perimeter lines even for empty infill (see GH #1476). toggle_field("gap_fill_speed", have_perimeters); @@ -360,7 +432,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("standby_temperature_delta", have_ooze_prevention); bool have_wipe_tower = config->opt_bool("wipe_tower"); - for (auto el : { "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_cone_angle", + for (auto el : { "wipe_tower_width", "wipe_tower_brim_width", "wipe_tower_cone_angle", "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); @@ -381,6 +453,16 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("min_feature_size", have_arachne); toggle_field("min_bead_width", have_arachne); toggle_field("thin_walls", !have_arachne); + + toggle_field("scarf_seam_placement", !has_spiral_vase); + const auto scarf_seam_placement{config->opt_enum("scarf_seam_placement")}; + const bool uses_scarf_seam{!has_spiral_vase && scarf_seam_placement != ScarfSeamPlacement::nowhere}; + toggle_field("scarf_seam_only_on_smooth", uses_scarf_seam); + toggle_field("scarf_seam_start_height", uses_scarf_seam); + toggle_field("scarf_seam_entire_loop", uses_scarf_seam); + 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); } void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 778b794..5671a6c 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -53,12 +53,10 @@ #include "Field.hpp" #include "DesktopIntegrationDialog.hpp" #include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" #include "format.hpp" #include "MsgDialog.hpp" #include "UnsavedChangesDialog.hpp" #include "UpdatesUIManager.hpp" -#include "PresetArchiveDatabase.hpp" #include "Plater.hpp" // #ysFIXME - implement getter for preset_archive_database from GetApp()??? #include "slic3r/Utils/AppUpdater.hpp" #include "slic3r/GUI/I18N.hpp" @@ -78,30 +76,9 @@ namespace Slic3r { namespace GUI { - using Config::Snapshot; using Config::SnapshotDB; - - -ConfigWizardLoadingDialog::ConfigWizardLoadingDialog(wxWindow* parent, const wxString& message) - : wxDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxFRAME_FLOAT_ON_PARENT) -{ - auto* text = new wxStaticText(this, wxID_ANY, message, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); - auto* vsizer = new wxBoxSizer(wxVERTICAL); - auto *top_sizer = new wxBoxSizer(wxVERTICAL); - vsizer->Add(text, 1, wxEXPAND); - top_sizer->Add(vsizer, 1, wxEXPAND | wxALL, 15); - SetSizer(top_sizer); - #ifdef _WIN32 - wxGetApp().UpdateDlgDarkUI(this); - #endif - Fit(); -} - - -// Configuration data structures extensions needed for the wizard - bool Bundle::load(fs::path source_path, BundleLocation location, bool ais_qidi_bundle) { this->preset_bundle = std::make_unique(); @@ -130,6 +107,7 @@ bool Bundle::load(fs::path source_path, BundleLocation location, bool ais_qidi_b return true; } +// Configuration data structures extensions needed for the wizard Bundle::Bundle(Bundle &&other) : preset_bundle(std::move(other.preset_bundle)) , vendor_profile(other.vendor_profile) @@ -143,7 +121,7 @@ BundleMap BundleMap::load() { BundleMap res; - const PresetArchiveDatabase* pad = wxGetApp().plater()->get_preset_archive_database(); + const Slic3r::PresetUpdaterWrapper* preset_updater_wrapper = wxGetApp().get_preset_updater_wrapper(); const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); const auto archive_dir = (boost::filesystem::path(Slic3r::data_dir()) / "cache" / "vendor").make_preferred(); const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); @@ -220,7 +198,7 @@ BundleMap BundleMap::load() BOOST_LOG_TRIVIAL(error) << format("Could not load bundle %1% due to corrupted profile file %2%. Message: %3%", id, dir_entry.path().string(), e.what()); continue; } - if (vp.repo_id.empty() || !pad->is_selected_repository_by_id(vp.repo_id)) { + if (vp.repo_id.empty() || !preset_updater_wrapper->is_selected_repository_by_id(vp.repo_id)) { continue; } // Don't load @@ -570,7 +548,6 @@ ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxStrin // Update!!! -> it looks like this workaround is no need any more after update of the wxWidgets to 3.2.0 // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages if (!wxLinux_gtk3) */ @@ -648,7 +625,7 @@ PageUpdateManager::PageUpdateManager(ConfigWizard* parent_in) const int em = em_unit(this); - manager = std::make_unique(this, wxGetApp().plater()->get_preset_archive_database(), em); + manager = std::make_unique(this, wxGetApp().get_preset_updater_wrapper(), em); warning_text = new wxStaticText(this, wxID_ANY, _L("WARNING: Select at least one source.")); warning_text->SetFont(wxGetApp().bold_font()); @@ -3414,15 +3391,16 @@ static std::string get_first_added_preset(const std::mapvendors(); - bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + bool show_info_msg = false; + bool suppress_sla_printer = model_has_parameter_modifiers_in_objects(wxGetApp().model()); PrinterTechnology preferred_pt = ptAny; - auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer, &show_info_msg](const std::string& bundle_name, const Bundle& bundle) { const auto config = enabled_vendors.find(bundle_name); PrinterTechnology pt = ptAny; if (config != enabled_vendors.end()) { @@ -3432,17 +3410,21 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese pt = model.technology; const auto config_old = enabled_vendors_old.find(bundle_name); if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) + // if preferred printer model has SLA printer technology it's important to check the model for modifiers + if (pt == ptSLA && suppress_sla_printer) { + show_info_msg = true; continue; + } return pt; } if (const auto model_it_old = config_old->second.find(model.id); model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) + // if preferred printer model has SLA printer technology it's important to check the model for modifiers + if (pt == ptSLA && suppress_sla_printer) { + show_info_msg = true; continue; + } return pt; } } @@ -3464,8 +3446,9 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese } } - if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) - return false; + if (show_info_msg) + show_info(nullptr, _L("It's impossible to print object(s) which contains parameter modifiers with SLA technology.\n\n" + "SLA-printer preset will not be selected"), caption); bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); if (check_unsaved_preset_changes) @@ -3548,9 +3531,7 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese if (install_bundles.size() > 0) { // Install bundles from resources or cache / vendor. // Don't create snapshot - we've already done that above if applicable. - GUI_App& app = wxGetApp(); - const auto* archive_db = app.plater()->get_preset_archive_database(); - bool install_result = updater->install_bundles_rsrc_or_cache_vendor(std::move(install_bundles), archive_db->get_selected_archive_repositories(), false); + bool install_result = updater->install_bundles_rsrc_or_cache_vendor(std::move(install_bundles), false); if (!install_result) return false; } else { @@ -3681,7 +3662,7 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese used_repo_ids.emplace_back(repo_id); } } - wxGetApp().plater()->get_preset_archive_database()->set_installed_printer_repositories(std::move(used_repo_ids)); + wxGetApp().get_preset_updater_wrapper()->set_installed_printer_repositories(std::move(used_repo_ids)); // apply materials in app_config for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) @@ -3802,18 +3783,9 @@ bool ConfigWizard::priv::check_sla_selected() void ConfigWizard::priv::set_config_updated_from_archive(bool load_installed_printers, bool run_preset_updater) { - if (run_preset_updater) { - // This block of preset_updater functions is done in GUI_App::run_wizard before ConfigWizard::run() - // It needs to be also done when repos are confirmed inside wizard. - // Possible optimalization - do not run this block if no repos were changed. - GUI_App& app = wxGetApp(); - // Do blocking sync on every change of archive repos, so user is always offered recent profiles. - const SharedArchiveRepositoryVector &repos = app.plater()->get_preset_archive_database()->get_selected_archive_repositories(); - app.preset_updater->sync_blocking(app.preset_bundle, &app, repos); - // Offer update installation. It used to be offered only when wizard run reason was RR_USER. - app.preset_updater->update_index_db(); - app.preset_updater->config_update(app.app_config->orig_version(), PresetUpdater::UpdateParams::SHOW_TEXT_BOX, repos); - + if (run_preset_updater) { + // TRN: Progress dialog title + wxGetApp().get_preset_updater_wrapper()->wizard_sync(wxGetApp().preset_bundle, wxGetApp().app_config->orig_version(), q, true, _L("Updating Configuration sources")); // We have now probably changed data. We need to rebuild database from which wizards constructs. // Just reload bundles and upadte installed printer from appconfig_new. bundles = BundleMap::load(); @@ -3849,8 +3821,7 @@ bool ConfigWizard::priv::any_installed_vendor_for_repo(const std::string& repo_i static bool to_delete(PagePrinters* page, const std::set& selected_uuids) { - const PresetArchiveDatabase* pad = wxGetApp().plater()->get_preset_archive_database(); - const SharedArchiveRepositoryVector& archs = pad->get_all_archive_repositories(); + const SharedArchiveRepositoryVector& archs = wxGetApp().get_preset_updater_wrapper()->get_all_archive_repositories(); bool unselect_all = true; @@ -3866,14 +3837,14 @@ static bool to_delete(PagePrinters* page, const std::set& selected_ static void unselect(PagePrinters* page) { - const PresetArchiveDatabase* pad = wxGetApp().plater()->get_preset_archive_database(); - const SharedArchiveRepositoryVector& archs = pad->get_all_archive_repositories(); + const Slic3r::PresetUpdaterWrapper* puw = wxGetApp().get_preset_updater_wrapper(); + const SharedArchiveRepositoryVector& archs = puw->get_all_archive_repositories(); bool unselect_all = true; for (const auto* archive : archs) { if (page->get_vendor_repo_id() == archive->get_manifest().id) { - if (pad->is_selected_repository_by_uuid(archive->get_uuid())) + if (puw->is_selected_repository_by_uuid(archive->get_uuid())) unselect_all = false; //break; ! don't break here, because there can be several archives with same repo_id } @@ -3964,16 +3935,16 @@ void ConfigWizard::priv::load_pages_from_archive() // fill vendors and printers pages from Update manager - auto pad = wxGetApp().plater()->get_preset_archive_database(); + const auto* puw = wxGetApp().get_preset_updater_wrapper(); - const SharedArchiveRepositoryVector& archs = pad->get_all_archive_repositories(); + const SharedArchiveRepositoryVector& archs = puw->get_all_archive_repositories(); only_sla_mode = true; bool is_primary_printer_page_set = false; for (const auto* archive : archs) { const auto& data = archive->get_manifest(); - const bool is_selected_arch = pad->is_selected_repository_by_uuid(archive->get_uuid()); + const bool is_selected_arch = puw->is_selected_repository_by_uuid(archive->get_uuid()); std::vector vendors; const bool any_installed_vendor = any_installed_vendor_for_repo(data.id, vendors); @@ -4289,7 +4260,7 @@ bool ConfigWizard::run(RunReason reason, StartPage start_page) if (ShowModal() == wxID_OK) { bool apply_keeped_changes = false; - if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + if (! p->apply_config(app.app_config, app.preset_bundle, app.get_preset_updater_wrapper(), apply_keeped_changes)) return false; if (apply_keeped_changes) @@ -4310,7 +4281,8 @@ void ConfigWizard::update_login() { if (p->page_login && p->page_login->login_changed()) { // repos changed - we need rebuild - wxGetApp().plater()->get_preset_archive_database()->sync_blocking(); + // TRN: Progress dialog title + wxGetApp().get_preset_updater_wrapper()->wizard_sync(wxGetApp().preset_bundle, wxGetApp().app_config->orig_version(), this, false, _L("Updating Configuration sources")); // now change PageUpdateManager //y15 // p->page_update_manager->manager->update(); diff --git a/src/slic3r/GUI/ConfigWizard.hpp b/src/slic3r/GUI/ConfigWizard.hpp index 664d4a2..b482548 100644 --- a/src/slic3r/GUI/ConfigWizard.hpp +++ b/src/slic3r/GUI/ConfigWizard.hpp @@ -13,15 +13,11 @@ namespace Slic3r { class PresetBundle; class PresetUpdater; +class AppConfig; +class PresetArchiveDatabase; namespace GUI { -class ConfigWizardLoadingDialog : public wxDialog -{ -public: - ConfigWizardLoadingDialog(wxWindow* parent, const wxString& message); -}; - namespace DownloaderUtils { class Worker : public wxBoxSizer { @@ -93,8 +89,6 @@ private: friend struct ConfigWizardPage; }; - - } } diff --git a/src/slic3r/GUI/ConfigWizardWebViewPage.cpp b/src/slic3r/GUI/ConfigWizardWebViewPage.cpp index b720200..2a85f88 100644 --- a/src/slic3r/GUI/ConfigWizardWebViewPage.cpp +++ b/src/slic3r/GUI/ConfigWizardWebViewPage.cpp @@ -42,13 +42,18 @@ ConfigWizardWebViewPage::ConfigWizardWebViewPage(ConfigWizard *parent) TargetUrl = m_qidinetwork.get_qidi_host(); #endif - m_browser = WebView::CreateWebView(this, TargetUrl, {"wx"}); + m_browser = WebView::webview_new(); if (!m_browser) { // TRN Config wizard page with a log in page. wxStaticText* fail_text = new wxStaticText(this, wxID_ANY, _L("Failed to load a web browser. Logging in is not possible in the moment.")); append(fail_text); return; } + + //y21 + // WebView::webview_create(m_browser, this, p_user_account->generate_login_redirect_url(), {"ExternalApp"}); + WebView::webview_create(m_browser, this, TargetUrl, {"wx"}); + if (logged) { // TRN Config wizard page with a log in web. m_text = new wxStaticText(this, wxID_ANY, format_wxstr(_L("You are logged as %1%."), p_user_account->get_username())); @@ -69,7 +74,10 @@ ConfigWizardWebViewPage::ConfigWizardWebViewPage(ConfigWizard *parent) // Bind(wxEVT_WEBVIEW_ERROR, &ConfigWizardWebViewPage::on_error, this, m_browser->GetId()); // Bind(wxEVT_WEBVIEW_NAVIGATED, &ConfigWizardWebViewPage::on_navigation_request, this, m_browser->GetId()); // Bind(wxEVT_IDLE, &ConfigWizardWebViewPage::on_idle, this); + + //y21 Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ConfigWizardWebViewPage::is_login, this); + // Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ConfigWizardWebViewPage::on_script_message, this, m_browser->GetId()); } bool ConfigWizardWebViewPage::login_changed() @@ -120,18 +128,29 @@ void ConfigWizardWebViewPage::load_error_page() { m_load_error_page = true; } +constexpr bool is_linux = +#if defined(__linux__) +true; +#else +false; +#endif + void ConfigWizardWebViewPage::on_idle(wxIdleEvent &WXUNUSED(evt)) { if (!m_browser) return; if (m_browser->IsBusy()) { - wxSetCursor(wxCURSOR_ARROWWAIT); + if constexpr (!is_linux) { + wxSetCursor(wxCURSOR_ARROWWAIT); + } } else { - wxSetCursor(wxNullCursor); + if constexpr (!is_linux) { + wxSetCursor(wxNullCursor); + } if (!m_vetoed && m_load_error_page) { m_load_error_page = false; m_browser->LoadURL(GUI::format_wxstr( - "file://%1%/web/connection_failed.html", + "file://%1%/web/other_error.html", boost::filesystem::path(resources_dir()).generic_string() )); } @@ -143,16 +162,16 @@ void ConfigWizardWebViewPage::on_navigation_request(wxWebViewEvent &evt) { wxString url = evt.GetURL(); if (url.starts_with(L"qidislicer")) { - delete_cookies(m_browser, "https://account.qidi3d.com"); + 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"); evt.Veto(); m_vetoed = true; wxPostEvent(wxGetApp().plater(), Event(EVT_LOGIN_VIA_WIZARD, into_u8(url))); - } else if (url.Find("accounts.google.com") != wxString::npos - || url.Find("appleid.apple.com") != wxString::npos - || url.Find("facebook.com") != wxString::npos) + } else if (url.Find("accounts.google.com") != wxNOT_FOUND + || url.Find("appleid.apple.com") != wxNOT_FOUND + || url.Find("facebook.com") != wxNOT_FOUND) { auto& sc = Utils::ServiceConfig::instance(); if (!m_evt_sent && !url.starts_with(GUI::from_u8(sc.account_url()))) { @@ -164,6 +183,12 @@ void ConfigWizardWebViewPage::on_navigation_request(wxWebViewEvent &evt) } } +void ConfigWizardWebViewPage::on_script_message(wxWebViewEvent& evt) +{ + // only reaload button on erro page + m_browser->LoadURL(p_user_account->generate_login_redirect_url()); +} + //y15 void ConfigWizardWebViewPage::is_login(wxWebViewEvent& evt) { diff --git a/src/slic3r/GUI/ConfigWizardWebViewPage.hpp b/src/slic3r/GUI/ConfigWizardWebViewPage.hpp index 119c605..40f14dd 100644 --- a/src/slic3r/GUI/ConfigWizardWebViewPage.hpp +++ b/src/slic3r/GUI/ConfigWizardWebViewPage.hpp @@ -53,6 +53,7 @@ public: void on_error(wxWebViewEvent &evt); void on_navigation_request(wxWebViewEvent &evt); void on_idle(wxIdleEvent &evt); + void on_script_message(wxWebViewEvent& evt); void load_error_page(); // returns true if logged in - wizard needs to update repos bool login_changed(); diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 23a21f6..7fee0bb 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -21,7 +21,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/PresetBundle.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" +#include "slic3r/Utils/PresetUpdaterWrapper.hpp" #include "BedShapeDialog.hpp" #include "GUI.hpp" #include "SavePresetDialog.hpp" @@ -709,7 +709,7 @@ struct ConfigWizard::priv bool can_select_all(); bool on_bnt_finish(); bool check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id = std::string()); - bool apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes); + bool apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdaterWrapper *updater, bool& apply_keeped_changes); // #ys_FIXME_alise void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add); //#ifdef __linux__ diff --git a/src/slic3r/GUI/ConnectRequestHandler.cpp b/src/slic3r/GUI/ConnectRequestHandler.cpp new file mode 100644 index 0000000..b538090 --- /dev/null +++ b/src/slic3r/GUI/ConnectRequestHandler.cpp @@ -0,0 +1,135 @@ +#include "ConnectRequestHandler.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/UserAccount.hpp" + +#include +#include +#include + +namespace pt = boost::property_tree; + +namespace Slic3r::GUI { + +ConnectRequestHandler::ConnectRequestHandler() +{ + m_actions["REQUEST_LOGIN"] = std::bind(&ConnectRequestHandler::on_connect_action_request_login, this, std::placeholders::_1); + m_actions["REQUEST_CONFIG"] = std::bind(&ConnectRequestHandler::on_connect_action_request_config, this, std::placeholders::_1); + m_actions["WEBAPP_READY"] = std::bind(&ConnectRequestHandler::on_connect_action_webapp_ready,this, std::placeholders::_1); + m_actions["SELECT_PRINTER"] = std::bind(&ConnectRequestHandler::on_connect_action_select_printer, this, std::placeholders::_1); + m_actions["PRINT"] = std::bind(&ConnectRequestHandler::on_connect_action_print, this, std::placeholders::_1); + m_actions["REQUEST_OPEN_IN_BROWSER"] = std::bind(&ConnectRequestHandler::on_connect_action_request_open_in_browser, this, std::placeholders::_1); + m_actions["ERROR"] = std::bind(&ConnectRequestHandler::on_connect_action_error, this, std::placeholders::_1); + m_actions["LOG"] = std::bind(&ConnectRequestHandler::on_connect_action_log, this, std::placeholders::_1); + m_actions["RELOAD_HOME_PAGE"] = std::bind(&ConnectRequestHandler::on_reload_event, this, std::placeholders::_1); + m_actions["CLOSE_DIALOG"] = std::bind(&ConnectRequestHandler::on_connect_action_close_dialog, this, std::placeholders::_1); +} +ConnectRequestHandler::~ConnectRequestHandler() +{ +} +void ConnectRequestHandler::handle_message(const std::string& message) +{ + // read msg and choose action + /* + v0: + {"type":"request","detail":{"action":"requestAccessToken"}} + v1: + {"action":"REQUEST_ACCESS_TOKEN"} + */ + std::string action_string; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + // v1: + if (const auto action = ptree.get_optional("action"); action) { + action_string = *action; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse _qidiConnect message. " << e.what(); + return; + } + + if (action_string.empty()) { + BOOST_LOG_TRIVIAL(error) << "Received invalid message from _qidiConnect (missing action). Message: " << message; + return; + } + assert(m_actions.find(action_string) != m_actions.end()); // this assert means there is a action that has no handling. + if (m_actions.find(action_string) != m_actions.end()) { + m_actions[action_string](message); + } +} + +void ConnectRequestHandler::on_connect_action_error(const std::string &message_data) +{ + BOOST_LOG_TRIVIAL(error) << "WebView runtime error: " << message_data; +} + +void ConnectRequestHandler::resend_config() +{ + on_connect_action_request_config({}); +} + +void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(info) << "WebView log: " << message_data; +} + +void ConnectRequestHandler::on_connect_action_request_login(const std::string &message_data) +{} + +void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) +{ + /* + accessToken?: string; + clientVersion?: string; + colorMode?: "LIGHT" | "DARK"; + language?: ConnectLanguage; + sessionId?: string; + */ + const std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + //const std::string sesh = wxGetApp().plater()->get_user_account()->get_shared_session_key(); + const std::string dark_mode = wxGetApp().dark_mode() ? "DARK" : "LIGHT"; + wxString language = GUI::wxGetApp().current_language_code(); + language = language.SubString(0, 1); + const std::string init_options = GUI::format("{\"accessToken\": \"%4%\",\"clientVersion\": \"%1%\", \"colorMode\": \"%2%\", \"language\": \"%3%\"}", SLIC3R_VERSION, dark_mode, language, token ); + wxString script = GUI::format_wxstr("window._qidiConnect_v2.init(%1%)", init_options); + run_script_bridge(script); + +} +void ConnectRequestHandler::on_connect_action_request_open_in_browser(const std::string& message_data) +{ + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto url = ptree.get_optional("url"); url) { + wxGetApp().open_browser_with_warning_dialog(GUI::from_u8(*url)); + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse _qidiConnect message. " << e.what(); + return; + } +} + +SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : + wxDialog(parent, wxID_ANY, "Source Code", + wxDefaultPosition, wxSize(700,500), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, source, + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | + wxTE_RICH | + wxTE_READONLY); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(text, 1, wxEXPAND); + SetSizer(sizer); +} +} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/ConnectRequestHandler.hpp b/src/slic3r/GUI/ConnectRequestHandler.hpp new file mode 100644 index 0000000..50053b5 --- /dev/null +++ b/src/slic3r/GUI/ConnectRequestHandler.hpp @@ -0,0 +1,46 @@ +#ifndef slic3r_ConnectRequestHandler_hpp_ +#define slic3r_ConnectRequestHandler_hpp_ + +#include +#include +#include +#include +#include +#include + +//#define DEBUG_URL_PANEL + +namespace Slic3r::GUI { +class ConnectRequestHandler +{ +public: + ConnectRequestHandler(); + ~ConnectRequestHandler(); + + void handle_message(const std::string& message); + void resend_config(); +protected: + // action callbacks stored in m_actions + virtual void on_connect_action_log(const std::string& message_data); + virtual void on_connect_action_error(const std::string& message_data); + virtual void on_connect_action_request_login(const std::string& message_data); + virtual void on_connect_action_request_config(const std::string& message_data); + virtual void on_connect_action_request_open_in_browser(const std::string& message_data); + virtual void on_connect_action_select_printer(const std::string& message_data) = 0; + virtual void on_connect_action_print(const std::string& message_data) = 0; + virtual void on_connect_action_webapp_ready(const std::string& message_data) = 0; + virtual void on_connect_action_close_dialog(const std::string& message_data) = 0; + virtual void on_reload_event(const std::string& message_data) = 0; + virtual void run_script_bridge(const wxString &script) = 0; + + std::map> m_actions; +}; + +class SourceViewDialog : public wxDialog +{ +public: + SourceViewDialog(wxWindow* parent, wxString source); +}; + +} // namespace Slic3r::GUI +#endif /* slic3r_ConnectRequestHandler_hpp_ */ \ No newline at end of file diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index 7ca7adf..38ef4f9 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -9,10 +9,10 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Platform.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/Utils/DirectoriesUtils.hpp" #include // IWYU pragma: keep #include -#include #include #include #include @@ -219,6 +219,7 @@ bool create_desktop_file(const std::string& path, const std::string& data) // methods that actually do / undo desktop integration. Static to be accesible from anywhere. bool DesktopIntegrationDialog::is_integrated() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; const AppConfig *app_config = wxGetApp().app_config; std::string path(app_config->get("desktop_integration_app_path")); BOOST_LOG_TRIVIAL(debug) << "Desktop integration desktop file path: " << path; @@ -232,10 +233,12 @@ bool DesktopIntegrationDialog::is_integrated() } bool DesktopIntegrationDialog::integration_possible() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; return true; } void DesktopIntegrationDialog::perform_desktop_integration() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; BOOST_LOG_TRIVIAL(debug) << "performing desktop integration."; // Path to appimage const char *appimage_env = std::getenv("APPIMAGE"); @@ -441,8 +444,9 @@ void DesktopIntegrationDialog::perform_desktop_integration() } wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess); } -void DesktopIntegrationDialog::undo_desktop_intgration() +void DesktopIntegrationDialog::undo_desktop_integration() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; const AppConfig *app_config = wxGetApp().app_config; // slicer .desktop std::string path = std::string(app_config->get("desktop_integration_app_path")); @@ -629,6 +633,7 @@ void DesktopIntegrationDialog::perform_downloader_desktop_integration() } void DesktopIntegrationDialog::undo_downloader_registration() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; const AppConfig *app_config = wxGetApp().app_config; std::string path = std::string(app_config->get("desktop_integration_URL_path")); if (!path.empty()) { @@ -639,6 +644,7 @@ void DesktopIntegrationDialog::undo_downloader_registration() } void DesktopIntegrationDialog::undo_downloader_registration_rigid() { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; // Try ro find any QIDISlicerURLProtocol.desktop files including alpha and beta and get rid of them // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. @@ -650,7 +656,7 @@ void DesktopIntegrationDialog::undo_downloader_registration_rigid() target_candidates.emplace_back(GUI::into_u8(wxFileName::GetHomeDir()) + "/.local/share"); resolve_path_from_var("XDG_DATA_HOME", target_candidates); resolve_path_from_var("XDG_DATA_DIRS", target_candidates); - for (const std::string cand : target_candidates) { + for (const std::string& cand : target_candidates) { boost::filesystem::path apps_path = get_existing_dir(cand, "applications"); if (apps_path.empty()) { continue; @@ -670,6 +676,57 @@ void DesktopIntegrationDialog::undo_downloader_registration_rigid() } } +void DesktopIntegrationDialog::find_all_desktop_files(std::vector& results) +{ + // Try ro find any QIDISlicer.desktop and QIDISlicerGcodeViewer.desktop and QIDISlicerURLProtocol.desktop files including alpha and beta + + // For regular apps (f.e. appimage) this is true: + // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. + // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. + // $XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data files in addition to the $XDG_DATA_HOME base directory. + // The directories in $XDG_DATA_DIRS should be seperated with a colon ':'. + // If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. + + // But flatpak resets XDG_DATA_HOME and XDG_DATA_DIRS, so we do not look into them + // Lets look into $HOME/.local/share, /usr/local/share/, /usr/share/ + std::vector target_candidates; + if (auto home_config_dir = Slic3r::get_home_local_dir(); home_config_dir) { + target_candidates.emplace_back((*home_config_dir).string() + "/share"); + } + target_candidates.emplace_back("usr/local/share/"); + target_candidates.emplace_back("usr/share/"); + for (const std::string& cand : target_candidates) { + boost::filesystem::path apps_path = get_existing_dir(cand, "applications"); + if (apps_path.empty()) { + continue; + } + for (const std::string& filename : {"QIDISlicer","QIDISlicerGcodeViewer","QIDISlicerURLProtocol"}) { + for (const std::string& suffix : {"" , "-beta", "-alpha", "_beta", "_alpha"}) { + boost::filesystem::path file_path = apps_path / GUI::format("%1%%2%.desktop", filename, suffix); + boost::system::error_code ec; + if (!boost::filesystem::exists(file_path, ec) || ec) { + continue; + } + BOOST_LOG_TRIVIAL(debug) << "Desktop File found: " << file_path; + results.emplace_back(std::move(file_path)); + } + } + } +} + +void DesktopIntegrationDialog::remove_desktop_file_list(const std::vector& list, std::vector& fails) +{ + for (const boost::filesystem::path& entry : list) { + boost::system::error_code ec; + if (!boost::filesystem::remove(entry, ec) || ec) { + BOOST_LOG_TRIVIAL(error) << "Failed to remove file " << entry << " ec: " << ec.message(); + fails.emplace_back(entry); + continue; + } + BOOST_LOG_TRIVIAL(info) << "Desktop File removed: " << entry; + } +} + DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) : wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { @@ -700,7 +757,7 @@ DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) if (can_undo){ wxButton *btn_undo = new wxButton(this, wxID_ANY, _L("Undo")); btn_szr->Add(btn_undo, 0, wxALL, 10); - btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::undo_desktop_intgration(); EndModal(wxID_ANY); }); + btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::undo_desktop_integration(); EndModal(wxID_ANY); }); } wxButton *btn_cancel = new wxButton(this, wxID_ANY, _L("Cancel")); btn_szr->Add(btn_cancel, 0, wxALL, 10); @@ -713,7 +770,6 @@ DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) DesktopIntegrationDialog::~DesktopIntegrationDialog() { - } } // namespace GUI diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.hpp b/src/slic3r/GUI/DesktopIntegrationDialog.hpp index 093b4f4..6afd50b 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.hpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.hpp @@ -3,6 +3,7 @@ #define slic3r_DesktopIntegrationDialog_hpp_ #include +#include namespace Slic3r { namespace GUI { @@ -31,13 +32,13 @@ public: // Regiters QIDISlicer to start on qidislicer:// URL static void perform_desktop_integration(); // Deletes Desktop files and icons for both QIDISlicer and GcodeViewer at paths stored in App Config. - static void undo_desktop_intgration(); + static void undo_desktop_integration(); static void perform_downloader_desktop_integration(); static void undo_downloader_registration(); static void undo_downloader_registration_rigid(); -private: - + static void find_all_desktop_files(std::vector& results); + static void remove_desktop_file_list(const std::vector& list, std::vector& fails); }; } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/DoubleSliderForLayers.cpp b/src/slic3r/GUI/DoubleSliderForLayers.cpp index a6b4b59..658cbd5 100644 --- a/src/slic3r/GUI/DoubleSliderForLayers.cpp +++ b/src/slic3r/GUI/DoubleSliderForLayers.cpp @@ -290,7 +290,7 @@ void DSForLayers::draw_ruler(const ImRect& slideable_region) const float tick_width = float(int(1.0f * m_scale +0.5f)); const float label_height = m_imgui->GetTextureCustomRect(ImGui::PausePrint)->Height; - const ImU32 tick_clr = IM_COL32(255, 255, 255, 255); + constexpr ImU32 tick_clr = IM_COL32(255, 255, 255, 255); const float x_center = slideable_region.GetCenter().x; @@ -323,7 +323,7 @@ void DSForLayers::draw_ruler(const ImRect& slideable_region) ImGui::RenderText(start, label.c_str()); }; - auto draw_tick = [tick_clr, x_center, tick_width, inner_x](const float tick_pos, const float outer_x) + auto draw_tick = [x_center, tick_width, inner_x, tick_clr](const float tick_pos, const float outer_x) { ImRect tick_right = ImRect(x_center + inner_x, tick_pos - tick_width, x_center + outer_x, tick_pos); ImGui::RenderFrame(tick_right.Min, tick_right.Max, tick_clr, false); diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index 043c267..0c0ff90 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -5,6 +5,7 @@ #include #include +#include namespace Slic3r { namespace GUI { @@ -68,16 +69,32 @@ std::string filename_from_url(const std::string& url) return std::string(); return std::string(url_plain.begin() + slash + 1, url_plain.end()); } + +std::string unescape_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + int decodelen; + char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} } -Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) +Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder, bool load_after) : m_id(ID) , m_filename(filename_from_url(url)) , m_dest_folder(dest_folder) { assert(boost::filesystem::is_directory(dest_folder)); m_final_path = dest_folder / m_filename; - m_file_get = std::make_shared(ID, std::move(url), m_filename, evt_handler, dest_folder); + m_file_get = std::make_shared(ID, std::move(url), m_filename, evt_handler, dest_folder, load_after); } void Download::start() @@ -112,11 +129,6 @@ void Download::resume() Downloader::Downloader() : wxEvtHandler() { - //Bind(EVT_DWNLDR_FILE_COMPLETE, [](const wxCommandEvent& evt) {}); - //Bind(EVT_DWNLDR_FILE_PROGRESS, [](const wxCommandEvent& evt) {}); - //Bind(EVT_DWNLDR_FILE_ERROR, [](const wxCommandEvent& evt) {}); - //Bind(EVT_DWNLDR_FILE_NAME_CHANGE, [](const wxCommandEvent& evt) {}); - Bind(EVT_DWNLDR_FILE_COMPLETE, &Downloader::on_complete, this); Bind(EVT_DWNLDR_FILE_PROGRESS, &Downloader::on_progress, this); Bind(EVT_DWNLDR_FILE_ERROR, &Downloader::on_error, this); @@ -129,23 +141,18 @@ void Downloader::start_download(const std::string& full_url) { assert(m_initialized); - // TODO: There is a misterious slash appearing in recieved msg on windows -#ifdef _WIN32 - if (!boost::starts_with(full_url, "qidislicer://open/?file=")) { -#else - if (!boost::starts_with(full_url, "qidislicer://open?file=")) { -#endif - BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; - // TODO: show error? + std::string escaped_url = unescape_url(full_url); + if (boost::starts_with(escaped_url, "qidislicer://open?file=")) { + escaped_url = escaped_url.substr(24); + }else if (boost::starts_with(escaped_url, "qidislicer://open/?file=")) { + escaped_url = escaped_url.substr(25); + } else { + BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; return; - } + } + size_t id = get_next_id(); - // TODO: still same mistery -#ifdef _WIN32 - std::string escaped_url = FileGet::escape_url(full_url.substr(25)); -#else - std::string escaped_url = FileGet::escape_url(full_url.substr(24)); -#endif + if (!boost::starts_with(escaped_url, "https://") || !FileGet::is_subdomain(escaped_url, "printables.com")) { std::string msg = format(_L("Download won't start. Download URL doesn't point to https://printables.com : %1%"), escaped_url); BOOST_LOG_TRIVIAL(error) << msg; @@ -154,14 +161,38 @@ void Downloader::start_download(const std::string& full_url) return; } - std::string text(escaped_url); - m_downloads.emplace_back(std::make_unique(id, std::move(escaped_url), this, m_dest_folder)); + m_downloads.emplace_back(std::make_unique(id, std::move(escaped_url), this, m_dest_folder, true)); NotificationManager* ntf_mngr = wxGetApp().notification_manager(); ntf_mngr->push_download_URL_progress_notification(id, m_downloads.back()->get_filename(), std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2)); m_downloads.back()->start(); BOOST_LOG_TRIVIAL(debug) << "started download"; } +void Downloader::start_download_printables(const std::string& url, bool load_after, const std::string& printables_url, GUI_App* app) +{ + assert(m_initialized); + + size_t id = get_next_id(); + + if (!boost::starts_with(url, "https://") || !FileGet::is_subdomain(url, "printables.com")) { + std::string msg = format(_L("Download won't start. Download URL doesn't point to https://printables.com : %1%"), url); + BOOST_LOG_TRIVIAL(error) << msg; + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, msg); + return; + } + + m_downloads.emplace_back(std::make_unique(id, url, this, m_dest_folder, load_after)); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_download_URL_progress_notification_with_printables_link( id + , m_downloads.back()->get_filename() + , printables_url + , std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2) + , std::bind(&GUI_App::open_link_in_printables, app, std::placeholders::_1) + ); + m_downloads.back()->start(); +} + void Downloader::on_progress(wxCommandEvent& event) { size_t id = event.GetInt(); @@ -180,14 +211,14 @@ void Downloader::on_error(wxCommandEvent& event) ntf_mngr->set_download_URL_error(id, into_u8(event.GetString())); show_error(nullptr, format_wxstr(L"%1%\n%2%", _L("The download has failed") + ":", event.GetString())); } -void Downloader::on_complete(wxCommandEvent& event) +void Downloader::on_complete(Event& event) { - // TODO: is this always true? : // here we open the file itself, notification should get 1.f progress from on progress. - set_download_state(event.GetInt(), DownloadState::DownloadDone); + set_download_state(event.data.id, DownloadState::DownloadDone); wxArrayString paths; - paths.Add(event.GetString()); - wxGetApp().plater()->load_files(paths); + paths.Add(event.data.path); + if (event.data.load_after) + wxGetApp().plater()->load_files(paths); } bool Downloader::user_action_callback(DownloaderUserAction action, int id) { diff --git a/src/slic3r/GUI/Downloader.hpp b/src/slic3r/GUI/Downloader.hpp index 84a9a95..c2ff66e 100644 --- a/src/slic3r/GUI/Downloader.hpp +++ b/src/slic3r/GUI/Downloader.hpp @@ -9,6 +9,7 @@ namespace Slic3r { namespace GUI { class NotificationManager; +class GUI_App; enum DownloadState { @@ -31,7 +32,7 @@ enum DownloaderUserAction class Download { public: - Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder, bool load_after); void start(); void cancel(); void pause(); @@ -63,6 +64,8 @@ public: m_initialized = true; } void start_download(const std::string& full_url); + + void start_download_printables(const std::string& url, bool load_after, const std::string& printables_url, GUI_App* app); // cancel = false -> just pause bool user_action_callback(DownloaderUserAction action, int id); private: @@ -76,7 +79,7 @@ private: void on_progress(wxCommandEvent& event); void on_error(wxCommandEvent& event); - void on_complete(wxCommandEvent& event); + void on_complete(Event& event); void on_name_change(wxCommandEvent& event); void on_paused(wxCommandEvent& event); void on_canceled(wxCommandEvent& event); diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp index e6ea936..afa08af 100644 --- a/src/slic3r/GUI/DownloaderFileGet.cpp +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -19,21 +19,6 @@ namespace GUI { const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024; const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024; -std::string FileGet::escape_url(const std::string& unescaped) -{ - std::string ret_val; - CURL* curl = curl_easy_init(); - if (curl) { - int decodelen; - char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); - if (decoded) { - ret_val = std::string(decoded); - curl_free(decoded); - } - curl_easy_cleanup(curl); - } - return ret_val; -} bool FileGet::is_subdomain(const std::string& url, const std::string& domain) { // domain should be f.e. printables.com (.com including) @@ -82,7 +67,7 @@ unsigned get_current_pid() } // int = DOWNLOAD ID; string = file path -wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, Event); // int = DOWNLOAD ID; string = error msg wxDEFINE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent); // int = DOWNLOAD ID; string = progress percent @@ -108,17 +93,19 @@ struct FileGet::priv std::atomic_bool m_stopped { false }; // either canceled or paused - download is not running size_t m_written { 0 }; size_t m_absolute_size { 0 }; - priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + bool m_load_after; + priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder, bool load_after); void get_perform(); }; -FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) +FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder, bool load_after) : m_id(ID) , m_url(std::move(url)) , m_filename(filename) , m_evt_handler(evt_handler) , m_dest_folder(dest_folder) + , m_load_after(load_after) { } @@ -275,23 +262,8 @@ void FileGet::priv::get_perform() m_evt_handler->QueueEvent(evt); }) .on_complete([&](std::string body, unsigned /* http_status */) { - - // TODO: perform a body size check - // - //size_t body_size = body.size(); - //if (body_size != expected_size) { - // return; - //} try { - /* - if (m_written < body.size()) - { - // this code should never be entered. As there should be on_progress call after last bit downloaded. - std::string part_for_write = body.substr(m_written); - fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); - } - */ fclose(file); boost::filesystem::rename(m_tmp_path, dest_path); } @@ -305,18 +277,15 @@ void FileGet::priv::get_perform() m_evt_handler->QueueEvent(evt); return; } - - wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_COMPLETE); - evt->SetString(dest_path.wstring()); - evt->SetInt(m_id); - m_evt_handler->QueueEvent(evt); + DownloadEventData event_data = {m_id, dest_path.wstring(), m_load_after}; + wxQueueEvent(m_evt_handler, new Event(EVT_DWNLDR_FILE_COMPLETE, event_data)); }) .perform_sync(); } -FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) - : p(new priv(ID, std::move(url), filename, evt_handler, dest_folder)) +FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder, bool load_after) + : p(new priv(ID, std::move(url), filename, evt_handler, dest_folder, load_after)) {} FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {} diff --git a/src/slic3r/GUI/DownloaderFileGet.hpp b/src/slic3r/GUI/DownloaderFileGet.hpp index 022d4c0..46ed540 100644 --- a/src/slic3r/GUI/DownloaderFileGet.hpp +++ b/src/slic3r/GUI/DownloaderFileGet.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_DownloaderFileGet_hpp_ #define slic3r_DownloaderFileGet_hpp_ +#include "Event.hpp" + #include "../Utils/Http.hpp" #include @@ -15,7 +17,7 @@ class FileGet : public std::enable_shared_from_this { private: struct priv; public: - FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder); + FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder, bool load_after); FileGet(FileGet&& other); ~FileGet(); @@ -23,13 +25,20 @@ public: void cancel(); void pause(); void resume(); - static std::string escape_url(const std::string& url); - static bool is_subdomain(const std::string& url, const std::string& domain); + static bool is_subdomain(const std::string& url, const std::string& domain); private: std::unique_ptr p; }; + +struct DownloadEventData +{ + int id; + wxString path; + bool load_after; +}; + // int = DOWNLOAD ID; string = file path -wxDECLARE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +wxDECLARE_EVENT(EVT_DWNLDR_FILE_COMPLETE, Event); // int = DOWNLOAD ID; string = error msg wxDECLARE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent); // int = DOWNLOAD ID; string = progress percent diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index e8bf1ce..dae1c51 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -323,19 +323,22 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true if ((m_opt.type == coFloatOrPercent || m_opt.type == coFloatsOrPercents) && !str.IsEmpty() && str.Last() != '%') { double val = 0.; + + bool is_na_value = m_opt.nullable && str == na_value(); + const char dec_sep = is_decimal_separator_point() ? '.' : ','; const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; // Replace the first incorrect separator in decimal number. - if (str.Replace(dec_sep_alt, dec_sep, false) != 0) + if (!is_na_value && str.Replace(dec_sep_alt, dec_sep, false) != 0) set_value(str, false); - // remove space and "mm" substring, if any exists str.Replace(" ", "", true); str.Replace("m", "", true); - if (!str.ToDouble(&val)) - { + if (is_na_value) { + val = ConfigOptionFloatsOrPercentsNullable::nil_value().value; + } else if (!str.ToDouble(&val)) { if (!check_value) { m_value.clear(); break; @@ -445,8 +448,11 @@ void TextCtrl::BUILD() { case coFloatsOrPercents: { const auto val = m_opt.get_default_value()->get_at(m_opt_idx); text_value = double_to_string(val.value); - if (val.percent) + if (val.percent) { text_value += "%"; + } + + m_last_meaningful_value = text_value; break; } case coPercent: diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index fb60ed2..4d35804 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -25,6 +25,8 @@ #include "GUI_ObjectManipulation.hpp" #include "MsgDialog.hpp" +#include "libslic3r/MultipleBeds.hpp" + #if ENABLE_ACTUAL_SPEED_DEBUG #define IMGUI_DEFINE_MATH_OPERATORS #endif // ENABLE_ACTUAL_SPEED_DEBUG @@ -80,7 +82,10 @@ void GCodeViewer::COG::render() const double inv_zoom = camera.get_inv_zoom(); model_matrix = model_matrix * Geometry::scale_transform(inv_zoom); } - const Transform3d& view_matrix = camera.get_view_matrix(); + + Transform3d view_matrix = camera.get_view_matrix(); + view_matrix.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + 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(); @@ -227,7 +232,10 @@ void GCodeViewer::SequentialView::Marker::render() shader->start_using(); shader->set_uniform("emission_factor", 0.0f); const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d& view_matrix = camera.get_view_matrix(); + + Transform3d view_matrix = camera.get_view_matrix(); + view_matrix.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + float scale_factor = m_scale_factor; if (m_fixed_screen_size) scale_factor *= 10.0f * camera.get_inv_zoom(); @@ -856,7 +864,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_viewer.toggle_top_layer_only_view_range(); // avoid processing if called with the same gcode_result - if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) { + if (m_last_result_id == gcode_result.id && ! s_beds_switched_since_last_gcode_load && wxGetApp().is_editor() && ! s_reload_preview_after_switching_beds) { // collect tool colors libvgcode::Palette tools_colors; tools_colors.reserve(str_tool_colors.size()); @@ -876,6 +884,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const } m_last_result_id = gcode_result.id; + s_beds_switched_since_last_gcode_load = false; // release gpu memory, if used reset(); @@ -1008,8 +1017,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const }); m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); - if (wxGetApp().is_editor()) + if (wxGetApp().is_editor()) { m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + if (!m_contained_in_bed) { + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::toolpath_outside; + } + } m_extruders_count = gcode_result.extruders_count; m_sequential_view.gcode_window.load_gcode(gcode_result); @@ -1102,6 +1115,9 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data) const libvgcode::AABox bbox = m_viewer.get_extrusion_bounding_box(); const BoundingBoxf3 paths_bounding_box(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(GCodeProcessorResult(), paths_bounding_box); + if (!m_contained_in_bed) { + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::toolpath_outside; + } } void GCodeViewer::update_shells_color_by_extruder(const DynamicPrintConfig* config) @@ -1685,9 +1701,9 @@ void GCodeViewer::load_wipetower_shell(const Print& print) const std::vector> z_and_depth_pairs = print.wipe_tower_data(extruders_count).z_and_depth_pairs; const float brim_width = wipe_tower_data.brim_width; if (depth != 0.) { - m_shells.volumes.load_wipe_tower_preview(config.wipe_tower_x, config.wipe_tower_y, config.wipe_tower_width, depth, z_and_depth_pairs, - max_z, config.wipe_tower_cone_angle, config.wipe_tower_rotation_angle, false, brim_width); - GLVolume* volume = m_shells.volumes.volumes.back(); + GLVolume* volume{m_shells.volumes.load_wipe_tower_preview(wxGetApp().plater()->model().wipe_tower().position.x(), wxGetApp().plater()->model().wipe_tower().position.y(), config.wipe_tower_width, depth, z_and_depth_pairs, + max_z, config.wipe_tower_cone_angle, wxGetApp().plater()->model().wipe_tower().rotation, false, brim_width, 0)}; + m_shells.volumes.volumes.emplace_back(volume); volume->color.a(0.25f); volume->force_native_color = true; volume->set_render_color(true); @@ -1701,7 +1717,12 @@ void GCodeViewer::load_wipetower_shell(const Print& print) void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); - const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast(camera.get_view_matrix().matrix().cast())); + + Transform3d tr = camera.get_view_matrix(); + tr.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + Matrix4f m = tr.matrix().cast(); + + const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(m); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size); @@ -1891,7 +1912,11 @@ void GCodeViewer::render_shells() shader->start_using(); shader->set_uniform("emission_factor", 0.1f); const Camera& camera = wxGetApp().plater()->get_camera(); - m_shells.volumes.render(GLVolumeCollection::ERenderType::Transparent, true, camera.get_view_matrix(), camera.get_projection_matrix()); + + Transform3d tr = camera.get_view_matrix(); + tr.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + + m_shells.volumes.render(GLVolumeCollection::ERenderType::Transparent, true, tr, camera.get_projection_matrix()); shader->set_uniform("emission_factor", 0.0f); shader->stop_using(); } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 20ecc44..3acec38 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3,6 +3,7 @@ #include // IWYU pragma: keep #include +#include #include "libslic3r/BuildVolume.hpp" #include "libslic3r/ClipperUtils.hpp" @@ -11,6 +12,7 @@ #include "libslic3r/Geometry/ConvexHull.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Layer.hpp" +#include "libslic3r/MultipleBeds.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Technologies.hpp" #include "libslic3r/Tesselate.hpp" @@ -33,6 +35,7 @@ #include "NotificationManager.hpp" #include "format.hpp" +#include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/Utils/UndoRedo.hpp" @@ -62,18 +65,23 @@ #include #include +#include #include #include #include #include +#include + #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif #include #include +extern std::vector s_th_tex_id; + static constexpr const float TRACKBALLSIZE = 0.8f; //B12 @@ -96,6 +104,52 @@ static bool show_imgui_demo_window = false; namespace Slic3r { namespace GUI { +void GLCanvas3D::select_bed(int i, bool triggered_by_user) +{ + int old_bed = s_multiple_beds.get_active_bed(); + if ((i == old_bed && !s_multiple_beds.is_autoslicing()) || i == -1) + return; + + if (current_printer_technology() == ptSLA) { + // Close SlaSupports or Hollow gizmos before switching beds. They rely on having access to SLAPrintObject to work. + if (GLGizmosManager::EType cur_giz = get_gizmos_manager().get_current_type(); + cur_giz == GLGizmosManager::EType::SlaSupports || cur_giz == GLGizmosManager::EType::Hollow) { + if (! get_gizmos_manager().open_gizmo(get_gizmos_manager().get_current_type())) + return; + } + } + 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]))); + + // The stop call above schedules some events that would be processed after the switch. + // Among else, on_process_completed would be called, which would stop slicing of + // the new bed. We need to stop the process, pump all the events out of the queue + // and then switch the beds. + wxGetApp().CallAfter([i, old_bed, triggered_by_user]() { + wxYield(); + s_multiple_beds.set_active_bed(i); + s_beds_just_switched = true; + s_beds_switched_since_last_gcode_load = true; + if (wxGetApp().plater()->is_preview_shown()) { + s_reload_preview_after_switching_beds = true; + wxPostEvent(wxGetApp().plater(), SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); + wxGetApp().plater()->get_camera().translate_world( + s_multiple_beds.get_bed_translation(i) + - s_multiple_beds.get_bed_translation(old_bed) + ); + } + wxGetApp().plater()->schedule_background_process(); + wxGetApp().plater()->object_list_changed(); // Updates Slice Now / Export buttons. + if (s_multiple_beds.is_autoslicing() && triggered_by_user) { + s_multiple_beds.stop_autoslice(false); + wxGetApp().sidebar().switch_from_autoslicing_mode(); + } + }); +} + #ifdef __WXGTK3__ // wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support. RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {} @@ -876,6 +930,8 @@ void GLCanvas3D::SequentialPrintClearance::set_contours(const ContoursList& cont 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 }; @@ -889,7 +945,7 @@ void GLCanvas3D::SequentialPrintClearance::set_contours(const ContoursList& cont 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)(v.cast() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting + 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); @@ -898,6 +954,8 @@ void GLCanvas3D::SequentialPrintClearance::set_contours(const ContoursList& cont 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 @@ -906,14 +964,14 @@ void GLCanvas3D::SequentialPrintClearance::set_contours(const ContoursList& cont if (contours.trafos.has_value()) { // create the requested instances for (const auto& instance : *contours.trafos) { - m_instances.emplace_back(instance.first, instance.second); + 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, Transform3f::Identity()); + m_instances.emplace_back(i, bed_transform); } } } @@ -931,9 +989,9 @@ void GLCanvas3D::SequentialPrintClearance::update_instances_trafos(const std::ve void GLCanvas3D::SequentialPrintClearance::render() { - const ColorRGBA FILL_COLOR = { 1.0f, 0.0f, 0.0f, 0.5f }; - const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f }; - const ColorRGBA NO_FILL_EVALUATING_COLOR = { 1.0f, 1.0f, 0.0f, 1.0f }; + 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; @@ -981,10 +1039,13 @@ void GLCanvas3D::SequentialPrintClearance::render() 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((!m_evaluating && m_fill.is_initialized()) ? FILL_COLOR : m_evaluating ? NO_FILL_EVALUATING_COLOR : NO_FILL_COLOR); + m_contours[id].set_color(color); m_contours[id].render(); } @@ -1000,6 +1061,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE_CURRENT_BED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); @@ -1009,8 +1071,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MIRRORED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_TOUCHED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); //Y5 wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_EXPORT_BUTTONS, Event); @@ -1290,7 +1351,16 @@ PrinterTechnology GLCanvas3D::current_printer_technology() const bool GLCanvas3D::is_arrange_alignment_enabled() const { - return m_config ? is_XL_printer(*m_config) && !this->get_wipe_tower_info() : false; + if (m_config == nullptr) { + return false; + } + if (!is_XL_printer(*m_config)) { + return false; + } + if (this->m_wipe_tower_bounding_boxes[s_multiple_beds.get_active_bed()]) { + return false; + } + return true; } GLCanvas3D::GLCanvas3D(wxGLCanvas *canvas, Bed3D &bed) @@ -1343,6 +1413,9 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas *canvas, Bed3D &bed) m_arrange_settings_dialog.on_arrange_btn([]{ wxGetApp().plater()->arrange(); }); + m_arrange_settings_dialog.on_arrange_bed_btn([]{ + wxGetApp().plater()->arrange_current_bed(); + }); } GLCanvas3D::~GLCanvas3D() @@ -1464,8 +1537,9 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI //B52 for (unsigned int vol_idx : volumes_idxs) { GLVolume* volume = volumes.volumes[vol_idx]; - if (!volume->is_modifier && (volume->shader_outside_printer_detection_enabled || (!volume->is_wipe_tower && volume->composite_id.volume_id >= 0))) { + if (!volume->is_modifier && (volume->shader_outside_printer_detection_enabled || (!volume->is_wipe_tower() && volume->composite_id.volume_id >= 0))) { BuildVolume::ObjectState state; + int bed_idx = -1; if (volume_below(*volume)) state = BuildVolume::ObjectState::Below; else { @@ -1473,7 +1547,7 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI // B66 case BuildVolume::Type::Rectangle: //FIXME this test does not evaluate collision of a build volume bounding box with non-convex objects. - state = build_volume.volume_state_bbox(volume_bbox(*volume)); + state = build_volume.volume_state_bbox(volume_bbox(*volume), true, &bed_idx); if (state == BuildVolume::ObjectState::Inside) { for (size_t i = 0; i < m_model->objects.size(); ++i) { ModelObject * object = m_model->objects[i]; @@ -1491,7 +1565,7 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI case BuildVolume::Type::Convex: //FIXME doing test on convex hull until we learn to do test on non-convex polygons efficiently. case BuildVolume::Type::Custom: - state = build_volume.object_state(volume_convex_mesh(*volume).its, volume->world_matrix().cast(), volume_sinking(*volume)); + state = build_volume.object_state(volume_convex_mesh(*volume).its, volume->world_matrix().cast(), volume_sinking(*volume), true, &bed_idx); break; default: // Ignore, don't produce any collision. @@ -1504,9 +1578,13 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI if (volume->printable) { if (overall_state == ModelInstancePVS_Inside && volume->is_outside) overall_state = ModelInstancePVS_Fully_Outside; - if (overall_state == ModelInstancePVS_Fully_Outside && volume->is_outside && state == BuildVolume::ObjectState::Colliding) + if (overall_state == ModelInstancePVS_Fully_Outside && volume->is_outside && state == BuildVolume::ObjectState::Colliding) { overall_state = ModelInstancePVS_Partly_Outside; + } contained_min_one |= !volume->is_outside; + + if (bed_idx != -1 && bed_idx == s_multiple_beds.get_number_of_beds()) + s_multiple_beds.request_next_bed(true); } } else if (volume->is_modifier) @@ -1553,7 +1631,7 @@ void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject { std::vector>* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume); for (GLVolume* vol : m_volumes.volumes) { - if (vol->is_wipe_tower) + if (vol->is_wipe_tower()) vol->is_active = (visible && mo == nullptr); else { if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) @@ -1571,11 +1649,12 @@ void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject auto gizmo_type = gm.get_current_type(); if ( (gizmo_type == GLGizmosManager::FdmSupports || gizmo_type == GLGizmosManager::Seam - || gizmo_type == GLGizmosManager::Cut) + || gizmo_type == GLGizmosManager::Cut + || gizmo_type == GLGizmosManager::FuzzySkin) && !vol->is_modifier) { vol->force_neutral_color = true; } - else if (gizmo_type == GLGizmosManager::MmuSegmentation) + else if (gizmo_type == GLGizmosManager::MmSegmentation) vol->is_active = false; else vol->force_native_color = true; @@ -1727,8 +1806,10 @@ void GLCanvas3D::enable_layers_editing(bool enable) void GLCanvas3D::zoom_to_bed() { BoundingBoxf3 box = m_bed.build_volume().bounding_volume(); + box.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); box.min.z() = 0.0; box.max.z() = 0.0; + _zoom_to_box(box); } @@ -1763,6 +1844,306 @@ void GLCanvas3D::update_volumes_colors_by_extruder() m_volumes.update_colors_by_extruder(m_config); } +using PerBedStatistics = std::vector> +>>; + +PerBedStatistics get_statistics(){ + PerBedStatistics result; + for (int bed_index=0; bed_indexget_fff_prints()[bed_index].get(); + if (print->empty() || !print->finished()) { + result.emplace_back(bed_index, std::nullopt); + } else { + result.emplace_back(bed_index, std::optional{std::ref(print->print_statistics())}); + } + } + return result; +} + +struct StatisticsSum { + float cost{}; + float filement_weight{}; + float filament_length{}; + float normal_print_time{}; + float silent_print_time{}; +}; + +StatisticsSum get_statistics_sum() { + StatisticsSum result; + for (const auto &[_, statistics] : get_statistics()) { + if (!statistics) { + continue; + } + result.cost += statistics->get().total_cost; + result.filement_weight += statistics->get().total_weight; + result.filament_length += statistics->get().total_used_filament; + result.normal_print_time += statistics->get().normal_print_time_seconds; + result.silent_print_time += statistics->get().silent_print_time_seconds; + } + + return result; +} + +// retur width of table +float project_overview_table(float scale) { + const float width_gap = 10.f * scale; + float total_width{ width_gap }; + + ImGui::Text("%s", _u8L("Project overview").c_str()); + if (ImGui::BeginTable("project_overview_table", 6)) { + + float width = std::max(ImGui::CalcTextSize(format(_u8L("Bed %1%"), 1).c_str()).x, ImGui::CalcTextSize(_u8L("Total").c_str()).x) + width_gap; + total_width += width; + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width); + + std::string name = _u8L("Cost"); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + total_width += width; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + + name = _u8L("Filament (g)"); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + total_width += width; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + + name = _u8L("Filament (m)"); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + total_width += width; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + + // TRN %1% is one "Stealth mode" or "Normal mode" + name = format(_u8L("Estimated Time (%1%)"), _u8L("Stealth mode")); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + total_width += width; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + + name = format(_u8L("Estimated Time (%1%)"), _u8L("Normal mode")); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + total_width += width; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + ImGui::TableHeadersRow(); + + for (const auto &[bed_index, optional_statistics] : get_statistics()) { + if (optional_statistics) { + const std::reference_wrapper statistics{*optional_statistics}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + // TRN %1% is a number of the Bed + ImGui::Text("%s", format(_u8L("Bed %1%"), bed_index + 1).c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics.get().total_cost); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics.get().total_weight); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics.get().total_used_filament / 1000); + ImGui::TableNextColumn(); + ImGui::Text("%s", statistics.get().estimated_silent_print_time.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%s", statistics.get().estimated_normal_print_time.c_str()); + } else { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", format(_u8L("Bed %1%"), bed_index + 1).c_str()); + ImGui::TableNextColumn(); + ImGui::Text("-"); + ImGui::TableNextColumn(); + ImGui::Text("-"); + ImGui::TableNextColumn(); + ImGui::Text("-"); + ImGui::TableNextColumn(); + ImGui::Text("-"); + ImGui::TableNextColumn(); + ImGui::Text("-"); + } + } + + ImGui::PushStyleColor(ImGuiCol_Text, ImGuiPureWrap::COL_ORANGE_LIGHT); + + const StatisticsSum statistics_sum{get_statistics_sum()}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", _u8L("Total").c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics_sum.cost); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics_sum.filement_weight); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics_sum.filament_length / 1000); + ImGui::TableNextColumn(); + ImGui::Text("%s", get_time_dhms(statistics_sum.silent_print_time).c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%s", get_time_dhms(statistics_sum.normal_print_time).c_str()); + + ImGui::PopStyleColor(); + + ImGui::EndTable(); + } + + return total_width + 2.f * width_gap; +} + +struct ExtruderStatistics { + float filament_weight{}; + float filament_length{}; +}; + +using PerExtruderStatistics = std::map< + std::size_t, + ExtruderStatistics +>; + +PerExtruderStatistics get_extruder_statistics(){ + PerExtruderStatistics result; + for (int bed_index=0; bed_indexget_fff_prints()[bed_index].get(); + if (print->empty() || !print->finished()) { + continue; + } + print->print_statistics(); + const auto& extruders_filaments{wxGetApp().preset_bundle->extruders_filaments}; + for (const auto &[filament_id, filament_volume] : print->print_statistics().filament_stats) { + const Preset* preset = extruders_filaments[filament_id].get_selected_preset(); + if (preset == nullptr) { + continue; + } + + const double filament_density = preset->config.opt_float("filament_density", 0); + const double diameter = preset->config.opt_float("filament_diameter", filament_id); + result[filament_id].filament_weight += filament_volume * filament_density / 1000.0f; + result[filament_id].filament_length += filament_volume / (M_PI * diameter * diameter / 4.0) / 1000.0; + } + } + return result; +} + +ExtruderStatistics sum_extruder_statistics( + const PerExtruderStatistics &per_extruder_statistics +) { + ExtruderStatistics result; + for (const auto &[_, statistics] : per_extruder_statistics) { + result.filament_weight += statistics.filament_weight; + result.filament_length += statistics.filament_length; + } + + return result; +} + +void extruder_usage_table(const PerExtruderStatistics &extruder_statistics, const float scale) { + + ImGui::Text("%s", _u8L("Extruders usage breakdown").c_str()); + if (ImGui::BeginTable("extruder_usage_table", 3)) { + const float width_gap = 10.f * scale; + float width = width_gap + std::max(ImGui::CalcTextSize(format(_u8L("Extruder %1%"), 1).c_str()).x, + ImGui::CalcTextSize(_u8L("Total").c_str()).x); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width); + + std::string name = _u8L("Filament (g)"); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + + name = _u8L("Filament (m)"); + width = ImGui::CalcTextSize(name.c_str()).x + width_gap; + ImGui::TableSetupColumn( + name.c_str(), + ImGuiTableColumnFlags_WidthFixed, + width + ); + ImGui::TableHeadersRow(); + + for (const auto &[extruder_index, statistics] : extruder_statistics) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", format(_u8L("Extruder %1%"), extruder_index + 1).c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics.filament_weight); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", statistics.filament_length); + } + + ImGui::PushStyleColor(ImGuiCol_Text, ImGuiPureWrap::COL_ORANGE_LIGHT); + + const ExtruderStatistics extruder_statistics_sum{sum_extruder_statistics(extruder_statistics)}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", _u8L("Total").c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", extruder_statistics_sum.filament_weight); + ImGui::TableNextColumn(); + ImGui::Text("%.2f", extruder_statistics_sum.filament_length); + + ImGui::PopStyleColor(); + + ImGui::EndTable(); + } +} + +void begin_statistics(const char *window_name) { + ImGuiWindowFlags windows_flags = + ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_AlwaysAutoResize + | ImGuiWindowFlags_HorizontalScrollbar; + + const ImVec2 center{ImGui::GetMainViewport()->GetCenter()}; + const float y_postion{std::max(0.5f * center.y, 150.0f)}; + const ImVec2 position{center.x, y_postion}; + ImGui::SetNextWindowPos(position, ImGuiCond_Always, ImVec2{0.5f, 0.f}); + + ImGui::Begin(window_name, nullptr, windows_flags); +} + +static float content_size_x = 0.0f; +void render_print_statistics(float scale) { + ImGui::SetNextWindowContentSize(ImVec2(content_size_x, 0.0f)); + + begin_statistics(_u8L("Statistics").c_str()); + ImGui::Spacing(); + content_size_x = project_overview_table(scale); + ImGui::Separator(); + + const PerExtruderStatistics extruder_statistics{get_extruder_statistics()}; + if (extruder_statistics.size() > 1) { + ImGui::NewLine(); + extruder_usage_table(extruder_statistics, scale); + ImGui::Separator(); + } + ImGui::End(); +} + +void render_autoslicing_wait() { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.f,30.f)); + begin_statistics((_u8L("Generating statistics") + " ...").c_str()); + ImGui::Text("%s", _u8L("Statistics will be available once all beds are sliced").c_str()); + ImGui::PopStyleVar(); + ImGui::End(); +} + void GLCanvas3D::render() { if (m_in_render) { @@ -1818,7 +2199,14 @@ void GLCanvas3D::render() camera.requires_zoom_to_bed = false; } - camera.apply_projection(_max_bounding_box(true, true)); + camera.apply_projection(_max_bounding_box(true)); + + const int curr_active_bed_id = s_multiple_beds.get_active_bed(); + if (m_last_active_bed_id != curr_active_bed_id) { + const Vec3d bed_offset = s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()); + const Vec2d bed_center = m_bed.build_volume().bed_center() + Vec2d(bed_offset.x(), bed_offset.y()); + m_last_active_bed_id = curr_active_bed_id; + } wxGetApp().imgui()->new_frame(); @@ -1843,57 +2231,94 @@ 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 glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); _render_background(); - _render_objects(GLVolumeCollection::ERenderType::Opaque); - _render_sla_slices(); - _render_selection(); - _render_bed_axes(); - if (is_looking_downward) - _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false); - if (!m_main_toolbar.is_enabled() && current_printer_technology() != ptSLA) - _render_gcode(); - _render_objects(GLVolumeCollection::ERenderType::Transparent); + if (! s_multiple_beds.is_autoslicing()) { + _render_objects(GLVolumeCollection::ERenderType::Opaque); + _render_sla_slices(); + _render_selection(); + _render_bed_axes(); + if (is_looking_downward) + _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false); + if (!m_main_toolbar.is_enabled() && current_printer_technology() != ptSLA) + _render_gcode(); + _render_objects(GLVolumeCollection::ERenderType::Transparent); - _render_sequential_clearance(); -#if ENABLE_RENDER_SELECTION_CENTER - _render_selection_center(); -#endif // ENABLE_RENDER_SELECTION_CENTER - if (!m_main_toolbar.is_enabled()) - _render_gcode_cog(); + _render_sequential_clearance(); + #if ENABLE_RENDER_SELECTION_CENTER + _render_selection_center(); + #endif // ENABLE_RENDER_SELECTION_CENTER + if (!m_main_toolbar.is_enabled()) + _render_gcode_cog(); - // we need to set the mouse's scene position here because the depth buffer - // could be invalidated by the following gizmo render methods - // this position is used later into on_mouse() to drag the objects - if (m_picking_enabled) - m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); + // we need to set the mouse's scene position here because the depth buffer + // could be invalidated by the following gizmo render methods + // this position is used later into on_mouse() to drag the objects + if (m_picking_enabled) + m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); - // sidebar hints need to be rendered before the gizmos because the depth buffer - // could be invalidated by the following gizmo render methods - _render_selection_sidebar_hints(); - _render_current_gizmo(); - if (!is_looking_downward) - _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true); + // sidebar hints need to be rendered before the gizmos because the depth buffer + // could be invalidated by the following gizmo render methods + _render_selection_sidebar_hints(); + _render_current_gizmo(); + if (!is_looking_downward) + _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true); -#if ENABLE_RAYCAST_PICKING_DEBUG - if (m_picking_enabled && !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging()) - m_scene_raycaster.render_hit(camera); -#endif // ENABLE_RAYCAST_PICKING_DEBUG + #if ENABLE_RAYCAST_PICKING_DEBUG + if (m_picking_enabled && !m_mouse.dragging && !m_gizmos.is_dragging() && !m_rectangle_selection.is_dragging()) + m_scene_raycaster.render_hit(camera); + #endif // ENABLE_RAYCAST_PICKING_DEBUG #if ENABLE_SHOW_CAMERA_TARGET _render_camera_target(); + _render_camera_target_validation_box(); #endif // ENABLE_SHOW_CAMERA_TARGET - if (m_picking_enabled && m_rectangle_selection.is_dragging()) - m_rectangle_selection.render(*this); + if (m_picking_enabled && m_rectangle_selection.is_dragging()) + m_rectangle_selection.render(*this); + } else { + const auto &prints{wxGetApp().plater()->get_fff_prints()}; + + bool all_finished{true}; + for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) { + const std::unique_ptr &print{prints[bed_index]}; + if (!print->finished() && is_sliceable(s_print_statuses[bed_index])) { + all_finished = false; + break; + } + } + + if (!all_finished) { + render_autoslicing_wait(); + if (fff_print()->finished() || !is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])) { + s_multiple_beds.autoslice_next_bed(); + wxYield(); + } else { + wxGetApp().plater()->schedule_background_process(); + } + } else { + wxGetApp().plater()->show_autoslicing_action_buttons(); +#if ENABLE_RETINA_GL + const float scale = m_retina_helper->get_scale_factor(); +#else + const float scale = 0.1f * wxGetApp().em_unit(); +#endif // ENABLE_RETINA_GL + render_print_statistics(scale); + } + } - // draw overlays _render_overlays(); + _render_bed_selector(); + if (wxGetApp().plater()->is_render_statistic_dialog_visible()) { ImGuiPureWrap::begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); ImGuiPureWrap::text("FPS (SwapBuffers() calls per second):"); @@ -1971,7 +2396,8 @@ void GLCanvas3D::render() wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width()); - wxGetApp().plater()->render_sliders(*this); + if (! s_multiple_beds.is_autoslicing()) + wxGetApp().plater()->render_sliders(*this); wxGetApp().imgui()->render(); @@ -2164,7 +2590,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re m_reload_delayed = !m_canvas->IsShown() && !refresh_immediately && !force_full_scene_refresh; PrinterTechnology printer_technology = current_printer_technology(); - int volume_idx_wipe_tower_old = -1; + std::map volume_idxs_wipe_towers_old; // map from geometry_id.second to volume_id // Release invalidated volumes to conserve GPU memory in case of delayed refresh (see m_reload_delayed). // First initialize model_volumes_new_sorted & model_instances_new_sorted. @@ -2236,13 +2662,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re instance_ids_selected.emplace_back(volume->geometry_id.second); if (mvs == nullptr || force_full_scene_refresh) { // This GLVolume will be released. - if (volume->is_wipe_tower) { - // There is only one wipe tower. - assert(volume_idx_wipe_tower_old == -1); + if (volume->is_wipe_tower()) { #if SLIC3R_OPENGL_ES - m_wipe_tower_mesh.clear(); + m_wipe_tower_meshes.clear(); #endif // SLIC3R_OPENGL_ES - volume_idx_wipe_tower_old = (int)volume_id; + volume_idxs_wipe_towers_old.emplace(std::make_pair(volume->geometry_id.second, volume_id)); } if (!m_reload_delayed) { deleted_volumes.emplace_back(volume, volume_id); @@ -2404,38 +2828,58 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re const bool wt = dynamic_cast(m_config->option("wipe_tower"))->value; const bool co = dynamic_cast(m_config->option("complete_objects"))->value; + const float w = dynamic_cast(m_config->option("wipe_tower_width"))->value; + const float bw = dynamic_cast(m_config->option("wipe_tower_brim_width"))->value; + const float ca = dynamic_cast(m_config->option("wipe_tower_cone_angle"))->value; if (extruders_count > 1 && wt && !co) { + for (size_t bed_idx = 0; bed_idx < s_multiple_beds.get_max_beds(); ++bed_idx) { + const Print *print = wxGetApp().plater()->get_fff_prints()[bed_idx].get(); - const float x = dynamic_cast(m_config->option("wipe_tower_x"))->value; - const float y = dynamic_cast(m_config->option("wipe_tower_y"))->value; - const float w = dynamic_cast(m_config->option("wipe_tower_width"))->value; - const float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; - const float bw = dynamic_cast(m_config->option("wipe_tower_brim_width"))->value; - const float ca = dynamic_cast(m_config->option("wipe_tower_cone_angle"))->value; + const float x = m_model->get_wipe_tower_vector()[bed_idx].position.x(); + const float y = m_model->get_wipe_tower_vector()[bed_idx].position.y(); + const float a = m_model->get_wipe_tower_vector()[bed_idx].rotation; + const float depth = print->wipe_tower_data(extruders_count).depth; + const std::vector> z_and_depth_pairs = print->wipe_tower_data(extruders_count).z_and_depth_pairs; + const float height_real = print->wipe_tower_data(extruders_count).height; // -1.f = unknown + const bool is_wipe_tower_step_done = print->is_step_done(psWipeTower); - const Print *print = m_process->fff_print(); - const float depth = print->wipe_tower_data(extruders_count).depth; - const std::vector> z_and_depth_pairs = print->wipe_tower_data(extruders_count).z_and_depth_pairs; - const float height_real = print->wipe_tower_data(extruders_count).height; // -1.f = unknown - - // Height of a print (Show at least a slab). - const double height = height_real < 0.f ? std::max(m_model->max_z(), 10.0) : height_real; - - if (depth != 0.) { + // Height of a print (Show at least a slab). + const double height = height_real < 0.f ? std::max(m_model->max_z(), 10.0) : height_real; + if (depth != 0.) { #if SLIC3R_OPENGL_ES - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( - x, y, w, depth, z_and_depth_pairs, (float)height, ca, a, !print->is_step_done(psWipeTower), - bw, &m_wipe_tower_mesh); + if (bed_idx >= m_wipe_tower_meshes.size()) + m_wipe_tower_meshes.resize(bed_idx + 1); + GLVolume* volume = m_volumes.load_wipe_tower_preview( + x, y, w, depth, z_and_depth_pairs, (float)height, ca, a, !is_wipe_tower_step_done, + bw, bed_idx, &m_wipe_tower_meshes[bed_idx]); #else - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview( - x, y, w, depth, z_and_depth_pairs, (float)height, ca, a, !print->is_step_done(psWipeTower), - bw); + GLVolume* volume = m_volumes.load_wipe_tower_preview( + x, y, w, depth, z_and_depth_pairs, (float)height, ca, a, !is_wipe_tower_step_done, + bw, bed_idx); #endif // SLIC3R_OPENGL_ES - if (volume_idx_wipe_tower_old != -1) - map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; + const BoundingBoxf3& bb = volume->bounding_box(); + m_wipe_tower_bounding_boxes[bed_idx] = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)}; + if(static_cast(bed_idx) < s_multiple_beds.get_number_of_beds()) { + m_volumes.volumes.emplace_back(volume); + const auto volume_idx_wipe_tower_new{static_cast(m_volumes.volumes.size() - 1)}; + auto it = volume_idxs_wipe_towers_old.find(m_volumes.volumes.back()->geometry_id.second); + if (it != volume_idxs_wipe_towers_old.end()) + map_glvolume_old_to_new[it->second] = volume_idx_wipe_tower_new; + m_volumes.volumes.back()->set_volume_offset(m_volumes.volumes.back()->get_volume_offset() + s_multiple_beds.get_bed_translation(bed_idx)); + } else { + delete volume; + } + } else { + m_wipe_tower_bounding_boxes[bed_idx] = std::nullopt; + } } + s_multiple_beds.ensure_wipe_towers_on_beds(wxGetApp().plater()->model(), wxGetApp().plater()->get_fff_prints()); + } else { + m_wipe_tower_bounding_boxes.fill(std::nullopt); } + } else { + m_wipe_tower_bounding_boxes.fill(std::nullopt); } update_volumes_colors_by_extruder(); @@ -2475,7 +2919,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // checks for geometry outside the print volume to render it accordingly if (!m_volumes.empty()) { ModelInstanceEPrintVolumeState state; - const bool contained_min_one = check_volumes_outside_state(m_volumes, &state, !force_full_scene_refresh); + check_volumes_outside_state(m_volumes, &state, !force_full_scene_refresh); const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside); const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside); @@ -2498,21 +2942,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re _set_warning_notification(EWarning::SlaSupportsOutside, false); } } - - //Y5 if ToolpathOutside, unable export button - if (isToolpathOutside) { - post_event(Event(EVT_GLCANVAS_ENABLE_EXPORT_BUTTONS, false)); - } - else { - post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, - contained_min_one && !m_model->objects.empty() && !partlyOut)); - } } else { _set_warning_notification(EWarning::ObjectOutside, false); _set_warning_notification(EWarning::ObjectClashed, false); _set_warning_notification(EWarning::SlaSupportsOutside, false); - post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); } refresh_camera_scene_box(); @@ -2872,6 +3306,8 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) case 'b': { zoom_to_bed(); break; } case 'C': case 'c': { m_gcode_viewer.toggle_gcode_window_visibility(); m_dirty = true; request_extra_frame(); break; } + case 'D': + case 'd': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE_CURRENT_BED)); break; } case 'E': case 'e': { m_labels.show(!m_labels.is_shown()); m_dirty = true; break; } case 'G': @@ -3169,7 +3605,14 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) } } - if (keyCode != WXK_TAB + const GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type(); + if (keyCode == WXK_ALT && (gizmo_type == GLGizmosManager::FdmSupports || + gizmo_type == GLGizmosManager::Seam || + gizmo_type == GLGizmosManager::MmSegmentation || + gizmo_type == GLGizmosManager::FuzzySkin)) { + // Prevents focusing on the menu bar when ALT is pressed in painting gizmos (FdmSupports, Seam, MmSegmentation, and FuzzySkin). + evt.Skip(false); + } else if (keyCode != WXK_TAB && keyCode != WXK_LEFT && keyCode != WXK_UP && keyCode != WXK_RIGHT @@ -3265,10 +3708,11 @@ void GLCanvas3D::on_timer(wxTimerEvent& evt) void GLCanvas3D::on_render_timer(wxTimerEvent& evt) { + m_dirty = true; + // no need to wake up idle // right after this event, idle event is fired - // m_dirty = true; - // wxWakeUpIdle(); + //wxWakeUpIdle(); } @@ -3360,6 +3804,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) Point pos(evt.GetX(), evt.GetY()); + static wxTimer s_virtual_bed_timer; + s_virtual_bed_timer.Bind(wxEVT_TIMER, [this](wxTimerEvent&) { s_multiple_beds.request_next_bed(true); schedule_extra_frame(100); }); + ImGuiWrapper* imgui = wxGetApp().imgui(); if (m_tooltip.is_in_imgui() && evt.LeftUp()) // ignore left up events coming from imgui windows and not processed by them @@ -3375,7 +3822,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_dirty = true; // do not return if dragging or tooltip not empty to allow for tooltip update // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection - if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation || !evt.Moving())) + if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation || !evt.Moving())) return; } @@ -3456,7 +3903,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) c == GLGizmosManager::EType::Scale || c == GLGizmosManager::EType::Rotate) { show_sinking_contours(); - if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) + if (_is_sequential_print_enabled()) update_sequential_clearance(true); } } @@ -3556,7 +4003,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_gizmos.get_current_type() != GLGizmosManager::Seam && m_gizmos.get_current_type() != GLGizmosManager::Cut && m_gizmos.get_current_type() != GLGizmosManager::Measure && - m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) { + m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation && + m_gizmos.get_current_type() != GLGizmosManager::FuzzySkin) { m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); m_dirty = true; } @@ -3596,6 +4044,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); m_dirty = true; } + } else if (evt.LeftDown()) { + select_bed(s_multiple_beds.get_last_hovered_bed(), true); } } @@ -3613,7 +4063,9 @@ 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_first_displacement = true; + m_sequential_print_clearance.m_first_displacement = true; + if (_is_sequential_print_enabled()) + update_sequential_clearance(true); m_sequential_print_clearance.start_dragging(); } } @@ -3623,6 +4075,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else if (evt.Dragging() && evt.LeftIsDown() && !evt.CmdDown() && m_layers_editing.state == LayersEditing::Unknown && m_mouse.drag.move_volume_idx != -1 && m_mouse.is_start_position_3D_defined()) { if (!m_mouse.drag.move_requires_threshold) { + static bool was_dragging = false; + was_dragging = m_mouse.dragging; m_mouse.dragging = true; Vec3d cur_pos = m_mouse.drag.start_position_3D; // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag @@ -3661,10 +4115,17 @@ 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 (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) + if (_is_sequential_print_enabled()) update_sequential_clearance(false); wxGetApp().obj_manipul()->set_dirty(); m_dirty = true; + + const Selection::IndicesList& list = m_selection.get_volume_idxs(); + static bool was_outside = true; + bool is_outside = std::any_of(list.begin(), list.end(), [this](unsigned int i) { return m_volumes.volumes[i]->is_outside; }); + if (is_outside && (! was_dragging || ! was_outside)) + s_virtual_bed_timer.Start(1000, true); + was_outside = is_outside; } } else if (evt.Dragging() && evt.LeftIsDown() && m_picking_enabled && m_rectangle_selection.is_dragging()) { @@ -3707,13 +4168,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } } else if (evt.MiddleIsDown() || evt.RightIsDown()) { + Camera& camera = wxGetApp().plater()->get_camera(); // If dragging over blank area with right/middle button, pan. if (m_mouse.is_start_position_2D_defined()) { // get point in model space at Z = 0 - float z = 0.0f; - const Vec3d cur_pos = _mouse_to_3d(pos, &z); - const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z); - Camera& camera = wxGetApp().plater()->get_camera(); + const float z = 0.0f; + const Vec3d cur_pos = _mouse_to_3d(pos, &z, true); + const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z, true); if (!wxGetApp().app_config->get_bool("use_free_camera")) // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), @@ -3721,25 +4182,50 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) // See GH issue #3816. camera.recover_from_free_camera(); - camera.set_target(camera.get_target() + orig - cur_pos); + camera.set_target(m_mouse.drag.camera_start_target + orig - cur_pos); m_dirty = true; } - - m_mouse.drag.start_position_2D = pos; + else { + m_mouse.drag.start_position_2D = pos; + m_mouse.drag.camera_start_target = camera.get_target(); + } } } 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(); + if (std::abs(camera.get_dir_forward().dot(Vec3d::UnitZ())) > EPSILON) { + const Vec3d old_pos = camera.get_position(); + const double old_distance = camera.get_distance(); + const Vec3d old_target = camera.get_target(); + const Linef3 ray(old_pos, old_target); + const Vec3d new_target = ray.intersect_plane(0.0); + const BoundingBoxf3 validation_box = camera.get_target_validation_box(); + if (validation_box.contains(new_target)) { + const double new_distance = (new_target - old_pos).norm(); + camera.set_target(new_target); + camera.set_distance(new_distance); + if (camera.get_type() == Camera::EType::Perspective) + camera.set_zoom(camera.get_zoom() * old_distance / new_distance); + } + } + } + if (m_layers_editing.state != LayersEditing::Unknown) { m_layers_editing.state = LayersEditing::Unknown; _stop_timer(); m_layers_editing.accept_changes(*this); } else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) { + s_multiple_beds.request_next_bed(false); + s_virtual_bed_timer.Stop(); do_move(L("Move Object")); wxGetApp().obj_manipul()->set_dirty(); - m_sequential_print_clearance.stop_dragging(); // Let the plater know that the dragging finished, so a delayed refresh // of the scene with the background processing data should be performed. post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); @@ -3762,7 +4248,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_hover_volume_idxs.empty()) { // if right clicking on volume, propagate event through callback (shows context menu) int volume_idx = get_first_hover_volume_idx(); - if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower + if (!m_volumes.volumes[volume_idx]->is_wipe_tower() // no context menu for the wipe tower && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && m_gizmos.get_current_type() != GLGizmosManager::Measure)) // disable context menu when the gizmo is open { // forces the selection of the volume @@ -3787,7 +4273,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.dragging) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower - const bool post_right_click_event = (m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower) && + const bool post_right_click_event = (m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower()) && m_gizmos.get_current_type() != GLGizmosManager::Measure; if (post_right_click_event) post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); @@ -3917,11 +4403,13 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) std::set> done; // keeps track of modified instances bool object_moved = false; - Vec3d wipe_tower_origin = Vec3d::Zero(); + std::vector wipe_tower_origin = std::vector(s_multiple_beds.get_max_beds(), Vec3d::Zero()); Selection::EMode selection_mode = m_selection.get_mode(); + int vol_id = -1; for (const GLVolume* v : m_volumes.volumes) { + ++vol_id; int object_idx = v->object_idx(); int instance_idx = v->instance_idx(); int volume_idx = v->volume_idx(); @@ -3946,9 +4434,15 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) model_object->invalidate_bounding_box(); } } - else if (m_selection.is_wipe_tower() && v->is_wipe_tower) + else if (m_selection.is_wipe_tower() && v->is_wipe_tower() && m_selection.contains_volume(vol_id)) { // Move a wipe tower proxy. - wipe_tower_origin = v->get_volume_offset(); + for (size_t bed_idx = 0; bed_idx < s_multiple_beds.get_max_beds(); ++bed_idx) { + if (v->geometry_id.second == wipe_tower_instance_id(bed_idx).id) { + wipe_tower_origin[bed_idx] = v->get_volume_offset(); + break; + } + } + } } // Fixes flying instances @@ -3975,10 +4469,13 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) if (object_moved) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); - if (wipe_tower_origin != Vec3d::Zero()) - post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_MOVED, std::move(wipe_tower_origin))); + if (auto it = std::find_if(wipe_tower_origin.begin(), wipe_tower_origin.end(), [](const Vec3d& pos) { return pos != Vec3d::Zero(); }); it != wipe_tower_origin.end()) { + size_t bed_idx = it - wipe_tower_origin.begin(); + m_model->get_wipe_tower_vector()[bed_idx].position = Vec2d((*it)[0] - s_multiple_beds.get_bed_translation(bed_idx).x(), (*it)[1] - s_multiple_beds.get_bed_translation(bed_idx).y()); + post_event(SimpleEvent(EVT_GLCANVAS_WIPETOWER_TOUCHED)); + } - if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) { + if (_is_sequential_print_enabled()) { update_sequential_clearance(true); m_sequential_print_clearance.m_evaluating = true; } @@ -4014,13 +4511,23 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) std::set> done; // keeps track of modified instances Selection::EMode selection_mode = m_selection.get_mode(); + int v_id = -1; for (const GLVolume* v : m_volumes.volumes) { - if (v->is_wipe_tower) { - const Vec3d offset = v->get_volume_offset(); - Vec3d rot_unit_x = v->get_volume_transformation().get_matrix().linear() * Vec3d::UnitX(); - double z_rot = std::atan2(rot_unit_x.y(), rot_unit_x.x()); - post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset.x(), offset.y(), z_rot))); + ++v_id; + if (v->is_wipe_tower()) { + if (m_selection.contains_volume(v_id)) { + for (size_t bed_idx = 0; bed_idx < s_multiple_beds.get_max_beds(); ++bed_idx) { + if (v->geometry_id.second == wipe_tower_instance_id(bed_idx).id) { + const Vec3d offset = v->get_volume_offset() - s_multiple_beds.get_bed_translation(bed_idx); + Vec3d rot_unit_x = v->get_volume_transformation().get_matrix().linear() * Vec3d::UnitX(); + double z_rot = std::atan2(rot_unit_x.y(), rot_unit_x.x()); + m_model->get_wipe_tower_vector()[bed_idx].position = Vec2d(offset.x(), offset.y()); + m_model->get_wipe_tower_vector()[bed_idx].rotation = (180. / M_PI) * z_rot; + break; + } + } + } } const int object_idx = v->object_idx(); if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) @@ -4066,7 +4573,7 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); - if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) { + if (_is_sequential_print_enabled()) { update_sequential_clearance(true); m_sequential_print_clearance.m_evaluating = true; } @@ -4143,7 +4650,7 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); - if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) { + if (_is_sequential_print_enabled()) { update_sequential_clearance(true); m_sequential_print_clearance.m_evaluating = true; } @@ -4331,22 +4838,23 @@ void GLCanvas3D::update_ui_from_settings() wxGetApp().plater()->enable_collapse_toolbar(wxGetApp().app_config->get_bool("show_collapse_button") || !wxGetApp().sidebar().IsShown()); } -GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const +std::vector GLCanvas3D::get_wipe_tower_infos() const { - WipeTowerInfo wti; - - for (const GLVolume* vol : m_volumes.volumes) { - if (vol->is_wipe_tower) { - wti.m_pos = Vec2d(m_config->opt_float("wipe_tower_x"), - m_config->opt_float("wipe_tower_y")); - wti.m_rotation = (M_PI/180.) * m_config->opt_float("wipe_tower_rotation_angle"); - const BoundingBoxf3& bb = vol->bounding_box(); - wti.m_bb = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)}; - break; + std::vector result; + + for (size_t bed_idx = 0; bed_idx < s_multiple_beds.get_max_beds(); ++bed_idx) { + if (m_wipe_tower_bounding_boxes[bed_idx]) { + const ModelWipeTower &wipe_tower{m_model->wipe_tower(bed_idx)}; + WipeTowerInfo wti; + wti.m_pos = Vec2d(wipe_tower.position.x(), wipe_tower.position.y()); + wti.m_rotation = (M_PI/180.) * wipe_tower.rotation; + wti.m_bb = *m_wipe_tower_bounding_boxes[bed_idx]; + wti.m_bed_index = bed_idx; + result.push_back(std::move(wti)); } } - - return wti; + + return result; } Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos) @@ -4402,7 +4910,7 @@ void GLCanvas3D::mouse_up_cleanup() void GLCanvas3D::update_sequential_clearance(bool force_contours_generation) { - if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects) + if (!_is_sequential_print_enabled()) return; if (m_layers_editing.is_enabled()) @@ -4443,7 +4951,7 @@ void GLCanvas3D::update_sequential_clearance(bool force_contours_generation) // second: fill temporary cache with data from volumes for (const GLVolume* v : m_volumes.volumes) { - if (v->is_wipe_tower) + if (v->is_wipe_tower()) continue; const int object_idx = v->object_idx(); @@ -4464,7 +4972,7 @@ void GLCanvas3D::update_sequential_clearance(bool force_contours_generation) // 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_first_displacement) { + 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)); @@ -4513,7 +5021,7 @@ void GLCanvas3D::update_sequential_clearance(bool force_contours_generation) } set_sequential_print_clearance_contours(contours, false); - m_sequential_print_clearance_first_displacement = false; + m_sequential_print_clearance.m_first_displacement = false; } else { if (!m_sequential_print_clearance.empty()) { @@ -4620,9 +5128,9 @@ bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) return action_taken; } -bool GLCanvas3D::_render_arrange_menu(float pos_x) +bool GLCanvas3D::_render_arrange_menu(float pos_x, bool current_bed) { - m_arrange_settings_dialog.render(pos_x, m_main_toolbar.get_height()); + m_arrange_settings_dialog.render(pos_x, m_main_toolbar.get_height(), current_bed); return true; } @@ -4637,9 +5145,11 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const GLVolumePtrs visible_volumes; for (GLVolume* vol : volumes.volumes) { - if (!vol->is_modifier && !vol->is_wipe_tower && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) { - if (!thumbnail_params.printable_only || is_visible(*vol)) - visible_volumes.emplace_back(vol); + if (!vol->is_modifier && !vol->is_wipe_tower() && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) { + if (!thumbnail_params.printable_only || is_visible(*vol)) { + if (s_multiple_beds.is_glvolume_on_thumbnail_bed(wxGetApp().model(), vol->composite_id.object_id, vol->composite_id.instance_id)) + visible_volumes.emplace_back(vol); + } } } @@ -4669,7 +5179,13 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const // extends the near and far z of the frustrum to avoid the bed being clipped // box in eye space - const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(view_matrix); + BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box(); + if (s_multiple_beds.get_thumbnail_bed_idx() != -1) { + BoundingBoxf3 bed_bb = m_bed.build_volume().bounding_volume(); + bed_bb.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_thumbnail_bed_idx())); + t_bed_box.merge(bed_bb); + } + t_bed_box = t_bed_box.transformed(view_matrix); near_z = -t_bed_box.max.z(); far_z = -t_bed_box.min.z(); } @@ -4681,7 +5197,7 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const const bool is_enabled_painted_thumbnail = !model_objects.empty() && !extruders_colors.empty(); //Y18 //B54 if (thumbnail_params.transparent_background) - glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f)); + glsafe(::glClearColor(0.4f, 0.4f, 0.4f, 0.0f)); glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); @@ -5065,7 +5581,23 @@ bool GLCanvas3D::_init_main_toolbar() item.right.toggable = true; item.right.render_callback = [this](float left, float right, float, float) { if (m_canvas != nullptr) - _render_arrange_menu(0.5f * (left + right)); + _render_arrange_menu(0.5f * (left + right), false); + }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "arrangecurrent"; + item.icon_filename = "arrange_current.svg"; + item.tooltip = + _u8L("Arrange current bed") + " [D]\n" + + _u8L("Arrange selection on current bed") + " [Shift+D]\n"; + item.sprite_id = sprite_id++; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ARRANGE_CURRENT_BED)); }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); }; + item.right.toggable = true; + item.right.render_callback = [this](float left, float right, float, float) { + if (m_canvas != nullptr) + _render_arrange_menu(0.5f * (left + right), true); }; if (!m_main_toolbar.add_item(item)) return false; @@ -5344,24 +5876,38 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h) _set_current(); } -BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const +BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_bed_model) const { + const bool is_preview = wxGetApp().plater()->is_preview_shown(); + BoundingBoxf3 bb = volumes_bounding_box(); // The following is a workaround for gizmos not being taken in account when calculating the tight camera frustrum // A better solution would ask the gizmo manager for the bounding box of the current active gizmo, if any - if (include_gizmos && m_gizmos.is_running()) { + if (!is_preview && m_gizmos.is_running()) { const BoundingBoxf3 sel_bb = m_selection.get_bounding_box(); const Vec3d sel_bb_center = sel_bb.center(); const Vec3d extend_by = sel_bb.max_size() * Vec3d::Ones(); bb.merge(BoundingBoxf3(sel_bb_center - extend_by, sel_bb_center + extend_by)); } - const BoundingBoxf3 bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume(); - bb.merge(bed_bb); + const BoundingBoxf3 first_bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume(); + BoundingBoxf3 bed_bb; - if (!m_main_toolbar.is_enabled()) - bb.merge(m_gcode_viewer.get_max_bounding_box()); + for (int i = 0; i < s_multiple_beds.get_number_of_beds() + int(s_multiple_beds.should_show_next_bed()); ++i) { + if (!is_preview || i == s_multiple_beds.get_active_bed()) { + BoundingBoxf3 this_bed = first_bed_bb; + this_bed.translate(s_multiple_beds.get_bed_translation(i)); + bed_bb.merge(this_bed); + } + } + bb.merge(bed_bb); + + if (is_preview) { + BoundingBoxf3 paths_bb = m_gcode_viewer.get_max_bounding_box(); + paths_bb.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + bb.merge(paths_bb); + } // clamp max bb size with respect to bed bb size if (!m_picking_enabled) { @@ -5763,16 +6309,27 @@ void GLCanvas3D::_render_background() use_error_color = m_dynamic_background_enabled && (current_printer_technology() != ptSLA || !m_volumes.empty()); - if (!m_volumes.empty()) - use_error_color &= _is_any_volume_outside().first; - else - use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); + if (s_multiple_beds.is_autoslicing()) { + use_error_color &= std::any_of( + s_print_statuses.begin(), + s_print_statuses.end(), + [](const PrintStatus status){ + return status == PrintStatus::toolpath_outside; + } + ); + } else { + if (!m_volumes.empty()) + use_error_color &= _is_any_volume_outside().first; + else + use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed(); + } } // Draws a bottom to top gradient over the complete screen. glsafe(::glDisable(GL_DEPTH_TEST)); //B12 bool is_dark_mode = GUI_App::dark_mode(); + const ColorRGBA top_color = use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR; const ColorRGBA bottom_color = use_error_color ? ERROR_BG_DARK_COLOR : is_dark_mode ? DARKMODE_BG_DARK_COLOR : DEFAULT_BG_DARK_COLOR; if (!m_background.is_initialized()) { @@ -5807,7 +6364,7 @@ void GLCanvas3D::_render_background() } else { - shader->set_uniform("top_color", use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR); + shader->set_uniform("top_color", top_color); shader->set_uniform("bottom_color", bottom_color); } m_background.render(); @@ -5829,7 +6386,8 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && m_gizmos.get_current_type() != GLGizmosManager::Hollow && m_gizmos.get_current_type() != GLGizmosManager::Seam - && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation); + && m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation + && m_gizmos.get_current_type() != GLGizmosManager::FuzzySkin); m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_texture); } @@ -5863,18 +6421,22 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) m_layers_editing.select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1); if (const BuildVolume &build_volume = m_bed.build_volume(); build_volume.valid()) { - switch (build_volume.type()) { + const Vec3d bed_offset = s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()); + switch (build_volume.type()) { case BuildVolume::Type::Rectangle: { const BoundingBox3Base bed_bb = build_volume.bounding_volume().inflated(BuildVolume::SceneEpsilon); - m_volumes.set_print_volume({ 0, // circle - { float(bed_bb.min.x()), float(bed_bb.min.y()), float(bed_bb.max.x()), float(bed_bb.max.y()) }, - { 0.0f, float(build_volume.max_print_height()) } }); + m_volumes.set_print_volume({ 0, // rectangle + { float(bed_bb.min.x() + bed_offset.x()), float(bed_bb.min.y() + bed_offset.y()), + float(bed_bb.max.x() + bed_offset.x()), float(bed_bb.max.y() + bed_offset.y()) }, + { float(0.0 + bed_offset.z()), float(build_volume.max_print_height() + bed_offset.z()) } }); break; } case BuildVolume::Type::Circle: { - m_volumes.set_print_volume({ 1, // rectangle - { unscaled(build_volume.circle().center.x()), unscaled(build_volume.circle().center.y()), unscaled(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f }, - { 0.0f, float(build_volume.max_print_height() + BuildVolume::SceneEpsilon) } }); + m_volumes.set_print_volume({ 1, // circle + { unscaled(build_volume.circle().center.x() + bed_offset.x()), + unscaled(build_volume.circle().center.y() + bed_offset.y()), + unscaled(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f }, + { float(0.0 + bed_offset.z()), float(build_volume.max_print_height() + bed_offset.z() + BuildVolume::SceneEpsilon) } }); break; } default: @@ -5901,6 +6463,12 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) m_volumes.set_show_sinking_contours(! m_gizmos.is_hiding_instances()); m_volumes.set_show_non_manifold_edges(!m_gizmos.is_hiding_instances() && m_gizmos.get_current_type() != GLGizmosManager::Simplify); + const Camera& camera = wxGetApp().plater()->get_camera(); + auto trafo = camera.get_view_matrix(); + if (current_printer_technology() == ptSLA && wxGetApp().plater()->is_preview_shown()) { + trafo.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + } + GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); if (shader != nullptr) { shader->start_using(); @@ -5912,8 +6480,8 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) { if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) { int object_id = m_layers_editing.last_object_id; - const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix(), [object_id](const GLVolume& volume) { + + m_volumes.render(type, false, trafo, camera.get_projection_matrix(), [object_id](const GLVolume& volume) { // Which volume to paint without the layer height profile shader? return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); }); @@ -5923,7 +6491,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) else { // do not cull backfaces to show broken geometry, if any const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, m_picking_enabled, camera.get_view_matrix(), camera.get_projection_matrix(), [this](const GLVolume& volume) { + m_volumes.render(type, m_picking_enabled, trafo, camera.get_projection_matrix(), [this](const GLVolume& volume) { return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); }); } @@ -5945,7 +6513,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type) case GLVolumeCollection::ERenderType::Transparent: { const Camera& camera = wxGetApp().plater()->get_camera(); - m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix()); + m_volumes.render(type, false, trafo, camera.get_projection_matrix()); break; } } @@ -5972,7 +6540,7 @@ void GLCanvas3D::_render_selection() void GLCanvas3D::_render_sequential_clearance() { - if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects) + if (!_is_sequential_print_enabled()) return; if (m_layers_editing.is_enabled()) @@ -5982,12 +6550,13 @@ void GLCanvas3D::_render_sequential_clearance() { case GLGizmosManager::EType::Flatten: case GLGizmosManager::EType::Cut: - case GLGizmosManager::EType::MmuSegmentation: + case GLGizmosManager::EType::MmSegmentation: case GLGizmosManager::EType::Measure: case GLGizmosManager::EType::Emboss: case GLGizmosManager::EType::Simplify: case GLGizmosManager::EType::FdmSupports: - case GLGizmosManager::EType::Seam: { return; } + case GLGizmosManager::EType::Seam: + case GLGizmosManager::EType::FuzzySkin: { return; } default: { break; } } @@ -6094,10 +6663,8 @@ void GLCanvas3D::_render_overlays() if (m_layers_editing.last_object_id >= 0 && m_layers_editing.object_max_z() > 0.0f) m_layers_editing.render_overlay(*this); - const ConfigOptionBool* opt = dynamic_cast(m_config->option("complete_objects")); - bool sequential_print = opt != nullptr && opt->value; std::vector sorted_instances; - if (sequential_print) { + 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); @@ -6106,6 +6673,339 @@ void GLCanvas3D::_render_overlays() m_labels.render(sorted_instances); } +#define use_scrolling 1 + +std::string get_status_text(PrintStatus status) { + switch(status) { + case PrintStatus::idle: return _u8L("Unsliced bed"); + case PrintStatus::running: return _u8L("Slicing") + "..."; + case PrintStatus::finished: return _u8L("Sliced bed"); + case PrintStatus::outside: return _u8L("Object at boundary"); + case PrintStatus::invalid: return _u8L("Invalid data"); + case PrintStatus::empty: return _u8L("Empty bed"); + case PrintStatus::toolpath_outside: return _u8L("Toolpath exceeds bounds"); + } + return {}; +} + +wchar_t get_raw_status_icon(const PrintStatus status) { + switch(status) { + case PrintStatus::finished: return ImGui::PrintFinished; + case PrintStatus::running: return ImGui::PrintRunning; + case PrintStatus::idle: return ImGui::PrintIdle; + case PrintStatus::outside: return ImGui::PrintIdle; + case PrintStatus::invalid: return ImGui::PrintIdle; + case PrintStatus::empty: return ImGui::PrintIdle; + case PrintStatus::toolpath_outside: return ImGui::PrintIdle; + } + return ImGui::PrintIdle; +} + +std::string get_status_icon(const PrintStatus status) { + return boost::nowide::narrow(std::wstring{get_raw_status_icon(status)}); +} + +bool bed_selector_thumbnail( + const ImVec2 size, + const ImVec2 padding, + const float side, + const float border, + const float scale, + const int bed_id, + const std::optional status +) { + ImGuiWindow* window = GImGui->CurrentWindow; + const ImVec2 current_position = GImGui->CurrentWindow->DC.CursorPos; + const ImVec2 state_pos = current_position + ImVec2(3.f * border, side - 20.f * scale); + + const GLuint texture_id = s_bed_selector_thumbnail_texture_ids[bed_id]; + const bool clicked{ImGui::ImageButton( + (void*)(int64_t)texture_id, + size - padding, + ImVec2(0, 1), + ImVec2(1, 0), + border + )}; + + if (status) { + const std::string icon{get_status_icon(*status)}; + + window->DrawList->AddText( + GImGui->Font, + GImGui->FontSize, + state_pos, + ImGui::GetColorU32(ImGuiCol_Text), + icon.c_str(), + icon.c_str() + icon.size() + ); + } + + const ImVec2 id_pos = current_position + ImVec2(3.f * border, 1.5f * border); + const std::string id = std::to_string(bed_id+1); + + window->DrawList->AddText( + GImGui->Font, + GImGui->FontSize * 1.5f, + id_pos, + ImGui::GetColorU32(ImGuiCol_Text), + id.c_str(), + id.c_str() + id.size() + ); + + return clicked; +} + +bool button_with_icon(const wchar_t icon, const std::string& tooltip, bool is_active, const ImVec2 size) +{ + std::string btn_name = boost::nowide::narrow(std::wstring{ icon }); + + ImGuiButtonFlags flags = ImGuiButtonFlags_None; + + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(btn_name.c_str()); + const ImFontAtlasCustomRect* const rect = wxGetApp().imgui()->GetTextureCustomRect(icon); + const ImVec2 label_size = ImVec2(rect->Width, rect->Height); + + ImVec2 pos = window->DC.CursorPos; + const ImRect bb(pos, pos + size); + ImGui::ItemSize(size, style.FramePadding.y); + if (!ImGui::ItemAdd(bb, id)) + return false; + + if (g.CurrentItemFlags & ImGuiItemFlags_ButtonRepeat) + flags |= ImGuiButtonFlags_Repeat; + + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = ImGui::GetColorU32((held && hovered) ? ImGuiPureWrap::COL_BLUE_DARK : hovered ? ImGuiPureWrap::COL_GREY_LIGHT : ImGuiPureWrap::COL_GREY_DARK); + ImGui::RenderNavHighlight(bb, id); + ImGui::PushStyleColor(ImGuiCol_Border, is_active ? ImGuiPureWrap::COL_BUTTON_ACTIVE : ImGuiPureWrap::COL_GREY_DARK); + ImGui::RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + ImGui::PopStyleColor(); + + if (g.LogEnabled) + ImGui::LogSetNextTextDecoration("[", "]"); + ImGui::RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, btn_name.c_str(), NULL, &label_size, style.ButtonTextAlign, &bb); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tooltip.c_str()); + + return pressed; +} + +void Slic3r::GUI::GLCanvas3D::_render_bed_selector() +{ + bool extra_frame{ false }; + static std::array, MAX_NUMBER_OF_BEDS> previous_print_status; + + if (s_multiple_beds.get_number_of_beds() != 1 && wxGetApp().plater()->is_preview_shown()) { +#if ENABLE_RETINA_GL + float scale = m_retina_helper->get_scale_factor(); +#else + float scale = 0.1f * wxGetApp().em_unit(); +#endif // ENABLE_RETINA_GL + + const float btn_side = 80.f * scale; + const float btn_border = 2.f * scale; + + const ImVec2 btn_size = ImVec2(btn_side, btn_side); + const ImVec2 btn_padding = ImVec2(btn_border, btn_border); + + auto render_bed_button = [btn_side, btn_border, btn_size, btn_padding, this, &extra_frame, scale](int i) + { + bool inactive = i != s_multiple_beds.get_active_bed() || s_multiple_beds.is_autoslicing(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImGuiPureWrap::COL_GREY_DARK); + ImGui::PushStyleColor(ImGuiCol_Border, inactive ? ImGuiPureWrap::COL_GREY_DARK : ImGuiPureWrap::COL_BUTTON_ACTIVE); + + const PrintStatus print_status{s_print_statuses[i]}; + + if (current_printer_technology() == ptFFF) { + if ( !previous_print_status[i] + || print_status != previous_print_status[i] + ) { + extra_frame = true; + } + previous_print_status[i] = print_status; + } + + if (s_bed_selector_thumbnail_changed[i]) { + extra_frame = true; + s_bed_selector_thumbnail_changed[i] = false; + } + + if ( + !is_sliceable(print_status) + ) { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + } + + bool clicked = false; + if ( + !is_sliceable(print_status) + ) { + clicked = button_with_icon( + ImGui::WarningMarkerDisabled, + get_status_text(print_status), + !inactive, + btn_size + btn_padding + ); + } else if (print_status == PrintStatus::toolpath_outside) { + clicked = button_with_icon( + ImGui::WarningMarker, + get_status_text(print_status), + !inactive, + btn_size + btn_padding + ); + } else if ( + i >= int(s_bed_selector_thumbnail_texture_ids.size()) + ) { + clicked = ImGui::Button( + std::to_string(i + 1).c_str(), btn_size + btn_padding + ); + } else { + clicked = bed_selector_thumbnail( + btn_size, + btn_padding, + btn_side, + btn_border, + scale, + i, + current_printer_technology() == ptFFF ? std::optional{print_status} : std::nullopt + ); + } + + if (clicked && is_sliceable(print_status)) + select_bed(i, true); + + ImGui::PopStyleColor(2); + if ( + !is_sliceable(print_status) + ) { + ImGui::PopItemFlag(); + } + + if (current_printer_technology() == ptFFF) { + const std::string status_text{get_status_text(print_status)}; + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", status_text.c_str()); + } + } + }; + + float win_x_pos = get_canvas_size().get_width(); + + float right_shift = 0.f; + if (const Preview* preview = dynamic_cast(m_canvas->GetParent())) + right_shift = preview->get_layers_slider_width(true); + if (right_shift == 0.f) { + GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar(); +#if ENABLE_HACK_GCODEVIEWER_SLOW_ON_MAC + // When the application is run as GCodeViewer the collapse toolbar is enabled but invisible, as it is renderer + // outside of the screen + const bool is_collapse_btn_shown = wxGetApp().is_editor() ? collapse_toolbar.is_enabled() : false; +#else + const bool is_collapse_btn_shown = collapse_toolbar.is_enabled(); +#endif // ENABLE_HACK_GCODEVIEWER_SLOW_ON_MAC + if (is_collapse_btn_shown) + right_shift = collapse_toolbar.get_width(); + } + win_x_pos -= right_shift; + +#if use_scrolling + static float width { 0.f }; + static float height { 0.f }; + static float v_pos { 1.f }; + + ImGui::SetNextWindowPos({ win_x_pos - scale * 5.f, v_pos }, ImGuiCond_Always, { 1.f, 0.f }); + ImGui::SetNextWindowSize({ width, height }); +#else + ImGuiPureWrap::set_next_window_pos(win_x_pos - scale * 5.f, 1.f, ImGuiCond_Always, 1.f); +#endif + ImGui::Begin("Bed selector", 0, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, btn_border); + + if ( + current_printer_technology() == ptFFF && + button_with_icon(ImGui::SliceAllBtnIcon, _u8L("Slice all"), s_multiple_beds.is_autoslicing(), btn_size + btn_padding) + ) { + if (!s_multiple_beds.is_autoslicing()) { + s_multiple_beds.start_autoslice([this](int i, bool user) { this->select_bed(i, user); }); + wxGetApp().sidebar().switch_to_autoslicing_mode(); + wxGetApp().plater()->show_autoslicing_action_buttons(); + } + } + + ImGui::SameLine(); + + int beds_num = s_multiple_beds.get_number_of_beds(); + + for (int i = 0; i < beds_num; ++i) { + render_bed_button(i); + if (i < beds_num - 1) + ImGui::SameLine(); + } + + ImGui::PopStyleVar(3); + +#if use_scrolling + bool is_legend_visible = is_legend_shown() && !s_multiple_beds.is_autoslicing(); + ImVec2 win_size = ImGui::GetCurrentWindow()->ContentSizeIdeal + + ImGui::GetCurrentWindow()->WindowPadding * 2.f + + ImGui::GetCurrentWindow()->ScrollbarSizes + + ImVec2(0.f, is_legend_visible ? ImGui::GetCurrentWindow()->TitleBarHeight() : 0.f); + + if (!is_approx(height, win_size.y)) { + height = win_size.y; + wxGetApp().imgui()->set_requires_extra_frame(); + } + m_bed_selector_current_height = height; + + float max_width = win_x_pos; + if (is_legend_visible) + max_width -= 400.f * scale; // 400.f is used instead of legend width + + if (max_width < height) { + width = win_x_pos - 5.f * scale; + + v_pos = ImGui::GetCurrentWindow()->CalcFontSize() + GImGui->Style.FramePadding.y * 2.f + 5.f; + extra_frame = true; + } + else { + if (v_pos > 1.f) { + v_pos = 1.f; + extra_frame = true; + } + + if (win_size.x > max_width) { + width = max_width; + extra_frame = true; + } + else if (!is_approx(width, win_size.x)) { + width = win_size.x; + extra_frame = true; + } + } + + if (extra_frame) + wxGetApp().imgui()->set_requires_extra_frame(); +#endif + ImGui::End(); + } +} + void GLCanvas3D::_render_volumes_for_picking(const Camera& camera) const { GLShaderProgram* shader = wxGetApp().get_shader("flat_clip"); @@ -6216,7 +7116,7 @@ void GLCanvas3D::_render_view_toolbar() const #if ENABLE_SHOW_CAMERA_TARGET void GLCanvas3D::_render_camera_target() { - static const float half_length = 5.0f; + static const float half_length = 10.0f; glsafe(::glDisable(GL_DEPTH_TEST)); #if !SLIC3R_OPENGL_ES @@ -6224,8 +7124,7 @@ void GLCanvas3D::_render_camera_target() glsafe(::glLineWidth(2.0f)); #endif // !SLIC3R_OPENGL_ES - const Vec3f& target = wxGetApp().plater()->get_camera().get_target().cast(); - m_camera_target.target = target.cast(); + m_camera_target.target = wxGetApp().plater()->get_camera().get_target(); for (int i = 0; i < 3; ++i) { if (!m_camera_target.axis[i].is_initialized()) { @@ -6233,7 +7132,7 @@ void GLCanvas3D::_render_camera_target() GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = (i == X) ? ColorRGBA::X() : ((i == Y) ? ColorRGBA::Y() : ColorRGBA::Z()); + init_data.color = (i == X) ? ColorRGBA::X() : (i == Y) ? ColorRGBA::Y() : ColorRGBA::Z(); init_data.reserve_vertices(2); init_data.reserve_indices(2); @@ -6284,11 +7183,79 @@ void GLCanvas3D::_render_camera_target() shader->stop_using(); } } + +void GLCanvas3D::_render_camera_target_validation_box() +{ + const BoundingBoxf3& curr_box = m_target_validation_box.get_bounding_box(); + const BoundingBoxf3 camera_box = wxGetApp().plater()->get_camera().get_target_validation_box(); + + if (!m_target_validation_box.is_initialized() || !is_approx(camera_box.min, curr_box.min) || !is_approx(camera_box.max, curr_box.max)) { + m_target_validation_box.reset(); + + const Vec3f b_min = camera_box.min.cast(); + const Vec3f b_max = camera_box.max.cast(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(12); + init_data.reserve_indices(12); + + // vertices + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + + // indices + for (unsigned int i = 0; i < 12; ++i) { + init_data.add_index(i); + } + + m_target_validation_box.init_from(std::move(init_data)); + } + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if SLIC3R_OPENGL_ES + GLShaderProgram* shader = wxGetApp().get_shader("dashed_lines"); +#else + if (!OpenGLManager::get_gl_info().is_core_profile()) + glsafe(::glLineWidth(2.0f)); + + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#endif // SLIC3R_OPENGL_ES + 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()); +#if !SLIC3R_OPENGL_ES + if (OpenGLManager::get_gl_info().is_core_profile()) { +#endif // !SLIC3R_OPENGL_ES + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 1.5f); + shader->set_uniform("gap_size", 0.0f); +#if !SLIC3R_OPENGL_ES + } +#endif // !SLIC3R_OPENGL_ES + m_target_validation_box.set_color(to_rgba(ColorRGB::WHITE())); + m_target_validation_box.render(); + shader->stop_using(); +} #endif // ENABLE_SHOW_CAMERA_TARGET - - -static void render_sla_layer_legend(const SLAPrint& print, int layer_idx, int cnv_width) +static void render_sla_layer_legend(const SLAPrint& print, int layer_idx, int cnv_width, float bed_sel_height) { const std::vector& areas = print.print_statistics().layers_areas; const std::vector& times = print.print_statistics().layers_times_running_total; @@ -6299,7 +7266,7 @@ static void render_sla_layer_legend(const SLAPrint& print, int layer_idx, int cn const double time_until_layer = times[layer_idx]; ImGuiWrapper& imgui = *wxGetApp().imgui(); - ImGuiPureWrap::set_next_window_pos(float(cnv_width) - imgui.get_style_scaling() * 5.f, 5.f, ImGuiCond_Always, 1.0f, 0.0f); + ImGuiPureWrap::set_next_window_pos(float(cnv_width) - imgui.get_style_scaling() * 5.f, 5.f + bed_sel_height, ImGuiCond_Always, 1.0f, 0.0f); ImGui::SetNextWindowBgAlpha(0.6f); ImGuiPureWrap::begin(_u8L("Layer statistics"), ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoFocusOnAppearing); @@ -6335,7 +7302,7 @@ void GLCanvas3D::_render_sla_slices() double slider_width = 0.; if (const Preview* preview = dynamic_cast(m_canvas->GetParent())) slider_width = preview->get_layers_slider_width(); - render_sla_layer_legend(*print, m_layer_slider_index, get_canvas_size().get_width() - slider_width); + render_sla_layer_legend(*print, m_layer_slider_index, get_canvas_size().get_width() - slider_width, m_bed_selector_current_height); } double clip_min_z = -m_clipping_planes[0].get_data()[3]; @@ -6436,6 +7403,7 @@ void GLCanvas3D::_render_sla_slices() for (const SLAPrintObject::Instance& inst : obj->instances()) { const Camera& camera = wxGetApp().plater()->get_camera(); Transform3d view_model_matrix = camera.get_view_matrix() * + Geometry::translation_transform(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())) * Geometry::translation_transform({ unscale(inst.shift.x()), unscale(inst.shift.y()), 0.0 }) * Geometry::rotation_transform(inst.rotation * Vec3d::UnitZ()); if (obj->is_left_handed()) @@ -6558,7 +7526,7 @@ void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt) _start_timer(); } -Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) +Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, const float* z, bool use_ortho) { if (m_canvas == nullptr) return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); @@ -6568,10 +7536,31 @@ Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) return hit.is_valid() ? hit.position.cast() : _mouse_to_bed_3d(mouse_pos); } else { - const Camera& camera = wxGetApp().plater()->get_camera(); + Camera& camera = wxGetApp().plater()->get_camera(); + const Camera::EType type = camera.get_type(); const Vec4i viewport(camera.get_viewport().data()); + Transform3d projection_matrix; + if (use_ortho && type != Camera::EType::Ortho) { + const double inv_zoom = camera.get_inv_zoom(); + const double left = -0.5 * inv_zoom * double(viewport[2]); + const double bottom = -0.5 * inv_zoom * double(viewport[3]); + const double right = 0.5 * inv_zoom * double(viewport[2]); + const double top = 0.5 * inv_zoom * double(viewport[3]); + const double near_z = camera.get_near_z(); + const double far_z = camera.get_far_z(); + const double inv_dx = 1.0 / (right - left); + const double inv_dy = 1.0 / (top - bottom); + const double inv_dz = 1.0 / (far_z - near_z); + projection_matrix.matrix() << 2.0 * near_z * inv_dx, 0.0, (left + right) * inv_dx, 0.0, + 0.0, 2.0 * near_z * inv_dy, (bottom + top) * inv_dy, 0.0, + 0.0, 0.0, -(near_z + far_z) * inv_dz, -2.0 * near_z * far_z * inv_dz, + 0.0, 0.0, -1.0, 0.0; + } + else + projection_matrix = camera.get_projection_matrix(); + Vec3d out; - igl::unproject(Vec3d(mouse_pos.x(), viewport[3] - mouse_pos.y(), *z), camera.get_view_matrix().matrix(), camera.get_projection_matrix().matrix(), viewport, out); + igl::unproject(Vec3d(mouse_pos.x(), viewport[3] - mouse_pos.y(), *z), camera.get_view_matrix().matrix(), projection_matrix.matrix(), viewport, out); return out; } } @@ -6776,6 +7765,11 @@ std::pair GLCanvas3D::_is_any_volume_outside() const return std::make_pair(false, nullptr); } +bool GLCanvas3D::_is_sequential_print_enabled() const +{ + return current_printer_technology() == ptFFF && fff_print()->config().complete_objects; +} + void GLCanvas3D::_update_selection_from_hover() { bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); @@ -6883,6 +7877,11 @@ bool GLCanvas3D::_deactivate_arrange_menu() return true; } + if (m_main_toolbar.is_item_pressed("arrangecurrent")) { + m_main_toolbar.force_right_action(m_main_toolbar.get_item_id("arrangecurrent"), *this); + return true; + } + return false; } @@ -6925,13 +7924,10 @@ const SLAPrint* GLCanvas3D::sla_print() const return (m_process == nullptr) ? nullptr : m_process->sla_print(); } -void GLCanvas3D::WipeTowerInfo::apply_wipe_tower(Vec2d pos, double rot) +void GLCanvas3D::WipeTowerInfo::apply_wipe_tower(Vec2d pos, double rot, int bed_index) { - DynamicPrintConfig cfg; - cfg.opt("wipe_tower_x", true)->value = pos.x(); - cfg.opt("wipe_tower_y", true)->value = pos.y(); - cfg.opt("wipe_tower_rotation_angle", true)->value = (180./M_PI) * rot; - wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg); + wxGetApp().plater()->model().wipe_tower(bed_index).position = pos; + wxGetApp().plater()->model().wipe_tower(bed_index).rotation = (180. / M_PI) * rot; } void GLCanvas3D::RenderTimer::Notify() diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 2aee27b..34b3dd7 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -19,7 +19,7 @@ #include "SceneRaycaster.hpp" #include "GUI_Utils.hpp" -#include "libslic3r/Arrange/ArrangeSettingsDb_AppCfg.hpp" +#include #include "ArrangeSettingsDialogImgui.hpp" #include "libslic3r/Slicing.hpp" @@ -153,17 +153,17 @@ wxDECLARE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); +wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE_CURRENT_BED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); // data: +1 => increase, -1 => decrease wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); -wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); +wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_TOUCHED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MIRRORED, SimpleEvent); -wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); //Y5 wxDECLARE_EVENT(EVT_GLCANVAS_ENABLE_EXPORT_BUTTONS, Event); @@ -320,6 +320,7 @@ class GLCanvas3D Point start_position_2D{ Invalid_2D_Point }; Vec3d start_position_3D{ Invalid_3D_Point }; + Vec3d camera_start_target{ Invalid_3D_Point }; int move_volume_idx{ -1 }; bool move_requires_threshold{ false }; Point move_start_threshold_position_2D{ Invalid_2D_Point }; @@ -333,10 +334,13 @@ class GLCanvas3D void set_start_position_2D_as_invalid() { drag.start_position_2D = Drag::Invalid_2D_Point; } void set_start_position_3D_as_invalid() { drag.start_position_3D = Drag::Invalid_3D_Point; } + void set_camera_start_target_as_invalid() { drag.camera_start_target = Drag::Invalid_3D_Point; } void set_move_start_threshold_position_2D_as_invalid() { drag.move_start_threshold_position_2D = Drag::Invalid_2D_Point; } - bool is_start_position_2D_defined() const { return (drag.start_position_2D != Drag::Invalid_2D_Point); } - bool is_start_position_3D_defined() const { return (drag.start_position_3D != Drag::Invalid_3D_Point); } + bool is_start_position_2D_defined() const { return drag.start_position_2D != Drag::Invalid_2D_Point; } + bool is_start_position_3D_defined() const { return drag.start_position_3D != Drag::Invalid_3D_Point; } + bool is_camera_start_target_defined() { return drag.camera_start_target != Drag::Invalid_3D_Point; } + bool is_move_start_threshold_position_2D_defined() const { return (drag.move_start_threshold_position_2D != Drag::Invalid_2D_Point); } bool is_move_threshold_met(const Point& mouse_pos) const { return (std::abs(mouse_pos(0) - drag.move_start_threshold_position_2D(0)) > Drag::MoveThresholdPx) @@ -485,6 +489,7 @@ private: wxGLContext* m_context; SceneRaycaster m_scene_raycaster; Bed3D &m_bed; + int m_last_active_bed_id{ -1 }; #if ENABLE_RETINA_GL std::unique_ptr m_retina_helper; #endif @@ -505,11 +510,14 @@ private: // see request_extra_frame() bool m_extra_frame_requested; bool m_event_handlers_bound{ false }; + float m_bed_selector_current_height = 0.f; GLVolumeCollection m_volumes; #if SLIC3R_OPENGL_ES - TriangleMesh m_wipe_tower_mesh; + std::vector m_wipe_tower_meshes; #endif // SLIC3R_OPENGL_ES + std::array, MAX_NUMBER_OF_BEDS> m_wipe_tower_bounding_boxes; + GCodeViewer m_gcode_viewer; RenderTimer m_render_timer; @@ -517,9 +525,13 @@ private: Selection m_selection; const DynamicPrintConfig* m_config; Model* m_model; +public: BackgroundSlicingProcess *m_process; +private: bool m_requires_check_outside_state{ false }; + void select_bed(int i, bool triggered_by_user); + std::array m_old_size{ 0, 0 }; // Screen is only refreshed from the OnIdle handler if it is dirty. @@ -619,6 +631,7 @@ private: std::vector> m_instances; bool m_evaluating{ false }; bool m_dragging{ false }; + bool m_first_displacement{ true }; std::vector> m_hulls_2d_cache; @@ -636,7 +649,6 @@ private: }; SequentialPrintClearance m_sequential_print_clearance; - bool m_sequential_print_clearance_first_displacement{ true }; struct ToolbarHighlighter { @@ -678,6 +690,7 @@ private: }; CameraTarget m_camera_target; + GLModel m_target_validation_box; #endif // ENABLE_SHOW_CAMERA_TARGET GLModel m_background; @@ -900,30 +913,30 @@ public: int get_move_volume_id() const { return m_mouse.drag.move_volume_idx; } int get_first_hover_volume_idx() const { return m_hover_volume_idxs.empty() ? -1 : m_hover_volume_idxs.front(); } void set_selected_extruder(int extruder) { m_selected_extruder = extruder;} - + class WipeTowerInfo { protected: Vec2d m_pos = {NaNd, NaNd}; double m_rotation = 0.; BoundingBoxf m_bb; + int m_bed_index{0}; friend class GLCanvas3D; - public: + public: inline operator bool() const { return !std::isnan(m_pos.x()) && !std::isnan(m_pos.y()); } - + inline const Vec2d& pos() const { return m_pos; } inline double rotation() const { return m_rotation; } inline const Vec2d bb_size() const { return m_bb.size(); } inline const BoundingBoxf& bounding_box() const { return m_bb; } - - void apply_wipe_tower() const { apply_wipe_tower(m_pos, m_rotation); } + inline const int bed_index() const { return m_bed_index; } - static void apply_wipe_tower(Vec2d pos, double rot); + static void apply_wipe_tower(Vec2d pos, double rot, int bed_index); }; - - WipeTowerInfo get_wipe_tower_info() const; + + std::vector get_wipe_tower_infos() const; // Returns the view ray line, in world coordinate, at the given mouse position. Linef3 mouse_ray(const Point& mouse_pos); @@ -977,7 +990,7 @@ public: void reset_sequential_print_clearance() { m_sequential_print_clearance.m_evaluating = false; if (m_sequential_print_clearance.is_dragging()) - m_sequential_print_clearance_first_displacement = true; + m_sequential_print_clearance.m_first_displacement = true; else m_sequential_print_clearance.set_contours(ContoursList(), false); set_as_dirty(); @@ -986,6 +999,8 @@ public: 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(); } @@ -1033,7 +1048,7 @@ private: bool _set_current(); void _resize(unsigned int w, unsigned int h); - BoundingBoxf3 _max_bounding_box(bool include_gizmos, bool include_bed_model) const; + BoundingBoxf3 _max_bounding_box(bool include_bed_model) const; void _zoom_to_box(const BoundingBoxf3& box, double margin_factor = DefaultCameraZoomToBoxMarginFactor); void _update_camera_zoom(double zoom); @@ -1057,6 +1072,7 @@ private: #endif // ENABLE_RENDER_SELECTION_CENTER void _check_and_update_toolbar_icon_scale(); void _render_overlays(); + void _render_bed_selector(); void _render_volumes_for_picking(const Camera& camera) const; void _render_current_gizmo() const { m_gizmos.render_current_gizmo(); } void _render_gizmos_overlay(); @@ -1066,11 +1082,12 @@ private: void _render_view_toolbar() const; #if ENABLE_SHOW_CAMERA_TARGET void _render_camera_target(); + void _render_camera_target_validation_box(); #endif // ENABLE_SHOW_CAMERA_TARGET void _render_sla_slices(); void _render_selection_sidebar_hints() { m_selection.render_sidebar_hints(m_sidebar_field); } bool _render_undo_redo_stack(const bool is_undo, float pos_x); - bool _render_arrange_menu(float pos_x); + bool _render_arrange_menu(float pos_x, bool current_bed); void _render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type); // render thumbnail using an off-screen framebuffer void _render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type); @@ -1085,7 +1102,7 @@ private: // Convert the screen space coordinate to an object space coordinate. // If the Z screen space coordinate is not provided, a depth buffer value is substituted. - Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr); + Vec3d _mouse_to_3d(const Point& mouse_pos, const float* z = nullptr, bool use_ortho = false); // Convert the screen space coordinate to world coordinate on the bed. Vec3d _mouse_to_bed_3d(const Point& mouse_pos); @@ -1102,6 +1119,7 @@ private: void _set_warning_notification(EWarning warning, bool state); std::pair _is_any_volume_outside() const; + bool _is_sequential_print_enabled() const; // updates the selection from the content of m_hover_volume_idxs void _update_selection_from_hover(); diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp index a90812d..f27f1c0 100644 --- a/src/slic3r/GUI/GLToolbar.cpp +++ b/src/slic3r/GUI/GLToolbar.cpp @@ -21,6 +21,7 @@ wxDEFINE_EVENT(EVT_GLTOOLBAR_ADD, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_DELETE, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_DELETE_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_ARRANGE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLTOOLBAR_ARRANGE_CURRENT_BED, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_COPY, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_PASTE, SimpleEvent); wxDEFINE_EVENT(EVT_GLTOOLBAR_MORE, SimpleEvent); diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp index 9dfd6ef..1550091 100644 --- a/src/slic3r/GUI/GLToolbar.hpp +++ b/src/slic3r/GUI/GLToolbar.hpp @@ -20,6 +20,7 @@ wxDECLARE_EVENT(EVT_GLTOOLBAR_ADD, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_DELETE, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_DELETE_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_ARRANGE, SimpleEvent); +wxDECLARE_EVENT(EVT_GLTOOLBAR_ARRANGE_CURRENT_BED, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_COPY, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_PASTE, SimpleEvent); wxDECLARE_EVENT(EVT_GLTOOLBAR_MORE, SimpleEvent); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index b0a5e3f..3ab35ae 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -67,6 +67,7 @@ #include "GLCanvas3D.hpp" #include "../Utils/PresetUpdater.hpp" +#include "../Utils/PresetUpdaterWrapper.hpp" #include "../Utils/PrintHost.hpp" #include "../Utils/Process.hpp" #include "../Utils/MacDarkMode.hpp" @@ -94,7 +95,6 @@ #include "WifiConfigDialog.hpp" #include "UserAccount.hpp" #include "UserAccountUtils.hpp" -#include "WebViewDialog.hpp" #include "LoginDialog.hpp" // IWYU pragma: keep #include "PresetArchiveDatabase.hpp" @@ -810,6 +810,26 @@ void GUI_App::post_init() } if (! this->init_params->extra_config.empty()) this->mainframe->load_config(this->init_params->extra_config); + + if (this->init_params->selected_presets.has_valid_data()) { + if (Tab* printer_tab = get_tab(Preset::TYPE_PRINTER)) + printer_tab->select_preset(this->init_params->selected_presets.printer); + + const bool is_fff = preset_bundle->printers.get_selected_preset().printer_technology() == ptFFF; + if (Tab* print_tab = get_tab(is_fff ? Preset::TYPE_PRINT : Preset::TYPE_SLA_PRINT)) + print_tab->select_preset(this->init_params->selected_presets.print); + + if (Tab* print_tab = get_tab(is_fff ? Preset::TYPE_FILAMENT : Preset::TYPE_SLA_MATERIAL)) { + const auto& materials = this->init_params->selected_presets.materials; + print_tab->select_preset(materials[0]); + + if (is_fff && materials.size() > 1) { + for (size_t idx = 1; idx < materials.size(); idx++) + preset_bundle->set_filament_preset(idx, materials[idx]); + sidebar().update_all_filament_comboboxes(); + } + } + } } // show "Did you know" notification @@ -820,17 +840,11 @@ void GUI_App::post_init() // to popup a modal dialog on start without screwing combo boxes. // This is ugly but I honestly found no better way to do it. // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - -#if 0 // This code was moved to EVT_CONFIG_UPDATER_SYNC_DONE bind - after preset_updater finishes synchronization. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; -#endif + if (this->get_preset_updater_wrapper()) { // G-Code Viewer does not initialize preset_updater. CallAfter([this] { // preset_updater->sync downloads profile updates and than via event checks updates and incompatible presets. We need to run it on startup. // start before cw so it is canceled by cw if needed? - this->preset_updater->sync(preset_bundle, this, std::move(plater()->get_preset_archive_database()->get_selected_archive_repositories())); + this->get_preset_updater_wrapper()->sync_preset_updater(this, preset_bundle); bool cw_showed = this->config_wizard_startup(); //B57 //if (! cw_showed) { @@ -874,7 +888,6 @@ GUI_App::~GUI_App() { delete app_config; delete preset_bundle; - delete preset_updater; } // If formatted for github, plaintext with OpenGL extensions enclosed into
. @@ -1052,6 +1065,58 @@ void GUI_App::legacy_app_config_vendor_check() copy_vendor_ini(vendors_to_create); } +std::array get_possible_app_names() { + const std::array suffixes{"-alpha", "-beta", ""}; + std::array result; + std::transform( + suffixes.begin(), + suffixes.end(), + result.begin(), + [](const std::string &suffix){ + return SLIC3R_APP_KEY + suffix; + } + ); + return result; +} + +constexpr bool is_linux = +#if defined(__linux__) +true +#else +false +#endif +; + +namespace fs = boost::filesystem; + +std::vector get_app_config_dir_candidates( + const std::string ¤t_app_name +) { + std::vector candidates; + + // e.g. $HOME/.config + const fs::path config_dir{fs::path{data_dir()}.parent_path()}; + const std::array possible_app_names{get_possible_app_names()}; + + for (const std::string &possible_app_name : possible_app_names){ + if (possible_app_name != current_app_name) { + candidates.emplace_back(config_dir / possible_app_name); + } + } + + if constexpr (is_linux) { + const std::optional home_config_dir{get_home_config_dir()}; + if (home_config_dir && config_dir != home_config_dir) { + for (const std::string &possible_app_name : possible_app_names){ + candidates.emplace_back(*home_config_dir / possible_app_name); + } + } + } + + return candidates; +} + + // returns old config path to copy from if such exists, // returns an empty string if such config path does not exists or if it cannot be loaded. std::string GUI_App::check_older_app_config(Semver current_version, bool backup) @@ -1063,22 +1128,20 @@ std::string GUI_App::check_older_app_config(Semver current_version, bool backup) return {}; // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); + const fs::path app_config_path{app_config->config_path()}; + const std::string filename{app_config_path.filename().string()}; - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + const std::string current_app_name{GetAppName().ToStdString()}; + const std::vector app_config_dir_candidates{get_app_config_dir_candidates( + current_app_name + )}; Semver last_semver = current_version; - for (const auto& candidate : candidates) { + for (const fs::path& candidate_dir : app_config_dir_candidates) { + const fs::path candidate{candidate_dir / filename}; if (boost::filesystem::exists(candidate)) { // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + const boost::optionalother_semver = parse_semver_from_ini(candidate.string()); if (other_semver && *other_semver > last_semver) { last_semver = *other_semver; older_data_dir_path = candidate.parent_path().string(); @@ -1242,6 +1305,31 @@ static int get_app_font_pt_size(const AppConfig* app_config) return (font_pt_size > max_font_pt_size) ? max_font_pt_size : font_pt_size; } +#if defined(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) +void GUI_App::remove_desktop_files_dialog() +{ + // Find all old existing desktop file + std::vector found_desktop_files; + DesktopIntegrationDialog::find_all_desktop_files(found_desktop_files); + if(found_desktop_files.empty()) { + return; + } + // Delete files. + std::vector fails; + DesktopIntegrationDialog::remove_desktop_file_list(found_desktop_files, fails); + if (fails.empty()) { + return; + } + // Inform about fails. + std::string text = "Failed to remove desktop files:"; + text += "\n"; + for (const boost::filesystem::path& entry : fails) { + text += GUI::format("%1%\n",entry.string()); + } + BOOST_LOG_TRIVIAL(error) << text; +} +#endif //(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) + bool GUI_App::on_init_inner() { // TODO: remove this when all asserts are gone. @@ -1260,7 +1348,7 @@ bool GUI_App::on_init_inner() RichMessageDialog dlg(nullptr, _L("You are running a 32 bit build of QIDISlicer on 64-bit Windows." "\n32 bit build of QIDISlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of QIDISlicer from https://qidi3d.com/pages/software-firmware/." + "\nPlease download and install a 64 bit build of QIDISlicer from https://qidi3d.com." "\nDo you wish to continue?"), "QIDISlicer", wxICON_QUESTION | wxYES_NO); if (dlg.ShowModal() != wxID_YES) @@ -1412,7 +1500,7 @@ bool GUI_App::on_init_inner() associate_step_files(); #endif // __WXMSW__ - preset_updater = new PresetUpdater(); + m_preset_updater_wrapper = std::make_unique(); Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { if (this->plater_ != nullptr && (m_app_updater->get_triggered_by_user() || app_config->get("notify_release") == "all")) { @@ -1503,6 +1591,10 @@ bool GUI_App::on_init_inner() // Call this check only after appconfig was loaded to mainframe, otherwise there will be duplicity error. legacy_app_config_vendor_check(); +#if defined(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) + remove_desktop_files_dialog(); +#endif //(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) + sidebar().obj_list()->init_objects(); // propagate model objects to object list update_mode(); // mode sizer doesn't exist anymore, so we came update mode here, before load_current_presets SetTopWindow(mainframe); @@ -2681,7 +2773,8 @@ wxMenu* GUI_App::get_config_menu(MainFrame* main_frame) local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + //y21 + //local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); #if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) //if (DesktopIntegrationDialog::integration_possible()) @@ -3302,11 +3395,13 @@ wxString GUI_App::current_language_code_safe() const void GUI_App::open_web_page_localized(const std::string &http_address) { - open_browser_with_warning_dialog(from_u8(http_address + "&lng=") + this->current_language_code_safe(), nullptr, false); + //y + // open_browser_with_warning_dialog(from_u8(http_address + "&lng=") + this->current_language_code_safe(), nullptr, false); + open_browser_with_warning_dialog(from_u8(http_address), nullptr, false); } -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have modifiers. +// Modifiers are not supported in SLA mode. bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) { if (model_has_parameter_modifiers_in_objects(model())) { @@ -3326,47 +3421,18 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage //y15 std::string old_token = wxGetApp().app_config->get("user_token"); - // Cancel sync before starting wizard to prevent two downloads at same time. - preset_updater->cancel_sync(); - // Show login dialog before wizard. -#if 0 - bool user_was_logged = plater()->get_user_account()->is_logged(); - if (!user_was_logged) { - m_login_dialog = std::make_unique(mainframe, plater()->get_user_account()); - m_login_dialog->ShowModal(); - mainframe->RemoveChild(m_login_dialog.get()); - m_login_dialog->Destroy(); - // Destructor does not call Destroy. - m_login_dialog.reset(); - } -#endif // 0 - // ConfigWizard can take some time to start. Because it is a wxWidgets window, it has to be done - // in UI thread, so displaying a nice modal dialog and letting the CW start in a worker thread - // is not an option. Let's at least show a modeless dialog before the UI thread freezes. - // TRN: Text showing while the ConfigWizard is loading, so the user knows something is happening. - auto cw_loading_dlg = new ConfigWizardLoadingDialog(mainframe, _L("Loading Configuration Wizard...")); - cw_loading_dlg->CenterOnParent(); - cw_loading_dlg->Show(); - wxYield(); - - // We have to update repos - //y15 - //plater()->get_preset_archive_database()->sync_blocking(); - - if (reason == ConfigWizard::RunReason::RR_USER) { - // Since there might be new repos, we need to sync preset updater - const SharedArchiveRepositoryVector &repos = plater()->get_preset_archive_database()->get_selected_archive_repositories(); - //y15 - // preset_updater->sync_blocking(preset_bundle, this, repos); - preset_updater->update_index_db(); - // Offer update installation. - preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::SHOW_TEXT_BOX, repos); - } + // Loading of Config Wizard takes some time. + // First part is to download neccessary data. + // That is done on worker thread while nice modal progress is shown. + // TRN: Progress dialog title + //y20 + //get_preset_updater_wrapper()->wizard_sync(preset_bundle, app_config->orig_version(), mainframe, reason == ConfigWizard::RunReason::RR_USER, _L("Opening Configuration Wizard")); + + // Then the wizard itself will start and that also takes time. + // But for now no ui is shown until then. (Showing modal progress dialog while showing another would be a headacke) m_config_wizard = new ConfigWizard(mainframe); - cw_loading_dlg->Close(); - const bool res = m_config_wizard->run(reason, start_page); @@ -3388,9 +3454,6 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage break; } } - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); } //y14 std::string new_token = wxGetApp().app_config->get("user_token"); @@ -3594,49 +3657,26 @@ bool GUI_App::config_wizard_startup() bool GUI_App::check_updates(const bool invoked_by_user) { - if (invoked_by_user) { - // do preset_updater sync so if user runs slicer for a long time, check for updates actually delivers updates. - // for preset_updater sync we need to sync archive database first - plater()->get_preset_archive_database()->sync_blocking(); - // Now re-extract offline repos - std::string failed_paths; - if (!plater()->get_preset_archive_database()->extract_archives_with_check(failed_paths)) { - int cnt = std::count(failed_paths.begin(), failed_paths.end(), '\n') + 1; - // TRN: %1% contains paths from which loading failed. They are separated by \n, there is no \n at the end. - failed_paths = GUI::format(_L_PLURAL("It was not possible to extract data from %1%. The source will not be updated.", - "It was not possible to extract data for following local sources. They will not be updated.\n\n %1%", cnt), failed_paths); - show_error(nullptr, failed_paths); - } - // then its time for preset_updater sync - preset_updater->sync_blocking(preset_bundle, this, plater()->get_preset_archive_database()->get_selected_archive_repositories()); - // and then we check updates + PresetUpdater::UpdateResult updater_result; + if (invoked_by_user) + { + updater_result = get_preset_updater_wrapper()->check_updates_on_user_request(preset_bundle, app_config->orig_version(), mainframe); + } else { + updater_result = get_preset_updater_wrapper()->check_updates_on_startup( app_config->orig_version()); } - PresetUpdater::UpdateResult updater_result; - try { - preset_updater->update_index_db(); - //y15 - Plater* hasplater = plater(); - if (hasplater == NULL) - updater_result == PresetUpdater::R_INCOMPAT_EXIT; - else - updater_result = preset_updater->config_update(app_config->orig_version(), invoked_by_user ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION, plater()->get_preset_archive_database()->get_selected_archive_repositories()); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (invoked_by_user && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; } + else if (invoked_by_user && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } // Applicaiton will continue. return true; } @@ -3964,20 +4004,8 @@ const Preset* find_preset_by_nozzle_and_options( for (const Preset &preset : collection) { // trim repo prefix std::string printer_model = preset.config.opt_string("printer_model"); - std::string vendor_repo_prefix; - if (preset.vendor) { - vendor_repo_prefix = preset.vendor->repo_prefix; - } else if (std::string inherits = preset.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model.find(vendor_repo_prefix) == 0) { - printer_model = printer_model.substr(vendor_repo_prefix.size() - ); - boost::trim_left(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; @@ -4000,7 +4028,7 @@ const Preset* find_preset_by_nozzle_and_options( 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(true); + assert(false); continue; } } @@ -4236,12 +4264,52 @@ void GUI_App::handle_connect_request_printer_select_inner(const std::string & ms // mainframe->show_printer_webview_tab(preset_bundle->physical_printers.get_selected_printer_config()); //} +void GUI_App::printables_download_request(const std::string& download_url, const std::string& model_url) +{ + //this->mainframe->select_tab(size_t(0)); + //lets always init so if the download dest folder was changed, new dest is used + boost::filesystem::path dest_folder(app_config->get("url_downloader_dest")); + if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { + std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return; + } + m_downloader->init(dest_folder); + m_downloader->start_download_printables(download_url, false, model_url, this); +} +void GUI_App::printables_slice_request(const std::string& download_url, const std::string& model_url) +{ + this->mainframe->select_tab(size_t(0)); + + //lets always init so if the download dest folder was changed, new dest is used + boost::filesystem::path dest_folder(app_config->get("url_downloader_dest")); + if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { + std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return; + } + m_downloader->init(dest_folder); + m_downloader->start_download_printables(download_url, true, model_url, this); +} + +void GUI_App::printables_login_request() +{ + plater_->get_user_account()->do_login(); +} + +void GUI_App::open_link_in_printables(const std::string& url) +{ + mainframe->show_printables_tab(url); +} bool LogGui::ignorred_message(const wxString& msg) { for(const wxString& err : std::initializer_list{ wxString("cHRM chunk does not match sRGB"), - wxString("known incorrect sRGB profile") }) { + wxString("known incorrect sRGB profile"), + wxString("Error running JavaScript")}) { if (msg.Contains(err)) return true; } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index dbe0a4c..025eb43 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -34,7 +34,7 @@ namespace Slic3r { class AppConfig; class PresetBundle; -class PresetUpdater; +class PresetUpdaterWrapper; class ModelObject; class PrintHostJobQueue; class Model; @@ -386,11 +386,9 @@ public: AppConfig* app_config{ nullptr }; PresetBundle* preset_bundle{ nullptr }; - PresetUpdater* preset_updater{ nullptr }; MainFrame* mainframe{ nullptr }; Plater* plater_{ nullptr }; - - PresetUpdater* get_preset_updater() { return preset_updater; } + PresetUpdaterWrapper* get_preset_updater_wrapper() { return m_preset_updater_wrapper.get(); } wxBookCtrlBase* tab_panel() const ; int extruders_cnt() const; @@ -449,14 +447,6 @@ public: //y3 void setExitHost(std::set exit_host) { m_exit_host = exit_host; }; std::set getExitHost() { return m_exit_host; }; - void request_login(bool show_user_info = false) {} - bool check_login() { return false; } - void get_login_info() {} - bool is_user_login() { return true; } - - void request_user_login(int online_login) {} - void request_user_logout() {} - int request_user_unbind(std::string dev_id) { return 0; } bool select_printer_from_connect(const std::string& cmd); void select_filament_from_connect(const std::string& cmd); void handle_connect_request_printer_select(const std::string& cmd); @@ -473,7 +463,10 @@ public: void request_project_download(std::string project_id) {} void request_open_project(std::string project_id) {} void request_remove_project(std::string project_id) {} - + void printables_download_request(const std::string& download_url, const std::string& model_url); + 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); private: bool on_init_inner(); void init_app_config(); @@ -495,6 +488,9 @@ private: void app_updater(bool from_user); // inititate read of version file online in separate thread void app_version_check(bool from_user); +#if defined(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) + void remove_desktop_files_dialog(); +#endif //(__linux__) && !defined(SLIC3R_DESKTOP_INTEGRATION) bool m_wifi_config_dialog_shown { false }; //y3 @@ -506,7 +502,7 @@ private: std::map< ConfigMenuIDs, wxMenuItem*> m_config_menu_updatable_items; ConfigWizard* m_config_wizard {nullptr}; - + std::unique_ptr m_preset_updater_wrapper; }; DECLARE_APP(GUI_App) diff --git a/src/slic3r/GUI/GUI_Init.hpp b/src/slic3r/GUI/GUI_Init.hpp index 8702772..05544fe 100644 --- a/src/slic3r/GUI/GUI_Init.hpp +++ b/src/slic3r/GUI/GUI_Init.hpp @@ -13,6 +13,15 @@ struct OpenGLVersions static const std::vector> core; }; +struct CLISelectedProfiles +{ + std::string print; + std::string printer; + std::vector materials; + + bool has_valid_data() { return !print.empty() && !printer.empty() && !materials.empty(); } +}; + struct GUI_InitParams { int argc; @@ -24,6 +33,7 @@ struct GUI_InitParams std::vector load_configs; DynamicPrintConfig extra_config; std::vector input_files; + CLISelectedProfiles selected_presets; bool start_as_gcodeviewer; bool start_downloader; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index d53fc55..81932b9 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -16,6 +16,8 @@ #include "Gizmos/GLGizmoCut.hpp" #include "Gizmos/GLGizmoScale.hpp" +#include "libslic3r/MultipleBeds.hpp" + #include "OptionsGroup.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" @@ -1828,6 +1830,9 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const std::string &n new_object->ensure_on_bed(); + if (! s_multiple_beds.get_loading_project_flag()) + new_object->instances.front()->set_offset(new_object->instances.front()->get_offset() + s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); + #ifdef _DEBUG check_model_ids_validity(model); #endif /* _DEBUG */ @@ -1914,13 +1919,20 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) } break; - case InfoItemType::MmuSegmentation: + case InfoItemType::MmSegmentation: cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove Multi Material painting")); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) mv->mm_segmentation_facets.reset(); break; + case InfoItemType::FuzzySkin: + cnv->get_gizmos_manager().reset_all_states(); + Plater::TakeSnapshot(plater, _L("Remove paint-on fuzzy skin")); + for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) + mv->fuzzy_skin_facets.reset(); + break; + case InfoItemType::Sinking: Plater::TakeSnapshot(plater, _L("Shift objects to bed")); (*m_objects)[obj_idx]->ensure_on_bed(); @@ -2125,8 +2137,8 @@ void ObjectList::split() take_snapshot(_(L("Split to Parts"))); - // Before splitting volume we have to remove all custom supports, seams, and multimaterial painting. - wxGetApp().plater()->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams and multimaterial painting were " + // Before splitting volume we have to remove all custom supports, seams, fuzzy skin and multi-material painting. + 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); @@ -2141,8 +2153,8 @@ void ObjectList::split() // update printable state for new volumes on canvas3D wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_object(obj_idx); - // After removing custom supports, seams, and multimaterial painting, we have to update info about the object to remove information about - // custom supports, seams, and multimaterial painting in the right panel. + // After removing custom supports, seams, fuzzy skin, and multi-material painting, we have to update info about the object to remove information about + // custom supports, seams, fuzzy skin, and multi-material painting in the right panel. wxGetApp().obj_list()->update_info_items(obj_idx); } @@ -2515,7 +2527,7 @@ void ObjectList::invalidate_cut_info_for_object(int obj_idx) take_snapshot(_L("Invalidate cut info")); - const CutObjectBase cut_id = init_obj->cut_id; + const CutId cut_id = init_obj->cut_id; // invalidate cut for related objects (which have the same cut_id) for (size_t idx = 0; idx < m_objects->size(); idx++) if (ModelObject* obj = object(int(idx)); obj->cut_id.is_equal(cut_id)) { @@ -2545,7 +2557,7 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx) take_snapshot(_L("Delete all connectors")); - const CutObjectBase cut_id = init_obj->cut_id; + const CutId cut_id = init_obj->cut_id; // Delete all connectors for related objects (which have the same cut_id) Model& model = wxGetApp().plater()->model(); for (int idx = int(m_objects->size())-1; idx >= 0; idx--) @@ -2668,7 +2680,7 @@ void ObjectList::part_selection_changed() disable_ss_manipulation = (*m_objects)[obj_idx]->is_cut(); } else if (selection.is_mixed() || selection.is_multiple_full_object()) { - std::map> cut_objects; + std::map> cut_objects; // find cut objects for (auto item : sels) { @@ -2720,11 +2732,13 @@ void ObjectList::part_selection_changed() } case InfoItemType::CustomSupports: case InfoItemType::CustomSeam: - case InfoItemType::MmuSegmentation: + case InfoItemType::MmSegmentation: + case InfoItemType::FuzzySkin: { GLGizmosManager::EType gizmo_type = info_type == InfoItemType::CustomSupports ? GLGizmosManager::EType::FdmSupports : info_type == InfoItemType::CustomSeam ? GLGizmosManager::EType::Seam : - GLGizmosManager::EType::MmuSegmentation; + info_type == InfoItemType::FuzzySkin ? GLGizmosManager::EType::FuzzySkin : + GLGizmosManager::EType::MmSegmentation; if (gizmos_mgr.get_current_type() != gizmo_type) gizmos_mgr.open_gizmo(gizmo_type); break; @@ -2894,7 +2908,8 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio for (InfoItemType type : {InfoItemType::CustomSupports, InfoItemType::CustomSeam, InfoItemType::CutConnectors, - InfoItemType::MmuSegmentation, + InfoItemType::MmSegmentation, + InfoItemType::FuzzySkin, InfoItemType::Sinking, InfoItemType::VariableLayerHeight}) { wxDataViewItem item = m_objects_model->GetInfoItemByType(item_obj, type); @@ -2904,12 +2919,14 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio switch (type) { case InfoItemType::CustomSupports : case InfoItemType::CustomSeam : - case InfoItemType::MmuSegmentation : + case InfoItemType::MmSegmentation : + case InfoItemType::FuzzySkin : should_show = printer_technology() == ptFFF && std::any_of(model_object->volumes.begin(), model_object->volumes.end(), [type](const ModelVolume *mv) { return !(type == InfoItemType::CustomSupports ? mv->supported_facets.empty() : type == InfoItemType::CustomSeam ? mv->seam_facets.empty() : + type == InfoItemType::FuzzySkin ? mv->fuzzy_skin_facets.empty() : mv->mm_segmentation_facets.empty()); }); break; @@ -4634,7 +4651,7 @@ void ObjectList::fix_through_winsdk() msg += "\n"; } - plater->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams and multimaterial painting were " + plater->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams, fuzzy skin and multimaterial painting were " "removed after repairing the mesh.")); std::string res; if (!fix_model_by_win10_sdk_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res)) @@ -4976,10 +4993,5 @@ ModelObject* ObjectList::object(const int obj_idx) const return (*m_objects)[obj_idx]; } -bool ObjectList::has_paint_on_segmentation() -{ - return m_objects_model->HasInfoItem(InfoItemType::MmuSegmentation); -} - } //namespace GUI } //namespace Slic3r diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index d783c00..1dc99dd 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -406,7 +406,6 @@ public: void set_extruder_for_selected_items(const int extruder) const ; wxDataViewItemArray reorder_volumes_and_get_selection(size_t obj_idx, std::function add_to_selection = nullptr); void apply_volumes_order(); - bool has_paint_on_segmentation(); bool is_editing() const { return m_is_editing_started; } diff --git a/src/slic3r/GUI/GUI_ObjectSettings.cpp b/src/slic3r/GUI/GUI_ObjectSettings.cpp index e9defa8..449d60a 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.cpp @@ -224,12 +224,12 @@ void ObjectSettings::update_config_values(ModelConfig* config) update_config_values(config); if (is_added) { -// #ysFIXME - Delete after testing! Very likely this CallAfret is no needed -// wxTheApp->CallAfter([this]() { + // #ysNOTE - CallAfter is needed here to avoid crash on add new override params! see GH#13450 + wxTheApp->CallAfter([this]() { wxWindowUpdateLocker noUpdates(m_parent); update_settings_list(); m_parent->Layout(); -// }); + }); } }; diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 74a209c..d233aa5 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -35,6 +35,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/SLAPrint.hpp" #include "NotificationManager.hpp" +#include "libslic3r/MultipleBeds.hpp" #ifdef _WIN32 #include "BitmapComboBox.hpp" @@ -178,10 +179,10 @@ void View3D::render() Preview::Preview( wxWindow* parent, Bed3D& bed, Model* model, DynamicPrintConfig* config, - BackgroundSlicingProcess* process, GCodeProcessorResult* gcode_result, std::function schedule_background_process_func) + BackgroundSlicingProcess* process, std::vector* gcode_results, std::function schedule_background_process_func) : m_config(config) , m_process(process) - , m_gcode_result(gcode_result) + , m_gcode_results(gcode_results) , m_schedule_background_process(schedule_background_process_func) { if (init(parent, bed, model)) @@ -194,6 +195,11 @@ void Preview::set_layers_slider_values_range(int bottom, int top) std::max(bottom, m_layers_slider->GetMinPos())); } +GCodeProcessorResult* Preview::active_gcode_result() +{ + return &(*m_gcode_results)[s_multiple_beds.get_active_bed()]; +} + bool Preview::init(wxWindow* parent, Bed3D& bed, Model* model) { if (!Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 /* disable wxTAB_TRAVERSAL */)) @@ -271,11 +277,6 @@ void Preview::set_drop_target(wxDropTarget* target) SetDropTarget(target); } -void Preview::load_gcode_shells() -{ - m_canvas->load_gcode_shells(); -} - void Preview::load_print(bool keep_z_range) { PrinterTechnology tech = m_process->current_printer_technology(); @@ -331,14 +332,14 @@ void Preview::render_sliders(GLCanvas3D& canvas) float Preview::get_moves_slider_height() const { - if (m_moves_slider && m_moves_slider->IsShown()) + if (!s_multiple_beds.is_autoslicing() && m_moves_slider && m_moves_slider->IsShown()) return m_moves_slider->GetHeight(); return 0.0f; } -float Preview::get_layers_slider_width() const +float Preview::get_layers_slider_width(bool disregard_visibility) const { - if (m_layers_slider && m_layers_slider->IsShown()) + if (!s_multiple_beds.is_autoslicing() && m_layers_slider && (m_layers_slider->IsShown() || disregard_visibility)) return m_layers_slider->GetWidth(); return 0.0f; } @@ -410,7 +411,7 @@ void Preview::create_sliders() if (wxGetApp().is_editor()) { m_layers_slider->set_callback_on_ticks_changed([this]() -> void { Model& model = wxGetApp().plater()->model(); - model.custom_gcode_per_print_z = m_layers_slider->GetTicksValues(); + model.custom_gcode_per_print_z() = m_layers_slider->GetTicksValues(); m_schedule_background_process(); m_keep_current_preview_type = false; @@ -431,7 +432,7 @@ void Preview::create_sliders() }); m_layers_slider->set_callback_on_get_print([]() -> const Print& { - return GUI::wxGetApp().plater()->fff_print(); + return GUI::wxGetApp().plater()->active_fff_print(); }); m_layers_slider->set_callback_on_get_custom_code([](const std::string& code_in, double height) -> std::string @@ -603,15 +604,15 @@ void Preview::update_layers_slider(const std::vector& layers_z, bool kee Plater* plater = wxGetApp().plater(); CustomGCode::Info ticks_info_from_model; if (wxGetApp().is_editor()) - ticks_info_from_model = plater->model().custom_gcode_per_print_z; + ticks_info_from_model = plater->model().custom_gcode_per_print_z(); else { ticks_info_from_model.mode = CustomGCode::Mode::SingleExtruder; - ticks_info_from_model.gcodes = m_gcode_result->custom_gcode_per_print_z; + ticks_info_from_model.gcodes = active_gcode_result()->custom_gcode_per_print_z; } check_layers_slider_values(ticks_info_from_model.gcodes, layers_z); //first of all update extruder colors to avoid crash, when we are switching printer preset from MM to SM - m_layers_slider->SetExtruderColors(plater->get_extruder_color_strings_from_plater_config(wxGetApp().is_editor() ? nullptr : m_gcode_result)); + m_layers_slider->SetExtruderColors(plater->get_extruder_color_strings_from_plater_config(wxGetApp().is_editor() ? nullptr : active_gcode_result())); m_layers_slider->SetSliderValues(layers_z); m_layers_slider->force_ruler_update(); assert(m_layers_slider->GetMinPos() == 0); @@ -641,9 +642,9 @@ void Preview::update_layers_slider(const std::vector& layers_z, bool kee bool sequential_print = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("complete_objects"); m_layers_slider->SetDrawMode(sla_print_technology, sequential_print); if (sla_print_technology) - m_layers_slider->SetLayersTimes(plater->sla_print().print_statistics().layers_times_running_total); + m_layers_slider->SetLayersTimes(plater->active_sla_print().print_statistics().layers_times_running_total); else - m_layers_slider->SetLayersTimes(m_canvas->get_gcode_layers_times_cache(), m_gcode_result->print_statistics.modes.front().time); + m_layers_slider->SetLayersTimes(m_canvas->get_gcode_layers_times_cache(), active_gcode_result()->print_statistics.modes.front().time); m_layers_slider->Thaw(); @@ -658,7 +659,7 @@ void Preview::update_layers_slider(const std::vector& layers_z, bool kee auto get_print_obj_idxs = [plater]() ->std::string { if (plater->printer_technology() == ptSLA) return "sla"; - const Print& print = GUI::wxGetApp().plater()->fff_print(); + const Print& print = GUI::wxGetApp().plater()->active_fff_print(); std::string idxs; for (auto object : print.objects()) idxs += std::to_string(object->id().id) + "_"; @@ -670,7 +671,7 @@ void Preview::update_layers_slider(const std::vector& layers_z, bool kee wxGetApp().app_config->get_bool("allow_auto_color_change") && m_layers_slider->is_new_print(get_print_obj_idxs())) { - const Print& print = wxGetApp().plater()->fff_print(); + const Print& print = wxGetApp().plater()->active_fff_print(); //bool is_possible_auto_color_change = false; for (auto object : print.objects()) { @@ -835,7 +836,7 @@ void Preview::update_sliders_from_canvas(wxKeyEvent& event) void Preview::update_moves_slider(std::optional visible_range_min, std::optional visible_range_max) { - if (m_gcode_result->moves.empty()) + if (active_gcode_result()->moves.empty()) return; const libvgcode::Interval& range = m_canvas->get_gcode_view_enabled_range(); @@ -931,7 +932,9 @@ void Preview::load_print_as_fff(bool keep_z_range) } if (wxGetApp().is_editor() && !has_layers) { + m_canvas->reset_gcode_toolpaths(); m_canvas->reset_gcode_layers_times_cache(); + m_canvas->load_gcode_shells(); hide_layers_slider(); m_moves_slider->Hide(); m_canvas_widget->Refresh(); @@ -939,15 +942,16 @@ void Preview::load_print_as_fff(bool keep_z_range) } libvgcode::EViewType gcode_view_type = m_canvas->get_gcode_view_type(); - const bool gcode_preview_data_valid = !m_gcode_result->moves.empty(); + const bool gcode_preview_data_valid = !active_gcode_result()->moves.empty(); const bool is_pregcode_preview = !gcode_preview_data_valid && wxGetApp().is_editor(); - const std::vector tool_colors = wxGetApp().plater()->get_extruder_color_strings_from_plater_config(m_gcode_result); + const std::vector tool_colors = wxGetApp().plater()->get_extruder_color_strings_from_plater_config(active_gcode_result()); const std::vector& color_print_values = wxGetApp().is_editor() ? - wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes : m_gcode_result->custom_gcode_per_print_z; + wxGetApp().plater()->model().custom_gcode_per_print_z().gcodes : active_gcode_result()->custom_gcode_per_print_z; + std::vector color_print_colors; if (!color_print_values.empty()) { - color_print_colors = wxGetApp().plater()->get_color_strings_for_color_print(m_gcode_result); + color_print_colors = wxGetApp().plater()->get_color_strings_for_color_print(active_gcode_result()); color_print_colors.push_back("#808080"); // gray color for pause print or custom G-code } @@ -957,7 +961,7 @@ void Preview::load_print_as_fff(bool keep_z_range) m_canvas->set_selected_extruder(0); if (gcode_preview_data_valid) { // Load the real G-code preview. - m_canvas->load_gcode_preview(*m_gcode_result, tool_colors, color_print_colors); + m_canvas->load_gcode_preview(*active_gcode_result(), tool_colors, color_print_colors); // the view type may have been changed by the call m_canvas->load_gcode_preview() gcode_view_type = m_canvas->get_gcode_view_type(); zs = m_canvas->get_gcode_layers_zs(); @@ -966,6 +970,7 @@ void Preview::load_print_as_fff(bool keep_z_range) else if (is_pregcode_preview) { // Load the initial preview based on slices, not the final G-code. m_canvas->load_preview(tool_colors, color_print_colors, color_print_values); + m_canvas->load_gcode_shells(); // the view type has been changed by the call m_canvas->load_gcode_preview() if (gcode_view_type == libvgcode::EViewType::ColorPrint && !color_print_values.empty()) m_canvas->set_gcode_view_type(gcode_view_type); diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index 8ba8902..d5378e7 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -82,7 +82,9 @@ class Preview : public wxPanel DynamicPrintConfig* m_config; BackgroundSlicingProcess* m_process; - GCodeProcessorResult* m_gcode_result; + std::vector* m_gcode_results; + + GCodeProcessorResult* active_gcode_result(); // Calling this function object forces Plater::schedule_background_process. std::function m_schedule_background_process; @@ -113,7 +115,7 @@ public: }; Preview(wxWindow* parent, Bed3D& bed, Model* model, DynamicPrintConfig* config, BackgroundSlicingProcess* process, - GCodeProcessorResult* gcode_result, std::function schedule_background_process = []() {}); + std::vector* gcode_results, std::function schedule_background_process = []() {}); virtual ~Preview(); wxGLCanvas* get_wxglcanvas() { return m_canvas_widget; } @@ -125,14 +127,13 @@ public: void select_view(const std::string& direction); void set_drop_target(wxDropTarget* target); - void load_gcode_shells(); void load_print(bool keep_z_range = false); void reload_print(); void msw_rescale(); void render_sliders(GLCanvas3D& canvas); - float get_layers_slider_width() const; + float get_layers_slider_width(bool disregard_visibility = false) const; float get_moves_slider_height() const; bool is_loaded() const { return m_loaded; } diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index 6238af6..741a97b 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -31,6 +31,7 @@ #include "libslic3r/Model.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Format/OBJ.hpp" +#include "libslic3r/MultipleBeds.hpp" #include "../Utils/MacDarkMode.hpp" namespace Slic3r { @@ -301,7 +302,9 @@ static void generate_thumbnail_from_model(const std::string& filename) ThumbnailData thumbnail_data; const ThumbnailsParams thumbnail_params = { {}, false, false, false, true }; + s_multiple_beds.set_thumbnail_bed_idx(-2); wxGetApp().plater()->canvas3D()->render_thumbnail(thumbnail_data, 256, 256, thumbnail_params, volumes, Camera::EType::Perspective); + s_multiple_beds.set_thumbnail_bed_idx(-1); if (thumbnail_data.width == 0 || thumbnail_data.height == 0) return; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 8dcb945..6f55d4d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -3252,7 +3252,7 @@ Transform3d GLGizmoCut3D::get_cut_matrix(const Selection& selection) return translation_transform(cut_center_offset) * m_rotation_m; } -void update_object_cut_id(CutObjectBase& cut_id, ModelObjectCutAttributes attributes, const int dowels_count) +void update_object_cut_id(CutId& cut_id, ModelObjectCutAttributes attributes, const int dowels_count) { // we don't save cut information, if result will not contains all parts of initial object if (!attributes.has(ModelObjectCutAttribute::KeepUpper) || @@ -3260,7 +3260,7 @@ void update_object_cut_id(CutObjectBase& cut_id, ModelObjectCutAttributes attrib attributes.has(ModelObjectCutAttribute::InvalidateCutInfo)) return; - if (cut_id.id().invalid()) + if (! cut_id.valid()) cut_id.init(); // increase check sum, if it's needed { @@ -3373,11 +3373,11 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects) } } -void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id) +void synchronize_model_after_cut(Model& model, const CutId& cut_id) { for (ModelObject* obj : model.objects) if (obj->is_cut() && obj->cut_id.has_same_id(cut_id) && !obj->cut_id.is_equal(cut_id)) - obj->cut_id.copy(cut_id); + obj->cut_id = cut_id; } void GLGizmoCut3D::perform_cut(const Selection& selection) @@ -3428,7 +3428,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) only_if(m_rotate_upper, ModelObjectCutAttribute::FlipUpper) | only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower) | only_if(dowels_count > 0, ModelObjectCutAttribute::CreateDowels) | - only_if(!has_connectors && !cut_with_groove && cut_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo); + only_if(!has_connectors && !cut_with_groove && ! cut_mo->cut_id.valid(), ModelObjectCutAttribute::InvalidateCutInfo); // update cut_id for the cut object in respect to the attributes update_object_cut_id(cut_mo->cut_id, attributes, dowels_count); @@ -3441,7 +3441,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) check_objects_after_cut(new_objects); // save cut_id to post update synchronization - const CutObjectBase cut_id = cut_mo->cut_id; + const CutId cut_id = cut_mo->cut_id; // update cut results on plater and in the model plater->apply_cut_object_to_model(object_idx, new_objects); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index 4fdd5f4..97c29a7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -2752,9 +2752,10 @@ void GLGizmoEmboss::draw_advanced() return; } + +#ifdef SHOW_FONT_FILE_PROPERTY FontProp &font_prop = m_style_manager.get_font_prop(); const FontFile::Info &font_info = get_font_info(*ff.font_file, font_prop); -#ifdef SHOW_FONT_FILE_PROPERTY ImGui::SameLine(); int cache_size = ff.has_value()? (int)ff.cache->size() : 0; std::string ff_property = diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index ca94919..9b30455 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -99,7 +99,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l if (! m_c->selection_info()->model_object()) return; - const float approx_height = m_imgui->scaled(25.f); + const float approx_height = m_imgui->scaled(26.3f); y = std::min(y, bottom_limit - approx_height); ImGuiPureWrap::set_next_window_pos(x, y, ImGuiCond_Always); @@ -111,7 +111,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l const float cursor_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); const float smart_fill_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); const float autoset_slider_label_max_width = m_imgui->scaled(7.5f); - const float autoset_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("highlight_by_angle"), autoset_slider_label_max_width).x + m_imgui->scaled(1.f); + const float autoset_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("highlight_by_angle"), false, autoset_slider_label_max_width).x + m_imgui->scaled(1.f); const float cursor_type_radio_circle = ImGuiPureWrap::calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); const float cursor_type_radio_sphere = ImGuiPureWrap::calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); @@ -298,11 +298,12 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) { + for (auto &triangle_selector: m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); } + } } ImGui::Separator(); @@ -456,7 +457,7 @@ void GLGizmoFdmSupports::update_model_object() const if (! mv->is_model_part()) continue; ++idx; - updated |= mv->supported_facets.set(*m_triangle_selectors[idx].get()); + updated |= mv->supported_facets.set(*m_triangle_selectors[idx]); } if (updated) { @@ -515,7 +516,7 @@ bool GLGizmoFdmSupports::has_backend_supports() void GLGizmoFdmSupports::auto_generate() { - std::string err = wxGetApp().plater()->fff_print().validate(); + std::string err = wxGetApp().plater()->active_fff_print().validate(); if (!err.empty()) { MessageDialog dlg(GUI::wxGetApp().plater(), _L("Automatic painting requires valid print setup.") + " \n" + from_u8(err), _L("Warning"), wxOK); dlg.ShowModal(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp new file mode 100644 index 0000000..e250b39 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp @@ -0,0 +1,311 @@ +#include "GLGizmoFuzzySkin.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/Print.hpp" + +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +#include +#include + +namespace Slic3r::GUI { + +void GLGizmoFuzzySkin::on_shutdown() +{ + m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); +} + +std::string GLGizmoFuzzySkin::on_get_name() const +{ + return _u8L("Paint-on fuzzy skin"); +} + +bool GLGizmoFuzzySkin::on_init() +{ + m_shortcut_key = WXK_CONTROL_H; + + m_desc["clipping_of_view"] = _u8L("Clipping of view") + ": "; + m_desc["reset_direction"] = _u8L("Reset direction"); + m_desc["cursor_size"] = _u8L("Brush size") + ": "; + m_desc["cursor_type"] = _u8L("Brush shape") + ": "; + m_desc["add_fuzzy_skin_caption"] = _u8L("Left mouse button") + ": "; + m_desc["add_fuzzy_skin"] = _u8L("Add fuzzy skin"); + m_desc["remove_fuzzy_skin_caption"] = _u8L("Shift + Left mouse button") + ": "; + m_desc["remove_fuzzy_skin"] = _u8L("Remove fuzzy skin"); + m_desc["remove_all"] = _u8L("Remove all selection"); + m_desc["circle"] = _u8L("Circle"); + m_desc["sphere"] = _u8L("Sphere"); + m_desc["pointer"] = _u8L("Triangles"); + m_desc["tool_type"] = _u8L("Tool type") + ": "; + m_desc["tool_brush"] = _u8L("Brush"); + m_desc["tool_smart_fill"] = _u8L("Smart fill"); + m_desc["smart_fill_angle"] = _u8L("Smart fill angle"); + m_desc["split_triangles"] = _u8L("Split triangles"); + + return true; +} + +void GLGizmoFuzzySkin::render_painter_gizmo() +{ + const Selection &selection = m_parent.get_selection(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + render_triangles(selection); + m_c->object_clipper()->render_cut(); + m_c->instances_hider()->render_cut(); + render_cursor(); + + glsafe(::glDisable(GL_BLEND)); +} + +void GLGizmoFuzzySkin::on_render_input_window(float x, float y, float bottom_limit) +{ + if (!m_c->selection_info()->model_object()) + return; + + const float approx_height = m_imgui->scaled(22.f); + + y = std::min(y, bottom_limit - approx_height); + ImGuiPureWrap::set_next_window_pos(x, y, ImGuiCond_Always); + + ImGuiPureWrap::begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + // 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 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 cursor_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); + const float smart_fill_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); + + const float cursor_type_radio_circle = ImGuiPureWrap::calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_sphere = ImGuiPureWrap::calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_pointer = ImGuiPureWrap::calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); + + const float button_width = ImGuiPureWrap::calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); + const float buttons_width = m_imgui->scaled(0.5f); + const float minimal_slider_width = m_imgui->scaled(4.f); + + const float tool_type_radio_left = ImGuiPureWrap::calc_text_size(m_desc["tool_type"]).x + m_imgui->scaled(1.f); + const float tool_type_radio_brush = ImGuiPureWrap::calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_smart_fill = ImGuiPureWrap::calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); + + const float split_triangles_checkbox_width = ImGuiPureWrap::calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); + + float caption_max = 0.f; + float total_text_max = 0.f; + for (const std::string t : {"add_fuzzy_skin", "remove_fuzzy_skin"}) { + caption_max = std::max(caption_max, ImGuiPureWrap::calc_text_size(m_desc[t + "_caption"]).x); + total_text_max = std::max(total_text_max, ImGuiPureWrap::calc_text_size(m_desc[t]).x); + } + + total_text_max += caption_max + m_imgui->scaled(1.f); + caption_max += m_imgui->scaled(1.f); + + const float sliders_left_width = std::max(smart_fill_slider_left, std::max(cursor_slider_left, clipping_slider_left)); + const float slider_icon_width = ImGuiPureWrap::get_slider_icon_size().x; + float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; + window_width = std::max(window_width, total_text_max); + window_width = std::max(window_width, button_width); + window_width = std::max(window_width, split_triangles_checkbox_width); + window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); + window_width = std::max(window_width, tool_type_radio_left + tool_type_radio_brush + tool_type_radio_smart_fill); + window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); + + auto draw_text_with_caption = [&caption_max](const std::string &caption, const std::string &text) { + ImGuiPureWrap::text_colored(ImGuiPureWrap::COL_ORANGE_LIGHT, caption); + ImGui::SameLine(caption_max); + ImGuiPureWrap::text(text); + }; + + for (const std::string t : {"add_fuzzy_skin", "remove_fuzzy_skin"}) { + draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); + } + + ImGui::Separator(); + + std::string format_str = std::string("%.f") + I18N::translate_utf8("°", + "Degree sign to use in the respective slider in fuzzy skin gizmo," + "placed after the number with no whitespace in between."); + + const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc["tool_type"]); + + float tool_type_offset = tool_type_radio_left + (window_width - tool_type_radio_left - tool_type_radio_brush - tool_type_radio_smart_fill + m_imgui->scaled(0.5f)) / 2.f; + ImGui::SameLine(tool_type_offset); + ImGui::PushItemWidth(tool_type_radio_brush); + if (ImGuiPureWrap::radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) + m_tool_type = ToolType::BRUSH; + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Paints facets according to the chosen painting brush."), max_tooltip_width); + + ImGui::SameLine(tool_type_offset + tool_type_radio_brush); + ImGui::PushItemWidth(tool_type_radio_smart_fill); + if (ImGuiPureWrap::radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) + m_tool_type = ToolType::SMART_FILL; + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); + + ImGui::Separator(); + + if (m_tool_type == ToolType::BRUSH) { + ImGuiPureWrap::text(m_desc.at("cursor_type")); + ImGui::NewLine(); + + float cursor_type_offset = (window_width - cursor_type_radio_sphere - cursor_type_radio_circle - cursor_type_radio_pointer + m_imgui->scaled(1.5f)) / 2.f; + ImGui::SameLine(cursor_type_offset); + ImGui::PushItemWidth(cursor_type_radio_sphere); + if (ImGuiPureWrap::radio_button(m_desc["sphere"], m_cursor_type == TriangleSelector::CursorType::SPHERE)) + m_cursor_type = TriangleSelector::CursorType::SPHERE; + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Paints all facets inside, regardless of their orientation."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere); + ImGui::PushItemWidth(cursor_type_radio_circle); + + if (ImGuiPureWrap::radio_button(m_desc["circle"], m_cursor_type == TriangleSelector::CursorType::CIRCLE)) + m_cursor_type = TriangleSelector::CursorType::CIRCLE; + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Ignores facets facing away from the camera."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere + cursor_type_radio_circle); + ImGui::PushItemWidth(cursor_type_radio_pointer); + + if (ImGuiPureWrap::radio_button(m_desc["pointer"], m_cursor_type == TriangleSelector::CursorType::POINTER)) + m_cursor_type = TriangleSelector::CursorType::POINTER; + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Paints only one facet."), max_tooltip_width); + + m_imgui->disabled_begin(m_cursor_type != TriangleSelector::CursorType::SPHERE && m_cursor_type != TriangleSelector::CursorType::CIRCLE); + + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("cursor_size")); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true, _L("Alt + Mouse wheel")); + + ImGuiPureWrap::checkbox(m_desc["split_triangles"], m_triangle_splitting_enabled); + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Splits bigger facets into smaller ones while the object is painted."), max_tooltip_width); + + m_imgui->disabled_end(); + } else { + assert(m_tool_type == ToolType::SMART_FILL); + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc["smart_fill_angle"] + ":"); + + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) + for (auto &triangle_selector : m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc.at("clipping_of_view")); + } else { + if (ImGuiPureWrap::button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position_by_ratio(-1., false); }); + } + } + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, from_u8(GUI::shortkey_ctrl_prefix()) + _L("Mouse wheel"))) + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + + ImGui::Separator(); + if (ImGuiPureWrap::button(m_desc.at("remove_all"))) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), UndoRedo::SnapshotType::GizmoAction); + ModelObject *mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume *mv : mo->volumes) + if (mv->is_model_part()) { + ++idx; + m_triangle_selectors[idx]->reset(); + m_triangle_selectors[idx]->request_update_render_data(); + } + + update_model_object(); + m_parent.set_as_dirty(); + } + + ImGuiPureWrap::end(); +} + +void GLGizmoFuzzySkin::update_model_object() const +{ + bool updated = false; + ModelObject *mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + ++idx; + updated |= mv->fuzzy_skin_facets.set(*m_triangle_selectors[idx]); + } + + if (updated) { + const ModelObjectPtrs &mos = wxGetApp().model().objects; + wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); + + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + } +} + +void GLGizmoFuzzySkin::update_from_model_object() +{ + wxBusyCursor wait; + + const ModelObject *mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + int volume_id = -1; + for (const ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + ++volume_id; + + // This mesh does not account for the possible Z up SLA offset. + const TriangleMesh *mesh = &mv->mesh(); + + m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + // Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize(). + m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false); + m_triangle_selectors.back()->request_update_render_data(); + } +} + +PainterGizmoType GLGizmoFuzzySkin::get_painter_type() const +{ + return PainterGizmoType::FUZZY_SKIN; +} + +wxString GLGizmoFuzzySkin::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + return shift_down ? _L("Remove fuzzy skin") : _L("Add fuzzy skin"); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp new file mode 100644 index 0000000..899e75c --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp @@ -0,0 +1,47 @@ +#ifndef slic3r_GLGizmoFuzzySkin_hpp_ +#define slic3r_GLGizmoFuzzySkin_hpp_ + +#include "GLGizmoPainterBase.hpp" + +#include "slic3r/GUI/I18N.hpp" + +namespace Slic3r::GUI { + +class GLGizmoFuzzySkin : public GLGizmoPainterBase +{ +public: + GLGizmoFuzzySkin(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id) : GLGizmoPainterBase(parent, icon_filename, sprite_id) {} + + void render_painter_gizmo() override; + +protected: + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; + + wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + + std::string get_gizmo_entering_text() const override { return _u8L("Entering Paint-on fuzzy skin"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Paint-on fuzzy skin"); } + std::string get_action_snapshot_name() const override { return _u8L("Paint-on fuzzy skin editing"); } + + TriangleStateType get_left_button_state_type() const override { return TriangleStateType::FUZZY_SKIN; } + TriangleStateType get_right_button_state_type() const override { return TriangleStateType::NONE; } + +private: + bool on_init() override; + + void update_model_object() const override; + void update_from_model_object() override; + + void on_opening() override {} + void on_shutdown() override; + PainterGizmoType get_painter_type() const override; + + // This map holds all translated description texts, so they can be easily referenced during layout calculations + // etc. When language changes, GUI is recreated, and this class constructed again, so the change takes effect. + std::map m_desc; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoFuzzySkin_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 661eaf3..34725bf 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -76,6 +76,12 @@ void GLGizmoHollow::data_changed(bool is_serializing) void GLGizmoHollow::on_render() { + if (! selected_print_object_exists(m_parent, wxEmptyString)) { + wxGetApp().CallAfter([this]() { + // Close current gizmo. + m_parent.get_gizmos_manager().open_gizmo(m_parent.get_gizmos_manager().get_current_type()); + }); + } const Selection& selection = m_parent.get_selection(); const CommonGizmosDataObjects::SelectionInfo* sel_info = m_c->selection_info(); @@ -132,7 +138,7 @@ void GLGizmoHollow::render_points(const Selection& selection) if (!inst) return; - double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + double shift_z = m_c->selection_info()->print_object() ? m_c->selection_info()->print_object()->get_current_elevation() : 0.; Transform3d trafo(inst->get_transformation().get_matrix()); trafo.translation()(2) += shift_z; const Geometry::Transformation transformation{trafo}; @@ -844,6 +850,14 @@ void GLGizmoHollow::on_set_state() if (m_state == m_old_state) return; + if (m_state == On) { + // Make sure that current object is on current bed. Refuse to turn on otherwise. + if (! selected_print_object_exists(m_parent, _L("Selected object has to be on the active bed."))) { + m_state = Off; + return; + } + } + if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 1441285..361381e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -105,6 +105,12 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["second_color"] = _u8L("Second color"); m_desc["remove_caption"] = _u8L("Shift + Left mouse button") + ": "; m_desc["remove"] = _u8L("Remove painted color"); + + m_desc["alt_caption"] = _u8L("Alt + Mouse wheel") + ": "; + m_desc["alt_brush"] = _u8L("Change brush size"); + m_desc["alt_fill"] = _u8L("Change angle"); + m_desc["alt_height_range"] = _u8L("Change height range"); + m_desc["remove_all"] = _u8L("Clear all"); m_desc["circle"] = _u8L("Circle"); m_desc["sphere"] = _u8L("Sphere"); @@ -114,10 +120,15 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["tool_brush"] = _u8L("Brush"); m_desc["tool_smart_fill"] = _u8L("Smart fill"); m_desc["tool_bucket_fill"] = _u8L("Bucket fill"); + m_desc["tool_height_range"] = _u8L("Height range"); m_desc["smart_fill_angle"] = _u8L("Smart fill angle"); + m_desc["bucket_fill_angle"] = _u8L("Bucket fill angle"); + m_desc["split_triangles"] = _u8L("Split triangles"); + m_desc["height_range_z_range"] = _u8L("Height range"); + init_extruders_data(); return true; @@ -258,17 +269,19 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (!m_c->selection_info()->model_object()) return; - const float approx_height = m_imgui->scaled(22.0f); + const float approx_height = m_imgui->scaled(25.35f); y = std::min(y, bottom_limit - approx_height); ImGuiPureWrap::set_next_window_pos(x, y, ImGuiCond_Always); ImGuiPureWrap::begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); - // 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 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); +// 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 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 cursor_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); const float smart_fill_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); + const float bucket_fill_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("bucket_fill_angle")).x + m_imgui->scaled(1.f); + const float height_range_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("height_range_z_range")).x + m_imgui->scaled(1.f); const float cursor_type_radio_circle = ImGuiPureWrap::calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); const float cursor_type_radio_sphere = ImGuiPureWrap::calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); @@ -281,30 +294,39 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float combo_label_width = std::max(ImGuiPureWrap::calc_text_size(m_desc.at("first_color")).x, ImGuiPureWrap::calc_text_size(m_desc.at("second_color")).x) + m_imgui->scaled(1.f); - const float tool_type_radio_brush = ImGuiPureWrap::calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); - const float tool_type_radio_bucket_fill = ImGuiPureWrap::calc_text_size(m_desc["tool_bucket_fill"]).x + m_imgui->scaled(2.5f); - const float tool_type_radio_smart_fill = ImGuiPureWrap::calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_brush = ImGuiPureWrap::calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_bucket_fill = ImGuiPureWrap::calc_text_size(m_desc["tool_bucket_fill"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_smart_fill = ImGuiPureWrap::calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_height_range = ImGuiPureWrap::calc_text_size(m_desc["tool_height_range"]).x + m_imgui->scaled(2.5f); + + const float tool_type_radio_first_line = tool_type_radio_brush + tool_type_radio_bucket_fill + tool_type_radio_smart_fill; + const float tool_type_radio_second_line = tool_type_radio_height_range; + const float tool_type_radio_max_width = std::max(tool_type_radio_first_line, tool_type_radio_second_line); const float split_triangles_checkbox_width = ImGuiPureWrap::calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); - float caption_max = 0.f; + float caption_max = 0.f; + for (const std::string t : {"first_color", "second_color", "remove", "alt"}) { + caption_max = std::max(caption_max, ImGuiPureWrap::calc_text_size(m_desc[t + "_caption"]).x); + } + float total_text_max = 0.f; - for (const auto &t : std::array{"first_color", "second_color", "remove"}) { - caption_max = std::max(caption_max, ImGuiPureWrap::calc_text_size(m_desc[t + "_caption"]).x); + for (const std::string t : {"first_color", "second_color", "remove", "alt_brush", "alt_fill", "alt_height_range"}) { total_text_max = std::max(total_text_max, ImGuiPureWrap::calc_text_size(m_desc[t]).x); } + total_text_max += caption_max + m_imgui->scaled(1.f); caption_max += m_imgui->scaled(1.f); - const float sliders_left_width = std::max(smart_fill_slider_left, std::max(cursor_slider_left, clipping_slider_left)); + const float sliders_left_width = std::max({smart_fill_slider_left, bucket_fill_slider_left, cursor_slider_left, clipping_slider_left, height_range_slider_left}); const float slider_icon_width = ImGuiPureWrap::get_slider_icon_size().x; float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; - window_width = std::max(window_width, total_text_max); - window_width = std::max(window_width, button_width); - window_width = std::max(window_width, split_triangles_checkbox_width); - window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); - window_width = std::max(window_width, tool_type_radio_brush + tool_type_radio_bucket_fill + tool_type_radio_smart_fill); - window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); + window_width = std::max(window_width, total_text_max); + window_width = std::max(window_width, button_width); + window_width = std::max(window_width, split_triangles_checkbox_width); + window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); + window_width = std::max(window_width, tool_type_radio_max_width); + window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); auto draw_text_with_caption = [&caption_max](const std::string &caption, const std::string& text) { ImGuiPureWrap::text_colored(ImGuiPureWrap::COL_BLUE_LIGHT, caption); @@ -312,8 +334,14 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGuiPureWrap::text(text); }; - for (const auto &t : std::array{"first_color", "second_color", "remove"}) + for (const std::string t : {"first_color", "second_color", "remove"}) { draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); + } + + std::string alt_hint_text = (m_tool_type == ToolType::BRUSH) ? "alt_brush" : + (m_tool_type == ToolType::HEIGHT_RANGE) ? "alt_height_range" + : "alt_fill"; + draw_text_with_caption(m_desc.at("alt_caption"), m_desc.at(alt_hint_text)); ImGui::Separator(); @@ -355,8 +383,8 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGuiPureWrap::text(m_desc.at("tool_type")); ImGui::NewLine(); - float tool_type_offset = (window_width - tool_type_radio_brush - tool_type_radio_bucket_fill - tool_type_radio_smart_fill + m_imgui->scaled(1.5f)) / 2.f; - ImGui::SameLine(tool_type_offset); + const float tool_type_first_line_offset = (window_width - tool_type_radio_first_line + m_imgui->scaled(1.5f)) / 2.f; + ImGui::SameLine(tool_type_first_line_offset); ImGui::PushItemWidth(tool_type_radio_brush); if (ImGuiPureWrap::radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) { m_tool_type = ToolType::BRUSH; @@ -369,7 +397,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (ImGui::IsItemHovered()) ImGuiPureWrap::tooltip(_u8L("Paints facets according to the chosen painting brush."), max_tooltip_width); - ImGui::SameLine(tool_type_offset + tool_type_radio_brush); + ImGui::SameLine(tool_type_first_line_offset + tool_type_radio_brush); ImGui::PushItemWidth(tool_type_radio_smart_fill); if (ImGuiPureWrap::radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) { m_tool_type = ToolType::SMART_FILL; @@ -382,7 +410,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (ImGui::IsItemHovered()) ImGuiPureWrap::tooltip(_u8L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); - ImGui::SameLine(tool_type_offset + tool_type_radio_brush + tool_type_radio_smart_fill); + ImGui::SameLine(tool_type_first_line_offset + tool_type_radio_brush + tool_type_radio_smart_fill); ImGui::PushItemWidth(tool_type_radio_bucket_fill); if (ImGuiPureWrap::radio_button(m_desc["tool_bucket_fill"], m_tool_type == ToolType::BUCKET_FILL)) { m_tool_type = ToolType::BUCKET_FILL; @@ -395,9 +423,25 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (ImGui::IsItemHovered()) ImGuiPureWrap::tooltip(_u8L("Paints neighboring facets that have the same color."), max_tooltip_width); + ImGui::NewLine(); + + const float tool_type_second_line_offset = (window_width - tool_type_radio_second_line + m_imgui->scaled(1.5f)) / 2.f; + ImGui::SameLine(tool_type_second_line_offset); + ImGui::PushItemWidth(tool_type_radio_height_range); + if (ImGuiPureWrap::radio_button(m_desc["tool_height_range"], m_tool_type == ToolType::HEIGHT_RANGE)) { + m_tool_type = ToolType::HEIGHT_RANGE; + for (auto &triangle_selector : m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } + + if (ImGui::IsItemHovered()) + ImGuiPureWrap::tooltip(_u8L("Paints facets within the chosen height range."), max_tooltip_width); + ImGui::Separator(); - if(m_tool_type == ToolType::BRUSH) { + if (m_tool_type == ToolType::BRUSH) { ImGuiPureWrap::text(m_desc.at("cursor_type")); ImGui::NewLine(); @@ -444,18 +488,35 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->disabled_end(); ImGui::Separator(); - } else if(m_tool_type == ToolType::SMART_FILL) { + } else if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL) { ImGui::AlignTextToFramePadding(); - ImGuiPureWrap::text(m_desc["smart_fill_angle"] + ":"); - std::string format_str = std::string("%.f") + I18N::translate_utf8("°", "Degree sign to use in the respective slider in MMU gizmo," - "placed after the number with no whitespace in between."); + ImGuiPureWrap::text((m_tool_type == ToolType::SMART_FILL ? m_desc["smart_fill_angle"] : m_desc["bucket_fill_angle"]) + ":"); + std::string format_str_angle = std::string("%.f") + I18N::translate_utf8("°", "Degree sign to use in the respective slider in MMU gizmo," + "placed after the number with no whitespace in between."); ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { + float &fill_angle = (m_tool_type == ToolType::SMART_FILL) ? m_smart_fill_angle : m_bucket_fill_angle; + if (m_imgui->slider_float("##fill_angle", &fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str_angle.data(), 1.0f, true, _L("Alt + Mouse wheel"))) { + for (auto &triangle_selector: m_triangle_selectors) { triangle_selector->seed_fill_unselect_all_triangles(); triangle_selector->request_update_render_data(); } + } + + ImGui::Separator(); + } else if (m_tool_type == ToolType::HEIGHT_RANGE) { + ImGui::AlignTextToFramePadding(); + ImGuiPureWrap::text(m_desc["height_range_z_range"] + ":"); + std::string format_str_angle = std::string("%.2f ") + I18N::translate_utf8("mm", "Millimeter sign to use in the respective slider in multi-material painting gizmo," + "placed after the number with space in between."); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##height_range_z_range", &m_height_range_z_range, HeightRangeZRangeMin, HeightRangeZRangeMax, format_str_angle.data(), 1.0f, true, _L("Alt + Mouse wheel"))) { + for (auto &triangle_selector: m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } ImGui::Separator(); } @@ -504,7 +565,7 @@ void GLGizmoMmuSegmentation::update_model_object() const if (! mv->is_model_part()) continue; ++idx; - updated |= mv->mm_segmentation_facets.set(*m_triangle_selectors[idx].get()); + updated |= mv->mm_segmentation_facets.set(*m_triangle_selectors[idx]); } if (updated) { @@ -555,7 +616,7 @@ void GLGizmoMmuSegmentation::update_from_model_object() PainterGizmoType GLGizmoMmuSegmentation::get_painter_type() const { - return PainterGizmoType::MMU_SEGMENTATION; + return PainterGizmoType::MM_SEGMENTATION; } ColorRGBA GLGizmoMmuSegmentation::get_cursor_sphere_left_button_color() const @@ -694,7 +755,7 @@ void GLMmSegmentationGizmo3DScene::render(size_t triangle_indices_idx) const glsafe(::glBindBuffer(GL_ARRAY_BUFFER, this->vertices_VBO_id)); const GLint position_id = shader->get_attrib_location("v_position"); if (position_id != -1) { - glsafe(::glVertexAttribPointer(position_id, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (GLvoid*)0)); + glsafe(::glVertexAttribPointer(position_id, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (GLvoid*)nullptr)); glsafe(::glEnableVertexAttribArray(position_id)); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index d520ea2..340cc75 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -100,7 +100,7 @@ public: // will be also extended to support additional states, requiring at least one state to remain free out of 19 states. static const constexpr size_t EXTRUDERS_LIMIT = 16; - const float get_cursor_radius_min() const override { return CursorRadiusMin; } + float get_cursor_radius_min() const override { return CursorRadiusMin; } protected: ColorRGBA get_cursor_sphere_left_button_color() const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 22c23a3..b9967cf 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -14,7 +14,9 @@ #include "libslic3r/TriangleMesh.hpp" #include +#include #include +#include namespace Slic3r::GUI { @@ -135,10 +137,12 @@ void GLGizmoPainterBase::render_cursor() return; if (m_tool_type == ToolType::BRUSH) { - if (m_cursor_type == TriangleSelector::SPHERE) + if (m_cursor_type == TriangleSelector::CursorType::SPHERE) render_cursor_sphere(trafo_matrices[m_rr.mesh_id]); - else if (m_cursor_type == TriangleSelector::CIRCLE) + else if (m_cursor_type == TriangleSelector::CursorType::CIRCLE) render_cursor_circle(); + } else if (m_tool_type == ToolType::HEIGHT_RANGE) { + render_cursor_height_range(trafo_matrices[m_rr.mesh_id]); } } @@ -266,7 +270,6 @@ void GLGizmoPainterBase::render_cursor_circle() glsafe(::glEnable(GL_DEPTH_TEST)); } - void GLGizmoPainterBase::render_cursor_sphere(const Transform3d& trafo) const { if (s_sphere == nullptr) { @@ -310,6 +313,78 @@ void GLGizmoPainterBase::render_cursor_sphere(const Transform3d& trafo) const shader->stop_using(); } +void GLGizmoPainterBase::render_cursor_height_range(const Transform3d &trafo) const { + const ModelObject &model_object = *m_c->selection_info()->model_object(); + const Vec3f mesh_hit_world = (trafo * m_rr.hit.cast()).cast(); + + const std::array z_range = {mesh_hit_world.z() - m_height_range_z_range / 2.f, + mesh_hit_world.z() + m_height_range_z_range / 2.f}; + + struct SlicedPolygonsAtZ + { + float z; + Polygons polygons; + }; + + std::vector sliced_polygons_per_z; + for (const float z: z_range) { + sliced_polygons_per_z.push_back({z, slice_mesh(model_object.volumes[m_rr.mesh_id]->mesh().its, z, MeshSlicingParams(trafo))}); + } + + const size_t max_vertices_cnt = std::accumulate(sliced_polygons_per_z.cbegin(), sliced_polygons_per_z.cend(), 0, + [](const size_t sum, const SlicedPolygonsAtZ &polygons_at_z) { + return sum + count_points(polygons_at_z.polygons); + }); + + GLModel::Geometry z_range_geometry; + z_range_geometry.format = {GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3}; + z_range_geometry.reserve_vertices(max_vertices_cnt); + z_range_geometry.reserve_indices(max_vertices_cnt); + z_range_geometry.color = ColorRGBA::WHITE(); + + size_t vertices_cnt = 0; + for (const SlicedPolygonsAtZ &polygons_at_z : sliced_polygons_per_z) { + for (const Polygon &polygon: polygons_at_z.polygons) { + for (const Point &pt: polygon.points) + z_range_geometry.add_vertex(Vec3f(unscaled(pt.x()), unscaled(pt.y()), polygons_at_z.z)); + + for (size_t pt_idx = 1; pt_idx < polygon.points.size(); ++pt_idx) + z_range_geometry.add_line(vertices_cnt + pt_idx - 1, vertices_cnt + pt_idx); + + z_range_geometry.add_line(vertices_cnt + polygon.points.size() - 1, vertices_cnt); + + vertices_cnt += polygon.points.size(); + } + } + + GLModel z_range_model; + if (!z_range_geometry.is_empty()) + z_range_model.init_from(std::move(z_range_geometry)); + + const Camera &camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix(); + + GLShaderProgram *shader = wxGetApp().get_shader("mm_contour"); + if (shader == nullptr) + return; + + shader->start_using(); + shader->set_uniform("offset", OpenGLManager::get_gl_info().is_mesa() ? 0.0005 : 0.00001); + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + ScopeGuard guard([shader]() { if (shader) shader->stop_using(); }); + + const bool is_left_handed = Geometry::Transformation(view_model_matrix).is_left_handed(); + if (is_left_handed) + glsafe(::glFrontFace(GL_CW)); + + z_range_model.render(); + + if (is_left_handed) + glsafe(::glFrontFace(GL_CCW)); + + shader->stop_using(); +} bool GLGizmoPainterBase::is_mesh_point_clipped(const Vec3d& point, const Transform3d& trafo) const { @@ -449,38 +524,54 @@ std::vector> GLGizmoPain // concludes that the event was not intended for it, it should return false. bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) { - if (action == SLAGizmoEventType::MouseWheelUp - || action == SLAGizmoEventType::MouseWheelDown) { - if (control_down) { + if (action == SLAGizmoEventType::MouseWheelUp || action == SLAGizmoEventType::MouseWheelDown) { + // On Windows Right ALT could be reported as Left ALT + Control. + // In such cases, we want to prioritize ALT over Control. + if (!alt_down && control_down) { double pos = m_c->object_clipper()->get_position(); pos = action == SLAGizmoEventType::MouseWheelDown ? std::max(0., pos - 0.01) : std::min(1., pos + 0.01); m_c->object_clipper()->set_position_by_ratio(pos, true); return true; - } - else if (alt_down) { + } else if (alt_down) { if (m_tool_type == ToolType::BRUSH && (m_cursor_type == TriangleSelector::CursorType::SPHERE || m_cursor_type == TriangleSelector::CursorType::CIRCLE)) { m_cursor_radius = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_cursor_radius - this->get_cursor_radius_step(), this->get_cursor_radius_min()) : std::min(m_cursor_radius + this->get_cursor_radius_step(), this->get_cursor_radius_max()); m_parent.set_as_dirty(); return true; - } else if (m_tool_type == ToolType::SMART_FILL) { - m_smart_fill_angle = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_smart_fill_angle - SmartFillAngleStep, SmartFillAngleMin) - : std::min(m_smart_fill_angle + SmartFillAngleStep, SmartFillAngleMax); + } else if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL) { + float &fill_angle = (m_tool_type == ToolType::SMART_FILL) ? m_smart_fill_angle : m_bucket_fill_angle; + fill_angle = (action == SLAGizmoEventType::MouseWheelDown) ? std::max(fill_angle - SmartFillAngleStep, SmartFillAngleMin) + : std::min(fill_angle + SmartFillAngleStep, SmartFillAngleMax); + m_parent.set_as_dirty(); if (m_rr.mesh_id != -1) { - const Selection &selection = m_parent.get_selection(); - const ModelObject *mo = m_c->selection_info()->model_object(); - const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; - const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); - const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); + const Selection &selection = m_parent.get_selection(); + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; + const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); + const TriangleSelector::ClippingPlane &clipping_plane = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); + + if (m_tool_type == ToolType::SMART_FILL) { + const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clipping_plane, m_smart_fill_angle, SmartFillGapArea, + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, TriangleSelector::ForceReselection::YES); + } else { + assert(m_tool_type == ToolType::BUCKET_FILL); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clipping_plane, m_bucket_fill_angle, BucketFillGapArea, + TriangleSelector::BucketFillPropagate::YES, TriangleSelector::ForceReselection::YES); + } + m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } return true; + } else if (m_tool_type == ToolType::HEIGHT_RANGE) { + m_height_range_z_range = action == SLAGizmoEventType::MouseWheelDown ? std::max(m_height_range_z_range - HeightRangeZRangeStep, HeightRangeZRangeMin) + : std::min(m_height_range_z_range + HeightRangeZRangeStep, HeightRangeZRangeMax); + m_parent.set_as_dirty(); + return true; } return false; @@ -552,18 +643,21 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous assert(mesh_idx < int(m_triangle_selectors.size())); const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL || (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)) { - for(const ProjectedMousePosition &projected_mouse_position : projected_mouse_positions) { + for (const ProjectedMousePosition &projected_mouse_position: projected_mouse_positions) { assert(projected_mouse_position.mesh_idx == mesh_idx); - const Vec3f mesh_hit = projected_mouse_position.mesh_hit; - const int facet_idx = int(projected_mouse_position.facet_idx); + const Vec3f mesh_hit = projected_mouse_position.mesh_hit; + const int facet_idx = int(projected_mouse_position.facet_idx); + m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); - if (m_tool_type == ToolType::SMART_FILL) - m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); - else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) - m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, false, true); - else if (m_tool_type == ToolType::BUCKET_FILL) - m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, true, true); + + if (m_tool_type == ToolType::SMART_FILL) { + m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, SmartFillGapArea, + (m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f), TriangleSelector::ForceReselection::YES); + } else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) { + m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, m_bucket_fill_angle, BucketFillGapArea, TriangleSelector::BucketFillPropagate::NO, TriangleSelector::ForceReselection::YES); + } else if (m_tool_type == ToolType::BUCKET_FILL) { + m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, m_bucket_fill_angle, BucketFillGapArea, TriangleSelector::BucketFillPropagate::YES, TriangleSelector::ForceReselection::YES); + } m_seed_fill_last_mesh_id = -1; } @@ -584,6 +678,16 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->select_patch(int(first_position_it->facet_idx), std::move(cursor), new_state, trafo_matrix_not_translate, m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); } } + } else if (m_tool_type == ToolType::HEIGHT_RANGE) { + for (const ProjectedMousePosition &projected_mouse_position: projected_mouse_positions) { + const Vec3f &mesh_hit = projected_mouse_position.mesh_hit; + const int facet_idx = int(projected_mouse_position.facet_idx); + const BoundingBoxf3 mesh_bbox = mo->volumes[projected_mouse_position.mesh_idx]->mesh().bounding_box(); + + std::unique_ptr cursor = std::make_unique(mesh_hit, mesh_bbox, m_height_range_z_range, trafo_matrix, clp); + m_triangle_selectors[mesh_idx]->select_patch(facet_idx, std::move(cursor), new_state, trafo_matrix_not_translate, + m_triangle_splitting_enabled, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); + } } m_triangle_selectors[mesh_idx]->request_update_render_data(); @@ -642,12 +746,12 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous assert(m_rr.mesh_id < int(m_triangle_selectors.size())); const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) - m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, SmartFillGapArea, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) - m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, false); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, m_bucket_fill_angle, BucketFillGapArea, TriangleSelector::BucketFillPropagate::NO); else if (m_tool_type == ToolType::BUCKET_FILL) - m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, true); + m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, m_bucket_fill_angle, BucketFillGapArea, TriangleSelector::BucketFillPropagate::YES); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; return true; @@ -756,13 +860,9 @@ void GLGizmoPainterBase::update_raycast_cache(const Vec2d& mouse_position, for (int mesh_id = 0; mesh_id < int(trafo_matrices.size()); ++mesh_id) { if (m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh( - mouse_position, - trafo_matrices[mesh_id], - camera, - hit, - normal, - m_c->object_clipper()->get_clipping_plane(), - &facet)) + mouse_position, trafo_matrices[mesh_id], camera, hit, normal, + m_c->object_clipper()->get_clipping_plane(), &facet, false + )) { // In case this hit is clipped, skip it. if (is_mesh_point_clipped(hit.cast(), trafo_matrices[mesh_id])) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 2698153..b46f378 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -27,7 +27,8 @@ class Selection; enum class PainterGizmoType { FDM_SUPPORTS, SEAM, - MMU_SEGMENTATION + MM_SEGMENTATION, + FUZZY_SKIN }; class TriangleSelectorGUI : public TriangleSelector { @@ -92,9 +93,9 @@ public: // after all volumes (including transparent ones) are rendered. virtual void render_painter_gizmo() = 0; - virtual const float get_cursor_radius_min() const { return CursorRadiusMin; } - virtual const float get_cursor_radius_max() const { return CursorRadiusMax; } - virtual const float get_cursor_radius_step() const { return CursorRadiusStep; } + virtual float get_cursor_radius_min() const { return CursorRadiusMin; } + virtual float get_cursor_radius_max() const { return CursorRadiusMax; } + virtual float get_cursor_radius_step() const { return CursorRadiusStep; } /// /// Implement when want to process mouse events in gizmo @@ -107,9 +108,12 @@ public: protected: virtual void render_triangles(const Selection& selection) const; + void render_cursor(); void render_cursor_circle(); - void render_cursor_sphere(const Transform3d& trafo) const; + void render_cursor_sphere(const Transform3d &trafo) const; + void render_cursor_height_range(const Transform3d &trafo) const; + virtual void update_model_object() const = 0; virtual void update_from_model_object() = 0; @@ -127,12 +131,13 @@ protected: // For each model-part volume, store status and division of the triangles. std::vector> m_triangle_selectors; - TriangleSelector::CursorType m_cursor_type = TriangleSelector::SPHERE; + TriangleSelector::CursorType m_cursor_type = TriangleSelector::CursorType::SPHERE; enum class ToolType { BRUSH, BUCKET_FILL, - SMART_FILL + SMART_FILL, + HEIGHT_RANGE }; struct ProjectedMousePosition @@ -145,6 +150,8 @@ protected: bool m_triangle_splitting_enabled = true; ToolType m_tool_type = ToolType::BRUSH; float m_smart_fill_angle = 30.f; + float m_bucket_fill_angle = 90.f; + float m_height_range_z_range = 1.00f; bool m_paint_on_overhangs_only = false; float m_highlight_by_angle_threshold_deg = 0.f; @@ -157,6 +164,13 @@ protected: static constexpr float SmartFillAngleMax = 90.f; static constexpr float SmartFillAngleStep = 1.f; + static constexpr float HeightRangeZRangeMin = 0.1f; + static constexpr float HeightRangeZRangeMax = 10.f; + static constexpr float HeightRangeZRangeStep = 0.1f; + + static constexpr float SmartFillGapArea = 0.02f; + static constexpr float BucketFillGapArea = 0.02f; + // It stores the value of the previous mesh_id to which the seed fill was applied. // It is used to detect when the mouse has moved from one volume to another one. int m_seed_fill_last_mesh_id = -1; @@ -187,7 +201,6 @@ private: static std::shared_ptr s_sphere; - bool m_internal_stack_active = false; bool m_schedule_update = false; Vec2d m_last_mouse_click = Vec2d::Zero(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 63f4beb..9c0080f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -75,7 +75,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) if (! m_c->selection_info()->model_object()) return; - const float approx_height = m_imgui->scaled(12.5f); + const float approx_height = m_imgui->scaled(13.45f); y = std::min(y, bottom_limit - approx_height); ImGuiPureWrap::set_next_window_pos(x, y, ImGuiCond_Always); ImGuiPureWrap::begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); @@ -197,7 +197,7 @@ void GLGizmoSeam::update_model_object() const if (! mv->is_model_part()) continue; ++idx; - updated |= mv->seam_facets.set(*m_triangle_selectors[idx].get()); + updated |= mv->seam_facets.set(*m_triangle_selectors[idx]); } if (updated) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp index 8451679..4fc5ce6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp @@ -5,6 +5,10 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/MainFrame.hpp" + +#include "libslic3r/MultipleBeds.hpp" namespace Slic3r { namespace GUI { @@ -17,6 +21,22 @@ GLGizmoSlaBase::GLGizmoSlaBase(GLCanvas3D& parent, const std::string& icon_filen , m_min_sla_print_object_step((int)min_step) {} +/*static*/ bool GLGizmoSlaBase::selected_print_object_exists(const GLCanvas3D& canvas, const wxString& text) +{ + if (const Selection& sel = canvas.get_selection(); !sel.is_single_full_instance() || !sel.get_model()->objects[sel.get_object_idx()] + || ! canvas.sla_print()->get_print_object_by_model_object_id(sel.get_model()->objects[sel.get_object_idx()]->id())) + { + if (! text.IsEmpty()) + wxGetApp().CallAfter([text]() { + MessageDialog dlg(GUI::wxGetApp().mainframe, text, + _L("Bed selection mismatch"), wxICON_INFORMATION | wxOK); + dlg.ShowModal(); + }); + return false; + } + return true; +} + void GLGizmoSlaBase::reslice_until_step(SLAPrintObjectStep step, bool postpone_error_messages) { wxGetApp().CallAfter([this, step, postpone_error_messages]() { @@ -70,6 +90,7 @@ void GLGizmoSlaBase::update_volumes() const int object_idx = m_parent.get_selection().get_object_idx(); const int instance_idx = m_parent.get_selection().get_instance_idx(); + const Geometry::Transformation& inst_trafo = po->model_object()->instances[instance_idx]->get_transformation(); const double current_elevation = po->get_current_elevation(); @@ -90,12 +111,14 @@ void GLGizmoSlaBase::update_volumes() const Transform3d po_trafo_inverse = po->trafo().inverse(); // main mesh + backend_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); backend_mesh.transform(po_trafo_inverse); add_volume(backend_mesh, 0, true); // supports mesh TriangleMesh supports_mesh = po->support_mesh(); if (!supports_mesh.empty()) { + supports_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); supports_mesh.transform(po_trafo_inverse); add_volume(supports_mesh, -int(slaposSupportTree)); } @@ -103,6 +126,7 @@ void GLGizmoSlaBase::update_volumes() // pad mesh TriangleMesh pad_mesh = po->pad_mesh(); if (!pad_mesh.empty()) { + pad_mesh.translate(s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed()).cast()); pad_mesh.transform(po_trafo_inverse); add_volume(pad_mesh, -int(slaposPad)); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp index acda5a8..d298632 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp @@ -45,6 +45,8 @@ protected: const GLVolumeCollection &volumes() const { return m_volumes; } + static bool selected_print_object_exists(const GLCanvas3D& canvas, const wxString& text); + private: GLVolumeCollection m_volumes; bool m_input_enabled{ false }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index e6e70eb..36621d6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -98,6 +98,13 @@ void GLGizmoSlaSupports::data_changed(bool is_serializing) void GLGizmoSlaSupports::on_render() { + if (! selected_print_object_exists(m_parent, wxEmptyString)) { + wxGetApp().CallAfter([this]() { + // Close current gizmo. + m_parent.get_gizmos_manager().open_gizmo(m_parent.get_gizmos_manager().get_current_type()); + }); + } + if (m_state == On) { // This gizmo is showing the object elevated. Tell the common // SelectionInfo object to lie about the actual shift. @@ -838,6 +845,13 @@ bool GLGizmoSlaSupports::ask_about_changes(std::function on_yes, std::fu void GLGizmoSlaSupports::on_set_state() { if (m_state == On) { // the gizmo was just turned on + + // Make sure that current object is on current bed. Refuse to turn on otherwise. + if (! selected_print_object_exists(m_parent, _L("Selected object has to be on the active bed."))) { + m_state = Off; + return; + } + // Set default head diameter from config. const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; m_new_point_head_diameter = static_cast(cfg.option("support_head_front_diameter"))->value; diff --git a/src/slic3r/GUI/Gizmos/GLGizmos.hpp b/src/slic3r/GUI/Gizmos/GLGizmos.hpp index 0252dee..c7315c0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmos.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmos.hpp @@ -32,6 +32,7 @@ enum class SLAGizmoEventType : unsigned char { #include "slic3r/GUI/Gizmos/GLGizmoFlatten.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp" #include "slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoCut.hpp" #include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 4926043..567b534 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -16,6 +16,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoFlatten.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp" #include "slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp" #include "slic3r/GUI/Gizmos/GLGizmoCut.hpp" #include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" @@ -108,8 +109,9 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 6)); m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "fdm_supports.svg", 7)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8)); - m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9)); - m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 10)); + m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, "fuzzy_skin_painting.svg", 9)); + m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 10)); + m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 11)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent)); m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoSimplify(m_parent)); @@ -289,12 +291,14 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[FdmSupports].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Seam) return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); - else if (m_current == MmuSegmentation) - return dynamic_cast(m_gizmos[MmuSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == MmSegmentation) + return dynamic_cast(m_gizmos[MmSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Measure) return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Cut) return dynamic_cast(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == FuzzySkin) + return dynamic_cast(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else return false; } @@ -362,7 +366,7 @@ bool GLGizmosManager::on_mouse_wheel(const wxMouseEvent &evt) { bool processed = false; - if (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam || m_current == MmuSegmentation) { + if (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam || m_current == MmSegmentation || m_current == FuzzySkin) { float rot = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta(); if (gizmo_event((rot > 0.f ? SLAGizmoEventType::MouseWheelUp : SLAGizmoEventType::MouseWheelDown), Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) processed = true; @@ -535,7 +539,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case 'r' : case 'R' : { - if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam || m_current == MmuSegmentation) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam || m_current == MmSegmentation || m_current == FuzzySkin) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) processed = true; break; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index c6285ee..a5f263a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -76,7 +76,8 @@ public: SlaSupports, FdmSupports, Seam, - MmuSegmentation, + FuzzySkin, + MmSegmentation, Measure, Emboss, Svg, diff --git a/src/slic3r/GUI/ImGuiDoubleSlider.cpp b/src/slic3r/GUI/ImGuiDoubleSlider.cpp index afc309b..e443697 100644 --- a/src/slic3r/GUI/ImGuiDoubleSlider.cpp +++ b/src/slic3r/GUI/ImGuiDoubleSlider.cpp @@ -600,7 +600,6 @@ bool ImGuiControl::draw_slider( int* higher_pos, int* lower_pos, const ImRect& slideable_region = m_selection == ssHigher ? m_regions.higher_slideable_region : m_regions.lower_slideable_region; const ImRect& active_thumb = m_selection == ssHigher ? m_regions.higher_thumb : m_regions.lower_thumb; - bool show_move_label = false; ImRect mouse_pos_rc = active_thumb; std::string move_label = ""; @@ -613,7 +612,6 @@ bool ImGuiControl::draw_slider( int* higher_pos, int* lower_pos, sl_region.Max.x += m_draw_opts.dummy_sz().x; behavior(id, sl_region, m_min_pos, m_max_pos, &m_mouse_pos, &mouse_pos_rc, m_flags, true); - show_move_label = true; move_label = get_label_on_move(m_mouse_pos); } diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 409534c..f3a4cb6 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -52,6 +52,9 @@ bool s_font_cjk; // a fallback glyph for c. void imgui_rendered_fallback_glyph(ImWchar c) { + if (c == 0) + return; + if (ImGui::GetIO().Fonts->Fonts[0] == ImGui::GetFont()) { // Only do this when we are using the default ImGui font. Otherwise this would conflict with // EmbossStyleManager's font handling and we would load glyphs needlessly. @@ -104,6 +107,9 @@ static const std::map font_icons = { {ImGui::SnapMarker , "snap" }, {ImGui::HorizontalHide , "horizontal_hide" }, {ImGui::HorizontalShow , "horizontal_show" }, + {ImGui::PrintIdle , "print_idle" }, + {ImGui::PrintRunning , "print_running" }, + {ImGui::PrintFinished , "print_finished" }, }; static const std::map font_icons_large = { @@ -168,6 +174,8 @@ static const std::map font_icons_medium = { static const std::map font_icons_extra_large = { {ImGui::ClippyMarker , "notification_clippy" }, + {ImGui::SliceAllBtnIcon , "slice_all" }, + {ImGui::WarningMarkerDisabled , "notification_warning_grey" }, }; ImGuiWrapper::ImGuiWrapper() diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp deleted file mode 100644 index 7609a94..0000000 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ /dev/null @@ -1,455 +0,0 @@ -#include "ArrangeJob.hpp" - -#include "libslic3r/BuildVolume.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/Print.hpp" -#include "libslic3r/SLAPrint.hpp" -#include "libslic3r/Geometry/ConvexHull.hpp" - -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#include "slic3r/GUI/NotificationManager.hpp" -#include "slic3r/GUI/format.hpp" - - -#include "libnest2d/common.hpp" - -#include -#include - -namespace Slic3r { namespace GUI { - -// Cache the wti info -class WipeTower: public GLCanvas3D::WipeTowerInfo { - using ArrangePolygon = arrangement::ArrangePolygon; -public: - explicit WipeTower(const GLCanvas3D::WipeTowerInfo &wti) - : GLCanvas3D::WipeTowerInfo(wti) - {} - - explicit WipeTower(GLCanvas3D::WipeTowerInfo &&wti) - : GLCanvas3D::WipeTowerInfo(std::move(wti)) - {} - - void apply_arrange_result(const Vec2d& tr, double rotation) - { - m_pos = unscaled(tr); m_rotation = rotation; - apply_wipe_tower(); - } - - ArrangePolygon get_arrange_polygon() const - { - Polygon ap({ - {scaled(m_bb.min)}, - {scaled(m_bb.max.x()), scaled(m_bb.min.y())}, - {scaled(m_bb.max)}, - {scaled(m_bb.min.x()), scaled(m_bb.max.y())} - }); - - ArrangePolygon ret; - ret.poly.contour = std::move(ap); - ret.translation = scaled(m_pos); - ret.rotation = m_rotation; - ++ret.priority; - - return ret; - } -}; - -static WipeTower get_wipe_tower(const Plater &plater) -{ - return WipeTower{plater.canvas3D()->get_wipe_tower_info()}; -} - -void ArrangeJob::clear_input() -{ - const Model &model = m_plater->model(); - - size_t count = 0, cunprint = 0; // To know how much space to reserve - for (auto obj : model.objects) - for (auto mi : obj->instances) - mi->printable ? count++ : cunprint++; - - m_selected.clear(); - m_unselected.clear(); - m_unprintable.clear(); - m_unarranged.clear(); - m_selected.reserve(count + 1 /* for optional wti */); - m_unselected.reserve(count + 1 /* for optional wti */); - m_unprintable.reserve(cunprint /* for optional wti */); -} - -void ArrangeJob::prepare_all() { - clear_input(); - - for (ModelObject *obj: m_plater->model().objects) - for (ModelInstance *mi : obj->instances) { - ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable; - cont.emplace_back(get_arrange_poly_(mi)); - } - - if (auto wti = get_wipe_tower_arrangepoly(*m_plater)) - m_selected.emplace_back(std::move(*wti)); -} - -void ArrangeJob::prepare_selected() { - clear_input(); - - Model &model = m_plater->model(); - - std::vector - obj_sel(model.objects.size(), nullptr); - - for (auto &s : m_plater->get_selection().get_content()) - if (s.first < int(obj_sel.size())) - obj_sel[size_t(s.first)] = &s.second; - - // Go through the objects and check if inside the selection - for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { - const Selection::InstanceIdxsList * instlist = obj_sel[oidx]; - ModelObject *mo = model.objects[oidx]; - - std::vector inst_sel(mo->instances.size(), false); - - if (instlist) - for (auto inst_id : *instlist) - inst_sel[size_t(inst_id)] = true; - - for (size_t i = 0; i < inst_sel.size(); ++i) { - ModelInstance * mi = mo->instances[i]; - ArrangePolygon &&ap = get_arrange_poly_(mi); - - ArrangePolygons &cont = mo->instances[i]->printable ? - (inst_sel[i] ? m_selected : - m_unselected) : - m_unprintable; - - cont.emplace_back(std::move(ap)); - } - } - - if (auto wti = get_wipe_tower(*m_plater)) { - ArrangePolygon &&ap = get_arrange_poly(wti, m_plater); - - auto &cont = m_plater->get_selection().is_wipe_tower() ? m_selected : - m_unselected; - cont.emplace_back(std::move(ap)); - } - - // If the selection was empty arrange everything - if (m_selected.empty()) - m_selected.swap(m_unselected); -} - -static void update_arrangepoly_slaprint(arrangement::ArrangePolygon &ret, - const SLAPrintObject &po, - const ModelInstance &inst) -{ - // The 1.1 multiplier is a safety gap, as the offset might be bigger - // in sharp edges of a polygon, depending on clipper's offset algorithm - coord_t pad_infl = 0; - { - double infl = po.config().pad_enable.getBool() * ( - po.config().pad_brim_size.getFloat() + - po.config().pad_around_object.getBool() * - po.config().pad_object_gap.getFloat() ); - - pad_infl = scaled(1.1 * infl); - } - - auto laststep = po.last_completed_step(); - - if (laststep < slaposCount && laststep > slaposSupportTree) { - auto omesh = po.get_mesh_to_print(); - auto &smesh = po.support_mesh(); - - Transform3f trafo_instance = inst.get_matrix().cast(); - trafo_instance = trafo_instance * po.trafo().cast().inverse(); - - Polygons polys; - polys.reserve(3); - auto zlvl = -po.get_elevation(); - - if (omesh) { - polys.emplace_back(its_convex_hull_2d_above(*omesh, trafo_instance, zlvl)); - } - - polys.emplace_back(its_convex_hull_2d_above(smesh.its, trafo_instance, zlvl)); - ret.poly.contour = Geometry::convex_hull(polys); - ret.poly.holes = {}; - } - - ret.inflation = pad_infl; -} - -static coord_t brim_offset(const PrintObject &po, const ModelInstance &inst) -{ - const BrimType brim_type = po.config().brim_type.value; - const float brim_separation = po.config().brim_separation.getFloat(); - const float brim_width = po.config().brim_width.getFloat(); - const bool has_outer_brim = brim_type == BrimType::btOuterOnly || - brim_type == BrimType::btOuterAndInner; - - // How wide is the brim? (in scaled units) - return has_outer_brim ? scaled(brim_width + brim_separation) : 0; -} - -arrangement::ArrangePolygon ArrangeJob::get_arrange_poly_(ModelInstance *mi) -{ - arrangement::ArrangePolygon ap = get_arrange_poly(mi, m_plater); - - auto setter = ap.setter; - ap.setter = [this, setter, mi](const arrangement::ArrangePolygon &set_ap) { - setter(set_ap); - if (!set_ap.is_arranged()) - m_unarranged.emplace_back(mi); - }; - - return ap; -} - -coord_t get_skirt_offset(const Plater* plater) { - float skirt_inset = 0.f; - // Try to subtract the skirt from the bed shape so we don't arrange outside of it. - if (plater->printer_technology() == ptFFF && plater->fff_print().has_skirt()) { - const auto& print = plater->fff_print(); - if (!print.objects().empty()) { - skirt_inset = print.config().skirts.value * print.skirt_flow().width() + - print.config().skirt_distance.value; - } - } - - return scaled(skirt_inset); -} - -void ArrangeJob::prepare() -{ - m_selection_only ? prepare_selected() : - prepare_all(); - - coord_t min_offset = 0; - for (auto &ap : m_selected) { - min_offset = std::max(ap.inflation, min_offset); - } - - if (m_plater->printer_technology() == ptSLA) { - // Apply the max offset for all the objects - for (auto &ap : m_selected) { - ap.inflation = min_offset; - } - } else { // it's fff, brims only need to be minded from bed edges - for (auto &ap : m_selected) { - ap.inflation = 0; - } - m_min_bed_inset = min_offset; - } - - double stride = bed_stride(m_plater); - get_bed_shape(*m_plater->config(), m_bed); - assign_logical_beds(m_unselected, m_bed, stride); -} - -void ArrangeJob::process(Ctl &ctl) -{ - static const auto arrangestr = _u8L("Arranging"); - - arrangement::ArrangeParams params; - ctl.call_on_main_thread([this, ¶ms]{ - prepare(); - params = get_arrange_params(m_plater); - coord_t min_inset = get_skirt_offset(m_plater) + m_min_bed_inset; - params.min_bed_distance = std::max(params.min_bed_distance, min_inset); - }).wait(); - - auto count = unsigned(m_selected.size() + m_unprintable.size()); - - if (count == 0) // Should be taken care of by plater, but doesn't hurt - return; - - ctl.update_status(0, arrangestr); - - params.stopcondition = [&ctl]() { return ctl.was_canceled(); }; - - params.progressind = [this, count, &ctl](unsigned st) { - st += m_unprintable.size(); - if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr); - }; - - ctl.update_status(0, arrangestr); - - arrangement::arrange(m_selected, m_unselected, m_bed, params); - - params.progressind = [this, count, &ctl](unsigned st) { - if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr); - }; - - arrangement::arrange(m_unprintable, {}, m_bed, params); - - // finalize just here. - ctl.update_status(int(count) * 100 / status_range(), ctl.was_canceled() ? - _u8L("Arranging canceled.") : - _u8L("Arranging done.")); -} - -ArrangeJob::ArrangeJob(Mode mode) - : m_plater{wxGetApp().plater()}, - m_selection_only{mode == Mode::SelectionOnly} -{} - -static std::string concat_strings(const std::set &strings, - const std::string &delim = "\n") -{ - return std::accumulate( - strings.begin(), strings.end(), std::string(""), - [delim](const std::string &s, const std::string &name) { - return s + name + delim; - }); -} - -void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (libnest2d::GeometryException &) { - show_error(m_plater, _(L("Could not arrange model objects! " - "Some geometries may be invalid."))); - eptr = nullptr; - } catch(...) { - eptr = std::current_exception(); - } - - if (canceled || eptr) - return; - - // Unprintable items go to the last virtual bed - int beds = 0; - - // Apply the arrange result to all selected objects - for (ArrangePolygon &ap : m_selected) { - beds = std::max(ap.bed_idx, beds); - ap.apply(); - } - - // Get the virtual beds from the unselected items - for (ArrangePolygon &ap : m_unselected) - beds = std::max(ap.bed_idx, beds); - - // Move the unprintable items to the last virtual bed. - for (ArrangePolygon &ap : m_unprintable) { - if (ap.bed_idx >= 0) - ap.bed_idx += beds + 1; - - ap.apply(); - } - - m_plater->update(static_cast( - Plater::UpdateParams::FORCE_FULL_SCREEN_REFRESH)); - - wxGetApp().obj_manipul()->set_dirty(); - - if (!m_unarranged.empty()) { - std::set names; - for (ModelInstance *mi : m_unarranged) - names.insert(mi->get_object()->name); - - m_plater->get_notification_manager()->push_notification(GUI::format( - _L("Arrangement ignored the following objects which can't fit into a single bed:\n%s"), - concat_strings(names, "\n"))); - } -} - -std::optional -get_wipe_tower_arrangepoly(const Plater &plater) -{ - if (auto wti = get_wipe_tower(plater)) - return get_arrange_poly(wti, &plater); - - return {}; -} - -double bed_stride(const Plater *plater) { - double bedwidth = plater->build_volume().bounding_volume().size().x(); - return scaled((1. + LOGICAL_BED_GAP) * bedwidth); -} - -template<> -arrangement::ArrangePolygon get_arrange_poly(ModelInstance *inst, - const Plater * plater) -{ - auto ap = get_arrange_poly(PtrWrapper{inst}, plater); - - auto obj_id = inst->get_object()->id(); - if (plater->printer_technology() == ptSLA) { - const SLAPrintObject *po = - plater->sla_print().get_print_object_by_model_object_id(obj_id); - - if (po) { - update_arrangepoly_slaprint(ap, *po, *inst); - } - } else { - const PrintObject *po = - plater->fff_print().get_print_object_by_model_object_id(obj_id); - - if (po) { - ap.inflation = brim_offset(*po, *inst); - } - } - - return ap; -} - -arrangement::ArrangeParams get_arrange_params(Plater *p) -{ - const arr2::ArrangeSettingsView *settings = - p->canvas3D()->get_arrange_settings_view(); - - arrangement::ArrangeParams params; - params.allow_rotations = settings->is_rotation_enabled(); - params.min_obj_distance = scaled(settings->get_distance_from_objects()); - params.min_bed_distance = scaled(settings->get_distance_from_bed()); - - arrangement::Pivots pivot = arrangement::Pivots::Center; - - int pivot_max = static_cast(arrangement::Pivots::TopRight); - if (settings->get_xl_alignment() < 0) { - pivot = arrangement::Pivots::Center; - } else if (settings->get_xl_alignment() == arr2::ArrangeSettingsView::xlpRandom) { - // means it should be random - std::random_device rd{}; - std::mt19937 rng(rd()); - std::uniform_int_distribution dist(0, pivot_max); - pivot = static_cast(dist(rng)); - } else { - pivot = static_cast(settings->get_xl_alignment()); - } - - params.alignment = pivot; - - return params; -} - -void assign_logical_beds(std::vector &items, - const arrangement::ArrangeBed &bed, - double stride) -{ - // The strides have to be removed from the fixed items. For the - // arrangeable (selected) items bed_idx is ignored and the - // translation is irrelevant. - coord_t bedx = bounding_box(bed).min.x(); - for (auto &itm : items) { - auto bedidx = std::max(arrangement::UNARRANGED, - static_cast(std::floor( - (get_extents(itm.transformed_poly()).min.x() - bedx) / - stride))); - - itm.bed_idx = bedidx; - - if (bedidx >= 0) - itm.translation.x() -= bedidx * stride; - } -} - -}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp deleted file mode 100644 index c7070f2..0000000 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ /dev/null @@ -1,118 +0,0 @@ -#ifndef ARRANGEJOB_HPP -#define ARRANGEJOB_HPP - -#include - -#include "Job.hpp" -#include "libslic3r/Arrange.hpp" - -namespace Slic3r { - -class ModelInstance; - -namespace GUI { - -class Plater; - -class ArrangeJob : public Job -{ - using ArrangePolygon = arrangement::ArrangePolygon; - using ArrangePolygons = arrangement::ArrangePolygons; - - ArrangePolygons m_selected, m_unselected, m_unprintable; - std::vector m_unarranged; - arrangement::ArrangeBed m_bed; - coord_t m_min_bed_inset = 0.; - - Plater *m_plater; - bool m_selection_only = false; - - // clear m_selected and m_unselected, reserve space for next usage - void clear_input(); - - // Prepare all objects on the bed regardless of the selection - void prepare_all(); - - // Prepare the selected and unselected items separately. If nothing is - // selected, behaves as if everything would be selected. - void prepare_selected(); - - ArrangePolygon get_arrange_poly_(ModelInstance *mi); - -public: - - enum Mode { Full, SelectionOnly }; - - void prepare(); - - void process(Ctl &ctl) override; - - ArrangeJob(Mode mode = Full); - - int status_range() const - { - return int(m_selected.size() + m_unprintable.size()); - } - - void finalize(bool canceled, std::exception_ptr &e) override; -}; - -std::optional get_wipe_tower_arrangepoly(const Plater &); - -// The gap between logical beds in the x axis expressed in ratio of -// the current bed width. -static const constexpr double LOGICAL_BED_GAP = 1. / 5.; - -// Stride between logical beds -double bed_stride(const Plater *plater); - -template struct PtrWrapper -{ - T *ptr; - - explicit PtrWrapper(T *p) : ptr{p} {} - - arrangement::ArrangePolygon get_arrange_polygon() const - { - return ptr->get_arrange_polygon(); - } - - void apply_arrange_result(const Vec2d &t, double rot) - { - ptr->apply_arrange_result(t, rot); - } -}; - -// Set up arrange polygon for a ModelInstance and Wipe tower -template -arrangement::ArrangePolygon get_arrange_poly(T obj, const Plater *plater) -{ - using ArrangePolygon = arrangement::ArrangePolygon; - - ArrangePolygon ap = obj.get_arrange_polygon(); - ap.setter = [obj, plater](const ArrangePolygon &p) { - if (p.is_arranged()) { - Vec2d t = p.translation.cast(); - t.x() += p.bed_idx * bed_stride(plater); - T{obj}.apply_arrange_result(t, p.rotation); - } - }; - - return ap; -} - -template<> -arrangement::ArrangePolygon get_arrange_poly(ModelInstance *inst, - const Plater * plater); - -arrangement::ArrangeParams get_arrange_params(Plater *p); - -coord_t get_skirt_offset(const Plater* plater); - -void assign_logical_beds(std::vector &items, - const arrangement::ArrangeBed &bed, - double stride); - -}} // namespace Slic3r::GUI - -#endif // ARRANGEJOB_HPP diff --git a/src/slic3r/GUI/Jobs/ArrangeJob2.cpp b/src/slic3r/GUI/Jobs/ArrangeJob2.cpp index 7710eec..61bbdc8 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob2.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob2.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -25,9 +26,13 @@ class GUISelectionMask: public arr2::SelectionMask { public: explicit GUISelectionMask(const Selection *sel) : m_sel{sel} {} - bool is_wipe_tower() const override + bool is_wipe_tower_selected(int wipe_tower_index) const override { - return m_sel->is_wipe_tower(); + const GLVolume *volume{GUI::get_selected_gl_volume(*m_sel)}; + if (volume != nullptr && volume->wipe_tower_bed_index == wipe_tower_index) { + return true; + } + return false; } std::vector selected_objects() const override @@ -94,9 +99,9 @@ class ArrangeableWT: public arr2::ArrangeableWipeTowerBase public: explicit ArrangeableWT(const ObjectID &oid, const GLCanvas3D::WipeTowerInfo &wti, - std::function sel_pred, + std::function sel_pred, const BoundingBox xl_bb = {}) - : arr2::ArrangeableWipeTowerBase{oid, get_wtpoly(wti), std::move(sel_pred)} + : arr2::ArrangeableWipeTowerBase{oid, get_wtpoly(wti), wti.bed_index(), std::move(sel_pred)} , m_orig_tr{wti.pos()} , m_orig_rot{wti.rotation()} , m_xl_bb{xl_bb} @@ -105,7 +110,7 @@ public: // Rotation is disabled for wipe tower in arrangement void transform(const Vec2d &transl, double /*rot*/) override { - GLCanvas3D::WipeTowerInfo::apply_wipe_tower(m_orig_tr + transl, m_orig_rot); + GLCanvas3D::WipeTowerInfo::apply_wipe_tower(m_orig_tr + transl, m_orig_rot, this->bed_index); } void imbue_data(arr2::AnyWritable &datastore) const override @@ -129,12 +134,12 @@ struct WTH : public arr2::WipeTowerHandler { GLCanvas3D::WipeTowerInfo wti; ObjectID oid; - std::function sel_pred; + std::function sel_pred; BoundingBox xl_bb; WTH(const ObjectID &objid, const GLCanvas3D::WipeTowerInfo &w, - std::function sel_predicate = [] { return false; }) + std::function sel_predicate = [](int){ return false; }) : wti(w), oid{objid}, sel_pred{std::move(sel_predicate)} {} @@ -155,45 +160,118 @@ struct WTH : public arr2::WipeTowerHandler visit_(*this, fn); } - void set_selection_predicate(std::function pred) override + void set_selection_predicate(std::function pred) override { sel_pred = std::move(pred); } + + ObjectID get_id() const override { + return this->oid; + } }; arr2::SceneBuilder build_scene(Plater &plater, ArrangeSelectionMode mode) { arr2::SceneBuilder builder; + const int current_bed{s_multiple_beds.get_active_bed()}; + const std::map &beds_map{s_multiple_beds.get_inst_map()}; if (mode == ArrangeSelectionMode::SelectionOnly) { - auto sel = std::make_unique(&plater.get_selection()); - builder.set_selection(std::move(sel)); + auto gui_selection = std::make_unique(&plater.get_selection()); + + std::set considered_instances; + for (std::size_t object_index{0}; object_index < plater.model().objects.size(); ++object_index) { + const ModelObject *object{plater.model().objects[object_index]}; + for (std::size_t instance_index{0}; instance_index < object->instances.size(); ++instance_index) { + const ModelInstance *instance{object->instances[instance_index]}; + + const bool is_selected{gui_selection->selected_instances(object_index)[instance_index]}; + const auto instance_bed_index{beds_map.find(instance->id())}; + + if ( + is_selected + || instance_bed_index != beds_map.end() + ) { + considered_instances.insert(instance->id()); + } + } + } + builder.set_selection(std::move(gui_selection)); + builder.set_considered_instances(std::move(considered_instances)); + } else if (mode == ArrangeSelectionMode::CurrentBedSelectionOnly) { + auto gui_selection{std::make_unique(&plater.get_selection())}; + + std::set considered_instances; + arr2::BedConstraints constraints; + for (std::size_t object_index{0}; object_index < plater.model().objects.size(); ++object_index) { + const ModelObject *object{plater.model().objects[object_index]}; + for (std::size_t instance_index{0}; instance_index < object->instances.size(); ++instance_index) { + const ModelInstance *instance{object->instances[instance_index]}; + + const bool is_selected{gui_selection->selected_instances(object_index)[instance_index]}; + + const auto instance_bed_index{beds_map.find(instance->id())}; + if ( + is_selected + || ( + instance_bed_index != beds_map.end() + && instance_bed_index->second == current_bed + ) + ) { + constraints.insert({instance->id(), current_bed}); + considered_instances.insert(instance->id()); + } + } + } + + builder.set_selection(std::move(gui_selection)); + builder.set_bed_constraints(std::move(constraints)); + builder.set_considered_instances(std::move(considered_instances)); + } else if (mode == ArrangeSelectionMode::CurrentBedFull) { + std::set instances_on_bed; + arr2::BedConstraints constraints; + for (const auto &instance_bed : beds_map) { + if (instance_bed.second == current_bed) { + instances_on_bed.insert(instance_bed.first); + constraints.insert(instance_bed); + } + } + builder.set_bed_constraints(std::move(constraints)); + builder.set_considered_instances(std::move(instances_on_bed)); } builder.set_arrange_settings(plater.canvas3D()->get_arrange_settings_view()); - auto wti = plater.canvas3D()->get_wipe_tower_info(); + const auto wipe_tower_infos = plater.canvas3D()->get_wipe_tower_infos(); - AnyPtr wth; + std::vector> handlers; - if (wti) { - wth = std::make_unique(plater.model().wipe_tower.id(), wti); - } - - if (plater.config()) { - builder.set_bed(*plater.config()); - if (wth && is_XL_printer(*plater.config())) { - wth->xl_bb = bounding_box(get_bed_shape(*plater.config())); + for (const auto &info : wipe_tower_infos) { + if (info) { + if (mode == ArrangeSelectionMode::CurrentBedFull && info.bed_index() != current_bed) { + continue; + } + auto handler{std::make_unique(wipe_tower_instance_id(info.bed_index()), info)}; + if (plater.config() && is_XL_printer(*plater.config())) { + handler->xl_bb = bounding_box(get_bed_shape(*plater.config())); + } + handlers.push_back(std::move(handler)); } } - builder.set_wipe_tower_handler(std::move(wth)); + if (plater.config()) { + const Vec2crd gap{s_multiple_beds.get_bed_gap()}; + builder.set_bed(*plater.config(), gap); + } + + builder.set_wipe_tower_handlers(std::move(handlers)); + builder.set_model(plater.model()); if (plater.printer_technology() == ptSLA) - builder.set_sla_print(&plater.sla_print()); + builder.set_sla_print(&plater.active_sla_print()); else - builder.set_fff_print(&plater.fff_print()); + builder.set_fff_print(&plater.active_fff_print()); return builder; } diff --git a/src/slic3r/GUI/Jobs/ArrangeJob2.hpp b/src/slic3r/GUI/Jobs/ArrangeJob2.hpp index 31f6d70..cce9487 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob2.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob2.hpp @@ -6,10 +6,10 @@ #include "Job.hpp" -#include "libslic3r/Arrange/Tasks/ArrangeTask.hpp" -#include "libslic3r/Arrange/Tasks/FillBedTask.hpp" -#include "libslic3r/Arrange/Items/ArrangeItem.hpp" -#include "libslic3r/Arrange/SceneBuilder.hpp" +#include +#include +#include +#include namespace Slic3r { @@ -24,7 +24,7 @@ namespace GUI { class Plater; -enum class ArrangeSelectionMode { SelectionOnly, Full }; +enum class ArrangeSelectionMode { SelectionOnly, Full, CurrentBedFull, CurrentBedSelectionOnly }; arr2::SceneBuilder build_scene( Plater &plater, ArrangeSelectionMode mode = ArrangeSelectionMode::Full); diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index 146d0a3..1b4ae4c 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -10,6 +10,8 @@ #include // create object #include +#include "libslic3r/MultipleBeds.hpp" + #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" @@ -318,9 +320,9 @@ void CreateObjectJob::process(Ctl &ctl) Points bed_shape_; bed_shape_.reserve(m_input.bed_shape.size()); for (const Vec2d &p : m_input.bed_shape) - bed_shape_.emplace_back(p.cast()); + bed_shape_.emplace_back(p.cast()); Slic3r::Polygon bed(bed_shape_); - if (!bed.contains(bed_coor.cast())) + if (!bed.contains(bed_coor.cast())) // mouse pose is out of build plate so create object in center of plate bed_coor = bed.centroid().cast(); @@ -369,6 +371,7 @@ void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) // set transformation Slic3r::Geometry::Transformation tr(m_transformation); new_object->instances.front()->set_transformation(tr); + new_object->instances.front()->set_offset(new_object->instances.front()->get_offset() + s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); new_object->ensure_on_bed(); // Actualize right panel and set inside of selection diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp deleted file mode 100644 index 7594d32..0000000 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "FillBedJob.hpp" - -#include "libslic3r/Model.hpp" -#include "libslic3r/ClipperUtils.hpp" - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI_ObjectList.hpp" - -#include - -namespace Slic3r { -namespace GUI { - -void FillBedJob::prepare() -{ - m_selected.clear(); - m_unselected.clear(); - m_min_bed_inset = 0.; - - m_object_idx = m_plater->get_selected_object_idx(); - if (m_object_idx == -1) - return; - - ModelObject *model_object = m_plater->model().objects[m_object_idx]; - if (model_object->instances.empty()) - return; - - m_selected.reserve(model_object->instances.size()); - for (ModelInstance *inst : model_object->instances) - if (inst->printable) { - ArrangePolygon ap = get_arrange_poly(inst, m_plater); - // Existing objects need to be included in the result. Only - // the needed amount of object will be added, no more. - ++ap.priority; - m_selected.emplace_back(ap); - } - - if (m_selected.empty()) - return; - - Points bedpts = get_bed_shape(*m_plater->config()); - - auto &objects = m_plater->model().objects; - BoundingBox bedbb = get_extents(bedpts); - - for (size_t idx = 0; idx < objects.size(); ++idx) - if (int(idx) != m_object_idx) - for (ModelInstance *mi : objects[idx]->instances) { - ArrangePolygon ap = get_arrange_poly(PtrWrapper{mi}, m_plater); - auto ap_bb = ap.transformed_poly().contour.bounding_box(); - - if (ap.bed_idx == 0 && !bedbb.contains(ap_bb)) - ap.bed_idx = arrangement::UNARRANGED; - - m_unselected.emplace_back(ap); - } - - if (auto wt = get_wipe_tower_arrangepoly(*m_plater)) - m_unselected.emplace_back(std::move(*wt)); - - double sc = scaled(1.) * scaled(1.); - - ExPolygon poly = m_selected.front().poly; - double poly_area = poly.area() / sc; - double unsel_area = std::accumulate(m_unselected.begin(), - m_unselected.end(), 0., - [](double s, const auto &ap) { - return s + (ap.bed_idx == 0) * ap.poly.area(); - }) / sc; - - double fixed_area = unsel_area + m_selected.size() * poly_area; - double bed_area = Polygon{bedpts}.area() / sc; - - // This is the maximum number of items, the real number will always be close but less. - int needed_items = (bed_area - fixed_area) / poly_area; - - int sel_id = m_plater->get_selection().get_instance_idx(); - // if the selection is not a single instance, choose the first as template - sel_id = std::max(sel_id, 0); - ModelInstance *mi = model_object->instances[sel_id]; - ArrangePolygon template_ap = get_arrange_poly(PtrWrapper{mi}, m_plater); - - for (int i = 0; i < needed_items; ++i) { - ArrangePolygon ap = template_ap; - ap.bed_idx = arrangement::UNARRANGED; - auto m = mi->get_transformation(); - ap.setter = [this, m](const ArrangePolygon &p) { - ModelObject *mo = m_plater->model().objects[m_object_idx]; - ModelInstance *inst = mo->add_instance(m); - inst->apply_arrange_result(p.translation.cast(), p.rotation); - }; - m_selected.emplace_back(ap); - } - - m_status_range = m_selected.size(); - - coord_t min_offset = 0; - for (auto &ap : m_selected) { - min_offset = std::max(ap.inflation, min_offset); - } - - if (m_plater->printer_technology() == ptSLA) { - // Apply the max offset for all the objects - for (auto &ap : m_selected) { - ap.inflation = min_offset; - } - } else { // it's fff, brims only need to be minded from bed edges - for (auto &ap : m_selected) { - ap.inflation = 0; - } - m_min_bed_inset = min_offset; - } - - // The strides have to be removed from the fixed items. For the - // arrangeable (selected) items bed_idx is ignored and the - // translation is irrelevant. - double stride = bed_stride(m_plater); - - m_bed = arrangement::to_arrange_bed(bedpts); - assign_logical_beds(m_unselected, m_bed, stride); -} - -void FillBedJob::process(Ctl &ctl) -{ - auto statustxt = _u8L("Filling bed"); - arrangement::ArrangeParams params; - ctl.call_on_main_thread([this, ¶ms] { - prepare(); - params = get_arrange_params(m_plater); - coord_t min_inset = get_skirt_offset(m_plater) + m_min_bed_inset; - params.min_bed_distance = std::max(params.min_bed_distance, min_inset); - }).wait(); - ctl.update_status(0, statustxt); - - if (m_object_idx == -1 || m_selected.empty()) - return; - - bool do_stop = false; - params.stopcondition = [&ctl, &do_stop]() { - return ctl.was_canceled() || do_stop; - }; - - params.progressind = [this, &ctl, &statustxt](unsigned st) { - if (st > 0) - ctl.update_status(int(m_status_range - st) * 100 / status_range(), statustxt); - }; - - params.on_packed = [&do_stop] (const ArrangePolygon &ap) { - do_stop = ap.bed_idx > 0 && ap.priority == 0; - }; - - arrangement::arrange(m_selected, m_unselected, m_bed, params); - - // finalize just here. - ctl.update_status(100, ctl.was_canceled() ? - _u8L("Bed filling canceled.") : - _u8L("Bed filling done.")); -} - -FillBedJob::FillBedJob() : m_plater{wxGetApp().plater()} {} - -void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) -{ - // Ignore the arrange result if aborted. - if (canceled || eptr) - return; - - if (m_object_idx == -1) - return; - - ModelObject *model_object = m_plater->model().objects[m_object_idx]; - if (model_object->instances.empty()) - return; - - size_t inst_cnt = model_object->instances.size(); - - int added_cnt = std::accumulate(m_selected.begin(), m_selected.end(), 0, [](int s, auto &ap) { - return s + int(ap.priority == 0 && ap.bed_idx == 0); - }); - - if (added_cnt > 0) { - for (ArrangePolygon &ap : m_selected) { - if (ap.bed_idx != arrangement::UNARRANGED && (ap.priority != 0 || ap.bed_idx == 0)) - ap.apply(); - } - - model_object->ensure_on_bed(); - - m_plater->update(static_cast( - Plater::UpdateParams::FORCE_FULL_SCREEN_REFRESH)); - - // FIXME: somebody explain why this is needed for increase_object_instances - if (inst_cnt == 1) - added_cnt++; - - m_plater->sidebar() - .obj_list()->increase_object_instances(m_object_idx, size_t(added_cnt)); - } -} - -}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/FillBedJob.hpp b/src/slic3r/GUI/Jobs/FillBedJob.hpp deleted file mode 100644 index ae8680d..0000000 --- a/src/slic3r/GUI/Jobs/FillBedJob.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef FILLBEDJOB_HPP -#define FILLBEDJOB_HPP - -#include "ArrangeJob.hpp" - -namespace Slic3r { namespace GUI { - -class Plater; - -class FillBedJob : public Job -{ - int m_object_idx = -1; - - using ArrangePolygon = arrangement::ArrangePolygon; - using ArrangePolygons = arrangement::ArrangePolygons; - - ArrangePolygons m_selected; - ArrangePolygons m_unselected; - coord_t m_min_bed_inset = 0.; - - arrangement::ArrangeBed m_bed; - - int m_status_range = 0; - Plater *m_plater; - -public: - void prepare(); - void process(Ctl &ctl) override; - - FillBedJob(); - - int status_range() const /*override*/ - { - return m_status_range; - } - - void finalize(bool canceled, std::exception_ptr &e) override; -}; - -}} // namespace Slic3r::GUI - -#endif // FILLBEDJOB_HPP diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index ecf5fec..bb27072 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -91,7 +91,7 @@ void SLAImportJob::reset() { p->sel = Sel::modelAndProfile; p->mesh = {}; - p->profile = p->plater->sla_print().full_print_config(); + p->profile = p->plater->active_sla_print().full_print_config(); p->quality = SLAImportQuality::Balanced; p->path.Clear(); p->err = ""; @@ -138,7 +138,7 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) if (p->sel != Sel::modelOnly) { if (p->profile.empty()) - p->profile = p->plater->sla_print().full_print_config(); + p->profile = p->plater->active_sla_print().full_print_config(); const ModelObjectPtrs& objects = p->plater->model().objects; for (auto object : objects) diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 30a5e47..a95a006 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -160,6 +160,7 @@ void KBShortcutsDialog::fill_shortcuts() { "C", L("Gizmo cut") }, { "F", L("Gizmo Place face on bed") }, { "H", L("Gizmo SLA hollow") }, + { "H", L("Gizmo FDM paint-on fuzzy skin") }, { "L", L("Gizmo SLA support points") }, { "L", L("Gizmo FDM paint-on supports") }, { "P", L("Gizmo FDM paint-on seam") }, diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index a387485..725cd36 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -424,8 +424,8 @@ public: if (wipe_tower_data.final_purge) m_final.emplace_back(*wipe_tower_data.final_purge.get()); - m_angle = config.wipe_tower_rotation_angle.value / 180.0f * PI; - m_position = Slic3r::Vec2f(config.wipe_tower_x.value, config.wipe_tower_y.value); + m_angle = print.model().wipe_tower().rotation / 180.0f * PI; + m_position = print.model().wipe_tower().position.cast(); m_layers_count = wipe_tower_data.tool_changes.size() + (m_priming.empty() ? 0 : 1); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 0acbef4..e551514 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -53,7 +53,7 @@ #include "GalleryDialog.hpp" #include "NotificationManager.hpp" #include "Preferences.hpp" -#include "WebViewDialog.hpp" +#include "WebViewPanel.hpp" #include "UserAccount.hpp" #ifdef _WIN32 @@ -699,6 +699,10 @@ void MainFrame::init_tabpanel() old_tab->validate_custom_gcodes(); } +#ifndef __APPLE__ + on_tab_change_rename_reload_item(e.GetSelection()); +#endif // !__APPLE__ + wxWindow* panel = m_tabpanel->GetCurrentPage(); size_t current_selected_tab = m_tabpanel->GetSelection(); Tab* tab = dynamic_cast(panel); @@ -721,10 +725,10 @@ void MainFrame::init_tabpanel() tab->OnActivate(); m_last_selected_tab = m_tabpanel->GetSelection(); select_tab(tab); - } - //y17 - else if (m_tabpanel->GetSelection() != 0) { - m_last_selected_tab = m_tabpanel->GetSelection(); + } + //y17 + else if (m_tabpanel->GetSelection() != 0) { + m_last_selected_tab = m_tabpanel->GetSelection(); } } else if (m_layout == ESettingsLayout::Dlg) { @@ -738,15 +742,13 @@ void MainFrame::init_tabpanel() m_last_selected_tab = current_selected_tab; } //y17 - else select_tab(size_t(0)); // select Plater }); m_plater = new Plater(this, this); - m_plater->Hide(); - wxGetApp().plater_ = m_plater; + m_plater->Hide(); if (wxGetApp().is_editor()) create_preset_tabs(); @@ -866,15 +868,41 @@ void MainFrame::create_preset_tabs() wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_btns_enabling(); wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_PRINTER); }); + + // m_printables_webview = new PrintablesWebViewPanel(m_tabpanel); + // add_printables_webview_tab(); + m_connect_webview = new ConnectWebViewPanel(m_tabpanel); m_printer_webview = new PrinterWebViewPanel(m_tabpanel, L""); // new created tabs have to be hidden by default + m_connect_webview->Hide(); m_printer_webview->Hide(); //y17 select_tab(size_t(0)); } +void MainFrame::on_account_login(const std::string& token) +{ + add_connect_webview_tab(); + assert (m_printables_webview); + m_printables_webview->login(token); +} +void MainFrame::on_account_will_refresh() +{ + m_printables_webview->send_will_refresh(); +} +void MainFrame::on_account_did_refresh(const std::string& token) +{ + m_printables_webview->send_refreshed_token(token); +} +void MainFrame::on_account_logout() +{ + remove_connect_webview_tab(); + assert (m_printables_webview); + m_printables_webview->logout(); +} + void MainFrame::add_connect_webview_tab() { if (m_connect_webview_added) { @@ -885,13 +913,13 @@ void MainFrame::add_connect_webview_tab() // insert "Connect" tab to position next to "Printer" tab // order of tabs: Plater - Print Settings - Filaments - Printers - QIDI Connect - QIDI Link - int n = m_tabpanel->FindPage(wxGetApp().get_tab(Preset::TYPE_PRINTER)) + 1; + int n = m_tabpanel->FindPage(m_printables_webview) + 1; wxWindow* page = m_connect_webview; const wxString text(L"QIDI Connect"); const std::string bmp_name = ""; bool bSelect = false; m_tabpanel->InsertNewPage(n, page, text, bmp_name, bSelect); - m_connect_webview->load_default_url_delayed(); + m_connect_webview->set_create_browser(); m_connect_webview_added = true; } void MainFrame::remove_connect_webview_tab() @@ -905,18 +933,68 @@ void MainFrame::remove_connect_webview_tab() m_tabpanel->RemovePage(size_t(n)); m_connect_webview_added = false; m_connect_webview->logout(); + m_connect_webview->destroy_browser(); +} + +void MainFrame::show_connect_tab(const wxString& url) +{ + if (!m_connect_webview_added) { + return; + } + m_tabpanel->SetSelection(m_tabpanel->FindPage(m_connect_webview)); + m_connect_webview->set_load_default_url_on_next_error(true); + m_connect_webview->load_url(url); +} +void MainFrame::show_printables_tab(const std::string& url) +{ + if (!m_printables_webview_added) { + return; + } + // we have to set next url first, than show the tab + // printables_tab has to reload on show everytime + // so it is not possible load_url right after show + m_printables_webview->set_load_default_url_on_next_error(true); + m_printables_webview->set_next_show_url(url); + m_tabpanel->SetSelection(m_tabpanel->FindPage(m_printables_webview)); +} +void MainFrame::add_printables_webview_tab() +{ + if (m_printables_webview_added) { + return; + } + + int n = m_tabpanel->FindPage(wxGetApp().get_tab(Preset::TYPE_PRINTER)) + 1; + wxWindow* page = m_printables_webview; + const wxString text(L"Printables"); + const std::string bmp_name = ""; + m_tabpanel->InsertNewPage(n, page, text, bmp_name, false); + m_printables_webview->set_create_browser(); + m_printables_webview_added = true; +} + +// no longer needed? +void MainFrame::remove_printables_webview_tab() +{ + if (!m_printables_webview_added) { + return; + } + int n = m_tabpanel->FindPage(m_printables_webview); + if (m_tabpanel->GetSelection() == n) + m_tabpanel->SetSelection(0); + m_tabpanel->RemovePage(size_t(n)); + m_printables_webview_added = false; + m_printables_webview->destroy_browser(); } void MainFrame::show_printer_webview_tab(DynamicPrintConfig* dpc) { + remove_printer_webview_tab(); // if physical printer is selected if (dpc && dpc->option>("host_type")->value != htQIDIConnect) { std::string url = dpc->opt_string("print_host"); - if (url.find("http://") != 0 && url.find("https://") != 0) { url = "http://" + url; } - // set password / api key if (dynamic_cast*>(dpc->option("printhost_authorization_type"))->value == AuthorizationType::atKeyPassword) { set_printer_webview_api_key(dpc->opt_string("printhost_apikey")); @@ -924,59 +1002,33 @@ void MainFrame::show_printer_webview_tab(DynamicPrintConfig* dpc) else { set_printer_webview_credentials(dpc->opt_string("printhost_user"), dpc->opt_string("printhost_password")); } - // add printer or change url - if (get_printer_webview_tab_added()) { - set_printer_webview_tab_url(from_u8(url)); - } - else { - add_printer_webview_tab(from_u8(url)); - } - } - // if physical printer isn't selected, so delete page from TopBar - else { - if (m_tabpanel->GetPageText(m_tabpanel->GetSelection()) == _L("Physical Printer")) - select_tab(size_t(0)); - remove_printer_webview_tab(); + add_printer_webview_tab(from_u8(url)); } } void MainFrame::add_printer_webview_tab(const wxString& url) { if (m_printer_webview_added) { - set_printer_webview_tab_url(url); + //set_printer_webview_tab_url(url); return; } m_printer_webview_added = true; // add as the last (rightmost) panel m_tabpanel->AddNewPage(m_printer_webview, _L("Physical Printer"), ""); m_printer_webview->set_default_url(url); - m_printer_webview->load_default_url_delayed(); + m_printer_webview->set_create_browser(); } void MainFrame::remove_printer_webview_tab() { if (!m_printer_webview_added) { return; } + if (m_tabpanel->GetPageText(m_tabpanel->GetSelection()) == _L("Physical Printer")) + select_tab(size_t(0)); m_printer_webview_added = false; m_printer_webview->Hide(); m_tabpanel->RemovePage(m_tabpanel->FindPage(m_printer_webview)); -} -void MainFrame::set_printer_webview_tab_url(const wxString& url) -{ - if (!m_printer_webview_added) { - add_printer_webview_tab(url); - return; - } - // TODO: this will reset already filled credential when bundle loaded, - // what's the reason of clearing credentials here? - //m_printer_webview->clear(); - m_printer_webview->set_default_url(url); - - if (m_tabpanel->GetSelection() == m_tabpanel->FindPage(m_printer_webview)) { - m_printer_webview->load_url(url); - } else { - m_printer_webview->load_default_url_delayed(); - } + m_printer_webview->destroy_browser(); } void MainFrame::set_printer_webview_api_key(const std::string& key) @@ -988,6 +1040,61 @@ void MainFrame::set_printer_webview_credentials(const std::string& usr, const st m_printer_webview->set_credentials(usr, psk); } +bool MainFrame::is_any_webview_selected() +{ + int selection = m_tabpanel->GetSelection(); + if ( selection == m_tabpanel->FindPage(m_printables_webview)) + return true; + if (m_connect_webview_added && selection == m_tabpanel->FindPage(m_connect_webview)) + return true; + if (m_printer_webview_added && selection == m_tabpanel->FindPage(m_printer_webview)) + return true; + return false; +} + +void MainFrame::reload_selected_webview() +{ + int selection = m_tabpanel->GetSelection(); + if ( selection == m_tabpanel->FindPage(m_printables_webview)) + m_printables_webview->do_reload(); + if (m_connect_webview_added && selection == m_tabpanel->FindPage(m_connect_webview)) + m_connect_webview->do_reload(); + if (m_printer_webview_added && selection == m_tabpanel->FindPage(m_printer_webview)) + m_printer_webview->do_reload(); +} + +void MainFrame::on_tab_change_rename_reload_item(int new_tab) +{ + if (!m_tabpanel) { + return; + } + if (!m_menu_item_reload) { + return; + } + if ( new_tab == m_tabpanel->FindPage(m_printables_webview) + || (m_connect_webview_added && new_tab == m_tabpanel->FindPage(m_connect_webview)) + || (m_printer_webview_added && new_tab == m_tabpanel->FindPage(m_printer_webview))) + { + m_menu_item_reload->SetItemLabel(_L("Re&load Web Content") + "\tF5"); + m_menu_item_reload->SetHelp(_L("Reload Web Content")); + } else { + m_menu_item_reload->SetItemLabel(_L("Re&load from Disk") + "\tF5"); + m_menu_item_reload->SetHelp(_L("Reload the plater from disk")); + } +} + +bool MainFrame::reload_item_condition_cb() +{ + return is_any_webview_selected() ? true : + !m_plater->model().objects.empty(); +} +void MainFrame::reload_item_function_cb() +{ + is_any_webview_selected() + ? reload_selected_webview() + : m_plater->reload_all_from_disk(); +} + void Slic3r::GUI::MainFrame::refresh_account_menu(bool avatar/* = false */) { // Update User name in TopBar @@ -1076,7 +1183,7 @@ bool MainFrame::can_export_supports() const return false; bool can_export = false; - const PrintObjects& objects = m_plater->sla_print().objects(); + const PrintObjects& objects = m_plater->active_sla_print().objects(); for (const SLAPrintObject* object : objects) { if (!object->support_mesh().empty() || !object->pad_mesh().empty()) @@ -1252,6 +1359,8 @@ void MainFrame::on_sys_color_changed() for (Tab* tab : wxGetApp().tabs_list) tab->sys_color_changed(); + if (m_printables_webview) + m_printables_webview->sys_color_changed(); if (m_connect_webview) m_connect_webview->sys_color_changed(); if (m_printer_webview) @@ -1315,7 +1424,7 @@ static wxMenu* generate_help_menu() //B6 append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("%s &Website"), SLIC3R_APP_NAME), wxString::Format(_L("Open the %s website in your browser"), SLIC3R_APP_NAME), - [](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://www.qidi3d.com/slicerweb"); }); + [](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://qidi3d.com"); }); // TRN Item from "Help" menu // append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("&Quick Start"), SLIC3R_APP_NAME), // wxString::Format(_L("Open the %s website in your browser"), SLIC3R_APP_NAME), @@ -1599,15 +1708,20 @@ void MainFrame::init_menubar_as_editor() append_menu_item(editMenu, wxID_ANY, _L("Re&load from Disk") + dots + "\tCtrl+Shift+R", _L("Reload the plater from disk"), [this](wxCommandEvent&) { m_plater->reload_all_from_disk(); }, "", nullptr, [this]() {return !m_plater->model().objects.empty(); }, this); + m_menu_item_reload = append_menu_item(editMenu, wxID_ANY, _L("Re&load Web Content") + "\tF5", + _L("Reload Web Content"), [this](wxCommandEvent&) { reload_selected_webview(); }, + "", nullptr, [this]() {return is_any_webview_selected(); }, this); #else - append_menu_item(editMenu, wxID_ANY, _L("Re&load from Disk") + sep + "F5", - _L("Reload the plater from disk"), [this](wxCommandEvent&) { m_plater->reload_all_from_disk(); }, - "", nullptr, [this]() {return !m_plater->model().objects.empty(); }, this); + m_menu_item_reload = append_menu_item(editMenu, wxID_ANY, _L("Re&load from Disk") + "\tF5", + _L("Reload the plater from disk"), [this](wxCommandEvent&) { reload_item_function_cb(); }, + "", nullptr, [this]() {return reload_item_condition_cb(); }, this); #endif // __APPLE__ editMenu->AppendSeparator(); append_menu_item(editMenu, wxID_ANY, _L("Searc&h") + "\tCtrl+F", - _L("Search in settings"), [](wxCommandEvent&) { wxGetApp().show_search_dialog(); }, + _L("Search in settings"), [this](wxCommandEvent&) { + m_tabpanel->GetTopBarItemsCtrl()->TriggerSearch(); + }, "search", nullptr, []() {return true; }, this); } @@ -2393,7 +2507,8 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) #else /* __APPLE__ */ case WXK_CONTROL_F: #endif /* __APPLE__ */ - case 'F': { wxGetApp().show_search_dialog(); break; } + case 'F': { m_tabpanel->GetTopBarItemsCtrl()->TriggerSearch(); + break; } default:break; } } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 1e11786..4b9549e 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -43,6 +43,7 @@ class PreferencesDialog; class GalleryDialog; class ConnectWebViewPanel; class PrinterWebViewPanel; +class PrintablesWebViewPanel; enum QuickSlice { @@ -89,15 +90,18 @@ class MainFrame : public DPIFrame TopBarMenus m_bar_menus; wxMenuItem* m_menu_item_reslice_now { nullptr }; + wxMenuItem* m_menu_item_reload { nullptr }; wxSizer* m_main_sizer{ nullptr }; size_t m_last_selected_tab; Search::OptionsSearcher m_searcher; - ConnectWebViewPanel* m_connect_webview{ nullptr }; - bool m_connect_webview_added{ false }; - PrinterWebViewPanel* m_printer_webview{ nullptr }; - bool m_printer_webview_added{ false }; + ConnectWebViewPanel* m_connect_webview{ nullptr }; + bool m_connect_webview_added{ false }; + PrintablesWebViewPanel* m_printables_webview{ nullptr }; + bool m_printables_webview_added{ false }; + PrinterWebViewPanel* m_printer_webview{ nullptr }; + bool m_printer_webview_added{ false }; std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const; std::string get_dir_name(const wxString &full_name) const; @@ -120,6 +124,11 @@ class MainFrame : public DPIFrame bool can_delete_all() const; bool can_reslice() const; + void add_connect_webview_tab(); + void remove_connect_webview_tab(); + void on_tab_change_rename_reload_item(int new_tab); + bool reload_item_condition_cb(); + void reload_item_function_cb(); // MenuBar items changeable in respect to printer technology enum MenuItems { // FFF SLA @@ -210,17 +219,25 @@ public: void add_to_recent_projects(const wxString& filename); void technology_changed(); - void add_connect_webview_tab(); - void remove_connect_webview_tab(); + void on_account_login(const std::string& token); + void on_account_will_refresh(); + void on_account_did_refresh(const std::string& token); + void on_account_logout(); + void show_connect_tab(const wxString& url); + void show_printables_tab(const std::string& url); + + void add_printables_webview_tab(); + void remove_printables_webview_tab(); void show_printer_webview_tab(DynamicPrintConfig* dpc); void add_printer_webview_tab(const wxString& url); void remove_printer_webview_tab(); - void set_printer_webview_tab_url(const wxString& url); bool get_printer_webview_tab_added() const { return m_printer_webview_added; } void set_printer_webview_api_key(const std::string& key); void set_printer_webview_credentials(const std::string& usr, const std::string& psk); + bool is_any_webview_selected(); + void reload_selected_webview(); void refresh_account_menu(bool avatar = false); diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 88a4f29..98c9596 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -422,14 +422,11 @@ void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3 bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane, - size_t* facet_idx) const + size_t* facet_idx, const bool require_even_number_of_hits) const { Vec3d point; Vec3d direction; - CameraUtils::ray_from_screen_pos(camera, mouse_pos, point, direction); - Transform3d inv = trafo.inverse(); - point = inv*point; - direction = inv.linear()*direction; + line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); std::vector hits = m_emesh.query_ray_hits(point, direction); @@ -447,7 +444,7 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& break; } - if (i==hits.size() || (hits.size()-i) % 2 != 0) { + if (i == hits.size() || (require_even_number_of_hits && (hits.size() - i) % 2 != 0)) { // All hits are either clipped, or there is an odd number of unclipped // hits - meaning the nearest must be from inside the mesh. return false; diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index a7dd3d7..cd81c38 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -182,7 +182,8 @@ public: Vec3f& position, // where to save the positibon of the hit (mesh coords) Vec3f& normal, // normal of the triangle that was hit const ClippingPlane* clipping_plane = nullptr, // clipping plane (if active) - size_t* facet_idx = nullptr // index of the facet hit + size_t* facet_idx = nullptr, // index of the facet hit + bool require_even_number_of_hits = true // When it is true, then an odd number (unclipped) of hits are ignored, and false is returned. ) const; const AABBMesh &get_aabb_mesh() const { return m_emesh; } diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index b333365..d8457e4 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -1,7 +1,3 @@ -///|/ Copyright (c) Prusa Research 2021 - 2022 Oleksandra Iushchenko @YuSanka, Lukáš Hejl @hejllukas -///|/ -///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher -///|/ #include "Notebook.hpp" #ifdef _WIN32 diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 753bf77..639e820 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -129,7 +129,7 @@ void NotificationManager::NotificationIDProvider::release_id(int) {} #endif //------PopNotification-------- -NotificationManager::PopNotification::PopNotification(const NotificationData &n, NotificationIDProvider &id_provider, wxEvtHandler* evt_handler) : +NotificationManager::PopNotification::PopNotification(const NotificationData &n, NotificationIDProvider &id_provider, wxEvtHandler* evt_handler, const bool multiline) : m_data (n) , m_id_provider (id_provider) , m_text1 (n.text1) @@ -137,6 +137,7 @@ NotificationManager::PopNotification::PopNotification(const NotificationData &n, , m_text2 (n.text2) , m_evt_handler (evt_handler) , m_notification_start (GLCanvas3D::timestamp_now()) + , m_multiline (multiline) {} void NotificationManager::PopNotification::render(GLCanvas3D& canvas, float initial_y, bool move_from_overlay, float overlay_width) @@ -291,7 +292,7 @@ void NotificationManager::PopNotification::count_lines() return; m_endlines.clear(); - while (last_end < text.length() - 1) + while (last_end < text.size() - 1) { size_t next_hard_end = text.find_first_of('\n', last_end); if (next_hard_end != std::string::npos && ImGui::CalcTextSize(text.substr(last_end, next_hard_end - last_end).c_str()).x < m_window_width - m_window_width_offset) { @@ -304,14 +305,14 @@ void NotificationManager::PopNotification::count_lines() if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - m_window_width_offset) { // more than one line till end size_t next_space = text.find_first_of(' ', last_end); - if (next_space > 0 && next_space < text.length()) { + if (next_space > 0 && next_space < text.size()) { size_t next_space_candidate = text.find_first_of(' ', next_space + 1); while (next_space_candidate > 0 && ImGui::CalcTextSize(text.substr(last_end, next_space_candidate - last_end).c_str()).x < m_window_width - m_window_width_offset) { next_space = next_space_candidate; next_space_candidate = text.find_first_of(' ', next_space + 1); } } else { - next_space = text.length(); + next_space = text.size(); } // when one word longer than line. if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset || @@ -330,8 +331,8 @@ void NotificationManager::PopNotification::count_lines() } } else { - m_endlines.push_back(text.length()); - last_end = text.length(); + m_endlines.push_back(text.size()); + last_end = text.size(); } } @@ -372,7 +373,7 @@ void NotificationManager::PopNotification::count_lines() m_endlines2.push_back(0); size_of_last_line = 0; } - while (last_end < text.length() - 1) + while (last_end < text.size() - 1) { size_t next_hard_end = text.find_first_of('\n', last_end); if (next_hard_end != std::string::npos && ImGui::CalcTextSize(text.substr(last_end, next_hard_end - last_end).c_str()).x < m_window_width - m_window_width_offset - size_of_last_line) { @@ -393,7 +394,7 @@ void NotificationManager::PopNotification::count_lines() } } else { - next_space = text.length(); + next_space = text.size(); } // when one word longer than line. if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset - size_of_last_line || @@ -413,8 +414,8 @@ void NotificationManager::PopNotification::count_lines() } } else { - m_endlines2.push_back(text.length()); - last_end = text.length(); + m_endlines2.push_back(text.size()); + last_end = text.size(); } } @@ -468,7 +469,7 @@ void NotificationManager::PopNotification::render_text(const float win_size_x, c assert(m_normal_lines_count - 2 >= 0); line = m_text1.substr(m_endlines[m_normal_lines_count - 2] + (m_text1[m_endlines[m_normal_lines_count - 2]] == '\n' || m_text1[m_endlines[m_normal_lines_count - 2]] == ' ' ? 1 : 0), m_endlines[m_normal_lines_count - 1] - m_endlines[m_normal_lines_count - 2] - (m_text1[m_endlines[m_normal_lines_count - 2]] == '\n' || m_text1[m_endlines[m_normal_lines_count - 2]] == ' ' ? 1 : 0)); while (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset - ImGui::CalcTextSize((" [" + _u8L("More") + "]").c_str()).x) { - line = line.substr(0, line.length() - 1); + line = line.substr(0, line.size() - 1); } line += " ";//".."; } @@ -944,9 +945,8 @@ void NotificationManager::ProgressBarNotification::render_text(const float win_s render_cancel_button(win_size_x, win_size_y, win_pos_x, win_pos_y); render_bar(win_size_x, win_size_y, win_pos_x, win_pos_y); } - - } + void NotificationManager::ProgressBarNotification::render_bar(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) { ImVec4 orange_color = ImVec4(.99f, .313f, .0f, 1.0f); @@ -1083,6 +1083,50 @@ void NotificationManager::ProgressBarWithCancelNotification::render_bar(const fl ImGuiPureWrap::text(text.c_str()); } +//------URLDownloadWithPrintablesLinkNotification---------------- +void NotificationManager::URLDownloadWithPrintablesLinkNotification::init() +{ + PopNotification::init(); + //m_lines_count++; + if (m_endlines.empty()) { + m_endlines.push_back(0); + } + + m_lines_count = 3; + m_multiline = true; + while (m_endlines.size() < 3) + m_endlines.push_back(m_endlines.back()); + + if(m_state == EState::Shown) + m_state = EState::NotFading; +} +bool NotificationManager::URLDownloadWithPrintablesLinkNotification::on_text_click() +{ + m_hypertext_callback_override(m_hypertext); + return false; +} +void NotificationManager::URLDownloadWithPrintablesLinkNotification::render_text(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + assert(m_multiline); + assert(m_text1.size() >= m_endlines[0] || m_text1.size() >= m_endlines[1]); + if(m_endlines[0] > m_text1.size() || m_endlines[1] > m_text1.size()) + return; + // 1 lines text (what doesn't fit, wont show), 1 line hypertext, 1 line bar + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height / 4); + ImGuiPureWrap::text(m_text1.substr(0, m_endlines[0]).c_str()); + + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height + m_line_height / 4); + std::string line = _u8L("Open Printables project page"); + //ImGuiPureWrap::text(line.c_str()); + render_hypertext(m_left_indentation, m_line_height + m_line_height / 4, line); + + if (m_has_cancel_button) + render_cancel_button(win_size_x, win_size_y, win_pos_x, win_pos_y); + render_bar(win_size_x, win_size_y, win_pos_x, win_pos_y); +} + //------URLDownloadNotification---------------- void NotificationManager::URLDownloadNotification::render_close_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) @@ -1271,11 +1315,11 @@ void NotificationManager::URLDownloadNotification::render_bar(const float win_si std::string line = text; bool did_shorten = false; while (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset) { - line = line.substr(0, line.length() - 1); + line = line.substr(0, line.size() - 1); did_shorten = true; } if (did_shorten && dots) { - line = line.substr(0, line.length() - 2); + line = line.substr(0, line.size() - 2); line += "..."; } return line; @@ -1356,47 +1400,61 @@ bool NotificationManager::PrintHostUploadNotification::push_background_color() void NotificationManager::PrintHostUploadNotification::generate_text() { - auto shorten_to_line = [this](const std::string& text, bool dots) -> std::string { + auto shorten_to_line = [this](const std::string& text, const std::string& endline) -> std::string { std::string line = text; bool did_shorten = false; - while (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset) { - line = line.substr(0, line.length() - 1); + float endline_width = ImGui::CalcTextSize(endline.c_str()).x; + while (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset - endline_width) { + line = line.substr(0, line.size() - 1); did_shorten = true; } - if (did_shorten && dots) { - line = line.substr(0, line.length() - 2); - line += "..."; + if (did_shorten && endline.size() != 0) { + line = line.substr(0, line.size() - endline.size()); + line += endline; } return line; }; - // whole text is no longer than 2 lines, filename is max 1 line long. - std::string rest = " -> " + (m_original_host == m_host ? m_host : m_host + " (" + m_original_host + ")"); - std::string line1; - if (ImGui::CalcTextSize(m_filename.c_str()).x > m_window_width - m_window_width_offset) { - line1 = shorten_to_line(m_filename, true); - } else { - line1 = shorten_to_line(m_filename + rest, false); - size_t over = line1.size() - m_filename.size(); - if (over < 0) - over = 0; - if (over < rest.size()) - rest = rest.substr(over); - else if (over >= rest.size()) - rest.clear(); - } - std::string line2 = shorten_to_line(rest, true); + if (m_hypertext_override) { + // always divide text into two lines if hypertext. second line is hypertext + m_normal_lines_count = 3; + std::string line1 = shorten_to_line(m_filename + " →", "… →"); + if (m_uj_state == UploadJobState::PB_COMPLETED || m_uj_state == UploadJobState::PB_COMPLETED_WITH_WARNING) { + m_text1 = line1; + m_hypertext = m_original_host; + m_text2 = "\n" + _u8L("UPLOADED") + "\n" + m_status_message; + } else { + m_text1 = line1 + "\n" + m_original_host; + } + } else { + // whole text is no longer than 2 lines, filename is max 1 line long. + std::string rest = " → " + (m_original_host == m_host ? m_host : m_host + " (" + m_original_host + ")"); + std::string line1; + if (ImGui::CalcTextSize(m_filename.c_str()).x > m_window_width - m_window_width_offset) { + line1 = shorten_to_line(m_filename, "…"); + } else { + line1 = shorten_to_line(m_filename + rest, {}); + size_t over = line1.size() - m_filename.size(); + if (over < 0) + over = 0; + if (over < rest.size()) + rest = rest.substr(over); + else if (over >= rest.size()) + rest.clear(); + } + std::string line2 = shorten_to_line(rest, "…"); - // ... if in total that makes more than 1 line, whole notification will behave as 3 line notification (as base height) - if (ImGui::CalcTextSize((line1 + line2).c_str()).x > m_window_width - m_window_width_offset) - m_normal_lines_count = 3; - else - m_normal_lines_count = 2; - - if (m_uj_state == UploadJobState::PB_COMPLETED || m_uj_state == UploadJobState::PB_COMPLETED_WITH_WARNING) - m_text1 = line1 + line2 + "\n" + _u8L("COMPLETED") + "\n" + m_status_message; - else - m_text1 = line1 + line2; + // ... if in total that makes more than 1 line, whole notification will behave as 3 line notification (as base height) + if (ImGui::CalcTextSize((line1 + line2).c_str()).x > m_window_width - m_window_width_offset) + m_normal_lines_count = 3; + else + m_normal_lines_count = 2; + + if (m_uj_state == UploadJobState::PB_COMPLETED || m_uj_state == UploadJobState::PB_COMPLETED_WITH_WARNING) + m_text1 = line1 + line2 + "\n" + _u8L("UPLOADED") + "\n" + m_status_message; + else + m_text1 = line1 + line2; + } } void NotificationManager::PrintHostUploadNotification::set_percentage(float percent) @@ -1414,6 +1472,12 @@ void NotificationManager::PrintHostUploadNotification::set_percentage(float perc } } +bool NotificationManager::PrintHostUploadNotification::on_text_click() { + if (m_callback_override != nullptr) + return m_callback_override(m_evt_handler); + return false; +} + //B64 void NotificationManager::PrintHostUploadNotification::set_waittime(int waittime) { @@ -1444,7 +1508,6 @@ void NotificationManager::PrintHostUploadNotification::render_text(const float w // If completed, whole text is part of m_text_1 and is rendered by PopNotification function. if (m_uj_state != UploadJobState::PB_COMPLETED && m_uj_state != UploadJobState::PB_COMPLETED_WITH_WARNING) { - // hypertext is not rendered at all. If it is needed, it needs to be added here. // m_endlines should have endline for each line and then for hypertext thus m_endlines[1] should always be in m_text1 if (m_endlines[0] != m_endlines[1]) { assert(m_text1.size() >= m_endlines[0] || m_text1.size() >= m_endlines[1]); @@ -1454,13 +1517,15 @@ void NotificationManager::PrintHostUploadNotification::render_text(const float w ImGui::SetCursorPosX(m_left_indentation); ImGui::SetCursorPosY(m_line_height / 4); ImGuiPureWrap::text(m_text1.substr(0, m_endlines[0]).c_str()); - ImGui::SetCursorPosX(m_left_indentation); - ImGui::SetCursorPosY(m_line_height + m_line_height / 4); - std::string line = m_text1.substr(m_endlines[0] + (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0), m_endlines[1] - m_endlines[0] - (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0)); - ImGuiPureWrap::text(line.c_str()); - // uncomment only if close and stop button should be next to each other - //if (m_has_cancel_button) - // render_cancel_button(win_size_x, win_size_y, win_pos_x, win_pos_y); + std::string line = m_text1.substr(m_endlines[0] + (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0), m_endlines[1] - m_endlines[0] - (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0)); + // if there is a hypertext, it is whole second line + if (m_hypertext_override) { + render_hypertext(m_left_indentation, m_line_height + m_line_height / 4, line); + } else { + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(m_line_height + m_line_height / 4); + ImGuiPureWrap::text(line.c_str()); + } render_bar(win_size_x, win_size_y, win_pos_x, win_pos_y); } else { @@ -1475,8 +1540,9 @@ void NotificationManager::PrintHostUploadNotification::render_text(const float w render_cancel_button(win_size_x, win_size_y, win_pos_x, win_pos_y); render_bar(win_size_x, win_size_y, win_pos_x, win_pos_y); } - } else - PopNotification::render_text(win_size_x, win_size_y, win_pos_x, win_pos_y); + } else { + PopNotification::render_text(win_size_x, win_size_y, win_pos_x, win_pos_y); + } } void NotificationManager::PrintHostUploadNotification::render_bar(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) { @@ -1537,8 +1603,7 @@ void NotificationManager::PrintHostUploadNotification::render_bar(const float wi } case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_COMPLETED: case Slic3r::GUI::NotificationManager::PrintHostUploadNotification::UploadJobState::PB_COMPLETED_WITH_WARNING: - // whole text with both "COMPLETED" and status message is generated in generate_text() - break; + break; } ImGuiPureWrap::text(text.c_str()); @@ -1704,10 +1769,11 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty switch ((*it).first) { case InfoItemType::CustomSupports: text += format(_L_PLURAL("%1$d object was loaded with custom supports.", "%1$d objects were loaded with custom supports.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::CustomSeam: text += format(_L_PLURAL("%1$d object was loaded with custom seam.", "%1$d objects were loaded with custom seam.", (*it).second), (*it).second) + "\n"; break; - case InfoItemType::MmuSegmentation: text += format(_L_PLURAL("%1$d object was loaded with multimaterial painting.", "%1$d objects were loaded with multimaterial painting.",(*it).second), (*it).second) + "\n"; break; + case InfoItemType::MmSegmentation: text += format(_L_PLURAL("%1$d object was loaded with multimaterial painting.", "%1$d objects were loaded with multimaterial painting.",(*it).second), (*it).second) + "\n"; break; case InfoItemType::VariableLayerHeight: text += format(_L_PLURAL("%1$d object was loaded with variable layer height.", "%1$d objects were loaded with variable layer height.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::Sinking: text += format(_L_PLURAL("%1$d object was loaded with partial sinking.", "%1$d objects were loaded with partial sinking.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::CutConnectors: text += format(_L_PLURAL("%1$d object was loaded as a part of cut object.", "%1$d objects were loaded as parts of cut object", (*it).second), (*it).second) + "\n"; break; + case InfoItemType::FuzzySkin: text += format(_L_PLURAL("%1$d object was loaded with fuzzy skin painting.", "%1$d objects were loaded with fuzzy skin painting.", (*it).second), (*it).second) + "\n"; break; default: BOOST_LOG_TRIVIAL(error) << "Unknown InfoItemType: " << (*it).second; break; } } @@ -2138,10 +2204,11 @@ void NotificationManager::push_notification(NotificationType type, const std::string& hypertext, std::function callback, const std::string& text_after, - int timestamp) + const int timestamp, + const bool multiline) { int duration = get_standard_duration(level); - push_notification_data({ type, level, duration, text, hypertext, callback, text_after }, timestamp); + push_notification_data({ type, level, duration, text, hypertext, callback, text_after }, timestamp, multiline); } void NotificationManager::push_delayed_notification(const NotificationType type, std::function condition_callback, int64_t initial_delay, int64_t delay_interval) @@ -2329,7 +2396,15 @@ void NotificationManager::push_exporting_finished_notification(const std::string { close_notification_of_type(NotificationType::ExportFinished); NotificationData data{ NotificationType::ExportFinished, NotificationLevel::RegularNotificationLevel, on_removable ? 0 : 20, _u8L("Exporting finished.") + "\n" + path }; - push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, on_removable, path, dir_path), 0); + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, on_removable, dir_path), 0); + set_slicing_progress_hidden(); +} + +void NotificationManager::push_bulk_exporting_finished_notification(const std::string& dir_path, bool on_removable) +{ + close_notification_of_type(NotificationType::ExportFinished); + NotificationData data{ NotificationType::ExportFinished, NotificationLevel::RegularNotificationLevel, on_removable ? 0 : 20, _u8L("Bulk export finished.") + "\n" + dir_path}; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, on_removable, dir_path), 0); set_slicing_progress_hidden(); } @@ -2449,6 +2524,20 @@ void NotificationManager::set_upload_job_notification_completed_with_warning(int } } +void NotificationManager::set_upload_job_notification_hypertext(int id, std::function callback) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::PrintHostUpload) { + PrintHostUploadNotification* phun = dynamic_cast(notification.get()); + if (phun->compare_job_id(id)) { + phun->set_hypertext_override(callback); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + break; + } + } + } +} + void NotificationManager::upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host) { for (std::unique_ptr& notification : m_pop_notifications) { @@ -2523,6 +2612,19 @@ void NotificationManager::push_download_URL_progress_notification(size_t id, con push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, id, user_action_callback), 0); } +void NotificationManager::push_download_URL_progress_notification_with_printables_link(size_t id, const std::string& text, const std::string& url, std::function user_action_callback, std::function hypertext_callback) +{ + // If already exists + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload && dynamic_cast(notification.get())->get_download_id() == id) { + return; + } + } + // push new one + NotificationData data{ NotificationType::URLDownload, NotificationLevel::ProgressBarNotificationLevel, 30, _u8L("Download") + ": " + text, url }; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, id, user_action_callback, hypertext_callback), 0); +} + void NotificationManager::set_download_URL_progress(size_t id, float percentage) { for (std::unique_ptr& notification : m_pop_notifications) { @@ -2809,9 +2911,9 @@ void NotificationManager::push_updated_item_info_notification(InfoItemType type) } } -bool NotificationManager::push_notification_data(const NotificationData& notification_data, int timestamp) +bool NotificationManager::push_notification_data(const NotificationData& notification_data, int timestamp, const bool multiline) { - return push_notification_data(std::make_unique(notification_data, m_id_provider, m_evt_handler), timestamp); + return push_notification_data(std::make_unique(notification_data, m_id_provider, m_evt_handler, multiline), timestamp); } bool NotificationManager::push_notification_data(std::unique_ptr notification, int timestamp) { diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index c0937bf..cf9fad3 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -136,6 +136,10 @@ enum class NotificationType BedTemperaturesDiffer, // Notification that shrinkage compensations for the used filaments differ. ShrinkageCompensationsDiffer, + // Notification about using wipe tower with different nozzle diameters. + WipeTowerNozzleDiameterDiffer, + // Notification about using supports with different nozzle diameters. + SupportNozzleDiameterDiffer, }; class NotificationManager @@ -175,7 +179,7 @@ public: // Push a NotificationType::CustomNotification with provided notification level and 10s for RegularNotificationLevel. // ErrorNotificationLevel are never faded out. void push_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext = "", - std::function callback = std::function(), const std::string& text_after = "", int timestamp = 0); + std::function callback = std::function(), const std::string& text_after = "", int timestamp = 0, bool multiline = false); // Pushes basic_notification with delay. See push_delayed_notification_data. void push_delayed_notification(const NotificationType type, std::function condition_callback, int64_t initial_delay, int64_t delay_interval); // Removes all notifications of type from m_waiting_notifications @@ -223,6 +227,7 @@ public: void set_sla(bool b) { set_fff(!b); } // Exporting finished, show this information with path, button to open containing folder and if ejectable - eject button void push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable); + void push_bulk_exporting_finished_notification(const std::string& dir_path, bool on_removable); // notifications with progress bar // print host upload void push_upload_job_notification(int id, float filesize, const std::string& filename, const std::string& host, float percentage = 0); @@ -234,6 +239,7 @@ public: void set_upload_job_notification_comp_on_100(int id, bool comp); void set_upload_job_notification_completed(int id); void set_upload_job_notification_completed_with_warning(int id); + void set_upload_job_notification_hypertext(int i, std::function callback); void upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host); void upload_job_notification_show_error(int id, const std::string& filename, const std::string& host); // Download App progress @@ -241,6 +247,7 @@ public: void set_download_progress_percentage(float percentage); // Download URL progress notif void push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback); + void push_download_URL_progress_notification_with_printables_link(size_t id, const std::string& text, const std::string& url, std::function user_action_callback, std::function hypertext_callback); void set_download_URL_progress(size_t id, float percentage); void set_download_URL_paused(size_t id); void set_download_URL_canceled(size_t id); @@ -340,7 +347,7 @@ private: Exporting }; - PopNotification(const NotificationData &n, NotificationIDProvider &id_provider, wxEvtHandler* evt_handler); + PopNotification(const NotificationData &n, NotificationIDProvider &id_provider, wxEvtHandler* evt_handler, bool multiline = false); virtual ~PopNotification() { if (m_id) m_id_provider.release_id(m_id); } virtual void render(GLCanvas3D& canvas, float initial_y, bool move_from_overlay, float overlay_width); // close will dissapear notification on next render @@ -578,6 +585,22 @@ private: std::string m_error_message; }; + class URLDownloadWithPrintablesLinkNotification : public URLDownloadNotification + { + public: + URLDownloadWithPrintablesLinkNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, size_t download_id, std::function user_action_callback, std::function hypertext_callback) + : URLDownloadNotification(n, id_provider, evt_handler, download_id, user_action_callback) + , m_hypertext_callback_override(hypertext_callback) + { + } + protected: + void render_text(const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void init() override; + bool on_text_click() override; + std::function m_hypertext_callback_override; + }; + class PrintHostUploadNotification : public ProgressBarNotification { public: @@ -605,6 +628,7 @@ private: set_percentage(percentage); } void set_percentage(float percent) override; + bool on_text_click() override; void cancel() { m_uj_state = UploadJobState::PB_CANCELLED; m_has_cancel_button = false; } void error() { m_uj_state = UploadJobState::PB_ERROR; m_has_cancel_button = false; init(); } bool compare_job_id(const int other_id) const { return m_job_id == other_id; } @@ -617,6 +641,12 @@ private: void complete_with_warning(); //B64 void set_waittime(int waittime); + void set_hypertext_override(std::function callback) + { + m_hypertext_override = true; + m_callback_override = callback; + init(); + } protected: void init() override; void count_spaces() override; @@ -649,6 +679,9 @@ private: bool m_more_hypertext_used { false }; // When m_complete_on_100 is set to false - percent >= 1 wont switch to PB_COMPLETED state. bool m_complete_on_100 { true }; + + bool m_hypertext_override { false }; + std::function m_callback_override; }; class SlicingProgressNotification : public ProgressBarNotification @@ -757,10 +790,15 @@ private: class ExportFinishedNotification : public PopNotification { public: - ExportFinishedNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, bool to_removable,const std::string& export_path,const std::string& export_dir_path) - : PopNotification(n, id_provider, evt_handler) + ExportFinishedNotification( + const NotificationData& n, + NotificationIDProvider& id_provider, + wxEvtHandler* evt_handler, + bool to_removable, + const std::string& export_dir_path + ): + PopNotification(n, id_provider, evt_handler) , m_to_removable(to_removable) - , m_export_path(export_path) , m_export_dir_path(export_dir_path) { m_multiline = true; @@ -831,7 +869,7 @@ private: //pushes notification into the queue of notifications that are rendered //can be used to create custom notification - bool push_notification_data(const NotificationData& notification_data, int timestamp); + bool push_notification_data(const NotificationData& notification_data, int timestamp, bool multiline = false); bool push_notification_data(std::unique_ptr notification, int timestamp); // Delayed notifications goes first to the m_waiting_notifications vector and only after remaining time is <= 0 // and condition callback is success, notification is regular pushed from update function. diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 7296b18..35ff30b 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -63,12 +63,13 @@ struct InfoItemAtributes { const std::map INFO_ITEMS{ // info_item Type info_item Name info_item BitmapName - { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports_" }, }, - { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, - { InfoItemType::CutConnectors, {L("Connectors"), "cut_connectors" }, }, - { InfoItemType::MmuSegmentation, {L("Multimaterial painting"), "mmu_segmentation_"}, }, - { InfoItemType::Sinking, {L("Sinking"), "sinking"}, }, - { InfoItemType::VariableLayerHeight, {L("Variable layer height"), "layers"}, }, + { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports_" }, }, + { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, + { InfoItemType::CutConnectors, {L("Connectors"), "cut_connectors" }, }, + { InfoItemType::MmSegmentation, {L("Multimaterial painting"), "mmu_segmentation_" }, }, + { InfoItemType::Sinking, {L("Sinking"), "sinking" }, }, + { InfoItemType::VariableLayerHeight, {L("Variable layer height"), "layers" }, }, + { InfoItemType::FuzzySkin, {L("Paint-on fuzzy skin"), "fuzzy_skin_painting_" }, }, }; ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index ffe9338..698db2e 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -52,6 +52,8 @@ enum class InfoItemType CustomSupports, CustomSeam, CutConnectors, + MmSegmentation, + FuzzySkin, MmuSegmentation, Sinking, VariableLayerHeight diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index a80d6c8..152dea5 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -10,6 +10,7 @@ #include #include #include "slic3r/GUI/Search.hpp" // IWYU pragma: keep +#include "slic3r/GUI/Field.hpp" #include "libslic3r/Exception.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/AppConfig.hpp" @@ -163,8 +164,10 @@ void OptionsGroup::change_opt_value(DynamicPrintConfig& config, const t_config_o str.pop_back(); percent = true; } - double val = std::stod(str); // locale-dependent (on purpose - the input is the actual content of the field) - ConfigOptionFloatsOrPercents* vec_new = new ConfigOptionFloatsOrPercents({ {val, percent} }); + + const bool is_na_value = opt_def->nullable && str == _(L("N/A")); + const FloatOrPercent val = is_na_value ? ConfigOptionFloatsOrPercentsNullable::nil_value() : FloatOrPercent{std::stod(str), percent}; + ConfigOptionFloatsOrPercents *vec_new = new ConfigOptionFloatsOrPercents({val}); config.option(opt_key)->set_at(vec_new, opt_index, opt_index); break; } @@ -1013,6 +1016,21 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = double_to_string(val); } } break; + case coFloatsOrPercents: { + if (config.option(opt_key)->is_nil()) { + ret = _(L("N/A")); + } else { + const auto &config_option = config.option(opt_key)->get_at(idx); + + text_value = double_to_string(config_option.value); + if (config_option.percent) { + text_value += "%"; + } + + ret = text_value; + } + break; + } case coBools: ret = config.option(opt_key)->values[idx]; break; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f7ceecb..d3556d6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,8 +1,11 @@ #include "Plater.hpp" +#include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/Jobs/UIThreadWorker.hpp" +#include "slic3r/Utils/QIDIConnect.hpp" #include #include +#include #include #include #include @@ -54,6 +57,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/miniz_extension.hpp" +#include "libslic3r/MultipleBeds.hpp" // For stl export #include "libslic3r/CSGMesh/ModelToCSGMesh.hpp" @@ -92,6 +96,7 @@ #include "../Utils/PrintHost.hpp" #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" +#include "../Utils/PresetUpdaterWrapper.hpp" #include "../Utils/Process.hpp" #include "RemovableDriveManager.hpp" #include "InstanceCheck.hpp" @@ -107,8 +112,10 @@ #include "UserAccountUtils.hpp" #include "DesktopIntegrationDialog.hpp" #include "WebViewDialog.hpp" +#include "WebViewPanel.hpp" #include "ConfigWizardWebViewPage.hpp" #include "PresetArchiveDatabase.hpp" +#include "BulkExportDialog.hpp" //B34 #include "Gizmos/GLGizmoEmboss.hpp" @@ -125,6 +132,8 @@ #include "Widgets/CheckBox.hpp" +#include "GL/glew.h" + using boost::optional; namespace fs = boost::filesystem; using Slic3r::_3DScene; @@ -147,6 +156,7 @@ wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); // BackgroundSlicingProcess finished either with success or error. wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); +wxDEFINE_EVENT(EVT_REGENERATE_BED_THUMBNAILS, SimpleEvent); // Plater::DropTarget @@ -224,6 +234,7 @@ enum ExportingStatus{ EXPORTING_TO_LOCAL }; + // Plater / private struct Plater::priv { @@ -235,11 +246,15 @@ struct Plater::priv // Data Slic3r::DynamicPrintConfig *config; // FIXME: leak? - Slic3r::Print fff_print; - Slic3r::SLAPrint sla_print; + std::vector> fff_prints; + std::vector> sla_prints; Slic3r::Model model; PrinterTechnology printer_technology = ptFFF; - Slic3r::GCodeProcessorResult gcode_result; + std::vector gcode_results; + + //y + Slic3r::Print fff_print; + Slic3r::SLAPrint sla_print; // GUI elements wxSizer* panel_sizer{ nullptr }; @@ -258,7 +273,6 @@ struct Plater::priv Preview *preview; std::unique_ptr notification_manager; std::unique_ptr user_account; - std::unique_ptr preset_archive_database; // Login dialog needs to be kept somewhere. // It is created inside evt Bind. But it might be closed from another event. LoginWebViewDialog* login_dialog { nullptr }; @@ -296,8 +310,12 @@ struct Plater::priv static const std::regex pattern_zip; static const std::regex pattern_printRequest; + //y20 + std::vector thumbnails; + priv(Plater *q, MainFrame *main_frame); ~priv(); + void init(); bool is_project_dirty() const { return dirty_state.is_dirty(); } bool is_presets_dirty() const { return dirty_state.is_presets_dirty(); } @@ -507,13 +525,12 @@ struct Plater::priv void on_object_select(SimpleEvent&); void on_right_click(RBtnEvent&); - void on_wipetower_moved(Vec3dEvent&); - void on_wipetower_rotated(Vec3dEvent&); void on_update_geometry(Vec3dsEvent<2>&); void on_3dcanvas_mouse_dragging_started(SimpleEvent&); void on_3dcanvas_mouse_dragging_finished(SimpleEvent&); void show_action_buttons(const bool is_ready_to_slice) const; + void show_autoslicing_action_buttons() const; bool can_show_upload_to_connect() const; // Set the bed shape to a single closed 2D polygon(array of two element arrays), // triangulate the bed and store the triangles into m_bed.m_triangles, @@ -541,6 +558,7 @@ struct Plater::priv void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type); ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type); + void regenerate_thumbnails(SimpleEvent&); void bring_instance_forward() const; @@ -558,8 +576,7 @@ struct Plater::priv std::string last_output_path; std::string last_output_dir_path; bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } - bool process_completed_with_error { false }; - + private: bool layers_height_allowed() const; @@ -583,8 +600,7 @@ private: // vector of all warnings generated by last slicing std::vector> current_warnings; - bool show_warning_dialog { false }; - + bool show_warning_dialog { false }; }; const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf|qidi)", std::regex::icase); @@ -595,18 +611,18 @@ 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); -Plater::priv::priv(Plater *q, MainFrame *main_frame) +Plater::priv::priv(Plater* q, MainFrame* main_frame) : q(q) , main_frame(main_frame) , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({ "bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance", "brim_width", "brim_separation", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material", - "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_extruder", + "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", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "first_layer_height", "min_layer_height", "max_layer_height", - "brim_width", "perimeters", "perimeter_extruder", "fill_density", "infill_extruder", "top_solid_layers", - "support_material", "support_material_extruder", "support_material_interface_extruder", + "brim_width", "perimeters", "perimeter_extruder", "fill_density", "infill_extruder", "top_solid_layers", + "support_material", "support_material_extruder", "support_material_interface_extruder", "support_material_contact_distance", "support_material_bottom_contact_distance", "raft_layers", //B52 "bed_exclude_area" @@ -614,17 +630,24 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) , user_account(std::make_unique(q, wxGetApp().app_config, wxGetApp().get_instance_hash_string())) - , preset_archive_database(std::make_unique(wxGetApp().app_config, q)) - , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} - , m_sla_import_dlg{new SLAImportDialog{q}} + , m_worker{ q, std::make_unique(notification_manager.get()), "ui_worker" } + , m_sla_import_dlg{ new SLAImportDialog{q} } , delayed_scene_refresh(false) , view_toolbar(GLToolbar::Radio, "View") , collapse_toolbar(GLToolbar::Normal, "Collapse") , m_project_filename(wxEmptyString) +{} + +void Plater::priv::init() { - background_process.set_fff_print(&fff_print); - background_process.set_sla_print(&sla_print); - background_process.set_gcode_result(&gcode_result); + for (int i = 0; i < s_multiple_beds.get_max_beds(); ++i) { + gcode_results.emplace_back(); + fff_prints.emplace_back(std::make_unique()); + sla_prints.emplace_back(std::make_unique()); + } + background_process.set_fff_print(fff_prints.front().get()); + background_process.set_sla_print(sla_prints.front().get()); + background_process.set_gcode_result(&gcode_results.front()); background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); @@ -636,12 +659,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) auto statuscb = [this](const Slic3r::PrintBase::SlicingStatus &status) { wxQueueEvent(this->q, new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status)); }; - fff_print.set_status_callback(statuscb); - sla_print.set_status_callback(statuscb); + std::for_each(fff_prints.begin(), fff_prints.end(), [statuscb](std::unique_ptr& p) { p->set_status_callback(statuscb); }); + std::for_each(sla_prints.begin(), sla_prints.end(), [statuscb](std::unique_ptr& p) { p->set_status_callback(statuscb); }); this->q->Bind(EVT_SLICING_UPDATE, &priv::on_slicing_update, this); view3D = new View3D(q, bed, &model, config, &background_process); - preview = new Preview(q, bed, &model, config, &background_process, &gcode_result, [this]() { schedule_background_process(); }); + preview = new Preview(q, bed, &model, config, &background_process, &gcode_results, [this]() { schedule_background_process(); }); // set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); @@ -683,16 +706,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); }); view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); - view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); + view3D_canvas->Bind(EVT_GLCANVAS_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_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) { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); }); - view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_MOVED, &priv::on_wipetower_moved, this); - view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_ROTATED, &priv::on_wipetower_rotated, this); + view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_TOUCHED,[this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_RESET_SKEW, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); @@ -715,23 +738,24 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // 3DScene/Toolbar: view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); - view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [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_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); }); - view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); }); - view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); }); - view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [q](SimpleEvent&) { q->decrease_instances(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE_CURRENT_BED, [this](SimpleEvent&) { this->q->arrange_current_bed(); }); + 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(); }); + view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [this](SimpleEvent&) { q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); } - view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); + view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [this](SimpleEvent&) { q->set_bed_shape(); }); // Preview events: preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [this](SimpleEvent&) { q->set_bed_shape(); }); if (wxGetApp().is_editor()) { preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); @@ -744,8 +768,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->Bind(EVT_SLICING_COMPLETED, &priv::on_slicing_completed, this); q->Bind(EVT_PROCESS_COMPLETED, &priv::on_process_completed, this); q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this); - q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); - q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview"); }); + q->Bind(EVT_GLVIEWTOOLBAR_3D, [this](SimpleEvent&) { q->select_view_3D("3D"); }); + q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [this](SimpleEvent&) { q->select_view_3D("Preview"); }); + q->Bind(EVT_REGENERATE_BED_THUMBNAILS, &priv::regenerate_thumbnails, this); } // Drop target: @@ -782,9 +807,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, [](PresetUpdateAvailableClickedEvent&) { GUI_App &app = wxGetApp(); - app.get_preset_updater()->on_update_notification_confirm(app.plater()->get_preset_archive_database()->get_selected_archive_repositories()); + app.get_preset_updater_wrapper()->on_update_notification_confirm(); }); - this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this, q](RemovableDriveEjectEvent &evt) { + this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent &evt) { if (evt.data.second) { q->show_action_buttons(); notification_manager->close_notification_of_type(NotificationType::ExportFinished); @@ -800,7 +825,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) ); } }); - this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this, q](RemovableDrivesChangedEvent &) { + this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent &) { q->show_action_buttons(); // Close notification ExportingFinished but only if last export was to removable notification_manager->device_ejected(); @@ -894,11 +919,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) #endif // SLIC3R_DESKTOP_INTEGRATION #endif // __linux__ std::string service; - if (evt.GetString().Find(L"accounts.google.com") != wxString::npos) { + if (evt.GetString().Find(L"accounts.google.com") != wxNOT_FOUND) { service = "google"; - } else if (evt.GetString().Find(L"appleid.apple.com") != wxString::npos) { + } else if (evt.GetString().Find(L"appleid.apple.com") != wxNOT_FOUND) { service = "apple"; - } else if (evt.GetString().Find(L"facebook.com") != wxString::npos) { + } else if (evt.GetString().Find(L"facebook.com") != wxNOT_FOUND) { service = "facebook"; } wxString url = user_account->get_login_redirect_url(service); @@ -907,13 +932,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN_WIZARD, open_external_login); this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN, open_external_login); - + this->q->Bind(EVT_UA_LOGGEDOUT, [this](UserAccountSuccessEvent& evt) { user_account->clear(); std::string text = _u8L("Logged out from QIDI Account."); this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); - this->main_frame->remove_connect_webview_tab(); + this->main_frame->on_account_logout(); this->main_frame->refresh_account_menu(true); // Update sidebar printer status sidebar->update_printer_presets_combobox(); @@ -931,15 +956,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // std::string who = user_account->get_username(); // std::string username; // if (user_account->on_user_id_success(evt.data, username)) { - // // Do not show notification on refresh. // 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->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); + // this->main_frame->on_account_login(user_account->get_access_token()); + // } else { + // // refresh do different operations than on_account_login + // this->main_frame->on_account_did_refresh(user_account->get_access_token()); // } - // this->main_frame->add_connect_webview_tab(); // // Update User name in TopBar // this->main_frame->refresh_account_menu(); // wxGetApp().update_wizard_login_page(); @@ -952,7 +980,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // user_account->clear(); // this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); // this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to QIDI Account.")); - // this->main_frame->remove_connect_webview_tab(); + // this->main_frame->on_account_logout(); // // Update User name in TopBar // this->main_frame->refresh_account_menu(true); // // Update sidebar printer status @@ -965,14 +993,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // user_account->clear(); // this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); // this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to QIDI Account.")); - // this->main_frame->remove_connect_webview_tab(); + // this->main_frame->on_account_logout(); // // Update User name in TopBar // this->main_frame->refresh_account_menu(true); // // Update sidebar printer status // sidebar->update_printer_presets_combobox(); // }); // this->q->Bind(EVT_UA_FAIL, [this](UserAccountFailEvent& evt) { - // BOOST_LOG_TRIVIAL(error) << "Failed communication with QIDI Account: " << evt.data; + // BOOST_LOG_TRIVIAL(error) << "Failed communication with Connect Printer endpoint: " << evt.data; // user_account->on_communication_fail(); // }); // this->q->Bind(EVT_UA_QIDICONNECT_STATUS_SUCCESS, [this](UserAccountSuccessEvent& evt) { @@ -1026,7 +1054,19 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_UA_REFRESH_TIME, [this](UserAccountTimeEvent& evt) { this->user_account->set_refresh_time(evt.data); - }); + }); + this->q->Bind(EVT_UA_ENQUEUED_REFRESH, [this](SimpleEvent& evt) { + this->main_frame->on_account_will_refresh(); + }); + + this->q->Bind(EVT_PRINTABLES_CONNECT_PRINT, [this](wxCommandEvent& evt) { + if (!this->user_account->is_logged()) { + // show login dialog instead of print dialog + this->user_account->do_login(); + return; + } + this->q->printables_to_connect_gcode(into_u8(evt.GetString())); + }); } wxGetApp().other_instance_message_handler()->init(this->q); @@ -1036,7 +1076,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) bool is_collapsed = get_config_bool("collapsed_sidebar"); sidebar->collapse(is_collapsed); } - } +} Plater::priv::~priv() { @@ -1048,26 +1088,19 @@ Plater::priv::~priv() void Plater::priv::update(unsigned int flags) { - // the following line, when enabled, causes flickering on NVIDIA graphics cards -// wxWindowUpdateLocker freeze_guard(q); - if (get_config_bool("autocenter")) - model.center_instances_around_point(this->bed.build_volume().bed_center()); - unsigned int update_status = 0; const bool force_background_processing_restart = this->printer_technology == ptSLA || (flags & (unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); if (force_background_processing_restart) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data. update_status = this->update_background_process(false, flags & (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + s_multiple_beds.update_shown_beds(model, q->build_volume(), true); this->view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH); this->preview->reload_print(); if (force_background_processing_restart) this->restart_background_process(update_status); else this->schedule_background_process(); - - if (get_config_bool("autocenter") && this->sidebar->obj_manipul()->IsShown()) - this->sidebar->obj_manipul()->UpdateAndShow(true); } void Plater::priv::select_view(const std::string& direction) @@ -1206,6 +1239,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); std::vector obj_idxs; + boost::optional qidislicer_generator_version; int answer_convert_from_meters = wxOK_DEFAULT; int answer_convert_from_imperial_units = wxOK_DEFAULT; @@ -1265,7 +1299,13 @@ std::vector Plater::priv::load_files(const std::vector& input_ { DynamicPrintConfig config_loaded; ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; - model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, only_if(load_config, Model::LoadAttribute::CheckVersion)); + 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); @@ -1294,8 +1334,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (! config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); - if (load_config) - this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; + 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(); + } } if (load_config) { @@ -1319,8 +1361,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ 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); + //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); @@ -1563,6 +1605,20 @@ std::vector Plater::priv::load_files(const std::vector& input_ GLGizmoSimplify::add_simplify_suggestion_notification( obj_idxs, model.objects, *notification_manager); + if ( + qidislicer_generator_version + && *qidislicer_generator_version < Semver("2.9.0-alpha1") + ) { + BOOST_LOG_TRIVIAL(info) << "Rearranging legacy project..."; + s_multiple_beds.rearrange_after_load(model, q->build_volume()); + } + q->canvas3D()->check_volumes_outside_state(); + 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; } @@ -1622,6 +1678,9 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode instance->set_scaling_factor(instance->get_scaling_factor() / max_ratio); scaled_down = true; } + + if (! s_multiple_beds.get_loading_project_flag()) + instance->set_offset(instance->get_offset() + s_multiple_beds.get_bed_translation(s_multiple_beds.get_active_bed())); } object->ensure_on_bed(allow_negative_z); @@ -1823,11 +1882,42 @@ void Plater::priv::selection_changed() void Plater::priv::object_list_changed() { + if (!wxGetApp().plater()) + return; const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty()); - // XXX: is this right? - const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; + // + 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()) { + s_print_statuses[bed_index] = PrintStatus::empty; + } + for (const ModelObject *object : wxGetApp().model().objects) { + for (const ModelInstance *instance : object->instances) { + const auto it{s_multiple_beds.get_inst_map().find(instance->id())}; + if ( + it != s_multiple_beds.get_inst_map().end() + && it->second == bed_index + && instance->printable + && instance->print_volume_state == ModelInstancePVS_Partly_Outside + ) { + s_print_statuses[bed_index] = PrintStatus::outside; + break; + } + } + } + } + } else { + if (model.objects.empty()) { + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::empty; + } + } - sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits); + sidebar->enable_buttons( + s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed()) + && !export_in_progress + && is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()]) + ); } void Plater::priv::select_all() @@ -1896,7 +1986,7 @@ void Plater::priv::delete_all_objects_from_model() view3D->enable_layers_editing(false); reset_gcode_toolpaths(); - gcode_result.reset(); + 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(); @@ -1914,7 +2004,8 @@ void Plater::priv::delete_all_objects_from_model() // The hiding of the slicing results, if shown, is not taken care by the background process, so we do it here sidebar->show_sliced_info_sizer(false); - model.custom_gcode_per_print_z.gcodes.clear(); + for (CustomGCode::Info& info : model.get_custom_gcode_per_print_z_vector()) + info.gcodes.clear(); } void Plater::priv::reset() @@ -1929,7 +2020,7 @@ void Plater::priv::reset() view3D->enable_layers_editing(false); reset_gcode_toolpaths(); - gcode_result.reset(); + std::for_each(gcode_results.begin(), gcode_results.end(), [](auto& g) { g.reset(); }); view3D->get_canvas3d()->reset_sequential_print_clearance(); @@ -1946,7 +2037,8 @@ void Plater::priv::reset() // The hiding of the slicing results, if shown, is not taken care by the background process, so we do it here this->sidebar->show_sliced_info_sizer(false); - model.custom_gcode_per_print_z.gcodes.clear(); + for (CustomGCode::Info& info : model.get_custom_gcode_per_print_z_vector()) + info.gcodes.clear(); } void Plater::priv::mirror(Axis axis) @@ -1989,7 +2081,11 @@ void Plater::priv::split_object() // load all model objects at once, otherwise the plate would be rearranged after each one // causing original positions not to be kept + // Note that load_model_objects shifts the object to the active bed. To prevent it, + // raise a flag as if a project was being loaded. + s_multiple_beds.set_loading_project_flag(true); std::vector idxs = load_model_objects(new_objects); + s_multiple_beds.set_loading_project_flag(false); // clear previosli selection get_selection().clear(); @@ -2016,8 +2112,8 @@ void Plater::priv::scale_selection_to_fit_print_volume() void Plater::priv::schedule_background_process() { delayed_error_message.clear(); - // Trigger the timer event after 0.5s - this->background_process_timer.Start(500, wxTIMER_ONE_SHOT); + // Trigger the timer event after 0.1s + this->background_process_timer.Start(100, wxTIMER_ONE_SHOT); // Notify the Canvas3D that something has changed, so it may invalidate some of the layer editing stuff. this->view3D->get_canvas3d()->set_config(this->config); } @@ -2032,19 +2128,23 @@ void Plater::priv::process_validation_warning(const std::vector& wa if (warnings.empty()) notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - // Always close warnings BedTemperaturesDiffer and ShrinkageCompensationsDiffer before next processing. + // Always close warnings BedTemperaturesDiffer, ShrinkageCompensationsDiffer, WipeTowerNozzleDiameterDiffer and SupportNozzleDiameterDiffer before next processing. notification_manager->close_notification_of_type(NotificationType::BedTemperaturesDiffer); notification_manager->close_notification_of_type(NotificationType::ShrinkageCompensationsDiffer); + notification_manager->close_notification_of_type(NotificationType::WipeTowerNozzleDiameterDiffer); + notification_manager->close_notification_of_type(NotificationType::SupportNozzleDiameterDiffer); for (std::string text : warnings) { std::string hypertext = ""; + std::string text_after = ""; + bool multiline = false; NotificationType notification_type = NotificationType::ValidateWarning; std::function action_fn = [](wxEvtHandler*){ return false; }; if (text == "_SUPPORTS_OFF") { text = _u8L("An object has custom support enforcers which will not be used " "because supports are disabled.")+"\n"; - hypertext = _u8L("Enable supports for enforcers only"); + hypertext = _u8L("Enable supports for enforcers only."); action_fn = [](wxEvtHandler*) { Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); assert(print_tab); @@ -2062,53 +2162,223 @@ void Plater::priv::process_validation_warning(const std::vector& wa text = _u8L("Filament shrinkage will not be used because filament shrinkage " "for the used filaments differs significantly."); notification_type = NotificationType::ShrinkageCompensationsDiffer; + } else if (text == "_WIPE_TOWER_NOZZLE_DIAMETER_DIFFER") { + text = _u8L("Using the wipe tower for extruders with different nozzle diameters " + "is experimental, so proceed with caution."); + notification_type = NotificationType::WipeTowerNozzleDiameterDiffer; + } else if (text == "_SUPPORT_NOZZLE_DIAMETER_DIFFER") { + // TRN: This is a first part of the notification text: + // "Printing supports with different nozzle diameters is experimental. For best results, switch to Organic supports and assign a specific extruder for supports." + text = _u8L("Printing supports with different nozzle diameters " + "is experimental. For best results, switch to Organic supports and"); + // TRN: This is a second part (hyperlink) of the notification text: + // "Printing supports with different nozzle diameters is experimental. For best results, switch to Organic supports and assign a specific extruder for supports." + hypertext = _u8L("assign a specific extruder for supports."); + multiline = true; + notification_type = NotificationType::SupportNozzleDiameterDiffer; + action_fn = [](wxEvtHandler*) { + GUI::wxGetApp().jump_to_option("support_material_extruder", Preset::Type::TYPE_PRINT, boost::nowide::widen("Multiple Extruders")); + return false; + }; } notification_manager->push_notification( notification_type, NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + text, hypertext, action_fn + _u8L("WARNING:") + "\n" + text, hypertext, action_fn, text_after, 0, multiline ); } } +std::vector apply_to_inactive_beds( + Model &model, + std::vector> &prints, + const DynamicPrintConfig &config +) { + std::vector result(MAX_NUMBER_OF_BEDS); + for (std::size_t bed_index{0}; bed_index < prints.size(); ++bed_index) { + const std::unique_ptr &print{prints[bed_index]}; + if (!print || bed_index == s_multiple_beds.get_active_bed()) { + continue; + } + using MultipleBedsUtils::with_single_bed_model_fff; + with_single_bed_model_fff(model, bed_index, [&](){ + result[bed_index] = print->apply(model, config); + }); + } + return result; +} + +void Plater::priv::regenerate_thumbnails(SimpleEvent&) { + const int num{s_multiple_beds.get_number_of_beds()}; + if (num <= 1 || num > MAX_NUMBER_OF_BEDS) { + return; + } + + //y20 + thumbnails.clear(); + ThumbnailData data; + ThumbnailsParams params; + params.parts_only = true; + params.printable_only = true; + params.show_bed = true; + params.transparent_background = true; + int w = 100, h = 100; + + int curr_bound_texture = 0; + glsafe(glGetIntegerv(GL_TEXTURE_BINDING_2D, &curr_bound_texture)); + int curr_unpack_alignment = 0; + glsafe(glGetIntegerv(GL_UNPACK_ALIGNMENT, &curr_unpack_alignment)); + glsafe(glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(glDeleteTextures(s_bed_selector_thumbnail_texture_ids.size(), s_bed_selector_thumbnail_texture_ids.data())); + s_bed_selector_thumbnail_changed.fill(false); + + s_bed_selector_thumbnail_texture_ids.resize(num); + glsafe(glGenTextures(num, s_bed_selector_thumbnail_texture_ids.data())); + for (int i = 0; i < num; ++i) { + s_multiple_beds.set_thumbnail_bed_idx(i); + generate_thumbnail(data, w, h, params, GUI::Camera::EType::Ortho); + + //y20 + thumbnails.push_back(data); + + s_multiple_beds.set_thumbnail_bed_idx(-1); + glsafe(glBindTexture(GL_TEXTURE_2D, s_bed_selector_thumbnail_texture_ids[i])); + glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + glsafe(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, static_cast(w), static_cast(h), 0, GL_RGBA, GL_UNSIGNED_BYTE, data.pixels.data())); + s_bed_selector_thumbnail_changed[i] = true; + } + glsafe(glBindTexture(GL_TEXTURE_2D, curr_bound_texture)); + glsafe(glPixelStorei(GL_UNPACK_ALIGNMENT, curr_unpack_alignment)); +} // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages) { +// assert(! s_beds_just_switched || background_process.idle()); + + int active_bed = s_multiple_beds.get_active_bed(); + background_process.set_temp_output_path(active_bed); + background_process.set_fff_print(&q->active_fff_print()); + background_process.set_sla_print(&q->active_sla_print()); + background_process.set_gcode_result(&gcode_results[active_bed]); + background_process.select_technology(this->printer_technology); + + + if (s_beds_just_switched && printer_technology == ptFFF) { + PrintBase::SlicingStatus status(q->active_fff_print(), -1); + SlicingStatusEvent evt(EVT_SLICING_UPDATE, 0, status); + on_slicing_update(evt); + s_beds_just_switched = false; + // we hide slicing notification here to prevent empty notification and it will init itself again via the incoming progress (if there is any) + notification_manager->set_slicing_progress_hidden(); + notification_manager->close_notification_of_type(NotificationType::ExportOngoing); + q->sidebar().show_sliced_info_sizer(background_process.finished()); + } + // bitmap of enum UpdateBackgroundProcessReturnState unsigned int return_state = 0; + if (s_multiple_beds.is_autoslicing()) { + return_state = return_state | UPDATE_BACKGROUND_PROCESS_FORCE_RESTART; + } // Get the config ready. The binary gcode flag depends on Preferences, which the backend has no access to. DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); if (full_config.has("binary_gcode")) // needed for SLA full_config.set("binary_gcode", bool(full_config.opt_bool("binary_gcode") & wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"))); + // Set printer_model in full_config a model without repo prefix (including inherited profiles) const Preset &selected_printer = wxGetApp().preset_bundle->printers.get_selected_preset(); - std::string printer_model_serialized = full_config.option("printer_model")->serialize(); - std::string vendor_repo_prefix; + std::string printer_model = selected_printer.config.opt_string("printer_model"); + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(selected_printer); + printer_model = selected_printer.trim_vendor_repo_prefix(printer_model, printer_with_vendor.vendor); + full_config.set("printer_model", printer_model); + if (selected_printer.vendor) { - vendor_repo_prefix = selected_printer.vendor->repo_prefix; - } else if (std::string inherits = selected_printer.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model_serialized.find(vendor_repo_prefix) == 0) { - printer_model_serialized = printer_model_serialized.substr(vendor_repo_prefix.size()); - boost::trim_left(printer_model_serialized); - full_config.set("printer_model", printer_model_serialized); + // Passing extra info about preset vendor and version, so these can be inserted as metadata to GCode + full_config.set("profile_vendor", selected_printer.vendor->name, true); + full_config.set("profile_version", selected_printer.vendor->config_version.to_string(), true); } + // If the update_background_process() was not called by the timer, kill the timer, // so the update_restart_background_process() will not be called again in vain. background_process_timer.Stop(); // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); + + Print::ApplyStatus invalidated{Print::ApplyStatus::APPLY_STATUS_INVALIDATED}; + bool was_running = background_process.running(); + using MultipleBedsUtils::with_single_bed_model_fff; + using MultipleBedsUtils::with_single_bed_model_sla; + + std::vector apply_statuses{ + printer_technology == ptFFF ? + apply_to_inactive_beds(q->model(), q->p->fff_prints, full_config) : + std::vector(1) + }; + // Apply new config to the possibly running background task. - bool was_running = background_process.running(); - Print::ApplyStatus invalidated = background_process.apply(q->model(), full_config); + 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); + 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); + apply_statuses[0] = invalidated; + }); + } else { + throw std::runtime_error{"Ivalid printer technology!"}; + } + + for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) { + if (printer_technology == ptFFF) { + if (apply_statuses[bed_index] != Print::ApplyStatus::APPLY_STATUS_UNCHANGED) { + s_print_statuses[bed_index] = PrintStatus::idle; + } + } else if (printer_technology == ptSLA) { + if (apply_statuses[0] != Print::ApplyStatus::APPLY_STATUS_UNCHANGED) { + s_print_statuses[bed_index] = PrintStatus::idle; + } + } else { + throw std::runtime_error{"Ivalid printer technology!"}; + } + } + + if (printer_technology == ptFFF) { + for (std::size_t bed_index{0}; bed_index < q->p->fff_prints.size(); ++bed_index) { + const std::unique_ptr &print{q->p->fff_prints[bed_index]}; + using MultipleBedsUtils::with_single_bed_model_fff; + with_single_bed_model_fff(model, bed_index, [&](){ + std::vector warnings; + std::string err{print->validate(&warnings)}; + if (!err.empty()) { + s_print_statuses[bed_index] = PrintStatus::invalid; + } + }); + } + } + + const bool any_status_changed{std::any_of( + apply_statuses.begin(), + apply_statuses.end(), + [](Print::ApplyStatus status){ + return status != Print::ApplyStatus::APPLY_STATUS_UNCHANGED; + } + )}; + + if (any_status_changed) { + this->show_autoslicing_action_buttons(); + } + + // If current bed was invalidated, update thumbnails for all beds: + if (any_status_changed) { + wxQueueEvent(this->q, new SimpleEvent(EVT_REGENERATE_BED_THUMBNAILS)); + } // Just redraw the 3D canvas without reloading the scene to consume the update of the layer height profile. if (view3D->is_layers_editing_enabled()) @@ -2188,10 +2458,19 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool } std::vector warnings; std::string err = background_process.validate(&warnings); - if (!err.empty()) + 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); + } return return_state; + } } - + if (! this->delayed_error_message.empty()) // Reusing the old state. return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; @@ -2204,7 +2483,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool actualize_slicing_warnings(*this->background_process.current_print()); actualize_object_warnings(*this->background_process.current_print()); show_warning_dialog = false; - process_completed_with_error = false; } if (invalidated != Print::APPLY_STATUS_UNCHANGED && was_running && ! this->background_process.running() && @@ -2220,7 +2498,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool const wxString invalid_str = _L("Invalid data"); for (auto btn : {ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export}) sidebar->set_btn_label(btn, invalid_str); - process_completed_with_error = true; + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::invalid; } else { @@ -2236,7 +2514,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool const wxString slice_string = background_process.running() && wxGetApp().get_mode() == comSimple ? _L("Slicing") + dots : _L("Slice now"); sidebar->set_btn_label(ActionButtonType::Reslice, slice_string); - if (background_process.finished()) show_action_buttons(false); else if (!background_process.empty() && @@ -2247,6 +2524,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool show_action_buttons(true); } + this->q->object_list_changed(); return return_state; } @@ -2258,12 +2536,17 @@ bool Plater::priv::restart_background_process(unsigned int state) return false; } - if ( ! this->background_process.empty() && - (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && - ( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) || - (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || - (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { + if ( + !this->background_process.empty() + && (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 + && ( + ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && !this->background_process.finished()) + || (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 + || (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 + ) + ) { // The print is valid and it can be started. + if (this->background_process.start()) { if (!show_warning_dialog) on_slicing_began(); @@ -2421,6 +2704,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->supported_facets.assign(old_volume->supported_facets); new_volume->seam_facets.assign(old_volume->seam_facets); new_volume->mm_segmentation_facets.assign(old_volume->mm_segmentation_facets); + new_volume->fuzzy_skin_facets.assign(old_volume->fuzzy_skin_facets); } std::swap(old_model_object->volumes[volume_idx], old_model_object->volumes.back()); old_model_object->delete_volume(old_model_object->volumes.size() - 1); @@ -2779,8 +3063,15 @@ void Plater::priv::set_current_panel(wxPanel* panel) bool force_render = (current_panel != nullptr); #endif // __WXMAC__ - if (current_panel == panel) - return; + ScopeGuard guard([]() { s_reload_preview_after_switching_beds = false; }); + + if (current_panel == panel) { + if (! s_reload_preview_after_switching_beds) + return; + else { + update_background_process(); + } + } wxPanel* old_panel = current_panel; current_panel = panel; @@ -2808,6 +3099,12 @@ void Plater::priv::set_current_panel(wxPanel* panel) panel_sizer->Layout(); if (current_panel == view3D) { + + if(s_multiple_beds.stop_autoslice(true)) { + sidebar->switch_from_autoslicing_mode(); + update_background_process(); + } + if (old_panel == preview) preview->get_canvas3d()->unbind_event_handlers(); @@ -2839,17 +3136,22 @@ void Plater::priv::set_current_panel(wxPanel* panel) if (wxGetApp().is_editor()) { // see: Plater::priv::object_list_changed() - // FIXME: it may be better to have a single function making this check and let it be called wherever needed bool export_in_progress = this->background_process.is_export_scheduled(); - bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; - if (!model.objects.empty() && !export_in_progress && model_fits) { + if ( + s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed()) + && !export_in_progress + && is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()]) + ) { preview->get_canvas3d()->init_gcode_viewer(); - if (! this->background_process.finished()) - preview->load_gcode_shells(); + preview->get_canvas3d()->load_gcode_shells(); q->reslice(); } // keeps current gcode preview, if any preview->reload_print(); + + if (! s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed())) + preview->get_canvas3d()->reset_gcode_toolpaths(); + } preview->set_as_dirty(); @@ -2927,36 +3229,71 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) this->preview->reload_print(); } - if ((evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) && - static_cast(evt.status.warning_step) == psAlertWhenSupportsNeeded && - !get_app_config()->get_bool("alert_when_supports_needed")) { - // This alerts are from psAlertWhenSupportsNeeded and the respective app settings is not Enabled, so discard the alerts. - } else if (evt.status.flags & - (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { - // Update notification center with warnings of object_id and its warning_step. - ObjectID object_id = evt.status.warning_object_id; - int warning_step = evt.status.warning_step; - PrintStateBase::StateWithWarnings state; - if (evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { - state = this->printer_technology == ptFFF ? - this->fff_print.step_state_with_warnings(static_cast(warning_step)) : - this->sla_print.step_state_with_warnings(static_cast(warning_step)); - } else if (this->printer_technology == ptFFF) { - const PrintObject *print_object = this->fff_print.get_object(object_id); - if (print_object) - state = print_object->step_state_with_warnings(static_cast(warning_step)); - } else { - const SLAPrintObject *print_object = this->sla_print.get_object(object_id); - if (print_object) - state = print_object->step_state_with_warnings(static_cast(warning_step)); + + std::vector object_ids = { evt.status.warning_object_id }; + std::vector warning_steps = { evt.status.warning_step }; + std::vector flagss = { int(evt.status.flags) }; + + if (warning_steps.front() == -1) { + flagss = { PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS, PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS }; + notification_manager->close_slicing_errors_and_warnings(); + } + + for (int flags : flagss ) { + if (warning_steps.front() == -1) { + warning_steps.clear(); + if (flags == PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { + int i = 0; + while (i < int(printer_technology == ptFFF ? psCount : slapsCount)) { warning_steps.push_back(i); ++i; } + } else { + int i = 0; + while (i < int(printer_technology == ptFFF ? posCount : slaposCount)) { warning_steps.push_back(i); ++i; } + for (const PrintObject* po : wxGetApp().plater()->active_fff_print().objects()) + object_ids.push_back(po->id()); + } + } + + + + + + for (int warning_step : warning_steps) { + for (ObjectID object_id : object_ids) { + if ((flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) && + static_cast(warning_step) == psAlertWhenSupportsNeeded && + !get_app_config()->get_bool("alert_when_supports_needed")) { + // This alerts are from psAlertWhenSupportsNeeded and the respective app settings is not Enabled, so discard the alerts. + } + else if (flags & + (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { + // Update notification center with warnings of object_id and its warning_step. + + PrintStateBase::StateWithWarnings state; + if (flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { + state = this->printer_technology == ptFFF ? + q->active_fff_print().step_state_with_warnings(static_cast(warning_step)) : + q->active_sla_print().step_state_with_warnings(static_cast(warning_step)); + } + else if (this->printer_technology == ptFFF) { + const PrintObject* print_object = q->active_fff_print().get_object(object_id); + if (print_object) + state = print_object->step_state_with_warnings(static_cast(warning_step)); + } + else { + const SLAPrintObject* print_object = q->active_sla_print().get_object(object_id); + if (print_object) + state = print_object->step_state_with_warnings(static_cast(warning_step)); + } + // Now process state.warnings. + for (auto const& warning : state.warnings) { + if (warning.current) { + notification_manager->push_slicing_warning_notification(warning.message, false, object_id, warning_step); + add_warning(warning, object_id.id); + } + } + } + } } - // Now process state.warnings. - for (auto const& warning : state.warnings) { - if (warning.current) { - notification_manager->push_slicing_warning_notification(warning.message, false, object_id, warning_step); - add_warning(warning, object_id.id); - } - } } } @@ -2983,6 +3320,7 @@ void Plater::priv::on_slicing_began() notification_manager->close_notification_of_type(NotificationType::SignDetected); notification_manager->close_notification_of_type(NotificationType::ExportFinished); notification_manager->set_slicing_progress_began(); + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::running; } void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid) { @@ -3034,14 +3372,9 @@ bool Plater::priv::warnings_dialog() return true; std::string text = _u8L("There are active warnings concerning sliced models:") + "\n"; for (auto const& it : current_critical_warnings) { - size_t next_n = it.first.message.find_first_of('\n', 0); - text += "\n"; - if (next_n != std::string::npos) - text += it.first.message.substr(0, next_n); - else - text += it.first.message; + text += "\n\n"; + text += it.first.message; } - //MessageDialog msg_wingow(this->q, from_u8(text), wxString(SLIC3R_APP_NAME " ") + _L("generated warnings"), wxOK); // Changed to InfoDialog so it can show hyperlinks InfoDialog msg_wingow(this->q, format_wxstr("%1% %2%", SLIC3R_APP_NAME, _L("generated warnings")), from_u8(text), true); const auto res = msg_wingow.ShowModal(); @@ -3079,15 +3412,19 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) const wxString invalid_str = _L("Invalid data"); for (auto btn : { ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export }) sidebar->set_btn_label(btn, invalid_str); - process_completed_with_error = true; } has_error = true; + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::invalid; } if (evt.cancelled()) { this->notification_manager->set_slicing_progress_canceled(_u8L("Slicing Cancelled.")); + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::idle; } this->sidebar->show_sliced_info_sizer(evt.success()); + if (evt.success()) { + s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::finished; + } // This updates the "Slice now", "Export G-code", "Arrange" buttons status. // Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables @@ -3179,7 +3516,8 @@ void Plater::priv::on_right_click(RBtnEvent& evt) if (evt.data.second) { // right button was clicked on empty space if (!get_selection().is_empty()) // several objects are selected in 3DScene return; - menu = menus.default_menu(); + if (s_multiple_beds.get_last_hovered_bed() != -1) + menu = menus.default_menu(); } else menu = menus.multi_selection_menu(); @@ -3232,23 +3570,6 @@ void Plater::priv::on_right_click(RBtnEvent& evt) } } -void Plater::priv::on_wipetower_moved(Vec3dEvent &evt) -{ - DynamicPrintConfig cfg; - cfg.opt("wipe_tower_x", true)->value = evt.data(0); - cfg.opt("wipe_tower_y", true)->value = evt.data(1); - wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg); -} - -void Plater::priv::on_wipetower_rotated(Vec3dEvent& evt) -{ - DynamicPrintConfig cfg; - cfg.opt("wipe_tower_x", true)->value = evt.data(0); - cfg.opt("wipe_tower_y", true)->value = evt.data(1); - cfg.opt("wipe_tower_rotation_angle", true)->value = Geometry::rad2deg(evt.data(2)); - wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg); -} - void Plater::priv::on_update_geometry(Vec3dsEvent<2>&) { // TODO @@ -3275,6 +3596,8 @@ void Plater::priv::generate_thumbnail(ThumbnailData& data, unsigned int w, unsig ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type) { + s_multiple_beds.set_thumbnail_bed_idx(s_multiple_beds.get_active_bed()); + ScopeGuard guard([]() { s_multiple_beds.set_thumbnail_bed_idx(-1); }); ThumbnailsList thumbnails; for (const Vec2d& size : params.sizes) { thumbnails.push_back(ThumbnailData()); @@ -3714,9 +4037,9 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice_) const const bool connect_gcode_shown = print_host_opt == nullptr && can_show_upload_to_connect(); //y18 - const bool local_has_devices = false; + bool local_has_devices = false; if(main_frame->m_printer_view) - const bool local_has_devices = main_frame->m_printer_view->Local_has_device(); + local_has_devices = main_frame->m_printer_view->Local_has_device(); //y #if QDT_RELEASE_TO_PUBLIC @@ -3755,6 +4078,36 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice_) const } } +void Plater::priv::show_autoslicing_action_buttons() const { + if (!s_multiple_beds.is_autoslicing()) { + return; + } + + DynamicPrintConfig* selected_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + const auto print_host_opt = selected_printer_config ? selected_printer_config->option("print_host") : nullptr; + const bool connect_gcode_shown = print_host_opt == nullptr && can_show_upload_to_connect(); + + RemovableDriveManager::RemovableDrivesStatus removable_media_status = wxGetApp().removable_drive_manager()->status(); + + bool updated{sidebar->show_export_all(true)}; + updated = sidebar->show_connect_all(connect_gcode_shown) || updated; + updated = sidebar->show_export_removable_all(removable_media_status.has_removable_drives) || updated; + if (updated) { + sidebar->Layout(); + } + + bool all_finished{true}; + for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) { + const std::unique_ptr &print{this->fff_prints[bed_index]}; + if (!print->finished() && is_sliceable(s_print_statuses[bed_index])) { + all_finished = false; + break; + } + } + + sidebar->enable_bulk_buttons(all_finished); +} + void Plater::priv::enter_gizmos_stack() { assert(m_undo_redo_stack_active == &m_undo_redo_stack_main); @@ -3811,13 +4164,6 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed if (view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; - //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - if (this->printer_technology == ptFFF) { - const DynamicPrintConfig &config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - model.wipe_tower.position = Vec2d(config.opt_float("wipe_tower_x"), config.opt_float("wipe_tower_y")); - model.wipe_tower.rotation = config.opt_float("wipe_tower_rotation_angle"); - } const GLGizmosManager& gizmos = view3D->get_canvas3d()->get_gizmos_manager(); if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator && get_config_bool("clear_undo_redo_stack_on_new_project")) @@ -3887,13 +4233,6 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator } // Save the last active preset name of a particular printer technology. ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); - //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - if (this->printer_technology == ptFFF) { - const DynamicPrintConfig &config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - model.wipe_tower.position = Vec2d(config.opt_float("wipe_tower_x"), config.opt_float("wipe_tower_y")); - model.wipe_tower.rotation = config.opt_float("wipe_tower_rotation_angle"); - } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; // Flags made of Snapshot::Flags enum values. unsigned int new_flags = it_snapshot->snapshot_data.flags; @@ -3943,22 +4282,6 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator // This also switches the printer technology based on the printer technology of the active printer profile. wxGetApp().load_current_presets(); } - //FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. - if (this->printer_technology == ptFFF) { - const DynamicPrintConfig ¤t_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - Vec2d current_position(current_config.opt_float("wipe_tower_x"), current_config.opt_float("wipe_tower_y")); - double current_rotation = current_config.opt_float("wipe_tower_rotation_angle"); - if (current_position != model.wipe_tower.position || current_rotation != model.wipe_tower.rotation) { - DynamicPrintConfig new_config; - new_config.set_key_value("wipe_tower_x", new ConfigOptionFloat(model.wipe_tower.position.x())); - new_config.set_key_value("wipe_tower_y", new ConfigOptionFloat(model.wipe_tower.position.y())); - new_config.set_key_value("wipe_tower_rotation_angle", new ConfigOptionFloat(model.wipe_tower.rotation)); - Tab *tab_print = wxGetApp().get_tab(Preset::TYPE_PRINT); - tab_print->load_config(new_config); - tab_print->update_dirty(); - } - } // set selection mode for ObjectList on sidebar this->sidebar->obj_list()->set_selection_mode(new_selected_settings_on_sidebar ? ObjectList::SELECTION_MODE::smSettings : new_selected_layer_on_sidebar ? ObjectList::SELECTION_MODE::smLayer : @@ -3990,6 +4313,7 @@ void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bo this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances); this->view3D->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot); + s_multiple_beds.update_shown_beds(model, q->build_volume(), false); wxGetApp().obj_list()->update_after_undo_redo(); if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) { @@ -4042,9 +4366,11 @@ void Plater::priv::bring_instance_forward() const Plater::Plater(wxWindow *parent, MainFrame *main_frame) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size(parent)) - , p(new priv(this, main_frame)) { - // Initialization performed in the private c-tor + // Creation and initialization is separated, the initialization needs + // Plater::p pointer already set up. + p = std::make_unique(this, main_frame); + p->init(); } Plater::~Plater() = default; @@ -4062,6 +4388,7 @@ void Plater::render_project_state_debug_window() const { p->render_project_state Sidebar& Plater::sidebar() { return *p->sidebar; } const Model& Plater::model() const { return p->model; } Model& Plater::model() { return p->model; } +//y const Print& Plater::fff_print() const { return p->fff_print; } Print& Plater::fff_print() { return p->fff_print; } const SLAPrint& Plater::sla_print() const { return p->sla_print; } @@ -4122,6 +4449,9 @@ void Plater::load_project(const wxString& filename) p->reset(); + 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()) { // At least one file was loaded. p->set_project_filename(filename); @@ -4778,7 +5108,7 @@ void Plater::load_gcode(const wxString& filename) m_last_loaded_gcode = filename; // cleanup view before to start loading/processing - p->gcode_result.reset(); + std::for_each(p->gcode_results.begin(), p->gcode_results.end(), [](auto& g) { g.reset(); }); reset_gcode_toolpaths(); p->preview->reload_print(); p->get_current_canvas3D()->render(); @@ -4792,7 +5122,13 @@ void Plater::load_gcode(const wxString& filename) p->notification_manager->push_download_progress_notification("Loading...", []() { return false; }); processor.process_file(filename.ToUTF8().data(), [this](float value) { p->notification_manager->set_download_progress_percentage(value); - p->get_current_canvas3D()->render(); + static auto clock = std::chrono::steady_clock(); + static auto old_t = clock.now(); + auto t = clock.now(); + if (std::chrono::duration_cast(t - old_t).count() > 200) { + p->get_current_canvas3D()->render(); + old_t = t; + } }); } catch (const std::exception& ex) @@ -4800,7 +5136,7 @@ void Plater::load_gcode(const wxString& filename) show_error(this, ex.what()); return; } - p->gcode_result = std::move(processor.extract_result()); + p->gcode_results.front() = std::move(processor.extract_result()); // show results try @@ -4810,7 +5146,7 @@ void Plater::load_gcode(const wxString& filename) catch (const std::exception&) { wxEndBusyCursor(); - p->gcode_result.reset(); + p->gcode_results.front().reset(); reset_gcode_toolpaths(); set_default_bed_shape(); p->preview->reload_print(); @@ -5029,6 +5365,11 @@ void Plater::reload_print() p->preview->reload_print(); } +void Plater::object_list_changed() +{ + p->object_list_changed(); +} + std::vector Plater::load_files(const std::vector& input_files, bool load_model, bool load_config, bool imperial_units /*= false*/) { return p->load_files(input_files, load_model, load_config, imperial_units); } // To be called when providing a list of files to the GUI slic3r on command line. @@ -5572,6 +5913,10 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* return false; // searches for project files + + bool load_just_one_file = paths.size() == 1; + bool load_config = true; + for (std::vector::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) { std::string filename = (*it).filename().string(); @@ -5580,7 +5925,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* 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) { + 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())) || @@ -5632,9 +5977,31 @@ 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), + 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" + "Would you like to continue anyway?"), filename), + format_wxstr("%1% - %2%", SLIC3R_APP_NAME, _L("Drag and drop several files")), wxYES_NO); + if (dlg.ShowModal() == wxID_NO) + return false; + } return preview_zip_archive(*it); - } + else if (handle_as_project) + load_config = false; + } + + if (!load_config) { + // Means that we DnDing several files and 3mf file(s) is/are among selection + // And as a result we need to upload just a geometry from all files + WarningDialog dlg(static_cast(this), _L("You have several files for loading.\n" + "Please note that only geometry will be uploaded from all 3mf files.\n\n" + "Would you like to continue anyway?"), + format_wxstr("%1% - %2%", SLIC3R_APP_NAME, _L("Drag and drop several files")), wxYES_NO); + if (dlg.ShowModal() == wxID_NO) + return false; } // other files @@ -5655,7 +6022,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* } } Plater::TakeSnapshot snapshot(this, snapshot_label); - load_files(paths); + load_files(paths, true, load_config); return true; } @@ -5755,9 +6122,6 @@ void Plater::increase_instances(size_t num, int obj_idx, int inst_idx) model_object->add_instance(trafo); } - if (p->get_config_bool("autocenter")) - arrange(); - p->update(); p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); @@ -5862,7 +6226,7 @@ void Plater::fill_bed_with_instances() }; auto scene = arr2::Scene{ - build_scene(*this, ArrangeSelectionMode::SelectionOnly)}; + build_scene(*this, ArrangeSelectionMode::CurrentBedSelectionOnly)}; cbs.on_finished = [this](arr2::FillBedTaskResult &result) { auto [prototype_mi, pos] = arr2::find_instance_by_id(model(), result.prototype_id); @@ -5957,6 +6321,22 @@ void Plater::toggle_layers_editing(bool enable) void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& new_objects) { + const int active_bed{s_multiple_beds.get_active_bed()}; + + const ModelInstancePtrs &instances{model().objects[obj_idx]->instances}; + const ObjectID first_instance_id{instances.front()->id()}; + const std::optional first_instance_bed_index{ + s_multiple_beds.get_inst_map().count(first_instance_id) > 0 ? + std::optional{s_multiple_beds.get_inst_map().at(first_instance_id)} : + std::nullopt + }; + + const auto bed_index{ + instances.size() == 1 && first_instance_bed_index ? + *first_instance_bed_index : + active_bed + }; + model().delete_object(obj_idx); sidebar().obj_list()->delete_object_from_list(obj_idx); @@ -5965,6 +6345,7 @@ void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& ne // now process all updates of the 3d scene update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call for (size_t idx = 0; idx < p->model.objects.size(); idx++) @@ -5972,12 +6353,20 @@ void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& ne Selection& selection = p->get_selection(); size_t last_id = p->model.objects.size() - 1; - for (size_t i = 0; i < new_objects.size(); ++i) + for (size_t i = 0; i < new_objects.size(); ++i) { selection.add_object((unsigned int)(last_id - i), i == 0); + const ModelInstance* mi = p->model.objects[last_id - i]->instances.front(); + const ObjectID instance_id{mi->id().id}; + s_multiple_beds.set_instance_bed(instance_id, mi->printable, bed_index); + } + + s_multiple_beds.inst_map_updated(); + s_multiple_beds.set_active_bed(bed_index); UIThreadWorker w; - arrange(w, true); + arrange(w, ArrangeSelectionMode::CurrentBedSelectionOnly); w.wait_for_idle(); + s_multiple_beds.set_active_bed(active_bed); } @@ -6009,14 +6398,24 @@ static wxString check_binary_vs_ascii_gcode_extension(PrinterTechnology pt, cons // This function should be deleted when binary G-codes become more common. The dialog is there to make the // transition period easier for the users, because bgcode files are not recognized by older firmwares // without any error message. -static void alert_when_exporting_binary_gcode(bool binary_output, const std::string& printer_notes) +void alert_when_exporting_binary_gcode(const std::string& printer_notes) { - if (binary_output - && (boost::algorithm::contains(printer_notes, "PRINTER_MODEL_XL") - || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MINI") - || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK4") - || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK3.9"))) - { + 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"; @@ -6038,7 +6437,112 @@ static void alert_when_exporting_binary_gcode(bool binary_output, const std::str } } +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. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) { + return std::nullopt; + } + const std::string path{ + this->p->background_process.output_filepath_for_project( + into_path(get_project_filename(".3mf")) + ) + }; + return fs::path(Slic3r::fold_utf8_to_ascii(path)); + } catch (const Slic3r::PlaceholderParserError &ex) { + // Show the error with monospaced font. + show_error(this, ex.what(), true); + return std::nullopt; + } catch (const std::exception &ex) { + show_error(this, ex.what(), false); + return std::nullopt; + } + +} +std::string get_output_start_dir(const bool prefer_removable, const fs::path &default_output_file) { + const AppConfig &appconfig{*wxGetApp().app_config}; + RemovableDriveManager &removable_drive_manager{*wxGetApp().removable_drive_manager()}; + // Get a last save path, either to removable media or to an internal media. + std::string last_output_dir{appconfig.get_last_output_dir( + default_output_file.parent_path().string(), + prefer_removable + )}; + if (!prefer_removable) { + return last_output_dir; + } + + // Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database. + std::string removable_dir{removable_drive_manager.get_removable_drive_path(last_output_dir)}; + if (removable_dir.empty()) { + // Direct user to the last internal media. + return appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); + } + + return removable_dir; +} + +std::optional Plater::check_output_path_has_error(const boost::filesystem::path& path) const { + const std::string filename = path.filename().string(); + const std::string ext = boost::algorithm::to_lower_copy(path.extension().string()); + if (has_illegal_characters(filename)) { + return { + _L("The provided file name is not valid.") + "\n" + + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\"" + }; + } + if (this->printer_technology() == ptFFF) { + bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode"); + bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"); + const wxString error{check_binary_vs_ascii_gcode_extension( + printer_technology(), ext, supports_binary && uses_binary + )}; + if (!error.IsEmpty()) { + return error; + } + } + return std::nullopt; +}; + +std::optional Plater::get_output_path(const std::string &start_dir, const fs::path &default_output_file) { + const std::string ext = default_output_file.extension().string(); + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SL1 / SL1S file as:"), + start_dir, + from_path(default_output_file.filename()), + printer_technology() == ptFFF ? GUI::file_wildcards(FT_GCODE, ext) : + GUI::sla_wildcards(active_sla_print().printer_config().sla_archive_format.value.c_str(), ext), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + if (dlg.ShowModal() != wxID_OK) { + return std::nullopt; + } + + const fs::path output_path{into_path(dlg.GetPath())}; + if (auto error{check_output_path_has_error(output_path)}) { + const t_link_clicked on_link_clicked = [](const std::string& key) -> void { wxGetApp().jump_to_option(key); }; + ErrorDialog(this, *error, on_link_clicked).ShowModal(); + return std::nullopt; + } + return output_path; +} + +std::optional Plater::get_multiple_output_dir(const std::string &start_dir) { + wxDirDialog dlg( + this, + _L("Choose export directory:"), + start_dir + ); + + if (dlg.ShowModal() != wxID_OK) { + return std::nullopt; + } + + const fs::path output_path{into_path(dlg.GetPath())}; + return output_path; +} void Plater::export_gcode(bool prefer_removable) { @@ -6048,97 +6552,175 @@ void Plater::export_gcode(bool prefer_removable) if (canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) return; - - if (p->process_completed_with_error) + if (!is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])) return; // If possible, remove accents from accented latin characters. // This function is useful for generating file names to be processed by legacy firmwares. - fs::path default_output_file; - try { - // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. - // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. - unsigned int state = this->p->update_restart_background_process(false, false); - if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) - return; - default_output_file = this->p->background_process.output_filepath_for_project(into_path(get_project_filename(".3mf"))); - } catch (const Slic3r::PlaceholderParserError &ex) { - // Show the error with monospaced font. - show_error(this, ex.what(), true); - return; - } catch (const std::exception &ex) { - show_error(this, ex.what(), false); + const auto optional_default_output_file{this->get_default_output_file()}; + if (!optional_default_output_file) { return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - AppConfig &appconfig = *wxGetApp().app_config; - RemovableDriveManager &removable_drive_manager = *wxGetApp().removable_drive_manager(); - // Get a last save path, either to removable media or to an internal media. - std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); - if (prefer_removable) { - // Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database. - start_dir = removable_drive_manager.get_removable_drive_path(start_dir); - if (start_dir.empty()) - // Direct user to the last internal media. - start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); - } + const fs::path &default_output_file{*optional_default_output_file}; + const std::string start_dir{get_output_start_dir(prefer_removable, default_output_file)}; - fs::path output_path; - { - std::string ext = default_output_file.extension().string(); - wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SL1 / SL1S file as:"), - start_dir, - from_path(default_output_file.filename()), - printer_technology() == ptFFF ? GUI::file_wildcards(FT_GCODE, ext) : - GUI::sla_wildcards(p->sla_print.printer_config().sla_archive_format.value.c_str(), ext), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT + const auto optional_output_path{get_output_path(start_dir, default_output_file)}; + if (!optional_output_path) { + return; + } + 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") ); - if (dlg.ShowModal() == wxID_OK) { - output_path = into_path(dlg.GetPath()); + } + export_gcode_to_path(output_path, [&](const bool path_on_removable_media){ + p->export_gcode(output_path, path_on_removable_media, PrintHostJob()); + }); +} - auto check_for_error = [this](const boost::filesystem::path& path, wxString& err_out) -> bool { - const std::string filename = path.filename().string(); - const std::string ext = boost::algorithm::to_lower_copy(path.extension().string()); - if (has_illegal_characters(filename)) { - err_out = _L("The provided file name is not valid.") + "\n" + - _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""; - return true; - } - if (this->printer_technology() == ptFFF) { - bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode"); - bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"); - err_out = check_binary_vs_ascii_gcode_extension(printer_technology(), ext, supports_binary && uses_binary); - } - return !err_out.IsEmpty(); - }; +void Plater::export_gcode_to_path( + const fs::path &output_path, + const std::function &export_callback +) { + AppConfig &appconfig{*wxGetApp().app_config}; + RemovableDriveManager &removable_drive_manager{*wxGetApp().removable_drive_manager()}; + bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string()); + p->notification_manager->new_export_began(path_on_removable_media); + p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; + p->last_output_path = output_path.string(); + p->last_output_dir_path = output_path.parent_path().string(); + export_callback(path_on_removable_media); + // Storing a path to AppConfig either as path to removable media or a path to internal media. + // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives + // while the dialog was open. + appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media); +} - wxString error_str; - if (check_for_error(output_path, error_str)) { - const t_link_clicked on_link_clicked = [](const std::string& key) -> void { wxGetApp().jump_to_option(key); }; - ErrorDialog(this, error_str, on_link_clicked).ShowModal(); - output_path.clear(); - } else if (printer_technology() == ptFFF) { - bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode"); - bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"); - alert_when_exporting_binary_gcode(supports_binary && uses_binary, - wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes")); - } - } +struct PrintToExport { + std::reference_wrapper print; + std::reference_wrapper processor_result; + int bed{}; +}; + +void Plater::with_mocked_fff_background_process( + Print &print, + GCodeProcessorResult &result, + const int bed_index, + const std::function &callable +) { + Print *original_print{&active_fff_print()}; + GCodeProcessorResult *original_result{this->p->background_process.get_gcode_result()}; + const int original_bed{s_multiple_beds.get_active_bed()}; + PrinterTechnology original_technology{this->printer_technology()}; + ScopeGuard guard{[&](){ + this->p->background_process.set_fff_print(original_print); + this->p->background_process.set_gcode_result(original_result); + this->p->background_process.select_technology(original_technology); + s_multiple_beds.set_active_bed(original_bed); + }}; + + this->p->background_process.set_fff_print(&print); + this->p->background_process.set_gcode_result(&result); + this->p->background_process.select_technology(this->p->printer_technology); + s_multiple_beds.set_active_bed(bed_index); + + callable(); +} + +void Plater::export_all_gcodes(bool prefer_removable) { + const auto optional_default_output_file{this->get_default_output_file()}; + if (!optional_default_output_file) { + return; } - if (! output_path.empty()) { - bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string()); - p->notification_manager->new_export_began(path_on_removable_media); - p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; - p->last_output_path = output_path.string(); - p->last_output_dir_path = output_path.parent_path().string(); - p->export_gcode(output_path, path_on_removable_media, PrintHostJob()); - // Storing a path to AppConfig either as path to removable media or a path to internal media. - // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives - // while the dialog was open. - appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media); - - } + const fs::path &default_output_file{*optional_default_output_file}; + const std::string start_dir{get_output_start_dir(prefer_removable, default_output_file)}; + const auto optional_output_dir{get_multiple_output_dir(start_dir)}; + if (!optional_output_dir) { + return; + } + const fs_path &output_dir{*optional_output_dir}; + + + std::map prints_to_export; + std::vector >> paths; + + for (int print_index{0}; print_index < s_multiple_beds.get_number_of_beds(); ++print_index) { + const std::unique_ptr &print{this->get_fff_prints()[print_index]}; + if (!print || !is_sliceable(s_print_statuses[print_index])) { + paths.emplace_back(print_index, std::nullopt); + continue; + } + + fs::path default_filename{default_output_file.filename()}; + 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; + } + const fs::path &default_file{*optional_file}; + default_filename = default_file.filename(); + } + ); + + const fs::path filename{ + default_filename.stem().string() + + "_bed" + + std::to_string(print_index + 1) + + default_filename.extension().string() + }; + const fs::path output_file{output_dir / filename}; + prints_to_export.insert({ + print_index, + {*print, this->p->gcode_results[print_index], print_index} + }); + paths.emplace_back(print_index, output_file); + } + + BulkExportDialog dialog{paths}; + if (dialog.ShowModal() != wxID_OK) { + return; + } + const std::vector>> output_paths{dialog.get_paths()}; + + bool path_on_removable_media{false}; + for (auto &[bed_index, optional_path] : output_paths) { + if (!optional_path) { + continue; + } + + const PrintToExport &print_to_export{prints_to_export.at(bed_index)}; + const fs::path &path{*optional_path}; + with_mocked_fff_background_process( + print_to_export.print, + print_to_export.processor_result, + print_to_export.bed, + [&](){ + this->p->background_process.set_temp_output_path(print_to_export.bed); + export_gcode_to_path( + path, + [&](const bool on_removable){ + this->p->background_process.finalize_gcode( + path.string(), + path_on_removable_media + ); + path_on_removable_media = on_removable || path_on_removable_media; + } + ); + } + ); + } + + p->notification_manager->push_bulk_exporting_finished_notification(output_dir.string(), path_on_removable_media); } void Plater::export_stl_obj(bool extended, bool selection_only) @@ -6207,7 +6789,7 @@ void Plater::export_stl_obj(bool extended, bool selection_only) auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) { TriangleMesh mesh; - const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); + const SLAPrintObject *object = this->active_sla_print().get_print_object_by_model_object_id(mo.id()); if (!object || !object->get_mesh_to_print() || object->get_mesh_to_print()->empty()) { if (!extended) @@ -6420,6 +7002,21 @@ ThumbnailData Plater::get_thumbnailldate_send() p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_SEND.first, THUMBNAIL_SIZE_SEND.second, thumbnail_params, Camera::EType::Ortho); return thumbnail_data; } + +//y20 +ThumbnailData Plater::get_thumbnailldate_from_bed(int num) +{ + return p->thumbnails[num]; +} + +int Plater::get_beds_num() { + return s_multiple_beds.get_number_of_beds(); +}; + +int Plater::get_active_bed() { + return s_multiple_beds.get_active_bed(); +}; + bool Plater::export_3mf(const boost::filesystem::path& output_path) { if (p->model.objects.empty()) { @@ -6511,7 +7108,7 @@ void Plater::export_toolpaths_to_obj() const void Plater::reslice() { // There is "invalid data" button instead "slice now" - if (p->process_completed_with_error) + if (!is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])) return; // In case SLA gizmo is in editing mode, refuse to continue @@ -6635,19 +7232,27 @@ bool load_secret(const std::string& id, const std::string& opt, std::string& usr #endif // wxUSE_SECRETSTORE } } -void Plater::connect_gcode() + + +void Plater::printables_to_connect_gcode(const std::string& url) { + PrintablesConnectUploadDialog dialog(this, url); + dialog.ShowModal(); + +} + +std::optional Plater::get_connect_print_host_job() { assert(p->user_account->is_logged()); std::string dialog_msg; { PrinterPickWebViewDialog dialog(this, dialog_msg); if (dialog.ShowModal() != wxID_OK) { - return; + return std::nullopt; } } if (dialog_msg.empty()) { show_error(this, _L("Failed to select a printer.")); - return; + return std::nullopt; } BOOST_LOG_TRIVIAL(debug) << "Message from Printer pick webview: " << dialog_msg; @@ -6662,9 +7267,9 @@ void Plater::connect_gcode() */ const Preset* selected_printer_preset = &wxGetApp().preset_bundle->printers.get_selected_preset(); - boost::property_tree::ptree ptree; + boost::property_tree::ptree ptree; const std::string filename = UserAccountUtils::get_keyword_from_json(ptree, dialog_msg, "filename"); - const std::string team_id = UserAccountUtils::get_keyword_from_json(ptree, dialog_msg, "team_id"); + const std::string team_id = UserAccountUtils::get_keyword_from_json(ptree, dialog_msg, "team_id"); std::string data_subtree = UserAccountUtils::get_print_data_from_json(dialog_msg, "data"); if (filename.empty() || team_id.empty() || data_subtree.empty()) { @@ -6672,9 +7277,10 @@ void Plater::connect_gcode() BOOST_LOG_TRIVIAL(error) << msg; BOOST_LOG_TRIVIAL(error) << "Response: " << dialog_msg; show_error(this, msg); - return; + return std::nullopt; } - + + PhysicalPrinter ph_printer("connect_temp_printer", wxGetApp().preset_bundle->physical_printers.default_config(), *selected_printer_preset); ph_printer.config.set_key_value("host_type", new ConfigOptionEnum(htQIDIConnectNew)); @@ -6688,8 +7294,80 @@ void Plater::connect_gcode() upload_job.upload_data.data_json = data_subtree; upload_job.upload_data.upload_path = boost::filesystem::path(filename); - p->export_gcode(fs::path(), false, std::move(upload_job)); + return upload_job; +} +void Plater::connect_gcode() +{ + if (auto upload_job{get_connect_print_host_job()}) { + p->export_gcode(fs::path(), false, std::move(*upload_job)); + } +} + +void Plater::connect_gcode_all() { + auto optional_upload_job{get_connect_print_host_job()}; + if (!optional_upload_job) { + return; + } + + // PringHostJob does not have copy constructor. + // Make a new job from this template for each bed. + const PrintHostJob &upload_job_template{*optional_upload_job}; + const auto print_host_ptr{dynamic_cast(upload_job_template.printhost.get())}; + if (print_host_ptr == nullptr) { + throw std::runtime_error{"Sending to connect requires QIDIConnectNew host."}; + } + const QIDIConnectNew connect{*print_host_ptr}; + + std::vector >> paths; + + for (std::size_t print_index{0}; print_index < s_multiple_beds.get_number_of_beds(); ++print_index) { + const std::unique_ptr &print{this->get_fff_prints()[print_index]}; + if (!print || !is_sliceable(s_print_statuses[print_index])) { + 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() + }); + } + + BulkExportDialog dialog{paths}; + if (dialog.ShowModal() != wxID_OK) { + return; + } + const std::vector>> output_paths{dialog.get_paths()}; + + for (const auto &key_value : output_paths) { + const int bed_index{key_value.first}; + const std::optional &optional_path{key_value.second}; + if (!optional_path) { + continue; + } + const fs::path &path{*optional_path}; + const std::unique_ptr &print{this->get_fff_prints()[bed_index]}; + if (!print || print->empty()) { + continue; + } + with_mocked_fff_background_process( + *print, + this->p->gcode_results[bed_index], + bed_index, + [&](){ + this->p->background_process.set_temp_output_path(bed_index); + PrintHostJob upload_job; + upload_job.upload_data = upload_job_template.upload_data; + upload_job.printhost = std::make_unique(connect); + upload_job.cancelled = upload_job_template.cancelled; + upload_job.upload_data.upload_path = path; + this->p->background_process.prepare_upload(upload_job); + } + ); + } } std::string Plater::get_upload_filename() @@ -6781,9 +7459,8 @@ void Plater::send_gcode() only_link = true; } max_send_number = std::stoi(wxGetApp().app_config->get("max_send")); - //B61 - PrintHostSendDialog dlg(default_output_file, PrintHostPostUploadAction::StartPrint, groups, storage_paths, storage_names, this, - (this->fff_print().print_statistics()), only_link); + //B61 //y20 + PrintHostSendDialog dlg(default_output_file, PrintHostPostUploadAction::StartPrint, groups, storage_paths, storage_names, this, only_link); if (dlg.ShowModal() == wxID_OK) { //y16 bool is_jump = false; @@ -6799,10 +7476,12 @@ void Plater::send_gcode() return; } - bool supports_binary = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_bool("binary_gcode"); - bool uses_binary = wxGetApp().app_config->get_bool("use_binary_gcode_when_supported"); - alert_when_exporting_binary_gcode(supports_binary && uses_binary, - wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes")); + alert_when_exporting_binary_gcode( + wxGetApp(). + preset_bundle->printers. + get_edited_preset(). + config.opt_string("printer_notes") + ); } //B53 //B62 //B64 @@ -7184,7 +7863,7 @@ std::vector Plater::get_extruder_color_strings_from_plater_config(c std::vector Plater::get_color_strings_for_color_print(const GCodeProcessorResult* const result) const { std::vector colors = get_extruder_color_strings_from_plater_config(result); - colors.reserve(colors.size() + p->model.custom_gcode_per_print_z.gcodes.size()); + colors.reserve(colors.size() + p->model.custom_gcode_per_print_z().gcodes.size()); if (wxGetApp().is_gcode_viewer() && result != nullptr) { for (const CustomGCode::Item& code : result->custom_gcode_per_print_z) { @@ -7193,7 +7872,7 @@ std::vector Plater::get_color_strings_for_color_print(const GCodePr } } else { - for (const CustomGCode::Item& code : p->model.custom_gcode_per_print_z.gcodes) { + for (const CustomGCode::Item& code : p->model.custom_gcode_per_print_z().gcodes) { if (code.type == CustomGCode::ColorChange) colors.emplace_back(code.color); } @@ -7280,18 +7959,33 @@ static std::string concat_strings(const std::set &strings, void Plater::arrange() { + const auto mode{ + wxGetKeyState(WXK_SHIFT) ? + ArrangeSelectionMode::SelectionOnly : + ArrangeSelectionMode::Full + }; + if (p->can_arrange()) { auto &w = get_ui_job_worker(); - arrange(w, wxGetKeyState(WXK_SHIFT)); + arrange(w, mode); } } -void Plater::arrange(Worker &w, bool selected) +void Plater::arrange_current_bed() { - ArrangeSelectionMode mode = selected ? - ArrangeSelectionMode::SelectionOnly : - ArrangeSelectionMode::Full; + const auto mode{ + wxGetKeyState(WXK_SHIFT) ? + ArrangeSelectionMode::CurrentBedSelectionOnly : + ArrangeSelectionMode::CurrentBedFull + }; + if (p->can_arrange()) { + auto &w = get_ui_job_worker(); + arrange(w, mode); + } +} +void Plater::arrange(Worker &w, const ArrangeSelectionMode &mode) +{ arr2::Scene arrscene{build_scene(*this, mode)}; ArrangeJob2::Callbacks cbs; @@ -7325,6 +8019,9 @@ void Plater::arrange(Worker &w, bool selected) concat_strings(names, "\n"))); } + s_multiple_beds.update_shown_beds(model(), build_volume()); + canvas3D()->check_volumes_outside_state(); + update(static_cast(UpdateParams::FORCE_FULL_SCREEN_REFRESH)); wxGetApp().obj_manipul()->set_dirty(); }; @@ -7359,6 +8056,7 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) p->printer_technology = printer_technology; bool ret = p->background_process.select_technology(printer_technology); if (ret) { + s_print_statuses.fill(PrintStatus::idle); // Update the active presets. } //FIXME for SLA synchronize @@ -7385,11 +8083,12 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) } void Plater::clear_before_change_volume(ModelVolume &mv, const std::string ¬ification_msg) { - // When we change the geometry of the volume, we remove any custom supports/seams/multi-material painting. - if (const bool paint_removed = !mv.supported_facets.empty() || !mv.seam_facets.empty() || !mv.mm_segmentation_facets.empty(); paint_removed) { + // When we change the geometry of the volume, we remove any custom supports/seams/multi-material/fuzzy skin painting. + if (const bool paint_removed = !mv.supported_facets.empty() || !mv.seam_facets.empty() || !mv.mm_segmentation_facets.empty() || !mv.fuzzy_skin_facets.empty(); paint_removed) { mv.supported_facets.reset(); mv.seam_facets.reset(); mv.mm_segmentation_facets.reset(); + mv.fuzzy_skin_facets.reset(); get_notification_manager()->push_notification( NotificationType::CustomSupportsAndSeamRemovedAfterRepair, @@ -7402,14 +8101,15 @@ void Plater::clear_before_change_mesh(int obj_idx, const std::string ¬ificati { ModelObject* mo = model().objects[obj_idx]; - // If there are custom supports/seams/mmu segmentation, remove them. Fixed mesh + // If there are custom supports/seams/mm segmentation/fuzzy skin, remove them. Fixed mesh // may be different and they would make no sense. bool paint_removed = false; - for (ModelVolume* mv : mo->volumes) { - paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mm_segmentation_facets.empty(); + for (ModelVolume *mv : mo->volumes) { + paint_removed |= !mv->supported_facets.empty() || !mv->seam_facets.empty() || !mv->mm_segmentation_facets.empty() || !mv->fuzzy_skin_facets.empty(); mv->supported_facets.reset(); mv->seam_facets.reset(); mv->mm_segmentation_facets.reset(); + mv->fuzzy_skin_facets.reset(); } if (paint_removed) { // snapshot_time is captured by copy so the lambda knows where to undo/redo to. @@ -7529,6 +8229,8 @@ void Plater::update_menus() { p->menus.update(); } void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } void Plater::show_action_buttons() const { p->show_action_buttons(p->ready_to_slice); } +void Plater::show_autoslicing_action_buttons() const { p->show_autoslicing_action_buttons(); }; + void Plater::copy_selection_to_clipboard() { // At first try to copy selected values to the ObjectList's clipboard @@ -7682,16 +8384,6 @@ const NotificationManager * Plater::get_notification_manager() const return p->notification_manager.get(); } -PresetArchiveDatabase* Plater::get_preset_archive_database() -{ - return p->preset_archive_database.get(); -} - -const PresetArchiveDatabase* Plater::get_preset_archive_database() const -{ - return p->preset_archive_database.get(); -} - UserAccount* Plater::get_user_account() { return p->user_account.get(); @@ -7825,6 +8517,16 @@ void Plater::bring_instance_forward() p->bring_instance_forward(); } +std::vector>& Plater::get_fff_prints() +{ + return p->fff_prints; +} + +const std::vector& Plater::get_gcode_results() const +{ + return p->gcode_results; +} + wxMenu* Plater::object_menu() { return p->menus.object_menu(); } wxMenu* Plater::part_menu() { return p->menus.part_menu(); } wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } @@ -7835,6 +8537,15 @@ wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } + +Print& Plater::active_fff_print() { return *p->fff_prints[s_multiple_beds.get_active_bed()]; } +//SLAPrint& Plater::active_sla_print() { return *p->sla_prints[s_multiple_beds.get_active_bed()]; } + +// For now, only use the first SLAPrint for all the beds - it means reslicing +// everything when a bed is changed. +SLAPrint& Plater::active_sla_print() { return *p->sla_prints.front(); } + + SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() : m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) { @@ -7851,7 +8562,7 @@ PlaterAfterLoadAutoArrange::PlaterAfterLoadAutoArrange() Plater* plater = wxGetApp().plater(); m_enabled = plater->model().objects.empty() && plater->printer_technology() == ptFFF && - is_XL_printer(plater->fff_print().config()); + is_XL_printer(plater->active_fff_print().config()); } PlaterAfterLoadAutoArrange::~PlaterAfterLoadAutoArrange() diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d550c4c..055920a 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -14,9 +14,9 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include "Jobs/Job.hpp" #include "Jobs/Worker.hpp" - -//y #include "libslic3r/GCode/ThumbnailData.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/Utils/PrintHost.hpp" class wxString; @@ -43,6 +43,7 @@ namespace UndoRedo { namespace GUI { wxDECLARE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); +wxDECLARE_EVENT(EVT_REGENERATE_BED_THUMBNAILS, SimpleEvent); class MainFrame; class GLCanvas3D; @@ -52,6 +53,7 @@ struct Camera; class GLToolbar; class UserAccount; class PresetArchiveDatabase; +enum class ArrangeSelectionMode; class Plater: public wxPanel { @@ -80,10 +82,17 @@ public: Sidebar& sidebar(); const Model& model() const; Model& model(); + //y const Print& fff_print() const; Print& fff_print(); const SLAPrint& sla_print() const; SLAPrint& sla_print(); + + Print& active_fff_print(); + SLAPrint& active_sla_print(); + + std::vector>& get_fff_prints(); + const std::vector& get_gcode_results() const; //B34 std::string double_to_str(const double value); @@ -114,6 +123,8 @@ public: void convert_gcode_to_ascii(); void convert_gcode_to_binary(); void reload_print(); + void object_list_changed(); + void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type); std::vector load_files(const std::vector& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false); // To be called when providing a list of files to the GUI slic3r on command line. @@ -207,9 +218,21 @@ public: void apply_cut_object_to_model(size_t init_obj_idx, const ModelObjectPtrs& cut_objects); + void with_mocked_fff_background_process( + Print &print, + GCodeProcessorResult &result, + const int bed_index, + const std::function &callable + ); //B64 ThumbnailData get_thumbnailldate_send(); + //y20 + ThumbnailData get_thumbnailldate_from_bed(int num); + int get_beds_num(); + int get_active_bed(); + void export_gcode(bool prefer_removable); + void export_all_gcodes(bool prefer_removable); void export_stl_obj(bool extended = false, bool selection_only = false); bool export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path()); void reload_from_disk(); @@ -235,7 +258,11 @@ public: //y15 // void send_gcode_inner(DynamicPrintConfig* physical_printer_config); void eject_drive(); + + std::optional get_connect_print_host_job(); void connect_gcode(); + void connect_gcode_all(); + void printables_to_connect_gcode(const std::string& url); std::string get_upload_filename(); void take_snapshot(const std::string &snapshot_name); @@ -272,6 +299,7 @@ public: void update_menus(); void show_action_buttons(const bool is_ready_to_slice) const; void show_action_buttons() const; + void show_autoslicing_action_buttons() const; wxString get_project_filename(const wxString& extension = wxEmptyString) const; void set_project_filename(const wxString& filename); @@ -286,9 +314,10 @@ public: GLCanvas3D* get_current_canvas3D(); void render_sliders(GLCanvas3D& canvas); - + void arrange(); - void arrange(Worker &w, bool selected); + void arrange_current_bed(); + void arrange(Worker &w, const ArrangeSelectionMode &selected); void set_current_canvas_as_dirty(); void unbind_canvas_event_handlers(); @@ -368,9 +397,6 @@ public: NotificationManager* get_notification_manager(); const NotificationManager* get_notification_manager() const; - PresetArchiveDatabase* get_preset_archive_database(); - const PresetArchiveDatabase* get_preset_archive_database() const; - UserAccount* get_user_account(); const UserAccount* get_user_account() const; @@ -451,6 +477,12 @@ public: }; private: + std::optional get_default_output_file(); + std::optional check_output_path_has_error(const boost::filesystem::path& path) const; + std::optional get_output_path(const std::string &start_dir, const fs_path &default_output_file); + std::optional get_multiple_output_dir(const std::string &start_dir); + + void export_gcode_to_path(const fs_path &output_path, const std::function &export_callback); void reslice_until_step_inner(int step, const ModelObject &object, bool postpone_error_messages); struct priv; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6cb6d5b..2da3e01 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -127,13 +127,13 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin downloader->set_path_name(app_config->get("url_downloader_dest")); downloader->allow(!app_config->has("downloader_url_registered") || app_config->get_bool("downloader_url_registered")); - for (const std::string& opt_key : {"suppress_hyperlinks", "downloader_url_registered", "show_login_button"}) + for (const std::string opt_key : {"suppress_hyperlinks", "downloader_url_registered", "show_login_button"}) m_optgroup_other->set_value(opt_key, app_config->get_bool(opt_key)); // by default "Log in" button is visible if (!app_config->has("show_login_button")) m_optgroup_other->set_value("show_login_button", true); - for (const std::string& opt_key : { "default_action_on_close_application" + for (const std::string opt_key : { "default_action_on_close_application" ,"default_action_on_new_project" ,"default_action_on_select_preset" }) m_optgroup_general->set_value(opt_key, app_config->get(opt_key) == "none"); @@ -293,11 +293,6 @@ void PreferencesDialog::build() L("If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files."), app_config->has("remember_output_path") ? app_config->get_bool("remember_output_path") : true); - append_bool_option(m_optgroup_general, "autocenter", - L("Auto-center parts"), - L("If this is enabled, Slic3r will auto-center objects around the print bed center."), - app_config->get_bool("autocenter")); - append_bool_option(m_optgroup_general, "background_processing", L("Background processing"), L("If this is enabled, Slic3r will pre-process objects as soon " diff --git a/src/slic3r/GUI/PresetArchiveDatabase.cpp b/src/slic3r/GUI/PresetArchiveDatabase.cpp index d4b671d..26cb499 100644 --- a/src/slic3r/GUI/PresetArchiveDatabase.cpp +++ b/src/slic3r/GUI/PresetArchiveDatabase.cpp @@ -6,6 +6,8 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/UserAccount.hpp" +#include "slic3r/Utils/PresetUpdaterWrapper.hpp" +#include "slic3r/GUI/Field.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/miniz_extension.hpp" @@ -25,7 +27,6 @@ namespace pt = boost::property_tree; namespace fs = boost::filesystem; namespace Slic3r { -namespace GUI { static const char* TMP_EXTENSION = ".download"; @@ -118,7 +119,7 @@ void delete_path_recursive(const fs::path& path) fs::remove(path); } } - catch (const std::exception& e) { + catch (const std::exception&) { BOOST_LOG_TRIVIAL(error) << "Failed to delete files at: " << path; } } @@ -154,25 +155,6 @@ bool extract_local_archive_repository( ArchiveRepository::RepositoryManifest& ma return true; } -void deserialize_string(const std::string& opt, std::vector& result) -{ - std::string val; - for (size_t i = 0; i < opt.length(); i++) { - if (std::isspace(opt[i])) { - continue; - } - if (opt[i] != ';') { - val += opt[i]; - } - else { - result.emplace_back(std::move(val)); - } - } - if (!val.empty()) { - result.emplace_back(std::move(val)); - } -} - std::string escape_string(const std::string& unescaped) { std::string ret_val; @@ -200,17 +182,20 @@ std::string escape_path_by_element(const std::string& path_string) return ret_val; } -void add_authorization_header(Http& http) +bool add_authorization_header(Http& http) { + if (wxApp::GetInstance() == nullptr || ! GUI::wxGetApp().plater()) + return false; const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); if (!access_token.empty()) { http.header("Authorization", "Bearer " + access_token); } + return true; } } -bool OnlineArchiveRepository::get_file_inner(const std::string& url, const fs::path& target_path) const +bool OnlineArchiveRepository::get_file_inner(const std::string& url, const fs::path& target_path, PresetUpdaterUIStatus* ui_status) const { bool res = false; @@ -222,17 +207,17 @@ bool OnlineArchiveRepository::get_file_inner(const std::string& url, const fs::p tmp_path.string()); auto http = Http::get(url); - add_authorization_header(http); + if (!add_authorization_header(http)) + return false; http .timeout_max(30) .on_progress([](Http::Progress, bool& cancel) { //if (cancel) { cancel = true; } }) .on_error([&](std::string body, std::string error, unsigned http_status) { - BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", - url, - http_status, - body); + BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", url, http_status, body); + ui_status->set_error(error); + res = false; }) .on_complete([&](std::string body, unsigned /* http_status */) { if (body.empty()) { @@ -244,30 +229,38 @@ bool OnlineArchiveRepository::get_file_inner(const std::string& url, const fs::p fs::rename(tmp_path, target_path); res = true; }) - .perform_sync(); + .on_retry([&](int attempt, unsigned delay) { + return !ui_status->on_attempt(attempt, delay); + }) + .perform_sync(ui_status->get_retry_policy()); return res; } -bool OnlineArchiveRepository::get_archive(const fs::path& target_path) const +bool OnlineArchiveRepository::get_archive(const fs::path& target_path, PresetUpdaterUIStatus* ui_status) const { - return get_file_inner(m_data.index_url.empty() ? m_data.url + "vendor_indices.zip" : m_data.index_url, target_path); + return get_file_inner(m_data.index_url.empty() ? m_data.url + "vendor_indices.zip" : m_data.index_url, target_path, ui_status); } -bool OnlineArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id) const +bool OnlineArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id, PresetUpdaterUIStatus* ui_status) const { if (repository_id != m_data.id) { BOOST_LOG_TRIVIAL(error) << "Error getting file " << source_subpath << ". The repository_id was not matching."; return false; } + + ui_status->set_target(target_path.filename().string()); + const std::string escaped_source_subpath = escape_path_by_element(source_subpath); - return get_file_inner(m_data.url + escaped_source_subpath, target_path); + return get_file_inner(m_data.url + escaped_source_subpath, target_path, ui_status); } -bool OnlineArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path) const +bool OnlineArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path, PresetUpdaterUIStatus* ui_status) const { + ui_status->set_target(target_path.filename().string()); + const std::string escaped_source_subpath = escape_path_by_element(source_subpath); - return get_file_inner(m_data.url + escaped_source_subpath, target_path); + return get_file_inner(m_data.url + escaped_source_subpath, target_path, ui_status); } bool LocalArchiveRepository::get_file_inner(const fs::path& source_path, const fs::path& target_path) const @@ -296,7 +289,7 @@ bool LocalArchiveRepository::get_file_inner(const fs::path& source_path, const f return true; } -bool LocalArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id) const +bool LocalArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id, PresetUpdaterUIStatus* ui_status) const { if (repository_id != m_data.id) { BOOST_LOG_TRIVIAL(error) << "Error getting file " << source_subpath << ". The repository_id was not matching."; @@ -304,11 +297,11 @@ bool LocalArchiveRepository::get_file(const std::string& source_subpath, const f } return get_file_inner(m_data.tmp_path / source_subpath, target_path); } -bool LocalArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path) const +bool LocalArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path, PresetUpdaterUIStatus* ui_status) const { return get_file_inner(m_data.tmp_path / source_subpath, target_path); } -bool LocalArchiveRepository::get_archive(const fs::path& target_path) const +bool LocalArchiveRepository::get_archive(const fs::path& target_path, PresetUpdaterUIStatus* ui_status) const { fs::path source_path = fs::path(m_data.tmp_path) / "vendor_indices.zip"; return get_file_inner(std::move(source_path), target_path); @@ -325,11 +318,9 @@ void LocalArchiveRepository::do_extract() //-------------------------------------PresetArchiveDatabase------------------------------------------------------------------------------------------------------------------------- -PresetArchiveDatabase::PresetArchiveDatabase(AppConfig* app_config, wxEvtHandler* evt_handler) - : p_evt_handler(evt_handler) +PresetArchiveDatabase::PresetArchiveDatabase() { - // - boost::system::error_code ec; + boost::system::error_code ec; m_unq_tmp_path = fs::temp_directory_path() / fs::unique_path(); fs::create_directories(m_unq_tmp_path, ec); assert(!ec); @@ -382,7 +373,8 @@ bool PresetArchiveDatabase::set_selected_repositories(const std::vector& pair : m_selected_repositories_uuid) { + // std::map m_selected_repositories_uuid + for (const auto& pair : m_selected_repositories_uuid) { if (!pair.second) { continue; } @@ -426,14 +418,14 @@ void PresetArchiveDatabase::set_installed_printer_repositories(const std::vector if (selected_uuid.empty() && unselected_uuid.empty()) { // there is id in used_ids that is not in m_archive_repositories - BAD - assert(true); + assert(false); continue; } else if (selected_uuid.size() == 1){ // regular case m_has_installed_printer_repositories_uuid[selected_uuid.front()] = true; } else if (selected_uuid.size() > 1) { // this should not happen, only one repo of same id should be selected (online / local conflict) - assert(true); + assert(false); // select first one to solve the conflict m_has_installed_printer_repositories_uuid[selected_uuid.front()] = true; // unselect the rest @@ -525,7 +517,7 @@ void PresetArchiveDatabase::load_app_manifest_json() file.close(); } else { - assert(true); + assert(false); BOOST_LOG_TRIVIAL(error) << "Failed to read Archive Source Manifest at " << path; } if (data.empty()) { @@ -552,14 +544,14 @@ void PresetArchiveDatabase::load_app_manifest_json() if(const auto used = subtree.second.get_optional("selected"); used) { m_selected_repositories_uuid[uuid] = extracted && *used; } else { - assert(true); + assert(false); m_selected_repositories_uuid[uuid] = extracted; } // "has_installed_printers" flag if (const auto used = subtree.second.get_optional("has_installed_printers"); used) { m_has_installed_printer_repositories_uuid[uuid] = extracted && *used; } else { - assert(true); + assert(false); m_has_installed_printer_repositories_uuid[uuid] = false; } m_archive_repositories.emplace_back(std::make_unique(std::move(uuid), std::move(manifest), extracted)); @@ -570,7 +562,7 @@ void PresetArchiveDatabase::load_app_manifest_json() ArchiveRepository::RepositoryManifest manifest; std::string uuid = get_next_uuid(); if (!extract_repository_header(subtree.second, manifest)) { - assert(true); + assert(false); BOOST_LOG_TRIVIAL(error) << "Failed to read one of source headers."; continue; } @@ -578,14 +570,14 @@ void PresetArchiveDatabase::load_app_manifest_json() if (const auto used = subtree.second.get_optional("selected"); used) { m_selected_repositories_uuid[uuid] = *used; } else { - assert(true); + assert(false); m_selected_repositories_uuid[uuid] = true; } // "has_installed_printers" flag if (const auto used = subtree.second.get_optional("has_installed_printers"); used) { m_has_installed_printer_repositories_uuid[uuid] = *used; } else { - assert(true); + assert(false); m_has_installed_printer_repositories_uuid[uuid] = false; } m_archive_repositories.emplace_back(std::make_unique(std::move(uuid), std::move(manifest))); @@ -687,7 +679,7 @@ void PresetArchiveDatabase::save_app_manifest_json() const file << data; file.close(); } else { - assert(true); + assert(false); BOOST_LOG_TRIVIAL(error) << "Failed to write Archive Repository Manifest to " << path; } } @@ -765,7 +757,7 @@ void PresetArchiveDatabase::read_server_manifest(const std::string& json_body) for (const auto& subtree : ptree) { ArchiveRepository::RepositoryManifest manifest; if (!extract_repository_header(subtree.second, manifest)) { - assert(true); + assert(false); BOOST_LOG_TRIVIAL(error) << "Failed to read one of repository headers."; continue; } @@ -888,33 +880,44 @@ std::string PresetArchiveDatabase::get_next_uuid() } namespace { -bool sync_inner(std::string& manifest) +bool sync_inner(std::string& manifest, PresetUpdaterUIStatus* ui_status) { bool ret = false; std::string url = Utils::ServiceConfig::instance().preset_repo_repos_url(); auto http = Http::get(std::move(url)); - add_authorization_header(http); + if (!add_authorization_header(http)) + return false; http .timeout_max(30) .on_error([&](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(error) << "Failed to get online archive source manifests: "<< body << " ; " << error << " ; " << http_status; + ui_status->set_error(error); ret = false; }) .on_complete([&](std::string body, unsigned /* http_status */) { manifest = body; ret = true; }) - .perform_sync(); + .on_retry([&](int attempt, unsigned delay) { + return !ui_status->on_attempt(attempt, delay); + }) + .perform_sync(ui_status->get_retry_policy()); return ret; } } -void PresetArchiveDatabase::sync_blocking() +bool PresetArchiveDatabase::sync_blocking(PresetUpdaterUIStatus* ui_status) { + assert(ui_status); std::string manifest; - if (!sync_inner(manifest)) - return; + bool sync_res = false; + ui_status->set_target("Archive Database Mainfest"); + sync_res = sync_inner(manifest, ui_status); + if (!sync_res) { + return false; + } read_server_manifest(std::move(manifest)); + return true; } -}} // Slic3r::GUI +} // Slic3r diff --git a/src/slic3r/GUI/PresetArchiveDatabase.hpp b/src/slic3r/GUI/PresetArchiveDatabase.hpp index e5720e1..359df8f 100644 --- a/src/slic3r/GUI/PresetArchiveDatabase.hpp +++ b/src/slic3r/GUI/PresetArchiveDatabase.hpp @@ -13,7 +13,8 @@ namespace Slic3r { class AppConfig; -namespace GUI { + +class PresetUpdaterUIStatus; struct ArchiveRepositoryGetFileArgs { boost::filesystem::path target_path; @@ -75,12 +76,12 @@ public: {} virtual ~ArchiveRepository() {} // Gets vendor_indices.zip to target_path - virtual bool get_archive(const boost::filesystem::path& target_path) const = 0; + virtual bool get_archive(const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const = 0; // Gets file if repository_id arg matches m_id. // Should be used to get the most recent ini file and every missing resource. - virtual bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const = 0; + virtual bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id, PresetUpdaterUIStatus* ui_status) const = 0; // Gets file without id check - for not yet encountered vendors only! - virtual bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const = 0; + virtual bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const = 0; const RepositoryManifest& get_manifest() const { return m_data; } std::string get_uuid() const { return m_uuid; } // Only local archvies can return false @@ -103,15 +104,15 @@ public: } } // Gets vendor_indices.zip to target_path. - bool get_archive(const boost::filesystem::path& target_path) const override; + bool get_archive(const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const override; // Gets file if repository_id arg matches m_id. // Should be used to get the most recent ini file and every missing resource. - bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const override; + bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id, PresetUpdaterUIStatus* ui_status) const override; // Gets file without checking id. // Should be used only if no previous ini file exists. - bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const override; + bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const override; private: - bool get_file_inner(const std::string& url, const boost::filesystem::path& target_path) const; + bool get_file_inner(const std::string& url, const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const; }; class LocalArchiveRepository : public ArchiveRepository @@ -120,13 +121,13 @@ public: LocalArchiveRepository(const std::string& uuid, RepositoryManifest&& data, bool extracted) : ArchiveRepository(uuid, std::move(data)), m_extracted(extracted) {} // Gets vendor_indices.zip to target_path. - bool get_archive(const boost::filesystem::path& target_path) const override; + bool get_archive(const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const override; // Gets file if repository_id arg matches m_id. // Should be used to get the most recent ini file and every missing resource. - bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const override; + bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id, PresetUpdaterUIStatus* ui_status) const override; // Gets file without checking id. // Should be used only if no previous ini file exists. - bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const override; + bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path, PresetUpdaterUIStatus* ui_status) const override; bool is_extracted() const override { return m_extracted; } void do_extract() override; @@ -141,10 +142,10 @@ typedef std::vector SharedArchiveRepositoryVector; class PresetArchiveDatabase { public: - PresetArchiveDatabase(AppConfig* app_config, wxEvtHandler* evt_handler); + PresetArchiveDatabase(); ~PresetArchiveDatabase() {} - - void sync_blocking(); + // returns true if successfully got the data + bool sync_blocking(PresetUpdaterUIStatus* ui_status = nullptr); // Do not use get_all_archive_repositories to perform any GET calls. Use get_selected_archive_repositories instead. SharedArchiveRepositoryVector get_all_archive_repositories() const; @@ -172,7 +173,6 @@ private: void consolidate_uuid_maps(); void extract_local_archives(); std::string get_next_uuid(); - wxEvtHandler* p_evt_handler; boost::filesystem::path m_unq_tmp_path; PrivateArchiveRepositoryVector m_archive_repositories; std::map m_selected_repositories_uuid; @@ -180,6 +180,6 @@ private: boost::uuids::random_generator m_uuid_generator; }; -}} // Slic3r::GUI +} // Slic3r #endif // PresetArchiveDatabase \ No newline at end of file diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index ed18d8c..1e9f323 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -210,20 +210,28 @@ void PresetComboBox::update_selection() // A workaround for a set of issues related to text fitting into gtk widgets: #if defined(__WXGTK20__) || defined(__WXGTK3__) - GList* cells = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(m_widget)); - - // 'cells' contains the GtkCellRendererPixBuf for the icon, - // 'cells->next' contains GtkCellRendererText for the text we need to ellipsize - if (!cells || !cells->next) return; - - auto cell = static_cast(cells->next->data); - - if (!cell) return; - - g_object_set(G_OBJECT(cell), "ellipsize", PANGO_ELLIPSIZE_END, (char*)NULL); - - // Only the list of cells must be freed, the renderer isn't ours to free - g_list_free(cells); + GtkWidget* widget = m_widget; + if (GTK_IS_CONTAINER(widget)) { + GList* children = gtk_container_get_children(GTK_CONTAINER(widget)); + if (children) { + widget = GTK_WIDGET(children->data); + g_list_free(children); + } + } + if (GTK_IS_ENTRY(widget)) { + // Set ellipsization for the entry + gtk_entry_set_width_chars(GTK_ENTRY(widget), 20); // Adjust this value as needed + gtk_entry_set_max_width_chars(GTK_ENTRY(widget), 20); // Adjust this value as needed + // Create a PangoLayout for the entry and set ellipsization + PangoLayout* layout = gtk_entry_get_layout(GTK_ENTRY(widget)); + if (layout) { + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } else { + g_warning("Unable to get PangoLayout from GtkEntry"); + } + } else { + g_warning("Expected GtkEntry, but got %s", G_OBJECT_TYPE_NAME(widget)); + } #endif } @@ -984,19 +992,9 @@ static std::string get_connect_state_suffix_for_printer(const Preset& printer_pr for (const auto& [printer_model_nozzle_pair, states] : printer_state_map) { std::string printer_model = printer_preset.config.opt_string("printer_model"); - std::string vendor_repo_prefix; - if (printer_preset.vendor) { - vendor_repo_prefix = printer_preset.vendor->repo_prefix; - } else if (std::string inherits = printer_preset.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model.find(vendor_repo_prefix) == 0) { - printer_model = printer_model.substr(vendor_repo_prefix.size()); - boost::trim_left(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]; @@ -1048,19 +1046,8 @@ static bool fill_data_to_connect_info_line( const Preset& printer_preset, 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"); - std::string vendor_repo_prefix; - if (printer_preset.vendor) { - vendor_repo_prefix = printer_preset.vendor->repo_prefix; - } else if (std::string inherits = printer_preset.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model.find(vendor_repo_prefix) == 0) { - printer_model = printer_model.substr(vendor_repo_prefix.size()); - boost::trim_left(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]; diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index a48c228..7883d1c 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -41,8 +41,8 @@ static const char *CONFIG_KEY_PATH = "printhost_path"; static const char *CONFIG_KEY_GROUP = "printhost_group"; static const char* CONFIG_KEY_STORAGE = "printhost_storage"; -//B61 -PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups, const wxArrayString& storage_paths, const wxArrayString& storage_names, Plater* plater, const PrintStatistics& ps, bool onlylink) +//B61//y20 +PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups, const wxArrayString& storage_paths, const wxArrayString& storage_names, Plater* plater, bool onlylink) : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L(""), 0) // Set style = 0 to avoid default creation of the "OK" button. // All buttons will be added later in this constructor , txt_filename(new wxTextCtrl(this, wxID_ANY)) @@ -63,8 +63,15 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo auto *label_dir_hint2 = new wxStaticText(this, wxID_ANY, _L("Upload to Printer Host with the following filename:")); label_dir_hint2->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); - //B64 - ThumbnailData thumbnail_data = m_plater->get_thumbnailldate_send(); + //B64//y20 + int bed_num = m_plater->get_beds_num(); + ThumbnailData thumbnail_data; + if (bed_num > 1) { + int active_bed = m_plater->get_active_bed(); + thumbnail_data = m_plater->get_thumbnailldate_from_bed(active_bed); + } + else + thumbnail_data = m_plater->get_thumbnailldate_send(); wxImage image(thumbnail_data.width, thumbnail_data.height); image.InitAlpha(); @@ -77,8 +84,11 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo image.SetAlpha((int) c, (int) r, px[3]); } } + //y21 + image.Rescale(128, 160); wxBitmap bitmap(image); wxStaticBitmap *static_bitmap = new wxStaticBitmap(this, wxID_ANY, bitmap); + //static_bitmap->SetSize(wxSize(20, 20)); //static_bitmap->SetMinSize(wxSize(100, 100)); @@ -96,19 +106,21 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo vbox1->Add(static_bitmap, 0, wxALL | wxALIGN_CENTER); // Add add.svg image //wxBitmap add_bitmap(*get_bmp_bundle("add.svg"), wxBITMAP_TYPE_SVG); + //y20 + PrintStatistics print_statistic = wxGetApp().plater()->get_fff_prints()[m_plater->get_active_bed()].get()->print_statistics(); wxStaticBitmap *add_bitmap = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle("print_time", 20)); row_sizer->Add(add_bitmap, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - // Add ps.estimated_normal_print_time text - wxStaticText *estimated_print_time_text = new wxStaticText(this, wxID_ANY, wxString::Format("%s", ps.estimated_normal_print_time)); + // Add print_statisticestimated_normal_print_time text + wxStaticText *estimated_print_time_text = new wxStaticText(this, wxID_ANY, wxString::Format("%s", print_statistic.estimated_normal_print_time)); row_sizer->Add(estimated_print_time_text, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); // Add delete.svg image wxStaticBitmap *delete_static_bitmap = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle("cost_weight", 20)); row_sizer->Add(delete_static_bitmap, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - // Add ps.total_weight text - wxStaticText *total_weight_text = new wxStaticText(this, wxID_ANY, wxString::Format("%.4fg", ps.total_weight)); + // Add print_statistictotal_weight text + wxStaticText *total_weight_text = new wxStaticText(this, wxID_ANY, wxString::Format("%.4fg", print_statistic.total_weight)); row_sizer->Add(total_weight_text, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); vbox1->Add(row_sizer, 0, wxALIGN_CENTER); @@ -415,8 +427,8 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo return msg_wingow.ShowModal() == wxID_YES; } - //y19 - std::string unusable_symbols = "<>[]:/\\|?*\""; + //y21 + wxString unusable_symbols = "<>[]:\\|?*\""; for(auto c : path){ if(unusable_symbols.find(c) != std::string::npos){ ErrorDialog msg(this, format_wxstr("%1%\n%2% %3%", _L("The provided name is not valid;"), @@ -874,7 +886,7 @@ void PrintHostQueueDialog::append_job(const PrintHostJob &job) // Both strings are UTF-8 encoded. upload_names.emplace_back(job.printhost->get_host(), job.upload_data.upload_path.string()); - wxGetApp().notification_manager()->push_upload_job_notification(job_list->GetItemCount(), (float)size_i / 1024 / 1024, job.upload_data.upload_path.string(), job.printhost->get_host()); + wxGetApp().notification_manager()->push_upload_job_notification(job_list->GetItemCount(), (float)size_i / 1024 / 1024, job.upload_data.upload_path.string(), job.printhost->get_notification_host()); } void PrintHostQueueDialog::on_dpi_changed(const wxRect &suggested_rect) @@ -954,9 +966,9 @@ void PrintHostQueueDialog::on_progress(Event &evt) wxVariant nm, hst; job_list->GetValue(nm, evt.job_id, COL_FILENAME); job_list->GetValue(hst, evt.job_id, COL_HOST); - const wchar_t * nm_str = nm.GetString(); - const wchar_t * hst_str = hst.GetString(); - wxGetApp().notification_manager()->set_upload_job_notification_percentage(evt.job_id + 1, into_u8(nm_str), into_u8(hst_str), evt.progress / 100.f); + const std::string& nm_str = into_u8(nm.GetString()); + const std::string& hst_str = into_u8(hst.GetString()); + wxGetApp().notification_manager()->set_upload_job_notification_percentage(evt.job_id + 1, nm_str, hst_str, evt.progress / 100.f); } } @@ -1039,8 +1051,13 @@ void PrintHostQueueDialog::on_info(Event& evt) job_list->SetValue(hst, evt.job_id, COL_ERRORMSG); wxGetApp().notification_manager()->set_upload_job_notification_completed_with_warning(evt.job_id + 1); wxGetApp().notification_manager()->set_upload_job_notification_status(evt.job_id + 1, into_u8(evt.status)); - } else if (evt.tag == L"set_complete_off") { - wxGetApp().notification_manager()->set_upload_job_notification_comp_on_100(evt.job_id + 1, false); + } else if (evt.tag == L"qidiconnect_printer_address") { + wxGetApp().notification_manager()->set_upload_job_notification_hypertext(evt.job_id + 1 + , [evt](wxEvtHandler *) { + wxGetApp().mainframe->show_connect_tab(into_u8(evt.status)); + return false ; + } + ); } } diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index a315254..d8eda40 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -62,8 +62,8 @@ struct PhysicalPrinterPresetData class PrintHostSendDialog : public GUI::MsgDialog { public: - //B61 - PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups, const wxArrayString& storage_paths, const wxArrayString& storage_names, Plater* plater, const PrintStatistics& ps, bool onlyLik); + //B61//y20 + PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups, const wxArrayString& storage_paths, const wxArrayString& storage_names, Plater* plater, bool onlyLik); boost::filesystem::path filename() const; PrintHostPostUploadAction post_action() const; std::string group() const; diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp index af35d8d..fc89a46 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -42,10 +42,13 @@ void ProjectDirtyStateManager::update_from_presets() void ProjectDirtyStateManager::update_from_preview() { - const bool is_dirty = m_initial_custom_gcode_per_print_z != wxGetApp().model().custom_gcode_per_print_z; + if (wxApp::GetInstance() == nullptr || wxGetApp().plater() == nullptr) + return; + const bool is_dirty = m_initial_custom_gcode_per_print_z != wxGetApp().model().custom_gcode_per_print_z(); if (m_custom_gcode_per_print_z_dirty != is_dirty) { m_custom_gcode_per_print_z_dirty = is_dirty; - wxGetApp().mainframe->update_title(); + if (wxApp::GetInstance() != nullptr) + wxGetApp().mainframe->update_title(); } } @@ -66,7 +69,7 @@ void ProjectDirtyStateManager::reset_initial_presets() for (const PresetCollection *preset_collection : app.get_active_preset_collections()) m_initial_presets[preset_collection->type()] = preset_collection->get_selected_preset_name(); m_initial_project_config = app.preset_bundle->project_config; - m_initial_custom_gcode_per_print_z = app.model().custom_gcode_per_print_z; + m_initial_custom_gcode_per_print_z = app.model().custom_gcode_per_print_z(); } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 08c3407..dd25d01 100644 --- a/src/slic3r/GUI/SceneRaycaster.cpp +++ b/src/slic3r/GUI/SceneRaycaster.cpp @@ -6,6 +6,8 @@ #include "Selection.hpp" #include "Plater.hpp" +#include "libslic3r/MultipleBeds.hpp" + namespace Slic3r { namespace GUI { @@ -111,7 +113,7 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came const Selection& selection = wxGetApp().plater()->get_selection(); if (selection.is_single_volume() || selection.is_single_modifier()) { const GLVolume* volume = selection.get_first_volume(); - if (!volume->is_wipe_tower && !volume->is_sla_pad() && !volume->is_sla_support()) + if (!volume->is_wipe_tower() && !volume->is_sla_pad() && !volume->is_sla_support()) m_selected_volume_id = *selection.get_volume_idxs().begin(); } } @@ -156,13 +158,31 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came const std::vector>* raycasters = get_raycasters(type); const Vec3f camera_forward = camera.get_dir_forward().cast(); HitResult current_hit = { type }; - for (std::shared_ptr item : *raycasters) { + for (const std::shared_ptr& item : *raycasters) { if (!item->is_active()) continue; + bool sth_hit = false; + current_hit.raycaster_id = item->get_id(); - const Transform3d& trafo = item->get_transform(); - if (item->get_raycaster()->closest_hit(mouse_pos, trafo, camera, current_hit.position, current_hit.normal, clip_plane)) { + const Transform3d& trafo_orig = item->get_transform(); + Transform3d trafo = trafo_orig; + if (type == EType::Bed) { + int bed_idx_hit = -1; + for (int i = 0; i < s_multiple_beds.get_number_of_beds(); ++i) { + trafo = trafo_orig; + trafo.translate(s_multiple_beds.get_bed_translation(i)); + sth_hit = item->get_raycaster()->closest_hit(mouse_pos, trafo, camera, current_hit.position, current_hit.normal, clip_plane); + if (sth_hit) { + bed_idx_hit = i; + break; + } + } + s_multiple_beds.set_last_hovered_bed(bed_idx_hit); + } else + sth_hit = item->get_raycaster()->closest_hit(mouse_pos, trafo, camera, current_hit.position, current_hit.normal, clip_plane); + + if (sth_hit) { current_hit.position = (trafo * current_hit.position.cast()).cast(); current_hit.normal = (trafo.matrix().block(0, 0, 3, 3).inverse().transpose() * current_hit.normal.cast()).normalized().cast(); if (item->use_back_faces() || current_hit.normal.dot(camera_forward) < 0.0f) { diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 1846f5e..ad0c5ac 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -492,6 +492,7 @@ void OptionsSearcher::show_dialog(bool show /*= true*/) search_dialog->Popup(); if (!search_input->HasFocus()) search_input->SetFocus(); + wxYield(); } void OptionsSearcher::dlg_sys_color_changed() @@ -523,13 +524,12 @@ void OptionsSearcher::edit_search_input() void OptionsSearcher::process_key_down_from_input(wxKeyEvent& e) { int key = e.GetKeyCode(); - if (key == WXK_ESCAPE) + if (key == WXK_ESCAPE) { + set_focus_to_parent(); search_dialog->Hide(); + } else if (search_dialog && (key == WXK_UP || key == WXK_DOWN || key == WXK_NUMPAD_ENTER || key == WXK_RETURN)) { search_dialog->KeyDown(e); -#ifdef __linux__ - search_dialog->SetFocus(); -#endif // __linux__ } } @@ -703,7 +703,7 @@ void SearchDialog::OnKeyDown(wxKeyEvent& event) if (key == WXK_UP || key == WXK_DOWN) { // So, for the next correct navigation, set focus on the search_list - // search_list->SetFocus(); // #ys_delete_after_test -> Looks like no need anymore + search_list->SetFocus(); auto item = search_list->GetSelection(); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index e7796f8..7c793d8 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -19,6 +19,7 @@ #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/BuildVolume.hpp" +#include "libslic3r/MultipleBeds.hpp" #include @@ -144,7 +145,7 @@ void Selection::add(unsigned int volume_idx, bool as_single_selection, bool chec return; // wipe tower is already selected - if (is_wipe_tower() && volume->is_wipe_tower) + if (is_wipe_tower() && volume->is_wipe_tower() && contains_volume(volume_idx)) return; bool keep_instance_mode = (m_mode == Instance) && !as_single_selection; @@ -152,8 +153,8 @@ void Selection::add(unsigned int volume_idx, bool as_single_selection, bool chec // resets the current list if needed bool needs_reset = as_single_selection && !already_contained; - needs_reset |= volume->is_wipe_tower; - needs_reset |= is_wipe_tower() && !volume->is_wipe_tower; + needs_reset |= volume->is_wipe_tower(); + needs_reset |= is_wipe_tower() && !volume->is_wipe_tower(); needs_reset |= as_single_selection && !is_any_modifier() && volume->is_modifier; needs_reset |= is_any_modifier() && !volume->is_modifier; @@ -377,7 +378,7 @@ void Selection::add_all() unsigned int count = 0; for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) + if (!(*m_volumes)[i]->is_wipe_tower()) ++count; } @@ -390,7 +391,7 @@ void Selection::add_all() clear(); for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) + if (!(*m_volumes)[i]->is_wipe_tower()) do_add_volume(i); } @@ -1442,7 +1443,7 @@ void Selection::translate(unsigned int object_idx, unsigned int instance_idx, co if (done.size() == m_volumes->size()) break; - if ((*m_volumes)[i]->is_wipe_tower) + if ((*m_volumes)[i]->is_wipe_tower()) continue; const int object_idx = (*m_volumes)[i]->object_idx(); @@ -1916,7 +1917,7 @@ void Selection::update_type() m_type = Empty; else if (m_list.size() == 1) { const GLVolume* first = (*m_volumes)[*m_list.begin()]; - if (first->is_wipe_tower) + if (first->is_wipe_tower()) m_type = WipeTower; else if (first->is_modifier) { m_type = SingleModifier; @@ -2738,7 +2739,7 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_ if (done.size() == m_volumes->size()) break; const GLVolume* volume_i = (*m_volumes)[i]; - if (volume_i->is_wipe_tower) + if (volume_i->is_wipe_tower()) continue; const int object_idx = volume_i->object_idx(); @@ -2783,7 +2784,7 @@ void Selection::synchronize_unselected_volumes() { for (unsigned int i : m_list) { const GLVolume* volume = (*m_volumes)[i]; - if (volume->is_wipe_tower) + if (volume->is_wipe_tower()) continue; const int object_idx = volume->object_idx(); @@ -2811,7 +2812,7 @@ void Selection::ensure_on_bed() for (size_t i = 0; i < m_volumes->size(); ++i) { GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier && + if (!volume->is_wipe_tower() && !volume->is_modifier && std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) == m_cache.sinking_volumes.end()) { const double min_z = volume->transformed_convex_hull_bounding_box().min.z(); std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); @@ -2838,7 +2839,7 @@ void Selection::ensure_not_below_bed() for (size_t i = 0; i < m_volumes->size(); ++i) { GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier) { + if (!volume->is_wipe_tower() && !volume->is_modifier) { const double max_z = volume->transformed_convex_hull_bounding_box().max.z(); const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); InstancesToZMap::iterator it = instances_max_z.find(instance); diff --git a/src/slic3r/GUI/Sidebar.cpp b/src/slic3r/GUI/Sidebar.cpp index d6d2b0f..cd3a86f 100644 --- a/src/slic3r/GUI/Sidebar.cpp +++ b/src/slic3r/GUI/Sidebar.cpp @@ -13,6 +13,7 @@ #include #include #include // IWYU pragma: keep +#include "libslic3r/MultipleBeds.hpp" #include "wx/generic/stattextg.h" #ifdef _WIN32 #include @@ -482,9 +483,9 @@ Sidebar::Sidebar(Plater *parent) enable_buttons(false); - auto *btns_sizer = new wxBoxSizer(wxVERTICAL); + m_btns_sizer = new wxBoxSizer(wxVERTICAL); - auto* complect_btns_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *complect_btns_sizer = new wxBoxSizer(wxHORIZONTAL); complect_btns_sizer->Add(m_btn_export_gcode, 1, wxEXPAND); //B @@ -495,16 +496,35 @@ Sidebar::Sidebar(Plater *parent) complect_btns_sizer->Add(m_btn_send_gcode, 0, wxLEFT, margin_5); complect_btns_sizer->Add(m_btn_export_gcode_removable, 0, wxLEFT, margin_5); - btns_sizer->Add(m_btn_reslice, 0, wxEXPAND | wxTOP, margin_5); - btns_sizer->Add(complect_btns_sizer, 0, wxEXPAND | wxTOP, margin_5); + m_btns_sizer->Add(m_btn_reslice, 0, wxEXPAND | wxTOP, margin_5); + m_btns_sizer->Add(complect_btns_sizer, 0, wxEXPAND | wxTOP, margin_5); auto *sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(m_scrolled_panel, 1, wxEXPAND); - sizer->Add(btns_sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM + + const int buttons_sizer_flags{ + wxEXPAND + | wxLEFT + | wxBOTTOM #ifndef _WIN32 | wxRIGHT #endif // __linux__ - , margin_5); + }; + sizer->Add(m_btns_sizer, 0, buttons_sizer_flags, margin_5); + + m_autoslicing_btns_sizer = new wxBoxSizer(wxHORIZONTAL); + + init_scalable_btn(&m_btn_export_all_gcode_removable, "export_to_sd", _L("Export all to SD card / Flash drive") + " " + GUI::shortkey_ctrl_prefix() + "U"); + init_btn(&m_btn_export_all_gcode, _L("Export all G-codes") + dots, scaled_height); + init_btn(&m_btn_connect_gcode_all, _L("Send all to Connect"), scaled_height); + + m_autoslicing_btns_sizer->Add(m_btn_export_all_gcode, 1, wxEXPAND); + m_autoslicing_btns_sizer->Add(m_btn_connect_gcode_all, 1, wxEXPAND | wxLEFT, margin_5); + m_autoslicing_btns_sizer->Add(m_btn_export_all_gcode_removable, 0, wxLEFT, margin_5); + + m_autoslicing_btns_sizer->Show(false); + + sizer->Add(m_autoslicing_btns_sizer, 0, buttons_sizer_flags | wxTOP, margin_5); SetSizer(sizer); // Events @@ -539,6 +559,17 @@ Sidebar::Sidebar(Plater *parent) this->Bind(wxEVT_COMBOBOX, &Sidebar::on_select_preset, this); + m_btn_export_all_gcode->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->m_plater->export_all_gcodes(false); + }); + + m_btn_export_all_gcode_removable->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->m_plater->export_all_gcodes(true); + }); + + m_btn_connect_gcode_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->m_plater->connect_gcode_all(); + }); } Sidebar::~Sidebar() {} @@ -574,6 +605,12 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) } } +void Sidebar::update_all_filament_comboboxes() +{ + for (PlaterPresetComboBox* cb : m_combos_filament) + cb->update(); +} + void Sidebar::update_all_preset_comboboxes() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; @@ -591,8 +628,7 @@ void Sidebar::update_all_preset_comboboxes() // Update the filament choosers to only contain the compatible presets, update the color preview, // update the dirty flags. if (print_tech == ptFFF) { - for (PlaterPresetComboBox* cb : m_combos_filament) - cb->update(); + update_all_filament_comboboxes(); } } @@ -709,6 +745,12 @@ void Sidebar::on_select_preset(wxCommandEvent& evt) * and for SLA presets they should be deleted */ m_object_list->update_object_list_by_printer_technology(); + s_multiple_beds.stop_autoslice(false); + this->switch_from_autoslicing_mode(); + wxQueueEvent(this->m_plater, new SimpleEvent(EVT_REGENERATE_BED_THUMBNAILS)); + this->m_plater->update(); + s_print_statuses.fill(PrintStatus::idle); + this->m_plater->canvas3D()->check_volumes_outside_state(false); } #ifdef __WXMSW__ @@ -842,11 +884,17 @@ void Sidebar::show_info_sizer() const int obj_idx = selection.get_object_idx(); const int inst_idx = selection.get_instance_idx(); - if (m_mode < comExpert || objects.empty() || obj_idx < 0 || int(objects.size()) <= obj_idx || - inst_idx < 0 || int(objects[obj_idx]->instances.size()) <= inst_idx || - objects[obj_idx]->volumes.empty() || // hack to avoid crash when deleting the last object on the bed - (selection.is_single_full_object() && objects[obj_idx]->instances.size()> 1) || - !(selection.is_single_full_instance() || selection.is_single_volume())) { + if ( + m_mode < comExpert + || objects.empty() + || obj_idx < 0 + || int(objects.size()) <= obj_idx + || inst_idx < 0 + || int(objects[obj_idx]->instances.size()) <= inst_idx + || objects[obj_idx]->volumes.empty() // hack to avoid crash when deleting the last object on the bed + || (selection.is_single_full_object() && objects[obj_idx]->instances.size()> 1) + || !(selection.is_single_full_instance() || selection.is_single_volume()) + ) { m_object_info->Show(false); return; } @@ -906,7 +954,7 @@ void Sidebar::update_sliced_info_sizer() { if (m_plater->printer_technology() == ptSLA) { - const SLAPrintStatistics& ps = m_plater->sla_print().print_statistics(); + const SLAPrintStatistics& ps = m_plater->active_sla_print().print_statistics(); wxString new_label = _L("Used Material (ml)") + ":"; const bool is_supports = ps.support_used_material > 0.0; if (is_supports) @@ -950,7 +998,7 @@ void Sidebar::update_sliced_info_sizer() } else { - const PrintStatistics& ps = m_plater->fff_print().print_statistics(); + const PrintStatistics& ps = m_plater->active_fff_print().print_statistics(); const bool is_wipe_tower = ps.total_wipe_tower_filament > 0; bool imperial_units = wxGetApp().app_config->get_bool("use_inches"); @@ -1050,6 +1098,10 @@ void Sidebar::update_sliced_info_sizer() void Sidebar::show_sliced_info_sizer(const bool show) { + if (m_autoslicing_mode) { + return; + } + wxWindowUpdateLocker freeze_guard(this); m_sliced_info->Show(show); @@ -1060,6 +1112,28 @@ void Sidebar::show_sliced_info_sizer(const bool show) m_scrolled_panel->Refresh(); } +void Sidebar::show_btns_sizer(const bool show) +{ + if (m_autoslicing_mode) { + return; + } + + wxWindowUpdateLocker freeze_guard(this); + m_btns_sizer->Show(show); + + Layout(); + m_scrolled_panel->Refresh(); +} + +void Sidebar::show_bulk_btns_sizer(const bool show) +{ + wxWindowUpdateLocker freeze_guard(this); + m_autoslicing_btns_sizer->Show(show); + + Layout(); + m_scrolled_panel->Refresh(); +} + void Sidebar::enable_buttons(bool enable) { m_btn_reslice->Enable(enable); @@ -1078,12 +1152,68 @@ void Sidebar::enable_export_buttons(bool enable) m_btn_export_gcode_removable->Enable(enable); } -bool Sidebar::show_reslice(bool show) const { return m_btn_reslice->Show(show); } -bool Sidebar::show_export(bool show) const { return m_btn_export_gcode->Show(show); } -bool Sidebar::show_send(bool show) const { return m_btn_send_gcode->Show(show); } -bool Sidebar::show_export_removable(bool show) const { return m_btn_export_gcode_removable->Show(show); } -bool Sidebar::show_connect(bool show) const { return m_btn_connect_gcode->Show(show); } +void Sidebar::enable_bulk_buttons(bool enable) +{ + m_btn_export_all_gcode->Enable(enable); + m_btn_export_all_gcode_removable->Enable(enable); + m_btn_connect_gcode_all->Enable(enable); +} +bool Sidebar::show_reslice(bool show) const { + if (this->m_autoslicing_mode) { + return false; + } + return m_btn_reslice->Show(show); +} +bool Sidebar::show_export(bool show) const { + if (this->m_autoslicing_mode) { + return false; + } + return m_btn_export_gcode->Show(show); +} +bool Sidebar::show_send(bool show) const { + if (this->m_autoslicing_mode) { + return false; + } + return m_btn_send_gcode->Show(show); +} +bool Sidebar::show_export_removable(bool show) const { + if (this->m_autoslicing_mode) { + return false; + } + return m_btn_export_gcode_removable->Show(show); +} +bool Sidebar::show_connect(bool show) const { + if (this->m_autoslicing_mode) { + return false; + } + return m_btn_connect_gcode->Show(show); +} + +bool Sidebar::show_export_all(bool show) const { + return m_btn_export_all_gcode->Show(show); +}; +bool Sidebar::show_export_removable_all(bool show) const { + return m_btn_export_all_gcode_removable->Show(show); +}; +bool Sidebar::show_connect_all(bool show) const { + return m_btn_connect_gcode_all->Show(show); +}; + +void Sidebar::switch_to_autoslicing_mode() { + this->show_sliced_info_sizer(false); + this->show_btns_sizer(false); + this->m_autoslicing_mode = true; +} + +void Sidebar::switch_from_autoslicing_mode() { + if (!this->m_autoslicing_mode) { + return; + } + this->m_autoslicing_mode = false; + this->show_sliced_info_sizer(true); + this->show_bulk_btns_sizer(false); +} void Sidebar::update_mode() { diff --git a/src/slic3r/GUI/Sidebar.hpp b/src/slic3r/GUI/Sidebar.hpp index 4ee9e46..cb8eef2 100644 --- a/src/slic3r/GUI/Sidebar.hpp +++ b/src/slic3r/GUI/Sidebar.hpp @@ -59,18 +59,26 @@ class Sidebar : public wxPanel ObjectList* m_object_list { nullptr }; ObjectInfo* m_object_info { nullptr }; SlicedInfo* m_sliced_info { nullptr }; + wxBoxSizer* m_btns_sizer { nullptr }; + wxBoxSizer* m_autoslicing_btns_sizer { nullptr }; + wxButton* m_btn_export_gcode { nullptr }; wxButton* m_btn_reslice { nullptr }; wxButton* m_btn_connect_gcode { nullptr }; ScalableButton* m_btn_send_gcode { nullptr }; ScalableButton* m_btn_export_gcode_removable{ nullptr }; //exports to removable drives (appears only if removable drive is connected) + // + wxButton* m_btn_export_all_gcode { nullptr }; + wxButton* m_btn_connect_gcode_all { nullptr }; + ScalableButton* m_btn_export_all_gcode_removable{ nullptr }; std::unique_ptr m_frequently_changed_parameters; std::unique_ptr m_object_manipulation; std::unique_ptr m_object_settings; std::unique_ptr m_object_layers; + bool m_autoslicing_mode{ false }; #ifdef _WIN32 wxString m_reslice_btn_tooltip; #endif @@ -103,6 +111,9 @@ public: void show_info_sizer(); void show_sliced_info_sizer(const bool show); + void show_btns_sizer(const bool show); + void show_bulk_btns_sizer(const bool show); + void update_sliced_info_sizer(); void enable_buttons(bool enable); @@ -115,6 +126,14 @@ public: bool show_export_removable(bool show) const; bool show_connect(bool show) const; + void enable_bulk_buttons(bool enable); + bool show_export_all(bool show) const; + bool show_export_removable_all(bool show) const; + bool show_connect_all(bool show) const; + + void switch_to_autoslicing_mode(); + void switch_from_autoslicing_mode(); + void collapse(bool collapse); void set_extruders_count(size_t extruders_count); @@ -123,6 +142,7 @@ public: void update_objects_list_extruder_column(size_t extruders_count); void update_presets(Preset::Type preset_type); void update_printer_presets_combobox(); + void update_all_filament_comboboxes(); void msw_rescale(); void sys_color_changed(); diff --git a/src/slic3r/GUI/SurfaceDrag.cpp b/src/slic3r/GUI/SurfaceDrag.cpp index 841c9a9..8cf6bde 100644 --- a/src/slic3r/GUI/SurfaceDrag.cpp +++ b/src/slic3r/GUI/SurfaceDrag.cpp @@ -538,7 +538,7 @@ bool start_dragging(const Vec2d &mouse_pos, // zero point of volume in world coordinate system Vec3d volume_center = to_world.translation(); // screen coordinate of volume center - Vec2i coor = CameraUtils::project(camera, volume_center); + Point coor = CameraUtils::project(camera, volume_center); Vec2d mouse_offset = coor.cast() - mouse_pos; Vec2d mouse_offset_without_sla_shift = mouse_offset; if (double sla_shift = gl_volume.get_sla_shift_z(); !is_approx(sla_shift, 0.)) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f56cc80..ced5425 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6,6 +6,7 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/SLAPrint.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeWriter.hpp" #include "libslic3r/GCode/Thumbnails.hpp" @@ -1447,6 +1448,7 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Quality (slower slicing)")); optgroup->append_single_option_line("extra_perimeters", category_path + "extra-perimeters-if-needed"); optgroup->append_single_option_line("extra_perimeters_on_overhangs", category_path + "extra-perimeters-on-overhangs"); + optgroup->append_single_option_line("ensure_vertical_shell_thickness", category_path + "ensure-vertical-shell-thickness"); optgroup->append_single_option_line("avoid_crossing_curled_overhangs", category_path + "avoid-crossing-curled-overhangs"); optgroup->append_single_option_line("avoid_crossing_perimeters", category_path + "avoid-crossing-perimeters"); optgroup->append_single_option_line("avoid_crossing_perimeters_max_detour", category_path + "avoid_crossing_perimeters_max_detour"); @@ -1456,10 +1458,17 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Advanced")); optgroup->append_single_option_line("seam_position", category_path + "seam-position"); - //Y21 - optgroup->append_single_option_line("seam_gap", category_path + "seam-gap"); - + 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"); + 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"); optgroup->append_single_option_line("perimeter_generator"); @@ -1495,9 +1504,10 @@ void TabPrint::build() optgroup->append_single_option_line("ironing_spacing", category_path + "spacing-between-ironing-passes"); optgroup = page->new_optgroup(L("Reducing printing time")); - category_path = "print-settings/infill#"; - optgroup->append_single_option_line("infill_every_layers", category_path + "combine-infill-every"); - // optgroup->append_single_option_line("infill_only_where_needed", category_path + "only-infill-where-needed"); + category_path = "infill_42#"; + optgroup->append_single_option_line("automatic_infill_combination"); + optgroup->append_single_option_line("automatic_infill_combination_max_layer_height"); + optgroup->append_single_option_line("infill_every_layers", category_path + "combine-infill-every-x-layers"); optgroup = page->new_optgroup(L("Advanced")); optgroup->append_single_option_line("solid_infill_every_layers", category_path + "solid-infill-every"); @@ -1652,10 +1662,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_x"); - optgroup->append_single_option_line("wipe_tower_y"); optgroup->append_single_option_line("wipe_tower_width"); - optgroup->append_single_option_line("wipe_tower_rotation_angle"); optgroup->append_single_option_line("wipe_tower_brim_width"); optgroup->append_single_option_line("wipe_tower_bridging"); optgroup->append_single_option_line("wipe_tower_cone_angle"); @@ -1679,6 +1686,7 @@ void TabPrint::build() optgroup->append_single_option_line("solid_infill_extrusion_width"); optgroup->append_single_option_line("top_infill_extrusion_width"); optgroup->append_single_option_line("support_material_extrusion_width"); + optgroup->append_single_option_line("automatic_extrusion_widths"); optgroup = page->new_optgroup(L("Overlap")); optgroup->append_single_option_line("infill_overlap"); @@ -2000,6 +2008,9 @@ std::vector>> filament_overrides {"Retraction when tool is disabled", { "filament_retract_length_toolchange", "filament_retract_restart_extra_toolchange" + }}, + {"Seams", { + "filament_seam_gap_distance" }} }; @@ -3768,8 +3779,9 @@ void TabPrinter::update_fff() bool Tab::is_qidi_printer() const { - std::string printer_model = m_preset_bundle->printers.get_edited_preset().config.opt_string("printer_model"); - return printer_model == "SL1" || printer_model == "SL1S" || printer_model == "M1"; + 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() @@ -4501,6 +4513,13 @@ void Tab::rename_preset() assert(old_name == edited_preset.name); + if (m_type == Preset::TYPE_FILAMENT) { + // Filaments will be sorted inside collection after remaning, + // so, cache preset names for each extruder to reset them after renaming + m_preset_bundle->cache_extruder_filaments_names(); + } + + bool was_renamed = true; using namespace boost; try { // rename selected and edited presets @@ -4523,10 +4542,18 @@ void Tab::rename_preset() catch (const exception& ex) { const std::string exception = diagnostic_information(ex); printf("Can't rename a preset : %s", exception.c_str()); + was_renamed = false; } // sort presets after renaming std::sort(m_presets->begin(), m_presets->end()); + + if (was_renamed && m_type == Preset::TYPE_FILAMENT) { + // Reset extruder_filaments only if preset was renamed + m_preset_bundle->reset_extruder_filaments(); + // and update compatibility for extruders after reset + m_preset_bundle->update_filaments_compatible(PresetSelectCompatibleType::OnlyIfWasCompatible); + } // update selection select_preset_by_name(new_name, true); diff --git a/src/slic3r/GUI/TopBar.cpp b/src/slic3r/GUI/TopBar.cpp index d81c8b5..ca15c7f 100644 --- a/src/slic3r/GUI/TopBar.cpp +++ b/src/slic3r/GUI/TopBar.cpp @@ -269,14 +269,13 @@ void TopBarItemsCtrl::CreateSearch() m_search->SetOnDropDownIcon([this]() { - wxGetApp().searcher().set_search_input(m_search); - wxGetApp().show_search_dialog(); + TriggerSearch(); }); m_search->Bind(wxEVT_KILL_FOCUS, [](wxFocusEvent& e) { - e.Skip(); wxGetApp().searcher().check_and_hide_dialog(); + e.Skip(); }); wxTextCtrl* ctrl = m_search->GetTextCtrl(); @@ -294,8 +293,7 @@ void TopBarItemsCtrl::CreateSearch() ctrl->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { - wxGetApp().searcher().set_search_input(m_search); - wxGetApp().show_search_dialog(); + TriggerSearch(); event.Skip(); }); @@ -307,6 +305,17 @@ void TopBarItemsCtrl::CreateSearch() }); } +void TopBarItemsCtrl::TriggerSearch() +{ + if (m_search && m_search->GetTextCtrl()) + { + wxGetApp().searcher().set_search_input(m_search); + wxGetApp().show_search_dialog(); + wxTextCtrl* ctrl = m_search->GetTextCtrl(); + ctrl->SetFocus(); // set focus back to search bar for typing + } +} + void TopBarItemsCtrl::UpdateSearchSizeAndPosition() { if (!m_workspace_btn || !m_account_btn) diff --git a/src/slic3r/GUI/TopBar.hpp b/src/slic3r/GUI/TopBar.hpp index 60c40a3..d43ce64 100644 --- a/src/slic3r/GUI/TopBar.hpp +++ b/src/slic3r/GUI/TopBar.hpp @@ -98,6 +98,7 @@ public: void UnselectPopupButtons(); void CreateSearch(); + void TriggerSearch(); void ShowFull(); void ShowJustMode(); void SetSettingsButtonTooltip(const wxString& tooltip); diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index 5ba0a28..c2cfc71 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -289,9 +289,9 @@ boost::filesystem::path AppUpdateDownloadDialog::get_download_path() const // MsgUpdateConfig -MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_before_wizard/* = false*/) : - MsgDialog(nullptr, force_before_wizard ? _L("Opening Configuration Wizard") : _L("Configuration update"), - force_before_wizard ? _L("QIDISlicer is not using the newest configuration available.\n" +MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, PresetUpdater::UpdateParams update_params) : + MsgDialog(nullptr, update_params == PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD ? _L("Opening Configuration Wizard") : _L("Configuration update"), + update_params == PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD ? _L("QIDISlicer is not using the newest configuration available.\n" "Configuration Wizard may not offer the latest printers, filaments and SLA materials to be installed.") : _L("Configuration update is available"), wxICON_ERROR) { @@ -346,12 +346,19 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_ content_sizer->Add(versions); content_sizer->AddSpacer(2*VERT_SPACING); - add_button(wxID_OK, true, force_before_wizard ? _L("Install") : "OK"); - if (force_before_wizard) { - auto* btn = add_button(wxID_CLOSE, false, _L("Don't install")); + if (update_params == PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) { + add_button(wxID_OK, true, _L("Install")); + auto* btn = add_button(wxID_CLOSE, false, _L("Don't install")); btn->Bind(wxEVT_BUTTON, [this](const wxCommandEvent&) { this->EndModal(wxID_CLOSE); }); - } - add_button(wxID_CANCEL); + add_button(wxID_CANCEL); + } else if (update_params == PresetUpdater::UpdateParams::SHOW_TEXT_BOX_YES_NO) { + add_button(wxID_OK, true, _L("Yes")); + add_button(wxID_CANCEL, false, _L("No")); + } else { + assert(update_params == PresetUpdater::UpdateParams::SHOW_TEXT_BOX); + add_button(wxID_OK, true, "OK"); + add_button(wxID_CANCEL); + } finalize(); } diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp index f531347..9726913 100644 --- a/src/slic3r/GUI/UpdateDialogs.hpp +++ b/src/slic3r/GUI/UpdateDialogs.hpp @@ -11,6 +11,8 @@ #include "libslic3r/Semver.hpp" #include "MsgDialog.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" + //B44 #include @@ -105,7 +107,7 @@ public: }; // force_before_wizard - indicates that check of updated is forced before ConfigWizard opening - MsgUpdateConfig(const std::vector &updates, bool force_before_wizard = false); + MsgUpdateConfig(const std::vector &updates, PresetUpdater::UpdateParams update_params); MsgUpdateConfig(MsgUpdateConfig &&) = delete; MsgUpdateConfig(const MsgUpdateConfig &) = delete; MsgUpdateConfig &operator=(MsgUpdateConfig &&) = delete; diff --git a/src/slic3r/GUI/UpdatesUIManager.cpp b/src/slic3r/GUI/UpdatesUIManager.cpp index 11c94ee..67232c7 100644 --- a/src/slic3r/GUI/UpdatesUIManager.cpp +++ b/src/slic3r/GUI/UpdatesUIManager.cpp @@ -1,7 +1,7 @@ #include "UpdatesUIManager.hpp" #include "I18N.hpp" #include "wxExtensions.hpp" -#include "PresetArchiveDatabase.hpp" +#include "slic3r/Utils/PresetUpdaterWrapper.hpp" #include "GUI.hpp" #include "GUI_App.hpp" @@ -18,9 +18,9 @@ namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { -RepositoryUpdateUIManager::RepositoryUpdateUIManager(wxWindow* parent, PresetArchiveDatabase* pad, int em) : +RepositoryUpdateUIManager::RepositoryUpdateUIManager(wxWindow* parent, Slic3r::PresetUpdaterWrapper* puw, int em) : m_parent(parent) - ,m_pad(pad) + ,m_puw(puw) ,m_main_sizer(new wxBoxSizer(wxVERTICAL)) { auto online_label = new wxStaticText(m_parent, wxID_ANY, _L("Online sources")); @@ -100,10 +100,10 @@ void RepositoryUpdateUIManager::fill_entries(bool init_selection/* = false*/) m_online_entries.clear(); m_offline_entries.clear(); - const SharedArchiveRepositoryVector& archs = m_pad->get_all_archive_repositories(); + const SharedArchiveRepositoryVector& archs = m_puw->get_all_archive_repositories(); for (const auto* archive : archs) { const std::string& uuid = archive->get_uuid(); - if (init_selection && m_pad->is_selected_repository_by_uuid(uuid)) + if (init_selection && m_puw->is_selected_repository_by_uuid(uuid)) m_selected_uuids.emplace(uuid); const bool is_selected = m_selected_uuids.find(uuid) != m_selected_uuids.end(); @@ -256,7 +256,7 @@ void RepositoryUpdateUIManager::update() void RepositoryUpdateUIManager::remove_offline_repos(const std::string& id) { - m_pad->remove_local_archive(id); + m_puw->remove_local_archive(id); m_selected_uuids.erase(id); check_selection(); @@ -287,7 +287,7 @@ void RepositoryUpdateUIManager::load_offline_repos() try { fs::path input_path = fs::path(input_file); std::string msg; - std::string uuid = m_pad->add_local_archive(input_path, msg); + std::string uuid = m_puw->add_local_archive(input_path, msg); if (uuid.empty()) { ErrorDialog(m_parent, from_u8(msg), false).ShowModal(); } @@ -310,7 +310,7 @@ bool RepositoryUpdateUIManager::set_selected_repositories() std::string msg; - if (m_pad->set_selected_repositories(used_ids, msg)) { + if (m_puw->set_selected_repositories(used_ids, msg)) { check_selection(); return true; } @@ -324,7 +324,7 @@ bool RepositoryUpdateUIManager::set_selected_repositories() void RepositoryUpdateUIManager::check_selection() { - for (const auto& [uuid, is_selected] : m_pad->get_selected_repositories_uuid() ) + for (const auto& [uuid, is_selected] : m_puw->get_selected_repositories_uuid() ) if ((is_selected && m_selected_uuids.find(uuid) == m_selected_uuids.end() )|| (!is_selected && m_selected_uuids.find(uuid) != m_selected_uuids.end())) { m_is_selection_changed = true; @@ -334,7 +334,7 @@ void RepositoryUpdateUIManager::check_selection() m_is_selection_changed = false; } -ManagePresetRepositoriesDialog::ManagePresetRepositoriesDialog(PresetArchiveDatabase* pad) +ManagePresetRepositoriesDialog::ManagePresetRepositoriesDialog(Slic3r::PresetUpdaterWrapper* puw) : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, format_wxstr("%1% - %2%", SLIC3R_APP_NAME, _L("Manage Updates")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) @@ -342,7 +342,7 @@ ManagePresetRepositoriesDialog::ManagePresetRepositoriesDialog(PresetArchiveData this->SetFont(wxGetApp().normal_font()); const int em = em_unit(); - m_manager = std::make_unique(this, pad, em); + m_manager = std::make_unique(this, puw, em); auto sizer = m_manager->get_sizer(); diff --git a/src/slic3r/GUI/UpdatesUIManager.hpp b/src/slic3r/GUI/UpdatesUIManager.hpp index f295ce6..b32443b 100644 --- a/src/slic3r/GUI/UpdatesUIManager.hpp +++ b/src/slic3r/GUI/UpdatesUIManager.hpp @@ -12,10 +12,10 @@ class wxSizer; class wxFlexGridSizer; namespace Slic3r { + +class PresetUpdaterWrapper; + namespace GUI { - -class PresetArchiveDatabase; - class RepositoryUpdateUIManager { struct OnlineEntry { @@ -42,7 +42,7 @@ class RepositoryUpdateUIManager boost::filesystem::path source_path; }; - PresetArchiveDatabase* m_pad { nullptr }; + PresetUpdaterWrapper* m_puw { nullptr }; wxWindow* m_parent { nullptr }; wxSizer* m_main_sizer { nullptr }; @@ -66,7 +66,7 @@ class RepositoryUpdateUIManager public: RepositoryUpdateUIManager() {} - RepositoryUpdateUIManager(wxWindow* parent, PresetArchiveDatabase* pad, int em); + RepositoryUpdateUIManager(wxWindow* parent, Slic3r::PresetUpdaterWrapper* puw, int em); ~RepositoryUpdateUIManager() {} void update(); @@ -81,7 +81,7 @@ public: class ManagePresetRepositoriesDialog : public DPIDialog { public: - ManagePresetRepositoriesDialog(PresetArchiveDatabase* pad); + ManagePresetRepositoriesDialog(PresetUpdaterWrapper* puw); ~ManagePresetRepositoriesDialog() {} protected: diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp index 6ceae7e..17b24e4 100644 --- a/src/slic3r/GUI/UserAccount.cpp +++ b/src/slic3r/GUI/UserAccount.cpp @@ -103,6 +103,10 @@ void UserAccount::enqueue_printer_data_action(const std::string& uuid) { m_communication->enqueue_printer_data_action(uuid); } +void UserAccount::request_refresh() +{ + m_communication->request_refresh(); +} bool UserAccount::on_login_code_recieved(const std::string& url_message) { @@ -199,7 +203,7 @@ bool UserAccount::on_connect_printers_success(const std::string& data, AppConfig if (auto pair = printer_state_table.find(*printer_state); pair != printer_state_table.end()) { state = pair->second; } else { - assert(true); // On this assert, printer_state_table needs to be updated with *state_opt and correct ConnectPrinterState + assert(false); // On this assert, printer_state_table needs to be updated with *state_opt and correct ConnectPrinterState continue; } if (m_printer_uuid_map.find(*printer_uuid) == m_printer_uuid_map.end()) { diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index 4988333..9a01932 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -48,6 +48,7 @@ public: void enqueue_connect_printer_models_action(); void enqueue_avatar_action(); void enqueue_printer_data_action(const std::string& uuid); + void request_refresh(); // Clears all data and connections, called on logout or EVT_UA_RESET void clear(); diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 7e37a70..10ffc8f 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -242,8 +242,6 @@ void UserAccountCommunication::set_username(const std::string& username) { m_username = username; { - // We don't need mutex lock here, as credentials are guarded by own mutex in m_session - //std::lock_guard lock(m_session_mutex); if (is_secret_store_ok()) { std::string tokens; if (m_remember_session) { @@ -292,36 +290,28 @@ void UserAccountCommunication::set_remember_session(bool b) std::string UserAccountCommunication::get_access_token() { - { - // We don't need mutex lock here, as credentials are guarded by own mutex in m_session - //std::lock_guard lock(m_session_mutex); - return m_session->get_access_token(); - } + + return m_session->get_access_token(); + } std::string UserAccountCommunication::get_shared_session_key() { - { - // We don't need mutex lock here, as credentials are guarded by own mutex in m_session - //std::lock_guard lock(m_session_mutex); - return m_session->get_shared_session_key(); - } + + return m_session->get_shared_session_key(); + } void UserAccountCommunication::set_polling_enabled(bool enabled) { - { - std::lock_guard lock(m_session_mutex); - return m_session->set_polling_action(enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY); - } + return m_session->set_polling_action(enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY); } void UserAccountCommunication::on_uuid_map_success() { - { - std::lock_guard lock(m_session_mutex); - return m_session->set_polling_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS); - } + + return m_session->set_polling_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS); + } // Generates and stores Code Verifier - second call deletes previous one. @@ -371,13 +361,10 @@ bool UserAccountCommunication::is_logged() } void UserAccountCommunication::do_login() { - { - std::lock_guard lock(m_session_mutex); - if (!m_session->is_initialized()) { - login_redirect(); - } else { - m_session->enqueue_test_with_refresh(); - } + if (!m_session->is_initialized()) { + login_redirect(); + } else { + m_session->enqueue_test_with_refresh(); } wakeup_session_thread(); } @@ -389,101 +376,85 @@ void UserAccountCommunication::do_logout() void UserAccountCommunication::do_clear() { - { - std::lock_guard lock(m_session_mutex); - m_session->clear(); - } + m_session->clear(); set_username({}); m_token_timer->Stop(); } void UserAccountCommunication::on_login_code_recieved(const std::string& url_message) { - { - std::lock_guard lock(m_session_mutex); - const std::string code = get_code_from_message(url_message); - m_session->init_with_code(code, m_code_verifier); - } + const std::string code = get_code_from_message(url_message); + m_session->init_with_code(code, m_code_verifier); wakeup_session_thread(); } void UserAccountCommunication::enqueue_connect_printer_models_action() { - { - std::lock_guard lock(m_session_mutex); - if (!m_session->is_initialized()) { - BOOST_LOG_TRIVIAL(error) << "Connect Printer Models connection failed - Not Logged in."; - return; - } - m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS, nullptr, nullptr, {}); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Printer Models connection failed - Not Logged in."; + return; } + m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS, nullptr, nullptr, {}); wakeup_session_thread(); } void UserAccountCommunication::enqueue_connect_status_action() { - { - std::lock_guard lock(m_session_mutex); - if (!m_session->is_initialized()) { - BOOST_LOG_TRIVIAL(error) << "Connect Status endpoint connection failed - Not Logged in."; - return; - } - m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS, nullptr, nullptr, {}); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Status endpoint connection failed - Not Logged in."; + return; } + m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS, nullptr, nullptr, {}); wakeup_session_thread(); } + void UserAccountCommunication::enqueue_test_connection() { - { - std::lock_guard lock(m_session_mutex); - if (!m_session->is_initialized()) { - BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; - return; - } - m_session->enqueue_test_with_refresh(); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Printers 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) { - { - std::lock_guard lock(m_session_mutex); - 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); + 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); wakeup_session_thread(); } void UserAccountCommunication::enqueue_printer_data_action(const std::string& uuid) { - { - std::lock_guard lock(m_session_mutex); - 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_CONNECT_DATA_FROM_UUID, nullptr, nullptr, uuid); + 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_CONNECT_DATA_FROM_UUID, nullptr, nullptr, uuid); wakeup_session_thread(); } + +void UserAccountCommunication::request_refresh() +{ + m_token_timer->Stop(); + enqueue_refresh(); +} + void UserAccountCommunication::enqueue_refresh() { - { - std::lock_guard lock(m_session_mutex); - if (!m_session->is_initialized()) { - BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; - return; - } - if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) { - BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping..."; - return; - } - m_session->enqueue_refresh({}); + if (!m_session->is_initialized()) { + BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; + return; } + if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) { + BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping..."; + return; + } + m_session->enqueue_refresh({}); wakeup_session_thread(); } @@ -505,10 +476,7 @@ void UserAccountCommunication::init_session_thread() continue; } m_thread_wakeup = false; - { - std::lock_guard lock(m_session_mutex); - m_session->process_action_queue(); - } + m_session->process_action_queue(); } }); } @@ -529,8 +497,7 @@ void UserAccountCommunication::on_activate_app(bool active) #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"; - m_token_timer->Stop(); - enqueue_refresh(); + request_refresh(); } } @@ -547,9 +514,10 @@ void UserAccountCommunication::set_refresh_time(int seconds) { assert(m_token_timer); m_token_timer->Stop(); - const auto prior_expiration_secs = 5 * 60; - int milliseconds = std::max((seconds - prior_expiration_secs) * 1000, 60000); + 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; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << milliseconds / 1000; m_token_timer->StartOnce(milliseconds); } diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index a267cea..0ba2652 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -53,6 +53,7 @@ public: void enqueue_test_connection(); void enqueue_printer_data_action(const std::string& uuid); void enqueue_refresh(); + void request_refresh(); // Callbacks - called from UI after receiving Event from Session thread. Some might use Session thread. // @@ -80,7 +81,6 @@ public: private: std::unique_ptr m_session; std::thread m_thread; - std::mutex m_session_mutex; std::mutex m_thread_stop_mutex; std::condition_variable m_thread_stop_condition; bool m_thread_stop { false }; @@ -96,7 +96,6 @@ private: bool m_remember_session { true }; // if default is true, on every login Remember me will be checked. wxTimer* m_token_timer; - wxEvtHandler* m_timer_evt_handler; std::time_t m_next_token_refresh_at{0}; void wakeup_session_thread(); diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 5bbc1ca..f06e710 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -31,10 +30,12 @@ wxDEFINE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_RESET, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_QIDICONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_REFRESH_TIME, UserAccountTimeEvent); +wxDEFINE_EVENT(EVT_UA_ENQUEUED_REFRESH, 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; auto http = Http::post(std::move(url)); if (!input.empty()) http.set_post_body(input); @@ -53,6 +54,7 @@ void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ co 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; auto http = Http::get(std::move(url)); if (!access_token.empty()) { http.header("Authorization", "Bearer " + access_token); @@ -82,79 +84,102 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin http.perform_sync(HttpRetryOpt::default_retry()); } -bool UserAccountSession::is_enqueued(UserAccountActionID action_id) const { - return std::any_of( - std::begin(m_priority_action_queue), std::end(m_priority_action_queue), - [action_id](const ActionQueueData& item) { return item.action_id == action_id; } - ); +bool UserAccountSession::is_enqueued(UserAccountActionID action_id) const +{ + { + std::lock_guard lock(m_session_mutex); + return std::any_of( + 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::process_action_queue() { - if (!m_proccessing_enabled) - return; - if (m_priority_action_queue.empty() && m_action_queue.empty()) { - // update printers periodically - enqueue_action(m_polling_action, nullptr, nullptr, {}); - } - // priority queue works even when tokens are empty or broken - while (!m_priority_action_queue.empty()) { - std::string access_token; - { - std::lock_guard lock(m_credentials_mutex); - access_token = m_access_token; + { + std::lock_guard lock(m_session_mutex); + if (!m_proccessing_enabled) + return; + if (m_priority_action_queue.empty() && m_action_queue.empty()) { + // update printers periodically + enqueue_action_inner(m_polling_action, nullptr, nullptr, {}); } - m_actions[m_priority_action_queue.front().action_id]->perform(p_evt_handler, access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input); - if (!m_priority_action_queue.empty()) + } + process_action_queue_inner(); +} +void UserAccountSession::process_action_queue_inner() +{ + bool call_priority = false; + bool call_standard = false; + ActionQueueData selected_data; + { + std::lock_guard lock(m_session_mutex); + + // priority queue works even when tokens are empty or broken + if (!m_priority_action_queue.empty()) { + // Do a copy here even its costly. We need to get data outside m_session_mutex protected code to perform background operation over it. + selected_data = m_priority_action_queue.front(); m_priority_action_queue.pop_front(); - } - // regular queue has to wait until priority fills tokens - if (!this->is_initialized()) - return; - while (!m_action_queue.empty()) { - std::string access_token; - { - std::lock_guard lock(m_credentials_mutex); - access_token = m_access_token; - } - m_actions[m_action_queue.front().action_id]->perform(p_evt_handler, access_token, m_action_queue.front().success_callback, m_action_queue.front().fail_callback, m_action_queue.front().input); - if (!m_action_queue.empty()) + call_priority = true; + } else if (this->is_initialized() && !m_action_queue.empty()) { + // regular queue has to wait until priority fills tokens + // Do a copy here even its costly. We need to get data outside m_session_mutex protected code to perform background operation over it. + selected_data = m_action_queue.front(); m_action_queue.pop(); + call_standard = true; + } } + 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); + process_action_queue_inner(); + } } void UserAccountSession::enqueue_action(UserAccountActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) { - m_proccessing_enabled = true; - m_action_queue.push({ id, success_callback, fail_callback, input }); + { + std::lock_guard lock(m_session_mutex); + enqueue_action_inner(id, success_callback, fail_callback, input); + } } +// called from m_session_mutex protected code only! +void UserAccountSession::enqueue_action_inner(UserAccountActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) +{ + m_proccessing_enabled = true; + m_action_queue.push({ id, success_callback, fail_callback, input }); +} void UserAccountSession::init_with_code(const std::string& code, const std::string& code_verifier) { - // Data we have - const std::string REDIRECT_URI = "qidislicer://login"; - std::string post_fields = "code=" + code + - "&client_id=" + client_id() + - "&grant_type=authorization_code" + - "&redirect_uri=" + REDIRECT_URI + - "&code_verifier="+ code_verifier; + { + std::lock_guard lock(m_session_mutex); + // Data we have + const std::string REDIRECT_URI = "qidislicer://login"; + std::string post_fields = "code=" + code + + "&client_id=" + client_id() + + "&grant_type=authorization_code" + + "&redirect_uri=" + REDIRECT_URI + + "&code_verifier="+ code_verifier; - m_proccessing_enabled = true; - // fail fn might be cancel_queue here - m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN - , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) - , std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1) - , post_fields }); + m_proccessing_enabled = true; + // fail fn might be cancel_queue here + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN + , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) + , std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1) + , post_fields }); + } } 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"; // Data we need std::string access_token, refresh_token, shared_session_key; - int expires_in = 300; try { std::stringstream ss(body); pt::ptree ptree; @@ -163,7 +188,6 @@ void UserAccountSession::token_success_callback(const std::string& body) const auto access_token_optional = ptree.get_optional("access_token"); const auto refresh_token_optional = ptree.get_optional("refresh_token"); const auto shared_session_key_optional = ptree.get_optional("shared_session_key"); - const auto expires_in_optional = ptree.get_optional("expires_in"); if (access_token_optional) access_token = *access_token_optional; @@ -171,17 +195,14 @@ void UserAccountSession::token_success_callback(const std::string& body) refresh_token = *refresh_token_optional; if (shared_session_key_optional) shared_session_key = *shared_session_key_optional; - assert(expires_in_optional); - if (expires_in_optional) - expires_in = *expires_in_optional; } catch (const std::exception&) { std::string msg = "Could not parse server response after code exchange."; wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, std::move(msg))); return; } - - if (access_token.empty() || refresh_token.empty() || shared_session_key.empty()) { + int expires_in = Utils::get_exp_seconds(access_token); + if (access_token.empty() || refresh_token.empty() || shared_session_key.empty() || expires_in <= 0) { // just debug msg, no need to translate std::string msg = GUI::format("Failed read tokens after POST.\nAccess token: %1%\nRefresh token: %2%\nShared session token: %3%\nbody: %4%", access_token, refresh_token, shared_session_key, body); { @@ -210,23 +231,28 @@ void UserAccountSession::token_success_callback(const std::string& body) void UserAccountSession::code_exchange_fail_callback(const std::string& body) { + BOOST_LOG_TRIVIAL(debug) << "Access token refresh failed, body: " << body; clear(); cancel_queue(); // Unlike refresh_fail_callback, no event was triggered so far, do it. (USER_ACCOUNT_ACTION_CODE_FOR_TOKEN does not send events) - wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, std::move(body))); + wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, body)); } void UserAccountSession::enqueue_test_with_refresh() { - // on test fail - try refresh - m_proccessing_enabled = true; - m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); + { + std::lock_guard lock(m_session_mutex); + // on test fail - try refresh + m_proccessing_enabled = true; + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); + } } void UserAccountSession::enqueue_refresh(const std::string& body) { + wxQueueEvent(p_evt_handler, new SimpleEvent(EVT_UA_ENQUEUED_REFRESH)); std::string post_fields; { std::lock_guard lock(m_credentials_mutex); @@ -235,11 +261,13 @@ void UserAccountSession::enqueue_refresh(const std::string& body) "&client_id=" + client_id() + "&refresh_token=" + m_refresh_token; } - - 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_callback, this, std::placeholders::_1) - , post_fields }); + { + 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_callback, this, std::placeholders::_1) + , post_fields }); + } } void UserAccountSession::refresh_fail_callback(const std::string& body) @@ -249,15 +277,17 @@ void UserAccountSession::refresh_fail_callback(const std::string& body) // No need to notify UI thread here // backtrace: load tokens -> TEST_TOKEN fail (access token bad) -> REFRESH_TOKEN fail (refresh token bad) // USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN triggers EVT_UA_FAIL, we need also RESET - wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, std::move(body))); - + wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, body)); } void UserAccountSession::cancel_queue() { - m_priority_action_queue.clear(); - while (!m_action_queue.empty()) { - m_action_queue.pop(); + { + std::lock_guard lock(m_session_mutex); + m_priority_action_queue.clear(); + while (!m_action_queue.empty()) { + m_action_queue.pop(); + } } } diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index 8d7fcd7..c186c0d 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -30,6 +30,7 @@ wxDECLARE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); // Soft fail - clears only a wxDECLARE_EVENT(EVT_UA_RESET, 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); typedef std::function UserActionSuccessFn; typedef std::function UserActionFailFn; @@ -121,7 +122,7 @@ public: 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_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_FAIL); + 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() { @@ -142,7 +143,10 @@ public: m_refresh_token.clear(); m_shared_session_key.clear(); } - m_proccessing_enabled = false; + { + std::lock_guard lock(m_session_mutex); + m_proccessing_enabled = false; + } } // Functions that automatically enable action queu processing @@ -151,8 +155,8 @@ public: // Special enques, that sets callbacks. void enqueue_test_with_refresh(); void enqueue_refresh(const std::string& body); - void process_action_queue(); + bool is_initialized() const { std::lock_guard lock(m_credentials_mutex); return !m_access_token.empty() || !m_refresh_token.empty(); @@ -176,21 +180,22 @@ public: } //void set_polling_enabled(bool enabled) {m_polling_action = enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY; } - void set_polling_action(UserAccountActionID action) { m_polling_action = action; } + 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 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(); - // false prevents action queu to be processed - no communication is done - // sets to true by init_with_code or enqueue_action call - bool m_proccessing_enabled {false}; - // action when woken up on idle - switches between USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS and USER_ACCOUNT_ACTION_CONNECT_STATUS - // set to USER_ACCOUNT_ACTION_DUMMY to switch off polling - UserAccountActionID m_polling_action; + // called from m_session_mutex protected code only + void enqueue_action_inner(UserAccountActionID id, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input); + + // Section of following vars is guarded by this mutex mutable std::mutex m_credentials_mutex; @@ -200,8 +205,21 @@ private: long long m_next_token_timeout; // End of section guarded by m_credentials_mutex + + // Section of following vars is guarded by this mutex + mutable std::mutex m_session_mutex; + std::queue m_action_queue; - std::deque m_priority_action_queue; + std::deque m_priority_action_queue; + // false prevents action queue to be processed - no communication is done + // sets to true by init_with_code or enqueue_action call + bool m_proccessing_enabled {false}; + // action when woken up on idle - switches between USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS and USER_ACCOUNT_ACTION_CONNECT_STATUS + // set to USER_ACCOUNT_ACTION_DUMMY to switch off polling + UserAccountActionID m_polling_action; + + // End of section guarded by m_session_mutex + std::map> m_actions; wxEvtHandler* p_evt_handler; diff --git a/src/slic3r/GUI/UserAccountUtils.cpp b/src/slic3r/GUI/UserAccountUtils.cpp index 81c8dfb..8b0b6db 100644 --- a/src/slic3r/GUI/UserAccountUtils.cpp +++ b/src/slic3r/GUI/UserAccountUtils.cpp @@ -26,17 +26,6 @@ std::string parse_tree_for_param(const pt::ptree& tree, const std::string& param return {}; } -void parse_tree_for_param_vector( -const pt::ptree &tree, const std::string& param, std::vector& results) { - for (const auto §ion : tree) { - if (section.first == param) { - results.emplace_back(section.second.data()); - } else { - parse_tree_for_param_vector(section.second, param, results); - } - } -} - pt::ptree parse_tree_for_subtree(const pt::ptree& tree, const std::string& param) { for (const auto §ion : tree) { if (section.first == param) { @@ -324,7 +313,6 @@ std::string get_print_data_from_json(const std::string& json, const std::string& size_t end_of_sub = json.find('}', end_of_filename_data); if (end_of_sub == std::string::npos) return {}; - size_t size = json.size(); std::string result = json.substr(start_of_sub, start_of_filename_data - start_of_sub + 1); result += "%1%"; result += json.substr(end_of_filename_data, end_of_sub - end_of_filename_data); diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 060ecc9..d79e475 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -64,7 +64,8 @@ ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_AN // set the frame icon // Create the webview - m_browser = WebView::CreateWebView(this, TargetUrl, {"wx"}); + m_browser = WebView::webview_new(); + WebView::webview_create(m_browser, this, TargetUrl, {"wx"}); if (m_browser == nullptr) { wxLogError("Could not init m_browser"); return; diff --git a/src/slic3r/GUI/WebView.cpp b/src/slic3r/GUI/WebView.cpp index 902cd43..edde73d 100644 --- a/src/slic3r/GUI/WebView.cpp +++ b/src/slic3r/GUI/WebView.cpp @@ -1,13 +1,16 @@ #include "WebView.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/format.hpp" + +#include "libslic3r/Platform.hpp" #include #include #include -wxWebView* WebView::CreateWebView(wxWindow * parent, const wxString& url, const std::vector& message_handlers) +wxWebView* WebView::webview_new() { #if wxUSE_WEBVIEW_EDGE bool backend_available = wxWebView::IsBackendAvailable(wxWebViewBackendEdge); @@ -18,46 +21,42 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, const wxString& url, const wxWebView* webView = nullptr; if (backend_available) webView = wxWebView::New(); - - if (webView) { - wxString correct_url = url.empty() ? wxString("") : wxURI(url).BuildURI(); - -#ifdef __WIN32__ - //y15 - //webView->SetUserAgent(SLIC3R_APP_FULL_NAME); - - webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); - //We register the wxfs:// protocol for testing purposes - //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); - //And the memory: file system - //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); -#else - // With WKWebView handlers need to be registered before creation - //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); - // And the memory: file system - //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); - webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); - webView->SetUserAgent(wxString::FromUTF8(SLIC3R_APP_FULL_NAME)); -#endif -#ifndef __WIN32__ - Slic3r::GUI::wxGetApp().CallAfter([message_handlers, webView] { -#endif - for (const std::string& handler : message_handlers) { - if (!webView->AddScriptMessageHandler(Slic3r::GUI::into_u8(handler))) { - // TODO: dialog to user !!! - //wxLogError("Could not add script message handler"); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Could not add script message handler " << handler; - } - } -#ifndef __WIN32__ - }); -#endif - webView->EnableContextMenu(false); - } else { - // TODO: dialog to user !!! + if (!webView) BOOST_LOG_TRIVIAL(error) << "Failed to create wxWebView object."; - } return webView; } +void WebView::webview_create(wxWebView* webView, wxWindow *parent, const wxString& url, const std::vector& message_handlers) +{ + assert(webView); + wxString correct_url = url.empty() ? wxString("") : wxURI(url).BuildURI(); + wxString user_agent = Slic3r::GUI::format_wxstr("%1%/%2% (%3%)",SLIC3R_APP_FULL_NAME, SLIC3R_VERSION, Slic3r::platform_to_string(Slic3r::platform())); - +#ifdef __WIN32__ + //y15 + //webView->SetUserAgent(user_agent); + webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); + //We register the wxfs:// protocol for testing purposes + //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); + //And the memory: file system + //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); +#else + // With WKWebView handlers need to be registered before creation + //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); + // And the memory: file system + //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); + webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); + //webView->SetUserAgent(user_agent); +#endif +#ifndef __WIN32__ + Slic3r::GUI::wxGetApp().CallAfter([message_handlers, webView] { +#endif + for (const std::string& handler : message_handlers) { + if (!webView->AddScriptMessageHandler(Slic3r::GUI::from_u8(handler))) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Could not add script message handler " << handler; + } + } +#ifndef __WIN32__ + }); +#endif + webView->EnableContextMenu(false); +} \ No newline at end of file diff --git a/src/slic3r/GUI/WebView.hpp b/src/slic3r/GUI/WebView.hpp index c7c4c79..11afc09 100644 --- a/src/slic3r/GUI/WebView.hpp +++ b/src/slic3r/GUI/WebView.hpp @@ -10,7 +10,8 @@ class wxString; namespace WebView { - wxWebView *CreateWebView(wxWindow *parent, const wxString& url, const std::vector& message_handlers); + wxWebView* webview_new(); + void webview_create(wxWebView* webview, wxWindow *parent, const wxString& url, const std::vector& message_handlers); }; #endif // !slic3r_GUI_WebView_hpp_ diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index a107dac..734f391 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -15,18 +15,13 @@ #include // IWYU pragma: keep - #include +#include #include #include #include -// if set to 1 the fetch() JS function gets override to include JWT in authorization header -// if set to 0, the /slicer/login is invoked from WebKit (passing JWT token only to this request) -// to set authorization cookie for all WebKit requests to Connect -#define AUTH_VIA_FETCH_OVERRIDE 0 - wxDEFINE_EVENT(EVT_OPEN_EXTERNAL_LOGIN, wxCommandEvent); namespace pt = boost::property_tree; @@ -34,897 +29,7 @@ namespace pt = boost::property_tree; namespace Slic3r { namespace GUI { - -WebViewPanel::~WebViewPanel() -{ - SetEvtHandlerEnabled(false); -#ifdef DEBUG_URL_PANEL - delete m_tools_menu; -#endif -} - -void WebViewPanel::load_url(const wxString& url) -{ - if (!m_browser) - return; - - this->on_page_will_load(); - - this->Show(); - this->Raise(); -#ifdef DEBUG_URL_PANEL - m_url->SetLabelText(url); -#endif - m_browser->LoadURL(url); - m_browser->SetFocus(); -} - - -WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html/* = "loading"*/) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) - , m_default_url (default_url) - , m_loading_html(loading_html) - , m_script_message_hadler_names(message_handler_names) -{ - wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); -#ifdef DEBUG_URL_PANEL - // Create the button - bSizer_toolbar = new wxBoxSizer(wxHORIZONTAL); - - m_button_back = new wxButton(this, wxID_ANY, wxT("Back"), wxDefaultPosition, wxDefaultSize, 0); - m_button_back->Enable(false); - bSizer_toolbar->Add(m_button_back, 0, wxALL, 5); - - m_button_forward = new wxButton(this, wxID_ANY, wxT("Forward"), wxDefaultPosition, wxDefaultSize, 0); - m_button_forward->Enable(false); - bSizer_toolbar->Add(m_button_forward, 0, wxALL, 5); - - m_button_stop = new wxButton(this, wxID_ANY, wxT("Stop"), wxDefaultPosition, wxDefaultSize, 0); - - bSizer_toolbar->Add(m_button_stop, 0, wxALL, 5); - - m_button_reload = new wxButton(this, wxID_ANY, wxT("Reload"), wxDefaultPosition, wxDefaultSize, 0); - bSizer_toolbar->Add(m_button_reload, 0, wxALL, 5); - - m_url = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); - bSizer_toolbar->Add(m_url, 1, wxALL | wxEXPAND, 5); - - m_button_tools = new wxButton(this, wxID_ANY, wxT("Tools"), wxDefaultPosition, wxDefaultSize, 0); - bSizer_toolbar->Add(m_button_tools, 0, wxALL, 5); - - // Create panel for find toolbar. - wxPanel* panel = new wxPanel(this); - topsizer->Add(bSizer_toolbar, 0, wxEXPAND, 0); - topsizer->Add(panel, wxSizerFlags().Expand()); - - // Create sizer for panel. - wxBoxSizer* panel_sizer = new wxBoxSizer(wxVERTICAL); - panel->SetSizer(panel_sizer); - - // Create the info panel - m_info = new wxInfoBar(this); - topsizer->Add(m_info, wxSizerFlags().Expand()); -#endif - - SetSizer(topsizer); - - // Create the webview - m_browser = WebView::CreateWebView(this, /*m_default_url*/ GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_loading_html), m_script_message_hadler_names); - if (Utils::ServiceConfig::instance().webdev_enabled()) { - m_browser->EnableContextMenu(); - m_browser->EnableAccessToDevTools(); - } - if (!m_browser) { - wxStaticText* text = new wxStaticText(this, wxID_ANY, _L("Failed to load a web browser.")); - topsizer->Add(text, 0, wxALIGN_LEFT | wxBOTTOM, 10); - return; - } - topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); -#ifdef DEBUG_URL_PANEL - // Create the Tools menu - m_tools_menu = new wxMenu(); - wxMenuItem* viewSource = m_tools_menu->Append(wxID_ANY, "View Source"); - wxMenuItem* viewText = m_tools_menu->Append(wxID_ANY, "View Text"); - m_tools_menu->AppendSeparator(); - - wxMenu* script_menu = new wxMenu; - - m_script_custom = script_menu->Append(wxID_ANY, "Custom script"); - m_tools_menu->AppendSubMenu(script_menu, "Run Script"); - wxMenuItem* addUserScript = m_tools_menu->Append(wxID_ANY, "Add user script"); - wxMenuItem* setCustomUserAgent = m_tools_menu->Append(wxID_ANY, "Set custom user agent"); - - m_context_menu = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Context Menu"); - m_dev_tools = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Dev Tools"); - -#endif - - Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); - - // Connect the webview events - Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::on_error, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::on_script_message, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::on_navigation_request, this, m_browser->GetId()); - -#ifdef DEBUG_URL_PANEL - // Connect the button events - Bind(wxEVT_BUTTON, &WebViewPanel::on_back_button, this, m_button_back->GetId()); - Bind(wxEVT_BUTTON, &WebViewPanel::on_forward_button, this, m_button_forward->GetId()); - Bind(wxEVT_BUTTON, &WebViewPanel::on_stop_button, this, m_button_stop->GetId()); - Bind(wxEVT_BUTTON, &WebViewPanel::on_reload_button, this, m_button_reload->GetId()); - Bind(wxEVT_BUTTON, &WebViewPanel::on_tools_clicked, this, m_button_tools->GetId()); - Bind(wxEVT_TEXT_ENTER, &WebViewPanel::on_url, this, m_url->GetId()); - - // Connect the menu events - Bind(wxEVT_MENU, &WebViewPanel::on_view_source_request, this, viewSource->GetId()); - Bind(wxEVT_MENU, &WebViewPanel::on_view_text_request, this, viewText->GetId()); - Bind(wxEVT_MENU, &WebViewPanel::On_enable_context_menu, this, m_context_menu->GetId()); - Bind(wxEVT_MENU, &WebViewPanel::On_enable_dev_tools, this, m_dev_tools->GetId()); - - Bind(wxEVT_MENU, &WebViewPanel::on_run_script_custom, this, m_script_custom->GetId()); - Bind(wxEVT_MENU, &WebViewPanel::on_add_user_script, this, addUserScript->GetId()); -#endif - //Connect the idle events - Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); -} - -void WebViewPanel::load_default_url_delayed() -{ - assert(!m_default_url.empty()); - m_load_default_url = true; -} - -void WebViewPanel::load_error_page() -{ - if (!m_browser) - return; - - m_browser->Stop(); - m_load_error_page = true; -} - -void WebViewPanel::on_show(wxShowEvent& evt) -{ - m_shown = evt.IsShown(); - if (evt.IsShown() && m_load_default_url) { - m_load_default_url = false; - load_url(m_default_url); - } -} - -void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - if (m_browser->IsBusy()) { - wxSetCursor(wxCURSOR_ARROWWAIT); - } else { - wxSetCursor(wxNullCursor); - - if (m_shown && m_load_error_page) { - m_load_error_page = false; - load_url(GUI::format_wxstr("file://%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string())); - } - } -#ifdef DEBUG_URL_PANEL - m_button_stop->Enable(m_browser->IsBusy()); -#endif -} - -/** - * Callback invoked when user entered an URL and pressed enter - */ -void WebViewPanel::on_url(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; -#ifdef DEBUG_URL_PANEL - m_browser->LoadURL(m_url->GetValue()); - m_browser->SetFocus(); -#endif -} - -/** - * Callback invoked when user pressed the "back" button - */ -void WebViewPanel::on_back_button(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - m_browser->GoBack(); -} - -/** - * Callback invoked when user pressed the "forward" button - */ -void WebViewPanel::on_forward_button(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - m_browser->GoForward(); -} - -/** - * Callback invoked when user pressed the "stop" button - */ -void WebViewPanel::on_stop_button(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - m_browser->Stop(); -} - -/** - * Callback invoked when user pressed the "reload" button - */ -void WebViewPanel::on_reload_button(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - m_browser->Reload(); -} - -void WebViewPanel::on_script_message(wxWebViewEvent& evt) -{ -} - -void WebViewPanel::on_navigation_request(wxWebViewEvent &evt) -{ -} - -void WebViewPanel::on_page_will_load() -{ -} - -/** - * Invoked when user selects the "View Source" menu item - */ -void WebViewPanel::on_view_source_request(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - SourceViewDialog dlg(this, m_browser->GetPageSource()); - dlg.ShowModal(); -} - -/** - * Invoked when user selects the "View Text" menu item - */ -void WebViewPanel::on_view_text_request(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - wxDialog textViewDialog(this, wxID_ANY, "Page Text", - wxDefaultPosition, wxSize(700, 500), - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); - - wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, m_browser->GetPageText(), - wxDefaultPosition, wxDefaultSize, - wxTE_MULTILINE | - wxTE_RICH | - wxTE_READONLY); - - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(text, 1, wxEXPAND); - SetSizer(sizer); - textViewDialog.ShowModal(); -} - -/** - * Invoked when user selects the "Menu" item - */ -void WebViewPanel::on_tools_clicked(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - -#ifdef DEBUG_URL_PANEL - m_context_menu->Check(m_browser->IsContextMenuEnabled()); - m_dev_tools->Check(m_browser->IsAccessToDevToolsEnabled()); - - wxPoint position = ScreenToClient(wxGetMousePosition()); - PopupMenu(m_tools_menu, position.x, position.y); -#endif -} - -void WebViewPanel::run_script(const wxString& javascript) -{ - if (!m_browser || !m_shown) - return; - // 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"; - m_browser->RunScriptAsync(javascript); -} - - -void WebViewPanel::on_run_script_custom(wxCommandEvent& WXUNUSED(evt)) -{ - wxTextEntryDialog dialog - ( - this, - "Please enter JavaScript code to execute", - wxGetTextFromUserPromptStr, - m_javascript, - wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE - ); - if (dialog.ShowModal() != wxID_OK) - return; - - run_script(dialog.GetValue()); -} - -void WebViewPanel::on_add_user_script(wxCommandEvent& WXUNUSED(evt)) -{ - wxString userScript = "window.wx_test_var = 'wxWidgets webview sample';"; - wxTextEntryDialog dialog - ( - this, - "Enter the JavaScript code to run as the initialization script that runs before any script in the HTML document.", - wxGetTextFromUserPromptStr, - userScript, - wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE - ); - if (dialog.ShowModal() != wxID_OK) - return; - - const wxString& javascript = dialog.GetValue(); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; - if (!m_browser->AddUserScript(javascript)) - wxLogError("Could not add user script"); -} - -void WebViewPanel::on_set_custom_user_agent(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - wxString customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1"; - wxTextEntryDialog dialog - ( - this, - "Enter the custom user agent string you would like to use.", - wxGetTextFromUserPromptStr, - customUserAgent, - wxOK | wxCANCEL | wxCENTRE - ); - if (dialog.ShowModal() != wxID_OK) - return; - - if (!m_browser->SetUserAgent(customUserAgent)) - wxLogError("Could not set custom user agent"); -} - -void WebViewPanel::on_clear_selection(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - m_browser->ClearSelection(); -} - -void WebViewPanel::on_delete_selection(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - m_browser->DeleteSelection(); -} - -void WebViewPanel::on_select_all(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - - m_browser->SelectAll(); -} - -void WebViewPanel::On_enable_context_menu(wxCommandEvent& evt) -{ - if (!m_browser) - return; - - m_browser->EnableContextMenu(evt.IsChecked()); -} -void WebViewPanel::On_enable_dev_tools(wxCommandEvent& evt) -{ - if (!m_browser) - return; - - m_browser->EnableAccessToDevTools(evt.IsChecked()); -} - -/** - * Callback invoked when a loading error occurs - */ -void WebViewPanel::on_error(wxWebViewEvent& evt) -{ -#define WX_ERROR_CASE(type) \ -case type: \ - category = #type; \ - break; - - wxString category; - switch (evt.GetInt()) - { - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CONNECTION); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CERTIFICATE); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_AUTH); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_SECURITY); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_NOT_FOUND); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_REQUEST); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_USER_CANCELLED); - WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_OTHER); - } - - BOOST_LOG_TRIVIAL(error) << "WebViewPanel error: " << category; - load_error_page(); -#ifdef DEBUG_URL_PANEL - m_info->ShowMessage(wxString("An error occurred loading ") + evt.GetURL() + "\n" + - "'" + category + "'", wxICON_ERROR); -#endif -} - -void WebViewPanel::sys_color_changed() -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this); -#endif -} - -SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : - wxDialog(parent, wxID_ANY, "Source Code", - wxDefaultPosition, wxSize(700,500), - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) -{ - wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, source, - wxDefaultPosition, wxDefaultSize, - wxTE_MULTILINE | - wxTE_RICH | - wxTE_READONLY); - - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(text, 1, wxEXPAND); - SetSizer(sizer); -} - -ConnectRequestHandler::ConnectRequestHandler() -{ - m_actions["REQUEST_LOGIN"] = std::bind(&ConnectRequestHandler::on_connect_action_request_login, this, std::placeholders::_1); - m_actions["REQUEST_CONFIG"] = std::bind(&ConnectRequestHandler::on_connect_action_request_config, this, std::placeholders::_1); - m_actions["WEBAPP_READY"] = std::bind(&ConnectRequestHandler::on_connect_action_webapp_ready,this, std::placeholders::_1); - m_actions["SELECT_PRINTER"] = std::bind(&ConnectRequestHandler::on_connect_action_select_printer, this, std::placeholders::_1); - m_actions["PRINT"] = std::bind(&ConnectRequestHandler::on_connect_action_print, this, std::placeholders::_1); - m_actions["REQUEST_OPEN_IN_BROWSER"] = std::bind(&ConnectRequestHandler::on_connect_action_request_open_in_browser, this, std::placeholders::_1); - m_actions["ERROR"] = std::bind(&ConnectRequestHandler::on_connect_action_error, this, std::placeholders::_1); - m_actions["LOG"] = std::bind(&ConnectRequestHandler::on_connect_action_log, this, std::placeholders::_1); -} -ConnectRequestHandler::~ConnectRequestHandler() -{ -} -void ConnectRequestHandler::handle_message(const std::string& message) -{ - // read msg and choose action - /* - v0: - {"type":"request","detail":{"action":"requestAccessToken"}} - v1: - {"action":"REQUEST_ACCESS_TOKEN"} - */ - std::string action_string; - try { - std::stringstream ss(message); - pt::ptree ptree; - pt::read_json(ss, ptree); - // v1: - if (const auto action = ptree.get_optional("action"); action) { - action_string = *action; - } - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Could not parse _qidiConnect message. " << e.what(); - return; - } - - if (action_string.empty()) { - BOOST_LOG_TRIVIAL(error) << "Recieved invalid message from _qidiConnect (missing action). Message: " << message; - return; - } - assert(m_actions.find(action_string) != m_actions.end()); // this assert means there is a action that has no handling. - if (m_actions.find(action_string) != m_actions.end()) { - m_actions[action_string](message); - } -} - -void ConnectRequestHandler::on_connect_action_error(const std::string &message_data) -{ - BOOST_LOG_TRIVIAL(error) << "WebView runtime error: " << message_data; -} - -void ConnectRequestHandler::resend_config() -{ - on_connect_action_request_config({}); -} - -void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) -{ - BOOST_LOG_TRIVIAL(info) << "WebView log: " << message_data; -} - -void ConnectRequestHandler::on_connect_action_request_login(const std::string &message_data) -{} - - -void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) -{ - /* - accessToken?: string; - clientVersion?: string; - colorMode?: "LIGHT" | "DARK"; - language?: ConnectLanguage; - sessionId?: string; - */ - const std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); - //const std::string sesh = wxGetApp().plater()->get_user_account()->get_shared_session_key(); - const std::string dark_mode = wxGetApp().dark_mode() ? "DARK" : "LIGHT"; - wxString language = GUI::wxGetApp().current_language_code(); - language = language.SubString(0, 1); - const std::string init_options = GUI::format("{\"accessToken\": \"%4%\",\"clientVersion\": \"%1%\", \"colorMode\": \"%2%\", \"language\": \"%3%\"}", SLIC3R_VERSION, dark_mode, language, token ); - wxString script = GUI::format_wxstr("window._qidiConnect_v1.init(%1%)", init_options); - run_script_bridge(script); - -} -void ConnectRequestHandler::on_connect_action_request_open_in_browser(const std::string& message_data) -{ - try { - std::stringstream ss(message_data); - pt::ptree ptree; - pt::read_json(ss, ptree); - if (const auto url = ptree.get_optional("url"); url) { - wxGetApp().open_browser_with_warning_dialog(GUI::from_u8(*url)); - } - } catch (const std::exception &e) { - BOOST_LOG_TRIVIAL(error) << "Could not parse _qidiConnect message. " << e.what(); - return; - } -} - -ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) - : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().connect_url()), { "_qidiSlicer" }, "connect_loading") -{ - // m_browser->RegisterHandler(wxSharedPtr(new WebViewHandler("https"))); - - auto* plater = wxGetApp().plater(); - plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); - plater->Bind(EVT_UA_LOGGEDOUT, &ConnectWebViewPanel::on_user_logged_out, this); -} - -ConnectWebViewPanel::~ConnectWebViewPanel() -{ - m_browser->Unbind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); -} - -wxString ConnectWebViewPanel::get_login_script(bool refresh) -{ - Plater* plater = wxGetApp().plater(); - const std::string& access_token = plater->get_user_account()->get_access_token(); - assert(!access_token.empty()); - auto javascript = wxString::Format( - -#if AUTH_VIA_FETCH_OVERRIDE - refresh - ? - "window.__access_token = '%s';window.__access_token_version = (window.__access_token_version || 0) + 1;console.log('Updated Auth token', window.__access_token);" - : - /* - * Notes: - * - The fetch() function has two distinct prototypes (i.e. input args): - * 1. fetch(url: string, options: object | undefined) - * 2. fetch(req: Request, options: object | undefined) - * - For some reason I can't explain the headers can be extended only via Request object - * i.e. the fetch prototype (2). So we need to convert (1) call into (2) before - * - */ - R"( - if (window.__fetch === undefined) { - window.__fetch = fetch; - window.fetch = function(req, opts = {}) { - if (typeof req === 'string') { - req = new Request(req, opts); - opts = {}; - } - if (window.__access_token && (req.url[0] == '/' || req.url.indexOf('qidi3d.com') > 0)) { - req.headers.set('Authorization', 'Bearer ' + window.__access_token); - console.log('Header updated: ', req.headers.get('Authorization')); - console.log('AT Version: ', __access_token_version); - } - //console.log('Injected fetch used', req, opts); - return __fetch(req, opts); - }; - } - window.__access_token = '%s'; - window.__access_token_version = 0; - )", -#else - refresh - ? - R"( - if (location.protocol === 'https:') { - if (window._qidiSlicer_initLogin !== undefined) { - console.log('Init login'); - if (window._qidiSlicer !== undefined) - _qidiSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); - _QIDISlicer_initLogin('%s'); - } else { - console.log('Refreshing login skipped as no _qidiSlicer_login defined (yet?)'); - if (window._qidiSlicer === undefined) { - console.log('Message handler _qidiSlicer not defined yet'); - } else { - _qidiSlicer.postMessage({action: 'LOG', message: 'Refreshing login skipped as no _qidiSlicer_initLogin defined (yet?)'}); - } - } - } - )" - : - R"( - function _qidiSlicer_log(msg) { - console.log(msg); - if (window._qidiSlicer !== undefined) - _qidiSlicer.postMessage({action: 'LOG', message: msg}); - } - function _qidiSlicer_errorHandler(err) { - const msg = { - action: 'ERROR', - error: typeof(err) === 'string' ? err : JSON.stringify(err), - critical: false - }; - console.error('Login error occurred', msg); - window._qidiSlicer.postMessage(msg); - }; - - function _qidiSlicer_delay(ms) { - return new Promise((resolve, reject) => { - setTimeout(resolve, ms); - }); - } - - async function _qidiSlicer_initLogin(token) { - const parts = token.split('.'); - const claims = JSON.parse(atob(parts[1])); - const now = new Date().getTime() / 1000; - if (claims.exp <= now) { - _qidiSlicer_log('Skipping initLogin as token is expired'); - return; - } - - let retry = false; - let backoff = 1000; - const maxBackoff = 64000; - do { - - let error = false; - - try { - _qidiSlicer_log('Slicer Login request ' + token.substring(token.length - 8)); - let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); - let body = await resp.text(); - _qidiSlicer_log('Slicer Login resp ' + resp.status + ' (' + token.substring(token.length - 8) + ') body: ' + body); - if (resp.status >= 500 || resp.status == 408) { - retry = true; - } else { - retry = false; - if (resp.status >= 400) - _qidiSlicer_errorHandler({status: resp.status, body}); - } - } catch (e) { - _qidiSlicer_log('Slicer Login failed: ' + e.toString()); - console.error('Slicer Login failed', e.toString()); - retry = true; - } - - if (retry) { - await _qidiSlicer_delay(backoff + 1000 * Math.random()); - if (backoff < maxBackoff) { - backoff *= 2; - } - } - } while (retry); - } - - if (location.protocol === 'https:' && window._qidiSlicer) { - _qidiSlicer_log('Requesting login'); - _qidiSlicer.postMessage({action: 'REQUEST_LOGIN'}); - } - )", -#endif - access_token - ); - return javascript; -} - -wxString ConnectWebViewPanel::get_logout_script() -{ - return "sessionStorage.removeItem('_slicer_token');"; -} - -void ConnectWebViewPanel::on_page_will_load() -{ - auto javascript = get_login_script(false); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; - m_browser->AddUserScript(javascript); -} - -void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) -{ - e.Skip(); - auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); - assert(!access_token.empty()); - - wxString javascript = get_login_script(true); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; - m_browser->RunScriptAsync(javascript); - resend_config(); -} - -void ConnectWebViewPanel::on_user_logged_out(UserAccountSuccessEvent& e) -{ - e.Skip(); - // clear token from session storage - m_browser->RunScriptAsync(get_logout_script()); -} - -void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) -{ - BOOST_LOG_TRIVIAL(debug) << "received message from QIDI Connect FE: " << evt.GetString(); - handle_message(into_u8(evt.GetString())); -} -void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt) -{ - BOOST_LOG_TRIVIAL(debug) << "Navigation requested to: " << into_u8(evt.GetURL()); - if (evt.GetURL() == m_default_url) { - m_reached_default_url = true; - return; - } - if (evt.GetURL() == (GUI::format_wxstr("file:///%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string()))) { - return; - } - if (m_reached_default_url && !evt.GetURL().StartsWith(m_default_url)) { - BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; - evt.Veto(); - } -} - -void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) -{ - 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( -// this, -// GUI::format_wxstr(_L("WebKit Runtime Error encountered:\n\n%s"), message_data), -// "WebKit Runtime Error", -// wxOK -// ); -// dialog.ShowModal(); - -} - -void ConnectWebViewPanel::logout() -{ - wxString script = L"window._qidiConnect_v1.logout()"; - run_script(script); - - Plater* plater = wxGetApp().plater(); - auto javascript = wxString::Format( - R"( - console.log('Preparing logout'); - window.fetch('/slicer/logout', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) - .then(function (resp){ - console.log('Logout resp', resp); - resp.text().then(function (json) { console.log('Logout resp body', json) }); - }); - )", - plater->get_user_account()->get_access_token() - ); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; - m_browser->RunScript(javascript); - -} - -void ConnectWebViewPanel::sys_color_changed() -{ - resend_config(); -} - -void ConnectWebViewPanel::on_connect_action_request_login(const std::string &message_data) -{ - run_script_bridge(get_login_script(true)); -} - - -void ConnectWebViewPanel::on_connect_action_select_printer(const std::string& message_data) -{ - assert(!message_data.empty()); - wxGetApp().handle_connect_request_printer_select(message_data); -} -void ConnectWebViewPanel::on_connect_action_print(const std::string& message_data) -{ - // PRINT request is not defined for ConnectWebViewPanel - assert(true); -} - -PrinterWebViewPanel::PrinterWebViewPanel(wxWindow* parent, const wxString& default_url) - : WebViewPanel(parent, default_url, {}) -{ - if (!m_browser) - return; - - m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebViewPanel::on_loaded, this); -#ifndef NDEBUG - m_browser->EnableAccessToDevTools(); - m_browser->EnableContextMenu(); -#endif -} - -void PrinterWebViewPanel::on_loaded(wxWebViewEvent& evt) -{ - if (evt.GetURL().IsEmpty()) - return; - if (!m_api_key.empty()) { - send_api_key(); - } else if (!m_usr.empty() && !m_psk.empty()) { - send_credentials(); - } -} - -void PrinterWebViewPanel::send_api_key() -{ - if (!m_browser || m_api_key_sent) - return; - m_api_key_sent = true; - wxString key = from_u8(m_api_key); - wxString script = wxString::Format(R"( - // Check if window.fetch exists before overriding - if (window.originalFetch === undefined) { - console.log('Patching fetch with API key'); - window.originalFetch = window.fetch; - window.fetch = function(input, init = {}) { - init.headers = init.headers || {}; - init.headers['X-Api-Key'] = sessionStorage.getItem('apiKey'); - console.log('Patched fetch', input, init); - return window.originalFetch(input, init); - }; - } - sessionStorage.setItem('authType', 'ApiKey'); - sessionStorage.setItem('apiKey', '%s'); -)", - key); - - m_browser->RemoveAllUserScripts(); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << script << "\n"; - m_browser->AddUserScript(script); - m_browser->Reload(); - remove_webview_credentials(m_browser); -} - -void PrinterWebViewPanel::send_credentials() -{ - if (!m_browser || m_api_key_sent) - return; - m_browser->RemoveAllUserScripts(); - m_browser->AddUserScript("sessionStorage.removeItem('authType'); sessionStorage.removeItem('apiKey'); console.log('Session Storage cleared');"); - m_browser->Reload(); - m_api_key_sent = true; - setup_webview_with_credentials(m_browser, m_usr, m_psk); -} - -void PrinterWebViewPanel::sys_color_changed() -{ -} - -WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url, const wxString& dialog_name, const wxSize& size, const std::vector& message_handler_names, const std::string& loading_html/* = "loading"*/) +WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url, const wxString& dialog_name, const wxSize& size, const std::vector& message_handler_names, const std::string& loading_html/* = "other_loading"*/) : DPIDialog(parent, wxID_ANY, dialog_name, wxDefaultPosition, size, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , m_loading_html(loading_html) , m_script_message_hadler_names (message_handler_names) @@ -968,16 +73,18 @@ WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url, const wxStri SetSizerAndFit(topsizer); // Create the webview - m_browser = WebView::CreateWebView(this, GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_loading_html), m_script_message_hadler_names); - if (Utils::ServiceConfig::instance().webdev_enabled()) { - m_browser->EnableContextMenu(); - m_browser->EnableAccessToDevTools(); - } + m_browser = WebView::webview_new(); if (!m_browser) { wxStaticText* text = new wxStaticText(this, wxID_ANY, _L("Failed to load a web browser.")); topsizer->Add(text, 0, wxALIGN_LEFT | wxBOTTOM, 10); return; } + WebView::webview_create(m_browser, this, url, m_script_message_hadler_names); + + if (Utils::ServiceConfig::instance().webdev_enabled()) { + m_browser->EnableContextMenu(); + m_browser->EnableAccessToDevTools(); + } topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); @@ -1030,7 +137,6 @@ WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url, const wxStri Bind(wxEVT_CLOSE_WINDOW, ([this](wxCloseEvent& evt) { EndModal(wxID_CANCEL); })); - m_browser->LoadURL(url); #ifdef DEBUG_URL_PANEL m_url->SetLabelText(url); #endif @@ -1039,17 +145,28 @@ WebViewDialog::~WebViewDialog() { } +constexpr bool is_linux = +#if defined(__linux__) +true; +#else +false; +#endif + void WebViewDialog::on_idle(wxIdleEvent& WXUNUSED(evt)) { if (!m_browser) return; if (m_browser->IsBusy()) { - wxSetCursor(wxCURSOR_ARROWWAIT); - } else { - wxSetCursor(wxNullCursor); + 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/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string())); + m_browser->LoadURL(GUI::format_wxstr("file://%1%/web/error_no_reload.html", boost::filesystem::path(resources_dir()).generic_string())); } } #ifdef DEBUG_URL_PANEL @@ -1110,13 +227,20 @@ void WebViewDialog::on_reload_button(wxCommandEvent& WXUNUSED(evt)) m_browser->Reload(); } - void WebViewDialog::on_navigation_request(wxWebViewEvent &evt) { + BOOST_LOG_TRIVIAL(debug) << "WebViewDialog::on_navigation_request " << evt.GetURL(); } +void WebViewDialog::on_loaded(wxWebViewEvent &evt) +{ + BOOST_LOG_TRIVIAL(debug) << "WebViewDialog::on_loaded " << evt.GetURL(); +} + + void WebViewDialog::on_script_message(wxWebViewEvent& evt) { + BOOST_LOG_TRIVIAL(error) << "Unhandled script message: " << evt.GetString(); } /** @@ -1331,13 +455,27 @@ PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string : WebViewDialog(parent , GUI::from_u8(Utils::ServiceConfig::instance().connect_select_printer_url()) , _L("Choose a printer") - , wxSize(std::max(parent->GetClientSize().x / 2, 100 * wxGetApp().em_unit()), std::max(parent->GetClientSize().y / 2, 50 * wxGetApp().em_unit())) + , wxSize(parent->GetClientSize().x / 4 * 3, parent->GetClientSize().y/ 4 * 3) ,{"_qidiSlicer"} , "connect_loading") , m_ret_val(ret_val) { + + wxDisplay display(wxDisplay::GetFromWindow(this)); + wxRect geometry = display.GetGeometry(); + SetMinSize(wxSize(geometry.GetWidth() / 2, geometry.GetHeight() / 2)); Centre(); } + +void PrinterPickWebViewDialog::on_dpi_changed(const wxRect &suggested_rect) +{ + wxDisplay display(wxDisplay::GetFromWindow(this)); + wxRect geometry = display.GetGeometry(); + SetMinSize(wxSize(geometry.GetWidth() / 2, geometry.GetHeight() / 2)); + Fit(); + Refresh(); +} + void PrinterPickWebViewDialog::on_show(wxShowEvent& evt) { /* @@ -1357,7 +495,7 @@ void PrinterPickWebViewDialog::on_script_message(wxWebViewEvent& evt) void PrinterPickWebViewDialog::on_connect_action_select_printer(const std::string& message_data) { // SELECT_PRINTER request is not defined for PrinterPickWebViewDialog - assert(true); + assert(false); } void PrinterPickWebViewDialog::on_connect_action_print(const std::string& message_data) { @@ -1436,19 +574,8 @@ void PrinterPickWebViewDialog::request_compatible_printers_FFF() { filament_abrasive_serialized += "]"; std::string printer_model_serialized = full_config.option("printer_model")->serialize(); - std::string vendor_repo_prefix; - if (selected_printer.vendor) { - vendor_repo_prefix = selected_printer.vendor->repo_prefix; - } else if (std::string inherits = selected_printer.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model_serialized.find(vendor_repo_prefix) == 0) { - printer_model_serialized = printer_model_serialized.substr(vendor_repo_prefix.size()); - boost::trim_left(printer_model_serialized); - } + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(selected_printer); + printer_model_serialized = selected_printer.trim_vendor_repo_prefix(printer_model_serialized, printer_with_vendor.vendor); 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(); @@ -1463,9 +590,9 @@ void PrinterPickWebViewDialog::request_compatible_printers_FFF() { "\"filament_abrasive\": %6%," "\"high_flow\": %7%" "}" - , filament_type_serialized, nozzle_diameter_serialized, printer_model_serialized, uuid, filename, nozzle_high_flow_serialized, filament_abrasive_serialized); + , filament_type_serialized, nozzle_diameter_serialized, printer_model_serialized, uuid, filename, filament_abrasive_serialized, nozzle_high_flow_serialized); - wxString script = GUI::format_wxstr("window._qidiConnect_v1.requestCompatiblePrinter(%1%)", request); + wxString script = GUI::format_wxstr("window._qidiConnect_v2.requestCompatiblePrinter(%1%)", request); run_script(script); } void PrinterPickWebViewDialog::request_compatible_printers_SLA() @@ -1474,18 +601,9 @@ void PrinterPickWebViewDialog::request_compatible_printers_SLA() std::string printer_model_serialized = selected_printer.config.option("printer_model")->serialize(); std::string vendor_repo_prefix; - if (selected_printer.vendor) { - vendor_repo_prefix = selected_printer.vendor->repo_prefix; - } else if (std::string inherits = selected_printer.inherits(); !inherits.empty()) { - const Preset *parent = wxGetApp().preset_bundle->printers.find_preset(inherits); - if (parent && parent->vendor) { - vendor_repo_prefix = parent->vendor->repo_prefix; - } - } - if (printer_model_serialized.find(vendor_repo_prefix) == 0) { - printer_model_serialized = printer_model_serialized.substr(vendor_repo_prefix.size()); - boost::trim_left(printer_model_serialized); - } + const PresetWithVendorProfile& printer_with_vendor = wxGetApp().preset_bundle->printers.get_preset_with_vendor_profile(selected_printer); + printer_model_serialized = selected_printer.trim_vendor_repo_prefix(printer_model_serialized, printer_with_vendor.vendor); + const Preset& selected_material = wxGetApp().preset_bundle->sla_materials.get_selected_preset(); 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); @@ -1498,21 +616,77 @@ void PrinterPickWebViewDialog::request_compatible_printers_SLA() "\"filename\": \"%4%\" " "}", material_type_serialized, printer_model_serialized, uuid, filename); - wxString script = GUI::format_wxstr("window._qidiConnect_v1.requestCompatiblePrinter(%1%)", request); + wxString script = GUI::format_wxstr("window._qidiConnect_v2.requestCompatiblePrinter(%1%)", request); run_script(script); } -void PrinterPickWebViewDialog::on_dpi_changed(const wxRect &suggested_rect) + + +void PrinterPickWebViewDialog::on_reload_event(const std::string& message_data) { - wxWindow *parent = GetParent(); - const wxSize &size = wxSize( - std::max(parent->GetClientSize().x / 2, 100 * wxGetApp().em_unit()), - std::max(parent->GetClientSize().y / 2, 50 * wxGetApp().em_unit()) - ); - SetMinSize(size); + if (!m_browser) { + return; + } + m_browser->LoadURL(m_default_url); +} + + +PrintablesConnectUploadDialog::PrintablesConnectUploadDialog(wxWindow* parent, const std::string url) + : WebViewDialog(parent + , GUI::from_u8(url) + , _L("Choose a printer") + , wxSize(parent->GetClientSize().x / 4 * 3, parent->GetClientSize().y/ 4 * 3) + ,{"_qidiSlicer"} + , "connect_loading") +{ + wxDisplay display(wxDisplay::GetFromWindow(this)); + wxRect geometry = display.GetGeometry(); + SetMinSize(wxSize(geometry.GetWidth() / 2, geometry.GetHeight() / 2)); + Centre(); +} + +void PrintablesConnectUploadDialog::on_dpi_changed(const wxRect &suggested_rect) +{ + wxDisplay display(wxDisplay::GetFromWindow(this)); + wxRect geometry = display.GetGeometry(); + SetMinSize(wxSize(geometry.GetWidth() / 2, geometry.GetHeight() / 2)); Fit(); Refresh(); } +void PrintablesConnectUploadDialog::on_script_message(wxWebViewEvent& evt) +{ + handle_message(into_u8(evt.GetString())); +} + +void PrintablesConnectUploadDialog::on_connect_action_select_printer(const std::string& message_data) +{ + // SELECT_PRINTER request is not defined for PrintablesConnectUploadDialog + assert(false); +} +void PrintablesConnectUploadDialog::on_connect_action_print(const std::string& message_data) +{ + assert(false); +} + +void PrintablesConnectUploadDialog::on_connect_action_webapp_ready(const std::string& message_data) +{ + // WEBAPP_READY request is not defined for PrintablesConnectUploadDialog + assert(false); +} + +void PrintablesConnectUploadDialog::on_reload_event(const std::string& message_data) +{ + if (!m_browser) { + return; + } + m_browser->LoadURL(m_default_url); +} + +void PrintablesConnectUploadDialog::on_connect_action_close_dialog(const std::string& message_data) +{ + this->EndModal(wxID_OK); +} + LoginWebViewDialog::LoginWebViewDialog(wxWindow *parent, std::string &ret_val, const wxString& url, wxEvtHandler* evt_handler) : WebViewDialog(parent, url, _L("Log in dialog"), wxSize(50 * wxGetApp().em_unit(), 80 * wxGetApp().em_unit()), {}) , m_ret_val(ret_val) @@ -1524,16 +698,16 @@ void LoginWebViewDialog::on_navigation_request(wxWebViewEvent &evt) { wxString url = evt.GetURL(); if (url.starts_with(L"qidislicer")) { - delete_cookies(m_browser, "https://account.qidi3d.com"); + 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"); evt.Veto(); m_ret_val = into_u8(url); EndModal(wxID_OK); - } else if (url.Find(L"accounts.google.com") != wxString::npos - || url.Find(L"appleid.apple.com") != wxString::npos - || url.Find(L"facebook.com") != wxString::npos) { + } 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) { auto& sc = Utils::ServiceConfig::instance(); if (!m_evt_sent && !url.starts_with(GUI::from_u8(sc.account_url()))) { wxCommandEvent* evt = new wxCommandEvent(EVT_OPEN_EXTERNAL_LOGIN); diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index 7f78ef3..e7d4c52 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -1,14 +1,13 @@ #ifndef slic3r_WebViewDialog_hpp_ #define slic3r_WebViewDialog_hpp_ -//#define DEBUG_URL_PANEL - #include #include #include #include "GUI_Utils.hpp" #include "UserAccountSession.hpp" +#include "ConnectRequestHandler.hpp" #ifdef DEBUG_URL_PANEL #include @@ -22,93 +21,10 @@ wxDECLARE_EVENT(EVT_OPEN_EXTERNAL_LOGIN, wxCommandEvent); namespace Slic3r { namespace GUI { -class WebViewPanel : public wxPanel -{ -public: - WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html = "loading"); - virtual ~WebViewPanel(); - - void load_url(const wxString& url); - void load_default_url_delayed(); - void load_error_page(); - - void on_show(wxShowEvent& evt); - virtual void on_script_message(wxWebViewEvent& evt); - - void on_idle(wxIdleEvent& evt); - void on_url(wxCommandEvent& evt); - void on_back_button(wxCommandEvent& evt); - void on_forward_button(wxCommandEvent& evt); - void on_stop_button(wxCommandEvent& evt); - void on_reload_button(wxCommandEvent& evt); - - void on_view_source_request(wxCommandEvent& evt); - void on_view_text_request(wxCommandEvent& evt); - void on_tools_clicked(wxCommandEvent& evt); - void on_error(wxWebViewEvent& evt); - - void run_script(const wxString& javascript); - void on_run_script_custom(wxCommandEvent& evt); - void on_add_user_script(wxCommandEvent& evt); - void on_set_custom_user_agent(wxCommandEvent& evt); - void on_clear_selection(wxCommandEvent& evt); - void on_delete_selection(wxCommandEvent& evt); - void on_select_all(wxCommandEvent& evt); - void On_enable_context_menu(wxCommandEvent& evt); - void On_enable_dev_tools(wxCommandEvent& evt); - virtual void on_navigation_request(wxWebViewEvent &evt); - - wxString get_default_url() const { return m_default_url; } - void set_default_url(const wxString& url) { m_default_url = url; } - - virtual void sys_color_changed(); -protected: - - virtual void on_page_will_load(); - -protected: - - wxWebView* m_browser { nullptr }; - bool m_load_default_url { false }; -#ifdef DEBUG_URL_PANEL - - wxBoxSizer *bSizer_toolbar; - wxButton * m_button_back; - wxButton * m_button_forward; - wxButton * m_button_stop; - wxButton * m_button_reload; - wxTextCtrl *m_url; - wxButton * m_button_tools; - - wxMenu* m_tools_menu; - wxMenuItem* m_script_custom; - - wxInfoBar *m_info; - wxStaticText* m_info_text; - - wxMenuItem* m_context_menu; - wxMenuItem* m_dev_tools; -#endif - - // Last executed JavaScript snippet, for convenience. - wxString m_javascript; - wxString m_response_js; - wxString m_default_url; - - std::string m_loading_html; - //DECLARE_EVENT_TABLE() - - bool m_load_error_page { false }; - bool m_shown { false }; - - std::vector m_script_message_hadler_names; -}; - - class WebViewDialog : public DPIDialog { public: - WebViewDialog(wxWindow* parent, const wxString& url, const wxString& dialog_name, const wxSize& size, const std::vector& message_handler_names, const std::string& loading_html = "loading"); + WebViewDialog(wxWindow* parent, const wxString& url, const wxString& dialog_name, const wxSize& size, const std::vector& message_handler_names, const std::string& loading_html = "other_loading"); virtual ~WebViewDialog(); virtual void on_show(wxShowEvent& evt) {}; @@ -136,7 +52,7 @@ public: void On_enable_dev_tools(wxCommandEvent& evt); virtual void on_navigation_request(wxWebViewEvent &evt); - virtual void on_loaded(wxWebViewEvent &evt) {} + virtual void on_loaded(wxWebViewEvent &evt); void run_script(const wxString& javascript); @@ -174,87 +90,6 @@ protected: std::vector m_script_message_hadler_names; }; -class ConnectRequestHandler -{ -public: - ConnectRequestHandler(); - ~ConnectRequestHandler(); - - void handle_message(const std::string& message); - void resend_config(); -protected: - // action callbacs stored in m_actions - virtual void on_connect_action_log(const std::string& message_data); - virtual void on_connect_action_error(const std::string& message_data); - virtual void on_connect_action_request_login(const std::string& message_data); - virtual void on_connect_action_request_config(const std::string& message_data); - virtual void on_connect_action_request_open_in_browser(const std::string& message_data); - virtual void on_connect_action_select_printer(const std::string& message_data) = 0; - virtual void on_connect_action_print(const std::string& message_data) = 0; - virtual void on_connect_action_webapp_ready(const std::string& message_data) = 0; - virtual void run_script_bridge(const wxString &script) = 0; - - std::map> m_actions; -}; - -class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler -{ -public: - ConnectWebViewPanel(wxWindow* parent); - ~ConnectWebViewPanel() override; - void on_script_message(wxWebViewEvent& evt) override; - void logout(); - void sys_color_changed() override; - void on_navigation_request(wxWebViewEvent &evt) override; -protected: - void on_connect_action_request_login(const std::string &message_data) override; - void on_connect_action_select_printer(const std::string& message_data) override; - void on_connect_action_print(const std::string& message_data) override; - void on_connect_action_webapp_ready(const std::string& message_data) override {} - void run_script_bridge(const wxString& script) override {run_script(script); } - void on_page_will_load() override; - void on_connect_action_error(const std::string &message_data) override; -private: - static wxString get_login_script(bool refresh); - static wxString get_logout_script(); - void on_user_token(UserAccountSuccessEvent& e); - void on_user_logged_out(UserAccountSuccessEvent& e); - bool m_reached_default_url {false}; -}; - -class PrinterWebViewPanel : public WebViewPanel -{ -public: - PrinterWebViewPanel(wxWindow* parent, const wxString& default_url); - - void on_loaded(wxWebViewEvent& evt); - - void send_api_key(); - void send_credentials(); - void set_api_key(const std::string &key) - { - if (m_api_key != key) { - clear(); - m_api_key = key; - } - } - void set_credentials(const std::string &usr, const std::string &psk) - { - if (m_usr != usr || m_psk != psk) { - clear(); - m_usr = usr; - m_psk = psk; - } - } - void clear() { m_api_key.clear(); m_usr.clear(); m_psk.clear(); m_api_key_sent = false; } - void sys_color_changed() override; -private: - std::string m_api_key; - std::string m_usr; - std::string m_psk; - bool m_api_key_sent {false}; -}; - class PrinterPickWebViewDialog : public WebViewDialog, public ConnectRequestHandler { public: @@ -269,15 +104,26 @@ protected: void request_compatible_printers_SLA(); void run_script_bridge(const wxString& script) override { run_script(script); } void on_dpi_changed(const wxRect &suggested_rect) override; - + void on_reload_event(const std::string& message_data) override; + void on_connect_action_close_dialog(const std::string& message_data) override {assert(false);} private: std::string& m_ret_val; }; -class SourceViewDialog : public wxDialog +class PrintablesConnectUploadDialog : public WebViewDialog, public ConnectRequestHandler { public: - SourceViewDialog(wxWindow* parent, wxString source); + PrintablesConnectUploadDialog(wxWindow* parent, const std::string url); + void on_script_message(wxWebViewEvent& evt) override; +protected: + void on_dpi_changed(const wxRect &suggested_rect) override; + + void on_connect_action_select_printer(const std::string& message_data) override; + void on_connect_action_print(const std::string& message_data) override; + void on_connect_action_webapp_ready(const std::string& message_data) override; + void on_reload_event(const std::string& message_data) override; + void run_script_bridge(const wxString &script) override { run_script(script); } + void on_connect_action_close_dialog(const std::string& message_data) override; }; class LoginWebViewDialog : public WebViewDialog @@ -295,4 +141,4 @@ private: } // GUI } // Slic3r -#endif /* slic3r_Tab_hpp_ */ +#endif /* slic3r_WebViewDialog_hpp_ */ diff --git a/src/slic3r/GUI/WebViewPanel.cpp b/src/slic3r/GUI/WebViewPanel.cpp new file mode 100644 index 0000000..53348df --- /dev/null +++ b/src/slic3r/GUI/WebViewPanel.cpp @@ -0,0 +1,1765 @@ +#include "WebViewPanel.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/UserAccount.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/WebView.hpp" +#include "slic3r/GUI/WebViewPlatformUtils.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/Field.hpp" +#include "slic3r/Utils/Http.hpp" +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/Config.hpp" + +#include // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include + +#include + +// if set to 1 the fetch() JS function gets override to include JWT in authorization header +// if set to 0, the /slicer/login is invoked from WebKit (passing JWT token only to this request) +// to set authorization cookie for all WebKit requests to Connect +#define AUTH_VIA_FETCH_OVERRIDE 0 + +wxDEFINE_EVENT(EVT_PRINTABLES_CONNECT_PRINT, wxCommandEvent); + +namespace pt = boost::property_tree; + +namespace Slic3r::GUI { + +WebViewPanel::~WebViewPanel() +{ + SetEvtHandlerEnabled(false); +#ifdef DEBUG_URL_PANEL + delete m_tools_menu; +#endif +} + +void WebViewPanel::destroy_browser() +{ + if (!m_browser || m_do_late_webview_create) { + return; + } + topsizer->Detach(m_browser); + m_browser->Destroy(); + m_browser = nullptr; +} + + +void WebViewPanel::load_url(const wxString& url) +{ + if (!m_browser) + return; + + this->on_page_will_load(); + + this->Show(); + this->Raise(); +#ifdef DEBUG_URL_PANEL + m_url->SetLabelText(url); +#endif + wxString correct_url = url.empty() ? wxString("") : wxURI(url).BuildURI(); + m_browser->LoadURL(correct_url); + m_browser->SetFocus(); +} + + +WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html, const std::string& error_html, bool do_create) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_default_url (default_url) + , m_loading_html(loading_html) + , m_error_html(error_html) + , m_script_message_hadler_names(message_handler_names) +{ + topsizer = new wxBoxSizer(wxVERTICAL); + m_sizer_top = new wxBoxSizer(wxHORIZONTAL); + topsizer->Add(m_sizer_top, 0, wxEXPAND, 0); + +#ifdef DEBUG_URL_PANEL + // Create the button + bSizer_toolbar = new wxBoxSizer(wxHORIZONTAL); + + m_button_back = new wxButton(this, wxID_ANY, wxT("Back"), wxDefaultPosition, wxDefaultSize, 0); + //m_button_back->Enable(false); + bSizer_toolbar->Add(m_button_back, 0, wxALL, 5); + + m_button_forward = new wxButton(this, wxID_ANY, wxT("Forward"), wxDefaultPosition, wxDefaultSize, 0); + //m_button_forward->Enable(false); + bSizer_toolbar->Add(m_button_forward, 0, wxALL, 5); + + m_button_stop = new wxButton(this, wxID_ANY, wxT("Stop"), wxDefaultPosition, wxDefaultSize, 0); + + bSizer_toolbar->Add(m_button_stop, 0, wxALL, 5); + + m_button_reload = new wxButton(this, wxID_ANY, wxT("Reload"), wxDefaultPosition, wxDefaultSize, 0); + bSizer_toolbar->Add(m_button_reload, 0, wxALL, 5); + + m_url = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + bSizer_toolbar->Add(m_url, 1, wxALL | wxEXPAND, 5); + + m_button_tools = new wxButton(this, wxID_ANY, wxT("Tools"), wxDefaultPosition, wxDefaultSize, 0); + bSizer_toolbar->Add(m_button_tools, 0, wxALL, 5); + + // Create panel for find toolbar. + wxPanel* panel = new wxPanel(this); + topsizer->Add(bSizer_toolbar, 0, wxEXPAND, 0); + topsizer->Add(panel, wxSizerFlags().Expand()); + + // Create sizer for panel. + wxBoxSizer* panel_sizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(panel_sizer); + + // Create the info panel + m_info = new wxInfoBar(this); + topsizer->Add(m_info, wxSizerFlags().Expand()); +#endif + + SetSizer(topsizer); + + Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); + Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); + +#ifdef DEBUG_URL_PANEL + // Create the Tools menu + m_tools_menu = new wxMenu(); + wxMenuItem* viewSource = m_tools_menu->Append(wxID_ANY, "View Source"); + wxMenuItem* viewText = m_tools_menu->Append(wxID_ANY, "View Text"); + m_tools_menu->AppendSeparator(); + + wxMenu* script_menu = new wxMenu; + + m_script_custom = script_menu->Append(wxID_ANY, "Custom script"); + m_tools_menu->AppendSubMenu(script_menu, "Run Script"); + wxMenuItem* addUserScript = m_tools_menu->Append(wxID_ANY, "Add user script"); + wxMenuItem* setCustomUserAgent = m_tools_menu->Append(wxID_ANY, "Set custom user agent"); + + m_context_menu = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Context Menu"); + m_dev_tools = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Dev Tools"); + + // Connect the button events + Bind(wxEVT_BUTTON, &WebViewPanel::on_back_button, this, m_button_back->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_forward_button, this, m_button_forward->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_stop_button, this, m_button_stop->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_reload_button, this, m_button_reload->GetId()); + Bind(wxEVT_BUTTON, &WebViewPanel::on_tools_clicked, this, m_button_tools->GetId()); + Bind(wxEVT_TEXT_ENTER, &WebViewPanel::on_url, this, m_url->GetId()); + + // Connect the menu events + Bind(wxEVT_MENU, &WebViewPanel::on_view_source_request, this, viewSource->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::on_view_text_request, this, viewText->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::On_enable_context_menu, this, m_context_menu->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::On_enable_dev_tools, this, m_dev_tools->GetId()); + + Bind(wxEVT_MENU, &WebViewPanel::on_run_script_custom, this, m_script_custom->GetId()); + Bind(wxEVT_MENU, &WebViewPanel::on_add_user_script, this, addUserScript->GetId()); +#endif + + // Create the webview + if (!do_create) { + m_do_late_webview_create = true; + return; + } + m_do_late_webview_create = false; + late_create(); +} + +void WebViewPanel::late_create() +{ + m_do_late_webview_create = false; + m_browser = WebView::webview_new(); + + if (!m_browser) { + wxStaticText* text = new wxStaticText(this, wxID_ANY, _L("Failed to load a web browser.")); + topsizer->Add(text, 0, wxALIGN_LEFT | wxBOTTOM, 10); + return; + } + WebView::webview_create(m_browser,this, GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_loading_html), m_script_message_hadler_names); + + if (Utils::ServiceConfig::instance().webdev_enabled()) { + m_browser->EnableContextMenu(); + m_browser->EnableAccessToDevTools(); + } + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + + // Connect the webview events + Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::on_error, this, m_browser->GetId()); + Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::on_script_message, this, m_browser->GetId()); + Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::on_navigation_request, this, m_browser->GetId()); + Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::on_loaded, this, m_browser->GetId()); + Layout(); +} + +void WebViewPanel::load_default_url_delayed() +{ + assert(!m_default_url.empty()); + m_load_default_url = true; +} + +void WebViewPanel::load_error_page() +{ + if (!m_browser || m_do_late_webview_create) { + return; + } + + m_browser->Stop(); + m_load_error_page = true; +} + +void WebViewPanel::on_show(wxShowEvent& evt) +{ + m_shown = evt.IsShown(); + if (!m_shown) { + wxSetCursor(wxNullCursor); + return; + } + if (m_do_late_webview_create) { + m_do_late_webview_create = false; + late_create(); + return; + } + if (m_load_default_url) { + m_load_default_url = false; + load_default_url(); + return; + } + + after_on_show(evt); +} + +void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) +{ + if (!m_browser || m_do_late_webview_create) + return; + + // The busy cursor on webview is switched off on Linux. + // Because m_browser->IsBusy() is almost always true on Printables / Connect. +#ifndef __linux__ + if (m_shown) { + if (m_browser->IsBusy()) { + wxSetCursor(wxCURSOR_ARROWWAIT); + } else { + wxSetCursor(wxNullCursor); + } + } +#endif // !__linux__ + + if (m_shown && m_load_error_page && !m_browser->IsBusy()) { + m_load_error_page = false; + if (m_load_default_url_on_next_error) { + m_load_default_url_on_next_error = false; + load_default_url(); + } else { + load_url(GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_error_html)); + // This is a fix of broken message handling after error. + // F.e. if there is an error but we do AddUserScript & Reload, the handling will break. + // So we just reset the handler here. + if (!m_script_message_hadler_names.empty()) { + m_browser->RemoveScriptMessageHandler(Slic3r::GUI::from_u8(m_script_message_hadler_names.front())); + m_browser->AddScriptMessageHandler(Slic3r::GUI::from_u8(m_script_message_hadler_names.front())); + } + } + } + +#ifdef DEBUG_URL_PANEL + m_button_stop->Enable(m_browser->IsBusy()); +#endif +} + +void WebViewPanel::on_loaded(wxWebViewEvent& evt) +{ + if (evt.GetURL().IsEmpty()) + return; + + if (evt.GetURL().StartsWith(m_default_url)) { + define_css(); + } else { + m_styles_defined = false; + } + + m_load_default_url_on_next_error = false; + if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { + m_load_default_url = false; + load_default_url(); + } +} + +/** + * Callback invoked when user entered an URL and pressed enter + */ +void WebViewPanel::on_url(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; +#ifdef DEBUG_URL_PANEL + m_browser->LoadURL(m_url->GetValue()); + m_browser->SetFocus(); +#endif +} + +/** + * Callback invoked when user pressed the "back" button + */ +void WebViewPanel::on_back_button(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + if (!m_browser->CanGoBack()) + return; + m_browser->GoBack(); +} + +/** + * Callback invoked when user pressed the "forward" button + */ +void WebViewPanel::on_forward_button(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + if (!m_browser->CanGoForward()) + return; + m_browser->GoForward(); +} + +/** + * Callback invoked when user pressed the "stop" button + */ +void WebViewPanel::on_stop_button(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + m_browser->Stop(); +} + +/** + * Callback invoked when user pressed the "reload" button + */ +void WebViewPanel::on_reload_button(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + m_browser->Reload(); +} + +void WebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + BOOST_LOG_TRIVIAL(error) << "unhandled script message: " << evt.GetString(); +} + +void WebViewPanel::on_navigation_request(wxWebViewEvent &evt) +{ +} + +void WebViewPanel::on_page_will_load() +{ +} + +/** + * Invoked when user selects the "View Source" menu item + */ +void WebViewPanel::on_view_source_request(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + SourceViewDialog dlg(this, m_browser->GetPageSource()); + dlg.ShowModal(); +} + +/** + * Invoked when user selects the "View Text" menu item + */ +void WebViewPanel::on_view_text_request(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + wxDialog textViewDialog(this, wxID_ANY, "Page Text", + wxDefaultPosition, wxSize(700, 500), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); + + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, m_browser->GetPageText(), + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | + wxTE_RICH | + wxTE_READONLY); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(text, 1, wxEXPAND); + SetSizer(sizer); + textViewDialog.ShowModal(); +} + +/** + * Invoked when user selects the "Menu" item + */ +void WebViewPanel::on_tools_clicked(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + +#ifdef DEBUG_URL_PANEL + m_context_menu->Check(m_browser->IsContextMenuEnabled()); + m_dev_tools->Check(m_browser->IsAccessToDevToolsEnabled()); + + wxPoint position = ScreenToClient(wxGetMousePosition()); + PopupMenu(m_tools_menu, position.x, position.y); +#endif +} + +void WebViewPanel::run_script(const wxString& javascript) +{ + if (!m_browser || !m_shown) + return; + // 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"; + m_browser->RunScriptAsync(javascript); +} + + +void WebViewPanel::on_run_script_custom(wxCommandEvent& WXUNUSED(evt)) +{ + wxTextEntryDialog dialog + ( + this, + "Please enter JavaScript code to execute", + wxGetTextFromUserPromptStr, + m_javascript, + wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + run_script(dialog.GetValue()); +} + +void WebViewPanel::on_add_user_script(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) { + return; + } + wxString userScript = "window.wx_test_var = 'wxWidgets webview sample';"; + wxTextEntryDialog dialog + ( + this, + "Enter the JavaScript code to run as the initialization script that runs before any script in the HTML document.", + wxGetTextFromUserPromptStr, + userScript, + wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + const wxString& javascript = dialog.GetValue(); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + if (!m_browser->AddUserScript(javascript)) + wxLogError("Could not add user script"); +} + +void WebViewPanel::on_set_custom_user_agent(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + wxString customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1"; + wxTextEntryDialog dialog + ( + this, + "Enter the custom user agent string you would like to use.", + wxGetTextFromUserPromptStr, + customUserAgent, + wxOK | wxCANCEL | wxCENTRE + ); + if (dialog.ShowModal() != wxID_OK) + return; + + if (!m_browser->SetUserAgent(customUserAgent)) + wxLogError("Could not set custom user agent"); +} + +void WebViewPanel::on_clear_selection(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + m_browser->ClearSelection(); +} + +void WebViewPanel::on_delete_selection(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + m_browser->DeleteSelection(); +} + +void WebViewPanel::on_select_all(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + + m_browser->SelectAll(); +} + +void WebViewPanel::On_enable_context_menu(wxCommandEvent& evt) +{ + if (!m_browser) + return; + + m_browser->EnableContextMenu(evt.IsChecked()); +} +void WebViewPanel::On_enable_dev_tools(wxCommandEvent& evt) +{ + if (!m_browser) + return; + + m_browser->EnableAccessToDevTools(evt.IsChecked()); +} + +/** + * Callback invoked when a loading error occurs + */ +void WebViewPanel::on_error(wxWebViewEvent& evt) +{ +#define WX_ERROR_CASE(type) \ +case type: \ + category = #type; \ + break; + + wxString category; + switch (evt.GetInt()) + { + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CONNECTION); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CERTIFICATE); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_AUTH); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_SECURITY); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_NOT_FOUND); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_REQUEST); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_USER_CANCELLED); + WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_OTHER); + } + + BOOST_LOG_TRIVIAL(error) << this <<" WebViewPanel error: " << category << " url: " << evt.GetURL(); + load_error_page(); +#ifdef DEBUG_URL_PANEL + m_info->ShowMessage(wxString("An error occurred loading ") + evt.GetURL() + "\n" + + "'" + category + "'", wxICON_ERROR); +#endif +} + +void WebViewPanel::do_reload() +{ + if (!m_browser) { + return; + } + // IsBusy on Linux very often returns true due to loading about:blank after loading requested url. +#ifndef __linux__ + if (m_browser->IsBusy()) { + return; + } +#endif + const wxString current_url = m_browser->GetCurrentURL(); + if (current_url.StartsWith(m_default_url)) { + m_browser->Reload(); + return; + } + load_default_url(); +} + +void WebViewPanel::load_default_url() +{ + if (!m_browser || m_do_late_webview_create) { + return; + } + m_styles_defined = false; + load_url(m_default_url); +} + +void WebViewPanel::sys_color_changed() +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this); +#endif +} + +void WebViewPanel::on_app_quit_event(const std::string& message_data) +{ + // MacOS only suplement for cmd+Q + wxGetApp().Exit(); +} + +void WebViewPanel::on_app_minimize_event(const std::string& message_data) +{ + // MacOS only suplement for cmd+M + wxGetApp().mainframe->Iconize(true); +} +void WebViewPanel::define_css() +{ + assert(false); +} + +ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) + : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().connect_url()), { "_qidiSlicer" }, "connect_loading", "connect_error", false) +{ + 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); + + 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); + m_actions["reloadHomePage"] = std::bind(&ConnectWebViewPanel::on_reload_event, this, std::placeholders::_1); + +} + +void ConnectWebViewPanel::late_create() +{ + WebViewPanel::late_create(); + if (!m_browser) { + return; + } + + // This code used to be inside plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this) + auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); + assert(!access_token.empty()); + + wxString javascript = get_login_script(true); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + m_browser->RunScriptAsync(javascript); + resend_config(); +} + +void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) +{ + e.Skip(); + if (!m_browser) { + return; + } + auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); + assert(!access_token.empty()); + + wxString javascript = get_login_script(true); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + m_browser->RunScriptAsync(javascript); + resend_config(); +} + +ConnectWebViewPanel::~ConnectWebViewPanel() +{ +} + +wxString ConnectWebViewPanel::get_login_script(bool refresh) +{ + Plater* plater = wxGetApp().plater(); + const std::string& access_token = plater->get_user_account()->get_access_token(); + assert(!access_token.empty()); + auto javascript = wxString::Format( + +#if AUTH_VIA_FETCH_OVERRIDE + refresh + ? + "window.__access_token = '%s';window.__access_token_version = (window.__access_token_version || 0) + 1;console.log('Updated Auth token', window.__access_token);" + : + /* + * Notes: + * - The fetch() function has two distinct prototypes (i.e. input args): + * 1. fetch(url: string, options: object | undefined) + * 2. fetch(req: Request, options: object | undefined) + * - For some reason I can't explain the headers can be extended only via Request object + * i.e. the fetch prototype (2). So we need to convert (1) call into (2) before + * + */ + R"( + if (window.__fetch === undefined) { + window.__fetch = fetch; + window.fetch = function(req, opts = {}) { + if (typeof req === 'string') { + req = new Request(req, opts); + opts = {}; + } + if (window.__access_token && (req.url[0] == '/' || req.url.indexOf('qidi3d.com') > 0)) { + req.headers.set('Authorization', 'Bearer ' + window.__access_token); + console.log('Header updated: ', req.headers.get('Authorization')); + console.log('AT Version: ', __access_token_version); + } + //console.log('Injected fetch used', req, opts); + return __fetch(req, opts); + }; + } + window.__access_token = '%s'; + window.__access_token_version = 0; + )", +#else + refresh + ? + R"( + if (location.protocol === 'https:') { + if (window._qidiSlicer_initLogin !== undefined) { + console.log('Init login'); + if (window._qidiSlicer !== undefined) + _qidiSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); + _qidiSlicer_initLogin('%s'); + } else { + console.log('Refreshing login skipped as no _qidiSlicer_login defined (yet?)'); + if (window._qidiSlicer === undefined) { + console.log('Message handler _qidiSlicer not defined yet'); + } else { + _qidiSlicer.postMessage({action: 'LOG', message: 'Refreshing login skipped as no _qidiSlicer_initLogin defined (yet?)'}); + } + } + } + )" + : + R"( + function _qidiSlicer_log(msg) { + console.log(msg); + if (window._qidiSlicer !== undefined) + _qidiSlicer.postMessage({action: 'LOG', message: msg}); + } + function _qidiSlicer_errorHandler(err) { + const msg = { + action: 'ERROR', + error: typeof(err) === 'string' ? err : JSON.stringify(err), + critical: false + }; + console.error('Login error occurred', msg); + window._qidiSlicer.postMessage(msg); + }; + + function _qidiSlicer_delay(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); + } + + async function _qidiSlicer_initLogin(token) { + const parts = token.split('.'); + const claims = JSON.parse(atob(parts[1])); + const now = new Date().getTime() / 1000; + if (claims.exp <= now) { + _qidiSlicer_log('Skipping initLogin as token is expired'); + return; + } + + let retry = false; + let backoff = 1000; + const maxBackoff = 64000 * 4; + const maxRetries = 16; + let numRetries = 0; + do { + + let error = false; + + try { + _qidiSlicer_log('Slicer Login request ' + token.substring(token.length - 8)); + let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); + let body = await resp.text(); + _qidiSlicer_log('Slicer Login resp ' + resp.status + ' (' + token.substring(token.length - 8) + ') body: ' + body); + if (resp.status >= 500 || resp.status == 408) { + numRetries++; + retry = maxRetries <= 0 || numRetries <= maxRetries; + } else { + retry = false; + if (resp.status >= 400) + _qidiSlicer_errorHandler({status: resp.status, body}); + } + } catch (e) { + _qidiSlicer_log('Slicer Login failed: ' + e.toString()); + console.error('Slicer Login failed', e.toString()); + // intentionally not taking care about max retry count, as this is not server error but likely being offline + retry = true; + } + + if (retry) { + await _qidiSlicer_delay(backoff + 1000 * Math.random()); + if (backoff < maxBackoff) { + backoff *= 2; + } + } + } while (retry); + } + + if (location.protocol === 'https:' && window._qidiSlicer) { + _qidiSlicer_log('Requesting login'); + _qidiSlicer.postMessage({action: 'REQUEST_LOGIN'}); + } + )", +#endif + access_token + ); + return javascript; +} + +wxString ConnectWebViewPanel::get_logout_script() +{ + return "sessionStorage.removeItem('_slicer_token');"; +} + +void ConnectWebViewPanel::on_page_will_load() +{ + if (!m_browser) { + return; + } + auto javascript = get_login_script(false); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + m_browser->AddUserScript(javascript); +} + +void ConnectWebViewPanel::on_user_logged_out(UserAccountSuccessEvent& e) +{ + e.Skip(); + if (!m_browser) + return; + // clear token from session storage + m_browser->RunScriptAsync(get_logout_script()); +} + +void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << "received message from QIDI Connect FE: " << evt.GetString(); + handle_message(into_u8(evt.GetString())); +} +void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt) +{ +#ifdef DEBUG_URL_PANEL + m_url->SetValue(evt.GetURL()); +#endif + BOOST_LOG_TRIVIAL(debug) << "Navigation requested to: " << into_u8(evt.GetURL()); + + // we need to do this to redefine css when reload is hit + if (evt.GetURL().StartsWith(m_default_url) && evt.GetURL() == m_browser->GetCurrentURL()) { + m_styles_defined = false; + } + + if (evt.GetURL() == m_default_url) { + m_reached_default_url = true; + return; + } + if (evt.GetURL() == (GUI::format_wxstr("file:///%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string()))) { + return; + } + if (m_reached_default_url && !evt.GetURL().StartsWith(m_default_url)) { + BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; + evt.Veto(); + } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { + // Do not allow back button to loading screen + evt.Veto(); + } +} + +void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) +{ + 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( +// this, +// GUI::format_wxstr(_L("WebKit Runtime Error encountered:\n\n%s"), message_data), +// "WebKit Runtime Error", +// wxOK +// ); +// dialog.ShowModal(); + +} + +void ConnectWebViewPanel::on_reload_event(const std::string& message_data) +{ + // Event from our error page button or keyboard shortcut + m_styles_defined = false; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { + do_reload(); + } else { + // On error page do load of default url. + load_default_url(); + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } +} + +void ConnectWebViewPanel::logout() +{ + if (!m_browser || m_do_late_webview_create) { + return; + } + wxString script = L"window._qidiConnect_v2.logout()"; + run_script(script); + + Plater* plater = wxGetApp().plater(); + auto javascript = wxString::Format( + R"( + console.log('Preparing logout'); + window.fetch('/slicer/logout', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) + .then(function (resp){ + console.log('Logout resp', resp); + resp.text().then(function (json) { console.log('Logout resp body', json) }); + }); + )", + plater->get_user_account()->get_access_token() + ); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + m_browser->RunScript(javascript); +} + +void ConnectWebViewPanel::sys_color_changed() +{ + resend_config(); +} + +void ConnectWebViewPanel::on_connect_action_request_login(const std::string &message_data) +{ + run_script_bridge(get_login_script(true)); +} + + +void ConnectWebViewPanel::on_connect_action_select_printer(const std::string& message_data) +{ + assert(!message_data.empty()); + wxGetApp().handle_connect_request_printer_select(message_data); +} +void ConnectWebViewPanel::on_connect_action_print(const std::string& message_data) +{ + // PRINT request is not defined for ConnectWebViewPanel + assert(false); +} + +void ConnectWebViewPanel::define_css() +{ + + if (m_styles_defined) { + return; + } + m_styles_defined = true; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; +#if defined(__APPLE__) + // WebView on Windows does read keyboard shortcuts + // Thus doing f.e. Reload twice would make the oparation to fail + std::string script = R"( + document.addEventListener('keydown', function (event) { + if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { + window.webkit.messageHandlers._qidiSlicer.postMessage(JSON.stringify({ action: 'reloadHomePage', fromKeyboard: 1})); + } + if (event.metaKey && event.key === 'q') { + window.webkit.messageHandlers._qidiSlicer.postMessage(JSON.stringify({ action: 'appQuit'})); + } + if (event.metaKey && event.key === 'm') { + window.webkit.messageHandlers._qidiSlicer.postMessage(JSON.stringify({ action: 'appMinimize'})); + } + }); + )"; + run_script(script); + +#endif // defined(__APPLE__) +} + +PrinterWebViewPanel::PrinterWebViewPanel(wxWindow* parent, const wxString& default_url) + : WebViewPanel(parent, default_url, {"ExternalApp"}, "other_loading", "other_error", false) +{ + m_events["reloadHomePage"] = std::bind(&PrinterWebViewPanel::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); +} + +void PrinterWebViewPanel::on_navigation_request(wxWebViewEvent &evt) +{ + const wxString url = evt.GetURL(); + if (url.StartsWith(m_default_url)) { + m_reached_default_url = true; + if (url == m_browser->GetCurrentURL()) { + // we need to do this to redefine css when reload is hit + m_styles_defined = false; + } + + if ( !m_api_key_sent) { + if (!m_usr.empty() && !m_psk.empty()) { + send_credentials(); + } + } + } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { + // Do not allow back button to loading screen + evt.Veto(); + } +} + +void PrinterWebViewPanel::on_loaded(wxWebViewEvent& evt) +{ + if (evt.GetURL().IsEmpty()) + return; + + if (evt.GetURL().StartsWith(m_default_url)) { + define_css(); + } else { + m_styles_defined = false; + } + + m_load_default_url_on_next_error = false; + if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { + m_load_default_url = false; + load_default_url(); + return; + } + if (!m_api_key.empty()) { + send_api_key(); + } +} +void PrinterWebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << "received message from Physical printer page: " << evt.GetString(); + handle_message(into_u8(evt.GetString())); +} + +void PrinterWebViewPanel::handle_message(const std::string& message) +{ + + std::string event_string; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto action = ptree.get_optional("event"); action) { + event_string = *action; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } + + if (event_string.empty()) { + BOOST_LOG_TRIVIAL(error) << "Received invalid message from printables (missing event). Message: " << message; + return; + } + assert(m_events.find(event_string) != m_events.end()); // this assert means there is an event that has no handling. + if (m_events.find(event_string) != m_events.end()) { + m_events[event_string](message); + } +} + +void PrinterWebViewPanel::send_api_key() +{ + if (!m_browser || m_api_key_sent) + return; + m_api_key_sent = true; + wxString key = from_u8(m_api_key); + wxString script = wxString::Format(R"( + // Check if window.fetch exists before overriding + if (window.originalFetch === undefined) { + console.log('Patching fetch with API key'); + window.originalFetch = window.fetch; + window.fetch = function(input, init = {}) { + init.headers = init.headers || {}; + init.headers['X-Api-Key'] = sessionStorage.getItem('apiKey'); + console.log('Patched fetch', input, init); + return window.originalFetch(input, init); + }; + } + sessionStorage.setItem('authType', 'ApiKey'); + sessionStorage.setItem('apiKey', '%s'); +)", + key); + m_browser->RemoveAllUserScripts(); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << script << "\n"; + m_browser->AddUserScript(script); + m_browser->Reload(); + remove_webview_credentials(m_browser); +} + +void PrinterWebViewPanel::send_credentials() +{ + if (!m_browser || m_api_key_sent) + return; + m_browser->RemoveAllUserScripts(); + m_browser->AddUserScript("sessionStorage.removeItem('authType'); sessionStorage.removeItem('apiKey'); console.log('Session Storage cleared');"); + // reload would be done only if called from on_loaded + //m_browser->Reload(); + m_api_key_sent = true; + setup_webview_with_credentials(m_browser, m_usr, m_psk); +} + +void PrinterWebViewPanel::define_css() +{ + + if (m_styles_defined) { + return; + } + m_styles_defined = true; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; +#if defined(__APPLE__) + // WebView on Windows does read keyboard shortcuts + // Thus doing f.e. Reload twice would make the oparation to fail + std::string script = R"( + document.addEventListener('keydown', function (event) { + if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { + window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'reloadHomePage', fromKeyboard: 1})); + } + if (event.metaKey && event.key === 'q') { + window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'appQuit'})); + } + if (event.metaKey && event.key === 'm') { + window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'appMinimize'})); + } + }); + )"; + run_script(script); + +#endif // defined(__APPLE__) +} + +void PrinterWebViewPanel::on_reload_event(const std::string& message_data) +{ + // Event from our error page button or keyboard shortcut + m_styles_defined = false; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { + do_reload(); + } else { + // On error page do load of default url. + load_default_url(); + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse message. " << e.what(); + return; + } +} + +PrintablesWebViewPanel::PrintablesWebViewPanel(wxWindow* parent) + : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().printables_url()), { "ExternalApp" }, "other_loading", "other_error", false) +{ + m_events["accessTokenExpired"] = std::bind(&PrintablesWebViewPanel::on_printables_event_access_token_expired, this, std::placeholders::_1); + m_events["printGcode"] = std::bind(&PrintablesWebViewPanel::on_printables_event_print_gcode, this, std::placeholders::_1); + m_events["downloadFile"] = std::bind(&PrintablesWebViewPanel::on_printables_event_download_file, this, std::placeholders::_1); + m_events["sliceFile"] = std::bind(&PrintablesWebViewPanel::on_printables_event_slice_file, this, std::placeholders::_1); + m_events["requiredLogin"] = std::bind(&PrintablesWebViewPanel::on_printables_event_required_login, this, std::placeholders::_1); + m_events["openExternalUrl"] = std::bind(&PrintablesWebViewPanel::on_printables_event_open_url, this, std::placeholders::_1); + 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); +} + +void PrintablesWebViewPanel::handle_message(const std::string& message) +{ + + std::string event_string; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto action = ptree.get_optional("event"); action) { + event_string = *action; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } + + if (event_string.empty()) { + BOOST_LOG_TRIVIAL(error) << "Received invalid message from printables (missing event). Message: " << message; + return; + } + assert(m_events.find(event_string) != m_events.end()); // this assert means there is an event that has no handling. + if (m_events.find(event_string) != m_events.end()) { + m_events[event_string](message); + } +} + +void PrintablesWebViewPanel::on_navigation_request(wxWebViewEvent &evt) +{ + const wxString url = evt.GetURL(); + if (url.StartsWith(m_default_url)) { + m_reached_default_url = true; + if (url == m_browser->GetCurrentURL()) { + // we need to do this to redefine css when reload is hit + m_styles_defined = false; + } + } else if (m_reached_default_url && url.StartsWith("http")) { + BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; + evt.Veto(); + } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { + // Do not allow back button to loading screen + evt.Veto(); + } +} + +wxString PrintablesWebViewPanel::get_default_url() const +{ + return GUI::from_u8(get_url_lang_theme(GUI::from_u8(Utils::ServiceConfig::instance().printables_url() + "/homepage"))); +} + +void PrintablesWebViewPanel::on_loaded(wxWebViewEvent& evt) +{ + if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { + m_load_default_url = false; + load_default_url(); + return; + } + if (evt.GetURL().StartsWith(m_default_url)) { + define_css(); + } else { + m_styles_defined = false; + } +#ifdef _WIN32 + // This is needed only once after add_request_authorization + if (m_remove_request_auth) { + m_remove_request_auth = false; + remove_request_authorization(m_browser); + } +#endif + m_load_default_url_on_next_error = false; +} + +std::string PrintablesWebViewPanel::get_url_lang_theme(const wxString& url) const +{ + // situations and reaction: + // 1) url is just a path (no query no fragment) -> query with lang and theme is added + // 2) url has query that contains lang and theme -> query and lang values are modified + // 3) url has query with just one of lang or theme -> query is modified and missing value is added + // 4) url has query of query and fragment without lang and theme -> query with lang and theme is added to the end of query + + std::string url_string = into_u8(url); + std::string theme = wxGetApp().dark_mode() ? "dark" : "light"; + wxString language = GUI::wxGetApp().current_language_code(); + if (language.size() > 2) + language = language.SubString(0, 1); + + // Replace lang and theme if already in url + bool lang_found = false; + std::regex lang_regex(R"((lang=)[^&#]*)"); + if (std::regex_search(url_string, lang_regex)) { + url_string = std::regex_replace(url_string, lang_regex, "$1" + into_u8(language)); + lang_found = true; + } + bool theme_found = false; + std::regex theme_regex(R"((theme=)[^&#]*)"); + if (std::regex_search(url_string, theme_regex)) { + url_string = std::regex_replace(url_string, theme_regex, "$1" + theme); + theme_found = true; + } + if (lang_found && theme_found) + return url_string; + + // missing params string + std::string new_params = lang_found ? GUI::format("theme=%1%", theme) + : theme_found ? GUI::format("lang=%1%", language) + : GUI::format("lang=%1%&theme=%2%", language, theme); + + // Regex to capture query and optional fragment + std::regex query_regex(R"((\?.*?)(#.*)?$)"); + + if (std::regex_search(url_string, query_regex)) { + // Append params before the fragment (if it exists) + return std::regex_replace(url_string, query_regex, "$1&" + new_params + "$2"); + } + std::regex fragment_regex(R"(#.*$)"); + if (std::regex_search(url_string, fragment_regex)) { + // Add params before the fragment + return std::regex_replace(url_string, fragment_regex, "?" + new_params + "$&"); + } + + return url_string + "?" + new_params; +} + +void PrintablesWebViewPanel::after_on_show(wxShowEvent& evt) +{ + // in case login changed, resend login / logout + // DK1: it seems to me, it is safer to do login / logout (where logout means requesting the page again) + // on every show of panel, + // than to keep information if we have printables page in same state as slicer in terms of login + // But im afraid it will be concidered not pretty... + const std::string access_token = wxGetApp().plater()->get_user_account()->get_access_token(); + if (access_token.empty()) { + logout(m_next_show_url); + } else { + login(access_token, m_next_show_url); + } + m_next_show_url.clear(); +} + +void PrintablesWebViewPanel::logout(const std::string& override_url/* = std::string()*/) +{ + if (!m_shown || !m_browser) { + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + hide_loading_overlay(); + m_styles_defined = false; + delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); + m_browser->RunScript("localStorage.clear();"); + + std::string next_url = override_url.empty() + ? get_url_lang_theme(m_browser->GetCurrentURL()) + : get_url_lang_theme(from_u8(override_url)); +#ifdef _WIN32 + load_url(GUI::from_u8(next_url)); +#else + // We cannot do simple reload here, it would keep the access token in the header + load_request(m_browser, next_url, std::string()); +#endif // + +} +void PrintablesWebViewPanel::login(const std::string& access_token, const std::string& override_url/* = std::string()*/) +{ + if (!m_shown) { + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + hide_loading_overlay(); + m_styles_defined = false; + // We cannot add token to header as when making the first request. + // In fact, we shall not do request here, only run scripts. + // postMessage accessTokenWillChange -> postMessage accessTokenChange -> window.location.reload(); + wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; + run_script(script); + + script = GUI::format_wxstr("window.postMessage(JSON.stringify({" + "event: 'accessTokenChange'," + "token: '%1%'" + "}));" + , access_token); + run_script(script); + + if ( override_url.empty()) { + run_script("window.location.reload();"); + } else { + load_url(GUI::from_u8(get_url_lang_theme(from_u8(override_url)))); + } +} + +void PrintablesWebViewPanel::load_default_url() +{ + if (!m_browser) { + return; + } + hide_loading_overlay(); + m_styles_defined = false; + std::string actual_default_url = get_url_lang_theme(from_u8(Utils::ServiceConfig::instance().printables_url() + "/homepage")); + const std::string access_token = wxGetApp().plater()->get_user_account()->get_access_token(); + // in case of opening printables logged out - delete cookies and localstorage to get rid of last login + if (access_token.empty()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " logout"; + delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); + m_browser->AddUserScript("localStorage.clear();"); + load_url(actual_default_url); + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " login"; + + // add token to first request +#ifdef _WIN32 + add_request_authorization(m_browser, m_default_url, access_token); + m_remove_request_auth = true; + load_url(GUI::from_u8(actual_default_url)); +#else + load_request(m_browser, actual_default_url, access_token); +#endif +} + +void PrintablesWebViewPanel::send_refreshed_token(const std::string& access_token) +{ + if (m_load_default_url) { + return; + } + hide_loading_overlay(); + wxString script = GUI::format_wxstr("window.postMessage(JSON.stringify({" + "event: 'accessTokenChange'," + "token: '%1%'" + "}));" + , access_token); + run_script(script); +} +void PrintablesWebViewPanel::send_will_refresh() +{ + if (m_load_default_url) { + return; + } + wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; + run_script(script); +} + +void PrintablesWebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + BOOST_LOG_TRIVIAL(debug) << "received message from Printables: " << evt.GetString(); + handle_message(into_u8(evt.GetString())); +} + +void PrintablesWebViewPanel::sys_color_changed() +{ + if (m_shown && m_browser) { + load_url(GUI::from_u8(get_url_lang_theme(m_browser->GetCurrentURL()))); + } + WebViewPanel::sys_color_changed(); +} + +void PrintablesWebViewPanel::on_printables_event_access_token_expired(const std::string& message_data) +{ + // { "event": "accessTokenExpired:) + // There seems to be a situation where we get accessTokenExpired when there is active token from Slicer POW + // We need get new token and freeze webview until its not refreshed + if (m_refreshing_token) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " already refreshing"; + return; + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + m_refreshing_token = true; + show_loading_overlay(); + wxGetApp().plater()->get_user_account()->request_refresh(); +} + +void PrintablesWebViewPanel::on_reload_event(const std::string& message_data) +{ + // Event from our error page button or keyboard shortcut + m_styles_defined = false; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { + do_reload(); + } else { + // On error page do load of default url. + load_default_url(); + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } +} + +namespace { +std::string escape_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +} + +void PrintablesWebViewPanel::on_printables_event_print_gcode(const std::string& message_data) +{ + // { "event": "downloadFile", "url": "https://media.printables.com/somesecure.stl", "modelUrl": "https://www.printables.com/model/123" } + std::string download_url; + std::string model_url; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto url = ptree.get_optional("url"); url) { + download_url = *url; + } + if (const auto url = ptree.get_optional("modelUrl"); url) { + model_url = *url; + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } + assert(!download_url.empty() && !model_url.empty()); + wxCommandEvent* evt = new wxCommandEvent(EVT_PRINTABLES_CONNECT_PRINT); + evt->SetString(from_u8(Utils::ServiceConfig::instance().connect_printables_print_url() +"?url=" + escape_url(download_url))); + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt); +} +void PrintablesWebViewPanel::on_printables_event_download_file(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; + // { "event": "printGcode", "url": "https://media.printables.com/somesecure.gcode", "modelUrl": "https://www.printables.com/model/123" } + std::string download_url; + std::string model_url; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto url = ptree.get_optional("url"); url) { + download_url = *url; + } + if (const auto url = ptree.get_optional("modelUrl"); url) { + model_url = *url; + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } + assert(!download_url.empty() && !model_url.empty()); + boost::filesystem::path url_path(download_url); + show_download_notification(url_path.filename().string()); + + wxGetApp().printables_download_request(download_url, model_url); +} +void PrintablesWebViewPanel::on_printables_event_slice_file(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; + // { "event": "sliceFile", "url": "https://media.printables.com/somesecure.zip", "modelUrl": "https://www.printables.com/model/123" } + std::string download_url; + std::string model_url; + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto url = ptree.get_optional("url"); url) { + download_url = *url; + } + if (const auto url = ptree.get_optional("modelUrl"); url) { + model_url = *url; + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); + return; + } + assert(!download_url.empty() && !model_url.empty()); + + wxGetApp().printables_slice_request(download_url, model_url); +} + +void PrintablesWebViewPanel::on_printables_event_required_login(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; + wxGetApp().printables_login_request(); +} +void PrintablesWebViewPanel::on_printables_event_open_url(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; + + try { + std::stringstream ss(message_data); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (const auto url = ptree.get_optional("url"); url) { + wxGetApp().open_browser_with_warning_dialog(GUI::from_u8(*url)); + } + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse Printables message. " << e.what(); + return; + } +} + +void PrintablesWebViewPanel::define_css() +{ + + if (m_styles_defined) { + return; + } + m_styles_defined = true; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + std::string script = R"( + // Loading overlay and Notification style + var style = document.createElement('style'); + style.innerHTML = ` + body {} + .slic3r-loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(127 127 127 / 50%); + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + } + .slic3r-loading-anim { + width: 60px; + aspect-ratio: 4; + --_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000); + background: + var(--_g) 0% 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 100%; + animation: slic3r-loading-anim 1s infinite linear; + } + @keyframes slic3r-loading-anim { + 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} + 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0% ,calc(100%/3) 100%} + 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0% } + } + .notification-popup { + position: fixed; + right: 10px; + bottom: 10px; + background-color: #333333; /* Dark background */ + padding: 10px; + border-radius: 6px; /* Slightly rounded corners */ + color: #ffffff; /* White text */ + font-family: Arial, sans-serif; + font-size: 12px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3); /* Add a subtle shadow */ + min-width: 350px; + max-width: 350px; + min-height: 50px; + } + .notification-popup div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 20px; /* Add padding to make text truncate earlier */ + } + .notification-popup b { + color: #ffa500; + } + .notification-popup a:hover { + text-decoration: underline; /* Underline on hover */ + } + .notification-popup .close-button { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid #ffa500; /* Orange border for the button */ + border-radius: 4px; + text-align: center; + font-size: 16px; + line-height: 16px; + cursor: pointer; + padding-top: 1px; + } + .notification-popup .close-button:hover { + background-color: #ffa500; /* Orange background on hover */ + color: #333333; /* Dark color for the "X" on hover */ + } + .notification-popup .close-button:before { + content: 'X'; + color: #ffa500; /* Orange "X" */ + font-weight: bold; + } + `; + document.head.appendChild(style); + + // Capture click on hypertext + // Rewritten from mobileApp code + (function() { + const listenerKey = 'custom-click-listener'; + if (!document[listenerKey]) { + document.addEventListener( 'click', function(event) { + const target = event.target.closest('a[href]'); + if (!target) return; // Ignore clicks that are not on links + const url = target.href; + // Allow empty iframe navigation + if (url === 'about:blank') { + return; // Let it proceed + } + // Debug log for navigation + console.log(`Printables:onNavigationRequest: ${url}`); + // Handle all non-printables.com domains in an external browser + if (!/printables\.com/.test(url)) { + window.ExternalApp.postMessage(JSON.stringify({ event: 'openExternalUrl', url })) + event.preventDefault(); + } + // Default: Allow navigation to proceed + },true); // Capture the event during the capture phase + document[listenerKey] = true; + } + })(); + )"; +#if defined(__APPLE__) + // WebView on Windows does read keyboard shortcuts + // Thus doing f.e. Reload twice would make the oparation to fail + script += R"( + document.addEventListener('keydown', function (event) { + if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { + window.ExternalApp.postMessage(JSON.stringify({ event: 'reloadHomePage', fromKeyboard: 1})); + } + if (event.metaKey && event.key === 'q') { + window.ExternalApp.postMessage(JSON.stringify({ event: 'appQuit'})); + } + if (event.metaKey && event.key === 'm') { + window.ExternalApp.postMessage(JSON.stringify({ event: 'appMinimize'})); + } + }); + )"; +#endif // defined(__APPLE__) + run_script(script); +} + +void PrintablesWebViewPanel::show_download_notification(const std::string& filename) +{ + // There was a trouble with passing wide characters to the script (it was displayed wrong) + // Solution is to URL-encode the strings here and pass it. + // Then inside javascript decodes it. + const std::string message_filename = Http::url_encode(GUI::format(_u8L("Downloading %1%"),filename)); + const std::string message_dest = Http::url_encode(GUI::format(_u8L("To %1%"), wxGetApp().app_config->get("url_downloader_dest"))); + std::string script = GUI::format(R"( + function removeNotification() { + const notifDiv = document.getElementById('slicer-notification'); + if (notifDiv) + notifDiv.remove(); + } + function appendNotification() { + const body = document.getElementsByTagName('body')[0]; + const notifDiv = document.createElement('div'); + notifDiv.innerHTML = ` +
+ QIDISlicer: ${decodeURIComponent('%1%')} +
${decodeURIComponent('%2%')} +
+ `; + notifDiv.className = 'notification-popup'; + notifDiv.id = 'slicer-notification'; + body.appendChild(notifDiv); + + window.setTimeout(removeNotification, 5000); + } + appendNotification(); + )", message_filename, message_dest); + run_script(script); +} + +void PrintablesWebViewPanel::show_loading_overlay() +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + std::string script = R"( + function slic3r_showLoadingOverlay() { + const body = document.getElementsByTagName('body')[0]; + const overlayDiv = document.createElement('div'); + overlayDiv.className = 'slic3r-loading-overlay' + overlayDiv.id = 'slic3r-loading-overlay'; + overlayDiv.innerHTML = '
'; + body.appendChild(overlayDiv); + } + slic3r_showLoadingOverlay(); + )"; + run_script(script); +} + +void PrintablesWebViewPanel::hide_loading_overlay() +{ + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; + m_refreshing_token = false; + std::string script = R"( + function slic3r_hideLoadingOverlay() { + const overlayDiv = document.getElementById('slic3r-loading-overlay'); + if (overlayDiv) + overlayDiv.remove(); + } + slic3r_hideLoadingOverlay(); + )"; + run_script(script); +} + + +} // namespace slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/WebViewPanel.hpp b/src/slic3r/GUI/WebViewPanel.hpp new file mode 100644 index 0000000..4d3326c --- /dev/null +++ b/src/slic3r/GUI/WebViewPanel.hpp @@ -0,0 +1,248 @@ +#ifndef slic3r_WebViewPanel_hpp_ +#define slic3r_WebViewPanel_hpp_ + +#include +#include +#include + +#include "GUI_Utils.hpp" +#include "UserAccountSession.hpp" +#include "ConnectRequestHandler.hpp" +#include "slic3r/Utils/ServiceConfig.hpp" + +#ifdef DEBUG_URL_PANEL +#include +#endif + +class wxWebView; +class wxWebViewEvent; + +wxDECLARE_EVENT(EVT_PRINTABLES_CONNECT_PRINT, wxCommandEvent); + +namespace Slic3r::GUI { + +class WebViewPanel : public wxPanel +{ +public: + WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html, const std::string& error_html, bool do_create); + virtual ~WebViewPanel(); + void destroy_browser(); + void set_create_browser() {m_do_late_webview_create = true; m_load_default_url = true; } + + void load_url(const wxString& url); + void load_default_url_delayed(); + void load_error_page(); + + // Let WebViewPanel do on_show so it can create webview properly + // and load default page + // override after_on_show for more actions in on_show + void on_show(wxShowEvent& evt); + virtual void after_on_show(wxShowEvent& evt) {} + + virtual void on_script_message(wxWebViewEvent& evt); + + void on_idle(wxIdleEvent& evt); + virtual void on_loaded(wxWebViewEvent& evt); + void on_url(wxCommandEvent& evt); + virtual void on_back_button(wxCommandEvent& evt); + virtual void on_forward_button(wxCommandEvent& evt); + void on_stop_button(wxCommandEvent& evt); + virtual void on_reload_button(wxCommandEvent& evt); + + void on_view_source_request(wxCommandEvent& evt); + void on_view_text_request(wxCommandEvent& evt); + void on_tools_clicked(wxCommandEvent& evt); + void on_error(wxWebViewEvent& evt); + + void run_script(const wxString& javascript); + void on_run_script_custom(wxCommandEvent& evt); + void on_add_user_script(wxCommandEvent& evt); + void on_set_custom_user_agent(wxCommandEvent& evt); + void on_clear_selection(wxCommandEvent& evt); + void on_delete_selection(wxCommandEvent& evt); + void on_select_all(wxCommandEvent& evt); + void On_enable_context_menu(wxCommandEvent& evt); + void On_enable_dev_tools(wxCommandEvent& evt); + virtual void on_navigation_request(wxWebViewEvent &evt); + + virtual wxString get_default_url() const { return m_default_url; } + void set_default_url(const wxString& url) { m_default_url = url; } + virtual void do_reload(); + virtual void load_default_url(); + + virtual void sys_color_changed(); + + void set_load_default_url_on_next_error(bool val) { m_load_default_url_on_next_error = val; } + + void on_app_quit_event(const std::string& message_data); + void on_app_minimize_event(const std::string& message_data); +protected: + virtual void late_create(); + virtual void define_css(); + virtual void on_page_will_load(); + + + wxWebView* m_browser { nullptr }; + bool m_load_default_url { false }; + + wxBoxSizer* topsizer; + wxBoxSizer* m_sizer_top; +#ifdef DEBUG_URL_PANEL + + wxBoxSizer *bSizer_toolbar; + wxButton * m_button_back; + wxButton * m_button_forward; + wxButton * m_button_stop; + wxButton * m_button_reload; + wxTextCtrl *m_url; + wxButton * m_button_tools; + + wxMenu* m_tools_menu; + wxMenuItem* m_script_custom; + + wxInfoBar *m_info; + wxStaticText* m_info_text; + + wxMenuItem* m_context_menu; + wxMenuItem* m_dev_tools; +#endif + + // Last executed JavaScript snippet, for convenience. + wxString m_javascript; + wxString m_response_js; + wxString m_default_url; + bool m_reached_default_url {false}; + + std::string m_loading_html; + std::string m_error_html; + //DECLARE_EVENT_TABLE() + + bool m_load_error_page { false }; + bool m_shown { false }; + bool m_load_default_url_on_next_error { false }; + bool m_do_late_webview_create {false}; + bool m_styles_defined {false}; + + std::vector m_script_message_hadler_names; +}; + +class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler +{ +public: + ConnectWebViewPanel(wxWindow* parent); + ~ConnectWebViewPanel() override; + void on_script_message(wxWebViewEvent& evt) override; + void logout(); + void sys_color_changed() override; + void on_navigation_request(wxWebViewEvent &evt) override; +protected: + void late_create() override; + void on_connect_action_request_login(const std::string &message_data) override; + void on_connect_action_select_printer(const std::string& message_data) override; + void on_connect_action_print(const std::string& message_data) override; + void on_connect_action_webapp_ready(const std::string& message_data) override {} + void run_script_bridge(const wxString& script) override {run_script(script); } + void on_page_will_load() override; + void on_connect_action_error(const std::string &message_data) override; + void on_reload_event(const std::string& message_data) override; + void on_connect_action_close_dialog(const std::string& message_data) override {assert(false);} + void on_user_token(UserAccountSuccessEvent& e); + void define_css() override; +private: + static wxString get_login_script(bool refresh); + static wxString get_logout_script(); + void on_user_logged_out(UserAccountSuccessEvent& e); +}; + +class PrinterWebViewPanel : public WebViewPanel +{ +public: + PrinterWebViewPanel(wxWindow* parent, const wxString& default_url); + + void on_loaded(wxWebViewEvent& evt) override; + void on_script_message(wxWebViewEvent& evt) override; + void on_navigation_request(wxWebViewEvent &evt) override; + void send_api_key(); + void send_credentials(); + void set_api_key(const std::string &key) + { + clear(); + m_api_key = key; + } + void set_credentials(const std::string &usr, const std::string &psk) + { + clear(); + m_usr = usr; + m_psk = psk; + } + void clear() { m_api_key.clear(); m_usr.clear(); m_psk.clear(); m_api_key_sent = false; } + + void on_reload_event(const std::string& message_data); +protected: + void define_css() override; +private: + std::string m_api_key; + std::string m_usr; + std::string m_psk; + bool m_api_key_sent {false}; + + void handle_message(const std::string& message); + std::map> m_events; +}; + +class PrintablesWebViewPanel : public WebViewPanel +{ +public: + PrintablesWebViewPanel(wxWindow* parent); + void on_navigation_request(wxWebViewEvent &evt) override; + void on_loaded(wxWebViewEvent& evt) override; + void after_on_show(wxShowEvent& evt) override; + void on_script_message(wxWebViewEvent& evt) override; + void sys_color_changed() override; + + void logout(const std::string& override_url = std::string()); + void login(const std::string& access_token, const std::string& override_url = std::string()); + void send_refreshed_token(const std::string& access_token); + void send_will_refresh(); + wxString get_default_url() const override; + void set_next_show_url(const std::string& url) {m_next_show_url = Utils::ServiceConfig::instance().printables_url() + url; } +protected: + void define_css() override; +private: + void handle_message(const std::string& message); + 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); + void on_printables_event_download_file(const std::string& message_data); + 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 load_default_url() override; + std::string get_url_lang_theme(const wxString& url) const; + void show_download_notification(const std::string& filename); + void show_loading_overlay(); + void hide_loading_overlay(); + + std::map> m_events; + std::string m_next_show_url; + + bool m_refreshing_token {false}; +#ifdef _WIN32 + bool m_remove_request_auth { false }; +#endif +/* +Eventy Slicer -> Printables +accessTokenWillChange +WebUI zavola event predtim nez udela refresh access tokenu proti qidi Accountu na Printables to bude znamenat pozastaveni requestu Mobile app muze chtit udelat refresh i bez explicitni predchozi printables zadosti skrz accessTokenExpired event +accessTokenChange +window postMessage JSON stringify { event 'accessTokenChange' token 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVC' } +volani po uspesne rotaci tokenu +historyBack +navigace zpet triggerovana z mobilni aplikace +historyForward +navigace vpred triggerovana z mobilni aplikace +*/ +}; +} // namespace Slic3r::GUI + +#endif /* slic3r_WebViewPanel_hpp_ */ \ No newline at end of file diff --git a/src/slic3r/GUI/WebViewPlatformUtils.hpp b/src/slic3r/GUI/WebViewPlatformUtils.hpp index 2c1429d..f6d5a8b 100644 --- a/src/slic3r/GUI/WebViewPlatformUtils.hpp +++ b/src/slic3r/GUI/WebViewPlatformUtils.hpp @@ -7,5 +7,8 @@ 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 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 35161a1..f5ffd8b 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp @@ -6,6 +6,7 @@ #include "WebViewPlatformUtils.hpp" #include +#include namespace Slic3r::GUI { @@ -60,7 +61,7 @@ void delete_cookie_callback (GObject* source_object, GAsyncResult* result, void* { WebKitCookieManager *cookie_manager = WEBKIT_COOKIE_MANAGER(source_object); GError* err = nullptr; - gboolean b = webkit_cookie_manager_delete_cookie_finish(cookie_manager, result, &err); + webkit_cookie_manager_delete_cookie_finish(cookie_manager, result, &err); if (err) { BOOST_LOG_TRIVIAL(error) << "Error deleting cookies: " << err->message; g_error_free(err); @@ -107,4 +108,39 @@ void delete_cookies(wxWebView* web_view, const std::string& url) WebKitCookieManager* cookieManager = webkit_web_context_get_cookie_manager(context); webkit_cookie_manager_get_cookies(cookieManager, uri, nullptr, (GAsyncReadyCallback)Slic3r::GUI::get_cookie_callback, nullptr); } + +void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) +{ + // unused on Linux + assert(true); } +void remove_request_authorization(wxWebView* web_view) +{ + // unused on Linux + assert(true); +} + +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + WebKitWebView* native_backend = static_cast(web_view->GetNativeBackend()); + WebKitURIRequest* request = webkit_uri_request_new(address.c_str()); + if(!request) + { + BOOST_LOG_TRIVIAL(error) << "load_request failed: request is nullptr. address: " << address; + return; + } + SoupMessageHeaders* soup_headers = webkit_uri_request_get_http_headers(request); + if (!soup_headers) + { + BOOST_LOG_TRIVIAL(error) << "load_request failed: soup_headers is nullptr."; + return; + } + if (!token.empty()) + { + soup_message_headers_append(soup_headers, "Authorization", ("External " + token).c_str()); + } + + // Load the request in the WebView + webkit_web_view_load_request(native_backend, request); +} +} \ No newline at end of file diff --git a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm index b09c69f..ad04db6 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm +++ b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm @@ -155,7 +155,27 @@ void delete_cookies(wxWebView* web_view, const std::string& url) } } }]; - +} +void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) +{ + // unused on MacOS + assert(true); +} +void remove_request_authorization(wxWebView* web_view) +{ + // unused on MacOS + assert(true); +} +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + WKWebView* backend = static_cast(web_view->GetNativeBackend()); + NSString *url_string = [NSString stringWithCString:address.c_str() encoding:[NSString defaultCStringEncoding]]; + NSString *token_string = [NSString stringWithCString:token.c_str() encoding:[NSString defaultCStringEncoding]]; + NSURL *url = [NSURL URLWithString:url_string]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + NSString *auth_value = [NSString stringWithFormat:@"External %@", token_string]; + [request setValue:auth_value forHTTPHeaderField:@"Authorization"]; + [backend loadRequest:request]; } } diff --git a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp index 4e98a0d..819b663 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp @@ -26,12 +26,14 @@ void setup_webview_with_credentials(wxWebView* webview, const std::string& usern { ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "setup_webview_with_credentials Failed: Webview 2 is null."; return; } wxCOMPtr wv2_10; HRESULT hr = webView2->QueryInterface(IID_PPV_ARGS(&wv2_10)); if (FAILED(hr)) { - return; + BOOST_LOG_TRIVIAL(error) << "setup_webview_with_credentials Failed: ICoreWebView2_10 is null."; + return; } remove_webview_credentials(webview); @@ -68,11 +70,13 @@ void remove_webview_credentials(wxWebView* webview) { ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "remove_webview_credentials Failed: webView2 is null."; return; } wxCOMPtr wv2_10; HRESULT hr = webView2->QueryInterface(IID_PPV_ARGS(&wv2_10)); if (FAILED(hr)) { + BOOST_LOG_TRIVIAL(error) << "remove_webview_credentials Failed: ICoreWebView2_10 is null."; return; } @@ -93,6 +97,7 @@ void delete_cookies(wxWebView* webview, const std::string& url) { ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "delete_cookies Failed: webView2 is null."; return; } @@ -135,6 +140,7 @@ void delete_cookies(wxWebView* webview, const std::string& url) 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; webView2->CallDevToolsProtocolMethod(L"Network.deleteCookies", name_and_domain.c_str(), Microsoft::WRL::Callback( [](HRESULT errorCode, LPCWSTR resultJson) -> HRESULT { return S_OK; }).Get()); @@ -145,6 +151,157 @@ void delete_cookies(wxWebView* webview, const std::string& url) } +static EventRegistrationToken m_webResourceRequestedTokenForImageBlocking = {}; +static wxString filter_patern; +namespace { +void RequestHeadersToLog(ICoreWebView2HttpRequestHeaders* requestHeaders) +{ + wxCOMPtr iterator; + requestHeaders->GetIterator(&iterator); + BOOL hasCurrent = FALSE; + BOOST_LOG_TRIVIAL(info) <<"Logging request headers:"; + while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) + { + wchar_t* name = nullptr; + wchar_t* value = nullptr; + + iterator->GetCurrentHeader(&name, &value); + BOOST_LOG_TRIVIAL(debug) <<"name: " << name << L", value: " << value; + if (name) { + CoTaskMemFree(name); + } + if (value) { + CoTaskMemFree(value); + } + + BOOL hasNext = FALSE; + iterator->MoveNext(&hasNext); + } +} +} + +void add_request_authorization(wxWebView* webview, const wxString& address, const std::string& token) +{ + // This function adds a filter so when pattern document is being requested, callback is triggered + // Inside add_WebResourceRequested callback, there is a Authorization header added. + // The filter needs to be removed to stop adding the auth header + ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); + if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization Failed: Webview 2 is null."; + return; + } + wxCOMPtr wv2_2; + HRESULT hr = webView2->QueryInterface(IID_PPV_ARGS(&wv2_2)); + if (FAILED(hr)) { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization Failed: QueryInterface ICoreWebView2_2 has failed."; + return; + } + filter_patern = address + "/*"; + webView2->AddWebResourceRequestedFilter( filter_patern.c_str(), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); + + if (FAILED(webView2->add_WebResourceRequested( + Microsoft::WRL::Callback( + [token](ICoreWebView2 *sender, ICoreWebView2WebResourceRequestedEventArgs *args) { + // Get the web resource request + wxCOMPtr request; + HRESULT hr = args->get_Request(&request); + if (FAILED(hr)) + { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to get_Request."; + return S_OK; + } + // Get the request headers + wxCOMPtr headers; + hr = request->get_Headers(&headers); + if (FAILED(hr)) + { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to get_Headers."; + return S_OK; + } + LPWSTR wideUri = nullptr; + request->get_Uri(&wideUri); + std::wstring ws(wideUri); + + std::string val = "External " + token; + // Add or modify the Authorization header + hr = headers->SetHeader(L"Authorization", GUI::from_u8(val).c_str()); + BOOST_LOG_TRIVIAL(debug) << "add_WebResourceRequested " << ws; + + // This function is only needed for debug purpose + RequestHeadersToLog(headers.Get()); + return S_OK; + } + ).Get(), &m_webResourceRequestedTokenForImageBlocking + ))) { + + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to add callback."; + } + + +} + +void remove_request_authorization(wxWebView* webview) +{ + ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); + if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "remove_request_authorization Failed: webView2 is null."; + return; + } + BOOST_LOG_TRIVIAL(info) << "remove_request_authorization"; + webView2->RemoveWebResourceRequestedFilter(filter_patern.c_str(), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); + if(FAILED(webView2->remove_WebResourceRequested( m_webResourceRequestedTokenForImageBlocking))) { + BOOST_LOG_TRIVIAL(error) << "WebView: Failed to remove resources"; + } +} + +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + // This function should create its own GET request and send it (works on linux) + // For that we would use NavigateWithWebResourceRequest. + // For that we need ICoreWebView2Environment smart pointer. + // Such pointer does exists inside wxWebView edge backend. (wxWebViewEdgeImpl::m_webViewEnvironment) + // But its currently private and not getable. (It wouldn't be such problem to create the getter) + + ICoreWebView2 *webView2 = static_cast(web_view->GetNativeBackend()); + if (!webView2) { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: webView2 is null."; + return; + } + + // GetEnviroment does not exists + wxCOMPtr webViewEnvironment; + //webViewEnvironment = static_cast(web_view->GetEnviroment()); + if (!webViewEnvironment.Get()) { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: ICoreWebView2Environment is null."; + return; + } + + wxCOMPtr webViewEnvironment2; + if (FAILED(webViewEnvironment->QueryInterface(IID_PPV_ARGS(&webViewEnvironment2)))) + { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: ICoreWebView2Environment2 is null."; + return; + } + wxCOMPtr webResourceRequest; + + if (FAILED(webViewEnvironment2->CreateWebResourceRequest( + L"https://www.printables.com/", L"GET", NULL, + L"Content-Type: application/x-www-form-urlencoded", &webResourceRequest))) + { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: CreateWebResourceRequest failed."; + return; + } + wxCOMPtr wv2_2; + if (FAILED(webView2->QueryInterface(IID_PPV_ARGS(&wv2_2)))) { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: ICoreWebView2_2 is null."; + return; + } + if (FAILED(wv2_2->NavigateWithWebResourceRequest(webResourceRequest.get()))) + { + BOOST_LOG_TRIVIAL(error) << "load_request Failed: NavigateWithWebResourceRequest failed."; + return; + } +} } // namespace Slic3r::GUI #endif // WIN32 diff --git a/src/slic3r/GUI/Widgets/TextInput.cpp b/src/slic3r/GUI/Widgets/TextInput.cpp index 26a1397..8b2437a 100644 --- a/src/slic3r/GUI/Widgets/TextInput.cpp +++ b/src/slic3r/GUI/Widgets/TextInput.cpp @@ -257,6 +257,7 @@ void TextInput::DoSetSize(int x, int y, int width, int height, int sizeFlags) wxClientDC dc(this); const int r_shift = int(dd_icon_size.x == 0 ? (3. * dc.GetContentScaleFactor()) : ((size.y - dd_icon_size.y) / 2)); textSize.x = size.x - textPos.x - labelSize.x - dd_icon_size.x - r_shift; + if (textSize.x < -1) textSize.x = -1; text_ctrl->SetSize(textSize); text_ctrl->SetPosition({textPos.x, (size.y - textSize.y) / 2}); } diff --git a/src/slic3r/GUI/format.hpp b/src/slic3r/GUI/format.hpp index 15a3f04..9cdcd0a 100644 --- a/src/slic3r/GUI/format.hpp +++ b/src/slic3r/GUI/format.hpp @@ -8,6 +8,7 @@ // This wrapper also manages implicit conversion from wxString to UTF8 and format_wxstr() variants are provided to format into wxString. #include +#include namespace Slic3r::internal::format { // Wrapper around wxScopedCharBuffer to indicate that the content is UTF8 formatted. diff --git a/src/slic3r/Utils/AppUpdater.cpp b/src/slic3r/Utils/AppUpdater.cpp index d18c829..aa21a3b 100644 --- a/src/slic3r/Utils/AppUpdater.cpp +++ b/src/slic3r/Utils/AppUpdater.cpp @@ -44,7 +44,8 @@ namespace { BOOST_LOG_TRIVIAL(error) << full_message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(full_message); - GUI::wxGetApp().QueueEvent(evt); + if (wxApp::GetInstance() != nullptr) + GUI::wxGetApp().QueueEvent(evt); } return res; } @@ -210,7 +211,8 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d BOOST_LOG_TRIVIAL(error) << message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); - GUI::wxGetApp().QueueEvent(evt); + if (wxApp::GetInstance() != nullptr) + GUI::wxGetApp().QueueEvent(evt); return boost::filesystem::path(); } @@ -224,43 +226,49 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d std::string line2 = GUI::format(_u8L("Can't create file at %1%"), tmp_path.string()); std::string message = GUI::format("%1%\n%2%", line1, line2); BOOST_LOG_TRIVIAL(error) << message; - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); - evt->SetString(message); - GUI::wxGetApp().QueueEvent(evt); + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); + } return boost::filesystem::path(); } std::string error_message; - bool res = http_get_file(data.url, 256 * 1024 * 1024 + bool res = http_get_file(data.url, 256 * 1024 * 1024 // on_progress , [&last_gui_progress, expected_size](Http::Progress progress) { // size check if (progress.dltotal > 0 && progress.dltotal > expected_size) { std::string message = GUI::format("Downloading new %1% has failed. The file has incorrect file size. Aborting download.\nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); BOOST_LOG_TRIVIAL(error) << message; - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); - evt->SetString(message); - GUI::wxGetApp().QueueEvent(evt); - return false; - } else if (progress.dltotal > 0 && progress.dltotal < expected_size) { + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); + } + return false; + } else if (progress.dltotal > 0 && progress.dltotal < expected_size) { // This is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download. BOOST_LOG_TRIVIAL(info) << GUI::format("Downloading new %1% has incorrect size. The download will continue. \nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); - } + } // progress event size_t gui_progress = progress.dltotal > 0 ? 100 * progress.dlnow / progress.dltotal : 0; BOOST_LOG_TRIVIAL(debug) << "App download " << gui_progress << "% " << progress.dlnow << " of " << progress.dltotal; if (last_gui_progress < gui_progress && (last_gui_progress != 0 || gui_progress != 100)) { last_gui_progress = gui_progress; - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS); - evt->SetString(GUI::from_u8(std::to_string(gui_progress))); - GUI::wxGetApp().QueueEvent(evt); - } + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS); + evt->SetString(GUI::from_u8(std::to_string(gui_progress))); + GUI::wxGetApp().QueueEvent(evt); + } + } return true; } // on_complete , [&file, dest_path, tmp_path, expected_size](std::string body, std::string& error_message){ // Size check. Does always 1 char == 1 byte? - size_t body_size = body.size(); + size_t body_size = body.size(); if (body_size != expected_size) { error_message = GUI::format(_u8L("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%"), expected_size, body_size); return false; @@ -287,23 +295,28 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d if (!res) { if (m_cancel) { - BOOST_LOG_TRIVIAL(info) << error_message; - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); // FAILED with empty msg only closes progress notification - GUI::wxGetApp().QueueEvent(evt); - } else { - std::string message = (error_message.empty() + BOOST_LOG_TRIVIAL(info) << error_message; + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED + ); // FAILED with empty msg only closes progress notification + GUI::wxGetApp().QueueEvent(evt); + } + } else { + std::string message = (error_message.empty() ? std::string() : GUI::format(_u8L("Downloading new %1% has failed:\n%2%"), SLIC3R_APP_NAME, error_message)); - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); - if (!message.empty()) { - BOOST_LOG_TRIVIAL(error) << message; - evt->SetString(message); - } - GUI::wxGetApp().QueueEvent(evt); - } + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + if (!message.empty()) { + BOOST_LOG_TRIVIAL(error) << message; + evt->SetString(message); + } + GUI::wxGetApp().QueueEvent(evt); + } + } return boost::filesystem::path(); } - + return dest_path; } @@ -313,7 +326,7 @@ bool AppUpdater::priv::run_downloaded_file(boost::filesystem::path path) return run_file(path); } -void AppUpdater::priv::version_check(const std::string& version_check_url) +void AppUpdater::priv::version_check(const std::string& version_check_url) { assert(!version_check_url.empty()); std::string error_message; @@ -331,7 +344,7 @@ void AppUpdater::priv::version_check(const std::string& version_check_url) if (!res) { std::string message = GUI::format("Downloading %1% version file has failed:\n%2%", SLIC3R_APP_NAME, error_message); BOOST_LOG_TRIVIAL(error) << message; - if (m_triggered_by_user) { + if (m_triggered_by_user && wxApp::GetInstance() != nullptr) { wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); GUI::wxGetApp().QueueEvent(evt); @@ -351,10 +364,12 @@ void AppUpdater::priv::parse_version_string(const std::string& body) BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed."; // Lets send event with current version, this way if user triggered this check, it will notify him about no new version online. std::string version = Semver().to_string(); - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); - evt->SetString(GUI::from_u8(version)); - GUI::wxGetApp().QueueEvent(evt); - return; + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + } + return; } std::string tree_string = body.substr(start); boost::property_tree::ptree tree; @@ -373,7 +388,7 @@ void AppUpdater::priv::parse_version_string(const std::string& body) std::string section_name = section.first; // online release version info - if (section_name == + if (section_name == #ifdef _WIN32 "release:win64" #elif __APPLE__ @@ -439,7 +454,7 @@ void AppUpdater::priv::parse_version_string(const std::string& body) } } // send prerelease version to UI layer - if (recent_version) { + if (recent_version && wxApp::GetInstance() != nullptr) { BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version_string); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); evt->SetString(GUI::from_u8(version_string)); @@ -454,9 +469,11 @@ void AppUpdater::priv::parse_version_string(const std::string& body) // send std::string version = new_data.version.get().to_string(); BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); - evt->SetString(GUI::from_u8(version)); - GUI::wxGetApp().QueueEvent(evt); + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent *evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + } } #if 0 //lm:is this meant to be ressurected? //dk: it is code that parses QIDISlicer.version2 in 2.4.0, It was deleted from PresetUpdater.cpp and I would keep it here for possible reference. @@ -476,9 +493,11 @@ void AppUpdater::priv::parse_version_string_old(const std::string& body) const return; } BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); - wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); - evt->SetString(GUI::from_u8(version)); - GUI::wxGetApp().QueueEvent(evt); + if (wxApp::GetInstance() != nullptr) { + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + } // alpha / beta version std::vector prerelease_versions; @@ -520,7 +539,7 @@ void AppUpdater::priv::parse_version_string_old(const std::string& body) const version = ver_string; } } - if (recent_version) { + if (recent_version && wxApp::GetInstance() != nullptr) { BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index d30bd0d..594accf 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -33,6 +33,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Format/3mf.hpp" +#include "libslic3r/Semver.hpp" #include "../GUI/GUI.hpp" #include "../GUI/I18N.hpp" #include "../GUI/MsgDialog.hpp" @@ -388,7 +389,8 @@ bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, wxPro on_progress(L("Loading repaired model"), 80); DynamicPrintConfig config; ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); + boost::optional qidislicer_generator_version; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false, qidislicer_generator_version); boost::filesystem::remove(path_dst); if (! loaded) throw Slic3r::RuntimeError("Import of the repaired 3mf file failed"); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 5c92ae4..1a354e7 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -131,6 +131,7 @@ struct Http::priv Http::ErrorFn errorfn; Http::ProgressFn progressfn; Http::IPResolveFn ipresolvefn; + Http::RetryFn retryfn; priv(const std::string &url); ~priv(); @@ -393,6 +394,12 @@ void Http::priv::http_perform(const HttpRetryOpt& retry_opts) std::chrono::milliseconds delay = std::chrono::milliseconds(randomized_delay(generator)); size_t num_retries = 0; do { + // break if canceled outside + if (retryfn && !retryfn(num_retries + 1, num_retries < retry_opts.max_retries ? delay.count() : 0)) { + res = CURLE_ABORTED_BY_CALLBACK; + cancel = true; + break; + } res = ::curl_easy_perform(curl); if (res == CURLE_OK) @@ -407,6 +414,7 @@ void Http::priv::http_perform(const HttpRetryOpt& retry_opts) << "), retrying in " << delay.count() / 1000.0f << " s"; std::this_thread::sleep_for(delay); delay = std::min(delay * 2, retry_opts.max_delay); + } } while (retry); @@ -454,7 +462,7 @@ Http::Http(const std::string &url) : p(new priv(url)) {} const HttpRetryOpt& HttpRetryOpt::default_retry() { using namespace std::chrono_literals; - static HttpRetryOpt val = {500ms, 64s, 0}; + static HttpRetryOpt val = {500ms, std::chrono::milliseconds(MAX_RETRY_DELAY_MS), MAX_RETRIES}; return val; } @@ -641,6 +649,12 @@ Http& Http::on_ip_resolve(IPResolveFn fn) return *this; } +Http& Http::on_retry(RetryFn fn) +{ + if (p) { p->retryfn = std::move(fn); } + return *this; +} + Http& Http::cookie_file(const std::string& file_path) { if (p) { diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 20e1a06..7ddd655 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -19,6 +19,9 @@ struct HttpRetryOpt static const HttpRetryOpt& no_retry(); static const HttpRetryOpt& default_retry(); + + static constexpr size_t MAX_RETRY_DELAY_MS = 4 * 64000; + static constexpr size_t MAX_RETRIES = 16; }; @@ -55,6 +58,8 @@ public: typedef std::function ProgressFn; typedef std::function IPResolveFn; + // + typedef std::function RetryFn; Http(Http &&other); @@ -134,6 +139,8 @@ public: // Called if curl_easy_getinfo resolved just used IP address. Http& on_ip_resolve(IPResolveFn fn); + Http& on_retry(RetryFn fn); + Http& cookie_file(const std::string& file_path); Http& cookie_jar(const std::string& file_path); Http& set_referer(const std::string& referer); diff --git a/src/slic3r/Utils/Jwt.cpp b/src/slic3r/Utils/Jwt.cpp index 448a062..1b8ba91 100644 --- a/src/slic3r/Utils/Jwt.cpp +++ b/src/slic3r/Utils/Jwt.cpp @@ -8,19 +8,21 @@ #include #include #include +#include namespace Slic3r::Utils { -bool verify_exp(const std::string& token) +namespace { +boost::optional get_exp(const std::string& token) { size_t payload_start = token.find('.'); if (payload_start == std::string::npos) - return false; + return boost::none; payload_start += 1; // payload starts after dot const size_t payload_end = token.find('.', payload_start); if (payload_end == std::string::npos) - return false; + return boost::none; size_t encoded_length = payload_end - payload_start; size_t decoded_length = boost::beast::detail::base64::decoded_size(encoded_length); @@ -40,7 +42,7 @@ bool verify_exp(const std::string& token) std::tie(written_bytes, read_bytes) = boost::beast::detail::base64::decode(json.data(), json_b64.data(), json_b64.length()); json.resize(written_bytes); if (written_bytes == 0) - return false; + return boost::none; namespace pt = boost::property_tree; @@ -48,10 +50,26 @@ bool verify_exp(const std::string& token) std::istringstream iss(json); pt::json_parser::read_json(iss, payload); - auto exp_opt = payload.get_optional("exp"); + return payload.get_optional("exp"); +} +} + +int get_exp_seconds(const std::string& token) +{ + auto exp_opt = get_exp(token); + if (!exp_opt) + return 0; + auto now = std::chrono::system_clock::now(); + auto now_in_seconds = std::chrono::duration_cast(now.time_since_epoch()).count(); + double remaining_time = *exp_opt - now_in_seconds; + return (int)remaining_time; +} + +bool verify_exp(const std::string& token) +{ + auto exp_opt = get_exp(token); if (!exp_opt) return false; - auto now = time(nullptr); return exp_opt.get() > now; } diff --git a/src/slic3r/Utils/Jwt.hpp b/src/slic3r/Utils/Jwt.hpp index b957e8d..468de28 100644 --- a/src/slic3r/Utils/Jwt.hpp +++ b/src/slic3r/Utils/Jwt.hpp @@ -5,6 +5,6 @@ namespace Slic3r::Utils { bool verify_exp(const std::string& token); - +int get_exp_seconds(const std::string& token); } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 6e165a7..f23b4e8 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -27,7 +27,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/UpdateDialogs.hpp" -#include "slic3r/GUI/ConfigWizard.hpp" +#include "slic3r/Utils/PresetUpdaterWrapper.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/format.hpp" @@ -70,23 +70,7 @@ void copy_file_fix(const fs::path &source, const fs::path &target) static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; fs::permissions(target, perms); } -std::string escape_string_url(const std::string& unescaped) -{ - std::string ret_val; - CURL* curl = curl_easy_init(); - if (curl) { - char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); - if (decoded) { - ret_val = std::string(decoded); - curl_free(decoded); - } - curl_easy_cleanup(curl); - } - return ret_val; } -} - -wxDEFINE_EVENT(EVT_CONFIG_UPDATER_SYNC_DONE, wxCommandEvent); struct Update { @@ -166,18 +150,13 @@ struct PresetUpdater::priv { std::vector index_db; - bool enabled_version_check; bool enabled_config_update; - std::string version_check_url; fs::path cache_path; fs::path cache_vendor_path; fs::path rsrc_path; fs::path vendor_path; - bool cancel; - std::thread thread; - bool has_waiting_updates { false }; Updates waiting_updates; @@ -186,16 +165,16 @@ struct PresetUpdater::priv void set_download_prefs(const AppConfig *app_config); void prune_tmps() const; void clear_cache_vendor() const; - void sync_config(const VendorMap& vendors, const GUI::ArchiveRepository* archive); + void sync_config(const VendorMap& vendors, const ArchiveRepository* archive, PresetUpdaterUIStatus* ui_status); void check_install_indices() const; Updates get_config_updates(const Semver& old_slic3r_version) const; - bool perform_updates(Updates &&updates, const SharedArchiveRepositoryVector& repositories, bool snapshot = true) const; + bool perform_updates(Updates &&updates, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status, bool snapshot = true) const; void set_waiting_updates(Updates u); // checks existence and downloads resource to cache - void get_missing_resource(const GUI::ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const; + void get_missing_resource(const ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini, PresetUpdaterUIStatus* ui_status) const; // checks existence and downloads resource to vendor or copy from cache to vendor - void get_or_copy_missing_resource(const GUI::ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const; + void get_or_copy_missing_resource(const ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini, PresetUpdaterUIStatus* ui_status) const; void update_index_db(); //w45 @@ -207,7 +186,7 @@ PresetUpdater::priv::priv() , cache_vendor_path(cache_path / "vendor") , rsrc_path(fs::path(resources_dir()) / "profiles") , vendor_path(fs::path(Slic3r::data_dir()) / "vendor") - , cancel(false) + //, cancel(false) { set_download_prefs(GUI::wxGetApp().app_config); // Install indicies from resources. Only installs those that are either missing or older than in resources. @@ -224,8 +203,6 @@ void PresetUpdater::priv::update_index_db() // Pull relevant preferences from AppConfig void PresetUpdater::priv::set_download_prefs(const AppConfig *app_config) { - enabled_version_check = app_config->get("notify_release") != "none"; - version_check_url = app_config->version_check_url(); enabled_config_update = app_config->get_bool("preset_update") && !app_config->legacy_datadir(); } @@ -252,7 +229,7 @@ void PresetUpdater::priv::clear_cache_vendor() const } // gets resource to cache// -void PresetUpdater::priv::get_missing_resource(const GUI::ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const +void PresetUpdater::priv::get_missing_resource(const ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini, PresetUpdaterUIStatus* ui_status) const { assert(!filename.empty() && !vendor.empty()); //if (filename.empty() || vendor.empty()) { @@ -281,13 +258,12 @@ void PresetUpdater::priv::get_missing_resource(const GUI::ArchiveRepository* arc if (!fs::exists(file_in_cache.parent_path())) fs::create_directory(file_in_cache.parent_path()); - //std::string escaped_filename = escape_string_url(filename); const std::string resource_subpath = GUI::format("%1%/%2%",vendor, filename); - archive->get_file(resource_subpath, file_in_cache, repository_id_from_ini); + archive->get_file(resource_subpath, file_in_cache, repository_id_from_ini, ui_status); return; } // gets resource to vendor// -void PresetUpdater::priv::get_or_copy_missing_resource(const GUI::ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const +void PresetUpdater::priv::get_or_copy_missing_resource(const ArchiveRepository* archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini, PresetUpdaterUIStatus* ui_status) const { assert(!filename.empty() && !vendor.empty()); @@ -312,9 +288,8 @@ void PresetUpdater::priv::get_or_copy_missing_resource(const GUI::ArchiveReposit if (!fs::exists(file_in_cache)) { BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename; - //std::string escaped_filename = escape_string_url(filename); const std::string resource_subpath = GUI::format("%1%/%2%", vendor, filename); - archive->get_file(resource_subpath, file_in_vendor, repository_id_from_ini); + archive->get_file(resource_subpath, file_in_vendor, repository_id_from_ini, ui_status); return; } BOOST_LOG_TRIVIAL(debug) << "Copiing: " << file_in_cache << " to " << file_in_vendor; @@ -323,19 +298,22 @@ void PresetUpdater::priv::get_or_copy_missing_resource(const GUI::ArchiveReposit // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. -void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::ArchiveRepository* archive_repository) +void PresetUpdater::priv::sync_config(const VendorMap& vendors, const ArchiveRepository* archive_repository, PresetUpdaterUIStatus* ui_status) { BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache"; if (!enabled_config_update) { return; } + assert(ui_status); + ui_status->set_target(archive_repository->get_manifest().id + " archive"); + // Download profiles archive zip fs::path archive_path(cache_path / "vendor_indices.zip"); - if (!archive_repository->get_archive(archive_path)) { - BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip failed."; + 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."; return; } - if (cancel) { + if (ui_status->get_canceled()) { return; } @@ -405,7 +383,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi // Update vendor preset bundles if in Vendor // Over all indices from the cache directory: for (auto &index : index_db) { - if (cancel) { + if (ui_status->get_canceled()) { return; } auto archive_it = std::find_if(vendors_with_status.begin(), vendors_with_status.end(), @@ -451,7 +429,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path, vendor.name); continue; } - if (cancel) + if (ui_status->get_canceled()) return; } @@ -479,9 +457,9 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name; const std::string source_subpath = GUI::format("%1%/%2%.ini", vendor.id, recommended.to_string()); const fs::path bundle_path = cache_path / (vendor.id + ".ini"); - if (!archive_repository->get_file(source_subpath, bundle_path, vendor.repo_id)) + if (!archive_repository->get_file(source_subpath, bundle_path, vendor.repo_id, ui_status)) continue; - if (cancel) + if (ui_status->get_canceled()) return; // vp is fully loaded to get all resources VendorProfile vp; @@ -498,7 +476,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (! res.empty()) { try { - get_missing_resource(archive_repository, vp.id, res, vendor.repo_id); + get_missing_resource(archive_repository, vp.id, res, vendor.repo_id, ui_status); } catch (const std::exception& e) { @@ -506,7 +484,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi } } - if (cancel) + if (ui_status->get_canceled()) return; } } @@ -538,7 +516,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (!fs::exists(ini_path_in_archive)){ // Download recommneded to vendor - we do not have any existing ini file so we have to use archive url. const std::string source_subpath = GUI::format("%1%/%2%.ini", vendor.first, recommended.to_string()); - if (!archive_repository->get_ini_no_id(source_subpath, ini_path_in_archive)) + if (!archive_repository->get_ini_no_id(source_subpath, ini_path_in_archive, ui_status)) continue; } else { // check existing ini version @@ -559,7 +537,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (vp.config_version != recommended) { // Take url from existing ini. This way we prevent downloading files from multiple sources. const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended.to_string()); - if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id)) + if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id, ui_status)) continue; } } @@ -576,14 +554,14 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (!model.thumbnail.empty()) { try { - get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id); + get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id, ui_status); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); } } - if (cancel) + if (ui_status->get_canceled()) return; } } else if (vendor.second == VendorStatus::IN_CACHE) { @@ -652,7 +630,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi continue; } const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended_archive.to_string()); - if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id)) { + if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id, ui_status)) { BOOST_LOG_TRIVIAL(error) << format("Failed to get new vendor .ini file when checking missing resources: %1%", ini_path_in_archive.string()); continue; } @@ -669,7 +647,7 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi } if (vp.config_version != recommended_archive) { const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended_archive.to_string()); - if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id)) { + if (!archive_repository->get_file(source_subpath, ini_path_in_archive, vp.repo_id, ui_status)) { BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_archive); continue; } @@ -693,14 +671,14 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (!model.thumbnail.empty()) { try { - get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id); + get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id, ui_status); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); } } - if (cancel) + if (ui_status->get_canceled()) return; } } else if (vendor.second == VendorStatus::INSTALLED || vendor.second == VendorStatus::NEW_VERSION) { @@ -723,14 +701,14 @@ void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::Archi if (!res.empty()) { try { - get_or_copy_missing_resource(archive_repository, vp.id, res, vp.repo_id); + get_or_copy_missing_resource(archive_repository, vp.id, res, vp.repo_id, ui_status); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to get " << res << " for " << vp.id << " " << model.id << ": " << e.what(); } } - if (cancel) + if (ui_status->get_canceled()) return; } } @@ -961,7 +939,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version return updates; } -bool PresetUpdater::priv::perform_updates(Updates &&updates, const SharedArchiveRepositoryVector& repositories, bool snapshot) const +bool PresetUpdater::priv::perform_updates(Updates &&updates, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status, bool snapshot) const { if (updates.incompats.size() > 0) { if (snapshot) { @@ -1050,7 +1028,7 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, const SharedArchive { auto it = std::find_if(repositories.begin(), repositories.end(), [&vp](const auto* i){ return vp.repo_id == i->get_manifest().id; }); if (it != repositories.end()) - get_or_copy_missing_resource((*it), vp.id, resource, vp.repo_id); + get_or_copy_missing_resource((*it), vp.id, resource, vp.repo_id, ui_status); else { BOOST_LOG_TRIVIAL(error) << "Failed to prepare " << resource << " for " << vp.id << " " << model.id << ": Missing record for source with repo_id " << vp.repo_id; } @@ -1084,74 +1062,20 @@ PresetUpdater::PresetUpdater() : PresetUpdater::~PresetUpdater() { - if (p && p->thread.joinable()) { - // This will stop transfers being done by the thread, if any. - // Cancelling takes some time, but should complete soon enough. - p->cancel = true; - p->thread.join(); - } } -void PresetUpdater::sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler,SharedArchiveRepositoryVector&& repositories) -{ - p->set_download_prefs(GUI::wxGetApp().app_config); - if (!p->enabled_config_update) { return; } - - p->thread = std::thread([this, &vendors = preset_bundle->vendors, repositories = std::move(repositories), evt_handler]() { - this->p->clear_cache_vendor(); - this->p->prune_tmps(); - for (const GUI::ArchiveRepository* archive : repositories) { - this->p->sync_config(vendors, archive); - } - wxCommandEvent* evt = new wxCommandEvent(EVT_CONFIG_UPDATER_SYNC_DONE); - evt_handler->QueueEvent(evt); - }); -} - -void PresetUpdater::cancel_sync() -{ - if (p && p->thread.joinable()) { - // This will stop transfers being done by the thread, if any. - // Cancelling takes some time, but should complete soon enough. - p->cancel = true; - p->thread.join(); - } - p->cancel = false; -} - -void PresetUpdater::sync_blocking(const PresetBundle* preset_bundle, wxEvtHandler* evt_handler, const SharedArchiveRepositoryVector& repositories) +void PresetUpdater::sync_blocking(const VendorMap& vendors, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status) { p->set_download_prefs(GUI::wxGetApp().app_config); if (!p->enabled_config_update) { return; } this->p->clear_cache_vendor(); this->p->prune_tmps(); - for (const GUI::ArchiveRepository* archive : repositories) { - this->p->sync_config(preset_bundle->vendors, archive); - } -} - -void PresetUpdater::slic3r_update_notify() -{ - if (! p->enabled_version_check) - return; - auto* app_config = GUI::wxGetApp().app_config; - const auto ver_online_str = app_config->get("version_online"); - const auto ver_online = Semver::parse(ver_online_str); - const auto ver_online_seen = Semver::parse(app_config->get("version_online_seen")); - - if (ver_online) { - // Only display the notification if the version available online is newer AND if we haven't seen it before - if (*ver_online > Slic3r::SEMVER && (! ver_online_seen || *ver_online_seen < *ver_online)) { - GUI::MsgUpdateSlic3r notification(Slic3r::SEMVER, *ver_online); - notification.ShowModal(); - if (notification.disable_version_check()) { - app_config->set("notify_release", "none"); - p->enabled_version_check = false; - } - } - - app_config->set("version_online_seen", ver_online_str); + for (const ArchiveRepository* archive : repositories) { + if (ui_status && ui_status->get_canceled()) { + break; + } + this->p->sync_config(vendors, archive, ui_status); } } @@ -1174,7 +1098,7 @@ static bool reload_configs_update_gui() return true; } -PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params, const SharedArchiveRepositoryVector& repositories) const +PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status) const { if (! p->enabled_config_update) { return R_NOOP; } @@ -1208,7 +1132,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 // This effectively removes the incompatible bundles: // (snapshot is taken beforehand) - if (! p->perform_updates(std::move(updates), repositories) || + if (! p->perform_updates(std::move(updates), repositories, ui_status) || ! GUI::wxGetApp().run_wizard(GUI::ConfigWizard::RR_DATA_INCOMPAT)) return R_INCOMPAT_EXIT; @@ -1250,7 +1174,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(info) << "User wants to update..."; - if (! p->perform_updates(std::move(updates), repositories) || + if (! p->perform_updates(std::move(updates), repositories, ui_status) || ! reload_configs_update_gui()) return R_INCOMPAT_EXIT; return R_UPDATE_INSTALLED; @@ -1261,6 +1185,11 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 } } + if (!wxApp::GetInstance() || ! GUI::wxGetApp().plater()) { + // The main thread might have start killing the UI. + return R_NOOP; + } + // regular update if (params == UpdateParams::SHOW_NOTIFICATION) { p->set_waiting_updates(updates); @@ -1276,7 +1205,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::PresetUpdateAvailableNewPrinter); else{ if(p->force_update_config()){ - if (p->perform_updates(std::move(p->waiting_updates), repositories) && + if (p->perform_updates(std::move(p->waiting_updates), repositories, ui_status) && reload_configs_update_gui()) { p->has_waiting_updates = false; } @@ -1301,12 +1230,12 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url), std::move(printers)); } - GUI::MsgUpdateConfig dlg(updates_msg, params == UpdateParams::FORCED_BEFORE_WIZARD); + GUI::MsgUpdateConfig dlg(updates_msg, params); const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; - if (! p->perform_updates(std::move(updates), repositories) || + if (! p->perform_updates(std::move(updates), repositories, ui_status) || ! reload_configs_update_gui()) return R_ALL_CANCELED; return R_UPDATE_INSTALLED; @@ -1327,7 +1256,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 return R_NOOP; } -bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector bundles, const SharedArchiveRepositoryVector& repositories, bool snapshot) const +bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector bundles, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status, bool snapshot) const { Updates updates; @@ -1430,7 +1359,7 @@ bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vectorperform_updates(std::move(updates), repositories, snapshot); + return p->perform_updates(std::move(updates), repositories, ui_status, snapshot); } //w45 @@ -1483,7 +1412,7 @@ bool PresetUpdater::priv::force_update_config() } } -void PresetUpdater::on_update_notification_confirm(const SharedArchiveRepositoryVector& repositories) +void PresetUpdater::on_update_notification_confirm(const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status) { if (!p->has_waiting_updates) return; @@ -1501,12 +1430,12 @@ void PresetUpdater::on_update_notification_confirm(const SharedArchiveRepository updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url), std::move(printers)); } - GUI::MsgUpdateConfig dlg(updates_msg); + GUI::MsgUpdateConfig dlg(updates_msg, UpdateParams::SHOW_TEXT_BOX); const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; - if (p->perform_updates(std::move(p->waiting_updates), repositories) && + if (p->perform_updates(std::move(p->waiting_updates), repositories, ui_status) && reload_configs_update_gui()) { p->has_waiting_updates = false; } @@ -1516,28 +1445,10 @@ void PresetUpdater::on_update_notification_confirm(const SharedArchiveRepository } } -bool PresetUpdater::version_check_enabled() const -{ - return p->enabled_version_check; -} - void PresetUpdater::update_index_db() { p->update_index_db(); } -void PresetUpdater::add_additional_archive(const std::string& archive_url, const std::string& download_url) -{ - if (std::find_if(m_additional_archives.begin(), m_additional_archives.end(), [archive_url](const std::pair& it) { return it.first == archive_url; }) == m_additional_archives.end()) { - m_additional_archives.emplace_back(archive_url, download_url); - } -} - -void PresetUpdater::add_additional_archives(const std::vector>& archives) -{ - for (const auto& pair : archives) { - add_additional_archive(pair.first, pair.second); - } -} } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index db54b79..cb719a3 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -6,16 +6,16 @@ #include #include -#include - namespace Slic3r { - +class VendorProfile; +typedef std::map VendorMap; class AppConfig; class PresetBundle; class Semver; +class PresetUpdaterUIStatus; -typedef std::vector SharedArchiveRepositoryVector; +typedef std::vector SharedArchiveRepositoryVector; static constexpr const int SLIC3R_VERSION_BODY_MAX = 256; @@ -29,14 +29,7 @@ public: PresetUpdater &operator=(const PresetUpdater &) = delete; ~PresetUpdater(); - // If either version check or config updating is enabled, get the appropriate data in the background and cache it. - void sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler, SharedArchiveRepositoryVector&& repositories); - void cancel_sync(); - - void sync_blocking(const PresetBundle* preset_bundle, wxEvtHandler* evt_handler, const SharedArchiveRepositoryVector& repositories); - - // If version check is enabled, check if chaced online slic3r version is newer, notify if so. - void slic3r_update_notify(); + void sync_blocking(const VendorMap& vendors, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status); enum UpdateResult { R_NOOP, @@ -51,35 +44,26 @@ public: enum class UpdateParams { SHOW_TEXT_BOX, // force modal textbox SHOW_NOTIFICATION, // only shows notification - FORCED_BEFORE_WIZARD // indicates that check of updated is forced before ConfigWizard opening + FORCED_BEFORE_WIZARD, // indicates that check of updated is forced before ConfigWizard opening + SHOW_TEXT_BOX_YES_NO // like first option but different buttons in dialog }; // If updating is enabled, check if updates are available in cache, if so, ask about installation. // A false return value implies Slic3r should exit due to incompatibility of configuration. // Providing old slic3r version upgrade profiles on upgrade of an application even in case // that the config index installed from the Internet is equal to the index contained in the installation package. - UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params, const SharedArchiveRepositoryVector& repositories) const; + UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status) const; void update_index_db(); // "Update" a list of bundles from resources or cache/vendor (behaves like an online update). - bool install_bundles_rsrc_or_cache_vendor(std::vector bundles, const SharedArchiveRepositoryVector& repositories, bool snapshot = true) const; + bool install_bundles_rsrc_or_cache_vendor(std::vector bundles, const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status, bool snapshot = true) const; - void on_update_notification_confirm(const SharedArchiveRepositoryVector& repositories); + void on_update_notification_confirm(const SharedArchiveRepositoryVector& repositories, PresetUpdaterUIStatus* ui_status); - bool version_check_enabled() const; - - void add_additional_archive(const std::string& archive_url, const std::string& download_url); - void add_additional_archives(const std::vector>& archives); private: struct priv; std::unique_ptr p; - - std::vector> m_additional_archives; }; - -//wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); -//wxDECLARE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); -wxDECLARE_EVENT(EVT_CONFIG_UPDATER_SYNC_DONE, wxCommandEvent); } #endif diff --git a/src/slic3r/Utils/PresetUpdaterWrapper.cpp b/src/slic3r/Utils/PresetUpdaterWrapper.cpp new file mode 100644 index 0000000..51123dd --- /dev/null +++ b/src/slic3r/Utils/PresetUpdaterWrapper.cpp @@ -0,0 +1,354 @@ +#include "PresetUpdaterWrapper.hpp" + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Semver.hpp" +#include "libslic3r/Utils.hpp" + +using namespace std::chrono; + +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); + +PresetUpdaterWrapper::PresetUpdaterWrapper() + : m_preset_updater(std::make_unique()) + , m_preset_archive_database(std::make_unique()) +{ +} +PresetUpdaterWrapper::~PresetUpdaterWrapper() +{ + if (m_worker_thread.joinable()) { + if (m_ui_status) + m_ui_status->set_canceled(true); + m_worker_thread.join(); + } +} + +bool PresetUpdaterWrapper::wizard_sync(const PresetBundle* preset_bundle, const Semver& old_slic3r_version, wxWindow* parent, bool full_sync, const wxString& headline) +{ + 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); + VendorMap vendors_copy = preset_bundle->vendors; + auto worker_body = [ui_status, this, vendors_copy, full_sync]() + { + if (!m_preset_archive_database->sync_blocking(ui_status)) { + ui_status->end(); + return; + } + if (ui_status->get_canceled()) { 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->update_index_db(); + } + ui_status->end(); + }; + m_modal_thread = std::thread(worker_body); + // We need to call ShowModal here instead of prompting it from event callback. + // Otherwise UI thread would freez on job_thread.join(); + dialog->CenterOnParent(); + dialog->ShowModal(); + m_modal_thread.join(); + parent->RemoveChild(dialog); + dialog->Destroy(); + 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()); + 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*/) { + return false; + } + + // 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); + } + bool res = !ui_status->get_canceled(); + return res; +} + +PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_user_request(const PresetBundle* preset_bundle, const Semver& old_slic3r_version, wxWindow* parent) +{ + 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; }); + // TRN: Headline of Progress dialog + GUI::ProgressUpdaterDialog* dialog = new GUI::ProgressUpdaterDialog(ui_status, parent, _L("Checking for Configuration Updates")); + 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]() + { + if (!m_preset_archive_database->sync_blocking(ui_status)) { + ui_status->end(); + return; + } + if (ui_status->get_canceled()) { 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->update_index_db(); + ui_status->end(); + }; + + m_modal_thread = std::thread(worker_body); + // We need to call ShowModal here instead of prompting it from event callback. + // Otherwise UI thread would freez on job_thread.join(); + dialog->CenterOnParent(); + dialog->ShowModal(); + m_modal_thread.join(); + parent->RemoveChild(dialog); + dialog->Destroy(); + 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()); + GUI::ErrorDialog err_msg(nullptr, s, false); + err_msg.ShowModal(); + return PresetUpdater::UpdateResult::R_ALL_CANCELED; + } + + if (ui_status->get_canceled()) { + return PresetUpdater::UpdateResult::R_ALL_CANCELED; + } + + if (!failed_paths.empty()) { + int cnt = std::count(failed_paths.begin(), failed_paths.end(), '\n') + 1; + // TRN: %1% contains paths from which loading failed. They are separated by \n, there is no \n at the end. + failed_paths = GUI::format(_L_PLURAL("It was not possible to extract data from %1%. The source will not be updated.", + "It was not possible to extract data for following local sources. They will not be updated.\n\n %1%", cnt), failed_paths); + GUI::ErrorDialog err_msg(nullptr, failed_paths, false); + err_msg.ShowModal(); + } + // 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); + return updater_result; +} + +PresetUpdater::UpdateResult PresetUpdaterWrapper::check_updates_on_startup(const Semver& old_slic3r_version) +{ + 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_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); +} + +void PresetUpdaterWrapper::on_update_notification_confirm() +{ + if (m_modal_thread.joinable()) { + return; + } + PresetUpdaterUIStatus ui_status(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); +} + +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); +} + +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); + 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); + if (this->m_ui_status->get_canceled()) { return; } + wxCommandEvent* evt = new wxCommandEvent(EVT_CONFIG_UPDATER_SYNC_DONE); + wxQueueEvent(end_evt_handler, evt); + }; + + m_worker_thread = std::thread(worker_body); +} + +void PresetUpdaterWrapper::cancel_worker_thread() +{ + if (m_worker_thread.joinable()) { + if (m_ui_status) { + m_ui_status->set_canceled(true); + } else assert(false); + + m_worker_thread.join(); + + if (m_ui_status) { + delete m_ui_status; + m_ui_status = nullptr; + } + } +} + +const std::map PresetUpdaterUIStatus::policy_map = { + {PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_5_TRIES, {500ms, 5s, 4}}, + {PresetUpdaterUIStatus::PresetUpdaterRetryPolicy::PURP_NO_RETRY, {0ms}} +}; +PresetUpdaterUIStatus::PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy) +{ + if (auto it = policy_map.find(policy); it != policy_map.end()) { + m_retry_policy = it->second; + } else { + assert(false); + m_retry_policy = {0ms}; + } +} +bool PresetUpdaterUIStatus::on_attempt(int attempt, unsigned delay) +{ + if (attempt == 1) { + // TRN: Text of progress dialog. %1% is a name of file. + set_status(GUI::format_wxstr(_L("Downloading Resources: %1%"), m_target)); + } else { + // TRN: Text of progress dialog. %1% is a name of file. %2% is a number of attept. + set_status(GUI::format_wxstr(_L("Downloading Resources: %1%. Attempt %2%."), m_target, std::to_string(attempt))); + } + return get_canceled(); +} +void PresetUpdaterUIStatus::set_target(const std::string& target) +{ + m_target = target; +} +void PresetUpdaterUIStatus::set_status(const wxString& status) +{ + if (m_evt_handler) + wxQueueEvent(m_evt_handler, new PresetUpdaterStatusMessageEvent(EVT_PRESET_UPDATER_STATUS_PRINT, status)); +} + +void PresetUpdaterUIStatus::end() +{ + if (m_evt_handler) + wxQueueEvent(m_evt_handler, new PresetUpdaterStatusSimpleEvent(EVT_PRESET_UPDATER_STATUS_END)); +} + +namespace GUI { +ProgressUpdaterDialog::ProgressUpdaterDialog(PresetUpdaterUIStatus* ui_status, wxWindow* parent, const wxString first_line) + // TRN: Text of progress dialog. + :wxGenericProgressDialog(first_line, _L("Initializing"), 100, parent, wxPD_AUTO_HIDE|wxPD_APP_MODAL|wxPD_CAN_ABORT) + , PresetUpdaterUIStatusCancel(ui_status) +{ + SetMinSize(wxSize(32 * wxGetApp().em_unit(),12 * wxGetApp().em_unit())); + Bind(EVT_PRESET_UPDATER_STATUS_END, &ProgressUpdaterDialog::on_end, this); + Bind(EVT_PRESET_UPDATER_STATUS_PRINT, &ProgressUpdaterDialog::on_set_status, this); +} +ProgressUpdaterDialog::~ProgressUpdaterDialog() +{ +} +void ProgressUpdaterDialog::on_set_status(const PresetUpdaterStatusMessageEvent& evt) +{ + if (!Pulse(evt.data)) { + set_cancel(true); + } +} +void ProgressUpdaterDialog::on_end(const PresetUpdaterStatusSimpleEvent& evt) +{ + EndModal(0); +} +#if 0 +CommonUpdaterDialog::CommonUpdaterDialog(PresetUpdaterUIStatus* ui_status, wxWindow* parent, const wxString first_line, int milisecond_until_cancel_shown) + : wxDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxFRAME_FLOAT_ON_PARENT) + , PresetUpdaterUIStatusCancel(ui_status) +{ + auto* headline = new wxStaticText(this, wxID_ANY, first_line, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + m_status_text = new wxStaticText(this, wxID_ANY, _L("Initializing") + dots, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + m_cancel_button = new wxButton(this, wxID_CANCEL, "Cancel"); + // Layout using sizer + wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(m_status_text, 1, wxALIGN_CENTER_VERTICAL | wxALL, 10); + hsizer->Add(m_cancel_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, 10); + wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); + vsizer->Add(headline, 0, wxALIGN_CENTER | wxALL, 10); + vsizer->Add(hsizer, 0, wxEXPAND | wxALL, 5); + this->SetSizer(vsizer); + SetMinSize(wxSize(wxGetApp().em_unit() * 40, wxGetApp().em_unit() * 5)); + m_cancel_button->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { + set_cancel(true); + m_status_text->SetLabel("Canceling..."); + Update(); + Fit(); + }); + + if (milisecond_until_cancel_shown > 0) { + m_cancel_button->Show(false); + m_show_cancel_timer = new wxTimer(this); + Bind(wxEVT_TIMER, [this](wxTimerEvent&) + { + m_cancel_button->Show(); + Layout(); + Fit(); + }); + m_show_cancel_timer->StartOnce(milisecond_until_cancel_shown); + } + + Bind(EVT_PRESET_UPDATER_STATUS_END, &CommonUpdaterDialog::on_end, this); + Bind(EVT_PRESET_UPDATER_STATUS_PRINT, &CommonUpdaterDialog::on_set_status, this); + + #ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); + #endif + Fit(); +} +CommonUpdaterDialog::~CommonUpdaterDialog() +{ + if (m_show_cancel_timer) { + m_show_cancel_timer->Stop(); + delete m_show_cancel_timer; + } +} +void CommonUpdaterDialog::on_set_status(const PresetUpdaterStatusMessageEvent& evt) +{ + m_status_text->SetLabel(evt.data); + Update(); + Fit(); +} +void CommonUpdaterDialog::on_end(const PresetUpdaterStatusSimpleEvent& evt) +{ + EndModal(0); +} + +DummyPresetUpdaterUIStatusHandler::DummyPresetUpdaterUIStatusHandler() + :wxEvtHandler() +{ + Bind(EVT_PRESET_UPDATER_STATUS_END, &DummyPresetUpdaterUIStatusHandler::on_end, this); + Bind(EVT_PRESET_UPDATER_STATUS_PRINT, &DummyPresetUpdaterUIStatusHandler::on_set_status, this); +} +#endif +} +} + diff --git a/src/slic3r/Utils/PresetUpdaterWrapper.hpp b/src/slic3r/Utils/PresetUpdaterWrapper.hpp new file mode 100644 index 0000000..bf21df0 --- /dev/null +++ b/src/slic3r/Utils/PresetUpdaterWrapper.hpp @@ -0,0 +1,215 @@ +#ifndef slic3r_PresetUpdateWrapper_hpp_ +#define slic3r_PresetUpdateWrapper_hpp_ + +#include "slic3r/GUI/PresetArchiveDatabase.hpp" +#include "slic3r/GUI/ConfigWizard.hpp" +#include "slic3r/GUI/Event.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" +#include "slic3r/Utils/Http.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Slic3r { + +using PresetUpdaterStatusSimpleEvent = GUI::SimpleEvent; +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); +class PresetBundle; +class Semver; + +// class that is passed to inner thread function. +// Each call from inner thread triggers event, that is handled by wx object on UI thread (m_evt_handler). +// Communication in opposite direction is done via pointer that should use only set_canceled. (PresetUpdaterUIStatusCancel) +class PresetUpdaterUIStatus +{ +public: + enum class PresetUpdaterRetryPolicy + { + PURP_5_TRIES, + PURP_NO_RETRY, + }; + // called from PresetUpdaterWrapper + PresetUpdaterUIStatus(PresetUpdaterUIStatus::PresetUpdaterRetryPolicy policy); + ~PresetUpdaterUIStatus(){} + void set_handler(wxEvtHandler* evt_handler) {m_evt_handler = evt_handler;} + + // called from worker thread + bool on_attempt(int attempt, unsigned delay); + void set_target(const std::string& target); + void set_status(const wxString& status); + void end(); + bool get_canceled() const {return m_canceled.load(); } + 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; } + + // called from PresetUpdaterUIStatusCancel (ui thread) + void set_canceled(bool val) { m_canceled.store(val); } + void set_error(const std::string& msg) { m_error_msg = msg; } +private: + wxEvtHandler* m_evt_handler {nullptr}; + std::atomic m_canceled {false}; + std::string m_error_msg; + + std::string m_target; + + HttpRetryOpt m_retry_policy; + static const std::map policy_map; +}; + +// Purpose of this class: +// Serves as a hub a entering 2 classes: PresetArchiveDatabase and PresetUpdater +// PresetUpdater: +// - Does not contain outside getters / setters +// - Sync function can be run in worker thread +// ! other functions like config_update does show modal dialogs or notification and must be run in UI tread +// PresetArchiveDatabase: +// - Many functions gets or sets its inner data +// ! Including sync function +// - Does not use direct UI components f.e. dialog +// This class is accessible via wxGetApp().get_preset_updater_wrapper() +// but it should be done only in certain cases: +// 1) Sync of PresetUpdater +// - Runs on worker thread +// - Is called only during startup +// - Needs to be canceled before other operations +// - Ends by queueing EVT_CONFIG_UPDATER_SYNC_DONE +// 2) Callbacks of EVT_CONFIG_UPDATER_SYNC_DONE +// - Needs to be run in UI thread +// - Might chain (evt -> PresetUpdater function -> notification -> Plater -> PresetUpdater function -> dialog) +// 3) Check of update triggered by user +// - Runs most of the operations in own thread while having Modal Dialog for user +// - Used before and inside Config Wizard or when Check for Config Updates +// - Might use UI thread PresetUpdater functions after its thread is done +// - The inner thread is stored as m_modal_thread due to +// 4) Config Wizard run +// - Config Wizard often needs to get or set data of PresetArchiveDatabase +// - Config Wizard runs in UI thread as Modal Dialog +// Possibility of conflicts: +// 1 x 2 - No conflict due 2 being triggered only by end of 1 +// 1 x 3 - No conflict due 3 calling Cancel on 1 before runing +// 1 x 4 - No conflict due 4 run after 3 +// 2 x 2 - All 2 functions does create modal window and are triggered by ui buttons - ui thread might work on other events but ui should be inaccessible +// 2 x 3 - If 1 finnished (2 starts) and 3 is triggered by user - both are triggered via events +// - If order of events is event triggering 3 first and then event queued by 1 is second +// - 2 would run when inner thread of 3 changes data - Therefor functions of 2 must check if inner thread of 3 (m_modal_thread) is joinable +// 2 x 4 - No conflict due 2 and 4 run on both UI thread +// 3 x 4 - No conflict due either 3 blocking ui or even calling 4 only after it finnishes + +class PresetUpdaterWrapper +{ +public: + PresetUpdaterWrapper(); + ~PresetUpdaterWrapper(); + + // 1) Sync of PresetUpdater functions + // runs worker thread and leaves + void sync_preset_updater(wxEvtHandler* end_evt_handler, const PresetBundle* preset_bundle); + + // 2) Callbacks of EVT_CONFIG_UPDATER_SYNC_DONE + // Runs on UI thread + PresetUpdater::UpdateResult check_updates_on_startup(const Semver& old_slic3r_version); + void on_update_notification_confirm(); + + // 3) Check of update triggered by user + // runs own thread but blocks until its done + bool wizard_sync(const PresetBundle* preset_bundle, const Semver& old_slic3r_version, wxWindow* parent, bool full_sync, const wxString& headline); + PresetUpdater::UpdateResult check_updates_on_user_request(const PresetBundle* preset_bundle, const Semver& old_slic3r_version, wxWindow* parent); + + // 4) Config Wizard run + // These function are either const reading from m_preset_archive_database, + // Or sets inner data of m_preset_archive_database + // problem would be if at same time worker thread runs m_preset_archive_database->sync_blocking + bool is_selected_repository_by_id(const std::string& repo_id) const { return m_preset_archive_database->is_selected_repository_by_id(repo_id); } + bool is_selected_repository_by_uuid(const std::string& uuid) const { return m_preset_archive_database->is_selected_repository_by_uuid(uuid); } + SharedArchiveRepositoryVector get_all_archive_repositories() const { return m_preset_archive_database->get_all_archive_repositories(); } + SharedArchiveRepositoryVector get_selected_archive_repositories() const { return m_preset_archive_database->get_selected_archive_repositories();} + const std::map& get_selected_repositories_uuid() const { return m_preset_archive_database->get_selected_repositories_uuid(); } + bool set_selected_repositories(const std::vector& used_uuids, std::string& msg) { return m_preset_archive_database->set_selected_repositories(used_uuids, msg); } + void set_installed_printer_repositories(const std::vector &used_ids) { m_preset_archive_database->set_installed_printer_repositories(used_ids); } + void remove_local_archive(const std::string& uuid) { m_preset_archive_database->remove_local_archive(uuid); } + std::string add_local_archive(const boost::filesystem::path path, std::string& msg) { return m_preset_archive_database->add_local_archive(path, msg); } + + bool install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot = true) const ; + +private: + void cancel_worker_thread(); + + // Do not share these 2 out of PresetUpdaterWrapper + std::unique_ptr m_preset_archive_database; + std::unique_ptr m_preset_updater; + + // 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::thread m_modal_thread; +}; + +namespace GUI { + +class PresetUpdaterUIStatusCancel +{ +public: + PresetUpdaterUIStatusCancel(PresetUpdaterUIStatus* ui_status) : p_ui_status(ui_status) {} + ~PresetUpdaterUIStatusCancel() {} + void set_cancel(bool c) {p_ui_status->set_canceled(c);} +private: + PresetUpdaterUIStatus* p_ui_status; +}; + +class ProgressUpdaterDialog : public wxGenericProgressDialog, public PresetUpdaterUIStatusCancel +{ +public: + ProgressUpdaterDialog(PresetUpdaterUIStatus* ui_status, wxWindow* parent, const wxString first_line); + ~ProgressUpdaterDialog(); + void on_set_status(const PresetUpdaterStatusMessageEvent& evt); + void on_end(const PresetUpdaterStatusSimpleEvent& evt); +private: +}; + +#if 0 +// basic dialog +class CommonUpdaterDialog : public wxDialog, public PresetUpdaterUIStatusCancel +{ +public: + CommonUpdaterDialog(PresetUpdaterUIStatus* ui_status, wxWindow* parent, const wxString first_line, int milisecond_until_cancel_shown); + ~CommonUpdaterDialog(); + void on_set_status(const PresetUpdaterStatusMessageEvent& evt); + void on_end(const PresetUpdaterStatusSimpleEvent& evt); +private: + wxStaticText* m_status_text; + wxButton* m_cancel_button; + wxTimer* m_show_cancel_timer {nullptr}; +}; + +// testing purpose dummy class +class DummyPresetUpdaterUIStatusHandler : public wxEvtHandler +{ +public: + DummyPresetUpdaterUIStatusHandler(); + ~DummyPresetUpdaterUIStatusHandler() {} + void on_set_status(const PresetUpdaterStatusMessageEvent& evt) {} + void on_end(const PresetUpdaterStatusSimpleEvent& evt) + { + if(m_end_callback) + m_end_callback(); + } + void set_end_callback(std::function callback) {m_end_callback = callback; } +private: + std::function m_end_callback; +}; +#endif +} // namespace GUI +} // namespace Slic3r +#endif //slic3r_PresetUpdateWrapper_hpp_ \ No newline at end of file diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index 0aa5259..b4762a8 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -68,6 +68,7 @@ public: // A print host usually does not support multiple printers, with the exception of Repetier server. virtual bool supports_multiple_printers() const { return false; } virtual std::string get_host() const = 0; + virtual std::string get_notification_host() const {return get_host(); } // Support for Repetier server multiple groups & printers. Not supported by other print hosts. // Returns false if not supported. May throw HostNetworkError. diff --git a/src/slic3r/Utils/QIDIConnect.cpp b/src/slic3r/Utils/QIDIConnect.cpp index 8f110c8..50c003c 100644 --- a/src/slic3r/Utils/QIDIConnect.cpp +++ b/src/slic3r/Utils/QIDIConnect.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/UserAccount.hpp" +#include "slic3r/GUI/UserAccountUtils.hpp" #include #include @@ -21,32 +22,6 @@ namespace pt = boost::property_tree; namespace Slic3r { namespace { -std::string escape_string(const std::string& unescaped) -{ - std::string ret_val; - CURL* curl = curl_easy_init(); - if (curl) { - char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); - if (decoded) { - ret_val = std::string(decoded); - curl_free(decoded); - } - curl_easy_cleanup(curl); - } - return ret_val; -} -std::string escape_path_by_element(const boost::filesystem::path& path) -{ - std::string ret_val = escape_string(path.filename().string()); - boost::filesystem::path parent(path.parent_path()); - while (!parent.empty() && parent.string() != "/") // "/" check is for case "/file.gcode" was inserted. Then boost takes "/" as parent_path. - { - ret_val = escape_string(parent.filename().string()) + "/" + ret_val; - parent = parent.parent_path(); - } - return ret_val; -} - boost::optional get_error_message_from_response_body(const std::string& body) { boost::optional message; @@ -110,19 +85,6 @@ bool QIDIConnectNew::init_upload(PrintHostUpload upload_data, std::string& out) const std::string upload_filename = upload_data.upload_path.filename().string(); std::string url = GUI::format("%1%/app/users/teams/%2%/uploads", get_host(), m_team_id); std::string request_body_json = upload_data.data_json; - // GUI::format( - // "{" - // "\"filename\": \"%1%\", " - // "\"size\": %2%, " - // "\"path\": \"%3%\", " - // "\"force\": true, " - // "\"printer_uuid\": \"%4%\"" - // "}" - // , upload_filename - // , file_size - // , upload_data.upload_path.generic_string() - // , m_uuid - //); // replace plaholder filename assert(request_body_json.find("%1%") != std::string::npos); @@ -153,6 +115,12 @@ bool QIDIConnectNew::init_upload(PrintHostUpload upload_data, std::string& out) bool QIDIConnectNew::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const { + 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); + info_fn(L"qidiconnect_printer_address", printer_page_url); + std::string init_out; if (!init_upload(upload_data, init_out)) { @@ -181,22 +149,13 @@ bool QIDIConnectNew::upload(PrintHostUpload upload_data, ProgressFn progress_fn, } const std::string name = get_name(); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); -// const std::string escaped_upload_path = upload_data.storage + "/" + escape_path_by_element(upload_data.upload_path.string()); -// const std::string set_ready = upload_data.set_ready.empty() ? "" : "&set_ready=" + upload_data.set_ready; -// const std::string position = upload_data.position.empty() ? "" : "&position=" + upload_data.position; -// const std::string wait_until = upload_data.wait_until.empty() ? "" : "&wait_until=" + upload_data.wait_until; const std::string url = GUI::format( "%1%/app/teams/%2%/files/raw" "?upload_id=%3%" - // "&force=true" - // "&printer_uuid=%4%" - // "&path=%5%" - // "%6%" - // "%7%" - // "%8%" - , get_host(), m_team_id, upload_id/*, m_uuid, escaped_upload_path, set_ready, position, wait_until*/); + , get_host(), m_team_id, upload_id); bool res = true; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") % name % upload_data.source_path @@ -230,6 +189,11 @@ bool QIDIConnectNew::upload(PrintHostUpload upload_data, ProgressFn progress_fn, return res; } +std::string QIDIConnectNew::get_notification_host() const +{ + return "QIDI Connect"; +} + bool QIDIConnectNew::get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const { const char* name = get_name(); diff --git a/src/slic3r/Utils/QIDIConnect.hpp b/src/slic3r/Utils/QIDIConnect.hpp index ea44b6e..312634c 100644 --- a/src/slic3r/Utils/QIDIConnect.hpp +++ b/src/slic3r/Utils/QIDIConnect.hpp @@ -38,6 +38,7 @@ public: bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; } std::string get_host() const override { return Utils::ServiceConfig::instance().connect_url(); } + std::string get_notification_host() const override; 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; } diff --git a/src/slic3r/Utils/ServiceConfig.cpp b/src/slic3r/Utils/ServiceConfig.cpp index c6ce8a8..6707390 100644 --- a/src/slic3r/Utils/ServiceConfig.cpp +++ b/src/slic3r/Utils/ServiceConfig.cpp @@ -24,7 +24,9 @@ ServiceConfig::ServiceConfig() , m_account_url("https://account.qidi3d.com") , m_account_client_id("oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O") , m_media_url("https://media.printables.com") - , m_preset_repo_url("https://preset-repo-api.qidi3d.com") { + , m_preset_repo_url("https://preset-repo-api.qidi3d.com") + , m_printables_url("https://www.printables.com") +{ #ifdef SLIC3R_REPO_URL m_preset_repo_url = SLIC3R_REPO_URL; #endif @@ -34,6 +36,7 @@ ServiceConfig::ServiceConfig() update_from_env(m_account_client_id, "QIDI_ACCOUNT_CLIENT_ID"); update_from_env(m_media_url, "QIDI_MEDIA_URL", true); update_from_env(m_preset_repo_url, "QIDI_PRESET_REPO_URL", true); + update_from_env(m_printables_url, "QIDI_PRINTABLES_URL", true); } ServiceConfig& ServiceConfig::instance() diff --git a/src/slic3r/Utils/ServiceConfig.hpp b/src/slic3r/Utils/ServiceConfig.hpp index fdcf40d..20c3547 100644 --- a/src/slic3r/Utils/ServiceConfig.hpp +++ b/src/slic3r/Utils/ServiceConfig.hpp @@ -14,6 +14,7 @@ public: 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"; } + std::string connect_printables_print_url() const { return m_connect_url + "/slicer-print"; } const std::string& account_url() const { return m_account_url; } const std::string& account_client_id() const { return m_account_client_id; } @@ -30,6 +31,8 @@ public: bool webdev_enabled() const { return m_webdev_enabled; } void set_webdev_enabled(bool enabled) { m_webdev_enabled = enabled; } + const std::string& printables_url() const { return m_printables_url; } + static ServiceConfig& instance(); private: std::string m_connect_url; @@ -37,6 +40,7 @@ private: std::string m_account_client_id; std::string m_media_url; std::string m_preset_repo_url; + std::string m_printables_url; bool m_webdev_enabled{false}; };