Applied (morally) as 2952
[PyX/mjg.git] / pyx / deco.py
blobac1ff5927ae68c4ae62f896e74cf382bef38f9b5
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 import sys, math
29 import attr, canvas, canvasitem, color, path, normpath, style, trafo, unit
31 _marker = object()
34 # Decorated path
37 class decoratedpath(canvasitem.canvasitem):
38 """Decorated path
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.
43 """
45 def __init__(self, path, strokepath=None, fillpath=None,
46 styles=None, strokestyles=None, fillstyles=None,
47 ornaments=None):
49 self.path = path
51 # global style for stroking and filling and subdps
52 self.styles = styles
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.
60 if ornaments is None:
61 self.ornaments = canvas.canvas()
62 else:
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)]
76 else:
77 ibegin = 0
78 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
79 ibegin += 1
81 if ibegin == len(self.nostrokeranges):
82 self.nostrokeranges.append((begin, end))
83 return
85 iend = len(self.nostrokeranges) - 1
86 while 0 <= iend and end < self.nostrokeranges[iend][0]:
87 iend -= 1
89 if iend == -1:
90 self.nostrokeranges.insert(0, (begin, end))
91 return
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)]
100 def bbox(self):
101 pathbbox = self.path.bbox()
102 ornamentsbbox = self.ornaments.bbox()
103 if ornamentsbbox is not None:
104 return ornamentsbbox + pathbbox
105 else:
106 return pathbbox
108 def strokepath(self):
109 if self.nostrokeranges:
110 splitlist = []
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?
116 result = split[0]
117 for i in range(2, len(split), 2):
118 result += split[i]
119 return result
120 else:
121 return self.path
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
128 # small helper
129 def _writestyles(styles, context, registry, bbox):
130 for style in styles:
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)
138 return
140 strokepath = self.strokepath()
141 fillpath = self.path
143 # apply global styles
144 if self.styles:
145 file.write("gsave\n")
146 context = context()
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")
157 if self.fillstyles:
158 _writestyles(self.fillstyles, context(), registry, bbox)
160 file.write("fill\n")
161 file.write("grestore\n")
163 acontext = context()
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")
174 else:
175 # only fill fillpath - for the moment
176 if self.fillstyles:
177 file.write("gsave\n")
178 _writestyles(self.fillstyles, context(), registry, bbox)
180 file.write("fill\n")
181 bbox += fillpath.bbox()
183 if self.fillstyles:
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.
189 acontext = context()
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
207 if self.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):
214 for style in styles:
215 style.processPDF(file, writer, context, registry, bbox)
217 def _writestrokestyles(strokestyles, context, registry, bbox):
218 context.fillattr = 0
219 for style in strokestyles:
220 style.processPDF(file, writer, context, registry, bbox)
221 context.fillattr = 1
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)
234 return
236 strokepath = self.strokepath()
237 fillpath = self.path
239 # apply global styles
240 if self.styles:
241 file.write("q\n") # gsave
242 context = context()
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
251 acontext = context()
253 if self.fillstyles:
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
263 else:
264 # only fill fillpath - for the moment
265 if self.fillstyles:
266 file.write("q\n") # gsave
267 _writefillstyles(self.fillstyles, context(), registry, bbox)
269 file.write("f\n") # fill
270 bbox += fillpath.bbox()
272 if self.fillstyles:
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.
278 acontext = context()
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
296 if self.styles:
297 file.write("Q\n") # grestore
300 # Path decorators
303 class deco:
305 """decorators
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.
320 pass
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
345 stroked = _stroked()
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
367 filled = _filled()
368 filled.clear = attr.clearclass(_filled)
371 # Arrows
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
405 else:
406 arrow = arrowl.reversed() << arrowr
408 arrow[-1].close()
410 return arrow
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])
422 self.pos = pos
423 self.reversed = reversed
424 self.size = size
425 self.angle = angle
426 self.constriction = constriction
428 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
429 if attrs is None:
430 attrs = self.attrs
431 if pos is None:
432 pos = self.pos
433 if reversed is None:
434 reversed = self.reversed
435 if size is None:
436 size = self.size
437 if angle is None:
438 angle = self.angle
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):
444 dp.ensurenormpath()
445 anormpath = dp.path
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))
454 else:
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
492 earrow = arrow()
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,
513 texrunner=None):
514 if arclenfrombegin is not None and arclenfromend is not None:
515 raise ValueError("either set arclenfrombegin or arclenfromend")
516 self.text = text
517 self.textattrs = textattrs
518 self.angle = angle
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):
527 if self.texrunner:
528 texrunner = self.texrunner
529 import text as textmodule
530 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
532 dp.ensurenormpath()
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
537 else:
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])
546 else:
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)
552 class ttext(deco, attr.attr):
553 """a simple text decorator with directed output"""
555 def __init__(self, text, textattrs=[], angle=None, textdist=0.2,
556 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
557 texrunner=None):
558 if arclenfrombegin is not None and arclenfromend is not None:
559 raise ValueError("either set arclenfrombegin or arclenfromend")
560 self.text = text
561 self.textattrs = textattrs
562 self.angle = angle
563 self.textdist = textdist
564 self.relarclenpos = relarclenpos
565 self.arclenfrombegin = arclenfrombegin
566 self.arclenfromend = arclenfromend
567 self.texrunner = texrunner
569 def decorate(self, dp, texrunner):
570 if self.texrunner:
571 texrunner = self.texrunner
572 import text as textmodule
573 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
575 dp.ensurenormpath()
576 if self.arclenfrombegin is not None:
577 textpos = dp.path.begin() + self.arclenfrombegin
578 elif self.arclenfromend is not None:
579 textpos = dp.path.end() - self.arclenfromend
580 else:
581 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
582 textpos = self.relarclenpos * dp.path.arclen()
583 x, y = dp.path.at(textpos)
584 if self.angle is None:
585 dx, dy = dp.path.trafo(textpos).apply(0,-1) # compute normal - this is the endpoint of the vector based at x, y!
586 dx, dy = unit.topt(dx-x), unit.topt(dy-y) # linealign doesn't like units in directions
587 else:
588 dx, dy = math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180)
590 t = texrunner.text(x, y, self.text, textattrs)
591 if self.textdist <0: # lienalign doesn't handle negative distances well
592 t.linealign(-self.textdist, -dx, -dy)
593 else:
594 t.linealign(self.textdist, dx, dy)
595 dp.ornaments.insert(t)
597 class shownormpath(deco, attr.attr):
599 def decorate(self, dp, texrunner):
600 r_pt = 2
601 dp.ensurenormpath()
602 for normsubpath in dp.path.normsubpaths:
603 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
604 if isinstance(normsubpathitem, normpath.normcurve_pt):
605 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.green])
606 else:
607 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.blue])
608 for normsubpath in dp.path.normsubpaths:
609 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
610 if isinstance(normsubpathitem, normpath.normcurve_pt):
611 dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt, normsubpathitem.x1_pt, normsubpathitem.y1_pt), [style.linestyle.dashed, color.rgb.red])
612 dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, normsubpathitem.x3_pt, normsubpathitem.y3_pt), [style.linestyle.dashed, color.rgb.red])
613 dp.ornaments.draw(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, r_pt), [filled([color.rgb.red])])
614 dp.ornaments.draw(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, r_pt), [filled([color.rgb.red])])
615 for normsubpath in dp.path.normsubpaths:
616 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
617 if not i:
618 x_pt, y_pt = normsubpathitem.atbegin_pt()
619 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
620 x_pt, y_pt = normsubpathitem.atend_pt()
621 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])