resources rework completed
[PyX.git] / pyx / deco.py
blob655cbf5057aee00d4ced5558ba9cd1685f477377
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, 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):
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, file=file):
159 for style in styles:
160 style.outputPS(file)
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 _writestyles(self.styles)
173 if self.fillstyles is not None:
174 file.write("newpath\n")
175 fillpath.outputPS(file)
177 if self.strokestyles is not None and strokepath is fillpath:
178 # do efficient stroking + filling if respective paths are identical
179 file.write("gsave\n")
181 if self.fillstyles:
182 _writestyles(self.fillstyles)
184 file.write("fill\n")
185 file.write("grestore\n")
187 if self.strokestyles:
188 file.write("gsave\n")
189 _writestyles(self.strokestyles)
191 file.write("stroke\n")
193 if self.strokestyles:
194 file.write("grestore\n")
195 else:
196 # only fill fillpath - for the moment
197 if self.fillstyles:
198 file.write("gsave\n")
199 _writestyles(self.fillstyles)
201 file.write("fill\n")
203 if self.fillstyles:
204 file.write("grestore\n")
206 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
207 # this is the only relevant case still left
208 # Note that a possible stroking has already been done.
210 if self.strokestyles:
211 file.write("gsave\n")
212 _writestyles(self.strokestyles)
214 file.write("newpath\n")
215 strokepath.outputPS(file)
216 file.write("stroke\n")
218 if self.strokestyles:
219 file.write("grestore\n")
221 # now, draw additional elements of decoratedpath
222 self.ornaments.outputPS(file)
224 # restore global styles
225 if self.styles:
226 file.write("grestore\n")
228 def outputPDF(self, file, writer, context):
229 # draw (stroke and/or fill) the decoratedpath on the canvas
231 def _writestyles(styles, file=file):
232 for style in styles:
233 style.outputPDF(file, writer, context)
235 def _writestrokestyles(strokestyles, file=file):
236 for style in strokestyles:
237 if isinstance(style, color.color):
238 style.outputPDF(file, writer, context(fillattr=0))
239 else:
240 style.outputPDF(file, writer, context)
242 def _writefillstyles(fillstyles, file=file):
243 for style in fillstyles:
244 if isinstance(style, color.color):
245 style.outputPDF(file, writer, context(strokeattr=0))
246 else:
247 style.outputPDF(file, writer, context)
249 if self.strokestyles is None and self.fillstyles is None:
250 raise RuntimeError("Path neither to be stroked nor filled")
252 strokepath = self.strokepath()
253 fillpath = self.path
255 # apply global styles
256 if self.styles:
257 file.write("q\n") # gsave
258 _writestyles(self.styles)
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
267 if self.fillstyles:
268 _writefillstyles(self.fillstyles)
269 if self.strokestyles:
270 _writestrokestyles(self.strokestyles)
272 file.write("B\n") # both stroke and fill
273 file.write("Q\n") # grestore
274 else:
275 # only fill fillpath - for the moment
276 if self.fillstyles:
277 file.write("q\n") # gsave
278 _writefillstyles(self.fillstyles)
280 file.write("f\n") # fill
282 if self.fillstyles:
283 file.write("Q\n") # grestore
285 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
286 # this is the only relevant case still left
287 # Note that a possible stroking has already been done.
289 if self.strokestyles:
290 file.write("q\n") # gsave
291 _writestrokestyles(self.strokestyles)
293 strokepath.outputPDF(file, writer, context)
294 file.write("S\n") # stroke
296 if self.strokestyles:
297 file.write("Q\n") # grestore
299 # now, draw additional elements of decoratedpath
300 self.ornaments.outputPDF(file, writer, context)
302 # restore global styles
303 if self.styles:
304 file.write("Q\n") # grestore
307 # Path decorators
310 class deco:
312 """decorators
314 In contrast to path styles, path decorators depend on the concrete
315 path to which they are applied. In particular, they don't make
316 sense without any path and can thus not be used in canvas.set!
320 def decorate(self, dp):
321 """apply a style to a given decoratedpath object dp
323 decorate accepts a decoratedpath object dp, applies PathStyle
324 by modifying dp in place and returning the new dp.
327 pass
330 # stroked and filled: basic decos which stroked and fill,
331 # respectively the path
334 class _stroked(deco, attr.exclusiveattr):
336 """stroked is a decorator, which draws the outline of the path"""
338 def __init__(self, styles=[]):
339 attr.exclusiveattr.__init__(self, _stroked)
340 self.styles = attr.mergeattrs(styles)
341 attr.checkattrs(self.styles, [style.strokestyle])
343 def __call__(self, styles=[]):
344 # XXX or should we also merge self.styles
345 return _stroked(styles)
347 def decorate(self, dp):
348 if dp.strokestyles is not None:
349 raise RuntimeError("Cannot stroke an already stroked path")
350 dp.strokestyles = self.styles
351 return dp
353 stroked = _stroked()
354 stroked.clear = attr.clearclass(_stroked)
357 class _filled(deco, attr.exclusiveattr):
359 """filled is a decorator, which fills the interior of the path"""
361 def __init__(self, styles=[]):
362 attr.exclusiveattr.__init__(self, _filled)
363 self.styles = attr.mergeattrs(styles)
364 attr.checkattrs(self.styles, [style.fillstyle])
366 def __call__(self, styles=[]):
367 # XXX or should we also merge self.styles
368 return _filled(styles)
370 def decorate(self, dp):
371 if dp.fillstyles is not None:
372 raise RuntimeError("Cannot fill an already filled path")
373 dp.fillstyles = self.styles
374 return dp
376 filled = _filled()
377 filled.clear = attr.clearclass(_filled)
380 # Arrows
383 # helper function which constructs the arrowhead
385 def _arrowhead(anormpath, size, angle, constrictionlen, reversed):
387 """helper routine, which returns an arrowhead from a given anormpath
389 returns arrowhead at begin of anormpath with size,
390 opening angle and constriction length constrictionlen. If constrictionlen is None, we
391 do not add a constriction.
394 if reversed:
395 anormpath = anormpath.reversed()
396 alen = anormpath.arclentoparam(size)
397 tx, ty = anormpath.atbegin()
399 # now we construct the template for our arrow but cutting
400 # the path a the corresponding length
401 arrowtemplate = anormpath.split(alen)[0]
403 # from this template, we construct the two outer curves
404 # 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
410 if constrictionlen is not None:
411 # constriction point (cx, cy) lies on path
412 cx, cy = anormpath.at(anormpath.arclentoparam(constrictionlen))
413 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
414 arrow = arrowl.reversed() << arrowr << arrowcr
415 else:
416 arrow = arrowl.reversed() << arrowr
418 arrow[-1].close()
420 return arrow
423 _base = 6 * unit.v_pt
425 class arrow(deco, attr.attr):
427 """arrow is a decorator which adds an arrow to either side of the path"""
429 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
430 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
431 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
432 self.position = position
433 self.size = size
434 self.angle = angle
435 self.constriction = constriction
437 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=helper.nodefault):
438 if attrs is None:
439 attrs = self.attrs
440 if position is None:
441 position = self.position
442 if size is None:
443 size = self.size
444 if angle is None:
445 angle = self.angle
446 if constriction is helper.nodefault:
447 constriction = self.constriction
448 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
450 def decorate(self, dp):
451 dp.ensurenormpath()
452 anormpath = dp.path
454 # calculate absolute arc length of constricition
455 # Note that we have to correct this length because the arrowtemplates are rotated
456 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
457 # self.constriction = 1, we actually have a length which is approximately shorter
458 # by the given geometrical factor.
459 if self.constriction is not None:
460 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
461 else:
462 # if we do not want a constriction, i.e. constriction is None, we still
463 # need constrictionlen for cutting the path
464 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
465 arrowheadconstrictionlen = None
467 if self.position == 0:
468 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
469 else:
470 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
472 # add arrowhead to decoratedpath
473 dp.ornaments.draw(arrowhead, self.attrs)
475 if self.position == 0:
476 # exclude first part of the first normsubpath from stroking
477 dp.excluderange(0, min(self.size, constrictionlen))
478 else:
479 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
481 return dp
483 arrow.clear = attr.clearclass(arrow)
485 # arrows at begin of path
486 barrow = arrow(position=0)
487 barrow.SMALL = barrow(size=_base/math.sqrt(64))
488 barrow.SMALl = barrow(size=_base/math.sqrt(32))
489 barrow.SMAll = barrow(size=_base/math.sqrt(16))
490 barrow.SMall = barrow(size=_base/math.sqrt(8))
491 barrow.Small = barrow(size=_base/math.sqrt(4))
492 barrow.small = barrow(size=_base/math.sqrt(2))
493 barrow.normal = barrow(size=_base)
494 barrow.large = barrow(size=_base*math.sqrt(2))
495 barrow.Large = barrow(size=_base*math.sqrt(4))
496 barrow.LArge = barrow(size=_base*math.sqrt(8))
497 barrow.LARge = barrow(size=_base*math.sqrt(16))
498 barrow.LARGe = barrow(size=_base*math.sqrt(32))
499 barrow.LARGE = barrow(size=_base*math.sqrt(64))
501 # arrows at end of path
502 earrow = arrow(position=1)
503 earrow.SMALL = earrow(size=_base/math.sqrt(64))
504 earrow.SMALl = earrow(size=_base/math.sqrt(32))
505 earrow.SMAll = earrow(size=_base/math.sqrt(16))
506 earrow.SMall = earrow(size=_base/math.sqrt(8))
507 earrow.Small = earrow(size=_base/math.sqrt(4))
508 earrow.small = earrow(size=_base/math.sqrt(2))
509 earrow.normal = earrow(size=_base)
510 earrow.large = earrow(size=_base*math.sqrt(2))
511 earrow.Large = earrow(size=_base*math.sqrt(4))
512 earrow.LArge = earrow(size=_base*math.sqrt(8))
513 earrow.LARge = earrow(size=_base*math.sqrt(16))
514 earrow.LARGe = earrow(size=_base*math.sqrt(32))
515 earrow.LARGE = earrow(size=_base*math.sqrt(64))