summaryrefslogtreecommitdiff
path: root/gerbonara/gerber
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber')
-rw-r--r--gerbonara/gerber/cam.py9
-rwxr-xr-xgerbonara/gerber/excellon.py7
-rw-r--r--gerbonara/gerber/graphic_objects.py32
-rw-r--r--gerbonara/gerber/graphic_primitives.py15
-rw-r--r--gerbonara/gerber/rs274x.py30
-rw-r--r--gerbonara/gerber/tests/test_rs274x.py23
6 files changed, 80 insertions, 36 deletions
diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py
index 554491d..7283316 100644
--- a/gerbonara/gerber/cam.py
+++ b/gerbonara/gerber/cam.py
@@ -168,6 +168,8 @@ class CamFile:
max_x = svg_unit(max_x, arg_unit)
max_y = svg_unit(max_y, arg_unit)
+ content_min_x, content_min_y = min_x, min_y
+ content_w, content_h = max_x - min_x, max_y - min_y
if margin:
margin = svg_unit(margin, arg_unit)
min_x -= margin
@@ -201,7 +203,7 @@ class CamFile:
tags.append(polyline.to_svg(tag, fg, bg))
# setup viewport transform flipping y axis
- xform = f'translate({min_x} {min_y+h}) scale(1 -1) translate({-min_x} {-min_y})'
+ xform = f'translate({content_min_x} {content_min_y+content_h}) scale(1 -1) translate({-content_min_x} {-content_min_y})'
svg_unit = 'in' if svg_unit == 'inch' else 'mm'
# TODO export apertures as <uses> where reasonable.
@@ -231,5 +233,10 @@ class CamFile:
max_x = max(x1 for (x0, y0), (x1, y1) in bounds)
max_y = max(y1 for (x0, y0), (x1, y1) in bounds)
+ #for p in self.objects:
+ # bb = (o_min_x, o_min_y), (o_max_x, o_max_y) = p.bounding_box(unit)
+ # if o_min_x == min_x or o_min_y == min_y or o_max_x == max_x or o_max_y == max_y:
+ # print('\033[91m bounds\033[0m', bb, p)
+
return ((min_x, min_y), (max_x, max_y))
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py
index 2e0add3..b5c9221 100755
--- a/gerbonara/gerber/excellon.py
+++ b/gerbonara/gerber/excellon.py
@@ -108,7 +108,7 @@ class ExcellonFile(CamFile):
self.generator_hints = generator_hints or [] # This is a purely informational goodie from the parser. Use it as you wish.
def __bool__(self):
- return bool(self.objects)
+ return not self.is_empty
@property
def is_plated(self):
@@ -257,8 +257,9 @@ class ExcellonFile(CamFile):
def is_nonplated(self):
return not any(obj.plated for obj in self.objects)
- def empty(self):
- return self.objects.empty()
+ @property
+ def is_empty(self):
+ return not self.objects
def __len__(self):
return len(self.objects)
diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py
index 1f357da..131bf57 100644
--- a/gerbonara/gerber/graphic_objects.py
+++ b/gerbonara/gerber/graphic_objects.py
@@ -226,7 +226,8 @@ class Line(GerberObject):
def to_primitives(self, unit=None):
conv = self.converted(unit)
- yield gp.Line(*conv.p1, *conv.p2, self.aperture.equivalent_width(unit), polarity_dark=self.polarity_dark)
+ w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
+ yield gp.Line(*conv.p1, *conv.p2, w, polarity_dark=self.polarity_dark)
def to_statements(self, gs):
yield from gs.set_polarity(self.polarity_dark)
@@ -277,16 +278,22 @@ class Arc(GerberObject):
return abs(r1 - r2)
def sweep_angle(self):
- f = math.atan2(self.x2, self.y2) - math.atan2(self.x1, self.y1)
- f = (f + math.pi) % (2*math.pi) - math.pi
-
- if self.clockwise:
- f = -f
-
- if f > math.pi:
- f = 2*math.pi - f
-
- return f
+ cx, cy = self.cx + self.x1, self.cy + self.y1
+ x1, y1 = self.x1 - cx, self.y1 - cy
+ x2, y2 = self.x2 - cx, self.y2 - cy
+
+ a1, a2 = math.atan2(y1, x1), math.atan2(y2, x2)
+ f = abs(a2 - a1)
+ if not self.clockwise:
+ if a2 > a1:
+ return a2 - a1
+ else:
+ return 2*math.pi - abs(a2 - a1)
+ else:
+ if a1 > a2:
+ return a1 - a2
+ else:
+ return 2*math.pi - abs(a1 - a2)
@property
def p1(self):
@@ -329,11 +336,12 @@ class Arc(GerberObject):
def to_primitives(self, unit=None):
conv = self.converted(unit)
+ w = self.aperture.equivalent_width(unit) if self.aperture else 0.1 # for debugging
yield gp.Arc(x1=conv.x1, y1=conv.y1,
x2=conv.x2, y2=conv.y2,
cx=conv.cx, cy=conv.cy,
clockwise=self.clockwise,
- width=self.aperture.equivalent_width(unit),
+ width=w,
polarity_dark=self.polarity_dark)
def to_statements(self, gs):
diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py
index 07ceb5c..401156e 100644
--- a/gerbonara/gerber/graphic_primitives.py
+++ b/gerbonara/gerber/graphic_primitives.py
@@ -89,8 +89,12 @@ def arc_bounds(x1, y1, x2, y2, cx, cy, clockwise):
#
# This solution manages to handle circular arcs given in gerber format (with explicit center and endpoints, plus
# sweep direction instead of a format with e.g. angles and radius) without any trigonometric functions (e.g. atan2).
+ #
+ # cx, cy are relative to p1.
# Center arc on cx, cy
+ cx += x1
+ cy += y1
x1 -= cx
x2 -= cx
y1 -= cy
@@ -212,7 +216,7 @@ class ArcPoly(GraphicPrimitive):
for (x1, y1), (x2, y2), arc in self.segments:
if arc:
clockwise, (cx, cy) = arc
- bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, (cx+x1, cy+y1), clockwise))
+ bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, cx, cy, clockwise))
else:
line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2))
@@ -277,7 +281,8 @@ class Polyline:
(x0, y0), *rest = self.coords
d = f'M {x0:.6} {y0:.6} ' + ' '.join(f'L {x:.6} {y:.6}' for x, y in rest)
- return tag('path', d=d, style=f'fill: none; stroke: {color}; stroke-width: {self.width:.6}; stroke-linecap: round')
+ width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
+ return tag('path', d=d, style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round')
@dataclass
class Line(GraphicPrimitive):
@@ -293,8 +298,9 @@ class Line(GraphicPrimitive):
def to_svg(self, tag, fg, bg):
color = fg if self.polarity_dark else bg
+ width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} L {self.x2:.6} {self.y2:.6}',
- style=f'fill: none; stroke: {color}; stroke-width: {self.width:.6}; stroke-linecap: round')
+ style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round')
@dataclass
class Arc(GraphicPrimitive):
@@ -330,8 +336,9 @@ class Arc(GraphicPrimitive):
def to_svg(self, tag, fg, bg):
color = fg if self.polarity_dark else bg
arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
+ width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
return tag('path', d=f'M {self.x1:.6} {self.y1:.6} {arc}',
- style=f'fill: none; stroke: {color}; stroke-width: {self.width:.6}; stroke-linecap: round; fill: none')
+ style=f'fill: none; stroke: {color}; stroke-width: {width}; stroke-linecap: round; fill: none')
def svg_rotation(angle_rad, cx=0, cy=0):
return f'rotate({float(rad_to_deg(angle_rad)):.4} {float(cx):.6} {float(cy):.6})'
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index cba08fc..db93fb5 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -54,7 +54,7 @@ class GerberFile(CamFile):
"""
def __init__(self, objects=None, comments=None, import_settings=None, filename=None, generator_hints=None,
- layer_hints=None):
+ layer_hints=None, file_attrs=None):
super().__init__(filename=filename)
self.objects = objects or []
self.comments = comments or []
@@ -62,6 +62,7 @@ class GerberFile(CamFile):
self.layer_hints = layer_hints or []
self.import_settings = import_settings
self.apertures = [] # FIXME get rid of this? apertures are already in the objects.
+ self.file_attrs = file_attrs or {}
def to_excellon(self):
new_objs = []
@@ -125,7 +126,6 @@ class GerberFile(CamFile):
seen_macro_names.add(new_name)
def dilate(self, offset, unit=MM, polarity_dark=True):
-
self.apertures = [ aperture.dilated(offset, unit) for aperture in self.apertures ]
offset_circle = CircleAperture(offset, unit=unit)
@@ -166,6 +166,9 @@ class GerberFile(CamFile):
def generate_statements(self, settings, drop_comments=True):
yield 'G04 Gerber file generated by Gerbonara*'
+ for name, value in self.file_attrs.items():
+ attrdef = ','.join([name, *map(str, value)])
+ yield f'%TF{attrdef}*%'
yield '%MOMM*%' if (settings.unit == 'mm') else '%MOIN*%'
zeros = 'T' if settings.zeros == 'trailing' else 'L' # default to leading if "None" is specified
@@ -224,6 +227,16 @@ class GerberFile(CamFile):
settings.number_format = (5,6)
return '\n'.join(self.generate_statements(settings))
+ @property
+ def is_empty(self):
+ return not self.objects
+
+ def __len__(self):
+ return len(self.objects)
+
+ def __bool__(self):
+ return not self.is_empty
+
def offset(self, dx=0, dy=0, unit=MM):
# TODO round offset to file resolution
@@ -275,9 +288,7 @@ class GraphicsState:
self.image_axes = 'AXBY' # AS axis mapping; deprecated
self._mat = None
self.file_settings = file_settings
- if aperture_map is not None:
- self.aperture_map = aperture_map
- self.aperture_map = {}
+ self.aperture_map = aperture_map or {}
def __setattr__(self, name, value):
# input validation
@@ -408,7 +419,11 @@ class GraphicsState:
arc = lambda cx, cy: go.Arc(*old_point, *new_point, cx, cy,
clockwise=clockwise, aperture=(self.aperture if aperture else None),
polarity_dark=self.polarity_dark, unit=self.file_settings.unit, attrs=attrs)
+ print('arcs:')
arcs = [ arc(cx, cy), arc(-cx, cy), arc(cx, -cy), arc(-cx, -cy) ]
+ for a in arcs:
+ print(f'{a.sweep_angle()=} {a.numeric_error()=} {a}')
+ print(GerberFile(arcs).to_svg())
arcs = [ a for a in arcs if a.sweep_angle() <= math.pi/2 ]
arcs = sorted(arcs, key=lambda a: a.numeric_error())
return arcs[0]
@@ -585,6 +600,7 @@ class GerberParser:
self.target.apertures = list(self.aperture_map.values())
self.target.import_settings = self.file_settings
self.target.unit = self.file_settings.unit
+ self.target.file_attrs = self.file_attrs
if not self.eof_found:
warnings.warn('File is missing mandatory M02 EOF marker. File may be truncated.', SyntaxWarning)
@@ -863,7 +879,7 @@ class GerberParser:
warnings.warn(f'Deprecated {match["mode"]} notation mode statement found. This deprecated since 2012.', DeprecationWarning)
self.target.comments.append('Replaced deprecated {match["mode"]} notation mode statement with FS statement')
- def _parse_attribtue(self, match):
+ def _parse_attribute(self, match):
if match['type'] == 'TD':
if match['value']:
raise SyntaxError('TD attribute deletion command must not contain attribute fields')
@@ -886,7 +902,7 @@ class GerberParser:
target = {'TF': self.file_attrs, 'TO': self.object_attrs, 'TA': self.aperture_attrs}[match['type']]
target[match['name']] = match['value'].split(',')
- if 'eagle' in self.file_attrs.get('.GenerationSoftware', '').lower() or match['eagle_garbage']:
+ if 'EAGLE' in self.file_attrs.get('.GenerationSoftware', []) or match['eagle_garbage']:
self.generator_hints.append('eagle')
def _parse_eof(self, _match):
diff --git a/gerbonara/gerber/tests/test_rs274x.py b/gerbonara/gerber/tests/test_rs274x.py
index 06150de..b7ddbd4 100644
--- a/gerbonara/gerber/tests/test_rs274x.py
+++ b/gerbonara/gerber/tests/test_rs274x.py
@@ -456,19 +456,22 @@ def test_svg_export(reference, tmpfile):
@filter_syntax_warnings
@pytest.mark.parametrize('reference', REFERENCE_FILES, indirect=True)
def test_bounding_box(reference, tmpfile):
+ if reference.name == 'MinnowMax_assy.art':
+ # This leads to worst-case performance in resvg, this testcase takes over 1h to finish. So skip.
+ pytest.skip()
# skip this check on files that contain lines with a zero-size aperture at the board edge
if any(reference.match(f'*/{f}') for f in HAS_ZERO_SIZE_APERTURES):
pytest.skip()
- # skip this file because it does not contain any graphical objects
- if reference.match('*/multiline_read.ger'):
- pytest.skip()
-
margin = 1.0 # inch
dpi = 200
margin_px = int(dpi*margin) # intentionally round down to avoid aliasing artifacts
grb = GerberFile.open(reference)
+
+ if grb.is_empty:
+ pytest.skip()
+
out_svg = tmpfile('Output', '.svg')
with open(out_svg, 'w') as f:
f.write(str(grb.to_svg(margin=margin, arg_unit='inch', fg='white', bg='black')))
@@ -484,11 +487,13 @@ def test_bounding_box(reference, tmpfile):
rows = img.sum(axis=0)
col_prefix, col_suffix = np.argmax(cols > 0), np.argmax(cols[::-1] > 0)
row_prefix, row_suffix = np.argmax(rows > 0), np.argmax(rows[::-1] > 0)
+ print('cols:', col_prefix, col_suffix)
+ print('rows:', row_prefix, row_suffix)
# Check that all margins are completely black and that the content touches the margins. Allow for some tolerance to
- # allow for antialiasing artifacts.
- assert margin_px-1 <= col_prefix <= margin_px+1
- assert margin_px-1 <= col_suffix <= margin_px+1
- assert margin_px-1 <= row_prefix <= margin_px+1
- assert margin_px-1 <= row_suffix <= margin_px+1
+ # allow for antialiasing artifacts and for things like very thin features.
+ assert margin_px-2 <= col_prefix <= margin_px+2
+ assert margin_px-2 <= col_suffix <= margin_px+2
+ assert margin_px-2 <= row_prefix <= margin_px+2
+ assert margin_px-2 <= row_suffix <= margin_px+2