From d3204b1edeb234591d5c65fbec993f2c2d16209e Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 20 Jun 2022 19:37:33 +0200 Subject: svg-flatten: Finish direct interpolation optimization --- svg-flatten/include/gerbolyze.hpp | 8 +- svg-flatten/src/main.cpp | 3 +- svg-flatten/src/out_gerber.cpp | 33 +++---- svg-flatten/src/out_scaler.cpp | 5 +- svg-flatten/src/svg_doc.cpp | 196 +++++++++++++++++++++----------------- 5 files changed, 134 insertions(+), 111 deletions(-) diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index b50f18c..a24236b 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -46,7 +46,9 @@ namespace gerbolyze { class ApertureToken { public: - ApertureToken(double size=0.0) : m_size(size) {} + ApertureToken() : m_has_aperture(false) {} + ApertureToken(double size) : m_has_aperture(true), m_size(size) {} + bool m_has_aperture = false; double m_size = 0.0; }; @@ -317,7 +319,7 @@ namespace gerbolyze { class SimpleGerberOutput : public StreamPolygonSink { public: - SimpleGerberOutput(std::ostream &out, bool only_polys=false, int digits_int=4, int digits_frac=6, double scale=1.0, d2p offset={0,0}, bool flip_polarity=false, bool outline_mode=false); + SimpleGerberOutput(std::ostream &out, bool only_polys=false, int digits_int=4, int digits_frac=6, double scale=1.0, d2p offset={0,0}, bool flip_polarity=false); virtual ~SimpleGerberOutput() {} virtual SimpleGerberOutput &operator<<(const Polygon &poly); virtual SimpleGerberOutput &operator<<(GerberPolarityToken pol); @@ -336,8 +338,8 @@ namespace gerbolyze { d2p m_offset; double m_scale; bool m_flip_pol; - bool m_outline_mode; double m_current_aperture; + bool m_aperture_set; bool m_macro_aperture; unsigned int m_aperture_num; }; diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 812db5d..1d55437 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -246,8 +246,7 @@ int main(int argc, char **argv) { cerr << "Info: Loading scaled input @scale=" << scale << endl; } - sink = new SimpleGerberOutput( - *out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"], outline_mode); + sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"]); } else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") { if (!args["sexp_mod_name"]) { diff --git a/svg-flatten/src/out_gerber.cpp b/svg-flatten/src/out_gerber.cpp index 543d641..14b6b32 100644 --- a/svg-flatten/src/out_gerber.cpp +++ b/svg-flatten/src/out_gerber.cpp @@ -27,15 +27,15 @@ using namespace gerbolyze; using namespace std; -SimpleGerberOutput::SimpleGerberOutput(ostream &out, bool only_polys, int digits_int, int digits_frac, double scale, d2p offset, bool flip_polarity, bool outline_mode) +SimpleGerberOutput::SimpleGerberOutput(ostream &out, bool only_polys, int digits_int, int digits_frac, double scale, d2p offset, bool flip_polarity) : StreamPolygonSink(out, only_polys), m_digits_int(digits_int), m_digits_frac(digits_frac), m_offset(offset), m_scale(scale), m_flip_pol(flip_polarity), - m_outline_mode(outline_mode), m_current_aperture(0.0), + m_aperture_set(false), m_macro_aperture(false), m_aperture_num(10) /* See gerber standard */ { @@ -63,28 +63,27 @@ void SimpleGerberOutput::header_impl(d2p origin, d2p size) { } SimpleGerberOutput& SimpleGerberOutput::operator<<(const ApertureToken &ap) { - if (!m_macro_aperture && ap.m_size == m_current_aperture) { + if (m_aperture_set && !m_macro_aperture && ap.m_size == m_current_aperture) { return *this; } + m_aperture_set = ap.m_has_aperture; m_macro_aperture = false; - m_current_aperture = ap.m_size; - m_aperture_num += 1; - double size = (ap.m_size > 0.0) ? ap.m_size : 0.05; - m_out << "%ADD" << m_aperture_num << "C," << size << "*%" << endl; - m_out << "D" << m_aperture_num << "*" << endl; + if (m_aperture_set) { + m_current_aperture = ap.m_size; + m_aperture_num += 1; + double size = (ap.m_size > 0.0) ? ap.m_size : 0.05; + m_out << "%ADD" << m_aperture_num << "C," << size << "*%" << endl; + m_out << "D" << m_aperture_num << "*" << endl; + } return *this; } SimpleGerberOutput& SimpleGerberOutput::operator<<(GerberPolarityToken pol) { assert(pol == GRB_POL_DARK || pol == GRB_POL_CLEAR); - if (m_outline_mode) { - assert(pol == GRB_POL_DARK); - } - if ((pol == GRB_POL_DARK) != m_flip_pol) { m_out << "%LPD*%" << endl; } else { @@ -94,15 +93,15 @@ SimpleGerberOutput& SimpleGerberOutput::operator<<(GerberPolarityToken pol) { return *this; } SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) { - if (poly.size() < 3 && !m_outline_mode) { - cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl; + if (poly.size() < 3 && !m_aperture_set) { + cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput in region mode" << endl; return *this; } /* NOTE: Clipper and gerber both have different fixed-point scales. We get points in double mm. */ double x = round((poly[0][0] * m_scale + m_offset[0]) * m_gerber_scale); double y = round((m_height - poly[0][1] * m_scale + m_offset[1]) * m_gerber_scale); - if (!m_outline_mode) { + if (!m_aperture_set) { m_out << "G36*" << endl; } @@ -119,7 +118,7 @@ SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) { << "D01*" << endl; } - if (!m_outline_mode) { + if (!m_aperture_set) { m_out << "G37*" << endl; } @@ -132,6 +131,8 @@ void SimpleGerberOutput::footer_impl() { SimpleGerberOutput &SimpleGerberOutput::operator<<(const FlashToken &tok) { + assert(m_aperture_set); + double x = round((tok.m_offset[0] * m_scale + m_offset[0]) * m_gerber_scale); double y = round((m_height - tok.m_offset[1] * m_scale + m_offset[1]) * m_gerber_scale); diff --git a/svg-flatten/src/out_scaler.cpp b/svg-flatten/src/out_scaler.cpp index c6e624d..ab65ab0 100644 --- a/svg-flatten/src/out_scaler.cpp +++ b/svg-flatten/src/out_scaler.cpp @@ -50,7 +50,10 @@ PolygonScaler &PolygonScaler::operator<<(GerberPolarityToken pol) { } PolygonScaler &PolygonScaler::operator<<(const ApertureToken &tok) { - m_sink << ApertureToken(tok.m_size * m_scale); + if (tok.m_has_aperture) + m_sink << ApertureToken(tok.m_size * m_scale); + else + m_sink << tok; return *this; } diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index 9eed1e6..019760b 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -350,10 +350,6 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml stroke_clip.StrictlySimple(true); stroke_clip.AddPaths(ctx.clip(), ptClip, /* closed */ true); - ClipperOffset offx; - offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */ - offx.MiterLimit = stroke_miterlimit; - /* We forward strokes as regular gerber interpolations instead of tracing their outline using clipper when one * of these is true: * @@ -362,107 +358,129 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml * * We have to ignore patterned strokes since then we recursively call down to the pattern renderer. The checks * in (2) are to make sure that the semantics of our source SVG align with gerber's aperture semantics. Gerber - * cannot express anything other than "round" joins and ends. If a clip is set, the clipped line ends would not - * be round so we have to exclude that as well. A possible future optimization would be to check if we actually - * did clip the stroke, but that is too expensive since then we'd have to outline it first to account for - * stroke thickness and end caps. + * cannot express anything other than "round" joins and ends. If any part of the path is clipped, the clipped + * line ends would not be round so we have to exclude that as well. In case of outline mode, we accept this + * inaccuracies for usability (the only alternative would be to halt and catch fire). */ - bool local_outline_mode = - stroke_color != GRB_PATTERN_FILL && ( - ctx.settings().outline_mode || ( - ctx.clip().empty() && - end_type == ClipperLib::etOpenRound && - join_type == ClipperLib::jtRound)); - - /* For stroking we have to separately handle open and closed paths */ - for (auto &poly : stroke_closed) { - if (poly.empty()) - continue; - - /* Special case: A closed path becomes a number of open paths when it is dashed. */ - if (dasharray.empty()) { - - if (local_outline_mode) { - stroke_clip.AddPath(poly, ptSubject, /* closed */ true); - } else { - offx.AddPath(poly, join_type, etClosedLine); + + /* Calculate out dashes: A closed path becomes a number of open paths when it is dashed. */ + if (!dasharray.empty()) { + for (auto &poly : stroke_closed) { + if (poly.empty()) { + continue; } - } else { - Path poly_copy(poly); - poly_copy.push_back(poly[0]); - Paths out; - dash_path(poly_copy, out, dasharray, stroke_dashoffset); - - if (local_outline_mode) { - stroke_clip.AddPaths(out, ptSubject, /* closed */ false); - } else { - offx.AddPaths(out, join_type, end_type); - } + poly.push_back(poly[0]); + dash_path(poly, stroke_open, dasharray, stroke_dashoffset); } } - for (const auto &poly : stroke_open) { - Paths out; - dash_path(poly, out, dasharray, stroke_dashoffset); + if (stroke_color != GRB_PATTERN_FILL) { + cerr << "Analyzing direct conversion of stroke" << endl; + cerr << " stroke_closed.size() = " << stroke_closed.size() << endl; + cerr << " stroke_open.size() = " << stroke_open.size() << endl; + ctx.sink() << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR); + + ClipperOffset offx; + offx.ArcTolerance = 0.01 * clipper_scale; /* see below. */ + offx.MiterLimit = 10; + offx.AddPaths(ctx.clip(), jtRound, etClosedPolygon); + PolyTree clip_ptree; + offx.Execute(clip_ptree, -0.5 * stroke_width * clipper_scale); + + Paths dilated_clip; + ClosedPathsFromPolyTree(clip_ptree, dilated_clip); + + 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.Execute(ctDifference, ptree, pftNonZero, pftNonZero); + + /* Did any part of the path clip the clip path (which defaults to the document border)? */ + bool nothing_clipped = ptree.Total() == 0; + + /* Can all joins be mapped? True if either jtRound, or if there are no joins. */ + bool joins_can_be_mapped = true; + if (join_type != ClipperLib::jtRound) { + for (auto &p : stroke_closed) { + if (p.size() > 2) { + joins_can_be_mapped = false; + } + } + } - if (ctx.settings().outline_mode) { - stroke_clip.AddPaths(out, ptSubject, /* closed */ false); - } else { - offx.AddPaths(out, join_type, end_type); + /* Can all ends be mapped? True if either etOpenRound or if there are no ends (we only have closed paths) */ + 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 << " 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); + for (auto &path : stroke_closed) { + if (path.empty()) { + continue; + } + /* We have to manually close these here. */ + path.push_back(path[0]); + ctx.sink() << path; + } + ctx.sink() << stroke_open; + return; } + cerr << " -> NOT converting directly" << endl; + /* else fall through to normal processing */ } - if (ctx.settings().outline_mode) { - stroke_clip.Execute(ctIntersection, ptree, pftNonZero, pftNonZero); - Paths outline_paths; - ctx.sink() << ApertureToken(stroke_width); - - ClosedPathsFromPolyTree(ptree, outline_paths); - for (auto &path : outline_paths) { - /* we have to connect the last and first point here */ - if (path.empty()) - continue; - path.push_back(path[0]); - ctx.sink() << path; - } + ClipperOffset offx; + offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */ + offx.MiterLimit = stroke_miterlimit; - OpenPathsFromPolyTree(ptree, outline_paths); - ctx.sink() << outline_paths; + /* 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); + offx.AddPaths(stroke_open, join_type, end_type); + /* Execute clipper offset operation to generate stroke outlines */ + offx.Execute(ptree, 0.5 * stroke_width * clipper_scale); - } else { - /* Execute clipper offset operation to generate stroke outlines */ - offx.Execute(ptree, 0.5 * stroke_width * clipper_scale); - - /* 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()) { - Paths outline_paths; - PolyTreeToPaths(ptree, outline_paths); - 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); - } + /* 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()) { + Paths outline_paths; + PolyTreeToPaths(ptree, outline_paths); + Clipper stroke_clip; + stroke_clip.StrictlySimple(true); + stroke_clip.AddPaths(ctx.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); + } - /* Call out to pattern tiler for pattern strokes. The stroke's outline becomes the clip here. */ - if (stroke_color == GRB_PATTERN_FILL) { - string stroke_pattern_id = usvg_id_url(node.attribute("stroke").value()); - Pattern *pattern = lookup_pattern(stroke_pattern_id); - if (!pattern) { - cerr << "Warning: Fill pattern with id \"" << stroke_pattern_id << "\" not found." << endl; - - } else { - Paths clip; - PolyTreeToPaths(ptree, clip); - RenderContext local_ctx(ctx, xform2d(), clip, true); - pattern->tile(local_ctx); - } + /* Call out to pattern tiler for pattern strokes. The stroke's outline becomes the clip here. */ + if (stroke_color == GRB_PATTERN_FILL) { + string stroke_pattern_id = usvg_id_url(node.attribute("stroke").value()); + Pattern *pattern = lookup_pattern(stroke_pattern_id); + if (!pattern) { + cerr << "Warning: Fill pattern with id \"" << stroke_pattern_id << "\" not found." << endl; } else { - Paths s_polys; - dehole_polytree(ptree, s_polys); - ctx.sink() << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << ApertureToken() << s_polys; + Paths clip; + PolyTreeToPaths(ptree, clip); + RenderContext local_ctx(ctx, xform2d(), clip, true); + pattern->tile(local_ctx); } + + } else { + Paths s_polys; + dehole_polytree(ptree, s_polys); + /* color has alredy been pushed above. */ + ctx.sink() << ApertureToken() << s_polys; } } } -- cgit