From 5476da8aa3f4ee424f56f4f2491e7af1c4b7b758 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Fix a bunch of rendering bugs. - 'clear' polarity primitives no longer erase background - Added aperture macro support for polygons - Added aperture macro rendring support - Renderer now creates a new surface for each layer and merges them instead of working directly on a single surface - Updated examples accordingly --- examples/cairo_example.png | Bin 104401 -> 104184 bytes examples/pcb_example.py | 7 +- examples/pcb_top.png | Bin 99269 -> 98827 bytes gerber/am_eval.py | 19 +- gerber/am_read.py | 7 +- gerber/am_statements.py | 125 ++-- gerber/cam.py | 18 +- gerber/common.py | 3 - gerber/excellon.py | 62 +- gerber/excellon_statements.py | 6 +- gerber/gerber_statements.py | 54 +- gerber/layers.py | 7 +- gerber/operations.py | 5 + gerber/pcb.py | 15 +- gerber/primitives.py | 1077 ++++++++++++++++++++---------- gerber/render/cairo_backend.py | 255 +++---- gerber/render/render.py | 8 +- gerber/render/theme.py | 4 +- gerber/rs274x.py | 74 +- gerber/tests/test_am_statements.py | 65 +- gerber/tests/test_cam.py | 26 +- gerber/tests/test_common.py | 8 +- gerber/tests/test_excellon.py | 10 +- gerber/tests/test_excellon_statements.py | 173 +++-- gerber/tests/test_gerber_statements.py | 145 +++- gerber/tests/test_ipc356.py | 29 +- gerber/tests/test_layers.py | 2 +- gerber/tests/test_primitives.py | 427 +++++++----- gerber/tests/test_rs274x.py | 9 +- gerber/tests/test_utils.py | 25 +- gerber/tests/tests.py | 3 +- gerber/utils.py | 41 +- requirements.txt | 1 + 33 files changed, 1771 insertions(+), 939 deletions(-) diff --git a/examples/cairo_example.png b/examples/cairo_example.png index f33cc69..671de5c 100644 Binary files a/examples/cairo_example.png and b/examples/cairo_example.png differ diff --git a/examples/pcb_example.py b/examples/pcb_example.py index 5341da0..bba030e 100644 --- a/examples/pcb_example.py +++ b/examples/pcb_example.py @@ -24,6 +24,7 @@ import os from gerber import PCB from gerber.render import GerberCairoContext, theme + GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers')) @@ -33,7 +34,7 @@ ctx = GerberCairoContext() # Create a new PCB pcb = PCB.from_directory(GERBER_FOLDER) -pcb.theme = theme.THEMES['OSH Park'] -ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png')) -ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png')) +# Render PCB +ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png',), theme.THEMES['OSH Park']) +ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), theme.THEMES['OSH Park']) diff --git a/examples/pcb_top.png b/examples/pcb_top.png index bf1d687..60bc245 100644 Binary files a/examples/pcb_top.png and b/examples/pcb_top.png differ diff --git a/gerber/am_eval.py b/gerber/am_eval.py index 29b380d..3a7e1ed 100644 --- a/gerber/am_eval.py +++ b/gerber/am_eval.py @@ -18,15 +18,16 @@ """ This module provides RS-274-X AM macro evaluation. """ + class OpCode: - PUSH = 1 - LOAD = 2 + PUSH = 1 + LOAD = 2 STORE = 3 - ADD = 4 - SUB = 5 - MUL = 6 - DIV = 7 - PRIM = 8 + ADD = 4 + SUB = 5 + MUL = 6 + DIV = 7 + PRIM = 8 @staticmethod def str(opcode): @@ -49,16 +50,18 @@ class OpCode: else: return "UNKNOWN" + def eval_macro(instructions, parameters={}): if not isinstance(parameters, type({})): p = {} for i, val in enumerate(parameters): - p[i+1] = val + p[i + 1] = val parameters = p stack = [] + def pop(): return stack.pop() diff --git a/gerber/am_read.py b/gerber/am_read.py index 65d08a6..4aff00b 100644 --- a/gerber/am_read.py +++ b/gerber/am_read.py @@ -26,7 +26,8 @@ import string class Token: ADD = "+" SUB = "-" - MULT = ("x", "X") # compatibility as many gerber writes do use non compliant X + # compatibility as many gerber writes do use non compliant X + MULT = ("x", "X") DIV = "/" OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV) LEFT_PARENS = "(" @@ -62,6 +63,7 @@ def is_op(token): class Scanner: + def __init__(self, s): self.buff = s self.n = 0 @@ -111,7 +113,8 @@ class Scanner: def print_instructions(instructions): for opcode, argument in instructions: - print("%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else "")) + print("%s %s" % (OpCode.str(opcode), + str(argument) if argument is not None else "")) def read_macro(macro): diff --git a/gerber/am_statements.py b/gerber/am_statements.py index 38f4d71..f67b0db 100644 --- a/gerber/am_statements.py +++ b/gerber/am_statements.py @@ -17,9 +17,7 @@ # limitations under the License. from .utils import validate_coordinates, inch, metric - - -# TODO: Add support for aperture macro variables +from .primitives import * __all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive', 'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive', @@ -51,12 +49,14 @@ class AMPrimitive(object): ------ TypeError, ValueError """ + def __init__(self, code, exposure=None): VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999) if not isinstance(code, int): raise TypeError('Aperture Macro Primitive code must be an integer') elif code not in VALID_CODES: - raise ValueError('Invalid Code. Valid codes are %s.' % ', '.join(map(str, VALID_CODES))) + raise ValueError('Invalid Code. Valid codes are %s.' % + ', '.join(map(str, VALID_CODES))) if exposure is not None and exposure.lower() not in ('on', 'off'): raise ValueError('Exposure must be either on or off') self.code = code @@ -68,9 +68,15 @@ class AMPrimitive(object): def to_metric(self): raise NotImplementedError('Subclass must implement `to-metric`') + def to_primitive(self, position, level_polarity, units): + """ Return a Primitive instance based on the specified macro params. + """ + print('Rendering {}s is not supported yet.'.format(str(self.__class__))) + def __eq__(self, other): return self.__dict__ == other.__dict__ + class AMCommentPrimitive(AMPrimitive): """ Aperture Macro Comment primitive. Code 0 @@ -181,12 +187,19 @@ class AMCirclePrimitive(AMPrimitive): self.diameter = metric(self.diameter) self.position = tuple([metric(x) for x in self.position]) + def to_primitive(self, position, level_polarity, units): + # Offset the primitive from macro position + position = tuple([a + b for a , b in zip (position, self.position)]) + # Return a renderable primitive + return Circle(position, self.diameter, level_polarity=level_polarity, + units=units) + def to_gerber(self, settings=None): - data = dict(code = self.code, - exposure = '1' if self.exposure == 'on' else 0, - diameter = self.diameter, - x = self.position[0], - y = self.position[1]) + data = dict(code=self.code, + exposure='1' if self.exposure == 'on' else 0, + diameter=self.diameter, + x=self.position[0], + y=self.position[1]) return '{code},{exposure},{diameter},{x},{y}*'.format(**data) @@ -261,17 +274,24 @@ class AMVectorLinePrimitive(AMPrimitive): self.start = tuple([metric(x) for x in self.start]) self.end = tuple([metric(x) for x in self.end]) + def to_primitive(self, position, level_polarity, units): + # Offset the primitive from macro position + start = tuple([a + b for a , b in zip (position, self.start)]) + end = tuple([a + b for a , b in zip (position, self.end)]) + # Return a renderable primitive + ap = Rectangle((0, 0), self.width, self.width) + return Line(start, end, ap, level_polarity=level_polarity, units=units) def to_gerber(self, settings=None): fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*' - data = dict(code = self.code, - exp = 1 if self.exposure == 'on' else 0, - width = self.width, - startx = self.start[0], - starty = self.start[1], - endx = self.end[0], - endy = self.end[1], - rotation = self.rotation) + data = dict(code=self.code, + exp=1 if self.exposure == 'on' else 0, + width=self.width, + startx=self.start[0], + starty=self.start[1], + endx=self.end[0], + endy=self.end[1], + rotation=self.rotation) return fmtstr.format(**data) @@ -323,7 +343,8 @@ class AMOutlinePrimitive(AMPrimitive): start_point = (float(modifiers[3]), float(modifiers[4])) points = [] for i in range(n): - points.append((float(modifiers[5 + i*2]), float(modifiers[5 + i*2 + 1]))) + points.append((float(modifiers[5 + i * 2]), + float(modifiers[5 + i * 2 + 1]))) rotation = float(modifiers[-1]) return cls(code, exposure, start_point, points, rotation) @@ -416,7 +437,6 @@ class AMPolygonPrimitive(AMPrimitive): rotation = float(modifiers[6]) return cls(code, exposure, vertices, position, diameter, rotation) - def __init__(self, code, exposure, vertices, position, diameter, rotation): """ Initialize AMPolygonPrimitive """ @@ -439,13 +459,21 @@ class AMPolygonPrimitive(AMPrimitive): self.position = tuple([metric(x) for x in self.position]) self.diameter = metric(self.diameter) + def to_primitive(self, position, level_polarity, units): + # Offset the primitive from macro position + position = tuple([a + b for a , b in zip (position, self.position)]) + # Return a renderable primitive + return Polygon(position, vertices, self.diameter/2., + rotation=self.rotation, level_polarity=level_polarity, + units=units) + def to_gerber(self, settings=None): data = dict( code=self.code, exposure="1" if self.exposure == "on" else "0", vertices=self.vertices, position="%.4g,%.4g" % self.position, - diameter = '%.4g' % self.diameter, + diameter='%.4g' % self.diameter, rotation=str(self.rotation) ) fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*" @@ -546,17 +574,16 @@ class AMMoirePrimitive(AMPrimitive): self.crosshair_thickness = metric(self.crosshair_thickness) self.crosshair_length = metric(self.crosshair_length) - def to_gerber(self, settings=None): data = dict( code=self.code, position="%.4g,%.4g" % self.position, - diameter = self.diameter, - ring_thickness = self.ring_thickness, - gap = self.gap, - max_rings = self.max_rings, - crosshair_thickness = self.crosshair_thickness, - crosshair_length = self.crosshair_length, + diameter=self.diameter, + ring_thickness=self.ring_thickness, + gap=self.gap, + max_rings=self.max_rings, + crosshair_thickness=self.crosshair_thickness, + crosshair_length=self.crosshair_length, rotation=self.rotation ) fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*" @@ -608,7 +635,7 @@ class AMThermalPrimitive(AMPrimitive): code = int(modifiers[0]) position = (float(modifiers[1]), float(modifiers[2])) outer_diameter = float(modifiers[3]) - inner_diameter= float(modifiers[4]) + inner_diameter = float(modifiers[4]) gap = float(modifiers[5]) return cls(code, position, outer_diameter, inner_diameter, gap) @@ -628,7 +655,6 @@ class AMThermalPrimitive(AMPrimitive): self.inner_diameter = inch(self.inner_diameter) self.gap = inch(self.gap) - def to_metric(self): self.position = tuple([metric(x) for x in self.position]) self.outer_diameter = metric(self.outer_diameter) @@ -639,9 +665,9 @@ class AMThermalPrimitive(AMPrimitive): data = dict( code=self.code, position="%.4g,%.4g" % self.position, - outer_diameter = self.outer_diameter, - inner_diameter = self.inner_diameter, - gap = self.gap, + outer_diameter=self.outer_diameter, + inner_diameter=self.inner_diameter, + gap=self.gap, ) fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap}*" return fmt.format(**data) @@ -693,14 +719,14 @@ class AMCenterLinePrimitive(AMPrimitive): exposure = 'on' if float(modifiers[1]) == 1 else 'off' width = float(modifiers[2]) height = float(modifiers[3]) - center= (float(modifiers[4]), float(modifiers[5])) + center = (float(modifiers[4]), float(modifiers[5])) rotation = float(modifiers[6]) return cls(code, exposure, width, height, center, rotation) def __init__(self, code, exposure, width, height, center, rotation): if code != 21: raise ValueError('CenterLinePrimitive code is 21') - super (AMCenterLinePrimitive, self).__init__(code, exposure) + super(AMCenterLinePrimitive, self).__init__(code, exposure) self.width = width self.height = height validate_coordinates(center) @@ -717,12 +743,19 @@ class AMCenterLinePrimitive(AMPrimitive): self.width = metric(self.width) self.height = metric(self.height) + def to_primitive(self, position, level_polarity, units): + # Offset the primitive from macro position + position = tuple([a + b for a , b in zip (position, self.center)]) + # Return a renderable primitive + return Rectangle(position, self.width, self.height, + level_polarity=level_polarity, units=units) + def to_gerber(self, settings=None): data = dict( code=self.code, - exposure = '1' if self.exposure == 'on' else '0', - width = self.width, - height = self.height, + exposure='1' if self.exposure == 'on' else '0', + width=self.width, + height=self.height, center="%.4g,%.4g" % self.center, rotation=self.rotation ) @@ -782,7 +815,7 @@ class AMLowerLeftLinePrimitive(AMPrimitive): def __init__(self, code, exposure, width, height, lower_left, rotation): if code != 22: raise ValueError('LowerLeftLinePrimitive code is 22') - super (AMLowerLeftLinePrimitive, self).__init__(code, exposure) + super(AMLowerLeftLinePrimitive, self).__init__(code, exposure) self.width = width self.height = height validate_coordinates(lower_left) @@ -799,12 +832,21 @@ class AMLowerLeftLinePrimitive(AMPrimitive): self.width = metric(self.width) self.height = metric(self.height) + def to_primitive(self, position, level_polarity, units): + # Offset the primitive from macro position + position = tuple([a + b for a , b in zip (position, self.lower_left)]) + position = tuple([pos + offset for pos, offset in + zip(position, (self.width/2, self.height/2))]) + # Return a renderable primitive + return Rectangle(position, self.width, self.height, + level_polarity=level_polarity, units=units) + def to_gerber(self, settings=None): data = dict( code=self.code, - exposure = '1' if self.exposure == 'on' else '0', - width = self.width, - height = self.height, + exposure='1' if self.exposure == 'on' else '0', + width=self.width, + height=self.height, lower_left="%.4g,%.4g" % self.lower_left, rotation=self.rotation ) @@ -813,6 +855,7 @@ class AMLowerLeftLinePrimitive(AMPrimitive): class AMUnsupportPrimitive(AMPrimitive): + @classmethod def from_gerber(cls, primitive): return cls(primitive) diff --git a/gerber/cam.py b/gerber/cam.py index 92ce83d..dda5c10 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -22,6 +22,7 @@ CAM File This module provides common base classes for Excellon/Gerber CNC files """ + class FileSettings(object): """ CAM File Settings @@ -52,6 +53,7 @@ class FileSettings(object): specify both. `zero_suppression` will take on the opposite value of `zeros` and vice versa """ + def __init__(self, notation='absolute', units='inch', zero_suppression=None, format=(2, 5), zeros=None, angle_units='degrees'): @@ -243,6 +245,12 @@ class CamFile(object): """ pass + def to_inch(self): + pass + + def to_metric(self): + pass + def render(self, ctx, invert=False, filename=None): """ Generate image of layer. @@ -256,15 +264,11 @@ class CamFile(object): """ ctx.set_bounds(self.bounds) ctx._paint_background() - - if invert: - ctx.invert = True - ctx._clear_mask() + ctx.invert = invert + ctx._new_render_layer() for p in self.primitives: ctx.render(p) - if invert: - ctx.invert = False - ctx._render_mask() + ctx._flatten() if filename is not None: ctx.dump(filename) diff --git a/gerber/common.py b/gerber/common.py index 04b6423..cf137dd 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -22,7 +22,6 @@ from .exceptions import ParseError from .utils import detect_file_format - def read(filename): """ Read a gerber or excellon file and return a representative object. @@ -73,5 +72,3 @@ def loads(data): return excellon.loads(data) else: raise TypeError('Unable to detect file format') - - diff --git a/gerber/excellon.py b/gerber/excellon.py index 3bb8611..b1b94df 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -76,6 +76,7 @@ def loads(data): class DrillHit(object): + def __init__(self, tool, position): self.tool = tool self.position = position @@ -118,6 +119,7 @@ class ExcellonFile(CamFile): either 'inch' or 'metric'. """ + def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFile, self).__init__(statements=statements, settings=settings, @@ -127,8 +129,7 @@ class ExcellonFile(CamFile): @property def primitives(self): - return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits] - + return [Drill(hit.position, hit.tool.diameter, units=self.settings.units) for hit in self.hits] @property def bounds(self): @@ -162,7 +163,8 @@ class ExcellonFile(CamFile): rprt += ' Code Size Hits Path Length\n' rprt += ' --------------------------------------\n' for tool in iter(self.tools.values()): - rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.path_length(tool.number)) + rprt += toolfmt.format(tool.number, tool.diameter, + tool.hit_count, self.path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: f.write(rprt) @@ -184,7 +186,8 @@ class ExcellonFile(CamFile): f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n') for hit in self.hits: if hit.tool.number == tool.number: - f.write(CoordinateStmt(*hit.position).to_excellon(self.settings) + '\n') + f.write(CoordinateStmt( + *hit.position).to_excellon(self.settings) + '\n') f.write(EndOfProgramStmt().to_excellon() + '\n') def to_inch(self): @@ -200,8 +203,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.to_inch() for hit in self.hits: - hit.position = tuple(map(inch, hit,position)) - + hit.position = tuple(map(inch, hit, position)) def to_metric(self): """ Convert units to metric @@ -223,7 +225,8 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.offset(x_offset, y_offset) for hit in self. hits: - hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset))) + hit.position = tuple(map(operator.add, hit.position, + (x_offset, y_offset))) def path_length(self, tool_number=None): """ Return the path length for a given tool @@ -233,9 +236,11 @@ 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))) + lengths[num] = lengths[ + num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position))) positions[num] = hit.position if tool_number is None: @@ -244,13 +249,13 @@ class ExcellonFile(CamFile): return lengths.get(tool_number) def hit_count(self, tool_number=None): - counts = {} - for tool in iter(self.tools.values()): - counts[tool.number] = tool.hit_count - if tool_number is None: - return counts - else: - return counts.get(tool_number) + counts = {} + for tool in iter(self.tools.values()): + counts[tool.number] = tool.hit_count + if tool_number is None: + return counts + else: + return counts.get(tool_number) def update_tool(self, tool_number, **kwargs): """ Change parameters of a tool @@ -274,7 +279,6 @@ class ExcellonFile(CamFile): hit.tool = newtool - class ExcellonParser(object): """ Excellon File Parser @@ -283,6 +287,7 @@ class ExcellonParser(object): settings : FileSettings or dict-like Excellon file settings to use when interpreting the excellon file. """ + def __init__(self, settings=None): self.notation = 'absolute' self.units = 'inch' @@ -300,7 +305,6 @@ class ExcellonParser(object): self.notation = settings.notation self.format = settings.format - @property def coordinates(self): return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)] @@ -350,7 +354,8 @@ class ExcellonParser(object): # get format from altium comment if "FILE_FORMAT" in comment_stmt.comment: - detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) + detected_format = tuple( + [int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) if detected_format: self.format = detected_format @@ -435,7 +440,7 @@ class ExcellonParser(object): self.zeros = stmt.zeros self.statements.append(stmt) - elif line[:3] == 'M71' or line [:3] == 'M72': + elif line[:3] == 'M71' or line[:3] == 'M72': stmt = MeasuringModeStmt.from_excellon(line) self.units = stmt.units self.statements.append(stmt) @@ -481,17 +486,20 @@ class ExcellonParser(object): # T0 is used as END marker, just ignore if stmt.tool != 0: - # FIXME: for weird files with no tools defined, original calc from gerbv + # FIXME: for weird files with no tools defined, original calc + # from gerbv if stmt.tool not in self.tools: if self._settings().units == "inch": - diameter = (16 + 8 * stmt.tool) / 1000.0; + diameter = (16 + 8 * stmt.tool) / 1000.0 else: - diameter = metric((16 + 8 * stmt.tool) / 1000.0); + diameter = metric((16 + 8 * stmt.tool) / 1000.0) - tool = ExcellonTool(self._settings(), number=stmt.tool, diameter=diameter) + tool = ExcellonTool( + self._settings(), number=stmt.tool, diameter=diameter) self.tools[tool.number] = tool - # FIXME: need to add this tool definition inside header to make sure it is properly written + # FIXME: need to add this tool definition inside header to + # make sure it is properly written for i, s in enumerate(self.statements): if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool): self.statements.insert(i, tool) @@ -575,7 +583,7 @@ def detect_excellon_format(data=None, filename=None): and 'FILE_FORMAT' in stmt.comment] detected_format = (tuple([int(val) for val in - format_comment[0].split('=')[1].split(':')]) + format_comment[0].split('=')[1].split(':')]) if len(format_comment) == 1 else None) detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None @@ -637,5 +645,5 @@ def _layer_size_score(size, hole_count, hole_area): board_area = size[0] * size[1] hole_percentage = hole_area / board_area hole_score = (hole_percentage - 0.25) ** 2 - size_score = (board_area - 8) **2 + size_score = (board_area - 8) ** 2 return hole_score * size_score diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 2be7a05..971a81d 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -55,6 +55,7 @@ class ExcellonStatement(object): def to_excellon(self, settings=None): raise NotImplementedError('to_excellon must be implemented in a ' 'subclass') + def to_inch(self): self.units = 'inch' @@ -67,6 +68,7 @@ class ExcellonStatement(object): def __eq__(self, other): return self.__dict__ == other.__dict__ + class ExcellonTool(ExcellonStatement): """ Excellon Tool class @@ -210,7 +212,6 @@ class ExcellonTool(ExcellonStatement): if self.diameter is not None: self.diameter = inch(self.diameter) - def to_metric(self): if self.settings.units != 'metric': self.settings.units = 'metric' @@ -573,6 +574,7 @@ class EndOfProgramStmt(ExcellonStatement): if self.y is not None: self.y += y_offset + class UnitStmt(ExcellonStatement): @classmethod @@ -598,6 +600,7 @@ class UnitStmt(ExcellonStatement): def to_metric(self): self.units = 'metric' + class IncrementalModeStmt(ExcellonStatement): @classmethod @@ -689,6 +692,7 @@ class MeasuringModeStmt(ExcellonStatement): def to_metric(self): self.units = 'metric' + class RouteModeStmt(ExcellonStatement): def __init__(self, **kwargs): diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 9931acf..74b3e54 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -43,6 +43,7 @@ class Statement(object): type : string String identifying the statement type. """ + def __init__(self, stype, units='inch'): self.type = stype self.units = units @@ -84,6 +85,7 @@ class ParamStmt(Statement): param : string Parameter type code """ + def __init__(self, param): Statement.__init__(self, "PARAM") self.param = param @@ -157,8 +159,6 @@ class FSParamStmt(ParamStmt): return '%FS{0}{1}X{2}Y{3}*%'.format(zero_suppression, notation, fmt, fmt) - - def __str__(self): return ('' % (self.format[0], self.format[1], self.zero_suppression, self.notation)) @@ -293,19 +293,22 @@ class ADParamStmt(ParamStmt): self.d = d self.shape = shape if modifiers: - self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) for m in modifiers.split(",") if len(m)] + self.modifiers = [tuple([float(x) for x in m.split("X") if len(x)]) + for m in modifiers.split(",") if len(m)] else: self.modifiers = [tuple()] def to_inch(self): if self.units == 'metric': - self.units = 'inch' - self.modifiers = [tuple([inch(x) for x in modifier]) for modifier in self.modifiers] + self.units = 'inch' + self.modifiers = [tuple([inch(x) for x in modifier]) + for modifier in self.modifiers] def to_metric(self): if self.units == 'inch': - self.units = 'metric' - self.modifiers = [tuple([metric(x) for x in modifier]) for modifier in self.modifiers] + self.units = 'metric' + self.modifiers = [tuple([metric(x) for x in modifier]) + for modifier in self.modifiers] def to_gerber(self, settings=None): if any(self.modifiers): @@ -382,12 +385,15 @@ class AMParamStmt(ParamStmt): self.primitives.append(AMOutlinePrimitive.from_gerber(primitive)) elif primitive[0] == '5': self.primitives.append(AMPolygonPrimitive.from_gerber(primitive)) - elif primitive[0] =='6': + elif primitive[0] == '6': self.primitives.append(AMMoirePrimitive.from_gerber(primitive)) elif primitive[0] == '7': - self.primitives.append(AMThermalPrimitive.from_gerber(primitive)) + self.primitives.append( + AMThermalPrimitive.from_gerber(primitive)) else: - self.primitives.append(AMUnsupportPrimitive.from_gerber(primitive)) + self.primitives.append( + AMUnsupportPrimitive.from_gerber(primitive)) + return self def to_inch(self): if self.units == 'metric': @@ -824,13 +830,17 @@ class CoordStmt(Statement): op = stmt_dict.get('op') if x is not None: - x = parse_gerber_value(stmt_dict.get('x'), settings.format, settings.zero_suppression) + x = parse_gerber_value(stmt_dict.get('x'), settings.format, + settings.zero_suppression) if y is not None: - y = parse_gerber_value(stmt_dict.get('y'), settings.format, settings.zero_suppression) + y = parse_gerber_value(stmt_dict.get('y'), settings.format, + settings.zero_suppression) if i is not None: - i = parse_gerber_value(stmt_dict.get('i'), settings.format, settings.zero_suppression) + i = parse_gerber_value(stmt_dict.get('i'), settings.format, + settings.zero_suppression) if j is not None: - j = parse_gerber_value(stmt_dict.get('j'), settings.format, settings.zero_suppression) + j = parse_gerber_value(stmt_dict.get('j'), settings.format, + settings.zero_suppression) return cls(function, x, y, i, j, op, settings) def __init__(self, function, x, y, i, j, op, settings): @@ -878,13 +888,17 @@ class CoordStmt(Statement): if self.function: ret += self.function if self.x is not None: - ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, settings.zero_suppression)) + ret += 'X{0}'.format(write_gerber_value(self.x, settings.format, + settings.zero_suppression)) if self.y is not None: - ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, settings.zero_suppression)) + ret += 'Y{0}'.format(write_gerber_value(self.y, settings.format, + settings.zero_suppression)) if self.i is not None: - ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, settings.zero_suppression)) + ret += 'I{0}'.format(write_gerber_value(self.i, settings.format, + settings.zero_suppression)) if self.j is not None: - ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, settings.zero_suppression)) + ret += 'J{0}'.format(write_gerber_value(self.j, settings.format, + settings.zero_suppression)) if self.op: ret += self.op return ret + '*' @@ -956,6 +970,7 @@ class CoordStmt(Statement): class ApertureStmt(Statement): """ Aperture Statement """ + def __init__(self, d, deprecated=None): Statement.__init__(self, "APERTURE") self.d = int(d) @@ -989,6 +1004,7 @@ class CommentStmt(Statement): class EofStmt(Statement): """ EOF Statement """ + def __init__(self): Statement.__init__(self, "EOF") @@ -1043,6 +1059,7 @@ class RegionModeStmt(Statement): class UnknownStmt(Statement): """ Unknown Statement """ + def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line @@ -1052,4 +1069,3 @@ class UnknownStmt(Statement): def __str__(self): return '' % self.line - diff --git a/gerber/layers.py b/gerber/layers.py index 2b73893..29e452b 100644 --- a/gerber/layers.py +++ b/gerber/layers.py @@ -95,7 +95,8 @@ def sort_layers(layers): 'bottompaste', 'drill', ] output = [] drill_layers = [layer for layer in layers if layer.layer_class == 'drill'] - internal_layers = list(sorted([layer for layer in layers if layer.layer_class == 'internal'])) + internal_layers = list(sorted([layer for layer in layers + if layer.layer_class == 'internal'])) for layer_class in layer_order: if layer_class == 'internal': @@ -151,6 +152,8 @@ class PCBLayer(object): else: return None + def __repr__(self): + return ''.format(self.layer_class) class DrillLayer(PCBLayer): @classmethod @@ -163,6 +166,7 @@ class DrillLayer(PCBLayer): class InternalLayer(PCBLayer): + @classmethod def from_gerber(cls, camfile): filename = camfile.filename @@ -208,6 +212,7 @@ class InternalLayer(PCBLayer): class LayerSet(object): + def __init__(self, name, layers, **kwargs): super(LayerSet, self).__init__(**kwargs) self.name = name diff --git a/gerber/operations.py b/gerber/operations.py index 4eb10e5..d06876e 100644 --- a/gerber/operations.py +++ b/gerber/operations.py @@ -22,6 +22,7 @@ CAM File Operations """ import copy + def to_inch(cam_file): """ Convert Gerber or Excellon file units to imperial @@ -39,6 +40,7 @@ def to_inch(cam_file): cam_file.to_inch() return cam_file + def to_metric(cam_file): """ Convert Gerber or Excellon file units to metric @@ -56,6 +58,7 @@ def to_metric(cam_file): cam_file.to_metric() return cam_file + def offset(cam_file, x_offset, y_offset): """ Offset a Cam file by a specified amount in the X and Y directions. @@ -79,6 +82,7 @@ def offset(cam_file, x_offset, y_offset): cam_file.offset(x_offset, y_offset) return cam_file + def scale(cam_file, x_scale, y_scale): """ Scale a Cam file by a specified amount in the X and Y directions. @@ -101,6 +105,7 @@ def scale(cam_file, x_scale, y_scale): # TODO pass + def rotate(cam_file, angle): """ Rotate a Cam file a specified amount about the origin. diff --git a/gerber/pcb.py b/gerber/pcb.py index 0518dd4..92a1f28 100644 --- a/gerber/pcb.py +++ b/gerber/pcb.py @@ -63,13 +63,15 @@ class PCB(object): @property def top_layers(self): - board_layers = [l for l in reversed(self.layers) if l.layer_class in ('topsilk', 'topmask', 'top')] + board_layers = [l for l in reversed(self.layers) if l.layer_class in + ('topsilk', 'topmask', 'top')] drill_layers = [l for l in self.drill_layers if 'top' in l.layers] return board_layers + drill_layers @property def bottom_layers(self): - board_layers = [l for l in self.layers if l.layer_class in ('bottomsilk', 'bottommask', 'bottom')] + board_layers = [l for l in self.layers if l.layer_class in + ('bottomsilk', 'bottommask', 'bottom')] drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers] return board_layers + drill_layers @@ -77,11 +79,17 @@ class PCB(object): def drill_layers(self): return [l for l in self.layers if l.layer_class == 'drill'] + @property + def copper_layers(self): + return [layer for layer in self.layers if layer.layer_class in + ('top', 'bottom', 'internal')] + @property def layer_count(self): """ Number of *COPPER* layers """ - return len([l for l in self.layers if l.layer_class in ('top', 'bottom', 'internal')]) + return len([l for l in self.layers if l.layer_class in + ('top', 'bottom', 'internal')]) @property def board_bounds(self): @@ -91,4 +99,3 @@ class PCB(object): for layer in self.layers: if layer.layer_class == 'top': return layer.bounds - diff --git a/gerber/primitives.py b/gerber/primitives.py index 0ac12af..24e13a2 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# copyright 2014 Hamilton Kibbe +# copyright 2016 Hamilton Kibbe # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,10 +14,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + + import math -from operator import add, sub +from operator import add +from itertools import combinations -from .utils import validate_coordinates, inch, metric +from .utils import validate_coordinates, inch, metric, convex_hull class Primitive(object): @@ -35,17 +38,65 @@ class Primitive(object): rotation : float Rotation of a primitive about its origin in degrees. Positive rotation is counter-clockwise as viewed from the board top. + + units : string + Units in which primitive was defined. 'inch' or 'metric' + + net_name : string + Name of the electrical net the primitive belongs to """ - def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None): + + def __init__(self, level_polarity='dark', rotation=0, units=None, net_name=None): self.level_polarity = level_polarity - self.rotation = rotation - self.units = units + self.net_name = net_name self._to_convert = list() - self.id = id - self.statement_id = statement_id + self._memoized = list() + self._units = units + self._rotation = rotation + self._cos_theta = math.cos(math.radians(rotation)) + self._sin_theta = math.sin(math.radians(rotation)) + self._bounding_box = None + self._vertices = None + self._segments = None + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def units(self): + return self._units + + @units.setter + def units(self, value): + self._changed() + self._units = value + + @property + def rotation(self): + return self._rotation + @rotation.setter + def rotation(self, value): + self._changed() + self._rotation = value + self._cos_theta = math.cos(math.radians(value)) + self._sin_theta = math.sin(math.radians(value)) + + @property + def vertices(self): + return None + + @property + def segments(self): + if self._segments is None: + if self.vertices is not None and len(self.vertices): + self._segments = [segment for segment in + combinations(self.vertices, 2)] + return self._segments + + @property def bounding_box(self): - """ Calculate bounding box + """ Calculate axis-aligned bounding box will be helpful for sweep & prune during DRC clearance checks. @@ -55,9 +106,12 @@ class Primitive(object): 'implemented in subclass') def to_inch(self): + """ Convert primitive units to inches. + """ if self.units == 'metric': self.units = 'inch' - for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: if hasattr(value, 'to_inch'): value.to_inch() else: @@ -67,18 +121,22 @@ class Primitive(object): for v in value: v.to_inch() elif isinstance(value[0], tuple): - setattr(self, attr, [tuple(map(inch, point)) for point in value]) + setattr(self, attr, + [tuple(map(inch, point)) + for point in value]) else: setattr(self, attr, tuple(map(inch, value))) except: if value is not None: setattr(self, attr, inch(value)) - def to_metric(self): + """ Convert primitive units to metric. + """ if self.units == 'inch': self.units = 'metric' - for attr, value in [(attr, getattr(self, attr)) for attr in self._to_convert]: + for attr, value in [(attr, getattr(self, attr)) + for attr in self._to_convert]: if hasattr(value, 'to_metric'): value.to_metric() else: @@ -88,7 +146,9 @@ class Primitive(object): for v in value: v.to_metric() elif isinstance(value[0], tuple): - setattr(self, attr, [tuple(map(metric, point)) for point in value]) + setattr(self, attr, + [tuple(map(metric, point)) + for point in value]) else: setattr(self, attr, tuple(map(metric, value))) except: @@ -96,120 +156,173 @@ class Primitive(object): setattr(self, attr, metric(value)) def offset(self, x_offset=0, y_offset=0): - pass + """ Move the primitive by the specified x and y offset amount. - def __eq__(self, other): - return self.__dict__ == other.__dict__ + values are specified in the primitive's native units + """ + if hasattr(self, 'position'): + self._changed() + self.position = tuple([coord + offset for coord, offset + in zip(self.position, + (x_offset, y_offset))]) + + + def _changed(self): + """ Clear memoized properties. + + Forces a recalculation next time any memoized propery is queried. + This must be called from a subclass every time a parameter that affects + a memoized property is changed. The easiest way to do this is to call + _changed() from property.setter methods. + """ + self._bounding_box = None + self._vertices = None + self._segments = None + for attr in self._memoized: + setattr(self, attr, None) class Line(Primitive): """ """ + def __init__(self, start, end, aperture, **kwargs): super(Line, self).__init__(**kwargs) - self.start = start - self.end = end + self._start = start + self._end = end self.aperture = aperture self._to_convert = ['start', 'end', 'aperture'] + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property def angle(self): - delta_x, delta_y = tuple(map(sub, self.end, self.start)) + delta_x, delta_y = tuple( + [end - start for end, start in zip(self.end, self.start)]) angle = math.atan2(delta_y, delta_x) return angle @property def bounding_box(self): - if isinstance(self.aperture, Circle): - width_2 = self.aperture.radius - height_2 = width_2 - else: - width_2 = self.aperture.width / 2. - height_2 = self.aperture.height / 2. - min_x = min(self.start[0], self.end[0]) - width_2 - max_x = max(self.start[0], self.end[0]) + width_2 - min_y = min(self.start[1], self.end[1]) - height_2 - max_y = max(self.start[1], self.end[1]) + height_2 - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + if isinstance(self.aperture, Circle): + width_2 = self.aperture.radius + height_2 = width_2 + else: + width_2 = self.aperture.width / 2. + height_2 = self.aperture.height / 2. + min_x = min(self.start[0], self.end[0]) - width_2 + max_x = max(self.start[0], self.end[0]) + width_2 + min_y = min(self.start[1], self.end[1]) - height_2 + max_y = max(self.start[1], self.end[1]) + height_2 + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + @property def vertices(self): - if not isinstance(self.aperture, Rectangle): - return None - else: - start = self.start - end = self.end - width = self.aperture.width - height = self.aperture.height - - # Find all the corners of the start and end position - start_ll = (start[0] - (width / 2.), - start[1] - (height / 2.)) - start_lr = (start[0] + (width / 2.), - start[1] - (height / 2.)) - start_ul = (start[0] - (width / 2.), - start[1] + (height / 2.)) - start_ur = (start[0] + (width / 2.), - start[1] + (height / 2.)) - end_ll = (end[0] - (width / 2.), - end[1] - (height / 2.)) - end_lr = (end[0] + (width / 2.), - end[1] - (height / 2.)) - end_ul = (end[0] - (width / 2.), - end[1] + (height / 2.)) - end_ur = (end[0] + (width / 2.), - end[1] + (height / 2.)) - - if end[0] == start[0] and end[1] == start[1]: - return (start_ll, start_lr, start_ur, start_ul) - elif end[0] == start[0] and end[1] > start[1]: - return (start_ll, start_lr, end_ur, end_ul) - elif end[0] > start[0] and end[1] > start[1]: - return (start_ll, start_lr, end_lr, end_ur, end_ul, start_ul) - elif end[0] > start[0] and end[1] == start[1]: - return (start_ll, end_lr, end_ur, start_ul) - elif end[0] > start[0] and end[1] < start[1]: - return (start_ll, end_ll, end_lr, end_ur, start_ur, start_ul) - elif end[0] == start[0] and end[1] < start[1]: - return (end_ll, end_lr, start_ur, start_ul) - elif end[0] < start[0] and end[1] < start[1]: - return (end_ll, end_lr, start_lr, start_ur, start_ul, end_ul) - elif end[0] < start[0] and end[1] == start[1]: - return (end_ll, start_lr, start_ur, end_ul) - elif end[0] < start[0] and end[1] > start[1]: - return (start_ll, start_lr, start_ur, end_ur, end_ul, end_ll) - + if self._vertices is None: + if isinstance(self.aperture, Rectangle): + start = self.start + end = self.end + width = self.aperture.width + height = self.aperture.height + + # Find all the corners of the start and end position + start_ll = (start[0] - (width / 2.), start[1] - (height / 2.)) + start_lr = (start[0] + (width / 2.), start[1] - (height / 2.)) + start_ul = (start[0] - (width / 2.), start[1] + (height / 2.)) + start_ur = (start[0] + (width / 2.), start[1] + (height / 2.)) + end_ll = (end[0] - (width / 2.), end[1] - (height / 2.)) + end_lr = (end[0] + (width / 2.), end[1] - (height / 2.)) + end_ul = (end[0] - (width / 2.), end[1] + (height / 2.)) + end_ur = (end[0] + (width / 2.), end[1] + (height / 2.)) + + # The line is defined by the convex hull of the points + self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur)) + return self._vertices 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))) + self._changed() + self.start = tuple([coord + offset for coord, offset + in zip(self.start, (x_offset, y_offset))]) + self.end = tuple([coord + offset for coord, offset + in zip(self.end, (x_offset, y_offset))]) class Arc(Primitive): """ """ + def __init__(self, start, end, center, direction, aperture, **kwargs): super(Arc, self).__init__(**kwargs) - self.start = start - self.end = end - self.center = center + self._start = start + self._end = end + self._center = center self.direction = direction self.aperture = aperture self._to_convert = ['start', 'end', 'center', 'aperture'] + @property + def start(self): + return self._start + + @start.setter + def start(self, value): + self._changed() + self._start = value + + @property + def end(self): + return self._end + + @end.setter + def end(self, value): + self._changed() + self._end = value + + @property + def center(self): + return self._center + + @center.setter + def center(self, value): + self._changed() + self._center = value + @property def radius(self): - dy, dx = map(sub, self.start, self.center) - return math.sqrt(dy**2 + dx**2) + dy, dx = tuple([start - center for start, center + in zip(self.start, self.center)]) + return math.sqrt(dy ** 2 + dx ** 2) @property def start_angle(self): - dy, dx = map(sub, self.start, self.center) + dy, dx = tuple([start - center for start, center + in zip(self.start, self.center)]) return math.atan2(dx, dy) @property def end_angle(self): - dy, dx = map(sub, self.end, self.center) + dy, dx = tuple([end - center for end, center + in zip(self.end, self.center)]) return math.atan2(dx, dy) @property @@ -225,44 +338,51 @@ class Arc(Primitive): @property def bounding_box(self): - two_pi = 2 * math.pi - theta0 = (self.start_angle + two_pi) % two_pi - theta1 = (self.end_angle + two_pi) % two_pi - points = [self.start, self.end] - if self.direction == 'counterclockwise': - # Passes through 0 degrees - if theta0 > theta1: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): - points.append((self.center[0], self.center[1] - self.radius )) - else: - # Passes through 0 degrees - if theta1 > theta0: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): - points.append((self.center[0], self.center[1] - self.radius )) - x, y = zip(*points) - min_x = min(x) - self.aperture.radius - max_x = max(x) + self.aperture.radius - min_y = min(y) - self.aperture.radius - max_y = max(y) + self.aperture.radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 > theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta0 <= math.pi / \ + 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta0 <= math.pi * \ + 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + points.append((self.center[0], self.center[1] - self.radius)) + else: + # Passes through 0 degrees + if theta1 > theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta1 <= math.pi / \ + 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta1 <= math.pi * \ + 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): + points.append((self.center[0], self.center[1] - self.radius)) + x, y = zip(*points) + min_x = min(x) - self.aperture.radius + max_x = max(x) + self.aperture.radius + min_y = min(y) - self.aperture.radius + max_y = max(y) + self.aperture.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box def offset(self, x_offset=0, y_offset=0): + self._changed() self.start = tuple(map(add, self.start, (x_offset, y_offset))) self.end = tuple(map(add, self.end, (x_offset, y_offset))) self.center = tuple(map(add, self.center, (x_offset, y_offset))) @@ -271,256 +391,465 @@ class Arc(Primitive): class Circle(Primitive): """ """ + def __init__(self, position, diameter, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.diameter = diameter + self._position = position + self._diameter = diameter self._to_convert = ['position', 'diameter'] + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value + @property def radius(self): return self.diameter / 2. @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box class Ellipse(Primitive): """ """ + def __init__(self, position, width, height, **kwargs): super(Ellipse, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def bounding_box(self): - min_x = self.position[0] - (self._abs_width / 2.0) - max_x = self.position[0] + (self._abs_width / 2.0) - min_y = self.position[1] - (self._abs_height / 2.0) - max_y = self.position[1] + (self._abs_height / 2.0) - return ((min_x, max_x), (min_y, max_y)) + def width(self): + return self._width - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def bounding_box(self): + if self._bounding_box is None: + min_x = self.position[0] - (self.axis_aligned_width / 2.0) + max_x = self.position[0] + (self.axis_aligned_width / 2.0) + min_y = self.position[1] - (self.axis_aligned_height / 2.0) + max_y = self.position[1] + (self.axis_aligned_height / 2.0) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box @property - def _abs_width(self): + def axis_aligned_width(self): ux = (self.width / 2.) * math.cos(math.radians(self.rotation)) - vx = (self.height / 2.) * math.cos(math.radians(self.rotation) + (math.pi / 2.)) + vx = (self.height / 2.) * \ + math.cos(math.radians(self.rotation) + (math.pi / 2.)) return 2 * math.sqrt((ux * ux) + (vx * vx)) - + @property - def _abs_height(self): + def axis_aligned_height(self): uy = (self.width / 2.) * math.sin(math.radians(self.rotation)) - vy = (self.height / 2.) * math.sin(math.radians(self.rotation) + (math.pi / 2.)) + vy = (self.height / 2.) * \ + math.sin(math.radians(self.rotation) + (math.pi / 2.)) return 2 * math.sqrt((uy * uy) + (vy * vy)) class Rectangle(Primitive): """ """ + def __init__(self, position, width, height, **kwargs): super(Rectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] - + self._lower_left = None + self._upper_right = None @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def lower_left(self): + return (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def vertices(self): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + ll = ((self.position[0] - delta_w), (self.position[1] - delta_h)) + ul = ((self.position[0] - delta_w), (self.position[1] + delta_h)) + ur = ((self.position[0] + delta_w), (self.position[1] + delta_h)) + lr = ((self.position[0] + delta_w), (self.position[1] - delta_h)) + self._vertices = [((x * self._cos_theta - y * self._sin_theta), + (x * self._sin_theta + y * self._cos_theta)) + for x, y in [ll, ul, ur, lr]] + return self._vertices @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) - + def axis_aligned_height(self): + return (self._cos_theta * self.height + self._sin_theta * self.width) + class Diamond(Primitive): """ """ + def __init__(self, position, width, height, **kwargs): super(Diamond, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def vertices(self): + if self._vertices is None: + delta_w = self.width / 2. + delta_h = self.height / 2. + top = (self.position[0], (self.position[1] + delta_h)) + right = ((self.position[0] + delta_w), self.position[1]) + bottom = (self.position[0], (self.position[1] - delta_h)) + left = ((self.position[0] - delta_w), self.position[1]) + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in [top, right, bottom, left]] + return self._vertices @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + self._sin_theta * self.width) class ChamferRectangle(Primitive): """ """ + def __init__(self, position, width, height, chamfer, corners, **kwargs): super(ChamferRectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height - self.chamfer = chamfer - self.corners = corners + self._position = position + self._width = width + self._height = height + self._chamfer = chamfer + self._corners = corners self._to_convert = ['position', 'width', 'height', 'chamfer'] @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def chamfer(self): + return self._chamfer + + @chamfer.setter + def chamfer(self, value): + self._changed() + self._chamfer = value + + @property + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def vertices(self): + # TODO + return self._vertices @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + class RoundRectangle(Primitive): """ """ + def __init__(self, position, width, height, radius, corners, **kwargs): super(RoundRectangle, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height - self.radius = radius - self.corners = corners + self._position = position + self._width = width + self._height = height + self._radius = radius + self._corners = corners self._to_convert = ['position', 'width', 'height', 'radius'] @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value @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 height(self): + return self._height - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @height.setter + def height(self, value): + self._changed() + self._height = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + + @property + def corners(self): + return self._corners + + @corners.setter + def corners(self, value): + self._changed() + self._corners = value @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def bounding_box(self): + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box + + @property + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + class Obround(Primitive): """ """ + def __init__(self, position, width, height, **kwargs): super(Obround, self).__init__(**kwargs) validate_coordinates(position) - self.position = position - self.width = width - self.height = height + self._position = position + self._width = width + self._height = height self._to_convert = ['position', 'width', 'height'] @property - def lower_left(self): - return (self.position[0] - (self._abs_width / 2.), - self.position[1] - (self._abs_height / 2.)) + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._changed() + self._width = value @property - def upper_right(self): - return (self.position[0] + (self._abs_width / 2.), - self.position[1] + (self._abs_height / 2.)) + def height(self): + return self._height + + @height.setter + def height(self, value): + self._changed() + self._height = value @property def orientation(self): @@ -528,68 +857,102 @@ class Obround(Primitive): @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)) + if self._bounding_box is None: + ll = (self.position[0] - (self.axis_aligned_width / 2.), + self.position[1] - (self.axis_aligned_height / 2.)) + ur = (self.position[0] + (self.axis_aligned_width / 2.), + self.position[1] + (self.axis_aligned_height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box @property def subshapes(self): if self.orientation == 'vertical': circle1 = Circle((self.position[0], self.position[1] + - (self.height-self.width) / 2.), self.width) + (self.height - self.width) / 2.), self.width) circle2 = Circle((self.position[0], self.position[1] - - (self.height-self.width) / 2.), self.width) + (self.height - self.width) / 2.), self.width) rect = Rectangle(self.position, self.width, - (self.height - self.width)) + (self.height - self.width)) else: - circle1 = Circle((self.position[0] - (self.height - self.width) / 2., + circle1 = Circle((self.position[0] + - (self.height - self.width) / 2., self.position[1]), self.height) - circle2 = Circle((self.position[0] + (self.height - self.width) / 2., + circle2 = Circle((self.position[0] + + (self.height - self.width) / 2., self.position[1]), self.height) rect = Rectangle(self.position, (self.width - self.height), - self.height) + self.height) return {'circle1': circle1, 'circle2': circle2, 'rectangle': rect} - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) - @property - def _abs_width(self): - return (math.cos(math.radians(self.rotation)) * self.width + - math.sin(math.radians(self.rotation)) * self.height) + def axis_aligned_width(self): + return (self._cos_theta * self.width + + self._sin_theta * self.height) + @property - def _abs_height(self): - return (math.cos(math.radians(self.rotation)) * self.height + - math.sin(math.radians(self.rotation)) * self.width) + def axis_aligned_height(self): + return (self._cos_theta * self.height + + self._sin_theta * self.width) + class Polygon(Primitive): """ """ + def __init__(self, position, sides, radius, **kwargs): super(Polygon, self).__init__(**kwargs) validate_coordinates(position) - self.position = position + self._position = position self.sides = sides - self.radius = radius + self._radius = radius self._to_convert = ['position', 'radius'] + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._changed() + self._radius = value + @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + @property + def vertices(self): + if self._vertices is None: + theta = math.radians(360/self.sides) + vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), + self.position[1] + (math.sin(theta * side) * self.radius)) + for side in range(self.sides)] + self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), + ((x * self._sin_theta) + (y * self._cos_theta))) + for x, y in vertices] + return self._vertices class Region(Primitive): """ """ + def __init__(self, primitives, **kwargs): super(Region, self).__init__(**kwargs) self.primitives = primitives @@ -597,16 +960,19 @@ class Region(Primitive): @property def bounding_box(self): - xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) - minx, maxx = zip(*xlims) - miny, maxy = zip(*ylims) - min_x = min(minx) - max_x = max(maxx) - min_y = min(miny) - max_y = max(maxy) - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + xlims, ylims = zip(*[p.bounding_box for p in self.primitives]) + minx, maxx = zip(*xlims) + miny, maxy = zip(*ylims) + min_x = min(minx) + max_x = max(maxx) + min_y = min(miny) + max_y = max(maxy) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box def offset(self, x_offset=0, y_offset=0): + self._changed() for p in self.primitives: p.offset(x_offset, y_offset) @@ -614,6 +980,7 @@ class Region(Primitive): class RoundButterfly(Primitive): """ A circle with two diagonally-opposite quadrants removed """ + def __init__(self, position, diameter, **kwargs): super(RoundButterfly, self).__init__(**kwargs) validate_coordinates(position) @@ -627,19 +994,19 @@ class RoundButterfly(Primitive): @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box class SquareButterfly(Primitive): """ A square with two diagonally-opposite quadrants removed """ + def __init__(self, position, side, **kwargs): super(SquareButterfly, self).__init__(**kwargs) validate_coordinates(position) @@ -647,31 +1014,33 @@ class SquareButterfly(Primitive): self.side = side self._to_convert = ['position', 'side'] - @property def bounding_box(self): - min_x = self.position[0] - (self.side / 2.) - max_x = self.position[0] + (self.side / 2.) - min_y = self.position[1] - (self.side / 2.) - max_y = self.position[1] + (self.side / 2.) - return ((min_x, max_x), (min_y, max_y)) - - def offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + min_x = self.position[0] - (self.side / 2.) + max_x = self.position[0] + (self.side / 2.) + min_y = self.position[1] - (self.side / 2.) + max_y = self.position[1] + (self.side / 2.) + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box class Donut(Primitive): """ A Shape with an identical concentric shape removed from its center """ - def __init__(self, position, shape, inner_diameter, outer_diameter, **kwargs): + + def __init__(self, position, shape, inner_diameter, + outer_diameter, **kwargs): super(Donut, self).__init__(**kwargs) validate_coordinates(position) self.position = position if shape not in ('round', 'square', 'hexagon', 'octagon'): - raise ValueError('Valid shapes are round, square, hexagon or octagon') + raise ValueError( + 'Valid shapes are round, square, hexagon or octagon') self.shape = shape if inner_diameter >= outer_diameter: - raise ValueError('Outer diameter must be larger than inner diameter.') + raise ValueError( + 'Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter if self.shape in ('round', 'square', 'octagon'): @@ -681,95 +1050,95 @@ class Donut(Primitive): # Hexagon self.width = 0.5 * math.sqrt(3.) * outer_diameter self.height = outer_diameter - self._to_convert = ['position', 'width', 'height', 'inner_diameter', 'outer_diameter'] - - @property - def lower_left(self): - return (self.position[0] - (self.width / 2.), - self.position[1] - (self.height / 2.)) - - @property - def upper_right(self): - return (self.position[0] + (self.width / 2.), - self.position[1] + (self.height / 2.)) + self._to_convert = ['position', 'width', + 'height', 'inner_diameter', 'outer_diameter'] @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 offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + ll = (self.position[0] - (self.width / 2.), + self.position[1] - (self.height / 2.)) + ur = (self.position[0] + (self.width / 2.), + self.position[1] + (self.height / 2.)) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box 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.') + raise ValueError( + 'Outer diameter must be larger than inner diameter.') self.inner_diameter = inner_diameter self.outer_diameter = outer_diameter self._to_convert = ['position', 'inner_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 offset(self, x_offset=0, y_offset=0): - self.position = tuple(map(add, self.position, (x_offset, y_offset))) + if self._bounding_box is None: + ll = tuple([c - self.outer_diameter / 2. for c in self.position]) + ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) + return self._bounding_box class Drill(Primitive): """ A drill hole """ + def __init__(self, position, diameter, **kwargs): super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) - self.position = position - self.diameter = diameter + self._position = position + self._diameter = diameter self._to_convert = ['position', 'diameter'] + @property + def position(self): + return self._position + + @position.setter + def position(self, value): + self._changed() + self._position = value + + @property + def diameter(self): + return self._diameter + + @diameter.setter + def diameter(self, value): + self._changed() + self._diameter = value + @property def radius(self): return self.diameter / 2. @property def bounding_box(self): - min_x = self.position[0] - self.radius - max_x = self.position[0] + self.radius - min_y = self.position[1] - self.radius - max_y = self.position[1] + self.radius - return ((min_x, max_x), (min_y, max_y)) + if self._bounding_box is None: + min_x = self.position[0] - self.radius + max_x = self.position[0] + self.radius + min_y = self.position[1] - self.radius + max_y = self.position[1] + self.radius + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box - 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 4e71e75..cc2722a 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,8 +17,6 @@ import cairocffi as cairo -from operator import mul -import math import tempfile from .render import GerberContext, RenderSettings @@ -32,11 +30,14 @@ except(ImportError): class GerberCairoContext(GerberContext): + def __init__(self, scale=300): - GerberContext.__init__(self) + super(GerberCairoContext, self).__init__() self.scale = (scale, scale) self.surface = None self.ctx = None + self.active_layer = None + self.output_ctx = None self.bg = False self.mask = None self.mask_ctx = None @@ -46,37 +47,40 @@ class GerberCairoContext(GerberContext): @property def origin_in_pixels(self): - return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0) + return (self.scale_point(self.origin_in_inch) + if self.origin_in_inch is not None else (0.0, 0.0)) @property def size_in_pixels(self): - return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0) + return (self.scale_point(self.size_in_inch) + if self.size_in_inch is not None else (0.0, 0.0)) def set_bounds(self, bounds, new_surface=False): origin_in_inch = (bounds[0][0], bounds[1][0]) - size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0])) - size_in_pixels = tuple(map(mul, size_in_inch, self.scale)) + size_in_inch = (abs(bounds[0][1] - bounds[0][0]), + abs(bounds[1][1] - bounds[1][0])) + size_in_pixels = self.scale_point(size_in_inch) self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch if (self.surface is None) or new_surface: self.surface_buffer = tempfile.NamedTemporaryFile() - self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) - self.ctx = cairo.Context(self.surface) - self.ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) - self.ctx.scale(1, -1) - self.ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1]) - self.mask = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) - self.mask_ctx = cairo.Context(self.mask) - self.mask_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) - self.mask_ctx.scale(1, -1) - self.mask_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1]) - self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) + self.surface = cairo.SVGSurface( + self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) + self.output_ctx = cairo.Context(self.surface) + self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + self.output_ctx.scale(1, -1) + self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]), + (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, + x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) def render_layers(self, layers, filename, theme=THEMES['default']): """ Render a set of layers """ self.set_bounds(layers[0].bounds, True) self._paint_background(True) + for layer in layers: self._render_layer(layer, theme) self.dump(filename) @@ -114,158 +118,181 @@ class GerberCairoContext(GerberContext): self.color = settings.color self.alpha = settings.alpha self.invert = settings.invert + + # Get a new clean layer to render on + self._new_render_layer() if settings.mirror: raise Warning('mirrored layers aren\'t supported yet...') - if self.invert: - self._clear_mask() for prim in layer.primitives: self.render(prim) - if self.invert: - self._render_mask() + # Add layer to image + self._flatten() def _render_line(self, line, color): - start = map(mul, line.start, self.scale) - end = map(mul, line.end, self.scale) + start = [pos * scale for pos, scale in zip(line.start, self.scale)] + end = [pos * scale for pos, scale in zip(line.end, self.scale)] if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if line.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): width = line.aperture.diameter - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) - ctx.line_to(*end) - ctx.stroke() + 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() elif isinstance(line.aperture, Rectangle): - points = [tuple(map(mul, x, self.scale)) for x in line.vertices] - ctx.set_line_width(0) - ctx.move_to(*points[0]) + points = [self.scale_point(x) for x in line.vertices] + self.ctx.set_line_width(0) + self.ctx.move_to(*points[0]) for point in points[1:]: - ctx.line_to(*point) - ctx.fill() + self.ctx.line_to(*point) + self.ctx.fill() def _render_arc(self, arc, color): - center = map(mul, arc.center, self.scale) - start = map(mul, arc.start, self.scale) - end = map(mul, arc.end, self.scale) + center = self.scale_point(arc.center) + start = self.scale_point(arc.start) + end = self.scale_point(arc.end) radius = self.scale[0] * arc.radius angle1 = arc.start_angle angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if arc.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(width * self.scale[0]) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*start) # You actually have to do this... + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(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) # You actually have to do this... if arc.direction == 'counterclockwise': - ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) else: - ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - ctx.move_to(*end) # ...lame + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if region.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator(cairo.OPERATOR_OVER + if region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.move_to(*tuple(map(mul, region.primitives[0].start, self.scale))) - for p in region.primitives: - if isinstance(p, Line): - ctx.line_to(*tuple(map(mul, p.end, self.scale))) + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + self.ctx.line_to(*self.scale_point(prim.end)) else: - center = map(mul, p.center, self.scale) - start = map(mul, p.start, self.scale) - end = map(mul, p.end, self.scale) - radius = self.scale[0] * p.radius - angle1 = p.start_angle - angle2 = p.end_angle - if p.direction == 'counterclockwise': - ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) else: - ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) - ctx.fill() + self.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + self.ctx.fill() def _render_circle(self, circle, color): - center = tuple(map(mul, circle.position, self.scale)) + center = self.scale_point(circle.position) if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if circle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.arc(*center, radius=circle.radius * self.scale[0], angle1=0, angle2=2 * math.pi) - ctx.fill() + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.arc(*center, radius=circle.radius * + self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.fill() def _render_rectangle(self, rectangle, color): - ll = map(mul, rectangle.lower_left, self.scale) - width, height = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) + if not self.invert: - ctx = self.ctx - ctx.set_source_rgba(*color, alpha=self.alpha) - ctx.set_operator(cairo.OPERATOR_OVER if rectangle.level_polarity == "dark" else cairo.OPERATOR_CLEAR) + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_operator( + cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) else: - ctx = self.mask_ctx - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_CLEAR) - ctx.set_line_width(0) - ctx.rectangle(*ll, width=width, height=height) - ctx.fill() + self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_line_width(0) + self.ctx.rectangle(*lower_left, width=width, height=height) + self.ctx.fill() def _render_obround(self, obround, color): self._render_circle(obround.subshapes['circle1'], color) self._render_circle(obround.subshapes['circle2'], color) self._render_rectangle(obround.subshapes['rectangle'], color) - def _render_drill(self, circle, color): + def _render_drill(self, circle, color=None): + color = color if color is not None else self.drill_color self._render_circle(circle, color) def _render_test_record(self, primitive, color): - position = tuple(map(add, primitive.position, self.origin_in_inch)) + position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)] self.ctx.set_operator(cairo.OPERATOR_OVER) - self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + self.ctx.select_font_face( + 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) self.ctx.set_font_size(13) self._render_circle(Circle(position, 0.015), color) self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER if primitive.level_polarity == "dark" else cairo.OPERATOR_CLEAR) - self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) + self.ctx.set_operator( + cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) + self.ctx.move_to(*[self.scale[0] * (coord + 0.015) + for coord in position]) self.ctx.scale(1, -1) self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _clear_mask(self): - self.mask_ctx.set_operator(cairo.OPERATOR_OVER) - self.mask_ctx.set_source_rgba(*self.color, alpha=self.alpha) - self.mask_ctx.paint() + def _new_render_layer(self, color=None): + size_in_pixels = self.scale_point(self.size_in_inch) + layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) + ctx = cairo.Context(layer) + ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + ctx.scale(1, -1) + ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), + (-self.origin_in_inch[1] * self.scale[0]) + - size_in_pixels[1]) + if self.invert: + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_rgba(*self.color, alpha=self.alpha) + ctx.paint() + self.ctx = ctx + self.active_layer = layer - def _render_mask(self): - self.ctx.set_operator(cairo.OPERATOR_OVER) - ptn = cairo.SurfacePattern(self.mask) + def _flatten(self): + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + ptn = cairo.SurfacePattern(self.active_layer) ptn.set_matrix(self._xform_matrix) - self.ctx.set_source(ptn) - self.ctx.paint() + self.output_ctx.set_source(ptn) + self.output_ctx.paint() + self.ctx = None + self.active_layer = None def _paint_background(self, force=False): if (not self.bg) or force: self.bg = True - self.ctx.set_source_rgba(*self.background_color, alpha=1.0) - self.ctx.paint() + self.output_ctx.set_operator(cairo.OPERATOR_OVER) + self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + self.output_ctx.paint() + + def scale_point(self, point): + return tuple([coord * scale for coord, scale in zip(point, self.scale)]) diff --git a/gerber/render/render.py b/gerber/render/render.py index 6af8bf1..d7a62e1 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -57,12 +57,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ + def __init__(self, units='inch'): self._units = units self._color = (0.7215, 0.451, 0.200) self._background_color = (0.0, 0.0, 0.0) self._alpha = 1.0 self._invert = False + self.ctx = None @property def units(self): @@ -132,8 +134,7 @@ class GerberContext(object): self._invert = invert def render(self, primitive): - color = (self.color if primitive.level_polarity == 'dark' - else self.background_color) + color = self.color if isinstance(primitive, Line): self._render_line(primitive, color) elif isinstance(primitive, Arc): @@ -155,6 +156,7 @@ class GerberContext(object): else: return + def _render_line(self, primitive, color): pass @@ -184,9 +186,9 @@ class GerberContext(object): class RenderSettings(object): + def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False): self.color = color self.alpha = alpha self.invert = invert self.mirror = mirror - diff --git a/gerber/render/theme.py b/gerber/render/theme.py index e538df8..6135ccb 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -23,7 +23,7 @@ COLORS = { 'white': (1.0, 1.0, 1.0), 'red': (1.0, 0.0, 0.0), 'green': (0.0, 1.0, 0.0), - 'blue' : (0.0, 0.0, 1.0), + 'blue': (0.0, 0.0, 1.0), 'fr-4': (0.290, 0.345, 0.0), 'green soldermask': (0.0, 0.612, 0.396), 'blue soldermask': (0.059, 0.478, 0.651), @@ -36,6 +36,7 @@ COLORS = { class Theme(object): + def __init__(self, name=None, **kwargs): self.name = 'Default' if name is None else name self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0)) @@ -67,4 +68,3 @@ THEMES = { topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), } - diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 319d58f..b19913b 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -93,6 +93,7 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ + def __init__(self, statements, settings, primitives, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) @@ -181,7 +182,8 @@ class GerberParser(object): DEPRECATED_FORMAT = re.compile(r'(?PG9[01])\*') # end deprecated - PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, + AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] @@ -362,7 +364,8 @@ class GerberParser(object): # deprecated codes (deprecated_unit, r) = _match_one(self.DEPRECATED_UNIT, line) if deprecated_unit: - stmt = MOParamStmt(param="MO", mo="inch" if "G70" in deprecated_unit["mode"] else "metric") + stmt = MOParamStmt(param="MO", mo="inch" if "G70" in + deprecated_unit["mode"] else "metric") self.settings.units = stmt.mode yield stmt line = r @@ -436,8 +439,9 @@ class GerberParser(object): height = modifiers[0][1] aperture = Obround(position=None, width=width, height=height) elif shape == 'P': - # FIXME: not supported yet? - pass + diameter = modifiers[0][0] + sides = modifiers[0][1] + aperture = Polygon(position=None, radius=diameter/2.0, sides=sides) else: aperture = self.macros[shape].build(modifiers) @@ -446,7 +450,8 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity)) + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity)) self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': @@ -476,7 +481,8 @@ class GerberParser(object): self.interpolation = 'linear' elif stmt.function in ('G02', 'G2', 'G03', 'G3'): self.interpolation = 'arc' - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + self.direction = ('clockwise' if stmt.function in + ('G02', 'G2') else 'counterclockwise') if stmt.op: self.op = stmt.op @@ -490,43 +496,71 @@ class GerberParser(object): if self.interpolation == 'linear': if self.region_mode == 'off': - self.primitives.append(Line(start, end, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Line(start, end, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region. + # The current aperture is associated with the region. This + # has no graphical effect, but allows all its attributes to + # be applied to the region. if self.current_region is None: - self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),] - else: - self.current_region.append(Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = [Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units), ] + else: + self.current_region.append(Line(start, end, + self.apertures.get(self.aperture, + Circle((0, 0), 0)), + level_polarity=self.level_polarity, + units=self.settings.units)) else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j center = (start[0] + i, start[1] + j) if self.region_mode == 'off': - self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) else: if self.current_region is None: - self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),] + self.current_region = [Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units), ] else: - self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region.append(Arc(start, end, center, self.direction, + self.apertures[self.aperture], + level_polarity=self.level_polarity, + units=self.settings.units)) elif self.op == "D02": pass elif self.op == "D03": primitive = copy.deepcopy(self.apertures[self.aperture]) - # XXX: temporary fix because there are no primitives for Macros and Polygon + + if primitive is not None: - # XXX: just to make it easy to spot - if isinstance(primitive, type([])): - print(primitive[0].to_gerber()) - else: + + if not isinstance(primitive, AMParamStmt): primitive.position = (x, y) primitive.level_polarity = self.level_polarity primitive.units = self.settings.units self.primitives.append(primitive) - + else: + # Aperture Macro + for am_prim in primitive.primitives: + renderable = am_prim.to_primitive((x, y), + self.level_polarity, + self.settings.units) + if renderable is not None: + self.primitives.append(renderable) self.x, self.y = x, y def _evaluate_aperture(self, stmt): diff --git a/gerber/tests/test_am_statements.py b/gerber/tests/test_am_statements.py index 0cee13d..8c95e6a 100644 --- a/gerber/tests/test_am_statements.py +++ b/gerber/tests/test_am_statements.py @@ -7,6 +7,7 @@ from .tests import * from ..am_statements import * from ..am_statements import inch, metric + def test_AMPrimitive_ctor(): for exposure in ('on', 'off', 'ON', 'OFF'): for code in (0, 1, 2, 4, 5, 6, 7, 20, 21, 22): @@ -20,13 +21,13 @@ def test_AMPrimitive_validation(): assert_raises(ValueError, AMPrimitive, 0, 'exposed') assert_raises(ValueError, AMPrimitive, 3, 'off') + def test_AMPrimitive_conversion(): p = AMPrimitive(4, 'on') assert_raises(NotImplementedError, p.to_inch) assert_raises(NotImplementedError, p.to_metric) - def test_AMCommentPrimitive_ctor(): c = AMCommentPrimitive(0, ' This is a comment *') assert_equal(c.code, 0) @@ -47,6 +48,7 @@ def test_AMCommentPrimitive_dump(): c = AMCommentPrimitive(0, 'Rectangle with rounded corners.') assert_equal(c.to_gerber(), '0 Rectangle with rounded corners. *') + def test_AMCommentPrimitive_conversion(): c = AMCommentPrimitive(0, 'Rectangle with rounded corners.') ci = c @@ -56,6 +58,7 @@ def test_AMCommentPrimitive_conversion(): assert_equal(c, ci) assert_equal(c, cm) + def test_AMCommentPrimitive_string(): c = AMCommentPrimitive(0, 'Test Comment') assert_equal(str(c), '') @@ -83,7 +86,7 @@ def test_AMCirclePrimitive_factory(): assert_equal(c.code, 1) assert_equal(c.exposure, 'off') assert_equal(c.diameter, 5) - assert_equal(c.position, (0,0)) + assert_equal(c.position, (0, 0)) def test_AMCirclePrimitive_dump(): @@ -92,6 +95,7 @@ def test_AMCirclePrimitive_dump(): c = AMCirclePrimitive(1, 'on', 5, (0, 0)) assert_equal(c.to_gerber(), '1,1,5,0,0*') + def test_AMCirclePrimitive_conversion(): c = AMCirclePrimitive(1, 'off', 25.4, (25.4, 0)) c.to_inch() @@ -103,8 +107,11 @@ def test_AMCirclePrimitive_conversion(): assert_equal(c.diameter, 25.4) assert_equal(c.position, (25.4, 0)) + def test_AMVectorLinePrimitive_validation(): - assert_raises(ValueError, AMVectorLinePrimitive, 3, 'on', 0.1, (0,0), (3.3, 5.4), 0) + assert_raises(ValueError, AMVectorLinePrimitive, + 3, 'on', 0.1, (0, 0), (3.3, 5.4), 0) + def test_AMVectorLinePrimitive_factory(): l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*') @@ -115,26 +122,32 @@ def test_AMVectorLinePrimitive_factory(): assert_equal(l.end, (12, 0.45)) assert_equal(l.rotation, 0) + def test_AMVectorLinePrimitive_dump(): l = AMVectorLinePrimitive.from_gerber('20,1,0.9,0,0.45,12,0.45,0*') assert_equal(l.to_gerber(), '20,1,0.9,0.0,0.45,12.0,0.45,0.0*') + def test_AMVectorLinePrimtive_conversion(): - l = AMVectorLinePrimitive(20, 'on', 25.4, (0,0), (25.4, 25.4), 0) + l = AMVectorLinePrimitive(20, 'on', 25.4, (0, 0), (25.4, 25.4), 0) l.to_inch() assert_equal(l.width, 1) assert_equal(l.start, (0, 0)) assert_equal(l.end, (1, 1)) - l = AMVectorLinePrimitive(20, 'on', 1, (0,0), (1, 1), 0) + l = AMVectorLinePrimitive(20, 'on', 1, (0, 0), (1, 1), 0) l.to_metric() assert_equal(l.width, 25.4) assert_equal(l.start, (0, 0)) assert_equal(l.end, (25.4, 25.4)) + def test_AMOutlinePrimitive_validation(): - assert_raises(ValueError, AMOutlinePrimitive, 7, 'on', (0,0), [(3.3, 5.4), (4.0, 5.4), (0, 0)], 0) - assert_raises(ValueError, AMOutlinePrimitive, 4, 'on', (0,0), [(3.3, 5.4), (4.0, 5.4), (0, 1)], 0) + assert_raises(ValueError, AMOutlinePrimitive, 7, 'on', + (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 0)], 0) + assert_raises(ValueError, AMOutlinePrimitive, 4, 'on', + (0, 0), [(3.3, 5.4), (4.0, 5.4), (0, 1)], 0) + def test_AMOutlinePrimitive_factory(): o = AMOutlinePrimitive.from_gerber('4,1,3,0,0,3,3,3,0,0,0,0*') @@ -144,12 +157,15 @@ def test_AMOutlinePrimitive_factory(): assert_equal(o.points, [(3, 3), (3, 0), (0, 0)]) assert_equal(o.rotation, 0) + def test_AMOUtlinePrimitive_dump(): o = AMOutlinePrimitive(4, 'on', (0, 0), [(3, 3), (3, 0), (0, 0)], 0) assert_equal(o.to_gerber(), '4,1,3,0,0,3,3,3,0,0,0,0*') + def test_AMOutlinePrimitive_conversion(): - o = AMOutlinePrimitive(4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0) + o = AMOutlinePrimitive( + 4, 'on', (0, 0), [(25.4, 25.4), (25.4, 0), (0, 0)], 0) o.to_inch() assert_equal(o.start_point, (0, 0)) assert_equal(o.points, ((1., 1.), (1., 0.), (0., 0.))) @@ -165,6 +181,7 @@ def test_AMPolygonPrimitive_validation(): assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 2, (3.3, 5.4), 3, 0) assert_raises(ValueError, AMPolygonPrimitive, 5, 'on', 13, (3.3, 5.4), 3, 0) + def test_AMPolygonPrimitive_factory(): p = AMPolygonPrimitive.from_gerber('5,1,3,3.3,5.4,3,0') assert_equal(p.code, 5) @@ -174,10 +191,12 @@ def test_AMPolygonPrimitive_factory(): assert_equal(p.diameter, 3) assert_equal(p.rotation, 0) + def test_AMPolygonPrimitive_dump(): p = AMPolygonPrimitive(5, 'on', 3, (3.3, 5.4), 3, 0) assert_equal(p.to_gerber(), '5,1,3,3.3,5.4,3,0*') + def test_AMPolygonPrimitive_conversion(): p = AMPolygonPrimitive(5, 'off', 3, (25.4, 0), 25.4, 0) p.to_inch() @@ -191,7 +210,9 @@ def test_AMPolygonPrimitive_conversion(): def test_AMMoirePrimitive_validation(): - assert_raises(ValueError, AMMoirePrimitive, 7, (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0) + assert_raises(ValueError, AMMoirePrimitive, 7, + (0, 0), 5.1, 0.2, 0.4, 6, 0.1, 6.1, 0) + def test_AMMoirePrimitive_factory(): m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*') @@ -205,10 +226,12 @@ def test_AMMoirePrimitive_factory(): assert_equal(m.crosshair_length, 6) assert_equal(m.rotation, 0) + def test_AMMoirePrimitive_dump(): m = AMMoirePrimitive.from_gerber('6,0,0,5,0.5,0.5,2,0.1,6,0*') assert_equal(m.to_gerber(), '6,0,0,5.0,0.5,0.5,2,0.1,6.0,0.0*') + def test_AMMoirePrimitive_conversion(): m = AMMoirePrimitive(6, (25.4, 25.4), 25.4, 25.4, 25.4, 6, 25.4, 25.4, 0) m.to_inch() @@ -228,10 +251,12 @@ def test_AMMoirePrimitive_conversion(): assert_equal(m.crosshair_thickness, 25.4) assert_equal(m.crosshair_length, 25.4) + def test_AMThermalPrimitive_validation(): assert_raises(ValueError, AMThermalPrimitive, 8, (0.0, 0.0), 7, 5, 0.2) assert_raises(TypeError, AMThermalPrimitive, 7, (0.0, '0'), 7, 5, 0.2) + def test_AMThermalPrimitive_factory(): t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2*') assert_equal(t.code, 7) @@ -240,10 +265,12 @@ def test_AMThermalPrimitive_factory(): assert_equal(t.inner_diameter, 6) assert_equal(t.gap, 0.2) + def test_AMThermalPrimitive_dump(): t = AMThermalPrimitive.from_gerber('7,0,0,7,6,0.2*') assert_equal(t.to_gerber(), '7,0,0,7.0,6.0,0.2*') + def test_AMThermalPrimitive_conversion(): t = AMThermalPrimitive(7, (25.4, 25.4), 25.4, 25.4, 25.4) t.to_inch() @@ -261,7 +288,9 @@ def test_AMThermalPrimitive_conversion(): def test_AMCenterLinePrimitive_validation(): - assert_raises(ValueError, AMCenterLinePrimitive, 22, 1, 0.2, 0.5, (0, 0), 0) + assert_raises(ValueError, AMCenterLinePrimitive, + 22, 1, 0.2, 0.5, (0, 0), 0) + def test_AMCenterLinePrimtive_factory(): l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*') @@ -272,10 +301,12 @@ def test_AMCenterLinePrimtive_factory(): assert_equal(l.center, (3.4, 0.6)) assert_equal(l.rotation, 0) + def test_AMCenterLinePrimitive_dump(): l = AMCenterLinePrimitive.from_gerber('21,1,6.8,1.2,3.4,0.6,0*') assert_equal(l.to_gerber(), '21,1,6.8,1.2,3.4,0.6,0.0*') + def test_AMCenterLinePrimitive_conversion(): l = AMCenterLinePrimitive(21, 'on', 25.4, 25.4, (25.4, 25.4), 0) l.to_inch() @@ -289,8 +320,11 @@ def test_AMCenterLinePrimitive_conversion(): assert_equal(l.height, 25.4) assert_equal(l.center, (25.4, 25.4)) + def test_AMLowerLeftLinePrimitive_validation(): - assert_raises(ValueError, AMLowerLeftLinePrimitive, 23, 1, 0.2, 0.5, (0, 0), 0) + assert_raises(ValueError, AMLowerLeftLinePrimitive, + 23, 1, 0.2, 0.5, (0, 0), 0) + def test_AMLowerLeftLinePrimtive_factory(): l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*') @@ -301,10 +335,12 @@ def test_AMLowerLeftLinePrimtive_factory(): assert_equal(l.lower_left, (3.4, 0.6)) assert_equal(l.rotation, 0) + def test_AMLowerLeftLinePrimitive_dump(): l = AMLowerLeftLinePrimitive.from_gerber('22,1,6.8,1.2,3.4,0.6,0*') assert_equal(l.to_gerber(), '22,1,6.8,1.2,3.4,0.6,0.0*') + def test_AMLowerLeftLinePrimitive_conversion(): l = AMLowerLeftLinePrimitive(22, 'on', 25.4, 25.4, (25.4, 25.4), 0) l.to_inch() @@ -318,24 +354,23 @@ def test_AMLowerLeftLinePrimitive_conversion(): assert_equal(l.height, 25.4) assert_equal(l.lower_left, (25.4, 25.4)) + def test_AMUnsupportPrimitive(): u = AMUnsupportPrimitive.from_gerber('Test') assert_equal(u.primitive, 'Test') u = AMUnsupportPrimitive('Test') assert_equal(u.to_gerber(), 'Test') + def test_AMUnsupportPrimitive_smoketest(): u = AMUnsupportPrimitive.from_gerber('Test') u.to_inch() u.to_metric() - def test_inch(): assert_equal(inch(25.4), 1) + def test_metric(): assert_equal(metric(1), 25.4) - - - diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index 00a8285..2f0a905 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -54,17 +54,20 @@ def test_filesettings_dict_assign(): assert_equal(fs.zero_suppression, 'leading') assert_equal(fs.format, (1, 2)) + def test_camfile_init(): """ Smoke test CamFile test """ cf = CamFile() + def test_camfile_settings(): """ Test CamFile Default Settings """ cf = CamFile() assert_equal(cf.settings, FileSettings()) + def test_bounds_override_smoketest(): cf = CamFile() cf.bounds @@ -89,7 +92,7 @@ def test_zeros(): assert_equal(fs.zeros, 'trailing') assert_equal(fs.zero_suppression, 'leading') - fs.zeros= 'leading' + fs.zeros = 'leading' assert_equal(fs.zeros, 'leading') assert_equal(fs.zero_suppression, 'trailing') @@ -113,12 +116,19 @@ def test_zeros(): def test_filesettings_validation(): """ Test FileSettings constructor argument validation """ - assert_raises(ValueError, FileSettings, 'absolute-ish', 'inch', None, (2, 5), None) - assert_raises(ValueError, FileSettings, 'absolute', 'degrees kelvin', None, (2, 5), None) - assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'leading', (2, 5), 'leading') - assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'following', (2, 5), None) - assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5), 'following') - assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5, 6), None) + assert_raises(ValueError, FileSettings, 'absolute-ish', + 'inch', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', + 'degrees kelvin', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', 'leading', (2, 5), 'leading') + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', 'following', (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', None, (2, 5), 'following') + assert_raises(ValueError, FileSettings, 'absolute', + 'inch', None, (2, 5, 6), None) + def test_key_validation(): fs = FileSettings() @@ -129,5 +139,3 @@ def test_key_validation(): assert_raises(ValueError, fs.__setitem__, 'zero_suppression', 'following') assert_raises(ValueError, fs.__setitem__, 'zeros', 'following') assert_raises(ValueError, fs.__setitem__, 'format', (2, 5, 6)) - - diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py index 5991e5e..357ed18 100644 --- a/gerber/tests/test_common.py +++ b/gerber/tests/test_common.py @@ -12,9 +12,10 @@ import os NCDRILL_FILE = os.path.join(os.path.dirname(__file__), - 'resources/ncdrill.DRD') + 'resources/ncdrill.DRD') TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), - 'resources/top_copper.GTL') + 'resources/top_copper.GTL') + def test_file_type_detection(): """ Test file type detection @@ -38,6 +39,3 @@ def test_file_type_validation(): """ Test file format validation """ assert_raises(ParseError, read, 'LICENSE') - - - diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index a9a33c7..e7c77c6 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -13,6 +13,7 @@ from .tests import * NCDRILL_FILE = os.path.join(os.path.dirname(__file__), 'resources/ncdrill.DRD') + def test_format_detection(): """ Test file type detection """ @@ -75,10 +76,11 @@ def test_conversion(): for statement in ncdrill_inch.statements: statement.to_metric() - for m_tool, i_tool in zip(iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values())): + for m_tool, i_tool in zip(iter(ncdrill.tools.values()), + iter(ncdrill_inch.tools.values())): assert_equal(i_tool, m_tool) - for m, i in zip(ncdrill.primitives,inch_primitives): + for m, i in zip(ncdrill.primitives, inch_primitives): assert_equal(m, i) @@ -187,12 +189,10 @@ def test_parse_incremental_position(): p = ExcellonParser(FileSettings(notation='incremental')) p._parse_line('X01Y01') p._parse_line('X01Y01') - assert_equal(p.pos, [2.,2.]) + assert_equal(p.pos, [2., 2.]) def test_parse_unknown(): p = ExcellonParser(FileSettings()) p._parse_line('Not A Valid Statement') assert_equal(p.statements[0].stmt, 'Not A Valid Statement') - - diff --git a/gerber/tests/test_excellon_statements.py b/gerber/tests/test_excellon_statements.py index 2f0ef10..8e6e06e 100644 --- a/gerber/tests/test_excellon_statements.py +++ b/gerber/tests/test_excellon_statements.py @@ -7,11 +7,13 @@ from .tests import assert_equal, assert_not_equal, assert_raises from ..excellon_statements import * from ..cam import FileSettings + def test_excellon_statement_implementation(): stmt = ExcellonStatement() assert_raises(NotImplementedError, stmt.from_excellon, None) assert_raises(NotImplementedError, stmt.to_excellon) + def test_excellontstmt(): """ Smoke test ExcellonStatement """ @@ -20,17 +22,18 @@ def test_excellontstmt(): stmt.to_metric() stmt.offset() + def test_excellontool_factory(): """ Test ExcellonTool factory methods """ exc_line = 'T8F01B02S00003H04Z05C0.12500' settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') tool = ExcellonTool.from_excellon(exc_line, settings) assert_equal(tool.number, 8) assert_equal(tool.diameter, 0.125) assert_equal(tool.feed_rate, 1) - assert_equal(tool.retract_rate,2) + assert_equal(tool.retract_rate, 2) assert_equal(tool.rpm, 3) assert_equal(tool.max_hit_count, 4) assert_equal(tool.depth_offset, 5) @@ -41,7 +44,7 @@ def test_excellontool_factory(): assert_equal(tool.number, 8) assert_equal(tool.diameter, 0.125) assert_equal(tool.feed_rate, 1) - assert_equal(tool.retract_rate,2) + assert_equal(tool.retract_rate, 2) assert_equal(tool.rpm, 3) assert_equal(tool.max_hit_count, 4) assert_equal(tool.depth_offset, 5) @@ -55,7 +58,7 @@ def test_excellontool_dump(): 'T07F0S0C0.04300', 'T08F0S0C0.12500', 'T09F0S0C0.13000', 'T08B01F02H03S00003C0.12500Z04', 'T01F0S300.999C0.01200'] settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') for line in exc_lines: tool = ExcellonTool.from_excellon(line, settings) assert_equal(tool.to_excellon(), line) @@ -63,7 +66,7 @@ def test_excellontool_dump(): def test_excellontool_order(): settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') line = 'T8F00S00C0.12500' tool1 = ExcellonTool.from_excellon(line, settings) line = 'T8C0.12500F00S00' @@ -72,36 +75,48 @@ def test_excellontool_order(): assert_equal(tool1.feed_rate, tool2.feed_rate) assert_equal(tool1.rpm, tool2.rpm) + def test_excellontool_conversion(): - tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 25.4}) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 25.4}) tool.to_inch() assert_equal(tool.diameter, 1.) - tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 1.}) + tool = ExcellonTool.from_dict(FileSettings(units='inch'), + {'number': 8, 'diameter': 1.}) tool.to_metric() assert_equal(tool.diameter, 25.4) # Shouldn't change units if we're already using target units - tool = ExcellonTool.from_dict(FileSettings(units='inch'), {'number': 8, 'diameter': 25.4}) + tool = ExcellonTool.from_dict(FileSettings(units='inch'), + {'number': 8, 'diameter': 25.4}) tool.to_inch() assert_equal(tool.diameter, 25.4) - tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 1.}) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 1.}) tool.to_metric() assert_equal(tool.diameter, 1.) def test_excellontool_repr(): - tool = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) + tool = ExcellonTool.from_dict(FileSettings(), + {'number': 8, 'diameter': 0.125}) assert_equal(str(tool), '') - tool = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125}) + tool = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 0.125}) assert_equal(str(tool), '') + def test_excellontool_equality(): - t = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) - t1 = ExcellonTool.from_dict(FileSettings(), {'number': 8, 'diameter': 0.125}) + t = ExcellonTool.from_dict( + FileSettings(), {'number': 8, 'diameter': 0.125}) + t1 = ExcellonTool.from_dict( + FileSettings(), {'number': 8, 'diameter': 0.125}) assert_equal(t, t1) - t1 = ExcellonTool.from_dict(FileSettings(units='metric'), {'number': 8, 'diameter': 0.125}) + t1 = ExcellonTool.from_dict(FileSettings(units='metric'), + {'number': 8, 'diameter': 0.125}) assert_not_equal(t, t1) + def test_toolselection_factory(): """ Test ToolSelectionStmt factory method """ @@ -115,6 +130,7 @@ def test_toolselection_factory(): assert_equal(stmt.tool, 42) assert_equal(stmt.compensation_index, None) + def test_toolselection_dump(): """ Test ToolSelectionStmt to_excellon() """ @@ -123,6 +139,7 @@ def test_toolselection_dump(): stmt = ToolSelectionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + def test_z_axis_infeed_rate_factory(): """ Test ZAxisInfeedRateStmt factory method """ @@ -133,6 +150,7 @@ def test_z_axis_infeed_rate_factory(): stmt = ZAxisInfeedRateStmt.from_excellon('F03') assert_equal(stmt.rate, 3) + def test_z_axis_infeed_rate_dump(): """ Test ZAxisInfeedRateStmt to_excellon() """ @@ -145,11 +163,12 @@ def test_z_axis_infeed_rate_dump(): stmt = ZAxisInfeedRateStmt.from_excellon(input_rate) assert_equal(stmt.to_excellon(), expected_output) + def test_coordinatestmt_factory(): """ Test CoordinateStmt factory method """ settings = FileSettings(format=(2, 5), zero_suppression='trailing', - units='inch', notation='absolute') + units='inch', notation='absolute') line = 'X0278207Y0065293' stmt = CoordinateStmt.from_excellon(line, settings) @@ -165,7 +184,7 @@ def test_coordinatestmt_factory(): # assert_equal(stmt.y, 0.575) settings = FileSettings(format=(2, 4), zero_suppression='leading', - units='inch', notation='absolute') + units='inch', notation='absolute') line = 'X9660Y4639' stmt = CoordinateStmt.from_excellon(line, settings) @@ -173,12 +192,12 @@ def test_coordinatestmt_factory(): assert_equal(stmt.y, 0.4639) assert_equal(stmt.to_excellon(settings), "X9660Y4639") assert_equal(stmt.units, 'inch') - + settings.units = 'metric' stmt = CoordinateStmt.from_excellon(line, settings) assert_equal(stmt.units, 'metric') - - + + def test_coordinatestmt_dump(): """ Test CoordinateStmt to_excellon() """ @@ -186,102 +205,110 @@ def test_coordinatestmt_dump(): 'X251295Y81528', 'X2525Y78', 'X255Y575', 'Y52', 'X2675', 'Y575', 'X2425', 'Y52', 'X23', ] settings = FileSettings(format=(2, 4), zero_suppression='leading', - units='inch', notation='absolute') + units='inch', notation='absolute') for line in lines: stmt = CoordinateStmt.from_excellon(line, settings) assert_equal(stmt.to_excellon(settings), line) + def test_coordinatestmt_conversion(): - + settings = FileSettings() settings.units = 'metric' stmt = CoordinateStmt.from_excellon('X254Y254', settings) - - #No effect + + # No effect stmt.to_metric() assert_equal(stmt.x, 25.4) assert_equal(stmt.y, 25.4) - + stmt.to_inch() assert_equal(stmt.units, 'inch') assert_equal(stmt.x, 1.) assert_equal(stmt.y, 1.) - - #No effect + + # No effect stmt.to_inch() assert_equal(stmt.x, 1.) assert_equal(stmt.y, 1.) - + settings.units = 'inch' stmt = CoordinateStmt.from_excellon('X01Y01', settings) - - #No effect + + # No effect stmt.to_inch() assert_equal(stmt.x, 1.) assert_equal(stmt.y, 1.) - + stmt.to_metric() assert_equal(stmt.units, 'metric') assert_equal(stmt.x, 25.4) assert_equal(stmt.y, 25.4) - - #No effect + + # No effect stmt.to_metric() assert_equal(stmt.x, 25.4) assert_equal(stmt.y, 25.4) + def test_coordinatestmt_offset(): stmt = CoordinateStmt.from_excellon('X01Y01', FileSettings()) stmt.offset() assert_equal(stmt.x, 1) assert_equal(stmt.y, 1) - stmt.offset(1,0) + stmt.offset(1, 0) assert_equal(stmt.x, 2.) assert_equal(stmt.y, 1.) - stmt.offset(0,1) + stmt.offset(0, 1) assert_equal(stmt.x, 2.) assert_equal(stmt.y, 2.) def test_coordinatestmt_string(): settings = FileSettings(format=(2, 4), zero_suppression='leading', - units='inch', notation='absolute') + units='inch', notation='absolute') stmt = CoordinateStmt.from_excellon('X9660Y4639', settings) assert_equal(str(stmt), '') def test_repeathole_stmt_factory(): - stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', FileSettings(zeros='leading', units='inch')) + stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', + FileSettings(zeros='leading', + units='inch')) assert_equal(stmt.count, 4) assert_equal(stmt.xdelta, 1.5) assert_equal(stmt.ydelta, 32) assert_equal(stmt.units, 'inch') - - stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', FileSettings(zeros='leading', units='metric')) + + stmt = RepeatHoleStmt.from_excellon('R0004X015Y32', + FileSettings(zeros='leading', + units='metric')) assert_equal(stmt.units, 'metric') + def test_repeatholestmt_dump(): line = 'R4X015Y32' stmt = RepeatHoleStmt.from_excellon(line, FileSettings()) assert_equal(stmt.to_excellon(FileSettings()), line) + def test_repeatholestmt_conversion(): line = 'R4X0254Y254' settings = FileSettings() settings.units = 'metric' stmt = RepeatHoleStmt.from_excellon(line, settings) - - #No effect + + # No effect stmt.to_metric() assert_equal(stmt.xdelta, 2.54) assert_equal(stmt.ydelta, 25.4) - + stmt.to_inch() assert_equal(stmt.units, 'inch') assert_equal(stmt.xdelta, 0.1) assert_equal(stmt.ydelta, 1.) - - #no effect + + # no effect stmt.to_inch() assert_equal(stmt.xdelta, 0.1) assert_equal(stmt.ydelta, 1.) @@ -289,26 +316,28 @@ def test_repeatholestmt_conversion(): line = 'R4X01Y1' settings.units = 'inch' stmt = RepeatHoleStmt.from_excellon(line, settings) - - #no effect + + # no effect stmt.to_inch() assert_equal(stmt.xdelta, 1.) assert_equal(stmt.ydelta, 10.) - + stmt.to_metric() assert_equal(stmt.units, 'metric') assert_equal(stmt.xdelta, 25.4) assert_equal(stmt.ydelta, 254.) - - #No effect + + # No effect stmt.to_metric() assert_equal(stmt.xdelta, 25.4) assert_equal(stmt.ydelta, 254.) + def test_repeathole_str(): stmt = RepeatHoleStmt.from_excellon('R4X015Y32', FileSettings()) assert_equal(str(stmt), '') + def test_commentstmt_factory(): """ Test CommentStmt factory method """ @@ -333,42 +362,52 @@ def test_commentstmt_dump(): stmt = CommentStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + def test_header_begin_stmt(): stmt = HeaderBeginStmt() assert_equal(stmt.to_excellon(None), 'M48') + def test_header_end_stmt(): stmt = HeaderEndStmt() assert_equal(stmt.to_excellon(None), 'M95') + def test_rewindstop_stmt(): stmt = RewindStopStmt() assert_equal(stmt.to_excellon(None), '%') + def test_z_axis_rout_position_stmt(): stmt = ZAxisRoutPositionStmt() assert_equal(stmt.to_excellon(None), 'M15') + def test_retract_with_clamping_stmt(): stmt = RetractWithClampingStmt() assert_equal(stmt.to_excellon(None), 'M16') + def test_retract_without_clamping_stmt(): stmt = RetractWithoutClampingStmt() assert_equal(stmt.to_excellon(None), 'M17') + def test_cutter_compensation_off_stmt(): stmt = CutterCompensationOffStmt() assert_equal(stmt.to_excellon(None), 'G40') + def test_cutter_compensation_left_stmt(): stmt = CutterCompensationLeftStmt() assert_equal(stmt.to_excellon(None), 'G41') + def test_cutter_compensation_right_stmt(): stmt = CutterCompensationRightStmt() assert_equal(stmt.to_excellon(None), 'G42') + def test_endofprogramstmt_factory(): settings = FileSettings(units='inch') stmt = EndOfProgramStmt.from_excellon('M30X01Y02', settings) @@ -384,61 +423,65 @@ def test_endofprogramstmt_factory(): assert_equal(stmt.x, None) assert_equal(stmt.y, 2.) + def test_endofprogramStmt_dump(): - lines = ['M30X01Y02',] + lines = ['M30X01Y02', ] for line in lines: stmt = EndOfProgramStmt.from_excellon(line, FileSettings()) assert_equal(stmt.to_excellon(FileSettings()), line) + def test_endofprogramstmt_conversion(): settings = FileSettings() settings.units = 'metric' stmt = EndOfProgramStmt.from_excellon('M30X0254Y254', settings) - #No effect + # No effect stmt.to_metric() assert_equal(stmt.x, 2.54) assert_equal(stmt.y, 25.4) - + stmt.to_inch() assert_equal(stmt.units, 'inch') assert_equal(stmt.x, 0.1) assert_equal(stmt.y, 1.0) - - #No effect + + # No effect stmt.to_inch() assert_equal(stmt.x, 0.1) assert_equal(stmt.y, 1.0) settings.units = 'inch' stmt = EndOfProgramStmt.from_excellon('M30X01Y1', settings) - - #No effect + + # No effect stmt.to_inch() assert_equal(stmt.x, 1.) assert_equal(stmt.y, 10.0) - + stmt.to_metric() assert_equal(stmt.units, 'metric') assert_equal(stmt.x, 25.4) assert_equal(stmt.y, 254.) - - #No effect + + # No effect stmt.to_metric() assert_equal(stmt.x, 25.4) assert_equal(stmt.y, 254.) + def test_endofprogramstmt_offset(): stmt = EndOfProgramStmt(1, 1) stmt.offset() assert_equal(stmt.x, 1) assert_equal(stmt.y, 1) - stmt.offset(1,0) + stmt.offset(1, 0) assert_equal(stmt.x, 2.) assert_equal(stmt.y, 1.) - stmt.offset(0,1) + stmt.offset(0, 1) assert_equal(stmt.x, 2.) assert_equal(stmt.y, 2.) + def test_unitstmt_factory(): """ Test UnitStmt factory method """ @@ -471,6 +514,7 @@ def test_unitstmt_dump(): stmt = UnitStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + def test_unitstmt_conversion(): stmt = UnitStmt.from_excellon('METRIC,TZ') stmt.to_inch() @@ -480,6 +524,7 @@ def test_unitstmt_conversion(): stmt.to_metric() assert_equal(stmt.units, 'metric') + def test_incrementalmode_factory(): """ Test IncrementalModeStmt factory method """ @@ -527,6 +572,7 @@ def test_versionstmt_dump(): stmt = VersionStmt.from_excellon(line) assert_equal(stmt.to_excellon(), line) + def test_versionstmt_validation(): """ Test VersionStmt input validation """ @@ -608,6 +654,7 @@ def test_measmodestmt_validation(): assert_raises(ValueError, MeasuringModeStmt.from_excellon, 'M70') assert_raises(ValueError, MeasuringModeStmt, 'millimeters') + def test_measmodestmt_conversion(): line = 'M72' stmt = MeasuringModeStmt.from_excellon(line) @@ -621,27 +668,33 @@ def test_measmodestmt_conversion(): stmt.to_inch() assert_equal(stmt.units, 'inch') + def test_routemode_stmt(): stmt = RouteModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G00') + def test_linearmode_stmt(): stmt = LinearModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G01') + def test_drillmode_stmt(): stmt = DrillModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G05') + def test_absolutemode_stmt(): stmt = AbsoluteModeStmt() assert_equal(stmt.to_excellon(FileSettings()), 'G90') + def test_unknownstmt(): stmt = UnknownStmt('TEST') assert_equal(stmt.stmt, 'TEST') assert_equal(str(stmt), '') + def test_unknownstmt_dump(): stmt = UnknownStmt('TEST') assert_equal(stmt.to_excellon(FileSettings()), 'TEST') diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 79ce76b..c1985e6 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -7,6 +7,7 @@ from .tests import * from ..gerber_statements import * from ..cam import FileSettings + def test_Statement_smoketest(): stmt = Statement('Test') assert_equal(stmt.type, 'Test') @@ -16,7 +17,8 @@ def test_Statement_smoketest(): assert_in('units=inch', str(stmt)) stmt.to_metric() stmt.offset(1, 1) - assert_in('type=Test',str(stmt)) + assert_in('type=Test', str(stmt)) + def test_FSParamStmt_factory(): """ Test FSParamStruct factory @@ -35,6 +37,7 @@ def test_FSParamStmt_factory(): assert_equal(fs.notation, 'incremental') assert_equal(fs.format, (2, 7)) + def test_FSParamStmt(): """ Test FSParamStmt initialization """ @@ -48,6 +51,7 @@ def test_FSParamStmt(): assert_equal(stmt.notation, notation) assert_equal(stmt.format, fmt) + def test_FSParamStmt_dump(): """ Test FSParamStmt to_gerber() """ @@ -62,16 +66,20 @@ def test_FSParamStmt_dump(): settings = FileSettings(zero_suppression='leading', notation='absolute') assert_equal(fs.to_gerber(settings), '%FSLAX25Y25*%') + def test_FSParamStmt_string(): """ Test FSParamStmt.__str__() """ stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} fs = FSParamStmt.from_dict(stmt) - assert_equal(str(fs), '') + assert_equal(str(fs), + '') stmt = {'param': 'FS', 'zero': 'T', 'notation': 'I', 'x': '25'} fs = FSParamStmt.from_dict(stmt) - assert_equal(str(fs), '') + assert_equal(str(fs), + '') + def test_MOParamStmt_factory(): """ Test MOParamStruct factory @@ -94,6 +102,7 @@ def test_MOParamStmt_factory(): stmt = {'param': 'MO', 'mo': 'degrees kelvin'} assert_raises(ValueError, MOParamStmt.from_dict, stmt) + def test_MOParamStmt(): """ Test MOParamStmt initialization """ @@ -106,6 +115,7 @@ def test_MOParamStmt(): stmt = MOParamStmt(param, mode) assert_equal(stmt.mode, mode) + def test_MOParamStmt_dump(): """ Test MOParamStmt to_gerber() """ @@ -117,6 +127,7 @@ def test_MOParamStmt_dump(): mo = MOParamStmt.from_dict(stmt) assert_equal(mo.to_gerber(), '%MOMM*%') + def test_MOParamStmt_conversion(): stmt = {'param': 'MO', 'mo': 'MM'} mo = MOParamStmt.from_dict(stmt) @@ -128,6 +139,7 @@ def test_MOParamStmt_conversion(): mo.to_metric() assert_equal(mo.mode, 'metric') + def test_MOParamStmt_string(): """ Test MOParamStmt.__str__() """ @@ -139,6 +151,7 @@ def test_MOParamStmt_string(): mo = MOParamStmt.from_dict(stmt) assert_equal(str(mo), '') + def test_IPParamStmt_factory(): """ Test IPParamStruct factory """ @@ -150,6 +163,7 @@ def test_IPParamStmt_factory(): ip = IPParamStmt.from_dict(stmt) assert_equal(ip.ip, 'negative') + def test_IPParamStmt(): """ Test IPParamStmt initialization """ @@ -159,6 +173,7 @@ def test_IPParamStmt(): assert_equal(stmt.param, param) assert_equal(stmt.ip, ip) + def test_IPParamStmt_dump(): """ Test IPParamStmt to_gerber() """ @@ -170,6 +185,7 @@ def test_IPParamStmt_dump(): ip = IPParamStmt.from_dict(stmt) assert_equal(ip.to_gerber(), '%IPNEG*%') + def test_IPParamStmt_string(): stmt = {'param': 'IP', 'ip': 'POS'} ip = IPParamStmt.from_dict(stmt) @@ -179,22 +195,26 @@ def test_IPParamStmt_string(): ip = IPParamStmt.from_dict(stmt) assert_equal(str(ip), '') + def test_IRParamStmt_factory(): stmt = {'param': 'IR', 'angle': '45'} ir = IRParamStmt.from_dict(stmt) assert_equal(ir.param, 'IR') assert_equal(ir.angle, 45) + def test_IRParamStmt_dump(): stmt = {'param': 'IR', 'angle': '45'} ir = IRParamStmt.from_dict(stmt) assert_equal(ir.to_gerber(), '%IR45*%') + def test_IRParamStmt_string(): stmt = {'param': 'IR', 'angle': '45'} ir = IRParamStmt.from_dict(stmt) assert_equal(str(ir), '') + def test_OFParamStmt_factory(): """ Test OFParamStmt factory """ @@ -203,6 +223,7 @@ def test_OFParamStmt_factory(): assert_equal(of.a, 0.1234567) assert_equal(of.b, 0.1234567) + def test_OFParamStmt(): """ Test IPParamStmt initialization """ @@ -213,6 +234,7 @@ def test_OFParamStmt(): assert_equal(stmt.a, val) assert_equal(stmt.b, val) + def test_OFParamStmt_dump(): """ Test OFParamStmt to_gerber() """ @@ -220,10 +242,11 @@ def test_OFParamStmt_dump(): of = OFParamStmt.from_dict(stmt) assert_equal(of.to_gerber(), '%OFA0.12345B0.12345*%') + def test_OFParamStmt_conversion(): stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} of = OFParamStmt.from_dict(stmt) - of.units='metric' + of.units = 'metric' # No effect of.to_metric() @@ -235,7 +258,7 @@ def test_OFParamStmt_conversion(): assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) - #No effect + # No effect of.to_inch() assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) @@ -244,7 +267,7 @@ def test_OFParamStmt_conversion(): of = OFParamStmt.from_dict(stmt) of.units = 'inch' - #No effect + # No effect of.to_inch() assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) @@ -254,11 +277,12 @@ def test_OFParamStmt_conversion(): assert_equal(of.a, 2.54) assert_equal(of.b, 25.4) - #No effect + # No effect of.to_metric() assert_equal(of.a, 2.54) assert_equal(of.b, 25.4) + def test_OFParamStmt_offset(): s = OFParamStmt('OF', 0, 0) s.offset(1, 0) @@ -268,6 +292,7 @@ def test_OFParamStmt_offset(): assert_equal(s.a, 1.) assert_equal(s.b, 1.) + def test_OFParamStmt_string(): """ Test OFParamStmt __str__ """ @@ -275,6 +300,7 @@ def test_OFParamStmt_string(): of = OFParamStmt.from_dict(stmt) assert_equal(str(of), '') + def test_SFParamStmt_factory(): stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} sf = SFParamStmt.from_dict(stmt) @@ -282,18 +308,20 @@ def test_SFParamStmt_factory(): assert_equal(sf.a, 1.4) assert_equal(sf.b, 0.9) + def test_SFParamStmt_dump(): stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} sf = SFParamStmt.from_dict(stmt) assert_equal(sf.to_gerber(), '%SFA1.4B0.9*%') + def test_SFParamStmt_conversion(): stmt = {'param': 'OF', 'a': '2.54', 'b': '25.4'} of = SFParamStmt.from_dict(stmt) of.units = 'metric' of.to_metric() - #No effect + # No effect assert_equal(of.a, 2.54) assert_equal(of.b, 25.4) @@ -302,7 +330,7 @@ def test_SFParamStmt_conversion(): assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) - #No effect + # No effect of.to_inch() assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) @@ -311,7 +339,7 @@ def test_SFParamStmt_conversion(): of = SFParamStmt.from_dict(stmt) of.units = 'inch' - #No effect + # No effect of.to_inch() assert_equal(of.a, 0.1) assert_equal(of.b, 1.0) @@ -321,11 +349,12 @@ def test_SFParamStmt_conversion(): assert_equal(of.a, 2.54) assert_equal(of.b, 25.4) - #No effect + # No effect of.to_metric() assert_equal(of.a, 2.54) assert_equal(of.b, 25.4) + def test_SFParamStmt_offset(): s = SFParamStmt('OF', 0, 0) s.offset(1, 0) @@ -335,11 +364,13 @@ def test_SFParamStmt_offset(): assert_equal(s.a, 1.) assert_equal(s.b, 1.) + def test_SFParamStmt_string(): stmt = {'param': 'SF', 'a': '1.4', 'b': '0.9'} sf = SFParamStmt.from_dict(stmt) assert_equal(str(sf), '') + def test_LPParamStmt_factory(): """ Test LPParamStmt factory """ @@ -351,6 +382,7 @@ def test_LPParamStmt_factory(): lp = LPParamStmt.from_dict(stmt) assert_equal(lp.lp, 'dark') + def test_LPParamStmt_dump(): """ Test LPParamStmt to_gerber() """ @@ -362,6 +394,7 @@ def test_LPParamStmt_dump(): lp = LPParamStmt.from_dict(stmt) assert_equal(lp.to_gerber(), '%LPD*%') + def test_LPParamStmt_string(): """ Test LPParamStmt.__str__() """ @@ -373,6 +406,7 @@ def test_LPParamStmt_string(): lp = LPParamStmt.from_dict(stmt) assert_equal(str(lp), '') + def test_AMParamStmt_factory(): name = 'DONUTVAR' macro = ( @@ -387,7 +421,7 @@ def test_AMParamStmt_factory(): 7,0,0,7,6,0.2,0* 8,THIS IS AN UNSUPPORTED PRIMITIVE* ''') - s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro }) + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) s.build() assert_equal(len(s.primitives), 10) assert_true(isinstance(s.primitives[0], AMCommentPrimitive)) @@ -401,15 +435,16 @@ def test_AMParamStmt_factory(): assert_true(isinstance(s.primitives[8], AMThermalPrimitive)) assert_true(isinstance(s.primitives[9], AMUnsupportPrimitive)) + def testAMParamStmt_conversion(): name = 'POLYGON' macro = '5,1,8,25.4,25.4,25.4,0*' - s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro }) + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) s.build() s.units = 'metric' - #No effect + # No effect s.to_metric() assert_equal(s.primitives[0].position, (25.4, 25.4)) assert_equal(s.primitives[0].diameter, 25.4) @@ -419,17 +454,17 @@ def testAMParamStmt_conversion(): assert_equal(s.primitives[0].position, (1., 1.)) assert_equal(s.primitives[0].diameter, 1.) - #No effect + # No effect s.to_inch() assert_equal(s.primitives[0].position, (1., 1.)) assert_equal(s.primitives[0].diameter, 1.) macro = '5,1,8,1,1,1,0*' - s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro }) + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) s.build() s.units = 'inch' - #No effect + # No effect s.to_inch() assert_equal(s.primitives[0].position, (1., 1.)) assert_equal(s.primitives[0].diameter, 1.) @@ -439,42 +474,48 @@ def testAMParamStmt_conversion(): assert_equal(s.primitives[0].position, (25.4, 25.4)) assert_equal(s.primitives[0].diameter, 25.4) - #No effect + # No effect s.to_metric() assert_equal(s.primitives[0].position, (25.4, 25.4)) assert_equal(s.primitives[0].diameter, 25.4) + def test_AMParamStmt_dump(): name = 'POLYGON' macro = '5,1,8,25.4,25.4,25.4,0.0' - s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro }) + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) s.build() assert_equal(s.to_gerber(), '%AMPOLYGON*5,1,8,25.4,25.4,25.4,0.0*%') + def test_AMParamStmt_string(): name = 'POLYGON' macro = '5,1,8,25.4,25.4,25.4,0*' - s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro }) + s = AMParamStmt.from_dict({'param': 'AM', 'name': name, 'macro': macro}) s.build() assert_equal(str(s), '') + def test_ASParamStmt_factory(): stmt = {'param': 'AS', 'mode': 'AXBY'} s = ASParamStmt.from_dict(stmt) assert_equal(s.param, 'AS') assert_equal(s.mode, 'AXBY') + def test_ASParamStmt_dump(): stmt = {'param': 'AS', 'mode': 'AXBY'} s = ASParamStmt.from_dict(stmt) assert_equal(s.to_gerber(), '%ASAXBY*%') + def test_ASParamStmt_string(): stmt = {'param': 'AS', 'mode': 'AXBY'} s = ASParamStmt.from_dict(stmt) assert_equal(str(s), '') + def test_INParamStmt_factory(): """ Test INParamStmt factory """ @@ -482,6 +523,7 @@ def test_INParamStmt_factory(): inp = INParamStmt.from_dict(stmt) assert_equal(inp.name, 'test') + def test_INParamStmt_dump(): """ Test INParamStmt to_gerber() """ @@ -489,11 +531,13 @@ def test_INParamStmt_dump(): inp = INParamStmt.from_dict(stmt) assert_equal(inp.to_gerber(), '%INtest*%') + def test_INParamStmt_string(): stmt = {'param': 'IN', 'name': 'test'} inp = INParamStmt.from_dict(stmt) assert_equal(str(inp), '') + def test_LNParamStmt_factory(): """ Test LNParamStmt factory """ @@ -501,6 +545,7 @@ def test_LNParamStmt_factory(): lnp = LNParamStmt.from_dict(stmt) assert_equal(lnp.name, 'test') + def test_LNParamStmt_dump(): """ Test LNParamStmt to_gerber() """ @@ -508,11 +553,13 @@ def test_LNParamStmt_dump(): lnp = LNParamStmt.from_dict(stmt) assert_equal(lnp.to_gerber(), '%LNtest*%') + def test_LNParamStmt_string(): stmt = {'param': 'LN', 'name': 'test'} lnp = LNParamStmt.from_dict(stmt) assert_equal(str(lnp), '') + def test_comment_stmt(): """ Test comment statement """ @@ -520,31 +567,37 @@ def test_comment_stmt(): assert_equal(stmt.type, 'COMMENT') assert_equal(stmt.comment, 'A comment') + def test_comment_stmt_dump(): """ Test CommentStmt to_gerber() """ stmt = CommentStmt('A comment') assert_equal(stmt.to_gerber(), 'G04A comment*') + def test_comment_stmt_string(): stmt = CommentStmt('A comment') assert_equal(str(stmt), '') + def test_eofstmt(): """ Test EofStmt """ stmt = EofStmt() assert_equal(stmt.type, 'EOF') + def test_eofstmt_dump(): """ Test EofStmt to_gerber() """ stmt = EofStmt() assert_equal(stmt.to_gerber(), 'M02*') + def test_eofstmt_string(): assert_equal(str(EofStmt()), '') + def test_quadmodestmt_factory(): """ Test QuadrantModeStmt.from_gerber() """ @@ -557,6 +610,7 @@ def test_quadmodestmt_factory(): stmt = QuadrantModeStmt.from_gerber(line) assert_equal(stmt.mode, 'multi-quadrant') + def test_quadmodestmt_validation(): """ Test QuadrantModeStmt input validation """ @@ -564,6 +618,7 @@ def test_quadmodestmt_validation(): assert_raises(ValueError, QuadrantModeStmt.from_gerber, line) assert_raises(ValueError, QuadrantModeStmt, 'quadrant-ful') + def test_quadmodestmt_dump(): """ Test QuadrantModeStmt.to_gerber() """ @@ -571,6 +626,7 @@ def test_quadmodestmt_dump(): stmt = QuadrantModeStmt.from_gerber(line) assert_equal(stmt.to_gerber(), line) + def test_regionmodestmt_factory(): """ Test RegionModeStmt.from_gerber() """ @@ -583,6 +639,7 @@ def test_regionmodestmt_factory(): stmt = RegionModeStmt.from_gerber(line) assert_equal(stmt.mode, 'off') + def test_regionmodestmt_validation(): """ Test RegionModeStmt input validation """ @@ -590,6 +647,7 @@ def test_regionmodestmt_validation(): assert_raises(ValueError, RegionModeStmt.from_gerber, line) assert_raises(ValueError, RegionModeStmt, 'off-ish') + def test_regionmodestmt_dump(): """ Test RegionModeStmt.to_gerber() """ @@ -597,6 +655,7 @@ def test_regionmodestmt_dump(): stmt = RegionModeStmt.from_gerber(line) assert_equal(stmt.to_gerber(), line) + def test_unknownstmt(): """ Test UnknownStmt """ @@ -605,6 +664,7 @@ def test_unknownstmt(): assert_equal(stmt.type, 'UNKNOWN') assert_equal(stmt.line, line) + def test_unknownstmt_dump(): """ Test UnknownStmt.to_gerber() """ @@ -613,15 +673,17 @@ def test_unknownstmt_dump(): stmt = UnknownStmt(line) assert_equal(stmt.to_gerber(), line) + def test_statement_string(): """ Test Statement.__str__() """ stmt = Statement('PARAM') assert_in('type=PARAM', str(stmt)) - stmt.test='PASS' + stmt.test = 'PASS' assert_in('test=PASS', str(stmt)) assert_in('type=PARAM', str(stmt)) + def test_ADParamStmt_factory(): """ Test ADParamStmt factory """ @@ -653,12 +715,14 @@ def test_ADParamStmt_factory(): assert_equal(ad.shape, 'R') assert_equal(ad.modifiers, [(1.42, 1.24)]) + def test_ADParamStmt_conversion(): - stmt = {'param': 'AD', 'd': 0, 'shape': 'C', 'modifiers': '25.4X25.4,25.4X25.4'} + stmt = {'param': 'AD', 'd': 0, 'shape': 'C', + 'modifiers': '25.4X25.4,25.4X25.4'} ad = ADParamStmt.from_dict(stmt) ad.units = 'metric' - #No effect + # No effect ad.to_metric() assert_equal(ad.modifiers[0], (25.4, 25.4)) assert_equal(ad.modifiers[1], (25.4, 25.4)) @@ -668,7 +732,7 @@ def test_ADParamStmt_conversion(): assert_equal(ad.modifiers[0], (1., 1.)) assert_equal(ad.modifiers[1], (1., 1.)) - #No effect + # No effect ad.to_inch() assert_equal(ad.modifiers[0], (1., 1.)) assert_equal(ad.modifiers[1], (1., 1.)) @@ -677,7 +741,7 @@ def test_ADParamStmt_conversion(): ad = ADParamStmt.from_dict(stmt) ad.units = 'inch' - #No effect + # No effect ad.to_inch() assert_equal(ad.modifiers[0], (1., 1.)) assert_equal(ad.modifiers[1], (1., 1.)) @@ -686,11 +750,12 @@ def test_ADParamStmt_conversion(): assert_equal(ad.modifiers[0], (25.4, 25.4)) assert_equal(ad.modifiers[1], (25.4, 25.4)) - #No effect + # No effect ad.to_metric() assert_equal(ad.modifiers[0], (25.4, 25.4)) assert_equal(ad.modifiers[1], (25.4, 25.4)) + def test_ADParamStmt_dump(): stmt = {'param': 'AD', 'd': 0, 'shape': 'C'} ad = ADParamStmt.from_dict(stmt) @@ -699,6 +764,7 @@ def test_ADParamStmt_dump(): ad = ADParamStmt.from_dict(stmt) assert_equal(ad.to_gerber(), '%ADD0C,1X1,1X1*%') + def test_ADPamramStmt_string(): stmt = {'param': 'AD', 'd': 0, 'shape': 'C'} ad = ADParamStmt.from_dict(stmt) @@ -716,12 +782,14 @@ def test_ADPamramStmt_string(): ad = ADParamStmt.from_dict(stmt) assert_equal(str(ad), '') + def test_MIParamStmt_factory(): stmt = {'param': 'MI', 'a': 1, 'b': 1} mi = MIParamStmt.from_dict(stmt) assert_equal(mi.a, 1) assert_equal(mi.b, 1) + def test_MIParamStmt_dump(): stmt = {'param': 'MI', 'a': 1, 'b': 1} mi = MIParamStmt.from_dict(stmt) @@ -733,6 +801,7 @@ def test_MIParamStmt_dump(): mi = MIParamStmt.from_dict(stmt) assert_equal(mi.to_gerber(), '%MIA0B1*%') + def test_MIParamStmt_string(): stmt = {'param': 'MI', 'a': 1, 'b': 1} mi = MIParamStmt.from_dict(stmt) @@ -746,6 +815,7 @@ def test_MIParamStmt_string(): mi = MIParamStmt.from_dict(stmt) assert_equal(str(mi), '') + def test_coordstmt_ctor(): cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings()) assert_equal(cs.function, 'G04') @@ -755,8 +825,10 @@ def test_coordstmt_ctor(): assert_equal(cs.j, 0.3) assert_equal(cs.op, 'D01') + def test_coordstmt_factory(): - stmt = {'function': 'G04', 'x': '0', 'y': '001', 'i': '002', 'j': '003', 'op': 'D01'} + stmt = {'function': 'G04', 'x': '0', 'y': '001', + 'i': '002', 'j': '003', 'op': 'D01'} cs = CoordStmt.from_dict(stmt, FileSettings()) assert_equal(cs.function, 'G04') assert_equal(cs.x, 0.0) @@ -765,15 +837,17 @@ def test_coordstmt_factory(): assert_equal(cs.j, 0.3) assert_equal(cs.op, 'D01') + def test_coordstmt_dump(): cs = CoordStmt('G04', 0.0, 0.1, 0.2, 0.3, 'D01', FileSettings()) assert_equal(cs.to_gerber(FileSettings()), 'G04X0Y001I002J003D01*') + def test_coordstmt_conversion(): cs = CoordStmt('G71', 25.4, 25.4, 25.4, 25.4, 'D01', FileSettings()) cs.units = 'metric' - #No effect + # No effect cs.to_metric() assert_equal(cs.x, 25.4) assert_equal(cs.y, 25.4) @@ -789,7 +863,7 @@ def test_coordstmt_conversion(): assert_equal(cs.j, 1.) assert_equal(cs.function, 'G70') - #No effect + # No effect cs.to_inch() assert_equal(cs.x, 1.) assert_equal(cs.y, 1.) @@ -800,7 +874,7 @@ def test_coordstmt_conversion(): cs = CoordStmt('G70', 1., 1., 1., 1., 'D01', FileSettings()) cs.units = 'inch' - #No effect + # No effect cs.to_inch() assert_equal(cs.x, 1.) assert_equal(cs.y, 1.) @@ -815,7 +889,7 @@ def test_coordstmt_conversion(): assert_equal(cs.j, 25.4) assert_equal(cs.function, 'G71') - #No effect + # No effect cs.to_metric() assert_equal(cs.x, 25.4) assert_equal(cs.y, 25.4) @@ -823,6 +897,7 @@ def test_coordstmt_conversion(): assert_equal(cs.j, 25.4) assert_equal(cs.function, 'G71') + def test_coordstmt_offset(): c = CoordStmt('G71', 0, 0, 0, 0, 'D01', FileSettings()) c.offset(1, 0) @@ -836,9 +911,11 @@ def test_coordstmt_offset(): assert_equal(c.i, 1.) assert_equal(c.j, 1.) + def test_coordstmt_string(): cs = CoordStmt('G04', 0, 1, 2, 3, 'D01', FileSettings()) - assert_equal(str(cs), '') + assert_equal(str(cs), + '') cs = CoordStmt('G04', None, None, None, None, 'D02', FileSettings()) assert_equal(str(cs), '') cs = CoordStmt('G04', None, None, None, None, 'D03', FileSettings()) @@ -846,6 +923,7 @@ def test_coordstmt_string(): cs = CoordStmt('G04', None, None, None, None, 'TEST', FileSettings()) assert_equal(str(cs), '') + def test_aperturestmt_ctor(): ast = ApertureStmt(3, False) assert_equal(ast.d, 3) @@ -860,11 +938,10 @@ def test_aperturestmt_ctor(): assert_equal(ast.d, 3) assert_equal(ast.deprecated, False) + def test_aperturestmt_dump(): ast = ApertureStmt(3, False) assert_equal(ast.to_gerber(), 'D3*') ast = ApertureStmt(3, True) assert_equal(ast.to_gerber(), 'G54D3*') assert_equal(str(ast), '') - - diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py index f123a38..45bb01b 100644 --- a/gerber/tests/test_ipc356.py +++ b/gerber/tests/test_ipc356.py @@ -2,18 +2,21 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe -from ..ipc356 import * +from ..ipc356 import * from ..cam import FileSettings from .tests import * import os IPC_D_356_FILE = os.path.join(os.path.dirname(__file__), - 'resources/ipc-d-356.ipc') + 'resources/ipc-d-356.ipc') + + def test_read(): ipcfile = read(IPC_D_356_FILE) assert(isinstance(ipcfile, IPC_D_356)) + def test_parser(): ipcfile = read(IPC_D_356_FILE) assert_equal(ipcfile.settings.units, 'inch') @@ -28,6 +31,7 @@ def test_parser(): assert_equal(set(ipcfile.outlines[0].points), {(0., 0.), (2.25, 0.), (2.25, 1.5), (0., 1.5), (0.13, 0.024)}) + def test_comment(): c = IPC356_Comment('Layer Stackup:') assert_equal(c.comment, 'Layer Stackup:') @@ -36,6 +40,7 @@ def test_comment(): assert_raises(ValueError, IPC356_Comment.from_line, 'P JOB') assert_equal(str(c), '') + def test_parameter(): p = IPC356_Parameter('VER', 'IPC-D-356A') assert_equal(p.parameter, 'VER') @@ -43,27 +48,32 @@ def test_parameter(): p = IPC356_Parameter.from_line('P VER IPC-D-356A ') assert_equal(p.parameter, 'VER') assert_equal(p.value, 'IPC-D-356A') - assert_raises(ValueError, IPC356_Parameter.from_line, 'C Layer Stackup: ') + assert_raises(ValueError, IPC356_Parameter.from_line, + 'C Layer Stackup: ') assert_equal(str(p), '') + def test_eof(): e = IPC356_EndOfFile() assert_equal(e.to_netlist(), '999') assert_equal(str(e), '') + def test_outline(): type = 'BOARD_EDGE' points = [(0.01, 0.01), (2., 2.), (4., 2.), (4., 6.)] b = IPC356_Outline(type, points) assert_equal(b.type, type) assert_equal(b.points, points) - b = IPC356_Outline.from_line('389BOARD_EDGE X100Y100 X20000Y20000' - ' X40000 Y60000', FileSettings(units='inch')) + b = IPC356_Outline.from_line('389BOARD_EDGE X100Y100 X20000Y20000 X40000 Y60000', + FileSettings(units='inch')) assert_equal(b.type, 'BOARD_EDGE') assert_equal(b.points, points) + def test_test_record(): - assert_raises(ValueError, IPC356_TestRecord.from_line, 'P JOB', FileSettings()) + assert_raises(ValueError, IPC356_TestRecord.from_line, + 'P JOB', FileSettings()) record_string = '317+5VDC VIA - D0150PA00X 006647Y 012900X0000 S3' r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) assert_equal(r.feature_type, 'through-hole') @@ -81,8 +91,7 @@ def test_test_record(): assert_almost_equal(r.x_coord, 6.647) assert_almost_equal(r.y_coord, 12.9) assert_equal(r.rect_x, 0.) - assert_equal(str(r), - '') + assert_equal(str(r), '') record_string = '327+3.3VDC R40 -1 PA01X 032100Y 007124X0236Y0315R180 S0' r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) @@ -98,13 +107,13 @@ def test_test_record(): assert_almost_equal(r.rect_y, 0.0315) assert_equal(r.rect_rotation, 180) assert_equal(r.soldermask_info, 'none') - r = IPC356_TestRecord.from_line(record_string, FileSettings(units='metric')) + r = IPC356_TestRecord.from_line( + record_string, FileSettings(units='metric')) assert_almost_equal(r.x_coord, 32.1) assert_almost_equal(r.y_coord, 7.124) assert_almost_equal(r.rect_x, 0.236) assert_almost_equal(r.rect_y, 0.315) - record_string = '317 J4 -M2 D0330PA00X 012447Y 008030X0000 S1' r = IPC356_TestRecord.from_line(record_string, FileSettings(units='inch')) assert_equal(r.feature_type, 'through-hole') diff --git a/gerber/tests/test_layers.py b/gerber/tests/test_layers.py index c77084d..3f2bcfc 100644 --- a/gerber/tests/test_layers.py +++ b/gerber/tests/test_layers.py @@ -15,7 +15,7 @@ def test_guess_layer_class(): test_vectors = [(None, 'unknown'), ('NCDRILL.TXT', 'unknown'), ('example_board.gtl', 'top'), ('exampmle_board.sst', 'topsilk'), - ('ipc-d-356.ipc', 'ipc_netlist'),] + ('ipc-d-356.ipc', 'ipc_netlist'), ] for hint in hints: for ext in hint.ext: diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index f8a32da..e6ed1cd 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -9,11 +9,12 @@ from operator import add def test_primitive_smoketest(): p = Primitive() - assert_raises(NotImplementedError, p.bounding_box) + #assert_raises(NotImplementedError, p.bounding_box) p.to_metric() p.to_inch() p.offset(1, 1) + def test_line_angle(): """ Test Line primitive angle calculation """ @@ -24,19 +25,20 @@ def test_line_angle(): ((0, 0), (-1, 0), math.radians(180)), ((0, 0), (-1, -1), math.radians(225)), ((0, 0), (0, -1), math.radians(270)), - ((0, 0), (1, -1), math.radians(315)),] + ((0, 0), (1, -1), math.radians(315)), ] for start, end, expected in cases: l = Line(start, end, 0) line_angle = (l.angle + 2 * math.pi) % (2 * math.pi) assert_almost_equal(line_angle, expected) + def test_line_bounds(): """ Test Line primitive bounding box calculation """ cases = [((0, 0), (1, 1), ((-1, 2), (-1, 2))), ((-1, -1), (1, 1), ((-2, 2), (-2, 2))), ((1, 1), (-1, -1), ((-2, 2), (-2, 2))), - ((-1, 1), (1, -1), ((-2, 2), (-2, 2))),] + ((-1, 1), (1, -1), ((-2, 2), (-2, 2))), ] c = Circle((0, 0), 2) r = Rectangle((0, 0), 2, 2) @@ -49,11 +51,12 @@ def test_line_bounds(): cases = [((0, 0), (1, 1), ((-1.5, 2.5), (-1, 2))), ((-1, -1), (1, 1), ((-2.5, 2.5), (-2, 2))), ((1, 1), (-1, -1), ((-2.5, 2.5), (-2, 2))), - ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))),] + ((-1, 1), (1, -1), ((-2.5, 2.5), (-2, 2))), ] for start, end, expected in cases: l = Line(start, end, r) assert_equal(l.bounding_box, expected) + def test_line_vertices(): c = Circle((0, 0), 2) l = Line((0, 0), (1, 1), c) @@ -61,20 +64,25 @@ def test_line_vertices(): # All 4 compass points, all 4 quadrants and the case where start == end test_cases = [((0, 0), (1, 0), ((-1, -1), (-1, 1), (2, 1), (2, -1))), - ((0, 0), (1, 1), ((-1, -1), (-1, 1), (0, 2), (2, 2), (2, 0), (1,-1))), - ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))), - ((0, 0), (-1, 1), ((-1, -1), (-2, 0), (-2, 2), (0, 2), (1, 1), (1, -1))), - ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))), - ((0, 0), (-1, -1), ((-2, -2), (1, -1), (1, 1), (-1, 1), (-2, 0), (0,-2))), - ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))), - ((0, 0), (1, -1), ((-1, -1), (0, -2), (2, -2), (2, 0), (1, 1), (-1, 1))), - ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))),] + ((0, 0), (1, 1), ((-1, -1), (-1, 1), + (0, 2), (2, 2), (2, 0), (1, -1))), + ((0, 0), (0, 1), ((-1, -1), (-1, 2), (1, 2), (1, -1))), + ((0, 0), (-1, 1), ((-1, -1), (-2, 0), + (-2, 2), (0, 2), (1, 1), (1, -1))), + ((0, 0), (-1, 0), ((-2, -1), (-2, 1), (1, 1), (1, -1))), + ((0, 0), (-1, -1), ((-2, -2), (1, -1), + (1, 1), (-1, 1), (-2, 0), (0, -2))), + ((0, 0), (0, -1), ((-1, -2), (-1, 1), (1, 1), (1, -2))), + ((0, 0), (1, -1), ((-1, -1), (0, -2), + (2, -2), (2, 0), (1, 1), (-1, 1))), + ((0, 0), (0, 0), ((-1, -1), (-1, 1), (1, 1), (1, -1))), ] r = Rectangle((0, 0), 2, 2) for start, end, vertices in test_cases: l = Line(start, end, r) assert_equal(set(vertices), set(l.vertices)) + def test_line_conversion(): c = Circle((0, 0), 25.4, units='metric') l = Line((2.54, 25.4), (254.0, 2540.0), c, units='metric') @@ -105,13 +113,12 @@ def test_line_conversion(): assert_equal(l.end, (10.0, 100.0)) assert_equal(l.aperture.diameter, 1.0) - l.to_metric() assert_equal(l.start, (2.54, 25.4)) assert_equal(l.end, (254.0, 2540.0)) assert_equal(l.aperture.diameter, 25.4) - #No effect + # No effect l.to_metric() assert_equal(l.start, (2.54, 25.4)) assert_equal(l.end, (254.0, 2540.0)) @@ -133,56 +140,63 @@ def test_line_conversion(): assert_equal(l.aperture.width, 25.4) assert_equal(l.aperture.height, 254.0) + def test_line_offset(): c = Circle((0, 0), 1) l = Line((0, 0), (1, 1), c) l.offset(1, 0) - assert_equal(l.start,(1., 0.)) + assert_equal(l.start, (1., 0.)) assert_equal(l.end, (2., 1.)) l.offset(0, 1) - assert_equal(l.start,(1., 1.)) + assert_equal(l.start, (1., 1.)) assert_equal(l.end, (2., 2.)) + def test_arc_radius(): """ Test Arc primitive radius calculation """ cases = [((-3, 4), (5, 0), (0, 0), 5), - ((0, 1), (1, 0), (0, 0), 1),] + ((0, 1), (1, 0), (0, 0), 1), ] for start, end, center, radius in cases: a = Arc(start, end, center, 'clockwise', 0) assert_equal(a.radius, radius) + def test_arc_sweep_angle(): """ Test Arc primitive sweep angle calculation """ cases = [((1, 0), (0, 1), (0, 0), 'counterclockwise', math.radians(90)), ((1, 0), (0, 1), (0, 0), 'clockwise', math.radians(270)), ((1, 0), (-1, 0), (0, 0), 'clockwise', math.radians(180)), - ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)),] + ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)), ] for start, end, center, direction, sweep in cases: - c = Circle((0,0), 1) + c = Circle((0, 0), 1) a = Arc(start, end, center, direction, c) assert_equal(a.sweep_angle, sweep) + def test_arc_bounds(): """ Test Arc primitive bounding box calculation """ - cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), - ((1, 0), (0, 1), (0, 0), 'counterclockwise', ((-0.5, 1.5), (-0.5, 1.5))), - #TODO: ADD MORE TEST CASES HERE + cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (0, 1), (0, 0), 'counterclockwise', + ((-0.5, 1.5), (-0.5, 1.5))), + # TODO: ADD MORE TEST CASES HERE ] for start, end, center, direction, bounds in cases: - c = Circle((0,0), 1) + c = Circle((0, 0), 1) a = Arc(start, end, center, direction, c) assert_equal(a.bounding_box, bounds) + def test_arc_conversion(): c = Circle((0, 0), 25.4, units='metric') - a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0),'clockwise', c, units='metric') + a = Arc((2.54, 25.4), (254.0, 2540.0), (25400.0, 254000.0), + 'clockwise', c, units='metric') - #No effect + # No effect a.to_metric() assert_equal(a.start, (2.54, 25.4)) assert_equal(a.end, (254.0, 2540.0)) @@ -195,7 +209,7 @@ def test_arc_conversion(): assert_equal(a.center, (1000.0, 10000.0)) assert_equal(a.aperture.diameter, 1.0) - #no effect + # no effect a.to_inch() assert_equal(a.start, (0.1, 1.0)) assert_equal(a.end, (10.0, 100.0)) @@ -203,41 +217,46 @@ def test_arc_conversion(): assert_equal(a.aperture.diameter, 1.0) c = Circle((0, 0), 1.0, units='inch') - a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0),'clockwise', c, units='inch') + a = Arc((0.1, 1.0), (10.0, 100.0), (1000.0, 10000.0), + 'clockwise', c, units='inch') a.to_metric() assert_equal(a.start, (2.54, 25.4)) assert_equal(a.end, (254.0, 2540.0)) assert_equal(a.center, (25400.0, 254000.0)) assert_equal(a.aperture.diameter, 25.4) + def test_arc_offset(): c = Circle((0, 0), 1) a = Arc((0, 0), (1, 1), (2, 2), 'clockwise', c) a.offset(1, 0) - assert_equal(a.start,(1., 0.)) + assert_equal(a.start, (1., 0.)) assert_equal(a.end, (2., 1.)) assert_equal(a.center, (3., 2.)) a.offset(0, 1) - assert_equal(a.start,(1., 1.)) + assert_equal(a.start, (1., 1.)) assert_equal(a.end, (2., 2.)) assert_equal(a.center, (3., 3.)) + def test_circle_radius(): """ Test Circle primitive radius calculation """ c = Circle((1, 1), 2) assert_equal(c.radius, 1) + def test_circle_bounds(): """ Test Circle bounding box calculation """ c = Circle((1, 1), 2) assert_equal(c.bounding_box, ((0, 2), (0, 2))) + def test_circle_conversion(): c = Circle((2.54, 25.4), 254.0, units='metric') - c.to_metric() #shouldn't do antyhing + c.to_metric() # shouldn't do antyhing assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) @@ -245,13 +264,13 @@ def test_circle_conversion(): assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) - #no effect + # no effect c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) c = Circle((0.1, 1.0), 10.0, units='inch') - #No effect + # No effect c.to_inch() assert_equal(c.position, (0.1, 1.)) assert_equal(c.diameter, 10.) @@ -260,17 +279,19 @@ def test_circle_conversion(): assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) - #no effect + # no effect c.to_metric() assert_equal(c.position, (2.54, 25.4)) assert_equal(c.diameter, 254.) + def test_circle_offset(): c = Circle((0, 0), 1) c.offset(1, 0) - assert_equal(c.position,(1., 0.)) + assert_equal(c.position, (1., 0.)) c.offset(0, 1) - assert_equal(c.position,(1., 1.)) + assert_equal(c.position, (1., 1.)) + def test_ellipse_ctor(): """ Test ellipse creation @@ -280,6 +301,7 @@ def test_ellipse_ctor(): assert_equal(e.width, 3) assert_equal(e.height, 2) + def test_ellipse_bounds(): """ Test ellipse bounding box calculation """ @@ -292,10 +314,11 @@ def test_ellipse_bounds(): e = Ellipse((2, 2), 4, 2, rotation=270) assert_equal(e.bounding_box, ((1, 3), (0, 4))) + def test_ellipse_conversion(): e = Ellipse((2.54, 25.4), 254.0, 2540., units='metric') - #No effect + # No effect e.to_metric() assert_equal(e.position, (2.54, 25.4)) assert_equal(e.width, 254.) @@ -306,7 +329,7 @@ def test_ellipse_conversion(): assert_equal(e.width, 10.) assert_equal(e.height, 100.) - #No effect + # No effect e.to_inch() assert_equal(e.position, (0.1, 1.)) assert_equal(e.width, 10.) @@ -314,7 +337,7 @@ def test_ellipse_conversion(): e = Ellipse((0.1, 1.), 10.0, 100., units='inch') - #no effect + # no effect e.to_inch() assert_equal(e.position, (0.1, 1.)) assert_equal(e.width, 10.) @@ -331,40 +354,44 @@ def test_ellipse_conversion(): assert_equal(e.width, 254.) assert_equal(e.height, 2540.) + def test_ellipse_offset(): e = Ellipse((0, 0), 1, 2) e.offset(1, 0) - assert_equal(e.position,(1., 0.)) + assert_equal(e.position, (1., 0.)) e.offset(0, 1) - assert_equal(e.position,(1., 1.)) + assert_equal(e.position, (1., 1.)) + def test_rectangle_ctor(): """ Test rectangle creation """ - test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2)) + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) for pos, width, height in test_cases: r = Rectangle(pos, width, height) assert_equal(r.position, pos) assert_equal(r.width, width) assert_equal(r.height, height) + def test_rectangle_bounds(): """ Test rectangle bounding box calculation """ - r = Rectangle((0,0), 2, 2) + r = Rectangle((0, 0), 2, 2) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - r = Rectangle((0,0), 2, 2, rotation=45) + r = Rectangle((0, 0), 2, 2, rotation=45) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + def test_rectangle_conversion(): r = Rectangle((2.54, 25.4), 254.0, 2540.0, units='metric') r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) @@ -385,44 +412,48 @@ def test_rectangle_conversion(): assert_equal(r.height, 100.0) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) + def test_rectangle_offset(): r = Rectangle((0, 0), 1, 2) r.offset(1, 0) - assert_equal(r.position,(1., 0.)) + assert_equal(r.position, (1., 0.)) r.offset(0, 1) - assert_equal(r.position,(1., 1.)) + assert_equal(r.position, (1., 1.)) + def test_diamond_ctor(): """ Test diamond creation """ - test_cases = (((0,0), 1, 1), ((0, 0), 1, 2), ((1,1), 1, 2)) + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), ((1, 1), 1, 2)) for pos, width, height in test_cases: d = Diamond(pos, width, height) assert_equal(d.position, pos) assert_equal(d.width, width) assert_equal(d.height, height) + def test_diamond_bounds(): """ Test diamond bounding box calculation """ - d = Diamond((0,0), 2, 2) + d = Diamond((0, 0), 2, 2) xbounds, ybounds = d.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - d = Diamond((0,0), math.sqrt(2), math.sqrt(2), rotation=45) + d = Diamond((0, 0), math.sqrt(2), math.sqrt(2), rotation=45) xbounds, ybounds = d.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) + def test_diamond_conversion(): d = Diamond((2.54, 25.4), 254.0, 2540.0, units='metric') @@ -458,19 +489,21 @@ def test_diamond_conversion(): assert_equal(d.width, 254.0) assert_equal(d.height, 2540.0) + def test_diamond_offset(): d = Diamond((0, 0), 1, 2) d.offset(1, 0) - assert_equal(d.position,(1., 0.)) + assert_equal(d.position, (1., 0.)) d.offset(0, 1) - assert_equal(d.position,(1., 1.)) + assert_equal(d.position, (1., 1.)) + def test_chamfer_rectangle_ctor(): """ Test chamfer rectangle creation """ - test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)), + test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)), ((0, 0), 1, 2, 0.3, (True, True, True, True)), - ((1,1), 1, 2, 0.4, (False, False, False, False))) + ((1, 1), 1, 2, 0.4, (False, False, False, False))) for pos, width, height, chamfer, corners in test_cases: r = ChamferRectangle(pos, width, height, chamfer, corners) assert_equal(r.position, pos) @@ -479,23 +512,27 @@ def test_chamfer_rectangle_ctor(): assert_equal(r.chamfer, chamfer) assert_array_almost_equal(r.corners, corners) + def test_chamfer_rectangle_bounds(): """ Test chamfer rectangle bounding box calculation """ - r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False)) + r = ChamferRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - r = ChamferRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45) + r = ChamferRectangle( + (0, 0), 2, 2, 0.2, (True, True, False, False), rotation=45) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + def test_chamfer_rectangle_conversion(): - r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units='metric') + r = ChamferRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, + (True, True, False, False), units='metric') r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.chamfer, 0.254) @@ -512,7 +549,8 @@ def test_chamfer_rectangle_conversion(): assert_equal(r.height, 100.0) assert_equal(r.chamfer, 0.01) - r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units='inch') + r = ChamferRectangle((0.1, 1.0), 10.0, 100.0, 0.01, + (True, True, False, False), units='inch') r.to_inch() assert_equal(r.position, (0.1, 1.0)) assert_equal(r.width, 10.0) @@ -520,30 +558,32 @@ def test_chamfer_rectangle_conversion(): assert_equal(r.chamfer, 0.01) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.chamfer, 0.254) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.chamfer, 0.254) + def test_chamfer_rectangle_offset(): r = ChamferRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) r.offset(1, 0) - assert_equal(r.position,(1., 0.)) + assert_equal(r.position, (1., 0.)) r.offset(0, 1) - assert_equal(r.position,(1., 1.)) + assert_equal(r.position, (1., 1.)) + def test_round_rectangle_ctor(): """ Test round rectangle creation """ - test_cases = (((0,0), 1, 1, 0.2, (True, True, False, False)), + test_cases = (((0, 0), 1, 1, 0.2, (True, True, False, False)), ((0, 0), 1, 2, 0.3, (True, True, True, True)), - ((1,1), 1, 2, 0.4, (False, False, False, False))) + ((1, 1), 1, 2, 0.4, (False, False, False, False))) for pos, width, height, radius, corners in test_cases: r = RoundRectangle(pos, width, height, radius, corners) assert_equal(r.position, pos) @@ -552,23 +592,27 @@ def test_round_rectangle_ctor(): assert_equal(r.radius, radius) assert_array_almost_equal(r.corners, corners) + def test_round_rectangle_bounds(): """ Test round rectangle bounding box calculation """ - r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False)) + r = RoundRectangle((0, 0), 2, 2, 0.2, (True, True, False, False)) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - r = RoundRectangle((0,0), 2, 2, 0.2, (True, True, False, False), rotation=45) + r = RoundRectangle((0, 0), 2, 2, 0.2, + (True, True, False, False), rotation=45) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + def test_round_rectangle_conversion(): - r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, (True, True, False, False), units='metric') + r = RoundRectangle((2.54, 25.4), 254.0, 2540.0, 0.254, + (True, True, False, False), units='metric') r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.radius, 0.254) @@ -585,7 +629,8 @@ def test_round_rectangle_conversion(): assert_equal(r.height, 100.0) assert_equal(r.radius, 0.01) - r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01, (True, True, False, False), units='inch') + r = RoundRectangle((0.1, 1.0), 10.0, 100.0, 0.01, + (True, True, False, False), units='inch') r.to_inch() assert_equal(r.position, (0.1, 1.0)) @@ -594,70 +639,76 @@ def test_round_rectangle_conversion(): assert_equal(r.radius, 0.01) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.radius, 0.254) r.to_metric() - assert_equal(r.position, (2.54,25.4)) + assert_equal(r.position, (2.54, 25.4)) assert_equal(r.width, 254.0) assert_equal(r.height, 2540.0) assert_equal(r.radius, 0.254) + def test_round_rectangle_offset(): r = RoundRectangle((0, 0), 1, 2, 0.01, (True, True, False, False)) r.offset(1, 0) - assert_equal(r.position,(1., 0.)) + assert_equal(r.position, (1., 0.)) r.offset(0, 1) - assert_equal(r.position,(1., 1.)) + assert_equal(r.position, (1., 1.)) + def test_obround_ctor(): """ Test obround creation """ - test_cases = (((0,0), 1, 1), + test_cases = (((0, 0), 1, 1), ((0, 0), 1, 2), - ((1,1), 1, 2)) + ((1, 1), 1, 2)) for pos, width, height in test_cases: o = Obround(pos, width, height) assert_equal(o.position, pos) assert_equal(o.width, width) assert_equal(o.height, height) + def test_obround_bounds(): """ Test obround bounding box calculation """ - o = Obround((2,2),2,4) + o = Obround((2, 2), 2, 4) xbounds, ybounds = o.bounding_box assert_array_almost_equal(xbounds, (1, 3)) assert_array_almost_equal(ybounds, (0, 4)) - o = Obround((2,2),4,2) + o = Obround((2, 2), 4, 2) xbounds, ybounds = o.bounding_box assert_array_almost_equal(xbounds, (0, 4)) assert_array_almost_equal(ybounds, (1, 3)) + def test_obround_orientation(): o = Obround((0, 0), 2, 1) assert_equal(o.orientation, 'horizontal') o = Obround((0, 0), 1, 2) assert_equal(o.orientation, 'vertical') + def test_obround_subshapes(): - o = Obround((0,0), 1, 4) + o = Obround((0, 0), 1, 4) ss = o.subshapes assert_array_almost_equal(ss['rectangle'].position, (0, 0)) assert_array_almost_equal(ss['circle1'].position, (0, 1.5)) assert_array_almost_equal(ss['circle2'].position, (0, -1.5)) - o = Obround((0,0), 4, 1) + o = Obround((0, 0), 4, 1) ss = o.subshapes assert_array_almost_equal(ss['rectangle'].position, (0, 0)) assert_array_almost_equal(ss['circle1'].position, (1.5, 0)) assert_array_almost_equal(ss['circle2'].position, (-1.5, 0)) + def test_obround_conversion(): - o = Obround((2.54,25.4), 254.0, 2540.0, units='metric') + o = Obround((2.54, 25.4), 254.0, 2540.0, units='metric') - #No effect + # No effect o.to_metric() assert_equal(o.position, (2.54, 25.4)) assert_equal(o.width, 254.0) @@ -668,15 +719,15 @@ def test_obround_conversion(): assert_equal(o.width, 10.0) assert_equal(o.height, 100.0) - #No effect + # No effect o.to_inch() assert_equal(o.position, (0.1, 1.0)) assert_equal(o.width, 10.0) assert_equal(o.height, 100.0) - o= Obround((0.1, 1.0), 10.0, 100.0, units='inch') + o = Obround((0.1, 1.0), 10.0, 100.0, units='inch') - #No effect + # No effect o.to_inch() assert_equal(o.position, (0.1, 1.0)) assert_equal(o.width, 10.0) @@ -687,98 +738,107 @@ def test_obround_conversion(): assert_equal(o.width, 254.0) assert_equal(o.height, 2540.0) - #No effect + # No effect o.to_metric() assert_equal(o.position, (2.54, 25.4)) assert_equal(o.width, 254.0) assert_equal(o.height, 2540.0) + def test_obround_offset(): o = Obround((0, 0), 1, 2) o.offset(1, 0) - assert_equal(o.position,(1., 0.)) + assert_equal(o.position, (1., 0.)) o.offset(0, 1) - assert_equal(o.position,(1., 1.)) + assert_equal(o.position, (1., 1.)) + def test_polygon_ctor(): """ Test polygon creation """ - test_cases = (((0,0), 3, 5), + test_cases = (((0, 0), 3, 5), ((0, 0), 5, 6), - ((1,1), 7, 7)) + ((1, 1), 7, 7)) for pos, sides, radius in test_cases: p = Polygon(pos, sides, radius) assert_equal(p.position, pos) assert_equal(p.sides, sides) assert_equal(p.radius, radius) + def test_polygon_bounds(): """ Test polygon bounding box calculation """ - p = Polygon((2,2), 3, 2) + p = Polygon((2, 2), 3, 2) xbounds, ybounds = p.bounding_box assert_array_almost_equal(xbounds, (0, 4)) assert_array_almost_equal(ybounds, (0, 4)) - p = Polygon((2,2),3, 4) + p = Polygon((2, 2), 3, 4) xbounds, ybounds = p.bounding_box assert_array_almost_equal(xbounds, (-2, 6)) assert_array_almost_equal(ybounds, (-2, 6)) + def test_polygon_conversion(): p = Polygon((2.54, 25.4), 3, 254.0, units='metric') - - #No effect + + # No effect p.to_metric() assert_equal(p.position, (2.54, 25.4)) assert_equal(p.radius, 254.0) - + p.to_inch() assert_equal(p.position, (0.1, 1.0)) assert_equal(p.radius, 10.0) - - #No effect + + # No effect p.to_inch() assert_equal(p.position, (0.1, 1.0)) assert_equal(p.radius, 10.0) p = Polygon((0.1, 1.0), 3, 10.0, units='inch') - - #No effect + + # No effect p.to_inch() assert_equal(p.position, (0.1, 1.0)) assert_equal(p.radius, 10.0) - + p.to_metric() assert_equal(p.position, (2.54, 25.4)) assert_equal(p.radius, 254.0) - - #No effect + + # No effect p.to_metric() assert_equal(p.position, (2.54, 25.4)) assert_equal(p.radius, 254.0) + def test_polygon_offset(): p = Polygon((0, 0), 5, 10) p.offset(1, 0) - assert_equal(p.position,(1., 0.)) + assert_equal(p.position, (1., 0.)) p.offset(0, 1) - assert_equal(p.position,(1., 1.)) + assert_equal(p.position, (1., 1.)) + def test_region_ctor(): """ Test Region creation """ - apt = Circle((0,0), 0) - lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt)) - points = ((0, 0), (1,0), (1,1), (0,1)) + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) + points = ((0, 0), (1, 0), (1, 1), (0, 1)) r = Region(lines) for i, p in enumerate(lines): assert_equal(r.primitives[i], p) + def test_region_bounds(): """ Test region bounding box calculation """ - apt = Circle((0,0), 0) - lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt)) + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) r = Region(lines) xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (0, 1)) @@ -786,68 +846,76 @@ def test_region_bounds(): def test_region_offset(): - apt = Circle((0,0), 0) - lines = (Line((0,0), (1,0), apt), Line((1,0), (1,1), apt), Line((1,1), (0,1), apt), Line((0,1), (0,0), apt)) + apt = Circle((0, 0), 0) + lines = (Line((0, 0), (1, 0), apt), Line((1, 0), (1, 1), apt), + Line((1, 1), (0, 1), apt), Line((0, 1), (0, 0), apt)) r = Region(lines) xlim, ylim = r.bounding_box r.offset(0, 1) - assert_array_almost_equal((xlim, tuple([y+1 for y in ylim])), r.bounding_box) + new_xlim, new_ylim = r.bounding_box + assert_array_almost_equal(new_xlim, xlim) + assert_array_almost_equal(new_ylim, tuple([y + 1 for y in ylim])) + def test_round_butterfly_ctor(): """ Test round butterfly creation """ - test_cases = (((0,0), 3), ((0, 0), 5), ((1,1), 7)) + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) for pos, diameter in test_cases: b = RoundButterfly(pos, diameter) assert_equal(b.position, pos) assert_equal(b.diameter, diameter) - assert_equal(b.radius, diameter/2.) + assert_equal(b.radius, diameter / 2.) + def test_round_butterfly_ctor_validation(): """ Test RoundButterfly argument validation """ assert_raises(TypeError, RoundButterfly, 3, 5) - assert_raises(TypeError, RoundButterfly, (3,4,5), 5) + assert_raises(TypeError, RoundButterfly, (3, 4, 5), 5) + def test_round_butterfly_conversion(): b = RoundButterfly((2.54, 25.4), 254.0, units='metric') - - #No Effect + + # No Effect b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.diameter, (254.0)) - + b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.diameter, 10.0) - - #No effect + + # No effect b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.diameter, 10.0) b = RoundButterfly((0.1, 1.0), 10.0, units='inch') - - #No effect + + # No effect b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.diameter, 10.0) - + b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.diameter, (254.0)) - - #No Effect + + # No Effect b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.diameter, (254.0)) + def test_round_butterfly_offset(): b = RoundButterfly((0, 0), 1) b.offset(1, 0) - assert_equal(b.position,(1., 0.)) + assert_equal(b.position, (1., 0.)) b.offset(0, 1) - assert_equal(b.position,(1., 1.)) + assert_equal(b.position, (1., 1.)) + def test_round_butterfly_bounds(): """ Test RoundButterfly bounding box calculation @@ -857,20 +925,23 @@ def test_round_butterfly_bounds(): assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) + def test_square_butterfly_ctor(): """ Test SquareButterfly creation """ - test_cases = (((0,0), 3), ((0, 0), 5), ((1,1), 7)) + test_cases = (((0, 0), 3), ((0, 0), 5), ((1, 1), 7)) for pos, side in test_cases: b = SquareButterfly(pos, side) assert_equal(b.position, pos) assert_equal(b.side, side) + def test_square_butterfly_ctor_validation(): """ Test SquareButterfly argument validation """ assert_raises(TypeError, SquareButterfly, 3, 5) - assert_raises(TypeError, SquareButterfly, (3,4,5), 5) + assert_raises(TypeError, SquareButterfly, (3, 4, 5), 5) + def test_square_butterfly_bounds(): """ Test SquareButterfly bounding box calculation @@ -880,51 +951,54 @@ def test_square_butterfly_bounds(): assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) + def test_squarebutterfly_conversion(): b = SquareButterfly((2.54, 25.4), 254.0, units='metric') - - #No effect + + # No effect b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.side, (254.0)) - + b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.side, 10.0) - - #No effect + + # No effect b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.side, 10.0) b = SquareButterfly((0.1, 1.0), 10.0, units='inch') - - #No effect + + # No effect b.to_inch() assert_equal(b.position, (0.1, 1.0)) assert_equal(b.side, 10.0) - + b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.side, (254.0)) - - #No effect + + # No effect b.to_metric() assert_equal(b.position, (2.54, 25.4)) assert_equal(b.side, (254.0)) + def test_square_butterfly_offset(): b = SquareButterfly((0, 0), 1) b.offset(1, 0) - assert_equal(b.position,(1., 0.)) + assert_equal(b.position, (1., 0.)) b.offset(0, 1) - assert_equal(b.position,(1., 1.)) + assert_equal(b.position, (1., 1.)) + def test_donut_ctor(): """ Test Donut primitive creation """ - test_cases = (((0,0), 'round', 3, 5), ((0, 0), 'square', 5, 7), - ((1,1), 'hexagon', 7, 9), ((2, 2), 'octagon', 9, 11)) + test_cases = (((0, 0), 'round', 3, 5), ((0, 0), 'square', 5, 7), + ((1, 1), 'hexagon', 7, 9), ((2, 2), 'octagon', 9, 11)) for pos, shape, in_d, out_d in test_cases: d = Donut(pos, shape, in_d, out_d) assert_equal(d.position, pos) @@ -932,65 +1006,68 @@ def test_donut_ctor(): assert_equal(d.inner_diameter, in_d) assert_equal(d.outer_diameter, out_d) + def test_donut_ctor_validation(): assert_raises(TypeError, Donut, 3, 'round', 5, 7) assert_raises(TypeError, Donut, (3, 4, 5), 'round', 5, 7) assert_raises(ValueError, Donut, (0, 0), 'triangle', 3, 5) assert_raises(ValueError, Donut, (0, 0), 'round', 5, 3) + def test_donut_bounds(): d = Donut((0, 0), 'round', 0.0, 2.0) - assert_equal(d.lower_left, (-1.0, -1.0)) - assert_equal(d.upper_right, (1.0, 1.0)) xbounds, ybounds = d.bounding_box assert_equal(xbounds, (-1., 1.)) assert_equal(ybounds, (-1., 1.)) + def test_donut_conversion(): d = Donut((2.54, 25.4), 'round', 254.0, 2540.0, units='metric') - - #No effect + + # No effect d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.inner_diameter, 254.0) assert_equal(d.outer_diameter, 2540.0) - + d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.inner_diameter, 10.0) assert_equal(d.outer_diameter, 100.0) - - #No effect + + # No effect d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.inner_diameter, 10.0) assert_equal(d.outer_diameter, 100.0) d = Donut((0.1, 1.0), 'round', 10.0, 100.0, units='inch') - - #No effect + + # No effect d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.inner_diameter, 10.0) assert_equal(d.outer_diameter, 100.0) - + d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.inner_diameter, 254.0) assert_equal(d.outer_diameter, 2540.0) - - #No effect + + # No effect d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.inner_diameter, 254.0) assert_equal(d.outer_diameter, 2540.0) + def test_donut_offset(): d = Donut((0, 0), 'round', 1, 10) d.offset(1, 0) - assert_equal(d.position,(1., 0.)) + assert_equal(d.position, (1., 0.)) d.offset(0, 1) - assert_equal(d.position,(1., 1.)) + assert_equal(d.position, (1., 1.)) + def test_drill_ctor(): """ Test drill primitive creation @@ -1000,13 +1077,15 @@ def test_drill_ctor(): d = Drill(position, diameter) assert_equal(d.position, position) assert_equal(d.diameter, diameter) - assert_equal(d.radius, diameter/2.) + assert_equal(d.radius, diameter / 2.) + def test_drill_ctor_validation(): """ Test drill argument validation """ assert_raises(TypeError, Drill, 3, 5) - assert_raises(TypeError, Drill, (3,4,5), 5) + assert_raises(TypeError, Drill, (3, 4, 5), 5) + def test_drill_bounds(): d = Drill((0, 0), 2) @@ -1018,46 +1097,48 @@ def test_drill_bounds(): assert_array_almost_equal(xbounds, (0, 2)) assert_array_almost_equal(ybounds, (1, 3)) + def test_drill_conversion(): d = Drill((2.54, 25.4), 254., units='metric') - - #No effect + + # No effect d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.diameter, 254.0) - + d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.diameter, 10.0) - - #No effect + + # No effect d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.diameter, 10.0) - d = Drill((0.1, 1.0), 10., units='inch') - - #No effect + + # No effect d.to_inch() assert_equal(d.position, (0.1, 1.0)) assert_equal(d.diameter, 10.0) - + d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.diameter, 254.0) - - #No effect + + # No effect d.to_metric() assert_equal(d.position, (2.54, 25.4)) assert_equal(d.diameter, 254.0) + def test_drill_offset(): d = Drill((0, 0), 1.) d.offset(1, 0) - assert_equal(d.position,(1., 0.)) + assert_equal(d.position, (1., 0.)) d.offset(0, 1) - assert_equal(d.position,(1., 1.)) + assert_equal(d.position, (1., 1.)) + def test_drill_equality(): d = Drill((2.54, 25.4), 254.) diff --git a/gerber/tests/test_rs274x.py b/gerber/tests/test_rs274x.py index c084e80..d5acfe8 100644 --- a/gerber/tests/test_rs274x.py +++ b/gerber/tests/test_rs274x.py @@ -9,31 +9,35 @@ from .tests import * TOP_COPPER_FILE = os.path.join(os.path.dirname(__file__), - 'resources/top_copper.GTL') + 'resources/top_copper.GTL') MULTILINE_READ_FILE = os.path.join(os.path.dirname(__file__), - 'resources/multiline_read.ger') + 'resources/multiline_read.ger') def test_read(): top_copper = read(TOP_COPPER_FILE) assert(isinstance(top_copper, GerberFile)) + def test_multiline_read(): multiline = read(MULTILINE_READ_FILE) assert(isinstance(multiline, GerberFile)) assert_equal(10, len(multiline.statements)) + def test_comments_parameter(): top_copper = read(TOP_COPPER_FILE) assert_equal(top_copper.comments[0], 'This is a comment,:') + def test_size_parameter(): top_copper = read(TOP_COPPER_FILE) size = top_copper.size assert_almost_equal(size[0], 2.256900, 6) assert_almost_equal(size[1], 1.500000, 6) + def test_conversion(): import copy top_copper = read(TOP_COPPER_FILE) @@ -50,4 +54,3 @@ def test_conversion(): for i, m in zip(top_copper.primitives, top_copper_inch.primitives): assert_equal(i, m) - diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py index fe9b2e6..35f6f47 100644 --- a/gerber/tests/test_utils.py +++ b/gerber/tests/test_utils.py @@ -52,7 +52,7 @@ def test_format(): ((2, 6), '-1', -0.000001), ((2, 5), '-1', -0.00001), ((2, 4), '-1', -0.0001), ((2, 3), '-1', -0.001), ((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), - ((2, 6), '0', 0) ] + ((2, 6), '0', 0)] for fmt, string, value in test_cases: assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) @@ -76,7 +76,7 @@ def test_decimal_truncation(): value = 1.123456789 for x in range(10): result = decimal_string(value, precision=x) - calculated = '1.' + ''.join(str(y) for y in range(1,x+1)) + calculated = '1.' + ''.join(str(y) for y in range(1, x + 1)) assert_equal(result, calculated) @@ -96,25 +96,34 @@ def test_parse_format_validation(): """ assert_raises(ValueError, parse_gerber_value, '00001111', (7, 5)) assert_raises(ValueError, parse_gerber_value, '00001111', (5, 8)) - assert_raises(ValueError, parse_gerber_value, '00001111', (13,1)) - + assert_raises(ValueError, parse_gerber_value, '00001111', (13, 1)) + + def test_write_format_validation(): """ Test write_gerber_value() format validation """ assert_raises(ValueError, write_gerber_value, 69.0, (7, 5)) assert_raises(ValueError, write_gerber_value, 69.0, (5, 8)) - assert_raises(ValueError, write_gerber_value, 69.0, (13,1)) + assert_raises(ValueError, write_gerber_value, 69.0, (13, 1)) def test_detect_format_with_short_file(): """ Verify file format detection works with short files """ assert_equal('unknown', detect_file_format('gerber/tests/__init__.py')) - + + def test_validate_coordinates(): assert_raises(TypeError, validate_coordinates, 3) assert_raises(TypeError, validate_coordinates, 3.1) assert_raises(TypeError, validate_coordinates, '14') assert_raises(TypeError, validate_coordinates, (0,)) - assert_raises(TypeError, validate_coordinates, (0,1,2)) - assert_raises(TypeError, validate_coordinates, (0,'string')) + assert_raises(TypeError, validate_coordinates, (0, 1, 2)) + assert_raises(TypeError, validate_coordinates, (0, 'string')) + + +def test_convex_hull(): + points = [(0, 0), (1, 0), (1, 1), (0.5, 0.5), (0, 1), (0, 0)] + expected = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)] + assert_equal(set(convex_hull(points)), set(expected)) + \ No newline at end of file diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py index 2c75acd..ac08208 100644 --- a/gerber/tests/tests.py +++ b/gerber/tests/tests.py @@ -16,7 +16,8 @@ from nose import with_setup __all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal', 'assert_almost_equal', 'assert_array_almost_equal', 'assert_true', - 'assert_false', 'assert_raises', 'raises', 'with_setup' ] + 'assert_false', 'assert_raises', 'raises', 'with_setup'] + def assert_array_almost_equal(arr1, arr2, decimal=6): assert_equal(len(arr1), len(arr2)) diff --git a/gerber/utils.py b/gerber/utils.py index 6653683..e3eda1d 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -23,15 +23,15 @@ This module provides utility functions for working with Gerber and Excellon files. """ -# Author: Hamilton Kibbe -# License: - import os from math import radians, sin, cos from operator import sub +from copy import deepcopy +from pyhull.convex_hull import ConvexHull MILLIMETERS_PER_INCH = 25.4 + def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -92,7 +92,8 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): else: digits = list(value) - result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + result = float( + ''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -result if negative else result @@ -132,7 +133,8 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - # Edge case... (per Gerber spec we should return 0 in all cases, see page 77) + # Edge case... (per Gerber spec we should return 0 in all cases, see page + # 77) if value == 0: return '0' @@ -222,7 +224,7 @@ def detect_file_format(data): elif '%FS' in line: return 'rs274x' elif ((len(line.split()) >= 2) and - (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): + (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): return 'ipc_d_356' return 'unknown' @@ -252,6 +254,7 @@ def metric(value): """ return value * MILLIMETERS_PER_INCH + def inch(value): """ Convert millimeter value to inches @@ -295,6 +298,26 @@ def rotate_point(point, angle, center=(0.0, 0.0)): def listdir(directory, ignore_hidden=True, ignore_os=True): + """ List files in given directory. + Differs from os.listdir() in that hidden and OS-generated files are ignored + by default. + + Parameters + ---------- + directory : str + path to the directory for which to list files. + + ignore_hidden : bool + If True, ignore files beginning with a leading '.' + + ignore_os : bool + If True, ignore OS-generated files, e.g. Thumbs.db + + Returns + ------- + files : list + list of files in specified directory + """ os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') files = os.listdir(directory) if ignore_hidden: @@ -302,3 +325,9 @@ def listdir(directory, ignore_hidden=True, ignore_os=True): if ignore_os: files = [f for f in files if not f in os_files] return files + + +def convex_hull(points): + vertices = ConvexHull(points).vertices + return [points[idx] for idx in + set([point for pair in vertices for point in pair])] diff --git a/requirements.txt b/requirements.txt index a7f5f01..014e92b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ ## The following requirements were added by pip --freeze: cairocffi==0.6 +pyhull==1.5.6 -- cgit From 66a0d09e72b078da5820820aa5c6a2a7d7430507 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 04:39:55 -0500 Subject: Add support for mirrored rendering - The default theme now renders the bottom layers mirrored. - see https://github.com/curtacircuitos/pcb-tools/blob/master/examples/pcb_bottom.png for an example. --- examples/pcb_bottom.png | Bin 39174 -> 40567 bytes gerber/render/cairo_backend.py | 19 ++++++++++++------- gerber/render/theme.py | 11 ++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/examples/pcb_bottom.png b/examples/pcb_bottom.png index 1e8c369..b2dab0c 100644 Binary files a/examples/pcb_bottom.png and b/examples/pcb_bottom.png differ diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index cc2722a..2370eb9 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -18,6 +18,7 @@ import cairocffi as cairo import tempfile +import copy from .render import GerberContext, RenderSettings from .theme import THEMES @@ -37,6 +38,7 @@ class GerberCairoContext(GerberContext): self.surface = None self.ctx = None self.active_layer = None + self.active_matrix = None self.output_ctx = None self.bg = False self.mask = None @@ -120,9 +122,7 @@ class GerberCairoContext(GerberContext): self.invert = settings.invert # Get a new clean layer to render on - self._new_render_layer() - if settings.mirror: - raise Warning('mirrored layers aren\'t supported yet...') + self._new_render_layer(mirror=settings.mirror) for prim in layer.primitives: self.render(prim) # Add layer to image @@ -262,30 +262,35 @@ class GerberCairoContext(GerberContext): self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) - def _new_render_layer(self, color=None): + def _new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) ctx.scale(1, -1) ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - - size_in_pixels[1]) + (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) if self.invert: ctx.set_operator(cairo.OPERATOR_OVER) ctx.set_source_rgba(*self.color, alpha=self.alpha) ctx.paint() + matrix = copy.copy(self._xform_matrix) + if mirror: + matrix.xx = -1.0 + matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] self.ctx = ctx self.active_layer = layer + self.active_matrix = matrix def _flatten(self): self.output_ctx.set_operator(cairo.OPERATOR_OVER) ptn = cairo.SurfacePattern(self.active_layer) - ptn.set_matrix(self._xform_matrix) + ptn.set_matrix(self.active_matrix) self.output_ctx.set_source(ptn) self.output_ctx.paint() self.ctx = None self.active_layer = None + self.active_matrix = None def _paint_background(self, force=False): if (not self.bg) or force: diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 6135ccb..4d325c5 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -41,11 +41,11 @@ class Theme(object): self.name = 'Default' if name is None else name self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0)) self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) - self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'])) + self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True)) self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) - self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True, mirror=True)) self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) - self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) + self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) @@ -61,9 +61,10 @@ THEMES = { 'default': Theme(), 'OSH Park': Theme(name='OSH Park', top=RenderSettings(COLORS['enig copper']), - bottom=RenderSettings(COLORS['enig copper']), + bottom=RenderSettings(COLORS['enig copper'], mirror=True), topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), - bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True)), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, mirror=True)), + 'Blue': Theme(name='Blue', topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), -- cgit From 1d9270d80981b70376eff4a8f275226969d5ebfd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 22 Jan 2016 03:24:50 -0200 Subject: Fix NameError on Polygon primitive rendering --- gerber/am_statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gerber/am_statements.py b/gerber/am_statements.py index f67b0db..11a6187 100644 --- a/gerber/am_statements.py +++ b/gerber/am_statements.py @@ -463,7 +463,7 @@ class AMPolygonPrimitive(AMPrimitive): # Offset the primitive from macro position position = tuple([a + b for a , b in zip (position, self.position)]) # Return a renderable primitive - return Polygon(position, vertices, self.diameter/2., + return Polygon(position, self.vertices, self.diameter/2., rotation=self.rotation, level_polarity=level_polarity, units=units) -- cgit From b9f1b106c3006f1dddb1279ae9622630a29d18c7 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 25 Jan 2016 12:42:12 -0200 Subject: Excellon format detection uses ExcelonFile.bounds now Long term we should have only one .bounds method. But ExcellonParser right now is not correct for cases with two drills in the same line (it will report one dimension being zero) --- gerber/excellon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gerber/excellon.py b/gerber/excellon.py index b1b94df..b29f7f0 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -604,8 +604,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 -- cgit From 5df38c014fd09792995b2b12b1982c535c962c9a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 28 Jan 2016 12:19:03 -0500 Subject: Cleanup, rendering fixes. fixed rendering of tented vias fixed rendering of semi-transparent layers fixed file type detection issues added some examples --- .gitignore | 4 +- Makefile | 4 + README.md | 3 +- examples/cairo_bottom.png | Bin 0 -> 42847 bytes examples/cairo_example.png | Bin 104184 -> 100397 bytes examples/cairo_example.py | 60 +++++++------ examples/pcb_bottom.png | Bin 40567 -> 36992 bytes examples/pcb_example.py | 23 +++-- examples/pcb_top.png | Bin 98827 -> 92403 bytes examples/pcb_transparent_copper.png | Bin 0 -> 85302 bytes gerber/__init__.py | 1 + gerber/cam.py | 7 +- gerber/common.py | 35 ++++---- gerber/excellon.py | 9 +- gerber/ipc356.py | 45 +++++++--- gerber/layers.py | 66 +++++++------- gerber/pcb.py | 23 +++-- gerber/primitives.py | 47 ++++++++-- gerber/render/__init__.py | 1 + gerber/render/cairo_backend.py | 172 +++++++++++++++++++----------------- gerber/render/render.py | 19 ++-- gerber/render/theme.py | 23 +++-- gerber/rs274x.py | 7 +- gerber/tests/test_ipc356.py | 2 +- gerber/tests/test_layers.py | 75 +++++++++++++++- gerber/utils.py | 6 +- 26 files changed, 401 insertions(+), 231 deletions(-) create mode 100644 examples/cairo_bottom.png create mode 100644 examples/pcb_transparent_copper.png diff --git a/.gitignore b/.gitignore index 01ba410..7c3daa0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,6 @@ nosetests.xml .mr.developer.cfg .project .pydevproject -.idea/workspace.xml -.idea/misc.xml .idea # Komodo Files @@ -43,4 +41,4 @@ nosetests.xml # OS Files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/Makefile b/Makefile index 87f0124..a1a88ea 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ test-coverage: rm -rf coverage .coverage $(NOSETESTS) -s -v --with-coverage --cover-package=gerber +.PHONY: install +install: + PYTHONPATH=. $(PYTHON) setup.py install + .PHONY: doc-html doc-html: (cd $(DOC_ROOT); make html) diff --git a/README.md b/README.md index 298cf0c..cb73b30 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ pcb-tools Tools to handle Gerber and Excellon files in Python. -Useage Example: +Usage Example: --------------- import gerber from gerber.render import GerberCairoContext @@ -27,6 +27,7 @@ Rendering Examples: ------------------- ###Top Composite rendering ![Composite Top Image](examples/cairo_example.png) +![Composite Bottom Image](examples/cairo_bottom.png) Source code for this example can be found [here](examples/cairo_example.py). diff --git a/examples/cairo_bottom.png b/examples/cairo_bottom.png new file mode 100644 index 0000000..03e40e7 Binary files /dev/null and b/examples/cairo_bottom.png differ diff --git a/examples/cairo_example.png b/examples/cairo_example.png index 671de5c..76e3bff 100644 Binary files a/examples/cairo_example.png and b/examples/cairo_example.png differ diff --git a/examples/cairo_example.py b/examples/cairo_example.py index 14a7037..fcd7a44 100644 --- a/examples/cairo_example.py +++ b/examples/cairo_example.py @@ -24,46 +24,54 @@ a .png file. """ import os -from gerber import read -from gerber.render import GerberCairoContext, theme +from gerber import load_layer +from gerber.render import GerberCairoContext, RenderSettings, theme GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers')) # Open the gerber files -copper = read(os.path.join(GERBER_FOLDER, 'copper.GTL')) -mask = read(os.path.join(GERBER_FOLDER, 'soldermask.GTS')) -silk = read(os.path.join(GERBER_FOLDER, 'silkscreen.GTO')) -drill = read(os.path.join(GERBER_FOLDER, 'ncdrill.DRD')) +copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL')) +mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS')) +silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO')) +drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD')) # Create a new drawing context ctx = GerberCairoContext() -# Set opacity and color for copper layer -ctx.alpha = 1.0 -ctx.color = theme.COLORS['hasl copper'] - -# Draw the copper layer -copper.render(ctx) - -# Set opacity and color for soldermask layer -ctx.alpha = 0.75 -ctx.color = theme.COLORS['green soldermask'] +# Draw the copper layer. render_layer() uses the default color scheme for the +# layer, based on the layer type. Copper layers are rendered as +ctx.render_layer(copper) # Draw the soldermask layer -mask.render(ctx, invert=True) +ctx.render_layer(mask) -# Set opacity and color for silkscreen layer -ctx.alpha = 1.0 -ctx.color = theme.COLORS['white'] -# Draw the silkscreen layer -silk.render(ctx) +# The default style can be overridden by passing a RenderSettings instance to +# render_layer(). +# First, create a settings object: +our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85) -# Set opacity for drill layer -ctx.alpha = 1.0 -ctx.color = theme.COLORS['black'] -drill.render(ctx) +# Draw the silkscreen layer, and specify the rendering settings to use +ctx.render_layer(silk, settings=our_settings) + +# Draw the drill layer +ctx.render_layer(drill) # Write output to png file ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png')) + +# Load the bottom layers +copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL')) +mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS')) + +# Clear the drawing +ctx.clear() + +# Render bottom layers +ctx.render_layer(copper) +ctx.render_layer(mask) +ctx.render_layer(drill) + +# Write png file +ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png')) diff --git a/examples/pcb_bottom.png b/examples/pcb_bottom.png index b2dab0c..1834c06 100644 Binary files a/examples/pcb_bottom.png and b/examples/pcb_bottom.png differ diff --git a/examples/pcb_example.py b/examples/pcb_example.py index bba030e..d09c17a 100644 --- a/examples/pcb_example.py +++ b/examples/pcb_example.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2015 Hamilton Kibbe +# Copyright 2016 Hamilton Kibbe # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,14 +27,25 @@ from gerber.render import GerberCairoContext, theme GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers')) - # Create a new drawing context ctx = GerberCairoContext() -# Create a new PCB +# Create a new PCB instance pcb = PCB.from_directory(GERBER_FOLDER) -# Render PCB -ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png',), theme.THEMES['OSH Park']) -ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), theme.THEMES['OSH Park']) +# Render PCB top view +ctx.render_layers(pcb.top_layers, + os.path.join(os.path.dirname(__file__), 'pcb_top.png',), + theme.THEMES['OSH Park']) + +# Render PCB bottom view +ctx.render_layers(pcb.bottom_layers, + os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), + theme.THEMES['OSH Park']) + +# Render copper layers only +ctx.render_layers(pcb.copper_layers + pcb.drill_layers, + os.path.join(os.path.dirname(__file__), + 'pcb_transparent_copper.png'), + theme.THEMES['Transparent Copper']) diff --git a/examples/pcb_top.png b/examples/pcb_top.png index 60bc245..4e4f656 100644 Binary files a/examples/pcb_top.png and b/examples/pcb_top.png differ diff --git a/examples/pcb_transparent_copper.png b/examples/pcb_transparent_copper.png new file mode 100644 index 0000000..f54d05f Binary files /dev/null and b/examples/pcb_transparent_copper.png differ diff --git a/gerber/__init__.py b/gerber/__init__.py index 5cfdad7..1faba53 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -24,4 +24,5 @@ files in python. """ from .common import read, loads +from .layers import load_layer, load_layer_data from .pcb import PCB diff --git a/gerber/cam.py b/gerber/cam.py index dda5c10..86312fb 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -251,7 +251,7 @@ class CamFile(object): def to_metric(self): pass - def render(self, ctx, invert=False, filename=None): + def render(self, ctx=None, invert=False, filename=None): """ Generate image of layer. Parameters @@ -262,13 +262,16 @@ class CamFile(object): filename : string If provided, save the rendered image to `filename` """ + if ctx is None: + from .render import GerberCairoContext + ctx = GerberCairoContext() ctx.set_bounds(self.bounds) ctx._paint_background() ctx.invert = invert ctx._new_render_layer() for p in self.primitives: ctx.render(p) - ctx._flatten() + ctx._paint() if filename is not None: ctx.dump(filename) diff --git a/gerber/common.py b/gerber/common.py index cf137dd..334714b 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -33,42 +33,41 @@ def read(filename): Returns ------- file : CncFile subclass - CncFile object representing the file, either GerberFile or - ExcellonFile. Returns None if file is not an Excellon or Gerber file. + CncFile object representing the file, either GerberFile, ExcellonFile, + or IPCNetlist. Returns None if file is not of the proper type. """ with open(filename, 'rU') as f: data = f.read() - fmt = detect_file_format(data) - if fmt == 'rs274x': - return rs274x.read(filename) - elif fmt == 'excellon': - return excellon.read(filename) - elif fmt == 'ipc_d_356': - return ipc356.read(filename) - else: - raise ParseError('Unable to detect file format') + return loads(data, filename) -def loads(data): +def loads(data, filename=None): """ Read gerber or excellon file contents from a string and return a representative object. Parameters ---------- data : string - gerber or excellon file contents as a string. + Source file contents as a string. + + filename : string, optional + String containing the filename of the data source. Returns ------- file : CncFile subclass - CncFile object representing the file, either GerberFile or - ExcellonFile. Returns None if file is not an Excellon or Gerber file. + CncFile object representing the data, either GerberFile, ExcellonFile, + or IPCNetlist. Returns None if data is not of the proper type. """ fmt = detect_file_format(data) if fmt == 'rs274x': - return rs274x.loads(data) + return rs274x.loads(data, filename) elif fmt == 'excellon': - return excellon.loads(data) + return excellon.loads(data, filename) + elif fmt == 'ipc_d_356': + return ipc356.loads(data, filename) else: - raise TypeError('Unable to detect file format') + raise ParseError('Unable to detect file format') + + diff --git a/gerber/excellon.py b/gerber/excellon.py index b29f7f0..24715d8 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -28,7 +28,7 @@ import operator try: from cStringIO import StringIO -except(ImportError): +except ImportError: from io import StringIO from .excellon_statements import * @@ -57,13 +57,16 @@ def read(filename): return ExcellonParser(settings).parse(filename) -def loads(data): +def loads(data, filename=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 + Returns ------- file : :class:`gerber.excellon.ExcellonFile` @@ -72,7 +75,7 @@ def loads(data): """ # File object should use settings from source file by default. settings = FileSettings(**detect_excellon_format(data)) - return ExcellonParser(settings).parse_raw(data) + return ExcellonParser(settings).parse_raw(data, filename) class DrillHit(object): diff --git a/gerber/ipc356.py b/gerber/ipc356.py index 7dadd22..a831c0f 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -35,7 +35,7 @@ _SM_FIELD = { def read(filename): - """ Read data from filename and return an IPC_D_356 + """ Read data from filename and return an IPCNetlist Parameters ---------- filename : string @@ -43,19 +43,38 @@ def read(filename): Returns ------- - file : :class:`gerber.ipc356.IPC_D_356` - An IPC_D_356 object created from the specified file. + file : :class:`gerber.ipc356.IPCNetlist` + An IPCNetlist object created from the specified file. """ # File object should use settings from source file by default. - return IPC_D_356.from_file(filename) + return IPCNetlist.from_file(filename) -class IPC_D_356(CamFile): +def loads(data, filename=None): + """ Generate an IPCNetlist object from IPC-D-356 data in memory + + Parameters + ---------- + data : string + string containing netlist file contents + + filename : string, optional + string containing the filename of the data source + + Returns + ------- + file : :class:`gerber.ipc356.IPCNetlist` + An IPCNetlist created from the specified file. + """ + return IPCNetlistParser().parse_raw(data, filename) + + +class IPCNetlist(CamFile): @classmethod def from_file(cls, filename): - parser = IPC_D_356_Parser() + parser = IPCNetlistParser() return parser.parse(filename) def __init__(self, statements, settings, primitives=None, filename=None): @@ -130,7 +149,7 @@ class IPC_D_356(CamFile): ctx.dump(filename) -class IPC_D_356_Parser(object): +class IPCNetlistParser(object): # TODO: Allow multi-line statements (e.g. Altium board edge) def __init__(self): @@ -145,9 +164,13 @@ class IPC_D_356_Parser(object): def parse(self, filename): with open(filename, 'rU') as f: - oldline = '' - for line in f: - # Check for existing multiline data... + data = f.read() + return self.parse_raw(data, filename) + + def parse_raw(self, data, filename=None): + oldline = '' + for line in data.splitlines(): + # Check for existing multiline data... if oldline != '': if len(line) and line[0] == '0': oldline = oldline.rstrip('\r\n') + line[3:].rstrip() @@ -158,7 +181,7 @@ class IPC_D_356_Parser(object): oldline = line self._parse_line(oldline) - return IPC_D_356(self.statements, self.settings, filename=filename) + return IPCNetlist(self.statements, self.settings, filename=filename) def _parse_line(self, line): if not len(line): diff --git a/gerber/layers.py b/gerber/layers.py index 29e452b..93f0e36 100644 --- a/gerber/layers.py +++ b/gerber/layers.py @@ -19,8 +19,9 @@ import os import re from collections import namedtuple +from . import common from .excellon import ExcellonFile -from .ipc356 import IPC_D_356 +from .ipc356 import IPCNetlist Hint = namedtuple('Hint', 'layer ext name') @@ -73,9 +74,21 @@ hints = [ ext=['ipc'], name=[], ), + Hint(layer='drawing', + ext=['fab'], + name=['assembly drawing', 'assembly', 'fabrication', 'fab drawing'] + ), ] +def load_layer(filename): + return PCBLayer.from_cam(common.read(filename)) + + +def load_layer_data(data, filename=None): + return PCBLayer.from_cam(common.loads(data, filename)) + + def guess_layer_class(filename): try: directory, name = os.path.split(filename) @@ -89,24 +102,30 @@ def guess_layer_class(filename): return 'unknown' -def sort_layers(layers): +def sort_layers(layers, from_top=True): layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top', 'internal', 'bottom', 'bottommask', 'bottomsilk', - 'bottompaste', 'drill', ] + 'bottompaste'] + append_after = ['drill', 'drawing'] + output = [] - drill_layers = [layer for layer in layers if layer.layer_class == 'drill'] internal_layers = list(sorted([layer for layer in layers if layer.layer_class == 'internal'])) for layer_class in layer_order: if layer_class == 'internal': output += internal_layers - elif layer_class == 'drill': - output += drill_layers else: for layer in layers: if layer.layer_class == layer_class: output.append(layer) + if not from_top: + output = list(reversed(output)) + + for layer_class in append_after: + for layer in layers: + if layer.layer_class == layer_class: + output.append(layer) return output @@ -126,14 +145,14 @@ class PCBLayer(object): """ @classmethod - def from_gerber(cls, camfile): + def from_cam(cls, camfile): filename = camfile.filename layer_class = guess_layer_class(filename) if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'): - return DrillLayer.from_gerber(camfile) + return DrillLayer.from_cam(camfile) elif layer_class == 'internal': - return InternalLayer.from_gerber(camfile) - if isinstance(camfile, IPC_D_356): + return InternalLayer.from_cam(camfile) + if isinstance(camfile, IPCNetlist): layer_class = 'ipc_netlist' return cls(filename, layer_class, camfile) @@ -155,9 +174,10 @@ class PCBLayer(object): def __repr__(self): return ''.format(self.layer_class) + class DrillLayer(PCBLayer): @classmethod - def from_gerber(cls, camfile): + def from_cam(cls, camfile): return cls(camfile.filename, camfile) def __init__(self, filename=None, cam_source=None, layers=None, **kwargs): @@ -168,11 +188,11 @@ class DrillLayer(PCBLayer): class InternalLayer(PCBLayer): @classmethod - def from_gerber(cls, camfile): + def from_cam(cls, camfile): filename = camfile.filename try: order = int(re.search(r'\d+', filename).group()) - except: + except AttributeError: order = 0 return cls(filename, camfile, order) @@ -209,23 +229,3 @@ class InternalLayer(PCBLayer): if not hasattr(other, 'order'): raise TypeError() return (self.order <= other.order) - - -class LayerSet(object): - - def __init__(self, name, layers, **kwargs): - super(LayerSet, self).__init__(**kwargs) - self.name = name - self.layers = list(layers) - - def __len__(self): - return len(self.layers) - - def __getitem__(self, item): - return self.layers[item] - - def to_render(self): - return self.layers - - def apply_theme(self, theme): - pass diff --git a/gerber/pcb.py b/gerber/pcb.py index 92a1f28..a213fb3 100644 --- a/gerber/pcb.py +++ b/gerber/pcb.py @@ -18,7 +18,7 @@ import os from .exceptions import ParseError -from .layers import PCBLayer, LayerSet, sort_layers +from .layers import PCBLayer, sort_layers from .common import read as gerber_read from .utils import listdir @@ -29,22 +29,26 @@ class PCB(object): def from_directory(cls, directory, board_name=None, verbose=False): layers = [] names = set() + # Validate directory = os.path.abspath(directory) if not os.path.isdir(directory): raise TypeError('{} is not a directory.'.format(directory)) + # Load gerber files for filename in listdir(directory, True, True): try: camfile = gerber_read(os.path.join(directory, filename)) - layer = PCBLayer.from_gerber(camfile) + layer = PCBLayer.from_cam(camfile) layers.append(layer) names.add(os.path.splitext(filename)[0]) if verbose: - print('Added {} layer <{}>'.format(layer.layer_class, filename)) + print('[PCB]: Added {} layer <{}>'.format(layer.layer_class, + filename)) except ParseError: if verbose: - print('Skipping file {}'.format(filename)) + print('[PCB]: Skipping file {}'.format(filename)) + # Try to guess board name if board_name is None: if len(names) == 1: @@ -66,14 +70,16 @@ class PCB(object): board_layers = [l for l in reversed(self.layers) if l.layer_class in ('topsilk', 'topmask', 'top')] drill_layers = [l for l in self.drill_layers if 'top' in l.layers] - return board_layers + drill_layers + # Drill layer goes under soldermask for proper rendering of tented vias + return [board_layers[0]] + drill_layers + board_layers[1:] @property def bottom_layers(self): board_layers = [l for l in self.layers if l.layer_class in ('bottomsilk', 'bottommask', 'bottom')] drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers] - return board_layers + drill_layers + # Drill layer goes under soldermask for proper rendering of tented vias + return [board_layers[0]] + drill_layers + board_layers[1:] @property def drill_layers(self): @@ -81,8 +87,9 @@ class PCB(object): @property def copper_layers(self): - return [layer for layer in self.layers if layer.layer_class in - ('top', 'bottom', 'internal')] + return list(reversed([layer for layer in self.layers if + layer.layer_class in + ('top', 'bottom', 'internal')])) @property def layer_count(self): diff --git a/gerber/primitives.py b/gerber/primitives.py index 24e13a2..fa611df 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -166,7 +166,6 @@ class Primitive(object): in zip(self.position, (x_offset, y_offset))]) - def _changed(self): """ Clear memoized properties. @@ -568,11 +567,11 @@ class Rectangle(Primitive): @property def axis_aligned_width(self): - return (self._cos_theta * self.width + self._sin_theta * self.height) + return (self._cos_theta * self.width) + (self._sin_theta * self.height) @property def axis_aligned_height(self): - return (self._cos_theta * self.height + self._sin_theta * self.width) + return (self._cos_theta * self.height) + (self._sin_theta * self.width) class Diamond(Primitive): @@ -640,25 +639,24 @@ class Diamond(Primitive): @property def axis_aligned_width(self): - return (self._cos_theta * self.width + self._sin_theta * self.height) + return (self._cos_theta * self.width) + (self._sin_theta * self.height) @property def axis_aligned_height(self): - return (self._cos_theta * self.height + self._sin_theta * self.width) + return (self._cos_theta * self.height) + (self._sin_theta * self.width) class ChamferRectangle(Primitive): """ """ - - def __init__(self, position, width, height, chamfer, corners, **kwargs): + def __init__(self, position, width, height, chamfer, corners=None, **kwargs): super(ChamferRectangle, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._width = width self._height = height self._chamfer = chamfer - self._corners = corners + self._corners = corners if corners is not None else [True] * 4 self._to_convert = ['position', 'width', 'height', 'chamfer'] @property @@ -718,7 +716,37 @@ class ChamferRectangle(Primitive): @property def vertices(self): - # TODO + if self._vertices is None: + vertices = [] + delta_w = self.width / 2. + delta_h = self.height / 2. + # order is UR, UL, LL, LR + rect_corners = [ + ((self.position[0] + delta_w), (self.position[1] + delta_h)), + ((self.position[0] - delta_w), (self.position[1] + delta_h)), + ((self.position[0] - delta_w), (self.position[1] - delta_h)), + ((self.position[0] + delta_w), (self.position[1] - delta_h)) + ] + for idx, corner, chamfered in enumerate((rect_corners, self.corners)): + x, y = corner + if chamfered: + if idx == 0: + vertices.append((x - self.chamfer, y)) + vertices.append((x, y - self.chamfer)) + elif idx == 1: + vertices.append((x + self.chamfer, y)) + vertices.append((x, y - self.chamfer)) + elif idx == 2: + vertices.append((x + self.chamfer, y)) + vertices.append((x, y + self.chamfer)) + elif idx == 3: + vertices.append((x - self.chamfer, y)) + vertices.append((x, y + self.chamfer)) + else: + vertices.append(corner) + self._vertices = [((x * self._cos_theta - y * self._sin_theta), + (x * self._sin_theta + y * self._cos_theta)) + for x, y in vertices] return self._vertices @property @@ -1142,3 +1170,4 @@ class TestRecord(Primitive): self.position = position self.net_name = net_name self.layer = layer + self._to_convert = ['position'] diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py index f76d28f..3598c4d 100644 --- a/gerber/render/__init__.py +++ b/gerber/render/__init__.py @@ -25,3 +25,4 @@ SVG is the only supported format. from .cairo_backend import GerberCairoContext +from .render import RenderSettings diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 2370eb9..df4fcf1 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -17,6 +17,7 @@ import cairocffi as cairo +import os import tempfile import copy @@ -36,16 +37,16 @@ class GerberCairoContext(GerberContext): super(GerberCairoContext, self).__init__() self.scale = (scale, scale) self.surface = None + self.surface_buffer = None self.ctx = None self.active_layer = None self.active_matrix = None self.output_ctx = None - self.bg = False - self.mask = None - self.mask_ctx = None + self.has_bg = False self.origin_in_inch = None self.size_in_inch = None self._xform_matrix = None + self._render_count = 0 @property def origin_in_pixels(self): @@ -66,10 +67,8 @@ class GerberCairoContext(GerberContext): self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch if (self.surface is None) or new_surface: self.surface_buffer = tempfile.NamedTemporaryFile() - self.surface = cairo.SVGSurface( - self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) + self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) self.output_ctx = cairo.Context(self.surface) - self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) self.output_ctx.scale(1, -1) self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) @@ -77,20 +76,44 @@ class GerberCairoContext(GerberContext): x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) - def render_layers(self, layers, filename, theme=THEMES['default']): + def render_layer(self, layer, filename=None, settings=None, bgsettings=None, + verbose=False): + if settings is None: + settings = THEMES['default'].get(layer.layer_class, RenderSettings()) + if bgsettings is None: + bgsettings = THEMES['default'].get('background', RenderSettings()) + + if self._render_count == 0: + if verbose: + print('[Render]: Rendering Background.') + self.clear() + self.set_bounds(layer.bounds) + self._paint_background(bgsettings) + if verbose: + print('[Render]: Rendering {} Layer.'.format(layer.layer_class)) + self._render_count += 1 + self._render_layer(layer, settings) + if filename is not None: + self.dump(filename, verbose) + + def render_layers(self, layers, filename, theme=THEMES['default'], + verbose=False): """ Render a set of layers """ - self.set_bounds(layers[0].bounds, True) - self._paint_background(True) - + self.clear() + bgsettings = theme['background'] for layer in layers: - self._render_layer(layer, theme) - self.dump(filename) + settings = theme.get(layer.layer_class, RenderSettings()) + self.render_layer(layer, settings=settings, bgsettings=bgsettings, + verbose=verbose) + self.dump(filename, verbose) - def dump(self, filename): + def dump(self, filename, verbose=False): """ Save image as `filename` """ - is_svg = filename.lower().endswith(".svg") + is_svg = os.path.splitext(filename.lower())[1] == '.svg' + if verbose: + print('[Render]: Writing image to {}'.format(filename)) if is_svg: self.surface.finish() self.surface_buffer.flush() @@ -115,30 +138,33 @@ class GerberCairoContext(GerberContext): self.surface_buffer.flush() return self.surface_buffer.read() - def _render_layer(self, layer, theme=THEMES['default']): - settings = theme.get(layer.layer_class, RenderSettings()) - self.color = settings.color - self.alpha = settings.alpha - self.invert = settings.invert + def clear(self): + self.surface = None + self.output_ctx = None + self.has_bg = False + self.origin_in_inch = None + self.size_in_inch = None + self._xform_matrix = None + self._render_count = 0 + if hasattr(self.surface_buffer, 'close'): + self.surface_buffer.close() + self.surface_buffer = None + def _render_layer(self, layer, settings): + self.invert = settings.invert # Get a new clean layer to render on self._new_render_layer(mirror=settings.mirror) for prim in layer.primitives: self.render(prim) # Add layer to image - self._flatten() + self._paint(settings.color, settings.alpha) def _render_line(self, line, color): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if line.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if line.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) if isinstance(line.aperture, Circle): width = line.aperture.diameter self.ctx.set_line_width(width * self.scale[0]) @@ -162,14 +188,9 @@ class GerberCairoContext(GerberContext): angle1 = arc.start_angle angle2 = arc.end_angle width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if arc.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if arc.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) # You actually have to do this... @@ -181,14 +202,9 @@ class GerberCairoContext(GerberContext): self.ctx.move_to(*end) # ...lame def _render_region(self, region, color): - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator(cairo.OPERATOR_OVER - if region.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if region.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.ctx.move_to(*self.scale_point(region.primitives[0].start)) @@ -210,29 +226,22 @@ class GerberCairoContext(GerberContext): def _render_circle(self, circle, color): center = self.scale_point(circle.position) - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator( - cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if circle.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) - self.ctx.arc(*center, radius=circle.radius * - self.scale[0], angle1=0, angle2=2 * math.pi) + self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0, + angle2=(2 * math.pi)) self.ctx.fill() def _render_rectangle(self, rectangle, color): lower_left = self.scale_point(rectangle.lower_left) - width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) - - if not self.invert: - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator( - cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) - else: - self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if rectangle.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.set_line_width(0) self.ctx.rectangle(*lower_left, width=width, height=height) self.ctx.fill() @@ -247,34 +256,31 @@ class GerberCairoContext(GerberContext): self._render_circle(circle, color) def _render_test_record(self, primitive, color): - position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)] - self.ctx.set_operator(cairo.OPERATOR_OVER) + position = [pos + origin for pos, origin in + zip(primitive.position, self.origin_in_inch)] self.ctx.select_font_face( 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) self.ctx.set_font_size(13) self._render_circle(Circle(position, 0.015), color) - self.ctx.set_source_rgba(*color, alpha=self.alpha) - self.ctx.set_operator( - cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR) - self.ctx.move_to(*[self.scale[0] * (coord + 0.015) - for coord in position]) + self.ctx.set_operator(cairo.OPERATOR_SOURCE + if primitive.level_polarity == 'dark' and + (not self.invert) else cairo.OPERATOR_CLEAR) + self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) self.ctx.scale(1, -1) self.ctx.show_text(primitive.net_name) self.ctx.scale(1, -1) def _new_render_layer(self, color=None, mirror=False): size_in_pixels = self.scale_point(self.size_in_inch) + matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) - ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) ctx.scale(1, -1) ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) + (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) if self.invert: ctx.set_operator(cairo.OPERATOR_OVER) - ctx.set_source_rgba(*self.color, alpha=self.alpha) ctx.paint() - matrix = copy.copy(self._xform_matrix) if mirror: matrix.xx = -1.0 matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] @@ -282,21 +288,23 @@ class GerberCairoContext(GerberContext): self.active_layer = layer self.active_matrix = matrix - def _flatten(self): - self.output_ctx.set_operator(cairo.OPERATOR_OVER) + def _paint(self, color=None, alpha=None): + color = color if color is not None else self.color + alpha = alpha if alpha is not None else self.alpha ptn = cairo.SurfacePattern(self.active_layer) ptn.set_matrix(self.active_matrix) - self.output_ctx.set_source(ptn) - self.output_ctx.paint() + self.output_ctx.set_source_rgba(*color, alpha=alpha) + self.output_ctx.mask(ptn) self.ctx = None self.active_layer = None self.active_matrix = None - def _paint_background(self, force=False): - if (not self.bg) or force: - self.bg = True - self.output_ctx.set_operator(cairo.OPERATOR_OVER) - self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0) + def _paint_background(self, settings=None): + color = settings.color if settings is not None else self.background_color + alpha = settings.alpha if settings is not None else 1.0 + if not self.has_bg: + self.has_bg = True + self.output_ctx.set_source_rgba(*color, alpha=alpha) self.output_ctx.paint() def scale_point(self, point): diff --git a/gerber/render/render.py b/gerber/render/render.py index d7a62e1..724aaea 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -45,7 +45,8 @@ class GerberContext(object): Measurement units. 'inch' or 'metric' color : tuple (, , ) - Color used for rendering as a tuple of normalized (red, green, blue) values. + Color used for rendering as a tuple of normalized (red, green, blue) + values. drill_color : tuple (, , ) Color used for rendering drill hits. Format is the same as for `color`. @@ -62,8 +63,9 @@ class GerberContext(object): self._units = units self._color = (0.7215, 0.451, 0.200) self._background_color = (0.0, 0.0, 0.0) + self._drill_color = (0.0, 0.0, 0.0) self._alpha = 1.0 - self._invert = False + self.invert = False self.ctx = None @property @@ -125,14 +127,6 @@ class GerberContext(object): raise ValueError('Alpha must be between 0.0 and 1.0') self._alpha = alpha - @property - def invert(self): - return self._invert - - @invert.setter - def invert(self, invert): - self._invert = invert - def render(self, primitive): color = self.color if isinstance(primitive, Line): @@ -156,7 +150,6 @@ class GerberContext(object): else: return - def _render_line(self, primitive, color): pass @@ -186,8 +179,8 @@ class GerberContext(object): class RenderSettings(object): - - def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False): + def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, + mirror=False): self.color = color self.alpha = alpha self.invert = invert diff --git a/gerber/render/theme.py b/gerber/render/theme.py index 4d325c5..d382a8d 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -25,12 +25,12 @@ COLORS = { 'green': (0.0, 1.0, 0.0), 'blue': (0.0, 0.0, 1.0), 'fr-4': (0.290, 0.345, 0.0), - 'green soldermask': (0.0, 0.612, 0.396), + 'green soldermask': (0.0, 0.412, 0.278), 'blue soldermask': (0.059, 0.478, 0.651), 'red soldermask': (0.968, 0.169, 0.165), 'black soldermask': (0.298, 0.275, 0.282), 'purple soldermask': (0.2, 0.0, 0.334), - 'enig copper': (0.686, 0.525, 0.510), + 'enig copper': (0.694, 0.533, 0.514), 'hasl copper': (0.871, 0.851, 0.839) } @@ -39,11 +39,11 @@ class Theme(object): def __init__(self, name=None, **kwargs): self.name = 'Default' if name is None else name - self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0)) + self.background = kwargs.get('background', RenderSettings(COLORS['fr-4'])) self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True)) - self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True)) - self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True, mirror=True)) + self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True)) + self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True)) self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) @@ -60,12 +60,21 @@ class Theme(object): THEMES = { 'default': Theme(), 'OSH Park': Theme(name='OSH Park', + background=RenderSettings(COLORS['purple soldermask']), top=RenderSettings(COLORS['enig copper']), bottom=RenderSettings(COLORS['enig copper'], mirror=True), - topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True), - bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, mirror=True)), + topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True), + bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True), + topsilk=RenderSettings(COLORS['white'], alpha=0.8), + bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)), 'Blue': Theme(name='Blue', topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), + + 'Transparent Copper': Theme(name='Transparent', + background=RenderSettings((0.9, 0.9, 0.9)), + top=RenderSettings(COLORS['red'], alpha=0.5), + bottom=RenderSettings(COLORS['blue'], alpha=0.5), + drill=RenderSettings((0.3, 0.3, 0.3))), } diff --git a/gerber/rs274x.py b/gerber/rs274x.py index b19913b..76e5101 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -48,7 +48,7 @@ def read(filename): return GerberParser().parse(filename) -def loads(data): +def loads(data, filename=None): """ Generate a GerberFile object from rs274x data in memory Parameters @@ -56,12 +56,15 @@ def loads(data): data : string string containing gerber file contents + filename : string, optional + string containing the filename of the data source + Returns ------- file : :class:`gerber.rs274x.GerberFile` A GerberFile created from the specified file. """ - return GerberParser().parse_raw(data) + return GerberParser().parse_raw(data, filename) class GerberFile(CamFile): diff --git a/gerber/tests/test_ipc356.py b/gerber/tests/test_ipc356.py index 45bb01b..4710633 100644 --- a/gerber/tests/test_ipc356.py +++ b/gerber/tests/test_ipc356.py @@ -14,7 +14,7 @@ IPC_D_356_FILE = os.path.join(os.path.dirname(__file__), def test_read(): ipcfile = read(IPC_D_356_FILE) - assert(isinstance(ipcfile, IPC_D_356)) + assert(isinstance(ipcfile, IPCNetlist)) def test_parser(): diff --git a/gerber/tests/test_layers.py b/gerber/tests/test_layers.py index 3f2bcfc..7e36dc2 100644 --- a/gerber/tests/test_layers.py +++ b/gerber/tests/test_layers.py @@ -1,11 +1,33 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -# Author: Hamilton Kibbe +# copyright 2016 Hamilton Kibbe +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os from .tests import * -from ..layers import guess_layer_class, hints +from ..layers import * +from ..common import read +NCDRILL_FILE = os.path.join(os.path.dirname(__file__), + 'resources/ncdrill.DRD') +NETLIST_FILE = os.path.join(os.path.dirname(__file__), + 'resources/ipc-d-356.ipc') +COPPER_FILE = os.path.join(os.path.dirname(__file__), + 'resources/top_copper.GTL') def test_guess_layer_class(): """ Test layer type inferred correctly from filename @@ -30,4 +52,51 @@ def test_guess_layer_class(): def test_sort_layers(): """ Test layer ordering """ - pass + layers = [ + PCBLayer(layer_class='drawing'), + PCBLayer(layer_class='drill'), + PCBLayer(layer_class='bottompaste'), + PCBLayer(layer_class='bottomsilk'), + PCBLayer(layer_class='bottommask'), + PCBLayer(layer_class='bottom'), + PCBLayer(layer_class='internal'), + PCBLayer(layer_class='top'), + PCBLayer(layer_class='topmask'), + PCBLayer(layer_class='topsilk'), + PCBLayer(layer_class='toppaste'), + PCBLayer(layer_class='outline'), + ] + + layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top', + 'internal', 'bottom', 'bottommask', 'bottomsilk', + 'bottompaste', 'drill', 'drawing'] + bottom_order = list(reversed(layer_order[:10])) + layer_order[10:] + assert_equal([l.layer_class for l in sort_layers(layers)], layer_order) + assert_equal([l.layer_class for l in sort_layers(layers, from_top=False)], + bottom_order) + + +def test_PCBLayer_from_file(): + layer = PCBLayer.from_cam(read(COPPER_FILE)) + assert_true(isinstance(layer, PCBLayer)) + layer = PCBLayer.from_cam(read(NCDRILL_FILE)) + assert_true(isinstance(layer, DrillLayer)) + layer = PCBLayer.from_cam(read(NETLIST_FILE)) + assert_true(isinstance(layer, PCBLayer)) + assert_equal(layer.layer_class, 'ipc_netlist') + + +def test_PCBLayer_bounds(): + source = read(COPPER_FILE) + layer = PCBLayer.from_cam(source) + assert_equal(source.bounds, layer.bounds) + + +def test_DrillLayer_from_cam(): + no_exceptions = True + try: + layer = DrillLayer.from_cam(read(NCDRILL_FILE)) + assert_true(isinstance(layer, DrillLayer)) + except: + no_exceptions = False + assert_true(no_exceptions) diff --git a/gerber/utils.py b/gerber/utils.py index e3eda1d..bee8a91 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -291,9 +291,9 @@ def rotate_point(point, angle, center=(0.0, 0.0)): `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) + x_delta, y_delta = tuple(map(sub, point, center)) + x = center[0] + (cos(angle) * x_delta) - (sin(angle) * y_delta) + y = center[1] + (sin(angle) * x_delta) - (cos(angle) * y_delta) return (x, y) -- cgit From d274b0823dbc672682c52c5cd649916aedf8f7e4 Mon Sep 17 00:00:00 2001 From: Robert Kirberich Date: Thu, 10 Mar 2016 20:51:53 +0000 Subject: Make sure apertures get a unit --- gerber/rs274x.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 76e5101..911b8ae 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -448,6 +448,7 @@ class GerberParser(object): else: aperture = self.macros[shape].build(modifiers) + aperture.units = self.settings.units self.apertures[d] = aperture def _evaluate_mode(self, stmt): -- cgit From 7c7a2e03d99714ce9e5fd6fa35b5577cdac1e67b Mon Sep 17 00:00:00 2001 From: Ben FrantzDale Date: Tue, 22 Mar 2016 08:13:30 -0400 Subject: Add install instructions. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index cb73b30..0d93550 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ Rendering Examples: Source code for this example can be found [here](examples/cairo_example.py). +Install from source: +``` +$ git clone https://github.com/curtacircuitos/pcb-tools.git +$ cd pcb-tools +$ make +$ python setup.py install +``` + Documentation: -------------- [PCB Tools Documentation](http://pcb-tools.readthedocs.org/en/latest/) -- cgit From 57c9dab99de34fd7ed77f75040d96ada25c41972 Mon Sep 17 00:00:00 2001 From: Ben FrantzDale Date: Tue, 22 Mar 2016 13:02:34 -0400 Subject: Remove "make" line. Apparently it's not necessary. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0d93550..5e46b02 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Install from source: ``` $ git clone https://github.com/curtacircuitos/pcb-tools.git $ cd pcb-tools -$ make $ python setup.py install ``` -- cgit From 0fedaedb6ebb8cc6abfc218d224a3ab69bb71b56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 29 Sep 2016 19:43:28 -0400 Subject: Add more layer hints as seen in the wild --- gerber/layers.py | 20 ++++++++++---------- gerber/render/theme.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gerber/layers.py b/gerber/layers.py index 93f0e36..c9e451a 100644 --- a/gerber/layers.py +++ b/gerber/layers.py @@ -29,46 +29,46 @@ Hint = namedtuple('Hint', 'layer ext name') hints = [ Hint(layer='top', ext=['gtl', 'cmp', 'top', ], - name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ] + name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', 'F.Cu', ] ), Hint(layer='bottom', ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ], - name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ] + name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', 'B.Cu', ] ), Hint(layer='internal', ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', ], name=['art', 'internal', 'pgp', 'pwr', 'gp1', 'gp2', 'gp3', 'gp4', - 'gt5', 'gp6', 'gnd', 'ground', ] + 'gt5', 'gp6', 'gnd', 'ground', 'In1.Cu', 'In2.Cu', 'In3.Cu', 'In4.Cu'] ), Hint(layer='topsilk', ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ], - name=['sst01', 'topsilk', 'silk', 'slk', 'sst', ] + name=['sst01', 'topsilk', 'silk', 'slk', 'sst', 'F.SilkS'] ), Hint(layer='bottomsilk', - ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ], + ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', 'B.SilkS'], name=['bsilk', 'ssb', 'botsilk', ] ), Hint(layer='topmask', ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ], name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', - 'mst', ] + 'mst', 'F.Mask',] ), Hint(layer='bottommask', ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ], - name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ] + name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', 'B.Mask',] ), Hint(layer='toppaste', ext=['gtp', 'tm', 'toppaste', ], - name=['sp01', 'toppaste', 'pst'] + name=['sp01', 'toppaste', 'pst', 'F.Paste'] ), Hint(layer='bottompaste', ext=['gbp', 'bm', 'bottompaste', ], - name=['sp02', 'botpaste', 'psb'] + name=['sp02', 'botpaste', 'psb', 'B.Paste', ] ), Hint(layer='outline', ext=['gko', 'outline', ], - name=['BDR', 'border', 'out', ] + name=['BDR', 'border', 'out', 'Edge.Cuts', ] ), Hint(layer='ipc_netlist', ext=['ipc'], diff --git a/gerber/render/theme.py b/gerber/render/theme.py index d382a8d..2887216 100644 --- a/gerber/render/theme.py +++ b/gerber/render/theme.py @@ -53,7 +53,7 @@ class Theme(object): return getattr(self, key) def get(self, key, noneval=None): - val = getattr(self, key) + val = getattr(self, key, None) return val if val is not None else noneval -- cgit From 22e668c75f24174d2090443ed98e804b3737bd84 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 5 Nov 2016 18:30:21 -0400 Subject: Fix tests --- gerber/layers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gerber/layers.py b/gerber/layers.py index c9e451a..212695a 100644 --- a/gerber/layers.py +++ b/gerber/layers.py @@ -46,8 +46,8 @@ hints = [ name=['sst01', 'topsilk', 'silk', 'slk', 'sst', 'F.SilkS'] ), Hint(layer='bottomsilk', - ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', 'B.SilkS'], - name=['bsilk', 'ssb', 'botsilk', ] + ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk',], + name=['bsilk', 'ssb', 'botsilk', 'B.SilkS'] ), Hint(layer='topmask', ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ], -- cgit