2016-07-06 04:51:11 +00:00
|
|
|
"""This submodule contains the class definitions of the the main five classes
|
|
|
|
svgpathtools is built around: Path, Line, QuadraticBezier, CubicBezier, and
|
|
|
|
Arc."""
|
|
|
|
|
|
|
|
# External dependencies
|
|
|
|
from __future__ import division, absolute_import, print_function
|
2020-07-06 18:01:15 +00:00
|
|
|
import re
|
2020-06-20 01:52:09 +00:00
|
|
|
try:
|
|
|
|
from collections.abc import MutableSequence # noqa
|
|
|
|
except ImportError:
|
|
|
|
from collections import MutableSequence # noqa
|
2016-07-06 04:51:11 +00:00
|
|
|
from warnings import warn
|
|
|
|
from operator import itemgetter
|
|
|
|
import numpy as np
|
2020-12-02 06:09:20 +00:00
|
|
|
|
|
|
|
# these imports were originally from math and cmath, now are from numpy
|
|
|
|
# in order to encourage code that generalizes to vector inputs
|
|
|
|
from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \
|
|
|
|
degrees, radians, log, pi, ceil
|
|
|
|
from numpy import exp, sqrt as csqrt, angle as phase
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
try:
|
|
|
|
from scipy.integrate import quad
|
|
|
|
_quad_available = True
|
|
|
|
except:
|
|
|
|
_quad_available = False
|
|
|
|
|
|
|
|
# Internal dependencies
|
|
|
|
from .bezier import (bezier_intersections, bezier_bounding_box, split_bezier,
|
2018-06-10 21:38:08 +00:00
|
|
|
bezier_by_line_intersections, polynomial2bezier,
|
|
|
|
bezier2polynomial)
|
2016-07-06 04:51:11 +00:00
|
|
|
from .misctools import BugException
|
|
|
|
from .polytools import rational_limit, polyroots, polyroots01, imag, real
|
|
|
|
|
2020-07-06 18:01:15 +00:00
|
|
|
# To maintain forward/backward compatibility
|
|
|
|
try:
|
|
|
|
str = basestring
|
|
|
|
except NameError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
|
|
|
UPPERCASE = set('MZLHVCSQTA')
|
|
|
|
|
|
|
|
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
|
|
|
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
# Default Parameters ##########################################################
|
|
|
|
|
|
|
|
# path segment .length() parameters for arc length computation
|
|
|
|
LENGTH_MIN_DEPTH = 5
|
|
|
|
LENGTH_ERROR = 1e-12
|
|
|
|
USE_SCIPY_QUAD = True # for elliptic Arc segment arc length computation
|
|
|
|
|
|
|
|
# path segment .ilength() parameters for inverse arc length computation
|
|
|
|
ILENGTH_MIN_DEPTH = 5
|
|
|
|
ILENGTH_ERROR = 1e-12
|
|
|
|
ILENGTH_S_TOL = 1e-12
|
|
|
|
ILENGTH_MAXITS = 10000
|
|
|
|
|
|
|
|
# compatibility/implementation related warnings and parameters
|
|
|
|
CLOSED_WARNING_ON = True
|
|
|
|
_NotImplemented4ArcException = \
|
|
|
|
Exception("This method has not yet been implemented for Arc objects.")
|
|
|
|
# _NotImplemented4QuadraticException = \
|
|
|
|
# Exception("This method has not yet been implemented for QuadraticBezier "
|
|
|
|
# "objects.")
|
|
|
|
_is_smooth_from_warning = \
|
|
|
|
("The name of this method is somewhat misleading (yet kept for "
|
|
|
|
"compatibility with scripts created using svg.path 2.0). This method "
|
|
|
|
"is meant only for d-string creation and should NOT be used to check "
|
|
|
|
"for kinks. To check a segment for differentiability, use the "
|
|
|
|
"joins_smoothly_with() method instead or the kinks() function (in "
|
|
|
|
"smoothing.py).\nTo turn off this warning, set "
|
|
|
|
"warning_on=False.")
|
|
|
|
|
|
|
|
|
|
|
|
# Miscellaneous ###############################################################
|
|
|
|
|
|
|
|
def bezier_segment(*bpoints):
|
|
|
|
if len(bpoints) == 2:
|
|
|
|
return Line(*bpoints)
|
|
|
|
elif len(bpoints) == 4:
|
|
|
|
return CubicBezier(*bpoints)
|
|
|
|
elif len(bpoints) == 3:
|
|
|
|
return QuadraticBezier(*bpoints)
|
|
|
|
else:
|
|
|
|
assert len(bpoints) in (2, 3, 4)
|
|
|
|
|
|
|
|
|
|
|
|
def is_bezier_segment(seg):
|
|
|
|
return (isinstance(seg, Line) or
|
|
|
|
isinstance(seg, QuadraticBezier) or
|
|
|
|
isinstance(seg, CubicBezier))
|
|
|
|
|
|
|
|
|
|
|
|
def is_path_segment(seg):
|
|
|
|
return is_bezier_segment(seg) or isinstance(seg, Arc)
|
|
|
|
|
|
|
|
|
|
|
|
def is_bezier_path(path):
|
|
|
|
"""Checks that all segments in path are a Line, QuadraticBezier, or
|
|
|
|
CubicBezier object."""
|
|
|
|
return isinstance(path, Path) and all(map(is_bezier_segment, path))
|
|
|
|
|
|
|
|
|
|
|
|
def concatpaths(list_of_paths):
|
|
|
|
"""Takes in a sequence of paths and returns their concatenations into a
|
|
|
|
single path (following the order of the input sequence)."""
|
|
|
|
return Path(*[seg for path in list_of_paths for seg in path])
|
|
|
|
|
|
|
|
|
|
|
|
def bbox2path(xmin, xmax, ymin, ymax):
|
|
|
|
"""Converts a bounding box 4-tuple to a Path object."""
|
|
|
|
b = Line(xmin + 1j*ymin, xmax + 1j*ymin)
|
|
|
|
t = Line(xmin + 1j*ymax, xmax + 1j*ymax)
|
|
|
|
r = Line(xmax + 1j*ymin, xmax + 1j*ymax)
|
|
|
|
l = Line(xmin + 1j*ymin, xmin + 1j*ymax)
|
|
|
|
return Path(b, r, t.reversed(), l.reversed())
|
|
|
|
|
|
|
|
|
2017-04-26 08:54:44 +00:00
|
|
|
def polyline(*points):
|
|
|
|
"""Converts a list of points to a Path composed of lines connecting those
|
|
|
|
points (i.e. a linear spline or polyline). See also `polygon()`."""
|
|
|
|
return Path(*[Line(points[i], points[i+1])
|
|
|
|
for i in range(len(points) - 1)])
|
|
|
|
|
|
|
|
|
|
|
|
def polygon(*points):
|
|
|
|
"""Converts a list of points to a Path composed of lines connecting those
|
|
|
|
points, then closes the path by connecting the last point to the first.
|
|
|
|
See also `polyline()`."""
|
|
|
|
return Path(*[Line(points[i], points[(i + 1) % len(points)])
|
|
|
|
for i in range(len(points))])
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
# Conversion###################################################################
|
|
|
|
|
|
|
|
def bpoints2bezier(bpoints):
|
|
|
|
"""Converts a list of length 2, 3, or 4 to a CubicBezier, QuadraticBezier,
|
|
|
|
or Line object, respectively.
|
|
|
|
See also: poly2bez."""
|
|
|
|
order = len(bpoints) - 1
|
|
|
|
if order == 3:
|
|
|
|
return CubicBezier(*bpoints)
|
|
|
|
elif order == 2:
|
|
|
|
return QuadraticBezier(*bpoints)
|
|
|
|
elif order == 1:
|
|
|
|
return Line(*bpoints)
|
|
|
|
else:
|
|
|
|
assert len(bpoints) in {2, 3, 4}
|
|
|
|
|
|
|
|
|
|
|
|
def poly2bez(poly, return_bpoints=False):
|
|
|
|
"""Converts a cubic or lower order Polynomial object (or a sequence of
|
|
|
|
coefficients) to a CubicBezier, QuadraticBezier, or Line object as
|
|
|
|
appropriate. If return_bpoints=True then this will instead only return
|
|
|
|
the control points of the corresponding Bezier curve.
|
|
|
|
Note: The inverse operation is available as a method of CubicBezier,
|
|
|
|
QuadraticBezier and Line objects."""
|
|
|
|
bpoints = polynomial2bezier(poly)
|
|
|
|
if return_bpoints:
|
|
|
|
return bpoints
|
|
|
|
else:
|
|
|
|
return bpoints2bezier(bpoints)
|
|
|
|
|
|
|
|
|
|
|
|
def bez2poly(bez, numpy_ordering=True, return_poly1d=False):
|
|
|
|
"""Converts a Bezier object or tuple of Bezier control points to a tuple
|
|
|
|
of coefficients of the expanded polynomial.
|
|
|
|
return_poly1d : returns a numpy.poly1d object. This makes computations
|
|
|
|
of derivatives/anti-derivatives and many other operations quite quick.
|
|
|
|
numpy_ordering : By default (to accommodate numpy) the coefficients will
|
|
|
|
be output in reverse standard order.
|
|
|
|
Note: This function is redundant thanks to the .poly() method included
|
|
|
|
with all bezier segment classes."""
|
|
|
|
if is_bezier_segment(bez):
|
|
|
|
bez = bez.bpoints()
|
|
|
|
return bezier2polynomial(bez,
|
|
|
|
numpy_ordering=numpy_ordering,
|
|
|
|
return_poly1d=return_poly1d)
|
|
|
|
|
|
|
|
|
|
|
|
# Geometric####################################################################
|
|
|
|
|
|
|
|
def rotate(curve, degs, origin=None):
|
|
|
|
"""Returns curve rotated by `degs` degrees (CCW) around the point `origin`
|
|
|
|
(a complex number). By default origin is either `curve.point(0.5)`, or in
|
|
|
|
the case that curve is an Arc object, `origin` defaults to `curve.center`.
|
|
|
|
"""
|
|
|
|
def transform(z):
|
|
|
|
return exp(1j*radians(degs))*(z - origin) + origin
|
|
|
|
|
2018-05-23 02:59:03 +00:00
|
|
|
if origin is None:
|
2016-07-06 04:51:11 +00:00
|
|
|
if isinstance(curve, Arc):
|
|
|
|
origin = curve.center
|
|
|
|
else:
|
|
|
|
origin = curve.point(0.5)
|
|
|
|
|
|
|
|
if isinstance(curve, Path):
|
|
|
|
return Path(*[rotate(seg, degs, origin=origin) for seg in curve])
|
|
|
|
elif is_bezier_segment(curve):
|
|
|
|
return bpoints2bezier([transform(bpt) for bpt in curve.bpoints()])
|
|
|
|
elif isinstance(curve, Arc):
|
|
|
|
new_start = transform(curve.start)
|
|
|
|
new_end = transform(curve.end)
|
|
|
|
new_rotation = curve.rotation + degs
|
|
|
|
return Arc(new_start, radius=curve.radius, rotation=new_rotation,
|
|
|
|
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
|
|
|
|
else:
|
|
|
|
raise TypeError("Input `curve` should be a Path, Line, "
|
|
|
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
|
|
|
|
|
|
|
|
|
|
|
def translate(curve, z0):
|
|
|
|
"""Shifts the curve by the complex quantity z such that
|
|
|
|
translate(curve, z0).point(t) = curve.point(t) + z0"""
|
|
|
|
if isinstance(curve, Path):
|
|
|
|
return Path(*[translate(seg, z0) for seg in curve])
|
|
|
|
elif is_bezier_segment(curve):
|
|
|
|
return bpoints2bezier([bpt + z0 for bpt in curve.bpoints()])
|
|
|
|
elif isinstance(curve, Arc):
|
|
|
|
new_start = curve.start + z0
|
|
|
|
new_end = curve.end + z0
|
|
|
|
return Arc(new_start, radius=curve.radius, rotation=curve.rotation,
|
|
|
|
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
|
|
|
|
else:
|
|
|
|
raise TypeError("Input `curve` should be a Path, Line, "
|
|
|
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
|
|
|
|
2018-05-23 02:59:03 +00:00
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scale(curve, sx, sy=None, origin=0j):
|
|
|
|
"""Scales `curve`, about `origin`, by diagonal matrix `[[sx,0],[0,sy]]`.
|
|
|
|
|
|
|
|
Notes:
|
|
|
|
------
|
|
|
|
* If `sy` is not specified, it is assumed to be equal to `sx` and
|
|
|
|
a scalar transformation of `curve` about `origin` will be returned.
|
|
|
|
I.e.
|
|
|
|
scale(curve, sx, origin).point(t) ==
|
|
|
|
((curve.point(t) - origin) * sx) + origin
|
2018-05-23 02:59:03 +00:00
|
|
|
"""
|
2018-05-22 19:45:04 +00:00
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
if sy is None:
|
|
|
|
isy = 1j*sx
|
|
|
|
else:
|
|
|
|
isy = 1j*sy
|
|
|
|
|
2018-07-07 02:59:28 +00:00
|
|
|
def _scale(z):
|
|
|
|
if sy is None:
|
|
|
|
return sx*z
|
|
|
|
return sx*z.real + isy*z.imag
|
|
|
|
|
|
|
|
def scale_bezier(bez):
|
|
|
|
p = [_scale(c) for c in bez2poly(bez)]
|
|
|
|
p[-1] += origin - _scale(origin)
|
|
|
|
return poly2bez(p)
|
2018-05-22 19:45:04 +00:00
|
|
|
|
|
|
|
if isinstance(curve, Path):
|
2018-05-31 02:07:58 +00:00
|
|
|
return Path(*[scale(seg, sx, sy, origin) for seg in curve])
|
2018-05-22 19:45:04 +00:00
|
|
|
elif is_bezier_segment(curve):
|
2018-07-07 02:59:28 +00:00
|
|
|
return scale_bezier(curve)
|
2018-05-22 19:45:04 +00:00
|
|
|
elif isinstance(curve, Arc):
|
2018-06-09 05:13:18 +00:00
|
|
|
if sy is None or sy == sx:
|
2018-07-07 02:59:28 +00:00
|
|
|
return Arc(start=sx*(curve.start - origin) + origin,
|
|
|
|
radius=sx*curve.radius,
|
2018-05-31 02:07:58 +00:00
|
|
|
rotation=curve.rotation,
|
|
|
|
large_arc=curve.large_arc,
|
|
|
|
sweep=curve.sweep,
|
2018-07-07 02:59:28 +00:00
|
|
|
end=sx*(curve.end - origin) + origin)
|
2018-05-31 02:07:58 +00:00
|
|
|
else:
|
2018-07-07 02:59:28 +00:00
|
|
|
raise Exception("\nFor `Arc` objects, only scale transforms "
|
|
|
|
"with sx==sy are implemented.\n")
|
2018-05-22 19:45:04 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Input `curve` should be a Path, Line, "
|
|
|
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2018-08-22 01:00:29 +00:00
|
|
|
def transform(curve, tf):
|
|
|
|
"""Transforms the curve by the homogeneous transformation matrix tf"""
|
|
|
|
def to_point(p):
|
2018-08-22 03:05:59 +00:00
|
|
|
return np.array([[p.real], [p.imag], [1.0]])
|
2018-08-22 01:00:29 +00:00
|
|
|
|
|
|
|
def to_vector(z):
|
2018-08-22 03:05:59 +00:00
|
|
|
return np.array([[z.real], [z.imag], [0.0]])
|
2018-08-22 01:00:29 +00:00
|
|
|
|
|
|
|
def to_complex(v):
|
|
|
|
return v.item(0) + 1j * v.item(1)
|
|
|
|
|
|
|
|
if isinstance(curve, Path):
|
|
|
|
return Path(*[transform(segment, tf) for segment in curve])
|
|
|
|
elif is_bezier_segment(curve):
|
2018-08-22 03:54:02 +00:00
|
|
|
return bpoints2bezier([to_complex(tf.dot(to_point(p)))
|
|
|
|
for p in curve.bpoints()])
|
2018-08-22 01:00:29 +00:00
|
|
|
elif isinstance(curve, Arc):
|
|
|
|
new_start = to_complex(tf.dot(to_point(curve.start)))
|
|
|
|
new_end = to_complex(tf.dot(to_point(curve.end)))
|
|
|
|
new_radius = to_complex(tf.dot(to_vector(curve.radius)))
|
2020-06-20 01:29:36 +00:00
|
|
|
if tf[0][0] * tf[1][1] >= 0.0:
|
|
|
|
new_sweep = curve.sweep
|
|
|
|
else:
|
|
|
|
new_sweep = not curve.sweep
|
2018-08-22 01:00:29 +00:00
|
|
|
return Arc(new_start, radius=new_radius, rotation=curve.rotation,
|
2020-06-20 01:29:36 +00:00
|
|
|
large_arc=curve.large_arc, sweep=new_sweep, end=new_end)
|
2018-08-22 01:00:29 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Input `curve` should be a Path, Line, "
|
|
|
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
|
|
|
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def bezier_unit_tangent(seg, t):
|
|
|
|
"""Returns the unit tangent of the segment at t.
|
|
|
|
|
|
|
|
Notes
|
|
|
|
-----
|
|
|
|
If you receive a RuntimeWarning, try the following:
|
|
|
|
>>> import numpy
|
|
|
|
>>> old_numpy_error_settings = numpy.seterr(invalid='raise')
|
|
|
|
This can be undone with:
|
|
|
|
>>> numpy.seterr(**old_numpy_error_settings)
|
|
|
|
"""
|
2020-12-02 06:49:38 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
dseg = seg.derivative(t)
|
|
|
|
|
|
|
|
# Note: dseg might be numpy value, use np.seterr(invalid='raise')
|
|
|
|
try:
|
|
|
|
unit_tangent = dseg/abs(dseg)
|
|
|
|
except (ZeroDivisionError, FloatingPointError):
|
|
|
|
# This may be a removable singularity, if so we just need to compute
|
|
|
|
# the limit.
|
|
|
|
# Note: limit{{dseg / abs(dseg)} = sqrt(limit{dseg**2 / abs(dseg)**2})
|
|
|
|
dseg_poly = seg.poly().deriv()
|
|
|
|
dseg_abs_squared_poly = (real(dseg_poly) ** 2 +
|
|
|
|
imag(dseg_poly) ** 2)
|
|
|
|
try:
|
|
|
|
unit_tangent = csqrt(rational_limit(dseg_poly**2,
|
|
|
|
dseg_abs_squared_poly, t))
|
|
|
|
except ValueError:
|
|
|
|
bef = seg.poly().deriv()(t - 1e-4)
|
|
|
|
aft = seg.poly().deriv()(t + 1e-4)
|
|
|
|
mes = ("Unit tangent appears to not be well-defined at "
|
|
|
|
"t = {}, \n".format(t) +
|
|
|
|
"seg.poly().deriv()(t - 1e-4) = {}\n".format(bef) +
|
|
|
|
"seg.poly().deriv()(t + 1e-4) = {}".format(aft))
|
|
|
|
raise ValueError(mes)
|
|
|
|
return unit_tangent
|
|
|
|
|
|
|
|
|
|
|
|
def segment_curvature(self, t, use_inf=False):
|
|
|
|
"""returns the curvature of the segment at t.
|
|
|
|
|
|
|
|
Notes
|
|
|
|
-----
|
|
|
|
If you receive a RuntimeWarning, run command
|
|
|
|
>>> old = np.seterr(invalid='raise')
|
|
|
|
This can be undone with
|
|
|
|
>>> np.seterr(**old)
|
|
|
|
"""
|
|
|
|
|
|
|
|
dz = self.derivative(t)
|
|
|
|
ddz = self.derivative(t, n=2)
|
|
|
|
dx, dy = dz.real, dz.imag
|
|
|
|
ddx, ddy = ddz.real, ddz.imag
|
|
|
|
old_np_seterr = np.seterr(invalid='raise')
|
|
|
|
try:
|
|
|
|
kappa = abs(dx*ddy - dy*ddx)/sqrt(dx*dx + dy*dy)**3
|
|
|
|
except (ZeroDivisionError, FloatingPointError):
|
|
|
|
# tangent vector is zero at t, use polytools to find limit
|
|
|
|
p = self.poly()
|
|
|
|
dp = p.deriv()
|
|
|
|
ddp = dp.deriv()
|
|
|
|
dx, dy = real(dp), imag(dp)
|
|
|
|
ddx, ddy = real(ddp), imag(ddp)
|
|
|
|
f2 = (dx*ddy - dy*ddx)**2
|
|
|
|
g2 = (dx*dx + dy*dy)**3
|
|
|
|
lim2 = rational_limit(f2, g2, t)
|
|
|
|
if lim2 < 0: # impossible, must be numerical error
|
|
|
|
return 0
|
|
|
|
kappa = sqrt(lim2)
|
|
|
|
finally:
|
|
|
|
np.seterr(**old_np_seterr)
|
|
|
|
return kappa
|
|
|
|
|
|
|
|
|
|
|
|
def bezier_radialrange(seg, origin, return_all_global_extrema=False):
|
|
|
|
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize and
|
|
|
|
maximize, respectively, the distance d = |self.point(t)-origin|.
|
|
|
|
return_all_global_extrema: Multiple such t_min or t_max values can exist.
|
|
|
|
By default, this will only return one. Set return_all_global_extrema=True
|
|
|
|
to return all such global extrema."""
|
|
|
|
|
|
|
|
def _radius(tau):
|
|
|
|
return abs(seg.point(tau) - origin)
|
|
|
|
|
|
|
|
shifted_seg_poly = seg.poly() - origin
|
|
|
|
r_squared = real(shifted_seg_poly) ** 2 + \
|
|
|
|
imag(shifted_seg_poly) ** 2
|
|
|
|
extremizers = [0, 1] + polyroots01(r_squared.deriv())
|
|
|
|
extrema = [(_radius(t), t) for t in extremizers]
|
|
|
|
|
|
|
|
if return_all_global_extrema:
|
|
|
|
raise NotImplementedError
|
|
|
|
else:
|
|
|
|
seg_global_min = min(extrema, key=itemgetter(0))
|
|
|
|
seg_global_max = max(extrema, key=itemgetter(0))
|
|
|
|
return seg_global_min, seg_global_max
|
|
|
|
|
|
|
|
|
|
|
|
def closest_point_in_path(pt, path):
|
|
|
|
"""returns (|path.seg.point(t)-pt|, t, seg_idx) where t and seg_idx
|
|
|
|
minimize the distance between pt and curve path[idx].point(t) for 0<=t<=1
|
|
|
|
and any seg_idx.
|
|
|
|
Warning: Multiple such global minima can exist. This will only return
|
|
|
|
one."""
|
|
|
|
return path.radialrange(pt)[0]
|
|
|
|
|
|
|
|
|
|
|
|
def farthest_point_in_path(pt, path):
|
|
|
|
"""returns (|path.seg.point(t)-pt|, t, seg_idx) where t and seg_idx
|
|
|
|
maximize the distance between pt and curve path[idx].point(t) for 0<=t<=1
|
|
|
|
and any seg_idx.
|
|
|
|
:rtype : object
|
|
|
|
:param pt:
|
|
|
|
:param path:
|
|
|
|
Warning: Multiple such global maxima can exist. This will only return
|
|
|
|
one."""
|
|
|
|
return path.radialrange(pt)[1]
|
|
|
|
|
|
|
|
|
|
|
|
def path_encloses_pt(pt, opt, path):
|
|
|
|
"""returns true if pt is a point enclosed by path (which must be a Path
|
|
|
|
object satisfying path.isclosed==True). opt is a point you know is
|
|
|
|
NOT enclosed by path."""
|
|
|
|
assert path.isclosed()
|
|
|
|
intersections = Path(Line(pt, opt)).intersect(path)
|
|
|
|
if len(intersections) % 2:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def segment_length(curve, start, end, start_point, end_point,
|
|
|
|
error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH, depth=0):
|
|
|
|
"""Recursively approximates the length by straight lines"""
|
|
|
|
mid = (start + end)/2
|
|
|
|
mid_point = curve.point(mid)
|
|
|
|
length = abs(end_point - start_point)
|
|
|
|
first_half = abs(mid_point - start_point)
|
|
|
|
second_half = abs(end_point - mid_point)
|
|
|
|
|
|
|
|
length2 = first_half + second_half
|
|
|
|
if (length2 - length > error) or (depth < min_depth):
|
|
|
|
# Calculate the length of each segment:
|
|
|
|
depth += 1
|
|
|
|
return (segment_length(curve, start, mid, start_point, mid_point,
|
|
|
|
error, min_depth, depth) +
|
|
|
|
segment_length(curve, mid, end, mid_point, end_point,
|
|
|
|
error, min_depth, depth))
|
|
|
|
# This is accurate enough.
|
|
|
|
return length2
|
|
|
|
|
|
|
|
|
|
|
|
def inv_arclength(curve, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""INPUT: curve should be a CubicBezier, Line, of Path of CubicBezier
|
|
|
|
and/or Line objects.
|
|
|
|
OUTPUT: Returns a float, t, such that the arc length of curve from 0 to
|
|
|
|
t is approximately s.
|
|
|
|
s_tol - exit when |s(t) - s| < s_tol where
|
|
|
|
s(t) = seg.length(0, t, error, min_depth) and seg is either curve or,
|
|
|
|
if curve is a Path object, then seg is a segment in curve.
|
|
|
|
error - used to compute lengths of cubics and arcs
|
|
|
|
min_depth - used to compute lengths of cubics and arcs
|
2017-03-01 07:04:37 +00:00
|
|
|
Note: This function is not designed to be efficient, but if it's slower
|
|
|
|
than you need, make sure you have scipy installed."""
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
curve_length = curve.length(error=error, min_depth=min_depth)
|
|
|
|
assert curve_length > 0
|
|
|
|
if not 0 <= s <= curve_length:
|
|
|
|
raise ValueError("s is not in interval [0, curve.length()].")
|
|
|
|
|
|
|
|
if s == 0:
|
|
|
|
return 0
|
|
|
|
if s == curve_length:
|
|
|
|
return 1
|
|
|
|
|
|
|
|
if isinstance(curve, Path):
|
2018-08-22 03:54:02 +00:00
|
|
|
seg_lengths = [seg.length(error=error, min_depth=min_depth)
|
|
|
|
for seg in curve]
|
2016-07-06 04:51:11 +00:00
|
|
|
lsum = 0
|
|
|
|
# Find which segment the point we search for is located on
|
|
|
|
for k, len_k in enumerate(seg_lengths):
|
|
|
|
if lsum <= s <= lsum + len_k:
|
2018-08-22 03:54:02 +00:00
|
|
|
t = inv_arclength(curve[k], s - lsum, s_tol=s_tol,
|
|
|
|
maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
2016-07-06 04:51:11 +00:00
|
|
|
return curve.t2T(k, t)
|
|
|
|
lsum += len_k
|
|
|
|
return 1
|
|
|
|
|
|
|
|
elif isinstance(curve, Line):
|
|
|
|
return s / curve.length(error=error, min_depth=min_depth)
|
|
|
|
|
|
|
|
elif (isinstance(curve, QuadraticBezier) or
|
|
|
|
isinstance(curve, CubicBezier) or
|
|
|
|
isinstance(curve, Arc)):
|
|
|
|
t_upper = 1
|
|
|
|
t_lower = 0
|
|
|
|
iteration = 0
|
|
|
|
while iteration < maxits:
|
|
|
|
iteration += 1
|
|
|
|
t = (t_lower + t_upper)/2
|
|
|
|
s_t = curve.length(t1=t, error=error, min_depth=min_depth)
|
|
|
|
if abs(s_t - s) < s_tol:
|
|
|
|
return t
|
|
|
|
elif s_t < s: # t too small
|
|
|
|
t_lower = t
|
|
|
|
else: # s < s_t, t too big
|
|
|
|
t_upper = t
|
|
|
|
if t_upper == t_lower:
|
|
|
|
warn("t is as close as a float can be to the correct value, "
|
|
|
|
"but |s(t) - s| = {} > s_tol".format(abs(s_t-s)))
|
|
|
|
return t
|
|
|
|
raise Exception("Maximum iterations reached with s(t) - s = {}."
|
|
|
|
"".format(s_t - s))
|
|
|
|
else:
|
|
|
|
raise TypeError("First argument must be a Line, QuadraticBezier, "
|
|
|
|
"CubicBezier, Arc, or Path object.")
|
|
|
|
|
|
|
|
# Operations###################################################################
|
|
|
|
|
|
|
|
|
|
|
|
def crop_bezier(seg, t0, t1):
|
|
|
|
"""returns a cropped copy of this segment which starts at self.point(t0)
|
|
|
|
and ends at self.point(t1)."""
|
|
|
|
assert t0 < t1
|
|
|
|
if t0 == 0:
|
|
|
|
cropped_seg = seg.split(t1)[0]
|
|
|
|
elif t1 == 1:
|
|
|
|
cropped_seg = seg.split(t0)[1]
|
|
|
|
else:
|
|
|
|
pt1 = seg.point(t1)
|
|
|
|
|
|
|
|
# trim off the 0 <= t < t0 part
|
|
|
|
trimmed_seg = crop_bezier(seg, t0, 1)
|
|
|
|
|
|
|
|
# find the adjusted t1 (i.e. the t1 such that
|
|
|
|
# trimmed_seg.point(t1) ~= pt))and trim off the t1 < t <= 1 part
|
|
|
|
t1_adj = trimmed_seg.radialrange(pt1)[0][1]
|
|
|
|
cropped_seg = crop_bezier(trimmed_seg, 0, t1_adj)
|
|
|
|
return cropped_seg
|
|
|
|
|
|
|
|
|
|
|
|
# Main Classes ################################################################
|
|
|
|
|
|
|
|
|
|
|
|
class Line(object):
|
|
|
|
def __init__(self, start, end):
|
|
|
|
self.start = start
|
|
|
|
self.end = end
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return 'Line(start=%s, end=%s)' % (self.start, self.end)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not isinstance(other, Line):
|
|
|
|
return NotImplemented
|
|
|
|
return self.start == other.start and self.end == other.end
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
if not isinstance(other, Line):
|
|
|
|
return NotImplemented
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
return self.bpoints()[item]
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return 2
|
|
|
|
|
|
|
|
def joins_smoothly_with(self, previous, wrt_parameterization=False):
|
|
|
|
"""Checks if this segment joins smoothly with previous segment. By
|
|
|
|
default, this only checks that this segment starts moving (at t=0) in
|
|
|
|
the same direction (and from the same positive) as previous stopped
|
|
|
|
moving (at t=1). To check if the tangent magnitudes also match, set
|
|
|
|
wrt_parameterization=True."""
|
|
|
|
if wrt_parameterization:
|
|
|
|
return self.start == previous.end and np.isclose(
|
|
|
|
self.derivative(0), previous.derivative(1))
|
|
|
|
else:
|
|
|
|
return self.start == previous.end and np.isclose(
|
|
|
|
self.unit_tangent(0), previous.unit_tangent(1))
|
|
|
|
|
|
|
|
def point(self, t):
|
|
|
|
"""returns the coordinates of the Bezier curve evaluated at t."""
|
|
|
|
distance = self.end - self.start
|
|
|
|
return self.start + distance*t
|
|
|
|
|
2020-06-24 05:16:09 +00:00
|
|
|
def points(self, ts):
|
|
|
|
"""Faster than running Path.point many times."""
|
|
|
|
return self.poly(ts)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
|
|
|
"""returns the length of the line segment between t0 and t1."""
|
|
|
|
return abs(self.end - self.start)*(t1-t0)
|
|
|
|
|
|
|
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
|
|
|
See the inv_arclength() docstring for more details."""
|
|
|
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
|
|
|
|
|
|
|
def bpoints(self):
|
|
|
|
"""returns the Bezier control points of the segment."""
|
|
|
|
return self.start, self.end
|
|
|
|
|
|
|
|
def poly(self, return_coeffs=False):
|
|
|
|
"""returns the line as a Polynomial object."""
|
|
|
|
p = self.bpoints()
|
|
|
|
coeffs = ([p[1] - p[0], p[0]])
|
|
|
|
if return_coeffs:
|
|
|
|
return coeffs
|
|
|
|
else:
|
|
|
|
return np.poly1d(coeffs)
|
|
|
|
|
|
|
|
def derivative(self, t=None, n=1):
|
|
|
|
"""returns the nth derivative of the segment at t."""
|
|
|
|
assert self.end != self.start
|
|
|
|
if n == 1:
|
|
|
|
return self.end - self.start
|
|
|
|
elif n > 1:
|
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
raise ValueError("n should be a positive integer.")
|
|
|
|
|
|
|
|
def unit_tangent(self, t=None):
|
|
|
|
"""returns the unit tangent of the segment at t."""
|
|
|
|
assert self.end != self.start
|
|
|
|
dseg = self.end - self.start
|
|
|
|
return dseg/abs(dseg)
|
|
|
|
|
|
|
|
def normal(self, t=None):
|
|
|
|
"""returns the (right hand rule) unit normal vector to self at t."""
|
|
|
|
return -1j*self.unit_tangent(t)
|
|
|
|
|
|
|
|
def curvature(self, t):
|
|
|
|
"""returns the curvature of the line, which is always zero."""
|
|
|
|
return 0
|
|
|
|
|
2016-07-24 00:15:22 +00:00
|
|
|
# def icurvature(self, kappa):
|
|
|
|
# """returns a list of t-values such that 0 <= t<= 1 and
|
|
|
|
# seg.curvature(t) = kappa."""
|
|
|
|
# if kappa:
|
|
|
|
# raise ValueError("The .icurvature() method for Line elements will "
|
|
|
|
# "return an empty list if kappa is nonzero and "
|
|
|
|
# "will raise this exception when kappa is zero as "
|
|
|
|
# "this is true at every point on the line.")
|
|
|
|
# return []
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def reversed(self):
|
|
|
|
"""returns a copy of the Line object with its orientation reversed."""
|
|
|
|
return Line(self.end, self.start)
|
|
|
|
|
|
|
|
def intersect(self, other_seg, tol=None):
|
|
|
|
"""Finds the intersections of two segments.
|
|
|
|
returns a list of tuples (t1, t2) such that
|
|
|
|
self.point(t1) == other_seg.point(t2).
|
|
|
|
Note: This will fail if the two segments coincide for more than a
|
|
|
|
finite collection of points.
|
|
|
|
tol is not used."""
|
|
|
|
if isinstance(other_seg, Line):
|
|
|
|
assert other_seg.end != other_seg.start and self.end != self.start
|
|
|
|
assert self != other_seg
|
|
|
|
# Solve the system [p1-p0, q1-q0]*[t1, t2]^T = q0 - p0
|
|
|
|
# where self == Line(p0, p1) and other_seg == Line(q0, q1)
|
|
|
|
a = (self.start.real, self.end.real)
|
|
|
|
b = (self.start.imag, self.end.imag)
|
|
|
|
c = (other_seg.start.real, other_seg.end.real)
|
|
|
|
d = (other_seg.start.imag, other_seg.end.imag)
|
|
|
|
denom = ((a[1] - a[0])*(d[0] - d[1]) -
|
|
|
|
(b[1] - b[0])*(c[0] - c[1]))
|
2018-02-28 06:38:01 +00:00
|
|
|
if np.isclose(denom, 0):
|
2016-07-06 04:51:11 +00:00
|
|
|
return []
|
|
|
|
t1 = (c[0]*(b[0] - d[1]) -
|
|
|
|
c[1]*(b[0] - d[0]) -
|
|
|
|
a[0]*(d[0] - d[1]))/denom
|
|
|
|
t2 = -(a[1]*(b[0] - d[0]) -
|
|
|
|
a[0]*(b[1] - d[0]) -
|
|
|
|
c[0]*(b[0] - b[1]))/denom
|
|
|
|
if 0 <= t1 <= 1 and 0 <= t2 <= 1:
|
|
|
|
return [(t1, t2)]
|
|
|
|
return []
|
|
|
|
elif isinstance(other_seg, QuadraticBezier):
|
2017-02-21 03:21:07 +00:00
|
|
|
t2t1s = bezier_by_line_intersections(other_seg, self)
|
|
|
|
return [(t1, t2) for t2, t1 in t2t1s]
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(other_seg, CubicBezier):
|
2017-02-21 03:21:07 +00:00
|
|
|
t2t1s = bezier_by_line_intersections(other_seg, self)
|
|
|
|
return [(t1, t2) for t2, t1 in t2t1s]
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(other_seg, Arc):
|
|
|
|
t2t1s = other_seg.intersect(self)
|
|
|
|
return [(t1, t2) for t2, t1 in t2t1s]
|
|
|
|
elif isinstance(other_seg, Path):
|
|
|
|
raise TypeError(
|
|
|
|
"other_seg must be a path segment, not a Path object, use "
|
|
|
|
"Path.intersect().")
|
|
|
|
else:
|
|
|
|
raise TypeError("other_seg must be a path segment.")
|
|
|
|
|
|
|
|
def bbox(self):
|
|
|
|
"""returns the bounding box for the segment in the form
|
|
|
|
(xmin, xmax, ymin, ymax)."""
|
|
|
|
xmin = min(self.start.real, self.end.real)
|
|
|
|
xmax = max(self.start.real, self.end.real)
|
|
|
|
ymin = min(self.start.imag, self.end.imag)
|
|
|
|
ymax = max(self.start.imag, self.end.imag)
|
|
|
|
return xmin, xmax, ymin, ymax
|
|
|
|
|
2018-11-05 04:55:17 +00:00
|
|
|
def point_to_t(self, point):
|
|
|
|
"""If the point lies on the Line, returns its `t` parameter.
|
|
|
|
If the point does not lie on the Line, returns None."""
|
|
|
|
|
|
|
|
# Single-precision floats have only 7 significant figures of
|
|
|
|
# resolution, so test that we're within 6 sig figs.
|
|
|
|
if np.isclose(point, self.start, rtol=0, atol=1e-6):
|
|
|
|
return 0.0
|
|
|
|
elif np.isclose(point, self.end, rtol=0, atol=1e-6):
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
# Finding the point "by hand" here is much faster than calling
|
|
|
|
# radialrange(), see the discussion on PR #40:
|
|
|
|
# https://github.com/mathandy/svgpathtools/pull/40#issuecomment-358134261
|
|
|
|
|
|
|
|
p = self.poly()
|
|
|
|
# p(t) = (p_1 * t) + p_0 = point
|
|
|
|
# t = (point - p_0) / p_1
|
|
|
|
t = (point - p[0]) / p[1]
|
|
|
|
if np.isclose(t.imag, 0) and (t.real >= 0.0) and (t.real <= 1.0):
|
|
|
|
return t.real
|
|
|
|
return None
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def cropped(self, t0, t1):
|
|
|
|
"""returns a cropped copy of this segment which starts at
|
|
|
|
self.point(t0) and ends at self.point(t1)."""
|
|
|
|
return Line(self.point(t0), self.point(t1))
|
|
|
|
|
|
|
|
def split(self, t):
|
|
|
|
"""returns two segments, whose union is this segment and which join at
|
|
|
|
self.point(t)."""
|
|
|
|
pt = self.point(t)
|
|
|
|
return Line(self.start, pt), Line(pt, self.end)
|
|
|
|
|
2020-07-02 05:31:57 +00:00
|
|
|
def radialrange(self, origin, **kwargs):
|
|
|
|
"""compute points in self that are min and max distance to origin.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
origin (complex): the point extremize distance to
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
tuples (d_min, t_min) and (d_max, t_max) which minimize and
|
|
|
|
maximize, respectively, the distance d = |self.point(t)-origin|.
|
|
|
|
"""
|
|
|
|
|
|
|
|
x, y = origin.real, origin.imag
|
|
|
|
p0, p1 = self.start, self.end
|
|
|
|
x0, y0, x1, y1 = p0.real, p0.imag, p1.real, p1.imag
|
|
|
|
|
|
|
|
dx, dy = x1 - x0, y1 - y0
|
|
|
|
numerator, denominator = dx * (x - x0) + dy * (y - y0), dx * dx + dy * dy
|
|
|
|
t = numerator / denominator
|
|
|
|
|
|
|
|
if 0 < t < 1:
|
|
|
|
# get distance to origin at 0, 1, and t
|
|
|
|
d0, d1, dt = (
|
|
|
|
abs(p0 - origin),
|
|
|
|
abs(p1 - origin),
|
|
|
|
abs(self.point(t) - origin)
|
|
|
|
)
|
|
|
|
if d0 < d1:
|
|
|
|
return (dt, t), (d1, 1)
|
|
|
|
return (dt, t), (d0, 0)
|
|
|
|
else:
|
|
|
|
# get distance to origin at t = 0 and t = 1
|
|
|
|
d0, d1 = abs(p0 - origin), abs(p1 - origin)
|
|
|
|
if d0 < d1:
|
|
|
|
return (d0, 0), (d1, 1)
|
|
|
|
return (d1, 1), (d0, 0)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def rotated(self, degs, origin=None):
|
|
|
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
|
|
|
point `origin` (a complex number). By default `origin` is either
|
|
|
|
`self.point(0.5)`, or in the case that self is an Arc object,
|
|
|
|
`origin` defaults to `self.center`."""
|
2016-10-28 13:11:31 +00:00
|
|
|
return rotate(self, degs, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def translated(self, z0):
|
|
|
|
"""Returns a copy of self shifted by the complex quantity `z0` such
|
|
|
|
that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
|
|
|
|
return translate(self, z0)
|
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scaled(self, sx, sy=None, origin=0j):
|
|
|
|
"""Scale transform. See `scale` function for further explanation."""
|
|
|
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
2018-05-22 19:45:04 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
class QuadraticBezier(object):
|
|
|
|
# For compatibility with old pickle files.
|
|
|
|
_length_info = {'length': None, 'bpoints': None}
|
|
|
|
|
|
|
|
def __init__(self, start, control, end):
|
|
|
|
self.start = start
|
|
|
|
self.end = end
|
|
|
|
self.control = control
|
|
|
|
|
|
|
|
# used to know if self._length needs to be updated
|
|
|
|
self._length_info = {'length': None, 'bpoints': None}
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
|
|
|
|
self.start, self.control, self.end)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not isinstance(other, QuadraticBezier):
|
|
|
|
return NotImplemented
|
|
|
|
return self.start == other.start and self.end == other.end \
|
|
|
|
and self.control == other.control
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
if not isinstance(other, QuadraticBezier):
|
|
|
|
return NotImplemented
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
return self.bpoints()[item]
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return 3
|
|
|
|
|
|
|
|
def is_smooth_from(self, previous, warning_on=True):
|
|
|
|
"""[Warning: The name of this method is somewhat misleading (yet kept
|
|
|
|
for compatibility with scripts created using svg.path 2.0). This
|
|
|
|
method is meant only for d string creation and should not be used to
|
|
|
|
check for kinks. To check a segment for differentiability, use the
|
|
|
|
joins_smoothly_with() method instead.]"""
|
|
|
|
if warning_on:
|
|
|
|
warn(_is_smooth_from_warning)
|
|
|
|
if isinstance(previous, QuadraticBezier):
|
|
|
|
return (self.start == previous.end and
|
|
|
|
(self.control - self.start) == (
|
|
|
|
previous.end - previous.control))
|
|
|
|
else:
|
|
|
|
return self.control == self.start
|
|
|
|
|
|
|
|
def joins_smoothly_with(self, previous, wrt_parameterization=False,
|
|
|
|
error=0):
|
|
|
|
"""Checks if this segment joins smoothly with previous segment. By
|
|
|
|
default, this only checks that this segment starts moving (at t=0) in
|
|
|
|
the same direction (and from the same positive) as previous stopped
|
|
|
|
moving (at t=1). To check if the tangent magnitudes also match, set
|
|
|
|
wrt_parameterization=True."""
|
|
|
|
if wrt_parameterization:
|
|
|
|
return self.start == previous.end and abs(
|
|
|
|
self.derivative(0) - previous.derivative(1)) <= error
|
|
|
|
else:
|
|
|
|
return self.start == previous.end and abs(
|
|
|
|
self.unit_tangent(0) - previous.unit_tangent(1)) <= error
|
|
|
|
|
|
|
|
def point(self, t):
|
|
|
|
"""returns the coordinates of the Bezier curve evaluated at t."""
|
2020-12-02 06:09:20 +00:00
|
|
|
tc = 1 - t
|
|
|
|
return tc*tc*self.start + 2*tc*t*self.control + t*t*self.end
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2020-06-24 05:16:09 +00:00
|
|
|
def points(self, ts):
|
|
|
|
"""Faster than running Path.point many times."""
|
|
|
|
return self.poly(ts)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
|
|
|
if t0 == 1 and t1 == 0:
|
|
|
|
if self._length_info['bpoints'] == self.bpoints():
|
|
|
|
return self._length_info['length']
|
|
|
|
a = self.start - 2*self.control + self.end
|
|
|
|
b = 2*(self.control - self.start)
|
|
|
|
a_dot_b = a.real*b.real + a.imag*b.imag
|
|
|
|
|
|
|
|
if abs(a) < 1e-12:
|
|
|
|
s = abs(b)*(t1 - t0)
|
|
|
|
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
|
|
|
|
tstar = abs(b)/(2*abs(a))
|
|
|
|
if t1 < tstar:
|
|
|
|
return abs(a)*(t0**2 - t1**2) - abs(b)*(t0 - t1)
|
|
|
|
elif tstar < t0:
|
|
|
|
return abs(a)*(t1**2 - t0**2) - abs(b)*(t1 - t0)
|
|
|
|
else:
|
|
|
|
return abs(a)*(t1**2 + t0**2) - abs(b)*(t1 + t0) + \
|
|
|
|
abs(b)**2/(2*abs(a))
|
|
|
|
else:
|
|
|
|
c2 = 4*(a.real**2 + a.imag**2)
|
|
|
|
c1 = 4*a_dot_b
|
|
|
|
c0 = b.real**2 + b.imag**2
|
|
|
|
|
|
|
|
beta = c1/(2*c2)
|
|
|
|
gamma = c0/c2 - beta**2
|
|
|
|
|
|
|
|
dq1_mag = sqrt(c2*t1**2 + c1*t1 + c0)
|
|
|
|
dq0_mag = sqrt(c2*t0**2 + c1*t0 + c0)
|
|
|
|
logarand = (sqrt(c2)*(t1 + beta) + dq1_mag) / \
|
|
|
|
(sqrt(c2)*(t0 + beta) + dq0_mag)
|
|
|
|
|
|
|
|
s = (t1 + beta)*dq1_mag - (t0 + beta)*dq0_mag + \
|
|
|
|
gamma*sqrt(c2)*log(logarand)
|
|
|
|
s /= 2
|
|
|
|
|
|
|
|
if t0 == 1 and t1 == 0:
|
|
|
|
self._length_info['length'] = s
|
|
|
|
self._length_info['bpoints'] = self.bpoints()
|
|
|
|
return self._length_info['length']
|
|
|
|
else:
|
|
|
|
return s
|
|
|
|
|
|
|
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
|
|
|
See the inv_arclength() docstring for more details."""
|
|
|
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
|
|
|
|
|
|
|
def bpoints(self):
|
|
|
|
"""returns the Bezier control points of the segment."""
|
|
|
|
return self.start, self.control, self.end
|
|
|
|
|
|
|
|
def poly(self, return_coeffs=False):
|
|
|
|
"""returns the quadratic as a Polynomial object."""
|
|
|
|
p = self.bpoints()
|
|
|
|
coeffs = (p[0] - 2*p[1] + p[2], 2*(p[1] - p[0]), p[0])
|
|
|
|
if return_coeffs:
|
|
|
|
return coeffs
|
|
|
|
else:
|
|
|
|
return np.poly1d(coeffs)
|
|
|
|
|
|
|
|
def derivative(self, t, n=1):
|
|
|
|
"""returns the nth derivative of the segment at t.
|
|
|
|
Note: Bezier curves can have points where their derivative vanishes.
|
|
|
|
If you are interested in the tangent direction, use the unit_tangent()
|
|
|
|
method instead."""
|
|
|
|
p = self.bpoints()
|
|
|
|
if n == 1:
|
|
|
|
return 2*((p[1] - p[0])*(1 - t) + (p[2] - p[1])*t)
|
|
|
|
elif n == 2:
|
|
|
|
return 2*(p[2] - 2*p[1] + p[0])
|
|
|
|
elif n > 2:
|
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
raise ValueError("n should be a positive integer.")
|
|
|
|
|
|
|
|
def unit_tangent(self, t):
|
|
|
|
"""returns the unit tangent vector of the segment at t (centered at
|
|
|
|
the origin and expressed as a complex number). If the tangent
|
|
|
|
vector's magnitude is zero, this method will find the limit of
|
|
|
|
self.derivative(tau)/abs(self.derivative(tau)) as tau approaches t."""
|
|
|
|
return bezier_unit_tangent(self, t)
|
|
|
|
|
|
|
|
def normal(self, t):
|
|
|
|
"""returns the (right hand rule) unit normal vector to self at t."""
|
|
|
|
return -1j*self.unit_tangent(t)
|
|
|
|
|
|
|
|
def curvature(self, t):
|
|
|
|
"""returns the curvature of the segment at t."""
|
|
|
|
return segment_curvature(self, t)
|
|
|
|
|
2016-07-24 00:15:22 +00:00
|
|
|
# def icurvature(self, kappa):
|
|
|
|
# """returns a list of t-values such that 0 <= t<= 1 and
|
|
|
|
# seg.curvature(t) = kappa."""
|
|
|
|
# z = self.poly()
|
|
|
|
# x, y = real(z), imag(z)
|
|
|
|
# dx, dy = x.deriv(), y.deriv()
|
|
|
|
# ddx, ddy = dx.deriv(), dy.deriv()
|
|
|
|
#
|
|
|
|
# p = kappa**2*(dx**2 + dy**2)**3 - (dx*ddy - ddx*dy)**2
|
|
|
|
# return polyroots01(p)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def reversed(self):
|
|
|
|
"""returns a copy of the QuadraticBezier object with its orientation
|
|
|
|
reversed."""
|
|
|
|
new_quad = QuadraticBezier(self.end, self.control, self.start)
|
|
|
|
if self._length_info['length']:
|
|
|
|
new_quad._length_info = self._length_info
|
|
|
|
new_quad._length_info['bpoints'] = (
|
|
|
|
self.end, self.control, self.start)
|
|
|
|
return new_quad
|
|
|
|
|
|
|
|
def intersect(self, other_seg, tol=1e-12):
|
|
|
|
"""Finds the intersections of two segments.
|
|
|
|
returns a list of tuples (t1, t2) such that
|
|
|
|
self.point(t1) == other_seg.point(t2).
|
|
|
|
Note: This will fail if the two segments coincide for more than a
|
|
|
|
finite collection of points."""
|
|
|
|
if isinstance(other_seg, Line):
|
|
|
|
return bezier_by_line_intersections(self, other_seg)
|
|
|
|
elif isinstance(other_seg, QuadraticBezier):
|
|
|
|
assert self != other_seg
|
|
|
|
longer_length = max(self.length(), other_seg.length())
|
|
|
|
return bezier_intersections(self, other_seg,
|
|
|
|
longer_length=longer_length,
|
|
|
|
tol=tol, tol_deC=tol)
|
|
|
|
elif isinstance(other_seg, CubicBezier):
|
|
|
|
longer_length = max(self.length(), other_seg.length())
|
|
|
|
return bezier_intersections(self, other_seg,
|
|
|
|
longer_length=longer_length,
|
|
|
|
tol=tol, tol_deC=tol)
|
|
|
|
elif isinstance(other_seg, Arc):
|
2017-02-21 06:10:16 +00:00
|
|
|
t2t1s = other_seg.intersect(self)
|
|
|
|
return [(t1, t2) for t2, t1 in t2t1s]
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(other_seg, Path):
|
|
|
|
raise TypeError(
|
|
|
|
"other_seg must be a path segment, not a Path object, use "
|
|
|
|
"Path.intersect().")
|
|
|
|
else:
|
|
|
|
raise TypeError("other_seg must be a path segment.")
|
|
|
|
|
|
|
|
def bbox(self):
|
|
|
|
"""returns the bounding box for the segment in the form
|
|
|
|
(xmin, xmax, ymin, ymax)."""
|
|
|
|
return bezier_bounding_box(self)
|
|
|
|
|
|
|
|
def split(self, t):
|
|
|
|
"""returns two segments, whose union is this segment and which join at
|
|
|
|
self.point(t)."""
|
|
|
|
bpoints1, bpoints2 = split_bezier(self.bpoints(), t)
|
|
|
|
return QuadraticBezier(*bpoints1), QuadraticBezier(*bpoints2)
|
|
|
|
|
|
|
|
def cropped(self, t0, t1):
|
|
|
|
"""returns a cropped copy of this segment which starts at
|
|
|
|
self.point(t0) and ends at self.point(t1)."""
|
|
|
|
return QuadraticBezier(*crop_bezier(self, t0, t1))
|
|
|
|
|
|
|
|
def radialrange(self, origin, return_all_global_extrema=False):
|
|
|
|
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
|
|
|
|
and maximize, respectively, the distance d = |self.point(t)-origin|."""
|
|
|
|
return bezier_radialrange(self, origin,
|
|
|
|
return_all_global_extrema=return_all_global_extrema)
|
|
|
|
|
|
|
|
def rotated(self, degs, origin=None):
|
|
|
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
|
|
|
point `origin` (a complex number). By default `origin` is either
|
|
|
|
`self.point(0.5)`, or in the case that self is an Arc object,
|
|
|
|
`origin` defaults to `self.center`."""
|
2016-10-28 13:11:31 +00:00
|
|
|
return rotate(self, degs, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def translated(self, z0):
|
|
|
|
"""Returns a copy of self shifted by the complex quantity `z0` such
|
|
|
|
that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
|
|
|
|
return translate(self, z0)
|
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scaled(self, sx, sy=None, origin=0j):
|
|
|
|
"""Scale transform. See `scale` function for further explanation."""
|
|
|
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
2018-05-22 19:45:04 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
class CubicBezier(object):
|
|
|
|
# For compatibility with old pickle files.
|
|
|
|
_length_info = {'length': None, 'bpoints': None, 'error': None,
|
|
|
|
'min_depth': None}
|
|
|
|
|
|
|
|
def __init__(self, start, control1, control2, end):
|
|
|
|
self.start = start
|
|
|
|
self.control1 = control1
|
|
|
|
self.control2 = control2
|
|
|
|
self.end = end
|
|
|
|
|
|
|
|
# used to know if self._length needs to be updated
|
|
|
|
self._length_info = {'length': None, 'bpoints': None, 'error': None,
|
|
|
|
'min_depth': None}
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
|
|
|
|
self.start, self.control1, self.control2, self.end)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not isinstance(other, CubicBezier):
|
|
|
|
return NotImplemented
|
|
|
|
return self.start == other.start and self.end == other.end \
|
|
|
|
and self.control1 == other.control1 \
|
|
|
|
and self.control2 == other.control2
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
if not isinstance(other, CubicBezier):
|
|
|
|
return NotImplemented
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
return self.bpoints()[item]
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return 4
|
|
|
|
|
|
|
|
def is_smooth_from(self, previous, warning_on=True):
|
|
|
|
"""[Warning: The name of this method is somewhat misleading (yet kept
|
|
|
|
for compatibility with scripts created using svg.path 2.0). This
|
|
|
|
method is meant only for d string creation and should not be used to
|
|
|
|
check for kinks. To check a segment for differentiability, use the
|
|
|
|
joins_smoothly_with() method instead.]"""
|
|
|
|
if warning_on:
|
|
|
|
warn(_is_smooth_from_warning)
|
|
|
|
if isinstance(previous, CubicBezier):
|
|
|
|
return (self.start == previous.end and
|
|
|
|
(self.control1 - self.start) == (
|
|
|
|
previous.end - previous.control2))
|
|
|
|
else:
|
|
|
|
return self.control1 == self.start
|
|
|
|
|
|
|
|
def joins_smoothly_with(self, previous, wrt_parameterization=False):
|
|
|
|
"""Checks if this segment joins smoothly with previous segment. By
|
|
|
|
default, this only checks that this segment starts moving (at t=0) in
|
|
|
|
the same direction (and from the same positive) as previous stopped
|
|
|
|
moving (at t=1). To check if the tangent magnitudes also match, set
|
|
|
|
wrt_parameterization=True."""
|
|
|
|
if wrt_parameterization:
|
|
|
|
return self.start == previous.end and np.isclose(
|
|
|
|
self.derivative(0), previous.derivative(1))
|
|
|
|
else:
|
|
|
|
return self.start == previous.end and np.isclose(
|
|
|
|
self.unit_tangent(0), previous.unit_tangent(1))
|
|
|
|
|
|
|
|
def point(self, t):
|
|
|
|
"""Evaluate the cubic Bezier curve at t using Horner's rule."""
|
|
|
|
# algebraically equivalent to
|
|
|
|
# P0*(1-t)**3 + 3*P1*t*(1-t)**2 + 3*P2*(1-t)*t**2 + P3*t**3
|
|
|
|
# for (P0, P1, P2, P3) = self.bpoints()
|
|
|
|
return self.start + t*(
|
|
|
|
3*(self.control1 - self.start) + t*(
|
|
|
|
3*(self.start + self.control2) - 6*self.control1 + t*(
|
|
|
|
-self.start + 3*(self.control1 - self.control2) + self.end
|
|
|
|
)))
|
|
|
|
|
2020-06-24 05:16:09 +00:00
|
|
|
def points(self, ts):
|
|
|
|
"""Faster than running Path.point many times."""
|
|
|
|
return self.poly(ts)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
|
|
|
"""Calculate the length of the path up to a certain position"""
|
|
|
|
if t0 == 0 and t1 == 1:
|
|
|
|
if self._length_info['bpoints'] == self.bpoints() \
|
|
|
|
and self._length_info['error'] >= error \
|
|
|
|
and self._length_info['min_depth'] >= min_depth:
|
|
|
|
return self._length_info['length']
|
|
|
|
|
|
|
|
# using scipy.integrate.quad is quick
|
|
|
|
if _quad_available:
|
|
|
|
s = quad(lambda tau: abs(self.derivative(tau)), t0, t1,
|
|
|
|
epsabs=error, limit=1000)[0]
|
|
|
|
else:
|
|
|
|
s = segment_length(self, t0, t1, self.point(t0), self.point(t1),
|
|
|
|
error, min_depth, 0)
|
|
|
|
|
|
|
|
if t0 == 0 and t1 == 1:
|
|
|
|
self._length_info['length'] = s
|
|
|
|
self._length_info['bpoints'] = self.bpoints()
|
|
|
|
self._length_info['error'] = error
|
|
|
|
self._length_info['min_depth'] = min_depth
|
|
|
|
return self._length_info['length']
|
|
|
|
else:
|
|
|
|
return s
|
|
|
|
|
|
|
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
|
|
|
See the inv_arclength() docstring for more details."""
|
|
|
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
|
|
|
|
|
|
|
def bpoints(self):
|
|
|
|
"""returns the Bezier control points of the segment."""
|
|
|
|
return self.start, self.control1, self.control2, self.end
|
|
|
|
|
|
|
|
def poly(self, return_coeffs=False):
|
|
|
|
"""Returns a the cubic as a Polynomial object."""
|
|
|
|
p = self.bpoints()
|
|
|
|
coeffs = (-p[0] + 3*(p[1] - p[2]) + p[3],
|
|
|
|
3*(p[0] - 2*p[1] + p[2]),
|
|
|
|
3*(-p[0] + p[1]),
|
|
|
|
p[0])
|
|
|
|
if return_coeffs:
|
|
|
|
return coeffs
|
|
|
|
else:
|
|
|
|
return np.poly1d(coeffs)
|
|
|
|
|
|
|
|
def derivative(self, t, n=1):
|
|
|
|
"""returns the nth derivative of the segment at t.
|
|
|
|
Note: Bezier curves can have points where their derivative vanishes.
|
|
|
|
If you are interested in the tangent direction, use the unit_tangent()
|
|
|
|
method instead."""
|
|
|
|
p = self.bpoints()
|
|
|
|
if n == 1:
|
|
|
|
return 3*(p[1] - p[0])*(1 - t)**2 + 6*(p[2] - p[1])*(1 - t)*t + 3*(
|
|
|
|
p[3] - p[2])*t**2
|
|
|
|
elif n == 2:
|
|
|
|
return 6*(
|
|
|
|
(1 - t)*(p[2] - 2*p[1] + p[0]) + t*(p[3] - 2*p[2] + p[1]))
|
|
|
|
elif n == 3:
|
|
|
|
return 6*(p[3] - 3*(p[2] - p[1]) - p[0])
|
|
|
|
elif n > 3:
|
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
raise ValueError("n should be a positive integer.")
|
|
|
|
|
|
|
|
def unit_tangent(self, t):
|
|
|
|
"""returns the unit tangent vector of the segment at t (centered at
|
|
|
|
the origin and expressed as a complex number). If the tangent
|
|
|
|
vector's magnitude is zero, this method will find the limit of
|
|
|
|
self.derivative(tau)/abs(self.derivative(tau)) as tau approaches t."""
|
|
|
|
return bezier_unit_tangent(self, t)
|
|
|
|
|
|
|
|
def normal(self, t):
|
|
|
|
"""returns the (right hand rule) unit normal vector to self at t."""
|
|
|
|
return -1j * self.unit_tangent(t)
|
|
|
|
|
|
|
|
def curvature(self, t):
|
|
|
|
"""returns the curvature of the segment at t."""
|
|
|
|
return segment_curvature(self, t)
|
|
|
|
|
2016-07-24 00:15:22 +00:00
|
|
|
# def icurvature(self, kappa):
|
|
|
|
# """returns a list of t-values such that 0 <= t<= 1 and
|
|
|
|
# seg.curvature(t) = kappa."""
|
|
|
|
# z = self.poly()
|
|
|
|
# x, y = real(z), imag(z)
|
|
|
|
# dx, dy = x.deriv(), y.deriv()
|
|
|
|
# ddx, ddy = dx.deriv(), dy.deriv()
|
|
|
|
#
|
|
|
|
# p = kappa**2*(dx**2 + dy**2)**3 - (dx*ddy - ddx*dy)**2
|
|
|
|
# return polyroots01(p)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def reversed(self):
|
|
|
|
"""returns a copy of the CubicBezier object with its orientation
|
|
|
|
reversed."""
|
|
|
|
new_cub = CubicBezier(self.end, self.control2, self.control1,
|
|
|
|
self.start)
|
|
|
|
if self._length_info['length']:
|
|
|
|
new_cub._length_info = self._length_info
|
|
|
|
new_cub._length_info['bpoints'] = (
|
|
|
|
self.end, self.control2, self.control1, self.start)
|
|
|
|
return new_cub
|
|
|
|
|
|
|
|
def intersect(self, other_seg, tol=1e-12):
|
|
|
|
"""Finds the intersections of two segments.
|
|
|
|
returns a list of tuples (t1, t2) such that
|
|
|
|
self.point(t1) == other_seg.point(t2).
|
|
|
|
Note: This will fail if the two segments coincide for more than a
|
|
|
|
finite collection of points."""
|
|
|
|
if isinstance(other_seg, Line):
|
|
|
|
return bezier_by_line_intersections(self, other_seg)
|
|
|
|
elif (isinstance(other_seg, QuadraticBezier) or
|
|
|
|
isinstance(other_seg, CubicBezier)):
|
|
|
|
assert self != other_seg
|
|
|
|
longer_length = max(self.length(), other_seg.length())
|
|
|
|
return bezier_intersections(self, other_seg,
|
|
|
|
longer_length=longer_length,
|
|
|
|
tol=tol, tol_deC=tol)
|
|
|
|
elif isinstance(other_seg, Arc):
|
|
|
|
t2t1s = other_seg.intersect(self)
|
|
|
|
return [(t1, t2) for t2, t1 in t2t1s]
|
|
|
|
elif isinstance(other_seg, Path):
|
|
|
|
raise TypeError(
|
|
|
|
"other_seg must be a path segment, not a Path object, use "
|
|
|
|
"Path.intersect().")
|
|
|
|
else:
|
|
|
|
raise TypeError("other_seg must be a path segment.")
|
|
|
|
|
|
|
|
def bbox(self):
|
|
|
|
"""returns the bounding box for the segment in the form
|
|
|
|
(xmin, xmax, ymin, ymax)."""
|
|
|
|
return bezier_bounding_box(self)
|
|
|
|
|
|
|
|
def split(self, t):
|
|
|
|
"""returns two segments, whose union is this segment and which join at
|
|
|
|
self.point(t)."""
|
|
|
|
bpoints1, bpoints2 = split_bezier(self.bpoints(), t)
|
|
|
|
return CubicBezier(*bpoints1), CubicBezier(*bpoints2)
|
|
|
|
|
|
|
|
def cropped(self, t0, t1):
|
|
|
|
"""returns a cropped copy of this segment which starts at
|
|
|
|
self.point(t0) and ends at self.point(t1)."""
|
|
|
|
return CubicBezier(*crop_bezier(self, t0, t1))
|
|
|
|
|
|
|
|
def radialrange(self, origin, return_all_global_extrema=False):
|
|
|
|
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
|
|
|
|
and maximize, respectively, the distance d = |self.point(t)-origin|."""
|
|
|
|
return bezier_radialrange(self, origin,
|
|
|
|
return_all_global_extrema=return_all_global_extrema)
|
|
|
|
|
|
|
|
def rotated(self, degs, origin=None):
|
|
|
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
|
|
|
point `origin` (a complex number). By default `origin` is either
|
|
|
|
`self.point(0.5)`, or in the case that self is an Arc object,
|
|
|
|
`origin` defaults to `self.center`."""
|
2016-10-28 13:11:31 +00:00
|
|
|
return rotate(self, degs, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def translated(self, z0):
|
|
|
|
"""Returns a copy of self shifted by the complex quantity `z0` such
|
|
|
|
that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
|
|
|
|
return translate(self, z0)
|
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scaled(self, sx, sy=None, origin=0j):
|
|
|
|
"""Scale transform. See `scale` function for further explanation."""
|
|
|
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
2018-05-22 19:45:04 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
class Arc(object):
|
|
|
|
def __init__(self, start, radius, rotation, large_arc, sweep, end,
|
|
|
|
autoscale_radius=True):
|
|
|
|
"""
|
|
|
|
This should be thought of as a part of an ellipse connecting two
|
|
|
|
points on that ellipse, start and end.
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
start : complex
|
2017-02-21 04:11:54 +00:00
|
|
|
The start point of the curve. Note: `start` and `end` cannot be the
|
|
|
|
same. To make a full ellipse or circle, use two `Arc` objects.
|
2016-07-06 04:51:11 +00:00
|
|
|
radius : complex
|
|
|
|
rx + 1j*ry, where rx and ry are the radii of the ellipse (also
|
|
|
|
known as its semi-major and semi-minor axes, or vice-versa or if
|
|
|
|
rx < ry).
|
|
|
|
Note: If rx = 0 or ry = 0 then this arc is treated as a
|
|
|
|
straight line segment joining the endpoints.
|
|
|
|
Note: If rx or ry has a negative sign, the sign is dropped; the
|
|
|
|
absolute value is used instead.
|
|
|
|
Note: If no such ellipse exists, the radius will be scaled so
|
|
|
|
that one does (unless autoscale_radius is set to False).
|
|
|
|
rotation : float
|
|
|
|
This is the CCW angle (in degrees) from the positive x-axis of the
|
|
|
|
current coordinate system to the x-axis of the ellipse.
|
|
|
|
large_arc : bool
|
2017-02-21 04:11:54 +00:00
|
|
|
Given two points on an ellipse, there are two elliptical arcs
|
|
|
|
connecting those points, the first going the short way around the
|
|
|
|
ellipse, and the second going the long way around the ellipse. If
|
|
|
|
`large_arc == False`, the shorter elliptical arc will be used. If
|
|
|
|
`large_arc == True`, then longer elliptical will be used.
|
|
|
|
In other words, `large_arc` should be 0 for arcs spanning less than
|
|
|
|
or equal to 180 degrees and 1 for arcs spanning greater than 180
|
2016-07-06 04:51:11 +00:00
|
|
|
degrees.
|
|
|
|
sweep : bool
|
2017-02-21 04:11:54 +00:00
|
|
|
For any acceptable parameters `start`, `end`, `rotation`, and
|
|
|
|
`radius`, there are two ellipses with the given major and minor
|
|
|
|
axes (radii) which connect `start` and `end`. One which connects
|
|
|
|
them in a CCW fashion and one which connected them in a CW
|
|
|
|
fashion. If `sweep == True`, the CCW ellipse will be used. If
|
|
|
|
`sweep == False`, the CW ellipse will be used. See note on curve
|
2017-02-21 03:46:16 +00:00
|
|
|
orientation below.
|
2016-07-06 04:51:11 +00:00
|
|
|
end : complex
|
2017-02-21 04:11:54 +00:00
|
|
|
The end point of the curve. Note: `start` and `end` cannot be the
|
|
|
|
same. To make a full ellipse or circle, use two `Arc` objects.
|
2017-02-21 03:46:16 +00:00
|
|
|
autoscale_radius : bool
|
2017-02-21 04:11:54 +00:00
|
|
|
If `autoscale_radius == True`, then will also scale `self.radius`
|
2017-02-21 03:46:16 +00:00
|
|
|
in the case that no ellipse exists with the input parameters
|
2017-02-21 04:11:54 +00:00
|
|
|
(see inline comments for further explanation).
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2017-02-21 03:46:16 +00:00
|
|
|
Derived Parameters/Attributes
|
|
|
|
-----------------------------
|
2016-07-06 04:51:11 +00:00
|
|
|
self.theta : float
|
|
|
|
This is the phase (in degrees) of self.u1transform(self.start).
|
|
|
|
It is $\theta_1$ in the official documentation and ranges from
|
|
|
|
-180 to 180.
|
|
|
|
self.delta : float
|
|
|
|
This is the angular distance (in degrees) between the start and
|
|
|
|
end of the arc after the arc has been sent to the unit circle
|
|
|
|
through self.u1transform().
|
|
|
|
It is $\Delta\theta$ in the official documentation and ranges from
|
|
|
|
-360 to 360; being positive when the arc travels CCW and negative
|
|
|
|
otherwise (i.e. is positive/negative when sweep == True/False).
|
|
|
|
self.center : complex
|
|
|
|
This is the center of the arc's ellipse.
|
2017-02-21 03:46:16 +00:00
|
|
|
self.phi : float
|
|
|
|
The arc's rotation in radians, i.e. `radians(self.rotation)`.
|
|
|
|
self.rot_matrix : complex
|
|
|
|
Equal to `exp(1j * self.phi)` which is also equal to
|
|
|
|
`cos(self.phi) + 1j*sin(self.phi)`.
|
|
|
|
|
|
|
|
|
|
|
|
Note on curve orientation (CW vs CCW)
|
|
|
|
-------------------------------------
|
|
|
|
The notions of clockwise (CW) and counter-clockwise (CCW) are reversed
|
|
|
|
in some sense when viewing SVGs (as the y coordinate starts at the top
|
|
|
|
of the image and increases towards the bottom).
|
2016-07-06 04:51:11 +00:00
|
|
|
"""
|
2017-02-21 04:11:54 +00:00
|
|
|
assert start != end
|
2017-02-21 06:10:16 +00:00
|
|
|
assert radius.real != 0 and radius.imag != 0
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
self.start = start
|
|
|
|
self.radius = abs(radius.real) + 1j*abs(radius.imag)
|
|
|
|
self.rotation = rotation
|
|
|
|
self.large_arc = bool(large_arc)
|
|
|
|
self.sweep = bool(sweep)
|
|
|
|
self.end = end
|
|
|
|
self.autoscale_radius = autoscale_radius
|
|
|
|
|
2020-06-20 01:45:56 +00:00
|
|
|
self.segment_length_hash = None
|
|
|
|
self.segment_length = None
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
# Convenience parameters
|
|
|
|
self.phi = radians(self.rotation)
|
|
|
|
self.rot_matrix = exp(1j*self.phi)
|
|
|
|
|
|
|
|
# Derive derived parameters
|
|
|
|
self._parameterize()
|
|
|
|
|
2020-06-20 01:45:56 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.start, self.radius, self.rotation, self.large_arc, self.sweep, self.end))
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def __repr__(self):
|
|
|
|
params = (self.start, self.radius, self.rotation,
|
|
|
|
self.large_arc, self.sweep, self.end)
|
|
|
|
return ("Arc(start={}, radius={}, rotation={}, "
|
|
|
|
"large_arc={}, sweep={}, end={})".format(*params))
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not isinstance(other, Arc):
|
|
|
|
return NotImplemented
|
|
|
|
return self.start == other.start and self.end == other.end \
|
|
|
|
and self.radius == other.radius \
|
|
|
|
and self.rotation == other.rotation \
|
|
|
|
and self.large_arc == other.large_arc and self.sweep == other.sweep
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
if not isinstance(other, Arc):
|
|
|
|
return NotImplemented
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
def _parameterize(self):
|
|
|
|
# See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
|
|
# my notation roughly follows theirs
|
|
|
|
rx = self.radius.real
|
|
|
|
ry = self.radius.imag
|
|
|
|
rx_sqd = rx*rx
|
|
|
|
ry_sqd = ry*ry
|
|
|
|
|
|
|
|
# Transform z-> z' = x' + 1j*y'
|
|
|
|
# = self.rot_matrix**(-1)*(z - (end+start)/2)
|
|
|
|
# coordinates. This translates the ellipse so that the midpoint
|
|
|
|
# between self.end and self.start lies on the origin and rotates
|
|
|
|
# the ellipse so that the its axes align with the xy-coordinate axes.
|
|
|
|
# Note: This sends self.end to -self.start
|
|
|
|
zp1 = (1/self.rot_matrix)*(self.start - self.end)/2
|
|
|
|
x1p, y1p = zp1.real, zp1.imag
|
|
|
|
x1p_sqd = x1p*x1p
|
|
|
|
y1p_sqd = y1p*y1p
|
|
|
|
|
|
|
|
# Correct out of range radii
|
|
|
|
# Note: an ellipse going through start and end with radius and phi
|
|
|
|
# exists if and only if radius_check is true
|
|
|
|
radius_check = (x1p_sqd/rx_sqd) + (y1p_sqd/ry_sqd)
|
|
|
|
if radius_check > 1:
|
|
|
|
if self.autoscale_radius:
|
|
|
|
rx *= sqrt(radius_check)
|
|
|
|
ry *= sqrt(radius_check)
|
|
|
|
self.radius = rx + 1j*ry
|
|
|
|
rx_sqd = rx*rx
|
|
|
|
ry_sqd = ry*ry
|
|
|
|
else:
|
|
|
|
raise ValueError("No such elliptic arc exists.")
|
|
|
|
|
|
|
|
# Compute c'=(c_x', c_y'), the center of the ellipse in (x', y') coords
|
|
|
|
# Noting that, in our new coord system, (x_2', y_2') = (-x_1', -x_2')
|
|
|
|
# and our ellipse is cut out by of the plane by the algebraic equation
|
|
|
|
# (x'-c_x')**2 / r_x**2 + (y'-c_y')**2 / r_y**2 = 1,
|
|
|
|
# we can find c' by solving the system of two quadratics given by
|
|
|
|
# plugging our transformed endpoints (x_1', y_1') and (x_2', y_2')
|
|
|
|
tmp = rx_sqd*y1p_sqd + ry_sqd*x1p_sqd
|
|
|
|
radicand = (rx_sqd*ry_sqd - tmp) / tmp
|
|
|
|
try:
|
|
|
|
radical = sqrt(radicand)
|
|
|
|
except ValueError:
|
|
|
|
radical = 0
|
|
|
|
if self.large_arc == self.sweep:
|
|
|
|
cp = -radical*(rx*y1p/ry - 1j*ry*x1p/rx)
|
|
|
|
else:
|
|
|
|
cp = radical*(rx*y1p/ry - 1j*ry*x1p/rx)
|
|
|
|
|
|
|
|
# The center in (x,y) coordinates is easy to find knowing c'
|
|
|
|
self.center = exp(1j*self.phi)*cp + (self.start + self.end)/2
|
|
|
|
|
|
|
|
# Now we do a second transformation, from (x', y') to (u_x, u_y)
|
|
|
|
# coordinates, which is a translation moving the center of the
|
|
|
|
# ellipse to the origin and a dilation stretching the ellipse to be
|
|
|
|
# the unit circle
|
|
|
|
u1 = (x1p - cp.real)/rx + 1j*(y1p - cp.imag)/ry # transformed start
|
|
|
|
u2 = (-x1p - cp.real)/rx + 1j*(-y1p - cp.imag)/ry # transformed end
|
|
|
|
|
2018-07-24 21:57:54 +00:00
|
|
|
# clip in case of floating point error
|
|
|
|
u1 = np.clip(u1.real, -1, 1) + 1j*np.clip(u1.imag, -1, 1)
|
|
|
|
u2 = np.clip(u2.real, -1, 1) + 1j * np.clip(u2.imag, -1, 1)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
# Now compute theta and delta (we'll define them as we go)
|
|
|
|
# delta is the angular distance of the arc (w.r.t the circle)
|
|
|
|
# theta is the angle between the positive x'-axis and the start point
|
|
|
|
# on the circle
|
|
|
|
if u1.imag > 0:
|
2018-07-24 21:57:54 +00:00
|
|
|
self.theta = degrees(acos(u1.real))
|
2016-07-06 04:51:11 +00:00
|
|
|
elif u1.imag < 0:
|
2018-07-24 21:57:54 +00:00
|
|
|
self.theta = -degrees(acos(u1.real))
|
2016-07-06 04:51:11 +00:00
|
|
|
else:
|
|
|
|
if u1.real > 0: # start is on pos u_x axis
|
|
|
|
self.theta = 0
|
|
|
|
else: # start is on neg u_x axis
|
|
|
|
# Note: This behavior disagrees with behavior documented in
|
|
|
|
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
|
|
# where theta is set to 0 in this case.
|
|
|
|
self.theta = 180
|
|
|
|
|
|
|
|
det_uv = u1.real*u2.imag - u1.imag*u2.real
|
|
|
|
|
|
|
|
acosand = u1.real*u2.real + u1.imag*u2.imag
|
2018-07-24 22:07:02 +00:00
|
|
|
acosand = np.clip(acosand.real, -1, 1) + np.clip(acosand.imag, -1, 1)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if det_uv > 0:
|
|
|
|
self.delta = degrees(acos(acosand))
|
|
|
|
elif det_uv < 0:
|
|
|
|
self.delta = -degrees(acos(acosand))
|
|
|
|
else:
|
|
|
|
if u1.real*u2.real + u1.imag*u2.imag > 0:
|
|
|
|
# u1 == u2
|
|
|
|
self.delta = 0
|
|
|
|
else:
|
|
|
|
# u1 == -u2
|
|
|
|
# Note: This behavior disagrees with behavior documented in
|
|
|
|
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
|
|
# where delta is set to 0 in this case.
|
|
|
|
self.delta = 180
|
|
|
|
|
|
|
|
if not self.sweep and self.delta >= 0:
|
|
|
|
self.delta -= 360
|
|
|
|
elif self.large_arc and self.delta <= 0:
|
|
|
|
self.delta += 360
|
|
|
|
|
|
|
|
def point(self, t):
|
2020-12-02 06:09:20 +00:00
|
|
|
|
|
|
|
angle = (self.theta + t*self.delta)*pi/180
|
2016-07-06 04:51:11 +00:00
|
|
|
cosphi = self.rot_matrix.real
|
|
|
|
sinphi = self.rot_matrix.imag
|
|
|
|
rx = self.radius.real
|
|
|
|
ry = self.radius.imag
|
|
|
|
|
|
|
|
x = rx*cosphi*cos(angle) - ry*sinphi*sin(angle) + self.center.real
|
|
|
|
y = rx*sinphi*cos(angle) + ry*cosphi*sin(angle) + self.center.imag
|
2020-12-02 06:09:20 +00:00
|
|
|
return x + y*1j
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2018-11-05 04:55:17 +00:00
|
|
|
def point_to_t(self, point):
|
|
|
|
"""If the point lies on the Arc, returns its `t` parameter.
|
|
|
|
If the point does not lie on the Arc, returns None.
|
|
|
|
This function only works on Arcs with rotation == 0.0"""
|
|
|
|
|
|
|
|
def in_range(min, max, val):
|
|
|
|
return (min <= val) and (max >= val)
|
|
|
|
|
|
|
|
# Single-precision floats have only 7 significant figures of
|
|
|
|
# resolution, so test that we're within 6 sig figs.
|
|
|
|
if np.isclose(point, self.start, rtol=0.0, atol=1e-6):
|
|
|
|
return 0.0
|
|
|
|
elif np.isclose(point, self.end, rtol=0.0, atol=1e-6):
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
if self.rotation != 0.0:
|
|
|
|
raise ValueError("Arc.point_to_t() only works on non-rotated Arcs.")
|
|
|
|
|
|
|
|
v = point - self.center
|
|
|
|
distance_from_center = sqrt((v.real * v.real) + (v.imag * v.imag))
|
|
|
|
min_radius = min(self.radius.real, self.radius.imag)
|
|
|
|
max_radius = max(self.radius.real, self.radius.imag)
|
|
|
|
if (distance_from_center < min_radius) and not np.isclose(distance_from_center, min_radius):
|
|
|
|
return None
|
|
|
|
if (distance_from_center > max_radius) and not np.isclose(distance_from_center, max_radius):
|
|
|
|
return None
|
|
|
|
|
|
|
|
# x = center_x + radius_x cos(radians(theta + t delta))
|
|
|
|
# y = center_y + radius_y sin(radians(theta + t delta))
|
|
|
|
#
|
|
|
|
# For x:
|
|
|
|
# cos(radians(theta + t delta)) = (x - center_x) / radius_x
|
|
|
|
# radians(theta + t delta) = acos((x - center_x) / radius_x)
|
|
|
|
# theta + t delta = degrees(acos((x - center_x) / radius_x))
|
|
|
|
# t_x = (degrees(acos((x - center_x) / radius_x)) - theta) / delta
|
|
|
|
#
|
|
|
|
# Similarly for y:
|
|
|
|
# t_y = (degrees(asin((y - center_y) / radius_y)) - theta) / delta
|
|
|
|
|
|
|
|
x = point.real
|
|
|
|
y = point.imag
|
|
|
|
|
|
|
|
#
|
|
|
|
# +Y points down!
|
|
|
|
#
|
|
|
|
# sweep mean clocwise
|
|
|
|
# sweep && (delta > 0)
|
|
|
|
# !sweep && (delta < 0)
|
|
|
|
#
|
|
|
|
# -180 <= theta_1 <= 180
|
|
|
|
#
|
|
|
|
# large_arc && (-360 <= delta <= 360)
|
|
|
|
# !large_arc && (-180 < delta < 180)
|
|
|
|
#
|
|
|
|
|
|
|
|
end_angle = self.theta + self.delta
|
|
|
|
min_angle = min(self.theta, end_angle)
|
|
|
|
max_angle = max(self.theta, end_angle)
|
|
|
|
|
|
|
|
acos_arg = (x - self.center.real) / self.radius.real
|
|
|
|
if acos_arg > 1.0:
|
|
|
|
acos_arg = 1.0
|
|
|
|
elif acos_arg < -1.0:
|
|
|
|
acos_arg = -1.0
|
|
|
|
|
|
|
|
x_angle_0 = degrees(acos(acos_arg))
|
|
|
|
while x_angle_0 < min_angle:
|
|
|
|
x_angle_0 += 360.0
|
|
|
|
while x_angle_0 > max_angle:
|
|
|
|
x_angle_0 -= 360.0
|
|
|
|
|
|
|
|
x_angle_1 = -1.0 * x_angle_0
|
|
|
|
while x_angle_1 < min_angle:
|
|
|
|
x_angle_1 += 360.0
|
|
|
|
while x_angle_1 > max_angle:
|
|
|
|
x_angle_1 -= 360.0
|
|
|
|
|
|
|
|
t_x_0 = (x_angle_0 - self.theta) / self.delta
|
|
|
|
t_x_1 = (x_angle_1 - self.theta) / self.delta
|
|
|
|
|
|
|
|
asin_arg = (y - self.center.imag) / self.radius.imag
|
|
|
|
if asin_arg > 1.0:
|
|
|
|
asin_arg = 1.0
|
|
|
|
elif asin_arg < -1.0:
|
|
|
|
asin_arg = -1.0
|
|
|
|
|
|
|
|
y_angle_0 = degrees(asin(asin_arg))
|
|
|
|
while y_angle_0 < min_angle:
|
|
|
|
y_angle_0 += 360.0
|
|
|
|
while y_angle_0 > max_angle:
|
|
|
|
y_angle_0 -= 360.0
|
|
|
|
|
|
|
|
y_angle_1 = 180 - y_angle_0
|
|
|
|
while y_angle_1 < min_angle:
|
|
|
|
y_angle_1 += 360.0
|
|
|
|
while y_angle_1 > max_angle:
|
|
|
|
y_angle_1 -= 360.0
|
|
|
|
|
|
|
|
t_y_0 = (y_angle_0 - self.theta) / self.delta
|
|
|
|
t_y_1 = (y_angle_1 - self.theta) / self.delta
|
|
|
|
|
|
|
|
t = None
|
|
|
|
if np.isclose(t_x_0, t_y_0):
|
|
|
|
t = (t_x_0 + t_y_0) / 2.0
|
|
|
|
elif np.isclose(t_x_0, t_y_1):
|
|
|
|
t= (t_x_0 + t_y_1) / 2.0
|
|
|
|
elif np.isclose(t_x_1, t_y_0):
|
|
|
|
t = (t_x_1 + t_y_0) / 2.0
|
|
|
|
elif np.isclose(t_x_1, t_y_1):
|
|
|
|
t = (t_x_1 + t_y_1) / 2.0
|
|
|
|
else:
|
|
|
|
# Comparing None and float yields a result in python2,
|
|
|
|
# but throws TypeError in python3. This fix (suggested by
|
|
|
|
# @CatherineH) explicitly handles and avoids the case where
|
|
|
|
# the None-vs-float comparison would have happened below.
|
|
|
|
return None
|
|
|
|
|
|
|
|
if (t >= 0.0) and (t <= 1.0):
|
|
|
|
return t
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def centeriso(self, z):
|
|
|
|
"""This is an isometry that translates and rotates self so that it
|
|
|
|
is centered on the origin and has its axes aligned with the xy axes."""
|
|
|
|
return (1/self.rot_matrix)*(z - self.center)
|
|
|
|
|
|
|
|
def icenteriso(self, zeta):
|
|
|
|
"""This is an isometry, the inverse of standardiso()."""
|
|
|
|
return self.rot_matrix*zeta + self.center
|
|
|
|
|
|
|
|
def u1transform(self, z):
|
|
|
|
"""This is an affine transformation (same as used in
|
|
|
|
self._parameterize()) that sends self to the unit circle."""
|
|
|
|
zeta = (1/self.rot_matrix)*(z - self.center) # same as centeriso(z)
|
|
|
|
x, y = real(zeta), imag(zeta)
|
|
|
|
return x/self.radius.real + 1j*y/self.radius.imag
|
|
|
|
|
|
|
|
def iu1transform(self, zeta):
|
|
|
|
"""This is an affine transformation, the inverse of
|
|
|
|
self.u1transform()."""
|
|
|
|
x = real(zeta)
|
|
|
|
y = imag(zeta)
|
|
|
|
z = x*self.radius.real + y*self.radius.imag
|
|
|
|
return self.rot_matrix*z + self.center
|
|
|
|
|
|
|
|
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
|
|
|
"""The length of an elliptical large_arc segment requires numerical
|
|
|
|
integration, and in that case it's simpler to just do a geometric
|
|
|
|
approximation, as for cubic bezier curves."""
|
|
|
|
assert 0 <= t0 <= 1 and 0 <= t1 <= 1
|
2020-06-20 01:45:56 +00:00
|
|
|
|
|
|
|
if t0 == 0 and t1 == 1:
|
|
|
|
h = hash(self)
|
|
|
|
if self.segment_length_hash is None or self.segment_length_hash != h:
|
|
|
|
self.segment_length_hash = h
|
|
|
|
if _quad_available:
|
|
|
|
self.segment_length = quad(lambda tau: abs(self.derivative(tau)),
|
|
|
|
t0, t1, epsabs=error, limit=1000)[0]
|
|
|
|
else:
|
|
|
|
self.segment_length = segment_length(self, t0, t1, self.point(t0),
|
|
|
|
self.point(t1), error, min_depth, 0)
|
|
|
|
return self.segment_length
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if _quad_available:
|
|
|
|
return quad(lambda tau: abs(self.derivative(tau)), t0, t1,
|
|
|
|
epsabs=error, limit=1000)[0]
|
|
|
|
else:
|
|
|
|
return segment_length(self, t0, t1, self.point(t0), self.point(t1),
|
|
|
|
error, min_depth, 0)
|
|
|
|
|
|
|
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
|
|
|
See the inv_arclength() docstring for more details."""
|
|
|
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
|
|
|
|
|
|
|
def joins_smoothly_with(self, previous, wrt_parameterization=False,
|
|
|
|
error=0):
|
|
|
|
"""Checks if this segment joins smoothly with previous segment. By
|
|
|
|
default, this only checks that this segment starts moving (at t=0) in
|
|
|
|
the same direction (and from the same positive) as previous stopped
|
|
|
|
moving (at t=1). To check if the tangent magnitudes also match, set
|
|
|
|
wrt_parameterization=True."""
|
|
|
|
if wrt_parameterization:
|
|
|
|
return self.start == previous.end and abs(
|
|
|
|
self.derivative(0) - previous.derivative(1)) <= error
|
|
|
|
else:
|
|
|
|
return self.start == previous.end and abs(
|
|
|
|
self.unit_tangent(0) - previous.unit_tangent(1)) <= error
|
|
|
|
|
|
|
|
def derivative(self, t, n=1):
|
|
|
|
"""returns the nth derivative of the segment at t."""
|
|
|
|
angle = radians(self.theta + t*self.delta)
|
|
|
|
phi = radians(self.rotation)
|
|
|
|
rx = self.radius.real
|
|
|
|
ry = self.radius.imag
|
2020-12-02 06:38:41 +00:00
|
|
|
k = (self.delta*pi/180)**n # ((d/dt)angle)**n
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
if n % 4 == 0 and n > 0:
|
|
|
|
return rx*cos(phi)*cos(angle) - ry*sin(phi)*sin(angle) + 1j*(
|
|
|
|
rx*sin(phi)*cos(angle) + ry*cos(phi)*sin(angle))
|
|
|
|
elif n % 4 == 1:
|
|
|
|
return k*(-rx*cos(phi)*sin(angle) - ry*sin(phi)*cos(angle) + 1j*(
|
|
|
|
-rx*sin(phi)*sin(angle) + ry*cos(phi)*cos(angle)))
|
|
|
|
elif n % 4 == 2:
|
|
|
|
return k*(-rx*cos(phi)*cos(angle) + ry*sin(phi)*sin(angle) + 1j*(
|
|
|
|
-rx*sin(phi)*cos(angle) - ry*cos(phi)*sin(angle)))
|
|
|
|
elif n % 4 == 3:
|
|
|
|
return k*(rx*cos(phi)*sin(angle) + ry*sin(phi)*cos(angle) + 1j*(
|
|
|
|
rx*sin(phi)*sin(angle) - ry*cos(phi)*cos(angle)))
|
|
|
|
else:
|
|
|
|
raise ValueError("n should be a positive integer.")
|
|
|
|
|
|
|
|
def unit_tangent(self, t):
|
|
|
|
"""returns the unit tangent vector of the segment at t (centered at
|
|
|
|
the origin and expressed as a complex number)."""
|
|
|
|
dseg = self.derivative(t)
|
|
|
|
return dseg/abs(dseg)
|
|
|
|
|
|
|
|
def normal(self, t):
|
|
|
|
"""returns the (right hand rule) unit normal vector to self at t."""
|
|
|
|
return -1j*self.unit_tangent(t)
|
|
|
|
|
|
|
|
def curvature(self, t):
|
|
|
|
"""returns the curvature of the segment at t."""
|
|
|
|
return segment_curvature(self, t)
|
|
|
|
|
2016-07-24 00:15:22 +00:00
|
|
|
# def icurvature(self, kappa):
|
|
|
|
# """returns a list of t-values such that 0 <= t<= 1 and
|
|
|
|
# seg.curvature(t) = kappa."""
|
|
|
|
#
|
|
|
|
# a, b = self.radius.real, self.radius.imag
|
|
|
|
# if kappa > min(a, b)/max(a, b)**2 or kappa <= 0:
|
|
|
|
# return []
|
|
|
|
# if a==b:
|
|
|
|
# if kappa != 1/a:
|
|
|
|
# return []
|
|
|
|
# else:
|
|
|
|
# raise ValueError(
|
|
|
|
# "The .icurvature() method for Arc elements with "
|
|
|
|
# "radius.real == radius.imag (i.e. circle segments) "
|
|
|
|
# "will raise this exception when kappa is 1/radius.real as "
|
|
|
|
# "this is true at every point on the circle segment.")
|
|
|
|
#
|
|
|
|
# # kappa = a*b / (a^2sin^2(tau) + b^2cos^2(tau))^(3/2), tau=2*pi*phase
|
|
|
|
# sin2 = np.poly1d([1, 0])
|
|
|
|
# p = kappa**2*(a*sin2 + b*(1 - sin2))**3 - a*b
|
|
|
|
# sin2s = polyroots01(p)
|
|
|
|
# taus = []
|
|
|
|
#
|
|
|
|
# for sin2 in sin2s:
|
|
|
|
# taus += [np.arcsin(sqrt(sin2)), np.arcsin(-sqrt(sin2))]
|
|
|
|
#
|
|
|
|
# # account for the other branch of arcsin
|
|
|
|
# sgn = lambda x: x/abs(x) if x else 0
|
|
|
|
# other_taus = [sgn(tau)*np.pi - tau for tau in taus if abs(tau) != np.pi/2]
|
|
|
|
# taus = taus + other_taus
|
|
|
|
#
|
|
|
|
# # get rid of points not included in segment
|
|
|
|
# ts = [phase2t(tau) for tau in taus]
|
|
|
|
#
|
|
|
|
# return [t for t in ts if 0<=t<=1]
|
|
|
|
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def reversed(self):
|
|
|
|
"""returns a copy of the Arc object with its orientation reversed."""
|
|
|
|
return Arc(self.end, self.radius, self.rotation, self.large_arc,
|
|
|
|
not self.sweep, self.start)
|
|
|
|
|
|
|
|
def phase2t(self, psi):
|
|
|
|
"""Given phase -pi < psi <= pi,
|
|
|
|
returns the t value such that
|
|
|
|
exp(1j*psi) = self.u1transform(self.point(t)).
|
|
|
|
"""
|
|
|
|
def _deg(rads, domain_lower_limit):
|
|
|
|
# Convert rads to degrees in [0, 360) domain
|
|
|
|
degs = degrees(rads % (2*pi))
|
|
|
|
|
|
|
|
# Convert to [domain_lower_limit, domain_lower_limit + 360) domain
|
|
|
|
k = domain_lower_limit // 360
|
|
|
|
degs += k * 360
|
|
|
|
if degs < domain_lower_limit:
|
|
|
|
degs += 360
|
|
|
|
return degs
|
|
|
|
|
|
|
|
if self.delta > 0:
|
|
|
|
degs = _deg(psi, domain_lower_limit=self.theta)
|
|
|
|
else:
|
|
|
|
degs = _deg(psi, domain_lower_limit=self.theta)
|
|
|
|
return (degs - self.theta)/self.delta
|
|
|
|
|
|
|
|
|
|
|
|
def intersect(self, other_seg, tol=1e-12):
|
2016-07-24 00:15:22 +00:00
|
|
|
"""NOT FULLY IMPLEMENTED. Finds the intersections of two segments.
|
2016-07-06 04:51:11 +00:00
|
|
|
returns a list of tuples (t1, t2) such that
|
|
|
|
self.point(t1) == other_seg.point(t2).
|
|
|
|
Note: This will fail if the two segments coincide for more than a
|
2017-02-21 06:10:16 +00:00
|
|
|
finite collection of points.
|
|
|
|
|
|
|
|
Note: Arc related intersections are only partially supported, i.e. are
|
|
|
|
only half-heartedly implemented and not well tested. Please feel free
|
|
|
|
to let me know if you're interested in such a feature -- or even better
|
|
|
|
please submit an implementation if you want to code one."""
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2018-11-05 04:55:17 +00:00
|
|
|
# This special case can be easily solved algebraically.
|
|
|
|
if (self.rotation == 0) and isinstance(other_seg, Line):
|
|
|
|
a = self.radius.real
|
|
|
|
b = self.radius.imag
|
|
|
|
|
|
|
|
# Ignore the ellipse's center point (to pretend that it's
|
|
|
|
# centered at the origin), and translate the Line to match.
|
|
|
|
l = Line(start=(other_seg.start-self.center), end=(other_seg.end-self.center))
|
|
|
|
|
|
|
|
# This gives us the translated Line as a parametric equation.
|
|
|
|
# s = p1 t + p0
|
|
|
|
p = l.poly()
|
|
|
|
|
|
|
|
if p[1].real == 0.0:
|
|
|
|
# The `x` value doesn't depend on `t`, the line is vertical.
|
|
|
|
c = p[0].real
|
|
|
|
x_values = [c]
|
|
|
|
|
|
|
|
# Substitute the line `x = c` into the equation for the
|
|
|
|
# (origin-centered) ellipse.
|
|
|
|
#
|
|
|
|
# x^2/a^2 + y^2/b^2 = 1
|
|
|
|
# c^2/a^2 + y^2/b^2 = 1
|
|
|
|
# y^2/b^2 = 1 - c^2/a^2
|
|
|
|
# y^2 = b^2(1 - c^2/a^2)
|
|
|
|
# y = +-b sqrt(1 - c^2/a^2)
|
|
|
|
|
|
|
|
discriminant = 1 - (c * c)/(a * a)
|
|
|
|
if discriminant < 0:
|
|
|
|
return []
|
|
|
|
elif discriminant == 0:
|
|
|
|
y_values = [0]
|
|
|
|
else:
|
|
|
|
val = b * sqrt(discriminant)
|
|
|
|
y_values = [val, -val]
|
|
|
|
|
|
|
|
else:
|
|
|
|
# This is a non-vertical line.
|
|
|
|
#
|
|
|
|
# Convert the Line's parametric equation to the "y = mx + c" format.
|
|
|
|
# x = p1.real t + p0.real
|
|
|
|
# y = p1.imag t + p0.imag
|
|
|
|
#
|
|
|
|
# t = (x - p0.real) / p1.real
|
|
|
|
# t = (y - p0.imag) / p1.imag
|
|
|
|
#
|
|
|
|
# (y - p0.imag) / p1.imag = (x - p0.real) / p1.real
|
|
|
|
# (y - p0.imag) = ((x - p0.real) * p1.imag) / p1.real
|
|
|
|
# y = ((x - p0.real) * p1.imag) / p1.real + p0.imag
|
|
|
|
# y = (x p1.imag - p0.real * p1.imag) / p1.real + p0.imag
|
|
|
|
# y = x p1.imag/p1.real - p0.real p1.imag / p1.real + p0.imag
|
|
|
|
# m = p1.imag/p1.real
|
|
|
|
# c = -m p0.real + p0.imag
|
|
|
|
m = p[1].imag / p[1].real
|
|
|
|
c = (-m * p[0].real) + p[0].imag
|
|
|
|
|
|
|
|
# Substitute the line's y(x) equation into the equation for
|
|
|
|
# the ellipse. We can pretend the ellipse is centered at the
|
|
|
|
# origin, since we shifted the Line by the ellipse's center.
|
|
|
|
#
|
|
|
|
# x^2/a^2 + y^2/b^2 = 1
|
|
|
|
# x^2/a^2 + (mx+c)^2/b^2 = 1
|
|
|
|
# (b^2 x^2 + a^2 (mx+c)^2)/(a^2 b^2) = 1
|
|
|
|
# b^2 x^2 + a^2 (mx+c)^2 = a^2 b^2
|
|
|
|
# b^2 x^2 + a^2(m^2 x^2 + 2mcx + c^2) = a^2 b^2
|
|
|
|
# b^2 x^2 + a^2 m^2 x^2 + 2a^2 mcx + a^2 c^2 - a^2 b^2 = 0
|
|
|
|
# (a^2 m^2 + b^2)x^2 + 2a^2 mcx + a^2(c^2 - b^2) = 0
|
|
|
|
#
|
|
|
|
# The quadratic forumla tells us: x = (-B +- sqrt(B^2 - 4AC)) / 2A
|
|
|
|
# Where:
|
|
|
|
# A = a^2 m^2 + b^2
|
|
|
|
# B = 2 a^2 mc
|
|
|
|
# C = a^2(c^2 - b^2)
|
|
|
|
#
|
|
|
|
# The determinant is: B^2 - 4AC
|
|
|
|
#
|
|
|
|
# The solution simplifies to:
|
|
|
|
# x = (-a^2 mc +- a b sqrt(a^2 m^2 + b^2 - c^2)) / (a^2 m^2 + b^2)
|
|
|
|
#
|
|
|
|
# Solving the line for x(y) and substituting *that* into
|
|
|
|
# the equation for the ellipse gives this solution for y:
|
|
|
|
# y = (b^2 c +- abm sqrt(a^2 m^2 + b^2 - c^2)) / (a^2 m^2 + b^2)
|
|
|
|
|
|
|
|
denominator = (a * a * m * m) + (b * b)
|
|
|
|
|
|
|
|
discriminant = denominator - (c * c)
|
|
|
|
if discriminant < 0:
|
|
|
|
return []
|
|
|
|
|
|
|
|
x_sqrt = a * b * sqrt(discriminant)
|
|
|
|
x1 = (-(a * a * m * c) + x_sqrt) / denominator
|
|
|
|
x2 = (-(a * a * m * c) - x_sqrt) / denominator
|
|
|
|
x_values = [x1]
|
|
|
|
if x1 != x2:
|
|
|
|
x_values.append(x2)
|
|
|
|
|
|
|
|
y_sqrt = x_sqrt * m
|
|
|
|
y1 = ((b * b * c) + y_sqrt) / denominator
|
|
|
|
y2 = ((b * b * c) - y_sqrt) / denominator
|
|
|
|
y_values = [y1]
|
|
|
|
if y1 != y2:
|
|
|
|
y_values.append(y2)
|
|
|
|
|
|
|
|
intersections = []
|
|
|
|
for x in x_values:
|
|
|
|
for y in y_values:
|
|
|
|
p = complex(x, y) + self.center
|
|
|
|
my_t = self.point_to_t(p)
|
|
|
|
if my_t == None:
|
|
|
|
continue
|
|
|
|
other_t = other_seg.point_to_t(p)
|
|
|
|
if other_t == None:
|
|
|
|
continue
|
|
|
|
intersections.append([my_t, other_t])
|
|
|
|
return intersections
|
|
|
|
|
|
|
|
elif is_bezier_segment(other_seg):
|
2016-07-06 04:51:11 +00:00
|
|
|
u1poly = self.u1transform(other_seg.poly())
|
|
|
|
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
|
|
|
|
t2s = polyroots01(u1poly_mag2 - 1)
|
|
|
|
t1s = [self.phase2t(phase(u1poly(t2))) for t2 in t2s]
|
2017-03-01 07:04:37 +00:00
|
|
|
return list(zip(t1s, t2s))
|
2020-06-20 02:40:38 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(other_seg, Arc):
|
2017-02-21 06:10:16 +00:00
|
|
|
assert other_seg != self
|
2020-06-20 02:40:38 +00:00
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
# From "Intersection of two circles", at
|
|
|
|
# http://paulbourke.net/geometry/circlesphere/
|
|
|
|
|
|
|
|
# It's easy to find the intersections of two circles, so
|
|
|
|
# compute that and see if any of those
|
|
|
|
# intersection points are on the arcs.
|
|
|
|
if (self.rotation == 0) and (self.radius.real == self.radius.imag) and (other_seg.rotation == 0) and (other_seg.radius.real == other_seg.radius.imag):
|
|
|
|
r0 = self.radius.real
|
|
|
|
r1 = other_seg.radius.real
|
|
|
|
p0 = self.center
|
|
|
|
p1 = other_seg.center
|
|
|
|
d = abs(p0 - p1)
|
|
|
|
possible_inters = []
|
|
|
|
|
|
|
|
if d > (r0 + r1):
|
|
|
|
# The circles are farther apart than the sum of
|
|
|
|
# their radii: no intersections possible.
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif d < abs(r0 - r1):
|
|
|
|
# The small circle is wholly contained within the
|
|
|
|
# large circle: no intersections possible.
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif (np.isclose(d, 0, rtol=0.0, atol=1e-6)) and (np.isclose(r0, r1, rtol=0.0, atol=1e-6)):
|
|
|
|
# The Arcs lie on the same circle: they have the
|
|
|
|
# same center and are of equal radius.
|
|
|
|
|
|
|
|
def point_in_seg_interior(point, seg):
|
|
|
|
t = seg.point_to_t(point)
|
|
|
|
if t is None: return False
|
|
|
|
if np.isclose(t, 0.0, rtol=0.0, atol=1e-6): return False
|
|
|
|
if np.isclose(t, 1.0, rtol=0.0, atol=1e-6): return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
# If either end of either segment is in the interior
|
|
|
|
# of the other segment, then the Arcs overlap
|
|
|
|
# in an infinite number of points, and we return
|
|
|
|
# "no intersections".
|
|
|
|
if point_in_seg_interior(self.start, other_seg): return []
|
|
|
|
if point_in_seg_interior(self.end, other_seg): return []
|
|
|
|
if point_in_seg_interior(other_seg.start, self): return []
|
|
|
|
if point_in_seg_interior(other_seg.end, self): return []
|
|
|
|
|
|
|
|
# If they touch at their endpoint(s) and don't go
|
|
|
|
# in "overlapping directions", then we accept that
|
|
|
|
# as intersections.
|
|
|
|
|
|
|
|
if (self.start == other_seg.start) and (self.sweep != other_seg.sweep):
|
|
|
|
possible_inters.append((0.0, 0.0))
|
|
|
|
|
|
|
|
if (self.start == other_seg.end) and (self.sweep == other_seg.sweep):
|
|
|
|
possible_inters.append((0.0, 1.0))
|
|
|
|
|
|
|
|
if (self.end == other_seg.start) and (self.sweep == other_seg.sweep):
|
|
|
|
possible_inters.append((1.0, 0.0))
|
|
|
|
|
|
|
|
if (self.end == other_seg.end) and (self.sweep != other_seg.sweep):
|
|
|
|
possible_inters.append((1.0, 1.0))
|
|
|
|
|
|
|
|
elif np.isclose(d, r0 + r1, rtol=0.0, atol=1e-6):
|
|
|
|
# The circles are tangent, so the Arcs may touch
|
|
|
|
# at exactly one point. The circles lie outside
|
|
|
|
# each other.
|
|
|
|
l = Line(start=p0, end=p1)
|
|
|
|
p = l.point(r0/d)
|
|
|
|
possible_inters.append((self.point_to_t(p), other_seg.point_to_t(p)))
|
|
|
|
|
|
|
|
elif np.isclose(d, abs(r0 - r1), rtol=0.0, atol=1e-6):
|
|
|
|
# The circles are tangent, so the Arcs may touch
|
|
|
|
# at exactly one point. One circle lies inside
|
|
|
|
# the other.
|
|
|
|
# Make a line from the center of the inside circle
|
|
|
|
# to the center of the outside circle, and walk
|
|
|
|
# along it the negative of the small radius.
|
|
|
|
l = Line(start=p0, end=p1)
|
|
|
|
little_r = r0
|
|
|
|
if r0 > r1:
|
|
|
|
l = Line(start=p1, end=p0)
|
|
|
|
little_r = r1
|
|
|
|
p = l.point(-little_r/d)
|
|
|
|
possible_inters.append((self.point_to_t(p), other_seg.point_to_t(p)))
|
|
|
|
|
|
|
|
else:
|
|
|
|
a = (pow(r0, 2.0) - pow(r1, 2.0) + pow(d, 2.0)) / (2.0 * d)
|
|
|
|
h = sqrt(pow(r0, 2.0) - pow(a, 2.0))
|
|
|
|
p2 = p0 + (a * (p1 - p0) / d)
|
|
|
|
|
|
|
|
x30 = p2.real + (h * (p1.imag - p0.imag) / d)
|
|
|
|
x31 = p2.real - (h * (p1.imag - p0.imag) / d)
|
|
|
|
|
|
|
|
y30 = p2.imag - (h * (p1.real - p0.real) / d)
|
|
|
|
y31 = p2.imag + (h * (p1.real - p0.real) / d)
|
|
|
|
|
|
|
|
p30 = complex(x30, y30)
|
|
|
|
p31 = complex(x31, y31)
|
|
|
|
|
|
|
|
possible_inters.append((self.point_to_t(p30), other_seg.point_to_t(p30)))
|
|
|
|
possible_inters.append((self.point_to_t(p31), other_seg.point_to_t(p31)))
|
|
|
|
|
|
|
|
inters = []
|
|
|
|
for p in possible_inters:
|
|
|
|
self_t = p[0]
|
|
|
|
if (self_t is None) or (self_t < 0.0) or (self_t > 1.0): continue
|
|
|
|
other_t = p[1]
|
|
|
|
if (other_t is None) or (other_t < 0.0) or (other_t > 1.0): continue
|
|
|
|
assert(np.isclose(self.point(self_t), other_seg.point(other_t), rtol=0.0, atol=1e-6))
|
|
|
|
i = (self_t, other_t)
|
|
|
|
inters.append(i)
|
|
|
|
|
|
|
|
return inters
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
# This could be made explicit to increase efficiency
|
|
|
|
longer_length = max(self.length(), other_seg.length())
|
|
|
|
inters = bezier_intersections(self, other_seg,
|
|
|
|
longer_length=longer_length,
|
|
|
|
tol=tol, tol_deC=tol)
|
|
|
|
|
|
|
|
# ad hoc fix for redundant solutions
|
|
|
|
if len(inters) > 2:
|
|
|
|
def keyfcn(tpair):
|
|
|
|
t1, t2 = tpair
|
|
|
|
return abs(self.point(t1) - other_seg.point(t2))
|
|
|
|
inters.sort(key=keyfcn)
|
|
|
|
for idx in range(1, len(inters)-1):
|
|
|
|
if (abs(inters[idx][0] - inters[idx + 1][0])
|
|
|
|
< abs(inters[idx][0] - inters[0][0])):
|
|
|
|
return [inters[0], inters[idx]]
|
|
|
|
else:
|
|
|
|
return [inters[0], inters[-1]]
|
|
|
|
return inters
|
2020-06-20 02:40:38 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("other_seg should be a Arc, Line, "
|
|
|
|
"QuadraticBezier, or CubicBezier object.")
|
|
|
|
|
|
|
|
def bbox(self):
|
|
|
|
"""returns a bounding box for the segment in the form
|
|
|
|
(xmin, xmax, ymin, ymax)."""
|
|
|
|
# a(t) = radians(self.theta + self.delta*t)
|
|
|
|
# = (2*pi/360)*(self.theta + self.delta*t)
|
|
|
|
# x'=0: ~~~~~~~~~
|
|
|
|
# -rx*cos(phi)*sin(a(t)) = ry*sin(phi)*cos(a(t))
|
|
|
|
# -(rx/ry)*cot(phi)*tan(a(t)) = 1
|
|
|
|
# a(t) = arctan(-(ry/rx)tan(phi)) + pi*k === atan_x
|
|
|
|
# y'=0: ~~~~~~~~~~
|
|
|
|
# rx*sin(phi)*sin(a(t)) = ry*cos(phi)*cos(a(t))
|
|
|
|
# (rx/ry)*tan(phi)*tan(a(t)) = 1
|
|
|
|
# a(t) = arctan((ry/rx)*cot(phi))
|
|
|
|
# atanres = arctan((ry/rx)*cot(phi)) === atan_y
|
|
|
|
# ~~~~~~~~
|
|
|
|
# (2*pi/360)*(self.theta + self.delta*t) = atanres + pi*k
|
|
|
|
# Therfore, for both x' and y', we have...
|
|
|
|
# t = ((atan_{x/y} + pi*k)*(360/(2*pi)) - self.theta)/self.delta
|
|
|
|
# for all k s.t. 0 < t < 1
|
|
|
|
from math import atan, tan
|
|
|
|
|
|
|
|
if cos(self.phi) == 0:
|
|
|
|
atan_x = pi/2
|
|
|
|
atan_y = 0
|
|
|
|
elif sin(self.phi) == 0:
|
|
|
|
atan_x = 0
|
|
|
|
atan_y = pi/2
|
|
|
|
else:
|
|
|
|
rx, ry = self.radius.real, self.radius.imag
|
|
|
|
atan_x = atan(-(ry/rx)*tan(self.phi))
|
|
|
|
atan_y = atan((ry/rx)/tan(self.phi))
|
|
|
|
|
|
|
|
def angle_inv(ang, k): # inverse of angle from Arc.derivative()
|
|
|
|
return ((ang + pi*k)*(360/(2*pi)) - self.theta)/self.delta
|
|
|
|
|
|
|
|
xtrema = [self.start.real, self.end.real]
|
|
|
|
ytrema = [self.start.imag, self.end.imag]
|
|
|
|
|
|
|
|
for k in range(-4, 5):
|
|
|
|
tx = angle_inv(atan_x, k)
|
|
|
|
ty = angle_inv(atan_y, k)
|
|
|
|
if 0 <= tx <= 1:
|
|
|
|
xtrema.append(self.point(tx).real)
|
|
|
|
if 0 <= ty <= 1:
|
|
|
|
ytrema.append(self.point(ty).imag)
|
|
|
|
xmin = max(xtrema)
|
|
|
|
return min(xtrema), max(xtrema), min(ytrema), max(ytrema)
|
|
|
|
|
|
|
|
def split(self, t):
|
|
|
|
"""returns two segments, whose union is this segment and which join
|
|
|
|
at self.point(t)."""
|
|
|
|
return self.cropped(0, t), self.cropped(t, 1)
|
|
|
|
|
|
|
|
def cropped(self, t0, t1):
|
|
|
|
"""returns a cropped copy of this segment which starts at
|
|
|
|
self.point(t0) and ends at self.point(t1)."""
|
|
|
|
if abs(self.delta*(t1 - t0)) <= 180:
|
|
|
|
new_large_arc = 0
|
|
|
|
else:
|
|
|
|
new_large_arc = 1
|
|
|
|
return Arc(self.point(t0), radius=self.radius, rotation=self.rotation,
|
|
|
|
large_arc=new_large_arc, sweep=self.sweep,
|
|
|
|
end=self.point(t1), autoscale_radius=self.autoscale_radius)
|
|
|
|
|
|
|
|
def radialrange(self, origin, return_all_global_extrema=False):
|
|
|
|
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
|
|
|
|
and maximize, respectively, the distance,
|
|
|
|
d = |self.point(t)-origin|."""
|
|
|
|
|
2018-08-22 03:54:02 +00:00
|
|
|
# u1orig = self.u1transform(origin)
|
|
|
|
# if abs(u1orig) == 1: # origin lies on ellipse
|
|
|
|
# t = self.phase2t(phase(u1orig))
|
|
|
|
# d_min = 0
|
|
|
|
#
|
|
|
|
# # Transform to a coordinate system where the ellipse is centered
|
|
|
|
# # at the origin and its axes are horizontal/vertical
|
|
|
|
# zeta0 = self.centeriso(origin)
|
|
|
|
# a, b = self.radius.real, self.radius.imag
|
|
|
|
# x0, y0 = zeta0.real, zeta0.imag
|
|
|
|
#
|
|
|
|
# # Find t s.t. z'(t)
|
|
|
|
# a2mb2 = (a**2 - b**2)
|
|
|
|
# if u1orig.imag: # x != x0
|
|
|
|
#
|
|
|
|
# coeffs = [a2mb2**2,
|
|
|
|
# 2*a2mb2*b**2*y0,
|
|
|
|
# (-a**4 + (2*a**2 - b**2 + y0**2)*b**2 + x0**2)*b**2,
|
|
|
|
# -2*a2mb2*b**4*y0,
|
|
|
|
# -b**6*y0**2]
|
|
|
|
# ys = polyroots(coeffs, realroots=True,
|
|
|
|
# condition=lambda r: -b <= r <= b)
|
|
|
|
# xs = (a*sqrt(1 - y**2/b**2) for y in ys)
|
|
|
|
#
|
|
|
|
# ts = [self.phase2t(phase(self.u1transform(self.icenteriso(
|
|
|
|
# complex(x, y))))) for x, y in zip(xs, ys)]
|
|
|
|
#
|
|
|
|
# else: # This case is very similar, see notes and assume instead y0!=y
|
|
|
|
# b2ma2 = (b**2 - a**2)
|
|
|
|
# coeffs = [b2ma2**2,
|
|
|
|
# 2*b2ma2*a**2*x0,
|
|
|
|
# (-b**4 + (2*b**2 - a**2 + x0**2)*a**2 + y0**2)*a**2,
|
|
|
|
# -2*b2ma2*a**4*x0,
|
|
|
|
# -a**6*x0**2]
|
|
|
|
# xs = polyroots(coeffs, realroots=True,
|
|
|
|
# condition=lambda r: -a <= r <= a)
|
|
|
|
# ys = (b*sqrt(1 - x**2/a**2) for x in xs)
|
|
|
|
#
|
|
|
|
# ts = [self.phase2t(phase(self.u1transform(self.icenteriso(
|
|
|
|
# complex(x, y))))) for x, y in zip(xs, ys)]
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
raise _NotImplemented4ArcException
|
|
|
|
|
|
|
|
def rotated(self, degs, origin=None):
|
|
|
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
|
|
|
point `origin` (a complex number). By default `origin` is either
|
|
|
|
`self.point(0.5)`, or in the case that self is an Arc object,
|
|
|
|
`origin` defaults to `self.center`."""
|
2016-10-28 13:11:31 +00:00
|
|
|
return rotate(self, degs, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def translated(self, z0):
|
|
|
|
"""Returns a copy of self shifted by the complex quantity `z0` such
|
|
|
|
that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
|
|
|
|
return translate(self, z0)
|
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scaled(self, sx, sy=None, origin=0j):
|
|
|
|
"""Scale transform. See `scale` function for further explanation."""
|
|
|
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
2020-12-02 02:37:15 +00:00
|
|
|
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
|
|
|
|
|
2018-05-23 02:59:03 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def is_bezier_segment(x):
|
|
|
|
return (isinstance(x, Line) or
|
|
|
|
isinstance(x, QuadraticBezier) or
|
|
|
|
isinstance(x, CubicBezier))
|
|
|
|
|
|
|
|
|
|
|
|
def is_path_segment(x):
|
|
|
|
return is_bezier_segment(x) or isinstance(x, Arc)
|
|
|
|
|
|
|
|
|
|
|
|
class Path(MutableSequence):
|
|
|
|
"""A Path is a sequence of path segments"""
|
|
|
|
|
|
|
|
# Put it here, so there is a default if unpickled.
|
|
|
|
_closed = False
|
|
|
|
_start = None
|
|
|
|
_end = None
|
2020-06-24 03:50:43 +00:00
|
|
|
element = None
|
|
|
|
transform = None
|
2020-06-20 03:21:28 +00:00
|
|
|
meta = None # meant as container for storage of arbitrary meta data
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def __init__(self, *segments, **kw):
|
|
|
|
self._length = None
|
|
|
|
self._lengths = None
|
|
|
|
if 'closed' in kw:
|
|
|
|
self.closed = kw['closed'] # DEPRECATED
|
2020-07-06 18:01:15 +00:00
|
|
|
if len(segments) >= 1:
|
|
|
|
if isinstance(segments[0], str):
|
|
|
|
if len(segments) >= 2:
|
|
|
|
current_pos = segments[1]
|
|
|
|
elif 'current_pos' in kw:
|
|
|
|
current_pos = kw['current_pos']
|
|
|
|
else:
|
|
|
|
current_pos = 0j
|
|
|
|
self._segments = list()
|
|
|
|
self._parse_path(segments[0], current_pos)
|
|
|
|
else:
|
|
|
|
self._segments = list(segments)
|
|
|
|
else:
|
|
|
|
self._segments = list()
|
2016-07-06 04:51:11 +00:00
|
|
|
if self._segments:
|
|
|
|
self._start = self._segments[0].start
|
|
|
|
self._end = self._segments[-1].end
|
|
|
|
else:
|
|
|
|
self._start = None
|
|
|
|
self._end = None
|
|
|
|
|
2018-08-22 01:00:29 +00:00
|
|
|
if 'tree_element' in kw:
|
|
|
|
self._tree_element = kw['tree_element']
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
def __getitem__(self, index):
|
|
|
|
return self._segments[index]
|
|
|
|
|
|
|
|
def __setitem__(self, index, value):
|
|
|
|
self._segments[index] = value
|
|
|
|
self._length = None
|
|
|
|
self._start = self._segments[0].start
|
|
|
|
self._end = self._segments[-1].end
|
|
|
|
|
|
|
|
def __delitem__(self, index):
|
|
|
|
del self._segments[index]
|
|
|
|
self._length = None
|
|
|
|
self._start = self._segments[0].start
|
|
|
|
self._end = self._segments[-1].end
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return self._segments.__iter__()
|
|
|
|
|
|
|
|
def __contains__(self, x):
|
|
|
|
return self._segments.__contains__(x)
|
|
|
|
|
|
|
|
def insert(self, index, value):
|
|
|
|
self._segments.insert(index, value)
|
|
|
|
self._length = None
|
|
|
|
self._start = self._segments[0].start
|
|
|
|
self._end = self._segments[-1].end
|
|
|
|
|
|
|
|
def reversed(self):
|
|
|
|
"""returns a copy of the Path object with its orientation reversed."""
|
|
|
|
newpath = [seg.reversed() for seg in self]
|
|
|
|
newpath.reverse()
|
|
|
|
return Path(*newpath)
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._segments)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "Path({})".format(
|
|
|
|
",\n ".join(repr(x) for x in self._segments))
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not isinstance(other, Path):
|
|
|
|
return NotImplemented
|
|
|
|
if len(self) != len(other):
|
|
|
|
return False
|
|
|
|
for s, o in zip(self._segments, other._segments):
|
|
|
|
if not s == o:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
if not isinstance(other, Path):
|
|
|
|
return NotImplemented
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
def _calc_lengths(self, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
|
|
|
if self._length is not None:
|
|
|
|
return
|
|
|
|
|
|
|
|
lengths = [each.length(error=error, min_depth=min_depth) for each in
|
|
|
|
self._segments]
|
|
|
|
self._length = sum(lengths)
|
|
|
|
self._lengths = [each/self._length for each in lengths]
|
|
|
|
|
|
|
|
def point(self, pos):
|
|
|
|
|
|
|
|
# Shortcuts
|
|
|
|
if pos == 0.0:
|
|
|
|
return self._segments[0].point(pos)
|
|
|
|
if pos == 1.0:
|
|
|
|
return self._segments[-1].point(pos)
|
|
|
|
|
|
|
|
self._calc_lengths()
|
|
|
|
# Find which segment the point we search for is located on:
|
|
|
|
segment_start = 0
|
|
|
|
for index, segment in enumerate(self._segments):
|
|
|
|
segment_end = segment_start + self._lengths[index]
|
|
|
|
if segment_end >= pos:
|
|
|
|
# This is the segment! How far in on the segment is the point?
|
|
|
|
segment_pos = (pos - segment_start)/(
|
|
|
|
segment_end - segment_start)
|
|
|
|
return segment.point(segment_pos)
|
|
|
|
segment_start = segment_end
|
|
|
|
|
|
|
|
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
|
|
|
self._calc_lengths(error=error, min_depth=min_depth)
|
|
|
|
if T0 == 0 and T1 == 1:
|
|
|
|
return self._length
|
|
|
|
else:
|
|
|
|
if len(self) == 1:
|
|
|
|
return self[0].length(t0=T0, t1=T1)
|
|
|
|
idx0, t0 = self.T2t(T0)
|
|
|
|
idx1, t1 = self.T2t(T1)
|
|
|
|
if idx0 == idx1:
|
|
|
|
return self[idx0].length(t0=t0, t1=t1)
|
|
|
|
return (self[idx0].length(t0=t0) +
|
|
|
|
sum(self[idx].length() for idx in range(idx0 + 1, idx1)) +
|
|
|
|
self[idx1].length(t1=t1))
|
|
|
|
|
|
|
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
|
|
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
|
|
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
|
|
|
See the inv_arclength() docstring for more details."""
|
|
|
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
|
|
|
min_depth=min_depth)
|
|
|
|
|
|
|
|
def iscontinuous(self):
|
|
|
|
"""Checks if a path is continuous with respect to its
|
|
|
|
parameterization."""
|
|
|
|
return all(self[i].end == self[i+1].start for i in range(len(self) - 1))
|
|
|
|
|
|
|
|
def continuous_subpaths(self):
|
|
|
|
"""Breaks self into its continuous components, returning a list of
|
|
|
|
continuous subpaths.
|
|
|
|
I.e.
|
|
|
|
(all(subpath.iscontinuous() for subpath in self.continuous_subpaths())
|
|
|
|
and self == concatpaths(self.continuous_subpaths()))
|
|
|
|
)
|
|
|
|
"""
|
|
|
|
subpaths = []
|
|
|
|
subpath_start = 0
|
|
|
|
for i in range(len(self) - 1):
|
|
|
|
if self[i].end != self[(i+1) % len(self)].start:
|
|
|
|
subpaths.append(Path(*self[subpath_start: i+1]))
|
|
|
|
subpath_start = i+1
|
|
|
|
subpaths.append(Path(*self[subpath_start: len(self)]))
|
|
|
|
return subpaths
|
|
|
|
|
|
|
|
def isclosed(self):
|
|
|
|
"""This function determines if a connected path is closed."""
|
|
|
|
assert len(self) != 0
|
|
|
|
assert self.iscontinuous()
|
|
|
|
return self.start == self.end
|
|
|
|
|
|
|
|
def isclosedac(self):
|
|
|
|
assert len(self) != 0
|
|
|
|
return self.start == self.end
|
|
|
|
|
|
|
|
def _is_closable(self):
|
|
|
|
end = self[-1].end
|
|
|
|
for segment in self:
|
|
|
|
if segment.start == end:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def closed(self, warning_on=CLOSED_WARNING_ON):
|
|
|
|
"""The closed attribute is deprecated, please use the isclosed()
|
|
|
|
method instead. See _closed_warning for more information."""
|
|
|
|
mes = ("This attribute is deprecated, consider using isclosed() "
|
|
|
|
"method instead.\n\nThis attribute is kept for compatibility "
|
|
|
|
"with scripts created using svg.path (v2.0). You can prevent "
|
|
|
|
"this warning in the future by setting "
|
|
|
|
"CLOSED_WARNING_ON=False.")
|
|
|
|
if warning_on:
|
|
|
|
warn(mes)
|
|
|
|
return self._closed and self._is_closable()
|
|
|
|
|
|
|
|
@closed.setter
|
|
|
|
def closed(self, value):
|
|
|
|
value = bool(value)
|
|
|
|
if value and not self._is_closable():
|
|
|
|
raise ValueError("End does not coincide with a segment start.")
|
|
|
|
self._closed = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def start(self):
|
|
|
|
if not self._start:
|
|
|
|
self._start = self._segments[0].start
|
|
|
|
return self._start
|
|
|
|
|
|
|
|
@start.setter
|
|
|
|
def start(self, pt):
|
|
|
|
self._start = pt
|
|
|
|
self._segments[0].start = pt
|
|
|
|
|
|
|
|
@property
|
|
|
|
def end(self):
|
|
|
|
if not self._end:
|
|
|
|
self._end = self._segments[-1].end
|
|
|
|
return self._end
|
|
|
|
|
|
|
|
@end.setter
|
|
|
|
def end(self, pt):
|
|
|
|
self._end = pt
|
|
|
|
self._segments[-1].end = pt
|
|
|
|
|
2020-06-20 01:43:29 +00:00
|
|
|
def d(self, useSandT=False, use_closed_attrib=False, rel=False):
|
2016-07-06 04:51:11 +00:00
|
|
|
"""Returns a path d-string for the path object.
|
|
|
|
For an explanation of useSandT and use_closed_attrib, see the
|
|
|
|
compatibility notes in the README."""
|
2020-06-20 01:43:29 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if use_closed_attrib:
|
2020-06-20 01:43:29 +00:00
|
|
|
self_closed = self.iscontinuous() and self.isclosed()
|
2016-07-06 04:51:11 +00:00
|
|
|
if self_closed:
|
|
|
|
segments = self[:-1]
|
|
|
|
else:
|
|
|
|
segments = self[:]
|
|
|
|
else:
|
|
|
|
self_closed = False
|
|
|
|
segments = self[:]
|
2020-06-20 01:43:29 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
current_pos = None
|
|
|
|
parts = []
|
|
|
|
previous_segment = None
|
|
|
|
end = self[-1].end
|
2020-06-20 01:43:29 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
for segment in segments:
|
|
|
|
seg_start = segment.start
|
|
|
|
# If the start of this segment does not coincide with the end of
|
|
|
|
# the last segment or if this segment is actually the close point
|
|
|
|
# of a closed path, then we should start a new subpath here.
|
|
|
|
if current_pos != seg_start or \
|
|
|
|
(self_closed and seg_start == end and use_closed_attrib):
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_start = seg_start - current_pos if current_pos is not None else seg_start
|
|
|
|
else:
|
|
|
|
_seg_start = seg_start
|
|
|
|
parts.append('M {},{}'.format(_seg_start.real, _seg_start.imag))
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if isinstance(segment, Line):
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_end = segment.end
|
|
|
|
parts.append('L {},{}'.format(_seg_end.real, _seg_end.imag))
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(segment, CubicBezier):
|
|
|
|
if useSandT and segment.is_smooth_from(previous_segment,
|
|
|
|
warning_on=False):
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_control2 = segment.control2 - seg_start
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_control2 = segment.control2
|
|
|
|
_seg_end = segment.end
|
|
|
|
args = (_seg_control2.real, _seg_control2.imag,
|
|
|
|
_seg_end.real, _seg_end.imag)
|
2016-07-06 04:51:11 +00:00
|
|
|
parts.append('S {},{} {},{}'.format(*args))
|
|
|
|
else:
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_control1 = segment.control1 - seg_start
|
|
|
|
_seg_control2 = segment.control2 - seg_start
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_control1 = segment.control1
|
|
|
|
_seg_control2 = segment.control2
|
|
|
|
_seg_end = segment.end
|
|
|
|
args = (_seg_control1.real, _seg_control1.imag,
|
|
|
|
_seg_control2.real, _seg_control2.imag,
|
|
|
|
_seg_end.real, _seg_end.imag)
|
2016-07-06 04:51:11 +00:00
|
|
|
parts.append('C {},{} {},{} {},{}'.format(*args))
|
|
|
|
elif isinstance(segment, QuadraticBezier):
|
|
|
|
if useSandT and segment.is_smooth_from(previous_segment,
|
|
|
|
warning_on=False):
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_end = segment.end
|
|
|
|
args = _seg_end.real, _seg_end.imag
|
2016-07-06 04:51:11 +00:00
|
|
|
parts.append('T {},{}'.format(*args))
|
|
|
|
else:
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_control = segment.control - seg_start
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_control = segment.control
|
|
|
|
_seg_end = segment.end
|
|
|
|
args = (_seg_control.real, _seg_control.imag,
|
|
|
|
_seg_end.real, _seg_end.imag)
|
2016-07-06 04:51:11 +00:00
|
|
|
parts.append('Q {},{} {},{}'.format(*args))
|
2020-06-20 01:43:29 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
elif isinstance(segment, Arc):
|
2020-06-20 01:43:29 +00:00
|
|
|
if rel:
|
|
|
|
_seg_end = segment.end - seg_start
|
|
|
|
else:
|
|
|
|
_seg_end = segment.end
|
2016-07-06 04:51:11 +00:00
|
|
|
args = (segment.radius.real, segment.radius.imag,
|
|
|
|
segment.rotation,int(segment.large_arc),
|
2020-06-20 01:43:29 +00:00
|
|
|
int(segment.sweep),_seg_end.real, _seg_end.imag)
|
2016-07-06 04:51:11 +00:00
|
|
|
parts.append('A {},{} {} {:d},{:d} {},{}'.format(*args))
|
|
|
|
current_pos = segment.end
|
|
|
|
previous_segment = segment
|
2020-06-20 01:43:29 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if self_closed:
|
|
|
|
parts.append('Z')
|
2020-06-20 01:43:29 +00:00
|
|
|
|
|
|
|
s = ' '.join(parts)
|
|
|
|
return s if not rel else s.lower()
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def joins_smoothly_with(self, previous, wrt_parameterization=False):
|
|
|
|
"""Checks if this Path object joins smoothly with previous
|
|
|
|
path/segment. By default, this only checks that this Path starts
|
|
|
|
moving (at t=0) in the same direction (and from the same positive) as
|
|
|
|
previous stopped moving (at t=1). To check if the tangent magnitudes
|
|
|
|
also match, set wrt_parameterization=True."""
|
|
|
|
if wrt_parameterization:
|
|
|
|
return self[0].start == previous.end and self.derivative(
|
|
|
|
0) == previous.derivative(1)
|
|
|
|
else:
|
|
|
|
return self[0].start == previous.end and self.unit_tangent(
|
|
|
|
0) == previous.unit_tangent(1)
|
|
|
|
|
|
|
|
def T2t(self, T):
|
2018-06-09 05:13:18 +00:00
|
|
|
"""returns the segment index, `seg_idx`, and segment parameter, `t`,
|
|
|
|
corresponding to the path parameter `T`. In other words, this is the
|
|
|
|
inverse of the `Path.t2T()` method."""
|
2016-07-06 04:51:11 +00:00
|
|
|
if T == 1:
|
|
|
|
return len(self)-1, 1
|
|
|
|
if T == 0:
|
|
|
|
return 0, 0
|
|
|
|
self._calc_lengths()
|
|
|
|
# Find which segment self.point(T) falls on:
|
|
|
|
T0 = 0 # the T-value the current segment starts on
|
|
|
|
for seg_idx, seg_length in enumerate(self._lengths):
|
|
|
|
T1 = T0 + seg_length # the T-value the current segment ends on
|
|
|
|
if T1 >= T:
|
|
|
|
# This is the segment!
|
|
|
|
t = (T - T0)/seg_length
|
|
|
|
return seg_idx, t
|
|
|
|
T0 = T1
|
|
|
|
|
|
|
|
assert 0 <= T <= 1
|
|
|
|
raise BugException
|
|
|
|
|
|
|
|
def t2T(self, seg, t):
|
|
|
|
"""returns the path parameter T which corresponds to the segment
|
|
|
|
parameter t. In other words, for any Path object, path, and any
|
|
|
|
segment in path, seg, T(t) = path.t2T(seg, t) is the unique
|
|
|
|
reparameterization such that path.point(T(t)) == seg.point(t) for all
|
|
|
|
0 <= t <= 1.
|
|
|
|
Input Note: seg can be a segment in the Path object or its
|
|
|
|
corresponding index."""
|
|
|
|
self._calc_lengths()
|
|
|
|
# Accept an index or a segment for seg
|
|
|
|
if isinstance(seg, int):
|
|
|
|
seg_idx = seg
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
seg_idx = self.index(seg)
|
|
|
|
except ValueError:
|
|
|
|
assert is_path_segment(seg) or isinstance(seg, int)
|
|
|
|
raise
|
|
|
|
|
|
|
|
segment_start = sum(self._lengths[:seg_idx])
|
|
|
|
segment_end = segment_start + self._lengths[seg_idx]
|
|
|
|
T = (segment_end - segment_start)*t + segment_start
|
|
|
|
return T
|
|
|
|
|
|
|
|
def derivative(self, T, n=1):
|
|
|
|
"""returns the tangent vector of the Path at T (centered at the origin
|
|
|
|
and expressed as a complex number).
|
|
|
|
Note: Bezier curves can have points where their derivative vanishes.
|
|
|
|
If you are interested in the tangent direction, use unit_tangent()
|
|
|
|
method instead."""
|
|
|
|
seg_idx, t = self.T2t(T)
|
|
|
|
seg = self._segments[seg_idx]
|
|
|
|
return seg.derivative(t, n=n)/seg.length()**n
|
|
|
|
|
|
|
|
def unit_tangent(self, T):
|
|
|
|
"""returns the unit tangent vector of the Path at T (centered at the
|
|
|
|
origin and expressed as a complex number). If the tangent vector's
|
|
|
|
magnitude is zero, this method will find the limit of
|
|
|
|
self.derivative(tau)/abs(self.derivative(tau)) as tau approaches T."""
|
|
|
|
seg_idx, t = self.T2t(T)
|
|
|
|
return self._segments[seg_idx].unit_tangent(t)
|
|
|
|
|
|
|
|
def normal(self, t):
|
|
|
|
"""returns the (right hand rule) unit normal vector to self at t."""
|
|
|
|
return -1j*self.unit_tangent(t)
|
|
|
|
|
|
|
|
def curvature(self, T):
|
|
|
|
"""returns the curvature of this Path object at T and outputs
|
|
|
|
float('inf') if not differentiable at T."""
|
|
|
|
seg_idx, t = self.T2t(T)
|
|
|
|
seg = self[seg_idx]
|
2016-07-24 00:15:22 +00:00
|
|
|
if np.isclose(t, 0) and (seg_idx != 0 or self.end==self.start):
|
2016-07-06 04:51:11 +00:00
|
|
|
previous_seg_in_path = self._segments[
|
|
|
|
(seg_idx - 1) % len(self._segments)]
|
2018-04-13 21:34:28 +00:00
|
|
|
if not seg.joins_smoothly_with(previous_seg_in_path):
|
2016-07-06 04:51:11 +00:00
|
|
|
return float('inf')
|
2018-08-22 03:54:02 +00:00
|
|
|
elif np.isclose(t, 1) and (seg_idx != len(self) - 1 or
|
|
|
|
self.end == self.start):
|
2016-07-06 04:51:11 +00:00
|
|
|
next_seg_in_path = self._segments[
|
|
|
|
(seg_idx + 1) % len(self._segments)]
|
|
|
|
if not next_seg_in_path.joins_smoothly_with(seg):
|
|
|
|
return float('inf')
|
2018-06-10 21:33:17 +00:00
|
|
|
dz = self.derivative(T)
|
|
|
|
ddz = self.derivative(T, n=2)
|
2016-07-06 04:51:11 +00:00
|
|
|
dx, dy = dz.real, dz.imag
|
|
|
|
ddx, ddy = ddz.real, ddz.imag
|
|
|
|
return abs(dx*ddy - dy*ddx)/(dx*dx + dy*dy)**1.5
|
|
|
|
|
2016-07-24 00:15:22 +00:00
|
|
|
# def icurvature(self, kappa):
|
|
|
|
# """returns a list of T-values such that 0 <= T <= 1 and
|
|
|
|
# seg.curvature(t) = kappa.
|
|
|
|
# Note: not implemented for paths containing Arc segments."""
|
|
|
|
# assert is_bezier_path(self)
|
|
|
|
# Ts = []
|
|
|
|
# for i, seg in enumerate(self):
|
|
|
|
# Ts += [self.t2T(i, t) for t in seg.icurvature(kappa)]
|
|
|
|
# return Ts
|
|
|
|
|
2018-12-04 06:27:59 +00:00
|
|
|
def area(self, chord_length=1e-4):
|
2018-11-05 05:40:56 +00:00
|
|
|
"""Find area enclosed by path.
|
|
|
|
|
|
|
|
Approximates any Arc segments in the Path with lines
|
|
|
|
approximately `chord_length` long, and returns the area enclosed
|
2018-11-05 06:22:20 +00:00
|
|
|
by the approximated Path. Default chord length is 0.01. If Arc
|
|
|
|
segments are included in path, to ensure accurate results, make
|
|
|
|
sure this `chord_length` is set to a reasonable value (e.g. by
|
|
|
|
checking curvature).
|
2018-11-05 05:40:56 +00:00
|
|
|
|
|
|
|
Notes
|
2018-11-05 06:22:20 +00:00
|
|
|
-----
|
2018-11-05 05:40:56 +00:00
|
|
|
* Negative area results from clockwise (as opposed to
|
|
|
|
counter-clockwise) parameterization of the input Path.
|
|
|
|
|
|
|
|
To Contributors
|
|
|
|
---------------
|
|
|
|
This is one of many parts of `svgpathtools` that could be
|
|
|
|
improved by a noble soul implementing a piecewise-linear
|
2018-11-05 06:22:20 +00:00
|
|
|
approximation scheme for paths (one with controls to guarantee a
|
|
|
|
desired accuracy).
|
2018-11-05 05:40:56 +00:00
|
|
|
"""
|
|
|
|
|
2018-11-05 06:22:20 +00:00
|
|
|
def area_without_arcs(path):
|
2018-11-05 05:40:56 +00:00
|
|
|
area_enclosed = 0
|
2018-11-05 06:22:20 +00:00
|
|
|
for seg in path:
|
2018-11-05 05:40:56 +00:00
|
|
|
x = real(seg.poly())
|
|
|
|
dy = imag(seg.poly()).deriv()
|
|
|
|
integrand = x*dy
|
|
|
|
integral = integrand.integ()
|
|
|
|
area_enclosed += integral(1) - integral(0)
|
|
|
|
return area_enclosed
|
|
|
|
|
2018-11-05 06:22:20 +00:00
|
|
|
def seg2lines(seg):
|
|
|
|
"""Find piecewise-linear approximation of `seg`."""
|
2018-12-04 06:27:59 +00:00
|
|
|
num_lines = int(ceil(seg.length() / chord_length))
|
|
|
|
pts = [seg.point(t) for t in np.linspace(0, 1, num_lines+1)]
|
|
|
|
return [Line(pts[i], pts[i+1]) for i in range(num_lines)]
|
2018-11-05 06:22:20 +00:00
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
assert self.isclosed()
|
2018-11-05 05:40:56 +00:00
|
|
|
|
2018-11-05 06:22:20 +00:00
|
|
|
bezier_path_approximation = []
|
2016-07-06 04:51:11 +00:00
|
|
|
for seg in self:
|
2018-11-05 05:40:56 +00:00
|
|
|
if isinstance(seg, Arc):
|
2018-11-05 06:22:20 +00:00
|
|
|
bezier_path_approximation += seg2lines(seg)
|
2018-11-05 05:40:56 +00:00
|
|
|
else:
|
2018-11-05 06:22:20 +00:00
|
|
|
bezier_path_approximation.append(seg)
|
|
|
|
return area_without_arcs(Path(*bezier_path_approximation))
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def intersect(self, other_curve, justonemode=False, tol=1e-12):
|
|
|
|
"""returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2))
|
|
|
|
giving the intersection points.
|
|
|
|
If justonemode==True, then returns just the first
|
|
|
|
intersection found.
|
|
|
|
tol is used to check for redundant intersections (see comment above
|
|
|
|
the code block where tol is used).
|
|
|
|
Note: If the two path objects coincide for more than a finite set of
|
|
|
|
points, this code will fail."""
|
|
|
|
path1 = self
|
|
|
|
if isinstance(other_curve, Path):
|
|
|
|
path2 = other_curve
|
|
|
|
else:
|
|
|
|
path2 = Path(other_curve)
|
|
|
|
assert path1 != path2
|
|
|
|
intersection_list = []
|
|
|
|
for seg1 in path1:
|
|
|
|
for seg2 in path2:
|
|
|
|
if justonemode and intersection_list:
|
|
|
|
return intersection_list[0]
|
|
|
|
for t1, t2 in seg1.intersect(seg2, tol=tol):
|
|
|
|
T1 = path1.t2T(seg1, t1)
|
|
|
|
T2 = path2.t2T(seg2, t2)
|
|
|
|
intersection_list.append(((T1, seg1, t1), (T2, seg2, t2)))
|
|
|
|
if justonemode and intersection_list:
|
|
|
|
return intersection_list[0]
|
|
|
|
|
|
|
|
# Note: If the intersection takes place at a joint (point one seg ends
|
|
|
|
# and next begins in path) then intersection_list may contain a
|
|
|
|
# redundant intersection. This code block checks for and removes said
|
|
|
|
# redundancies.
|
|
|
|
if intersection_list:
|
2018-08-22 03:54:02 +00:00
|
|
|
pts = [seg1.point(_t1)
|
|
|
|
for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
|
2016-07-06 04:51:11 +00:00
|
|
|
indices2remove = []
|
|
|
|
for ind1 in range(len(pts)):
|
|
|
|
for ind2 in range(ind1 + 1, len(pts)):
|
|
|
|
if abs(pts[ind1] - pts[ind2]) < tol:
|
|
|
|
# then there's a redundancy. Remove it.
|
|
|
|
indices2remove.append(ind2)
|
|
|
|
intersection_list = [inter for ind, inter in
|
|
|
|
enumerate(intersection_list) if
|
|
|
|
ind not in indices2remove]
|
|
|
|
return intersection_list
|
|
|
|
|
|
|
|
def bbox(self):
|
|
|
|
"""returns a bounding box for the input Path object in the form
|
|
|
|
(xmin, xmax, ymin, ymax)."""
|
|
|
|
bbs = [seg.bbox() for seg in self._segments]
|
2017-03-01 07:04:37 +00:00
|
|
|
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
|
2016-07-06 04:51:11 +00:00
|
|
|
xmin = min(xmins)
|
|
|
|
xmax = max(xmaxs)
|
|
|
|
ymin = min(ymins)
|
|
|
|
ymax = max(ymaxs)
|
|
|
|
return xmin, xmax, ymin, ymax
|
|
|
|
|
|
|
|
def cropped(self, T0, T1):
|
|
|
|
"""returns a cropped copy of the path."""
|
2017-03-30 08:38:27 +00:00
|
|
|
assert 0 <= T0 <= 1 and 0 <= T1<= 1
|
2016-07-06 04:51:11 +00:00
|
|
|
assert T0 != T1
|
2017-03-30 08:38:27 +00:00
|
|
|
assert not (T0 == 1 and T1 == 0)
|
|
|
|
|
|
|
|
if T0 == 1 and 0 < T1 < 1 and self.isclosed():
|
|
|
|
return self.cropped(0, T1)
|
|
|
|
|
2016-07-06 04:51:11 +00:00
|
|
|
if T1 == 1:
|
|
|
|
seg1 = self[-1]
|
|
|
|
t_seg1 = 1
|
|
|
|
i1 = len(self) - 1
|
|
|
|
else:
|
|
|
|
seg1_idx, t_seg1 = self.T2t(T1)
|
|
|
|
seg1 = self[seg1_idx]
|
|
|
|
if np.isclose(t_seg1, 0):
|
|
|
|
i1 = (self.index(seg1) - 1) % len(self)
|
|
|
|
seg1 = self[i1]
|
|
|
|
t_seg1 = 1
|
|
|
|
else:
|
|
|
|
i1 = self.index(seg1)
|
|
|
|
if T0 == 0:
|
|
|
|
seg0 = self[0]
|
|
|
|
t_seg0 = 0
|
|
|
|
i0 = 0
|
|
|
|
else:
|
|
|
|
seg0_idx, t_seg0 = self.T2t(T0)
|
|
|
|
seg0 = self[seg0_idx]
|
|
|
|
if np.isclose(t_seg0, 1):
|
|
|
|
i0 = (self.index(seg0) + 1) % len(self)
|
|
|
|
seg0 = self[i0]
|
|
|
|
t_seg0 = 0
|
|
|
|
else:
|
|
|
|
i0 = self.index(seg0)
|
|
|
|
|
|
|
|
if T0 < T1 and i0 == i1:
|
|
|
|
new_path = Path(seg0.cropped(t_seg0, t_seg1))
|
|
|
|
else:
|
|
|
|
new_path = Path(seg0.cropped(t_seg0, 1))
|
|
|
|
|
|
|
|
# T1<T0 must cross discontinuity case
|
|
|
|
if T1 < T0:
|
2017-03-30 08:38:27 +00:00
|
|
|
if not self.isclosed():
|
2016-07-06 04:51:11 +00:00
|
|
|
raise ValueError("This path is not closed, thus T0 must "
|
|
|
|
"be less than T1.")
|
|
|
|
else:
|
|
|
|
for i in range(i0 + 1, len(self)):
|
|
|
|
new_path.append(self[i])
|
|
|
|
for i in range(0, i1):
|
|
|
|
new_path.append(self[i])
|
|
|
|
|
|
|
|
# T0<T1 straight-forward case
|
|
|
|
else:
|
|
|
|
for i in range(i0 + 1, i1):
|
|
|
|
new_path.append(self[i])
|
|
|
|
|
|
|
|
if t_seg1 != 0:
|
|
|
|
new_path.append(seg1.cropped(0, t_seg1))
|
|
|
|
return new_path
|
|
|
|
|
|
|
|
def radialrange(self, origin, return_all_global_extrema=False):
|
|
|
|
"""returns the tuples (d_min, t_min, idx_min), (d_max, t_max, idx_max)
|
|
|
|
which minimize and maximize, respectively, the distance
|
|
|
|
d = |self[idx].point(t)-origin|."""
|
|
|
|
if return_all_global_extrema:
|
|
|
|
raise NotImplementedError
|
|
|
|
else:
|
|
|
|
global_min = (np.inf, None, None)
|
|
|
|
global_max = (0, None, None)
|
|
|
|
for seg_idx, seg in enumerate(self):
|
|
|
|
seg_global_min, seg_global_max = seg.radialrange(origin)
|
|
|
|
if seg_global_min[0] < global_min[0]:
|
|
|
|
global_min = seg_global_min + (seg_idx,)
|
|
|
|
if seg_global_max[0] > global_max[0]:
|
|
|
|
global_max = seg_global_max + (seg_idx,)
|
|
|
|
return global_min, global_max
|
|
|
|
|
|
|
|
def rotated(self, degs, origin=None):
|
|
|
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
|
|
|
point `origin` (a complex number). By default `origin` is either
|
|
|
|
`self.point(0.5)`, or in the case that self is an Arc object,
|
|
|
|
`origin` defaults to `self.center`."""
|
2016-10-28 13:11:31 +00:00
|
|
|
return rotate(self, degs, origin=origin)
|
2016-07-06 04:51:11 +00:00
|
|
|
|
|
|
|
def translated(self, z0):
|
|
|
|
"""Returns a copy of self shifted by the complex quantity `z0` such
|
|
|
|
that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
|
|
|
|
return translate(self, z0)
|
2018-05-22 19:45:04 +00:00
|
|
|
|
2018-05-31 02:07:58 +00:00
|
|
|
def scaled(self, sx, sy=None, origin=0j):
|
|
|
|
"""Scale transform. See `scale` function for further explanation."""
|
|
|
|
return scale(self, sx=sx, sy=sy, origin=origin)
|
2020-06-20 02:36:22 +00:00
|
|
|
|
|
|
|
def is_contained_by(self, other):
|
|
|
|
"""Returns true if the path is fully contained in other closed path"""
|
|
|
|
assert isinstance(other, Path)
|
|
|
|
assert other.isclosed()
|
|
|
|
assert self != other
|
|
|
|
|
|
|
|
if self.intersect(other, justonemode=True):
|
|
|
|
return False
|
|
|
|
|
|
|
|
pt = self.point(0)
|
|
|
|
xmin, xmax, ymin, ymax = other.bbox()
|
|
|
|
pt_in_bbox = (xmin <= pt.real <= xmax) and (ymin <= pt.imag <= ymax)
|
|
|
|
|
|
|
|
if not pt_in_bbox:
|
|
|
|
return False
|
|
|
|
|
|
|
|
opt = complex(xmin-1, ymin-1)
|
|
|
|
return path_encloses_pt(pt, opt, other)
|
2020-07-06 18:01:15 +00:00
|
|
|
|
2020-12-02 02:37:15 +00:00
|
|
|
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))
|
|
|
|
|
2020-07-06 18:01:15 +00:00
|
|
|
def _tokenize_path(self, pathdef):
|
|
|
|
for x in COMMAND_RE.split(pathdef):
|
|
|
|
if x in COMMANDS:
|
|
|
|
yield x
|
|
|
|
for token in FLOAT_RE.findall(x):
|
|
|
|
yield token
|
|
|
|
|
|
|
|
def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
|
|
|
|
# In the SVG specs, initial movetos are absolute, even if
|
|
|
|
# specified as 'm'. This is the default behavior here as well.
|
|
|
|
# But if you pass in a current_pos variable, the initial moveto
|
|
|
|
# will be relative to that current_pos. This is useful.
|
|
|
|
elements = list(self._tokenize_path(pathdef))
|
|
|
|
# Reverse for easy use of .pop()
|
|
|
|
elements.reverse()
|
|
|
|
|
|
|
|
segments = self._segments
|
|
|
|
|
|
|
|
start_pos = None
|
|
|
|
command = None
|
|
|
|
|
|
|
|
while elements:
|
|
|
|
|
|
|
|
if elements[-1] in COMMANDS:
|
|
|
|
# New command.
|
|
|
|
last_command = command # Used by S and T
|
|
|
|
command = elements.pop()
|
|
|
|
absolute = command in UPPERCASE
|
|
|
|
command = command.upper()
|
|
|
|
else:
|
|
|
|
# If this element starts with numbers, it is an implicit command
|
|
|
|
# and we don't change the command. Check that it's allowed:
|
|
|
|
if command is None:
|
|
|
|
raise ValueError("Unallowed implicit command in %s, position %s" % (
|
|
|
|
pathdef, len(pathdef.split()) - len(elements)))
|
2020-07-17 00:51:07 +00:00
|
|
|
last_command = command # Used by S and T
|
2020-07-06 18:01:15 +00:00
|
|
|
|
|
|
|
if command == 'M':
|
|
|
|
# Moveto command.
|
|
|
|
x = elements.pop()
|
|
|
|
y = elements.pop()
|
|
|
|
pos = float(x) + float(y) * 1j
|
|
|
|
if absolute:
|
|
|
|
current_pos = pos
|
|
|
|
else:
|
|
|
|
current_pos += pos
|
|
|
|
|
|
|
|
# when M is called, reset start_pos
|
|
|
|
# This behavior of Z is defined in svg spec:
|
|
|
|
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
|
|
|
start_pos = current_pos
|
|
|
|
|
|
|
|
# Implicit moveto commands are treated as lineto commands.
|
|
|
|
# So we set command to lineto here, in case there are
|
|
|
|
# further implicit commands after this moveto.
|
|
|
|
command = 'L'
|
|
|
|
|
|
|
|
elif command == 'Z':
|
|
|
|
# Close path
|
|
|
|
if not (current_pos == start_pos):
|
|
|
|
segments.append(Line(current_pos, start_pos))
|
2020-11-16 00:23:27 +00:00
|
|
|
self._closed = True
|
2020-07-06 18:01:15 +00:00
|
|
|
current_pos = start_pos
|
|
|
|
command = None
|
|
|
|
|
|
|
|
elif command == 'L':
|
|
|
|
x = elements.pop()
|
|
|
|
y = elements.pop()
|
|
|
|
pos = float(x) + float(y) * 1j
|
|
|
|
if not absolute:
|
|
|
|
pos += current_pos
|
|
|
|
segments.append(Line(current_pos, pos))
|
|
|
|
current_pos = pos
|
|
|
|
|
|
|
|
elif command == 'H':
|
|
|
|
x = elements.pop()
|
|
|
|
pos = float(x) + current_pos.imag * 1j
|
|
|
|
if not absolute:
|
|
|
|
pos += current_pos.real
|
|
|
|
segments.append(Line(current_pos, pos))
|
|
|
|
current_pos = pos
|
|
|
|
|
|
|
|
elif command == 'V':
|
|
|
|
y = elements.pop()
|
|
|
|
pos = current_pos.real + float(y) * 1j
|
|
|
|
if not absolute:
|
|
|
|
pos += current_pos.imag * 1j
|
|
|
|
segments.append(Line(current_pos, pos))
|
|
|
|
current_pos = pos
|
|
|
|
|
|
|
|
elif command == 'C':
|
|
|
|
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
control1 += current_pos
|
|
|
|
control2 += current_pos
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(CubicBezier(current_pos, control1, control2, end))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
elif command == 'S':
|
|
|
|
# Smooth curve. First control point is the "reflection" of
|
|
|
|
# the second control point in the previous path.
|
|
|
|
|
|
|
|
if last_command not in 'CS':
|
|
|
|
# If there is no previous command or if the previous command
|
|
|
|
# was not an C, c, S or s, assume the first control point is
|
|
|
|
# coincident with the current point.
|
|
|
|
control1 = current_pos
|
|
|
|
else:
|
|
|
|
# The first control point is assumed to be the reflection of
|
|
|
|
# the second control point on the previous command relative
|
|
|
|
# to the current point.
|
|
|
|
control1 = current_pos + current_pos - segments[-1].control2
|
|
|
|
|
|
|
|
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
control2 += current_pos
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(CubicBezier(current_pos, control1, control2, end))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
elif command == 'Q':
|
|
|
|
control = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
control += current_pos
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(QuadraticBezier(current_pos, control, end))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
elif command == 'T':
|
|
|
|
# Smooth curve. Control point is the "reflection" of
|
|
|
|
# the second control point in the previous path.
|
|
|
|
|
|
|
|
if last_command not in 'QT':
|
|
|
|
# If there is no previous command or if the previous command
|
|
|
|
# was not an Q, q, T or t, assume the first control point is
|
|
|
|
# coincident with the current point.
|
|
|
|
control = current_pos
|
|
|
|
else:
|
|
|
|
# The control point is assumed to be the reflection of
|
|
|
|
# the control point on the previous command relative
|
|
|
|
# to the current point.
|
|
|
|
control = current_pos + current_pos - segments[-1].control
|
|
|
|
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(QuadraticBezier(current_pos, control, end))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
elif command == 'A':
|
2020-11-16 00:23:27 +00:00
|
|
|
|
2020-07-06 18:01:15 +00:00
|
|
|
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
rotation = float(elements.pop())
|
|
|
|
arc = float(elements.pop())
|
|
|
|
sweep = float(elements.pop())
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
end += current_pos
|
|
|
|
|
2020-11-16 00:23:27 +00:00
|
|
|
if radius.real == 0 or radius.imag == 0:
|
|
|
|
# Note: In browsers AFAIK, zero radius arcs are displayed
|
|
|
|
# as lines (see "examples/zero-radius-arcs.svg").
|
|
|
|
# Thus zero radius arcs are substituted for lines here.
|
2020-12-02 04:32:26 +00:00
|
|
|
warn('Replacing degenerate (zero radius) Arc with a Line: '
|
|
|
|
'Arc(start={}, radius={}, rotation={}, large_arc={}, '
|
|
|
|
'sweep={}, end={})'.format(
|
|
|
|
current_pos, radius, rotation, arc, sweep, end) +
|
|
|
|
' --> Line(start={}, end={})'
|
|
|
|
''.format(current_pos, end))
|
2020-11-16 00:23:27 +00:00
|
|
|
segments.append(Line(current_pos, end))
|
|
|
|
else:
|
|
|
|
segments.append(
|
|
|
|
Arc(current_pos, radius, rotation, arc, sweep, end))
|
2020-07-06 18:01:15 +00:00
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
return segments
|