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
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
30 import attr
, base
, canvas
, helper
, path
, style
, trafo
, unit
36 class decoratedpath(base
.PSCmd
):
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.
44 def __init__(self
, path
, strokepath
=None, fillpath
=None,
45 styles
=None, strokestyles
=None, fillstyles
=None,
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.,
64 self
.subcanvas
= canvas
.canvas()
66 self
.subcanvas
= subcanvas
70 return self
.subcanvas
.bbox() + self
.path
.bbox()
74 for style
in list(self
.styles
) + list(self
.fillstyles
) + list(self
.strokestyles
):
75 result
.extend(style
.prolog())
76 result
.extend(self
.subcanvas
.prolog())
79 def write(self
, file):
80 # draw (stroke and/or fill) the decoratedpath on the canvas
81 # while trying to produce an efficient output, e.g., by
82 # not writing one path two times
85 def _writestyles(styles
, file=file):
91 canvas
._gsave
().write(file)
92 _writestyles(self
.styles
)
94 if self
.fillpath
is not None:
95 canvas
._newpath
().write(file)
96 self
.fillpath
.write(file)
98 if self
.strokepath
==self
.fillpath
:
99 # do efficient stroking + filling
100 canvas
._gsave
().write(file)
103 _writestyles(self
.fillstyles
)
105 canvas
._fill
().write(file)
106 canvas
._grestore
().write(file)
108 if self
.strokestyles
:
109 canvas
._gsave
().write(file)
110 _writestyles(self
.strokestyles
)
112 canvas
._stroke
().write(file)
114 if self
.strokestyles
:
115 canvas
._grestore
().write(file)
117 # only fill fillpath - for the moment
119 canvas
._gsave
().write(file)
120 _writestyles(self
.fillstyles
)
122 canvas
._fill
().write(file)
125 canvas
._grestore
().write(file)
127 if self
.strokepath
is not None and self
.strokepath
!=self
.fillpath
:
128 # this is the only relevant case still left
129 # Note that a possible stroking has already been done.
131 if self
.strokestyles
:
132 canvas
._gsave
().write(file)
133 _writestyles(self
.strokestyles
)
135 canvas
._newpath
().write(file)
136 self
.strokepath
.write(file)
137 canvas
._stroke
().write(file)
139 if self
.strokestyles
:
140 canvas
._grestore
().write(file)
142 if not self
.strokepath
is not None and not self
.fillpath
:
143 raise RuntimeError("Path neither to be stroked nor filled")
145 # now, draw additional elements of decoratedpath
146 self
.subcanvas
.write(file)
148 # restore global styles
150 canvas
._grestore
().write(file)
160 In contrast to path styles, path decorators depend on the concrete
161 path to which they are applied. In particular, they don't make
162 sense without any path and can thus not be used in canvas.set!
166 def decorate(self
, dp
):
167 """apply a style to a given decoratedpath object dp
169 decorate accepts a decoratedpath object dp, applies PathStyle
170 by modifying dp in place and returning the new dp.
176 # stroked and filled: basic decos which stroked and fill,
177 # respectively the path
180 class _stroked(deco
, attr
.exclusiveattr
):
182 """stroked is a decorator, which draws the outline of the path"""
184 def __init__(self
, styles
=[]):
185 attr
.exclusiveattr
.__init
__(self
, _stroked
)
186 self
.styles
= attr
.mergeattrs(styles
)
187 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
189 def __call__(self
, styles
=[]):
190 # XXX or should we also merge self.styles
191 return _stroked(styles
)
193 def decorate(self
, dp
):
194 dp
.strokepath
= dp
.path
195 dp
.strokestyles
= self
.styles
199 stroked
.clear
= attr
.clearclass(_stroked
)
202 class _filled(deco
, attr
.exclusiveattr
):
204 """filled is a decorator, which fills the interior of the path"""
206 def __init__(self
, styles
=[]):
207 attr
.exclusiveattr
.__init
__(self
, _filled
)
208 self
.styles
= attr
.mergeattrs(styles
)
209 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
211 def __call__(self
, styles
=[]):
212 # XXX or should we also merge self.styles
213 return _filled(styles
)
215 def decorate(self
, dp
):
216 dp
.fillpath
= dp
.path
217 dp
.fillstyles
= self
.styles
221 filled
.clear
= attr
.clearclass(_filled
)
227 # two helper functions which construct the arrowhead and return its size, respectively
229 def _arrowheadtemplatelength(anormpath
, size
):
230 "calculate length of arrowhead template (in parametrisation of anormpath)"
232 tx
, ty
= anormpath
.begin()
234 # obtain arrow template by using path up to first intersection
235 # with circle around tip (as suggested by Michael Schindler)
236 ipar
= anormpath
.intersect(path
.circle(tx
, ty
, size
))
240 # if this doesn't work, use first order conversion from pts to
241 # the bezier curve's parametrization
242 tlen
= unit
.topt(anormpath
.tangent(0).arclength())
244 alen
= unit
.topt(size
)/tlen
245 except ArithmeticError:
246 # take maximum, we can get
247 alen
= anormpath
.range()
248 if alen
> anormpath
.range(): alen
= anormpath
.range()
253 def _arrowhead(anormpath
, size
, angle
, constriction
):
255 """helper routine, which returns an arrowhead for a normpath
257 returns arrowhead at begin of anormpath with size,
258 opening angle and relative constriction
261 alen
= _arrowheadtemplatelength(anormpath
, size
)
262 tx
, ty
= anormpath
.begin()
264 # now we construct the template for our arrow but cutting
265 # the path a the corresponding length
266 arrowtemplate
= anormpath
.split([alen
])[0]
268 # from this template, we construct the two outer curves
270 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
271 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
273 # now come the joining backward parts
275 # arrow with constriction
277 # constriction point (cx, cy) lies on path
278 cx
, cy
= anormpath
.at(constriction
*alen
)
280 arrowcr
= path
.line(*(arrowr
.end()+(cx
,cy
)))
282 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
283 arrow
.append(path
.closepath())
285 # arrow without constriction
286 arrow
= arrowl
.reversed() << arrowr
287 arrow
.append(path
.closepath())
294 class arrow(deco
, attr
.attr
):
296 """arrow is a decorator which adds an arrow to either side of the path"""
298 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
299 self
.attrs
= attr
.mergeattrs(attrs
, defaults
=[style
.linestyle
.solid
, stroked
, filled
])
300 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
301 self
.position
= position
302 self
.size
= unit
.length(size
, default_type
="v")
304 self
.constriction
= constriction
306 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=None):
310 position
= self
.position
315 if constriction
is None:
316 constriction
= self
.constriction
317 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
319 def decorate(self
, dp
):
320 # XXX raise exception error, when strokepath is not defined
322 # convert to normpath if necessary
323 if isinstance(dp
.strokepath
, path
.normpath
):
324 anormpath
= dp
.strokepath
326 anormpath
= path
.normpath(dp
.path
)
328 anormpath
= anormpath
.reversed()
330 # add arrowhead to decoratedpath
331 dp
.subcanvas
.draw(_arrowhead(anormpath
, self
.size
, self
.angle
, self
.constriction
),
334 # calculate new strokepath
335 alen
= _arrowheadtemplatelength(anormpath
, self
.size
)
336 if self
.constriction
:
337 ilen
= alen
*self
.constriction
341 # correct somewhat for rotation of arrow segments
342 ilen
= ilen
*math
.cos(math
.pi
*self
.angle
/360.0)
344 # this is the rest of the path, we have to draw
345 anormpath
= anormpath
.split([ilen
])[1]
347 # go back to original orientation, if necessary
349 anormpath
=anormpath
.reversed()
351 # set the new (shortened) strokepath
352 dp
.strokepath
=anormpath
356 arrow
.clear
= attr
.clearclass(arrow
)
358 # arrows at begin of path
359 barrow
= arrow(position
=0)
360 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
361 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
362 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
363 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
364 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
365 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
366 barrow
.normal
= barrow(size
=_base
)
367 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
368 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
369 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
370 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
371 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
372 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
374 # arrows at end of path
375 earrow
= arrow(position
=1)
376 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
377 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
378 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
379 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
380 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
381 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
382 earrow
.normal
= earrow(size
=_base
)
383 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
384 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
385 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
386 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
387 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
388 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))