Approximate Arcs With Beziers (#130)
* Approximate Arcs With Beziers * Quadratic in documentation. * Test Coverage, approximate arcs.vectorize-path-point
parent
45dc873f82
commit
b3d9544624
|
@ -4,7 +4,7 @@ Arc."""
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
from math import sqrt, cos, sin, acos, asin, degrees, radians, log, pi, ceil
|
from math import sqrt, cos, sin, tan, acos, asin, degrees, radians, log, pi, ceil
|
||||||
from cmath import exp, sqrt as csqrt, phase
|
from cmath import exp, sqrt as csqrt, phase
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
|
@ -2235,6 +2235,86 @@ class Arc(object):
|
||||||
"""Scale transform. See `scale` function for further explanation."""
|
"""Scale transform. See `scale` function for further explanation."""
|
||||||
return scale(self, sx=sx, sy=sy, origin=origin)
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
||||||
|
|
||||||
|
def as_cubic_curves(self, curves=1):
|
||||||
|
"""Generates cubic curves to approximate this arc"""
|
||||||
|
slice_t = radians(self.delta) / float(curves)
|
||||||
|
|
||||||
|
current_t = radians(self.theta)
|
||||||
|
rx = self.radius.real # * self.radius_scale
|
||||||
|
ry = self.radius.imag # * self.radius_scale
|
||||||
|
p_start = self.start
|
||||||
|
|
||||||
|
theta = radians(self.rotation)
|
||||||
|
x0 = self.center.real
|
||||||
|
y0 = self.center.imag
|
||||||
|
cos_theta = cos(theta)
|
||||||
|
sin_theta = sin(theta)
|
||||||
|
|
||||||
|
for i in range(curves):
|
||||||
|
next_t = current_t + slice_t
|
||||||
|
|
||||||
|
alpha = sin(slice_t) * (sqrt(4 + 3 * pow(tan((slice_t) / 2.0), 2)) - 1) / 3.0
|
||||||
|
|
||||||
|
cos_start_t = cos(current_t)
|
||||||
|
sin_start_t = sin(current_t)
|
||||||
|
|
||||||
|
ePrimen1x = -rx * cos_theta * sin_start_t - ry * sin_theta * cos_start_t
|
||||||
|
ePrimen1y = -rx * sin_theta * sin_start_t + ry * cos_theta * cos_start_t
|
||||||
|
|
||||||
|
cos_end_t = cos(next_t)
|
||||||
|
sin_end_t = sin(next_t)
|
||||||
|
|
||||||
|
p2En2x = x0 + rx * cos_end_t * cos_theta - ry * sin_end_t * sin_theta
|
||||||
|
p2En2y = y0 + rx * cos_end_t * sin_theta + ry * sin_end_t * cos_theta
|
||||||
|
p_end = p2En2x + p2En2y * 1j
|
||||||
|
if i == curves - 1:
|
||||||
|
p_end = self.end
|
||||||
|
|
||||||
|
ePrimen2x = -rx * cos_theta * sin_end_t - ry * sin_theta * cos_end_t
|
||||||
|
ePrimen2y = -rx * sin_theta * sin_end_t + ry * cos_theta * cos_end_t
|
||||||
|
|
||||||
|
p_c1 = (p_start.real + alpha * ePrimen1x) + (p_start.imag + alpha * ePrimen1y) * 1j
|
||||||
|
p_c2 = (p_end.real - alpha * ePrimen2x) + (p_end.imag - alpha * ePrimen2y) * 1j
|
||||||
|
|
||||||
|
yield CubicBezier(p_start, p_c1, p_c2, p_end)
|
||||||
|
p_start = p_end
|
||||||
|
current_t = next_t
|
||||||
|
|
||||||
|
def as_quad_curves(self, curves=1):
|
||||||
|
"""Generates quadratic curves to approximate this arc"""
|
||||||
|
slice_t = radians(self.delta) / float(curves)
|
||||||
|
|
||||||
|
current_t = radians(self.theta)
|
||||||
|
a = self.radius.real # * self.radius_scale
|
||||||
|
b = self.radius.imag # * self.radius_scale
|
||||||
|
p_start = self.start
|
||||||
|
|
||||||
|
theta = radians(self.rotation)
|
||||||
|
cx = self.center.real
|
||||||
|
cy = self.center.imag
|
||||||
|
|
||||||
|
cos_theta = cos(theta)
|
||||||
|
sin_theta = sin(theta)
|
||||||
|
|
||||||
|
for i in range(curves):
|
||||||
|
next_t = current_t + slice_t
|
||||||
|
mid_t = (next_t + current_t) / 2
|
||||||
|
cos_end_t = cos(next_t)
|
||||||
|
sin_end_t = sin(next_t)
|
||||||
|
p2En2x = cx + a * cos_end_t * cos_theta - b * sin_end_t * sin_theta
|
||||||
|
p2En2y = cy + a * cos_end_t * sin_theta + b * sin_end_t * cos_theta
|
||||||
|
p_end = p2En2x + p2En2y * 1j
|
||||||
|
if i == curves - 1:
|
||||||
|
p_end = self.end
|
||||||
|
cos_mid_t = cos(mid_t)
|
||||||
|
sin_mid_t = sin(mid_t)
|
||||||
|
alpha = (4.0 - cos(slice_t)) / 3.0
|
||||||
|
px = cx + alpha * (a * cos_mid_t * cos_theta - b * sin_mid_t * sin_theta)
|
||||||
|
py = cy + alpha * (a * cos_mid_t * sin_theta + b * sin_mid_t * cos_theta)
|
||||||
|
yield QuadraticBezier(p_start, px + py * 1j, p_end)
|
||||||
|
p_start = p_end
|
||||||
|
current_t = next_t
|
||||||
|
|
||||||
|
|
||||||
def is_bezier_segment(x):
|
def is_bezier_segment(x):
|
||||||
return (isinstance(x, Line) or
|
return (isinstance(x, Line) or
|
||||||
|
@ -2906,6 +2986,32 @@ class Path(MutableSequence):
|
||||||
opt = complex(xmin-1, ymin-1)
|
opt = complex(xmin-1, ymin-1)
|
||||||
return path_encloses_pt(pt, opt, other)
|
return path_encloses_pt(pt, opt, other)
|
||||||
|
|
||||||
|
def approximate_arcs_with_cubics(self, error=0.1):
|
||||||
|
"""
|
||||||
|
Iterates through this path and replaces any Arcs with cubic bezier curves.
|
||||||
|
"""
|
||||||
|
tau = pi * 2
|
||||||
|
sweep_limit = degrees(tau * error)
|
||||||
|
for s in range(len(self)-1, -1, -1):
|
||||||
|
segment = self[s]
|
||||||
|
if not isinstance(segment, Arc):
|
||||||
|
continue
|
||||||
|
arc_required = int(ceil(abs(segment.delta) / sweep_limit))
|
||||||
|
self[s:s+1] = list(segment.as_cubic_curves(arc_required))
|
||||||
|
|
||||||
|
def approximate_arcs_with_quads(self, error=0.1):
|
||||||
|
"""
|
||||||
|
Iterates through this path and replaces any Arcs with quadratic bezier curves.
|
||||||
|
"""
|
||||||
|
tau = pi * 2
|
||||||
|
sweep_limit = degrees(tau * error)
|
||||||
|
for s in range(len(self)-1, -1, -1):
|
||||||
|
segment = self[s]
|
||||||
|
if not isinstance(segment, Arc):
|
||||||
|
continue
|
||||||
|
arc_required = int(ceil(abs(segment.delta) / sweep_limit))
|
||||||
|
self[s:s+1] = list(segment.as_quad_curves(arc_required))
|
||||||
|
|
||||||
def _tokenize_path(self, pathdef):
|
def _tokenize_path(self, pathdef):
|
||||||
for x in COMMAND_RE.split(pathdef):
|
for x in COMMAND_RE.split(pathdef):
|
||||||
if x in COMMANDS:
|
if x in COMMANDS:
|
||||||
|
|
|
@ -658,6 +658,33 @@ class ArcTest(unittest.TestCase):
|
||||||
computed_t = a.point_to_t(p)
|
computed_t = a.point_to_t(p)
|
||||||
self.assertAlmostEqual(orig_t, computed_t, msg="arc %s at t=%f is point %s, but got %f back" % (a, orig_t, p, computed_t))
|
self.assertAlmostEqual(orig_t, computed_t, msg="arc %s at t=%f is point %s, but got %f back" % (a, orig_t, p, computed_t))
|
||||||
|
|
||||||
|
def test_approx_quad(self):
|
||||||
|
n = 100
|
||||||
|
for i in range(n):
|
||||||
|
arc = random_arc()
|
||||||
|
if arc.radius.real > 2000 or arc.radius.imag > 2000:
|
||||||
|
continue # Random Arc too large, by autoscale.
|
||||||
|
path1 = Path(arc)
|
||||||
|
path2 = Path(*path1)
|
||||||
|
path2.approximate_arcs_with_quads(error=0.05)
|
||||||
|
d = abs(path1.length() - path2.length())
|
||||||
|
# Error less than 1% typically less than 0.5%
|
||||||
|
self.assertAlmostEqual(d, 0.0, delta=20)
|
||||||
|
|
||||||
|
def test_approx_cubic(self):
|
||||||
|
n = 100
|
||||||
|
for i in range(n):
|
||||||
|
arc = random_arc()
|
||||||
|
if arc.radius.real > 2000 or arc.radius.imag > 2000:
|
||||||
|
continue # Random Arc too large, by autoscale.
|
||||||
|
path1 = Path(arc)
|
||||||
|
path2 = Path(*path1)
|
||||||
|
path2.approximate_arcs_with_cubics(error=0.1)
|
||||||
|
d = abs(path1.length() - path2.length())
|
||||||
|
# Error less than 0.1% typically less than 0.001%
|
||||||
|
self.assertAlmostEqual(d,0.0, delta=2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestPath(unittest.TestCase):
|
class TestPath(unittest.TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue