summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHamilton Kibbe <hamilton.kibbe@gmail.com>2014-10-28 22:11:43 -0400
committerHamilton Kibbe <hamilton.kibbe@gmail.com>2014-10-28 22:11:43 -0400
commitf5abd5b0bdc0b9f524456dc9216bd0f3732e82a0 (patch)
tree896c4398f4ee680a63bd8a29b86cee21b95ec836
parentb488ab6af9d7925263b2d0712abfd2ba55dc96d2 (diff)
downloadgerbonara-f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0.tar.gz
gerbonara-f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0.tar.bz2
gerbonara-f5abd5b0bdc0b9f524456dc9216bd0f3732e82a0.zip
Add arc rendering and tests
-rw-r--r--gerber/primitives.py63
-rw-r--r--gerber/render/cairo_backend.py20
-rw-r--r--gerber/render/svgwrite_backend.py14
-rw-r--r--gerber/tests/test_primitives.py85
-rw-r--r--gerber/tests/tests.py5
5 files changed, 179 insertions, 8 deletions
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
@@ -70,18 +70,71 @@ class Arc(Primitive):
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 <ham@hamiltonkib.be>
+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' ]