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
, normpath
, 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 if not len(self
.ornaments
):
168 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
169 # just draw additional elements of decoratedpath
170 self
.ornaments
.outputPS(file, writer
, context
)
173 strokepath
= self
.strokepath()
176 # apply global styles
178 file.write("gsave\n")
180 _writestyles(self
.styles
, context
)
182 if self
.fillstyles
is not None:
183 file.write("newpath\n")
184 fillpath
.outputPS(file, writer
, context
)
186 if self
.strokestyles
is not None and strokepath
is fillpath
:
187 # do efficient stroking + filling if respective paths are identical
188 file.write("gsave\n")
191 _writestyles(self
.fillstyles
, context())
194 file.write("grestore\n")
196 if self
.strokestyles
:
197 file.write("gsave\n")
198 _writestyles(self
.strokestyles
, context())
200 file.write("stroke\n")
202 if self
.strokestyles
:
203 file.write("grestore\n")
205 # only fill fillpath - for the moment
207 file.write("gsave\n")
208 _writestyles(self
.fillstyles
, context())
213 file.write("grestore\n")
215 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
216 # this is the only relevant case still left
217 # Note that a possible stroking has already been done.
221 if self
.strokestyles
:
222 file.write("gsave\n")
223 _writestyles(self
.strokestyles
, context
)
225 file.write("newpath\n")
226 strokepath
.outputPS(file, writer
, context
)
227 file.write("stroke\n")
229 if self
.strokestyles
:
230 file.write("grestore\n")
234 # now, draw additional elements of decoratedpath
235 self
.ornaments
.outputPS(file, writer
, context
)
237 # restore global styles
239 file.write("grestore\n")
241 def outputPDF(self
, file, writer
, context
):
242 # draw (stroke and/or fill) the decoratedpath on the canvas
244 def _writestyles(styles
, context
):
246 style
.outputPDF(file, writer
, context
)
248 def _writestrokestyles(strokestyles
, context
):
249 for style
in strokestyles
:
250 style
.outputPDF(file, writer
, context(fillattr
=0))
252 def _writefillstyles(fillstyles
, context
):
253 for style
in fillstyles
:
254 style
.outputPDF(file, writer
, context(strokeattr
=0))
256 if self
.strokestyles
is None and self
.fillstyles
is None:
257 if not len(self
.ornaments
):
258 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
259 # just draw additional elements of decoratedpath
260 self
.ornaments
.outputPDF(file, writer
, context
)
263 strokepath
= self
.strokepath()
266 # apply global styles
268 file.write("q\n") # gsave
270 _writestyles(self
.styles
, context
)
272 if self
.fillstyles
is not None:
273 fillpath
.outputPDF(file, writer
, context
)
275 if self
.strokestyles
is not None and strokepath
is fillpath
:
276 # do efficient stroking + filling
277 file.write("q\n") # gsave
282 _writefillstyles(self
.fillstyles
, context
)
283 if self
.strokestyles
:
284 _writestrokestyles(self
.strokestyles
, context
)
286 file.write("B\n") # both stroke and fill
287 file.write("Q\n") # grestore
290 # only fill fillpath - for the moment
292 file.write("q\n") # gsave
293 _writefillstyles(self
.fillstyles
, context())
295 file.write("f\n") # fill
298 file.write("Q\n") # grestore
300 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
301 # this is the only relevant case still left
302 # Note that a possible stroking has already been done.
306 if self
.strokestyles
:
307 file.write("q\n") # gsave
308 _writestrokestyles(self
.strokestyles
, context
)
310 strokepath
.outputPDF(file, writer
, context
)
311 file.write("S\n") # stroke
313 if self
.strokestyles
:
314 file.write("Q\n") # grestore
317 # now, draw additional elements of decoratedpath
318 self
.ornaments
.outputPDF(file, writer
, context
)
320 # restore global styles
322 file.write("Q\n") # grestore
332 In contrast to path styles, path decorators depend on the concrete
333 path to which they are applied. In particular, they don't make
334 sense without any path and can thus not be used in canvas.set!
338 def decorate(self
, dp
, texrunner
):
339 """apply a style to a given decoratedpath object dp
341 decorate accepts a decoratedpath object dp, applies PathStyle
342 by modifying dp in place.
348 # stroked and filled: basic decos which stroked and fill,
349 # respectively the path
352 class _stroked(deco
, attr
.exclusiveattr
):
354 """stroked is a decorator, which draws the outline of the path"""
356 def __init__(self
, styles
=[]):
357 attr
.exclusiveattr
.__init
__(self
, _stroked
)
358 self
.styles
= attr
.mergeattrs(styles
)
359 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
361 def __call__(self
, styles
=[]):
362 # XXX or should we also merge self.styles
363 return _stroked(styles
)
365 def decorate(self
, dp
, texrunner
):
366 if dp
.strokestyles
is not None:
367 raise RuntimeError("Cannot stroke an already stroked path")
368 dp
.strokestyles
= self
.styles
371 stroked
.clear
= attr
.clearclass(_stroked
)
374 class _filled(deco
, attr
.exclusiveattr
):
376 """filled is a decorator, which fills the interior of the path"""
378 def __init__(self
, styles
=[]):
379 attr
.exclusiveattr
.__init
__(self
, _filled
)
380 self
.styles
= attr
.mergeattrs(styles
)
381 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
383 def __call__(self
, styles
=[]):
384 # XXX or should we also merge self.styles
385 return _filled(styles
)
387 def decorate(self
, dp
, texrunner
):
388 if dp
.fillstyles
is not None:
389 raise RuntimeError("Cannot fill an already filled path")
390 dp
.fillstyles
= self
.styles
393 filled
.clear
= attr
.clearclass(_filled
)
399 # helper function which constructs the arrowhead
401 def _arrowhead(anormpath
, arclenfrombegin
, direction
, size
, angle
, constrictionlen
):
403 """helper routine, which returns an arrowhead from a given anormpath
405 - arclenfrombegin: position of arrow in arc length from the start of the path
406 - direction: +1 for an arrow pointing along the direction of anormpath or
407 -1 for an arrow pointing opposite to the direction of normpath
408 - size: size of the arrow as arc length
409 - angle. opening angle
410 - constrictionlen: None (no constriction) or arc length of constriction.
413 # arc length and coordinates of tip
414 tx
, ty
= anormpath
.at(arclenfrombegin
)
416 # construct the template for the arrow by cutting the path at the
417 # corresponding length
418 arrowtemplate
= anormpath
.split([arclenfrombegin
, arclenfrombegin
- direction
* size
])[1]
420 # from this template, we construct the two outer curves of the arrow
421 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
422 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
424 # now come the joining backward parts
425 if constrictionlen
is not None:
426 # constriction point (cx, cy) lies on path
427 cx
, cy
= anormpath
.at(arclenfrombegin
- direction
* constrictionlen
)
428 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
429 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
431 arrow
= arrowl
.reversed() << arrowr
438 _base
= 6 * unit
.v_pt
440 class arrow(deco
, attr
.attr
):
442 """arrow is a decorator which adds an arrow to either side of the path"""
444 def __init__(self
, attrs
=[], pos
=1, reversed=0, size
=_base
, angle
=45, constriction
=0.8):
445 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
446 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
448 self
.reversed = reversed
451 self
.constriction
= constriction
453 def __call__(self
, attrs
=None, pos
=None, reversed=None, size
=None, angle
=None, constriction
=_marker
):
459 reversed = self
.reversed
464 if constriction
is _marker
:
465 constriction
= self
.constriction
466 return arrow(attrs
=attrs
, pos
=pos
, reversed=reversed, size
=size
, angle
=angle
, constriction
=constriction
)
468 def decorate(self
, dp
, texrunner
):
472 # calculate absolute arc length of constricition
473 # Note that we have to correct this length because the arrowtemplates are rotated
474 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
475 # self.constriction = 1, we actually have a length which is approximately shorter
476 # by the given geometrical factor.
477 if self
.constriction
is not None:
478 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
480 # if we do not want a constriction, i.e. constriction is None, we still
481 # need constrictionlen for cutting the path
482 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
483 arrowheadconstrictionlen
= None
485 arclenfrombegin
= self
.pos
* anormpath
.arclen()
486 direction
= self
.reversed and -1 or 1
487 arrowhead
= _arrowhead(anormpath
, arclenfrombegin
, direction
, self
.size
, self
.angle
, arrowheadconstrictionlen
)
489 # add arrowhead to decoratedpath
490 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
492 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
493 if self
.pos
== 0 and self
.reversed:
494 dp
.excluderange(0, min(self
.size
, constrictionlen
))
495 elif self
.pos
== 1 and not self
.reversed:
496 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
498 arrow
.clear
= attr
.clearclass(arrow
)
500 # arrows at begin of path
501 barrow
= arrow(pos
=0, reversed=1)
502 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
503 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
504 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
505 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
506 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
507 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
508 barrow
.normal
= barrow(size
=_base
)
509 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
510 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
511 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
512 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
513 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
514 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
516 # arrows at end of path
518 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
519 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
520 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
521 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
522 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
523 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
524 earrow
.normal
= earrow(size
=_base
)
525 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
526 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
527 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
528 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
529 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
530 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
533 class text(deco
, attr
.attr
):
534 """a simple text decorator"""
536 def __init__(self
, text
, textattrs
=[], angle
=0, textdist
=0.2,
537 relarclenpos
=0.5, arclenfrombegin
=None, arclenfromend
=None,
539 if arclenfrombegin
is not None and arclenfromend
is not None:
540 raise ValueError("either set arclenfrombegin or arclenfromend")
542 self
.textattrs
= textattrs
544 self
.textdist
= textdist
545 self
.relarclenpos
= relarclenpos
546 self
.arclenfrombegin
= arclenfrombegin
547 self
.arclenfromend
= arclenfromend
548 self
.texrunner
= texrunner
550 def decorate(self
, dp
, texrunner
):
552 texrunner
= self
.texrunner
553 import text
as textmodule
554 textattrs
= attr
.mergeattrs([textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
] + self
.textattrs
)
557 if self
.arclenfrombegin
is not None:
558 x
, y
= dp
.path
.at(dp
.path
.begin() + self
.arclenfrombegin
)
559 elif self
.arclenfromend
is not None:
560 x
, y
= dp
.path
.at(dp
.path
.end() - self
.arclenfromend
)
562 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
563 x
, y
= dp
.path
.at(self
.relarclenpos
* dp
.path
.arclen())
565 t
= texrunner
.text(x
, y
, self
.text
, textattrs
)
566 t
.linealign(self
.textdist
, math
.cos(self
.angle
*math
.pi
/180), math
.sin(self
.angle
*math
.pi
/180))
567 dp
.ornaments
.insert(t
)
570 class shownormpath(deco
, attr
.attr
):
572 def decorate(self
, dp
, texrunner
):
575 for normsubpath
in dp
.path
.normsubpaths
:
576 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
577 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
578 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.green
])
580 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.blue
])
581 for normsubpath
in dp
.path
.normsubpaths
:
582 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
583 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
584 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x0_pt
, normsubpathitem
.y0_pt
, normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
585 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, normsubpathitem
.x3_pt
, normsubpathitem
.y3_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
586 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
, r_pt
), [filled([color
.rgb
.red
])])
587 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, r_pt
), [filled([color
.rgb
.red
])])
588 for normsubpath
in dp
.path
.normsubpaths
:
589 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
591 x_pt
, y_pt
= normsubpathitem
.atbegin_pt()
592 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])
593 x_pt
, y_pt
= normsubpathitem
.atend_pt()
594 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])