aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst6
-rwxr-xr-xgerbolyze/gerbolyze.py34
-rw-r--r--svg-flatten/src/out_dilater.cpp2
-rw-r--r--svg-flatten/src/svg_path.cpp39
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<double> &out) {