diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index 50114fee0..e07e4f421 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -67,24 +67,189 @@ void visualize_heal(const std::string& svg_filepath, const ExPolygons &expolygon const Points pts_2x2({Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)}); const Points pts_3x3({Point(-1, -1), Point(1, -1), Point(1, 1), Point(-1, 1)}); -// Reduce number of Points in polygons with respect to flatness -// Flatness is area that could be modified -void simplify(Polygon &polygon, float flatness); -void simplify(Polygons &polygons, float flatness); +struct SpikeDesc +{ + // cosinus of max spike angle + double cos_angle; // speed up to skip acos + // Half of Wanted bevel size + double half_bevel; + + SpikeDesc(double bevel_size) { + // create min angle given by spike_length + // Use it as minimal height of 1 pixel base spike + double length = 6; // has to be smaller than count of heal iteration + double angle = 2. * atan2(length, .5); // [rad] + cos_angle = std::fabs(cos(angle)); + + // When remove spike this angle is set. + // Value must be grater than min_angle + half_bevel = bevel_size / 2; + } }; -void priv::simplify(Polygon &polygon, float flatness) { - polygon.douglas_peucker(flatness); +// Remove long sharp corners aka spikes +// by adding points to bevel tip of spikes - Not printable parts +// Try to not modify long sides of spike and add points on it's side +void remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc); +void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc); +void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc); +}; + +#include +void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) +{ + enum class Type { + add, // Move with point B on A-side and add new point on C-side + move, // Only move with point B + erase // left only points A and C without move + }; + struct SpikeHeal + { + Type type; + size_t index; + Point b; + Point add; + }; + using SpikeHeals = std::vector; + SpikeHeals heals; + + size_t count = polygon.size(); + if (count < 3) + return; + + const Point *ptr_a = &polygon[count - 2]; + const Point *ptr_b = &polygon[count - 1]; + for (const Point &c : polygon) { + const Point &a = *ptr_a; + const Point &b = *ptr_b; + ScopeGuard sg([&ptr_a, &ptr_b, &c]() { + // prepare for next loop + ptr_a = ptr_b; + ptr_b = &c; + }); + + // calc sides + Point ba = a - b; + Point bc = c - b; + + Vec2d ba_f = ba.cast(); + Vec2d bc_f = bc.cast(); + double dot_product = ba_f.dot(bc_f); + + // sqrt together after multiplication save one sqrt + double ba_size_sq = ba_f.squaredNorm(); + double bc_size_sq = bc_f.squaredNorm(); + double norm = sqrt(ba_size_sq * bc_size_sq); + double cos_angle = dot_product / norm; + + // small angle are around 1 --> cos(0) = 1 + if (cos_angle < spike_desc.cos_angle) + continue; + + SpikeHeal heal; + heal.index = &b - &polygon.points.front(); + + // has to be in range <-1, 1> + // Due to preccission of floating point number could be sligtly out of range + if (cos_angle > 1.) + cos_angle = 1.; + if (cos_angle < -1.) + cos_angle = -1.; + + // Current Spike angle + double angle = acos(cos_angle); + double wanted_size = spike_desc.half_bevel / cos(angle / 2.); + double wanted_size_sq = wanted_size * wanted_size; + + bool is_ba_short = ba_size_sq < wanted_size_sq; + bool is_bc_short = bc_size_sq < wanted_size_sq; + auto a_side = [&b, &ba_f, &ba_size_sq, &wanted_size]() { + Vec2d ab_norm = ba_f / sqrt(ba_size_sq); + return b + (wanted_size * ab_norm).cast(); + }; + auto c_side = [&b, &bc_f, &bc_size_sq, &wanted_size]() { + Vec2d bc_norm = bc_f / sqrt(bc_size_sq); + return b + (wanted_size * bc_norm).cast(); + }; + if (is_ba_short && is_bc_short) { + // remove short spike + heal.type = Type::erase; + } else if (is_ba_short){ + // move point B on C-side + heal.type = Type::move; + heal.b = c_side(); + } else if (is_bc_short) { + // move point B on A-side + heal.type = Type::move; + heal.b = a_side(); + } else { + // move point B on A-side and add point on C-side + heal.type = Type::add; + heal.b = a_side(); + heal.add = c_side(); + } + heals.push_back(heal); + } + + if (heals.empty()) + return; + + // sort index from high to low + if (heals.front().index == (count - 1)) + std::rotate(heals.begin(), heals.begin()+1, heals.end()); + std::reverse(heals.begin(), heals.end()); + + int extend = 0; + int curr_extend = 0; + for (const SpikeHeal &h : heals) + switch (h.type) { + case Type::add: + ++curr_extend; + if (extend < curr_extend) + extend = curr_extend; + break; + case Type::erase: + --curr_extend; + } + + Points &pts = polygon.points; + if (extend > 0) + pts.reserve(pts.size() + extend); + + for (const SpikeHeal &h : heals) { + switch (h.type) { + case Type::add: + pts[h.index] = h.b; + pts.insert(pts.begin() + h.index+1, h.add); + break; + case Type::erase: + pts.erase(pts.begin() + h.index); + break; + case Type::move: + pts[h.index] = h.b; + break; + default: break; + } + } } -void priv::simplify(Polygons &polygons, float flatness) { +void priv::remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) +{ for (Polygon &polygon : polygons) - simplify(polygon, flatness); - // simplification could ceate empty polygon + remove_spikes(polygon, spike_desc); remove_bad(polygons); } +void priv::remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) +{ + for (ExPolygon &expolygon : expolygons) { + remove_spikes(expolygon.contour, spike_desc); + remove_spikes(expolygon.holes, spike_desc); + } + remove_bad(expolygons); +} + bool priv::is_valid(const FontFile &font, unsigned int index) { if (font.data == nullptr) return false; if (font.data->empty()) return false; @@ -380,7 +545,8 @@ bool priv::remove_self_intersections(ExPolygons &shape, unsigned max_iteration) return false; } -ExPolygons Emboss::heal_shape(const Polygons &shape) { +ExPolygons Emboss::heal_shape(const Polygons &shape, double precision) +{ // 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 @@ -388,7 +554,15 @@ ExPolygons Emboss::heal_shape(const Polygons &shape) { const double clean_distance = 1.415; // little grater than sqrt(2) 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()); + + // use douglas peucker to reduce amount of used points + for (Polygon &polygon : polygons) + polygon.douglas_peucker(precision); + + priv::SpikeDesc spike(precision); + priv::remove_spikes(polygons, spike); + // Do not remove all duplicits but do it better way // Overlap all duplicit points by rectangle 3x3 Points duplicits = collect_duplicates(to_points(polygons)); @@ -405,6 +579,12 @@ ExPolygons Emboss::heal_shape(const Polygons &shape) { // 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); + + priv::remove_spikes(polygons, spike); + + double min_area = precision * precision; + priv::remove_small_islands(res, min_area); + heal_shape(res); return res; } @@ -438,6 +618,8 @@ bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) { if (shape.empty()) return true; + //priv::visualize_heal("C:/data/temp/heal_prev.svg", shape); + // create loop permanent memory Polygons holes; Points intersections; @@ -488,7 +670,7 @@ bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) intersections.clear(); } - // priv::visualize_heal("C:/data/temp/heal.svg", shape); + //priv::visualize_heal("C:/data/temp/heal.svg", shape); assert(false); shape = {priv::create_bounding_rect(shape)}; return false; @@ -622,18 +804,10 @@ std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicod // change outer cw to ccw and inner ccw to cw order std::reverse(pts.begin(), pts.end()); - - { // use douglas peucker to reduce amount of used points - pts.push_back(pts.front()); - pts = MultiPoint::_douglas_peucker(pts, flatness); - pts.pop_back(); - if (pts.size() < 3) - continue; - } glyph_polygons.emplace_back(pts); } if (!glyph_polygons.empty()) - glyph.shape = Emboss::heal_shape(glyph_polygons); + glyph.shape = Emboss::heal_shape(glyph_polygons, flatness / SHAPE_SCALE); return glyph; } diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index 9803534c0..76a066d4b 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -154,11 +154,13 @@ namespace Emboss ExPolygons text2shapes(FontFileWithCache &font, const char *text, const FontProp &font_prop, std::function was_canceled = nullptr); /// - /// Fix intersections and self intersections in polygons glyph shape + /// Fix duplicit points and self intersections in polygons. + /// Also try to reduce amount of points and remove useless polygon parts /// /// Input shape to heal + /// Define wanted precision of shape after heal /// Healed shapes - ExPolygons heal_shape(const Polygons &shape); + ExPolygons heal_shape(const Polygons &shape, double precision); /// /// NOTE: call Slic3r::union_ex before this call diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp index fb51fce5b..e3f3146b7 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_shape(polygons, 10); // Is default shape for unhealabled shape? bool is_default_shape =