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
29 return (x
>= 0) and 1 or -1
31 def curvescontrols_from_endlines_pt (B
, tangent1
, tangent2
, r1
, r2
, softness
): # <<<
32 # calculates the parameters for two bezier curves connecting two lines (curvature=0)
33 # starting at B - r1*tangent1
34 # ending at B + r2*tangent2
37 # and two tangent vectors heading to and from B
38 # and two radii r1 and r2:
39 # All arguments must be in Points
40 # Returns the seven control points of the two bezier curves:
42 # - control points g1 and f1
44 # - control points f2 and g2
47 # make direction vectors d1: from B to A
49 d1
= -tangent1
[0] / math
.hypot(*tangent1
), -tangent1
[1] / math
.hypot(*tangent1
)
50 d2
= tangent2
[0] / math
.hypot(*tangent2
), tangent2
[1] / math
.hypot(*tangent2
)
52 # 0.3192 has turned out to be the maximum softness available
53 # for straight lines ;-)
55 g
= (15.0 * f
+ math
.sqrt(-15.0*f
*f
+ 24.0*f
))/12.0
57 # make the control points of the two bezier curves
58 f1
= B
[0] + f
* r1
* d1
[0], B
[1] + f
* r1
* d1
[1]
59 f2
= B
[0] + f
* r2
* d2
[0], B
[1] + f
* r2
* d2
[1]
60 g1
= B
[0] + g
* r1
* d1
[0], B
[1] + g
* r1
* d1
[1]
61 g2
= B
[0] + g
* r2
* d2
[0], B
[1] + g
* r2
* d2
[1]
62 d1
= B
[0] + r1
* d1
[0], B
[1] + r1
* d1
[1]
63 d2
= B
[0] + r2
* d2
[0], B
[1] + r2
* d2
[1]
64 e
= 0.5 * (f1
[0] + f2
[0]), 0.5 * (f1
[1] + f2
[1])
66 return (d1
, g1
, f1
, e
, f2
, g2
, d2
)
69 def controldists_from_endpoints_pt (A
, B
, tangA
, tangB
, curvA
, curvB
): # <<<
71 """distances for a curve given by tangents and curvatures at the endpoints
73 This helper routine returns the two distances between the endpoints and the
74 corresponding control points of a (cubic) bezier curve that has
75 prescribed tangents tangentA, tangentB and curvatures curvA, curvB at the
80 T
= tangB
[0] * tangA
[1] - tangB
[1] * tangA
[0]
81 D
= tangA
[0] * (B
[1]-A
[1]) - tangA
[1] * (B
[0]-A
[0])
82 E
= - tangB
[0] * (B
[1]-A
[1]) + tangB
[1] * (B
[0]-A
[0])
83 # the variables: \dot X(0) = 3 * a * tangA
84 # \dot X(1) = 3 * b * tangB
88 # try some special cases where the equations decouple
91 except ZeroDivisionError:
93 a
= math
.sqrt(2.0 * D
/ (3.0 * curvA
))
94 b
= math
.sqrt(2.0 * E
/ (3.0 * curvB
))
95 except ZeroDivisionError:
102 except ZeroDivisionError:
104 a
= (1.5*curvB
*b
*b
- E
) / T
108 except ZeroDivisionError:
110 b
= (1.5*curvA
*a
*a
- D
) / T
115 # else find a solution for the full problem
117 # we first try to find all the zeros of the polynomials for a or b (4th order)
118 # this needs Numeric and LinearAlgebra
120 # 0 = Ga(a,b) = 0.5 a |a| curvA + b * T - D
121 # 0 = Gb(a,b) = 0.5 b |b| curvB + a * T + E
123 coeffs_a
= (3.375*curvA
*curvA
*curvB
, 0, -4.5*curvA
*curvB
*D
, -T
**3, 1.5*curvB
*D
*D
- T
*T
*E
)
124 coeffs_b
= (3.375*curvA
*curvB
*curvB
, 0, -4.5*curvA
*curvB
*E
, -T
**3, 1.5*curvA
*E
*E
- T
*T
*D
)
126 # First try the equation for a
127 cands_a
= [cand
for cand
in helper
.realpolyroots(coeffs_a
) if cand
> 0]
131 b
= (1.5*curvA
*a
*a
- D
) / T
133 # then, try the equation for b
134 cands_b
= [cand
for cand
in helper
.realpolyroots(coeffs_b
) if cand
> 0]
137 a
= (1.5*curvB
*b
*b
- E
) / T
147 def intersection (A
, D
, tangA
, tangD
): # <<<
149 """returns the intersection parameters of two evens
155 det
= -tangA
[0] * tangD
[1] + tangA
[1] * tangD
[0]
158 except ArithmeticError:
161 DA
= D
[0] - A
[0], D
[1] - A
[1]
163 t
= (-tangD
[1]*DA
[0] + tangD
[0]*DA
[1]) / det
164 s
= (-tangA
[1]*DA
[0] + tangA
[0]*DA
[1]) / det
169 def parallel_curvespoints_pt (orig_ncurve
, shift
, expensive
=0, relerr
=0.05, epsilon
=1e-5, counter
=1): # <<<
171 A
= orig_ncurve
.x0_pt
, orig_ncurve
.y0_pt
172 B
= orig_ncurve
.x1_pt
, orig_ncurve
.y1_pt
173 C
= orig_ncurve
.x2_pt
, orig_ncurve
.y2_pt
174 D
= orig_ncurve
.x3_pt
, orig_ncurve
.y3_pt
176 # non-normalized tangential vector
177 # from begin/end point to the corresponding controlpoint
178 tangA
= (B
[0] - A
[0], B
[1] - A
[1])
179 tangD
= (D
[0] - C
[0], D
[1] - C
[1])
181 # normalized normal vectors
182 # turned to the left (+90 degrees) from the tangents
183 NormA
= (-tangA
[1] / math
.hypot(*tangA
), tangA
[0] / math
.hypot(*tangA
))
184 NormD
= (-tangD
[1] / math
.hypot(*tangD
), tangD
[0] / math
.hypot(*tangD
))
187 radiusA
, radiusD
= orig_ncurve
.curveradius_pt([0,1])
189 # get the new begin/end points
190 A
= A
[0] + shift
* NormA
[0], A
[1] + shift
* NormA
[1]
191 D
= D
[0] + shift
* NormD
[0], D
[1] + shift
* NormD
[1]
197 curvA
= 1.0 / (radiusA
- shift
)
201 curvD
= 1.0 / (radiusD
- shift
)
202 except ZeroDivisionError:
205 a
, d
= controldists_from_endpoints_pt (A
, D
, tangA
, tangD
, curvA
, curvD
)
207 if a
is None or d
is None:
209 a
= (radiusA
- shift
) / radiusA
210 d
= (radiusD
- shift
) / radiusD
212 B
= A
[0] + a
* tangA
[0], A
[1] + a
* tangA
[1]
213 C
= D
[0] - d
* tangD
[0], D
[1] - d
* tangD
[1]
215 controlpoints
= [(A
,B
,C
,D
)]
217 # check if the distance is really the wanted distance
218 if expensive
and counter
< 10:
219 # measure the distance in the "middle" of the original curve
220 trafo
= orig_ncurve
.trafo([0.5])[0]
221 M
= trafo
.apply_pt(0,0)
222 NormM
= trafo
.apply_pt(0,1)
223 NormM
= NormM
[0] - M
[0], NormM
[1] - M
[1]
225 nline
= path
.normline_pt (
226 M
[0] + (1.0 - 2*relerr
) * shift
* NormM
[0],
227 M
[1] + (1.0 - 2*relerr
) * shift
* NormM
[1],
228 M
[0] + (1.0 + 2*relerr
) * shift
* NormM
[0],
229 M
[1] + (1.0 + 2*relerr
) * shift
* NormM
[1])
231 new_ncurve
= path
.normcurve_pt(A
[0],A
[1], B
[0],B
[1], C
[0],C
[1], D
[0],D
[1])
233 #cutparams = nline.intersect(orig_ncurve, epsilon)
234 cutparams
= new_ncurve
.intersect(nline
, epsilon
)
236 cutpoints
= nline
.at_pt(cutparams
[0])
240 for cutpoint
in cutpoints
:
241 if cutpoint
is not None:
242 dist
= math
.hypot(M
[0] - cutpoint
[0], M
[1] - cutpoint
[1])
243 if abs(dist
- abs(shift
)) < relerr
* abs(shift
):
247 first
, second
= orig_ncurve
.segments([0,0.5,1])
249 parallel_curvespoints_pt (first
, shift
, expensive
, relerr
, epsilon
, counter
+1) + \
250 parallel_curvespoints_pt (second
, shift
, expensive
, relerr
, epsilon
, counter
+1)
255 # too big curvatures: intersect curves
256 # there is something wrong with the recursion
261 class deformer(attr
.attr
):
263 def deform (self
, basepath
):
266 class cycloid(deformer
): # <<<
267 """Wraps a cycloid around a path.
269 The outcome looks like a metal spring with the originalpath as the axis.
270 radius: radius of the cycloid
271 loops: number of loops from beginning to end of the original path
272 skipfirst/skiplast: undeformed end lines of the original path
276 def __init__(self
, radius
=0.5*unit
.t_cm
, halfloops
=10,
277 skipfirst
=1*unit
.t_cm
, skiplast
=1*unit
.t_cm
, curvesperhloop
=3, sign
=1, turnangle
=45):
278 self
.skipfirst
= skipfirst
279 self
.skiplast
= skiplast
281 self
.halfloops
= halfloops
282 self
.curvesperhloop
= curvesperhloop
284 self
.turnangle
= turnangle
286 def __call__(self
, radius
=None, halfloops
=None,
287 skipfirst
=None, skiplast
=None, curvesperhloop
=None, sign
=None, turnangle
=None):
290 if halfloops
is None:
291 halfloops
= self
.halfloops
292 if skipfirst
is None:
293 skipfirst
= self
.skipfirst
295 skiplast
= self
.skiplast
296 if curvesperhloop
is None:
297 curvesperhloop
= self
.curvesperhloop
300 if turnangle
is None:
301 turnangle
= self
.turnangle
303 return cycloid(radius
=radius
, halfloops
=halfloops
, skipfirst
=skipfirst
, skiplast
=skiplast
,
304 curvesperhloop
=curvesperhloop
, sign
=sign
, turnangle
=turnangle
)
306 def deform(self
, basepath
):
307 resultnormsubpaths
= [self
.deformsubpath(nsp
) for nsp
in basepath
.normpath().normsubpaths
]
308 return path
.normpath(resultnormsubpaths
)
310 def deformsubpath(self
, normsubpath
):
312 skipfirst
= abs(unit
.topt(self
.skipfirst
))
313 skiplast
= abs(unit
.topt(self
.skiplast
))
314 radius
= abs(unit
.topt(self
.radius
))
315 turnangle
= self
.turnangle
* math
.pi
/ 180.0
317 cosTurn
= math
.cos(turnangle
)
318 sinTurn
= math
.sin(turnangle
)
320 # make list of the lengths and parameters at points on normsubpath where we will add cycloid-points
321 totlength
= normsubpath
.arclen_pt()
322 if totlength
<= skipfirst
+ skiplast
+ 2*radius
*sinTurn
:
323 warnings
.warn("normsubpath is too short for deformation with cycloid -- skipping...")
326 # parametrisation is in rotation-angle around the basepath
327 # differences in length, angle ... between two basepoints
328 # and between basepoints and controlpoints
329 Dphi
= math
.pi
/ self
.curvesperhloop
330 phis
= [i
* Dphi
for i
in range(self
.halfloops
* self
.curvesperhloop
+ 1)]
331 DzDphi
= (totlength
- skipfirst
- skiplast
- 2*radius
*sinTurn
) * 1.0 / (self
.halfloops
* math
.pi
* cosTurn
)
332 # Dz = (totlength - skipfirst - skiplast - 2*radius*sinTurn) * 1.0 / (self.halfloops * self.curvesperhloop * cosTurn)
333 # zs = [i * Dz for i in range(self.halfloops * self.curvesperhloop + 1)]
334 # from path._arctobcurve:
335 # optimal relative distance along tangent for second and third control point
336 L
= 4 * radius
* (1 - math
.cos(Dphi
/2)) / (3 * math
.sin(Dphi
/2))
338 # Now the transformation of z into the turned coordinate system
339 Zs
= [ skipfirst
+ radius
*sinTurn
# here the coordinate z starts
340 - sinTurn
*radius
*math
.cos(phi
) + cosTurn
*DzDphi
*phi
# the transformed z-coordinate
342 params
= normsubpath
._arclentoparam
_pt
(Zs
)[0]
344 # get the positions of the splitpoints in the cycloid
346 for phi
, param
in zip(phis
, params
):
347 # the cycloid is a circle that is stretched along the normsubpath
348 # here are the points of that circle
349 basetrafo
= normsubpath
.trafo([param
])[0]
351 # The point on the cycloid, in the basepath's local coordinate system
352 baseZ
, baseY
= 0, radius
*math
.sin(phi
)
354 # The tangent there, also in local coords
355 tangentX
= -cosTurn
*radius
*math
.sin(phi
) + sinTurn
*DzDphi
356 tangentY
= radius
*math
.cos(phi
)
357 tangentZ
= sinTurn
*radius
*math
.sin(phi
) + DzDphi
*cosTurn
358 norm
= math
.sqrt(tangentX
*tangentX
+ tangentY
*tangentY
+ tangentZ
*tangentZ
)
359 tangentY
, tangentZ
= tangentY
/norm
, tangentZ
/norm
361 # Respect the curvature of the basepath for the cycloid's curvature
362 # XXX this is only a heuristic, not a "true" expression for
363 # the curvature in curved coordinate systems
364 pathradius
= normsubpath
.curveradius_pt([param
])[0]
365 if pathradius
is not None:
366 factor
= (pathradius
- baseY
) / pathradius
372 # The control points prior and after the point on the cycloid
373 preeZ
, preeY
= baseZ
- l
* tangentZ
, baseY
- l
* tangentY
374 postZ
, postY
= baseZ
+ l
* tangentZ
, baseY
+ l
* tangentY
376 # Now put everything at the proper place
377 points
.append(basetrafo
.apply_pt(preeZ
, self
.sign
* preeY
) +
378 basetrafo
.apply_pt(baseZ
, self
.sign
* baseY
) +
379 basetrafo
.apply_pt(postZ
, self
.sign
* postY
))
382 warnings
.warn("normsubpath is too short for deformation with cycloid -- skipping...")
385 # Build the path from the pointlist
386 # containing (control x 2, base x 2, control x 2)
387 if skipfirst
> normsubpath
.epsilon
:
388 normsubpathitems
= normsubpath
.segments([0, params
[0]])[0]
389 normsubpathitems
.append(path
.normcurve_pt(*(points
[0][2:6] + points
[1][0:4])))
391 normsubpathitems
= [path
.normcurve_pt(*(points
[0][2:6] + points
[1][0:4]))]
392 for i
in range(1, len(points
)-1):
393 normsubpathitems
.append(path
.normcurve_pt(*(points
[i
][2:6] + points
[i
+1][0:4])))
394 if skiplast
> normsubpath
.epsilon
:
395 for nsp
in normsubpath
.segments([params
[-1], len(normsubpath
)]):
396 normsubpathitems
.extend(nsp
.normsubpathitems
)
399 return path
.normsubpath(normsubpathitems
, epsilon
=normsubpath
.epsilon
)
402 cycloid
.clear
= attr
.clearclass(cycloid
)
404 class smoothed(deformer
): # <<<
406 """Bends corners in a path.
408 This decorator replaces corners in a path with bezier curves. There are two cases:
409 - If the corner lies between two lines, _two_ bezier curves will be used
410 that are highly optimized to look good (their curvature is to be zero at the ends
411 and has to have zero derivative in the middle).
412 Additionally, it can controlled by the softness-parameter.
413 - If the corner lies between curves then _one_ bezier is used that is (except in some
414 special cases) uniquely determined by the tangents and curvatures at its end-points.
415 In some cases it is necessary to use only the absolute value of the curvature to avoid a
416 cusp-shaped connection of the new bezier to the old path. In this case the use of
417 "obeycurv=0" allows the sign of the curvature to switch.
418 - The radius argument gives the arclength-distance of the corner to the points where the
419 old path is cut and the beziers are inserted.
420 - Path elements that are too short (shorter than the radius) are skipped
423 def __init__(self
, radius
, softness
=1, obeycurv
=0, relskipthres
=0.01):
425 self
.softness
= softness
426 self
.obeycurv
= obeycurv
427 self
.relskipthres
= relskipthres
429 def __call__(self
, radius
=None, softness
=None, obeycurv
=None, relskipthres
=None):
433 softness
= self
.softness
435 obeycurv
= self
.obeycurv
436 if relskipthres
is None:
437 relskipthres
= self
.relskipthres
438 return smoothed(radius
=radius
, softness
=softness
, obeycurv
=obeycurv
, relskipthres
=relskipthres
)
440 def deform(self
, basepath
):
441 return path
.normpath([self
.deformsubpath(normsubpath
)
442 for normsubpath
in basepath
.normpath().normsubpaths
])
444 def deformsubpath(self
, normsubpath
):
445 radius_pt
= unit
.topt(self
.radius
)
446 epsilon
= normsubpath
.epsilon
448 # remove too short normsubpath items (shorter than self.relskipthres*radius_pt or epsilon)
449 pertinentepsilon
= max(epsilon
, self
.relskipthres
*radius_pt
)
450 pertinentnormsubpath
= path
.normsubpath(normsubpath
.normsubpathitems
,
451 epsilon
=pertinentepsilon
)
452 pertinentnormsubpath
.flushskippedline()
453 pertinentnormsubpathitems
= pertinentnormsubpath
.normsubpathitems
455 # calculate the splitting parameters for the pertinentnormsubpathitems
458 for pertinentnormsubpathitem
in pertinentnormsubpathitems
:
459 arclen_pt
= pertinentnormsubpathitem
.arclen_pt(epsilon
)
460 arclens_pt
.append(arclen_pt
)
461 l1_pt
= min(radius_pt
, 0.5*arclen_pt
)
462 l2_pt
= max(0.5*arclen_pt
, arclen_pt
- radius_pt
)
463 params
.append(pertinentnormsubpathitem
.arclentoparam_pt([l1_pt
, l2_pt
], epsilon
))
465 # handle the first and last pertinentnormsubpathitems for a non-closed normsubpath
466 if not normsubpath
.closed
:
468 l2_pt
= max(0, arclens_pt
[0] - radius_pt
)
469 params
[0] = pertinentnormsubpathitem
.arclentoparam_pt([l1_pt
, l2_pt
], epsilon
)
470 l1_pt
= min(radius_pt
, arclens_pt
[-1])
471 l2_pt
= arclens_pt
[-1]
472 params
[-1] = pertinentnormsubpathitem
.arclentoparam_pt([l1_pt
, l2_pt
], epsilon
)
474 newnormsubpath
= path
.normsubpath(epsilon
=normsubpath
.epsilon
)
475 for i
in range(len(pertinentnormsubpathitems
)):
477 next
= (i
+1) % len(pertinentnormsubpathitems
)
478 thisparams
= params
[this
]
479 nextparams
= params
[next
]
480 thisnormsubpathitem
= pertinentnormsubpathitems
[this
]
481 nextnormsubpathitem
= pertinentnormsubpathitems
[next
]
482 thisarclen_pt
= arclens_pt
[this
]
483 nextarclen_pt
= arclens_pt
[next
]
485 # insert the middle segment
486 newnormsubpath
.append(thisnormsubpathitem
.segments(thisparams
)[0])
488 # insert replacement curves for the corners
489 if next
or normsubpath
.closed
:
491 t1
= thisnormsubpathitem
.rotation([thisparams
[1]])[0].apply_pt(1, 0)
492 t2
= nextnormsubpathitem
.rotation([nextparams
[0]])[0].apply_pt(1, 0)
494 if (isinstance(thisnormsubpathitem
, path
.normline_pt
) and
495 isinstance(nextnormsubpathitem
, path
.normline_pt
)):
497 # case of two lines -> replace by two curves
498 d1
, g1
, f1
, e
, f2
, g2
, d2
= curvescontrols_from_endlines_pt(
499 thisnormsubpathitem
.atend_pt(), t1
, t2
,
500 thisarclen_pt
*(1-thisparams
[1]), nextarclen_pt
*(nextparams
[0]), softness
=self
.softness
)
502 p1
= thisnormsubpathitem
.at_pt([thisparams
[1]])[0]
503 p2
= nextnormsubpathitem
.at_pt([nextparams
[0]])[0]
505 newnormsubpath
.append(path
.normcurve_pt(*(d1
+ g1
+ f1
+ e
)))
506 newnormsubpath
.append(path
.normcurve_pt(*(e
+ f2
+ g2
+ d2
)))
510 # generic case -> replace by a single curve with prescribed tangents and curvatures
511 p1
= thisnormsubpathitem
.at_pt([thisparams
[1]])[0]
512 p2
= nextnormsubpathitem
.at_pt([nextparams
[0]])[0]
514 # XXX supply curvature_pt methods in path module or transfere algorithms to work with curveradii
515 def curvature(normsubpathitem
, param
):
516 r
= normsubpathitem
.curveradius_pt([param
])[0]
521 c1
= curvature(thisnormsubpathitem
, thisparams
[1])
522 c2
= curvature(nextnormsubpathitem
, nextparams
[0])
524 if not self
.obeycurv
:
525 # do not obey the sign of the curvature but
526 # make the sign such that the curve smoothly passes to the next point
527 # this results in a discontinuous curvature
528 # (but the absolute value is still continuous)
529 s1
= sign1(t1
[0] * (p2
[1]-p1
[1]) - t1
[1] * (p2
[0]-p1
[0]))
530 s2
= sign1(t2
[0] * (p2
[1]-p1
[1]) - t2
[1] * (p2
[0]-p1
[0]))
534 # get the length of the control "arms"
535 a
, d
= controldists_from_endpoints_pt(p1
, p2
, t1
, t2
, c1
, c2
)
537 # avoid overshooting at the corners:
538 # this changes not only the sign of the curvature
539 # but also the magnitude
540 if not self
.obeycurv
:
541 t
, s
= intersection(p1
, p2
, t1
, t2
)
542 if t
is None or t
< 0:
547 if s
is None or s
> 0:
552 # if there is no useful result:
553 # take arbitrary smoothing curve that does not obey
554 # the curvature constraints
555 if a
is None or d
is None:
556 dist
= math
.hypot(p1
[0] - p2
[0], p1
[1] - p2
[1])
557 a
= dist
/ (3.0 * math
.hypot(*t1
))
558 d
= dist
/ (3.0 * math
.hypot(*t2
))
560 # calculate the two missing control points
561 q1
= p1
[0] + a
* t1
[0], p1
[1] + a
* t1
[1]
562 q2
= p2
[0] - d
* t2
[0], p2
[1] - d
* t2
[1]
564 newnormsubpath
.append(path
.normcurve_pt(*(p1
+ q1
+ q2
+ p2
)))
566 if normsubpath
.closed
:
567 newnormsubpath
.close()
568 return newnormsubpath
572 smoothed
.clear
= attr
.clearclass(smoothed
)
574 class parallel(deformer
): # <<<
576 """creates a parallel path with constant distance to the original path
578 A positive 'distance' results in a curve left of the original one -- and a
579 negative 'distance' in a curve at the right. Left/Right are understood in
580 terms of the parameterization of the original curve.
581 At corners, either a circular arc is drawn around the corner, or, if the
582 curve is on the other side, the parallel curve also exhibits a corner.
584 For each path element a parallel curve/line is constructed. For curves, the
585 accuracy can be adjusted with the parameter 'relerr', thus, relerr*distance
586 is the maximum allowable error somewhere in the middle of the curve (at
587 parameter value 0.5).
588 'relerr' only applies for the 'expensive' mode where the parallel curve for
589 a single curve items may be composed of several (many) curve items.
593 # - check for greatest curvature and introduce extra corners
594 # if a normcurve is too heavily curved
595 # - do relerr-checks at better points than just at parameter 0.5
597 def __init__(self
, distance
, relerr
=0.05, expensive
=1):
598 self
.distance
= distance
600 self
.expensive
= expensive
602 def __call__(self
, distance
=None, relerr
=None, expensive
=None):
603 # returns a copy of the deformer with different parameters
608 if expensive
is None:
611 return parallel(distance
=d
, relerr
=r
, expensive
=e
)
613 def deform(self
, basepath
):
614 resultnormsubpaths
= [self
.deformsubpath(nsp
) for nsp
in basepath
.normpath().normsubpaths
]
615 return path
.normpath(resultnormsubpaths
)
617 def deformsubpath(self
, orig_nspath
):
619 distance
= unit
.topt(self
.distance
)
621 expensive
= self
.expensive
622 epsilon
= orig_nspath
.epsilon
624 new_nspath
= path
.normsubpath(epsilon
=epsilon
)
626 # 1. Store endpoints, tangents and curvatures for each element
627 points
, tangents
, curvatures
= [], [], []
628 for npitem
in orig_nspath
:
631 trafos
= npitem
.trafo([0,1])
636 ts
.append((t
[0]-p
[0], t
[1]-p
[1]))
638 rs
= npitem
.curveradius_pt([0,1])
648 curvatures
.append(cs
)
650 closeparallel
= (tangents
[-1][1][0]*tangents
[0][0][1] - tangents
[-1][1][1]*tangents
[0][0][0])
652 # 2. append the parallel path for each element:
653 for cur
in range(len(orig_nspath
)):
657 # OldEnd = points[old][0]
658 OldEndTang
= tangents
[old
][0]
661 # OldEnd = points[old][1]
662 OldEndTang
= tangents
[old
][1]
664 CurBeg
, CurEnd
= points
[cur
]
665 CurBegTang
, CurEndTang
= tangents
[cur
]
666 CurBegCurv
, CurEndCurv
= curvatures
[cur
]
668 npitem
= orig_nspath
[cur
]
670 # get the control points for the shifted pathelement
671 if isinstance(npitem
, path
.normline_pt
):
672 # get the points explicitly from the normal vector
673 A
= CurBeg
[0] - distance
* CurBegTang
[1], CurBeg
[1] + distance
* CurBegTang
[0]
674 D
= CurEnd
[0] - distance
* CurEndTang
[1], CurEnd
[1] + distance
* CurEndTang
[0]
675 new_npitems
= [path
.normline_pt(A
[0], A
[1], D
[0], D
[1])]
676 elif isinstance(npitem
, path
.normcurve_pt
):
677 # call a function to return a list of controlpoints
678 cpoints_list
= parallel_curvespoints_pt(npitem
, distance
, expensive
, relerr
, epsilon
)
680 for cpoints
in cpoints_list
:
682 new_npitems
.append(path
.normcurve_pt(A
[0],A
[1], B
[0],B
[1], C
[0],C
[1], D
[0],D
[1]))
683 # we will need the starting point of the new normpath items
684 A
= cpoints_list
[0][0]
687 # append the next piece of the path:
688 # it might contain of an extra arc or must be intersected before appending
689 parallel
= (OldEndTang
[0]*CurBegTang
[1] - OldEndTang
[1]*CurBegTang
[0])
690 if parallel
*distance
< -epsilon
:
692 # append an arc around the corner
693 # from the preceding piece to the current piece
694 # we can never get here for the first npitem! (because cur==old)
695 endpoint
= new_nspath
.atend_pt()
697 angle1
= math
.atan2(endpoint
[1] - center
[1], endpoint
[0] - center
[0]) * 180.0 / math
.pi
698 angle2
= math
.atan2(A
[1] - center
[1], A
[0] - center
[0]) * 180.0 / math
.pi
700 arc_npath
= path
.path(path
.arc_pt(
701 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
703 arc_npath
= path
.path(path
.arcn_pt(
704 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
706 for new_npitem
in arc_npath
[0]:
707 new_nspath
.append(new_npitem
)
710 elif parallel
*distance
> epsilon
:
711 # intersect the extra piece of the path with the rest of the new path
712 # and throw away the void parts
714 # build a subpath for intersection
715 extra_nspath
= path
.normsubpath(normsubpathitems
=new_npitems
, epsilon
=epsilon
)
717 intsparams
= extra_nspath
.intersect(new_nspath
)
720 # take the first intersection point:
721 extra_param
, new_param
= intsparams
[0][0], intsparams
[1][0]
722 new_nspath
= new_nspath
.segments([0, new_param
])[0]
723 extra_nspath
= extra_nspath
.segments([extra_param
, len(extra_nspath
)])[0]
724 new_npitems
= extra_nspath
.normsubpathitems
725 # in case the intersection was not sufficiently exact:
726 # CAREFUL! because we newly created all the new_npitems and
727 # the items in extra_nspath, we may in-place change the starting point
728 new_npitems
[0] = new_npitems
[0].modifiedbegin_pt(*new_nspath
.atend_pt())
730 raise # how did we get here?
733 # at the (possible) closing corner we may have to intersect another time
734 # or add another corner:
735 # the intersection must be done before appending the parallel piece
736 if orig_nspath
.closed
and cur
== len(orig_nspath
) - 1:
737 if closeparallel
* distance
> epsilon
:
738 intsparams
= extra_nspath
.intersect(new_nspath
)
741 # take the last intersection point:
742 extra_param
, new_param
= intsparams
[0][-1], intsparams
[1][-1]
743 new_nspath
= new_nspath
.segments([new_param
, len(new_nspath
)])[0]
744 extra_nspath
= extra_nspath
.segments([0, extra_param
])[0]
745 new_npitems
= extra_nspath
.normsubpathitems
746 # in case the intersection was not sufficiently exact:
747 # CAREFUL! because we newly created all the new_npitems and
748 # the items in extra_nspath, we may in-place change the end point
749 new_npitems
[-1] = new_npitems
[-1].modifiedend_pt(*new_nspath
.atbegin_pt())
751 raise # how did we get here?
756 # append the parallel piece
757 for new_npitem
in new_npitems
:
758 new_nspath
.append(new_npitem
)
761 # the curve around the closing corner must be added at last:
762 if orig_nspath
.closed
:
763 if closeparallel
* distance
< -epsilon
:
764 endpoint
= new_nspath
.atend_pt()
765 center
= orig_nspath
.atend_pt()
766 A
= new_nspath
.atbegin_pt()
767 angle1
= math
.atan2(endpoint
[1] - center
[1], endpoint
[0] - center
[0]) * 180.0 / math
.pi
768 angle2
= math
.atan2(A
[1] - center
[1], A
[0] - center
[0]) * 180.0 / math
.pi
770 arc_npath
= path
.path(path
.arc_pt(
771 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
773 arc_npath
= path
.path(path
.arcn_pt(
774 center
[0], center
[1], abs(distance
), angle1
, angle2
)).normpath()
776 for new_npitem
in arc_npath
[0]:
777 new_nspath
.append(new_npitem
)
779 # 3. extra handling of closed paths
780 if orig_nspath
.closed
:
786 parallel
.clear
= attr
.clearclass(parallel
)
788 # vim:foldmethod=marker:foldmarker=<<<,>>>