correct context handling (but maybe too complicated)
[PyX/mjg.git] / pyx / deco.py
blob19f7add313d06d4846d2b1471d26b0d67cb90318
1 #!/usr/bin/env python
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
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, canvas, color, helper, path, style, trafo, unit
32 try:
33 from math import radians
34 except ImportError:
35 # fallback implementation for Python 2.1 and below
36 def radians(x): return x*math.pi/180
39 # Decorated path
42 class decoratedpath(canvas.canvasitem):
43 """Decorated path
45 The main purpose of this class is during the drawing
46 (stroking/filling) of a path. It collects attributes for the
47 stroke and/or fill operations.
48 """
50 def __init__(self, path, strokepath=None, fillpath=None,
51 styles=None, strokestyles=None, fillstyles=None,
52 ornaments=None):
54 self.path = path
56 # global style for stroking and filling and subdps
57 self.styles = styles
59 # styles which apply only for stroking and filling
60 self.strokestyles = strokestyles
61 self.fillstyles = fillstyles
63 # the decoratedpath can contain additional elements of the
64 # path (ornaments), e.g., arrowheads.
65 if ornaments is None:
66 self.ornaments = canvas.canvas()
67 else:
68 self.ornaments = ornaments
70 self.nostrokeranges = None
72 def ensurenormpath(self):
73 """convert self.path into a normpath"""
74 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
75 self.path = self.path.normpath()
77 def excluderange(self, begin, end):
78 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
79 if self.nostrokeranges is None:
80 self.nostrokeranges = [(begin, end)]
81 else:
82 ibegin = 0
83 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
84 ibegin += 1
86 if ibegin == len(self.nostrokeranges):
87 self.nostrokeranges.append((begin, end))
88 return
90 iend = len(self.nostrokeranges) - 1
91 while 0 <= iend and end < self.nostrokeranges[iend][0]:
92 iend -= 1
94 if iend == -1:
95 self.nostrokeranges.insert(0, (begin, end))
96 return
98 if self.nostrokeranges[ibegin][0] < begin:
99 begin = self.nostrokeranges[ibegin][0]
100 if end < self.nostrokeranges[iend][1]:
101 end = self.nostrokeranges[iend][1]
103 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
105 def bbox(self):
106 pathbbox = self.path.bbox()
107 ornamentsbbox = self.ornaments.bbox()
108 if ornamentsbbox is not None:
109 return ornamentsbbox + pathbbox
110 else:
111 return pathbbox
113 def registerPS(self, registry):
114 if self.styles:
115 for style in self.styles:
116 style.registerPS(registry)
117 if self.fillstyles:
118 for style in self.fillstyles:
119 style.registerPS(registry)
120 if self.strokestyles:
121 for style in self.strokestyles:
122 style.registerPS(registry)
123 self.ornaments.registerPS(registry)
125 def registerPDF(self, registry):
126 if self.styles:
127 for style in self.styles:
128 style.registerPDF(registry)
129 if self.fillstyles:
130 for style in self.fillstyles:
131 style.registerPDF(registry)
132 if self.strokestyles:
133 for style in self.strokestyles:
134 style.registerPDF(registry)
135 self.ornaments.registerPDF(registry)
137 def strokepath(self):
138 if self.nostrokeranges:
139 splitlist = []
140 for begin, end in self.nostrokeranges:
141 splitlist.append(begin)
142 splitlist.append(end)
143 split = self.path.split(splitlist)
144 # XXX properly handle closed paths?
145 result = split[0]
146 for i in range(2, len(split), 2):
147 result += split[i]
148 return result
149 else:
150 return self.path
152 def outputPS(self, file, writer, context):
153 # draw (stroke and/or fill) the decoratedpath on the canvas
154 # while trying to produce an efficient output, e.g., by
155 # not writing one path two times
157 # small helper
158 def _writestyles(styles, context):
159 for style in styles:
160 style.outputPS(file, writer, context)
162 if self.strokestyles is None and self.fillstyles is None:
163 raise RuntimeError("Path neither to be stroked nor filled")
165 strokepath = self.strokepath()
166 fillpath = self.path
168 # apply global styles
169 if self.styles:
170 file.write("gsave\n")
171 context = context()
172 _writestyles(self.styles, context)
174 if self.fillstyles is not None:
175 file.write("newpath\n")
176 fillpath.outputPS(file, writer, context)
178 if self.strokestyles is not None and strokepath is fillpath:
179 # do efficient stroking + filling if respective paths are identical
180 file.write("gsave\n")
182 if self.fillstyles:
183 _writestyles(self.fillstyles, context())
185 file.write("fill\n")
186 file.write("grestore\n")
188 if self.strokestyles:
189 file.write("gsave\n")
190 _writestyles(self.strokestyles, context())
192 file.write("stroke\n")
194 if self.strokestyles:
195 file.write("grestore\n")
196 else:
197 # only fill fillpath - for the moment
198 if self.fillstyles:
199 file.write("gsave\n")
200 _writestyles(self.fillstyles, context())
202 file.write("fill\n")
204 if self.fillstyles:
205 file.write("grestore\n")
207 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
208 # this is the only relevant case still left
209 # Note that a possible stroking has already been done.
210 oldcontext = context
211 context = context()
213 if self.strokestyles:
214 file.write("gsave\n")
215 _writestyles(self.strokestyles, context)
217 file.write("newpath\n")
218 strokepath.outputPS(file, writer, context)
219 file.write("stroke\n")
221 if self.strokestyles:
222 file.write("grestore\n", context)
224 context = oldcontext
226 # now, draw additional elements of decoratedpath
227 self.ornaments.outputPS(file, writer, context)
229 # restore global styles
230 if self.styles:
231 file.write("grestore\n")
233 def outputPDF(self, file, writer, context):
234 # draw (stroke and/or fill) the decoratedpath on the canvas
236 def _writestyles(styles, context):
237 for style in styles:
238 style.outputPDF(file, writer, context)
240 def _writestrokestyles(strokestyles, context):
241 for style in strokestyles:
242 style.outputPDF(file, writer, context(fillattr=0))
244 def _writefillstyles(fillstyles, context):
245 for style in fillstyles:
246 style.outputPDF(file, writer, context(strokeattr=0))
248 if self.strokestyles is None and self.fillstyles is None:
249 raise RuntimeError("Path neither to be stroked nor filled")
251 strokepath = self.strokepath()
252 fillpath = self.path
254 # apply global styles
255 if self.styles:
256 file.write("q\n") # gsave
257 context = context()
258 _writestyles(self.styles, context)
260 if self.fillstyles is not None:
261 fillpath.outputPDF(file, writer, context)
263 if self.strokestyles is not None and strokepath is fillpath:
264 # do efficient stroking + filling
265 file.write("q\n") # gsave
266 oldcontext = context
267 context = context()
269 if self.fillstyles:
270 _writefillstyles(self.fillstyles, context)
271 if self.strokestyles:
272 _writestrokestyles(self.strokestyles, context)
274 file.write("B\n") # both stroke and fill
275 file.write("Q\n") # grestore
276 context = oldcontext
277 else:
278 # only fill fillpath - for the moment
279 if self.fillstyles:
280 file.write("q\n") # gsave
281 _writefillstyles(self.fillstyles, context())
283 file.write("f\n") # fill
285 if self.fillstyles:
286 file.write("Q\n") # grestore
288 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
289 # this is the only relevant case still left
290 # Note that a possible stroking has already been done.
291 oldcontext = context
292 context = context()
294 if self.strokestyles:
295 file.write("q\n") # gsave
296 _writestrokestyles(self.strokestyles, context)
298 strokepath.outputPDF(file, writer, context)
299 file.write("S\n") # stroke
301 if self.strokestyles:
302 file.write("Q\n") # grestore
303 context = oldcontext
305 # now, draw additional elements of decoratedpath
306 self.ornaments.outputPDF(file, writer, context)
308 # restore global styles
309 if self.styles:
310 file.write("Q\n") # grestore
313 # Path decorators
316 class deco:
318 """decorators
320 In contrast to path styles, path decorators depend on the concrete
321 path to which they are applied. In particular, they don't make
322 sense without any path and can thus not be used in canvas.set!
326 def decorate(self, dp):
327 """apply a style to a given decoratedpath object dp
329 decorate accepts a decoratedpath object dp, applies PathStyle
330 by modifying dp in place and returning the new dp.
333 pass
336 # stroked and filled: basic decos which stroked and fill,
337 # respectively the path
340 class _stroked(deco, attr.exclusiveattr):
342 """stroked is a decorator, which draws the outline of the path"""
344 def __init__(self, styles=[]):
345 attr.exclusiveattr.__init__(self, _stroked)
346 self.styles = attr.mergeattrs(styles)
347 attr.checkattrs(self.styles, [style.strokestyle])
349 def __call__(self, styles=[]):
350 # XXX or should we also merge self.styles
351 return _stroked(styles)
353 def decorate(self, dp):
354 if dp.strokestyles is not None:
355 raise RuntimeError("Cannot stroke an already stroked path")
356 dp.strokestyles = self.styles
357 return dp
359 stroked = _stroked()
360 stroked.clear = attr.clearclass(_stroked)
363 class _filled(deco, attr.exclusiveattr):
365 """filled is a decorator, which fills the interior of the path"""
367 def __init__(self, styles=[]):
368 attr.exclusiveattr.__init__(self, _filled)
369 self.styles = attr.mergeattrs(styles)
370 attr.checkattrs(self.styles, [style.fillstyle])
372 def __call__(self, styles=[]):
373 # XXX or should we also merge self.styles
374 return _filled(styles)
376 def decorate(self, dp):
377 if dp.fillstyles is not None:
378 raise RuntimeError("Cannot fill an already filled path")
379 dp.fillstyles = self.styles
380 return dp
382 filled = _filled()
383 filled.clear = attr.clearclass(_filled)
386 # Arrows
389 # helper function which constructs the arrowhead
391 def _arrowhead(anormpath, size, angle, constrictionlen, reversed):
393 """helper routine, which returns an arrowhead from a given anormpath
395 returns arrowhead at begin of anormpath with size,
396 opening angle and constriction length constrictionlen. If constrictionlen is None, we
397 do not add a constriction.
400 if reversed:
401 anormpath = anormpath.reversed()
402 alen = anormpath.arclentoparam(size)
403 tx, ty = anormpath.atbegin()
405 # now we construct the template for our arrow but cutting
406 # the path a the corresponding length
407 arrowtemplate = anormpath.split(alen)[0]
409 # from this template, we construct the two outer curves
410 # of the arrow
411 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
412 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
414 # now come the joining backward parts
416 if constrictionlen is not None:
417 # constriction point (cx, cy) lies on path
418 cx, cy = anormpath.at(anormpath.arclentoparam(constrictionlen))
419 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
420 arrow = arrowl.reversed() << arrowr << arrowcr
421 else:
422 arrow = arrowl.reversed() << arrowr
424 arrow[-1].close()
426 return arrow
429 _base = 6 * unit.v_pt
431 class arrow(deco, attr.attr):
433 """arrow is a decorator which adds an arrow to either side of the path"""
435 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
436 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
437 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
438 self.position = position
439 self.size = size
440 self.angle = angle
441 self.constriction = constriction
443 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=helper.nodefault):
444 if attrs is None:
445 attrs = self.attrs
446 if position is None:
447 position = self.position
448 if size is None:
449 size = self.size
450 if angle is None:
451 angle = self.angle
452 if constriction is helper.nodefault:
453 constriction = self.constriction
454 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
456 def decorate(self, dp):
457 dp.ensurenormpath()
458 anormpath = dp.path
460 # calculate absolute arc length of constricition
461 # Note that we have to correct this length because the arrowtemplates are rotated
462 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
463 # self.constriction = 1, we actually have a length which is approximately shorter
464 # by the given geometrical factor.
465 if self.constriction is not None:
466 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
467 else:
468 # if we do not want a constriction, i.e. constriction is None, we still
469 # need constrictionlen for cutting the path
470 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
471 arrowheadconstrictionlen = None
473 if self.position == 0:
474 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
475 else:
476 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
478 # add arrowhead to decoratedpath
479 dp.ornaments.draw(arrowhead, self.attrs)
481 if self.position == 0:
482 # exclude first part of the first normsubpath from stroking
483 dp.excluderange(0, min(self.size, constrictionlen))
484 else:
485 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
487 return dp
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))