example for the numerical instability of a tangent at a bezier cusp
[PyX/mjg.git] / pyx / deco.py
blob0476f4910af85dc1af949633f0374042ba7550b5
1 #!/usr/bin/env python
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
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, 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 registerPS(self, registry):
118 if self.styles:
119 for style in self.styles:
120 style.registerPS(registry)
121 if self.fillstyles:
122 for style in self.fillstyles:
123 style.registerPS(registry)
124 if self.strokestyles:
125 for style in self.strokestyles:
126 style.registerPS(registry)
127 self.ornaments.registerPS(registry)
129 def registerPDF(self, registry):
130 if self.styles:
131 for style in self.styles:
132 style.registerPDF(registry)
133 if self.fillstyles:
134 for style in self.fillstyles:
135 style.registerPDF(registry)
136 if self.strokestyles:
137 for style in self.strokestyles:
138 style.registerPDF(registry)
139 self.ornaments.registerPDF(registry)
141 def strokepath(self):
142 if self.nostrokeranges:
143 splitlist = []
144 for begin, end in self.nostrokeranges:
145 splitlist.append(begin)
146 splitlist.append(end)
147 split = self.path.split(splitlist)
148 # XXX properly handle closed paths?
149 result = split[0]
150 for i in range(2, len(split), 2):
151 result += split[i]
152 return result
153 else:
154 return self.path
156 def outputPS(self, file, writer, context):
157 # draw (stroke and/or fill) the decoratedpath on the canvas
158 # while trying to produce an efficient output, e.g., by
159 # not writing one path two times
161 # small helper
162 def _writestyles(styles, context):
163 for style in styles:
164 style.outputPS(file, writer, context)
166 if self.strokestyles is None and self.fillstyles is None:
167 raise RuntimeError("Path neither to be stroked nor filled")
169 strokepath = self.strokepath()
170 fillpath = self.path
172 # apply global styles
173 if self.styles:
174 file.write("gsave\n")
175 context = context()
176 _writestyles(self.styles, context)
178 if self.fillstyles is not None:
179 file.write("newpath\n")
180 fillpath.outputPS(file, writer, context)
182 if self.strokestyles is not None and strokepath is fillpath:
183 # do efficient stroking + filling if respective paths are identical
184 file.write("gsave\n")
186 if self.fillstyles:
187 _writestyles(self.fillstyles, context())
189 file.write("fill\n")
190 file.write("grestore\n")
192 if self.strokestyles:
193 file.write("gsave\n")
194 _writestyles(self.strokestyles, context())
196 file.write("stroke\n")
198 if self.strokestyles:
199 file.write("grestore\n")
200 else:
201 # only fill fillpath - for the moment
202 if self.fillstyles:
203 file.write("gsave\n")
204 _writestyles(self.fillstyles, context())
206 file.write("fill\n")
208 if self.fillstyles:
209 file.write("grestore\n")
211 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
212 # this is the only relevant case still left
213 # Note that a possible stroking has already been done.
214 oldcontext = context
215 context = context()
217 if self.strokestyles:
218 file.write("gsave\n")
219 _writestyles(self.strokestyles, context)
221 file.write("newpath\n")
222 strokepath.outputPS(file, writer, context)
223 file.write("stroke\n")
225 if self.strokestyles:
226 file.write("grestore\n")
228 context = oldcontext
230 # now, draw additional elements of decoratedpath
231 self.ornaments.outputPS(file, writer, context)
233 # restore global styles
234 if self.styles:
235 file.write("grestore\n")
237 def outputPDF(self, file, writer, context):
238 # draw (stroke and/or fill) the decoratedpath on the canvas
240 def _writestyles(styles, context):
241 for style in styles:
242 style.outputPDF(file, writer, context)
244 def _writestrokestyles(strokestyles, context):
245 for style in strokestyles:
246 style.outputPDF(file, writer, context(fillattr=0))
248 def _writefillstyles(fillstyles, context):
249 for style in fillstyles:
250 style.outputPDF(file, writer, context(strokeattr=0))
252 if self.strokestyles is None and self.fillstyles is None:
253 raise RuntimeError("Path neither to be stroked nor filled")
255 strokepath = self.strokepath()
256 fillpath = self.path
258 # apply global styles
259 if self.styles:
260 file.write("q\n") # gsave
261 context = context()
262 _writestyles(self.styles, context)
264 if self.fillstyles is not None:
265 fillpath.outputPDF(file, writer, context)
267 if self.strokestyles is not None and strokepath is fillpath:
268 # do efficient stroking + filling
269 file.write("q\n") # gsave
270 oldcontext = context
271 context = context()
273 if self.fillstyles:
274 _writefillstyles(self.fillstyles, context)
275 if self.strokestyles:
276 _writestrokestyles(self.strokestyles, context)
278 file.write("B\n") # both stroke and fill
279 file.write("Q\n") # grestore
280 context = oldcontext
281 else:
282 # only fill fillpath - for the moment
283 if self.fillstyles:
284 file.write("q\n") # gsave
285 _writefillstyles(self.fillstyles, context())
287 file.write("f\n") # fill
289 if self.fillstyles:
290 file.write("Q\n") # grestore
292 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
293 # this is the only relevant case still left
294 # Note that a possible stroking has already been done.
295 oldcontext = context
296 context = context()
298 if self.strokestyles:
299 file.write("q\n") # gsave
300 _writestrokestyles(self.strokestyles, context)
302 strokepath.outputPDF(file, writer, context)
303 file.write("S\n") # stroke
305 if self.strokestyles:
306 file.write("Q\n") # grestore
307 context = oldcontext
309 # now, draw additional elements of decoratedpath
310 self.ornaments.outputPDF(file, writer, context)
312 # restore global styles
313 if self.styles:
314 file.write("Q\n") # grestore
317 # Path decorators
320 class deco:
322 """decorators
324 In contrast to path styles, path decorators depend on the concrete
325 path to which they are applied. In particular, they don't make
326 sense without any path and can thus not be used in canvas.set!
330 def decorate(self, dp, texrunner):
331 """apply a style to a given decoratedpath object dp
333 decorate accepts a decoratedpath object dp, applies PathStyle
334 by modifying dp in place.
337 pass
340 # stroked and filled: basic decos which stroked and fill,
341 # respectively the path
344 class _stroked(deco, attr.exclusiveattr):
346 """stroked is a decorator, which draws the outline of the path"""
348 def __init__(self, styles=[]):
349 attr.exclusiveattr.__init__(self, _stroked)
350 self.styles = attr.mergeattrs(styles)
351 attr.checkattrs(self.styles, [style.strokestyle])
353 def __call__(self, styles=[]):
354 # XXX or should we also merge self.styles
355 return _stroked(styles)
357 def decorate(self, dp, texrunner):
358 if dp.strokestyles is not None:
359 raise RuntimeError("Cannot stroke an already stroked path")
360 dp.strokestyles = self.styles
362 stroked = _stroked()
363 stroked.clear = attr.clearclass(_stroked)
366 class _filled(deco, attr.exclusiveattr):
368 """filled is a decorator, which fills the interior of the path"""
370 def __init__(self, styles=[]):
371 attr.exclusiveattr.__init__(self, _filled)
372 self.styles = attr.mergeattrs(styles)
373 attr.checkattrs(self.styles, [style.fillstyle])
375 def __call__(self, styles=[]):
376 # XXX or should we also merge self.styles
377 return _filled(styles)
379 def decorate(self, dp, texrunner):
380 if dp.fillstyles is not None:
381 raise RuntimeError("Cannot fill an already filled path")
382 dp.fillstyles = self.styles
384 filled = _filled()
385 filled.clear = attr.clearclass(_filled)
388 # Arrows
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.
402 if reversed:
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
412 # of the arrow
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
423 else:
424 arrow = arrowl.reversed() << arrowr
426 arrow[-1].close()
428 return arrow
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
441 self.size = size
442 self.angle = angle
443 self.constriction = constriction
445 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=_marker):
446 if attrs is None:
447 attrs = self.attrs
448 if position is None:
449 position = self.position
450 if size is None:
451 size = self.size
452 if angle is None:
453 angle = self.angle
454 if constriction is _marker:
455 constriction = self.constriction
456 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
458 def decorate(self, dp, texrunner):
459 dp.ensurenormpath()
460 anormpath = dp.path
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))
469 else:
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)
477 else:
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))
486 else:
487 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
489 arrow.clear = attr.clearclass(arrow)
491 # arrows at begin of path
492 barrow = arrow(position=0)
493 barrow.SMALL = barrow(size=_base/math.sqrt(64))
494 barrow.SMALl = barrow(size=_base/math.sqrt(32))
495 barrow.SMAll = barrow(size=_base/math.sqrt(16))
496 barrow.SMall = barrow(size=_base/math.sqrt(8))
497 barrow.Small = barrow(size=_base/math.sqrt(4))
498 barrow.small = barrow(size=_base/math.sqrt(2))
499 barrow.normal = barrow(size=_base)
500 barrow.large = barrow(size=_base*math.sqrt(2))
501 barrow.Large = barrow(size=_base*math.sqrt(4))
502 barrow.LArge = barrow(size=_base*math.sqrt(8))
503 barrow.LARge = barrow(size=_base*math.sqrt(16))
504 barrow.LARGe = barrow(size=_base*math.sqrt(32))
505 barrow.LARGE = barrow(size=_base*math.sqrt(64))
507 # arrows at end of path
508 earrow = arrow(position=1)
509 earrow.SMALL = earrow(size=_base/math.sqrt(64))
510 earrow.SMALl = earrow(size=_base/math.sqrt(32))
511 earrow.SMAll = earrow(size=_base/math.sqrt(16))
512 earrow.SMall = earrow(size=_base/math.sqrt(8))
513 earrow.Small = earrow(size=_base/math.sqrt(4))
514 earrow.small = earrow(size=_base/math.sqrt(2))
515 earrow.normal = earrow(size=_base)
516 earrow.large = earrow(size=_base*math.sqrt(2))
517 earrow.Large = earrow(size=_base*math.sqrt(4))
518 earrow.LArge = earrow(size=_base*math.sqrt(8))
519 earrow.LARge = earrow(size=_base*math.sqrt(16))
520 earrow.LARGe = earrow(size=_base*math.sqrt(32))
521 earrow.LARGE = earrow(size=_base*math.sqrt(64))
525 class text(deco, attr.attr):
526 """a simple text decorator"""
528 def __init__(self, text, textattrs=[], angle=0, textdist=0.2,
529 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
530 texrunner=None):
531 if arclenfrombegin is not None and arclenfromend is not None:
532 raise ValueError("either set arclenfrombegin or arclenfromend")
533 self.text = text
534 self.textattrs = textattrs
535 self.angle = angle
536 self.textdist = textdist
537 self.relarclenpos = relarclenpos
538 self.arclenfrombegin = arclenfrombegin
539 self.arclenfromend = arclenfromend
540 self.texrunner = texrunner
542 def decorate(self, dp, texrunner):
543 if self.texrunner:
544 texrunner = self.texrunner
545 import text as textmodule
546 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
548 # note that we cannot call ensurenormpath (the asserts might be a bit too strong)
549 if self.arclenfrombegin is not None:
550 x, y = dp.path.at(dp.path.begin() + self.arclenfrombegin)
551 elif self.arclenfromend is not None:
552 x, y = dp.path.at(dp.path.end() - self.arclenfromend)
553 else:
554 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
555 x, y = dp.path.at(self.relarclenpos * dp.path.arclen())
557 t = texrunner.text(x, y, self.text, textattrs)
558 t.linealign(self.textdist, math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180))
559 dp.ornaments.insert(t)