Fix Healing of SVG shape for overlapped contour points in thin "bay".

SVG from @LukasMatena
This commit is contained in:
Filip Sykala - NTB T15p 2023-10-11 15:01:29 +02:00
parent df256cc2bd
commit 70460ae790
7 changed files with 161 additions and 62 deletions

View File

@ -92,17 +92,17 @@ struct SpikeDesc
/// </summary>
/// <param name="bevel_size">Size of spike width after cut of the tip, has to be grater than 2.5</param>
/// <param name="pixel_spike_length">When spike has same or more pixels with width less than 1 pixel</param>
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<Point> 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<int>(); });
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;
}
/// <summary>
/// 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
/// </summary>
/// <param name="holes">Holes which was substracted from shape previous</param>
/// <param name="duplicates">Current duplicates in shape</param>
/// <param name="intersections">Current intersections in shape</param>
/// <param name="shape">Partialy healed shape[could be modified]</param>
/// <returns>True when modify shape otherwise False</returns>
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<Glyph> 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;
}

View File

@ -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
/// </summary>
/// <param name="precision">Define wanted precision of shape after heal</param>
/// <param name="is_non_zero">Fill type ClipperLib::pftNonZero for overlapping otherwise </param>
/// <returns>Healed shapes</returns>
ExPolygons heal_shape(const Polygons &shape);
ExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true);
/// <summary>
/// NOTE: call Slic3r::union_ex before this call
@ -182,7 +182,7 @@ namespace Emboss
/// <param name="max_iteration">Heal could create another issue,
/// After healing it is checked again until shape is good or maximal count of iteration</param>
/// <returns>True when shapes is good otherwise False</returns>
bool heal_shape(ExPolygons &shape, unsigned max_iteration = 10);
bool heal_expolygons(ExPolygons &shape, unsigned max_iteration = 10);
/// <summary>
/// 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_

View File

@ -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

View File

@ -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;

View File

@ -792,7 +792,8 @@ bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread)
template<typename Fnc>
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

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg">
<path fill="#ed6b21" d="m 68.34,199.06 c 0,6.2 -5.04,11.24 -11.24,11.24 l 0.03507,-11.27073 z" />
<path fill="#808080" d="m 68.34,199.06 5.608583,1.97096 -14.648722,13.67 L 57.1,210.3 c 6.2,0 11.24,-5.04 11.24,-11.24 z" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -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";