From 602e51ca104381990e0bf9b90e0afc4bf8b86801 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 29 Mar 2023 15:45:14 +0200 Subject: svg_doc: Fix gerber mapping of strokes with skewed or non-uniform transforms --- svg-flatten/include/geom2d.hpp | 91 +++++++++++++++++++++++++++++++-------- svg-flatten/include/gerbolyze.hpp | 1 + svg-flatten/src/main.cpp | 7 ++- svg-flatten/src/svg_doc.cpp | 39 +++++++++++------ 4 files changed, 108 insertions(+), 30 deletions(-) diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp index 9650575..128da0f 100644 --- a/svg-flatten/include/geom2d.hpp +++ b/svg-flatten/include/geom2d.hpp @@ -99,6 +99,7 @@ namespace gerbolyze { yy = n_yy; x0 = n_x0; y0 = n_y0; + decomposed = false; return *this; }; @@ -111,48 +112,90 @@ namespace gerbolyze { return dist_doc / sqrt(xx*xx + xy*xy); } - double doc2phys_skew(double dist_doc) { + void decompose() { + /* FIXME unit tests, especially for degenerate cases! */ + if (decomposed) { + return; + } + /* https://math.stackexchange.com/a/3521141 */ /* https://stackoverflow.com/a/70381885 */ /* xx yx x0 * xy yy y0 */ - double s_x = sqrt(xx*xx + xy*xy); + s_x = sqrt(xx*xx + xy*xy); if (xx == 0 && xy == 0) { - return std::numeric_limits::infinity; + theta = 0; + } else { + theta = atan2(xy, xx); } - double theta = atan2(xy, xx); double f = (xx*yy - xy*yx); if (f == 0) { - return std::numeric_limits::infinity; + m = 0; + } else { + m = (xx*yx + yy*xy) / f; } - double m = (xx*yx + yy*xy) / f; - - double f = xx + m*xy; - double s_y = 0; - + f = xx + m*xy; if (f == 0) { f = m*xx - xy; if (f == 0) { - return std::numeric_limits::infinity; + s_y = 0; } s_y = yx*s_x / f; } else { s_y = yy*s_x / f; } - return s_x - s_y > + double b = sqrt(s_y*s_y + m*m); + f_min = fmin(s_x, b); + f_max = fmax(s_x, b); + + decomposed = true; + } + + bool doc2phys_skew_ok(double dist_doc, double rel_tol, double abs_tol) { + decompose(); + + if (f_min == 0) { + return false; + } + + double imbalance = f_max / f_min - 1.0; + //cerr << " * skew check: " << dbg_str(); + //cerr << " imbalance=" << imbalance << endl; + //cerr << " rel=" << (imbalance < rel_tol) << " abs=" << (imbalance*fabs(dist_doc) < abs_tol) << endl; + return imbalance < rel_tol && imbalance*fabs(dist_doc) < abs_tol; } double doc2phys_min(double dist_doc) { - return dist_doc * fmin(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx)); + decompose(); + return dist_doc * f_min; } double doc2phys_max(double dist_doc) { - return dist_doc * fmax(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx)); + decompose(); + return dist_doc * f_max; + } + + double phys2doc_min(double dist_doc) { + decompose(); + + if (f_min == 0) + return std::nan("9"); + + return dist_doc / f_min; + } + + double phys2doc_max(double dist_doc) { + decompose(); + + if (f_max == 0) + return std::nan("9"); + + return dist_doc / f_max; } d2p doc2phys(const d2p p) { @@ -181,6 +224,7 @@ namespace gerbolyze { if (success_out) *success_out = true; + return *this; } @@ -210,9 +254,17 @@ namespace gerbolyze { } void phys2doc_clipper(ClipperLib::Path &path) { + xform2d copy(*this); + bool inverted = false; + copy.invert(&inverted); + if (!inverted) { + path.clear(); + return; + } + 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})); + [this, ©](ClipperLib::IntPoint p) -> ClipperLib::IntPoint { + d2p out(copy.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) @@ -231,7 +283,9 @@ namespace gerbolyze { ostringstream os; os << "xform2d< " << setw(5); os << xx << ", " << xy << ", " << x0 << " / "; - os << yy << ", " << yx << ", " << y0; + os << yy << ", " << yx << ", " << y0 << " / "; + os << "θ=" << theta << ", m=" << m << " s=(" << s_x << ", " << s_y << " | "; + os << "f_min=" << f_min << ", f_max=" << f_max; os << " >"; return os.str(); } @@ -240,5 +294,8 @@ namespace gerbolyze { double xx, yx, xy, yy, x0, y0; + double theta, m, s_x, s_y; + double f_min, f_max; + bool decomposed = false; }; } diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index f90b9bf..7c54337 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -218,6 +218,7 @@ namespace gerbolyze { bool flip_color_interpretation = false; bool pattern_complete_tiles_only = false; bool use_apertures_for_patterns = false; + bool do_gerber_interpolation = true; }; class RenderContext { diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 6ba32a9..122b75b 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -88,6 +88,9 @@ int main(int argc, char **argv) { {"stroke_width_cutoff", {"--min-stroke-width"}, "Don't render strokes thinner than the given width in mm. Default: 0.01mm.", 1}, + {"no_stroke_interpolation", {"--no-stroke-interpolation"}, + "Always outline SVG strokes as regions instead of rendering them using Geber interpolation commands where possible.", + 0}, {"drill_test_polsby_popper_tolerance", {"--drill-test-tolerance"}, "Tolerance for identifying circles as drills in outline mode", 1}, @@ -316,7 +319,7 @@ int main(int argc, char **argv) { delete vec; double min_feature_size = args["min_feature_size"].as(0.1); /* mm */ - double geometric_tolerance = args["geometric_tolerance"].as(0.1); /* mm */ + double geometric_tolerance = args["geometric_tolerance"].as(0.01); /* 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); @@ -451,6 +454,7 @@ int main(int argc, char **argv) { bool flip_svg_colors = args["flip_svg_color_interpretation"]; bool pattern_complete_tiles_only = args["pattern_complete_tiles_only"]; bool use_apertures_for_patterns = args["use_apertures_for_patterns"]; + bool do_gerber_interpolation = !args["no_stroke_interpolation"]; RenderSettings rset { min_feature_size, @@ -464,6 +468,7 @@ int main(int argc, char **argv) { flip_svg_colors, pattern_complete_tiles_only, use_apertures_for_patterns, + do_gerber_interpolation, }; SVGDocument doc; diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index df6e67a..6cb6138 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -253,7 +253,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml Paths stroke_open, stroke_closed; PolyTree ptree_fill; PolyTree ptree; - double geometric_tolerance_px = ctx.mat().doc2phys_min(ctx.settings().geometric_tolerance_mm); + double geometric_tolerance_px = ctx.mat().phys2doc_min(ctx.settings().geometric_tolerance_mm); load_svg_path(node, stroke_open, stroke_closed, ptree_fill, geometric_tolerance_px); Paths fill_paths; @@ -267,6 +267,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml bool has_stroke = stroke_color && ctx.mat().doc2phys_min(stroke_width) > ctx.settings().stroke_width_cutoff; cerr << "processing svg path" << endl; + cerr << " * " << (has_stroke ? "has stroke" : "no stroke") << " / " << (has_fill ? "has fill" : "no fill") << endl; cerr << " * " << fill_paths.size() << " fill paths" << endl; cerr << " * " << stroke_closed.size() << " closed strokes" << endl; cerr << " * " << stroke_open.size() << " open strokes" << endl; @@ -298,7 +299,8 @@ 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 = round(diameter * 1000.0) / 1000.0; /* Round to micrometer precsion; FIXME: make configurable */ + double tolerance = ctx.settings().geometric_tolerance_mm / 2; + diameter = round(diameter/tolerance) * tolerance; ctx.sink() << ApertureToken(diameter) << FlashToken(centroid); } } @@ -315,10 +317,10 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml c.AddPaths(ctx.clip(), ptClip, /* closed */ true); c.StrictlySimple(true); - cerr << "clipping " << fill_paths.size() << " paths, got polytree with " << ptree_fill.ChildCount() << " top-level children" << endl; + //cerr << "clipping " << fill_paths.size() << " paths, got polytree with " << ptree_fill.ChildCount() << " top-level children" << endl; /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ c.Execute(ctIntersection, ptree_fill, pftNonZero, pftNonZero); - cerr << " > " << ptree_fill.ChildCount() << " clipped fill ptree top-level children" << endl; + //cerr << " > " << ptree_fill.ChildCount() << " clipped fill ptree top-level children" << endl; } /* Call out to pattern tiler for pattern fills. The path becomes the clip here. */ @@ -399,8 +401,9 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml if (stroke_color != GRB_PATTERN_FILL && ctx.sink().can_do_apertures() + && ctx.settings().do_gerber_interpolation /* check if we have an uniform transform */ - && ctx.mat().doc2phys_skew(stroke_width) < ctx.settings().geometric_tolerance_mm) { + && ctx.mat().doc2phys_skew_ok(stroke_width, 0.05, 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; @@ -411,7 +414,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml offx.MiterLimit = 10; offx.AddPaths(ctx.clip(), jtRound, etClosedPolygon); PolyTree clip_ptree; - offx.Execute(clip_ptree, -0.5 * stroke_width * clipper_scale); + offx.Execute(clip_ptree, -0.5 * ctx.mat().doc2phys_dist(stroke_width) * clipper_scale); Paths dilated_clip; ClosedPathsFromPolyTree(clip_ptree, dilated_clip); @@ -426,7 +429,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml 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; + // cerr << " > " << ptree.ChildCount() << " clipped stroke ptree top-level children" << endl; /* Did any part of the path clip the clip path (which defaults to the document border)? */ bool nothing_clipped = ptree.Total() == 0; @@ -445,14 +448,18 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml bool ends_can_be_mapped = (end_type == ClipperLib::etOpenRound) || (stroke_open.size() == 0); /* Can gerber losslessly express this path? */ bool gerber_lossless = nothing_clipped && ends_can_be_mapped && joins_can_be_mapped; + //cerr << " ends_can_be_mapped=" << ends_can_be_mapped << ", nothing_clipped=" << nothing_clipped << ", joins_can_be_mapped=" << joins_can_be_mapped << endl; // cerr << " nothing_clipped = " << nothing_clipped << endl; // 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.settings().outline_mode || gerber_lossless) { - // cerr << " -> converting directly" << endl; - ctx.sink() << ApertureToken(stroke_width); + //cerr << " -> converting directly" << endl; + ctx.mat().doc2phys_clipper(stroke_closed); + ctx.mat().doc2phys_clipper(stroke_open); + + ctx.sink() << ApertureToken(ctx.mat().doc2phys_dist(stroke_width)); for (auto &path : stroke_closed) { if (path.empty()) { continue; @@ -464,7 +471,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml ctx.sink() << stroke_open; return; } - // cerr << " -> NOT converting directly" << endl; + //cerr << " -> NOT converting directly" << endl; /* else fall through to normal processing */ } @@ -472,7 +479,12 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml 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; + //cerr << " offsetting " << stroke_closed.size() << " closed and " << stroke_open.size() << " open paths" << endl; + //cerr << " geometric tolerance = " << ctx.settings().geometric_tolerance_mm << " mm" << endl; + //cerr << " arc tolerance = " << offx.ArcTolerance/clipper_scale << " px" << endl; + //cerr << " stroke_width=" << stroke_width << "px" << endl; + //cerr << " offset = " << (0.5 * stroke_width * clipper_scale) << endl; + /* For stroking we have to separately handle open and closed paths since coincident start and end points may * render differently than joined start and end points. */ offx.AddPaths(stroke_closed, join_type, etClosedLine); @@ -483,6 +495,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml /* Clip. Note that (outside of outline mode) after the clipper outline operation, all we have is closed paths as * any open path's stroke outline is itself a closed path. */ if (!ctx.clip().empty()) { + //cerr << " Clipping polytree" << endl; Paths outline_paths; PolyTreeToPaths(ptree, outline_paths); @@ -518,6 +531,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml dehole_polytree(ptree, s_polys); ctx.mat().doc2phys_clipper(s_polys); /* color has alredy been pushed above. */ + //cerr << " sinking " << s_polys.size() << " paths" << endl; ctx.sink() << ApertureToken() << s_polys; } } @@ -590,10 +604,11 @@ 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.geometric_tolerance_mm); + load_svg_path(child, _stroke_open, _stroke_closed, ptree_fill, rset.geometric_tolerance_mm); Paths paths; PolyTreeToPaths(ptree_fill, paths); + child_xf.doc2phys_clipper(paths); c.AddPaths(paths, ptSubject, /* closed */ false); } -- cgit