further attribute work
[PyX/mjg.git] / pyx / deco.py
blob4c5390af75669218f88f8a481c04b269b1f36980
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002, 2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002, 2003 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?
28 # - How should we handle the passing of stroke and fill styles to
29 # arrows? Calls, new instances, ...?
31 import math
32 import attr, base, canvas, helper, path, trafo, unit
35 # Decorated path
38 class decoratedpath(base.PSCmd):
39 """Decorated path
41 The main purpose of this class is during the drawing
42 (stroking/filling) of a path. It collects attributes for the
43 stroke and/or fill operations.
44 """
46 def __init__(self,
47 path, strokepath=None, fillpath=None,
48 styles=None, strokestyles=None, fillstyles=None,
49 subdps=None):
51 self.path = path
53 # path to be stroked or filled (or None)
54 self.strokepath = strokepath
55 self.fillpath = fillpath
57 # global style for stroking and filling and subdps
58 self.styles = helper.ensurelist(styles)
60 # styles which apply only for stroking and filling
61 self.strokestyles = helper.ensurelist(strokestyles)
62 self.fillstyles = helper.ensurelist(fillstyles)
64 # additional elements of the path, e.g., arrowheads,
65 # which are by themselves decoratedpaths
66 self.subdps = helper.ensurelist(subdps)
68 def addsubdp(self, subdp):
69 """add a further decorated path to the list of subdps"""
70 self.subdps.append(subdp)
72 def bbox(self):
73 return reduce(lambda x,y: x+y.bbox(),
74 self.subdps,
75 self.path.bbox())
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 return result
83 def write(self, file):
84 # draw (stroke and/or fill) the decoratedpath on the canvas
85 # while trying to produce an efficient output, e.g., by
86 # not writing one path two times
88 # small helper
89 def _writestyles(styles, file=file):
90 for style in styles:
91 style.write(file)
93 # apply global styles
94 if self.styles:
95 canvas._gsave().write(file)
96 _writestyles(self.styles)
98 if self.fillpath is not None:
99 canvas._newpath().write(file)
100 self.fillpath.write(file)
102 if self.strokepath==self.fillpath:
103 # do efficient stroking + filling
104 canvas._gsave().write(file)
106 if self.fillstyles:
107 _writestyles(self.fillstyles)
109 canvas._fill().write(file)
110 canvas._grestore().write(file)
112 if self.strokestyles:
113 canvas._gsave().write(file)
114 _writestyles(self.strokestyles)
116 canvas._stroke().write(file)
118 if self.strokestyles:
119 canvas._grestore().write(file)
120 else:
121 # only fill fillpath - for the moment
122 if self.fillstyles:
123 canvas._gsave().write(file)
124 _writestyles(self.fillstyles)
126 canvas._fill().write(file)
128 if self.fillstyles:
129 canvas._grestore().write(file)
131 if self.strokepath is not None and self.strokepath!=self.fillpath:
132 # this is the only relevant case still left
133 # Note that a possible stroking has already been done.
135 if self.strokestyles:
136 _gsave().write(file)
137 _writestyles(self.strokestyles)
139 canvas._newpath().write(file)
140 self.strokepath.write(file)
141 canvas._stroke().write(file)
143 if self.strokestyles:
144 canvas._grestore().write(file)
146 if not self.strokepath is not None and not self.fillpath:
147 raise RuntimeError("Path neither to be stroked nor filled")
149 # now, draw additional subdps
150 for subdp in self.subdps:
151 subdp.write(file)
153 # restore global styles
154 if self.styles:
155 canvas._grestore().write(file)
158 # Path decorators
161 class deco(attr.attr):
163 """decorators
165 In contrast to path styles, path decorators depend on the concrete
166 path to which they are applied. In particular, they don't make
167 sense without any path and can thus not be used in canvas.set!
171 def decorate(self, dp):
172 """apply a style to a given decoratedpath object dp
174 decorate accepts a decoratedpath object dp, applies PathStyle
175 by modifying dp in place and returning the new dp.
178 pass
181 # stroked and filled: basic decos which stroked and fill,
182 # respectively the path
185 class stroked(deco):
187 """stroked is a decorator, which draws the outline of the path"""
189 def __init__(self, *styles):
190 self.styles = list(styles)
192 def decorate(self, dp):
193 dp.strokepath = dp.path
194 dp.strokestyles = self.styles
196 return dp
198 stroked.clear = attr.clearclass(stroked)
201 class filled(deco):
203 """filled is a decorator, which fills the interior of the path"""
205 def __init__(self, *styles):
206 self.styles = list(styles)
208 def decorate(self, dp):
209 dp.fillpath = dp.path
210 dp.fillstyles = self.styles
212 return dp
214 filled.clear = attr.clearclass(filled)
217 def _arrowheadtemplatelength(anormpath, size):
218 "calculate length of arrowhead template (in parametrisation of anormpath)"
219 # get tip (tx, ty)
220 tx, ty = anormpath.begin()
222 # obtain arrow template by using path up to first intersection
223 # with circle around tip (as suggested by Michael Schindler)
224 ipar = anormpath.intersect(path.circle(tx, ty, size))
225 if ipar[0]:
226 alen = ipar[0][0]
227 else:
228 # if this doesn't work, use first order conversion from pts to
229 # the bezier curve's parametrization
230 tlen = unit.topt(anormpath.tangent(0).arclength())
231 try:
232 alen = unit.topt(size)/tlen
233 except ArithmeticError:
234 # take maximum, we can get
235 alen = anormpath.range()
236 if alen > anormpath.range(): alen = anormpath.range()
238 return alen
241 def _arrowhead(anormpath, size, angle, constriction):
243 """helper routine, which returns an arrowhead for a normpath
245 returns arrowhead at begin of anormpath with size,
246 opening angle and relative constriction
249 alen = _arrowheadtemplatelength(anormpath, size)
250 tx, ty = anormpath.begin()
252 # now we construct the template for our arrow but cutting
253 # the path a the corresponding length
254 arrowtemplate = anormpath.split(alen)[0]
256 # from this template, we construct the two outer curves
257 # of the arrow
258 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
259 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
261 # now come the joining backward parts
262 if constriction:
263 # arrow with constriction
265 # constriction point (cx, cy) lies on path
266 cx, cy = anormpath.at(constriction*alen)
268 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
270 arrow = arrowl.reversed() << arrowr << arrowcr
271 arrow.append(path.closepath())
272 else:
273 # arrow without constriction
274 arrow = arrowl.reversed() << arrowr
275 arrow.append(path.closepath())
277 return arrow
279 # XXX rewrite arrow without using __call__
280 # XXX do not forget arrow.clear
282 class arrow(deco):
284 """arrow is a decorator which adds an arrow to either side of the path"""
286 def __init__(self,
287 position, size, angle=45, constriction=0.8,
288 styles=None, strokestyles=None, fillstyles=None):
289 self.position = position
290 self.size = size
291 self.angle = angle
292 self.constriction = constriction
293 self.styles = helper.ensurelist(styles)
294 self.strokestyles = helper.ensurelist(strokestyles)
295 self.fillstyles = helper.ensurelist(fillstyles)
297 def __call__(self, *styles):
298 fillstyles = [ style for s in styles if isinstance(s, filled)
299 for style in s.styles ]
301 strokestyles = [ style for s in styles if isinstance(s, stroked)
302 for style in s.styles ]
304 styles = [ style for style in styles
305 if not (isinstance(style, filled) or
306 isinstance(style, stroked)) ]
308 return arrow(position=self.position,
309 size=self.size,
310 angle=self.angle,
311 constriction=self.constriction,
312 styles=styles,
313 strokestyles=strokestyles,
314 fillstyles=fillstyles)
316 def decorate(self, dp):
318 # TODO: error, when strokepath is not defined
320 # convert to normpath if necessary
321 if isinstance(dp.strokepath, path.normpath):
322 anormpath=dp.strokepath
323 else:
324 anormpath=path.normpath(dp.path)
326 if self.position:
327 anormpath=anormpath.reversed()
329 ahead = _arrowhead(anormpath, self.size, self.angle, self.constriction)
331 dp.addsubdp(decoratedpath(ahead,
332 strokepath=ahead, fillpath=ahead,
333 styles=self.styles,
334 strokestyles=self.strokestyles,
335 fillstyles=self.fillstyles))
337 alen = _arrowheadtemplatelength(anormpath, self.size)
339 if self.constriction:
340 ilen = alen*self.constriction
341 else:
342 ilen = alen
344 # correct somewhat for rotation of arrow segments
345 ilen = ilen*math.cos(math.pi*self.angle/360.0)
347 # this is the rest of the path, we have to draw
348 anormpath = anormpath.split(ilen)[1]
350 # go back to original orientation, if necessary
351 if self.position:
352 anormpath=anormpath.reversed()
354 # set the new (shortened) strokepath
355 dp.strokepath=anormpath
357 return dp
360 class barrow(arrow):
362 """arrow at begin of path"""
364 def __init__(self, size, angle=45, constriction=0.8,
365 styles=None, strokestyles=None, fillstyles=None):
366 arrow.__init__(self,
367 position=0,
368 size=size,
369 angle=angle,
370 constriction=constriction,
371 styles=styles,
372 strokestyles=strokestyles,
373 fillstyles=fillstyles)
375 _base = unit.v_pt(4)
377 barrow.SMALL = barrow(_base/math.sqrt(64))
378 barrow.SMALl = barrow(_base/math.sqrt(32))
379 barrow.SMAll = barrow(_base/math.sqrt(16))
380 barrow.SMall = barrow(_base/math.sqrt(8))
381 barrow.Small = barrow(_base/math.sqrt(4))
382 barrow.small = barrow(_base/math.sqrt(2))
383 barrow.normal = barrow(_base)
384 barrow.large = barrow(_base*math.sqrt(2))
385 barrow.Large = barrow(_base*math.sqrt(4))
386 barrow.LArge = barrow(_base*math.sqrt(8))
387 barrow.LARge = barrow(_base*math.sqrt(16))
388 barrow.LARGe = barrow(_base*math.sqrt(32))
389 barrow.LARGE = barrow(_base*math.sqrt(64))
392 class earrow(arrow):
394 """arrow at end of path"""
396 def __init__(self, size, angle=45, constriction=0.8,
397 styles=[], strokestyles=[], fillstyles=[]):
398 arrow.__init__(self,
399 position=1,
400 size=size,
401 angle=angle,
402 constriction=constriction,
403 styles=styles,
404 strokestyles=strokestyles,
405 fillstyles=fillstyles)
408 earrow.SMALL = earrow(_base/math.sqrt(64))
409 earrow.SMALl = earrow(_base/math.sqrt(32))
410 earrow.SMAll = earrow(_base/math.sqrt(16))
411 earrow.SMall = earrow(_base/math.sqrt(8))
412 earrow.Small = earrow(_base/math.sqrt(4))
413 earrow.small = earrow(_base/math.sqrt(2))
414 earrow.normal = earrow(_base)
415 earrow.large = earrow(_base*math.sqrt(2))
416 earrow.Large = earrow(_base*math.sqrt(4))
417 earrow.LArge = earrow(_base*math.sqrt(8))
418 earrow.LARge = earrow(_base*math.sqrt(16))
419 earrow.LARGe = earrow(_base*math.sqrt(32))
420 earrow.LARGE = earrow(_base*math.sqrt(64))