From 4eb0e063bcd34c21b737023aa6ed5baed80658d1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 13 Jun 2021 15:00:17 +0200 Subject: Repo re-org, make gerberex tests run --- gerber/render/__init__.py | 31 -- gerber/render/cairo_backend.py | 616 -------------------------------------- gerber/render/excellon_backend.py | 188 ------------ gerber/render/render.py | 246 --------------- gerber/render/rs274x_backend.py | 510 ------------------------------- gerber/render/theme.py | 112 ------- 6 files changed, 1703 deletions(-) delete mode 100644 gerber/render/__init__.py delete mode 100644 gerber/render/cairo_backend.py delete mode 100644 gerber/render/excellon_backend.py delete mode 100644 gerber/render/render.py delete mode 100644 gerber/render/rs274x_backend.py delete mode 100644 gerber/render/theme.py (limited to 'gerber/render') diff --git a/gerber/render/__init__.py b/gerber/render/__init__.py deleted file mode 100644 index c7dbdd5..0000000 --- a/gerber/render/__init__.py +++ /dev/null @@ -1,31 +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. -""" -gerber.render -============ -**Gerber Renderers** - -This module provides contexts for rendering images of gerber layers. Currently -SVG is the only supported format. -""" - -from .render import RenderSettings -from .cairo_backend import GerberCairoContext - -available_renderers = { - 'cairo': GerberCairoContext, -} diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py deleted file mode 100644 index e1d1408..0000000 --- a/gerber/render/cairo_backend.py +++ /dev/null @@ -1,616 +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. - -try: - import cairo -except ImportError: - import cairocffi as cairo - -from operator import mul -import tempfile -import copy -import os - -from .render import GerberContext, RenderSettings -from .theme import THEMES -from ..primitives import * -from ..utils import rotate_point - -from io import BytesIO - - -class GerberCairoContext(GerberContext): - - def __init__(self, scale=300): - 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.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): - 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 (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 = 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 - self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, - x0=-self.origin_in_pixels[0], - y0=self.size_in_pixels[1]) - 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.output_ctx = cairo.Context(self.surface) - - def render_layer(self, layer, filename=None, settings=None, bgsettings=None, - verbose=False, bounds=None): - 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() - if bounds is not None: - self.set_bounds(bounds) - else: - 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, max_width=800, max_height=600): - """ Render a set of layers - """ - # Calculate scale parameter - x_range = [10000, -10000] - y_range = [10000, -10000] - for layer in layers: - bounds = layer.bounds - if bounds is not None: - layer_x, layer_y = bounds - x_range[0] = min(x_range[0], layer_x[0]) - x_range[1] = max(x_range[1], layer_x[1]) - y_range[0] = min(y_range[0], layer_y[0]) - y_range[1] = max(y_range[1], layer_y[1]) - width = x_range[1] - x_range[0] - height = y_range[1] - y_range[0] - - scale = math.floor(min(float(max_width)/width, float(max_height)/height)) - self.scale = (scale, scale) - - self.clear() - - # Render layers - bgsettings = theme['background'] - for layer in layers: - 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=None, verbose=False): - """ Save image as `filename` - """ - try: - is_svg = os.path.splitext(filename.lower())[1] == '.svg' - except: - is_svg = False - if verbose: - print('[Render]: Writing image to {}'.format(filename)) - if is_svg: - self.surface.finish() - self.surface_buffer.flush() - with open(filename, "wb") as f: - self.surface_buffer.seek(0) - f.write(self.surface_buffer.read()) - f.flush() - else: - return self.surface.write_to_png(filename) - - def dump_str(self): - """ Return a byte-string containing the rendered image. - """ - fobj = BytesIO() - self.surface.write_to_png(fobj) - return fobj.getvalue() - - def dump_svg_str(self): - """ Return a string containg the rendered SVG. - """ - self.surface.finish() - self.surface_buffer.flush() - return self.surface_buffer.read() - - 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 - self.surface_buffer = None - - def _new_mask(self): - class Mask: - def __enter__(msk): - size_in_pixels = self.size_in_pixels - msk.surface = cairo.SVGSurface(None, size_in_pixels[0], - size_in_pixels[1]) - msk.ctx = cairo.Context(msk.surface) - msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) - return msk - - - def __exit__(msk, exc_type, exc_val, traceback): - if hasattr(msk.surface, 'finish'): - msk.surface.finish() - - return Mask() - - 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(settings.color, settings.alpha) - - def _render_line(self, line, color): - start = self.scale_point(line.start) - end = self.scale_point(line.end) - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and line.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - - with self._clip_primitive(line): - with self._new_mask() as mask: - if isinstance(line.aperture, Circle): - width = line.aperture.diameter - mask.ctx.set_line_width(width * self.scale[0]) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*start) - mask.ctx.line_to(*end) - mask.ctx.stroke() - - elif hasattr(line, 'vertices') and line.vertices is not None: - points = [self.scale_point(x) for x in line.vertices] - mask.ctx.set_line_width(0) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_arc(self, arc, color): - center = self.scale_point(arc.center) - start = self.scale_point(arc.start) - end = self.scale_point(arc.end) - radius = self.scale[0] * arc.radius - two_pi = 2 * math.pi - angle1 = (arc.start_angle + two_pi) % two_pi - angle2 = (arc.end_angle + two_pi) % two_pi - if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': - # Make the angles slightly different otherwise Cario will draw nothing - angle2 -= 0.000000001 - if isinstance(arc.aperture, Circle): - width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001 - else: - width = max(arc.aperture.width, arc.aperture.height, 0.001) - - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and arc.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(arc): - with self._new_mask() as mask: - mask.ctx.set_line_width(width * self.scale[0]) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE) - mask.ctx.move_to(*start) # You actually have to do this... - if arc.direction == 'counterclockwise': - mask.ctx.arc(center[0], center[1], radius, angle1, angle2) - else: - mask.ctx.arc_negative(center[0], center[1], radius, - angle1, angle2) - mask.ctx.move_to(*end) # ...lame - mask.ctx.stroke() - - #if isinstance(arc.aperture, Rectangle): - # print("Flash Rectangle Ends") - # print(arc.aperture.rotation * 180/math.pi) - # rect = arc.aperture - # width = self.scale[0] * rect.width - # height = self.scale[1] * rect.height - # for point, angle in zip((start, end), (angle1, angle2)): - # print("{} w {} h{}".format(point, rect.width, rect.height)) - # mask.ctx.rectangle(point[0] - width/2.0, - # point[1] - height/2.0, width, height) - # mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_region(self, region, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) and region.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(region): - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) - for prim in region.primitives: - if isinstance(prim, Line): - mask.ctx.line_to(*self.scale_point(prim.end)) - else: - center = self.scale_point(prim.center) - radius = self.scale[0] * prim.radius - angle1 = prim.start_angle - angle2 = prim.end_angle - if prim.direction == 'counterclockwise': - mask.ctx.arc(center[0], center[1], radius, - angle1, angle2) - else: - mask.ctx.arc_negative(center[0], center[1], radius, - angle1, angle2) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_circle(self, circle, color): - center = self.scale_point(circle.position) - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and circle.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(circle): - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) - mask.ctx.fill() - - if hasattr(circle, 'hole_diameter') and circle.hole_diameter is not None and circle.hole_diameter > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], center[1], circle.hole_radius * self.scale[0], 0, 2 * math.pi) - mask.ctx.fill() - - if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') - and circle.hole_width is not None and circle.hole_height is not None - and circle.hole_width > 0 and circle.hole_height > 0): - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if circle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((circle.hole_width, circle.hole_height)) - lower_left = rotate_point( - (center[0] - width / 2.0, center[1] - height / 2.0), - circle.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - circle.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - circle.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - circle.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - 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))]) - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and rectangle.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(rectangle): - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) - mask.ctx.fill() - - center = self.scale_point(rectangle.position) - if rectangle.hole_diameter > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - - mask.ctx.arc(center[0], center[1], rectangle.hole_radius * self.scale[0], 0, 2 * math.pi) - mask.ctx.fill() - - if rectangle.hole_width > 0 and rectangle.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) - lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) - lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_obround(self, obround, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and obround.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(obround): - with self._new_mask() as mask: - mask.ctx.set_line_width(0) - - # Render circles - for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): - center = self.scale_point(circle.position) - mask.ctx.arc(center[0], center[1], (circle.radius * self.scale[0]), 0, (2 * math.pi)) - mask.ctx.fill() - - # Render Rectangle - rectangle = obround.subshapes['rectangle'] - lower_left = self.scale_point(rectangle.lower_left) - width, height = tuple([abs(coord) for coord in - self.scale_point((rectangle.width, - rectangle.height))]) - mask.ctx.rectangle(lower_left[0], lower_left[1], width, height) - mask.ctx.fill() - - center = self.scale_point(obround.position) - if obround.hole_diameter > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR) - mask.ctx.arc(center[0], center[1], obround.hole_radius * self.scale[0], 0, 2 * math.pi) - mask.ctx.fill() - - if obround.hole_width > 0 and obround.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height =self.scale_point((obround.hole_width, obround.hole_height)) - lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), - obround.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - obround.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - obround.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - obround.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_polygon(self, polygon, color): - self.ctx.set_operator(cairo.OPERATOR_OVER - if (not self.invert) - and polygon.level_polarity == 'dark' - else cairo.OPERATOR_CLEAR) - with self._clip_primitive(polygon): - with self._new_mask() as mask: - - vertices = polygon.vertices - mask.ctx.set_line_width(0) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - # Start from before the end so it is easy to iterate and make sure - # it is closed - mask.ctx.move_to(*self.scale_point(vertices[-1])) - for v in vertices: - mask.ctx.line_to(*self.scale_point(v)) - mask.ctx.fill() - - center = self.scale_point(polygon.position) - if polygon.hole_radius > 0: - # Render the center clear - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - mask.ctx.set_line_width(0) - mask.ctx.arc(center[0], - center[1], - polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - mask.ctx.fill() - - if polygon.hole_width > 0 and polygon.hole_height > 0: - mask.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_OVER) - width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) - lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), - polygon.rotation, center) - lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), - polygon.rotation, center) - upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), - polygon.rotation, center) - upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), - polygon.rotation, center) - points = (lower_left, lower_right, upper_right, upper_left) - mask.ctx.move_to(*points[-1]) - for point in points: - mask.ctx.line_to(*point) - mask.ctx.fill() - - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - 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_slot(self, slot, color): - start = map(mul, slot.start, self.scale) - end = map(mul, slot.end, self.scale) - - width = slot.diameter - - self.ctx.set_operator(cairo.OPERATOR_OVER - if slot.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - with self._clip_primitive(slot): - with self._new_mask() as mask: - mask.ctx.set_line_width(width * self.scale[0]) - mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - mask.ctx.move_to(*start) - mask.ctx.line_to(*end) - mask.ctx.stroke() - self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) - - def _render_amgroup(self, amgroup, color): - for primitive in amgroup.primitives: - self.render(primitive) - - def _render_test_record(self, primitive, color): - 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_operator(cairo.OPERATOR_OVER - 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) - - if self.invert: - ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) - ctx.set_operator(cairo.OPERATOR_OVER) - ctx.paint() - if mirror: - matrix.xx = -1.0 - matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] - self.ctx = ctx - self.ctx.set_matrix(matrix) - self.active_layer = layer - self.active_matrix = matrix - - def flatten(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 - self.output_ctx.set_source_rgba(color[0], color[1], color[2], alpha) - self.output_ctx.mask_surface(self.active_layer) - self.ctx = None - self.active_layer = None - self.active_matrix = None - - 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[0], color[1], color[2], alpha) - self.output_ctx.paint() - - def _clip_primitive(self, primitive): - """ Clip rendering context to pixel-aligned bounding box - - Calculates pixel- and axis- aligned bounding box, and clips current - context to that region. Improves rendering speed significantly. This - returns a context manager, use as follows: - - with self._clip_primitive(some_primitive): - do_rendering_stuff() - do_more_rendering stuff(with, arguments) - - The context manager will reset the context's clipping region when it - goes out of scope. - - """ - class Clip: - def __init__(clp, primitive): - x_range, y_range = primitive.bounding_box - xmin, xmax = x_range - ymin, ymax = y_range - - # Round bounds to the nearest pixel outside of the primitive - clp.xmin = math.floor(self.scale[0] * xmin) - clp.xmax = math.ceil(self.scale[0] * xmax) - - # We need to offset Y to take care of the difference in y-pos - # caused by flipping the axis. - clp.ymin = math.floor( - (self.scale[1] * ymin) - math.ceil(self.origin_in_pixels[1])) - clp.ymax = math.floor( - (self.scale[1] * ymax) - math.floor(self.origin_in_pixels[1])) - - # Calculate width and height, rounded to the nearest pixel - clp.width = abs(clp.xmax - clp.xmin) - clp.height = abs(clp.ymax - clp.ymin) - - def __enter__(clp): - # Clip current context to primitive's bounding box - self.ctx.rectangle(clp.xmin, clp.ymin, clp.width, clp.height) - self.ctx.clip() - - def __exit__(clp, exc_type, exc_val, traceback): - # Reset context clip region - self.ctx.reset_clip() - - return Clip(primitive) - - def scale_point(self, point): - return tuple([coord * scale for coord, scale in zip(point, self.scale)]) diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py deleted file mode 100644 index 765d68c..0000000 --- a/gerber/render/excellon_backend.py +++ /dev/null @@ -1,188 +0,0 @@ - -from .render import GerberContext -from ..excellon import DrillSlot -from ..excellon_statements import * - -class ExcellonContext(GerberContext): - - MODE_DRILL = 1 - MODE_SLOT =2 - - def __init__(self, settings): - GerberContext.__init__(self) - - # Statements that we write - self.comments = [] - self.header = [] - self.tool_def = [] - self.body_start = [RewindStopStmt()] - self.body = [] - self.start = [HeaderBeginStmt()] - - # Current tool and position - self.handled_tools = set() - self.cur_tool = None - self.drill_mode = ExcellonContext.MODE_DRILL - self.drill_down = False - self._pos = (None, None) - - self.settings = settings - - self._start_header() - self._start_comments() - - def _start_header(self): - """Create the header from the settings""" - - self.header.append(UnitStmt.from_settings(self.settings)) - - if self.settings.notation == 'incremental': - raise NotImplementedError('Incremental mode is not implemented') - else: - self.body.append(AbsoluteModeStmt()) - - def _start_comments(self): - - # Write the digits used - this isn't valid Excellon statement, so we write as a comment - self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1]))) - - def _get_end(self): - """How we end depends on our mode""" - - end = [] - - if self.drill_down: - end.append(RetractWithClampingStmt()) - end.append(RetractWithoutClampingStmt()) - - end.append(EndOfProgramStmt()) - - return end - - @property - def statements(self): - return self.start + self.comments + self.header + self.body_start + self.body + self._get_end() - - def set_bounds(self, bounds, *args, **kwargs): - pass - - def paint_background(self): - pass - - def _render_line(self, line, color): - raise ValueError('Invalid Excellon object') - def _render_arc(self, arc, color): - raise ValueError('Invalid Excellon object') - - def _render_region(self, region, color): - raise ValueError('Invalid Excellon object') - - def _render_level_polarity(self, region): - raise ValueError('Invalid Excellon object') - - def _render_circle(self, circle, color): - raise ValueError('Invalid Excellon object') - - def _render_rectangle(self, rectangle, color): - raise ValueError('Invalid Excellon object') - - def _render_obround(self, obround, color): - raise ValueError('Invalid Excellon object') - - def _render_polygon(self, polygon, color): - raise ValueError('Invalid Excellon object') - - def _simplify_point(self, point): - return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) - - def _render_drill(self, drill, color): - - if self.drill_mode != ExcellonContext.MODE_DRILL: - self._start_drill_mode() - - tool = drill.hit.tool - if not tool in self.handled_tools: - self.handled_tools.add(tool) - self.header.append(ExcellonTool.from_tool(tool)) - - if tool != self.cur_tool: - self.body.append(ToolSelectionStmt(tool.number)) - self.cur_tool = tool - - point = self._simplify_point(drill.position) - self._pos = drill.position - self.body.append(CoordinateStmt.from_point(point)) - - def _start_drill_mode(self): - """ - If we are not in drill mode, then end the ROUT so we can do basic drilling - """ - - if self.drill_mode == ExcellonContext.MODE_SLOT: - - # Make sure we are retracted before changing modes - last_cmd = self.body[-1] - if self.drill_down: - self.body.append(RetractWithClampingStmt()) - self.body.append(RetractWithoutClampingStmt()) - self.drill_down = False - - # Switch to drill mode - self.body.append(DrillModeStmt()) - self.drill_mode = ExcellonContext.MODE_DRILL - - else: - raise ValueError('Should be in slot mode') - - def _render_slot(self, slot, color): - - # Set the tool first, before we might go into drill mode - tool = slot.hit.tool - if not tool in self.handled_tools: - self.handled_tools.add(tool) - self.header.append(ExcellonTool.from_tool(tool)) - - if tool != self.cur_tool: - self.body.append(ToolSelectionStmt(tool.number)) - self.cur_tool = tool - - # Two types of drilling - normal drill and slots - if slot.hit.slot_type == DrillSlot.TYPE_ROUT: - - # For ROUT, setting the mode is part of the actual command. - - # Are we in the right position? - if slot.start != self._pos: - if self.drill_down: - # We need to move into the right position, so retract - self.body.append(RetractWithClampingStmt()) - self.drill_down = False - - # Move to the right spot - point = self._simplify_point(slot.start) - self._pos = slot.start - self.body.append(CoordinateStmt.from_point(point, mode="ROUT")) - - # Now we are in the right spot, so drill down - if not self.drill_down: - self.body.append(ZAxisRoutPositionStmt()) - self.drill_down = True - - # Do a linear move from our current position to the end position - point = self._simplify_point(slot.end) - self._pos = slot.end - self.body.append(CoordinateStmt.from_point(point, mode="LINEAR")) - - self.drill_mode = ExcellonContext.MODE_SLOT - - else: - # This is a G85 slot, so do this in normally drilling mode - if self.drill_mode != ExcellonContext.MODE_DRILL: - self._start_drill_mode() - - # Slots don't use simplified points - self._pos = slot.end - self.body.append(SlotStmt.from_points(slot.start, slot.end)) - - def _render_inverted_layer(self): - pass diff --git a/gerber/render/render.py b/gerber/render/render.py deleted file mode 100644 index 580a7ea..0000000 --- a/gerber/render/render.py +++ /dev/null @@ -1,246 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# Modified from code by Paulo Henrique Silva - -# 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. -""" -Rendering -============ -**Gerber (RS-274X) and Excellon file rendering** - -Render Gerber and Excellon files to a variety of formats. The render module -currently supports SVG rendering using the `svgwrite` library. -""" - - -from ..primitives import * -from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, - CoordStmt, ApertureStmt, RegionModeStmt, - QuadrantModeStmt,) - - -class GerberContext(object): - """ Gerber rendering context base class - - Provides basic functionality and API for rendering gerber files. Medium- - specific renderers should subclass GerberContext and implement the drawing - functions. Colors are stored internally as 32-bit RGB and may need to be - converted to a native format in the rendering subclass. - - Attributes - ---------- - units : string - Measurement units. 'inch' or 'metric' - - color : tuple (, , ) - 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`. - - background_color : tuple (, , ) - Color of the background. Used when exposing areas in 'clear' level - polarity mode. Format is the same as for `color`. - - 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._drill_color = (0.0, 0.0, 0.0) - self._alpha = 1.0 - self._invert = False - self.ctx = None - - @property - def units(self): - return self._units - - @units.setter - def units(self, units): - if units not in ('inch', 'metric'): - raise ValueError('Units may be "inch" or "metric"') - self._units = units - - @property - def color(self): - return self._color - - @color.setter - def color(self, color): - if len(color) != 3: - raise TypeError('Color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._color = color - - @property - def drill_color(self): - return self._drill_color - - @drill_color.setter - def drill_color(self, color): - if len(color) != 3: - raise TypeError('Drill color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._drill_color = color - - @property - def background_color(self): - return self._background_color - - @background_color.setter - def background_color(self, color): - if len(color) != 3: - raise TypeError('Background color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._background_color = color - - @property - def alpha(self): - return self._alpha - - @alpha.setter - def alpha(self, alpha): - if alpha < 0 or alpha > 1: - 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): - if not primitive: - return - - self.pre_render_primitive(primitive) - - color = self.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(primitive, color) - elif isinstance(primitive, Drill): - self._render_drill(primitive, self.color) - elif isinstance(primitive, Slot): - self._render_slot(primitive, self.color) - elif isinstance(primitive, AMGroup): - self._render_amgroup(primitive, color) - elif isinstance(primitive, Outline): - self._render_region(primitive, color) - elif isinstance(primitive, TestRecord): - self._render_test_record(primitive, color) - - self.post_render_primitive(primitive) - - def set_bounds(self, bounds, *args, **kwargs): - """Called by the renderer to set the extents of the file to render. - - Parameters - ---------- - bounds: Tuple[Tuple[float, float], Tuple[float, float]] - ( (x_min, x_max), (y_min, y_max) - """ - pass - - def paint_background(self): - pass - - def new_render_layer(self): - pass - - def flatten(self): - pass - - def pre_render_primitive(self, primitive): - """ - Called before rendering a primitive. Use the callback to perform some action before rendering - a primitive, for example adding a comment. - """ - return - - def post_render_primitive(self, primitive): - """ - Called after rendering a primitive. Use the callback to perform some action after rendering - a primitive - """ - return - - - def _render_line(self, primitive, color): - pass - - def _render_arc(self, primitive, color): - pass - - def _render_region(self, primitive, color): - pass - - def _render_circle(self, primitive, color): - pass - - def _render_rectangle(self, primitive, color): - pass - - def _render_obround(self, primitive, color): - pass - - def _render_polygon(self, primitive, color): - pass - - def _render_drill(self, primitive, color): - pass - - def _render_slot(self, primitive, color): - pass - - def _render_amgroup(self, primitive, color): - pass - - def _render_test_record(self, primitive, color): - pass - - -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/rs274x_backend.py b/gerber/render/rs274x_backend.py deleted file mode 100644 index c7af2ea..0000000 --- a/gerber/render/rs274x_backend.py +++ /dev/null @@ -1,510 +0,0 @@ -"""Renders an in-memory Gerber file to statements which can be written to a string -""" -from copy import deepcopy - -try: - from cStringIO import StringIO -except(ImportError): - from io import StringIO - -from .render import GerberContext -from ..am_statements import * -from ..gerber_statements import * -from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle - - -class AMGroupContext(object): - '''A special renderer to generate aperature macros from an AMGroup''' - - def __init__(self): - self.statements = [] - - def render(self, amgroup, name): - - if amgroup.stmt: - # We know the statement it was generated from, so use that to create the AMParamStmt - # It will give a much better result - - stmt = deepcopy(amgroup.stmt) - stmt.name = name - - return stmt - - else: - # Clone ourselves, then offset by the psotion so that - # our render doesn't have to consider offset. Just makes things simpler - nooffset_group = deepcopy(amgroup) - nooffset_group.position = (0, 0) - - # Now draw the shapes - for primitive in nooffset_group.primitives: - if isinstance(primitive, Outline): - self._render_outline(primitive) - elif isinstance(primitive, Circle): - self._render_circle(primitive) - elif isinstance(primitive, Rectangle): - self._render_rectangle(primitive) - elif isinstance(primitive, Line): - self._render_line(primitive) - elif isinstance(primitive, Polygon): - self._render_polygon(primitive) - else: - raise ValueError('amgroup') - - statement = AMParamStmt('AM', name, self._statements_to_string()) - return statement - - def _statements_to_string(self): - macro = '' - - for statement in self.statements: - macro += statement.to_gerber() - - return macro - - def _render_circle(self, circle): - self.statements.append(AMCirclePrimitive.from_primitive(circle)) - - def _render_rectangle(self, rectangle): - self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle)) - - def _render_line(self, line): - self.statements.append(AMVectorLinePrimitive.from_primitive(line)) - - def _render_outline(self, outline): - self.statements.append(AMOutlinePrimitive.from_primitive(outline)) - - def _render_polygon(self, polygon): - self.statements.append(AMPolygonPrimitive.from_primitive(polygon)) - - def _render_thermal(self, thermal): - pass - - -class Rs274xContext(GerberContext): - - def __init__(self, settings): - GerberContext.__init__(self) - self.comments = [] - self.header = [] - self.body = [] - self.end = [EofStmt()] - - # Current values so we know if we have to execute - # moves, levey changes before anything else - self._level_polarity = None - self._pos = (None, None) - self._func = None - self._quadrant_mode = None - self._dcode = None - - # Primarily for testing and comarison to files, should we write - # flashes as a single statement or a move plus flash? Set to true - # to do in a single statement. Normally this can be false - self.condensed_flash = True - - # When closing a region, force a D02 staement to close a region. - # This is normally not necessary because regions are closed with a G37 - # staement, but this will add an extra statement for doubly close - # the region - self.explicit_region_move_end = False - - self._next_dcode = 10 - self._rects = {} - self._circles = {} - self._obrounds = {} - self._polygons = {} - self._macros = {} - - self._i_none = 0 - self._j_none = 0 - - self.settings = settings - - self._start_header(settings) - - def _start_header(self, settings): - self.header.append(FSParamStmt.from_settings(settings)) - self.header.append(MOParamStmt.from_units(settings.units)) - - def _simplify_point(self, point): - return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) - - def _simplify_offset(self, point, offset): - - if point[0] != offset[0]: - xoffset = point[0] - offset[0] - else: - xoffset = self._i_none - - if point[1] != offset[1]: - yoffset = point[1] - offset[1] - else: - yoffset = self._j_none - - return (xoffset, yoffset) - - @property - def statements(self): - return self.comments + self.header + self.body + self.end - - def set_bounds(self, bounds, *args, **kwargs): - pass - - def paint_background(self): - pass - - def _select_aperture(self, aperture): - - # Select the right aperture if not already selected - if aperture: - if isinstance(aperture, Circle): - aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height) - elif isinstance(aperture, Rectangle): - aper = self._get_rectangle(aperture.width, aperture.height) - elif isinstance(aperture, Obround): - aper = self._get_obround(aperture.width, aperture.height) - elif isinstance(aperture, AMGroup): - aper = self._get_amacro(aperture) - else: - raise NotImplementedError('Line with invalid aperture type') - - if aper.d != self._dcode: - self.body.append(ApertureStmt(aper.d)) - self._dcode = aper.d - - def pre_render_primitive(self, primitive): - - if hasattr(primitive, 'comment'): - self.body.append(CommentStmt(primitive.comment)) - - def _render_line(self, line, color, default_polarity='dark'): - - self._select_aperture(line.aperture) - - self._render_level_polarity(line, default_polarity) - - # Get the right function - if self._func != CoordStmt.FUNC_LINEAR: - func = CoordStmt.FUNC_LINEAR - else: - func = None - self._func = CoordStmt.FUNC_LINEAR - - if self._pos != line.start: - self.body.append(CoordStmt.move(func, self._simplify_point(line.start))) - self._pos = line.start - # We already set the function, so the next command doesn't require that - func = None - - point = self._simplify_point(line.end) - - # In some files, we see a lot of duplicated ponts, so omit those - if point[0] != None or point[1] != None: - self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) - self._pos = line.end - elif func: - self.body.append(CoordStmt.mode(func)) - - def _render_arc(self, arc, color, default_polarity='dark'): - - # Optionally set the quadrant mode if it has changed: - if arc.quadrant_mode != self._quadrant_mode: - - if arc.quadrant_mode != 'multi-quadrant': - self.body.append(QuadrantModeStmt.single()) - else: - self.body.append(QuadrantModeStmt.multi()) - - self._quadrant_mode = arc.quadrant_mode - - # Select the right aperture if not already selected - self._select_aperture(arc.aperture) - - self._render_level_polarity(arc, default_polarity) - - # Find the right movement mode. Always set to be sure it is really right - dir = arc.direction - if dir == 'clockwise': - func = CoordStmt.FUNC_ARC_CW - self._func = CoordStmt.FUNC_ARC_CW - elif dir == 'counterclockwise': - func = CoordStmt.FUNC_ARC_CCW - self._func = CoordStmt.FUNC_ARC_CCW - else: - raise ValueError('Invalid circular interpolation mode') - - if self._pos != arc.start: - # TODO I'm not sure if this is right - self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start))) - self._pos = arc.start - - center = self._simplify_offset(arc.center, arc.start) - end = self._simplify_point(arc.end) - self.body.append(CoordStmt.arc(func, end, center)) - self._pos = arc.end - - def _render_region(self, region, color): - - self._render_level_polarity(region) - - self.body.append(RegionModeStmt.on()) - - for p in region.primitives: - - # Make programmatically generated primitives within a region with - # unset level polarity inherit the region's level polarity - if isinstance(p, Line): - self._render_line(p, color, default_polarity=region.level_polarity) - else: - self._render_arc(p, color, default_polarity=region.level_polarity) - - if self.explicit_region_move_end: - self.body.append(CoordStmt.move(None, None)) - - self.body.append(RegionModeStmt.off()) - - def _render_level_polarity(self, obj, default='dark'): - obj_polarity = obj.level_polarity if obj.level_polarity is not None else default - if obj_polarity != self._level_polarity: - self._level_polarity = obj_polarity - self.body.append(LPParamStmt('LP', obj_polarity)) - - def _render_flash(self, primitive, aperture): - - self._render_level_polarity(primitive) - - if aperture.d != self._dcode: - self.body.append(ApertureStmt(aperture.d)) - self._dcode = aperture.d - - if self.condensed_flash: - self.body.append(CoordStmt.flash(self._simplify_point(primitive.position))) - else: - self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position))) - self.body.append(CoordStmt.flash(None)) - - self._pos = primitive.position - - def _get_circle(self, diameter, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - '''Define a circlar aperture''' - - key = (diameter, hole_diameter, hole_width, hole_height) - aper = self._circles.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height) - self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper - self.header.append(aper) - - return aper - - def _render_circle(self, circle, color): - - aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height) - self._render_flash(circle, aper) - - def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - '''Get a rectanglar aperture. If it isn't defined, create it''' - - key = (width, height, hole_diameter, hole_width, hole_height) - aper = self._rects.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height) - self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper - self.header.append(aper) - - return aper - - def _render_rectangle(self, rectangle, color): - - aper = self._get_rectangle(rectangle.width, rectangle.height, - rectangle.hole_diameter, - rectangle.hole_width, rectangle.hole_height) - self._render_flash(rectangle, aper) - - def _get_obround(self, width, height, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - - key = (width, height, hole_diameter, hole_width, hole_height) - aper = self._obrounds.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height) - self._obrounds[key] = aper - self.header.append(aper) - - return aper - - def _render_obround(self, obround, color): - - aper = self._get_obround(obround.width, obround.height, - obround.hole_diameter, obround.hole_width, - obround.hole_height) - self._render_flash(obround, aper) - - def _render_polygon(self, polygon, color): - - aper = self._get_polygon(polygon.radius, polygon.sides, - polygon.rotation, polygon.hole_diameter, - polygon.hole_width, polygon.hole_height) - self._render_flash(polygon, aper) - - def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None, - hole_width=None, hole_height=None, dcode = None): - - key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height) - aper = self._polygons.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, - rotation, hole_diameter, hole_width, - hole_height) - self._polygons[key] = aper - self.header.append(aper) - - return aper - - def _render_drill(self, drill, color): - raise ValueError('Drills are not valid in RS274X files') - - def _hash_amacro(self, amgroup): - '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' - - # We always start with an X because this forms part of the name - # Basically, in some cases, the name might start with a C, R, etc. That can appear - # to conflict with normal aperture definitions. Technically, it shouldn't because normal - # aperture definitions should have a comma, but in some cases the commit is omitted - hash = 'X' - for primitive in amgroup.primitives: - - hash += primitive.__class__.__name__[0] - - bbox = primitive.bounding_box - hash += str((bbox[0][1] - bbox[0][0]) * 100000)[0:2] - hash += str((bbox[1][1] - bbox[1][0]) * 100000)[0:2] - - if hasattr(primitive, 'primitives'): - hash += str(len(primitive.primitives)) - - if isinstance(primitive, Rectangle): - hash += str(primitive.width * 1000000)[0:2] - hash += str(primitive.height * 1000000)[0:2] - elif isinstance(primitive, Circle): - hash += str(primitive.diameter * 1000000)[0:2] - - if len(hash) > 20: - # The hash might actually get quite complex, so stop before - # it gets too long - break - - return hash - - def _get_amacro(self, amgroup, dcode = None): - # Macros are a little special since we don't have a good way to compare them quickly - # but in most cases, this should work - - hash = self._hash_amacro(amgroup) - macro = None - macroinfo = self._macros.get(hash, None) - - if macroinfo: - - # We have a definition, but check that the groups actually are the same - for macro in macroinfo: - - # Macros should have positions, right? But if the macro is selected for non-flashes - # then it won't have a position. This is of course a bad gerber, but they do exist - if amgroup.position: - position = amgroup.position - else: - position = (0, 0) - - offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1]) - if amgroup.equivalent(macro[1], offset): - break - macro = None - - # Did we find one in the group0 - if not macro: - # This is a new macro, so define it - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - # Create the statements - # TODO - amrenderer = AMGroupContext() - statement = amrenderer.render(amgroup, hash) - - self.header.append(statement) - - aperdef = ADParamStmt.macro(dcode, hash) - self.header.append(aperdef) - - # Store the dcode and the original so we can check if it really is the same - # If it didn't have a postition, set it to 0, 0 - if amgroup.position == None: - amgroup.position = (0, 0) - macro = (aperdef, amgroup) - - if macroinfo: - macroinfo.append(macro) - else: - self._macros[hash] = [macro] - - return macro[0] - - def _render_amgroup(self, amgroup, color): - - aper = self._get_amacro(amgroup) - self._render_flash(amgroup, aper) - - def _render_inverted_layer(self): - pass - - def new_render_layer(self): - # TODO Might need to implement this - pass - - def flatten(self): - # TODO Might need to implement this - pass - - def dump(self): - """Write the rendered file to a StringIO steam""" - statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) - stream = StringIO() - for statement in statements: - stream.write(statement + '\n') - - return stream diff --git a/gerber/render/theme.py b/gerber/render/theme.py deleted file mode 100644 index 2f558a1..0000000 --- a/gerber/render/theme.py +++ /dev/null @@ -1,112 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 Paulo Henrique Silva - -# 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. - - -from .render import RenderSettings - -COLORS = { - 'black': (0.0, 0.0, 0.0), - 'white': (1.0, 1.0, 1.0), - 'red': (1.0, 0.0, 0.0), - 'green': (0.0, 1.0, 0.0), - 'yellow': (1.0, 1.0, 0), - 'blue': (0.0, 0.0, 1.0), - 'fr-4': (0.290, 0.345, 0.0), - '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.694, 0.533, 0.514), - 'hasl copper': (0.871, 0.851, 0.839) -} - - -SPECTRUM = [ - (0.804, 0.216, 0), - (0.78, 0.776, 0.251), - (0.545, 0.451, 0.333), - (0.545, 0.137, 0.137), - (0.329, 0.545, 0.329), - (0.133, 0.545, 0.133), - (0, 0.525, 0.545), - (0.227, 0.373, 0.804), -] - - -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['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.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'])) - self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) - self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM]) - self._internal_gen = None - - def __getitem__(self, key): - return getattr(self, key) - - @property - def internal(self): - if not self._internal_gen: - self._internal_gen = self._internal_gen_func() - return next(self._internal_gen) - - def _internal_gen_func(self): - for setting in self._internal: - yield setting - - def get(self, key, noneval=None): - val = getattr(self, key, None) - return val if val is not None else noneval - - -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.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))), - - 'Transparent Multilayer': Theme(name='Transparent Multilayer', - background=RenderSettings((0, 0, 0)), - top=RenderSettings(SPECTRUM[0], alpha=0.8), - bottom=RenderSettings(SPECTRUM[-1], alpha=0.8), - drill=RenderSettings((0.3, 0.3, 0.3)), - internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]), -} -- cgit