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
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
30 import attr
, canvas
, color
, helper
, path
, style
, trafo
, unit
33 from math
import radians
35 # fallback implementation for Python 2.1 and below
36 def radians(x
): return x
*math
.pi
/180
42 class decoratedpath(canvas
.canvasitem
):
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.
50 def __init__(self
, path
, strokepath
=None, fillpath
=None,
51 styles
=None, strokestyles
=None, fillstyles
=None,
56 # global style for stroking and filling and subdps
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.
66 self
.ornaments
= canvas
.canvas()
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
)]
83 while ibegin
< len(self
.nostrokeranges
) and self
.nostrokeranges
[ibegin
][1] < begin
:
86 if ibegin
== len(self
.nostrokeranges
):
87 self
.nostrokeranges
.append((begin
, end
))
90 iend
= len(self
.nostrokeranges
) - 1
91 while 0 <= iend
and end
< self
.nostrokeranges
[iend
][0]:
95 self
.nostrokeranges
.insert(0, (begin
, end
))
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
)]
106 pathbbox
= self
.path
.bbox()
107 ornamentsbbox
= self
.ornaments
.bbox()
108 if ornamentsbbox
is not None:
109 return ornamentsbbox
+ pathbbox
113 def registerPS(self
, registry
):
115 for style
in self
.styles
:
116 style
.registerPS(registry
)
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
):
127 for style
in self
.styles
:
128 style
.registerPDF(registry
)
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
:
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?
146 for i
in range(2, len(split
), 2):
152 def outputPS(self
, file, writer
, context
):
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
158 def _writestyles(styles
, file=file):
160 style
.outputPS(file, writer
, context
)
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()
168 # apply global 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, writer
, context
)
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")
182 _writestyles(self
.fillstyles
)
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")
196 # only fill fillpath - for the moment
198 file.write("gsave\n")
199 _writestyles(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, writer
, context
)
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, writer
, context
)
224 # restore global 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):
233 style
.outputPDF(file, writer
, context
)
235 def _writestrokestyles(strokestyles
, file=file):
236 for style
in strokestyles
:
237 style
.outputPDF(file, writer
, context(fillattr
=0))
239 def _writefillstyles(fillstyles
, file=file):
240 for style
in fillstyles
:
241 style
.outputPDF(file, writer
, context(strokeattr
=0))
243 if self
.strokestyles
is None and self
.fillstyles
is None:
244 raise RuntimeError("Path neither to be stroked nor filled")
246 strokepath
= self
.strokepath()
249 # apply global styles
251 file.write("q\n") # gsave
252 _writestyles(self
.styles
)
254 if self
.fillstyles
is not None:
255 fillpath
.outputPDF(file, writer
, context
)
257 if self
.strokestyles
is not None and strokepath
is fillpath
:
258 # do efficient stroking + filling
259 file.write("q\n") # gsave
262 _writefillstyles(self
.fillstyles
)
263 if self
.strokestyles
:
264 _writestrokestyles(self
.strokestyles
)
266 file.write("B\n") # both stroke and fill
267 file.write("Q\n") # grestore
269 # only fill fillpath - for the moment
271 file.write("q\n") # gsave
272 _writefillstyles(self
.fillstyles
)
274 file.write("f\n") # fill
277 file.write("Q\n") # grestore
279 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
280 # this is the only relevant case still left
281 # Note that a possible stroking has already been done.
283 if self
.strokestyles
:
284 file.write("q\n") # gsave
285 _writestrokestyles(self
.strokestyles
)
287 strokepath
.outputPDF(file, writer
, context
)
288 file.write("S\n") # stroke
290 if self
.strokestyles
:
291 file.write("Q\n") # grestore
293 # now, draw additional elements of decoratedpath
294 self
.ornaments
.outputPDF(file, writer
, context
)
296 # restore global styles
298 file.write("Q\n") # grestore
308 In contrast to path styles, path decorators depend on the concrete
309 path to which they are applied. In particular, they don't make
310 sense without any path and can thus not be used in canvas.set!
314 def decorate(self
, dp
):
315 """apply a style to a given decoratedpath object dp
317 decorate accepts a decoratedpath object dp, applies PathStyle
318 by modifying dp in place and returning the new dp.
324 # stroked and filled: basic decos which stroked and fill,
325 # respectively the path
328 class _stroked(deco
, attr
.exclusiveattr
):
330 """stroked is a decorator, which draws the outline of the path"""
332 def __init__(self
, styles
=[]):
333 attr
.exclusiveattr
.__init
__(self
, _stroked
)
334 self
.styles
= attr
.mergeattrs(styles
)
335 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
337 def __call__(self
, styles
=[]):
338 # XXX or should we also merge self.styles
339 return _stroked(styles
)
341 def decorate(self
, dp
):
342 if dp
.strokestyles
is not None:
343 raise RuntimeError("Cannot stroke an already stroked path")
344 dp
.strokestyles
= self
.styles
348 stroked
.clear
= attr
.clearclass(_stroked
)
351 class _filled(deco
, attr
.exclusiveattr
):
353 """filled is a decorator, which fills the interior of the path"""
355 def __init__(self
, styles
=[]):
356 attr
.exclusiveattr
.__init
__(self
, _filled
)
357 self
.styles
= attr
.mergeattrs(styles
)
358 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
360 def __call__(self
, styles
=[]):
361 # XXX or should we also merge self.styles
362 return _filled(styles
)
364 def decorate(self
, dp
):
365 if dp
.fillstyles
is not None:
366 raise RuntimeError("Cannot fill an already filled path")
367 dp
.fillstyles
= self
.styles
371 filled
.clear
= attr
.clearclass(_filled
)
377 # helper function which constructs the arrowhead
379 def _arrowhead(anormpath
, size
, angle
, constrictionlen
, reversed):
381 """helper routine, which returns an arrowhead from a given anormpath
383 returns arrowhead at begin of anormpath with size,
384 opening angle and constriction length constrictionlen. If constrictionlen is None, we
385 do not add a constriction.
389 anormpath
= anormpath
.reversed()
390 alen
= anormpath
.arclentoparam(size
)
391 tx
, ty
= anormpath
.atbegin()
393 # now we construct the template for our arrow but cutting
394 # the path a the corresponding length
395 arrowtemplate
= anormpath
.split(alen
)[0]
397 # from this template, we construct the two outer curves
399 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
400 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
402 # now come the joining backward parts
404 if constrictionlen
is not None:
405 # constriction point (cx, cy) lies on path
406 cx
, cy
= anormpath
.at(anormpath
.arclentoparam(constrictionlen
))
407 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
408 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
410 arrow
= arrowl
.reversed() << arrowr
417 _base
= 6 * unit
.v_pt
419 class arrow(deco
, attr
.attr
):
421 """arrow is a decorator which adds an arrow to either side of the path"""
423 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
424 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
425 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
426 self
.position
= position
429 self
.constriction
= constriction
431 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=helper
.nodefault
):
435 position
= self
.position
440 if constriction
is helper
.nodefault
:
441 constriction
= self
.constriction
442 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
444 def decorate(self
, dp
):
448 # calculate absolute arc length of constricition
449 # Note that we have to correct this length because the arrowtemplates are rotated
450 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
451 # self.constriction = 1, we actually have a length which is approximately shorter
452 # by the given geometrical factor.
453 if self
.constriction
is not None:
454 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
456 # if we do not want a constriction, i.e. constriction is None, we still
457 # need constrictionlen for cutting the path
458 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
459 arrowheadconstrictionlen
= None
461 if self
.position
== 0:
462 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=0)
464 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=1)
466 # add arrowhead to decoratedpath
467 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
469 if self
.position
== 0:
470 # exclude first part of the first normsubpath from stroking
471 dp
.excluderange(0, min(self
.size
, constrictionlen
))
473 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
477 arrow
.clear
= attr
.clearclass(arrow
)
479 # arrows at begin of path
480 barrow
= arrow(position
=0)
481 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
482 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
483 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
484 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
485 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
486 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
487 barrow
.normal
= barrow(size
=_base
)
488 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
489 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
490 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
491 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
492 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
493 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
495 # arrows at end of path
496 earrow
= arrow(position
=1)
497 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
498 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
499 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
500 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
501 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
502 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
503 earrow
.normal
= earrow(size
=_base
)
504 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
505 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
506 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
507 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
508 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
509 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))