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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
29 from __future__
import nested_scopes
32 import attr
, canvas
, color
, helper
, path
, style
, trafo
, unit
35 from math
import radians
37 # fallback implementation for Python 2.1 and below
38 def radians(x
): return x
*math
.pi
/180
44 class decoratedpath(canvas
.canvasitem
):
47 The main purpose of this class is during the drawing
48 (stroking/filling) of a path. It collects attributes for the
49 stroke and/or fill operations.
52 def __init__(self
, path
, strokepath
=None, fillpath
=None,
53 styles
=None, strokestyles
=None, fillstyles
=None,
58 # global style for stroking and filling and subdps
61 # styles which apply only for stroking and filling
62 self
.strokestyles
= strokestyles
63 self
.fillstyles
= fillstyles
65 # the decoratedpath can contain additional elements of the
66 # path (ornaments), e.g., arrowheads.
68 self
.ornaments
= canvas
.canvas()
70 self
.ornaments
= ornaments
72 self
.nostrokeranges
= None
74 def ensurenormpath(self
):
75 """convert self.path into a normpath"""
76 assert self
.nostrokeranges
is None or isinstance(self
.path
, path
.normpath
), "you don't understand what you are doing"
77 self
.path
= self
.path
.normpath()
79 def excluderange(self
, begin
, end
):
80 assert isinstance(self
.path
, path
.normpath
), "you don't understand what this is about"
81 if self
.nostrokeranges
is None:
82 self
.nostrokeranges
= [(begin
, end
)]
85 while ibegin
< len(self
.nostrokeranges
) and self
.nostrokeranges
[ibegin
][1] < begin
:
88 if ibegin
== len(self
.nostrokeranges
):
89 self
.nostrokeranges
.append((begin
, end
))
92 iend
= len(self
.nostrokeranges
) - 1
93 while 0 <= iend
and end
< self
.nostrokeranges
[iend
][0]:
97 self
.nostrokeranges
.insert(0, (begin
, end
))
100 if self
.nostrokeranges
[ibegin
][0] < begin
:
101 begin
= self
.nostrokeranges
[ibegin
][0]
102 if end
< self
.nostrokeranges
[iend
][1]:
103 end
= self
.nostrokeranges
[iend
][1]
105 self
.nostrokeranges
[ibegin
:iend
+1] = [(begin
, end
)]
108 pathbbox
= self
.path
.bbox()
109 ornamentsbbox
= self
.ornaments
.bbox()
110 if ornamentsbbox
is not None:
111 return ornamentsbbox
+ pathbbox
115 def registerPS(self
, registry
):
117 for style
in self
.styles
:
118 style
.registerPS(registry
)
120 for style
in self
.fillstyles
:
121 style
.registerPS(registry
)
122 if self
.strokestyles
:
123 for style
in self
.strokestyles
:
124 style
.registerPS(registry
)
125 self
.ornaments
.registerPS(registry
)
127 def registerPDF(self
, registry
):
129 for style
in self
.styles
:
130 style
.registerPDF(registry
)
132 for style
in self
.fillstyles
:
133 style
.registerPDF(registry
)
134 if self
.strokestyles
:
135 for style
in self
.strokestyles
:
136 style
.registerPDF(registry
)
137 self
.ornaments
.registerPDF(registry
)
139 def strokepath(self
):
140 if self
.nostrokeranges
:
142 for begin
, end
in self
.nostrokeranges
:
143 splitlist
.append(begin
)
144 splitlist
.append(end
)
145 split
= self
.path
.split(splitlist
)
146 # XXX properly handle closed paths?
148 for i
in range(2, len(split
), 2):
154 def outputPS(self
, file, writer
, context
):
155 # draw (stroke and/or fill) the decoratedpath on the canvas
156 # while trying to produce an efficient output, e.g., by
157 # not writing one path two times
160 def _writestyles(styles
, context
):
162 style
.outputPS(file, writer
, context
)
164 if self
.strokestyles
is None and self
.fillstyles
is None:
165 raise RuntimeError("Path neither to be stroked nor filled")
167 strokepath
= self
.strokepath()
170 # apply global styles
172 file.write("gsave\n")
174 _writestyles(self
.styles
, context
)
176 if self
.fillstyles
is not None:
177 file.write("newpath\n")
178 fillpath
.outputPS(file, writer
, context
)
180 if self
.strokestyles
is not None and strokepath
is fillpath
:
181 # do efficient stroking + filling if respective paths are identical
182 file.write("gsave\n")
185 _writestyles(self
.fillstyles
, context())
188 file.write("grestore\n")
190 if self
.strokestyles
:
191 file.write("gsave\n")
192 _writestyles(self
.strokestyles
, context())
194 file.write("stroke\n")
196 if self
.strokestyles
:
197 file.write("grestore\n")
199 # only fill fillpath - for the moment
201 file.write("gsave\n")
202 _writestyles(self
.fillstyles
, context())
207 file.write("grestore\n")
209 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
210 # this is the only relevant case still left
211 # Note that a possible stroking has already been done.
215 if self
.strokestyles
:
216 file.write("gsave\n")
217 _writestyles(self
.strokestyles
, context
)
219 file.write("newpath\n")
220 strokepath
.outputPS(file, writer
, context
)
221 file.write("stroke\n")
223 if self
.strokestyles
:
224 file.write("grestore\n")
228 # now, draw additional elements of decoratedpath
229 self
.ornaments
.outputPS(file, writer
, context
)
231 # restore global styles
233 file.write("grestore\n")
235 def outputPDF(self
, file, writer
, context
):
236 # draw (stroke and/or fill) the decoratedpath on the canvas
238 def _writestyles(styles
, context
):
240 style
.outputPDF(file, writer
, context
)
242 def _writestrokestyles(strokestyles
, context
):
243 for style
in strokestyles
:
244 style
.outputPDF(file, writer
, context(fillattr
=0))
246 def _writefillstyles(fillstyles
, context
):
247 for style
in fillstyles
:
248 style
.outputPDF(file, writer
, context(strokeattr
=0))
250 if self
.strokestyles
is None and self
.fillstyles
is None:
251 raise RuntimeError("Path neither to be stroked nor filled")
253 strokepath
= self
.strokepath()
256 # apply global styles
258 file.write("q\n") # gsave
260 _writestyles(self
.styles
, context
)
262 if self
.fillstyles
is not None:
263 fillpath
.outputPDF(file, writer
, context
)
265 if self
.strokestyles
is not None and strokepath
is fillpath
:
266 # do efficient stroking + filling
267 file.write("q\n") # gsave
272 _writefillstyles(self
.fillstyles
, context
)
273 if self
.strokestyles
:
274 _writestrokestyles(self
.strokestyles
, context
)
276 file.write("B\n") # both stroke and fill
277 file.write("Q\n") # grestore
280 # only fill fillpath - for the moment
282 file.write("q\n") # gsave
283 _writefillstyles(self
.fillstyles
, context())
285 file.write("f\n") # fill
288 file.write("Q\n") # grestore
290 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
291 # this is the only relevant case still left
292 # Note that a possible stroking has already been done.
296 if self
.strokestyles
:
297 file.write("q\n") # gsave
298 _writestrokestyles(self
.strokestyles
, context
)
300 strokepath
.outputPDF(file, writer
, context
)
301 file.write("S\n") # stroke
303 if self
.strokestyles
:
304 file.write("Q\n") # grestore
307 # now, draw additional elements of decoratedpath
308 self
.ornaments
.outputPDF(file, writer
, context
)
310 # restore global styles
312 file.write("Q\n") # grestore
322 In contrast to path styles, path decorators depend on the concrete
323 path to which they are applied. In particular, they don't make
324 sense without any path and can thus not be used in canvas.set!
328 def decorate(self
, dp
):
329 """apply a style to a given decoratedpath object dp
331 decorate accepts a decoratedpath object dp, applies PathStyle
332 by modifying dp in place and returning the new dp.
338 # stroked and filled: basic decos which stroked and fill,
339 # respectively the path
342 class _stroked(deco
, attr
.exclusiveattr
):
344 """stroked is a decorator, which draws the outline of the path"""
346 def __init__(self
, styles
=[]):
347 attr
.exclusiveattr
.__init
__(self
, _stroked
)
348 self
.styles
= attr
.mergeattrs(styles
)
349 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
351 def __call__(self
, styles
=[]):
352 # XXX or should we also merge self.styles
353 return _stroked(styles
)
355 def decorate(self
, dp
):
356 if dp
.strokestyles
is not None:
357 raise RuntimeError("Cannot stroke an already stroked path")
358 dp
.strokestyles
= self
.styles
362 stroked
.clear
= attr
.clearclass(_stroked
)
365 class _filled(deco
, attr
.exclusiveattr
):
367 """filled is a decorator, which fills the interior of the path"""
369 def __init__(self
, styles
=[]):
370 attr
.exclusiveattr
.__init
__(self
, _filled
)
371 self
.styles
= attr
.mergeattrs(styles
)
372 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
374 def __call__(self
, styles
=[]):
375 # XXX or should we also merge self.styles
376 return _filled(styles
)
378 def decorate(self
, dp
):
379 if dp
.fillstyles
is not None:
380 raise RuntimeError("Cannot fill an already filled path")
381 dp
.fillstyles
= self
.styles
385 filled
.clear
= attr
.clearclass(_filled
)
391 # helper function which constructs the arrowhead
393 def _arrowhead(anormpath
, size
, angle
, constrictionlen
, reversed):
395 """helper routine, which returns an arrowhead from a given anormpath
397 returns arrowhead at begin of anormpath with size,
398 opening angle and constriction length constrictionlen. If constrictionlen is None, we
399 do not add a constriction.
403 anormpath
= anormpath
.reversed()
404 alen
= anormpath
.arclentoparam(size
)
405 tx
, ty
= anormpath
.atbegin()
407 # now we construct the template for our arrow but cutting
408 # the path a the corresponding length
409 arrowtemplate
= anormpath
.split(alen
)[0]
411 # from this template, we construct the two outer curves
413 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
414 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
416 # now come the joining backward parts
418 if constrictionlen
is not None:
419 # constriction point (cx, cy) lies on path
420 cx
, cy
= anormpath
.at(anormpath
.arclentoparam(constrictionlen
))
421 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
422 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
424 arrow
= arrowl
.reversed() << arrowr
431 _base
= 6 * unit
.v_pt
433 class arrow(deco
, attr
.attr
):
435 """arrow is a decorator which adds an arrow to either side of the path"""
437 def __init__(self
, attrs
=[], position
=0, size
=_base
, angle
=45, constriction
=0.8):
438 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
439 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
440 self
.position
= position
443 self
.constriction
= constriction
445 def __call__(self
, attrs
=None, position
=None, size
=None, angle
=None, constriction
=helper
.nodefault
):
449 position
= self
.position
454 if constriction
is helper
.nodefault
:
455 constriction
= self
.constriction
456 return arrow(attrs
=attrs
, position
=position
, size
=size
, angle
=angle
, constriction
=constriction
)
458 def decorate(self
, dp
):
462 # calculate absolute arc length of constricition
463 # Note that we have to correct this length because the arrowtemplates are rotated
464 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
465 # self.constriction = 1, we actually have a length which is approximately shorter
466 # by the given geometrical factor.
467 if self
.constriction
is not None:
468 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(radians(self
.angle
/2.0))
470 # if we do not want a constriction, i.e. constriction is None, we still
471 # need constrictionlen for cutting the path
472 constrictionlen
= self
.size
* 1 * math
.cos(radians(self
.angle
/2.0))
473 arrowheadconstrictionlen
= None
475 if self
.position
== 0:
476 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=0)
478 arrowhead
= _arrowhead(anormpath
, self
.size
, self
.angle
, arrowheadconstrictionlen
, reversed=1)
480 # add arrowhead to decoratedpath
481 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
483 if self
.position
== 0:
484 # exclude first part of the first normsubpath from stroking
485 dp
.excluderange(0, min(self
.size
, constrictionlen
))
487 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
491 arrow
.clear
= attr
.clearclass(arrow
)
493 # arrows at begin of path
494 barrow
= arrow(position
=0)
495 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
496 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
497 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
498 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
499 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
500 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
501 barrow
.normal
= barrow(size
=_base
)
502 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
503 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
504 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
505 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
506 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
507 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
509 # arrows at end of path
510 earrow
= arrow(position
=1)
511 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
512 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
513 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
514 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
515 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
516 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
517 earrow
.normal
= earrow(size
=_base
)
518 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
519 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
520 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
521 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
522 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
523 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))