From 6b0382ab776dd8abcaefa0103c855f16372f62c3 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 25 Mar 2023 22:05:03 +0100 Subject: WIP --- svg-flatten/include/geom2d.hpp | 43 ++++++++++++++++++++++++++++---- svg-flatten/include/gerbolyze.hpp | 3 ++- svg-flatten/src/main.cpp | 13 +++++++--- svg-flatten/src/svg_doc.cpp | 52 ++++++++++++++++++++++++++++----------- svg-flatten/src/svg_path.cpp | 21 +++------------- svg-flatten/src/svg_path.h | 2 +- 6 files changed, 92 insertions(+), 42 deletions(-) (limited to 'svg-flatten') diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp index 4fafd80..47cf3be 100644 --- a/svg-flatten/include/geom2d.hpp +++ b/svg-flatten/include/geom2d.hpp @@ -104,11 +104,26 @@ namespace gerbolyze { }; double doc2phys_dist(double dist_doc) { - return dist_doc * sqrt(xx*xx + xy * xy); + return dist_doc * sqrt(xx*xx + xy*xy); } double phys2doc_dist(double dist_doc) { - return dist_doc / sqrt(xx*xx + xy * xy); + return dist_doc / sqrt(xx*xx + xy*xy); + } + + double doc2phys_skew(double dist_doc) { + /* https://math.stackexchange.com/a/3521141 */ + /* xx yx x0 + * xy yy y0 */ + s_x = sqrt(); + } + + double doc2phys_min(double dist_doc) { + return dist_doc * fmin(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx)); + } + + double doc2phys_max(double dist_doc) { + return dist_doc * fmax(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx)); } d2p doc2phys(const d2p p) { @@ -141,13 +156,31 @@ namespace gerbolyze { } /* Transform given clipper paths */ - void transform_paths(ClipperLib::Paths &paths) { + void doc2phys_clipper(ClipperLib::Paths &paths) { + for (auto &p : paths) { + doc2phys_clipper(p); + } + } + + void doc2phys_clipper(ClipperLib::Path &path) { + std::transform(path.begin(), path.end(), path.begin(), + [this](ClipperLib::IntPoint p) -> ClipperLib::IntPoint { + d2p out(this->doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale})); + return { + (ClipperLib::cInt)round(out[0] * clipper_scale), + (ClipperLib::cInt)round(out[1] * clipper_scale) + }; + }); + } + + /* Transform given clipper paths */ + void phys2doc_clipper(ClipperLib::Paths &paths) { for (auto &p : paths) { - transform_clipper_path(p); + phys2doc_clipper(p); } } - void transform_clipper_path(ClipperLib::Path &path) { + void phys2doc_clipper(ClipperLib::Path &path) { std::transform(path.begin(), path.end(), path.begin(), [this](ClipperLib::IntPoint p) -> ClipperLib::IntPoint { d2p out(this->doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale})); diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index 8bd948a..f90b9bf 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -208,7 +208,8 @@ namespace gerbolyze { class RenderSettings { public: double m_minimum_feature_size_mm = 0.1; - double curve_tolerance_mm; + double geometric_tolerance_mm = 0.1; + double stroke_width_cutoff = 0.01; double drill_test_polsby_popper_tolerance = 0.01; double aperture_circle_test_tolerance = 0.01; double aperture_rect_test_tolerance = 0.01; diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 1fe3454..6ba32a9 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -82,8 +82,11 @@ int main(int argc, char **argv) { {"min_feature_size", {"-d", "--trace-space"}, "Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.", 1}, - {"curve_tolerance", {"-c", "--curve-tolerance"}, - "Tolerance for curve flattening in mm. Default: 0.1mm.", + {"geometric_tolerance", {"-t", "--tolerance"}, + "Tolerance in mm for geometric approximation such as curve flattening. Default: 0.1mm.", + 1}, + {"stroke_width_cutoff", {"--min-stroke-width"}, + "Don't render strokes thinner than the given width in mm. Default: 0.01mm.", 1}, {"drill_test_polsby_popper_tolerance", {"--drill-test-tolerance"}, "Tolerance for identifying circles as drills in outline mode", @@ -313,7 +316,8 @@ int main(int argc, char **argv) { delete vec; double min_feature_size = args["min_feature_size"].as(0.1); /* mm */ - double curve_tolerance = args["curve_tolerance"].as(0.1); /* mm */ + double geometric_tolerance = args["geometric_tolerance"].as(0.1); /* mm */ + double stroke_width_cutoff = args["stroke_width_cutoff"].as(0.01); /* mm */ double drill_test_polsby_popper_tolerance = args["drill_test_polsby_popper_tolerance"].as(0.1); double aperture_rect_test_tolerance = args["aperture_rect_test_tolerance"].as(0.1); double aperture_circle_test_tolerance = args["aperture_circle_test_tolerance"].as(0.1); @@ -450,7 +454,8 @@ int main(int argc, char **argv) { RenderSettings rset { min_feature_size, - curve_tolerance, + geometric_tolerance, + stroke_width_cutoff, drill_test_polsby_popper_tolerance, aperture_circle_test_tolerance, aperture_rect_test_tolerance, diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index 323a12b..df6e67a 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -159,7 +159,7 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm } else { clip_path = *lookup; - ctx.mat().transform_paths(clip_path); + ctx.mat().doc2phys_clipper(clip_path); } /* Clip against parent's clip path (both are now in document coordinates) */ @@ -225,6 +225,13 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm /* Export an SVG path element to gerber. Apply patterns and clip on the fly. */ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml_node &node) { + /* Important note on the document transform: + * + * We have to make sure that we dash & stroke (outline) the path *before* transforming into physical units because + * the transform may not be uniform, i.e. scale may depend on direction. As an example, imagine you stroke a 10 by + * 10mm square with an 1mm stroke, but there is a transform that scales by 1 in y-direction, and 2 in x-direction. + * In the output, the stroke is going to be 2mm wide on the left and right, and 1mm wide on the top/bottom. + */ enum gerber_color fill_color = gerber_fill_color(node, ctx.settings()); enum gerber_color stroke_color = gerber_stroke_color(node, ctx.settings()); //cerr << "path: resolved colors, stroke=" << stroke_color << ", fill=" << fill_color << endl; @@ -242,19 +249,22 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml return; } - /* Load path from SVG path data and transform into document units. */ - stroke_width = ctx.mat().doc2phys_dist(stroke_width); - + /* Load path from SVG path data */ Paths stroke_open, stroke_closed; PolyTree ptree_fill; PolyTree ptree; - load_svg_path(ctx.mat(), node, stroke_open, stroke_closed, ptree_fill, ctx.settings().curve_tolerance_mm); + double geometric_tolerance_px = ctx.mat().doc2phys_min(ctx.settings().geometric_tolerance_mm); + load_svg_path(node, stroke_open, stroke_closed, ptree_fill, geometric_tolerance_px); Paths fill_paths; PolyTreeToPaths(ptree_fill, fill_paths); + /* Since we do not need to stroke them, transform the fill paths to physical units now. For polsby-popper to work + * properly, they need to be transformed already. However, we leave the stroke paths un-transformed since they can + * only be transformed after outlining. */ + ctx.mat().doc2phys_clipper(fill_paths); bool has_fill = fill_color; - bool has_stroke = stroke_color && stroke_width > 0.0; + bool has_stroke = stroke_color && ctx.mat().doc2phys_min(stroke_width) > ctx.settings().stroke_width_cutoff; cerr << "processing svg path" << endl; cerr << " * " << fill_paths.size() << " fill paths" << endl; @@ -288,7 +298,6 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml centroid[0] /= clipper_scale; centroid[1] /= clipper_scale; double diameter = sqrt(4*fabs(area)/M_PI) / clipper_scale; - diameter = ctx.mat().doc2phys_dist(diameter); /* FIXME is this correct w.r.t. PolygonScaler? */ diameter = round(diameter * 1000.0) / 1000.0; /* Round to micrometer precsion; FIXME: make configurable */ ctx.sink() << ApertureToken(diameter) << FlashToken(centroid); } @@ -375,6 +384,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml auto open_copy(stroke_open); stroke_open.clear(); + /* FIXME do we handle really really long dashes correctly? */ for (auto &poly : stroke_closed) { poly.push_back(poly[0]); dash_path(poly, stroke_open, dasharray, stroke_dashoffset); @@ -387,7 +397,10 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml } } - if (stroke_color != GRB_PATTERN_FILL) { + if (stroke_color != GRB_PATTERN_FILL + && ctx.sink().can_do_apertures() + /* check if we have an uniform transform */ + && ctx.mat().doc2phys_skew(stroke_width) < ctx.settings().geometric_tolerance_mm) { // cerr << "Analyzing direct conversion of stroke" << endl; // cerr << " stroke_closed.size() = " << stroke_closed.size() << endl; // cerr << " stroke_open.size() = " << stroke_open.size() << endl; @@ -403,11 +416,15 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml Paths dilated_clip; ClosedPathsFromPolyTree(clip_ptree, dilated_clip); + Paths stroke_open_phys(stroke_open), stroke_closed_phys(stroke_closed); + ctx.mat().doc2phys_clipper(stroke_open_phys); + ctx.mat().doc2phys_clipper(stroke_closed_phys); + Clipper stroke_clip; stroke_clip.StrictlySimple(true); stroke_clip.AddPaths(dilated_clip, ptClip, /* closed */ true); - stroke_clip.AddPaths(stroke_closed, ptSubject, /* closed */ true); - stroke_clip.AddPaths(stroke_open, ptSubject, /* closed */ false); + stroke_clip.AddPaths(stroke_closed_phys, ptSubject, /* closed */ true); + stroke_clip.AddPaths(stroke_open_phys, ptSubject, /* closed */ false); stroke_clip.Execute(ctDifference, ptree, pftNonZero, pftNonZero); cerr << " > " << ptree.ChildCount() << " clipped stroke ptree top-level children" << endl; @@ -433,7 +450,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml // cerr << " ends_can_be_mapped = " << ends_can_be_mapped << endl; // cerr << " joins_can_be_mapped = " << joins_can_be_mapped << endl; /* Accept loss of precision in outline mode. */ - if (ctx.sink().can_do_apertures() && (ctx.settings().outline_mode || gerber_lossless )) { + if (ctx.settings().outline_mode || gerber_lossless) { // cerr << " -> converting directly" << endl; ctx.sink() << ApertureToken(stroke_width); for (auto &path : stroke_closed) { @@ -452,7 +469,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml } ClipperOffset offx; - offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */ + offx.ArcTolerance = ctx.mat().phys2doc_min(ctx.settings().geometric_tolerance_mm) * clipper_scale; offx.MiterLimit = stroke_miterlimit; //cerr << "offsetting " << stroke_closed.size() << " closed and " << stroke_open.size() << " open paths" << endl; @@ -468,9 +485,13 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml if (!ctx.clip().empty()) { Paths outline_paths; PolyTreeToPaths(ptree, outline_paths); + + Paths clip(ctx.clip()); + ctx.mat().phys2doc_clipper(clip); + Clipper stroke_clip; stroke_clip.StrictlySimple(true); - stroke_clip.AddPaths(ctx.clip(), ptClip, /* closed */ true); + stroke_clip.AddPaths(clip, ptClip, /* closed */ true); stroke_clip.AddPaths(outline_paths, ptSubject, /* closed */ true); /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ stroke_clip.Execute(ctIntersection, ptree, pftNonZero, pftNonZero); @@ -486,6 +507,8 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml } else { Paths clip; PolyTreeToPaths(ptree, clip); + ctx.mat().phys2doc_clipper(clip); + RenderContext local_ctx(ctx, xform2d(), clip, true); pattern->tile(local_ctx); } @@ -493,6 +516,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml } else { Paths s_polys; dehole_polytree(ptree, s_polys); + ctx.mat().doc2phys_clipper(s_polys); /* color has alredy been pushed above. */ ctx.sink() << ApertureToken() << s_polys; } @@ -566,7 +590,7 @@ void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) { xform2d child_xf(local_xf); child_xf.transform(xform2d(child.attribute("transform").value())); - load_svg_path(child_xf, child, _stroke_open, _stroke_closed, ptree_fill, rset.curve_tolerance_mm); + load_svg_path(child_xf, child, _stroke_open, _stroke_closed, ptree_fill, rset.geometric_tolerance_mm); Paths paths; PolyTreeToPaths(ptree_fill, paths); diff --git a/svg-flatten/src/svg_path.cpp b/svg-flatten/src/svg_path.cpp index 71c7bd2..e2ed370 100644 --- a/svg-flatten/src/svg_path.cpp +++ b/svg-flatten/src/svg_path.cpp @@ -28,7 +28,7 @@ using namespace std; -static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) { +static pair flatten_path(ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_px) { istringstream in(path_data); string cmd; @@ -63,14 +63,6 @@ static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths in >> a[0] >> a[1]; assert (!in.fail()); /* guaranteed by usvg */ - /* We need to transform all points ourselves here, and cannot use the transform feature of cairo_to_clipper: - * Our transform may contain offsets, and clipper only passes its data into cairo's transform functions - * after scaling up to its internal fixed-point ints, but it does not scale the transform accordingly. This - * means a scale/rotation we set before calling clipper works out fine, but translations get lost as they - * get scaled by something like 1e-6. - */ - a = mat.doc2phys(a); - in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(a[0]*clipper_scale), (ClipperLib::cInt)round(a[1]*clipper_scale) @@ -80,7 +72,6 @@ static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths in >> a[0] >> a[1]; assert (!in.fail()); /* guaranteed by usvg */ - a = mat.doc2phys(a); in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(a[0]*clipper_scale), (ClipperLib::cInt)round(a[1]*clipper_scale) @@ -93,11 +84,7 @@ static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths in >> d[0] >> d[1]; /* end point */ assert (!in.fail()); /* guaranteed by usvg */ - b = mat.doc2phys(b); - c = mat.doc2phys(c); - d = mat.doc2phys(d); - - gerbolyze::curve4_div c4div(distance_tolerance_mm); + gerbolyze::curve4_div c4div(distance_tolerance_px); c4div.run(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]); for (auto &pt : c4div.points()) { @@ -122,7 +109,7 @@ static pair flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths return {has_closed, num_subpaths > 1}; } -void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance) { +void gerbolyze::load_svg_path(const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double geometric_tolerance_px) { auto *path_data = node.attribute("d").value(); auto fill_rule = clipper_fill_rule(node); @@ -131,7 +118,7 @@ void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperL * open/closed properties for stroke offsetting. */ ClipperLib::Clipper c_fill; c_fill.StrictlySimple(true); - auto res = flatten_path(mat, stroke_open, stroke_closed, c_fill, path_data, curve_tolerance); + auto res = flatten_path(stroke_open, stroke_closed, c_fill, path_data, geometric_tolerance_px); bool has_closed = res.first, has_multiple = res.second; if (!has_closed && !has_multiple) { diff --git a/svg-flatten/src/svg_path.h b/svg-flatten/src/svg_path.h index c0b2d88..4e62a1b 100644 --- a/svg-flatten/src/svg_path.h +++ b/svg-flatten/src/svg_path.h @@ -23,7 +23,7 @@ #include "geom2d.hpp" namespace gerbolyze { -void load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance); +void load_svg_path(const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double geometric_tolerance_px); void parse_dasharray(const pugi::xml_node &node, std::vector &out); void dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const std::vector dasharray, double dash_offset=0.0); } -- cgit