summaryrefslogtreecommitdiff
path: root/gerber/excellon.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerber/excellon.py')
-rwxr-xr-xgerber/excellon.py182
1 files changed, 97 insertions, 85 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py
index 0626819..9825c5a 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -58,14 +58,17 @@ def read(filename):
data = f.read()
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse(filename)
-
-def loads(data, settings = None, tools = None):
+
+def loads(data, filename=None, settings=None, tools=None):
""" Read data from string and return an ExcellonFile
Parameters
----------
data : string
string containing Excellon file contents
-
+
+ filename : string, optional
+ string containing the filename of the data source
+
tools: dict (optional)
externally defined tools
@@ -78,55 +81,59 @@ def loads(data, settings = None, tools = None):
# File object should use settings from source file by default.
if not settings:
settings = FileSettings(**detect_excellon_format(data))
- return ExcellonParser(settings, tools).parse_raw(data)
+ return ExcellonParser(settings, tools).parse_raw(data, filename)
+
-
-class DrillHit(object):
+class DrillHit(object):
"""Drill feature that is a single drill hole.
-
+
Attributes
----------
tool : ExcellonTool
Tool to drill the hole. Defines the size of the hole that is generated.
position : tuple(float, float)
Center position of the drill.
-
- """
+
+ """
def __init__(self, tool, position):
self.tool = tool
self.position = position
def to_inch(self):
- self.position = tuple(map(inch, self.position))
+ if self.tool.units == 'metric':
+ self.tool.to_inch()
+ self.position = tuple(map(inch, self.position))
def to_metric(self):
- self.position = tuple(map(metric, self.position))
-
+ 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))
-
+
def offset(self, x_offset, y_offset):
self.position = tuple(map(operator.add, self.position, (x_offset, y_offset)))
-
+
def __str__(self):
return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool)
-
+
class DrillSlot(object):
"""
A slot is created between two points. The way the slot is created depends on the statement used to create it
"""
-
+
TYPE_ROUT = 1
TYPE_G85 = 2
-
+
def __init__(self, tool, start, end, slot_type):
self.tool = tool
self.start = start
@@ -134,13 +141,17 @@ class DrillSlot(object):
self.slot_type = slot_type
def to_inch(self):
- self.start = tuple(map(inch, self.start))
- self.end = tuple(map(inch, self.end))
+ 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):
- self.start = tuple(map(metric, self.start))
- self.end = tuple(map(metric, self.end))
-
+ 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
@@ -151,7 +162,7 @@ class DrillSlot(object):
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))
-
+
def offset(self, x_offset, y_offset):
self.start = tuple(map(operator.add, self.start, (x_offset, y_offset)))
self.end = tuple(map(operator.add, self.end, (x_offset, y_offset)))
@@ -193,7 +204,7 @@ class ExcellonFile(CamFile):
self.hits = hits
@property
- def primitives(self):
+ def primitives(self):
"""
Gets the primitives. Note that unlike Gerber, this generates new objects
"""
@@ -205,8 +216,8 @@ class ExcellonFile(CamFile):
primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
else:
raise ValueError('Unknown hit type')
-
- return primitives
+
+ return primitives
@property
def bounds(self):
@@ -248,7 +259,9 @@ class ExcellonFile(CamFile):
def write(self, filename=None):
filename = filename if filename is not None else self.filename
- with open(filename, 'w') as f:
+ with open(filename, 'w') as f:
+
+ # Copy the header verbatim
for statement in self.statements:
if not isinstance(statement, ToolSelectionStmt):
f.write(statement.to_excellon(self.settings) + '\n')
@@ -261,8 +274,8 @@ class ExcellonFile(CamFile):
for hit in self.hits:
if hit.tool.number == tool.number:
f.write(CoordinateStmt(
- *hit.position).to_excellon(self.settings) + '\n')
- f.write(EndOfProgramStmt().to_excellon() + '\n')
+ *hit.position).to_excellon(self.settings) + '\n')
+ f.write(EndOfProgramStmt().to_excellon() + '\n')
def to_inch(self):
"""
@@ -275,9 +288,9 @@ class ExcellonFile(CamFile):
for tool in iter(self.tools.values()):
tool.to_inch()
for primitive in self.primitives:
- primitive.to_inch()
- for hit in self.hits:
- hit.to_inch()
+ primitive.to_inch()
+ for hit in self.hits:
+ hit.to_inch()
def to_metric(self):
""" Convert units to metric
@@ -297,9 +310,9 @@ class ExcellonFile(CamFile):
for statement in self.statements:
statement.offset(x_offset, y_offset)
for primitive in self.primitives:
- primitive.offset(x_offset, y_offset)
- for hit in self. hits:
- hit.offset(x_offset, y_offset)
+ primitive.offset(x_offset, y_offset)
+ for hit in self. hits:
+ hit.offset(x_offset, y_offset)
def path_length(self, tool_number=None):
""" Return the path length for a given tool
@@ -309,8 +322,8 @@ class ExcellonFile(CamFile):
for hit in self.hits:
tool = hit.tool
num = tool.number
- positions[num] = (0, 0) if positions.get(
- num) is None else positions[num]
+ positions[num] = ((0, 0) if positions.get(num) is None
+ else positions[num])
lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
lengths[num] = lengths[
num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
@@ -358,9 +371,9 @@ class ExcellonParser(object):
Parameters
----------
settings : FileSettings or dict-like
- Excellon file settings to use when interpreting the excellon file.
- """
- def __init__(self, settings=None, ext_tools=None):
+ Excellon file settings to use when interpreting the excellon file.
+ """
+ def __init__(self, settings=None, ext_tools=None):
self.notation = 'absolute'
self.units = 'inch'
self.zeros = 'leading'
@@ -374,7 +387,7 @@ class ExcellonParser(object):
self.active_tool = None
self.pos = [0., 0.]
self.drill_down = False
- # Default for lated is None, which means we don't know
+ # Default for plated is None, which means we don't know
self.plated = ExcellonTool.PLATED_UNKNOWN
if settings is not None:
self.units = settings.units
@@ -435,19 +448,19 @@ class ExcellonParser(object):
[int(x) for x in comment_stmt.comment.split('=')[1].split(":")])
if detected_format:
self.format = detected_format
-
+
if "TYPE=PLATED" in comment_stmt.comment:
self.plated = ExcellonTool.PLATED_YES
-
+
if "TYPE=NON_PLATED" in comment_stmt.comment:
self.plated = ExcellonTool.PLATED_NO
-
+
if "HEADER:" in comment_stmt.comment:
self.state = "HEADER"
-
+
if " Holesize " in comment_stmt.comment:
self.state = "HEADER"
-
+
# Parse this as a hole definition
tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment)
if len(tools) == 1:
@@ -464,12 +477,12 @@ class ExcellonParser(object):
self.state = 'DRILL'
elif self.state == 'INIT':
self.state = 'HEADER'
-
+
elif line[:3] == 'M00' and self.state == 'DRILL':
if self.active_tool:
cur_tool_number = self.active_tool.number
next_tool = self._get_tool(cur_tool_number + 1)
-
+
self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool))
self.active_tool = next_tool
else:
@@ -523,7 +536,7 @@ class ExcellonParser(object):
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
stmt.mode = self.state
-
+
# The start position is where we were before the rout command
start = (self.pos[0], self.pos[1])
@@ -540,17 +553,17 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
-
+
# Our ending position
end = (self.pos[0], self.pos[1])
-
+
if self.drill_down:
if not self.active_tool:
self.active_tool = self._get_tool(1)
-
+
self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
self.active_tool._hit()
-
+
elif line[:3] == 'G05':
self.statements.append(DrillModeStmt())
self.drill_down = False
@@ -613,12 +626,12 @@ class ExcellonParser(object):
stmt = ToolSelectionStmt.from_excellon(line)
self.statements.append(stmt)
- # T0 is used as END marker, just ignore
- if stmt.tool != 0:
+ # T0 is used as END marker, just ignore
+ if stmt.tool != 0:
tool = self._get_tool(stmt.tool)
-
+
if not tool:
- # FIXME: for weird files with no tools defined, original calc from gerb
+ # FIXME: for weird files with no tools defined, original calc from gerb
if self._settings().units == "inch":
diameter = (16 + 8 * stmt.tool) / 1000.0
else:
@@ -649,13 +662,13 @@ class ExcellonParser(object):
elif line[0] in ['X', 'Y']:
if 'G85' in line:
stmt = SlotStmt.from_excellon(line, self._settings())
-
+
# 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
@@ -666,19 +679,19 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
-
+
if self.state == 'DRILL' or self.state == 'HEADER':
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), DrillSlot.TYPE_G85))
self.active_tool._hit()
else:
stmt = CoordinateStmt.from_excellon(line, self._settings())
-
+
# We need this in case we are in rout mode
start = (self.pos[0], self.pos[1])
-
+
x = stmt.x
y = stmt.y
self.statements.append(stmt)
@@ -692,71 +705,71 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
-
+
if self.state == 'LINEAR' and self.drill_down:
if not self.active_tool:
self.active_tool = self._get_tool(1)
-
+
self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT))
-
+
elif self.state == 'DRILL' or self.state == 'HEADER':
# Yes, drills in the header doesn't follow the specification, but it there are many
# files like this
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))
def _settings(self):
return FileSettings(units=self.units, format=self.format,
zeros=self.zeros, notation=self.notation)
-
+
def _add_comment_tool(self, tool):
"""
Add a tool that was defined in the comments to this file.
-
+
If we have already found this tool, then we will merge this comment tool definition into
the information for the tool
"""
-
+
existing = self.tools.get(tool.number)
if existing and existing.plated == None:
existing.plated = tool.plated
-
+
self.comment_tools[tool.number] = tool
-
+
def _merge_properties(self, tool):
"""
When we have externally defined tools, merge the properties of that tool into this one
-
+
For now, this is only plated
"""
-
+
if tool.plated == ExcellonTool.PLATED_UNKNOWN:
ext_tool = self.ext_tools.get(tool.number)
-
+
if ext_tool:
tool.plated = ext_tool.plated
def _get_tool(self, toolid):
-
+
tool = self.tools.get(toolid)
if not tool:
tool = self.comment_tools.get(toolid)
if tool:
tool.settings = self._settings()
self.tools[toolid] = tool
-
+
if not tool:
tool = self.ext_tools.get(toolid)
if tool:
tool.settings = self._settings()
self.tools[toolid] = tool
-
+
return tool
def detect_excellon_format(data=None, filename=None):
@@ -821,8 +834,8 @@ def detect_excellon_format(data=None, filename=None):
settings = FileSettings(zeros=zeros, format=fmt)
try:
p = ExcellonParser(settings)
- p.parse_raw(data)
- size = tuple([t[0] - t[1] for t in p.bounds])
+ ef = p.parse_raw(data)
+ size = tuple([t[0] - t[1] for t in ef.bounds])
hole_area = 0.0
for hit in p.hits:
tool = hit.tool
@@ -862,9 +875,8 @@ def _layer_size_score(size, hole_count, hole_area):
board_area = size[0] * size[1]
if board_area == 0:
return 0
-
+
hole_percentage = hole_area / board_area
hole_score = (hole_percentage - 0.25) ** 2
size_score = (board_area - 8) ** 2
return hole_score * size_score
- \ No newline at end of file