3d function plots
[PyX/mjg.git] / pyx / deco.py
blob72c18a3bc7956178b17f26f2156435e747225a32
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
24 # TODO:
25 # - should we improve on the arc length -> arg parametrization routine or
26 # should we at least factor it out?
28 from __future__ import nested_scopes
30 import sys, math
31 import attr, canvas, color, path, normpath, style, trafo, unit
33 try:
34 from math import radians
35 except ImportError:
36 # fallback implementation for Python 2.1 and below
37 def radians(x): return x*math.pi/180
39 class _marker: pass
42 # Decorated path
45 class decoratedpath(canvas.canvasitem):
46 """Decorated path
48 The main purpose of this class is during the drawing
49 (stroking/filling) of a path. It collects attributes for the
50 stroke and/or fill operations.
51 """
53 def __init__(self, path, strokepath=None, fillpath=None,
54 styles=None, strokestyles=None, fillstyles=None,
55 ornaments=None):
57 self.path = path
59 # global style for stroking and filling and subdps
60 self.styles = styles
62 # styles which apply only for stroking and filling
63 self.strokestyles = strokestyles
64 self.fillstyles = fillstyles
66 # the decoratedpath can contain additional elements of the
67 # path (ornaments), e.g., arrowheads.
68 if ornaments is None:
69 self.ornaments = canvas.canvas()
70 else:
71 self.ornaments = ornaments
73 self.nostrokeranges = None
75 def ensurenormpath(self):
76 """convert self.path into a normpath"""
77 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
78 self.path = self.path.normpath()
80 def excluderange(self, begin, end):
81 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
82 if self.nostrokeranges is None:
83 self.nostrokeranges = [(begin, end)]
84 else:
85 ibegin = 0
86 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
87 ibegin += 1
89 if ibegin == len(self.nostrokeranges):
90 self.nostrokeranges.append((begin, end))
91 return
93 iend = len(self.nostrokeranges) - 1
94 while 0 <= iend and end < self.nostrokeranges[iend][0]:
95 iend -= 1
97 if iend == -1:
98 self.nostrokeranges.insert(0, (begin, end))
99 return
101 if self.nostrokeranges[ibegin][0] < begin:
102 begin = self.nostrokeranges[ibegin][0]
103 if end < self.nostrokeranges[iend][1]:
104 end = self.nostrokeranges[iend][1]
106 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
108 def bbox(self):
109 pathbbox = self.path.bbox()
110 ornamentsbbox = self.ornaments.bbox()
111 if ornamentsbbox is not None:
112 return ornamentsbbox + pathbbox
113 else:
114 return pathbbox
116 def strokepath(self):
117 if self.nostrokeranges:
118 splitlist = []
119 for begin, end in self.nostrokeranges:
120 splitlist.append(begin)
121 splitlist.append(end)
122 split = self.path.split(splitlist)
123 # XXX properly handle closed paths?
124 result = split[0]
125 for i in range(2, len(split), 2):
126 result += split[i]
127 return result
128 else:
129 return self.path
131 def processPS(self, file, writer, context, registry, bbox):
132 # draw (stroke and/or fill) the decoratedpath on the canvas
133 # while trying to produce an efficient output, e.g., by
134 # not writing one path two times
136 # small helper
137 def _writestyles(styles, context, registry, bbox):
138 for style in styles:
139 style.processPS(file, writer, context, registry, bbox)
141 if self.strokestyles is None and self.fillstyles is None:
142 if not len(self.ornaments):
143 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
144 # just draw additional elements of decoratedpath
145 self.ornaments.processPS(file, writer, context, registry, bbox)
146 return
148 strokepath = self.strokepath()
149 fillpath = self.path
151 # apply global styles
152 if self.styles:
153 file.write("gsave\n")
154 context = context()
155 _writestyles(self.styles, context, registry, bbox)
157 if self.fillstyles is not None:
158 file.write("newpath\n")
159 fillpath.outputPS(file, writer)
161 if self.strokestyles is not None and strokepath is fillpath:
162 # do efficient stroking + filling if respective paths are identical
163 file.write("gsave\n")
165 if self.fillstyles:
166 _writestyles(self.fillstyles, context(), registry, bbox)
168 file.write("fill\n")
169 file.write("grestore\n")
171 acontext = context()
172 if self.strokestyles:
173 file.write("gsave\n")
174 _writestyles(self.strokestyles, acontext, registry, bbox)
176 file.write("stroke\n")
177 # take linewidth into account for bbox when stroking a path
178 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
180 if self.strokestyles:
181 file.write("grestore\n")
182 else:
183 # only fill fillpath - for the moment
184 if self.fillstyles:
185 file.write("gsave\n")
186 _writestyles(self.fillstyles, context(), registry, bbox)
188 file.write("fill\n")
189 bbox += fillpath.bbox()
191 if self.fillstyles:
192 file.write("grestore\n")
194 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
195 # this is the only relevant case still left
196 # Note that a possible stroking has already been done.
197 acontext = context()
198 if self.strokestyles:
199 file.write("gsave\n")
200 _writestyles(self.strokestyles, acontext, registry, bbox)
202 file.write("newpath\n")
203 strokepath.outputPS(file, writer)
204 file.write("stroke\n")
205 # take linewidth into account for bbox when stroking a path
206 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
208 if self.strokestyles:
209 file.write("grestore\n")
211 # now, draw additional elements of decoratedpath
212 self.ornaments.processPS(file, writer, context, registry, bbox)
214 # restore global styles
215 if self.styles:
216 file.write("grestore\n")
218 def processPDF(self, file, writer, context, registry, bbox):
219 # draw (stroke and/or fill) the decoratedpath on the canvas
221 def _writestyles(styles, context, registry, bbox):
222 for style in styles:
223 style.processPDF(file, writer, context, registry, bbox)
225 def _writestrokestyles(strokestyles, context, registry, bbox):
226 context.fillattr = 0
227 for style in strokestyles:
228 style.processPDF(file, writer, context, registry, bbox)
229 context.fillattr = 1
231 def _writefillstyles(fillstyles, context, registry, bbox):
232 context.strokeattr = 0
233 for style in fillstyles:
234 style.processPDF(file, writer, context, registry, bbox)
235 context.strokeattr = 1
237 if self.strokestyles is None and self.fillstyles is None:
238 if not len(self.ornaments):
239 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
240 # just draw additional elements of decoratedpath
241 self.ornaments.processPDF(file, writer, context, registry, bbox)
242 return
244 strokepath = self.strokepath()
245 fillpath = self.path
247 # apply global styles
248 if self.styles:
249 file.write("q\n") # gsave
250 context = context()
251 _writestyles(self.styles, context, registry, bbox)
253 if self.fillstyles is not None:
254 fillpath.outputPDF(file, writer)
256 if self.strokestyles is not None and strokepath is fillpath:
257 # do efficient stroking + filling
258 file.write("q\n") # gsave
259 acontext = context()
261 if self.fillstyles:
262 _writefillstyles(self.fillstyles, acontext, registry, bbox)
263 if self.strokestyles:
264 _writestrokestyles(self.strokestyles, acontext, registry, bbox)
266 file.write("B\n") # both stroke and fill
267 # take linewidth into account for bbox when stroking a path
268 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
270 file.write("Q\n") # grestore
271 else:
272 # only fill fillpath - for the moment
273 if self.fillstyles:
274 file.write("q\n") # gsave
275 _writefillstyles(self.fillstyles, context(), registry, bbox)
277 file.write("f\n") # fill
278 bbox += fillpath.bbox()
280 if self.fillstyles:
281 file.write("Q\n") # grestore
283 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
284 # this is the only relevant case still left
285 # Note that a possible stroking has already been done.
286 acontext = context()
288 if self.strokestyles:
289 file.write("q\n") # gsave
290 _writestrokestyles(self.strokestyles, acontext, registry, bbox)
292 strokepath.outputPDF(file, writer)
293 file.write("S\n") # stroke
294 # take linewidth into account for bbox when stroking a path
295 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
297 if self.strokestyles:
298 file.write("Q\n") # grestore
300 # now, draw additional elements of decoratedpath
301 self.ornaments.processPDF(file, writer, context, registry, bbox)
303 # restore global styles
304 if self.styles:
305 file.write("Q\n") # grestore
308 # Path decorators
311 class deco:
313 """decorators
315 In contrast to path styles, path decorators depend on the concrete
316 path to which they are applied. In particular, they don't make
317 sense without any path and can thus not be used in canvas.set!
321 def decorate(self, dp, texrunner):
322 """apply a style to a given decoratedpath object dp
324 decorate accepts a decoratedpath object dp, applies PathStyle
325 by modifying dp in place.
328 pass
331 # stroked and filled: basic decos which stroked and fill,
332 # respectively the path
335 class _stroked(deco, attr.exclusiveattr):
337 """stroked is a decorator, which draws the outline of the path"""
339 def __init__(self, styles=[]):
340 attr.exclusiveattr.__init__(self, _stroked)
341 self.styles = attr.mergeattrs(styles)
342 attr.checkattrs(self.styles, [style.strokestyle])
344 def __call__(self, styles=[]):
345 # XXX or should we also merge self.styles
346 return _stroked(styles)
348 def decorate(self, dp, texrunner):
349 if dp.strokestyles is not None:
350 raise RuntimeError("Cannot stroke an already stroked path")
351 dp.strokestyles = self.styles
353 stroked = _stroked()
354 stroked.clear = attr.clearclass(_stroked)
357 class _filled(deco, attr.exclusiveattr):
359 """filled is a decorator, which fills the interior of the path"""
361 def __init__(self, styles=[]):
362 attr.exclusiveattr.__init__(self, _filled)
363 self.styles = attr.mergeattrs(styles)
364 attr.checkattrs(self.styles, [style.fillstyle])
366 def __call__(self, styles=[]):
367 # XXX or should we also merge self.styles
368 return _filled(styles)
370 def decorate(self, dp, texrunner):
371 if dp.fillstyles is not None:
372 raise RuntimeError("Cannot fill an already filled path")
373 dp.fillstyles = self.styles
375 filled = _filled()
376 filled.clear = attr.clearclass(_filled)
379 # Arrows
382 # helper function which constructs the arrowhead
384 def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constrictionlen):
386 """helper routine, which returns an arrowhead from a given anormpath
388 - arclenfrombegin: position of arrow in arc length from the start of the path
389 - direction: +1 for an arrow pointing along the direction of anormpath or
390 -1 for an arrow pointing opposite to the direction of normpath
391 - size: size of the arrow as arc length
392 - angle. opening angle
393 - constrictionlen: None (no constriction) or arc length of constriction.
396 # arc length and coordinates of tip
397 tx, ty = anormpath.at(arclenfrombegin)
399 # construct the template for the arrow by cutting the path at the
400 # corresponding length
401 arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
403 # from this template, we construct the two outer curves of the arrow
404 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
405 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
407 # now come the joining backward parts
408 if constrictionlen is not None:
409 # constriction point (cx, cy) lies on path
410 cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
411 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
412 arrow = arrowl.reversed() << arrowr << arrowcr
413 else:
414 arrow = arrowl.reversed() << arrowr
416 arrow[-1].close()
418 return arrow
421 _base = 6 * unit.v_pt
423 class arrow(deco, attr.attr):
425 """arrow is a decorator which adds an arrow to either side of the path"""
427 def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
428 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
429 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
430 self.pos = pos
431 self.reversed = reversed
432 self.size = size
433 self.angle = angle
434 self.constriction = constriction
436 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
437 if attrs is None:
438 attrs = self.attrs
439 if pos is None:
440 pos = self.pos
441 if reversed is None:
442 reversed = self.reversed
443 if size is None:
444 size = self.size
445 if angle is None:
446 angle = self.angle
447 if constriction is _marker:
448 constriction = self.constriction
449 return arrow(attrs=attrs, pos=pos, reversed=reversed, size=size, angle=angle, constriction=constriction)
451 def decorate(self, dp, texrunner):
452 dp.ensurenormpath()
453 anormpath = dp.path
455 # calculate absolute arc length of constricition
456 # Note that we have to correct this length because the arrowtemplates are rotated
457 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
458 # self.constriction = 1, we actually have a length which is approximately shorter
459 # by the given geometrical factor.
460 if self.constriction is not None:
461 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
462 else:
463 # if we do not want a constriction, i.e. constriction is None, we still
464 # need constrictionlen for cutting the path
465 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
466 arrowheadconstrictionlen = None
468 arclenfrombegin = self.pos * anormpath.arclen()
469 direction = self.reversed and -1 or 1
470 arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle, arrowheadconstrictionlen)
472 # add arrowhead to decoratedpath
473 dp.ornaments.draw(arrowhead, self.attrs)
475 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
476 if self.pos == 0 and self.reversed:
477 dp.excluderange(0, min(self.size, constrictionlen))
478 elif self.pos == 1 and not self.reversed:
479 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
481 arrow.clear = attr.clearclass(arrow)
483 # arrows at begin of path
484 barrow = arrow(pos=0, reversed=1)
485 barrow.SMALL = barrow(size=_base/math.sqrt(64))
486 barrow.SMALl = barrow(size=_base/math.sqrt(32))
487 barrow.SMAll = barrow(size=_base/math.sqrt(16))
488 barrow.SMall = barrow(size=_base/math.sqrt(8))
489 barrow.Small = barrow(size=_base/math.sqrt(4))
490 barrow.small = barrow(size=_base/math.sqrt(2))
491 barrow.normal = barrow(size=_base)
492 barrow.large = barrow(size=_base*math.sqrt(2))
493 barrow.Large = barrow(size=_base*math.sqrt(4))
494 barrow.LArge = barrow(size=_base*math.sqrt(8))
495 barrow.LARge = barrow(size=_base*math.sqrt(16))
496 barrow.LARGe = barrow(size=_base*math.sqrt(32))
497 barrow.LARGE = barrow(size=_base*math.sqrt(64))
499 # arrows at end of path
500 earrow = arrow()
501 earrow.SMALL = earrow(size=_base/math.sqrt(64))
502 earrow.SMALl = earrow(size=_base/math.sqrt(32))
503 earrow.SMAll = earrow(size=_base/math.sqrt(16))
504 earrow.SMall = earrow(size=_base/math.sqrt(8))
505 earrow.Small = earrow(size=_base/math.sqrt(4))
506 earrow.small = earrow(size=_base/math.sqrt(2))
507 earrow.normal = earrow(size=_base)
508 earrow.large = earrow(size=_base*math.sqrt(2))
509 earrow.Large = earrow(size=_base*math.sqrt(4))
510 earrow.LArge = earrow(size=_base*math.sqrt(8))
511 earrow.LARge = earrow(size=_base*math.sqrt(16))
512 earrow.LARGe = earrow(size=_base*math.sqrt(32))
513 earrow.LARGE = earrow(size=_base*math.sqrt(64))
516 class text(deco, attr.attr):
517 """a simple text decorator"""
519 def __init__(self, text, textattrs=[], angle=0, textdist=0.2,
520 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
521 texrunner=None):
522 if arclenfrombegin is not None and arclenfromend is not None:
523 raise ValueError("either set arclenfrombegin or arclenfromend")
524 self.text = text
525 self.textattrs = textattrs
526 self.angle = angle
527 self.textdist = textdist
528 self.relarclenpos = relarclenpos
529 self.arclenfrombegin = arclenfrombegin
530 self.arclenfromend = arclenfromend
531 self.texrunner = texrunner
533 def decorate(self, dp, texrunner):
534 if self.texrunner:
535 texrunner = self.texrunner
536 import text as textmodule
537 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
539 dp.ensurenormpath()
540 if self.arclenfrombegin is not None:
541 x, y = dp.path.at(dp.path.begin() + self.arclenfrombegin)
542 elif self.arclenfromend is not None:
543 x, y = dp.path.at(dp.path.end() - self.arclenfromend)
544 else:
545 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
546 x, y = dp.path.at(self.relarclenpos * dp.path.arclen())
548 t = texrunner.text(x, y, self.text, textattrs)
549 t.linealign(self.textdist, math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180))
550 dp.ornaments.insert(t)
553 class shownormpath(deco, attr.attr):
555 def decorate(self, dp, texrunner):
556 r_pt = 2
557 dp.ensurenormpath()
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])
562 else:
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):
573 if not i:
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])