add bitmap graph style (TODO: documentation)
[PyX/mjg.git] / pyx / deco.py
blobd068f6dbadd52895ca89ef9605e99a903e031844
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2011 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2011 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, deformer
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, fillrule=style.fillrule.nonzero_winding):
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 # the fillrule is either fillrule.nonzero_winding or fillrule.even_odd
66 self.fillrule = fillrule
68 self.nostrokeranges = None
70 def ensurenormpath(self):
71 """convert self.path into a normpath"""
72 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
73 self.path = self.path.normpath()
75 def excluderange(self, begin, end):
76 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
77 if self.nostrokeranges is None:
78 self.nostrokeranges = [(begin, end)]
79 else:
80 ibegin = 0
81 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
82 ibegin += 1
84 if ibegin == len(self.nostrokeranges):
85 self.nostrokeranges.append((begin, end))
86 return
88 iend = len(self.nostrokeranges) - 1
89 while 0 <= iend and end < self.nostrokeranges[iend][0]:
90 iend -= 1
92 if iend == -1:
93 self.nostrokeranges.insert(0, (begin, end))
94 return
96 if self.nostrokeranges[ibegin][0] < begin:
97 begin = self.nostrokeranges[ibegin][0]
98 if end < self.nostrokeranges[iend][1]:
99 end = self.nostrokeranges[iend][1]
101 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
103 def bbox(self):
104 pathbbox = self.path.bbox()
105 ornamentsbbox = self.ornaments.bbox()
106 if ornamentsbbox is not None:
107 return ornamentsbbox + pathbbox
108 else:
109 return pathbbox
111 def strokepath(self):
112 if self.nostrokeranges:
113 splitlist = []
114 for begin, end in self.nostrokeranges:
115 splitlist.append(begin)
116 splitlist.append(end)
117 split = self.path.split(splitlist)
118 # XXX properly handle closed paths?
119 result = split[0]
120 for i in range(2, len(split), 2):
121 result += split[i]
122 return result
123 else:
124 return self.path
126 def processPS(self, file, writer, context, registry, bbox):
127 # draw (stroke and/or fill) the decoratedpath on the canvas
128 # while trying to produce an efficient output, e.g., by
129 # not writing one path two times
131 # small helper
132 def _writestyles(styles, context, registry, bbox):
133 for style in styles:
134 style.processPS(file, writer, context, registry, bbox)
136 if self.strokestyles is None and self.fillstyles is None:
137 if not len(self.ornaments):
138 raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
139 # just draw additional elements of decoratedpath
140 self.ornaments.processPS(file, writer, context, registry, bbox)
141 return
143 strokepath = self.strokepath()
144 fillpath = self.path
146 # apply global styles
147 if self.styles:
148 file.write("gsave\n")
149 context = context()
150 _writestyles(self.styles, context, registry, bbox)
152 if self.fillstyles is not None:
153 file.write("newpath\n")
154 fillpath.outputPS(file, writer)
156 if self.strokestyles is not None and strokepath is fillpath:
157 # do efficient stroking + filling if respective paths are identical
158 file.write("gsave\n")
160 if self.fillstyles:
161 _writestyles(self.fillstyles, context(), registry, bbox)
163 if self.fillrule.even_odd:
164 file.write("eofill\n")
165 else:
166 file.write("fill\n")
167 file.write("grestore\n")
169 acontext = context()
170 if self.strokestyles:
171 file.write("gsave\n")
172 _writestyles(self.strokestyles, acontext, registry, bbox)
174 file.write("stroke\n")
175 # take linewidth into account for bbox when stroking a path
176 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
178 if self.strokestyles:
179 file.write("grestore\n")
180 else:
181 # only fill fillpath - for the moment
182 if self.fillstyles:
183 file.write("gsave\n")
184 _writestyles(self.fillstyles, context(), registry, bbox)
186 if self.fillrule.even_odd:
187 file.write("eofill\n")
188 else:
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 if self.fillrule.even_odd:
268 file.write("B*\n")
269 else:
270 file.write("B\n") # both stroke and fill
271 # take linewidth into account for bbox when stroking a path
272 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
274 file.write("Q\n") # grestore
275 else:
276 # only fill fillpath - for the moment
277 if self.fillstyles:
278 file.write("q\n") # gsave
279 _writefillstyles(self.fillstyles, context(), registry, bbox)
281 if self.fillrule.even_odd:
282 file.write("f*\n")
283 else:
284 file.write("f\n") # fill
285 bbox += fillpath.bbox()
287 if self.fillstyles:
288 file.write("Q\n") # grestore
290 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
291 # this is the only relevant case still left
292 # Note that a possible stroking has already been done.
293 acontext = context()
295 if self.strokestyles:
296 file.write("q\n") # gsave
297 _writestrokestyles(self.strokestyles, acontext, registry, bbox)
299 strokepath.outputPDF(file, writer)
300 file.write("S\n") # stroke
301 # take linewidth into account for bbox when stroking a path
302 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
304 if self.strokestyles:
305 file.write("Q\n") # grestore
307 # now, draw additional elements of decoratedpath
308 self.ornaments.processPDF(file, writer, context, registry, bbox)
310 # restore global styles
311 if self.styles:
312 file.write("Q\n") # grestore
315 # Path decorators
318 class deco:
320 """decorators
322 In contrast to path styles, path decorators depend on the concrete
323 path to which they are applied. In particular, they don't make
324 sense without any path and can thus not be used in canvas.set!
328 def decorate(self, dp, texrunner):
329 """apply a style to a given decoratedpath object dp
331 decorate accepts a decoratedpath object dp, applies PathStyle
332 by modifying dp in place.
335 pass
338 # stroked and filled: basic decos which stroked and fill,
339 # respectively the path
342 class _stroked(deco, attr.exclusiveattr):
344 """stroked is a decorator, which draws the outline of the path"""
346 def __init__(self, styles=[]):
347 attr.exclusiveattr.__init__(self, _stroked)
348 self.styles = attr.mergeattrs(styles)
349 attr.checkattrs(self.styles, [style.strokestyle])
351 def __call__(self, styles=[]):
352 # XXX or should we also merge self.styles
353 return _stroked(styles)
355 def decorate(self, dp, texrunner):
356 if dp.strokestyles is not None:
357 raise RuntimeError("Cannot stroke an already stroked path")
358 dp.strokestyles = self.styles
360 stroked = _stroked()
361 stroked.clear = attr.clearclass(_stroked)
364 class _filled(deco, attr.exclusiveattr):
366 """filled is a decorator, which fills the interior of the path"""
368 def __init__(self, styles=[]):
369 attr.exclusiveattr.__init__(self, _filled)
370 self.styles = attr.mergeattrs(styles)
371 attr.checkattrs(self.styles, [style.fillstyle])
373 def __call__(self, styles=[]):
374 # XXX or should we also merge self.styles
375 return _filled(styles)
377 def decorate(self, dp, texrunner):
378 if dp.fillstyles is not None:
379 raise RuntimeError("Cannot fill an already filled path")
380 dp.fillstyles = self.styles
382 filled = _filled()
383 filled.clear = attr.clearclass(_filled)
386 # Arrows
389 # helper function which constructs the arrowhead
391 def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constrictionlen):
393 """helper routine, which returns an arrowhead from a given anormpath
395 - arclenfrombegin: position of arrow in arc length from the start of the path
396 - direction: +1 for an arrow pointing along the direction of anormpath or
397 -1 for an arrow pointing opposite to the direction of normpath
398 - size: size of the arrow as arc length
399 - angle. opening angle
400 - constrictionlen: None (no constriction) or arc length of constriction.
403 # arc length and coordinates of tip
404 tx, ty = anormpath.at(arclenfrombegin)
406 # construct the template for the arrow by cutting the path at the
407 # corresponding length
408 arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
410 # from this template, we construct the two outer curves of the arrow
411 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
412 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
414 # now come the joining backward parts
415 if constrictionlen is not None:
416 # constriction point (cx, cy) lies on path
417 cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
418 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
419 arrow = arrowl.reversed() << arrowr << arrowcr
420 else:
421 arrow = arrowl.reversed() << arrowr
423 arrow[-1].close()
425 return arrow
428 _base = 6 * unit.v_pt
430 class arrow(deco, attr.attr):
432 """arrow is a decorator which adds an arrow to either side of the path"""
434 def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
435 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
436 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
437 self.pos = pos
438 self.reversed = reversed
439 self.size = size
440 self.angle = angle
441 self.constriction = constriction
443 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
444 if attrs is None:
445 attrs = self.attrs
446 if pos is None:
447 pos = self.pos
448 if reversed is None:
449 reversed = self.reversed
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, pos=pos, reversed=reversed, 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(math.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(math.radians(self.angle/2.0))
473 arrowheadconstrictionlen = None
475 arclenfrombegin = (1-self.reversed)*constrictionlen + self.pos * (anormpath.arclen() - constrictionlen)
476 direction = self.reversed and -1 or 1
477 arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle, arrowheadconstrictionlen)
479 # add arrowhead to decoratedpath
480 dp.ornaments.draw(arrowhead, self.attrs)
482 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
483 if self.pos == 0 and self.reversed:
484 dp.excluderange(0, min(self.size, constrictionlen))
485 elif self.pos == 1 and not self.reversed:
486 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
488 arrow.clear = attr.clearclass(arrow)
490 # arrows at begin of path
491 barrow = arrow(pos=0, reversed=1)
492 barrow.SMALL = barrow(size=_base/math.sqrt(64))
493 barrow.SMALl = barrow(size=_base/math.sqrt(32))
494 barrow.SMAll = barrow(size=_base/math.sqrt(16))
495 barrow.SMall = barrow(size=_base/math.sqrt(8))
496 barrow.Small = barrow(size=_base/math.sqrt(4))
497 barrow.small = barrow(size=_base/math.sqrt(2))
498 barrow.normal = barrow(size=_base)
499 barrow.large = barrow(size=_base*math.sqrt(2))
500 barrow.Large = barrow(size=_base*math.sqrt(4))
501 barrow.LArge = barrow(size=_base*math.sqrt(8))
502 barrow.LARge = barrow(size=_base*math.sqrt(16))
503 barrow.LARGe = barrow(size=_base*math.sqrt(32))
504 barrow.LARGE = barrow(size=_base*math.sqrt(64))
506 # arrows at end of path
507 earrow = arrow()
508 earrow.SMALL = earrow(size=_base/math.sqrt(64))
509 earrow.SMALl = earrow(size=_base/math.sqrt(32))
510 earrow.SMAll = earrow(size=_base/math.sqrt(16))
511 earrow.SMall = earrow(size=_base/math.sqrt(8))
512 earrow.Small = earrow(size=_base/math.sqrt(4))
513 earrow.small = earrow(size=_base/math.sqrt(2))
514 earrow.normal = earrow(size=_base)
515 earrow.large = earrow(size=_base*math.sqrt(2))
516 earrow.Large = earrow(size=_base*math.sqrt(4))
517 earrow.LArge = earrow(size=_base*math.sqrt(8))
518 earrow.LARge = earrow(size=_base*math.sqrt(16))
519 earrow.LARGe = earrow(size=_base*math.sqrt(32))
520 earrow.LARGE = earrow(size=_base*math.sqrt(64))
523 class text(deco, attr.attr):
524 """a simple text decorator"""
526 def __init__(self, text, textattrs=[], angle=0, relangle=None, textdist=0.2,
527 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
528 texrunner=None):
529 if arclenfrombegin is not None and arclenfromend is not None:
530 raise ValueError("either set arclenfrombegin or arclenfromend")
531 self.text = text
532 self.textattrs = textattrs
533 self.angle = angle
534 self.relangle = relangle
535 self.textdist = textdist
536 self.relarclenpos = relarclenpos
537 self.arclenfrombegin = arclenfrombegin
538 self.arclenfromend = arclenfromend
539 self.texrunner = texrunner
541 def decorate(self, dp, texrunner):
542 if self.texrunner:
543 texrunner = self.texrunner
544 import text as textmodule
545 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
547 dp.ensurenormpath()
548 if self.arclenfrombegin is not None:
549 param = dp.path.begin() + self.arclenfrombegin
550 elif self.arclenfromend is not None:
551 param = dp.path.end() - self.arclenfromend
552 else:
553 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
554 param = self.relarclenpos * dp.path.arclen()
555 x, y = dp.path.at(param)
557 if self.relangle is not None:
558 a = dp.path.trafo(param).apply_pt(math.cos(self.relangle*math.pi/180), math.sin(self.relangle*math.pi/180))
559 b = dp.path.trafo(param).apply_pt(0, 0)
560 angle = math.atan2(a[1] - b[1], a[0] - b[0])
561 else:
562 angle = self.angle*math.pi/180
563 t = texrunner.text(x, y, self.text, textattrs)
564 t.linealign(self.textdist, math.cos(angle), math.sin(angle))
565 dp.ornaments.insert(t)
568 class shownormpath(deco, attr.attr):
570 def decorate(self, dp, texrunner):
571 r_pt = 2
572 dp.ensurenormpath()
573 for normsubpath in dp.path.normsubpaths:
574 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
575 if isinstance(normsubpathitem, normpath.normcurve_pt):
576 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.green])
577 else:
578 dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.blue])
579 for normsubpath in dp.path.normsubpaths:
580 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
581 if isinstance(normsubpathitem, normpath.normcurve_pt):
582 dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt, normsubpathitem.x1_pt, normsubpathitem.y1_pt), [style.linestyle.dashed, color.rgb.red])
583 dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, normsubpathitem.x3_pt, normsubpathitem.y3_pt), [style.linestyle.dashed, color.rgb.red])
584 dp.ornaments.draw(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, r_pt), [filled([color.rgb.red])])
585 dp.ornaments.draw(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, r_pt), [filled([color.rgb.red])])
586 for normsubpath in dp.path.normsubpaths:
587 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
588 if not i:
589 x_pt, y_pt = normsubpathitem.atbegin_pt()
590 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
591 x_pt, y_pt = normsubpathitem.atend_pt()
592 dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
595 class linehatched(deco, attr.exclusiveattr, attr.clearclass):
596 """draws a pattern with explicit lines
598 This class acts as a drop-in replacement for postscript patterns
599 from the pattern module which are not understood by some printers"""
601 def __init__(self, dist, angle, strokestyles=[], cross=0):
602 attr.clearclass.__init__(self, _filled)
603 attr.exclusiveattr.__init__(self, linehatched)
604 self.dist = dist
605 self.angle = angle
606 self.strokestyles = attr.mergeattrs([style.linewidth.THIN] + strokestyles)
607 attr.checkattrs(self.strokestyles, [style.strokestyle])
608 self.cross = cross
610 def __call__(self, dist=None, angle=None, strokestyles=None, cross=None):
611 if dist is None:
612 dist = self.dist
613 if angle is None:
614 angle = self.angle
615 if strokestyles is None:
616 strokestyles = self.strokestyles
617 if cross is None:
618 cross = self.cross
619 return linehatched(dist, angle, strokestyles, cross)
621 def _decocanvas(self, angle, dp, texrunner):
622 dp.ensurenormpath()
623 dist_pt = unit.topt(self.dist)
625 c = canvas.canvas([canvas.clip(dp.path)])
626 llx_pt, lly_pt, urx_pt, ury_pt = dp.path.bbox().highrestuple_pt()
627 center_pt = 0.5*(llx_pt+urx_pt), 0.5*(lly_pt+ury_pt)
628 radius_pt = 0.5*math.hypot(urx_pt-llx_pt, ury_pt-lly_pt) + dist_pt
629 n = int(2*radius_pt / dist_pt) + 1
630 for i in range(n):
631 x_pt = center_pt[0] - radius_pt + i*dist_pt
632 c.stroke(path.line_pt(x_pt, center_pt[1]-radius_pt, x_pt, center_pt[1]+radius_pt),
633 [trafo.rotate_pt(angle, center_pt[0], center_pt[1])] + self.strokestyles)
634 return c
636 def decorate(self, dp, texrunner):
637 dp.ornaments.insert(self._decocanvas(self.angle, dp, texrunner))
638 if self.cross:
639 dp.ornaments.insert(self._decocanvas(self.angle+90, dp, texrunner))
641 def merge(self, attrs):
642 # act as attr.clearclass and as attr.exclusiveattr at the same time
643 newattrs = attr.exclusiveattr.merge(self, attrs)
644 return attr.clearclass.merge(self, newattrs)
646 linehatched.clear = attr.clearclass(linehatched)
648 _hatch_base = 0.1 * unit.v_cm
650 linehatched0 = linehatched(_hatch_base, 0)
651 linehatched0.SMALL = linehatched0(_hatch_base/math.sqrt(64))
652 linehatched0.SMALL = linehatched0(_hatch_base/math.sqrt(64))
653 linehatched0.SMALl = linehatched0(_hatch_base/math.sqrt(32))
654 linehatched0.SMAll = linehatched0(_hatch_base/math.sqrt(16))
655 linehatched0.SMall = linehatched0(_hatch_base/math.sqrt(8))
656 linehatched0.Small = linehatched0(_hatch_base/math.sqrt(4))
657 linehatched0.small = linehatched0(_hatch_base/math.sqrt(2))
658 linehatched0.normal = linehatched0(_hatch_base)
659 linehatched0.large = linehatched0(_hatch_base*math.sqrt(2))
660 linehatched0.Large = linehatched0(_hatch_base*math.sqrt(4))
661 linehatched0.LArge = linehatched0(_hatch_base*math.sqrt(8))
662 linehatched0.LARge = linehatched0(_hatch_base*math.sqrt(16))
663 linehatched0.LARGe = linehatched0(_hatch_base*math.sqrt(32))
664 linehatched0.LARGE = linehatched0(_hatch_base*math.sqrt(64))
666 linehatched45 = linehatched(_hatch_base, 45)
667 linehatched45.SMALL = linehatched45(_hatch_base/math.sqrt(64))
668 linehatched45.SMALl = linehatched45(_hatch_base/math.sqrt(32))
669 linehatched45.SMAll = linehatched45(_hatch_base/math.sqrt(16))
670 linehatched45.SMall = linehatched45(_hatch_base/math.sqrt(8))
671 linehatched45.Small = linehatched45(_hatch_base/math.sqrt(4))
672 linehatched45.small = linehatched45(_hatch_base/math.sqrt(2))
673 linehatched45.normal = linehatched45(_hatch_base)
674 linehatched45.large = linehatched45(_hatch_base*math.sqrt(2))
675 linehatched45.Large = linehatched45(_hatch_base*math.sqrt(4))
676 linehatched45.LArge = linehatched45(_hatch_base*math.sqrt(8))
677 linehatched45.LARge = linehatched45(_hatch_base*math.sqrt(16))
678 linehatched45.LARGe = linehatched45(_hatch_base*math.sqrt(32))
679 linehatched45.LARGE = linehatched45(_hatch_base*math.sqrt(64))
681 linehatched90 = linehatched(_hatch_base, 90)
682 linehatched90.SMALL = linehatched90(_hatch_base/math.sqrt(64))
683 linehatched90.SMALl = linehatched90(_hatch_base/math.sqrt(32))
684 linehatched90.SMAll = linehatched90(_hatch_base/math.sqrt(16))
685 linehatched90.SMall = linehatched90(_hatch_base/math.sqrt(8))
686 linehatched90.Small = linehatched90(_hatch_base/math.sqrt(4))
687 linehatched90.small = linehatched90(_hatch_base/math.sqrt(2))
688 linehatched90.normal = linehatched90(_hatch_base)
689 linehatched90.large = linehatched90(_hatch_base*math.sqrt(2))
690 linehatched90.Large = linehatched90(_hatch_base*math.sqrt(4))
691 linehatched90.LArge = linehatched90(_hatch_base*math.sqrt(8))
692 linehatched90.LARge = linehatched90(_hatch_base*math.sqrt(16))
693 linehatched90.LARGe = linehatched90(_hatch_base*math.sqrt(32))
694 linehatched90.LARGE = linehatched90(_hatch_base*math.sqrt(64))
696 linehatched135 = linehatched(_hatch_base, 135)
697 linehatched135.SMALL = linehatched135(_hatch_base/math.sqrt(64))
698 linehatched135.SMALl = linehatched135(_hatch_base/math.sqrt(32))
699 linehatched135.SMAll = linehatched135(_hatch_base/math.sqrt(16))
700 linehatched135.SMall = linehatched135(_hatch_base/math.sqrt(8))
701 linehatched135.Small = linehatched135(_hatch_base/math.sqrt(4))
702 linehatched135.small = linehatched135(_hatch_base/math.sqrt(2))
703 linehatched135.normal = linehatched135(_hatch_base)
704 linehatched135.large = linehatched135(_hatch_base*math.sqrt(2))
705 linehatched135.Large = linehatched135(_hatch_base*math.sqrt(4))
706 linehatched135.LArge = linehatched135(_hatch_base*math.sqrt(8))
707 linehatched135.LARge = linehatched135(_hatch_base*math.sqrt(16))
708 linehatched135.LARGe = linehatched135(_hatch_base*math.sqrt(32))
709 linehatched135.LARGE = linehatched135(_hatch_base*math.sqrt(64))
711 crosslinehatched0 = linehatched(_hatch_base, 0, cross=1)
712 crosslinehatched0.SMALL = crosslinehatched0(_hatch_base/math.sqrt(64))
713 crosslinehatched0.SMALl = crosslinehatched0(_hatch_base/math.sqrt(32))
714 crosslinehatched0.SMAll = crosslinehatched0(_hatch_base/math.sqrt(16))
715 crosslinehatched0.SMall = crosslinehatched0(_hatch_base/math.sqrt(8))
716 crosslinehatched0.Small = crosslinehatched0(_hatch_base/math.sqrt(4))
717 crosslinehatched0.small = crosslinehatched0(_hatch_base/math.sqrt(2))
718 crosslinehatched0.normal = crosslinehatched0
719 crosslinehatched0.large = crosslinehatched0(_hatch_base*math.sqrt(2))
720 crosslinehatched0.Large = crosslinehatched0(_hatch_base*math.sqrt(4))
721 crosslinehatched0.LArge = crosslinehatched0(_hatch_base*math.sqrt(8))
722 crosslinehatched0.LARge = crosslinehatched0(_hatch_base*math.sqrt(16))
723 crosslinehatched0.LARGe = crosslinehatched0(_hatch_base*math.sqrt(32))
724 crosslinehatched0.LARGE = crosslinehatched0(_hatch_base*math.sqrt(64))
726 crosslinehatched45 = linehatched(_hatch_base, 45, cross=1)
727 crosslinehatched45.SMALL = crosslinehatched45(_hatch_base/math.sqrt(64))
728 crosslinehatched45.SMALl = crosslinehatched45(_hatch_base/math.sqrt(32))
729 crosslinehatched45.SMAll = crosslinehatched45(_hatch_base/math.sqrt(16))
730 crosslinehatched45.SMall = crosslinehatched45(_hatch_base/math.sqrt(8))
731 crosslinehatched45.Small = crosslinehatched45(_hatch_base/math.sqrt(4))
732 crosslinehatched45.small = crosslinehatched45(_hatch_base/math.sqrt(2))
733 crosslinehatched45.normal = crosslinehatched45
734 crosslinehatched45.large = crosslinehatched45(_hatch_base*math.sqrt(2))
735 crosslinehatched45.Large = crosslinehatched45(_hatch_base*math.sqrt(4))
736 crosslinehatched45.LArge = crosslinehatched45(_hatch_base*math.sqrt(8))
737 crosslinehatched45.LARge = crosslinehatched45(_hatch_base*math.sqrt(16))
738 crosslinehatched45.LARGe = crosslinehatched45(_hatch_base*math.sqrt(32))
739 crosslinehatched45.LARGE = crosslinehatched45(_hatch_base*math.sqrt(64))
742 class colorgradient(deco, attr.attr):
743 """inserts pieces of the path in different colors"""
745 def __init__(self, grad, attrs=[], steps=20):
746 self.attrs = attrs
747 self.grad = grad
748 self.steps = steps
750 def decorate(self, dp, texrunner):
751 dp.ensurenormpath()
752 l = dp.path.arclen()
754 colors = [self.grad.select(n, self.steps) for n in range(self.steps)]
755 colors.reverse()
756 params = dp.path.arclentoparam([l*i/float(self.steps) for i in range(self.steps)])
757 params.reverse()
759 c = canvas.canvas()
760 # treat the end pieces separately
761 c.stroke(dp.path.split(params[1])[1], attr.mergeattrs([colors[0]] + self.attrs))
762 for n in range(1,self.steps-1):
763 c.stroke(dp.path.split([params[n-1],params[n+1]])[1], attr.mergeattrs([colors[n]] + self.attrs))
764 c.stroke(dp.path.split(params[-2])[0], attr.mergeattrs([colors[-1]] + self.attrs))
765 dp.ornaments.insert(c)
768 class brace(deco, attr.attr):
769 r"""draws a nicely curled brace
771 In most cases, the original line is not wanted use canvas.canvas.draw(..) for it
773 Geometrical parameters:
775 inner /\ strokes
776 ____________/ \__________
777 / bar bar \ outer
778 / \ strokes
780 totalheight distance from the jaws to the middle cap
781 barthickness thickness of the main bars
782 innerstrokesthickness thickness of the two ending strokes
783 outerstrokesthickness thickness of the inner strokes at the middle cap
784 innerstrokesrelheight height of the inner/outer strokes, relative to the total height
785 outerstrokesrelheight this determines the angle of the main bars!
786 should be around 0.5
787 Note: if innerstrokesrelheight + outerstrokesrelheight == 1 then the main bars
788 will be aligned parallel to the connecting line between the endpoints
789 outerstrokesangle angle of the two ending strokes
790 innerstrokesangle angle between the inner strokes at the middle cap
791 slantstrokesangle extra slanting of the inner/outer strokes
792 innerstrokessmoothness smoothing parameter for the inner + outer strokes
793 outerstrokessmoothness should be around 1 (allowed: [0,infty))
794 middlerelpos position of the middle cap (0 == left, 1 == right)
796 # This code is experimental because it is unclear
797 # how the brace fits into the concepts of PyX
799 # Some thoughts:
800 # - a brace needs to be decoratable with text
801 # it needs stroking and filling attributes
802 # - the brace is not really a box:
803 # it has two "anchor" points that are important for aligning it to other things
804 # and one "anchor" point (plus direction) for aligning other things
805 # - a brace is not a deformer:
806 # it does not look at anything else than begin/endpoint of a path
807 # - a brace might be a connector (which is to be dissolved into the box concept later?)
809 def __init__(self, reverse=1, stretch=None, dist=None, fillattrs=[],
810 totalheight=12*unit.x_pt,
811 barthickness=0.5*unit.x_pt, innerstrokesthickness=0.25*unit.x_pt, outerstrokesthickness=0.25*unit.x_pt,
812 innerstrokesrelheight=0.6, outerstrokesrelheight=0.7,
813 innerstrokesangle=30, outerstrokesangle=25, slantstrokesangle=5,
814 innerstrokessmoothness=2.0, outerstrokessmoothness=2.5,
815 middlerelpos=0.5):
816 self.fillattrs = fillattrs
817 self.reverse = reverse
818 self.stretch = stretch
819 self.dist = dist
820 self.totalheight = totalheight
821 self.barthickness = barthickness
822 self.innerstrokesthickness = innerstrokesthickness
823 self.outerstrokesthickness = outerstrokesthickness
824 self.innerstrokesrelheight = innerstrokesrelheight
825 self.outerstrokesrelheight = outerstrokesrelheight
826 self.innerstrokesangle = innerstrokesangle
827 self.outerstrokesangle = outerstrokesangle
828 self.slantstrokesangle = slantstrokesangle
829 self.innerstrokessmoothness = innerstrokessmoothness
830 self.outerstrokessmoothness = outerstrokessmoothness
831 self.middlerelpos = middlerelpos
833 def __call__(self, **kwargs):
834 for name in ["reverse", "stretch", "dist", "fillattrs",
835 "totalheight", "barthickness", "innerstrokesthickness", "outerstrokesthickness",
836 "innerstrokesrelheight", "outerstrokesrelheight", "innerstrokesangle", "outerstrokesangle", "slantstrokesangle",
837 "innerstrokessmoothness", "outerstrokessmoothness", "middlerelpos"]:
838 if not kwargs.has_key(name):
839 kwargs[name] = self.__dict__[name]
840 return brace(**kwargs)
842 def _halfbracepath_pt(self, length_pt, height_pt, ilength_pt, olength_pt, # <<<
843 ithick_pt, othick_pt, bthick_pt, cos_iangle, sin_iangle, cos_oangle,
844 sin_oangle, cos_slangle, sin_slangle):
846 ismooth = self.innerstrokessmoothness
847 osmooth = self.outerstrokessmoothness
849 # these two parameters are not important enough to be seen outside
850 inner_cap_param = 1.5
851 outer_cap_param = 2.5
852 outerextracurved = 0.6 # in (0, 1]
853 # 1.0 will lead to F=G, the outer strokes will not be curved at their ends.
854 # The smaller, the more curvature
856 # build an orientation path (three straight lines)
858 # \q1
859 # / \
860 # / \
861 # _/ \______________________________________q5
862 # q2 q3 q4 \
865 # \q6
867 # get the points for that:
868 q1 = (0, height_pt - inner_cap_param * ithick_pt + 0.5*ithick_pt/sin_iangle)
869 q2 = (q1[0] + ilength_pt * sin_iangle,
870 q1[1] - ilength_pt * cos_iangle)
871 q6 = (length_pt, 0)
872 q5 = (q6[0] - olength_pt * sin_oangle,
873 q6[1] + olength_pt * cos_oangle)
874 bardir = (q5[0] - q2[0], q5[1] - q2[1])
875 bardirnorm = math.hypot(*bardir)
876 bardir = (bardir[0]/bardirnorm, bardir[1]/bardirnorm)
877 ismoothlength_pt = ilength_pt * ismooth
878 osmoothlength_pt = olength_pt * osmooth
879 if bardirnorm < ismoothlength_pt + osmoothlength_pt:
880 ismoothlength_pt = bardirnorm * ismoothlength_pt / (ismoothlength_pt + osmoothlength_pt)
881 osmoothlength_pt = bardirnorm * osmoothlength_pt / (ismoothlength_pt + osmoothlength_pt)
882 q3 = (q2[0] + ismoothlength_pt * bardir[0],
883 q2[1] + ismoothlength_pt * bardir[1])
884 q4 = (q5[0] - osmoothlength_pt * bardir[0],
885 q5[1] - osmoothlength_pt * bardir[1])
888 # P _O
889 # / | \A2
890 # / A1\ \
891 # / \ B2C2________D2___________E2_______F2___G2
892 # \______________________________________ \
893 # B1,C1 D1 E1 F1 G1 \
894 # \ \
895 # \ \H2
896 # H1\_/I2
897 # I1
899 # the halfbraces meet in P and A1:
900 P = (0, height_pt)
901 A1 = (0, height_pt - inner_cap_param * ithick_pt)
902 # A2 is A1, shifted by the inner thickness
903 A2 = (A1[0] + ithick_pt * cos_iangle,
904 A1[1] + ithick_pt * sin_iangle)
905 s, t = deformer.intersection(P, A2, (cos_slangle, sin_slangle), (sin_iangle, -cos_iangle))
906 O = (P[0] + s * cos_slangle,
907 P[1] + s * sin_slangle)
909 # from D1 to E1 is the straight part of the brace
910 # also back from E2 to D1
911 D1 = (q3[0] + bthick_pt * bardir[1],
912 q3[1] - bthick_pt * bardir[0])
913 D2 = (q3[0] - bthick_pt * bardir[1],
914 q3[1] + bthick_pt * bardir[0])
915 E1 = (q4[0] + bthick_pt * bardir[1],
916 q4[1] - bthick_pt * bardir[0])
917 E2 = (q4[0] - bthick_pt * bardir[1],
918 q4[1] + bthick_pt * bardir[0])
919 # I1, I2 are the control points at the outer stroke
920 I1 = (q6[0] - 0.5 * othick_pt * cos_oangle,
921 q6[1] - 0.5 * othick_pt * sin_oangle)
922 I2 = (q6[0] + 0.5 * othick_pt * cos_oangle,
923 q6[1] + 0.5 * othick_pt * sin_oangle)
924 # get the control points for the curved parts of the brace
925 s, t = deformer.intersection(A1, D1, (sin_iangle, -cos_iangle), bardir)
926 B1 = (D1[0] + t * bardir[0],
927 D1[1] + t * bardir[1])
928 s, t = deformer.intersection(A2, D2, (sin_iangle, -cos_iangle), bardir)
929 B2 = (D2[0] + t * bardir[0],
930 D2[1] + t * bardir[1])
931 s, t = deformer.intersection(E1, I1, bardir, (-sin_oangle, cos_oangle))
932 G1 = (E1[0] + s * bardir[0],
933 E1[1] + s * bardir[1])
934 s, t = deformer.intersection(E2, I2, bardir, (-sin_oangle, cos_oangle))
935 G2 = (E2[0] + s * bardir[0],
936 E2[1] + s * bardir[1])
937 # at the inner strokes: use curvature zero at both ends
938 C1 = B1
939 C2 = B2
940 # at the outer strokes: use curvature zero only at the connection to
941 # the straight part
942 F1 = (outerextracurved * G1[0] + (1 - outerextracurved) * E1[0],
943 outerextracurved * G1[1] + (1 - outerextracurved) * E1[1])
944 F2 = (outerextracurved * G2[0] + (1 - outerextracurved) * E2[0],
945 outerextracurved * G2[1] + (1 - outerextracurved) * E2[1])
946 # the tip of the outer stroke, endpoints of the bezier curve
947 H1 = (I1[0] - outer_cap_param * othick_pt * sin_oangle,
948 I1[1] + outer_cap_param * othick_pt * cos_oangle)
949 H2 = (I2[0] - outer_cap_param * othick_pt * sin_oangle,
950 I2[1] + outer_cap_param * othick_pt * cos_oangle)
952 #for qq in [A1,B1,C1,D1,E1,F1,G1,H1,I1,
953 # A2,B2,C2,D2,E2,F2,G2,H2,I2,
954 # O,P
955 # ]:
956 # cc.fill(path.circle(qq[0], qq[1], 0.5), [color.rgb.green])
958 # now build the right halfbrace
959 bracepath = path.path(path.moveto_pt(*A1))
960 bracepath.append(path.curveto_pt(B1[0], B1[1], C1[0], C1[1], D1[0], D1[1]))
961 bracepath.append(path.lineto_pt(E1[0], E1[1]))
962 bracepath.append(path.curveto_pt(F1[0], F1[1], G1[0], G1[1], H1[0], H1[1]))
963 # the tip of the right halfbrace
964 bracepath.append(path.curveto_pt(I1[0], I1[1], I2[0], I2[1], H2[0], H2[1]))
965 # the rest of the right halfbrace
966 bracepath.append(path.curveto_pt(G2[0], G2[1], F2[0], F2[1], E2[0], E2[1]))
967 bracepath.append(path.lineto_pt(D2[0], D2[1]))
968 bracepath.append(path.curveto_pt(C2[0], C2[1], B2[0], B2[1], A2[0], A2[1]))
969 # the tip in the middle of the brace
970 bracepath.append(path.curveto_pt(O[0], O[1], O[0], O[1], P[0], P[1]))
972 return bracepath
973 # >>>
975 def _bracepath(self, x0_pt, y0_pt, x1_pt, y1_pt): # <<<
976 height_pt = unit.topt(self.totalheight)
977 totallength_pt = math.hypot(x1_pt - x0_pt, y1_pt - y0_pt)
978 leftlength_pt = self.middlerelpos * totallength_pt
979 rightlength_pt = totallength_pt - leftlength_pt
980 ithick_pt = unit.topt(self.innerstrokesthickness)
981 othick_pt = unit.topt(self.outerstrokesthickness)
982 bthick_pt = unit.topt(self.barthickness)
984 # create the left halfbrace with positive slanting
985 # because we will mirror this part
986 cos_iangle = math.cos(math.radians(0.5*self.innerstrokesangle - self.slantstrokesangle))
987 sin_iangle = math.sin(math.radians(0.5*self.innerstrokesangle - self.slantstrokesangle))
988 cos_oangle = math.cos(math.radians(self.outerstrokesangle - self.slantstrokesangle))
989 sin_oangle = math.sin(math.radians(self.outerstrokesangle - self.slantstrokesangle))
990 cos_slangle = math.cos(math.radians(-self.slantstrokesangle))
991 sin_slangle = math.sin(math.radians(-self.slantstrokesangle))
992 ilength_pt = self.innerstrokesrelheight * height_pt / cos_iangle
993 olength_pt = self.outerstrokesrelheight * height_pt / cos_oangle
995 bracepath = self._halfbracepath_pt(leftlength_pt, height_pt,
996 ilength_pt, olength_pt, ithick_pt, othick_pt, bthick_pt, cos_iangle,
997 sin_iangle, cos_oangle, sin_oangle, cos_slangle,
998 sin_slangle).reversed().transformed(trafo.mirror(90))
1000 # create the right halfbrace with negative slanting
1001 cos_iangle = math.cos(math.radians(0.5*self.innerstrokesangle + self.slantstrokesangle))
1002 sin_iangle = math.sin(math.radians(0.5*self.innerstrokesangle + self.slantstrokesangle))
1003 cos_oangle = math.cos(math.radians(self.outerstrokesangle + self.slantstrokesangle))
1004 sin_oangle = math.sin(math.radians(self.outerstrokesangle + self.slantstrokesangle))
1005 cos_slangle = math.cos(math.radians(-self.slantstrokesangle))
1006 sin_slangle = math.sin(math.radians(-self.slantstrokesangle))
1007 ilength_pt = self.innerstrokesrelheight * height_pt / cos_iangle
1008 olength_pt = self.outerstrokesrelheight * height_pt / cos_oangle
1010 bracepath = bracepath << self._halfbracepath_pt(rightlength_pt, height_pt,
1011 ilength_pt, olength_pt, ithick_pt, othick_pt, bthick_pt, cos_iangle,
1012 sin_iangle, cos_oangle, sin_oangle, cos_slangle,
1013 sin_slangle)
1015 return bracepath.transformed(
1016 # two trafos for matching the given endpoints
1017 trafo.translate_pt(x0_pt, y0_pt) *
1018 trafo.rotate_pt(math.degrees(math.atan2(y1_pt-y0_pt, x1_pt-x0_pt))) *
1019 # one trafo to move the brace's left outer stroke to zero
1020 trafo.translate_pt(leftlength_pt, 0))
1021 # >>>
1023 def decorate(self, dp, texrunner):
1024 dp.ensurenormpath()
1025 x0_pt, y0_pt = dp.path.atbegin_pt()
1026 x1_pt, y1_pt = dp.path.atend_pt()
1027 if self.reverse:
1028 x0_pt, y0_pt, x1_pt, y1_pt = x1_pt, y1_pt, x0_pt, y0_pt
1029 if self.stretch is not None:
1030 xm, ym = 0.5*(x0_pt+x1_pt), 0.5*(y0_pt+y1_pt)
1031 x0_pt, y0_pt = xm + self.stretch*(x0_pt-xm), ym + self.stretch*(y0_pt-ym)
1032 x1_pt, y1_pt = xm + self.stretch*(x1_pt-xm), ym + self.stretch*(y1_pt-ym)
1033 if self.dist is not None:
1034 d = unit.topt(self.dist)
1035 dx, dy = dp.path.rotation_pt(dp.path.begin()).apply_pt(0, 1)
1036 x0_pt += d*dx; y0_pt += d*dy
1037 dx, dy = dp.path.rotation_pt(dp.path.end()).apply_pt(0, 1)
1038 x1_pt += d*dx; y1_pt += d*dy
1039 dp.ornaments.fill(self._bracepath(x0_pt, y0_pt, x1_pt, y1_pt), self.fillattrs)
1041 brace.clear = attr.clearclass(brace)
1043 leftbrace = brace(reverse=0, middlerelpos=0.55, innerstrokesrelheight=0.6, outerstrokesrelheight=0.7, slantstrokesangle=-10)
1044 rightbrace = brace(reverse=1, middlerelpos=0.45, innerstrokesrelheight=0.6, outerstrokesrelheight=0.7, slantstrokesangle=10)
1045 belowbrace = brace(reverse=1, middlerelpos=0.55, innerstrokesrelheight=0.7, outerstrokesrelheight=0.9, slantstrokesangle=-10)
1046 abovebrace = brace(reverse=0, middlerelpos=0.45, innerstrokesrelheight=0.7, outerstrokesrelheight=0.9, slantstrokesangle=-10)
1047 straightbrace = brace(innerstrokesrelheight=0.5, outerstrokesrelheight=0.5,
1048 innerstrokesangle=30, outerstrokesangle=30, slantstrokesangle=0,
1049 innerstrokessmoothness=1.0, outerstrokessmoothness=1.0)