add release date
[PyX.git] / pyx / deco.py
blob27b5eeda9444c8f23e55bada2acc68cd54f86b35
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
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(base.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 prolog(self):
114 result = []
115 if self.styles:
116 for style in self.styles:
117 result.extend(style.prolog())
118 if self.fillstyles:
119 for style in self.fillstyles:
120 result.extend(style.prolog())
121 if self.strokestyles:
122 for style in self.strokestyles:
123 result.extend(style.prolog())
124 result.extend(self.ornaments.prolog())
125 return result
127 def strokepath(self):
128 if self.nostrokeranges:
129 splitlist = []
130 for begin, end in self.nostrokeranges:
131 splitlist.append(begin)
132 splitlist.append(end)
133 split = self.path.split(splitlist)
134 # XXX properly handle closed paths?
135 result = split[0]
136 for i in range(2, len(split), 2):
137 result += split[i]
138 return result
139 else:
140 return self.path
142 def outputPS(self, file):
143 # draw (stroke and/or fill) the decoratedpath on the canvas
144 # while trying to produce an efficient output, e.g., by
145 # not writing one path two times
147 # small helper
148 def _writestyles(styles, file=file):
149 for style in styles:
150 style.outputPS(file)
152 if self.strokestyles is None and self.fillstyles is None:
153 raise RuntimeError("Path neither to be stroked nor filled")
155 strokepath = self.strokepath()
156 fillpath = self.path
158 # apply global styles
159 if self.styles:
160 file.write("gsave\n")
161 _writestyles(self.styles)
163 if self.fillstyles is not None:
164 file.write("newpath\n")
165 fillpath.outputPS(file)
167 if self.strokestyles is not None and strokepath is fillpath:
168 # do efficient stroking + filling if respective paths are identical
169 file.write("gsave\n")
171 if self.fillstyles:
172 _writestyles(self.fillstyles)
174 file.write("fill\n")
175 file.write("grestore\n")
177 if self.strokestyles:
178 file.write("gsave\n")
179 _writestyles(self.strokestyles)
181 file.write("stroke\n")
183 if self.strokestyles:
184 file.write("grestore\n")
185 else:
186 # only fill fillpath - for the moment
187 if self.fillstyles:
188 file.write("gsave\n")
189 _writestyles(self.fillstyles)
191 file.write("fill\n")
193 if self.fillstyles:
194 file.write("grestore\n")
196 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
197 # this is the only relevant case still left
198 # Note that a possible stroking has already been done.
200 if self.strokestyles:
201 file.write("gsave\n")
202 _writestyles(self.strokestyles)
204 file.write("newpath\n")
205 strokepath.outputPS(file)
206 file.write("stroke\n")
208 if self.strokestyles:
209 file.write("grestore\n")
211 # now, draw additional elements of decoratedpath
212 self.ornaments.outputPS(file)
214 # restore global styles
215 if self.styles:
216 file.write("grestore\n")
218 def outputPDF(self, file):
219 # draw (stroke and/or fill) the decoratedpath on the canvas
221 def _writestyles(styles, file=file):
222 for style in styles:
223 style.outputPDF(file)
225 def _writestrokestyles(strokestyles, file=file):
226 for style in strokestyles:
227 if isinstance(style, color.color):
228 style.outputPDF(file, fillattr=0)
229 else:
230 style.outputPDF(file)
232 def _writefillstyles(fillstyles, file=file):
233 for style in fillstyles:
234 if isinstance(style, color.color):
235 style.outputPDF(file, strokeattr=0)
236 else:
237 style.outputPDF(file)
239 if self.strokestyles is None and self.fillstyles is None:
240 raise RuntimeError("Path neither to be stroked nor filled")
242 strokepath = self.strokepath()
243 fillpath = self.path
245 # apply global styles
246 if self.styles:
247 file.write("q\n") # gsave
248 _writestyles(self.styles)
250 if self.fillstyles is not None:
251 fillpath.outputPDF(file)
253 if self.strokestyles is not None and strokepath is fillpath:
254 # do efficient stroking + filling
255 file.write("q\n") # gsave
257 if self.fillstyles:
258 _writefillstyles(self.fillstyles)
259 if self.strokestyles:
260 _writestrokestyles(self.strokestyles)
262 file.write("B\n") # both stroke and fill
263 file.write("Q\n") # grestore
264 else:
265 # only fill fillpath - for the moment
266 if self.fillstyles:
267 file.write("q\n") # gsave
268 _writefillstyles(self.fillstyles)
270 file.write("f\n") # fill
272 if self.fillstyles:
273 file.write("Q\n") # grestore
275 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
276 # this is the only relevant case still left
277 # Note that a possible stroking has already been done.
279 if self.strokestyles:
280 file.write("q\n") # gsave
281 _writestrokestyles(self.strokestyles)
283 strokepath.outputPDF(file)
284 file.write("S\n") # stroke
286 if self.strokestyles:
287 file.write("Q\n") # grestore
289 # now, draw additional elements of decoratedpath
290 self.ornaments.outputPDF(file)
292 # restore global styles
293 if self.styles:
294 file.write("Q\n") # grestore
297 # Path decorators
300 class deco:
302 """decorators
304 In contrast to path styles, path decorators depend on the concrete
305 path to which they are applied. In particular, they don't make
306 sense without any path and can thus not be used in canvas.set!
310 def decorate(self, dp):
311 """apply a style to a given decoratedpath object dp
313 decorate accepts a decoratedpath object dp, applies PathStyle
314 by modifying dp in place and returning the new dp.
317 pass
320 # stroked and filled: basic decos which stroked and fill,
321 # respectively the path
324 class _stroked(deco, attr.exclusiveattr):
326 """stroked is a decorator, which draws the outline of the path"""
328 def __init__(self, styles=[]):
329 attr.exclusiveattr.__init__(self, _stroked)
330 self.styles = attr.mergeattrs(styles)
331 attr.checkattrs(self.styles, [style.strokestyle])
333 def __call__(self, styles=[]):
334 # XXX or should we also merge self.styles
335 return _stroked(styles)
337 def decorate(self, dp):
338 if dp.strokestyles is not None:
339 raise RuntimeError("Cannot stroke an already stroked path")
340 dp.strokestyles = self.styles
341 return dp
343 stroked = _stroked()
344 stroked.clear = attr.clearclass(_stroked)
347 class _filled(deco, attr.exclusiveattr):
349 """filled is a decorator, which fills the interior of the path"""
351 def __init__(self, styles=[]):
352 attr.exclusiveattr.__init__(self, _filled)
353 self.styles = attr.mergeattrs(styles)
354 attr.checkattrs(self.styles, [style.fillstyle])
356 def __call__(self, styles=[]):
357 # XXX or should we also merge self.styles
358 return _filled(styles)
360 def decorate(self, dp):
361 if dp.fillstyles is not None:
362 raise RuntimeError("Cannot fill an already filled path")
363 dp.fillstyles = self.styles
364 return dp
366 filled = _filled()
367 filled.clear = attr.clearclass(_filled)
370 # Arrows
373 # helper function which constructs the arrowhead
375 def _arrowhead(anormsubpath, size, angle, constrictionlen, reversed):
377 """helper routine, which returns an arrowhead from a given anormsubpath
379 returns arrowhead at begin of anormpath with size,
380 opening angle and constriction length constrictionlen. If constrictionlen is None, we
381 do not add a constriction.
384 if reversed:
385 anormsubpath = anormsubpath.reversed()
386 alen = anormsubpath.arclentoparam(size)
387 tx, ty = anormsubpath.begin()
389 # now we construct the template for our arrow but cutting
390 # the path a the corresponding length
391 arrowtemplate = anormsubpath.split([alen])[0]
393 # from this template, we construct the two outer curves
394 # of the arrow
395 arrowl = path.normpath([arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))])
396 arrowr = path.normpath([arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))])
398 # now come the joining backward parts
400 if constrictionlen is not None:
401 # constriction point (cx, cy) lies on path
402 cx, cy = anormsubpath.at(anormsubpath.arclentoparam(constrictionlen))
403 arrowcr= path.line(*(arrowr.end() + (cx,cy)))
404 arrow = arrowl.reversed() << arrowr << arrowcr
405 else:
406 arrow = arrowl.reversed() << arrowr
408 arrow[-1].close()
410 return arrow
413 _base = 6 * unit.v_pt
415 class arrow(deco, attr.attr):
417 """arrow is a decorator which adds an arrow to either side of the path"""
419 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
420 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
421 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
422 self.position = position
423 self.size = size
424 self.angle = angle
425 self.constriction = constriction
427 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=helper.nodefault):
428 if attrs is None:
429 attrs = self.attrs
430 if position is None:
431 position = self.position
432 if size is None:
433 size = self.size
434 if angle is None:
435 angle = self.angle
436 if constriction is helper.nodefault:
437 constriction = self.constriction
438 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
440 def decorate(self, dp):
441 dp.ensurenormpath()
442 anormpath = dp.path
444 # calculate absolute arc length of constricition
445 # Note that we have to correct this length because the arrowtemplates are rotated
446 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
447 # self.constriction = 1, we actually have a length which is approximately shorter
448 # by the given geometrical factor.
449 if self.constriction is not None:
450 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
451 else:
452 # if we do not want a constriction, i.e. constriction is None, we still
453 # need constrictionlen for cutting the path
454 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
455 arrowheadconstrictionlen = None
457 if self.position == 0:
458 # Note that the template for the arrow head should only be constructed
459 # from the first normsubpath
460 firstnormsubpath = anormpath[0]
461 arrowhead = _arrowhead(firstnormsubpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
462 else:
463 lastnormsubpath = anormpath[-1]
464 arrowhead = _arrowhead(lastnormsubpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
466 # add arrowhead to decoratedpath
467 dp.ornaments.draw(arrowhead, self.attrs)
469 if self.position == 0:
470 # exclude first part of the first normsubpath from stroking
471 ilen = firstnormsubpath.arclentoparam(min(self.size, constrictionlen))
472 dp.excluderange((0, 0), (0,ilen))
473 else:
474 ilen = lastnormsubpath.arclentoparam(lastnormsubpath.arclen()-min(self.size, constrictionlen))
475 # TODO. provide a better way to access the number of normsubpaths in a normpath
476 lastnormsubpathindex = len(anormpath.normsubpaths)-1
477 dp.excluderange((lastnormsubpathindex, ilen), (lastnormsubpathindex, lastnormsubpath.range()))
479 return dp
481 arrow.clear = attr.clearclass(arrow)
483 # arrows at begin of path
484 barrow = arrow(position=0)
485 barrow.SMALL = barrow(size=_base/math.sqrt(64))
486 barrow.SMALl = barrow(size=_base/math.sqrt(32))
487 barrow.SMAll = barrow(size=_base/math.sqrt(16))
488 barrow.SMall = barrow(size=_base/math.sqrt(8))
489 barrow.Small = barrow(size=_base/math.sqrt(4))
490 barrow.small = barrow(size=_base/math.sqrt(2))
491 barrow.normal = barrow(size=_base)
492 barrow.large = barrow(size=_base*math.sqrt(2))
493 barrow.Large = barrow(size=_base*math.sqrt(4))
494 barrow.LArge = barrow(size=_base*math.sqrt(8))
495 barrow.LARge = barrow(size=_base*math.sqrt(16))
496 barrow.LARGe = barrow(size=_base*math.sqrt(32))
497 barrow.LARGE = barrow(size=_base*math.sqrt(64))
499 # arrows at end of path
500 earrow = arrow(position=1)
501 earrow.SMALL = earrow(size=_base/math.sqrt(64))
502 earrow.SMALl = earrow(size=_base/math.sqrt(32))
503 earrow.SMAll = earrow(size=_base/math.sqrt(16))
504 earrow.SMall = earrow(size=_base/math.sqrt(8))
505 earrow.Small = earrow(size=_base/math.sqrt(4))
506 earrow.small = earrow(size=_base/math.sqrt(2))
507 earrow.normal = earrow(size=_base)
508 earrow.large = earrow(size=_base*math.sqrt(2))
509 earrow.Large = earrow(size=_base*math.sqrt(4))
510 earrow.LArge = earrow(size=_base*math.sqrt(8))
511 earrow.LARge = earrow(size=_base*math.sqrt(16))
512 earrow.LARGe = earrow(size=_base*math.sqrt(32))
513 earrow.LARGE = earrow(size=_base*math.sqrt(64))