2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2003-2004 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26 import attr
, color
, helper
, path
, style
, trafo
, unit
28 import Numeric
, LinearAlgebra
31 def realpolyroots(coeffs
, epsilon
=1e-5): # <<<
33 """returns the roots of a polynom with given coefficients
35 This helper routine uses the package Numeric to find the roots
36 of the polynomial with coefficients given in coeffs:
37 0 = \sum_{i=0}^N x^{N-i} coeffs[i]
38 The solution is found via an equivalent eigenvalue problem
44 return realpolyroots(coeffs
[1:], epsilon
=epsilon
)
48 # build the Matrix of the polynomial problem
49 mat
= Numeric
.zeros((N
, N
), Numeric
.Float
)
53 mat
[0][i
] = -coeffs
[i
+1]/coeffs
[0]
54 # find the eigenvalues of the matrix (== the zeros of the polynomial)
55 zeros
= [complex(zero
) for zero
in LinearAlgebra
.eigenvalues(mat
)]
56 # take only the real zeros
57 zeros
= [zero
.real
for zero
in zeros
if -epsilon
< zero
.imag
< epsilon
]
59 ## check if the zeros are really zeros!
63 # p += coeffs[i] * zero**(N-i)
64 # if abs(p) > epsilon:
65 # raise Exception("value %f instead of 0" % p)
75 return (x
>= 0) and 1 or -1
77 def curvescontrols_from_endlines_pt (B
, tangent1
, tangent2
, r1
, r2
, softness
): # <<<
78 # calculates the parameters for two bezier curves connecting two lines (curvature=0)
79 # starting at B - r1*tangent1
80 # ending at B + r2*tangent2
83 # and two tangent vectors heading to and from B
84 # and two radii r1 and r2:
85 # All arguments must be in Points
86 # Returns the seven control points of the two bezier curves:
88 # - control points g1 and f1
90 # - control points f2 and g2
93 # make direction vectors d1: from B to A
95 d1
= -tangent1
[0] / math
.hypot(*tangent1
), -tangent1
[1] / math
.hypot(*tangent1
)
96 d2
= tangent2
[0] / math
.hypot(*tangent2
), tangent2
[1] / math
.hypot(*tangent2
)
98 # 0.3192 has turned out to be the maximum softness available
99 # for straight lines ;-)
100 f
= 0.3192 * softness
101 g
= (15.0 * f
+ math
.sqrt(-15.0*f
*f
+ 24.0*f
))/12.0
103 # make the control points of the two bezier curves
104 f1
= B
[0] + f
* r1
* d1
[0], B
[1] + f
* r1
* d1
[1]
105 f2
= B
[0] + f
* r2
* d2
[0], B
[1] + f
* r2
* d2
[1]
106 g1
= B
[0] + g
* r1
* d1
[0], B
[1] + g
* r1
* d1
[1]
107 g2
= B
[0] + g
* r2
* d2
[0], B
[1] + g
* r2
* d2
[1]
108 d1
= B
[0] + r1
* d1
[0], B
[1] + r1
* d1
[1]
109 d2
= B
[0] + r2
* d2
[0], B
[1] + r2
* d2
[1]
110 e
= 0.5 * (f1
[0] + f2
[0]), 0.5 * (f1
[1] + f2
[1])
112 return (d1
, g1
, f1
, e
, f2
, g2
, d2
)
115 def controldists_from_endpoints_pt (A
, B
, tangentA
, tangentB
, curvA
, curvB
, epsilon
=1e-5): # <<<
117 """distances for a curve given by tangents and curvatures at the endpoints
119 This helper routine returns the two distances between the endpoints and the
120 corresponding control points of a (cubic) bezier curve that has
121 prescribed tangents tangentA, tangentB and curvatures curvA, curvB at the
125 # normalise the tangent vectors
126 normA
= math
.hypot(*tangentA
)
127 tangA
= (tangentA
[0] / normA
, tangentA
[1] / normA
)
128 normB
= math
.hypot(*tangentB
)
129 tangB
= (tangentB
[0] / normB
, tangentB
[1] / normB
)
131 T
= tangB
[0] * tangA
[1] - tangB
[1] * tangA
[0]
132 D
= tangA
[0] * (B
[1]-A
[1]) - tangA
[1] * (B
[0]-A
[0])
133 E
= - tangB
[0] * (B
[1]-A
[1]) + tangB
[1] * (B
[0]-A
[0])
134 # the variables: \dot X(0) = 3 * a * tangA
135 # \dot X(1) = 3 * b * tangB
139 # try some special cases where the equations decouple
142 except ZeroDivisionError:
144 a
= math
.sqrt(2.0 * D
/ (3.0 * curvA
))
145 b
= math
.sqrt(2.0 * E
/ (3.0 * curvB
))
146 except ZeroDivisionError:
153 except ZeroDivisionError:
155 a
= (1.5*curvB
*b
*b
- E
) / T
159 except ZeroDivisionError:
161 b
= (1.5*curvA
*a
*a
- D
) / T
166 # else find a solution for the full problem
169 # we first try to find all the zeros of the polynomials for a or b (4th order)
170 # this needs Numeric and LinearAlgebra
172 coeffs_a
= (3.375*curvA
*curvA
*curvB
, 0, -4.5*curvA
*curvB
*D
, -T
**3, 1.5*curvB
*D
*D
- T
*T
*E
)
173 coeffs_b
= (3.375*curvA
*curvB
*curvB
, 0, -4.5*curvA
*curvB
*E
, -T
**3, 1.5*curvA
*E
*E
- T
*T
*D
)
175 # First try the equation for a
176 cands_a
= [cand
for cand
in realpolyroots(coeffs_a
) if cand
> 0]
180 b
= (1.5*curvA
*a
*a
- D
) / T
182 # then, try the equation for b
183 cands_b
= [cand
for cand
in realpolyroots(coeffs_b
) if cand
> 0]
186 a
= (1.5*curvB
*b
*b
- E
) / T
191 # if the Numeric modules are not available:
192 # solve the coupled system by Newton iteration
193 # 0 = Ga(a,b) = 0.5 a |a| curvA + b * T - D
194 # 0 = Gb(a,b) = 0.5 b |b| curvB + a * T + E
195 # this system is equivalent to the geometric contraints:
196 # the curvature and the normal tangent vectors
197 # at parameters 0 and 1 are to be continuous
198 # the system is solved by 2-dim Newton-Iteration
199 # (a,b)^{i+1} = (a,b)^i - (DG)^{-1} (Ga(a^i,b^i), Gb(a^i,b^i))
202 eps
= 1.0 # stepwith for the Newton iteration
205 while max(abs(da
),abs(db
)) > epsilon
and counter
< 1000:
207 Ga
= eps
* (1.5*curvA
*a
*a
- T
*b
- D
)
208 Gb
= eps
* (1.5*curvB
*b
*b
- T
*a
- E
)
210 detDG
= 9.0*a
*b
*curvA
*curvB
- T
*T
211 invDG
= ((3.0*curvB
*b
/detDG
, T
/detDG
), (T
/detDG
, 3.0*curvA
*a
/detDG
))
213 da
= invDG
[0][0] * Ga
+ invDG
[0][1] * Gb
214 db
= invDG
[1][0] * Ga
+ invDG
[1][1] * Gb
224 if a
is not None: a
/= normA
225 if b
is not None: b
/= normB
230 def intersection (A
, D
, tangA
, tangD
): # <<<
232 """returns the intersection parameters of two evens
238 det
= -tangA
[0] * tangD
[1] + tangA
[1] * tangD
[0]
241 except ArithmeticError:
244 DA
= D
[0] - A
[0], D
[1] - A
[1]
246 t
= (-tangD
[1]*DA
[0] + tangD
[0]*DA
[1]) / det
247 s
= (-tangA
[1]*DA
[0] + tangA
[0]*DA
[1]) / det
252 def parallel_curvespoints_pt (orig_ncurve
, shift
, expensive
=0, relerr
=0.05, epsilon
=1e-5, counter
=1): # <<<
254 A
= orig_ncurve
.x0_pt
, orig_ncurve
.y0_pt
255 B
= orig_ncurve
.x1_pt
, orig_ncurve
.y1_pt
256 C
= orig_ncurve
.x2_pt
, orig_ncurve
.y2_pt
257 D
= orig_ncurve
.x3_pt
, orig_ncurve
.y3_pt
259 # non-normalized tangential vector
260 # from begin/end point to the corresponding controlpoint
261 tangA
= (B
[0] - A
[0], B
[1] - A
[1])
262 tangD
= (D
[0] - C
[0], D
[1] - C
[1])
264 # normalized normal vectors
265 # turned to the left (+90 degrees) from the tangents
266 NormA
= (-tangA
[1] / math
.hypot(*tangA
), tangA
[0] / math
.hypot(*tangA
))
267 NormD
= (-tangD
[1] / math
.hypot(*tangD
), tangD
[0] / math
.hypot(*tangD
))
270 radiusA
, radiusD
= orig_ncurve
.curveradius_pt([0,1])
272 # get the new begin/end points
273 A
= A
[0] + shift
* NormA
[0], A
[1] + shift
* NormA
[1]
274 D
= D
[0] + shift
* NormD
[0], D
[1] + shift
* NormD
[1]
280 curvA
= 1.0 / (radiusA
- shift
)
284 curvD
= 1.0 / (radiusD
- shift
)
285 except ZeroDivisionError:
288 a
, d
= controldists_from_endpoints_pt (A
, D
, tangA
, tangD
, curvA
, curvD
, epsilon
=epsilon
)
290 if a
is None or d
is None:
292 a
= (radiusA
- shift
) / radiusA
293 d
= (radiusD
- shift
) / radiusD
295 B
= A
[0] + a
* tangA
[0], A
[1] + a
* tangA
[1]
296 C
= D
[0] - d
* tangD
[0], D
[1] - d
* tangD
[1]
298 controlpoints
= [(A
,B
,C
,D
)]
300 # check if the distance is really the wanted distance
301 if expensive
and counter
< 10:
302 # measure the distance in the "middle" of the original curve
303 trafo
= orig_ncurve
.trafo([0.5])[0]
304 M
= trafo
._apply
(0,0)
305 NormM
= trafo
._apply
(0,1)
306 NormM
= NormM
[0] - M
[0], NormM
[1] - M
[1]
308 nline
= path
.normline_pt (
309 M
[0] + (1.0 - 2*relerr
) * shift
* NormM
[0],
310 M
[1] + (1.0 - 2*relerr
) * shift
* NormM
[1],
311 M
[0] + (1.0 + 2*relerr
) * shift
* NormM
[0],
312 M
[1] + (1.0 + 2*relerr
) * shift
* NormM
[1])
314 new_ncurve
= path
.normcurve_pt(A
[0],A
[1], B
[0],B
[1], C
[0],C
[1], D
[0],D
[1])
316 #cutparams = nline.intersect(orig_ncurve, epsilon)
317 cutparams
= new_ncurve
.intersect(nline
, epsilon
)
319 cutpoints
= nline
.at_pt(cutparams
[0])
323 for cutpoint
in cutpoints
:
324 if cutpoint
is not None:
325 dist
= math
.hypot(M
[0] - cutpoint
[0], M
[1] - cutpoint
[1])
326 if abs(dist
- abs(shift
)) < relerr
* abs(shift
):
330 first
, second
= orig_ncurve
.segments([0,0.5,1])
332 parallel_curvespoints_pt (first
, shift
, expensive
, relerr
, epsilon
, counter
+1) + \
333 parallel_curvespoints_pt (second
, shift
, expensive
, relerr
, epsilon
, counter
+1)
338 # too big curvatures: intersect curves
339 # there is something wrong with the recursion
344 class deformer(attr
.attr
):
346 def deform (self
, basepath
):
349 class cycloid(deformer
): # <<<
350 """Wraps a cycloid around a path.
352 The outcome looks like a metal spring with the originalpath as the axis.
353 radius: radius of the cycloid
354 loops: number of loops from beginning to end of the original path
355 skipfirst/skiplast: undeformed end lines of the original path
359 def __init__(self
, radius
=0.5*unit
.t_cm
, halfloops
=10,
360 skipfirst
=1*unit
.t_cm
, skiplast
=1*unit
.t_cm
, curvesperhloop
=3, sign
=1, turnangle
=45):
361 self
.skipfirst
= skipfirst
362 self
.skiplast
= skiplast
364 self
.halfloops
= halfloops
365 self
.curvesperhloop
= curvesperhloop
367 self
.turnangle
= turnangle
369 def __call__(self
, radius
=None, halfloops
=None,
370 skipfirst
=None, skiplast
=None, curvesperhloop
=None, sign
=None, turnangle
=None):
373 if halfloops
is None:
374 halfloops
= self
.halfloops
375 if skipfirst
is None:
376 skipfirst
= self
.skipfirst
378 skiplast
= self
.skiplast
379 if curvesperhloop
is None:
380 curvesperhloop
= self
.curvesperhloop
383 if turnangle
is None:
384 turnangle
= self
.turnangle
386 return cycloid(radius
=radius
, halfloops
=halfloops
, skipfirst
=skipfirst
, skiplast
=skiplast
,
387 curvesperhloop
=curvesperhloop
, sign
=sign
, turnangle
=turnangle
)
389 def deform(self
, basepath
):
390 resultnormsubpaths
= [self
.deformsubpath(nsp
) for nsp
in basepath
.normpath().normsubpaths
]
391 return path
.normpath(resultnormsubpaths
)
393 def deformsubpath(self
, normsubpath
):
395 skipfirst
= abs(unit
.topt(self
.skipfirst
))
396 skiplast
= abs(unit
.topt(self
.skiplast
))
397 radius
= abs(unit
.topt(self
.radius
))
398 turnangle
= self
.turnangle
* math
.pi
/ 180.0
400 cosTurn
= math
.cos(turnangle
)
401 sinTurn
= math
.sin(turnangle
)
403 # make list of the lengths and parameters at points on normsubpath where we will add cycloid-points
404 totlength
= normsubpath
.arclen_pt()
405 if totlength
<= skipfirst
+ skiplast
+ 2*radius
*sinTurn
:
406 warnings
.warn("normsubpath is too short for deformation with cycloid -- skipping...")
409 # parametrisation is in rotation-angle around the basepath
410 # differences in length, angle ... between two basepoints
411 # and between basepoints and controlpoints
412 Dphi
= math
.pi
/ self
.curvesperhloop
413 phis
= [i
* Dphi
for i
in range(self
.halfloops
* self
.curvesperhloop
+ 1)]
414 DzDphi
= (totlength
- skipfirst
- skiplast
- 2*radius
*sinTurn
) * 1.0 / (self
.halfloops
* math
.pi
* cosTurn
)
415 # Dz = (totlength - skipfirst - skiplast - 2*radius*sinTurn) * 1.0 / (self.halfloops * self.curvesperhloop * cosTurn)
416 # zs = [i * Dz for i in range(self.halfloops * self.curvesperhloop + 1)]
417 # from path._arctobcurve:
418 # optimal relative distance along tangent for second and third control point
419 L
= 4 * radius
* (1 - math
.cos(Dphi
/2)) / (3 * math
.sin(Dphi
/2))
421 # Now the transformation of z into the turned coordinate system
422 Zs
= [ skipfirst
+ radius
*sinTurn
# here the coordinate z starts
423 - sinTurn
*radius
*math
.cos(phi
) + cosTurn
*DzDphi
*phi
# the transformed z-coordinate
425 params
= normsubpath
._arclentoparam
_pt
(Zs
)[0]
427 # get the positions of the splitpoints in the cycloid
429 for phi
, param
in zip(phis
, params
):
430 # the cycloid is a circle that is stretched along the normsubpath
431 # here are the points of that circle
432 basetrafo
= normsubpath
.trafo([param
])[0]
434 # The point on the cycloid, in the basepath's local coordinate system
435 baseZ
, baseY
= 0, radius
*math
.sin(phi
)
437 # The tangent there, also in local coords
438 tangentX
= -cosTurn
*radius
*math
.sin(phi
) + sinTurn
*DzDphi
439 tangentY
= radius
*math
.cos(phi
)
440 tangentZ
= sinTurn
*radius
*math
.sin(phi
) + DzDphi
*cosTurn
441 norm
= math
.sqrt(tangentX
*tangentX
+ tangentY
*tangentY
+ tangentZ
*tangentZ
)
442 tangentY
, tangentZ
= tangentY
/norm
, tangentZ
/norm
444 # Respect the curvature of the basepath for the cycloid's curvature
445 # XXX this is only a heuristic, not a "true" expression for
446 # the curvature in curved coordinate systems
447 pathradius
= normsubpath
.curveradius_pt([param
])[0]
448 if pathradius
is not None:
449 factor
= (pathradius
- baseY
) / pathradius
455 # The control points prior and after the point on the cycloid
456 preeZ
, preeY
= baseZ
- l
* tangentZ
, baseY
- l
* tangentY
457 postZ
, postY
= baseZ
+ l
* tangentZ
, baseY
+ l
* tangentY
459 # Now put everything at the proper place
460 points
.append(basetrafo
._apply
(preeZ
, self
.sign
* preeY
) +
461 basetrafo
._apply
(baseZ
, self
.sign
* baseY
) +
462 basetrafo
._apply
(postZ
, self
.sign
* postY
))
465 warnings
.warn("normsubpath is too short for deformation with cycloid -- skipping...")
468 # Build the path from the pointlist
469 # containing (control x 2, base x 2, control x 2)
470 if skipfirst
> normsubpath
.epsilon
:
471 normsubpathitems
= normsubpath
.segments([0, params
[0]])[0]
472 normsubpathitems
.append(path
.normcurve_pt(*(points
[0][2:6] + points
[1][0:4])))
474 normsubpathitems
= [path
.normcurve_pt(*(points
[0][2:6] + points
[1][0:4]))]
475 for i
in range(1, len(points
)-1):
476 normsubpathitems
.append(path
.normcurve_pt(*(points
[i
][2:6] + points
[i
+1][0:4])))
477 if skiplast
> normsubpath
.epsilon
:
478 for nsp
in normsubpath
.segments([params
[-1], len(normsubpath
)]):
479 normsubpathitems
.extend(nsp
.normsubpathitems
)
482 return path
.normsubpath(normsubpathitems
, epsilon
=normsubpath
.epsilon
)
485 cycloid
.clear
= attr
.clearclass(cycloid
)
487 class smoothed(deformer
): # <<<
489 """Bends corners in a path.
491 This decorator replaces corners in a path with bezier curves. There are two cases:
492 - If the corner lies between two lines, _two_ bezier curves will be used
493 that are highly optimized to look good (their curvature is to be zero at the ends
494 and has to have zero derivative in the middle).
495 Additionally, it can controlled by the softness-parameter.
496 - If the corner lies between curves then _one_ bezier is used that is (except in some
497 special cases) uniquely determined by the tangents and curvatures at its end-points.
498 In some cases it is necessary to use only the absolute value of the curvature to avoid a
499 cusp-shaped connection of the new bezier to the old path. In this case the use of
500 "obeycurv=0" allows the sign of the curvature to switch.
501 - The radius argument gives the arclength-distance of the corner to the points where the
502 old path is cut and the beziers are inserted.
503 - Path elements that are too short (shorter than the radius) are skipped
506 def __init__(self
, radius
, softness
=1, obeycurv
=0):
508 self
.softness
= softness
509 self
.obeycurv
= obeycurv
511 def __call__(self
, radius
=None, softness
=None, obeycurv
=None):
515 softness
= self
.softness
517 obeycurv
= self
.obeycurv
518 return smoothed(radius
=radius
, softness
=softness
, obeycurv
=obeycurv
)
520 def deform(self
, basepath
):
521 basepath
= basepath
.normpath()
522 smoothpath
= path
.path()
524 for sp
in basepath
.normsubpaths
:
525 smoothpath
+= self
.deformsubpath(sp
)
529 def deformsubpath(self
, normsubpath
):
531 radius
= unit
.topt(self
.radius
)
532 epsilon
= normsubpath
.epsilon
534 # 1. Build up a list of all relevant normsubpathitems
535 # and the lengths where they will be cut (length with respect to the normsubpath)
536 rel_npitems
, arclengths
= [], []
537 for npitem
in normsubpath
.normsubpathitems
:
539 arclength
= npitem
.arclen_pt(epsilon
)
541 # items that should be totally skipped:
542 # (we throw away the possible ending "closepath" piece of zero length)
543 if (arclength
> radius
):
544 rel_npitems
.append(npitem
)
545 arclengths
.append(arclength
)
547 warnings
.warn("smoothed is skipping a too short normsubpathitem")
549 # 2. Find the parameters, points,
550 # and calculate tangents and curvatures
551 params
, points
, tangents
, curvatures
= [], [], [], []
552 for npitem
, arclength
in zip(rel_npitems
, arclengths
):
554 # find the parameter(s): either one or two
555 # for short items we squeeze the radius
556 if arclength
> 2 * radius
:
557 cut_alengths
= [radius
, arclength
- radius
]
559 cut_alengths
= [0.5 * radius
]
562 pars
= npitem
._arclentoparam
_pt
(cut_alengths
, epsilon
)[0]
564 # the endpoints of an open subpath must be handled specially
565 if not normsubpath
.closed
:
566 if npitem
is rel_npitems
[0]:
568 if npitem
is rel_npitems
[-1]:
571 # find points, tangents and curvatures
574 thetrafo
= npitem
.trafo([par
])[0]
575 p
= thetrafo
._apply
(0,0)
576 t
= thetrafo
._apply
(1,0)
578 ts
.append((t
[0]-p
[0], t
[1]-p
[1]))
579 r
= npitem
.curveradius_pt([par
])[0]
588 curvatures
.append(cs
)
591 # create an empty path to collect pathitems
592 # this will be returned as normpath, later
593 smoothpath
= path
.path()
594 do_moveto
= 1 # we do not know yet where to moveto
596 # 3. Do the splitting for the first to the last element,
597 # a closed path must be closed later
599 for i
in range(len(rel_npitems
)):
602 next
= (i
+1) % len(rel_npitems
)
603 thisnpitem
= rel_npitems
[this
]
604 nextnpitem
= rel_npitems
[next
]
606 # split thisnpitem apart and take the middle piece
607 # We start the new path with the middle piece of the first path-elem
608 if len(points
[this
]) == 2:
610 middlepiece
= thisnpitem
.segments(params
[this
])[0]
613 smoothpath
.append(path
.moveto_pt(*middlepiece
.atbegin_pt()))
616 if isinstance(middlepiece
, path
.normline_pt
):
617 smoothpath
.append(path
.lineto_pt(*middlepiece
.atend_pt()))
618 elif isinstance(middlepiece
, path
.normcurve_pt
):
619 smoothpath
.append(path
.curveto_pt(
620 middlepiece
.x1_pt
, middlepiece
.y1_pt
,
621 middlepiece
.x2_pt
, middlepiece
.y2_pt
,
622 middlepiece
.x3_pt
, middlepiece
.y3_pt
))
624 if (not normsubpath
.closed
) and (thisnpitem
is rel_npitems
[-1]):
627 # add the curve(s) replacing the corner
628 if isinstance(thisnpitem
, path
.normline_pt
) and \
629 isinstance(nextnpitem
, path
.normline_pt
) and \
630 epsilon
> math
.hypot(thisnpitem
.atend_pt()[0] - nextnpitem
.atbegin_pt()[0],
631 thisnpitem
.atend_pt()[0] - nextnpitem
.atbegin_pt()[0]):
633 d1
,g1
,f1
,e
,f2
,g2
,d2
= curvescontrols_from_endlines_pt(
634 thisnpitem
.atend_pt(), tangents
[this
][-1], tangents
[next
][0],
635 math
.hypot(points
[this
][-1][0] - thisnpitem
.atend_pt()[0], points
[this
][-1][1] - thisnpitem
.atend_pt()[1]),
636 math
.hypot(points
[next
][0][0] - nextnpitem
.atbegin_pt()[0], points
[next
][0][1] - nextnpitem
.atbegin_pt()[1]),
637 softness
=self
.softness
)
640 smoothpath
.append(path
.moveto_pt(*d1
))
643 smoothpath
.append(path
.curveto_pt(*(g1
+ f1
+ e
)))
644 smoothpath
.append(path
.curveto_pt(*(f2
+ g2
+ d2
)))
648 A
, D
= points
[this
][-1], points
[next
][0]
649 tangA
, tangD
= tangents
[this
][-1], tangents
[next
][0]
650 curvA
, curvD
= curvatures
[this
][-1], curvatures
[next
][0]
651 if not self
.obeycurv
:
652 # do not obey the sign of the curvature but
653 # make the sign such that the curve smoothly passes to the next point
654 # this results in a discontinuous curvature
655 # (but the absolute value is still continuous)
656 sA
= sign1(tangA
[0] * (D
[1]-A
[1]) - tangA
[1] * (D
[0]-A
[0]))
657 sD
= sign1(tangD
[0] * (D
[1]-A
[1]) - tangD
[1] * (D
[0]-A
[0]))
658 curvA
= sA
* abs(curvA
)
659 curvD
= sD
* abs(curvD
)
661 # get the length of the control "arms"
662 a
, d
= controldists_from_endpoints_pt (
663 A
, D
, tangA
, tangD
, curvA
, curvD
,
666 # avoid overshooting at the corners:
667 # this changes not only the sign of the curvature
668 # but also the magnitude
669 if not self
.obeycurv
:
670 t
, s
= intersection(A
, D
, tangA
, tangD
)
671 if t
is None or t
< 0:
676 if s
is None or s
> 0:
681 # if there is no useful result:
682 # take arbitrary smoothing curve that does not obey
683 # the curvature constraints
684 if a
is None or d
is None:
685 dist
= math
.hypot(A
[0] - D
[0], A
[1] - D
[1])
686 a
= dist
/ (3.0 * math
.hypot(*tangA
))
687 d
= dist
/ (3.0 * math
.hypot(*tangD
))
688 #warnings.warn("The connecting bezier cannot be found. Using a simple fallback.")
690 # calculate the two missing control points
691 B
= A
[0] + a
* tangA
[0], A
[1] + a
* tangA
[1]
692 C
= D
[0] - d
* tangD
[0], D
[1] - d
* tangD
[1]
695 smoothpath
.append(path
.moveto_pt(*A
))
698 smoothpath
.append(path
.curveto_pt(*(B
+ C
+ D
)))
701 # 4. Second part of extra handling of closed paths
702 if normsubpath
.closed
:
704 # XXX the following does not work since dp is not defined
705 # probably, we just want a moveto_pt(*normsubpath.atbegin())
706 smoothpath
.append(path
.moveto_pt(*dp
.strokepath
.atbegin()))
707 warnings
.warn("The whole subpath has been smoothed away -- sorry")
708 smoothpath
.append(path
.closepath())
713 smoothed
.clear
= attr
.clearclass(smoothed
)
715 class parallel(deformer
): # <<<
717 """creates a parallel path with constant distance to the original path
719 A positive 'distance' results in a curve left of the original one -- and a
720 negative 'distance' in a curve at the right. Left/Right are understood in
721 terms of the parameterization of the original curve.
722 At corners, either a circular arc is drawn around the corner, or, if the
723 curve is on the other side, the parallel curve also exhibits a corner.
725 For each path element a parallel curve/line is constructed. For curves, the
726 accuracy can be adjusted with the parameter 'relerr', thus, relerr*distance
727 is the maximum allowable error somewhere in the middle of the curve (at
728 parameter value 0.5).
729 'relerr' only applies for the 'expensive' mode where the parallel curve for
730 a single curve items may be composed of several (many) curve items.
734 # - check for greatest curvature and introduce extra corners
735 # if a normcurve is too heavily curved
736 # - do relerr-checks at better points than just at parameter 0.5
738 def __init__(self
, distance
, relerr
=0.05, expensive
=1):
739 self
.distance
= distance
741 self
.expensive
= expensive
743 def __call__(self
, distance
=None, relerr
=None, expensive
=None):
744 # returns a copy of the deformer with different parameters
749 if expensive
is None:
752 return parallel(distance
=d
, relerr
=r
, expensive
=e
)
754 def deform(self
, basepath
):
755 resultnormsubpaths
= [self
.deformsubpath(nsp
) for nsp
in basepath
.normpath().normsubpaths
]
756 return path
.normpath(resultnormsubpaths
)
758 def deformsubpath(self
, orig_nspath
):
760 distance
= unit
.topt(self
.distance
)
762 expensive
= self
.expensive
763 epsilon
= orig_nspath
.epsilon
765 new_nspath
= path
.normsubpath(epsilon
=epsilon
)
767 # 1. Store endpoints, tangents and curvatures for each element
768 points
, tangents
, curvatures
= [], [], []
769 for npitem
in orig_nspath
:
772 trafos
= npitem
.trafo([0,1])
777 ts
.append((t
[0]-p
[0], t
[1]-p
[1]))
779 rs
= npitem
.curveradius_pt([0,1])
789 curvatures
.append(cs
)
791 closeparallel
= (tangents
[-1][1][0]*tangents
[0][0][1] - tangents
[-1][1][1]*tangents
[0][0][0])
793 # 2. append the parallel path for each element:
794 for cur
in range(len(orig_nspath
)):
798 # OldEnd = points[old][0]
799 OldEndTang
= tangents
[old
][0]
802 # OldEnd = points[old][1]
803 OldEndTang
= tangents
[old
][1]
805 CurBeg
, CurEnd
= points
[cur
]
806 CurBegTang
, CurEndTang
= tangents
[cur
]
807 CurBegCurv
, CurEndCurv
= curvatures
[cur
]
809 npitem
= orig_nspath
[cur
]
811 # get the control points for the shifted pathelement
812 if isinstance(npitem
, path
.normline_pt
):
813 # get the points explicitly from the normal vector
814 A
= CurBeg
[0] - distance
* CurBegTang
[1], CurBeg
[1] + distance
* CurBegTang
[0]
815 D
= CurEnd
[0] - distance
* CurEndTang
[1], CurEnd
[1] + distance
* CurEndTang
[0]
816 new_npitems
= [path
.normline_pt(A
[0], A
[1], D
[0], D
[1])]
817 elif isinstance(npitem
, path
.normcurve_pt
):
818 # call a function to return a list of controlpoints
819 cpoints_list
= parallel_curvespoints_pt(npitem
, distance
, expensive
, relerr
, epsilon
)
821 for cpoints
in cpoints_list
:
823 new_npitems
.append(path
.normcurve_pt(A
[0],A
[1], B
[0],B
[1], C
[0],C
[1], D
[0],D
[1]))
824 # we will need the starting point of the new normpath items
825 A
= cpoints_list
[0][0]
828 # append the next piece of the path:
829 # it might contain of an extra arc or must be intersected before appending
830 parallel
= (OldEndTang
[0]*CurBegTang
[1] - OldEndTang
[1]*CurBegTang
[0])
831 if parallel
*distance
< -epsilon
:
833 # append an arc around the corner
834 # from the preceding piece to the current piece
835 # we can never get here for the first npitem! (because cur==old)
836 endpoint
= new_nspath
.atend_pt()
838 angle1
= math
.atan2(endpoint
[1] - center
[1], endpoint
[0] - center
[0]) * 180.0 / math
.pi
839 angle2
= math
.atan2(A
[1] - center
[1], A
[0] - center
[0]) * 180.0 / math
.pi
841 arc_npath
= path
.path(path
.arc_pt(
842 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
844 arc_npath
= path
.path(path
.arcn_pt(
845 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
847 for new_npitem
in arc_npath
[0]:
848 new_nspath
.append(new_npitem
)
851 elif parallel
*distance
> epsilon
:
852 # intersect the extra piece of the path with the rest of the new path
853 # and throw away the void parts
855 # build a subpath for intersection
856 extra_nspath
= path
.normsubpath(normsubpathitems
=new_npitems
, epsilon
=epsilon
)
858 intsparams
= extra_nspath
.intersect(new_nspath
)
861 # take the first intersection point:
862 extra_param
, new_param
= intsparams
[0][0], intsparams
[1][0]
863 new_nspath
= new_nspath
.segments([0, new_param
])[0]
864 extra_nspath
= extra_nspath
.segments([extra_param
, len(extra_nspath
)])[0]
865 new_npitems
= extra_nspath
.normsubpathitems
866 # in case the intersection was not sufficiently exact:
867 # CAREFUL! because we newly created all the new_npitems and
868 # the items in extra_nspath, we may in-place change the starting point
869 new_npitems
[0] = new_npitems
[0].modifiedbegin_pt(*new_nspath
.atend_pt())
871 raise # how did we get here?
874 # at the (possible) closing corner we may have to intersect another time
875 # or add another corner:
876 # the intersection must be done before appending the parallel piece
877 if orig_nspath
.closed
and cur
== len(orig_nspath
) - 1:
878 if closeparallel
* distance
> epsilon
:
879 intsparams
= extra_nspath
.intersect(new_nspath
)
882 # take the last intersection point:
883 extra_param
, new_param
= intsparams
[0][-1], intsparams
[1][-1]
884 new_nspath
= new_nspath
.segments([new_param
, len(new_nspath
)])[0]
885 extra_nspath
= extra_nspath
.segments([0, extra_param
])[0]
886 new_npitems
= extra_nspath
.normsubpathitems
887 # in case the intersection was not sufficiently exact:
888 # CAREFUL! because we newly created all the new_npitems and
889 # the items in extra_nspath, we may in-place change the end point
890 new_npitems
[-1] = new_npitems
[-1].modifiedend_pt(*new_nspath
.atbegin_pt())
892 raise # how did we get here?
897 # append the parallel piece
898 for new_npitem
in new_npitems
:
899 new_nspath
.append(new_npitem
)
902 # the curve around the closing corner must be added at last:
903 if orig_nspath
.closed
:
904 if closeparallel
* distance
< -epsilon
:
905 endpoint
= new_nspath
.atend_pt()
906 center
= orig_nspath
.atend_pt()
907 A
= new_nspath
.atbegin_pt()
908 angle1
= math
.atan2(endpoint
[1] - center
[1], endpoint
[0] - center
[0]) * 180.0 / math
.pi
909 angle2
= math
.atan2(A
[1] - center
[1], A
[0] - center
[0]) * 180.0 / math
.pi
911 arc_npath
= path
.path(path
.arc_pt(
912 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
914 arc_npath
= path
.path(path
.arcn_pt(
915 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
917 for new_npitem
in arc_npath
[0]:
918 new_nspath
.append(new_npitem
)
920 # 3. extra handling of closed paths
921 if orig_nspath
.closed
:
927 parallel
.clear
= attr
.clearclass(parallel
)
929 # vim:foldmethod=marker:foldmarker=<<<,>>>