kopia lustrzana https://github.com/jaseg/gerbolyze
wip
rodzic
a6adfe4d1d
commit
6b0382ab77
Plik diff jest za duży
Load Diff
Po Szerokość: | Wysokość: | Rozmiar: 182 KiB |
|
@ -63,3 +63,11 @@ def test_template(reference):
|
|||
run_command('python3', '-m', 'gerbolyze', 'template', '--top', '--force', infile, out_svg.name)
|
||||
run_command('python3', '-m', 'gerbolyze', 'template', '--bottom', '--force', '--vector', infile, out_svg.name)
|
||||
|
||||
def test_convert_layers():
|
||||
infile = reference_path('layers.svg')
|
||||
with tempfile.TemporaryDirectory() as out_dir:
|
||||
run_command('python3', '-m', 'gerbolyze', 'convert', infile, out_dir)
|
||||
out_dir = Path(out_dir)
|
||||
print(list(out_dir.glob('*')))
|
||||
assert False
|
||||
|
||||
|
|
|
@ -104,11 +104,26 @@ namespace gerbolyze {
|
|||
};
|
||||
|
||||
double doc2phys_dist(double dist_doc) {
|
||||
return dist_doc * sqrt(xx*xx + xy * xy);
|
||||
return dist_doc * sqrt(xx*xx + xy*xy);
|
||||
}
|
||||
|
||||
double phys2doc_dist(double dist_doc) {
|
||||
return dist_doc / sqrt(xx*xx + xy * xy);
|
||||
return dist_doc / sqrt(xx*xx + xy*xy);
|
||||
}
|
||||
|
||||
double doc2phys_skew(double dist_doc) {
|
||||
/* https://math.stackexchange.com/a/3521141 */
|
||||
/* xx yx x0
|
||||
* xy yy y0 */
|
||||
s_x = sqrt();
|
||||
}
|
||||
|
||||
double doc2phys_min(double dist_doc) {
|
||||
return dist_doc * fmin(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx));
|
||||
}
|
||||
|
||||
double doc2phys_max(double dist_doc) {
|
||||
return dist_doc * fmax(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx));
|
||||
}
|
||||
|
||||
d2p doc2phys(const d2p p) {
|
||||
|
@ -141,13 +156,31 @@ namespace gerbolyze {
|
|||
}
|
||||
|
||||
/* Transform given clipper paths */
|
||||
void transform_paths(ClipperLib::Paths &paths) {
|
||||
void doc2phys_clipper(ClipperLib::Paths &paths) {
|
||||
for (auto &p : paths) {
|
||||
transform_clipper_path(p);
|
||||
doc2phys_clipper(p);
|
||||
}
|
||||
}
|
||||
|
||||
void transform_clipper_path(ClipperLib::Path &path) {
|
||||
void doc2phys_clipper(ClipperLib::Path &path) {
|
||||
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}));
|
||||
return {
|
||||
(ClipperLib::cInt)round(out[0] * clipper_scale),
|
||||
(ClipperLib::cInt)round(out[1] * clipper_scale)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* Transform given clipper paths */
|
||||
void phys2doc_clipper(ClipperLib::Paths &paths) {
|
||||
for (auto &p : paths) {
|
||||
phys2doc_clipper(p);
|
||||
}
|
||||
}
|
||||
|
||||
void phys2doc_clipper(ClipperLib::Path &path) {
|
||||
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}));
|
||||
|
|
|
@ -208,7 +208,8 @@ namespace gerbolyze {
|
|||
class RenderSettings {
|
||||
public:
|
||||
double m_minimum_feature_size_mm = 0.1;
|
||||
double curve_tolerance_mm;
|
||||
double geometric_tolerance_mm = 0.1;
|
||||
double stroke_width_cutoff = 0.01;
|
||||
double drill_test_polsby_popper_tolerance = 0.01;
|
||||
double aperture_circle_test_tolerance = 0.01;
|
||||
double aperture_rect_test_tolerance = 0.01;
|
||||
|
|
|
@ -82,8 +82,11 @@ int main(int argc, char **argv) {
|
|||
{"min_feature_size", {"-d", "--trace-space"},
|
||||
"Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.",
|
||||
1},
|
||||
{"curve_tolerance", {"-c", "--curve-tolerance"},
|
||||
"Tolerance for curve flattening in mm. Default: 0.1mm.",
|
||||
{"geometric_tolerance", {"-t", "--tolerance"},
|
||||
"Tolerance in mm for geometric approximation such as curve flattening. Default: 0.1mm.",
|
||||
1},
|
||||
{"stroke_width_cutoff", {"--min-stroke-width"},
|
||||
"Don't render strokes thinner than the given width in mm. Default: 0.01mm.",
|
||||
1},
|
||||
{"drill_test_polsby_popper_tolerance", {"--drill-test-tolerance"},
|
||||
"Tolerance for identifying circles as drills in outline mode",
|
||||
|
@ -313,7 +316,8 @@ int main(int argc, char **argv) {
|
|||
delete vec;
|
||||
|
||||
double min_feature_size = args["min_feature_size"].as<double>(0.1); /* mm */
|
||||
double curve_tolerance = args["curve_tolerance"].as<double>(0.1); /* mm */
|
||||
double geometric_tolerance = args["geometric_tolerance"].as<double>(0.1); /* mm */
|
||||
double stroke_width_cutoff = args["stroke_width_cutoff"].as<double>(0.01); /* mm */
|
||||
double drill_test_polsby_popper_tolerance = args["drill_test_polsby_popper_tolerance"].as<double>(0.1);
|
||||
double aperture_rect_test_tolerance = args["aperture_rect_test_tolerance"].as<double>(0.1);
|
||||
double aperture_circle_test_tolerance = args["aperture_circle_test_tolerance"].as<double>(0.1);
|
||||
|
@ -450,7 +454,8 @@ int main(int argc, char **argv) {
|
|||
|
||||
RenderSettings rset {
|
||||
min_feature_size,
|
||||
curve_tolerance,
|
||||
geometric_tolerance,
|
||||
stroke_width_cutoff,
|
||||
drill_test_polsby_popper_tolerance,
|
||||
aperture_circle_test_tolerance,
|
||||
aperture_rect_test_tolerance,
|
||||
|
|
|
@ -159,7 +159,7 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm
|
|||
|
||||
} else {
|
||||
clip_path = *lookup;
|
||||
ctx.mat().transform_paths(clip_path);
|
||||
ctx.mat().doc2phys_clipper(clip_path);
|
||||
}
|
||||
|
||||
/* Clip against parent's clip path (both are now in document coordinates) */
|
||||
|
@ -225,6 +225,13 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm
|
|||
|
||||
/* Export an SVG path element to gerber. Apply patterns and clip on the fly. */
|
||||
void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml_node &node) {
|
||||
/* Important note on the document transform:
|
||||
*
|
||||
* We have to make sure that we dash & stroke (outline) the path *before* transforming into physical units because
|
||||
* the transform may not be uniform, i.e. scale may depend on direction. As an example, imagine you stroke a 10 by
|
||||
* 10mm square with an 1mm stroke, but there is a transform that scales by 1 in y-direction, and 2 in x-direction.
|
||||
* In the output, the stroke is going to be 2mm wide on the left and right, and 1mm wide on the top/bottom.
|
||||
*/
|
||||
enum gerber_color fill_color = gerber_fill_color(node, ctx.settings());
|
||||
enum gerber_color stroke_color = gerber_stroke_color(node, ctx.settings());
|
||||
//cerr << "path: resolved colors, stroke=" << stroke_color << ", fill=" << fill_color << endl;
|
||||
|
@ -242,19 +249,22 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
return;
|
||||
}
|
||||
|
||||
/* Load path from SVG path data and transform into document units. */
|
||||
stroke_width = ctx.mat().doc2phys_dist(stroke_width);
|
||||
|
||||
/* Load path from SVG path data */
|
||||
Paths stroke_open, stroke_closed;
|
||||
PolyTree ptree_fill;
|
||||
PolyTree ptree;
|
||||
load_svg_path(ctx.mat(), node, stroke_open, stroke_closed, ptree_fill, ctx.settings().curve_tolerance_mm);
|
||||
double geometric_tolerance_px = ctx.mat().doc2phys_min(ctx.settings().geometric_tolerance_mm);
|
||||
load_svg_path(node, stroke_open, stroke_closed, ptree_fill, geometric_tolerance_px);
|
||||
|
||||
Paths fill_paths;
|
||||
PolyTreeToPaths(ptree_fill, fill_paths);
|
||||
/* Since we do not need to stroke them, transform the fill paths to physical units now. For polsby-popper to work
|
||||
* properly, they need to be transformed already. However, we leave the stroke paths un-transformed since they can
|
||||
* only be transformed after outlining. */
|
||||
ctx.mat().doc2phys_clipper(fill_paths);
|
||||
|
||||
bool has_fill = fill_color;
|
||||
bool has_stroke = stroke_color && stroke_width > 0.0;
|
||||
bool has_stroke = stroke_color && ctx.mat().doc2phys_min(stroke_width) > ctx.settings().stroke_width_cutoff;
|
||||
|
||||
cerr << "processing svg path" << endl;
|
||||
cerr << " * " << fill_paths.size() << " fill paths" << endl;
|
||||
|
@ -288,7 +298,6 @@ 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 = ctx.mat().doc2phys_dist(diameter); /* FIXME is this correct w.r.t. PolygonScaler? */
|
||||
diameter = round(diameter * 1000.0) / 1000.0; /* Round to micrometer precsion; FIXME: make configurable */
|
||||
ctx.sink() << ApertureToken(diameter) << FlashToken(centroid);
|
||||
}
|
||||
|
@ -375,6 +384,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
auto open_copy(stroke_open);
|
||||
stroke_open.clear();
|
||||
|
||||
/* FIXME do we handle really really long dashes correctly? */
|
||||
for (auto &poly : stroke_closed) {
|
||||
poly.push_back(poly[0]);
|
||||
dash_path(poly, stroke_open, dasharray, stroke_dashoffset);
|
||||
|
@ -387,7 +397,10 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
}
|
||||
}
|
||||
|
||||
if (stroke_color != GRB_PATTERN_FILL) {
|
||||
if (stroke_color != GRB_PATTERN_FILL
|
||||
&& ctx.sink().can_do_apertures()
|
||||
/* check if we have an uniform transform */
|
||||
&& ctx.mat().doc2phys_skew(stroke_width) < 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;
|
||||
|
@ -403,11 +416,15 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
Paths dilated_clip;
|
||||
ClosedPathsFromPolyTree(clip_ptree, dilated_clip);
|
||||
|
||||
Paths stroke_open_phys(stroke_open), stroke_closed_phys(stroke_closed);
|
||||
ctx.mat().doc2phys_clipper(stroke_open_phys);
|
||||
ctx.mat().doc2phys_clipper(stroke_closed_phys);
|
||||
|
||||
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.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;
|
||||
|
||||
|
@ -433,7 +450,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
// 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.sink().can_do_apertures() && (ctx.settings().outline_mode || gerber_lossless )) {
|
||||
if (ctx.settings().outline_mode || gerber_lossless) {
|
||||
// cerr << " -> converting directly" << endl;
|
||||
ctx.sink() << ApertureToken(stroke_width);
|
||||
for (auto &path : stroke_closed) {
|
||||
|
@ -452,7 +469,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
}
|
||||
|
||||
ClipperOffset offx;
|
||||
offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */
|
||||
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;
|
||||
|
@ -468,9 +485,13 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
if (!ctx.clip().empty()) {
|
||||
Paths outline_paths;
|
||||
PolyTreeToPaths(ptree, outline_paths);
|
||||
|
||||
Paths clip(ctx.clip());
|
||||
ctx.mat().phys2doc_clipper(clip);
|
||||
|
||||
Clipper stroke_clip;
|
||||
stroke_clip.StrictlySimple(true);
|
||||
stroke_clip.AddPaths(ctx.clip(), ptClip, /* closed */ true);
|
||||
stroke_clip.AddPaths(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);
|
||||
|
@ -486,6 +507,8 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
} else {
|
||||
Paths clip;
|
||||
PolyTreeToPaths(ptree, clip);
|
||||
ctx.mat().phys2doc_clipper(clip);
|
||||
|
||||
RenderContext local_ctx(ctx, xform2d(), clip, true);
|
||||
pattern->tile(local_ctx);
|
||||
}
|
||||
|
@ -493,6 +516,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
} else {
|
||||
Paths s_polys;
|
||||
dehole_polytree(ptree, s_polys);
|
||||
ctx.mat().doc2phys_clipper(s_polys);
|
||||
/* color has alredy been pushed above. */
|
||||
ctx.sink() << ApertureToken() << s_polys;
|
||||
}
|
||||
|
@ -566,7 +590,7 @@ 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.curve_tolerance_mm);
|
||||
load_svg_path(child_xf, child, _stroke_open, _stroke_closed, ptree_fill, rset.geometric_tolerance_mm);
|
||||
|
||||
Paths paths;
|
||||
PolyTreeToPaths(ptree_fill, paths);
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
using namespace std;
|
||||
|
||||
static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) {
|
||||
static pair<bool, bool> flatten_path(ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_px) {
|
||||
istringstream in(path_data);
|
||||
|
||||
string cmd;
|
||||
|
@ -63,14 +63,6 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths
|
|||
in >> a[0] >> a[1];
|
||||
assert (!in.fail()); /* guaranteed by usvg */
|
||||
|
||||
/* We need to transform all points ourselves here, and cannot use the transform feature of cairo_to_clipper:
|
||||
* Our transform may contain offsets, and clipper only passes its data into cairo's transform functions
|
||||
* after scaling up to its internal fixed-point ints, but it does not scale the transform accordingly. This
|
||||
* means a scale/rotation we set before calling clipper works out fine, but translations get lost as they
|
||||
* get scaled by something like 1e-6.
|
||||
*/
|
||||
a = mat.doc2phys(a);
|
||||
|
||||
in_poly.emplace_back(ClipperLib::IntPoint{
|
||||
(ClipperLib::cInt)round(a[0]*clipper_scale),
|
||||
(ClipperLib::cInt)round(a[1]*clipper_scale)
|
||||
|
@ -80,7 +72,6 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths
|
|||
in >> a[0] >> a[1];
|
||||
assert (!in.fail()); /* guaranteed by usvg */
|
||||
|
||||
a = mat.doc2phys(a);
|
||||
in_poly.emplace_back(ClipperLib::IntPoint{
|
||||
(ClipperLib::cInt)round(a[0]*clipper_scale),
|
||||
(ClipperLib::cInt)round(a[1]*clipper_scale)
|
||||
|
@ -93,11 +84,7 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths
|
|||
in >> d[0] >> d[1]; /* end point */
|
||||
assert (!in.fail()); /* guaranteed by usvg */
|
||||
|
||||
b = mat.doc2phys(b);
|
||||
c = mat.doc2phys(c);
|
||||
d = mat.doc2phys(d);
|
||||
|
||||
gerbolyze::curve4_div c4div(distance_tolerance_mm);
|
||||
gerbolyze::curve4_div c4div(distance_tolerance_px);
|
||||
c4div.run(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]);
|
||||
|
||||
for (auto &pt : c4div.points()) {
|
||||
|
@ -122,7 +109,7 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths
|
|||
return {has_closed, num_subpaths > 1};
|
||||
}
|
||||
|
||||
void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance) {
|
||||
void gerbolyze::load_svg_path(const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double geometric_tolerance_px) {
|
||||
auto *path_data = node.attribute("d").value();
|
||||
auto fill_rule = clipper_fill_rule(node);
|
||||
|
||||
|
@ -131,7 +118,7 @@ void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperL
|
|||
* open/closed properties for stroke offsetting. */
|
||||
ClipperLib::Clipper c_fill;
|
||||
c_fill.StrictlySimple(true);
|
||||
auto res = flatten_path(mat, stroke_open, stroke_closed, c_fill, path_data, curve_tolerance);
|
||||
auto res = flatten_path(stroke_open, stroke_closed, c_fill, path_data, geometric_tolerance_px);
|
||||
bool has_closed = res.first, has_multiple = res.second;
|
||||
|
||||
if (!has_closed && !has_multiple) {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
#include "geom2d.hpp"
|
||||
|
||||
namespace gerbolyze {
|
||||
void load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance);
|
||||
void load_svg_path(const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double geometric_tolerance_px);
|
||||
void parse_dasharray(const pugi::xml_node &node, std::vector<double> &out);
|
||||
void dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const std::vector<double> dasharray, double dash_offset=0.0);
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue