1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 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
25 # - should we improve on the arc length -> arg parametrization routine or
26 # should we at least factor it out?
28 from __future__
import nested_scopes
31 import attr
, canvas
, color
, path
, normpath
, style
, trafo
, type1font
, unit
34 from math
import radians
36 # fallback implementation for Python 2.1 and below
37 def radians(x
): return x
*math
.pi
/180
45 class decoratedpath(canvas
.canvasitem
):
48 The main purpose of this class is during the drawing
49 (stroking/filling) of a path. It collects attributes for the
50 stroke and/or fill operations.
53 def __init__(self
, path
, strokepath
=None, fillpath
=None,
54 styles
=None, strokestyles
=None, fillstyles
=None,
59 # global style for stroking and filling and subdps
62 # styles which apply only for stroking and filling
63 self
.strokestyles
= strokestyles
64 self
.fillstyles
= fillstyles
66 # the decoratedpath can contain additional elements of the
67 # path (ornaments), e.g., arrowheads.
69 self
.ornaments
= canvas
.canvas()
71 self
.ornaments
= ornaments
73 self
.nostrokeranges
= None
75 def ensurenormpath(self
):
76 """convert self.path into a normpath"""
77 assert self
.nostrokeranges
is None or isinstance(self
.path
, path
.normpath
), "you don't understand what you are doing"
78 self
.path
= self
.path
.normpath()
80 def excluderange(self
, begin
, end
):
81 assert isinstance(self
.path
, path
.normpath
), "you don't understand what this is about"
82 if self
.nostrokeranges
is None:
83 self
.nostrokeranges
= [(begin
, end
)]
86 while ibegin
< len(self
.nostrokeranges
) and self
.nostrokeranges
[ibegin
][1] < begin
:
89 if ibegin
== len(self
.nostrokeranges
):
90 self
.nostrokeranges
.append((begin
, end
))
93 iend
= len(self
.nostrokeranges
) - 1
94 while 0 <= iend
and end
< self
.nostrokeranges
[iend
][0]:
98 self
.nostrokeranges
.insert(0, (begin
, end
))
101 if self
.nostrokeranges
[ibegin
][0] < begin
:
102 begin
= self
.nostrokeranges
[ibegin
][0]
103 if end
< self
.nostrokeranges
[iend
][1]:
104 end
= self
.nostrokeranges
[iend
][1]
106 self
.nostrokeranges
[ibegin
:iend
+1] = [(begin
, end
)]
109 pathbbox
= self
.path
.bbox()
110 ornamentsbbox
= self
.ornaments
.bbox()
111 if ornamentsbbox
is not None:
112 return ornamentsbbox
+ pathbbox
116 def strokepath(self
):
117 if self
.nostrokeranges
:
119 for begin
, end
in self
.nostrokeranges
:
120 splitlist
.append(begin
)
121 splitlist
.append(end
)
122 split
= self
.path
.split(splitlist
)
123 # XXX properly handle closed paths?
125 for i
in range(2, len(split
), 2):
131 def processPS(self
, file, writer
, context
, registry
, bbox
):
132 # draw (stroke and/or fill) the decoratedpath on the canvas
133 # while trying to produce an efficient output, e.g., by
134 # not writing one path two times
137 def _writestyles(styles
, context
, registry
, bbox
):
139 style
.processPS(file, writer
, context
, registry
, bbox
)
141 if self
.strokestyles
is None and self
.fillstyles
is None:
142 if not len(self
.ornaments
):
143 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
144 # just draw additional elements of decoratedpath
145 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
148 strokepath
= self
.strokepath()
151 # apply global styles
153 file.write("gsave\n")
155 _writestyles(self
.styles
, context
, registry
, bbox
)
157 if self
.fillstyles
is not None:
158 file.write("newpath\n")
159 fillpath
.outputPS(file, writer
)
161 if self
.strokestyles
is not None and strokepath
is fillpath
:
162 # do efficient stroking + filling if respective paths are identical
163 file.write("gsave\n")
166 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
169 file.write("grestore\n")
172 if self
.strokestyles
:
173 file.write("gsave\n")
174 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
176 file.write("stroke\n")
177 # take linewidth into account for bbox when stroking a path
178 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
180 if self
.strokestyles
:
181 file.write("grestore\n")
183 # only fill fillpath - for the moment
185 file.write("gsave\n")
186 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
189 bbox
+= fillpath
.bbox()
192 file.write("grestore\n")
194 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
195 # this is the only relevant case still left
196 # Note that a possible stroking has already been done.
198 if self
.strokestyles
:
199 file.write("gsave\n")
200 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
202 file.write("newpath\n")
203 strokepath
.outputPS(file, writer
)
204 file.write("stroke\n")
205 # take linewidth into account for bbox when stroking a path
206 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
208 if self
.strokestyles
:
209 file.write("grestore\n")
211 # now, draw additional elements of decoratedpath
212 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
214 # restore global styles
216 file.write("grestore\n")
218 def processPDF(self
, file, writer
, context
, registry
, bbox
):
219 # draw (stroke and/or fill) the decoratedpath on the canvas
221 def _writestyles(styles
, context
, registry
, bbox
):
223 style
.processPDF(file, writer
, context
, registry
, bbox
)
225 def _writestrokestyles(strokestyles
, context
, registry
, bbox
):
227 for style
in strokestyles
:
228 style
.processPDF(file, writer
, context
, registry
, bbox
)
231 def _writefillstyles(fillstyles
, context
, registry
, bbox
):
232 context
.strokeattr
= 0
233 for style
in fillstyles
:
234 style
.processPDF(file, writer
, context
, registry
, bbox
)
235 context
.strokeattr
= 1
237 if self
.strokestyles
is None and self
.fillstyles
is None:
238 if not len(self
.ornaments
):
239 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
240 # just draw additional elements of decoratedpath
241 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
244 strokepath
= self
.strokepath()
247 # apply global styles
249 file.write("q\n") # gsave
251 _writestyles(self
.styles
, context
, registry
, bbox
)
253 if self
.fillstyles
is not None:
254 fillpath
.outputPDF(file, writer
)
256 if self
.strokestyles
is not None and strokepath
is fillpath
:
257 # do efficient stroking + filling
258 file.write("q\n") # gsave
262 _writefillstyles(self
.fillstyles
, acontext
, registry
, bbox
)
263 if self
.strokestyles
:
264 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
266 file.write("B\n") # both stroke and fill
267 # take linewidth into account for bbox when stroking a path
268 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
270 file.write("Q\n") # grestore
272 # only fill fillpath - for the moment
274 file.write("q\n") # gsave
275 _writefillstyles(self
.fillstyles
, context(), registry
, bbox
)
277 file.write("f\n") # fill
278 bbox
+= fillpath
.bbox()
281 file.write("Q\n") # grestore
283 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
284 # this is the only relevant case still left
285 # Note that a possible stroking has already been done.
288 if self
.strokestyles
:
289 file.write("q\n") # gsave
290 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
292 strokepath
.outputPDF(file, writer
)
293 file.write("S\n") # stroke
294 # take linewidth into account for bbox when stroking a path
295 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
297 if self
.strokestyles
:
298 file.write("Q\n") # grestore
300 # now, draw additional elements of decoratedpath
301 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
303 # restore global styles
305 file.write("Q\n") # grestore
315 In contrast to path styles, path decorators depend on the concrete
316 path to which they are applied. In particular, they don't make
317 sense without any path and can thus not be used in canvas.set!
321 def decorate(self
, dp
, texrunner
):
322 """apply a style to a given decoratedpath object dp
324 decorate accepts a decoratedpath object dp, applies PathStyle
325 by modifying dp in place.
331 # stroked and filled: basic decos which stroked and fill,
332 # respectively the path
335 class _stroked(deco
, attr
.exclusiveattr
):
337 """stroked is a decorator, which draws the outline of the path"""
339 def __init__(self
, styles
=[]):
340 attr
.exclusiveattr
.__init
__(self
, _stroked
)
341 self
.styles
= attr
.mergeattrs(styles
)
342 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
344 def __call__(self
, styles
=[]):
345 # XXX or should we also merge self.styles
346 return _stroked(styles
)
348 def decorate(self
, dp
, texrunner
):
349 if dp
.strokestyles
is not None:
350 raise RuntimeError("Cannot stroke an already stroked path")
351 dp
.strokestyles
= self
.styles
354 stroked
.clear
= attr
.clearclass(_stroked
)
357 class _filled(deco
, attr
.exclusiveattr
):
359 """filled is a decorator, which fills the interior of the path"""
361 def __init__(self
, styles
=[]):
362 attr
.exclusiveattr
.__init
__(self
, _filled
)
363 self
.styles
= attr
.mergeattrs(styles
)
364 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
366 def __call__(self
, styles
=[]):
367 # XXX or should we also merge self.styles
368 return _filled(styles
)
370 def decorate(self
, dp
, texrunner
):
371 if dp
.fillstyles
is not None:
372 raise RuntimeError("Cannot fill an already filled path")
373 dp
.fillstyles
= self
.styles
376 filled
.clear
= attr
.clearclass(_filled
)
382 # helper function which constructs the arrowhead
384 def _arrowhead(anormpath
, arclenfrombegin
, direction
, size
, angle
, constrictionlen
):
386 """helper routine, which returns an arrowhead from a given anormpath
388 - arclenfrombegin: position of arrow in arc length from the start of the path
389 - direction: +1 for an arrow pointing along the direction of anormpath or
390 -1 for an arrow pointing opposite to the direction of normpath
391 - size: size of the arrow as arc length
392 - angle. opening angle
393 - constrictionlen: None (no constriction) or arc length of constriction.
396 # arc length and coordinates of tip
397 tx
, ty
= anormpath
.at(arclenfrombegin
)
399 # construct the template for the arrow by cutting the path at the
400 # corresponding length
401 arrowtemplate
= anormpath
.split([arclenfrombegin
, arclenfrombegin
- direction
* size
])[1]
403 # from this template, we construct the two outer curves of the arrow
404 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
405 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
407 # now come the joining backward parts
408 if constrictionlen
is not None:
409 # constriction point (cx, cy) lies on path
410 cx
, cy
= anormpath
.at(arclenfrombegin
- direction
* constrictionlen
)
411 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
412 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
414 arrow
= arrowl
.reversed() << arrowr
421 _base
= 6 * unit
.v_pt
423 class arrow(deco
, attr
.attr
):
425 """arrow is a decorator which adds an arrow to either side of the path"""
427 def __init__(self
, attrs
=[], pos
=1, reversed=0, size
=_base
, angle
=45, constriction
=0.8):
428 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
429 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
431 self
.reversed = reversed
434 self
.constriction
= constriction
436 def __call__(self
, attrs
=None, pos
=None, reversed=None, size
=None, angle
=None, constriction
=_marker
):
442 reversed = self
.reversed
447 if constriction
is _marker
:
448 constriction
= self
.constriction
449 return arrow(attrs
=attrs
, pos
=pos
, reversed=reversed, size
=size
, angle
=angle
, constriction
=constriction
)
451 def decorate(self
, dp
, texrunner
):
455 # calculate absolute arc length of constricition
456 # Note that we have to correct this length because the arrowtemplates are rotated
457 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
458 # self.constriction = 1, we actually have a length which is approximately shorter
459 # by the given geometrical factor.
460 if self
.constriction
is not None:
461 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
463 # if we do not want a constriction, i.e. constriction is None, we still
464 # need constrictionlen for cutting the path
465 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
466 arrowheadconstrictionlen
= None
468 arclenfrombegin
= self
.pos
* anormpath
.arclen()
469 direction
= self
.reversed and -1 or 1
470 arrowhead
= _arrowhead(anormpath
, arclenfrombegin
, direction
, self
.size
, self
.angle
, arrowheadconstrictionlen
)
472 # add arrowhead to decoratedpath
473 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
475 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
476 if self
.pos
== 0 and self
.reversed:
477 dp
.excluderange(0, min(self
.size
, constrictionlen
))
478 elif self
.pos
== 1 and not self
.reversed:
479 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
481 arrow
.clear
= attr
.clearclass(arrow
)
483 # arrows at begin of path
484 barrow
= arrow(pos
=0, reversed=1)
485 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
486 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
487 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
488 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
489 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
490 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
491 barrow
.normal
= barrow(size
=_base
)
492 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
493 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
494 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
495 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
496 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
497 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
499 # arrows at end of path
501 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
502 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
503 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
504 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
505 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
506 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
507 earrow
.normal
= earrow(size
=_base
)
508 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
509 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
510 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
511 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
512 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
513 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
516 class text(deco
, attr
.attr
):
517 """a simple text decorator"""
519 def __init__(self
, text
, textattrs
=[], angle
=0, textdist
=0.2,
520 relarclenpos
=0.5, arclenfrombegin
=None, arclenfromend
=None,
522 if arclenfrombegin
is not None and arclenfromend
is not None:
523 raise ValueError("either set arclenfrombegin or arclenfromend")
525 self
.textattrs
= textattrs
527 self
.textdist
= textdist
528 self
.relarclenpos
= relarclenpos
529 self
.arclenfrombegin
= arclenfrombegin
530 self
.arclenfromend
= arclenfromend
531 self
.texrunner
= texrunner
533 def decorate(self
, dp
, texrunner
):
535 texrunner
= self
.texrunner
536 import text
as textmodule
537 textattrs
= attr
.mergeattrs([textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
] + self
.textattrs
)
540 if self
.arclenfrombegin
is not None:
541 x
, y
= dp
.path
.at(dp
.path
.begin() + self
.arclenfrombegin
)
542 elif self
.arclenfromend
is not None:
543 x
, y
= dp
.path
.at(dp
.path
.end() - self
.arclenfromend
)
545 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
546 x
, y
= dp
.path
.at(self
.relarclenpos
* dp
.path
.arclen())
548 t
= texrunner
.text(x
, y
, self
.text
, textattrs
)
549 t
.linealign(self
.textdist
, math
.cos(self
.angle
*math
.pi
/180), math
.sin(self
.angle
*math
.pi
/180))
550 dp
.ornaments
.insert(t
)
552 class curvedtext(deco
, attr
.attr
):
553 """a text decorator for curved text
555 - text: is typeset along the path to which this decorator is applied
556 - relarclenpos: position for the base point of the text (default: 0)
557 - arlenfrombegin, arclenfromend: alternative ways of specifying the position of the base point;
558 use of relarclenpos, arclenfrombegin and arclenfromend is mutually exclusive
559 - textattrs, texrunner: standard text arguments (defaults: [] resp None)
563 def __init__(self
, text
, textattrs
=[],
564 relarclenpos
=0, arclenfrombegin
=None, arclenfromend
=None,
566 if arclenfrombegin
is not None and arclenfromend
is not None:
567 raise ValueError("either set arclenfrombegin or arclenfromend")
569 self
.textattrs
= textattrs
570 self
.relarclenpos
= relarclenpos
571 self
.arclenfrombegin
= arclenfrombegin
572 self
.arclenfromend
= arclenfromend
573 self
.texrunner
= texrunner
575 def decorate(self
, dp
, texrunner
):
577 texrunner
= self
.texrunner
578 import text
as textmodule
581 if self
.arclenfrombegin
is not None:
582 textpos
= dp
.path
.begin() + self
.arclenfrombegin
583 elif self
.arclenfromend
is not None:
584 textpos
= dp
.path
.end() - self
.arclenfromend
586 # relarcpos is used if neither arcfrombegin nor arcfromend is given
587 textpos
= self
.relarclenpos
* dp
.path
.arclen()
591 singlecharmode
=texrunner
.singlecharmode
# usually 0
592 texrunner
.singlecharmode
=1
593 t
= texrunner
.text(0, 0, self
.text
, self
.textattrs
)
595 # copy over attr ops (colour...)
596 # isinstance(op, canvas._canvas) should not occur before ensuredvicanvas; should we even care to check?
597 [ c
.insert(op
) for op
in t
.items
if not isinstance(op
, canvas
._canvas
)]
600 texrunner
.singlecharmode
=singlecharmode
602 items
= t
.dvicanvas
.items
603 xs
= [item
.bbox().center()[0] for item
in items
]
604 trafos
= dp
.path
.trafo([textpos
+x
for x
in xs
])
605 for x
, op
, atrafo
in zip(xs
, items
, trafos
):
606 c
.insert(op
, [trafo
.translate(-x
, 0), atrafo
]) # reversed trafos: fix for change in canvas.py from r2728 to 2730
608 dp
.ornaments
.insert(c
)
612 class shownormpath(deco
, attr
.attr
):
614 def decorate(self
, dp
, texrunner
):
617 for normsubpath
in dp
.path
.normsubpaths
:
618 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
619 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
620 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.green
])
622 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.blue
])
623 for normsubpath
in dp
.path
.normsubpaths
:
624 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
625 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
626 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x0_pt
, normsubpathitem
.y0_pt
, normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
627 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, normsubpathitem
.x3_pt
, normsubpathitem
.y3_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
628 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
, r_pt
), [filled([color
.rgb
.red
])])
629 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, r_pt
), [filled([color
.rgb
.red
])])
630 for normsubpath
in dp
.path
.normsubpaths
:
631 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
633 x_pt
, y_pt
= normsubpathitem
.atbegin_pt()
634 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])
635 x_pt
, y_pt
= normsubpathitem
.atend_pt()
636 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])