Add workaround to clipper zero-height path bug

This commit is contained in:
jaseg 2021-02-06 14:59:44 +01:00
parent 13cb49b218
commit 3e323c953c
4 changed files with 67 additions and 14 deletions

View file

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

View file

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

View file

@ -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) {

View file

@ -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<double> &out) {