From 6d2db67e6d0973ce26ce3a6700ca44295f73fea7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 18 Oct 2014 01:44:51 -0400 Subject: Refactor rendering --- gerber/cam.py | 25 ++- gerber/excellon.py | 38 ++-- gerber/gerber_statements.py | 3 +- gerber/layer_names.py | 51 ----- gerber/layers.py | 54 +++++ gerber/primitives.py | 67 ++++-- gerber/render/render.py | 418 ++++---------------------------------- gerber/render/svgwrite_backend.py | 305 +++++++++++---------------- gerber/rs274x.py | 189 +++++++++++++---- 9 files changed, 454 insertions(+), 696 deletions(-) delete mode 100644 gerber/layer_names.py create mode 100644 gerber/layers.py (limited to 'gerber') diff --git a/gerber/cam.py b/gerber/cam.py index e7a49d1..4c19588 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -70,6 +70,9 @@ class CamFile(object): settings : FileSettings The current file configuration. + primitives : iterable + List of primitives in the file. + filename : string Name of the file that this CamFile represents. @@ -95,8 +98,8 @@ class CamFile(object): decimal digits) """ - def __init__(self, statements=None, settings=None, filename=None, - layer_name=None): + def __init__(self, statements=None, settings=None, primitives=None, + filename=None, layer_name=None): if settings is not None: self.notation = settings['notation'] self.units = settings['units'] @@ -108,6 +111,7 @@ class CamFile(object): self.zero_suppression = 'trailing' self.format = (2, 5) self.statements = statements if statements is not None else [] + self.primitives = primitives self.filename = filename self.layer_name = layer_name @@ -122,3 +126,20 @@ class CamFile(object): """ return FileSettings(self.notation, self.units, self.zero_suppression, self.format) + + def render(self, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, save the rendered image to `filename` + """ + ctx.set_bounds(self.bounds) + for p in self.primitives: + ctx.render(p) + if filename is not None: + ctx.dump(filename) diff --git a/gerber/excellon.py b/gerber/excellon.py index 13aacc6..ca2f7c8 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -25,7 +25,7 @@ This module provides Excellon file classes and parsing utilities from .excellon_statements import * from .cam import CamFile, FileSettings - +from .primitives import Drill import math def read(filename): @@ -74,30 +74,33 @@ class ExcellonFile(CamFile): """ def __init__(self, statements, tools, hits, settings, filename=None): - super(ExcellonFile, self).__init__(statements, settings, filename) + super(ExcellonFile, self).__init__(statements=statements, + settings=settings, + filename=filename) self.tools = tools self.hits = hits + self.primitives = [Drill(position, tool.diameter) + for tool, position in self.hits] + + @property + def bounds(self): + xmin = ymin = 100000000000 + xmax = ymax = -100000000000 + for tool, position in self.hits: + radius = tool.diameter / 2. + x = position[0] + y = position[1] + xmin = min(x - radius, xmin) + xmax = max(x + radius, xmax) + ymin = min(y - radius, ymin) + ymax = max(y + radius, ymax) + return ((xmin, xmax), (ymin, ymax)) def report(self): """ Print drill report """ pass - def render(self, ctx, filename=None): - """ Generate image of file - - Parameters - ---------- - ctx : :class:`gerber.render.GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - for tool, pos in self.hits: - ctx.drill(pos[0], pos[1], tool.diameter) - if filename is not None: - ctx.dump(filename) def write(self, filename): with open(filename, 'w') as f: @@ -105,6 +108,7 @@ class ExcellonFile(CamFile): f.write(statement.to_excellon() + '\n') + class ExcellonParser(object): """ Excellon File Parser diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 218074f..6f7b73d 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -12,7 +12,8 @@ from .utils import parse_gerber_value, write_gerber_value, decimal_string __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', 'LPParamStmt', 'ADParamStmt', 'AMParamStmt', 'INParamStmt', 'LNParamStmt', 'CoordStmt', 'ApertureStmt', 'CommentStmt', - 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt'] + 'EofStmt', 'QuadrantModeStmt', 'RegionModeStmt', 'UnknownStmt', + 'ParamStmt'] class Statement(object): diff --git a/gerber/layer_names.py b/gerber/layer_names.py deleted file mode 100644 index 372c40d..0000000 --- a/gerber/layer_names.py +++ /dev/null @@ -1,51 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 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. - -top_copper_ext = ['gtl', 'cmp', 'top', ] -top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ] - -bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ] -bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ] - -internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1', - 'g2', 'g3', 'g4', 'g5', 'g6', ] -internal_layer_name = ['art', 'internal'] -power_plane_name = ['pgp', 'pwr', ] -ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd', - 'ground', ] - -top_silk_ext = ['gto', 'ts', 'skt', ] -top_silk_name = ['sst01', 'topsilk, 'silk', 'slk', 'sst', ] - -bottom_silk_ext = ['gbo, 'bs', 'skb', ] -bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ] - -top_mask_ext = ['gts', 'tmk', 'smt', 'tr', ] -top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', - 'mst', ] - -bottom_mask_ext = ['gbs', bmk', 'smb', 'br', ] -bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ] - -top_paste_ext = ['gtp', 'tm'] -top_paste_name = ['sp01', 'toppaste', 'pst'] - -bottom_paste_ext = ['gbp', 'bm'] -bottom_paste_name = ['sp02', 'botpaste', 'psb'] - -board_outline_ext = ['gko'] -board_outline_name = ['BDR', 'border', 'out', ] diff --git a/gerber/layers.py b/gerber/layers.py new file mode 100644 index 0000000..b10cf16 --- /dev/null +++ b/gerber/layers.py @@ -0,0 +1,54 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 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. + +top_copper_ext = ['gtl', 'cmp', 'top', ] +top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ] + +bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ] +bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ] + +internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1', + 'g2', 'g3', 'g4', 'g5', 'g6', ] +internal_layer_name = ['art', 'internal'] + +power_plane_name = ['pgp', 'pwr', ] +ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd', + 'ground', ] + +top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ] +top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ] + +bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ] +bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ] + +top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ] +top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask', + 'mst', ] + +bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ] +bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ] + +top_paste_ext = ['gtp', 'tm'] +top_paste_name = ['sp01', 'toppaste', 'pst'] + +bottom_paste_ext = ['gbp', 'bm'] +bottom_paste_name = ['sp02', 'botpaste', 'psb'] + +board_outline_ext = ['gko'] +board_outline_name = ['BDR', 'border', 'out', ] + + diff --git a/gerber/primitives.py b/gerber/primitives.py index 366c397..670b758 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -19,11 +19,15 @@ from operator import sub class Primitive(object): + + def __init__(self, level_polarity='dark'): + self.level_polarity = level_polarity + def bounding_box(self): """ Calculate bounding box will be helpful for sweep & prune during DRC clearance checks. - + Return ((min x, max x), (min y, max y)) """ pass @@ -32,16 +36,19 @@ class Primitive(object): class Line(Primitive): """ """ - def __init__(self, start, end, width): + def __init__(self, start, end, width, level_polarity='dark'): + super(Line, self).__init__(level_polarity) self.start = start self.end = end self.width = width - + @property def angle(self): - dx, dy = tuple(map(sub, end, start)) - angle = degrees(math.tan(dy/dx)) + delta_x, delta_y = tuple(map(sub, end, start)) + angle = degrees(math.tan(delta_y/delta_x)) + return angle + @property def bounding_box(self): width_2 = self.width / 2. min_x = min(self.start[0], self.end[0]) - width_2 @@ -54,7 +61,8 @@ class Line(Primitive): class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, width): + def __init__(self, start, end, center, direction, width, level_polarity='dark'): + super(Arc, self).__init__(level_polarity) self.start = start self.end = end self.center = center @@ -71,17 +79,23 @@ class Arc(Primitive): dy, dx = map(sub, self.end, self.center) return math.atan2(dy, dx) + @property def bounding_box(self): pass class Circle(Primitive): """ """ - def __init__(self, position, diameter): + def __init__(self, position, diameter, level_polarity='dark'): + super(Circle, self).__init__(level_polarity) self.position = position self.diameter = diameter - self.radius = diameter / 2. + @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 @@ -89,11 +103,16 @@ class Circle(Primitive): max_y = self.position[1] + self.radius return ((min_x, max_x), (min_y, max_y)) + @property + def stroke_width(self): + return self.diameter + class Rectangle(Primitive): """ """ - def __init__(self, position, width, height): + def __init__(self, position, width, height, level_polarity='dark'): + super(Rectangle, self).__init__(level_polarity) self.position = position self.width = width self.height = height @@ -108,6 +127,7 @@ class Rectangle(Primitive): return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) + @property def bounding_box(self): min_x = self.lower_left[0] max_x = self.upper_right[0] @@ -115,11 +135,16 @@ class Rectangle(Primitive): max_y = self.upper_right[1] return ((min_x, max_x), (min_y, max_y)) + @property + def stroke_width(self): + return max((self.width, self.height)) + class Obround(Primitive): """ """ - def __init__(self, position, width, height) + def __init__(self, position, width, height, level_polarity='dark'): + super(Obround, self).__init__(level_polarity) self.position = position self.width = width self.height = height @@ -138,6 +163,7 @@ class Obround(Primitive): return (self.position[0] + (self.width / 2.), self.position[1] + (self.height / 2.)) + @property def bounding_box(self): min_x = self.lower_left[0] max_x = self.upper_right[0] @@ -149,11 +175,13 @@ class Obround(Primitive): class Polygon(Primitive): """ """ - def __init__(self, position, sides, radius): + def __init__(self, position, sides, radius, level_polarity='dark'): + super(Polygon, self).__init__(level_polarity) self.position = position self.sides = sides self.radius = radius - + + @property def bounding_box(self): min_x = self.position[0] - self.radius max_x = self.position[0] + self.radius @@ -165,9 +193,11 @@ class Polygon(Primitive): class Region(Primitive): """ """ - def __init__(self, points): + def __init__(self, points, level_polarity='dark'): + super(Region, self).__init__(level_polarity) self.points = points - + + @property def bounding_box(self): x_list, y_list = zip(*self.points) min_x = min(x_list) @@ -181,10 +211,15 @@ class Drill(Primitive): """ """ def __init__(self, position, diameter): + super(Drill, self).__init__('dark') self.position = position self.diameter = diameter - self.radius = diameter / 2. - + + @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 diff --git a/gerber/render/render.py b/gerber/render/render.py index f7e4485..f5c58d8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -28,6 +28,7 @@ from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, QuadrantModeStmt, ) +from ..primitives import * class GerberContext(object): """ Gerber rendering context base class @@ -39,40 +40,8 @@ class GerberContext(object): Attributes ---------- - settings : FileSettings (dict-like) - Gerber file settings - - x : float - X-coordinate of the "photoplotter" head. - - y : float - Y-coordinate of the "photoplotter" head - - aperture : int - The aperture that is currently in use - - interpolation : str - Current interpolation mode. may be 'linear' or 'arc' - - direction : string - Current arc direction. May be either 'clockwise' or 'counterclockwise' - - image_polarity : string - Current image polarity setting. May be 'positive' or 'negative' - - level_polarity : string - Level polarity. May be 'dark' or 'clear'. Dark polarity indicates the - existance of copper/silkscreen/etc. in the exposed area, whereas clear - polarity indicates material should be removed from the exposed area. - - region_mode : string - Region mode. May be 'on' or 'off'. When region mode is set to 'on' the - following "contours" define the outline of a region. When region mode - is subsequently turned 'off', the defined area is filled. - - quadrant_mode : string - Quadrant mode. May be 'single-quadrant' or 'multi-quadrant'. Defines - how arcs are specified. + units : string + Measurement units color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -87,73 +56,14 @@ class GerberContext(object): alpha : float Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ - def __init__(self): - self.settings = {} - self.x = 0 - self.y = 0 - - self.aperture = 0 - self.interpolation = 'linear' - self.direction = 'clockwise' - self.image_polarity = 'positive' - self.level_polarity = 'dark' - self.region_mode = 'off' - self.quadrant_mode = 'multi-quadrant' - self.step_and_repeat = (1, 1, 0, 0) + def __init__(self, units='inch'): + self.units = units self.color = (0.7215, 0.451, 0.200) self.drill_color = (0.25, 0.25, 0.25) self.background_color = (0.0, 0.0, 0.0) self.alpha = 1.0 - - def set_format(self, settings): - """ Set source file format. - - Parameters - ---------- - settings : FileSettings instance or dict-like - Gerber file settings used in source file. - """ - self.settings = settings - - def set_coord_format(self, zero_suppression, decimal_format, notation): - """ Set coordinate format used in source gerber file - Parameters - ---------- - zero_suppression : string - Zero suppression mode. may be 'leading' or 'trailling' - - decimal_format : tuple (, ) - Decimal precision format specified as (integer digits, decimal digits) - - notation : string - Notation mode. 'absolute' or 'incremental' - """ - if zero_suppression not in ('leading', 'trailling'): - raise ValueError('Zero suppression must be "leading" or "trailing"') - self.settings['zero_suppression'] = zero_suppression - self.settings['format'] = decimal_format - self.settings['notation'] = notation - - def set_coord_notation(self, notation): - """ Set context notation mode - - Parameters - ---------- - notation : string - Notation mode. may be 'absolute' or 'incremental' - - Raises - ------ - ValueError - If `notation` is not either "absolute" or "incremental" - - """ - if notation not in ('absolute', 'incremental'): - raise ValueError('Notation may be "absolute" or "incremental"') - self.settings['notation'] = notation - - def set_coord_unit(self, unit): + def set_units(self, units): """ Set context measurement units Parameters @@ -166,70 +76,9 @@ class GerberContext(object): ValueError If `unit` is not 'inch' or 'metric' """ - if unit not in ('inch', 'metric'): - raise ValueError('Unit may be "inch" or "metric"') - self.settings['units'] = unit - - def set_image_polarity(self, polarity): - """ Set context image polarity - - Parameters - ---------- - polarity : string - Image polarity. May be "positive" or "negative" - - Raises - ------ - ValueError - If polarity is not 'positive' or 'negative' - """ - if polarity not in ('positive', 'negative'): - raise ValueError('Polarity may be "positive" or "negative"') - self.image_polarity = polarity - - def set_level_polarity(self, polarity): - """ Set context level polarity - - Parameters - ---------- - polarity : string - Level polarity. May be "dark" or "clear" - - Raises - ------ - ValueError - If polarity is not 'dark' or 'clear' - """ - if polarity not in ('dark', 'clear'): - raise ValueError('Polarity may be "dark" or "clear"') - self.level_polarity = polarity - - def set_interpolation(self, interpolation): - """ Set arc interpolation mode - - Parameters - ---------- - interpolation : string - Interpolation mode. May be 'linear' or 'arc' - - Raises - ------ - ValueError - If `interpolation` is not 'linear' or 'arc' - """ - if interpolation not in ('linear', 'arc'): - raise ValueError('Interpolation may be "linear" or "arc"') - self.interpolation = interpolation - - def set_aperture(self, d): - """ Set active aperture - - Parameters - ---------- - aperture : int - Aperture number to activate. - """ - self.aperture = d + if units not in ('inch', 'metric'): + raise ValueError('Units may be "inch" or "metric"') + self.units = units def set_color(self, color): """ Set rendering color. @@ -277,238 +126,49 @@ class GerberContext(object): """ self.alpha = alpha - def resolve(self, x, y): - """ Resolve missing x or y coordinates in a coordinate command. - - Replace missing x or y values with the current x or y position. This - is the default method for handling coordinate pairs pulled from gerber - file statments, as a move/line/arc involving a change in only one axis - will drop the redundant axis coordinate to reduce file size. - - Parameters - ---------- - x : float - X-coordinate. If `None`, will be replaced with current - "photoplotter" head x-coordinate - - y : float - Y-coordinate. If `None`, will be replaced with current - "photoplotter" head y-coordinate - - Returns - ------- - coordinates : tuple (, ) - Coordinates in absolute notation - """ - x = x if x is not None else self.x - y = y if y is not None else self.y - return x, y - - def define_aperture(self, d, shape, modifiers): - pass - - def move(self, x, y, resolve=True): - """ Lights-off move. - - Move the "photoplotter" head to (x, y) without drawing a line. If x or - y is `None`, remain at the same point in that axis. - - Parameters - ----------- - x : float - X-coordinate to move to. If x is `None`, do not move in the X - direction - - y : float - Y-coordinate to move to. if y is `None`, do not move in the Y - direction - - resolve : bool - If resolve is `True` the context will replace missing x or y - coordinates with the current plotter head position. This is the - default behavior. - """ - if resolve: - self.x, self.y = self.resolve(x, y) + def render(self, primitive): + color = (self.color if primitive.level_polarity == 'dark' + else self.background_color) + if isinstance(primitive, Line): + self._render_line(primitive, color) + elif isinstance(primitive, Arc): + self._render_arc(primitive, color) + elif isinstance(primitive, Region): + self._render_region(primitive, color) + elif isinstance(primitive, Circle): + self._render_circle(primitive, color) + elif isinstance(primitive, Rectangle): + self._render_rectangle(primitive, color) + elif isinstance(primitive, Obround): + self._render_obround(primitive, color) + elif isinstance(primitive, Polygon): + self._render_polygon(Polygon, color) + elif isinstance(primitive, Drill): + self._render_drill(primitive, self.drill_color) else: - self.x, self.y = x, y - - def stroke(self, x, y, i, j): - """ Lights-on move. (draws a line or arc) - - The stroke method is called when a Lights-on move statement is - encountered. This will call the `line` or `arc` method as necessary - based on the move statement's parameters. The `stroke` method should - be overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. + return - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_line(self, primitive, color): pass - def line(self, x, y): - """ Draw a line - - Draws a line from the current position to (x, y) using the currently - selected aperture. The `line` method should be overridden in - `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - """ + def _render_arc(self, primitive, color): pass - def arc(self, x, y, i, j): - """ Draw an arc - - Draw an arc from the current position to (x, y) using the currently - selected aperture. `i` and `j` specify the offset from the starting - position to the center of the arc.The `arc` method should be - overridden in `GerberContext` subclasses. - - Parameters - ---------- - x : float - X coordinate of target position - - y : float - Y coordinate of target position - - i : float - Offset in X-direction from current position of arc center. - - j : float - Offset in Y-direction from current position of arc center. - """ + def _render_region(self, primitive, color): pass - def flash(self, x, y): - """ Flash the current aperture - - Draw a filled shape defined by the currently selected aperture. - - Parameters - ---------- - x : float - X coordinate of the position at which to flash - - y : float - Y coordinate of the position at which to flash - """ + def _render_circle(self, primitive, color): pass - def drill(self, x, y, diameter): - """ Draw a drill hit - - Draw a filled circle representing a drill hit at the specified - position and with the specified diameter. - - Parameters - ---------- - x : float - X coordinate of the drill hit - - y : float - Y coordinate of the drill hit - - diameter : float - Finished hole diameter to draw. - """ + def _render_rectangle(self, primitive, color): pass - def region_contour(self, x, y): - pass - - def fill_region(self): + def _render_obround(self, primitive, color): pass - - def evaluate(self, stmt): - """ Evaluate Gerber statement and update image accordingly. - - This method is called once for each statement in a Gerber/Excellon - file when the file's `render` method is called. The evaluate method - should forward the statement on to the relevant handling method based - on the statement type. - - Parameters - ---------- - statement : Statement - Gerber/Excellon statement to evaluate. - """ - if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): - return - - elif isinstance(stmt, ParamStmt): - self._evaluate_param(stmt) - - elif isinstance(stmt, CoordStmt): - self._evaluate_coord(stmt) - - elif isinstance(stmt, ApertureStmt): - self._evaluate_aperture(stmt) - - elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): - self._evaluate_mode(stmt) + def _render_polygon(self, primitive, color): + pass - else: - raise Exception("Invalid statement to evaluate") - - def _evaluate_mode(self, stmt): - if stmt.type == 'RegionMode': - if self.region_mode == 'on' and stmt.mode == 'off': - self.fill_region() - self.region_mode = stmt.mode - elif stmt.type == 'QuadrantMode': - self.quadrant_mode = stmt.mode - - def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.set_coord_format(stmt.zero_suppression, stmt.format, - stmt.notation) - self.set_coord_notation(stmt.notation) - elif stmt.param == "MO": - self.set_coord_unit(stmt.mode) - elif stmt.param == "IP": - self.set_image_polarity(stmt.ip) - elif stmt.param == "LP": - self.set_level_polarity(stmt.lp) - elif stmt.param == "AD": - self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) - - def _evaluate_coord(self, stmt): - if stmt.function in ("G01", "G1"): - self.set_interpolation('linear') - elif stmt.function in ('G02', 'G2', 'G03', 'G3'): - self.set_interpolation('arc') - self.direction = ('clockwise' if stmt.function in ('G02', 'G2') - else 'counterclockwise') - if stmt.op == "D01": - if self.region_mode == 'on': - self.region_contour(stmt.x, stmt.y) - else: - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) - elif stmt.op == "D02": - self.move(stmt.x, stmt.y) - elif stmt.op == "D03": - self.flash(stmt.x, stmt.y) - - def _evaluate_aperture(self, stmt): - self.set_aperture(stmt.d) + def _render_drill(self, primitive, color): + pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 15d7bd3..d9456a5 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -17,214 +17,139 @@ # limitations under the License. from .render import GerberContext -from .apertures import Circle, Rect, Obround, Polygon +from operator import mul import svgwrite SCALE = 300 -def convert_color(color): +def svg_color(color): color = tuple([int(ch * 255) for ch in color]) return 'rgb(%d, %d, %d)' % color -class SvgCircle(Circle): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, - stroke_width=SCALE * self.diameter, - stroke_linecap="round") - aline.stroke(opacity=alpha) - return aline - - def arc(self, ctx, x, y, i, j, direction, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (self.diameter / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - - -class SvgRect(Rect): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - aline = ctx.dwg.line(start=(ctx.x * SCALE, -ctx.y * SCALE), - end=(x * SCALE, -y * SCALE), - stroke=color, stroke_width=2, - stroke_linecap="butt") - aline.stroke(opacity=alpha) - return aline - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - rectangle = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2)), - -SCALE * (y + (ysize / 2))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - rectangle.fill(opacity=alpha) - return [rectangle, ] - - -class SvgObround(Obround): - def line(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - pass - - def flash(self, ctx, x, y, color='rgb(184, 115, 51)', alpha=1.0): - xsize, ysize = self.size - - # horizontal obround - if xsize == ysize: - circle = ctx.dwg.circle(center=(x * SCALE, -y * SCALE), - r = SCALE * (x / 2.0), - fill=color) - circle.fill(opacity=alpha) - return [circle, ] - if xsize > ysize: - rectx = xsize - ysize - recty = ysize - lcircle = ctx.dwg.circle(center=((x - (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rcircle = ctx.dwg.circle(center=((x + (rectx / 2.0)) * SCALE, - -y * SCALE), - r = SCALE * (ysize / 2.0), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - rcircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, rcircle, rect, ] - - # Vertical obround - else: - rectx = xsize - recty = ysize - xsize - lcircle = ctx.dwg.circle(center=(x * SCALE, - (y - (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - ucircle = ctx.dwg.circle(center=(x * SCALE, - (y + (recty / 2.)) * -SCALE), - r = SCALE * (xsize / 2.), - fill=color) - - rect = ctx.dwg.rect(insert=(SCALE * (x - (xsize / 2.)), - -SCALE * (y + (ysize / 2.))), - size=(SCALE * xsize, SCALE * ysize), - fill=color) - lcircle.fill(opacity=alpha) - ucircle.fill(opacity=alpha) - rect.fill(opacity=alpha) - return [lcircle, ucircle, rect, ] - class GerberSvgContext(GerberContext): def __init__(self): GerberContext.__init__(self) - - self.apertures = {} + self.scale = (SCALE, -SCALE) self.dwg = svgwrite.Drawing() - self.dwg.transform = 'scale 1 -1' self.background = False - self.region_path = None + + def dump(self, filename): + self.dwg.saveas(filename) def set_bounds(self, bounds): xbounds, ybounds = bounds - size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) + size = (SCALE * (xbounds[1] - xbounds[0]), + SCALE * (ybounds[1] - ybounds[0])) if not self.background: - self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) - self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], - -SCALE * ybounds[1]), - size=size, fill=convert_color(self.background_color))) + vbox = '%f, %f, %f, %f' % (SCALE * xbounds[0], -SCALE * ybounds[1], + size[0], size[1]) + self.dwg = svgwrite.Drawing(viewBox=vbox) + rect = self.dwg.rect(insert=(SCALE * xbounds[0], + -SCALE * ybounds[1]), + size=size, + fill=svg_color(self.background_color)) + self.dwg.add(rect) self.background = True - def define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - aperture = SvgCircle(diameter=float(modifiers[0][0])) - elif shape == 'R': - aperture = SvgRect(size=modifiers[0][0:2]) - elif shape == 'O': - aperture = SvgObround(size=modifiers[0][0:2]) - self.apertures[d] = aperture - - def stroke(self, x, y, i, j): - super(GerberSvgContext, self).stroke(x, y, i, j) - - if self.interpolation == 'linear': - self.line(x, y) - elif self.interpolation == 'arc': - self.arc(x, y, i, j) - - def line(self, x, y): - super(GerberSvgContext, self).line(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - self.dwg.add(ap.line(self, x, y, color, alpha)) - self.move(x, y, resolve=False) - - def arc(self, x, y, i, j): - super(GerberSvgContext, self).arc(x, y, i, j) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - #self.dwg.add(ap.arc(self, x, y, i, j, self.direction, - # convert_color(self.color), self.alpha)) - self.move(x, y, resolve=False) - - def flash(self, x, y): - super(GerberSvgContext, self).flash(x, y) - x, y = self.resolve(x, y) - ap = self.apertures.get(self.aperture, None) - if ap is None: - return - - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - for shape in ap.flash(self, x, y, color, alpha): - self.dwg.add(shape) - self.move(x, y, resolve=False) - - def drill(self, x, y, diameter): - hit = self.dwg.circle(center=(x*SCALE, -y*SCALE), - r=SCALE*(diameter/2.0), - fill=convert_color(self.drill_color)) - #hit.fill(opacity=self.alpha) - self.dwg.add(hit) - - def region_contour(self, x, y): - super(GerberSvgContext, self).region_contour(x, y) - x, y = self.resolve(x, y) - color = (convert_color(self.color) if self.level_polarity == 'dark' - else convert_color(self.background_color)) - alpha = self.alpha if self.level_polarity == 'dark' else 1.0 - if self.region_path is None: - self.region_path = self.dwg.path(d = 'M %f, %f' % - (self.x*SCALE, -self.y*SCALE), - fill = color, stroke = 'none') - self.region_path.fill(opacity=alpha) - self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) - self.move(x, y, resolve=False) - - def fill_region(self): - self.dwg.add(self.region_path) - self.region_path = None + def _render_line(self, line, color): + start = map(mul, line.start, self.scale) + end = map(mul, line.end, self.scale) + aline = self.dwg.line(start=start, end=end, + stroke=svg_color(color), + stroke_width=SCALE * line.width, + stroke_linecap='round') + aline.stroke(opacity=self.alpha) + self.dwg.add(aline) + + def _render_region(self, region, color): + points = [tuple(map(mul, point, self.scale)) for point in region.points] + region_path = self.dwg.path(d='M %f, %f' % points[0], + fill=svg_color(color), + stroke='none') + region_path.fill(opacity=self.alpha) + for point in points[1:]: + region_path.push('L %f, %f' % point) + self.dwg.add(region_path) + + def _render_circle(self, circle, color): + center = map(mul, circle.position, self.scale) + acircle = self.dwg.circle(center=center, + r = SCALE * circle.radius, + fill=svg_color(color)) + acircle.fill(opacity=self.alpha) + self.dwg.add(acircle) + + def _render_rectangle(self, rectangle, color): + center = map(mul, rectangle.position, self.scale) + size = tuple(map(mul, (rectangle.width, rectangle.height), map(abs, self.scale))) + insert = center[0] - size[0] / 2., center[1] - size[1] / 2. + arect = self.dwg.rect(insert=insert, size=size, + fill=svg_color(color)) + arect.fill(opacity=self.alpha) + self.dwg.add(arect) + + def _render_obround(self, obround, color): + x, y = tuple(map(mul, obround.position, self.scale)) + xsize, ysize = tuple(map(mul, (obround.width, obround.height), + self.scale)) + xscale, yscale = self.scale + + # Corner case... + if xsize == ysize: + circle = self.dwg.circle(center=(x, y), + r = (xsize / 2.0), + fill=svg_color(color)) + circle.fill(opacity=self.alpha) + self.dwg.add(circle) + + # Horizontal obround + elif xsize > ysize: + rectx = xsize - ysize + recty = ysize + c1 = self.dwg.circle(center=(x - (rectx / 2.0), y), + r = (ysize / 2.0), + fill=svg_color(color)) + + c2 = self.dwg.circle(center=(x + (rectx / 2.0), y), + r = (ysize / 2.0), + fill=svg_color(color)) + + rect = self.dwg.rect(insert=(x, y), + size=(xsize, ysize), + fill=svg_color(color)) + c1.fill(opacity=self.alpha) + c2.fill(opacity=self.alpha) + rect.fill(opacity=self.alpha) + self.dwg.add(c1) + self.dwg.add(c2) + self.dwg.add(rect) - def dump(self, filename): - self.dwg.saveas(filename) + # Vertical obround + else: + rectx = xsize + recty = ysize - xsize + c1 = self.dwg.circle(center=(x, y - (recty / 2.)), + r = (xsize / 2.), + fill=svg_color(color)) + + c2 = self.dwg.circle(center=(x, y + (recty / 2.)), + r = (xsize / 2.), + fill=svg_color(color)) + + rect = self.dwg.rect(insert=(x, y), + size=(xsize, ysize), + fill=svg_color(color)) + c1.fill(opacity=self.alpha) + c2.fill(opacity=self.alpha) + rect.fill(opacity=self.alpha) + self.dwg.add(c1) + self.dwg.add(c2) + self.dwg.add(rect) + + def _render_drill(self, primitive, color): + center = map(mul, primitive.position, self.scale) + hit = self.dwg.circle(center=center, r=SCALE * primitive.radius, + fill=svg_color(color)) + self.dwg.add(hit) diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 4076f77..39693c9 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -19,14 +19,13 @@ """ -import re +import copy import json +import re from .gerber_statements import * +from .primitives import * from .cam import CamFile, FileSettings - - - def read(filename): """ Read data from filename and return a GerberFile @@ -72,8 +71,9 @@ class GerberFile(CamFile): `bounds` is stored as ((min x, max x), (min y, max y)) """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) + def __init__(self, statements, settings, primitives, filename=None): + super(GerberFile, self).__init__(statements, settings, primitives, filename) + @property def comments(self): @@ -111,22 +111,7 @@ class GerberFile(CamFile): for statement in self.statements: f.write(statement.to_gerber()) - def render(self, ctx, filename=None): - """ Generate image of layer. - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) class GerberParser(object): @@ -178,15 +163,31 @@ class GerberParser(object): def __init__(self): self.settings = FileSettings() self.statements = [] + self.primitives = [] + self.apertures = {} + self.current_region = None + self.x = 0 + self.y = 0 + + self.aperture = 0 + self.interpolation = 'linear' + self.direction = 'clockwise' + self.image_polarity = 'positive' + self.level_polarity = 'dark' + self.region_mode = 'off' + self.quadrant_mode = 'multi-quadrant' + self.step_and_repeat = (1, 1, 0, 0) + def parse(self, filename): fp = open(filename, "r") data = fp.readlines() for stmt in self._parse(data): + self.evaluate(stmt) self.statements.append(stmt) - return GerberFile(self.statements, self.settings, filename) + return GerberFile(self.statements, self.settings, self.primitives, filename) def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -218,7 +219,7 @@ class GerberParser(object): did_something = False # Region Mode - (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + (mode, r) = _match_one(self.REGION_MODE_STMT, line) if mode: yield RegionModeStmt.from_gerber(line) line = r @@ -226,7 +227,7 @@ class GerberParser(object): continue # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + (mode, r) = _match_one(self.QUAD_MODE_STMT, line) if mode: yield QuadrantModeStmt.from_gerber(line) line = r @@ -234,7 +235,7 @@ class GerberParser(object): continue # coord - (coord, r) = self._match_one(self.COORD_STMT, line) + (coord, r) = _match_one(self.COORD_STMT, line) if coord: yield CoordStmt.from_dict(coord, self.settings) line = r @@ -242,7 +243,7 @@ class GerberParser(object): continue # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) + (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) @@ -251,7 +252,7 @@ class GerberParser(object): continue # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) + (comment, r) = _match_one(self.COMMENT_STMT, line) if comment: yield CommentStmt(comment["comment"]) did_something = True @@ -259,7 +260,7 @@ class GerberParser(object): continue # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + (param, r) = _match_one_from_many(self.PARAM_STMT, line) if param: if param["param"] == "FS": stmt = FSParamStmt.from_dict(param) @@ -292,7 +293,7 @@ class GerberParser(object): continue # eof - (eof, r) = self._match_one(self.EOF_STMT, line) + (eof, r) = _match_one(self.EOF_STMT, line) if eof: yield EofStmt() did_something = True @@ -311,17 +312,125 @@ class GerberParser(object): yield UnknownStmt(line) oldline = line - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) + def evaluate(self, stmt): + """ Evaluate Gerber statement and update image accordingly. - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) + This method is called once for each statement in the file as it + is parsed. + Parameters + ---------- + statement : Statement + Gerber/Excellon statement to evaluate. + + """ + if isinstance(stmt, (CommentStmt, UnknownStmt, EofStmt)): + return + + elif isinstance(stmt, ParamStmt): + self._evaluate_param(stmt) + + elif isinstance(stmt, CoordStmt): + self._evaluate_coord(stmt) + + elif isinstance(stmt, ApertureStmt): + self._evaluate_aperture(stmt) + + elif isinstance(stmt, (RegionModeStmt, QuadrantModeStmt)): + self._evaluate_mode(stmt) + + else: + raise Exception("Invalid statement to evaluate") + + + def _define_aperture(self, d, shape, modifiers): + aperture = None + if shape == 'C': + diameter = float(modifiers[0][0]) + aperture = Circle(position=None, diameter=diameter) + elif shape == 'R': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Rectangle(position=None, width=width, height=height) + elif shape == 'O': + width = float(modifiers[0][0]) + height = float(modifiers[0][1]) + aperture = Obround(position=None, width=width, height=height) + self.apertures[d] = aperture + + 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, self.level_polarity)) + self.current_region = None + self.region_mode = stmt.mode + elif stmt.type == 'QuadrantMode': + self.quadrant_mode = stmt.mode + + def _evaluate_param(self, stmt): + if stmt.param == "FS": + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + elif stmt.param == "MO": + self.settings.units = stmt.mode + elif stmt.param == "IP": + self.image_polarity = stmt.ip + elif stmt.param == "LP": + self.level_polarity = stmt.lp + elif stmt.param == "AD": + self._define_aperture(stmt.d, stmt.shape, stmt.modifiers) + + def _evaluate_coord(self, stmt): + x = self.x if stmt.x is None else stmt.x + y = self.y if stmt.y is None else stmt.y + if stmt.function in ("G01", "G1"): + 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') + if stmt.op == "D01": + if self.region_mode == 'on': + if self.current_region is None: + self.current_region = [(self.x, self.y), ] + self.current_region.append((x, y,)) + else: + start = (self.x, self.y) + end = (x, y) + width = self.apertures[self.aperture].stroke_width + if self.interpolation == 'linear': + self.primitives.append(Line(start, end, width, self.level_polarity)) + else: + center = (start[0] + stmt.i, start[1] + stmt.j) + self.primitives.append(Arc(start, end, center, self.direction, width, self.level_polarity)) + + elif stmt.op == "D02": + pass + + elif stmt.op == "D03": + primitive = copy.deepcopy(self.apertures[self.aperture]) + primitive.position = (x, y) + primitive.level_polarity = self.level_polarity + self.primitives.append(primitive) + self.x, self.y = x, y + + def _evaluate_aperture(self, stmt): + self.aperture = stmt.d + + +def _match_one(expr, data): + match = expr.match(data) + if match is None: return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + +def _match_one_from_many(exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit