2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2006 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 strokepath(self
):
118 if self
.nostrokeranges
:
120 for begin
, end
in self
.nostrokeranges
:
121 splitlist
.append(begin
)
122 splitlist
.append(end
)
123 split
= self
.path
.split(splitlist
)
124 # XXX properly handle closed paths?
126 for i
in range(2, len(split
), 2):
132 def processPS(self
, file, writer
, context
, registry
, bbox
):
133 # draw (stroke and/or fill) the decoratedpath on the canvas
134 # while trying to produce an efficient output, e.g., by
135 # not writing one path two times
138 def _writestyles(styles
, context
, registry
, bbox
):
140 style
.processPS(file, writer
, context
, registry
, bbox
)
142 if self
.strokestyles
is None and self
.fillstyles
is None:
143 if not len(self
.ornaments
):
144 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
145 # just draw additional elements of decoratedpath
146 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
149 strokepath
= self
.strokepath()
152 # apply global styles
154 file.write("gsave\n")
156 _writestyles(self
.styles
, context
, registry
, bbox
)
158 if self
.fillstyles
is not None:
159 file.write("newpath\n")
160 fillpath
.outputPS(file, writer
)
162 if self
.strokestyles
is not None and strokepath
is fillpath
:
163 # do efficient stroking + filling if respective paths are identical
164 file.write("gsave\n")
167 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
170 file.write("grestore\n")
173 if self
.strokestyles
:
174 file.write("gsave\n")
175 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
177 file.write("stroke\n")
178 # take linewidth into account for bbox when stroking a path
179 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
181 if self
.strokestyles
:
182 file.write("grestore\n")
184 # only fill fillpath - for the moment
186 file.write("gsave\n")
187 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
190 bbox
+= fillpath
.bbox()
193 file.write("grestore\n")
195 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
196 # this is the only relevant case still left
197 # Note that a possible stroking has already been done.
199 if self
.strokestyles
:
200 file.write("gsave\n")
201 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
203 file.write("newpath\n")
204 strokepath
.outputPS(file, writer
)
205 file.write("stroke\n")
206 # take linewidth into account for bbox when stroking a path
207 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
209 if self
.strokestyles
:
210 file.write("grestore\n")
212 # now, draw additional elements of decoratedpath
213 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
215 # restore global styles
217 file.write("grestore\n")
219 def processPDF(self
, file, writer
, context
, registry
, bbox
):
220 # draw (stroke and/or fill) the decoratedpath on the canvas
222 def _writestyles(styles
, context
, registry
, bbox
):
224 style
.processPDF(file, writer
, context
, registry
, bbox
)
226 def _writestrokestyles(strokestyles
, context
, registry
, bbox
):
228 for style
in strokestyles
:
229 style
.processPDF(file, writer
, context
, registry
, bbox
)
232 def _writefillstyles(fillstyles
, context
, registry
, bbox
):
233 context
.strokeattr
= 0
234 for style
in fillstyles
:
235 style
.processPDF(file, writer
, context
, registry
, bbox
)
236 context
.strokeattr
= 1
238 if self
.strokestyles
is None and self
.fillstyles
is None:
239 if not len(self
.ornaments
):
240 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
241 # just draw additional elements of decoratedpath
242 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
245 strokepath
= self
.strokepath()
248 # apply global styles
250 file.write("q\n") # gsave
252 _writestyles(self
.styles
, context
, registry
, bbox
)
254 if self
.fillstyles
is not None:
255 fillpath
.outputPDF(file, writer
)
257 if self
.strokestyles
is not None and strokepath
is fillpath
:
258 # do efficient stroking + filling
259 file.write("q\n") # gsave
263 _writefillstyles(self
.fillstyles
, acontext
, registry
, bbox
)
264 if self
.strokestyles
:
265 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
267 file.write("B\n") # both stroke and fill
268 # take linewidth into account for bbox when stroking a path
269 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
271 file.write("Q\n") # grestore
273 # only fill fillpath - for the moment
275 file.write("q\n") # gsave
276 _writefillstyles(self
.fillstyles
, context(), registry
, bbox
)
278 file.write("f\n") # fill
279 bbox
+= fillpath
.bbox()
282 file.write("Q\n") # grestore
284 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
285 # this is the only relevant case still left
286 # Note that a possible stroking has already been done.
289 if self
.strokestyles
:
290 file.write("q\n") # gsave
291 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
293 strokepath
.outputPDF(file, writer
)
294 file.write("S\n") # stroke
295 # take linewidth into account for bbox when stroking a path
296 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
298 if self
.strokestyles
:
299 file.write("Q\n") # grestore
301 # now, draw additional elements of decoratedpath
302 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
304 # restore global styles
306 file.write("Q\n") # grestore
316 In contrast to path styles, path decorators depend on the concrete
317 path to which they are applied. In particular, they don't make
318 sense without any path and can thus not be used in canvas.set!
322 def decorate(self
, dp
, texrunner
):
323 """apply a style to a given decoratedpath object dp
325 decorate accepts a decoratedpath object dp, applies PathStyle
326 by modifying dp in place.
332 # stroked and filled: basic decos which stroked and fill,
333 # respectively the path
336 class _stroked(deco
, attr
.exclusiveattr
):
338 """stroked is a decorator, which draws the outline of the path"""
340 def __init__(self
, styles
=[]):
341 attr
.exclusiveattr
.__init
__(self
, _stroked
)
342 self
.styles
= attr
.mergeattrs(styles
)
343 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
345 def __call__(self
, styles
=[]):
346 # XXX or should we also merge self.styles
347 return _stroked(styles
)
349 def decorate(self
, dp
, texrunner
):
350 if dp
.strokestyles
is not None:
351 raise RuntimeError("Cannot stroke an already stroked path")
352 dp
.strokestyles
= self
.styles
355 stroked
.clear
= attr
.clearclass(_stroked
)
358 class _filled(deco
, attr
.exclusiveattr
):
360 """filled is a decorator, which fills the interior of the path"""
362 def __init__(self
, styles
=[]):
363 attr
.exclusiveattr
.__init
__(self
, _filled
)
364 self
.styles
= attr
.mergeattrs(styles
)
365 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
367 def __call__(self
, styles
=[]):
368 # XXX or should we also merge self.styles
369 return _filled(styles
)
371 def decorate(self
, dp
, texrunner
):
372 if dp
.fillstyles
is not None:
373 raise RuntimeError("Cannot fill an already filled path")
374 dp
.fillstyles
= self
.styles
377 filled
.clear
= attr
.clearclass(_filled
)
383 # helper function which constructs the arrowhead
385 def _arrowhead(anormpath
, arclenfrombegin
, direction
, size
, angle
, constrictionlen
):
387 """helper routine, which returns an arrowhead from a given anormpath
389 - arclenfrombegin: position of arrow in arc length from the start of the path
390 - direction: +1 for an arrow pointing along the direction of anormpath or
391 -1 for an arrow pointing opposite to the direction of normpath
392 - size: size of the arrow as arc length
393 - angle. opening angle
394 - constrictionlen: None (no constriction) or arc length of constriction.
397 # arc length and coordinates of tip
398 tx
, ty
= anormpath
.at(arclenfrombegin
)
400 # construct the template for the arrow by cutting the path at the
401 # corresponding length
402 arrowtemplate
= anormpath
.split([arclenfrombegin
, arclenfrombegin
- direction
* size
])[1]
404 # from this template, we construct the two outer curves of the arrow
405 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
406 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
408 # now come the joining backward parts
409 if constrictionlen
is not None:
410 # constriction point (cx, cy) lies on path
411 cx
, cy
= anormpath
.at(arclenfrombegin
- direction
* constrictionlen
)
412 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
413 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
415 arrow
= arrowl
.reversed() << arrowr
422 _base
= 6 * unit
.v_pt
424 class arrow(deco
, attr
.attr
):
426 """arrow is a decorator which adds an arrow to either side of the path"""
428 def __init__(self
, attrs
=[], pos
=1, reversed=0, size
=_base
, angle
=45, constriction
=0.8):
429 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
430 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
432 self
.reversed = reversed
435 self
.constriction
= constriction
437 def __call__(self
, attrs
=None, pos
=None, reversed=None, size
=None, angle
=None, constriction
=_marker
):
443 reversed = self
.reversed
448 if constriction
is _marker
:
449 constriction
= self
.constriction
450 return arrow(attrs
=attrs
, pos
=pos
, reversed=reversed, size
=size
, angle
=angle
, constriction
=constriction
)
452 def decorate(self
, dp
, texrunner
):
456 # calculate absolute arc length of constricition
457 # Note that we have to correct this length because the arrowtemplates are rotated
458 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
459 # self.constriction = 1, we actually have a length which is approximately shorter
460 # by the given geometrical factor.
461 if self
.constriction
is not None:
462 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
464 # if we do not want a constriction, i.e. constriction is None, we still
465 # need constrictionlen for cutting the path
466 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
467 arrowheadconstrictionlen
= None
469 arclenfrombegin
= self
.pos
* anormpath
.arclen()
470 direction
= self
.reversed and -1 or 1
471 arrowhead
= _arrowhead(anormpath
, arclenfrombegin
, direction
, self
.size
, self
.angle
, arrowheadconstrictionlen
)
473 # add arrowhead to decoratedpath
474 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
476 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
477 if self
.pos
== 0 and self
.reversed:
478 dp
.excluderange(0, min(self
.size
, constrictionlen
))
479 elif self
.pos
== 1 and not self
.reversed:
480 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
482 arrow
.clear
= attr
.clearclass(arrow
)
484 # arrows at begin of path
485 barrow
= arrow(pos
=0, reversed=1)
486 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
487 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
488 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
489 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
490 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
491 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
492 barrow
.normal
= barrow(size
=_base
)
493 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
494 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
495 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
496 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
497 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
498 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
500 # arrows at end of path
502 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
503 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
504 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
505 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
506 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
507 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
508 earrow
.normal
= earrow(size
=_base
)
509 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
510 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
511 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
512 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
513 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
514 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
517 class text(deco
, attr
.attr
):
518 """a simple text decorator"""
520 def __init__(self
, text
, textattrs
=[], angle
=0, textdist
=0.2,
521 relarclenpos
=0.5, arclenfrombegin
=None, arclenfromend
=None,
523 if arclenfrombegin
is not None and arclenfromend
is not None:
524 raise ValueError("either set arclenfrombegin or arclenfromend")
526 self
.textattrs
= textattrs
528 self
.textdist
= textdist
529 self
.relarclenpos
= relarclenpos
530 self
.arclenfrombegin
= arclenfrombegin
531 self
.arclenfromend
= arclenfromend
532 self
.texrunner
= texrunner
534 def decorate(self
, dp
, texrunner
):
536 texrunner
= self
.texrunner
537 import text
as textmodule
538 textattrs
= attr
.mergeattrs([textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
] + self
.textattrs
)
541 if self
.arclenfrombegin
is not None:
542 x
, y
= dp
.path
.at(dp
.path
.begin() + self
.arclenfrombegin
)
543 elif self
.arclenfromend
is not None:
544 x
, y
= dp
.path
.at(dp
.path
.end() - self
.arclenfromend
)
546 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
547 x
, y
= dp
.path
.at(self
.relarclenpos
* dp
.path
.arclen())
549 t
= texrunner
.text(x
, y
, self
.text
, textattrs
)
550 t
.linealign(self
.textdist
, math
.cos(self
.angle
*math
.pi
/180), math
.sin(self
.angle
*math
.pi
/180))
551 dp
.ornaments
.insert(t
)
554 class shownormpath(deco
, attr
.attr
):
556 def decorate(self
, dp
, texrunner
):
559 for normsubpath
in dp
.path
.normsubpaths
:
560 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
561 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
562 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.green
])
564 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.blue
])
565 for normsubpath
in dp
.path
.normsubpaths
:
566 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
567 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
568 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x0_pt
, normsubpathitem
.y0_pt
, normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
569 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, normsubpathitem
.x3_pt
, normsubpathitem
.y3_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
570 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
, r_pt
), [filled([color
.rgb
.red
])])
571 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, r_pt
), [filled([color
.rgb
.red
])])
572 for normsubpath
in dp
.path
.normsubpaths
:
573 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
575 x_pt
, y_pt
= normsubpathitem
.atbegin_pt()
576 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])
577 x_pt
, y_pt
= normsubpathitem
.atend_pt()
578 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])