From 3e323c953c1916b6d342f01e1a28916b86b692c9 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Feb 2021 14:59:44 +0100 Subject: Add workaround to clipper zero-height path bug --- README.rst | 6 ++++++ gerbolyze/gerbolyze.py | 34 ++++++++++++++++++++++++++-------- svg-flatten/src/out_dilater.cpp | 2 +- svg-flatten/src/svg_path.cpp | 39 ++++++++++++++++++++++++++++++++++----- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 3cfd64a..f2dd4c4 100644 --- a/README.rst +++ b/README.rst @@ -269,6 +269,12 @@ Options: ``-b, --bottom TEXT`` Bottom side SVG overlay input file. At least one of this and ``--top`` should be given. +``--layer-top`` + Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk. + +``--layer-bottom`` + Bottom side SVG or PNG target layer. See ``--layer-top``. + ``--bbox TEXT`` Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber coördinates. This **must match the ``--bbox`` value given to diff --git a/gerbolyze/gerbolyze.py b/gerbolyze/gerbolyze.py index 297789c..396ca18 100755 --- a/gerbolyze/gerbolyze.py +++ b/gerbolyze/gerbolyze.py @@ -54,16 +54,15 @@ def vectorize(ctx, side, layer, exact, source, target, image, trace_space): @click.argument('output_gerbers') @click.option('-t', '--top', help='Top side SVG or PNG overlay') @click.option('-b', '--bottom', help='Bottom side SVG or PNG overlay') -@click.option('--layer-top', help='Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk') -@click.option('--layer-bottom', help='Bottom side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk') +@click.option('--layer-top', help='Top side SVG or PNG target layer. Default: Map SVG layers to Gerber layers, map PNG to Silk.') +@click.option('--layer-bottom', help='Bottom side SVG or PNG target layer. See --layer-top.') @click.option('--bbox', help='Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR ' '"x,y,w,h" to force [w] mm by [h] mm output canvas with its bottom left corner at the given input gerber ' 'coördinates. MUST MATCH --bbox GIVEN TO PREVIEW') @click.option('--dilate', default=0.1, help='Default dilation for subtraction operations in mm') @click.option('--no-subtract', 'no_subtract', flag_value=True, help='Disable subtraction') @click.option('--subtract', help='Use user subtraction script from argument (see description above)') -#@click.option('--mask-clips-silk/--silk-clips-mask', default=True, help='Set clipping order of mask and silk') -#@click.option('--copper-clips-copper/--no-copper-clips-copper', default=True, help='Set whether output copper features clip input copper features') +@click.option('--input-on-top/--overlay-on-top', default=True, help='Set paint order of input and overlay') @click.option('--trace-space', type=float, default=0.1, help='passed through to svg-flatten') @click.option('--vectorizer', help='passed through to svg-flatten') @click.option('--vectorizer-map', help='passed through to svg-flatten') @@ -74,6 +73,7 @@ def paste(input_gerbers, output_gerbers, bbox, dilate, no_subtract, subtract, preserve_aspect_ratio, + input_on_top, trace_space, vectorizer, vectorizer_map, exclude_groups): """ Render vector data and raster images from SVG file into gerbers. """ @@ -93,8 +93,14 @@ def paste(input_gerbers, output_gerbers, matches = match_gerbers_in_dir(source) if input_gerbers.is_dir(): + # Create output dir if it does not exist yet output_gerbers.mkdir(exist_ok=True) + # In case output dir already existed, remove files we will overwrite + for in_file in source.iterdir(): + out_cand = output_gerbers / in_file.name + out_cand.unlink(missing_ok=True) + for side, in_svg_or_png, target_layer in [ ('top', top, layer_top), ('bottom', bottom, layer_bottom)]: @@ -128,7 +134,7 @@ def paste(input_gerbers, output_gerbers, @functools.lru_cache() def do_dilate(layer, amount): print('dilating', layer, 'by', amount) - outfile = tmpdir / 'dilated-{layer}-{amount}.gbr' + outfile = tmpdir / f'dilated-{layer}-{amount}.gbr' dilate_gerber(layers, layer, amount, bbox, tmpdir, outfile, units) gbr = gerberex.read(str(outfile)) gbr.offset(bounds[0][0], bounds[1][0]) @@ -163,15 +169,21 @@ def paste(input_gerbers, output_gerbers, print('compositing') comp = gerberex.GerberComposition() - foo = gerberex.rs274x.GerberFile.from_gerber_file(in_grb.cam_source) - comp.merge(foo) + if not input_on_top: + # input below everything + comp.merge(gerberex.rs274x.GerberFile.from_gerber_file(in_grb.cam_source)) + # overlay on bottom overlay_grb.offset(bounds[0][0], bounds[1][0]) comp.merge(overlay_grb) + # dilated subtract layers on top of overlay dilations = subtract_map.get(layer, []) for d_layer, amount in dilations: print('processing dilation', d_layer, amount) dilated = do_dilate(d_layer, amount) comp.merge(dilated) + if input_on_top: + # input on top of everything + comp.merge(gerberex.rs274x.GerberFile.from_gerber_file(in_grb.cam_source)) if input_gerbers.is_dir(): this_out = output_gerbers / in_grb_path.name @@ -270,6 +282,9 @@ def template(input, top, bottom, bbox, vector, raster_dpi): DEFAULT_SUB_SCRIPT = ''' out.silk -= in.mask +out.silk -= in.silk+0.5 +out.mask -= in.mask+0.5 +out.copper -= in.copper+0.5 ''' def parse_subtract_script(script, default_dilation=0.1): @@ -578,11 +593,12 @@ def dilate_gerber(layers, layer_name, dilation, bbox, tmpdir, outfile, units): f'--origin={origin_x:.6f}x{origin_y:.6f}', f'--window_inch={width:.6f}x{height:.6f}', '--foreground=#ffffff', '-o', str(tmpfile), str(path)] + print('dilation cmd:', ' '.join(cmd)) subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # dilate & render back to gerber # TODO: the scale parameter is a hack. ideally we would fix svg-flatten to handle input units correctly. - svg_to_gerber(tmpfile, outfile, dilate=-dilation, dpi=72, scale=25.4/72.0) + svg_to_gerber(tmpfile, outfile, dilate=-dilation*72.0/25.4, dpi=72, scale=25.4/72.0) def svg_to_gerber(infile, outfile, layer=None, trace_space:'mm'=0.1, @@ -641,9 +657,11 @@ def svg_to_gerber(infile, outfile, args += [str(infile), str(outfile)] + print('svg-flatten args:', ' '.join(args)) for candidate in candidates: try: res = subprocess.run([candidate, *args], check=True) + print('used svg-flatten:', candidate) break except FileNotFoundError: continue diff --git a/svg-flatten/src/out_dilater.cpp b/svg-flatten/src/out_dilater.cpp index 1ac1040..e9aefa8 100644 --- a/svg-flatten/src/out_dilater.cpp +++ b/svg-flatten/src/out_dilater.cpp @@ -58,7 +58,7 @@ Dilater &Dilater::operator<<(const Polygon &poly) { } ClipperLib::ClipperOffset offx; - offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */ + offx.ArcTolerance = 0.05 * clipper_scale; /* 10µm; TODO: Make this configurable */ offx.AddPath(poly_c, ClipperLib::jtRound, ClipperLib::etClosedPolygon); double dilation = m_dilation; if (m_current_polarity == GRB_POL_CLEAR) { diff --git a/svg-flatten/src/svg_path.cpp b/svg-flatten/src/svg_path.cpp index 537b5dd..e45a0c9 100644 --- a/svg-flatten/src/svg_path.cpp +++ b/svg-flatten/src/svg_path.cpp @@ -32,13 +32,14 @@ static void clipper_add_cairo_path(cairo_t *cr, ClipperLib::Clipper &c, bool clo c.AddPaths(in_poly, ClipperLib::ptSubject, closed); } -static void path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c, const pugi::char_t *path_data) { +static bool path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c, const pugi::char_t *path_data) { istringstream d(path_data); string cmd; double x, y, c1x, c1y, c2x, c2y; bool first = true; + bool has_closed = false; bool path_is_empty = true; while (!d.eof()) { d >> cmd; @@ -48,6 +49,7 @@ static void path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c, const if (cmd == "Z") { /* Close path */ cairo_close_path(cr); clipper_add_cairo_path(cr, c, /* closed= */ true); + has_closed = true; cairo_new_path(cr); path_is_empty = true; @@ -61,8 +63,9 @@ static void path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c, const */ cairo_user_to_device(cr, &x, &y); assert (!d.fail()); - if (!first) + if (!first) { clipper_add_cairo_path(cr, c, /* closed= */ false); + } cairo_new_path (cr); path_is_empty = true; cairo_move_to(cr, x, y); @@ -93,6 +96,8 @@ static void path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c, const cairo_close_path(cr); clipper_add_cairo_path(cr, c, /* closed= */ false); } + + return has_closed; } void gerbolyze::load_svg_path(cairo_t *cr, const pugi::xml_node &node, ClipperLib::PolyTree &ptree) { @@ -107,9 +112,33 @@ void gerbolyze::load_svg_path(cairo_t *cr, const pugi::xml_node &node, ClipperLi ClipperLib::Clipper c; c.StrictlySimple(true); - path_to_clipper_via_cairo(cr, c, path_data); - /* We canont clip the polygon here since that would produce incorrect results for our stroke. */ - c.Execute(ClipperLib::ctUnion, ptree, fill_rule, ClipperLib::pftNonZero); + bool has_closed = path_to_clipper_via_cairo(cr, c, path_data); + + if (!has_closed) { + /* FIXME: Workaround! + * + * When we render silkscreen layers from gerbv's output, we get a lot of two-point paths (lines). Many of these are + * horizontal. Now, clipper seems to have a bug (probably related to its scan-line algorithm) that makes it + * misbehave here: + * + * It seems that when the input paths are all perfectly colinear and horizontal, so that the resulting bounding box + * has zero height, clipper doesn't output anything. At least for open input paths. + * + * Since there is no way to get paths out of a Clipper once they're Add'ed, we work around this by just doing an + * intersection with a maximum-size rectangle instead, that seems to work. + * + * TODO: Fix clipper instead. + */ + auto le_min = -ClipperLib::loRange; + auto le_max = ClipperLib::hiRange; + ClipperLib::Path p = {{le_min, le_min}, {le_max, le_min}, {le_max, le_max}, {le_min, le_max}}; + c.AddPath(p, ClipperLib::ptClip, /* closed= */ true); + c.Execute(ClipperLib::ctIntersection, ptree, fill_rule, ClipperLib::pftNonZero); + + } else { + /* We cannot clip the polygon here since that would produce incorrect results for our stroke. */ + c.Execute(ClipperLib::ctUnion, ptree, fill_rule, ClipperLib::pftNonZero); + } } void gerbolyze::parse_dasharray(const pugi::xml_node &node, vector &out) { -- cgit