diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index 7afccda67e..4a9f98f01e 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -92,17 +92,17 @@ struct SpikeDesc /// /// Size of spike width after cut of the tip, has to be grater than 2.5 /// When spike has same or more pixels with width less than 1 pixel - SpikeDesc(double bevel_size, double pixel_spike_length = 6) - { + SpikeDesc(double bevel_size, double pixel_spike_length = 6): // create min angle given by spike_length // Use it as minimal height of 1 pixel base spike - double angle = 2. * atan2(pixel_spike_length, .5); // [rad] - cos_angle = std::fabs(cos(angle)); + cos_angle(std::fabs(std::cos( + /*angle*/ 2. * std::atan2(pixel_spike_length, .5) + ))), // When remove spike this angle is set. // Value must be grater than min_angle - half_bevel = bevel_size / 2; - } + half_bevel(bevel_size / 2) + {} }; // return TRUE when remove point. It could create polygon with 2 points. @@ -120,6 +120,8 @@ void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc); }; +// spike ... very sharp corner - when not removed cause iteration of heal process +// index ... index of duplicit point in polygon bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { std::optional add; @@ -205,7 +207,8 @@ bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &sp } void priv::remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { - + if (duplicates.empty()) + return; auto check = [](Polygon &polygon, const Point &d) -> bool { double spike_bevel = 1 / SHAPE_SCALE; double spike_length = 5.; @@ -486,16 +489,20 @@ bool priv::remove_self_intersections(ExPolygons &shape, unsigned max_iteration) return false; } -ExPolygons Emboss::heal_shape(const Polygons &shape) +ExPolygons Emboss::heal_polygons(const Polygons &shape, bool is_non_zero) { + const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::PolyFillType fill_type = is_non_zero ? + ClipperLib::pftNonZero : ClipperLib::pftEvenOdd; + // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work // fix of self intersections // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm - ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), ClipperLib::pftNonZero); - const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), fill_type); ClipperLib::CleanPolygons(paths, clean_distance); Polygons polygons = to_polygons(paths); - polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), + [](const Polygon &p) { return p.size() < 3; }), polygons.end()); // Do not remove all duplicates but do it better way // Overlap all duplicit points by rectangle 3x3 @@ -508,12 +515,11 @@ ExPolygons Emboss::heal_shape(const Polygons &shape) polygons.push_back(rect_3x3); } } + ExPolygons res = Slic3r::union_ex(polygons, fill_type); - // TrueTypeFonts use non zero winding number - // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html - ExPolygons res = Slic3r::union_ex(polygons, ClipperLib::pftNonZero); - heal_shape(res); + + const unsigned int max_iteration = 10; + bool is_healed = heal_expolygons(res, max_iteration); return res; } @@ -527,52 +533,105 @@ void priv::visualize_heal(const std::string &svg_filepath, const ExPolygons &exp svg.draw(expolygons); Points duplicits = collect_duplicates(pts); - svg.draw(duplicits, "black", 7 / SHAPE_SCALE); + int black_size = std::max(bb.size().x(), bb.size().y()) / 20; + svg.draw(duplicits, "black", black_size); Pointfs intersections_f = intersection_points(expolygons); Points intersections; intersections.reserve(intersections_f.size()); std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), [](const Vec2d &p) { return p.cast(); }); - svg.draw(intersections, "red", 8 / SHAPE_SCALE); + svg.draw(intersections, "red", black_size * 1.2); } -bool Emboss::heal_shape(ExPolygons &shape, unsigned max_iteration) +bool Emboss::heal_expolygons(ExPolygons &shape, unsigned max_iteration) { return priv::heal_dupl_inter(shape, max_iteration); } #ifndef HEAL_WITH_CLOSING +namespace { + +Points get_unique_intersections(const ExPolygons &shape) +{ + Pointfs intersections_f = intersection_points(shape); + Points intersections; // result + if (intersections_f.empty()) + return intersections; + + // convert intersections into Points + intersections.reserve(intersections_f.size()); + std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), + [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); + + // intersections should be unique poits + std::sort(intersections.begin(), intersections.end()); + auto it = std::unique(intersections.begin(), intersections.end()); + intersections.erase(it, intersections.end()); + return intersections; +} + +Polygons get_holes_with_points(const Polygons &holes, const Points &points) +{ + Polygons result; + for (const Slic3r::Polygon &hole : holes) + for (const Point &p : points) + for (const Point &h : hole) + if (p == h) { + result.push_back(hole); + break; + } + return result; +} + +/// +/// Fill holes which create duplicits or intersections +/// When healing hole creates trouble in shape again try to heal by an union instead of diff_ex +/// +/// Holes which was substracted from shape previous +/// Current duplicates in shape +/// Current intersections in shape +/// Partialy healed shape[could be modified] +/// True when modify shape otherwise False +bool fill_trouble_holes(const Polygons &holes, const Points &duplicates, const Points &intersections, ExPolygons &shape) +{ + if (holes.empty()) + return false; + if (duplicates.empty() && intersections.empty()) + return false; + + Polygons fill = get_holes_with_points(holes, duplicates); + append(fill, get_holes_with_points(holes, intersections)); + if (fill.empty()) + return false; + + shape = union_ex(shape, fill); + return true; +} + +} // namespace + bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) { if (shape.empty()) return true; // create loop permanent memory Polygons holes; - Points intersections; while (--max_iteration) { remove_same_neighbor(shape); - Pointfs intersections_f = intersection_points(shape); - - // convert intersections into Points - assert(intersections.empty()); - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); - - // intersections should be unique poits - std::sort(intersections.begin(), intersections.end()); - auto it = std::unique(intersections.begin(), intersections.end()); - intersections.erase(it, intersections.end()); - Points duplicates = collect_duplicates(to_points(shape)); - // duplicates are already uniqua and sorted - + Points intersections = get_unique_intersections(shape); + // Check whether shape is already healed if (intersections.empty() && duplicates.empty()) return true; - assert(holes.empty()); + if (fill_trouble_holes(holes, duplicates, intersections, shape)) { + holes.clear(); + continue; + } + + holes.clear(); holes.reserve(intersections.size() + duplicates.size()); remove_spikes_in_duplicates(shape, duplicates); @@ -591,11 +650,8 @@ bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) holes.push_back(hole); } - shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); - - // prepare for next loop - holes.clear(); - intersections.clear(); + shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::No); + // ApplySafetyOffset::Yes is incompatible with function fill_trouble_holes } //priv::visualize_heal("C:/data/temp/heal.svg", shape); @@ -736,8 +792,12 @@ std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicod std::reverse(pts.begin(), pts.end()); glyph_polygons.emplace_back(pts); } - if (!glyph_polygons.empty()) - glyph.shape = Emboss::heal_shape(glyph_polygons); + if (!glyph_polygons.empty()) { + // TrueTypeFonts use non zero winding number + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + glyph.shape = Emboss::heal_polygons(glyph_polygons); + } return glyph; } @@ -1238,7 +1298,22 @@ ExPolygons Slic3r::union_ex(const ExPolygonsWithIds &shapes) expolygons_append(result, shape.expoly); } result = union_ex(result); - heal_shape(result); + bool is_healed = heal_expolygons(result); + return result; +} + +ExPolygons Slic3r::union_with_delta(const ExPolygonsWithIds &shapes, float delta) +{ + // unify to one expolygons + ExPolygons expolygons; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) + continue; + expolygons_append(expolygons, offset_ex(shape.expoly, delta)); + } + ExPolygons result = union_ex(expolygons); + result = offset_ex(result, -delta); + bool is_healed = heal_expolygons(result); return result; } diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index 870fa3183b..ebf3c508ec 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -166,9 +166,9 @@ namespace Emboss /// Fix duplicit points and self intersections in polygons. /// Also try to reduce amount of points and remove useless polygon parts /// - /// Define wanted precision of shape after heal + /// Fill type ClipperLib::pftNonZero for overlapping otherwise /// Healed shapes - ExPolygons heal_shape(const Polygons &shape); + ExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true); /// /// NOTE: call Slic3r::union_ex before this call @@ -182,7 +182,7 @@ namespace Emboss /// Heal could create another issue, /// After healing it is checked again until shape is good or maximal count of iteration /// True when shapes is good otherwise False - bool heal_shape(ExPolygons &shape, unsigned max_iteration = 10); + bool heal_expolygons(ExPolygons &shape, unsigned max_iteration = 10); /// /// Divide line segments in place near to point @@ -467,6 +467,8 @@ void translate(ExPolygonsWithIds &e, const Point &p); BoundingBox get_extents(const ExPolygonsWithIds &e); void center(ExPolygonsWithIds &e); ExPolygons union_ex(const ExPolygonsWithIds &shapes); - +// delta .. safe offset before union (use as boolean close) +// NOTE: remove unprintable spaces between neighbor curves (made by linearization of curve) +ExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta); } // namespace Slic3r #endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index 8a569ba4aa..688b42a1b4 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -53,11 +53,6 @@ ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLinePa } } - // heal shapes - if (param.max_heal_iteration > 0) - for (auto &[id, expoly] : result) - Slic3r::Emboss::heal_shape(expoly, param.max_heal_iteration); - // SVG is used as centered // Do not disturb user by settings of pivot position center(result); @@ -366,11 +361,12 @@ ExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shap if (fill.empty()) return {}; - ClipperLib::PolyFillType fill_type = ClipperLib::pftNonZero; // if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_NONZERO) + bool is_non_zero = true; if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD) - fill_type = ClipperLib::pftEvenOdd; - return union_ex(fill, fill_type); + is_non_zero = false; + + return Emboss::heal_polygons(fill, is_non_zero); } struct DashesParam{ @@ -518,7 +514,7 @@ ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &sh polygons_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); } - return union_ex(result, ClipperLib::pftNonZero); + return Emboss::heal_polygons(result); } } // namespace \ No newline at end of file diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp index 12cd2c3f59..3d1b69450d 100644 --- a/src/libslic3r/NSVGUtils.hpp +++ b/src/libslic3r/NSVGUtils.hpp @@ -32,9 +32,6 @@ struct NSVGLineParams // NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer --> Slic3r::Point double scale = 1. / SCALING_FACTOR; - // count of iteration to heal shape - unsigned max_heal_iteration = 10; - // Flag wether y is negative, when true than y coor is multiplied by -1 bool is_y_negative = true; diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index 36544c26f6..1d28e518ce 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -792,7 +792,8 @@ bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread) template ExPolygons create_shape(DataBase &input, Fnc was_canceled) { const EmbossShape &es = input.create_shape(); - return union_ex(es.shapes_with_ids); + float delta = 50.f; + return union_with_delta(es.shapes_with_ids, delta); } //#define STORE_SAMPLING diff --git a/tests/data/contour_neighbor.svg b/tests/data/contour_neighbor.svg new file mode 100644 index 0000000000..3f520ea1d2 --- /dev/null +++ b/tests/data/contour_neighbor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp index adff1b7a37..d487d1e9d9 100644 --- a/tests/libslic3r/test_emboss.cpp +++ b/tests/libslic3r/test_emboss.cpp @@ -203,7 +203,7 @@ ExPolygons heal_and_check(const Polygons &polygons) Points polygons_points = to_points(polygons); Points duplicits_prev = collect_duplicates(polygons_points); - ExPolygons shape = Emboss::heal_shape(polygons); + ExPolygons shape = Emboss::heal_polygons(polygons); // Is default shape for unhealabled shape? bool is_default_shape = @@ -286,6 +286,30 @@ TEST_CASE("Heal of 'm' in Allura_Script.ttf", "[Emboss]") auto a = heal_and_check(polygons); } +#include "libslic3r/NSVGUtils.hpp" +TEST_CASE("Heal of svg contour overlap", "[Emboss]") { + + std::string svg_file = "contour_neighbor.svg"; + auto image = nsvgParseFromFile(TEST_DATA_DIR PATH_SEPARATOR + svg_file, "mm"); + NSVGLineParams param(1e10); + ExPolygonsWithIds shapes = create_shape_with_ids(*image, param); + Polygons polygons; + for (ExPolygonsWithId &shape : shapes) + polygons.push_back(shape.expoly.front().contour); + auto a = heal_and_check(polygons); +} + +// Input contour is extracted from case above "contour_neighbor.svg" with trouble shooted scale +TEST_CASE("Heal of overlaping contour", "[Emboss]"){ + // Extracted from svg: + Points contour{{2228926, 1543620}, {745002, 2065101}, {745002, 2065094}, {744990, 2065094}, {684487, 1466338}, + {510999, 908378}, {236555, 403250}, {-126813, -37014}, {-567074, -400382}, {-1072201, -674822}, + {-567074, -400378}, {-126813, -37010}, {236555, 403250}, {510999, 908382}, {684487, 1466346}, + {744990, 2065105}, {-2219648, 2073234}, {-2228926, -908814}, {-1646879, -2073235}}; + ExPolygons shapes = {ExPolygon{contour}}; + CHECK(Emboss::heal_expolygons(shapes)); +} + TEST_CASE("Heal of points close to line", "[Emboss]") { std::string file_name = "points_close_to_line.svg";