diff --git a/resources/icons/burn.svg b/resources/icons/burn.svg new file mode 100644 index 0000000000..e7b58cd83f --- /dev/null +++ b/resources/icons/burn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/refresh.svg b/resources/icons/refresh.svg new file mode 100644 index 0000000000..c9dedef26f --- /dev/null +++ b/resources/icons/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/libslic3r/EmbossShape.hpp b/src/libslic3r/EmbossShape.hpp index 1685ae1a48..897cf134c1 100644 --- a/src/libslic3r/EmbossShape.hpp +++ b/src/libslic3r/EmbossShape.hpp @@ -3,6 +3,7 @@ #include #include +#include // unique_ptr #include #include #include @@ -11,6 +12,7 @@ #include "Point.hpp" // Transform3d #include "ExPolygon.hpp" #include "ExPolygonSerialize.hpp" +#include "nanosvg/nanosvg.h" // NSVGimage namespace Slic3r { @@ -83,19 +85,31 @@ struct EmbossShape // Stored_Transform3d * fix_3mf_tr = Transform3d_before_store_to_3mf std::optional fix_3mf_tr; - // file(.svg) path to source of shape - // When empty can't reload from disk - std::string svg_file_path; - + struct SvgFile { + // File(.svg) path on local computer + // When empty can't reload from disk + std::string path; + + // File path into .3mf(.zip) + // When empty svg is not stored into .3mf file yet. + // and will create dialog to delete private data on save. + std::string path_in_3mf; + + // Loaded svg file data. + // !!! It is not serialized on undo/redo stack + std::shared_ptr image; + }; + SvgFile svg_file; + // undo / redo stack recovery template void save(Archive &ar) const { - ar(shapes_with_ids, scale, projection, svg_file_path); + ar(shapes_with_ids, scale, projection, svg_file.path, svg_file.path_in_3mf); cereal::save(ar, fix_3mf_tr); } template void load(Archive &ar) { - ar(shapes_with_ids, scale, projection, svg_file_path); + ar(shapes_with_ids, scale, projection, svg_file.path, svg_file.path_in_3mf); cereal::load(ar, fix_3mf_tr); } }; diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 43fa9f93e9..5576b5e68d 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -3691,7 +3691,7 @@ std::string to_string(const ExPolygonsWithIds &shapes) void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume) { stream << " <" << SHAPE_TAG << " "; - stream << SVG_FILE_PATH_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(es.svg_file_path) << "\" "; + stream << SVG_FILE_PATH_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(es.svg_file.path) << "\" "; stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" "; std::string expolygons_str = to_string(es.shapes_with_ids); // cereal serialize expolygons diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index 53dfda3cb2..30256378d8 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -23,10 +23,10 @@ Point::coord_type to_coor(float val, float scale) { return static_castshapes; shape != NULL; shape = shape->next) { + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; if (shape->fill.type == NSVG_PAINT_NONE) @@ -64,10 +64,16 @@ Polygons to_polygons(NSVGimage *image, float tessTol, int max_level, float scale return polygons; } -ExPolygons to_expolygons(NSVGimage *image, float tessTol, int max_level, float scale, bool is_y_negative){ +ExPolygons to_expolygons(const NSVGimage &image, float tessTol, int max_level, float scale, bool is_y_negative){ return union_ex(to_polygons(image, tessTol, max_level, scale, is_y_negative)); } +NSVGimage_ptr nsvgParseFromFile(const std::string &filename, const char *units, float dpi) +{ + NSVGimage *image = ::nsvgParseFromFile(filename.c_str(), units, dpi); + return {image, ::nsvgDelete}; +} + } // namespace Slic3r namespace { diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp index d379e90908..06f2faf0bb 100644 --- a/src/libslic3r/NSVGUtils.hpp +++ b/src/libslic3r/NSVGUtils.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_NSVGUtils_hpp_ #define slic3r_NSVGUtils_hpp_ +#include +#include #include "Polygon.hpp" #include "ExPolygon.hpp" #include "nanosvg/nanosvg.h" // load SVG file @@ -19,8 +21,11 @@ namespace Slic3r { /// NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer /// Flag is y negative, when true than y coor is multiplied by -1 /// Polygons extracted from svg -Polygons to_polygons(NSVGimage *image, float tessTol = 10., int max_level = 10, float scale = 1.f, bool is_y_negative = true); -ExPolygons to_expolygons(NSVGimage *image, float tessTol = 10., int max_level = 10, float scale = 1.f, bool is_y_negative = true); +Polygons to_polygons(const NSVGimage &image, float tessTol = 10., int max_level = 10, float scale = 1.f, bool is_y_negative = true); +ExPolygons to_expolygons(const NSVGimage &image, float tessTol = 10., int max_level = 10, float scale = 1.f, bool is_y_negative = true); + +using NSVGimage_ptr = std::unique_ptr; +NSVGimage_ptr nsvgParseFromFile(const std::string& filename, const char *units = "mm", float dpi = 96.0f); } // namespace Slic3r #endif // slic3r_NSVGUtils_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp index 32c1c7ae8c..af315590f7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -68,7 +68,7 @@ std::string choose_svg_file(); /// Let user to choose file with (S)calable (V)ector (G)raphics - SVG. /// Than let select contour /// -/// SVG file path +/// SVG file path, when empty promt user to select one /// EmbossShape to create EmbossShape select_shape(std::string_view filepath = ""); @@ -80,14 +80,6 @@ EmbossShape select_shape(std::string_view filepath = ""); /// Base data for emboss SVG DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, std::string_view filepath = ""); -/// -/// Create symbol '?' as default shape -/// without source file -/// with size 2cm -/// -/// Default shape to emboss -ExPolygons default_shape(); - /// /// Separate file name from file path. /// String after last delimiter and before last point @@ -115,6 +107,12 @@ CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager &raycaster, M enum class IconType : unsigned { reset_value, reset_value_hover, + refresh, + refresh_hover, + change_file, + change_file_hover, + bake, + bake_hover, lock, lock_hover, unlock, @@ -126,6 +124,8 @@ enum class IconType : unsigned { // automatic calc of icon's count _count }; +// Do not forgot add loading of file in funtion: +// IconManager::Icons init_icons( const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { return *icons[static_cast(type)]; } @@ -140,7 +140,7 @@ struct GuiCfg float main_toolbar_height; // Define bigger size(width or height) - unsigned texture_max_size_px = 64; + unsigned texture_max_size_px = 256; // Zero means it is calculated in init function ImVec2 minimal_window_size = ImVec2(0, 0); @@ -385,6 +385,12 @@ IconManager::Icons init_icons(IconManager &mng, const GuiCfg &cfg) IconManager::InitTypes init_types{ {"undo.svg", size, IconManager::RasterType::white_only_data}, // undo {"undo.svg", size, IconManager::RasterType::color}, // undo_hovered + {"refresh.svg", size, IconManager::RasterType::white_only_data}, // refresh + {"refresh.svg", size, IconManager::RasterType::color}, // refresh_hovered + {"open.svg", size, IconManager::RasterType::white_only_data}, // changhe_file + {"open.svg", size, IconManager::RasterType::color}, // changhe_file_hovered + {"burn.svg", size, IconManager::RasterType::white_only_data}, // bake_file + {"burn.svg", size, IconManager::RasterType::color}, // bake_hovered {"lock_closed.svg", size, IconManager::RasterType::white_only_data}, // lock {"lock_open_f.svg", size, IconManager::RasterType::white_only_data}, // lock_hovered {"lock_open.svg", size, IconManager::RasterType::white_only_data}, // unlock @@ -554,7 +560,7 @@ bool init_texture(Texture &texture, const ModelVolume &mv, unsigned max_size_px) return false; const EmbossShape &es = *mv.emboss_shape; - const std::string &filepath = es.svg_file_path; + const std::string &filepath = es.svg_file.path; if (filepath.empty()) return false; @@ -566,7 +572,7 @@ bool init_texture(Texture &texture, const ModelVolume &mv, unsigned max_size_px) ScopeGuard sg_image([image]() { nsvgDelete(image); }); // NOTE: Can not use es.shape --> it is aligned and one need offset in svg - ExPolygons shape = to_expolygons(image); + ExPolygons shape = to_expolygons(*image); if (shape.empty()) return false; @@ -793,14 +799,10 @@ bool GLGizmoSVG::draw_preview(){ if (m_filename_preview.empty()){ // create filename preview - m_filename_preview = get_file_name(m_volume->emboss_shape->svg_file_path); + m_filename_preview = get_file_name(m_volume->emboss_shape->svg_file.path); m_filename_preview = ImGuiWrapper::trunc(m_filename_preview, m_gui_cfg->input_width); } - ImGui::SameLine(); - ImGui::BeginGroup(); - ScopeGuard sg_group([]() { ImGui::EndGroup(); }); - // Remove space between filename and gray suffix ".svg" ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); @@ -812,19 +814,19 @@ bool GLGizmoSVG::draw_preview(){ is_hovered |= ImGui::IsItemHovered(); if (is_hovered) { - std::string tooltip = GUI::format(_L("SVG file path is \"%1%\" "), m_volume->emboss_shape->svg_file_path); + std::string tooltip = GUI::format(_L("SVG file path is \"%1%\" "), m_volume->emboss_shape->svg_file.path); ImGui::SetTooltip("%s", tooltip.c_str()); } // Re-Load button - bool can_reload = !m_volume_shape.svg_file_path.empty(); + bool can_reload = !m_volume_shape.svg_file.path.empty(); if (can_reload) { ImGui::SameLine(); - if (clickable(get_icon(m_icons, IconType::reset_value), get_icon(m_icons, IconType::reset_value_hover))) { - if (!boost::filesystem::exists(m_volume_shape.svg_file_path)) { - m_volume_shape.svg_file_path.clear(); + if (clickable(get_icon(m_icons, IconType::refresh), get_icon(m_icons, IconType::refresh_hover))) { + if (!boost::filesystem::exists(m_volume_shape.svg_file.path)) { + m_volume_shape.svg_file.path.clear(); } else { - m_volume_shape.shapes_with_ids = select_shape(m_volume_shape.svg_file_path).shapes_with_ids; + m_volume_shape.shapes_with_ids = select_shape(m_volume_shape.svg_file.path).shapes_with_ids; init_texture(m_texture, *m_volume, m_gui_cfg->texture_max_size_px); process(); } @@ -832,19 +834,22 @@ bool GLGizmoSVG::draw_preview(){ ImGui::SetTooltip("%s", _u8L("Re-load SVG file from disk.").c_str()); } - if (ImGui::Button(_u8L("Change file").c_str())) { + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::change_file), get_icon(m_icons, IconType::change_file_hover))) { m_volume_shape.shapes_with_ids = select_shape().shapes_with_ids; init_texture(m_texture, *m_volume, m_gui_cfg->texture_max_size_px); process(); + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Change to another .svg file").c_str()); } ImGui::SameLine(); - if (ImGui::Button(_u8L("Bake").c_str())) { + if (clickable(get_icon(m_icons, IconType::bake), get_icon(m_icons, IconType::bake_hover))) { m_volume->emboss_shape.reset(); close(); return false; } else if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", _u8L("Remove connection to source file to take care about copyright").c_str()); + ImGui::SetTooltip("%s", _u8L("Bake to uneditable part and save copyright of svg").c_str()); } return true; } @@ -1221,7 +1226,7 @@ std::string get_file_name(const std::string &file_path) std::string volume_name(const EmbossShape &shape) { - std::string file_name = get_file_name(shape.svg_file_path); + std::string file_name = get_file_name(shape.svg_file.path); if (!file_name.empty()) return file_name; return "SVG shape"; @@ -1244,7 +1249,7 @@ GuiCfg create_gui_configuration() { float space = line_height_with_spacing - line_height; - cfg.icon_width = std::floor(line_height/8)*8; + cfg.icon_width = std::max(std::round(line_height/8)*8, 8.f); GuiCfg::Translations &tr = cfg.translations; @@ -1271,7 +1276,7 @@ GuiCfg create_gui_configuration() { ImVec2 letter_m_size = ImGui::CalcTextSize("M"); const float count_letter_M_in_input = 12.f; cfg.input_width = letter_m_size.x * count_letter_M_in_input; - + cfg.texture_max_size_px = std::round((cfg.input_width + cfg.input_offset + cfg.icon_width +space)/8) * 8; return cfg; } @@ -1299,8 +1304,19 @@ std::string choose_svg_file() if (input_files.size() != 1) BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result contain multiple files but only first is used."; - auto &input_file = input_files.front(); - std::string path = std::string(input_file.c_str()); + auto &input_file = input_files.front(); + std::string path = std::string(input_file.c_str()); + + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return invalid path."; + return {}; + } + + if (!boost::algorithm::iends_with(path, ".svg")) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return path without '.svg' tail"; + return {}; + } + return path; } @@ -1309,32 +1325,6 @@ void translate(ExPolygons &expolys, const Point &p) { expoly.translate(p); } -NSVGimage *parse_from_file(const char *filepath){ - const char *unit_mm{"mm"}; - // common used DPI is 96 or 72 - float dpi = 96.0f; - return nsvgParseFromFile(filepath, unit_mm, dpi); -} - -ExPolygons default_shape() -{ - std::string file = Slic3r::resources_dir() + "/icons/question.svg"; - assert(boost::filesystem::exists(file)); - NSVGimage *image = parse_from_file(file.c_str()); - assert(image != nullptr); - - // tesselation tolerance - float tol = 1e-2f; - int max_level = 10; - float scale = static_cast(2. / DEFAULT_SCALE); - bool is_y_negative = true; - ExPolygons shape = to_expolygons(image, tol, max_level, scale, is_y_negative); - assert(!shape.empty()); - - nsvgDelete(image); - return shape; -} - EmbossShape select_shape(std::string_view filepath) { EmbossShape shape; @@ -1342,32 +1332,42 @@ EmbossShape select_shape(std::string_view filepath) shape.projection.use_surface = false; if (filepath.empty()) { - shape.svg_file_path = choose_svg_file(); - if (shape.svg_file_path.empty()) - return {}; + // When empty open file dialog + shape.svg_file.path = choose_svg_file(); + if (shape.svg_file.path.empty()) + return {}; // file was not selected } else { - shape.svg_file_path = filepath; // copy + shape.svg_file.path = filepath; // copy + } + + if (!boost::filesystem::exists(shape.svg_file.path)) { + show_error(nullptr, GUI::format(_u8L("File(%1%) does NOT exists."), shape.svg_file.path)); + return {}; } - if (!boost::filesystem::exists(shape.svg_file_path) || - !boost::algorithm::iends_with(shape.svg_file_path, ".svg")) + if (!boost::algorithm::iends_with(shape.svg_file.path, ".svg")){ + show_error(nullptr, GUI::format(_u8L("File has to end with \".svg\" but you select: %1%"), shape.svg_file.path)); return {}; + } - NSVGimage *image = parse_from_file(shape.svg_file_path.c_str()); - if (image == nullptr) return {}; - ScopeGuard sg([image]() { nsvgDelete(image); }); + shape.svg_file.image = nsvgParseFromFile(shape.svg_file.path); + if (shape.svg_file.image.get() == nullptr) { + show_error(nullptr, GUI::format(_u8L("Nano SVG parser can't load from file(%1%)."), shape.svg_file.path)); + return {}; + } shape.scale = DEFAULT_SCALE; // loaded in mm - constexpr float tesselation_tolerance = 1e-2f; int max_level = 10; float scale = static_cast(1 / shape.scale); bool is_y_negative = true; - ExPolygons expoly = to_expolygons(image, tesselation_tolerance, max_level, scale, is_y_negative); + ExPolygons expoly = to_expolygons(*shape.svg_file.image, tesselation_tolerance, max_level, scale, is_y_negative); // Must contain some shapes !!! - if (expoly.empty()) - expoly = default_shape(); + if (expoly.empty()) { + show_error(nullptr, GUI::format(_u8L("SVG file(%1%) do NOT contain path to be able embossed."), shape.svg_file.path)); + return {}; + } // SVG is used as centered // Do not disturb user by settings of pivot position diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp index 22566fe8ef..0c599b0ebf 100644 --- a/tests/libslic3r/test_emboss.cpp +++ b/tests/libslic3r/test_emboss.cpp @@ -244,7 +244,7 @@ void scale(Polygons &polygons, double multiplicator) { Polygons load_polygons(const std::string &svg_file) { std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + svg_file; NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f); - Polygons polygons = to_polygons(image); + Polygons polygons = to_polygons(*image); nsvgDelete(image); return polygons; } @@ -289,7 +289,7 @@ TEST_CASE("Heal of points close to line", "[Emboss]") std::string file_name = "points_close_to_line.svg"; std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + file_name; NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f); - Polygons polygons = to_polygons(image); + Polygons polygons = to_polygons(*image); nsvgDelete(image); REQUIRE(polygons.size() == 1); Polygon polygon = polygons.front(); @@ -536,7 +536,7 @@ TEST_CASE("UndoRedo EmbossShape serialization", "[Emboss]") emboss.projection.depth = 5.; emboss.projection.use_surface = true; emboss.fix_3mf_tr = Transform3d::Identity(); - emboss.svg_file_path = "Everything starts somewhere, though many physicists disagree.\ + emboss.svg_file.path = "Everything starts somewhere, though many physicists disagree.\ But people have always been dimly aware of the problem with the start of things.\ They wonder how the snowplough driver gets to work,\ or how the makers of dictionaries look up the spelling of words.";