Add workaround to clipper zero-height path bug
This commit is contained in:
parent
13cb49b218
commit
3e323c953c
4 changed files with 67 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue