2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
29 from __future__
import nested_scopes
32 import attr
, canvas
, color
, path
, style
, trafo
, unit
35 from math
import radians
37 # fallback implementation for Python 2.1 and below
38 def radians(x
): return x
*math
.pi
/180
46 class decoratedpath(canvas
.canvasitem
):
49 The main purpose of this class is during the drawing
50 (stroking/filling) of a path. It collects attributes for the
51 stroke and/or fill operations.
54 def __init__(self
, path
, strokepath
=None, fillpath
=None,
55 styles
=None, strokestyles
=None, fillstyles
=None,
60 # global style for stroking and filling and subdps
63 # styles which apply only for stroking and filling
64 self
.strokestyles
= strokestyles
65 self
.fillstyles
= fillstyles
67 # the decoratedpath can contain additional elements of the
68 # path (ornaments), e.g., arrowheads.
70 self
.ornaments
= canvas
.canvas()
72 self
.ornaments
= ornaments
74 self
.nostrokeranges
= None
76 def ensurenormpath(self
):
77 """convert self.path into a normpath"""
78 assert self
.nostrokeranges
is None or isinstance(self
.path
, path
.normpath
), "you don't understand what you are doing"
79 self
.path
= self
.path
.normpath()
81 def excluderange(self
, begin
, end
):
82 assert isinstance(self
.path
, path
.normpath
), "you don't understand what this is about"
83 if self
.nostrokeranges
is None:
84 self
.nostrokeranges
= [(begin
, end
)]
87 while ibegin
< len(self
.nostrokeranges
) and self
.nostrokeranges
[ibegin
][1] < begin
:
90 if ibegin
== len(self
.nostrokeranges
):
91 self
.nostrokeranges
.append((begin
, end
))
94 iend
= len(self
.nostrokeranges
) - 1
95 while 0 <= iend
and end
< self
.nostrokeranges
[iend
][0]:
99 self
.nostrokeranges
.insert(0, (begin
, end
))
102 if self
.nostrokeranges
[ibegin
][0] < begin
:
103 begin
= self
.nostrokeranges
[ibegin
][0]
104 if end
< self
.nostrokeranges
[iend
][1]:
105 end
= self
.nostrokeranges
[iend
][1]
107 self
.nostrokeranges
[ibegin
:iend
+1] = [(begin
, end
)]
110 pathbbox
= self
.path
.bbox()
111 ornamentsbbox
= self
.ornaments
.bbox()
112 if ornamentsbbox
is not None:
113 return ornamentsbbox
+ pathbbox
117 def registerPS(self
, registry
):
119 for style
in self
.styles
:
120 style
.registerPS(registry
)
122 for style
in self
.fillstyles
:
123 style
.registerPS(registry
)
124 if self
.strokestyles
:
125 for style
in self
.strokestyles
:
126 style
.registerPS(registry
)
127 self
.ornaments
.registerPS(registry
)
129 def registerPDF(self
, registry
):
131 for style
in self
.styles
:
132 style
.registerPDF(registry
)
134 for style
in self
.fillstyles
:
135 style
.registerPDF(registry
)
136 if self
.strokestyles
:
137 for style
in self
.strokestyles
:
138 style
.registerPDF(registry
)
139 self
.ornaments
.registerPDF(registry
)
141 def strokepath(self
):
142 if self
.nostrokeranges
:
144 for begin
, end
in self
.nostrokeranges
:
145 splitlist
.append(begin
)
146 splitlist
.append(end
)
147 split
= self
.path
.split(splitlist
)
148 # XXX properly handle closed paths?
150 for i
in range(2, len(split
), 2):
156 def outputPS(self
, file, writer
, context
):
157 # draw (stroke and/or fill) the decoratedpath on the canvas
158 # while trying to produce an efficient output, e.g., by
159 # not writing one path two times
162 def _writestyles(styles
, context
):
164 style
.outputPS(file, writer
, context
)
166 if self
.strokestyles
is None and self
.fillstyles
is None:
167 raise RuntimeError("Path neither to be stroked nor filled")
169 strokepath
= self
.strokepath()
172 # apply global styles
174 file.write("gsave\n")
176 _writestyles(self
.styles
, context
)
178 if self
.fillstyles
is not None:
179 file.write("newpath\n")
180 fillpath
.outputPS(file, writer
, context
)
182 if self
.strokestyles
is not None and strokepath
is fillpath
:
183 # do efficient stroking + filling if respective paths are identical
184 file.write("gsave\n")
187 _writestyles(self
.fillstyles
, context())
190 file.write("grestore\n")
192 if self
.strokestyles
:
193 file.write("gsave\n")
194 _writestyles(self
.strokestyles
, context())
196 file.write("stroke\n")
198 if self
.strokestyles
:
199 file.write("grestore\n")
201 # only fill fillpath - for the moment
203 file.write("gsave\n")
204 _writestyles(self
.fillstyles
, context())
209 file.write("grestore\n")
211 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
212 # this is the only relevant case still left
213 # Note that a possible stroking has already been done.
217 if self
.strokestyles
:
218 file.write("gsave\n")
219 _writestyles(self
.strokestyles
, context
)
221 file.write("newpath\n")
222 strokepath
.outputPS(file, writer
, context
)
223 file.write("stroke\n")
225 if self
.strokestyles
:
226 file.write("grestore\n")
230 # now, draw additional elements of decoratedpath
231 self
.ornaments
.outputPS(file, writer
, context
)
233 # restore global styles
235 file.write("grestore\n")
237 def outputPDF(self
, file, writer
, context
):
238 # draw (stroke and/or fill) the decoratedpath on the canvas
240 def _writestyles(styles
, context
):
242 style
.outputPDF(file, writer
, context
)
244 def _writestrokestyles(strokestyles
, context
):
245 for style
in strokestyles
:
246 style
.outputPDF(file, writer
, context(fillattr
=0))
248 def _writefillstyles(fillstyles
, context
):
249 for style
in fillstyles
:
250 style
.outputPDF(file, writer
, context(strokeattr
=0))
252 if self
.strokestyles
is None and self
.fillstyles
is None:
253 raise RuntimeError("Path neither to be stroked nor filled")
255 strokepath
= self
.strokepath()
258 # apply global styles
260 file.write("q\n") # gsave
262 _writestyles(self
.styles
, context
)
264 if self
.fillstyles
is not None:
265 fillpath
.outputPDF(file, writer
, context
)
267 if self
.strokestyles
is not None and strokepath
is fillpath
:
268 # do efficient stroking + filling
269 file.write("q\n") # gsave
274 _writefillstyles(self
.fillstyles
, context
)
275 if self
.strokestyles
:
276 _writestrokestyles(self
.strokestyles
, context
)
278 file.write("B\n") # both stroke and fill
279 file.write("Q\n") # grestore
282 # only fill fillpath - for the moment
284 file.write("q\n") # gsave
285 _writefillstyles(self
.fillstyles
, context())
287 file.write("f\n") # fill
290 file.write("Q\n") # grestore
292 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
293 # this is the only relevant case still left
294 # Note that a possible stroking has already been done.
298 if self
.strokestyles
:
299 file.write("q\n") # gsave
300 _writestrokestyles(self
.strokestyles
, context
)
302 strokepath
.outputPDF(file, writer
, context
)
303 file.write("S\n") # stroke
305 if self
.strokestyles
:
306 file.write("Q\n") # grestore
309 # now, draw additional elements of decoratedpath
310 self
.ornaments
.outputPDF(file, writer
, context
)
312 # restore global styles
314 file.write("Q\n") # grestore
324 In contrast to path styles, path decorators depend on the concrete
325 path to which they are applied. In particular, they don't make
326 sense without any path and can thus not be used in canvas.set!
330 def decorate(self
, dp
, texrunner
):
331 """apply a style to a given decoratedpath object dp
333 decorate accepts a decoratedpath object dp, applies PathStyle
334 by modifying dp in place.
340 # stroked and filled: basic decos which stroked and fill,
341 # respectively the path
344 class _stroked(deco
, attr
.exclusiveattr
):
346 """stroked is a decorator, which draws the outline of the path"""
348 def __init__(self
, styles
=[]):
349 attr
.exclusiveattr
.__init
__(self
, _stroked
)
350 self
.styles
= attr
.mergeattrs(styles
)
351 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
353 def __call__(self
, styles
=[]):
354 # XXX or should we also merge self.styles
355 return _stroked(styles
)
357 def decorate(self
, dp
, texrunner
):
358 if dp
.strokestyles
is not None:
359 raise RuntimeError("Cannot stroke an already stroked path")
360 dp
.strokestyles
= self
.styles
363 stroked
.clear
= attr
.clearclass(_stroked
)
366 class _filled(deco
, attr
.exclusiveattr
):
368 """filled is a decorator, which fills the interior of the path"""
370 def __init__(self
, styles
=[]):
371 attr
.exclusiveattr
.__init
__(self
, _filled
)
372 self
.styles
= attr
.mergeattrs(styles
)
373 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
375 def __call__(self
, styles
=[]):
376 # XXX or should we also merge self.styles
377 return _filled(styles
)
379 def decorate(self
, dp
, texrunner
):
380 if dp
.fillstyles
is not None:
381 raise RuntimeError("Cannot fill an already filled path")
382 dp
.fillstyles
= self
.styles
385 filled
.clear
= attr
.clearclass(_filled
)
391 # helper function which constructs the arrowhead
393 def _arrowhead(anormpath
, size
, angle
, constrictionlen
, reversed):
395 """helper routine, which returns an arrowhead from a given anormpath
397 returns arrowhead at begin of anormpath with size,
398 opening angle and constriction length constrictionlen. If constrictionlen is None, we
399 do not add a constriction.
403 anormpath
= anormpath
.reversed()
404 alen
= anormpath
.arclentoparam(size
)
405 tx
, ty
= anormpath
.atbegin()
407 # now we construct the template for our arrow but cutting
408 # the path a the corresponding length
409 arrowtemplate
= anormpath
.split(alen
)[0]
411 # from this template, we construct the two outer curves
413 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
414 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
416 # now come the joining backward parts
418 if constrictionlen
is not None:
419 # constriction point (cx, cy) lies on path
420 cx
, cy
= anormpath
.at(anormpath
.arclentoparam(constrictionlen
))
421 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
422 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
424 arrow
= arrowl
.reversed() << arrowr
431 _base
= 6 * unit
.v_pt
433 class arrow(deco
, attr
.attr
):
435 """arrow is a decorator which adds an arrow to either side of the path"""
437 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
438 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
439 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
440 self
.position
= position
443 self
.constriction
= constriction
445 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=_marker
):
449 position
= self
.position
454 if constriction
is _marker
:
455 constriction
= self
.constriction
456 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
458 def decorate(self
, dp
, texrunner
):
462 # calculate absolute arc length of constricition
463 # Note that we have to correct this length because the arrowtemplates are rotated
464 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
465 # self.constriction = 1, we actually have a length which is approximately shorter
466 # by the given geometrical factor.
467 if self
.constriction
is not None:
468 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
470 # if we do not want a constriction, i.e. constriction is None, we still
471 # need constrictionlen for cutting the path
472 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
473 arrowheadconstrictionlen
= None
475 if self
.position
== 0:
476 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=0)
478 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=1)
480 # add arrowhead to decoratedpath
481 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
483 if self
.position
== 0:
484 # exclude first part of the first normsubpath from stroking
485 dp
.excluderange(0, min(self
.size
, constrictionlen
))
487 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
489 arrow
.clear
= attr
.clearclass(arrow
)
491 # arrows at begin of path
492 barrow
= arrow(position
=0)
493 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
494 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
495 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
496 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
497 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
498 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
499 barrow
.normal
= barrow(size
=_base
)
500 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
501 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
502 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
503 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
504 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
505 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
507 # arrows at end of path
508 earrow
= arrow(position
=1)
509 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
510 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
511 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
512 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
513 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
514 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
515 earrow
.normal
= earrow(size
=_base
)
516 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
517 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
518 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
519 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
520 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
521 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
525 class text(deco
, attr
.attr
):
526 """a simple text decorator"""
528 def __init__(self
, text
, textattrs
=[], angle
=0, textdist
=0.2,
529 relarclenpos
=0.5, arclenfrombegin
=None, arclenfromend
=None,
531 if arclenfrombegin
is not None and arclenfromend
is not None:
532 raise ValueError("either set arclenfrombegin or arclenfromend")
534 self
.textattrs
= textattrs
536 self
.textdist
= textdist
537 self
.relarclenpos
= relarclenpos
538 self
.arclenfrombegin
= arclenfrombegin
539 self
.arclenfromend
= arclenfromend
540 self
.texrunner
= texrunner
542 def decorate(self
, dp
, texrunner
):
544 texrunner
= self
.texrunner
545 import text
as textmodule
546 textattrs
= attr
.mergeattrs([textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
] + self
.textattrs
)
548 # note that we cannot call ensurenormpath (the asserts might be a bit too strong)
549 if self
.arclenfrombegin
is not None:
550 x
, y
= dp
.path
.at(dp
.path
.begin() + self
.arclenfrombegin
)
551 elif self
.arclenfromend
is not None:
552 x
, y
= dp
.path
.at(dp
.path
.end() - self
.arclenfromend
)
554 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
555 x
, y
= dp
.path
.at(self
.relarclenpos
* dp
.path
.arclen())
557 t
= texrunner
.text(x
, y
, self
.text
, textattrs
)
558 t
.linealign(self
.textdist
, math
.cos(self
.angle
*math
.pi
/180), math
.sin(self
.angle
*math
.pi
/180))
559 dp
.ornaments
.insert(t
)