svg-flatten: Add outline/edge layer mode

This commit is contained in:
jaseg 2021-04-25 14:03:16 +02:00
parent 1180ebdc1f
commit f2c891533f
4 changed files with 120 additions and 58 deletions

View file

@ -49,6 +49,21 @@ namespace gerbolyze {
virtual ~PolygonSink() {}
virtual void header(d2p origin, d2p size) {(void) origin; (void) size;}
virtual PolygonSink &operator<<(const Polygon &poly) = 0;
virtual PolygonSink &operator<<(const ClipperLib::Paths paths) {
for (const auto &poly : paths) {
*this << poly;
}
return *this;
};
virtual PolygonSink &operator<<(const ClipperLib::Path poly) {
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
});
}
return *this << out;
};
virtual PolygonSink &operator<<(const LayerNameToken &) { return *this; };
virtual PolygonSink &operator<<(GerberPolarityToken pol) = 0;
virtual void footer() {}
@ -143,6 +158,7 @@ namespace gerbolyze {
double m_minimum_feature_size_mm = 0.1;
double curve_tolerance_mm;
VectorizerSelectorizer &m_vec_sel;
bool outline_mode = false;
};
class SVGDocument {
@ -209,7 +225,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);
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);
virtual ~SimpleGerberOutput() {}
virtual SimpleGerberOutput &operator<<(const Polygon &poly);
virtual SimpleGerberOutput &operator<<(GerberPolarityToken pol);
@ -225,6 +241,7 @@ namespace gerbolyze {
d2p m_offset;
double m_scale;
bool m_flip_pol;
bool m_outline_mode;
};
class SimpleSVGOutput : public StreamPolygonSink {

View file

@ -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;

View file

@ -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;
}

View file

@ -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);
/* 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);
}
/* 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;
if (rset.outline_mode) {
*polygon_sink << out;
} else {
Paths clip;
PolyTreeToPaths(ptree, clip);
pattern->tile(local_xf, rset, clip);
offx.AddPaths(out, join_type, end_type);
}
}
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);
}
} else {
Paths s_polys;
dehole_polytree(ptree, s_polys);
/* 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;
/* 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;
} else {
Paths clip;
PolyTreeToPaths(ptree, clip);
pattern->tile(local_xf, rset, clip);
}
} else {
Paths s_polys;
dehole_polytree(ptree, s_polys);
*polygon_sink << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << s_polys;
}
}
}