#include "NSVGUtils.hpp" #include #include // to_chars #include #include #include "ClipperUtils.hpp" #include "Emboss.hpp" // heal for shape namespace { using namespace Slic3r; // Polygon // see function nsvg__lineTo(NSVGparser* p, float x, float y) bool is_line(const float *p, float precision = 1e-4f); // convert curve in path to lines struct LinesPath{ Polygons polygons; Polylines polylines; }; LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m); HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); } // namespace namespace Slic3r { ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m) { ExPolygonsWithIds result; size_t shape_id = 0; for (NSVGshape *shape_ptr = image.shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next, ++shape_id) { const NSVGshape &shape = *shape_ptr; if (!(shape.flags & NSVG_FLAGS_VISIBLE)) continue; bool is_fill_used = shape.fill.type != NSVG_PAINT_NONE; bool is_stroke_used = shape.stroke.type != NSVG_PAINT_NONE && shape.strokeWidth > 1e-5f; if (!is_fill_used && !is_stroke_used) continue; const LinesPath lines_path = linearize_path(shape.paths, param); if (is_fill_used) { unsigned unique_id = static_cast(2 * shape_id); HealedExPolygons expoly = fill_to_expolygons(lines_path, shape, param); result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); } if (is_stroke_used) { unsigned unique_id = static_cast(2 * shape_id + 1); HealedExPolygons expoly = stroke_to_expolygons(lines_path, shape, param); result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); } } // SVG is used as centered // Do not disturb user by settings of pivot position center(result); return result; } Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m) { Polygons result; for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; if (shape->fill.type == NSVG_PAINT_NONE) continue; const LinesPath lines_path = linearize_path(shape->paths, param); polygons_append(result, lines_path.polygons); // close polyline to create polygon polygons_append(result, to_polygons(lines_path.polylines)); } return result; } void bounds(const NSVGimage &image, Vec2f& min, Vec2f &max) { for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { if (min.x() > path->bounds[0]) min.x() = path->bounds[0]; if (min.y() > path->bounds[1]) min.y() = path->bounds[1]; if (max.x() < path->bounds[2]) max.x() = path->bounds[2]; if (max.y() < path->bounds[3]) max.y() = path->bounds[3]; } } NSVGimage_ptr nsvgParseFromFile(const std::string &filename, const char *units, float dpi) { NSVGimage *image = ::nsvgParseFromFile(filename.c_str(), units, dpi); return {image, &nsvgDelete}; } std::unique_ptr read_from_disk(const std::string &path) { boost::nowide::ifstream fs{path}; if (!fs.is_open()) return nullptr; std::stringstream ss; ss << fs.rdbuf(); return std::make_unique(ss.str()); } NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units, float dpi){ // NOTE: nsvg parser consume data from input(char *) size_t size = file_data.size(); // file data could be big, so it is allocated on heap std::unique_ptr data_copy(new char[size+1]); memcpy(data_copy.get(), file_data.c_str(), size); data_copy[size] = '\0'; // data for nsvg must be null terminated NSVGimage *image = ::nsvgParse(data_copy.get(), units, dpi); return {image, &nsvgDelete}; } NSVGimage *init_image(EmbossShape::SvgFile &svg_file){ // is already initialized? if (svg_file.image.get() != nullptr) return svg_file.image.get(); if (svg_file.file_data == nullptr) { // chech if path is known if (svg_file.path.empty()) return nullptr; svg_file.file_data = read_from_disk(svg_file.path); if (svg_file.file_data == nullptr) return nullptr; } // init svg image svg_file.image = nsvgParse(*svg_file.file_data); if (svg_file.image.get() == NULL) return nullptr; return svg_file.image.get(); } size_t get_shapes_count(const NSVGimage &image) { size_t count = 0; for (NSVGshape * s = image.shapes; s != NULL; s = s->next) ++count; return count; } //void save(const NSVGimage &image, std::ostream &data) //{ // data << ""; // // // tl .. top left // Vec2f tl(std::numeric_limits::max(), std::numeric_limits::max()); // // br .. bottom right // Vec2f br(std::numeric_limits::min(), std::numeric_limits::min()); // bounds(image, tl, br); // // tl.x() = std::floor(tl.x()); // tl.y() = std::floor(tl.y()); // // br.x() = std::ceil(br.x()); // br.y() = std::ceil(br.y()); // Vec2f s = br - tl; // Point size = s.cast(); // // data << "\n"; // data << "\n"; // // std::array buffer; // auto write_point = [&tl, &buffer](std::string &d, const float *p) { // float x = p[0] - tl.x(); // float y = p[1] - tl.y(); // auto to_string = [&buffer](float f) -> std::string { // auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), f); // if (ec != std::errc{}) // return "0"; // return std::string(buffer.data(), ptr); // }; // d += to_string(x) + "," + to_string(y) + " "; // }; // // for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { // enum struct Type { move, line, curve, close }; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d // Type type = Type::move; // std::string d = "M "; // move on start point // for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { // if (path->npts <= 1) // continue; // // if (type == Type::close) { // type = Type::move; // // NOTE: After close must be a space // d += " M "; // move on start point // } // write_point(d, path->pts); // size_t path_size = static_cast(path->npts - 1); // // if (path->closed) { // // Do not use last point in path it is duplicit // if (path->npts <= 4) // continue; // path_size = static_cast(path->npts - 4); // } // // for (size_t i = 0; i < path_size; i += 3) { // const float *p = &path->pts[i * 2]; // if (!::is_line(p)) { // if (type != Type::curve) { // type = Type::curve; // d += "C "; // start sequence of triplets defining curves // } // write_point(d, &p[2]); // write_point(d, &p[4]); // } else { // // if (type != Type::line) { // type = Type::line; // d += "L "; // start sequence of line points // } // } // write_point(d, &p[6]); // } // if (path->closed) { // type = Type::close; // d += "Z"; // start sequence of line points // } // } // if (type != Type::close) { // //type = Type::close; // d += "Z"; // closed path // } // data << "\n"; // } // data << "\n"; //} // //bool save(const NSVGimage &image, const std::string &svg_file_path) //{ // std::ofstream file{svg_file_path}; // if (!file.is_open()) // return false; // save(image, file); // return true; //} } // namespace Slic3r namespace { using namespace Slic3r; // Polygon + Vec2f Point::coord_type to_coor(float val, double scale) { return static_cast(std::round(val * scale)); } bool need_flattening(float tessTol, const Vec2f &p1, const Vec2f &p2, const Vec2f &p3, const Vec2f &p4) { // f .. first // s .. second auto det = [](const Vec2f &f, const Vec2f &s) { return std::fabs(f.x() * s.y() - f.y() * s.x()); }; Vec2f pd = (p4 - p1); Vec2f pd2 = (p2 - p4); float d2 = det(pd2, pd); Vec2f pd3 = (p3 - p4); float d3 = det(pd3, pd); float d23 = d2 + d3; return (d23 * d23) >= tessTol * pd.squaredNorm(); } // see function nsvg__lineTo(NSVGparser* p, float x, float y) bool is_line(const float *p, float precision){ //Vec2f p1(p[0], p[1]); //Vec2f p2(p[2], p[3]); //Vec2f p3(p[4], p[5]); //Vec2f p4(p[6], p[7]); float dx_3 = (p[6] - p[0]) / 3.f; float dy_3 = (p[7] - p[1]) / 3.f; return is_approx(p[2], p[0] + dx_3, precision) && is_approx(p[4], p[6] - dx_3, precision) && is_approx(p[3], p[1] + dy_3, precision) && is_approx(p[5], p[7] - dy_3, precision); } /// /// Convert cubic curve to lines /// Inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez /// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 /// /// Result points /// Tesselation tolerance /// Curve point /// Curve point /// Curve point /// Curve point /// Actual depth of recursion void flatten_cubic_bez(Points &points, float tessTol, const Vec2f& p1, const Vec2f& p2, const Vec2f& p3, const Vec2f& p4, int level) { if (!need_flattening(tessTol, p1, p2, p3, p4)) { Point::coord_type x = static_cast(std::round(p4.x())); Point::coord_type y = static_cast(std::round(p4.y())); points.emplace_back(x, y); return; } --level; if (level == 0) return; Vec2f p12 = (p1 + p2) * 0.5f; Vec2f p23 = (p2 + p3) * 0.5f; Vec2f p34 = (p3 + p4) * 0.5f; Vec2f p123 = (p12 + p23) * 0.5f; Vec2f p234 = (p23 + p34) * 0.5f; Vec2f p1234 = (p123 + p234) * 0.5f; flatten_cubic_bez(points, tessTol, p1, p12, p123, p1234, level); flatten_cubic_bez(points, tessTol, p1234, p234, p34, p4, level); } LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m) { LinesPath result; Polygons &polygons = result.polygons; Polylines &polylines = result.polylines; // multiple use of allocated memmory for points between paths Points points; for (NSVGpath *path = first_path; path != NULL; path = path->next) { // Flatten path Point::coord_type x = to_coor(path->pts[0], param.scale); Point::coord_type y = to_coor(path->pts[1], param.scale); points.emplace_back(x, y); size_t path_size = (path->npts > 1) ? static_cast(path->npts - 1) : 0; for (size_t i = 0; i < path_size; i += 3) { const float *p = &path->pts[i * 2]; if (is_line(p)) { // point p4 Point::coord_type xx = to_coor(p[6], param.scale); Point::coord_type yy = to_coor(p[7], param.scale); points.emplace_back(xx, yy); continue; } Vec2f p1(p[0], p[1]); Vec2f p2(p[2], p[3]); Vec2f p3(p[4], p[5]); Vec2f p4(p[6], p[7]); flatten_cubic_bez(points, param.tesselation_tolerance, p1 * param.scale, p2 * param.scale, p3 * param.scale, p4 * param.scale, param.max_level); } assert(!points.empty()); if (points.empty()) continue; if (param.is_y_negative) for (Point &p : points) p.y() = -p.y(); if (path->closed) { polygons.emplace_back(points); } else { polylines.emplace_back(points); } // prepare for new path - recycle alocated memory points.clear(); } remove_same_neighbor(polygons); remove_same_neighbor(polylines); return result; } HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { Polygons fill = lines_path.polygons; // copy // close polyline to create polygon polygons_append(fill, to_polygons(lines_path.polylines)); if (fill.empty()) return {}; // if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_NONZERO) bool is_non_zero = true; if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD) is_non_zero = false; return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration); } struct DashesParam{ // first dash length float dash_length = 1.f; // scaled // is current dash .. true // is current space .. false bool is_line = true; // current index to array unsigned char dash_index = 0; static constexpr size_t max_dash_array_size = 8; // limitation of nanosvg strokeDashArray std::array dash_array; // scaled unsigned char dash_count = 0; // count of values in array explicit DashesParam(const NSVGshape &shape, double scale) : dash_count(shape.strokeDashCount) { assert(dash_count > 0); assert(dash_count <= max_dash_array_size); // limitation of nanosvg strokeDashArray for (size_t i = 0; i < dash_count; ++i) dash_array[i] = static_cast(shape.strokeDashArray[i] * scale); // Figure out dash offset. float all_dash_length = 0; for (unsigned char j = 0; j < dash_count; ++j) all_dash_length += dash_array[j]; if (dash_count%2 == 1) // (shape.strokeDashCount & 1) all_dash_length *= 2.0f; // Find location inside pattern float dash_offset = fmodf(static_cast(shape.strokeDashOffset * scale), all_dash_length); if (dash_offset < 0.0f) dash_offset += all_dash_length; while (dash_offset > dash_array[dash_index]) { dash_offset -= dash_array[dash_index]; dash_index = (dash_index + 1) % shape.strokeDashCount; is_line = !is_line; } dash_length = dash_array[dash_index] - dash_offset; } }; Polylines to_dashes(const Polyline &polyline, const DashesParam& param) { Polylines dashes; Polyline dash; // cache for one dash in dashed line Point prev_point; bool is_line = param.is_line; unsigned char dash_index = param.dash_index; float dash_length = param.dash_length; // current rest of dash distance for (const Point &point : polyline.points) { if (&point == &polyline.points.front()) { // is first point prev_point = point; // copy continue; } Point diff = point - prev_point; float line_segment_length = diff.cast().norm(); while (dash_length < line_segment_length) { // Calculate intermediate point float d = dash_length / line_segment_length; Point move_point = diff * d; Point intermediate = prev_point + move_point; // add Dash in stroke if (is_line) { if (dash.empty()) { dashes.emplace_back(Points{prev_point, intermediate}); } else { dash.append(prev_point); dash.append(intermediate); dashes.push_back(dash); dash.clear(); } } diff -= move_point; line_segment_length -= dash_length; prev_point = intermediate; // Advance dash pattern is_line = !is_line; dash_index = (dash_index + 1) % param.dash_count; dash_length = param.dash_array[dash_index]; } if (is_line) dash.append(prev_point); dash_length -= line_segment_length; prev_point = point; // copy } // add last dash if (is_line){ assert(!dash.empty()); dash.append(prev_point); // prev_point == polyline.points.back() dashes.push_back(dash); } return dashes; } HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { // convert stroke to polygon ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare; switch (static_cast(shape.strokeLineJoin)) { case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = ClipperLib::JoinType::jtSquare; break; case NSVGlineJoin::NSVG_JOIN_MITER: join_type = ClipperLib::JoinType::jtMiter; break; case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = ClipperLib::JoinType::jtRound; break; } double mitter = shape.miterLimit * param.scale; if (join_type == ClipperLib::JoinType::jtRound) { // mitter is used as ArcTolerance // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm mitter = std::pow(param.tesselation_tolerance, 1/3.); } float stroke_width = static_cast(shape.strokeWidth * param.scale); ClipperLib::EndType end_type = ClipperLib::EndType::etOpenButt; switch (static_cast(shape.strokeLineCap)) { case NSVGlineCap::NSVG_CAP_BUTT: end_type = ClipperLib::EndType::etOpenButt; break; case NSVGlineCap::NSVG_CAP_ROUND: end_type = ClipperLib::EndType::etOpenRound; break; case NSVGlineCap::NSVG_CAP_SQUARE: end_type = ClipperLib::EndType::etOpenSquare; break; } Polygons result; if (shape.strokeDashCount > 0) { DashesParam params(shape, param.scale); Polylines dashes; for (const Polyline &polyline : lines_path.polylines) polylines_append(dashes, to_dashes(polyline, params)); for (const Polygon &polygon : lines_path.polygons) polylines_append(dashes, to_dashes(to_polyline(polygon), params)); result = offset(dashes, stroke_width / 2, join_type, mitter, end_type); } else { result = contour_to_polygons(lines_path.polygons, stroke_width, join_type, mitter); polygons_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); } bool is_non_zero = true; return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration); } } // namespace