From f2c891533f5179bf3d8a1625b1fe490a0cd06a42 Mon Sep 17 00:00:00 2001
From: jaseg <git@jaseg.de>
Date: Sun, 25 Apr 2021 14:03:16 +0200
Subject: svg-flatten: Add outline/edge layer mode

---
 svg-flatten/src/main.cpp       |  16 ++++--
 svg-flatten/src/out_gerber.cpp |  24 ++++++---
 svg-flatten/src/svg_doc.cpp    | 115 +++++++++++++++++++++++++----------------
 3 files changed, 100 insertions(+), 55 deletions(-)

(limited to 'svg-flatten/src')

diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp
index 722b356..3093d6a 100644
--- a/svg-flatten/src/main.cpp
+++ b/svg-flatten/src/main.cpp
@@ -28,7 +28,7 @@ int main(int argc, char **argv) {
                 "Print version and exit",
                 0},
             {"ofmt", {"-o", "--format"},
-                "Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression)",
+                "Output format. Supported: gerber, gerber-outline (for board outline layer), svg, s-exp (KiCAD S-Expression)",
                 1},
             {"precision", {"-p", "--precision"},
                 "Number of decimal places use for exported coordinates (gerber: 1-9, SVG: 0-*)",
@@ -181,6 +181,7 @@ int main(int argc, char **argv) {
 
     bool force_flatten = false;
     bool is_sexp = false;
+    bool outline_mode = false;
     PolygonSink *sink = nullptr;
     PolygonSink *flattener = nullptr;
     PolygonSink *dilater = nullptr;
@@ -189,10 +190,16 @@ int main(int argc, char **argv) {
         string clear_color = args["svg_clear_color"] ? args["svg_clear_color"].as<string>() : "#ffffff";
         sink = new SimpleSVGOutput(*out_f, only_polys, precision, dark_color, clear_color);
 
-    } else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber") {
+    } else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber" || fmt == "gerber-outline") {
+        outline_mode = fmt == "gerber-outline";
+
         double scale = args["scale"].as<double>(1.0);
-        cerr << "loading @scale=" << scale << endl;
-        sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"]);
+        if (scale != 1.0) {
+            cerr << "loading @scale=" << scale << endl;
+        }
+
+        sink = new SimpleGerberOutput(
+                *out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"], outline_mode);
 
     } else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") {
         if (!args["sexp_mod_name"]) {
@@ -417,6 +424,7 @@ int main(int argc, char **argv) {
         min_feature_size,
         curve_tolerance,
         vec_sel,
+        outline_mode,
     };
 
     SVGDocument doc;
diff --git a/svg-flatten/src/out_gerber.cpp b/svg-flatten/src/out_gerber.cpp
index 9513c0b..94c01de 100644
--- a/svg-flatten/src/out_gerber.cpp
+++ b/svg-flatten/src/out_gerber.cpp
@@ -27,13 +27,14 @@
 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)
+SimpleGerberOutput::SimpleGerberOutput(ostream &out, bool only_polys, int digits_int, int digits_frac, double scale, d2p offset, bool flip_polarity, bool outline_mode)
     : 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_flip_pol(flip_polarity),
+    m_outline_mode(outline_mode)
 {
     assert(1 <= digits_int && digits_int <= 9);
     assert(0 <= digits_frac && digits_frac <= 9);
@@ -61,16 +62,20 @@ void SimpleGerberOutput::header_impl(d2p origin, d2p size) {
 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 if (pol == GRB_POL_CLEAR) {
+    } else {
         m_out << "%LPC*%" << endl;
     }
 
     return *this;
 }
 SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) {
-    if (poly.size() < 3) {
+    if (poly.size() < 3 && !m_outline_mode) {
         cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl;
         return *this;
     }
@@ -78,11 +83,15 @@ SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) {
     /* 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);
-    m_out << "G36*" << endl;
+    if (!m_outline_mode) {
+        m_out << "G36*" << endl;
+    }
+
     m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal /* isn't C++ a marvel of engineering? */ << (long long int)x
           << "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y
           << "D02*" << endl;
     m_out << "G01*" << endl;
+
     for (size_t i=1; i<poly.size(); i++) {
         double x = round((poly[i][0] * m_scale + m_offset[0]) * m_gerber_scale);
         double y = round((m_height - poly[i][1] * m_scale + m_offset[1]) * m_gerber_scale);
@@ -90,7 +99,10 @@ SimpleGerberOutput& SimpleGerberOutput::operator<<(const Polygon &poly) {
               << "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y
               << "D01*" << endl;
     }
-    m_out << "G37*" << endl;
+
+    if (!m_outline_mode) {
+        m_out << "G37*" << endl;
+    }
 
     return *this;
 }
diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp
index b7ae61b..48ee56d 100644
--- a/svg-flatten/src/svg_doc.cpp
+++ b/svg-flatten/src/svg_doc.cpp
@@ -237,8 +237,12 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
     ClosedPathsFromPolyTree(ptree_stroke, closed_paths);
     PolyTreeToPaths(ptree_fill, fill_paths);
 
-    /* Skip filling for transparent fills */
-    if (fill_color) {
+    bool has_fill = fill_color;
+    bool has_stroke = stroke_color && stroke_width > 0.0;
+
+    /* Skip filling for transparent fills. In outline mode, skip filling if a stroke is also set to avoid double lines.
+     */
+    if (fill_color && !(rset.outline_mode && has_stroke)) {
         /* Clip paths. Consider all paths closed for filling. */
         if (!clip_path.empty()) {
             Clipper c;
@@ -262,6 +266,10 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
             }
 
         } else { /* solid fill */
+            if (rset.outline_mode) {
+                fill_color = GRB_DARK;
+            }
+
             Paths f_polys;
             /* Important for gerber spec compliance and also for reliable rendering results irrespective of board house
              * and gerber viewer. */
@@ -274,81 +282,98 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
                     out.push_back(std::array<double, 2>{
                             ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale
                             });
+
+                /* In outline mode, manually close polys */
+                if (rset.outline_mode && !out.empty())
+                    out.push_back(out[0]);
+
                 *polygon_sink << (fill_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out;
             }
         }
     }
 
-    if (stroke_color && stroke_width > 0.0) {
+    if (has_stroke) {
         ClipperOffset offx;
         offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */
 
         /* For stroking we have to separately handle open and closed paths */
-        for (const auto &poly : closed_paths) {
-            if (poly.empty()) /* do we need this? */
+        for (auto &poly : closed_paths) {
+            if (poly.empty())
                 continue;
 
             /* Special case: A closed path becomes a number of open paths when it is dashed. */
             if (dasharray.empty()) {
-                offx.AddPath(poly, join_type, etClosedLine);
+
+                if (rset.outline_mode) {
+                    /* In outline mode, manually close polys */
+                    poly.push_back(poly[0]);
+                    *polygon_sink << poly;
+
+                } else {
+                    offx.AddPath(poly, join_type, etClosedLine);
+                }
 
             } else {
                 Path poly_copy(poly);
                 poly_copy.push_back(poly[0]);
                 Paths out;
                 dash_path(poly_copy, out, dasharray);
-                offx.AddPaths(out, join_type, end_type);
+
+                if (rset.outline_mode) {
+                    *polygon_sink << out;
+                } else {
+                    offx.AddPaths(out, join_type, end_type);
+                }
             }
         }
 
         for (const auto &poly : open_paths) {
             Paths out;
             dash_path(poly, out, dasharray);
-            offx.AddPaths(out, join_type, end_type);
-        }
 
-        /* Execute clipper offset operation to generate stroke outlines */
-        offx.Execute(ptree, 0.5 * stroke_width * clipper_scale);
+            if (rset.outline_mode) {
+                *polygon_sink << out;
+            } else {
+                offx.AddPaths(out, join_type, end_type);
+            }
+        }
 
-        /* Clip. Note that after the outline, all we have is closed paths as any open path's stroke outline is itself
-         * a closed path. */
-        if (!clip_path.empty()) {
-            Clipper c;
+        if (!rset.outline_mode) {
+            /* Execute clipper offset operation to generate stroke outlines */
+            offx.Execute(ptree, 0.5 * stroke_width * clipper_scale);
+
+            /* Clip. Note that after the outline, all we have is closed paths as any open path's stroke outline is itself
+             * a closed path. */
+            if (!clip_path.empty()) {
+                Clipper c;
+
+                Paths outline_paths;
+                PolyTreeToPaths(ptree, outline_paths);
+                c.AddPaths(outline_paths, ptSubject, /* closed */ true);
+                c.AddPaths(clip_path, ptClip, /* closed */ true);
+                c.StrictlySimple(true);
+                /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ 
+                c.Execute(ctIntersection, ptree, pftNonZero, pftNonZero);
+            }
 
-            Paths outline_paths;
-            PolyTreeToPaths(ptree, outline_paths);
-            c.AddPaths(outline_paths, ptSubject, /* closed */ true);
-            c.AddPaths(clip_path, ptClip, /* closed */ true);
-            c.StrictlySimple(true);
-            /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ 
-            c.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;
 
-        /* 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);
+                    pattern->tile(local_xf, rset, clip);
+                }
 
             } else {
-                Paths clip;
-                PolyTreeToPaths(ptree, clip);
-                pattern->tile(local_xf, rset, clip);
-            }
+                Paths s_polys;
+                dehole_polytree(ptree, s_polys);
 
-        } else {
-            Paths s_polys;
-            dehole_polytree(ptree, s_polys);
-
-            /* export gerber */
-            for (const auto &poly : s_polys) {
-                vector<array<double, 2>> out;
-                for (const auto &p : poly)
-                    out.push_back(std::array<double, 2>{
-                            ((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale
-                            });
-                *polygon_sink << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out;
+                *polygon_sink << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << s_polys;
             }
         }
     }
-- 
cgit