647 lines
23 KiB
Python
647 lines
23 KiB
Python
|
from __future__ import division
|
||
|
from math import sqrt, cos, sin, acos, degrees, radians, log
|
||
|
from collections.abc import MutableSequence
|
||
|
|
||
|
# This file contains classes for the different types of SVG path segments as
|
||
|
# well as a Path object that contains a sequence of path segments.
|
||
|
|
||
|
MIN_DEPTH = 5
|
||
|
ERROR = 1e-12
|
||
|
|
||
|
def segment_length(curve, start, end, start_point, end_point, error, min_depth, depth):
|
||
|
"""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 approximate(path, start, end, start_point, end_point, max_error, depth, max_depth):
|
||
|
if depth >= max_depth:
|
||
|
return [start_point, end_point]
|
||
|
actual_length = path.measure(start, end, error=max_error/4)
|
||
|
linear_length = abs(end_point - start_point)
|
||
|
# Worst case deviation given a fixed linear_length and actual_length would probably be
|
||
|
# a symmetric tent shape (I haven't proved it -- TODO).
|
||
|
deviationSquared = (actual_length/2)**2 - (linear_length/2)**2
|
||
|
if deviationSquared <= max_error ** 2:
|
||
|
return [start_point, end_point]
|
||
|
else:
|
||
|
mid = (start+end)/2.
|
||
|
mid_point = path.point(mid)
|
||
|
return ( approximate(path, start, mid, start_point, mid_point, max_error, depth+1, max_depth)[:-1] +
|
||
|
approximate(path, mid, end, mid_point, end_point, max_error, depth+1, max_depth) )
|
||
|
|
||
|
def removeCollinear(points, error, pointsToKeep=set()):
|
||
|
out = []
|
||
|
|
||
|
lengths = [0]
|
||
|
|
||
|
for i in range(1,len(points)):
|
||
|
lengths.append(lengths[-1] + abs(points[i]-points[i-1]))
|
||
|
|
||
|
def length(a,b):
|
||
|
return lengths[b] - lengths[a]
|
||
|
|
||
|
i = 0
|
||
|
|
||
|
while i < len(points):
|
||
|
j = len(points) - 1
|
||
|
while i < j:
|
||
|
deviationSquared = (length(i, j)/2)**2 - (abs(points[j]-points[i])/2)**2
|
||
|
if deviationSquared <= error ** 2 and set(range(i+1,j)).isdisjoint(pointsToKeep):
|
||
|
out.append(points[i])
|
||
|
i = j
|
||
|
break
|
||
|
j -= 1
|
||
|
out.append(points[j])
|
||
|
i += 1
|
||
|
|
||
|
return out
|
||
|
|
||
|
class Segment(object):
|
||
|
def __init__(self, start, end):
|
||
|
self.start = start
|
||
|
self.end = end
|
||
|
|
||
|
def measure(self, start, end, error=ERROR, min_depth=MIN_DEPTH):
|
||
|
return Path(self).measure(start, end, error=error, min_depth=min_depth)
|
||
|
|
||
|
def getApproximatePoints(self, error=0.001, max_depth=32):
|
||
|
points = approximate(self, 0., 1., self.point(0.), self.point(1.), error, 0, max_depth)
|
||
|
return points
|
||
|
|
||
|
class Line(Segment):
|
||
|
def __init__(self, start, end):
|
||
|
super(Line, self).__init__(start,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 getApproximatePoints(self, error=0.001, max_depth=32):
|
||
|
return [self.start, self.end]
|
||
|
|
||
|
def point(self, pos):
|
||
|
if pos == 0.:
|
||
|
return self.start
|
||
|
elif pos == 1.:
|
||
|
return self.end
|
||
|
distance = self.end - self.start
|
||
|
return self.start + distance * pos
|
||
|
|
||
|
def length(self, error=None, min_depth=None):
|
||
|
distance = (self.end - self.start)
|
||
|
return sqrt(distance.real ** 2 + distance.imag ** 2)
|
||
|
|
||
|
|
||
|
class CubicBezier(Segment):
|
||
|
def __init__(self, start, control1, control2, end):
|
||
|
super(CubicBezier, self).__init__(start,end)
|
||
|
self.control1 = control1
|
||
|
self.control2 = control2
|
||
|
|
||
|
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 is_smooth_from(self, previous):
|
||
|
"""Checks if this segment would be a smooth segment following the previous"""
|
||
|
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 point(self, pos):
|
||
|
"""Calculate the x,y position at a certain position of the path"""
|
||
|
if pos == 0.:
|
||
|
return self.start
|
||
|
elif pos == 1.:
|
||
|
return self.end
|
||
|
return ((1 - pos) ** 3 * self.start) + \
|
||
|
(3 * (1 - pos) ** 2 * pos * self.control1) + \
|
||
|
(3 * (1 - pos) * pos ** 2 * self.control2) + \
|
||
|
(pos ** 3 * self.end)
|
||
|
|
||
|
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||
|
"""Calculate the length of the path up to a certain position"""
|
||
|
start_point = self.point(0)
|
||
|
end_point = self.point(1)
|
||
|
return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0)
|
||
|
|
||
|
|
||
|
class QuadraticBezier(Segment):
|
||
|
def __init__(self, start, control, end):
|
||
|
super(QuadraticBezier, self).__init__(start,end)
|
||
|
self.control = control
|
||
|
|
||
|
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 is_smooth_from(self, previous):
|
||
|
"""Checks if this segment would be a smooth segment following the previous"""
|
||
|
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 point(self, pos):
|
||
|
if pos == 0.:
|
||
|
return self.start
|
||
|
elif pos == 1.:
|
||
|
return self.end
|
||
|
return (1 - pos) ** 2 * self.start + 2 * (1 - pos) * pos * self.control + \
|
||
|
pos ** 2 * self.end
|
||
|
|
||
|
def length(self, error=None, min_depth=None):
|
||
|
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)
|
||
|
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
|
||
|
k = abs(b)/abs(a)
|
||
|
if k >= 2:
|
||
|
s = abs(b) - abs(a)
|
||
|
else:
|
||
|
s = abs(a)*(k**2/2 - k + 1)
|
||
|
else:
|
||
|
# For an explanation of this case, see
|
||
|
# http://www.malczak.info/blog/quadratic-bezier-curve-length/
|
||
|
A = 4 * (a.real ** 2 + a.imag ** 2)
|
||
|
B = 4 * (a.real * b.real + a.imag * b.imag)
|
||
|
C = b.real ** 2 + b.imag ** 2
|
||
|
|
||
|
Sabc = 2 * sqrt(A + B + C)
|
||
|
A2 = sqrt(A)
|
||
|
A32 = 2 * A * A2
|
||
|
C2 = 2 * sqrt(C)
|
||
|
BA = B / A2
|
||
|
|
||
|
s = (A32 * Sabc + A2 * B * (Sabc - C2) + (4 * C * A - B ** 2) *
|
||
|
log((2 * A2 + BA + Sabc) / (BA + C2))) / (4 * A32)
|
||
|
return s
|
||
|
|
||
|
class Arc(Segment):
|
||
|
def __init__(self, start, radius, rotation, arc, sweep, end, scaler=lambda z:z):
|
||
|
"""radius is complex, rotation is in degrees,
|
||
|
large and sweep are 1 or 0 (True/False also work)"""
|
||
|
|
||
|
super(Arc, self).__init__(scaler(start),scaler(end))
|
||
|
self.start0 = start
|
||
|
self.end0 = end
|
||
|
self.radius = radius
|
||
|
self.rotation = rotation
|
||
|
self.arc = bool(arc)
|
||
|
self.sweep = bool(sweep)
|
||
|
self.scaler = scaler
|
||
|
|
||
|
self._parameterize()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'Arc(start0=%s, radius=%s, rotation=%s, arc=%s, sweep=%s, end0=%s, scaler=%s)' % (
|
||
|
self.start0, self.radius, self.rotation, self.arc, self.sweep, self.end0, self.scaler)
|
||
|
|
||
|
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.arc == other.arc and self.sweep == other.sweep
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
if not isinstance(other, Arc):
|
||
|
return NotImplemented
|
||
|
return not self == other
|
||
|
|
||
|
def _parameterize(self):
|
||
|
# Conversion from endpoint to center parameterization
|
||
|
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||
|
|
||
|
cosr = cos(radians(self.rotation))
|
||
|
sinr = sin(radians(self.rotation))
|
||
|
dx = (self.start0.real - self.end0.real) / 2
|
||
|
dy = (self.start0.imag - self.end0.imag) / 2
|
||
|
x1prim = cosr * dx + sinr * dy
|
||
|
x1prim_sq = x1prim * x1prim
|
||
|
y1prim = -sinr * dx + cosr * dy
|
||
|
y1prim_sq = y1prim * y1prim
|
||
|
|
||
|
rx = self.radius.real
|
||
|
rx_sq = rx * rx
|
||
|
ry = self.radius.imag
|
||
|
ry_sq = ry * ry
|
||
|
|
||
|
# Correct out of range radii
|
||
|
radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
|
||
|
if radius_check > 1:
|
||
|
rx *= sqrt(radius_check)
|
||
|
ry *= sqrt(radius_check)
|
||
|
rx_sq = rx * rx
|
||
|
ry_sq = ry * ry
|
||
|
|
||
|
t1 = rx_sq * y1prim_sq
|
||
|
t2 = ry_sq * x1prim_sq
|
||
|
c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2)))
|
||
|
|
||
|
if self.arc == self.sweep:
|
||
|
c = -c
|
||
|
cxprim = c * rx * y1prim / ry
|
||
|
cyprim = -c * ry * x1prim / rx
|
||
|
|
||
|
self.center = complex((cosr * cxprim - sinr * cyprim) +
|
||
|
((self.start0.real + self.end0.real) / 2),
|
||
|
(sinr * cxprim + cosr * cyprim) +
|
||
|
((self.start0.imag + self.end0.imag) / 2))
|
||
|
|
||
|
ux = (x1prim - cxprim) / rx
|
||
|
uy = (y1prim - cyprim) / ry
|
||
|
vx = (-x1prim - cxprim) / rx
|
||
|
vy = (-y1prim - cyprim) / ry
|
||
|
n = sqrt(ux * ux + uy * uy)
|
||
|
p = ux
|
||
|
theta = degrees(acos(p / n))
|
||
|
if uy < 0:
|
||
|
theta = -theta
|
||
|
self.theta = theta % 360
|
||
|
|
||
|
n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
|
||
|
p = ux * vx + uy * vy
|
||
|
d = p/n
|
||
|
# In certain cases the above calculation can through inaccuracies
|
||
|
# become just slightly out of range, f ex -1.0000000000000002.
|
||
|
if d > 1.0:
|
||
|
d = 1.0
|
||
|
elif d < -1.0:
|
||
|
d = -1.0
|
||
|
delta = degrees(acos(d))
|
||
|
if (ux * vy - uy * vx) < 0:
|
||
|
delta = -delta
|
||
|
self.delta = delta % 360
|
||
|
if not self.sweep:
|
||
|
self.delta -= 360
|
||
|
|
||
|
def point(self, pos):
|
||
|
if pos == 0.:
|
||
|
return self.start
|
||
|
elif pos == 1.:
|
||
|
return self.end
|
||
|
angle = radians(self.theta + (self.delta * pos))
|
||
|
cosr = cos(radians(self.rotation))
|
||
|
sinr = sin(radians(self.rotation))
|
||
|
|
||
|
x = (cosr * cos(angle) * self.radius.real - sinr * sin(angle) *
|
||
|
self.radius.imag + self.center.real)
|
||
|
y = (sinr * cos(angle) * self.radius.real + cosr * sin(angle) *
|
||
|
self.radius.imag + self.center.imag)
|
||
|
return self.scaler(complex(x, y))
|
||
|
|
||
|
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||
|
"""The length of an elliptical arc segment requires numerical
|
||
|
integration, and in that case it's simpler to just do a geometric
|
||
|
approximation, as for cubic bezier curves.
|
||
|
"""
|
||
|
start_point = self.point(0)
|
||
|
end_point = self.point(1)
|
||
|
return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0)
|
||
|
|
||
|
class SVGState(object):
|
||
|
def __init__(self, fill=(0.,0.,0.), fillOpacity=None, fillRule='nonzero', stroke=None, strokeOpacity=None, strokeWidth=0.1, strokeWidthScaling=True):
|
||
|
self.fill = fill
|
||
|
self.fillOpacity = fillOpacity
|
||
|
self.fillRule = fillRule
|
||
|
self.stroke = stroke
|
||
|
self.strokeOpacity = strokeOpacity
|
||
|
self.strokeWidth = strokeWidth
|
||
|
self.strokeWidthScaling = strokeWidthScaling
|
||
|
|
||
|
def clone(self):
|
||
|
return SVGState(fill=self.fill, fillOpacity=self.fillOpacity, fillRule=self.fillRule, stroke=self.stroke, strokeOpacity=self.strokeOpacity,
|
||
|
strokeWidth=self.strokeWidth, strokeWidthScaling=self.strokeWidthScaling)
|
||
|
|
||
|
class Path(MutableSequence):
|
||
|
"""A Path is a sequence of path segments"""
|
||
|
|
||
|
# Put it here, so there is a default if unpickled.
|
||
|
_closed = False
|
||
|
|
||
|
def __init__(self, *segments, **kw):
|
||
|
self._segments = list(segments)
|
||
|
self._length = None
|
||
|
self._lengths = None
|
||
|
if 'closed' in kw:
|
||
|
self.closed = kw['closed']
|
||
|
if 'svgState' in kw:
|
||
|
self.svgState = kw['svgState']
|
||
|
else:
|
||
|
self.svgState = SVGState()
|
||
|
|
||
|
def __getitem__(self, index):
|
||
|
return self._segments[index]
|
||
|
|
||
|
def __setitem__(self, index, value):
|
||
|
self._segments[index] = value
|
||
|
self._length = None
|
||
|
|
||
|
def __delitem__(self, index):
|
||
|
del self._segments[index]
|
||
|
self._length = None
|
||
|
|
||
|
def insert(self, index, value):
|
||
|
self._segments.insert(index, value)
|
||
|
self._length = None
|
||
|
|
||
|
def reverse(self):
|
||
|
# Reversing the order of a path would require reversing each element
|
||
|
# as well. That's not implemented.
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._segments)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'Path(%s, closed=%s)' % (
|
||
|
', '.join(repr(x) for x in self._segments), self.closed)
|
||
|
|
||
|
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=ERROR, min_depth=MIN_DEPTH):
|
||
|
## TODO: check if error has decreased since last calculation
|
||
|
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 / (1 if self._length==0. else self._length) for each in lengths]
|
||
|
|
||
|
def point(self, pos, error=ERROR):
|
||
|
# Shortcuts
|
||
|
if pos == 0.0:
|
||
|
return self._segments[0].point(pos)
|
||
|
if pos == 1.0:
|
||
|
return self._segments[-1].point(pos)
|
||
|
|
||
|
self._calc_lengths(error=error)
|
||
|
# 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)
|
||
|
break
|
||
|
segment_start = segment_end
|
||
|
|
||
|
return segment.point(segment_pos)
|
||
|
|
||
|
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||
|
self._calc_lengths(error, min_depth)
|
||
|
return self._length
|
||
|
|
||
|
def measure(self, start, end, error=ERROR, min_depth=MIN_DEPTH):
|
||
|
self._calc_lengths(error=error)
|
||
|
if start == 0.0 and end == 1.0:
|
||
|
return self.length()
|
||
|
length = 0
|
||
|
segment_start = 0
|
||
|
for index, segment in enumerate(self._segments):
|
||
|
if end <= segment_start:
|
||
|
break
|
||
|
segment_end = segment_start + self._lengths[index]
|
||
|
if start < segment_end:
|
||
|
# this segment intersects the part of the path we want
|
||
|
if start <= segment_start and segment_end <= end:
|
||
|
# whole segment is contained in the part of the path
|
||
|
length += self._lengths[index] * self._length
|
||
|
else:
|
||
|
if start <= segment_start:
|
||
|
start_in_segment = 0.
|
||
|
else:
|
||
|
start_in_segment = (start-segment_start)/(segment_end-segment_start)
|
||
|
if segment_end <= end:
|
||
|
end_in_segment = 1.
|
||
|
else:
|
||
|
end_in_segment = (end-segment_start)/(segment_end-segment_start)
|
||
|
segment = self._segments[index]
|
||
|
length += segment_length(segment, start_in_segment, end_in_segment, segment.point(start_in_segment),
|
||
|
segment.point(end_in_segment), error, MIN_DEPTH, 0)
|
||
|
segment_start = segment_end
|
||
|
return length
|
||
|
|
||
|
def _is_closable(self):
|
||
|
"""Returns true if the end is on the start of a segment"""
|
||
|
try:
|
||
|
end = self[-1].end
|
||
|
except:
|
||
|
return False
|
||
|
for segment in self:
|
||
|
if segment.start == end:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def breakup(self):
|
||
|
paths = []
|
||
|
prevEnd = None
|
||
|
segments = []
|
||
|
for segment in self._segments:
|
||
|
if prevEnd is None or segment.point(0.) == prevEnd:
|
||
|
segments.append(segment)
|
||
|
else:
|
||
|
paths.append(Path(*segments, svgState=self.svgState))
|
||
|
segments = [segment]
|
||
|
prevEnd = segment.point(1.)
|
||
|
|
||
|
if len(segments) > 0:
|
||
|
paths.append(Path(*segments, svgState=self.svgState))
|
||
|
|
||
|
return paths
|
||
|
|
||
|
def linearApproximation(self, error=0.001, max_depth=32):
|
||
|
closed = False
|
||
|
keepSegmentIndex = 0
|
||
|
if self.closed:
|
||
|
end = self[-1].end
|
||
|
for i,segment in enumerate(self):
|
||
|
if segment.start == end:
|
||
|
keepSegmentIndex = i
|
||
|
closed = True
|
||
|
break
|
||
|
|
||
|
keepSubpathIndex = 0
|
||
|
keepPointIndex = 0
|
||
|
|
||
|
subpaths = []
|
||
|
subpath = []
|
||
|
prevEnd = None
|
||
|
for i,segment in enumerate(self._segments):
|
||
|
if prevEnd is None or segment.start == prevEnd:
|
||
|
if i == keepSegmentIndex:
|
||
|
keepSubpathIndex = len(subpaths)
|
||
|
keepPointIndex = len(subpath)
|
||
|
else:
|
||
|
subpaths.append(subpath)
|
||
|
subpath = []
|
||
|
subpath += segment.getApproximatePoints(error=error/2., max_depth=max_depth)
|
||
|
prevEnd = segment.end
|
||
|
|
||
|
if len(subpath) > 0:
|
||
|
subpaths.append(subpath)
|
||
|
|
||
|
linearPath = Path(svgState=self.svgState)
|
||
|
|
||
|
for i,subpath in enumerate(subpaths):
|
||
|
keep = set((keepPointIndex,)) if i == keepSubpathIndex else set()
|
||
|
special = None
|
||
|
if i == keepSubpathIndex:
|
||
|
special = subpath[keepPointIndex]
|
||
|
points = removeCollinear(subpath, error=error/2., pointsToKeep=keep)
|
||
|
# points = subpath
|
||
|
|
||
|
for j in range(len(points)-1):
|
||
|
linearPath.append(Line(points[j], points[j+1]))
|
||
|
|
||
|
linearPath.closed = self.closed and linearPath._is_closable()
|
||
|
linearPath.svgState = self.svgState
|
||
|
|
||
|
return linearPath
|
||
|
|
||
|
def getApproximateLines(self, error=0.001, max_depth=32):
|
||
|
lines = []
|
||
|
for subpath in self.breakup():
|
||
|
points = subpath.getApproximatePoints(error=error, max_depth=max_depth)
|
||
|
for i in range(len(points)-1):
|
||
|
lines.append(points[i],points[i+1])
|
||
|
return lines
|
||
|
|
||
|
@property
|
||
|
def closed(self):
|
||
|
"""Checks that the path is closed"""
|
||
|
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
|
||
|
|
||
|
def d(self):
|
||
|
if self.closed:
|
||
|
segments = self[:-1]
|
||
|
else:
|
||
|
segments = self[:]
|
||
|
|
||
|
current_pos = None
|
||
|
parts = []
|
||
|
previous_segment = None
|
||
|
end = self[-1].end
|
||
|
|
||
|
for segment in segments:
|
||
|
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 != start or (self.closed and start == end):
|
||
|
parts.append('M {0:G},{1:G}'.format(start.real, start.imag))
|
||
|
|
||
|
if isinstance(segment, Line):
|
||
|
parts.append('L {0:G},{1:G}'.format(
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
elif isinstance(segment, CubicBezier):
|
||
|
if segment.is_smooth_from(previous_segment):
|
||
|
parts.append('S {0:G},{1:G} {2:G},{3:G}'.format(
|
||
|
segment.control2.real, segment.control2.imag,
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
else:
|
||
|
parts.append('C {0:G},{1:G} {2:G},{3:G} {4:G},{5:G}'.format(
|
||
|
segment.control1.real, segment.control1.imag,
|
||
|
segment.control2.real, segment.control2.imag,
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
elif isinstance(segment, QuadraticBezier):
|
||
|
if segment.is_smooth_from(previous_segment):
|
||
|
parts.append('T {0:G},{1:G}'.format(
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
else:
|
||
|
parts.append('Q {0:G},{1:G} {2:G},{3:G}'.format(
|
||
|
segment.control.real, segment.control.imag,
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
|
||
|
elif isinstance(segment, Arc):
|
||
|
parts.append('A {0:G},{1:G} {2:G} {3:d},{4:d} {5:G},{6:G}'.format(
|
||
|
segment.radius.real, segment.radius.imag, segment.rotation,
|
||
|
int(segment.arc), int(segment.sweep),
|
||
|
segment.end.real, segment.end.imag)
|
||
|
)
|
||
|
current_pos = segment.end
|
||
|
previous_segment = segment
|
||
|
|
||
|
if self.closed:
|
||
|
parts.append('Z')
|
||
|
|
||
|
return ' '.join(parts)
|
||
|
|