From f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 28 Oct 2014 22:11:43 -0400 Subject: Add arc rendering and tests --- gerber/primitives.py | 63 ++++++++++++++++++++++++++--- gerber/render/cairo_backend.py | 20 ++++++++- gerber/render/svgwrite_backend.py | 14 +++++++ gerber/tests/test_primitives.py | 85 +++++++++++++++++++++++++++++++++++++++ gerber/tests/tests.py | 5 ++- 5 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 gerber/tests/test_primitives.py (limited to 'gerber') diff --git a/gerber/primitives.py b/gerber/primitives.py index b3869e1..f934f74 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -44,8 +44,8 @@ class Line(Primitive): @property def angle(self): - delta_x, delta_y = tuple(map(sub, end, start)) - angle = degrees(math.tan(delta_y/delta_x)) + delta_x, delta_y = tuple(map(sub, self.end, self.start)) + angle = math.atan2(delta_y, delta_x) return angle @property @@ -69,19 +69,72 @@ class Arc(Primitive): self.direction = direction self.width = width + @property + def radius(self): + dy, dx = map(sub, self.start, self.center) + return math.sqrt(dy**2 + dx**2) + @property def start_angle(self): dy, dx = map(sub, self.start, self.center) - return math.atan2(dy, dx) + return math.atan2(dx, dy) @property def end_angle(self): dy, dx = map(sub, self.end, self.center) - return math.atan2(dy, dx) + return math.atan2(dx, dy) + + @property + def sweep_angle(self): + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + if self.direction == 'counterclockwise': + return abs(theta1 - theta0) + else: + theta0 += two_pi + return abs(theta0 - theta1) % two_pi @property def bounding_box(self): - pass + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + #Shit's about to get ugly... + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 > theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + points.append((self.center[0], self.center[1] - self.radius )) + else: + # Passes through 0 degrees + if theta1 > theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): + points.append((self.center[0], self.center[1] - self.radius )) + x, y = zip(*points) + min_x = min(x) + max_x = max(x) + min_y = min(y) + max_y = max(y) + return ((min_x, max_x), (min_y, max_y)) + class Circle(Primitive): """ diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 1c69725..125a125 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -56,13 +56,31 @@ class GerberCairoContext(GerberContext): self.ctx.line_to(*end) self.ctx.stroke() + def _render_arc(self, arc, color): + center = map(mul, arc.center, self.scale) + start = map(mul, arc.start, self.scale) + end = map(mul, arc.end, self.scale) + radius = SCALE * arc.radius + angle1 = arc.start_angle + angle2 = arc.end_angle + width = arc.width if arc.width != 0 else 0.001 + self.ctx.set_source_rgba(*color, alpha=self.alpha) + self.ctx.set_line_width(width * SCALE) + self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + self.ctx.move_to(*start) # You actually have to do this... + if arc.direction == 'counterclockwise': + self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + self.ctx.arc_negative(*center, radius=radius, angle1=angle1, angle2=angle2) + self.ctx.move_to(*end) # ...lame + def _render_region(self, region, color): points = [tuple(map(mul, point, self.scale)) for point in region.points] self.ctx.set_source_rgba(*color, alpha=self.alpha) self.ctx.set_line_width(0) self.ctx.move_to(*points[0]) for point in points[1:]: - self.ctx.move_to(*point) + self.ctx.line_to(*point) self.ctx.fill() def _render_circle(self, circle, color): diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index aeb680c..27783d6 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -18,6 +18,7 @@ from .render import GerberContext from operator import mul +import math import svgwrite SCALE = 400. @@ -64,6 +65,19 @@ class GerberSvgContext(GerberContext): aline.stroke(opacity=self.alpha) self.dwg.add(aline) + def _render_arc(self, arc, color): + start = tuple(map(mul, arc.start, self.scale)) + end = tuple(map(mul, arc.end, self.scale)) + radius = SCALE * arc.radius + width = arc.width if arc.width != 0 else 0.001 + arc_path = self.dwg.path(d='M %f, %f' % start, + stroke=svg_color(color), + stroke_width=SCALE * width) + large_arc = arc.sweep_angle >= 2 * math.pi + direction = '-' if arc.direction == 'clockwise' else '+' + arc_path.push_arc(end, 0, radius, large_arc, direction, True) + self.dwg.add(arc_path) + 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], diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py new file mode 100644 index 0000000..29036b4 --- /dev/null +++ b/gerber/tests/test_primitives.py @@ -0,0 +1,85 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe +from ..primitives import * +from tests import * + + + +def test_line_angle(): + """ Test Line primitive angle calculation + """ + cases = [((0, 0), (1, 0), math.radians(0)), + ((0, 0), (1, 1), math.radians(45)), + ((0, 0), (0, 1), math.radians(90)), + ((0, 0), (-1, 1), math.radians(135)), + ((0, 0), (-1, 0), math.radians(180)), + ((0, 0), (-1, -1), math.radians(225)), + ((0, 0), (0, -1), math.radians(270)), + ((0, 0), (1, -1), math.radians(315)),] + for start, end, expected in cases: + l = Line(start, end, 0) + line_angle = (l.angle + 2 * math.pi) % (2 * math.pi) + assert_almost_equal(line_angle, expected) + +def test_line_bounds(): + """ Test Line primitive bounding box calculation + """ + cases = [((0, 0), (1, 1), ((0, 1), (0, 1))), + ((-1, -1), (1, 1), ((-1, 1), (-1, 1))), + ((1, 1), (-1, -1), ((-1, 1), (-1, 1))), + ((-1, 1), (1, -1), ((-1, 1), (-1, 1))),] + for start, end, expected in cases: + l = Line(start, end, 0) + assert_equal(l.bounding_box, expected) + +def test_arc_radius(): + """ Test Arc primitive radius calculation + """ + cases = [((-3, 4), (5, 0), (0, 0), 5), + ((0, 1), (1, 0), (0, 0), 1),] + + for start, end, center, radius in cases: + a = Arc(start, end, center, 'clockwise', 0) + assert_equal(a.radius, radius) + + +def test_arc_sweep_angle(): + """ Test Arc primitive sweep angle calculation + """ + cases = [((1, 0), (0, 1), (0, 0), 'counterclockwise', math.radians(90)), + ((1, 0), (0, 1), (0, 0), 'clockwise', math.radians(270)), + ((1, 0), (-1, 0), (0, 0), 'clockwise', math.radians(180)), + ((1, 0), (-1, 0), (0, 0), 'counterclockwise', math.radians(180)),] + + for start, end, center, direction, sweep in cases: + a = Arc(start, end, center, direction, 0) + assert_equal(a.sweep_angle, sweep) + + +def test_arc_bounds(): + """ Test Arc primitive bounding box calculation + """ + cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1, 1), (-1, 1))), + ((1, 0), (0, 1), (0, 0), 'counterclockwise', ((0, 1), (0, 1))), + #TODO: ADD MORE TEST CASES HERE + ] + + for start, end, center, direction, bounds in cases: + a = Arc(start, end, center, direction, 0) + assert_equal(a.bounding_box, bounds) + +def test_circle_radius(): + """ Test Circle primitive radius calculation + """ + c = Circle((1, 1), 2) + assert_equal(c.radius, 1) + +def test_circle_bounds(): + """ Test Circle bounding box calculation + """ + c = Circle((1, 1), 2) + assert_equal(c.bounding_box, ((0, 2), (0, 2))) + + diff --git a/gerber/tests/tests.py b/gerber/tests/tests.py index 29b7899..222eea3 100644 --- a/gerber/tests/tests.py +++ b/gerber/tests/tests.py @@ -7,6 +7,7 @@ from nose.tools import assert_in from nose.tools import assert_not_in from nose.tools import assert_equal from nose.tools import assert_not_equal +from nose.tools import assert_almost_equal from nose.tools import assert_true from nose.tools import assert_false from nose.tools import assert_raises @@ -14,5 +15,5 @@ from nose.tools import raises from nose import with_setup __all__ = ['assert_in', 'assert_not_in', 'assert_equal', 'assert_not_equal', - 'assert_true', 'assert_false', 'assert_raises', 'raises', - 'with_setup' ] + 'assert_almost_equal', 'assert_true', 'assert_false', + 'assert_raises', 'raises', 'with_setup' ] -- cgit