- added new helper methods _distributeparams and _findnormpathitem to
[PyX/mjg.git] / pyx / deco.py
blob6825ccdf8d3f07ad957d057a641537c98574c4f6
1 #!/usr/bin/env python
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
25 # TODO:
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
29 import sys, math
30 import attr, base, canvas, color, helper, path, style, trafo, unit
33 # Decorated path
36 class decoratedpath(base.canvasitem):
37 """Decorated path
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.
42 """
44 def __init__(self, path, strokepath=None, fillpath=None,
45 styles=None, strokestyles=None, fillstyles=None,
46 subcanvas=None):
48 self.path = path
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.,
62 # arrowheads,
63 if subcanvas is None:
64 self.subcanvas = canvas.canvas()
65 else:
66 self.subcanvas = subcanvas
69 def bbox(self):
70 scbbox = self.subcanvas.bbox()
71 pbbox = self.path.bbox()
72 if scbbox is not None:
73 return scbbox+pbbox
74 else:
75 return pbbox
77 def prolog(self):
78 result = []
79 for style in list(self.styles) + list(self.fillstyles) + list(self.strokestyles):
80 result.extend(style.prolog())
81 result.extend(self.subcanvas.prolog())
82 return result
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
89 # small helper
90 def _writestyles(styles, file=file):
91 for style in styles:
92 style.outputPS(file)
94 # apply global styles
95 if self.styles:
96 file.write("gsave\n")
97 _writestyles(self.styles)
99 if self.fillpath is not None:
100 file.write("newpath\n")
101 self.fillpath.outputPS(file)
103 if self.strokepath==self.fillpath:
104 # do efficient stroking + filling
105 file.write("gsave\n")
107 if self.fillstyles:
108 _writestyles(self.fillstyles)
110 file.write("fill\n")
111 file.write("grestore\n")
113 if self.strokestyles:
114 file.write("gsave\n")
115 _writestyles(self.strokestyles)
117 file.write("stroke\n")
119 if self.strokestyles:
120 file.write("grestore\n")
121 else:
122 # only fill fillpath - for the moment
123 if self.fillstyles:
124 file.write("gsave\n")
125 _writestyles(self.fillstyles)
127 file.write("fill\n")
129 if self.fillstyles:
130 file.write("grestore\n")
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 file.write("gsave\n")
138 _writestyles(self.strokestyles)
140 file.write("newpath\n")
141 self.strokepath.outputPS(file)
142 file.write("stroke\n")
144 if self.strokestyles:
145 file.write("grestore\n")
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
154 if self.styles:
155 file.write("grestore\n")
157 def outputPDF(self, file):
158 # draw (stroke and/or fill) the decoratedpath on the canvas
160 def _writestyles(styles, file=file):
161 for style in styles:
162 style.outputPDF(file)
164 def _writestrokestyles(strokestyles, file=file):
165 for style in strokestyles:
166 if isinstance(style, color.color):
167 style.outputPDF(file, fillattr=0)
168 else:
169 style.outputPDF(file)
171 def _writefillstyles(fillstyles, file=file):
172 for style in fillstyles:
173 if isinstance(style, color.color):
174 style.outputPDF(file, strokeattr=0)
175 else:
176 style.outputPDF(file)
178 # apply global styles
179 if self.styles:
180 file.write("q\n") # gsave
181 _writestyles(self.styles)
183 if self.fillpath is not None:
184 self.fillpath.outputPDF(file)
186 if self.strokepath==self.fillpath:
187 # do efficient stroking + filling
188 file.write("q\n") # gsave
190 if self.fillstyles:
191 _writefillstyles(self.fillstyles)
192 if self.strokestyles:
193 _writestrokestyles(self.strokestyles)
195 file.write("B\n") # both stroke and fill
196 file.write("Q\n") # grestore
197 else:
198 # only fill fillpath - for the moment
199 if self.fillstyles:
200 file.write("q\n") # gsave
201 _writefillstyles(self.fillstyles)
203 file.write("f\n") # fill
205 if self.fillstyles:
206 file.write("Q\n") # grestore
208 if self.strokepath is not None and self.strokepath!=self.fillpath:
209 # this is the only relevant case still left
210 # Note that a possible stroking has already been done.
212 if self.strokestyles:
213 file.write("q\n") # gsave
214 _writestrokestyles(self.strokestyles)
216 self.strokepath.outputPDF(file)
217 file.write("S\n") # stroke
219 if self.strokestyles:
220 file.write("Q\n") # grestore
222 if self.strokepath is None and self.fillpath is None:
223 raise RuntimeError("Path neither to be stroked nor filled")
225 # now, draw additional elements of decoratedpath
226 self.subcanvas.outputPDF(file)
228 # restore global styles
229 if self.styles:
230 file.write("Q\n") # grestore
233 # Path decorators
236 class deco:
238 """decorators
240 In contrast to path styles, path decorators depend on the concrete
241 path to which they are applied. In particular, they don't make
242 sense without any path and can thus not be used in canvas.set!
246 def decorate(self, dp):
247 """apply a style to a given decoratedpath object dp
249 decorate accepts a decoratedpath object dp, applies PathStyle
250 by modifying dp in place and returning the new dp.
253 pass
256 # stroked and filled: basic decos which stroked and fill,
257 # respectively the path
260 class _stroked(deco, attr.exclusiveattr):
262 """stroked is a decorator, which draws the outline of the path"""
264 def __init__(self, styles=[]):
265 attr.exclusiveattr.__init__(self, _stroked)
266 self.styles = attr.mergeattrs(styles)
267 attr.checkattrs(self.styles, [style.strokestyle])
269 def __call__(self, styles=[]):
270 # XXX or should we also merge self.styles
271 return _stroked(styles)
273 def decorate(self, dp):
274 dp.strokepath = dp.path
275 dp.strokestyles = self.styles
276 return dp
278 stroked = _stroked()
279 stroked.clear = attr.clearclass(_stroked)
282 class _filled(deco, attr.exclusiveattr):
284 """filled is a decorator, which fills the interior of the path"""
286 def __init__(self, styles=[]):
287 attr.exclusiveattr.__init__(self, _filled)
288 self.styles = attr.mergeattrs(styles)
289 attr.checkattrs(self.styles, [style.fillstyle])
291 def __call__(self, styles=[]):
292 # XXX or should we also merge self.styles
293 return _filled(styles)
295 def decorate(self, dp):
296 dp.fillpath = dp.path
297 dp.fillstyles = self.styles
298 return dp
300 filled = _filled()
301 filled.clear = attr.clearclass(_filled)
304 # Arrows
307 # two helper functions which construct the arrowhead and return its size, respectively
309 def _arrowheadtemplatelength(anormpath, size):
310 "returns length of arrowhead template (in parametrisation of anormpath)"
311 # get tip (tx, ty)
312 tx, ty = anormpath.begin()
314 # obtain arrow template by using path up to first intersection
315 # with circle around tip (as suggested by Michael Schindler)
316 ipar = anormpath.intersect(path.circle(tx, ty, size))[0]
317 if ipar:
318 return ipar[0]
319 else:
320 raise RuntimeError("arrow head too big for path")
322 def _arrowhead(anormpath, size, angle, constriction):
324 """helper routine, which returns an arrowhead for a normpath
326 returns arrowhead at begin of anormpath with size,
327 opening angle and relative constriction
330 alen = _arrowheadtemplatelength(anormpath, size)
331 tx, ty = anormpath.begin()
333 # now we construct the template for our arrow but cutting
334 # the path a the corresponding length
335 arrowtemplate = anormpath.split([alen])[0]
337 # from this template, we construct the two outer curves
338 # of the arrow
339 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
340 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
342 # now come the joining backward parts
344 # constriction point (cx, cy) lies on path
345 cx, cy = anormpath.at(_arrowheadtemplatelength(anormpath, constriction*size))
347 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
349 arrow = arrowl.reversed() << arrowr << arrowcr
350 arrow.append(path.closepath())
352 return arrow
355 _base = 6 * unit.v_pt
357 class arrow(deco, attr.attr):
359 """arrow is a decorator which adds an arrow to either side of the path"""
361 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
362 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
363 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
364 self.position = position
365 self.size = size
366 self.angle = angle
367 self.constriction = constriction
369 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=None):
370 if attrs is None:
371 attrs = self.attrs
372 if position is None:
373 position = self.position
374 if size is None:
375 size = self.size
376 if angle is None:
377 angle = self.angle
378 if constriction is None:
379 constriction = self.constriction
380 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
382 def decorate(self, dp):
383 # XXX raise exception error, when strokepath is not defined
385 # convert to normpath if necessary
386 if isinstance(dp.strokepath, path.normpath):
387 anormpath = dp.strokepath
388 else:
389 anormpath = path.normpath(dp.path)
390 if self.position:
391 anormpath = anormpath.reversed()
393 # add arrowhead to decoratedpath
394 dp.subcanvas.draw(_arrowhead(anormpath, self.size, self.angle, self.constriction),
395 self.attrs)
397 # calculate new strokepath
398 ilen = _arrowheadtemplatelength(anormpath, self.size*min(self.constriction, 1))
400 # this is the rest of the path, we have to draw
401 anormpath = anormpath.split([ilen])[1]
403 # go back to original orientation, if necessary
404 if self.position:
405 anormpath.reverse()
407 # set the new (shortened) strokepath
408 dp.strokepath = anormpath
410 return dp
412 arrow.clear = attr.clearclass(arrow)
414 # arrows at begin of path
415 barrow = arrow(position=0)
416 barrow.SMALL = barrow(size=_base/math.sqrt(64))
417 barrow.SMALl = barrow(size=_base/math.sqrt(32))
418 barrow.SMAll = barrow(size=_base/math.sqrt(16))
419 barrow.SMall = barrow(size=_base/math.sqrt(8))
420 barrow.Small = barrow(size=_base/math.sqrt(4))
421 barrow.small = barrow(size=_base/math.sqrt(2))
422 barrow.normal = barrow(size=_base)
423 barrow.large = barrow(size=_base*math.sqrt(2))
424 barrow.Large = barrow(size=_base*math.sqrt(4))
425 barrow.LArge = barrow(size=_base*math.sqrt(8))
426 barrow.LARge = barrow(size=_base*math.sqrt(16))
427 barrow.LARGe = barrow(size=_base*math.sqrt(32))
428 barrow.LARGE = barrow(size=_base*math.sqrt(64))
430 # arrows at end of path
431 earrow = arrow(position=1)
432 earrow.SMALL = earrow(size=_base/math.sqrt(64))
433 earrow.SMALl = earrow(size=_base/math.sqrt(32))
434 earrow.SMAll = earrow(size=_base/math.sqrt(16))
435 earrow.SMall = earrow(size=_base/math.sqrt(8))
436 earrow.Small = earrow(size=_base/math.sqrt(4))
437 earrow.small = earrow(size=_base/math.sqrt(2))
438 earrow.normal = earrow(size=_base)
439 earrow.large = earrow(size=_base*math.sqrt(2))
440 earrow.Large = earrow(size=_base*math.sqrt(4))
441 earrow.LArge = earrow(size=_base*math.sqrt(8))
442 earrow.LARge = earrow(size=_base*math.sqrt(16))
443 earrow.LARGe = earrow(size=_base*math.sqrt(32))
444 earrow.LARGE = earrow(size=_base*math.sqrt(64))