pyx/connector.py
[PyX/mjg.git] / pyx / deco.py
blob2f6be5d5d19400dd189761d44d4bc55f4daa0296
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, 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 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 if not len(self.ornaments):
168 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
169 # just draw additional elements of decoratedpath
170 self.ornaments.outputPS(file, writer, context)
171 return
173 strokepath = self.strokepath()
174 fillpath = self.path
176 # apply global styles
177 if self.styles:
178 file.write("gsave\n")
179 context = context()
180 _writestyles(self.styles, context)
182 if self.fillstyles is not None:
183 file.write("newpath\n")
184 fillpath.outputPS(file, writer, context)
186 if self.strokestyles is not None and strokepath is fillpath:
187 # do efficient stroking + filling if respective paths are identical
188 file.write("gsave\n")
190 if self.fillstyles:
191 _writestyles(self.fillstyles, context())
193 file.write("fill\n")
194 file.write("grestore\n")
196 if self.strokestyles:
197 file.write("gsave\n")
198 _writestyles(self.strokestyles, context())
200 file.write("stroke\n")
202 if self.strokestyles:
203 file.write("grestore\n")
204 else:
205 # only fill fillpath - for the moment
206 if self.fillstyles:
207 file.write("gsave\n")
208 _writestyles(self.fillstyles, context())
210 file.write("fill\n")
212 if self.fillstyles:
213 file.write("grestore\n")
215 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
216 # this is the only relevant case still left
217 # Note that a possible stroking has already been done.
218 oldcontext = context
219 context = context()
221 if self.strokestyles:
222 file.write("gsave\n")
223 _writestyles(self.strokestyles, context)
225 file.write("newpath\n")
226 strokepath.outputPS(file, writer, context)
227 file.write("stroke\n")
229 if self.strokestyles:
230 file.write("grestore\n")
232 context = oldcontext
234 # now, draw additional elements of decoratedpath
235 self.ornaments.outputPS(file, writer, context)
237 # restore global styles
238 if self.styles:
239 file.write("grestore\n")
241 def outputPDF(self, file, writer, context):
242 # draw (stroke and/or fill) the decoratedpath on the canvas
244 def _writestyles(styles, context):
245 for style in styles:
246 style.outputPDF(file, writer, context)
248 def _writestrokestyles(strokestyles, context):
249 for style in strokestyles:
250 style.outputPDF(file, writer, context(fillattr=0))
252 def _writefillstyles(fillstyles, context):
253 for style in fillstyles:
254 style.outputPDF(file, writer, context(strokeattr=0))
256 if self.strokestyles is None and self.fillstyles is None:
257 if not len(self.ornaments):
258 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
259 # just draw additional elements of decoratedpath
260 self.ornaments.outputPDF(file, writer, context)
261 return
263 strokepath = self.strokepath()
264 fillpath = self.path
266 # apply global styles
267 if self.styles:
268 file.write("q\n") # gsave
269 context = context()
270 _writestyles(self.styles, context)
272 if self.fillstyles is not None:
273 fillpath.outputPDF(file, writer, context)
275 if self.strokestyles is not None and strokepath is fillpath:
276 # do efficient stroking + filling
277 file.write("q\n") # gsave
278 oldcontext = context
279 context = context()
281 if self.fillstyles:
282 _writefillstyles(self.fillstyles, context)
283 if self.strokestyles:
284 _writestrokestyles(self.strokestyles, context)
286 file.write("B\n") # both stroke and fill
287 file.write("Q\n") # grestore
288 context = oldcontext
289 else:
290 # only fill fillpath - for the moment
291 if self.fillstyles:
292 file.write("q\n") # gsave
293 _writefillstyles(self.fillstyles, context())
295 file.write("f\n") # fill
297 if self.fillstyles:
298 file.write("Q\n") # grestore
300 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
301 # this is the only relevant case still left
302 # Note that a possible stroking has already been done.
303 oldcontext = context
304 context = context()
306 if self.strokestyles:
307 file.write("q\n") # gsave
308 _writestrokestyles(self.strokestyles, context)
310 strokepath.outputPDF(file, writer, context)
311 file.write("S\n") # stroke
313 if self.strokestyles:
314 file.write("Q\n") # grestore
315 context = oldcontext
317 # now, draw additional elements of decoratedpath
318 self.ornaments.outputPDF(file, writer, context)
320 # restore global styles
321 if self.styles:
322 file.write("Q\n") # grestore
325 # Path decorators
328 class deco:
330 """decorators
332 In contrast to path styles, path decorators depend on the concrete
333 path to which they are applied. In particular, they don't make
334 sense without any path and can thus not be used in canvas.set!
338 def decorate(self, dp, texrunner):
339 """apply a style to a given decoratedpath object dp
341 decorate accepts a decoratedpath object dp, applies PathStyle
342 by modifying dp in place.
345 pass
348 # stroked and filled: basic decos which stroked and fill,
349 # respectively the path
352 class _stroked(deco, attr.exclusiveattr):
354 """stroked is a decorator, which draws the outline of the path"""
356 def __init__(self, styles=[]):
357 attr.exclusiveattr.__init__(self, _stroked)
358 self.styles = attr.mergeattrs(styles)
359 attr.checkattrs(self.styles, [style.strokestyle])
361 def __call__(self, styles=[]):
362 # XXX or should we also merge self.styles
363 return _stroked(styles)
365 def decorate(self, dp, texrunner):
366 if dp.strokestyles is not None:
367 raise RuntimeError("Cannot stroke an already stroked path")
368 dp.strokestyles = self.styles
370 stroked = _stroked()
371 stroked.clear = attr.clearclass(_stroked)
374 class _filled(deco, attr.exclusiveattr):
376 """filled is a decorator, which fills the interior of the path"""
378 def __init__(self, styles=[]):
379 attr.exclusiveattr.__init__(self, _filled)
380 self.styles = attr.mergeattrs(styles)
381 attr.checkattrs(self.styles, [style.fillstyle])
383 def __call__(self, styles=[]):
384 # XXX or should we also merge self.styles
385 return _filled(styles)
387 def decorate(self, dp, texrunner):
388 if dp.fillstyles is not None:
389 raise RuntimeError("Cannot fill an already filled path")
390 dp.fillstyles = self.styles
392 filled = _filled()
393 filled.clear = attr.clearclass(_filled)
396 # Arrows
399 # helper function which constructs the arrowhead
401 def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constrictionlen):
403 """helper routine, which returns an arrowhead from a given anormpath
405 - arclenfrombegin: position of arrow in arc length from the start of the path
406 - direction: +1 for an arrow pointing along the direction of anormpath or
407 -1 for an arrow pointing opposite to the direction of normpath
408 - size: size of the arrow as arc length
409 - angle. opening angle
410 - constrictionlen: None (no constriction) or arc length of constriction.
413 # arc length and coordinates of tip
414 tx, ty = anormpath.at(arclenfrombegin)
416 # construct the template for the arrow by cutting the path at the
417 # corresponding length
418 arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
420 # from this template, we construct the two outer curves of the arrow
421 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
422 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
424 # now come the joining backward parts
425 if constrictionlen is not None:
426 # constriction point (cx, cy) lies on path
427 cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
428 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
429 arrow = arrowl.reversed() << arrowr << arrowcr
430 else:
431 arrow = arrowl.reversed() << arrowr
433 arrow[-1].close()
435 return arrow
438 _base = 6 * unit.v_pt
440 class arrow(deco, attr.attr):
442 """arrow is a decorator which adds an arrow to either side of the path"""
444 def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
445 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
446 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
447 self.pos = pos
448 self.reversed = reversed
449 self.size = size
450 self.angle = angle
451 self.constriction = constriction
453 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
454 if attrs is None:
455 attrs = self.attrs
456 if pos is None:
457 pos = self.pos
458 if reversed is None:
459 reversed = self.reversed
460 if size is None:
461 size = self.size
462 if angle is None:
463 angle = self.angle
464 if constriction is _marker:
465 constriction = self.constriction
466 return arrow(attrs=attrs, pos=pos, reversed=reversed, size=size, angle=angle, constriction=constriction)
468 def decorate(self, dp, texrunner):
469 dp.ensurenormpath()
470 anormpath = dp.path
472 # calculate absolute arc length of constricition
473 # Note that we have to correct this length because the arrowtemplates are rotated
474 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
475 # self.constriction = 1, we actually have a length which is approximately shorter
476 # by the given geometrical factor.
477 if self.constriction is not None:
478 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
479 else:
480 # if we do not want a constriction, i.e. constriction is None, we still
481 # need constrictionlen for cutting the path
482 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
483 arrowheadconstrictionlen = None
485 arclenfrombegin = self.pos * anormpath.arclen()
486 direction = self.reversed and -1 or 1
487 arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle, arrowheadconstrictionlen)
489 # add arrowhead to decoratedpath
490 dp.ornaments.draw(arrowhead, self.attrs)
492 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
493 if self.pos == 0 and self.reversed:
494 dp.excluderange(0, min(self.size, constrictionlen))
495 elif self.pos == 1 and not self.reversed:
496 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
498 arrow.clear = attr.clearclass(arrow)
500 # arrows at begin of path
501 barrow = arrow(pos=0, reversed=1)
502 barrow.SMALL = barrow(size=_base/math.sqrt(64))
503 barrow.SMALl = barrow(size=_base/math.sqrt(32))
504 barrow.SMAll = barrow(size=_base/math.sqrt(16))
505 barrow.SMall = barrow(size=_base/math.sqrt(8))
506 barrow.Small = barrow(size=_base/math.sqrt(4))
507 barrow.small = barrow(size=_base/math.sqrt(2))
508 barrow.normal = barrow(size=_base)
509 barrow.large = barrow(size=_base*math.sqrt(2))
510 barrow.Large = barrow(size=_base*math.sqrt(4))
511 barrow.LArge = barrow(size=_base*math.sqrt(8))
512 barrow.LARge = barrow(size=_base*math.sqrt(16))
513 barrow.LARGe = barrow(size=_base*math.sqrt(32))
514 barrow.LARGE = barrow(size=_base*math.sqrt(64))
516 # arrows at end of path
517 earrow = arrow()
518 earrow.SMALL = earrow(size=_base/math.sqrt(64))
519 earrow.SMALl = earrow(size=_base/math.sqrt(32))
520 earrow.SMAll = earrow(size=_base/math.sqrt(16))
521 earrow.SMall = earrow(size=_base/math.sqrt(8))
522 earrow.Small = earrow(size=_base/math.sqrt(4))
523 earrow.small = earrow(size=_base/math.sqrt(2))
524 earrow.normal = earrow(size=_base)
525 earrow.large = earrow(size=_base*math.sqrt(2))
526 earrow.Large = earrow(size=_base*math.sqrt(4))
527 earrow.LArge = earrow(size=_base*math.sqrt(8))
528 earrow.LARge = earrow(size=_base*math.sqrt(16))
529 earrow.LARGe = earrow(size=_base*math.sqrt(32))
530 earrow.LARGE = earrow(size=_base*math.sqrt(64))
533 class text(deco, attr.attr):
534 """a simple text decorator"""
536 def __init__(self, text, textattrs=[], angle=0, textdist=0.2,
537 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
538 texrunner=None):
539 if arclenfrombegin is not None and arclenfromend is not None:
540 raise ValueError("either set arclenfrombegin or arclenfromend")
541 self.text = text
542 self.textattrs = textattrs
543 self.angle = angle
544 self.textdist = textdist
545 self.relarclenpos = relarclenpos
546 self.arclenfrombegin = arclenfrombegin
547 self.arclenfromend = arclenfromend
548 self.texrunner = texrunner
550 def decorate(self, dp, texrunner):
551 if self.texrunner:
552 texrunner = self.texrunner
553 import text as textmodule
554 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
556 dp.ensurenormpath()
557 if self.arclenfrombegin is not None:
558 x, y = dp.path.at(dp.path.begin() + self.arclenfrombegin)
559 elif self.arclenfromend is not None:
560 x, y = dp.path.at(dp.path.end() - self.arclenfromend)
561 else:
562 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
563 x, y = dp.path.at(self.relarclenpos * dp.path.arclen())
565 t = texrunner.text(x, y, self.text, textattrs)
566 t.linealign(self.textdist, math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180))
567 dp.ornaments.insert(t)
570 class shownormpath(deco, attr.attr):
572 def decorate(self, dp, texrunner):
573 r_pt = 2
574 dp.ensurenormpath()
575 for normsubpath in dp.path.normsubpaths:
576 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
577 if isinstance(normsubpathitem, normpath.normcurve_pt):
578 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.green])
579 else:
580 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.blue])
581 for normsubpath in dp.path.normsubpaths:
582 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
583 if isinstance(normsubpathitem, normpath.normcurve_pt):
584 dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt, normsubpathitem.x1_pt, normsubpathitem.y1_pt), [style.linestyle.dashed, color.rgb.red])
585 dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, normsubpathitem.x3_pt, normsubpathitem.y3_pt), [style.linestyle.dashed, color.rgb.red])
586 dp.ornaments.draw(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, r_pt), [filled([color.rgb.red])])
587 dp.ornaments.draw(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, r_pt), [filled([color.rgb.red])])
588 for normsubpath in dp.path.normsubpaths:
589 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
590 if not i:
591 x_pt, y_pt = normsubpathitem.atbegin_pt()
592 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
593 x_pt, y_pt = normsubpathitem.atend_pt()
594 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])