1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # - should we improve on the arc length -> arg parametrization routine or
26 # should we at least factor it out?
29 import attr
, canvas
, canvasitem
, color
, path
, normpath
, style
, trafo
, unit
37 class decoratedpath(canvasitem
.canvasitem
):
40 The main purpose of this class is during the drawing
41 (stroking/filling) of a path. It collects attributes for the
42 stroke and/or fill operations.
45 def __init__(self
, path
, strokepath
=None, fillpath
=None,
46 styles
=None, strokestyles
=None, fillstyles
=None,
51 # global style for stroking and filling and subdps
54 # styles which apply only for stroking and filling
55 self
.strokestyles
= strokestyles
56 self
.fillstyles
= fillstyles
58 # the decoratedpath can contain additional elements of the
59 # path (ornaments), e.g., arrowheads.
61 self
.ornaments
= canvas
.canvas()
63 self
.ornaments
= ornaments
65 self
.nostrokeranges
= None
67 def ensurenormpath(self
):
68 """convert self.path into a normpath"""
69 assert self
.nostrokeranges
is None or isinstance(self
.path
, path
.normpath
), "you don't understand what you are doing"
70 self
.path
= self
.path
.normpath()
72 def excluderange(self
, begin
, end
):
73 assert isinstance(self
.path
, path
.normpath
), "you don't understand what this is about"
74 if self
.nostrokeranges
is None:
75 self
.nostrokeranges
= [(begin
, end
)]
78 while ibegin
< len(self
.nostrokeranges
) and self
.nostrokeranges
[ibegin
][1] < begin
:
81 if ibegin
== len(self
.nostrokeranges
):
82 self
.nostrokeranges
.append((begin
, end
))
85 iend
= len(self
.nostrokeranges
) - 1
86 while 0 <= iend
and end
< self
.nostrokeranges
[iend
][0]:
90 self
.nostrokeranges
.insert(0, (begin
, end
))
93 if self
.nostrokeranges
[ibegin
][0] < begin
:
94 begin
= self
.nostrokeranges
[ibegin
][0]
95 if end
< self
.nostrokeranges
[iend
][1]:
96 end
= self
.nostrokeranges
[iend
][1]
98 self
.nostrokeranges
[ibegin
:iend
+1] = [(begin
, end
)]
101 pathbbox
= self
.path
.bbox()
102 ornamentsbbox
= self
.ornaments
.bbox()
103 if ornamentsbbox
is not None:
104 return ornamentsbbox
+ pathbbox
108 def strokepath(self
):
109 if self
.nostrokeranges
:
111 for begin
, end
in self
.nostrokeranges
:
112 splitlist
.append(begin
)
113 splitlist
.append(end
)
114 split
= self
.path
.split(splitlist
)
115 # XXX properly handle closed paths?
117 for i
in range(2, len(split
), 2):
123 def processPS(self
, file, writer
, context
, registry
, bbox
):
124 # draw (stroke and/or fill) the decoratedpath on the canvas
125 # while trying to produce an efficient output, e.g., by
126 # not writing one path two times
129 def _writestyles(styles
, context
, registry
, bbox
):
131 style
.processPS(file, writer
, context
, registry
, bbox
)
133 if self
.strokestyles
is None and self
.fillstyles
is None:
134 if not len(self
.ornaments
):
135 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
136 # just draw additional elements of decoratedpath
137 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
140 strokepath
= self
.strokepath()
143 # apply global styles
145 file.write("gsave\n")
147 _writestyles(self
.styles
, context
, registry
, bbox
)
149 if self
.fillstyles
is not None:
150 file.write("newpath\n")
151 fillpath
.outputPS(file, writer
)
153 if self
.strokestyles
is not None and strokepath
is fillpath
:
154 # do efficient stroking + filling if respective paths are identical
155 file.write("gsave\n")
158 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
161 file.write("grestore\n")
164 if self
.strokestyles
:
165 file.write("gsave\n")
166 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
168 file.write("stroke\n")
169 # take linewidth into account for bbox when stroking a path
170 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
172 if self
.strokestyles
:
173 file.write("grestore\n")
175 # only fill fillpath - for the moment
177 file.write("gsave\n")
178 _writestyles(self
.fillstyles
, context(), registry
, bbox
)
181 bbox
+= fillpath
.bbox()
184 file.write("grestore\n")
186 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
187 # this is the only relevant case still left
188 # Note that a possible stroking has already been done.
190 if self
.strokestyles
:
191 file.write("gsave\n")
192 _writestyles(self
.strokestyles
, acontext
, registry
, bbox
)
194 file.write("newpath\n")
195 strokepath
.outputPS(file, writer
)
196 file.write("stroke\n")
197 # take linewidth into account for bbox when stroking a path
198 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
200 if self
.strokestyles
:
201 file.write("grestore\n")
203 # now, draw additional elements of decoratedpath
204 self
.ornaments
.processPS(file, writer
, context
, registry
, bbox
)
206 # restore global styles
208 file.write("grestore\n")
210 def processPDF(self
, file, writer
, context
, registry
, bbox
):
211 # draw (stroke and/or fill) the decoratedpath on the canvas
213 def _writestyles(styles
, context
, registry
, bbox
):
215 style
.processPDF(file, writer
, context
, registry
, bbox
)
217 def _writestrokestyles(strokestyles
, context
, registry
, bbox
):
219 for style
in strokestyles
:
220 style
.processPDF(file, writer
, context
, registry
, bbox
)
223 def _writefillstyles(fillstyles
, context
, registry
, bbox
):
224 context
.strokeattr
= 0
225 for style
in fillstyles
:
226 style
.processPDF(file, writer
, context
, registry
, bbox
)
227 context
.strokeattr
= 1
229 if self
.strokestyles
is None and self
.fillstyles
is None:
230 if not len(self
.ornaments
):
231 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
232 # just draw additional elements of decoratedpath
233 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
236 strokepath
= self
.strokepath()
239 # apply global styles
241 file.write("q\n") # gsave
243 _writestyles(self
.styles
, context
, registry
, bbox
)
245 if self
.fillstyles
is not None:
246 fillpath
.outputPDF(file, writer
)
248 if self
.strokestyles
is not None and strokepath
is fillpath
:
249 # do efficient stroking + filling
250 file.write("q\n") # gsave
254 _writefillstyles(self
.fillstyles
, acontext
, registry
, bbox
)
255 if self
.strokestyles
:
256 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
258 file.write("B\n") # both stroke and fill
259 # take linewidth into account for bbox when stroking a path
260 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
262 file.write("Q\n") # grestore
264 # only fill fillpath - for the moment
266 file.write("q\n") # gsave
267 _writefillstyles(self
.fillstyles
, context(), registry
, bbox
)
269 file.write("f\n") # fill
270 bbox
+= fillpath
.bbox()
273 file.write("Q\n") # grestore
275 if self
.strokestyles
is not None and (strokepath
is not fillpath
or self
.fillstyles
is None):
276 # this is the only relevant case still left
277 # Note that a possible stroking has already been done.
280 if self
.strokestyles
:
281 file.write("q\n") # gsave
282 _writestrokestyles(self
.strokestyles
, acontext
, registry
, bbox
)
284 strokepath
.outputPDF(file, writer
)
285 file.write("S\n") # stroke
286 # take linewidth into account for bbox when stroking a path
287 bbox
+= strokepath
.bbox().enlarged_pt(0.5*acontext
.linewidth_pt
)
289 if self
.strokestyles
:
290 file.write("Q\n") # grestore
292 # now, draw additional elements of decoratedpath
293 self
.ornaments
.processPDF(file, writer
, context
, registry
, bbox
)
295 # restore global styles
297 file.write("Q\n") # grestore
307 In contrast to path styles, path decorators depend on the concrete
308 path to which they are applied. In particular, they don't make
309 sense without any path and can thus not be used in canvas.set!
313 def decorate(self
, dp
, texrunner
):
314 """apply a style to a given decoratedpath object dp
316 decorate accepts a decoratedpath object dp, applies PathStyle
317 by modifying dp in place.
323 # stroked and filled: basic decos which stroked and fill,
324 # respectively the path
327 class _stroked(deco
, attr
.exclusiveattr
):
329 """stroked is a decorator, which draws the outline of the path"""
331 def __init__(self
, styles
=[]):
332 attr
.exclusiveattr
.__init
__(self
, _stroked
)
333 self
.styles
= attr
.mergeattrs(styles
)
334 attr
.checkattrs(self
.styles
, [style
.strokestyle
])
336 def __call__(self
, styles
=[]):
337 # XXX or should we also merge self.styles
338 return _stroked(styles
)
340 def decorate(self
, dp
, texrunner
):
341 if dp
.strokestyles
is not None:
342 raise RuntimeError("Cannot stroke an already stroked path")
343 dp
.strokestyles
= self
.styles
346 stroked
.clear
= attr
.clearclass(_stroked
)
349 class _filled(deco
, attr
.exclusiveattr
):
351 """filled is a decorator, which fills the interior of the path"""
353 def __init__(self
, styles
=[]):
354 attr
.exclusiveattr
.__init
__(self
, _filled
)
355 self
.styles
= attr
.mergeattrs(styles
)
356 attr
.checkattrs(self
.styles
, [style
.fillstyle
])
358 def __call__(self
, styles
=[]):
359 # XXX or should we also merge self.styles
360 return _filled(styles
)
362 def decorate(self
, dp
, texrunner
):
363 if dp
.fillstyles
is not None:
364 raise RuntimeError("Cannot fill an already filled path")
365 dp
.fillstyles
= self
.styles
368 filled
.clear
= attr
.clearclass(_filled
)
374 # helper function which constructs the arrowhead
376 def _arrowhead(anormpath
, arclenfrombegin
, direction
, size
, angle
, constrictionlen
):
378 """helper routine, which returns an arrowhead from a given anormpath
380 - arclenfrombegin: position of arrow in arc length from the start of the path
381 - direction: +1 for an arrow pointing along the direction of anormpath or
382 -1 for an arrow pointing opposite to the direction of normpath
383 - size: size of the arrow as arc length
384 - angle. opening angle
385 - constrictionlen: None (no constriction) or arc length of constriction.
388 # arc length and coordinates of tip
389 tx
, ty
= anormpath
.at(arclenfrombegin
)
391 # construct the template for the arrow by cutting the path at the
392 # corresponding length
393 arrowtemplate
= anormpath
.split([arclenfrombegin
, arclenfrombegin
- direction
* size
])[1]
395 # from this template, we construct the two outer curves of the arrow
396 arrowl
= arrowtemplate
.transformed(trafo
.rotate(-angle
/2.0, tx
, ty
))
397 arrowr
= arrowtemplate
.transformed(trafo
.rotate( angle
/2.0, tx
, ty
))
399 # now come the joining backward parts
400 if constrictionlen
is not None:
401 # constriction point (cx, cy) lies on path
402 cx
, cy
= anormpath
.at(arclenfrombegin
- direction
* constrictionlen
)
403 arrowcr
= path
.line(*(arrowr
.atend() + (cx
,cy
)))
404 arrow
= arrowl
.reversed() << arrowr
<< arrowcr
406 arrow
= arrowl
.reversed() << arrowr
413 _base
= 6 * unit
.v_pt
415 class arrow(deco
, attr
.attr
):
417 """arrow is a decorator which adds an arrow to either side of the path"""
419 def __init__(self
, attrs
=[], pos
=1, reversed=0, size
=_base
, angle
=45, constriction
=0.8):
420 self
.attrs
= attr
.mergeattrs([style
.linestyle
.solid
, filled
] + attrs
)
421 attr
.checkattrs(self
.attrs
, [deco
, style
.fillstyle
, style
.strokestyle
])
423 self
.reversed = reversed
426 self
.constriction
= constriction
428 def __call__(self
, attrs
=None, pos
=None, reversed=None, size
=None, angle
=None, constriction
=_marker
):
434 reversed = self
.reversed
439 if constriction
is _marker
:
440 constriction
= self
.constriction
441 return arrow(attrs
=attrs
, pos
=pos
, reversed=reversed, size
=size
, angle
=angle
, constriction
=constriction
)
443 def decorate(self
, dp
, texrunner
):
447 # calculate absolute arc length of constricition
448 # Note that we have to correct this length because the arrowtemplates are rotated
449 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
450 # self.constriction = 1, we actually have a length which is approximately shorter
451 # by the given geometrical factor.
452 if self
.constriction
is not None:
453 constrictionlen
= arrowheadconstrictionlen
= self
.size
* self
.constriction
* math
.cos(math
.radians(self
.angle
/2.0))
455 # if we do not want a constriction, i.e. constriction is None, we still
456 # need constrictionlen for cutting the path
457 constrictionlen
= self
.size
* 1 * math
.cos(math
.radians(self
.angle
/2.0))
458 arrowheadconstrictionlen
= None
460 arclenfrombegin
= self
.pos
* anormpath
.arclen()
461 direction
= self
.reversed and -1 or 1
462 arrowhead
= _arrowhead(anormpath
, arclenfrombegin
, direction
, self
.size
, self
.angle
, arrowheadconstrictionlen
)
464 # add arrowhead to decoratedpath
465 dp
.ornaments
.draw(arrowhead
, self
.attrs
)
467 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
468 if self
.pos
== 0 and self
.reversed:
469 dp
.excluderange(0, min(self
.size
, constrictionlen
))
470 elif self
.pos
== 1 and not self
.reversed:
471 dp
.excluderange(anormpath
.end() - min(self
.size
, constrictionlen
), anormpath
.end())
473 arrow
.clear
= attr
.clearclass(arrow
)
475 # arrows at begin of path
476 barrow
= arrow(pos
=0, reversed=1)
477 barrow
.SMALL
= barrow(size
=_base
/math
.sqrt(64))
478 barrow
.SMALl
= barrow(size
=_base
/math
.sqrt(32))
479 barrow
.SMAll
= barrow(size
=_base
/math
.sqrt(16))
480 barrow
.SMall
= barrow(size
=_base
/math
.sqrt(8))
481 barrow
.Small
= barrow(size
=_base
/math
.sqrt(4))
482 barrow
.small
= barrow(size
=_base
/math
.sqrt(2))
483 barrow
.normal
= barrow(size
=_base
)
484 barrow
.large
= barrow(size
=_base
*math
.sqrt(2))
485 barrow
.Large
= barrow(size
=_base
*math
.sqrt(4))
486 barrow
.LArge
= barrow(size
=_base
*math
.sqrt(8))
487 barrow
.LARge
= barrow(size
=_base
*math
.sqrt(16))
488 barrow
.LARGe
= barrow(size
=_base
*math
.sqrt(32))
489 barrow
.LARGE
= barrow(size
=_base
*math
.sqrt(64))
491 # arrows at end of path
493 earrow
.SMALL
= earrow(size
=_base
/math
.sqrt(64))
494 earrow
.SMALl
= earrow(size
=_base
/math
.sqrt(32))
495 earrow
.SMAll
= earrow(size
=_base
/math
.sqrt(16))
496 earrow
.SMall
= earrow(size
=_base
/math
.sqrt(8))
497 earrow
.Small
= earrow(size
=_base
/math
.sqrt(4))
498 earrow
.small
= earrow(size
=_base
/math
.sqrt(2))
499 earrow
.normal
= earrow(size
=_base
)
500 earrow
.large
= earrow(size
=_base
*math
.sqrt(2))
501 earrow
.Large
= earrow(size
=_base
*math
.sqrt(4))
502 earrow
.LArge
= earrow(size
=_base
*math
.sqrt(8))
503 earrow
.LARge
= earrow(size
=_base
*math
.sqrt(16))
504 earrow
.LARGe
= earrow(size
=_base
*math
.sqrt(32))
505 earrow
.LARGE
= earrow(size
=_base
*math
.sqrt(64))
508 class text(deco
, attr
.attr
):
509 """a simple text decorator"""
511 def __init__(self
, text
, textattrs
=[], angle
=0, relangle
=None, textdist
=0.2,
512 relarclenpos
=0.5, arclenfrombegin
=None, arclenfromend
=None,
514 if arclenfrombegin
is not None and arclenfromend
is not None:
515 raise ValueError("either set arclenfrombegin or arclenfromend")
517 self
.textattrs
= textattrs
519 self
.relangle
= relangle
520 self
.textdist
= textdist
521 self
.relarclenpos
= relarclenpos
522 self
.arclenfrombegin
= arclenfrombegin
523 self
.arclenfromend
= arclenfromend
524 self
.texrunner
= texrunner
526 def decorate(self
, dp
, texrunner
):
528 texrunner
= self
.texrunner
529 import text
as textmodule
530 textattrs
= attr
.mergeattrs([textmodule
.halign
.center
, textmodule
.vshift
.mathaxis
] + self
.textattrs
)
533 if self
.arclenfrombegin
is not None:
534 param
= dp
.path
.begin() + self
.arclenfrombegin
535 elif self
.arclenfromend
is not None:
536 param
= dp
.path
.end() - self
.arclenfromend
538 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
539 param
= self
.relarclenpos
* dp
.path
.arclen()
540 x
, y
= dp
.path
.at(param
)
542 if self
.relangle
is not None:
543 a
= dp
.path
.trafo(param
).apply_pt(math
.cos(self
.relangle
*math
.pi
/180), math
.sin(self
.relangle
*math
.pi
/180))
544 b
= dp
.path
.trafo(param
).apply_pt(0, 0)
545 angle
= math
.atan2(a
[1] - b
[1], a
[0] - b
[0])
547 angle
= self
.angle
*math
.pi
/180
548 t
= texrunner
.text(x
, y
, self
.text
, textattrs
)
549 t
.linealign(self
.textdist
, math
.cos(angle
), math
.sin(angle
))
550 dp
.ornaments
.insert(t
)
553 class shownormpath(deco
, attr
.attr
):
555 def decorate(self
, dp
, texrunner
):
558 for normsubpath
in dp
.path
.normsubpaths
:
559 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
560 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
561 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.green
])
563 dp
.ornaments
.stroke(normpath
.normpath([normpath
.normsubpath([normsubpathitem
])]), [color
.rgb
.blue
])
564 for normsubpath
in dp
.path
.normsubpaths
:
565 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
566 if isinstance(normsubpathitem
, normpath
.normcurve_pt
):
567 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x0_pt
, normsubpathitem
.y0_pt
, normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
568 dp
.ornaments
.stroke(path
.line_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, normsubpathitem
.x3_pt
, normsubpathitem
.y3_pt
), [style
.linestyle
.dashed
, color
.rgb
.red
])
569 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x1_pt
, normsubpathitem
.y1_pt
, r_pt
), [filled([color
.rgb
.red
])])
570 dp
.ornaments
.draw(path
.circle_pt(normsubpathitem
.x2_pt
, normsubpathitem
.y2_pt
, r_pt
), [filled([color
.rgb
.red
])])
571 for normsubpath
in dp
.path
.normsubpaths
:
572 for i
, normsubpathitem
in enumerate(normsubpath
.normsubpathitems
):
574 x_pt
, y_pt
= normsubpathitem
.atbegin_pt()
575 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])
576 x_pt
, y_pt
= normsubpathitem
.atend_pt()
577 dp
.ornaments
.draw(path
.circle_pt(x_pt
, y_pt
, r_pt
), [filled
])