summaryrefslogtreecommitdiff
path: root/gerber/render/rs274x_backend.py
diff options
context:
space:
mode:
authorHamilton Kibbe <hamilton.kibbe@gmail.com>2016-11-06 14:44:40 -0500
committerHamilton Kibbe <hamilton.kibbe@gmail.com>2016-11-06 14:44:40 -0500
commit422c86bcc684ea94515862b0dd3a39ce0f4bd86f (patch)
treea1efe6504e40083e61dcbe412c243fd8c00628f3 /gerber/render/rs274x_backend.py
parentede065e6d16e1e4ffe970c8b13945139b3f4bcb2 (diff)
parent22e668c75f24174d2090443ed98e804b3737bd84 (diff)
downloadgerbonara-422c86bcc684ea94515862b0dd3a39ce0f4bd86f.tar.gz
gerbonara-422c86bcc684ea94515862b0dd3a39ce0f4bd86f.tar.bz2
gerbonara-422c86bcc684ea94515862b0dd3a39ce0f4bd86f.zip
Merge upstream changes
Diffstat (limited to 'gerber/render/rs274x_backend.py')
-rw-r--r--gerber/render/rs274x_backend.py233
1 files changed, 116 insertions, 117 deletions
diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py
index b4b4612..13e871c 100644
--- a/gerber/render/rs274x_backend.py
+++ b/gerber/render/rs274x_backend.py
@@ -6,7 +6,7 @@ try:
from cStringIO import StringIO
except(ImportError):
from io import StringIO
-
+
from .render import GerberContext
from ..am_statements import *
from ..gerber_statements import *
@@ -15,27 +15,27 @@ from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon,
class AMGroupContext(object):
'''A special renderer to generate aperature macros from an AMGroup'''
-
+
def __init__(self):
self.statements = []
-
+
def render(self, amgroup, name):
-
+
if amgroup.stmt:
# We know the statement it was generated from, so use that to create the AMParamStmt
# It will give a much better result
-
+
stmt = deepcopy(amgroup.stmt)
stmt.name = name
-
+
return stmt
-
+
else:
# Clone ourselves, then offset by the psotion so that
# our render doesn't have to consider offset. Just makes things simpler
nooffset_group = deepcopy(amgroup)
nooffset_group.position = (0, 0)
-
+
# Now draw the shapes
for primitive in nooffset_group.primitives:
if isinstance(primitive, Outline):
@@ -50,46 +50,46 @@ class AMGroupContext(object):
self._render_polygon(primitive)
else:
raise ValueError('amgroup')
-
+
statement = AMParamStmt('AM', name, self._statements_to_string())
return statement
-
+
def _statements_to_string(self):
macro = ''
for statement in self.statements:
macro += statement.to_gerber()
-
+
return macro
-
+
def _render_circle(self, circle):
self.statements.append(AMCirclePrimitive.from_primitive(circle))
-
+
def _render_rectangle(self, rectangle):
self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle))
-
+
def _render_line(self, line):
self.statements.append(AMVectorLinePrimitive.from_primitive(line))
-
+
def _render_outline(self, outline):
self.statements.append(AMOutlinePrimitive.from_primitive(outline))
-
+
def _render_polygon(self, polygon):
self.statements.append(AMPolygonPrimitive.from_primitive(polygon))
-
+
def _render_thermal(self, thermal):
pass
-
+
class Rs274xContext(GerberContext):
-
+
def __init__(self, settings):
GerberContext.__init__(self)
self.comments = []
self.header = []
self.body = []
self.end = [EofStmt()]
-
+
# Current values so we know if we have to execute
# moves, levey changes before anything else
self._level_polarity = None
@@ -97,65 +97,65 @@ class Rs274xContext(GerberContext):
self._func = None
self._quadrant_mode = None
self._dcode = None
-
+
# Primarily for testing and comarison to files, should we write
# flashes as a single statement or a move plus flash? Set to true
# to do in a single statement. Normally this can be false
self.condensed_flash = True
-
+
# When closing a region, force a D02 staement to close a region.
# This is normally not necessary because regions are closed with a G37
# staement, but this will add an extra statement for doubly close
# the region
self.explicit_region_move_end = False
-
+
self._next_dcode = 10
self._rects = {}
self._circles = {}
self._obrounds = {}
self._polygons = {}
self._macros = {}
-
+
self._i_none = 0
self._j_none = 0
-
+
self.settings = settings
self._start_header(settings)
-
+
def _start_header(self, settings):
self.header.append(FSParamStmt.from_settings(settings))
self.header.append(MOParamStmt.from_units(settings.units))
-
+
def _simplify_point(self, point):
return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
-
+
def _simplify_offset(self, point, offset):
-
+
if point[0] != offset[0]:
xoffset = point[0] - offset[0]
else:
xoffset = self._i_none
-
+
if point[1] != offset[1]:
yoffset = point[1] - offset[1]
else:
yoffset = self._j_none
-
+
return (xoffset, yoffset)
-
+
@property
def statements(self):
return self.comments + self.header + self.body + self.end
-
+
def set_bounds(self, bounds):
pass
-
+
def _paint_background(self):
pass
-
+
def _select_aperture(self, aperture):
-
+
# Select the right aperture if not already selected
if aperture:
if isinstance(aperture, Circle):
@@ -168,61 +168,61 @@ class Rs274xContext(GerberContext):
aper = self._get_amacro(aperture)
else:
raise NotImplementedError('Line with invalid aperture type')
-
+
if aper.d != self._dcode:
self.body.append(ApertureStmt(aper.d))
self._dcode = aper.d
-
+
def _pre_render_primitive(self, primitive):
-
+
if hasattr(primitive, 'comment'):
self.body.append(CommentStmt(primitive.comment))
-
+
def _render_line(self, line, color):
-
+
self._select_aperture(line.aperture)
-
+
self._render_level_polarity(line)
-
+
# Get the right function
if self._func != CoordStmt.FUNC_LINEAR:
func = CoordStmt.FUNC_LINEAR
else:
func = None
self._func = CoordStmt.FUNC_LINEAR
-
+
if self._pos != line.start:
self.body.append(CoordStmt.move(func, self._simplify_point(line.start)))
self._pos = line.start
# We already set the function, so the next command doesn't require that
func = None
-
+
point = self._simplify_point(line.end)
-
+
# In some files, we see a lot of duplicated ponts, so omit those
if point[0] != None or point[1] != None:
self.body.append(CoordStmt.line(func, self._simplify_point(line.end)))
self._pos = line.end
elif func:
self.body.append(CoordStmt.mode(func))
-
+
def _render_arc(self, arc, color):
-
+
# Optionally set the quadrant mode if it has changed:
if arc.quadrant_mode != self._quadrant_mode:
-
+
if arc.quadrant_mode != 'multi-quadrant':
self.body.append(QuadrantModeStmt.single())
else:
self.body.append(QuadrantModeStmt.multi())
-
+
self._quadrant_mode = arc.quadrant_mode
-
+
# Select the right aperture if not already selected
self._select_aperture(arc.aperture)
-
+
self._render_level_polarity(arc)
-
+
# Find the right movement mode. Always set to be sure it is really right
dir = arc.direction
if dir == 'clockwise':
@@ -233,59 +233,59 @@ class Rs274xContext(GerberContext):
self._func = CoordStmt.FUNC_ARC_CCW
else:
raise ValueError('Invalid circular interpolation mode')
-
+
if self._pos != arc.start:
# TODO I'm not sure if this is right
self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start)))
self._pos = arc.start
-
+
center = self._simplify_offset(arc.center, arc.start)
end = self._simplify_point(arc.end)
self.body.append(CoordStmt.arc(func, end, center))
self._pos = arc.end
def _render_region(self, region, color):
-
+
self._render_level_polarity(region)
-
+
self.body.append(RegionModeStmt.on())
-
+
for p in region.primitives:
-
+
if isinstance(p, Line):
self._render_line(p, color)
else:
self._render_arc(p, color)
-
+
if self.explicit_region_move_end:
self.body.append(CoordStmt.move(None, None))
self.body.append(RegionModeStmt.off())
-
+
def _render_level_polarity(self, region):
if region.level_polarity != self._level_polarity:
self._level_polarity = region.level_polarity
self.body.append(LPParamStmt.from_region(region))
-
+
def _render_flash(self, primitive, aperture):
-
+
self._render_level_polarity(primitive)
-
+
if aperture.d != self._dcode:
self.body.append(ApertureStmt(aperture.d))
self._dcode = aperture.d
-
+
if self.condensed_flash:
self.body.append(CoordStmt.flash(self._simplify_point(primitive.position)))
else:
self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position)))
self.body.append(CoordStmt.flash(None))
-
+
self._pos = primitive.position
-
+
def _get_circle(self, diameter, hole_diameter, dcode = None):
'''Define a circlar aperture'''
-
+
aper = self._circles.get((diameter, hole_diameter), None)
if not aper:
@@ -294,13 +294,13 @@ class Rs274xContext(GerberContext):
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
-
+
aper = ADParamStmt.circle(dcode, diameter, hole_diameter)
self._circles[(diameter, hole_diameter)] = aper
self.header.append(aper)
return aper
-
+
def _render_circle(self, circle, color):
aper = self._get_circle(circle.diameter, circle.hole_diameter)
@@ -308,122 +308,122 @@ class Rs274xContext(GerberContext):
def _get_rectangle(self, width, height, dcode = None):
'''Get a rectanglar aperture. If it isn't defined, create it'''
-
+
key = (width, height)
aper = self._rects.get(key, None)
-
+
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
-
+
aper = ADParamStmt.rect(dcode, width, height)
self._rects[(width, height)] = aper
self.header.append(aper)
-
+
return aper
def _render_rectangle(self, rectangle, color):
-
+
aper = self._get_rectangle(rectangle.width, rectangle.height)
self._render_flash(rectangle, aper)
-
+
def _get_obround(self, width, height, dcode = None):
-
+
key = (width, height)
aper = self._obrounds.get(key, None)
-
+
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
-
+
aper = ADParamStmt.obround(dcode, width, height)
self._obrounds[key] = aper
self.header.append(aper)
-
+
return aper
-
+
def _render_obround(self, obround, color):
-
+
aper = self._get_obround(obround.width, obround.height)
self._render_flash(obround, aper)
-
+
def _render_polygon(self, polygon, color):
-
+
aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius)
self._render_flash(polygon, aper)
-
+
def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None):
-
+
key = (radius, num_vertices, rotation, hole_radius)
aper = self._polygons.get(key, None)
-
+
if not aper:
if not dcode:
dcode = self._next_dcode
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
-
+
aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2)
self._polygons[key] = aper
self.header.append(aper)
-
+
return aper
-
+
def _render_drill(self, drill, color):
raise ValueError('Drills are not valid in RS274X files')
-
+
def _hash_amacro(self, amgroup):
'''Calculate a very quick hash code for deciding if we should even check AM groups for comparision'''
-
+
# We always start with an X because this forms part of the name
# Basically, in some cases, the name might start with a C, R, etc. That can appear
# to conflict with normal aperture definitions. Technically, it shouldn't because normal
# aperture definitions should have a comma, but in some cases the commit is omitted
hash = 'X'
for primitive in amgroup.primitives:
-
+
hash += primitive.__class__.__name__[0]
-
+
bbox = primitive.bounding_box
hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2]
hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2]
-
+
if hasattr(primitive, 'primitives'):
hash += str(len(primitive.primitives))
-
+
if isinstance(primitive, Rectangle):
hash += str(primitive.width * 1000000)[0:2]
hash += str(primitive.height * 1000000)[0:2]
elif isinstance(primitive, Circle):
hash += str(primitive.diameter * 1000000)[0:2]
-
+
if len(hash) > 20:
# The hash might actually get quite complex, so stop before
# it gets too long
break
-
+
return hash
def _get_amacro(self, amgroup, dcode = None):
# Macros are a little special since we don't have a good way to compare them quickly
# but in most cases, this should work
-
+
hash = self._hash_amacro(amgroup)
macro = None
macroinfo = self._macros.get(hash, None)
-
+
if macroinfo:
-
+
# We have a definition, but check that the groups actually are the same
for macro in macroinfo:
-
+
# Macros should have positions, right? But if the macro is selected for non-flashes
# then it won't have a position. This is of course a bad gerber, but they do exist
if amgroup.position:
@@ -435,7 +435,7 @@ class Rs274xContext(GerberContext):
if amgroup.equivalent(macro[1], offset):
break
macro = None
-
+
# Did we find one in the group0
if not macro:
# This is a new macro, so define it
@@ -444,52 +444,51 @@ class Rs274xContext(GerberContext):
self._next_dcode += 1
else:
self._next_dcode = max(dcode + 1, self._next_dcode)
-
+
# Create the statements
# TODO
amrenderer = AMGroupContext()
statement = amrenderer.render(amgroup, hash)
-
+
self.header.append(statement)
-
+
aperdef = ADParamStmt.macro(dcode, hash)
self.header.append(aperdef)
-
+
# Store the dcode and the original so we can check if it really is the same
# If it didn't have a postition, set it to 0, 0
if amgroup.position == None:
amgroup.position = (0, 0)
macro = (aperdef, amgroup)
-
+
if macroinfo:
macroinfo.append(macro)
else:
- self._macros[hash] = [macro]
-
+ self._macros[hash] = [macro]
+
return macro[0]
-
+
def _render_amgroup(self, amgroup, color):
-
+
aper = self._get_amacro(amgroup)
self._render_flash(amgroup, aper)
def _render_inverted_layer(self):
pass
-
+
def _new_render_layer(self):
# TODO Might need to implement this
pass
-
+
def _flatten(self):
# TODO Might need to implement this
pass
-
+
def dump(self):
"""Write the rendered file to a StringIO steam"""
statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements)
stream = StringIO()
for statement in statements:
stream.write(statement + '\n')
-
+
return stream
- \ No newline at end of file