mention one deformer in feature list
[PyX/mjg.git] / pyx / deco.py
blob91a7a251a5add83e369120a27d3f7e291a4f7a92
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2006 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2006 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
25 # TODO:
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
31 import sys, math
32 import attr, canvas, color, path, normpath, style, trafo, unit
34 try:
35 from math import radians
36 except ImportError:
37 # fallback implementation for Python 2.1 and below
38 def radians(x): return x*math.pi/180
40 class _marker: pass
43 # Decorated path
46 class decoratedpath(canvas.canvasitem):
47 """Decorated path
49 The main purpose of this class is during the drawing
50 (stroking/filling) of a path. It collects attributes for the
51 stroke and/or fill operations.
52 """
54 def __init__(self, path, strokepath=None, fillpath=None,
55 styles=None, strokestyles=None, fillstyles=None,
56 ornaments=None):
58 self.path = path
60 # global style for stroking and filling and subdps
61 self.styles = styles
63 # styles which apply only for stroking and filling
64 self.strokestyles = strokestyles
65 self.fillstyles = fillstyles
67 # the decoratedpath can contain additional elements of the
68 # path (ornaments), e.g., arrowheads.
69 if ornaments is None:
70 self.ornaments = canvas.canvas()
71 else:
72 self.ornaments = ornaments
74 self.nostrokeranges = None
76 def ensurenormpath(self):
77 """convert self.path into a normpath"""
78 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
79 self.path = self.path.normpath()
81 def excluderange(self, begin, end):
82 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
83 if self.nostrokeranges is None:
84 self.nostrokeranges = [(begin, end)]
85 else:
86 ibegin = 0
87 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
88 ibegin += 1
90 if ibegin == len(self.nostrokeranges):
91 self.nostrokeranges.append((begin, end))
92 return
94 iend = len(self.nostrokeranges) - 1
95 while 0 <= iend and end < self.nostrokeranges[iend][0]:
96 iend -= 1
98 if iend == -1:
99 self.nostrokeranges.insert(0, (begin, end))
100 return
102 if self.nostrokeranges[ibegin][0] < begin:
103 begin = self.nostrokeranges[ibegin][0]
104 if end < self.nostrokeranges[iend][1]:
105 end = self.nostrokeranges[iend][1]
107 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
109 def bbox(self):
110 pathbbox = self.path.bbox()
111 ornamentsbbox = self.ornaments.bbox()
112 if ornamentsbbox is not None:
113 return ornamentsbbox + pathbbox
114 else:
115 return pathbbox
117 def strokepath(self):
118 if self.nostrokeranges:
119 splitlist = []
120 for begin, end in self.nostrokeranges:
121 splitlist.append(begin)
122 splitlist.append(end)
123 split = self.path.split(splitlist)
124 # XXX properly handle closed paths?
125 result = split[0]
126 for i in range(2, len(split), 2):
127 result += split[i]
128 return result
129 else:
130 return self.path
132 def processPS(self, file, writer, context, registry, bbox):
133 # draw (stroke and/or fill) the decoratedpath on the canvas
134 # while trying to produce an efficient output, e.g., by
135 # not writing one path two times
137 # small helper
138 def _writestyles(styles, context, registry, bbox):
139 for style in styles:
140 style.processPS(file, writer, context, registry, bbox)
142 if self.strokestyles is None and self.fillstyles is None:
143 if not len(self.ornaments):
144 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
145 # just draw additional elements of decoratedpath
146 self.ornaments.processPS(file, writer, context, registry, bbox)
147 return
149 strokepath = self.strokepath()
150 fillpath = self.path
152 # apply global styles
153 if self.styles:
154 file.write("gsave\n")
155 context = context()
156 _writestyles(self.styles, context, registry, bbox)
158 if self.fillstyles is not None:
159 file.write("newpath\n")
160 fillpath.outputPS(file, writer)
162 if self.strokestyles is not None and strokepath is fillpath:
163 # do efficient stroking + filling if respective paths are identical
164 file.write("gsave\n")
166 if self.fillstyles:
167 _writestyles(self.fillstyles, context(), registry, bbox)
169 file.write("fill\n")
170 file.write("grestore\n")
172 acontext = context()
173 if self.strokestyles:
174 file.write("gsave\n")
175 _writestyles(self.strokestyles, acontext, registry, bbox)
177 file.write("stroke\n")
178 # take linewidth into account for bbox when stroking a path
179 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
181 if self.strokestyles:
182 file.write("grestore\n")
183 else:
184 # only fill fillpath - for the moment
185 if self.fillstyles:
186 file.write("gsave\n")
187 _writestyles(self.fillstyles, context(), registry, bbox)
189 file.write("fill\n")
190 bbox += fillpath.bbox()
192 if self.fillstyles:
193 file.write("grestore\n")
195 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
196 # this is the only relevant case still left
197 # Note that a possible stroking has already been done.
198 acontext = context()
199 if self.strokestyles:
200 file.write("gsave\n")
201 _writestyles(self.strokestyles, acontext, registry, bbox)
203 file.write("newpath\n")
204 strokepath.outputPS(file, writer)
205 file.write("stroke\n")
206 # take linewidth into account for bbox when stroking a path
207 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
209 if self.strokestyles:
210 file.write("grestore\n")
212 # now, draw additional elements of decoratedpath
213 self.ornaments.processPS(file, writer, context, registry, bbox)
215 # restore global styles
216 if self.styles:
217 file.write("grestore\n")
219 def processPDF(self, file, writer, context, registry, bbox):
220 # draw (stroke and/or fill) the decoratedpath on the canvas
222 def _writestyles(styles, context, registry, bbox):
223 for style in styles:
224 style.processPDF(file, writer, context, registry, bbox)
226 def _writestrokestyles(strokestyles, context, registry, bbox):
227 context.fillattr = 0
228 for style in strokestyles:
229 style.processPDF(file, writer, context, registry, bbox)
230 context.fillattr = 1
232 def _writefillstyles(fillstyles, context, registry, bbox):
233 context.strokeattr = 0
234 for style in fillstyles:
235 style.processPDF(file, writer, context, registry, bbox)
236 context.strokeattr = 1
238 if self.strokestyles is None and self.fillstyles is None:
239 if not len(self.ornaments):
240 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
241 # just draw additional elements of decoratedpath
242 self.ornaments.processPDF(file, writer, context, registry, bbox)
243 return
245 strokepath = self.strokepath()
246 fillpath = self.path
248 # apply global styles
249 if self.styles:
250 file.write("q\n") # gsave
251 context = context()
252 _writestyles(self.styles, context, registry, bbox)
254 if self.fillstyles is not None:
255 fillpath.outputPDF(file, writer)
257 if self.strokestyles is not None and strokepath is fillpath:
258 # do efficient stroking + filling
259 file.write("q\n") # gsave
260 acontext = context()
262 if self.fillstyles:
263 _writefillstyles(self.fillstyles, acontext, registry, bbox)
264 if self.strokestyles:
265 _writestrokestyles(self.strokestyles, acontext, registry, bbox)
267 file.write("B\n") # both stroke and fill
268 # take linewidth into account for bbox when stroking a path
269 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
271 file.write("Q\n") # grestore
272 else:
273 # only fill fillpath - for the moment
274 if self.fillstyles:
275 file.write("q\n") # gsave
276 _writefillstyles(self.fillstyles, context(), registry, bbox)
278 file.write("f\n") # fill
279 bbox += fillpath.bbox()
281 if self.fillstyles:
282 file.write("Q\n") # grestore
284 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
285 # this is the only relevant case still left
286 # Note that a possible stroking has already been done.
287 acontext = context()
289 if self.strokestyles:
290 file.write("q\n") # gsave
291 _writestrokestyles(self.strokestyles, acontext, registry, bbox)
293 strokepath.outputPDF(file, writer)
294 file.write("S\n") # stroke
295 # take linewidth into account for bbox when stroking a path
296 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
298 if self.strokestyles:
299 file.write("Q\n") # grestore
301 # now, draw additional elements of decoratedpath
302 self.ornaments.processPDF(file, writer, context, registry, bbox)
304 # restore global styles
305 if self.styles:
306 file.write("Q\n") # grestore
309 # Path decorators
312 class deco:
314 """decorators
316 In contrast to path styles, path decorators depend on the concrete
317 path to which they are applied. In particular, they don't make
318 sense without any path and can thus not be used in canvas.set!
322 def decorate(self, dp, texrunner):
323 """apply a style to a given decoratedpath object dp
325 decorate accepts a decoratedpath object dp, applies PathStyle
326 by modifying dp in place.
329 pass
332 # stroked and filled: basic decos which stroked and fill,
333 # respectively the path
336 class _stroked(deco, attr.exclusiveattr):
338 """stroked is a decorator, which draws the outline of the path"""
340 def __init__(self, styles=[]):
341 attr.exclusiveattr.__init__(self, _stroked)
342 self.styles = attr.mergeattrs(styles)
343 attr.checkattrs(self.styles, [style.strokestyle])
345 def __call__(self, styles=[]):
346 # XXX or should we also merge self.styles
347 return _stroked(styles)
349 def decorate(self, dp, texrunner):
350 if dp.strokestyles is not None:
351 raise RuntimeError("Cannot stroke an already stroked path")
352 dp.strokestyles = self.styles
354 stroked = _stroked()
355 stroked.clear = attr.clearclass(_stroked)
358 class _filled(deco, attr.exclusiveattr):
360 """filled is a decorator, which fills the interior of the path"""
362 def __init__(self, styles=[]):
363 attr.exclusiveattr.__init__(self, _filled)
364 self.styles = attr.mergeattrs(styles)
365 attr.checkattrs(self.styles, [style.fillstyle])
367 def __call__(self, styles=[]):
368 # XXX or should we also merge self.styles
369 return _filled(styles)
371 def decorate(self, dp, texrunner):
372 if dp.fillstyles is not None:
373 raise RuntimeError("Cannot fill an already filled path")
374 dp.fillstyles = self.styles
376 filled = _filled()
377 filled.clear = attr.clearclass(_filled)
380 # Arrows
383 # helper function which constructs the arrowhead
385 def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constrictionlen):
387 """helper routine, which returns an arrowhead from a given anormpath
389 - arclenfrombegin: position of arrow in arc length from the start of the path
390 - direction: +1 for an arrow pointing along the direction of anormpath or
391 -1 for an arrow pointing opposite to the direction of normpath
392 - size: size of the arrow as arc length
393 - angle. opening angle
394 - constrictionlen: None (no constriction) or arc length of constriction.
397 # arc length and coordinates of tip
398 tx, ty = anormpath.at(arclenfrombegin)
400 # construct the template for the arrow by cutting the path at the
401 # corresponding length
402 arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
404 # from this template, we construct the two outer curves of the arrow
405 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
406 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
408 # now come the joining backward parts
409 if constrictionlen is not None:
410 # constriction point (cx, cy) lies on path
411 cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
412 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
413 arrow = arrowl.reversed() << arrowr << arrowcr
414 else:
415 arrow = arrowl.reversed() << arrowr
417 arrow[-1].close()
419 return arrow
422 _base = 6 * unit.v_pt
424 class arrow(deco, attr.attr):
426 """arrow is a decorator which adds an arrow to either side of the path"""
428 def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
429 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
430 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
431 self.pos = pos
432 self.reversed = reversed
433 self.size = size
434 self.angle = angle
435 self.constriction = constriction
437 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
438 if attrs is None:
439 attrs = self.attrs
440 if pos is None:
441 pos = self.pos
442 if reversed is None:
443 reversed = self.reversed
444 if size is None:
445 size = self.size
446 if angle is None:
447 angle = self.angle
448 if constriction is _marker:
449 constriction = self.constriction
450 return arrow(attrs=attrs, pos=pos, reversed=reversed, size=size, angle=angle, constriction=constriction)
452 def decorate(self, dp, texrunner):
453 dp.ensurenormpath()
454 anormpath = dp.path
456 # calculate absolute arc length of constricition
457 # Note that we have to correct this length because the arrowtemplates are rotated
458 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
459 # self.constriction = 1, we actually have a length which is approximately shorter
460 # by the given geometrical factor.
461 if self.constriction is not None:
462 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
463 else:
464 # if we do not want a constriction, i.e. constriction is None, we still
465 # need constrictionlen for cutting the path
466 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
467 arrowheadconstrictionlen = None
469 arclenfrombegin = self.pos * anormpath.arclen()
470 direction = self.reversed and -1 or 1
471 arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle, arrowheadconstrictionlen)
473 # add arrowhead to decoratedpath
474 dp.ornaments.draw(arrowhead, self.attrs)
476 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
477 if self.pos == 0 and self.reversed:
478 dp.excluderange(0, min(self.size, constrictionlen))
479 elif self.pos == 1 and not self.reversed:
480 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
482 arrow.clear = attr.clearclass(arrow)
484 # arrows at begin of path
485 barrow = arrow(pos=0, reversed=1)
486 barrow.SMALL = barrow(size=_base/math.sqrt(64))
487 barrow.SMALl = barrow(size=_base/math.sqrt(32))
488 barrow.SMAll = barrow(size=_base/math.sqrt(16))
489 barrow.SMall = barrow(size=_base/math.sqrt(8))
490 barrow.Small = barrow(size=_base/math.sqrt(4))
491 barrow.small = barrow(size=_base/math.sqrt(2))
492 barrow.normal = barrow(size=_base)
493 barrow.large = barrow(size=_base*math.sqrt(2))
494 barrow.Large = barrow(size=_base*math.sqrt(4))
495 barrow.LArge = barrow(size=_base*math.sqrt(8))
496 barrow.LARge = barrow(size=_base*math.sqrt(16))
497 barrow.LARGe = barrow(size=_base*math.sqrt(32))
498 barrow.LARGE = barrow(size=_base*math.sqrt(64))
500 # arrows at end of path
501 earrow = arrow()
502 earrow.SMALL = earrow(size=_base/math.sqrt(64))
503 earrow.SMALl = earrow(size=_base/math.sqrt(32))
504 earrow.SMAll = earrow(size=_base/math.sqrt(16))
505 earrow.SMall = earrow(size=_base/math.sqrt(8))
506 earrow.Small = earrow(size=_base/math.sqrt(4))
507 earrow.small = earrow(size=_base/math.sqrt(2))
508 earrow.normal = earrow(size=_base)
509 earrow.large = earrow(size=_base*math.sqrt(2))
510 earrow.Large = earrow(size=_base*math.sqrt(4))
511 earrow.LArge = earrow(size=_base*math.sqrt(8))
512 earrow.LARge = earrow(size=_base*math.sqrt(16))
513 earrow.LARGe = earrow(size=_base*math.sqrt(32))
514 earrow.LARGE = earrow(size=_base*math.sqrt(64))
517 class text(deco, attr.attr):
518 """a simple text decorator"""
520 def __init__(self, text, textattrs=[], angle=0, textdist=0.2,
521 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
522 texrunner=None):
523 if arclenfrombegin is not None and arclenfromend is not None:
524 raise ValueError("either set arclenfrombegin or arclenfromend")
525 self.text = text
526 self.textattrs = textattrs
527 self.angle = angle
528 self.textdist = textdist
529 self.relarclenpos = relarclenpos
530 self.arclenfrombegin = arclenfrombegin
531 self.arclenfromend = arclenfromend
532 self.texrunner = texrunner
534 def decorate(self, dp, texrunner):
535 if self.texrunner:
536 texrunner = self.texrunner
537 import text as textmodule
538 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
540 dp.ensurenormpath()
541 if self.arclenfrombegin is not None:
542 x, y = dp.path.at(dp.path.begin() + self.arclenfrombegin)
543 elif self.arclenfromend is not None:
544 x, y = dp.path.at(dp.path.end() - self.arclenfromend)
545 else:
546 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
547 x, y = dp.path.at(self.relarclenpos * dp.path.arclen())
549 t = texrunner.text(x, y, self.text, textattrs)
550 t.linealign(self.textdist, math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180))
551 dp.ornaments.insert(t)
554 class shownormpath(deco, attr.attr):
556 def decorate(self, dp, texrunner):
557 r_pt = 2
558 dp.ensurenormpath()
559 for normsubpath in dp.path.normsubpaths:
560 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
561 if isinstance(normsubpathitem, normpath.normcurve_pt):
562 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.green])
563 else:
564 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.blue])
565 for normsubpath in dp.path.normsubpaths:
566 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
567 if isinstance(normsubpathitem, normpath.normcurve_pt):
568 dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt, normsubpathitem.x1_pt, normsubpathitem.y1_pt), [style.linestyle.dashed, color.rgb.red])
569 dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, normsubpathitem.x3_pt, normsubpathitem.y3_pt), [style.linestyle.dashed, color.rgb.red])
570 dp.ornaments.draw(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, r_pt), [filled([color.rgb.red])])
571 dp.ornaments.draw(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, r_pt), [filled([color.rgb.red])])
572 for normsubpath in dp.path.normsubpaths:
573 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
574 if not i:
575 x_pt, y_pt = normsubpathitem.atbegin_pt()
576 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
577 x_pt, y_pt = normsubpathitem.atend_pt()
578 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])