summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHamilton Kibbe <hamilton.kibbe@gmail.com>2015-03-05 22:42:42 -0500
committerHamilton Kibbe <hamilton.kibbe@gmail.com>2015-03-05 22:42:42 -0500
commit68619d4d5a7beb38dc81d953b43bf4196ca1d3a6 (patch)
treedbc6a5ebe7403c95d9c37b0ef338d4eb60e4980a
parentc40683b6a216f29fe473c31680ade7ab294002cd (diff)
downloadgerbonara-68619d4d5a7beb38dc81d953b43bf4196ca1d3a6.tar.gz
gerbonara-68619d4d5a7beb38dc81d953b43bf4196ca1d3a6.tar.bz2
gerbonara-68619d4d5a7beb38dc81d953b43bf4196ca1d3a6.zip
Fix parsing for multiline ipc-d-356 records
-rw-r--r--gerber/ipc356.py132
-rw-r--r--gerber/primitives.py63
-rw-r--r--gerber/render/cairo_backend.py12
-rw-r--r--gerber/render/render.py6
-rw-r--r--gerber/tests/resources/ipc-d-356.ipc1
-rw-r--r--gerber/tests/test_ipc356.py2
-rw-r--r--gerber/tests/test_primitives.py4
-rw-r--r--gerber/utils.py53
8 files changed, 218 insertions, 55 deletions
diff --git a/gerber/ipc356.py b/gerber/ipc356.py
index 2b6f1f6..1762480 100644
--- a/gerber/ipc356.py
+++ b/gerber/ipc356.py
@@ -18,7 +18,8 @@
import math
import re
-from .cam import FileSettings
+from .cam import CamFile, FileSettings
+from .primitives import TestRecord
# Net Name Variables
_NNAME = re.compile(r'^NNAME\d+$')
@@ -44,7 +45,7 @@ def read(filename):
return IPC_D_356.from_file(filename)
-class IPC_D_356(object):
+class IPC_D_356(CamFile):
@classmethod
def from_file(self, filename):
@@ -52,10 +53,12 @@ class IPC_D_356(object):
return p.parse(filename)
- def __init__(self, statements, settings):
+ def __init__(self, statements, settings, primitives=None):
self.statements = statements
self.units = settings.units
self.angle_units = settings.angle_units
+ self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name,
+ rec.access) for rec in self.test_records]
@property
def settings(self):
@@ -98,6 +101,19 @@ class IPC_D_356(object):
else:
return None
+
+ def render(self, ctx, layer='both', filename=None):
+ for p in self.primitives:
+ if layer == 'both' and p.layer in ('top', 'bottom', 'both'):
+ ctx.render(p)
+ elif layer == 'top' and p.layer in ('top', 'both'):
+ ctx.render(p)
+ elif layer == 'bottom' and p.layer in ('bottom', 'both'):
+ ctx.render(p)
+ if filename is not None:
+ ctx.dump(filename)
+
+
class IPC_D_356_Parser(object):
# TODO: Allow multi-line statements (e.g. Altium board edge)
def __init__(self):
@@ -112,51 +128,68 @@ class IPC_D_356_Parser(object):
def parse(self, filename):
with open(filename, 'r') as f:
+ oldline = ''
for line in f:
-
- if line[0] == 'C':
- # Comment
- self.statements.append(IPC356_Comment.from_line(line))
-
- elif line[0] == 'P':
- # Parameter
- p = IPC356_Parameter.from_line(line)
- if p.parameter == 'UNITS':
- if p.value in ('CUST', 'CUST 0'):
- self.units = 'inch'
- self.angle_units = 'degrees'
- elif p.value == 'CUST 1':
- self.units = 'metric'
- self.angle_units = 'degrees'
- elif p.value == 'CUST 2':
- self.units = 'inch'
- self.angle_units = 'radians'
- self.statements.append(p)
- if _NNAME.match(p.parameter):
- # Add to list of net name variables
- self.nnames[p.parameter] = p.value
-
- elif line[0] == '3' and line[2] == '7':
- # Test Record
- record = IPC356_TestRecord.from_line(line, self.settings)
-
- # Substitute net name variables
- net = record.net_name
- if (_NNAME.match(net) and net in self.nnames.keys()):
- record.net_name = self.nnames[record.net_name]
- self.statements.append(record)
-
- elif line[0:3] == '389':
- # Altium Board Edge Info
- self.statements.append(IPC356_BoardEdge.from_line(line, self.settings))
-
- elif line[0] == '9':
- self.multiline = False
- self.statements.append(IPC356_EndOfFile())
+ # Check for existing multiline data...
+ if oldline != '':
+ if len(line) and line[0] == '0':
+ oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
+ else:
+ self._parse_line(oldline)
+ oldline = line
+ else:
+ oldline = line
+ self._parse_line(oldline)
return IPC_D_356(self.statements, self.settings)
+ def _parse_line(self, line):
+ if not len(line):
+ return
+ if line[0] == 'C':
+ # Comment
+ self.statements.append(IPC356_Comment.from_line(line))
+
+ elif line[0] == 'P':
+ # Parameter
+ p = IPC356_Parameter.from_line(line)
+ if p.parameter == 'UNITS':
+ if p.value in ('CUST', 'CUST 0'):
+ self.units = 'inch'
+ self.angle_units = 'degrees'
+ elif p.value == 'CUST 1':
+ self.units = 'metric'
+ self.angle_units = 'degrees'
+ elif p.value == 'CUST 2':
+ self.units = 'inch'
+ self.angle_units = 'radians'
+ self.statements.append(p)
+ if _NNAME.match(p.parameter):
+ # Add to list of net name variables
+ self.nnames[p.parameter] = p.value
+
+ elif line[0] == '9':
+ self.statements.append(IPC356_EndOfFile())
+
+ elif line[0:3] in ('317', '327', '367'):
+ # Test Record
+ record = IPC356_TestRecord.from_line(line, self.settings)
+
+ # Substitute net name variables
+ net = record.net_name
+ if (_NNAME.match(net) and net in self.nnames.keys()):
+ record.net_name = self.nnames[record.net_name]
+ self.statements.append(record)
+
+ elif line[0:3] == '379':
+ # Net Adjacency Info
+ pass
+ elif line[0:3] == '389':
+ # Altium Board Edge Info
+ self.statements.append(IPC356_BoardEdge.from_line(line, self.settings))
+
+
class IPC356_Comment(object):
@classmethod
def from_line(cls, line):
@@ -302,6 +335,19 @@ class IPC356_BoardEdge(object):
return '<IPC-D-356 Board Edge Definition>'
+class IPC356_Adjacency(object):
+
+ @classmethod
+ def from_line(cls, line):
+ nets = line.strip().split()[1:]
+ return cls(nets)
+
+ def __init__(self, nets):
+ self.nets = nets
+
+ def __repr__(self):
+ return '<IPC-D-356 Adjacency Record>'
+
class IPC356_EndOfFile(object):
def __init__(self):
diff --git a/gerber/primitives.py b/gerber/primitives.py
index 3469880..5d0b8cf 100644
--- a/gerber/primitives.py
+++ b/gerber/primitives.py
@@ -713,8 +713,7 @@ class Donut(Primitive):
"""
def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs):
super(Donut, self).__init__(**kwargs)
- if len(position) != 2:
- raise TypeError('Position must be a tuple (n=2) of coordinates')
+ validate_coordinates(position)
self.position = position
if shape not in ('round', 'square', 'hexagon', 'octagon'):
raise ValueError('Valid shapes are round, square, hexagon or octagon')
@@ -731,7 +730,6 @@ class Donut(Primitive):
self.width = 0.5 * math.sqrt(3.) * outer_diameter
self.height = outer_diameter
-
@property
def lower_left(self):
return (self.position[0] - (self.width / 2.),
@@ -755,14 +753,56 @@ class Donut(Primitive):
self.width = inch(self.width)
self.height = inch(self.height)
self.inner_diameter = inch(self.inner_diameter)
- self.outer_diaemter = inch(self.outer_diameter)
+ self.outer_diameter = inch(self.outer_diameter)
def to_metric(self):
self.position = tuple(map(metric, self.position))
self.width = metric(self.width)
self.height = metric(self.height)
self.inner_diameter = metric(self.inner_diameter)
- self.outer_diaemter = metric(self.outer_diameter)
+ self.outer_diameter = metric(self.outer_diameter)
+
+ def offset(self, x_offset=0, y_offset=0):
+ self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+
+
+class SquareRoundDonut(Primitive):
+ """ A Square with a circular cutout in the center
+ """
+ def __init__(self, position, inner_diameter, outer_diameter, **kwargs):
+ super(SquareRoundDonut, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ if inner_diameter >= outer_diameter:
+ raise ValueError('Outer diameter must be larger than inner diameter.')
+ self.inner_diameter = inner_diameter
+ self.outer_diameter = outer_diameter
+
+ @property
+ def lower_left(self):
+ return tuple([c - self.outer_diameter / 2. for c in self.position])
+
+ @property
+ def upper_right(self):
+ return tuple([c + self.outer_diameter / 2. for c in self.position])
+
+ @property
+ def bounding_box(self):
+ min_x = self.lower_left[0]
+ max_x = self.upper_right[0]
+ min_y = self.lower_left[1]
+ max_y = self.upper_right[1]
+ return ((min_x, max_x), (min_y, max_y))
+
+ def to_inch(self):
+ self.position = tuple(map(inch, self.position))
+ self.inner_diameter = inch(self.inner_diameter)
+ self.outer_diameter = inch(self.outer_diameter)
+
+ def to_metric(self):
+ self.position = tuple(map(metric, self.position))
+ self.inner_diameter = metric(self.inner_diameter)
+ self.outer_diameter = metric(self.outer_diameter)
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
@@ -773,8 +813,7 @@ class Drill(Primitive):
"""
def __init__(self, position, diameter):
super(Drill, self).__init__('dark')
- if len(position) != 2:
- raise TypeError('Position must be a tuple (n=2) of coordinates')
+ validate_coordinates(position)
self.position = position
self.diameter = diameter
@@ -801,3 +840,13 @@ class Drill(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+class TestRecord(Primitive):
+ """ Netlist Test record
+ """
+ def __init__(self, position, net_name, layer, **kwargs):
+ super(TestRecord, self).__init__(**kwargs)
+ validate_coordinates(position)
+ self.position = position
+ self.net_name = net_name
+ self.layer = layer
+
diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py
index 326f44e..fa1aecc 100644
--- a/gerber/render/cairo_backend.py
+++ b/gerber/render/cairo_backend.py
@@ -104,7 +104,7 @@ class GerberCairoContext(GerberContext):
self.ctx.fill()
def _render_circle(self, circle, color):
- center = map(mul, circle.position, self.scale)
+ center = tuple(map(mul, circle.position, self.scale))
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_line_width(0)
self.ctx.arc(*center, radius=circle.radius * SCALE, angle1=0, angle2=2 * math.pi)
@@ -126,5 +126,15 @@ class GerberCairoContext(GerberContext):
def _render_drill(self, circle, color):
self._render_circle(circle, color)
+ def _render_test_record(self, primitive, color):
+ self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+ self.ctx.set_font_size(200)
+ self._render_circle(Circle(primitive.position, 0.01), color)
+ self.ctx.set_source_rgb(*color)
+ self.ctx.move_to(*[SCALE * (coord + 0.01) for coord in primitive.position])
+ self.ctx.scale(1, -1)
+ self.ctx.show_text(primitive.net_name)
+ self.ctx.scale(1, -1)
+
def dump(self, filename):
self.surface.write_to_png(filename)
diff --git a/gerber/render/render.py b/gerber/render/render.py
index 2e4abfa..68c2115 100644
--- a/gerber/render/render.py
+++ b/gerber/render/render.py
@@ -138,9 +138,11 @@ class GerberContext(object):
elif isinstance(primitive, Obround):
self._render_obround(primitive, color)
elif isinstance(primitive, Polygon):
- self._render_polygon(Polygon, color)
+ self._render_polygon(primitive, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.drill_color)
+ elif isinstance(primitive, TestRecord):
+ self._render_test_record(primitive, color)
else:
return
@@ -168,3 +170,5 @@ class GerberContext(object):
def _render_drill(self, primitive, color):
pass
+ def _render_test_record(self, primitive, color):
+ pass
diff --git a/gerber/tests/resources/ipc-d-356.ipc b/gerber/tests/resources/ipc-d-356.ipc
index b0086c9..2ed3f49 100644
--- a/gerber/tests/resources/ipc-d-356.ipc
+++ b/gerber/tests/resources/ipc-d-356.ipc
@@ -111,4 +111,5 @@ P NNAME1 A_REALLY_LONG_NET_NAME
327VCC U4 -8 A01X 8396Y 3850X 394Y 500R 0
327NNAME1 NA -69 A01X 8396Y 3850X 394Y 500R 0
389BOARD_EDGE X0Y0 X22500 Y15000 X0
+089 X1300Y240
999
diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py
index 760608c..88726a5 100644
--- a/gerber/tests/test_ipc356.py
+++ b/gerber/tests/test_ipc356.py
@@ -25,7 +25,7 @@ def test_parser():
assert_equal(len(ipcfile.vias), 14)
assert_equal(ipcfile.test_records[-1].net_name, 'A_REALLY_LONG_NET_NAME')
assert_equal(set(ipcfile.board_outline),
- {(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5)})
+ {(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5), (0.13, 0.024)})
def test_comment():
c = IPC356_Comment('Layer Stackup:')
diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py
index 2909d8f..f3b1189 100644
--- a/gerber/tests/test_primitives.py
+++ b/gerber/tests/test_primitives.py
@@ -681,13 +681,13 @@ def test_donut_conversion():
d.to_inch()
assert_equal(d.position, (0.1, 1.0))
assert_equal(d.inner_diameter, 10.0)
- assert_equal(d.outer_diaemter, 100.0)
+ assert_equal(d.outer_diameter, 100.0)
d = Donut((0.1, 1.0), 'round', 10.0, 100.0)
d.to_metric()
assert_equal(d.position, (2.54, 25.4))
assert_equal(d.inner_diameter, 254.0)
- assert_equal(d.outer_diaemter, 2540.0)
+ assert_equal(d.outer_diameter, 2540.0)
def test_donut_offset():
d = Donut((0, 0), 'round', 1, 10)
diff --git a/gerber/utils.py b/gerber/utils.py
index 8cd4965..1c43550 100644
--- a/gerber/utils.py
+++ b/gerber/utils.py
@@ -26,6 +26,9 @@ files.
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# License:
+from math import radians, sin, cos
+from operator import sub
+
MILLIMETERS_PER_INCH = 25.4
def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'):
@@ -238,7 +241,57 @@ def validate_coordinates(position):
def metric(value):
+ """ Convert inch value to millimeters
+
+ Parameters
+ ----------
+ value : float
+ A value in inches.
+
+ Returns
+ -------
+ value : float
+ The equivalent value expressed in millimeters.
+ """
return value * MILLIMETERS_PER_INCH
def inch(value):
+ """ Convert millimeter value to inches
+
+ Parameters
+ ----------
+ value : float
+ A value in millimeters.
+
+ Returns
+ -------
+ value : float
+ The equivalent value expressed in inches.
+ """
return value / MILLIMETERS_PER_INCH
+
+
+def rotate_point(point, angle, center=(0.0, 0.0)):
+ """ Rotate a point about another point.
+
+ Parameters
+ -----------
+ point : tuple(<float>, <float>)
+ Point to rotate about origin or center point
+
+ angle : float
+ Angle to rotate the point [degrees]
+
+ center : tuple(<float>, <float>)
+ Coordinates about which the point is rotated. Defaults to the origin.
+
+ Returns
+ -------
+ rotated_point : tuple(<float>, <float>)
+ `point` rotated about `center` by `angle` degrees.
+ """
+ angle = radians(angle)
+ xdelta, ydelta = tuple(map(sub, point, center))
+ x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta)
+ y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta)
+ return (x, y)