svg-flatten: Add outline/edge layer mode
This commit is contained in:
parent
1180ebdc1f
commit
f2c891533f
4 changed files with 120 additions and 58 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue