Support for the G85 slot statement

This commit is contained in:
Garret Fick 2016-03-26 15:59:42 +08:00
parent d12f6385a4
commit acde19f205
6 changed files with 322 additions and 32 deletions

View file

@ -34,7 +34,7 @@ except(ImportError):
from .excellon_statements import *
from .excellon_tool import ExcellonToolDefinitionParser
from .cam import CamFile, FileSettings
from .primitives import Drill
from .primitives import Drill, Slot
from .utils import inch, metric
@ -93,6 +93,51 @@ class DrillHit(object):
if self.tool.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
@property
def bounding_box(self):
position = self.position
radius = self.tool.diameter / 2.
min_x = position[0] - radius
max_x = position[0] + radius
min_y = position[1] - radius
max_y = position[1] + radius
return ((min_x, max_x), (min_y, max_y))
class DrillSlot(object):
"""
A slot is created between two points. The way the slot is created depends on the statement used to create it
"""
def __init__(self, tool, start, end):
self.tool = tool
self.start = start
self.end = end
def to_inch(self):
if self.tool.units == 'metric':
self.tool.to_inch()
self.start = tuple(map(inch, self.start))
self.end = tuple(map(inch, self.end))
def to_metric(self):
if self.tool.units == 'inch':
self.tool.to_metric()
self.start = tuple(map(metric, self.start))
self.end = tuple(map(metric, self.end))
@property
def bounding_box(self):
start = self.start
end = self.end
radius = self.tool.diameter / 2.
min_x = min(start[0], end[0]) - radius
max_x = max(start[0], end[0]) + radius
min_y = min(start[1], end[1]) - radius
max_y = max(start[1], end[1]) + radius
return ((min_x, max_x), (min_y, max_y))
class ExcellonFile(CamFile):
@ -131,7 +176,17 @@ class ExcellonFile(CamFile):
@property
def primitives(self):
return [Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units) for hit in self.hits]
primitives = []
for hit in self.hits:
if isinstance(hit, DrillHit):
primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
elif isinstance(hit, DrillSlot):
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
else:
raise ValueError('Unknown hit type')
return primitives
@property
@ -139,12 +194,11 @@ class ExcellonFile(CamFile):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for hit in self.hits:
radius = hit.tool.diameter / 2.
x, y = hit.position
xmin = min(x - radius, xmin)
xmax = max(x + radius, xmax)
ymin = min(y - radius, ymin)
ymax = max(y + radius, ymax)
bbox = hit.bounding_box
xmin = min(bbox[0][0], xmin)
xmax = max(bbox[0][1], xmax)
ymin = min(bbox[1][0], ymin)
ymax = max(bbox[1][1], ymax)
return ((xmin, xmax), (ymin, ymax))
def report(self, filename=None):
@ -545,26 +599,54 @@ class ExcellonParser(object):
self.active_tool._hit()
elif line[0] in ['X', 'Y']:
stmt = CoordinateStmt.from_excellon(line, self._settings())
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
if not self.active_tool:
self.active_tool = self._get_tool(1)
if 'G85' in line:
stmt = SlotStmt.from_excellon(line, self._settings())
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
# I don't know if this is actually correct, but it makes sense that this is where the tool would end
x = stmt.x_end
y = stmt.y_end
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end)))
self.active_tool._hit()
else:
stmt = CoordinateStmt.from_excellon(line, self._settings())
x = stmt.x
y = stmt.y
self.statements.append(stmt)
if self.notation == 'absolute':
if x is not None:
self.pos[0] = x
if y is not None:
self.pos[1] = y
else:
if x is not None:
self.pos[0] += x
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
if not self.active_tool:
self.active_tool = self._get_tool(1)
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))

View file

@ -37,7 +37,7 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
'NextToolSelectionStmt']
'NextToolSelectionStmt', 'SlotStmt']
class ExcellonStatement(object):
@ -645,6 +645,12 @@ class EndOfProgramStmt(ExcellonStatement):
self.y += y_offset
class UnitStmt(ExcellonStatement):
@classmethod
def from_settings(cls, settings):
"""Create the unit statement from the FileSettings"""
return cls(settings.units, settings.zeros)
@classmethod
def from_excellon(cls, line, **kwargs):
@ -827,6 +833,128 @@ class UnknownStmt(ExcellonStatement):
return "<Unknown Statement: %s>" % self.stmt
class SlotStmt(ExcellonStatement):
"""
G85 statement. Defines a slot created by multiple drills between two specified points.
Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
"""
@classmethod
def from_points(cls, start, end):
return cls(start[0], start[1], end[0], end[1])
@classmethod
def from_excellon(cls, line, settings, **kwargs):
# Split the line based on the G85 separator
sub_coords = line.split('G85')
(x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
(x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
c.units = settings.units
return c
@staticmethod
def parse_sub_coords(line, settings):
x_coord = None
y_coord = None
if line[0] == 'X':
splitline = line.strip('X').split('Y')
x_coord = parse_gerber_value(splitline[0], settings.format,
settings.zero_suppression)
if len(splitline) == 2:
y_coord = parse_gerber_value(splitline[1], settings.format,
settings.zero_suppression)
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
return (x_coord, y_coord)
def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
super(SlotStmt, self).__init__(**kwargs)
self.x_start = x_start
self.y_start = y_start
self.x_end = x_end
self.y_end = y_end
self.mode = None
def to_excellon(self, settings):
stmt = ''
if self.x_start is not None:
stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
settings.zero_suppression)
if self.y_start is not None:
stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
settings.zero_suppression)
stmt += 'G85'
if self.x_end is not None:
stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
settings.zero_suppression)
if self.y_end is not None:
stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
settings.zero_suppression)
return stmt
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
if self.x_start is not None:
self.x_start = inch(self.x_start)
if self.y_start is not None:
self.y_start = inch(self.y_start)
if self.x_end is not None:
self.x_end = inch(self.x_end)
if self.y_end is not None:
self.y_end = inch(self.y_end)
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
if self.x_start is not None:
self.x_start = metric(self.x_start)
if self.y_start is not None:
self.y_start = metric(self.y_start)
if self.x_end is not None:
self.x_end = metric(self.x_end)
if self.y_end is not None:
self.y_end = metric(self.y_end)
def offset(self, x_offset=0, y_offset=0):
if self.x_start is not None:
self.x_start += x_offset
if self.y_start is not None:
self.y_start += y_offset
if self.x_end is not None:
self.x_end += x_offset
if self.y_end is not None:
self.y_end += y_offset
def __str__(self):
start_str = ''
if self.x_start is not None:
start_str += 'X: %g ' % self.x_start
if self.y_start is not None:
start_str += 'Y: %g ' % self.y_start
end_str = ''
if self.x_end is not None:
end_str += 'X: %g ' % self.x_end
if self.y_end is not None:
end_str += 'Y: %g ' % self.y_end
return '<Slot Statement: %s to %s>' % (start_str, end_str)
def pairwise(iterator):
""" Iterate over list taking two elements at a time.

View file

@ -1109,6 +1109,42 @@ class Drill(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
class Slot(Primitive):
""" A drilled slot
"""
def __init__(self, start, end, diameter, hit, **kwargs):
super(Slot, self).__init__('dark', **kwargs)
validate_coordinates(start)
validate_coordinates(end)
self.start = start
self.end = end
self.diameter = diameter
self.hit = hit
self._to_convert = ['start', 'end', 'diameter']
@property
def flashed(self):
return False
@property
def radius(self):
return self.diameter / 2.
@property
def bounding_box(self):
radius = self.radius
min_x = min(self.start[0], self.end[0]) - radius
max_x = max(self.start[0], self.end[0]) + radius
min_y = min(self.start[1], self.end[1]) - radius
max_y = max(self.start[1], self.end[1]) + radius
return ((min_x, max_x), (min_y, max_y))
def offset(self, x_offset=0, y_offset=0):
self.start = tuple(map(add, self.start, (x_offset, y_offset)))
self.end = tuple(map(add, self.end, (x_offset, y_offset)))
class TestRecord(Primitive):
""" Netlist Test record

View file

@ -173,6 +173,20 @@ class GerberCairoContext(GerberContext):
def _render_drill(self, circle, color):
self._render_circle(circle, color)
def _render_slot(self, slot, color):
start = map(mul, slot.start, self.scale)
end = map(mul, slot.end, self.scale)
width = slot.diameter
self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER if (slot.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(width * self.scale[0])
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
self.ctx.move_to(*start)
self.ctx.line_to(*end)
self.ctx.stroke()
def _render_amgroup(self, amgroup, color):
for primitive in amgroup.primitives:
self.render(primitive)

View file

@ -9,6 +9,7 @@ class ExcellonContext(GerberContext):
self.comments = []
self.header = []
self.tool_def = []
self.body_start = [RewindStopStmt()]
self.body = []
self.start = [HeaderBeginStmt()]
self.end = [EndOfProgramStmt()]
@ -19,14 +20,22 @@ class ExcellonContext(GerberContext):
self.settings = settings
self._start_header(settings)
self._start_header()
self._start_comments()
def _start_header(self, settings):
pass
def _start_header(self):
"""Create the header from the settings"""
self.header.append(UnitStmt.from_settings(self.settings))
def _start_comments(self):
# Write the digits used - this isn't valid Excellon statement, so we write as a comment
self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
@property
def statements(self):
return self.start + self.comments + self.header + self.body + self.end
return self.start + self.comments + self.header + self.body_start + self.body + self.end
def set_bounds(self, bounds):
pass
@ -69,10 +78,26 @@ class ExcellonContext(GerberContext):
if tool != self.cur_tool:
self.body.append(ToolSelectionStmt(tool.number))
self.cur_tool = tool
point = self._simplify_point(drill.position)
self._pos = drill.position
self.body.append(CoordinateStmt.from_point(point))
def _render_slot(self, slot, color):
tool = slot.hit.tool
if not tool in self.handled_tools:
self.handled_tools.add(tool)
self.header.append(ExcellonTool.from_tool(tool))
if tool != self.cur_tool:
self.body.append(ToolSelectionStmt(tool.number))
self.cur_tool = tool
# Slots don't use simplified points
self._pos = slot.end
self.body.append(SlotStmt.from_points(slot.start, slot.end))
def _render_inverted_layer(self):
pass

View file

@ -150,6 +150,8 @@ class GerberContext(object):
self._render_polygon(primitive, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.drill_color)
elif isinstance(primitive, Slot):
self._render_slot(primitive, self.drill_color)
elif isinstance(primitive, AMGroup):
self._render_amgroup(primitive, color)
elif isinstance(primitive, Outline):
@ -183,6 +185,9 @@ class GerberContext(object):
def _render_drill(self, primitive, color):
pass
def _render_slot(self, primitive, color):
pass
def _render_amgroup(self, primitive, color):
pass