Replace prolog module by new, more generic resource module and replace prolog
[PyX/mjg.git] / pyx / deco.py
blob7daeda6b9ed07086dfd5cfe2804888d5cd9048f1
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., 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 registerresources(self, registry):
114 if self.styles:
115 for style in self.styles:
116 style.registerresources(registry)
117 if self.fillstyles:
118 for style in self.fillstyles:
119 style.registerresources(registry)
120 if self.strokestyles:
121 for style in self.strokestyles:
122 style.registerresources(registry)
123 self.ornaments.registerresources(registry)
125 def strokepath(self):
126 if self.nostrokeranges:
127 splitlist = []
128 for begin, end in self.nostrokeranges:
129 splitlist.append(begin)
130 splitlist.append(end)
131 split = self.path.split(splitlist)
132 # XXX properly handle closed paths?
133 result = split[0]
134 for i in range(2, len(split), 2):
135 result += split[i]
136 return result
137 else:
138 return self.path
140 def outputPS(self, file):
141 # draw (stroke and/or fill) the decoratedpath on the canvas
142 # while trying to produce an efficient output, e.g., by
143 # not writing one path two times
145 # small helper
146 def _writestyles(styles, file=file):
147 for style in styles:
148 style.outputPS(file)
150 if self.strokestyles is None and self.fillstyles is None:
151 raise RuntimeError("Path neither to be stroked nor filled")
153 strokepath = self.strokepath()
154 fillpath = self.path
156 # apply global styles
157 if self.styles:
158 file.write("gsave\n")
159 _writestyles(self.styles)
161 if self.fillstyles is not None:
162 file.write("newpath\n")
163 fillpath.outputPS(file)
165 if self.strokestyles is not None and strokepath is fillpath:
166 # do efficient stroking + filling if respective paths are identical
167 file.write("gsave\n")
169 if self.fillstyles:
170 _writestyles(self.fillstyles)
172 file.write("fill\n")
173 file.write("grestore\n")
175 if self.strokestyles:
176 file.write("gsave\n")
177 _writestyles(self.strokestyles)
179 file.write("stroke\n")
181 if self.strokestyles:
182 file.write("grestore\n")
183 else:
184 # only fill fillpath - for the moment
185 if self.fillstyles:
186 file.write("gsave\n")
187 _writestyles(self.fillstyles)
189 file.write("fill\n")
191 if self.fillstyles:
192 file.write("grestore\n")
194 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
195 # this is the only relevant case still left
196 # Note that a possible stroking has already been done.
198 if self.strokestyles:
199 file.write("gsave\n")
200 _writestyles(self.strokestyles)
202 file.write("newpath\n")
203 strokepath.outputPS(file)
204 file.write("stroke\n")
206 if self.strokestyles:
207 file.write("grestore\n")
209 # now, draw additional elements of decoratedpath
210 self.ornaments.outputPS(file)
212 # restore global styles
213 if self.styles:
214 file.write("grestore\n")
216 def outputPDF(self, file):
217 # draw (stroke and/or fill) the decoratedpath on the canvas
219 def _writestyles(styles, file=file):
220 for style in styles:
221 style.outputPDF(file)
223 def _writestrokestyles(strokestyles, file=file):
224 for style in strokestyles:
225 if isinstance(style, color.color):
226 style.outputPDF(file, fillattr=0)
227 else:
228 style.outputPDF(file)
230 def _writefillstyles(fillstyles, file=file):
231 for style in fillstyles:
232 if isinstance(style, color.color):
233 style.outputPDF(file, strokeattr=0)
234 else:
235 style.outputPDF(file)
237 if self.strokestyles is None and self.fillstyles is None:
238 raise RuntimeError("Path neither to be stroked nor filled")
240 strokepath = self.strokepath()
241 fillpath = self.path
243 # apply global styles
244 if self.styles:
245 file.write("q\n") # gsave
246 _writestyles(self.styles)
248 if self.fillstyles is not None:
249 fillpath.outputPDF(file)
251 if self.strokestyles is not None and strokepath is fillpath:
252 # do efficient stroking + filling
253 file.write("q\n") # gsave
255 if self.fillstyles:
256 _writefillstyles(self.fillstyles)
257 if self.strokestyles:
258 _writestrokestyles(self.strokestyles)
260 file.write("B\n") # both stroke and fill
261 file.write("Q\n") # grestore
262 else:
263 # only fill fillpath - for the moment
264 if self.fillstyles:
265 file.write("q\n") # gsave
266 _writefillstyles(self.fillstyles)
268 file.write("f\n") # fill
270 if self.fillstyles:
271 file.write("Q\n") # grestore
273 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
274 # this is the only relevant case still left
275 # Note that a possible stroking has already been done.
277 if self.strokestyles:
278 file.write("q\n") # gsave
279 _writestrokestyles(self.strokestyles)
281 strokepath.outputPDF(file)
282 file.write("S\n") # stroke
284 if self.strokestyles:
285 file.write("Q\n") # grestore
287 # now, draw additional elements of decoratedpath
288 self.ornaments.outputPDF(file)
290 # restore global styles
291 if self.styles:
292 file.write("Q\n") # grestore
295 # Path decorators
298 class deco:
300 """decorators
302 In contrast to path styles, path decorators depend on the concrete
303 path to which they are applied. In particular, they don't make
304 sense without any path and can thus not be used in canvas.set!
308 def decorate(self, dp):
309 """apply a style to a given decoratedpath object dp
311 decorate accepts a decoratedpath object dp, applies PathStyle
312 by modifying dp in place and returning the new dp.
315 pass
318 # stroked and filled: basic decos which stroked and fill,
319 # respectively the path
322 class _stroked(deco, attr.exclusiveattr):
324 """stroked is a decorator, which draws the outline of the path"""
326 def __init__(self, styles=[]):
327 attr.exclusiveattr.__init__(self, _stroked)
328 self.styles = attr.mergeattrs(styles)
329 attr.checkattrs(self.styles, [style.strokestyle])
331 def __call__(self, styles=[]):
332 # XXX or should we also merge self.styles
333 return _stroked(styles)
335 def decorate(self, dp):
336 if dp.strokestyles is not None:
337 raise RuntimeError("Cannot stroke an already stroked path")
338 dp.strokestyles = self.styles
339 return dp
341 stroked = _stroked()
342 stroked.clear = attr.clearclass(_stroked)
345 class _filled(deco, attr.exclusiveattr):
347 """filled is a decorator, which fills the interior of the path"""
349 def __init__(self, styles=[]):
350 attr.exclusiveattr.__init__(self, _filled)
351 self.styles = attr.mergeattrs(styles)
352 attr.checkattrs(self.styles, [style.fillstyle])
354 def __call__(self, styles=[]):
355 # XXX or should we also merge self.styles
356 return _filled(styles)
358 def decorate(self, dp):
359 if dp.fillstyles is not None:
360 raise RuntimeError("Cannot fill an already filled path")
361 dp.fillstyles = self.styles
362 return dp
364 filled = _filled()
365 filled.clear = attr.clearclass(_filled)
368 # Arrows
371 # helper function which constructs the arrowhead
373 def _arrowhead(anormpath, size, angle, constrictionlen, reversed):
375 """helper routine, which returns an arrowhead from a given anormpath
377 returns arrowhead at begin of anormpath with size,
378 opening angle and constriction length constrictionlen. If constrictionlen is None, we
379 do not add a constriction.
382 if reversed:
383 anormpath = anormpath.reversed()
384 alen = anormpath.arclentoparam(size)
385 tx, ty = anormpath.atbegin()
387 # now we construct the template for our arrow but cutting
388 # the path a the corresponding length
389 arrowtemplate = anormpath.split(alen)[0]
391 # from this template, we construct the two outer curves
392 # of the arrow
393 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
394 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
396 # now come the joining backward parts
398 if constrictionlen is not None:
399 # constriction point (cx, cy) lies on path
400 cx, cy = anormpath.at(anormpath.arclentoparam(constrictionlen))
401 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
402 arrow = arrowl.reversed() << arrowr << arrowcr
403 else:
404 arrow = arrowl.reversed() << arrowr
406 arrow[-1].close()
408 return arrow
411 _base = 6 * unit.v_pt
413 class arrow(deco, attr.attr):
415 """arrow is a decorator which adds an arrow to either side of the path"""
417 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
418 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
419 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
420 self.position = position
421 self.size = size
422 self.angle = angle
423 self.constriction = constriction
425 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=helper.nodefault):
426 if attrs is None:
427 attrs = self.attrs
428 if position is None:
429 position = self.position
430 if size is None:
431 size = self.size
432 if angle is None:
433 angle = self.angle
434 if constriction is helper.nodefault:
435 constriction = self.constriction
436 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
438 def decorate(self, dp):
439 dp.ensurenormpath()
440 anormpath = dp.path
442 # calculate absolute arc length of constricition
443 # Note that we have to correct this length because the arrowtemplates are rotated
444 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
445 # self.constriction = 1, we actually have a length which is approximately shorter
446 # by the given geometrical factor.
447 if self.constriction is not None:
448 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
449 else:
450 # if we do not want a constriction, i.e. constriction is None, we still
451 # need constrictionlen for cutting the path
452 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
453 arrowheadconstrictionlen = None
455 if self.position == 0:
456 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
457 else:
458 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
460 # add arrowhead to decoratedpath
461 dp.ornaments.draw(arrowhead, self.attrs)
463 if self.position == 0:
464 # exclude first part of the first normsubpath from stroking
465 dp.excluderange(0, min(self.size, constrictionlen))
466 else:
467 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
469 return dp
471 arrow.clear = attr.clearclass(arrow)
473 # arrows at begin of path
474 barrow = arrow(position=0)
475 barrow.SMALL = barrow(size=_base/math.sqrt(64))
476 barrow.SMALl = barrow(size=_base/math.sqrt(32))
477 barrow.SMAll = barrow(size=_base/math.sqrt(16))
478 barrow.SMall = barrow(size=_base/math.sqrt(8))
479 barrow.Small = barrow(size=_base/math.sqrt(4))
480 barrow.small = barrow(size=_base/math.sqrt(2))
481 barrow.normal = barrow(size=_base)
482 barrow.large = barrow(size=_base*math.sqrt(2))
483 barrow.Large = barrow(size=_base*math.sqrt(4))
484 barrow.LArge = barrow(size=_base*math.sqrt(8))
485 barrow.LARge = barrow(size=_base*math.sqrt(16))
486 barrow.LARGe = barrow(size=_base*math.sqrt(32))
487 barrow.LARGE = barrow(size=_base*math.sqrt(64))
489 # arrows at end of path
490 earrow = arrow(position=1)
491 earrow.SMALL = earrow(size=_base/math.sqrt(64))
492 earrow.SMALl = earrow(size=_base/math.sqrt(32))
493 earrow.SMAll = earrow(size=_base/math.sqrt(16))
494 earrow.SMall = earrow(size=_base/math.sqrt(8))
495 earrow.Small = earrow(size=_base/math.sqrt(4))
496 earrow.small = earrow(size=_base/math.sqrt(2))
497 earrow.normal = earrow(size=_base)
498 earrow.large = earrow(size=_base*math.sqrt(2))
499 earrow.Large = earrow(size=_base*math.sqrt(4))
500 earrow.LArge = earrow(size=_base*math.sqrt(8))
501 earrow.LARge = earrow(size=_base*math.sqrt(16))
502 earrow.LARGe = earrow(size=_base*math.sqrt(32))
503 earrow.LARGE = earrow(size=_base*math.sqrt(64))