Fix parsing for multiline ipc-d-356 records
This commit is contained in:
parent
c40683b6a2
commit
68619d4d5a
8 changed files with 218 additions and 55 deletions
132
gerber/ipc356.py
132
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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue