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
, 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):
96 canvas
._gsave
().outputPS(file)
97 _writestyles(self
.styles
)
99 if self
.fillpath
is not None:
100 canvas
._newpath
().outputPS(file)
101 self
.fillpath
.outputPS(file)
103 if self
.strokepath
==self
.fillpath
:
104 # do efficient stroking + filling
105 canvas
._gsave
().outputPS(file)
108 _writestyles(self
.fillstyles
)
110 canvas
._fill
().outputPS(file)
111 canvas
._grestore
().outputPS(file)
113 if self
.strokestyles
:
114 canvas
._gsave
().outputPS(file)
115 _writestyles(self
.strokestyles
)
117 canvas
._stroke
().outputPS(file)
119 if self
.strokestyles
:
120 canvas
._grestore
().outputPS(file)
122 # only fill fillpath - for the moment
124 canvas
._gsave
().outputPS(file)
125 _writestyles(self
.fillstyles
)
127 canvas
._fill
().outputPS(file)
130 canvas
._grestore
().outputPS(file)
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 canvas
._gsave
().outputPS(file)
138 _writestyles(self
.strokestyles
)
140 canvas
._newpath
().outputPS(file)
141 self
.strokepath
.outputPS(file)
142 canvas
._stroke
().outputPS(file)
144 if self
.strokestyles
:
145 canvas
._grestore
().outputPS(file)
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 canvas
._grestore
().outputPS(file)
157 def outputPDF(self
, file):
158 # draw (stroke and/or fill) the decoratedpath on the canvas
159 # while trying to produce an efficient output, e.g., by
160 # not writing one path two times
162 def _writestyles(styles
, file=file):
164 style
.outputPDF(file)
166 # apply global styles
168 canvas
._gsave
().outputPDF(file)
169 _writestyles(self
.styles
)
171 if self
.fillpath
is not None:
172 canvas
._newpath
().outputPDF(file)
173 self
.fillpath
.outputPDF(file)
175 if self
.strokepath
==self
.fillpath
:
176 # do efficient stroking + filling
177 canvas
._gsave
().outputPDF(file)
180 _writestyles(self
.fillstyles
)
182 canvas
._fill
().outputPDF(file)
183 canvas
._grestore
().outputPDF(file)
185 if self
.strokestyles
:
186 canvas
._gsave
().outputPDF(file)
187 _writestyles(self
.strokestyles
)
189 canvas
._stroke
().outputPDF(file)
191 if self
.strokestyles
:
192 canvas
._grestore
().outputPDF(file)
194 # only fill fillpath - for the moment
196 canvas
._gsave
().outputPDF(file)
197 _writestyles(self
.fillstyles
)
199 canvas
._fill
().outputPDF(file)
202 canvas
._grestore
().outputPDF(file)
204 if self
.strokepath
is not None and self
.strokepath
!=self
.fillpath
:
205 # this is the only relevant case still left
206 # Note that a possible stroking has already been done.
208 if self
.strokestyles
:
209 canvas
._gsave
().outputPDF(file)
210 _writestyles(self
.strokestyles
)
212 canvas
._newpath
().outputPDF(file)
213 self
.strokepath
.outputPDF(file)
214 canvas
._stroke
().outputPDF(file)
216 if self
.strokestyles
:
217 canvas
._grestore
().outputPDF(file)
219 if self
.strokepath
is None and self
.fillpath
is None:
220 raise RuntimeError("Path neither to be stroked nor filled")
222 # now, draw additional elements of decoratedpath
223 self
.subcanvas
.outputPDF(file)
225 # restore global styles
227 canvas
._grestore
().outputPDF(file)
237 In contrast to path styles, path decorators depend on the concrete
238 path to which they are applied. In particular, they don't make
239 sense without any path and can thus not be used in canvas.set!
243 def decorate(self
, dp
):
244 """apply a style to a given decoratedpath object dp
246 decorate accepts a decoratedpath object dp, applies PathStyle
247 by modifying dp in place and returning the new dp.
253 # stroked and filled: basic decos which stroked and fill,
254 # respectively the path
257 class _stroked(deco
, attr
.exclusiveattr
):
259 """stroked is a decorator, which draws the outline of the path"""
261 def __init__(self
, styles
=[]):
262 attr
.exclusiveattr
.__init
__(self
, _stroked
)
263 self
.styles
= attr
.mergeattrs(styles
)
264 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
266 def __call__(self
, styles
=[]):
267 # XXX or should we also merge self.styles
268 return _stroked(styles
)
270 def decorate(self
, dp
):
271 dp
.strokepath
= dp
.path
272 dp
.strokestyles
= self
.styles
276 stroked
.clear
= attr
.clearclass(_stroked
)
279 class _filled(deco
, attr
.exclusiveattr
):
281 """filled is a decorator, which fills the interior of the path"""
283 def __init__(self
, styles
=[]):
284 attr
.exclusiveattr
.__init
__(self
, _filled
)
285 self
.styles
= attr
.mergeattrs(styles
)
286 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
288 def __call__(self
, styles
=[]):
289 # XXX or should we also merge self.styles
290 return _filled(styles
)
292 def decorate(self
, dp
):
293 dp
.fillpath
= dp
.path
294 dp
.fillstyles
= self
.styles
298 filled
.clear
= attr
.clearclass(_filled
)
304 # two helper functions which construct the arrowhead and return its size, respectively
306 def _arrowheadtemplatelength(anormpath
, size
):
307 "calculate length of arrowhead template (in parametrisation of anormpath)"
309 tx
, ty
= anormpath
.begin()
311 # obtain arrow template by using path up to first intersection
312 # with circle around tip (as suggested by Michael Schindler)
313 ipar
= anormpath
.intersect(path
.circle(tx
, ty
, size
))
317 # if this doesn't work, use first order conversion from pts to
318 # the bezier curve's parametrization
319 tlen
= anormpath
.tangent(0).arclength_pt()
321 alen
= unit
.topt(size
)/tlen
322 except ArithmeticError:
323 # take maximum, we can get
324 alen
= anormpath
.range()
325 if alen
> anormpath
.range(): alen
= anormpath
.range()
330 def _arrowhead(anormpath
, size
, angle
, constriction
):
332 """helper routine, which returns an arrowhead for a normpath
334 returns arrowhead at begin of anormpath with size,
335 opening angle and relative constriction
338 alen
= _arrowheadtemplatelength(anormpath
, size
)
339 tx
, ty
= anormpath
.begin()
341 # now we construct the template for our arrow but cutting
342 # the path a the corresponding length
343 arrowtemplate
= anormpath
.split([alen
])[0]
345 # from this template, we construct the two outer curves
347 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
348 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
350 # now come the joining backward parts
352 # arrow with constriction
354 # constriction point (cx, cy) lies on path
355 cx
, cy
= anormpath
.at(constriction
*alen
)
357 arrowcr
= path
.line(*(arrowr
.end()+(cx
,cy
)))
359 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
360 arrow
.append(path
.closepath())
362 # arrow without constriction
363 arrow
= arrowl
.reversed() << arrowr
364 arrow
.append(path
.closepath())
371 class arrow(deco
, attr
.attr
):
373 """arrow is a decorator which adds an arrow to either side of the path"""
375 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
376 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, stroked
, filled
] + attrs
)
377 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
378 self
.position
= position
379 self
.size
= unit
.length(size
, default_type
="v")
381 self
.constriction
= constriction
383 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=None):
387 position
= self
.position
392 if constriction
is None:
393 constriction
= self
.constriction
394 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
396 def decorate(self
, dp
):
397 # XXX raise exception error, when strokepath is not defined
399 # convert to normpath if necessary
400 if isinstance(dp
.strokepath
, path
.normpath
):
401 anormpath
= dp
.strokepath
403 anormpath
= path
.normpath(dp
.path
)
405 anormpath
= anormpath
.reversed()
407 # add arrowhead to decoratedpath
408 dp
.subcanvas
.draw(_arrowhead(anormpath
, self
.size
, self
.angle
, self
.constriction
),
411 # calculate new strokepath
412 alen
= _arrowheadtemplatelength(anormpath
, self
.size
)
413 if self
.constriction
:
414 ilen
= alen
*self
.constriction
418 # correct somewhat for rotation of arrow segments
419 ilen
= ilen
*math
.cos(math
.pi
*self
.angle
/360.0)
421 # this is the rest of the path, we have to draw
422 anormpath
= anormpath
.split([ilen
])[1]
424 # go back to original orientation, if necessary
428 # set the new (shortened) strokepath
429 dp
.strokepath
= anormpath
433 arrow
.clear
= attr
.clearclass(arrow
)
435 # arrows at begin of path
436 barrow
= arrow(position
=0)
437 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
438 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
439 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
440 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
441 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
442 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
443 barrow
.normal
= barrow(size
=_base
)
444 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
445 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
446 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
447 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
448 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
449 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
451 # arrows at end of path
452 earrow
= arrow(position
=1)
453 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
454 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
455 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
456 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
457 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
458 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
459 earrow
.normal
= earrow(size
=_base
)
460 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
461 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
462 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
463 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
464 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
465 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
468 class wriggle(deco
, attr
.attr
):
470 def __init__(self
, skipleft
=1, skipright
=1, radius
=0.5, loops
=8, curvesperloop
=4):
471 self
.skipleft_str
= skipleft
472 self
.skipright_str
= skipright
473 self
.radius_str
= radius
475 self
.curvesperloop
= curvesperloop
477 def decorate(self
, dp
):
478 # XXX: is this the correct way to select the basepath???!!!
479 if isinstance(dp
.strokepath
, path
.normpath
):
480 basepath
= dp
.strokepath
481 elif dp
.strokepath
is not None:
482 basepath
= path
.normpath(dp
.strokepath
)
483 elif isinstance(dp
.path
, path
.normpath
):
486 basepath
= path
.normpath(dp
.path
)
488 skipleft
= unit
.topt(unit
.length(self
.skipleft_str
, default_type
="v"))
489 skipright
= unit
.topt(unit
.length(self
.skipright_str
, default_type
="v"))
490 startpar
, endpar
= basepath
.lentopar(map(unit
.t_pt
, [skipleft
, basepath
.arclength_pt() - skipright
]))
491 radius
= unit
.topt(unit
.length(self
.radius_str
))
493 # search for the first intersection of a circle around start point x, y bigger than startpar
494 x
, y
= basepath
.at_pt(startpar
)
496 for intersectpar
in basepath
.intersect(path
.circle_pt(x
, y
, radius
))[0]:
497 if startpar
< intersectpar
and (startcircpar
is None or startcircpar
> intersectpar
):
498 startcircpar
= intersectpar
499 if startcircpar
is None:
500 raise RuntimeError("couldn't find wriggle start point")
501 # calculate start position and angle
502 xcenter
, ycenter
= basepath
.at_pt(startcircpar
)
503 startpos
= basepath
.split([startcircpar
])[0].arclength_pt()
504 startangle
= math
.atan2(y
-ycenter
, x
-xcenter
)
506 # find the last intersection of a circle around x, y smaller than endpar
507 x
, y
= basepath
.at_pt(endpar
)
509 for intersectpar
in basepath
.intersect(path
.circle_pt(x
, y
, radius
))[0]:
510 if endpar
> intersectpar
and (endcircpar
is None or endcircpar
< intersectpar
):
511 endcircpar
= intersectpar
512 if endcircpar
is None:
513 raise RuntimeError("couldn't find wriggle end point")
514 # calculate end position and angle
515 x2
, y2
= basepath
.at_pt(endcircpar
)
516 endpos
= basepath
.split([endcircpar
])[0].arclength_pt()
517 endangle
= math
.atan2(y
-y2
, x
-x2
)
519 if endangle
< startangle
:
520 endangle
+= 2*math
.pi
522 # calculate basepath points
523 sections
= self
.loops
* self
.curvesperloop
524 posrange
= endpos
- startpos
525 poslist
= [startpos
+ i
*posrange
/sections
for i
in range(sections
+1)]
526 parlist
= basepath
.lentopar(map(unit
.t_pt
, poslist
))
527 atlist
= [basepath
.at_pt(x
) for x
in parlist
]
529 # from pyx import color
531 # dp.subcanvas.stroke(path.circle_pt(at[0], at[1], 1), [color.rgb.blue])
533 # calculate wriggle points and tangents
534 anglerange
= 2*math
.pi
*self
.loops
+ endangle
- startangle
535 deltaangle
= anglerange
/ sections
536 tangentlength
= radius
* 4 * (1 - math
.cos(deltaangle
/2)) / (3 * math
.sin(deltaangle
/2))
537 wriggleat
= [None]*(sections
+1)
538 wriggletangentstart
= [None]*(sections
+1)
539 wriggletangentend
= [None]*(sections
+1)
540 for i
in range(sections
+1):
542 angle
= startangle
+ i
*anglerange
/sections
543 dx
, dy
= math
.cos(angle
), math
.sin(angle
)
544 wriggleat
[i
] = x
+ radius
*dx
, y
+ radius
*dy
545 # dp.subcanvas.stroke(path.line_pt(x, y, x + radius*dx, y + radius*dy), [color.rgb.blue])
546 wriggletangentstart
[i
] = x
+ radius
*dx
+ tangentlength
*dy
, y
+ radius
*dy
- tangentlength
*dx
547 wriggletangentend
[i
] = x
+ radius
*dx
- tangentlength
*dy
, y
+ radius
*dy
+ tangentlength
*dx
550 wrigglepath
= basepath
.split([startpar
])[0]
551 wrigglepath
.append(path
.multicurveto_pt([wriggletangentend
[i
-1] +
552 wriggletangentstart
[i
] +
554 for i
in range(1, sections
+1)]))
555 wrigglepath
= wrigglepath
.glue(basepath
.split([endpar
])[1]) # glue and glued?!?
558 dp
.path
= wrigglepath
# otherwise the bbox is wrong!
559 dp
.strokepath
= wrigglepath
563 class curvecorners(deco
, attr
.attr
):
565 """bends corners in a path
567 curvecorners replaces corners between two lines in a path by an optimized
568 curve that has zero curvature at the connections to the lines.
569 Corners between curves and lines are left as they are."""
571 def __init__(self
, radius
=None, softness
=1):
572 self
.radius
= unit
.topt(radius
)
573 self
.softness
= softness
575 def controlpoints_pt(self
, A
, B
, C
, r1
, r2
, softness
):
576 # Takes three endpoints of two straight lines:
577 # start A, connecting midpoint B, endpoint C
578 # and two radii r1 and r2:
579 # Returns the seven control points of the two bezier curves:
581 # - control points g1 and f1
583 # - control points f2 and g2
587 n
= math
.sqrt(v
[0] * v
[0] + v
[1] * v
[1])
588 return v
[0] / n
, v
[1] / n
589 # make direction vectors d1: from B to A
591 d1
= normed([A
[i
] - B
[i
] for i
in [0,1]])
592 d2
= normed([C
[i
] - B
[i
] for i
in [0,1]])
594 # 0.3192 has turned out to be the maximum softness available
595 # for straight lines ;-)
596 f
= 0.3192 * softness
597 g
= (15.0 * f
+ math
.sqrt(-15.0*f
*f
+ 24.0*f
))/12.0
599 # make the control points
600 f1
= [B
[i
] + f
* r1
* d1
[i
] for i
in [0,1]]
601 f2
= [B
[i
] + f
* r2
* d2
[i
] for i
in [0,1]]
602 g1
= [B
[i
] + g
* r1
* d1
[i
] for i
in [0,1]]
603 g2
= [B
[i
] + g
* r2
* d2
[i
] for i
in [0,1]]
604 d1
= [B
[i
] + r1
* d1
[i
] for i
in [0,1]]
605 d2
= [B
[i
] + r2
* d2
[i
] for i
in [0,1]]
606 e
= [0.5 * (f1
[i
] + f2
[i
]) for i
in [0,1]]
608 return [d1
, g1
, f1
, e
, f2
, g2
, d2
]
610 def decorate(self
, dp
):
611 # XXX: is this the correct way to select the basepath???!!!
612 # compare to wriggle()
613 if isinstance(dp
.strokepath
, path
.normpath
):
614 basepath
= dp
.strokepath
615 elif dp
.strokepath
is not None:
616 basepath
= path
.normpath(dp
.strokepath
)
617 elif isinstance(dp
.path
, path
.normpath
):
620 basepath
= path
.normpath(dp
.path
)
622 newpath
= path
.path()
623 for subpath
in basepath
.subpaths
:
625 # it is not clear yet, where to moveto (e.g. with a closed subpath we
626 # will get the starting point when inserting the bended corner)
627 domoveto
= subpath
.begin_pt()
630 # for a closed subpath we eventually have to bend the initial corner
632 A
= subpath
.normpathels
[-1].begin_pt()
633 previsline
= isinstance(subpath
.normpathels
[-1], path
.normline
)
635 A
= subpath
.begin_pt()
638 # go through the list of normpathels in this subpath
639 for i
in range(len(subpath
.normpathels
)):
640 # XXX: at the moment, we have to build up a path, not a normpath
641 # this should be changed later
642 thispel
= subpath
.normpathels
[i
]
643 prevpel
= subpath
.normpathels
[i
-1]
644 # from this pel: B,C, thisstraight
645 # from previus pel: A, prevstraight
646 B
, C
= thispel
.begin_pt(), thispel
.end_pt()
647 thisisline
= isinstance(thispel
, path
.normline
)
648 if thisisline
and previsline
:
649 d1
,g1
,f1
,e
,f2
,g2
,d2
= self
.controlpoints_pt(A
,B
,C
, self
.radius
, self
.radius
, self
.softness
)
650 if domoveto
is not None:
651 newpath
.append(path
.moveto_pt(d1
[0],d1
[1]))
652 if dolineto
is not None:
653 newpath
.append(path
.lineto_pt(d1
[0],d1
[1]))
654 newpath
.append(path
.curveto_pt(*(g1
+ f1
+ e
)))
655 newpath
.append(path
.curveto_pt(*(f2
+ g2
+ d2
)))
658 if domoveto
is not None:
659 newpath
.append(path
.moveto_pt(*domoveto
))
660 if dolineto
is not None:
661 newpath
.append(path
.lineto_pt(*dolineto
))
662 if isinstance(thispel
, path
.normcurve
):
663 # convert the normcurve to a curveto
664 newpath
.append(path
.curveto_pt(thispel
.x1
,thispel
.y1
,thispel
.x2
,thispel
.y2
,thispel
.x3
,thispel
.y3
))
666 elif isinstance (thispel
, path
.normline
):
667 dolineto
= C
# just store something here which is not None
670 previsline
= thisisline
673 if dolineto
is not None:
674 newpath
.append(path
.lineto_pt(*dolineto
))
676 newpath
.append(path
.closepath())
678 newpath
= path
.normpath(newpath
)
680 dp
.strokepath
= newpath