kopia lustrzana https://github.com/jaseg/gerbolyze
svg_doc: Fix gerber mapping of strokes with skewed or non-uniform transforms
rodzic
b4753e66e2
commit
602e51ca10
|
@ -99,6 +99,7 @@ namespace gerbolyze {
|
|||
yy = n_yy;
|
||||
x0 = n_x0;
|
||||
y0 = n_y0;
|
||||
decomposed = false;
|
||||
|
||||
return *this;
|
||||
};
|
||||
|
@ -111,48 +112,90 @@ namespace gerbolyze {
|
|||
return dist_doc / sqrt(xx*xx + xy*xy);
|
||||
}
|
||||
|
||||
double doc2phys_skew(double dist_doc) {
|
||||
void decompose() {
|
||||
/* FIXME unit tests, especially for degenerate cases! */
|
||||
if (decomposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* https://math.stackexchange.com/a/3521141 */
|
||||
/* https://stackoverflow.com/a/70381885 */
|
||||
/* xx yx x0
|
||||
* xy yy y0 */
|
||||
double s_x = sqrt(xx*xx + xy*xy);
|
||||
s_x = sqrt(xx*xx + xy*xy);
|
||||
|
||||
if (xx == 0 && xy == 0) {
|
||||
return std::numeric_limits<double>::infinity;
|
||||
theta = 0;
|
||||
} else {
|
||||
theta = atan2(xy, xx);
|
||||
}
|
||||
|
||||
double theta = atan2(xy, xx);
|
||||
double f = (xx*yy - xy*yx);
|
||||
|
||||
if (f == 0) {
|
||||
return std::numeric_limits<double>::infinity;
|
||||
m = 0;
|
||||
} else {
|
||||
m = (xx*yx + yy*xy) / f;
|
||||
}
|
||||
|
||||
double m = (xx*yx + yy*xy) / f;
|
||||
|
||||
double f = xx + m*xy;
|
||||
double s_y = 0;
|
||||
|
||||
f = xx + m*xy;
|
||||
if (f == 0) {
|
||||
f = m*xx - xy;
|
||||
if (f == 0) {
|
||||
return std::numeric_limits<double>::infinity;
|
||||
s_y = 0;
|
||||
}
|
||||
s_y = yx*s_x / f;
|
||||
} else {
|
||||
s_y = yy*s_x / f;
|
||||
}
|
||||
|
||||
return s_x - s_y >
|
||||
double b = sqrt(s_y*s_y + m*m);
|
||||
f_min = fmin(s_x, b);
|
||||
f_max = fmax(s_x, b);
|
||||
|
||||
decomposed = true;
|
||||
}
|
||||
|
||||
bool doc2phys_skew_ok(double dist_doc, double rel_tol, double abs_tol) {
|
||||
decompose();
|
||||
|
||||
if (f_min == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double imbalance = f_max / f_min - 1.0;
|
||||
//cerr << " * skew check: " << dbg_str();
|
||||
//cerr << " imbalance=" << imbalance << endl;
|
||||
//cerr << " rel=" << (imbalance < rel_tol) << " abs=" << (imbalance*fabs(dist_doc) < abs_tol) << endl;
|
||||
return imbalance < rel_tol && imbalance*fabs(dist_doc) < abs_tol;
|
||||
}
|
||||
|
||||
double doc2phys_min(double dist_doc) {
|
||||
return dist_doc * fmin(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx));
|
||||
decompose();
|
||||
return dist_doc * f_min;
|
||||
}
|
||||
|
||||
double doc2phys_max(double dist_doc) {
|
||||
return dist_doc * fmax(sqrt(xx*xx + xy*xy), sqrt(yy*yy + yx*yx));
|
||||
decompose();
|
||||
return dist_doc * f_max;
|
||||
}
|
||||
|
||||
double phys2doc_min(double dist_doc) {
|
||||
decompose();
|
||||
|
||||
if (f_min == 0)
|
||||
return std::nan("9");
|
||||
|
||||
return dist_doc / f_min;
|
||||
}
|
||||
|
||||
double phys2doc_max(double dist_doc) {
|
||||
decompose();
|
||||
|
||||
if (f_max == 0)
|
||||
return std::nan("9");
|
||||
|
||||
return dist_doc / f_max;
|
||||
}
|
||||
|
||||
d2p doc2phys(const d2p p) {
|
||||
|
@ -181,6 +224,7 @@ namespace gerbolyze {
|
|||
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
@ -210,9 +254,17 @@ namespace gerbolyze {
|
|||
}
|
||||
|
||||
void phys2doc_clipper(ClipperLib::Path &path) {
|
||||
xform2d copy(*this);
|
||||
bool inverted = false;
|
||||
copy.invert(&inverted);
|
||||
if (!inverted) {
|
||||
path.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
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}));
|
||||
[this, ©](ClipperLib::IntPoint p) -> ClipperLib::IntPoint {
|
||||
d2p out(copy.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)
|
||||
|
@ -231,7 +283,9 @@ namespace gerbolyze {
|
|||
ostringstream os;
|
||||
os << "xform2d< " << setw(5);
|
||||
os << xx << ", " << xy << ", " << x0 << " / ";
|
||||
os << yy << ", " << yx << ", " << y0;
|
||||
os << yy << ", " << yx << ", " << y0 << " / ";
|
||||
os << "θ=" << theta << ", m=" << m << " s=(" << s_x << ", " << s_y << " | ";
|
||||
os << "f_min=" << f_min << ", f_max=" << f_max;
|
||||
os << " >";
|
||||
return os.str();
|
||||
}
|
||||
|
@ -240,5 +294,8 @@ namespace gerbolyze {
|
|||
double xx, yx,
|
||||
xy, yy,
|
||||
x0, y0;
|
||||
double theta, m, s_x, s_y;
|
||||
double f_min, f_max;
|
||||
bool decomposed = false;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -218,6 +218,7 @@ namespace gerbolyze {
|
|||
bool flip_color_interpretation = false;
|
||||
bool pattern_complete_tiles_only = false;
|
||||
bool use_apertures_for_patterns = false;
|
||||
bool do_gerber_interpolation = true;
|
||||
};
|
||||
|
||||
class RenderContext {
|
||||
|
|
|
@ -88,6 +88,9 @@ int main(int argc, char **argv) {
|
|||
{"stroke_width_cutoff", {"--min-stroke-width"},
|
||||
"Don't render strokes thinner than the given width in mm. Default: 0.01mm.",
|
||||
1},
|
||||
{"no_stroke_interpolation", {"--no-stroke-interpolation"},
|
||||
"Always outline SVG strokes as regions instead of rendering them using Geber interpolation commands where possible.",
|
||||
0},
|
||||
{"drill_test_polsby_popper_tolerance", {"--drill-test-tolerance"},
|
||||
"Tolerance for identifying circles as drills in outline mode",
|
||||
1},
|
||||
|
@ -316,7 +319,7 @@ int main(int argc, char **argv) {
|
|||
delete vec;
|
||||
|
||||
double min_feature_size = args["min_feature_size"].as<double>(0.1); /* mm */
|
||||
double geometric_tolerance = args["geometric_tolerance"].as<double>(0.1); /* mm */
|
||||
double geometric_tolerance = args["geometric_tolerance"].as<double>(0.01); /* 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);
|
||||
|
@ -451,6 +454,7 @@ int main(int argc, char **argv) {
|
|||
bool flip_svg_colors = args["flip_svg_color_interpretation"];
|
||||
bool pattern_complete_tiles_only = args["pattern_complete_tiles_only"];
|
||||
bool use_apertures_for_patterns = args["use_apertures_for_patterns"];
|
||||
bool do_gerber_interpolation = !args["no_stroke_interpolation"];
|
||||
|
||||
RenderSettings rset {
|
||||
min_feature_size,
|
||||
|
@ -464,6 +468,7 @@ int main(int argc, char **argv) {
|
|||
flip_svg_colors,
|
||||
pattern_complete_tiles_only,
|
||||
use_apertures_for_patterns,
|
||||
do_gerber_interpolation,
|
||||
};
|
||||
|
||||
SVGDocument doc;
|
||||
|
|
|
@ -253,7 +253,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
Paths stroke_open, stroke_closed;
|
||||
PolyTree ptree_fill;
|
||||
PolyTree ptree;
|
||||
double geometric_tolerance_px = ctx.mat().doc2phys_min(ctx.settings().geometric_tolerance_mm);
|
||||
double geometric_tolerance_px = ctx.mat().phys2doc_min(ctx.settings().geometric_tolerance_mm);
|
||||
load_svg_path(node, stroke_open, stroke_closed, ptree_fill, geometric_tolerance_px);
|
||||
|
||||
Paths fill_paths;
|
||||
|
@ -267,6 +267,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
bool has_stroke = stroke_color && ctx.mat().doc2phys_min(stroke_width) > ctx.settings().stroke_width_cutoff;
|
||||
|
||||
cerr << "processing svg path" << endl;
|
||||
cerr << " * " << (has_stroke ? "has stroke" : "no stroke") << " / " << (has_fill ? "has fill" : "no fill") << endl;
|
||||
cerr << " * " << fill_paths.size() << " fill paths" << endl;
|
||||
cerr << " * " << stroke_closed.size() << " closed strokes" << endl;
|
||||
cerr << " * " << stroke_open.size() << " open strokes" << endl;
|
||||
|
@ -298,7 +299,8 @@ 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 = round(diameter * 1000.0) / 1000.0; /* Round to micrometer precsion; FIXME: make configurable */
|
||||
double tolerance = ctx.settings().geometric_tolerance_mm / 2;
|
||||
diameter = round(diameter/tolerance) * tolerance;
|
||||
ctx.sink() << ApertureToken(diameter) << FlashToken(centroid);
|
||||
}
|
||||
}
|
||||
|
@ -315,10 +317,10 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
c.AddPaths(ctx.clip(), ptClip, /* closed */ true);
|
||||
c.StrictlySimple(true);
|
||||
|
||||
cerr << "clipping " << fill_paths.size() << " paths, got polytree with " << ptree_fill.ChildCount() << " top-level children" << endl;
|
||||
//cerr << "clipping " << fill_paths.size() << " paths, got polytree with " << ptree_fill.ChildCount() << " top-level children" << endl;
|
||||
/* fill rules are nonzero since both subject and clip have already been normalized by clipper. */
|
||||
c.Execute(ctIntersection, ptree_fill, pftNonZero, pftNonZero);
|
||||
cerr << " > " << ptree_fill.ChildCount() << " clipped fill ptree top-level children" << endl;
|
||||
//cerr << " > " << ptree_fill.ChildCount() << " clipped fill ptree top-level children" << endl;
|
||||
}
|
||||
|
||||
/* Call out to pattern tiler for pattern fills. The path becomes the clip here. */
|
||||
|
@ -399,8 +401,9 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
|
||||
if (stroke_color != GRB_PATTERN_FILL
|
||||
&& ctx.sink().can_do_apertures()
|
||||
&& ctx.settings().do_gerber_interpolation
|
||||
/* check if we have an uniform transform */
|
||||
&& ctx.mat().doc2phys_skew(stroke_width) < ctx.settings().geometric_tolerance_mm) {
|
||||
&& ctx.mat().doc2phys_skew_ok(stroke_width, 0.05, 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;
|
||||
|
@ -411,7 +414,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
offx.MiterLimit = 10;
|
||||
offx.AddPaths(ctx.clip(), jtRound, etClosedPolygon);
|
||||
PolyTree clip_ptree;
|
||||
offx.Execute(clip_ptree, -0.5 * stroke_width * clipper_scale);
|
||||
offx.Execute(clip_ptree, -0.5 * ctx.mat().doc2phys_dist(stroke_width) * clipper_scale);
|
||||
|
||||
Paths dilated_clip;
|
||||
ClosedPathsFromPolyTree(clip_ptree, dilated_clip);
|
||||
|
@ -426,7 +429,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
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;
|
||||
// cerr << " > " << ptree.ChildCount() << " clipped stroke ptree top-level children" << endl;
|
||||
|
||||
/* Did any part of the path clip the clip path (which defaults to the document border)? */
|
||||
bool nothing_clipped = ptree.Total() == 0;
|
||||
|
@ -445,14 +448,18 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
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 << " ends_can_be_mapped=" << ends_can_be_mapped << ", nothing_clipped=" << nothing_clipped << ", joins_can_be_mapped=" << joins_can_be_mapped << endl;
|
||||
|
||||
// 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);
|
||||
//cerr << " -> converting directly" << endl;
|
||||
ctx.mat().doc2phys_clipper(stroke_closed);
|
||||
ctx.mat().doc2phys_clipper(stroke_open);
|
||||
|
||||
ctx.sink() << ApertureToken(ctx.mat().doc2phys_dist(stroke_width));
|
||||
for (auto &path : stroke_closed) {
|
||||
if (path.empty()) {
|
||||
continue;
|
||||
|
@ -464,7 +471,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
ctx.sink() << stroke_open;
|
||||
return;
|
||||
}
|
||||
// cerr << " -> NOT converting directly" << endl;
|
||||
//cerr << " -> NOT converting directly" << endl;
|
||||
/* else fall through to normal processing */
|
||||
}
|
||||
|
||||
|
@ -472,7 +479,12 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
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;
|
||||
//cerr << " offsetting " << stroke_closed.size() << " closed and " << stroke_open.size() << " open paths" << endl;
|
||||
//cerr << " geometric tolerance = " << ctx.settings().geometric_tolerance_mm << " mm" << endl;
|
||||
//cerr << " arc tolerance = " << offx.ArcTolerance/clipper_scale << " px" << endl;
|
||||
//cerr << " stroke_width=" << stroke_width << "px" << endl;
|
||||
//cerr << " offset = " << (0.5 * stroke_width * clipper_scale) << endl;
|
||||
|
||||
/* 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);
|
||||
|
@ -483,6 +495,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
/* 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()) {
|
||||
//cerr << " Clipping polytree" << endl;
|
||||
Paths outline_paths;
|
||||
PolyTreeToPaths(ptree, outline_paths);
|
||||
|
||||
|
@ -518,6 +531,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
|
|||
dehole_polytree(ptree, s_polys);
|
||||
ctx.mat().doc2phys_clipper(s_polys);
|
||||
/* color has alredy been pushed above. */
|
||||
//cerr << " sinking " << s_polys.size() << " paths" << endl;
|
||||
ctx.sink() << ApertureToken() << s_polys;
|
||||
}
|
||||
}
|
||||
|
@ -590,10 +604,11 @@ 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.geometric_tolerance_mm);
|
||||
load_svg_path(child, _stroke_open, _stroke_closed, ptree_fill, rset.geometric_tolerance_mm);
|
||||
|
||||
Paths paths;
|
||||
PolyTreeToPaths(ptree_fill, paths);
|
||||
child_xf.doc2phys_clipper(paths);
|
||||
c.AddPaths(paths, ptSubject, /* closed */ false);
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue