path module:
[PyX/mjg.git] / pyx / deco.py
blob18f594122de0c1eb59089866f78f5d0cf25afac2
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 math
30 import attr, base, canvas, helper, path, style, trafo, unit
33 # Decorated path
36 class decoratedpath(base.PSCmd):
37 """Decorated path
39 The main purpose of this class is during the drawing
40 (stroking/filling) of a path. It collects attributes for the
41 stroke and/or fill operations.
42 """
44 def __init__(self, path, strokepath=None, fillpath=None,
45 styles=None, strokestyles=None, fillstyles=None,
46 subcanvas=None):
48 self.path = path
50 # path to be stroked or filled (or None)
51 self.strokepath = strokepath
52 self.fillpath = fillpath
54 # global style for stroking and filling and subdps
55 self.styles = helper.ensurelist(styles)
57 # styles which apply only for stroking and filling
58 self.strokestyles = helper.ensurelist(strokestyles)
59 self.fillstyles = helper.ensurelist(fillstyles)
61 # the canvas can contain additional elements of the path, e.g.,
62 # arrowheads,
63 if subcanvas is None:
64 self.subcanvas = canvas.canvas()
65 else:
66 self.subcanvas = subcanvas
69 def bbox(self):
70 scbbox = self.subcanvas.bbox()
71 pbbox = self.path.bbox()
72 if scbbox is not None:
73 return scbbox+pbbox
74 else:
75 return pbbox
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 result.extend(self.subcanvas.prolog())
82 return result
84 def write(self, file):
85 # draw (stroke and/or fill) the decoratedpath on the canvas
86 # while trying to produce an efficient output, e.g., by
87 # not writing one path two times
89 # small helper
90 def _writestyles(styles, file=file):
91 for style in styles:
92 style.write(file)
94 # apply global styles
95 if self.styles:
96 canvas._gsave().write(file)
97 _writestyles(self.styles)
99 if self.fillpath is not None:
100 canvas._newpath().write(file)
101 self.fillpath.write(file)
103 if self.strokepath==self.fillpath:
104 # do efficient stroking + filling
105 canvas._gsave().write(file)
107 if self.fillstyles:
108 _writestyles(self.fillstyles)
110 canvas._fill().write(file)
111 canvas._grestore().write(file)
113 if self.strokestyles:
114 canvas._gsave().write(file)
115 _writestyles(self.strokestyles)
117 canvas._stroke().write(file)
119 if self.strokestyles:
120 canvas._grestore().write(file)
121 else:
122 # only fill fillpath - for the moment
123 if self.fillstyles:
124 canvas._gsave().write(file)
125 _writestyles(self.fillstyles)
127 canvas._fill().write(file)
129 if self.fillstyles:
130 canvas._grestore().write(file)
132 if self.strokepath is not None and self.strokepath!=self.fillpath:
133 # this is the only relevant case still left
134 # Note that a possible stroking has already been done.
136 if self.strokestyles:
137 canvas._gsave().write(file)
138 _writestyles(self.strokestyles)
140 canvas._newpath().write(file)
141 self.strokepath.write(file)
142 canvas._stroke().write(file)
144 if self.strokestyles:
145 canvas._grestore().write(file)
147 if not self.strokepath is not None and not self.fillpath:
148 raise RuntimeError("Path neither to be stroked nor filled")
150 # now, draw additional elements of decoratedpath
151 self.subcanvas.write(file)
153 # restore global styles
154 if self.styles:
155 canvas._grestore().write(file)
158 # Path decorators
161 class deco:
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, attr.exclusiveattr):
187 """stroked is a decorator, which draws the outline of the path"""
189 def __init__(self, styles=[]):
190 attr.exclusiveattr.__init__(self, _stroked)
191 self.styles = attr.mergeattrs(styles)
192 attr.checkattrs(self.styles, [style.strokestyle])
194 def __call__(self, styles=[]):
195 # XXX or should we also merge self.styles
196 return _stroked(styles)
198 def decorate(self, dp):
199 dp.strokepath = dp.path
200 dp.strokestyles = self.styles
201 return dp
203 stroked = _stroked()
204 stroked.clear = attr.clearclass(_stroked)
207 class _filled(deco, attr.exclusiveattr):
209 """filled is a decorator, which fills the interior of the path"""
211 def __init__(self, styles=[]):
212 attr.exclusiveattr.__init__(self, _filled)
213 self.styles = attr.mergeattrs(styles)
214 attr.checkattrs(self.styles, [style.fillstyle])
216 def __call__(self, styles=[]):
217 # XXX or should we also merge self.styles
218 return _filled(styles)
220 def decorate(self, dp):
221 dp.fillpath = dp.path
222 dp.fillstyles = self.styles
223 return dp
225 filled = _filled()
226 filled.clear = attr.clearclass(_filled)
229 # Arrows
232 # two helper functions which construct the arrowhead and return its size, respectively
234 def _arrowheadtemplatelength(anormpath, size):
235 "calculate length of arrowhead template (in parametrisation of anormpath)"
236 # get tip (tx, ty)
237 tx, ty = anormpath.begin()
239 # obtain arrow template by using path up to first intersection
240 # with circle around tip (as suggested by Michael Schindler)
241 ipar = anormpath.intersect(path.circle(tx, ty, size))
242 if ipar[0]:
243 alen = ipar[0][0]
244 else:
245 # if this doesn't work, use first order conversion from pts to
246 # the bezier curve's parametrization
247 tlen = anormpath.tangent(0).arclength_pt()
248 try:
249 alen = unit.topt(size)/tlen
250 except ArithmeticError:
251 # take maximum, we can get
252 alen = anormpath.range()
253 if alen > anormpath.range(): alen = anormpath.range()
255 return alen
258 def _arrowhead(anormpath, size, angle, constriction):
260 """helper routine, which returns an arrowhead for a normpath
262 returns arrowhead at begin of anormpath with size,
263 opening angle and relative constriction
266 alen = _arrowheadtemplatelength(anormpath, size)
267 tx, ty = anormpath.begin()
269 # now we construct the template for our arrow but cutting
270 # the path a the corresponding length
271 arrowtemplate = anormpath.split([alen])[0]
273 # from this template, we construct the two outer curves
274 # of the arrow
275 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
276 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
278 # now come the joining backward parts
279 if constriction:
280 # arrow with constriction
282 # constriction point (cx, cy) lies on path
283 cx, cy = anormpath.at(constriction*alen)
285 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
287 arrow = arrowl.reversed() << arrowr << arrowcr
288 arrow.append(path.closepath())
289 else:
290 # arrow without constriction
291 arrow = arrowl.reversed() << arrowr
292 arrow.append(path.closepath())
294 return arrow
297 _base = unit.v_pt(4)
299 class arrow(deco, attr.attr):
301 """arrow is a decorator which adds an arrow to either side of the path"""
303 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
304 self.attrs = attr.mergeattrs([style.linestyle.solid, stroked, filled] + attrs)
305 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
306 self.position = position
307 self.size = unit.length(size, default_type="v")
308 self.angle = angle
309 self.constriction = constriction
311 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=None):
312 if attrs is None:
313 attrs = self.attrs
314 if position is None:
315 position = self.position
316 if size is None:
317 size = self.size
318 if angle is None:
319 angle = self.angle
320 if constriction is None:
321 constriction = self.constriction
322 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
324 def decorate(self, dp):
325 # XXX raise exception error, when strokepath is not defined
327 # convert to normpath if necessary
328 if isinstance(dp.strokepath, path.normpath):
329 anormpath = dp.strokepath
330 else:
331 anormpath = path.normpath(dp.path)
332 if self.position:
333 anormpath = anormpath.reversed()
335 # add arrowhead to decoratedpath
336 dp.subcanvas.draw(_arrowhead(anormpath, self.size, self.angle, self.constriction),
337 self.attrs)
339 # calculate new strokepath
340 alen = _arrowheadtemplatelength(anormpath, self.size)
341 if self.constriction:
342 ilen = alen*self.constriction
343 else:
344 ilen = alen
346 # correct somewhat for rotation of arrow segments
347 ilen = ilen*math.cos(math.pi*self.angle/360.0)
349 # this is the rest of the path, we have to draw
350 anormpath = anormpath.split([ilen])[1]
352 # go back to original orientation, if necessary
353 if self.position:
354 anormpath=anormpath.reversed()
356 # set the new (shortened) strokepath
357 dp.strokepath=anormpath
359 return dp
361 arrow.clear = attr.clearclass(arrow)
363 # arrows at begin of path
364 barrow = arrow(position=0)
365 barrow.SMALL = barrow(size=_base/math.sqrt(64))
366 barrow.SMALl = barrow(size=_base/math.sqrt(32))
367 barrow.SMAll = barrow(size=_base/math.sqrt(16))
368 barrow.SMall = barrow(size=_base/math.sqrt(8))
369 barrow.Small = barrow(size=_base/math.sqrt(4))
370 barrow.small = barrow(size=_base/math.sqrt(2))
371 barrow.normal = barrow(size=_base)
372 barrow.large = barrow(size=_base*math.sqrt(2))
373 barrow.Large = barrow(size=_base*math.sqrt(4))
374 barrow.LArge = barrow(size=_base*math.sqrt(8))
375 barrow.LARge = barrow(size=_base*math.sqrt(16))
376 barrow.LARGe = barrow(size=_base*math.sqrt(32))
377 barrow.LARGE = barrow(size=_base*math.sqrt(64))
379 # arrows at end of path
380 earrow = arrow(position=1)
381 earrow.SMALL = earrow(size=_base/math.sqrt(64))
382 earrow.SMALl = earrow(size=_base/math.sqrt(32))
383 earrow.SMAll = earrow(size=_base/math.sqrt(16))
384 earrow.SMall = earrow(size=_base/math.sqrt(8))
385 earrow.Small = earrow(size=_base/math.sqrt(4))
386 earrow.small = earrow(size=_base/math.sqrt(2))
387 earrow.normal = earrow(size=_base)
388 earrow.large = earrow(size=_base*math.sqrt(2))
389 earrow.Large = earrow(size=_base*math.sqrt(4))
390 earrow.LArge = earrow(size=_base*math.sqrt(8))
391 earrow.LARge = earrow(size=_base*math.sqrt(16))
392 earrow.LARGe = earrow(size=_base*math.sqrt(32))
393 earrow.LARGE = earrow(size=_base*math.sqrt(64))