Add arc rendering and tests
This commit is contained in:
parent
b488ab6af9
commit
f5abd5b0bd
5 changed files with 179 additions and 8 deletions
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
85
gerber/tests/test_primitives.py
Normal file
85
gerber/tests/test_primitives.py
Normal 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)))
|
||||
|
||||
|
||||
|
|
@ -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' ]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue