2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
30 import attr
, base
, canvas
, color
, helper
, path
, style
, trafo
, unit
36 class decoratedpath(base
.PSCmd
):
39 The main purpose of this class is during the drawing
40 (stroking/filling) of a path. It collects attributes for the
41 stroke and/or fill operations.
44 def __init__(self
, path
, strokepath
=None, fillpath
=None,
45 styles
=None, strokestyles
=None, fillstyles
=None,
50 # path to be stroked or filled (or None)
51 self
.strokepath
= strokepath
52 self
.fillpath
= fillpath
54 # global style for stroking and filling and subdps
55 self
.styles
= helper
.ensurelist(styles
)
57 # styles which apply only for stroking and filling
58 self
.strokestyles
= helper
.ensurelist(strokestyles
)
59 self
.fillstyles
= helper
.ensurelist(fillstyles
)
61 # the canvas can contain additional elements of the path, e.g.,
64 self
.subcanvas
= canvas
.canvas()
66 self
.subcanvas
= subcanvas
70 scbbox
= self
.subcanvas
.bbox()
71 pbbox
= self
.path
.bbox()
72 if scbbox
is not None:
79 for style
in list(self
.styles
) + list(self
.fillstyles
) + list(self
.strokestyles
):
80 result
.extend(style
.prolog())
81 result
.extend(self
.subcanvas
.prolog())
84 def outputPS(self
, file):
85 # draw (stroke and/or fill) the decoratedpath on the canvas
86 # while trying to produce an efficient output, e.g., by
87 # not writing one path two times
90 def _writestyles(styles
, file=file):
97 _writestyles(self
.styles
)
99 if self
.fillpath
is not None:
100 file.write("newpath\n")
101 self
.fillpath
.outputPS(file)
103 if self
.strokepath
==self
.fillpath
:
104 # do efficient stroking + filling
105 file.write("gsave\n")
108 _writestyles(self
.fillstyles
)
111 file.write("grestore\n")
113 if self
.strokestyles
:
114 file.write("gsave\n")
115 _writestyles(self
.strokestyles
)
117 file.write("stroke\n")
119 if self
.strokestyles
:
120 file.write("grestore\n")
122 # only fill fillpath - for the moment
124 file.write("gsave\n")
125 _writestyles(self
.fillstyles
)
130 file.write("grestore\n")
132 if self
.strokepath
is not None and self
.strokepath
!=self
.fillpath
:
133 # this is the only relevant case still left
134 # Note that a possible stroking has already been done.
136 if self
.strokestyles
:
137 file.write("gsave\n")
138 _writestyles(self
.strokestyles
)
140 file.write("newpath\n")
141 self
.strokepath
.outputPS(file)
142 file.write("stroke\n")
144 if self
.strokestyles
:
145 file.write("grestore\n")
147 if self
.strokepath
is None and self
.fillpath
is None:
148 raise RuntimeError("Path neither to be stroked nor filled")
150 # now, draw additional elements of decoratedpath
151 self
.subcanvas
.outputPS(file)
153 # restore global styles
155 file.write("grestore\n")
157 def outputPDF(self
, file):
158 # draw (stroke and/or fill) the decoratedpath on the canvas
160 def _writestyles(styles
, file=file):
162 style
.outputPDF(file)
164 def _writestrokestyles(strokestyles
, file=file):
165 for style
in strokestyles
:
166 if isinstance(style
, color
.color
):
167 style
.outputPDF(file, fillattr
=0)
169 style
.outputPDF(file)
171 def _writefillstyles(fillstyles
, file=file):
172 for style
in fillstyles
:
173 if isinstance(style
, color
.color
):
174 style
.outputPDF(file, strokeattr
=0)
176 style
.outputPDF(file)
178 # apply global styles
180 file.write("q\n") # gsave
181 _writestyles(self
.styles
)
183 if self
.fillpath
is not None:
184 self
.fillpath
.outputPDF(file)
186 if self
.strokepath
==self
.fillpath
:
187 # do efficient stroking + filling
188 file.write("q\n") # gsave
191 _writefillstyles(self
.fillstyles
)
192 if self
.strokestyles
:
193 _writestrokestyles(self
.strokestyles
)
195 file.write("B\n") # both stroke and fill
196 file.write("Q\n") # grestore
198 # only fill fillpath - for the moment
200 file.write("q\n") # gsave
201 _writefillstyles(self
.fillstyles
)
203 file.write("f\n") # fill
206 file.write("Q\n") # grestore
208 if self
.strokepath
is not None and self
.strokepath
!=self
.fillpath
:
209 # this is the only relevant case still left
210 # Note that a possible stroking has already been done.
212 if self
.strokestyles
:
213 file.write("q\n") # gsave
214 _writestrokestyles(self
.strokestyles
)
216 self
.strokepath
.outputPDF(file)
217 file.write("S\n") # stroke
219 if self
.strokestyles
:
220 file.write("Q\n") # grestore
222 if self
.strokepath
is None and self
.fillpath
is None:
223 raise RuntimeError("Path neither to be stroked nor filled")
225 # now, draw additional elements of decoratedpath
226 self
.subcanvas
.outputPDF(file)
228 # restore global styles
230 file.write("Q\n") # grestore
240 In contrast to path styles, path decorators depend on the concrete
241 path to which they are applied. In particular, they don't make
242 sense without any path and can thus not be used in canvas.set!
246 def decorate(self
, dp
):
247 """apply a style to a given decoratedpath object dp
249 decorate accepts a decoratedpath object dp, applies PathStyle
250 by modifying dp in place and returning the new dp.
256 # stroked and filled: basic decos which stroked and fill,
257 # respectively the path
260 class _stroked(deco
, attr
.exclusiveattr
):
262 """stroked is a decorator, which draws the outline of the path"""
264 def __init__(self
, styles
=[]):
265 attr
.exclusiveattr
.__init
__(self
, _stroked
)
266 self
.styles
= attr
.mergeattrs(styles
)
267 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
269 def __call__(self
, styles
=[]):
270 # XXX or should we also merge self.styles
271 return _stroked(styles
)
273 def decorate(self
, dp
):
274 dp
.strokepath
= dp
.path
275 dp
.strokestyles
= self
.styles
279 stroked
.clear
= attr
.clearclass(_stroked
)
282 class _filled(deco
, attr
.exclusiveattr
):
284 """filled is a decorator, which fills the interior of the path"""
286 def __init__(self
, styles
=[]):
287 attr
.exclusiveattr
.__init
__(self
, _filled
)
288 self
.styles
= attr
.mergeattrs(styles
)
289 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
291 def __call__(self
, styles
=[]):
292 # XXX or should we also merge self.styles
293 return _filled(styles
)
295 def decorate(self
, dp
):
296 dp
.fillpath
= dp
.path
297 dp
.fillstyles
= self
.styles
301 filled
.clear
= attr
.clearclass(_filled
)
307 # two helper functions which construct the arrowhead and return its size, respectively
309 def _arrowheadtemplatelength(anormpath
, size
):
310 "calculate length of arrowhead template (in parametrisation of anormpath)"
312 tx
, ty
= anormpath
.begin()
314 # obtain arrow template by using path up to first intersection
315 # with circle around tip (as suggested by Michael Schindler)
316 ipar
= anormpath
.intersect(path
.circle(tx
, ty
, size
))
320 # if this doesn't work, use first order conversion from pts to
321 # the bezier curve's parametrization
322 tvec
= anormpath
.tangent(0)
323 tlen
= tvec
.arclen_pt()
325 alen
= unit
.topt(size
)/tlen
326 except ArithmeticError:
327 # take maximum, we can get
328 alen
= anormpath
.range()
329 if alen
> anormpath
.range(): alen
= anormpath
.range()
334 def _arrowhead(anormpath
, size
, angle
, constriction
):
336 """helper routine, which returns an arrowhead for a normpath
338 returns arrowhead at begin of anormpath with size,
339 opening angle and relative constriction
342 alen
= _arrowheadtemplatelength(anormpath
, size
)
343 tx
, ty
= anormpath
.begin()
345 # now we construct the template for our arrow but cutting
346 # the path a the corresponding length
347 arrowtemplate
= anormpath
.split([alen
])[0]
349 # from this template, we construct the two outer curves
351 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
352 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
354 # now come the joining backward parts
356 # arrow with constriction
358 # constriction point (cx, cy) lies on path
359 cx
, cy
= anormpath
.at(constriction
*alen
)
361 arrowcr
= path
.line(*(arrowr
.end()+(cx
,cy
)))
363 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
364 arrow
.append(path
.closepath())
366 # arrow without constriction
367 arrow
= arrowl
.reversed() << arrowr
368 arrow
.append(path
.closepath())
375 class arrow(deco
, attr
.attr
):
377 """arrow is a decorator which adds an arrow to either side of the path"""
379 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
380 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
381 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
382 self
.position
= position
383 self
.size
= unit
.length(size
, default_type
="v")
385 self
.constriction
= constriction
387 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=None):
391 position
= self
.position
396 if constriction
is None:
397 constriction
= self
.constriction
398 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
400 def decorate(self
, dp
):
401 # XXX raise exception error, when strokepath is not defined
403 # convert to normpath if necessary
404 if isinstance(dp
.strokepath
, path
.normpath
):
405 anormpath
= dp
.strokepath
407 anormpath
= path
.normpath(dp
.path
)
409 anormpath
= anormpath
.reversed()
411 # add arrowhead to decoratedpath
412 dp
.subcanvas
.draw(_arrowhead(anormpath
, self
.size
, self
.angle
, self
.constriction
),
415 # calculate new strokepath
416 alen
= _arrowheadtemplatelength(anormpath
, self
.size
)
417 if self
.constriction
:
418 ilen
= alen
*self
.constriction
422 # correct somewhat for rotation of arrow segments
423 ilen
= ilen
*math
.cos(math
.pi
*self
.angle
/360.0)
425 # this is the rest of the path, we have to draw
426 anormpath
= anormpath
.split([ilen
])[1]
428 # go back to original orientation, if necessary
432 # set the new (shortened) strokepath
433 dp
.strokepath
= anormpath
437 arrow
.clear
= attr
.clearclass(arrow
)
439 # arrows at begin of path
440 barrow
= arrow(position
=0)
441 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
442 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
443 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
444 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
445 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
446 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
447 barrow
.normal
= barrow(size
=_base
)
448 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
449 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
450 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
451 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
452 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
453 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
455 # arrows at end of path
456 earrow
= arrow(position
=1)
457 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
458 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
459 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
460 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
461 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
462 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
463 earrow
.normal
= earrow(size
=_base
)
464 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
465 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
466 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
467 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
468 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
469 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
472 class wriggle(deco
, attr
.attr
):
474 def __init__(self
, skipleft
=1, skipright
=1, radius
=0.5, loops
=8, curvesperloop
=10):
475 self
.skipleft_str
= skipleft
476 self
.skipright_str
= skipright
477 self
.radius_str
= radius
479 self
.curvesperloop
= curvesperloop
481 def decorate(self
, dp
):
482 # XXX: is this the correct way to select the basepath???!!!
483 if isinstance(dp
.strokepath
, path
.normpath
):
484 basepath
= dp
.strokepath
485 elif dp
.strokepath
is not None:
486 basepath
= path
.normpath(dp
.strokepath
)
487 elif isinstance(dp
.path
, path
.normpath
):
490 basepath
= path
.normpath(dp
.path
)
492 skipleft
= unit
.topt(unit
.length(self
.skipleft_str
, default_type
="v"))
493 skipright
= unit
.topt(unit
.length(self
.skipright_str
, default_type
="v"))
494 startpar
, endpar
= basepath
.arclentoparam(map(unit
.t_pt
, [skipleft
, basepath
.arclen_pt() - skipright
]))
495 radius
= unit
.topt(unit
.length(self
.radius_str
))
497 # search for the first intersection of a circle around start point x, y bigger than startpar
498 x
, y
= basepath
.at_pt(startpar
)
500 for intersectpar
in basepath
.intersect(path
.circle_pt(x
, y
, radius
))[0]:
501 if startpar
< intersectpar
and (startcircpar
is None or startcircpar
> intersectpar
):
502 startcircpar
= intersectpar
503 if startcircpar
is None:
504 raise RuntimeError("couldn't find wriggle start point")
505 # calculate start position and angle
506 xcenter
, ycenter
= basepath
.at_pt(startcircpar
)
507 startpos
= basepath
.split([startcircpar
])[0].arclen_pt()
508 startangle
= math
.atan2(y
-ycenter
, x
-xcenter
)
510 # find the last intersection of a circle around x, y smaller than endpar
511 x
, y
= basepath
.at_pt(endpar
)
513 for intersectpar
in basepath
.intersect(path
.circle_pt(x
, y
, radius
))[0]:
514 if endpar
> intersectpar
and (endcircpar
is None or endcircpar
< intersectpar
):
515 endcircpar
= intersectpar
516 if endcircpar
is None:
517 raise RuntimeError("couldn't find wriggle end point")
518 # calculate end position and angle
519 x2
, y2
= basepath
.at_pt(endcircpar
)
520 endpos
= basepath
.split([endcircpar
])[0].arclen_pt()
521 endangle
= math
.atan2(y
-y2
, x
-x2
)
523 if endangle
< startangle
:
524 endangle
+= 2*math
.pi
526 # calculate basepath points
527 sections
= self
.loops
* self
.curvesperloop
528 posrange
= endpos
- startpos
529 poslist
= [startpos
+ i
*posrange
/sections
for i
in range(sections
+1)]
530 parlist
= basepath
.arclentoparam(map(unit
.t_pt
, poslist
))
531 atlist
= [basepath
.at_pt(x
) for x
in parlist
]
533 # from pyx import color
535 # dp.subcanvas.stroke(path.circle_pt(at[0], at[1], 1), [color.rgb.blue])
537 # calculate wriggle points and tangents
538 anglerange
= 2*math
.pi
*self
.loops
+ endangle
- startangle
539 deltaangle
= anglerange
/ sections
540 tangentlength
= radius
* 4 * (1 - math
.cos(deltaangle
/2)) / (3 * math
.sin(deltaangle
/2))
541 wriggleat
= [None]*(sections
+1)
542 wriggletangentstart
= [None]*(sections
+1)
543 wriggletangentend
= [None]*(sections
+1)
544 for i
in range(sections
+1):
546 angle
= startangle
+ i
*anglerange
/sections
547 dx
, dy
= math
.cos(angle
), math
.sin(angle
)
548 wriggleat
[i
] = x
+ radius
*dx
, y
+ radius
*dy
549 # dp.subcanvas.stroke(path.line_pt(x, y, x + radius*dx, y + radius*dy), [color.rgb.blue])
550 wriggletangentstart
[i
] = x
+ radius
*dx
+ tangentlength
*dy
, y
+ radius
*dy
- tangentlength
*dx
551 wriggletangentend
[i
] = x
+ radius
*dx
- tangentlength
*dy
, y
+ radius
*dy
+ tangentlength
*dx
554 wrigglepath
= basepath
.split([startpar
])[0]
555 wrigglepath
.append(path
.multicurveto_pt([wriggletangentend
[i
-1] +
556 wriggletangentstart
[i
] +
558 for i
in range(1, sections
+1)]))
559 wrigglepath
= wrigglepath
.glue(basepath
.split([endpar
])[1]) # glue and glued?!?
562 dp
.path
= wrigglepath
# otherwise the bbox is wrong!
563 dp
.strokepath
= wrigglepath
567 class cycloid(deco
, attr
.attr
):
568 """Wraps a cycloid around a path.
570 The outcome looks like a metal spring with the originalpath as the axis.
573 def __init__(self
, radius
=0.5, loops
=10, skipfirst
=1, skiplast
=1, curvesperloop
=2, left
=1):
574 self
.skipfirst
= unit
.length(skipfirst
, default_type
="v")
575 self
.skiplast
= unit
.length(skiplast
, default_type
="v")
576 self
.radius
= unit
.length(radius
, default_type
="v")
577 self
.halfloops
= 2 * int(loops
) + 1
578 self
.curvesperhloop
= int(0.5 * curvesperloop
)
579 self
.sign
= left
and 1 or -1
581 def decorate(self
, dp
):
582 # XXX: is this the correct way to select the basepath???!!!
583 if isinstance(dp
.strokepath
, path
.normpath
):
584 basepath
= dp
.strokepath
585 elif dp
.strokepath
is not None:
586 basepath
= path
.normpath(dp
.strokepath
)
587 elif isinstance(dp
.path
, path
.normpath
):
590 basepath
= path
.normpath(dp
.path
)
592 skipfirst
= abs(unit
.topt(self
.skipfirst
))
593 skiplast
= abs(unit
.topt(self
.skiplast
))
594 radius
= abs(unit
.topt(self
.radius
))
596 # make list of the lengths and parameters at points on basepath where we will add cycloid-points
597 totlength
= basepath
.arclen_pt()
598 if totlength
< skipfirst
+ skiplast
+ radius
:
599 raise RuntimeError("Path is too short for decoration with cycloid")
601 # differences in length, angle ... between two basepoints
602 # and between basepoints and controlpoints
603 Dphi
= math
.pi
/ self
.curvesperhloop
604 Dlength
= (totlength
- skipfirst
- skiplast
- 2*radius
) * 1.0 / (self
.halfloops
* self
.curvesperhloop
)
605 # from path._arctobcurve:
606 # optimal relative distance along tangent for second and third control point
607 l
= 4 * (1 - math
.cos(Dphi
/2)) / (3 * math
.sin(Dphi
/2))
608 controlDphi
= math
.atan2(l
, 1.0)
609 controlDlength
= Dlength
* controlDphi
/ Dphi
611 # for every point on the cycloid we need the basepoint and two controlpoints
612 lengths
= [skipfirst
+ radius
+ i
* Dlength
for i
in range(self
.halfloops
* self
.curvesperhloop
+ 1)]
613 lengths
[0] = skipfirst
614 lengths
[-1] = totlength
- skiplast
615 phis
= [i
* Dphi
for i
in range(self
.halfloops
* self
.curvesperhloop
+ 1)]
616 params
= basepath
.arclentoparam_pt(lengths
)
618 #for param in params:
619 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.blue])
620 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.blue])
621 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.red])
623 # get the positions of the splitpoints in the cycloid
625 for phi
, param
, length
in zip(phis
, params
, lengths
):
626 # the cycloid is a circle that is stretched along the basepath
627 # here are the points of that circle
628 basetrafo
= basepath
.trafo(param
)
629 basex
, basey
= -radius
* math
.cos(phi
), radius
* math
.sin(phi
)
630 preex
, preey
= basex
- l
* basey
, basey
+ l
* basex
631 postx
, posty
= basex
+ l
* basey
, basey
- l
* basex
632 # and put everything at the proper place
633 preex
= preex
- controlDlength
634 postx
= postx
+ controlDlength
635 if length
is lengths
[0]:
637 if length
is lengths
[-1]:
638 basex
, preex
= basex
- radius
, preex
- radius
639 points
.append(basetrafo
._apply
(preex
, self
.sign
* preey
) +
640 basetrafo
._apply
(basex
, self
.sign
* basey
) +
641 basetrafo
._apply
(postx
, self
.sign
* posty
))
643 cycloidpath
= basepath
.split([params
[0]])[0]
645 cycloidpath
.append(path
.multicurveto_pt(
646 [(points
[i
][4:6] + points
[i
+1][0:4]) for i
in range(len(points
)-1)]))
648 raise RuntimeError("Not enough points while decorating with cycloid")
649 cycloidpath
.glue(basepath
.split([params
[-1]])[-1])
652 # XXX bbox of dp.path is wrong
653 dp
.strokepath
= cycloidpath
657 class smoothed(deco
, attr
.attr
):
659 """Bends corners in a path.
661 This decorator replaces corners in a path with bezier curves. There are two cases:
662 - If the corner lies between two lines, _two_ bezier curves will be used
663 that are highly optimized to look good (their curvature is to be zero at the ends
664 and has to have zero derivative in the middle).
665 Additionally, it can controlled by the softness-parameter.
666 - If the corner lies between curves then _one_ bezier is used that is (except in some
667 special cases) uniquely determined by the tangents and curvatures at its end-points.
668 In some cases it is necessary to use only the absolute value of the curvature to avoid a
669 cusp-shaped connection of the new bezier to the old path. In this case the use of
670 "strict=0" allows the sign of the curvature to switch.
671 - The radius argument gives the arclength-distance of the corner to the points where the
672 old path is cut and the beziers are inserted.
673 - Path elements that are too short (shorter than the radius) are skipped
676 def __init__(self
, radius
, softness
=1, strict
=0):
677 self
.radius
= unit
.length(radius
, default_type
="v")
678 self
.softness
= softness
681 def _twobeziersbetweentolines(self
, B
, tangent1
, tangent2
, r1
, r2
, softness
=1):
683 # and two tangent vectors heading to and from B
684 # and two radii r1 and r2:
685 # All arguments must be in Points
686 # Returns the seven control points of the two bezier curves:
688 # - control points g1 and f1
690 # - control points f2 and g2
693 # make direction vectors d1: from B to A
695 d1
= -tangent1
[0] / math
.hypot(*tangent1
), -tangent1
[1] / math
.hypot(*tangent1
)
696 d2
= tangent2
[0] / math
.hypot(*tangent2
), tangent2
[1] / math
.hypot(*tangent2
)
698 # 0.3192 has turned out to be the maximum softness available
699 # for straight lines ;-)
700 f
= 0.3192 * softness
701 g
= (15.0 * f
+ math
.sqrt(-15.0*f
*f
+ 24.0*f
))/12.0
703 # make the control points
704 f1
= B
[0] + f
* r1
* d1
[0], B
[1] + f
* r1
* d1
[1]
705 f2
= B
[0] + f
* r2
* d2
[0], B
[1] + f
* r2
* d2
[1]
706 g1
= B
[0] + g
* r1
* d1
[0], B
[1] + g
* r1
* d1
[1]
707 g2
= B
[0] + g
* r2
* d2
[0], B
[1] + g
* r2
* d2
[1]
708 d1
= B
[0] + r1
* d1
[0], B
[1] + r1
* d1
[1]
709 d2
= B
[0] + r2
* d2
[0], B
[1] + r2
* d2
[1]
710 e
= 0.5 * (f1
[0] + f2
[0]), 0.5 * (f1
[1] + f2
[1])
712 return [d1
, g1
, f1
, e
, f2
, g2
, d2
]
714 def _onebezierbetweentwopathels(self
, A
, B
, tangentA
, tangentB
, curvA
, curvB
, strict
=0):
715 # connects points A and B with a bezier curve that has
716 # prescribed tangents dirA, dirB and curvatures curA, curB.
717 # If strict, the sign of the curvature will be forced which may invert
718 # the sign of the tangents. If not strict, the sign of the curvature may
719 # be switched but the tangent may not.
722 try: return abs(a
) / a
723 except ZeroDivisionError: return 0
725 # normalise the tangent vectors
726 dirA
= (tangentA
[0] / math
.hypot(*tangentA
), tangentA
[1] / math
.hypot(*tangentA
))
727 dirB
= (tangentB
[0] / math
.hypot(*tangentB
), tangentB
[1] / math
.hypot(*tangentB
))
729 T
= dirA
[0] * dirB
[1] - dirA
[1] * dirB
[0]
730 D
= 3 * (dirA
[0] * (B
[1]-A
[1]) - dirA
[1] * (B
[0]-A
[0]))
731 E
= 3 * (dirB
[0] * (B
[1]-A
[1]) - dirB
[1] * (B
[0]-A
[0]))
732 # the variables: \dot X(0) = a * dirA
733 # \dot X(1) = b * dirB
736 # ask for some special cases:
737 # Newton iteration is likely to fail if T==0 or curvA,curvB==0
741 a
= math
.sqrt(abs(a
)) * sign(a
)
743 b
= math
.sqrt(abs(b
)) * sign(b
)
744 except ZeroDivisionError:
745 sys
.stderr
.write("*** PyX Warning: The connecting bezier is not uniquely determined."
746 "The simple heuristic solution may not be optimal.")
747 a
= b
= 1.5 * math
.hypot(A
[0] - B
[0], A
[1] - B
[1])
749 if abs(curvA
) < 1.0e-4:
751 a
= - (E
+ b
*abs(b
)*curvB
*0.5) / T
752 elif abs(curvB
) < 1.0e-4:
754 b
= (D
- a
*abs(a
)*curvA
*0.5) / T
758 # do the general case: Newton iteration
760 # solve the coupled system
761 # 0 = Ga(a,b) = 0.5 a |a| curvA + b * T - D
762 # 0 = Gb(a,b) = 0.5 b |b| curvB + a * T + E
763 # this system is equivalent to the geometric contraints:
764 # the curvature and the normal tangent vectors
765 # at parameters 0 and 1 are to be continuous
766 # the system is solved by 2-dim Newton-Iteration
767 # (a,b)^{i+1} = (a,b)^i - (DG)^{-1} (Ga(a^i,b^i), Gb(a^i,b^i))
770 while max(abs(Ga
),abs(Gb
)) > 1.0e-5:
771 detDG
= abs(a
*b
) * curvA
*curvB
- T
*T
772 invDG
= [[curvB
*abs(b
)/detDG
, -T
/detDG
], [-T
/detDG
, curvA
*abs(a
)/detDG
]]
774 Ga
= a
*abs(a
)*curvA
*0.5 + b
*T
- D
775 Gb
= b
*abs(b
)*curvB
*0.5 + a
*T
+ E
777 a
, b
= a
- invDG
[0][0]*Ga
- invDG
[0][1]*Gb
, b
- invDG
[1][0]*Ga
- invDG
[1][1]*Gb
779 # the curvature may change its sign if we would get a cusp
780 # in the optimal case we have a>0 and b>0
782 a
, b
= abs(a
), abs(b
)
784 return [A
, (A
[0] + a
* dirA
[0] / 3.0, A
[1] + a
* dirA
[1] / 3.0),
785 (B
[0] - b
* dirB
[0] / 3.0, B
[1] - b
* dirB
[1] / 3.0), B
]
788 def decorate(self
, dp
):
789 radius
= unit
.topt(self
.radius
)
790 # XXX: is this the correct way to select the basepath???!!!
791 # compare to wriggle()
792 if isinstance(dp
.strokepath
, path
.normpath
):
793 basepath
= dp
.strokepath
794 elif dp
.strokepath
is not None:
795 basepath
= path
.normpath(dp
.strokepath
)
796 elif isinstance(dp
.path
, path
.normpath
):
799 basepath
= path
.normpath(dp
.path
)
801 newpath
= path
.path()
802 for normsubpath
in basepath
.subpaths
:
803 npels
= normsubpath
.normpathels
804 arclens
= [npel
.arclen_pt() for npel
in npels
]
806 # 1. Build up a list of all relevant normpathels
807 # and the lengths where they will be cut (length with respect to the normsubpath)
810 for no
in range(len(arclens
)):
812 # a first selection criterion for skipping too short normpathels
813 # the rest will queeze the radius
815 npelnumbers
.append(no
)
817 sys
.stderr
.write("*** PyX Warning: smoothed is skipping a normpathel that is too short\n")
819 # XXX: what happens, if 0 or -1 is skipped and path not closed?
821 # 2. Find the parameters, points,
822 # and calculate tangents and curvatures
823 params
, tangents
, curvatures
, points
= [], [], [], []
824 for no
in npelnumbers
:
828 # find the parameter(s): either one or two
829 if no
is npelnumbers
[0] and not normsubpath
.closed
:
830 pars
= npel
._arclentoparam
_pt
([max(0, alen
- radius
)])[0]
831 elif alen
> 2 * radius
:
832 pars
= npel
._arclentoparam
_pt
([radius
, alen
- radius
])[0]
834 pars
= npel
._arclentoparam
_pt
([0.5 * alen
])[0]
836 # find points, tangents and curvatures
839 # XXX: there is no trafo method for normpathels?
840 thetrafo
= normsubpath
.trafo(par
+ no
)
841 p
= thetrafo
._apply
(0,0)
842 t
= thetrafo
._apply
(1,0)
844 ts
.append((t
[0]-p
[0], t
[1]-p
[1]))
845 c
= npel
.curvradius_pt(par
)
846 if c
is None: cs
.append(0)
847 else: cs
.append(1.0/c
)
852 curvatures
.append(cs
)
854 do_moveto
= 1 # we do not know yet where to moveto
855 # 3. First part of extra handling of closed paths
856 if not normsubpath
.closed
:
857 bpart
= npels
[npelnumbers
[0]].split(params
[0])[0]
859 newpath
.append(path
.moveto_pt(*bpart
.begin_pt()))
861 if isinstance(bpart
, path
.normline
):
862 newpath
.append(path
.lineto_pt(*bpart
.end_pt()))
863 elif isinstance(bpart
, path
.normcurve
):
864 newpath
.append(path
.curveto_pt(bpart
.x1
, bpart
.y1
, bpart
.x2
, bpart
.y2
, bpart
.x3
, bpart
.y3
))
867 # 4. Do the splitting for the first to the last element,
868 # a closed path must be closed later
869 for i
in range(len(npelnumbers
)-1+(normsubpath
.closed
==1)):
870 this
= npelnumbers
[i
]
871 next
= npelnumbers
[(i
+1) % len(npelnumbers
)]
872 thisnpel
, nextnpel
= npels
[this
], npels
[next
]
874 # split thisnpel apart and take the middle peace
875 if len(points
[this
]) == 2:
876 mpart
= thisnpel
.split(params
[this
])[1]
878 newpath
.append(path
.moveto_pt(*mpart
.begin_pt()))
880 if isinstance(mpart
, path
.normline
):
881 newpath
.append(path
.lineto_pt(*mpart
.end_pt()))
882 elif isinstance(mpart
, path
.normcurve
):
883 newpath
.append(path
.curveto_pt(mpart
.x1
, mpart
.y1
, mpart
.x2
, mpart
.y2
, mpart
.x3
, mpart
.y3
))
885 # add the curve(s) replacing the corner
886 if isinstance(thisnpel
, path
.normline
) and isinstance(nextnpel
, path
.normline
) \
887 and (next
-this
== 1 or (this
==0 and next
==len(npels
)-1)):
888 d1
,g1
,f1
,e
,f2
,g2
,d2
= self
._twobeziersbetweentolines
(
889 thisnpel
.end_pt(), tangents
[this
][-1], tangents
[next
][0],
890 math
.hypot(points
[this
][-1][0] - thisnpel
.end_pt()[0], points
[this
][-1][1] - thisnpel
.end_pt()[1]),
891 math
.hypot(points
[next
][0][0] - nextnpel
.begin_pt()[0], points
[next
][0][1] - nextnpel
.begin_pt()[1]),
892 softness
=self
.softness
)
894 newpath
.append(path
.moveto_pt(*d1
))
896 newpath
.append(path
.curveto_pt(*(g1
+ f1
+ e
)))
897 newpath
.append(path
.curveto_pt(*(f2
+ g2
+ d2
)))
898 #for X in [d1,g1,f1,e,f2,g2,d2]:
899 # dp.subcanvas.fill(path.circle_pt(X[0], X[1], 1.0))
902 # the curvature may have the wrong sign -- produce a heuristic for the sign:
903 vx
, vy
= thisnpel
.end_pt()[0] - points
[this
][-1][0], thisnpel
.end_pt()[1] - points
[this
][-1][1]
904 wx
, wy
= points
[next
][0][0] - thisnpel
.end_pt()[0], points
[next
][0][1] - thisnpel
.end_pt()[1]
905 sign
= vx
* wy
- vy
* wx
906 sign
= sign
/ abs(sign
)
907 curvatures
[this
][-1] = sign
* abs(curvatures
[this
][-1])
908 curvatures
[next
][0] = sign
* abs(curvatures
[next
][0])
909 A
,B
,C
,D
= self
._onebezierbetweentwopathels
(
910 points
[this
][-1], points
[next
][0], tangents
[this
][-1], tangents
[next
][0],
911 curvatures
[this
][-1], curvatures
[next
][0], strict
=self
.strict
)
913 newpath
.append(path
.moveto_pt(*A
))
915 newpath
.append(path
.curveto_pt(*(B
+ C
+ D
)))
917 # dp.subcanvas.fill(path.circle_pt(X[0], X[1], 1.0))
919 # 5. Second part of extra handling of closed paths
920 if normsubpath
.closed
:
922 newpath
.append(path
.moveto_pt(*dp
.strokepath
.begin()))
923 sys
.stderr
.write("*** PyXWarning: The whole path has been smoothed away -- sorry\n")
924 newpath
.append(path
.closepath())
926 epart
= npels
[npelnumbers
[-1]].split([params
[-1][0]])[-1]
928 newpath
.append(path
.moveto_pt(*epart
.begin_pt()))
930 if isinstance(epart
, path
.normline
):
931 newpath
.append(path
.lineto_pt(*epart
.end_pt()))
932 elif isinstance(epart
, path
.normcurve
):
933 newpath
.append(path
.curveto_pt(epart
.x1
, epart
.y1
, epart
.x2
, epart
.y2
, epart
.x3
, epart
.y3
))
935 dp
.strokepath
= newpath
938 smoothed
.clear
= attr
.clearclass(smoothed
)
941 smoothed
.SHARP
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(64)))
942 smoothed
.SHARp
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(32)))
943 smoothed
.SHArp
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(16)))
944 smoothed
.SHarp
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(8)))
945 smoothed
.Sharp
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(4)))
946 smoothed
.sharp
= smoothed(radius
="%f cm" % (_base
/math
.sqrt(2)))
947 smoothed
.normal
= smoothed(radius
="%f cm" % (_base
))
948 smoothed
.round = smoothed(radius
="%f cm" % (_base
*math
.sqrt(2)))
949 smoothed
.Round
= smoothed(radius
="%f cm" % (_base
*math
.sqrt(4)))
950 smoothed
.ROund
= smoothed(radius
="%f cm" % (_base
*math
.sqrt(8)))
951 smoothed
.ROUnd
= smoothed(radius
="%f cm" % (_base
*math
.sqrt(16)))
952 smoothed
.ROUNd
= smoothed(radius
="%f cm" % (_base
*math
.sqrt(32)))
953 smoothed
.ROUND
= smoothed(radius
="%f cm" % (_base
*math
.sqrt(64)))