Add arc rendering and tests

This commit is contained in:
Hamilton Kibbe 2014-10-28 22:11:43 -04:00
parent b488ab6af9
commit f5abd5b0bd
5 changed files with 179 additions and 8 deletions

View file

@ -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):
"""

View file

@ -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):

View file

@ -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],

View file

@ -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)))

View file

@ -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' ]