fix parameter handling of path.split_pt
[PyX.git] / pyx / deco.py
bloba5be92ba7d887f706479dc04cd3fec2a8710549b
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-2013 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 from . import attr, baseclasses, canvas, color, path, normpath, style, trafo, unit, deformer
31 _marker = object()
34 # Decorated path
37 class decoratedpath(baseclasses.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):
130 for style in styles:
131 style.processPS(file, writer, context, registry)
133 strokepath = self.strokepath()
134 fillpath = self.path
136 # apply global styles
137 if self.styles:
138 file.write("gsave\n")
139 context = context()
140 _writestyles(self.styles, context, registry)
142 if self.fillstyles is not None:
143 file.write("newpath\n")
144 fillpath.outputPS(file, writer)
146 if self.strokestyles is not None and strokepath is fillpath:
147 # do efficient stroking + filling if respective paths are identical
148 file.write("gsave\n")
150 if self.fillstyles:
151 _writestyles(self.fillstyles, context(), registry)
153 if context.fillrule:
154 file.write("eofill\n")
155 else:
156 file.write("fill\n")
157 file.write("grestore\n")
159 acontext = context()
160 if self.strokestyles:
161 file.write("gsave\n")
162 _writestyles(self.strokestyles, acontext, registry)
164 file.write("stroke\n")
165 # take linewidth into account for bbox when stroking a path
166 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
168 if self.strokestyles:
169 file.write("grestore\n")
170 else:
171 # only fill fillpath - for the moment
172 if self.fillstyles:
173 file.write("gsave\n")
174 _writestyles(self.fillstyles, context(), registry)
176 if context.fillrule:
177 file.write("eofill\n")
178 else:
179 file.write("fill\n")
180 bbox += fillpath.bbox()
182 if self.fillstyles:
183 file.write("grestore\n")
185 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
186 # this is the only relevant case still left
187 # Note that a possible filling has already been done.
188 acontext = context()
189 if self.strokestyles:
190 file.write("gsave\n")
191 _writestyles(self.strokestyles, acontext, registry)
193 file.write("newpath\n")
194 strokepath.outputPS(file, writer)
195 file.write("stroke\n")
196 # take linewidth into account for bbox when stroking a path
197 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
199 if self.strokestyles:
200 file.write("grestore\n")
202 # now, draw additional elements of decoratedpath
203 self.ornaments.processPS(file, writer, context, registry, bbox)
205 # restore global styles
206 if self.styles:
207 file.write("grestore\n")
209 def processPDF(self, file, writer, context, registry, bbox):
210 # draw (stroke and/or fill) the decoratedpath on the canvas
212 def _writestyles(styles, context, registry):
213 for style in styles:
214 style.processPDF(file, writer, context, registry)
216 def _writestrokestyles(strokestyles, context, registry):
217 context.fillattr = 0
218 for style in strokestyles:
219 style.processPDF(file, writer, context, registry)
220 context.fillattr = 1
222 def _writefillstyles(fillstyles, context, registry):
223 context.strokeattr = 0
224 for style in fillstyles:
225 style.processPDF(file, writer, context, registry)
226 context.strokeattr = 1
228 strokepath = self.strokepath()
229 fillpath = self.path
231 # apply global styles
232 if self.styles:
233 file.write("q\n") # gsave
234 context = context()
235 _writestyles(self.styles, context, registry)
237 if self.fillstyles is not None:
238 fillpath.outputPDF(file, writer)
240 if self.strokestyles is not None and strokepath is fillpath:
241 # do efficient stroking + filling
242 file.write("q\n") # gsave
243 acontext = context()
245 if self.fillstyles:
246 _writefillstyles(self.fillstyles, acontext, registry)
247 if self.strokestyles:
248 _writestrokestyles(self.strokestyles, acontext, registry)
250 if context.fillrule:
251 file.write("B*\n")
252 else:
253 file.write("B\n") # both stroke and fill
254 # take linewidth into account for bbox when stroking a path
255 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
257 file.write("Q\n") # grestore
258 else:
259 # only fill fillpath - for the moment
260 if self.fillstyles:
261 file.write("q\n") # gsave
262 _writefillstyles(self.fillstyles, context(), registry)
264 if context.fillrule:
265 file.write("f*\n")
266 else:
267 file.write("f\n") # fill
268 bbox += fillpath.bbox()
270 if self.fillstyles:
271 file.write("Q\n") # grestore
273 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
274 # this is the only relevant case still left
275 # Note that a possible stroking has already been done.
276 acontext = context()
278 if self.strokestyles:
279 file.write("q\n") # gsave
280 _writestrokestyles(self.strokestyles, acontext, registry)
282 strokepath.outputPDF(file, writer)
283 file.write("S\n") # stroke
284 # take linewidth into account for bbox when stroking a path
285 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
287 if self.strokestyles:
288 file.write("Q\n") # grestore
290 # now, draw additional elements of decoratedpath
291 self.ornaments.processPDF(file, writer, context, registry, bbox)
293 # restore global styles
294 if self.styles:
295 file.write("Q\n") # grestore
297 def processSVG(self, xml, writer, context, registry, bbox):
298 def _writestyles(attrs, context):
299 for style in self.styles or []:
300 style.processSVGattrs(attrs, writer, context, registry)
302 def _writestrokestyles(attrs, context):
303 context.fillattr = False
304 for style in self.strokestyles or []:
305 style.processSVGattrs(attrs, writer, context, registry)
306 context.fillattr = True
308 def _writefillstyles(attrs, context):
309 context.strokeattr = False
310 for style in self.fillstyles or []:
311 style.processSVGattrs(attrs, writer, context, registry)
312 context.strokeattr = True
314 strokepath = self.strokepath()
315 fillpath = self.path
317 if self.styles:
318 acontext = context()
319 attrs = {}
320 _writestyles(attrs, acontext)
321 xml.startSVGElement("g", attrs)
322 else:
323 acontext = context
325 if strokepath is not fillpath:
326 if self.strokestyles is not None:
327 attrs = {"d": strokepath.returnSVGdata()}
328 _writestrokestyles(attrs, acontext)
329 attrs["stroke"] = acontext.strokecolor
330 if acontext.strokeopacity != 1:
331 attrs["opacity"] = "%f" % acontext.strokeopacity
332 xml.startSVGElement("path", attrs)
333 xml.endSVGElement("path")
334 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
335 if self.fillstyles is not None:
336 attrs = {"d": fillpath.returnSVGdata()}
337 _writefillstyles(attrs, acontext)
338 attrs["fill"] = acontext.fillcolor
339 if acontext.fillopacity != 1:
340 attrs["opacity"] = "%f" % acontext.fillopacity
341 xml.startSVGElement("path", attrs)
342 xml.endSVGElement("path")
343 bbox += fillpath.bbox()
344 else:
345 attrs = {"d": fillpath.returnSVGdata()}
346 _writestrokestyles(attrs, acontext)
347 _writefillstyles(attrs, acontext)
348 if self.strokestyles is not None:
349 attrs["stroke"] = acontext.strokecolor
350 if self.fillstyles is not None:
351 attrs["fill"] = acontext.fillcolor
352 if acontext.strokeopacity != acontext.fillopacity and self.strokestyles is not None and self.fillstyles is not None:
353 if acontext.strokeopacity != 1:
354 attrs["opacity"] = "%f" % acontext.strokeopacity
355 attrs["stroke"] = acontext.strokecolor
356 attrs["fill"] = "none"
357 xml.startSVGElement("path", attrs)
358 xml.endSVGElement("path")
359 if acontext.fillopacity != 1:
360 attrs["opacity"] = "%f" % acontext.fillopacity
361 attrs["stroke"] = "none"
362 attrs["fill"] = acontext.fillcolor
363 xml.startSVGElement("path", attrs)
364 xml.endSVGElement("path")
365 else:
366 if acontext.strokeopacity != 1 and self.strokestyles is not None:
367 attrs["opacity"] = "%f" % acontext.strokeopacity
368 if acontext.fillopacity != 1 and self.fillstyles is not None:
369 attrs["opacity"] = "%f" % acontext.fillopacity
370 xml.startSVGElement("path", attrs)
371 xml.endSVGElement("path")
372 if self.strokestyles is not None:
373 bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
374 else:
375 bbox += strokepath.bbox()
377 self.ornaments.processSVG(xml, writer, acontext, registry, bbox)
379 if self.styles:
380 xml.endSVGElement("g")
383 # Path decorators
386 class deco:
388 """decorators
390 In contrast to path styles, path decorators depend on the concrete
391 path to which they are applied. In particular, they don't make
392 sense without any path and can thus not be used in canvas.set!
396 def decorate(self, dp, texrunner):
397 """apply a style to a given decoratedpath object dp
399 decorate accepts a decoratedpath object dp, applies PathStyle
400 by modifying dp in place.
403 pass
406 # stroked and filled: basic decos which stroked and fill,
407 # respectively the path
410 class _stroked(deco, attr.exclusiveattr):
412 """stroked is a decorator, which draws the outline of the path"""
414 def __init__(self, styles=[]):
415 attr.exclusiveattr.__init__(self, _stroked)
416 self.styles = attr.mergeattrs(styles)
417 attr.checkattrs(self.styles, [style.strokestyle])
419 def __call__(self, styles=[]):
420 # XXX or should we also merge self.styles
421 return _stroked(styles)
423 def decorate(self, dp, texrunner):
424 if dp.strokestyles is not None:
425 raise RuntimeError("Cannot stroke an already stroked path")
426 dp.strokestyles = self.styles
428 stroked = _stroked()
429 stroked.clear = attr.clearclass(_stroked)
432 class _filled(deco, attr.exclusiveattr):
434 """filled is a decorator, which fills the interior of the path"""
436 def __init__(self, styles=[]):
437 attr.exclusiveattr.__init__(self, _filled)
438 self.styles = attr.mergeattrs(styles)
439 attr.checkattrs(self.styles, [style.fillstyle])
441 def __call__(self, styles=[]):
442 # XXX or should we also merge self.styles
443 return _filled(styles)
445 def decorate(self, dp, texrunner):
446 if dp.fillstyles is not None:
447 raise RuntimeError("Cannot fill an already filled path")
448 dp.fillstyles = self.styles
450 filled = _filled()
451 filled.clear = attr.clearclass(_filled)
454 # Arrows
457 # helper function which constructs the arrowhead
459 def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constriction, constrictionlen):
461 """helper routine, which returns an arrowhead from a given anormpath
463 - arclenfrombegin: position of arrow in arc length from the start of the path
464 - direction: +1 for an arrow pointing along the direction of anormpath or
465 -1 for an arrow pointing opposite to the direction of normpath
466 - size: size of the arrow as arc length
467 - angle. opening angle
468 - constriction: boolean to indicate whether the constriction point is to be taken into account or not
469 - constrictionlen: arc length of constriction. (not used when constriction is false)
472 # arc length and coordinates of tip
473 tx, ty = anormpath.at(arclenfrombegin)
475 # construct the template for the arrow by cutting the path at the
476 # corresponding length
477 arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
479 # from this template, we construct the two outer curves of the arrow
480 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
481 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
483 # now come the joining backward parts
484 if constriction:
485 # constriction point (cx, cy) lies on path
486 cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
487 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
488 arrow = arrowl.reversed() << arrowr << arrowcr
489 else:
490 arrow = arrowl.reversed() << arrowr
492 arrow[-1].close()
494 return arrow
497 _base = 6 * unit.v_pt
499 class arrow(deco, attr.attr):
501 """arrow is a decorator which adds an arrow to either side of the path"""
503 def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
504 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
505 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
506 self.pos = pos
507 self.reversed = reversed
508 self.size = size
509 self.angle = angle
510 self.constriction = constriction
512 # calculate absolute arc length of constricition
513 # Note that we have to correct this length because the arrowtemplates are rotated
514 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
515 # self.constriction = 1, we actually have a length which is approximately shorter
516 # by the given geometrical factor.
517 if self.constriction is not None:
518 self.constrictionlen = self.size * self.constriction * math.cos(math.radians(self.angle/2.0))
519 else:
520 # if we do not want a constriction, i.e. constriction is None, we still
521 # need constrictionlen for cutting the path
522 self.constrictionlen = self.size * 1 * math.cos(math.radians(self.angle/2.0))
524 def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
525 if attrs is None:
526 attrs = self.attrs
527 if pos is None:
528 pos = self.pos
529 if reversed is None:
530 reversed = self.reversed
531 if size is None:
532 size = self.size
533 if angle is None:
534 angle = self.angle
535 if constriction is _marker:
536 constriction = self.constriction
537 return arrow(attrs=attrs, pos=pos, reversed=reversed, size=size, angle=angle, constriction=constriction)
539 def decorate(self, dp, texrunner):
540 dp.ensurenormpath()
541 anormpath = dp.path
543 arclenfrombegin = (1-self.reversed)*self.constrictionlen + self.pos * (anormpath.arclen() - self.constrictionlen)
544 direction = self.reversed and -1 or 1
545 arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle,
546 self.constriction is not None, self.constrictionlen)
548 # add arrowhead to decoratedpath
549 dp.ornaments.draw(arrowhead, self.attrs)
551 # exlude part of the path from stroking when the arrow is strictly at the begin or the end
552 if self.pos == 0 and self.reversed:
553 dp.excluderange(0, min(self.size, self.constrictionlen))
554 elif self.pos == 1 and not self.reversed:
555 dp.excluderange(anormpath.end() - min(self.size, self.constrictionlen), anormpath.end())
557 arrow.clear = attr.clearclass(arrow)
559 # arrows at begin of path
560 barrow = arrow(pos=0, reversed=1)
561 barrow.SMALL = barrow(size=_base/math.sqrt(64))
562 barrow.SMALl = barrow(size=_base/math.sqrt(32))
563 barrow.SMAll = barrow(size=_base/math.sqrt(16))
564 barrow.SMall = barrow(size=_base/math.sqrt(8))
565 barrow.Small = barrow(size=_base/math.sqrt(4))
566 barrow.small = barrow(size=_base/math.sqrt(2))
567 barrow.normal = barrow(size=_base)
568 barrow.large = barrow(size=_base*math.sqrt(2))
569 barrow.Large = barrow(size=_base*math.sqrt(4))
570 barrow.LArge = barrow(size=_base*math.sqrt(8))
571 barrow.LARge = barrow(size=_base*math.sqrt(16))
572 barrow.LARGe = barrow(size=_base*math.sqrt(32))
573 barrow.LARGE = barrow(size=_base*math.sqrt(64))
575 # arrows at end of path
576 earrow = arrow()
577 earrow.SMALL = earrow(size=_base/math.sqrt(64))
578 earrow.SMALl = earrow(size=_base/math.sqrt(32))
579 earrow.SMAll = earrow(size=_base/math.sqrt(16))
580 earrow.SMall = earrow(size=_base/math.sqrt(8))
581 earrow.Small = earrow(size=_base/math.sqrt(4))
582 earrow.small = earrow(size=_base/math.sqrt(2))
583 earrow.normal = earrow(size=_base)
584 earrow.large = earrow(size=_base*math.sqrt(2))
585 earrow.Large = earrow(size=_base*math.sqrt(4))
586 earrow.LArge = earrow(size=_base*math.sqrt(8))
587 earrow.LARge = earrow(size=_base*math.sqrt(16))
588 earrow.LARGe = earrow(size=_base*math.sqrt(32))
589 earrow.LARGE = earrow(size=_base*math.sqrt(64))
592 class text(deco, attr.attr):
593 """a simple text decorator"""
595 def __init__(self, text, textattrs=[], angle=0, relangle=None, textdist=0.2,
596 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
597 texrunner=None):
598 if arclenfrombegin is not None and arclenfromend is not None:
599 raise ValueError("either set arclenfrombegin or arclenfromend")
600 self.text = text
601 self.textattrs = textattrs
602 self.angle = angle
603 self.relangle = relangle
604 self.textdist = textdist
605 self.relarclenpos = relarclenpos
606 self.arclenfrombegin = arclenfrombegin
607 self.arclenfromend = arclenfromend
608 self.texrunner = texrunner
610 def decorate(self, dp, texrunner):
611 if self.texrunner:
612 texrunner = self.texrunner
613 from . import text as textmodule
614 textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
616 dp.ensurenormpath()
617 if self.arclenfrombegin is not None:
618 param = dp.path.begin() + self.arclenfrombegin
619 elif self.arclenfromend is not None:
620 param = dp.path.end() - self.arclenfromend
621 else:
622 # relarcpos is used, when neither arcfrombegin nor arcfromend is given
623 param = self.relarclenpos * dp.path.arclen()
624 x, y = dp.path.at(param)
626 if self.relangle is not None:
627 a = dp.path.trafo(param).apply_pt(math.cos(self.relangle*math.pi/180), math.sin(self.relangle*math.pi/180))
628 b = dp.path.trafo(param).apply_pt(0, 0)
629 angle = math.atan2(a[1] - b[1], a[0] - b[0])
630 else:
631 angle = self.angle*math.pi/180
632 t = texrunner.text(x, y, self.text, textattrs)
633 t.linealign(self.textdist, math.cos(angle), math.sin(angle))
634 dp.ornaments.insert(t)
636 class curvedtext(deco, attr.attr):
637 """a text decorator for curved text
639 - text: is typeset along the path to which this decorator is applied
640 - relarclenpos: position for the base point of the text (default: 0)
641 - arlenfrombegin, arclenfromend: alternative ways of specifying the position of the base point;
642 use of relarclenpos, arclenfrombegin and arclenfromend is mutually exclusive
643 - textattrs, texrunner: standard text arguments (defaults: [] resp None)
647 # defaulttextattrs = [textmodule.halign.center] # TODO: not possible due to cyclic import issue
649 def __init__(self, text, textattrs=[],
650 relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
651 texrunner=None, exclude=None):
652 if arclenfrombegin is not None and arclenfromend is not None:
653 raise ValueError("either set arclenfrombegin or arclenfromend")
654 self.text = text
655 self.textattrs = textattrs
656 self.relarclenpos = relarclenpos
657 self.arclenfrombegin = arclenfrombegin
658 self.arclenfromend = arclenfromend
659 self.texrunner = texrunner
660 self.exclude = exclude
662 def decorate(self, dp, texrunner):
663 if self.texrunner:
664 texrunner = self.texrunner
665 from . import text as textmodule
666 self.defaulttextattrs = [textmodule.halign.center]
668 dp.ensurenormpath()
669 if self.arclenfrombegin is not None:
670 textpos = dp.path.begin() + self.arclenfrombegin
671 elif self.arclenfromend is not None:
672 textpos = dp.path.end() - self.arclenfromend
673 else:
674 # relarcpos is used if neither arcfrombegin nor arcfromend is given
675 textpos = self.relarclenpos * dp.path.arclen()
677 textattrs = self.defaulttextattrs + self.textattrs
678 t = texrunner.text(0, 0, self.text, textattrs, singlecharmode=1)
679 t.do_finish()
681 # we copy the style from the original textbox and modify the position for each dvicanvas item
682 c = canvas.canvas(t.dvicanvas.styles)
683 for item in t.dvicanvas.items:
684 bbox = item.bbox()
685 bbox = bbox.transformed(t.texttrafo)
686 x = bbox.center()[0]
687 atrafo = dp.path.trafo(textpos+x)
688 c.insert(item, [t.texttrafo] + [trafo.translate(-x, 0)] + [atrafo])
689 if self.exclude is not None:
690 dp.excluderange(textpos+bbox.left()-self.exclude, textpos+bbox.right()+self.exclude)
692 dp.ornaments.insert(c)
695 class shownormpath(deco, attr.attr):
697 default_normline_attrs = [color.rgb.blue]
698 default_normcurve_attrs = [color.rgb.green]
699 default_endpoint_attrs = []
700 default_controlline_attrs = [color.rgb.red, style.linestyle.dashed]
701 default_controlpoint_attrs = [color.rgb.red]
703 def __init__(self, normline_attrs=[], normcurve_attrs=[],
704 endpoint_size=0.05*unit.v_cm, endpoint_attrs=[],
705 controlline_attrs=[],
706 controlpoint_size=0.05*unit.v_cm, controlpoint_attrs=[]):
707 self.normline_attrs = attr.refineattrs(normline_attrs, self.default_normline_attrs, [style.strokestyle])
708 self.normcurve_attrs = attr.refineattrs(normcurve_attrs, self.default_normcurve_attrs, [style.strokestyle])
709 self.endpoint_size_pt = unit.topt(endpoint_size)
710 self.endpoint_attrs = attr.refineattrs(endpoint_attrs, self.default_endpoint_attrs, [style.fillstyle])
711 self.controlline_attrs = attr.refineattrs(controlline_attrs, self.default_controlline_attrs, [style.strokestyle])
712 self.controlpoint_size_pt = unit.topt(controlpoint_size)
713 self.controlpoint_attrs = attr.refineattrs(controlpoint_attrs, self.default_controlpoint_attrs, [style.fillstyle])
715 def decorate(self, dp, texrunner):
716 dp.ensurenormpath()
717 for normsubpath in dp.path.normsubpaths:
718 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
719 p = path.path(path.moveto_pt(*normsubpathitem.atbegin_pt()), normsubpathitem.pathitem())
720 if isinstance(normsubpathitem, normpath.normcurve_pt):
721 if self.normcurve_attrs is not None:
722 dp.ornaments.stroke(p, self.normcurve_attrs)
723 else:
724 if self.normline_attrs is not None:
725 dp.ornaments.stroke(p, self.normline_attrs)
726 for normsubpath in dp.path.normsubpaths:
727 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
728 if isinstance(normsubpathitem, normpath.normcurve_pt):
729 if self.controlline_attrs is not None:
730 dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt,
731 normsubpathitem.x1_pt, normsubpathitem.y1_pt), self.controlline_attrs)
732 dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt,
733 normsubpathitem.x3_pt, normsubpathitem.y3_pt), self.controlline_attrs)
734 if self.controlpoint_attrs is not None:
735 dp.ornaments.fill(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, self.controlpoint_size_pt), self.controlpoint_attrs)
736 dp.ornaments.fill(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, self.controlpoint_size_pt), self.controlpoint_attrs)
737 if self.endpoint_attrs is not None:
738 for normsubpath in dp.path.normsubpaths:
739 for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
740 if not i:
741 x_pt, y_pt = normsubpathitem.atbegin_pt()
742 dp.ornaments.fill(path.circle_pt(x_pt, y_pt, self.endpoint_size_pt), self.endpoint_attrs)
743 x_pt, y_pt = normsubpathitem.atend_pt()
744 dp.ornaments.fill(path.circle_pt(x_pt, y_pt, self.endpoint_size_pt), self.endpoint_attrs)
747 class linehatched(deco, attr.exclusiveattr, attr.clearclass):
748 """draws a pattern with explicit lines
750 This class acts as a drop-in replacement for postscript patterns
751 from the pattern module which are not understood by some printers"""
753 def __init__(self, dist, angle, strokestyles=[], cross=0):
754 attr.clearclass.__init__(self, _filled)
755 attr.exclusiveattr.__init__(self, linehatched)
756 self.dist = dist
757 self.angle = angle
758 self.strokestyles = attr.mergeattrs([style.linewidth.THIN] + strokestyles)
759 attr.checkattrs(self.strokestyles, [style.strokestyle])
760 self.cross = cross
762 def __call__(self, dist=None, angle=None, strokestyles=None, cross=None):
763 if dist is None:
764 dist = self.dist
765 if angle is None:
766 angle = self.angle
767 if strokestyles is None:
768 strokestyles = self.strokestyles
769 if cross is None:
770 cross = self.cross
771 return linehatched(dist, angle, strokestyles, cross)
773 def _decocanvas(self, angle, dp, texrunner):
774 dp.ensurenormpath()
775 dist_pt = unit.topt(self.dist)
777 c = canvas.canvas([canvas.clip(dp.path)])
778 llx_pt, lly_pt, urx_pt, ury_pt = dp.path.bbox().highrestuple_pt()
779 center_pt = 0.5*(llx_pt+urx_pt), 0.5*(lly_pt+ury_pt)
780 radius_pt = 0.5*math.hypot(urx_pt-llx_pt, ury_pt-lly_pt) + dist_pt
781 n = int(2*radius_pt / dist_pt) + 1
782 for i in range(n):
783 x_pt = center_pt[0] - radius_pt + i*dist_pt
784 c.stroke(path.line_pt(x_pt, center_pt[1]-radius_pt, x_pt, center_pt[1]+radius_pt),
785 [trafo.rotate_pt(angle, center_pt[0], center_pt[1])] + self.strokestyles)
786 return c
788 def decorate(self, dp, texrunner):
789 dp.ornaments.insert(self._decocanvas(self.angle, dp, texrunner))
790 if self.cross:
791 dp.ornaments.insert(self._decocanvas(self.angle+90, dp, texrunner))
793 def merge(self, attrs):
794 # act as attr.clearclass and as attr.exclusiveattr at the same time
795 newattrs = attr.exclusiveattr.merge(self, attrs)
796 return attr.clearclass.merge(self, newattrs)
798 linehatched.clear = attr.clearclass(linehatched)
800 _hatch_base = 0.1 * unit.v_cm
802 linehatched0 = linehatched(_hatch_base, 0)
803 linehatched0.SMALL = linehatched0(_hatch_base/math.sqrt(64))
804 linehatched0.SMALL = linehatched0(_hatch_base/math.sqrt(64))
805 linehatched0.SMALl = linehatched0(_hatch_base/math.sqrt(32))
806 linehatched0.SMAll = linehatched0(_hatch_base/math.sqrt(16))
807 linehatched0.SMall = linehatched0(_hatch_base/math.sqrt(8))
808 linehatched0.Small = linehatched0(_hatch_base/math.sqrt(4))
809 linehatched0.small = linehatched0(_hatch_base/math.sqrt(2))
810 linehatched0.normal = linehatched0(_hatch_base)
811 linehatched0.large = linehatched0(_hatch_base*math.sqrt(2))
812 linehatched0.Large = linehatched0(_hatch_base*math.sqrt(4))
813 linehatched0.LArge = linehatched0(_hatch_base*math.sqrt(8))
814 linehatched0.LARge = linehatched0(_hatch_base*math.sqrt(16))
815 linehatched0.LARGe = linehatched0(_hatch_base*math.sqrt(32))
816 linehatched0.LARGE = linehatched0(_hatch_base*math.sqrt(64))
818 linehatched45 = linehatched(_hatch_base, 45)
819 linehatched45.SMALL = linehatched45(_hatch_base/math.sqrt(64))
820 linehatched45.SMALl = linehatched45(_hatch_base/math.sqrt(32))
821 linehatched45.SMAll = linehatched45(_hatch_base/math.sqrt(16))
822 linehatched45.SMall = linehatched45(_hatch_base/math.sqrt(8))
823 linehatched45.Small = linehatched45(_hatch_base/math.sqrt(4))
824 linehatched45.small = linehatched45(_hatch_base/math.sqrt(2))
825 linehatched45.normal = linehatched45(_hatch_base)
826 linehatched45.large = linehatched45(_hatch_base*math.sqrt(2))
827 linehatched45.Large = linehatched45(_hatch_base*math.sqrt(4))
828 linehatched45.LArge = linehatched45(_hatch_base*math.sqrt(8))
829 linehatched45.LARge = linehatched45(_hatch_base*math.sqrt(16))
830 linehatched45.LARGe = linehatched45(_hatch_base*math.sqrt(32))
831 linehatched45.LARGE = linehatched45(_hatch_base*math.sqrt(64))
833 linehatched90 = linehatched(_hatch_base, 90)
834 linehatched90.SMALL = linehatched90(_hatch_base/math.sqrt(64))
835 linehatched90.SMALl = linehatched90(_hatch_base/math.sqrt(32))
836 linehatched90.SMAll = linehatched90(_hatch_base/math.sqrt(16))
837 linehatched90.SMall = linehatched90(_hatch_base/math.sqrt(8))
838 linehatched90.Small = linehatched90(_hatch_base/math.sqrt(4))
839 linehatched90.small = linehatched90(_hatch_base/math.sqrt(2))
840 linehatched90.normal = linehatched90(_hatch_base)
841 linehatched90.large = linehatched90(_hatch_base*math.sqrt(2))
842 linehatched90.Large = linehatched90(_hatch_base*math.sqrt(4))
843 linehatched90.LArge = linehatched90(_hatch_base*math.sqrt(8))
844 linehatched90.LARge = linehatched90(_hatch_base*math.sqrt(16))
845 linehatched90.LARGe = linehatched90(_hatch_base*math.sqrt(32))
846 linehatched90.LARGE = linehatched90(_hatch_base*math.sqrt(64))
848 linehatched135 = linehatched(_hatch_base, 135)
849 linehatched135.SMALL = linehatched135(_hatch_base/math.sqrt(64))
850 linehatched135.SMALl = linehatched135(_hatch_base/math.sqrt(32))
851 linehatched135.SMAll = linehatched135(_hatch_base/math.sqrt(16))
852 linehatched135.SMall = linehatched135(_hatch_base/math.sqrt(8))
853 linehatched135.Small = linehatched135(_hatch_base/math.sqrt(4))
854 linehatched135.small = linehatched135(_hatch_base/math.sqrt(2))
855 linehatched135.normal = linehatched135(_hatch_base)
856 linehatched135.large = linehatched135(_hatch_base*math.sqrt(2))
857 linehatched135.Large = linehatched135(_hatch_base*math.sqrt(4))
858 linehatched135.LArge = linehatched135(_hatch_base*math.sqrt(8))
859 linehatched135.LARge = linehatched135(_hatch_base*math.sqrt(16))
860 linehatched135.LARGe = linehatched135(_hatch_base*math.sqrt(32))
861 linehatched135.LARGE = linehatched135(_hatch_base*math.sqrt(64))
863 crosslinehatched0 = linehatched(_hatch_base, 0, cross=1)
864 crosslinehatched0.SMALL = crosslinehatched0(_hatch_base/math.sqrt(64))
865 crosslinehatched0.SMALl = crosslinehatched0(_hatch_base/math.sqrt(32))
866 crosslinehatched0.SMAll = crosslinehatched0(_hatch_base/math.sqrt(16))
867 crosslinehatched0.SMall = crosslinehatched0(_hatch_base/math.sqrt(8))
868 crosslinehatched0.Small = crosslinehatched0(_hatch_base/math.sqrt(4))
869 crosslinehatched0.small = crosslinehatched0(_hatch_base/math.sqrt(2))
870 crosslinehatched0.normal = crosslinehatched0
871 crosslinehatched0.large = crosslinehatched0(_hatch_base*math.sqrt(2))
872 crosslinehatched0.Large = crosslinehatched0(_hatch_base*math.sqrt(4))
873 crosslinehatched0.LArge = crosslinehatched0(_hatch_base*math.sqrt(8))
874 crosslinehatched0.LARge = crosslinehatched0(_hatch_base*math.sqrt(16))
875 crosslinehatched0.LARGe = crosslinehatched0(_hatch_base*math.sqrt(32))
876 crosslinehatched0.LARGE = crosslinehatched0(_hatch_base*math.sqrt(64))
878 crosslinehatched45 = linehatched(_hatch_base, 45, cross=1)
879 crosslinehatched45.SMALL = crosslinehatched45(_hatch_base/math.sqrt(64))
880 crosslinehatched45.SMALl = crosslinehatched45(_hatch_base/math.sqrt(32))
881 crosslinehatched45.SMAll = crosslinehatched45(_hatch_base/math.sqrt(16))
882 crosslinehatched45.SMall = crosslinehatched45(_hatch_base/math.sqrt(8))
883 crosslinehatched45.Small = crosslinehatched45(_hatch_base/math.sqrt(4))
884 crosslinehatched45.small = crosslinehatched45(_hatch_base/math.sqrt(2))
885 crosslinehatched45.normal = crosslinehatched45
886 crosslinehatched45.large = crosslinehatched45(_hatch_base*math.sqrt(2))
887 crosslinehatched45.Large = crosslinehatched45(_hatch_base*math.sqrt(4))
888 crosslinehatched45.LArge = crosslinehatched45(_hatch_base*math.sqrt(8))
889 crosslinehatched45.LARge = crosslinehatched45(_hatch_base*math.sqrt(16))
890 crosslinehatched45.LARGe = crosslinehatched45(_hatch_base*math.sqrt(32))
891 crosslinehatched45.LARGE = crosslinehatched45(_hatch_base*math.sqrt(64))
894 class colorgradient(deco, attr.attr):
895 """inserts pieces of the path in different colors"""
897 def __init__(self, grad, attrs=[], steps=20):
898 self.attrs = attrs
899 self.grad = grad
900 self.steps = steps
902 def decorate(self, dp, texrunner):
903 dp.ensurenormpath()
904 l = dp.path.arclen()
906 colors = [self.grad.select(n, self.steps) for n in range(self.steps)]
907 colors.reverse()
908 params = dp.path.arclentoparam([l*i/float(self.steps) for i in range(self.steps)])
909 params.reverse()
911 c = canvas.canvas()
912 # treat the end pieces separately
913 c.stroke(dp.path.split(params[1])[1], attr.mergeattrs([colors[0]] + self.attrs))
914 for n in range(1,self.steps-1):
915 c.stroke(dp.path.split([params[n-1],params[n+1]])[1], attr.mergeattrs([colors[n]] + self.attrs))
916 c.stroke(dp.path.split(params[-2])[0], attr.mergeattrs([colors[-1]] + self.attrs))
917 dp.ornaments.insert(c)
920 class brace(deco, attr.attr):
921 r"""draws a nicely curled brace
923 In most cases, the original line is not wanted use canvas.canvas.draw(..) for it
925 Geometrical parameters:
927 inner /\ strokes
928 ____________/ \__________
929 / bar bar \ outer
930 / \ strokes
932 totalheight distance from the jaws to the middle cap
933 barthickness thickness of the main bars
934 innerstrokesthickness thickness of the two ending strokes
935 outerstrokesthickness thickness of the inner strokes at the middle cap
936 innerstrokesrelheight height of the inner/outer strokes, relative to the total height
937 outerstrokesrelheight this determines the angle of the main bars!
938 should be around 0.5
939 Note: if innerstrokesrelheight + outerstrokesrelheight == 1 then the main bars
940 will be aligned parallel to the connecting line between the endpoints
941 outerstrokesangle angle of the two ending strokes
942 innerstrokesangle angle between the inner strokes at the middle cap
943 slantstrokesangle extra slanting of the inner/outer strokes
944 innerstrokessmoothness smoothing parameter for the inner + outer strokes
945 outerstrokessmoothness should be around 1 (allowed: [0,infty))
946 middlerelpos position of the middle cap (0 == left, 1 == right)
948 # This code is experimental because it is unclear
949 # how the brace fits into the concepts of PyX
951 # Some thoughts:
952 # - a brace needs to be decoratable with text
953 # it needs stroking and filling attributes
954 # - the brace is not really a box:
955 # it has two "anchor" points that are important for aligning it to other things
956 # and one "anchor" point (plus direction) for aligning other things
957 # - a brace is not a deformer:
958 # it does not look at anything else than begin/endpoint of a path
959 # - a brace might be a connector (which is to be dissolved into the box concept later?)
961 def __init__(self, reverse=1, stretch=None, dist=None, fillattrs=[],
962 totalheight=12*unit.x_pt,
963 barthickness=0.5*unit.x_pt, innerstrokesthickness=0.25*unit.x_pt, outerstrokesthickness=0.25*unit.x_pt,
964 innerstrokesrelheight=0.6, outerstrokesrelheight=0.7,
965 innerstrokesangle=30, outerstrokesangle=25, slantstrokesangle=5,
966 innerstrokessmoothness=2.0, outerstrokessmoothness=2.5,
967 middlerelpos=0.5):
968 self.fillattrs = fillattrs
969 self.reverse = reverse
970 self.stretch = stretch
971 self.dist = dist
972 self.totalheight = totalheight
973 self.barthickness = barthickness
974 self.innerstrokesthickness = innerstrokesthickness
975 self.outerstrokesthickness = outerstrokesthickness
976 self.innerstrokesrelheight = innerstrokesrelheight
977 self.outerstrokesrelheight = outerstrokesrelheight
978 self.innerstrokesangle = innerstrokesangle
979 self.outerstrokesangle = outerstrokesangle
980 self.slantstrokesangle = slantstrokesangle
981 self.innerstrokessmoothness = innerstrokessmoothness
982 self.outerstrokessmoothness = outerstrokessmoothness
983 self.middlerelpos = middlerelpos
985 def __call__(self, **kwargs):
986 for name in ["reverse", "stretch", "dist", "fillattrs",
987 "totalheight", "barthickness", "innerstrokesthickness", "outerstrokesthickness",
988 "innerstrokesrelheight", "outerstrokesrelheight", "innerstrokesangle", "outerstrokesangle", "slantstrokesangle",
989 "innerstrokessmoothness", "outerstrokessmoothness", "middlerelpos"]:
990 if name not in kwargs:
991 kwargs[name] = self.__dict__[name]
992 return brace(**kwargs)
994 def _halfbracepath_pt(self, length_pt, height_pt, ilength_pt, olength_pt, # <<<
995 ithick_pt, othick_pt, bthick_pt, cos_iangle, sin_iangle, cos_oangle,
996 sin_oangle, cos_slangle, sin_slangle):
998 ismooth = self.innerstrokessmoothness
999 osmooth = self.outerstrokessmoothness
1001 # these two parameters are not important enough to be seen outside
1002 inner_cap_param = 1.5
1003 outer_cap_param = 2.5
1004 outerextracurved = 0.6 # in (0, 1]
1005 # 1.0 will lead to F=G, the outer strokes will not be curved at their ends.
1006 # The smaller, the more curvature
1008 # build an orientation path (three straight lines)
1010 # \q1
1011 # / \
1012 # / \
1013 # _/ \______________________________________q5
1014 # q2 q3 q4 \
1017 # \q6
1019 # get the points for that:
1020 q1 = (0, height_pt - inner_cap_param * ithick_pt + 0.5*ithick_pt/sin_iangle)
1021 q2 = (q1[0] + ilength_pt * sin_iangle,
1022 q1[1] - ilength_pt * cos_iangle)
1023 q6 = (length_pt, 0)
1024 q5 = (q6[0] - olength_pt * sin_oangle,
1025 q6[1] + olength_pt * cos_oangle)
1026 bardir = (q5[0] - q2[0], q5[1] - q2[1])
1027 bardirnorm = math.hypot(*bardir)
1028 bardir = (bardir[0]/bardirnorm, bardir[1]/bardirnorm)
1029 ismoothlength_pt = ilength_pt * ismooth
1030 osmoothlength_pt = olength_pt * osmooth
1031 if bardirnorm < ismoothlength_pt + osmoothlength_pt:
1032 ismoothlength_pt = bardirnorm * ismoothlength_pt / (ismoothlength_pt + osmoothlength_pt)
1033 osmoothlength_pt = bardirnorm * osmoothlength_pt / (ismoothlength_pt + osmoothlength_pt)
1034 q3 = (q2[0] + ismoothlength_pt * bardir[0],
1035 q2[1] + ismoothlength_pt * bardir[1])
1036 q4 = (q5[0] - osmoothlength_pt * bardir[0],
1037 q5[1] - osmoothlength_pt * bardir[1])
1040 # P _O
1041 # / | \A2
1042 # / A1\ \
1043 # / \ B2C2________D2___________E2_______F2___G2
1044 # \______________________________________ \
1045 # B1,C1 D1 E1 F1 G1 \
1046 # \ \
1047 # \ \H2
1048 # H1\_/I2
1049 # I1
1051 # the halfbraces meet in P and A1:
1052 P = (0, height_pt)
1053 A1 = (0, height_pt - inner_cap_param * ithick_pt)
1054 # A2 is A1, shifted by the inner thickness
1055 A2 = (A1[0] + ithick_pt * cos_iangle,
1056 A1[1] + ithick_pt * sin_iangle)
1057 s, t = deformer.intersection(P, A2, (cos_slangle, sin_slangle), (sin_iangle, -cos_iangle))
1058 O = (P[0] + s * cos_slangle,
1059 P[1] + s * sin_slangle)
1061 # from D1 to E1 is the straight part of the brace
1062 # also back from E2 to D1
1063 D1 = (q3[0] + bthick_pt * bardir[1],
1064 q3[1] - bthick_pt * bardir[0])
1065 D2 = (q3[0] - bthick_pt * bardir[1],
1066 q3[1] + bthick_pt * bardir[0])
1067 E1 = (q4[0] + bthick_pt * bardir[1],
1068 q4[1] - bthick_pt * bardir[0])
1069 E2 = (q4[0] - bthick_pt * bardir[1],
1070 q4[1] + bthick_pt * bardir[0])
1071 # I1, I2 are the control points at the outer stroke
1072 I1 = (q6[0] - 0.5 * othick_pt * cos_oangle,
1073 q6[1] - 0.5 * othick_pt * sin_oangle)
1074 I2 = (q6[0] + 0.5 * othick_pt * cos_oangle,
1075 q6[1] + 0.5 * othick_pt * sin_oangle)
1076 # get the control points for the curved parts of the brace
1077 s, t = deformer.intersection(A1, D1, (sin_iangle, -cos_iangle), bardir)
1078 B1 = (D1[0] + t * bardir[0],
1079 D1[1] + t * bardir[1])
1080 s, t = deformer.intersection(A2, D2, (sin_iangle, -cos_iangle), bardir)
1081 B2 = (D2[0] + t * bardir[0],
1082 D2[1] + t * bardir[1])
1083 s, t = deformer.intersection(E1, I1, bardir, (-sin_oangle, cos_oangle))
1084 G1 = (E1[0] + s * bardir[0],
1085 E1[1] + s * bardir[1])
1086 s, t = deformer.intersection(E2, I2, bardir, (-sin_oangle, cos_oangle))
1087 G2 = (E2[0] + s * bardir[0],
1088 E2[1] + s * bardir[1])
1089 # at the inner strokes: use curvature zero at both ends
1090 C1 = B1
1091 C2 = B2
1092 # at the outer strokes: use curvature zero only at the connection to
1093 # the straight part
1094 F1 = (outerextracurved * G1[0] + (1 - outerextracurved) * E1[0],
1095 outerextracurved * G1[1] + (1 - outerextracurved) * E1[1])
1096 F2 = (outerextracurved * G2[0] + (1 - outerextracurved) * E2[0],
1097 outerextracurved * G2[1] + (1 - outerextracurved) * E2[1])
1098 # the tip of the outer stroke, endpoints of the bezier curve
1099 H1 = (I1[0] - outer_cap_param * othick_pt * sin_oangle,
1100 I1[1] + outer_cap_param * othick_pt * cos_oangle)
1101 H2 = (I2[0] - outer_cap_param * othick_pt * sin_oangle,
1102 I2[1] + outer_cap_param * othick_pt * cos_oangle)
1104 #for qq in [A1,B1,C1,D1,E1,F1,G1,H1,I1,
1105 # A2,B2,C2,D2,E2,F2,G2,H2,I2,
1106 # O,P
1107 # ]:
1108 # cc.fill(path.circle(qq[0], qq[1], 0.5), [color.rgb.green])
1110 # now build the right halfbrace
1111 bracepath = path.path(path.moveto_pt(*A1))
1112 bracepath.append(path.curveto_pt(B1[0], B1[1], C1[0], C1[1], D1[0], D1[1]))
1113 bracepath.append(path.lineto_pt(E1[0], E1[1]))
1114 bracepath.append(path.curveto_pt(F1[0], F1[1], G1[0], G1[1], H1[0], H1[1]))
1115 # the tip of the right halfbrace
1116 bracepath.append(path.curveto_pt(I1[0], I1[1], I2[0], I2[1], H2[0], H2[1]))
1117 # the rest of the right halfbrace
1118 bracepath.append(path.curveto_pt(G2[0], G2[1], F2[0], F2[1], E2[0], E2[1]))
1119 bracepath.append(path.lineto_pt(D2[0], D2[1]))
1120 bracepath.append(path.curveto_pt(C2[0], C2[1], B2[0], B2[1], A2[0], A2[1]))
1121 # the tip in the middle of the brace
1122 bracepath.append(path.curveto_pt(O[0], O[1], O[0], O[1], P[0], P[1]))
1124 return bracepath
1125 # >>>
1127 def _bracepath(self, x0_pt, y0_pt, x1_pt, y1_pt): # <<<
1128 height_pt = unit.topt(self.totalheight)
1129 totallength_pt = math.hypot(x1_pt - x0_pt, y1_pt - y0_pt)
1130 leftlength_pt = self.middlerelpos * totallength_pt
1131 rightlength_pt = totallength_pt - leftlength_pt
1132 ithick_pt = unit.topt(self.innerstrokesthickness)
1133 othick_pt = unit.topt(self.outerstrokesthickness)
1134 bthick_pt = unit.topt(self.barthickness)
1136 # create the left halfbrace with positive slanting
1137 # because we will mirror this part
1138 cos_iangle = math.cos(math.radians(0.5*self.innerstrokesangle - self.slantstrokesangle))
1139 sin_iangle = math.sin(math.radians(0.5*self.innerstrokesangle - self.slantstrokesangle))
1140 cos_oangle = math.cos(math.radians(self.outerstrokesangle - self.slantstrokesangle))
1141 sin_oangle = math.sin(math.radians(self.outerstrokesangle - self.slantstrokesangle))
1142 cos_slangle = math.cos(math.radians(-self.slantstrokesangle))
1143 sin_slangle = math.sin(math.radians(-self.slantstrokesangle))
1144 ilength_pt = self.innerstrokesrelheight * height_pt / cos_iangle
1145 olength_pt = self.outerstrokesrelheight * height_pt / cos_oangle
1147 bracepath = self._halfbracepath_pt(leftlength_pt, height_pt,
1148 ilength_pt, olength_pt, ithick_pt, othick_pt, bthick_pt, cos_iangle,
1149 sin_iangle, cos_oangle, sin_oangle, cos_slangle,
1150 sin_slangle).reversed().transformed(trafo.mirror(90))
1152 # create the right halfbrace with negative slanting
1153 cos_iangle = math.cos(math.radians(0.5*self.innerstrokesangle + self.slantstrokesangle))
1154 sin_iangle = math.sin(math.radians(0.5*self.innerstrokesangle + self.slantstrokesangle))
1155 cos_oangle = math.cos(math.radians(self.outerstrokesangle + self.slantstrokesangle))
1156 sin_oangle = math.sin(math.radians(self.outerstrokesangle + self.slantstrokesangle))
1157 cos_slangle = math.cos(math.radians(-self.slantstrokesangle))
1158 sin_slangle = math.sin(math.radians(-self.slantstrokesangle))
1159 ilength_pt = self.innerstrokesrelheight * height_pt / cos_iangle
1160 olength_pt = self.outerstrokesrelheight * height_pt / cos_oangle
1162 bracepath = bracepath << self._halfbracepath_pt(rightlength_pt, height_pt,
1163 ilength_pt, olength_pt, ithick_pt, othick_pt, bthick_pt, cos_iangle,
1164 sin_iangle, cos_oangle, sin_oangle, cos_slangle,
1165 sin_slangle)
1167 return bracepath.transformed(
1168 # two trafos for matching the given endpoints
1169 trafo.translate_pt(x0_pt, y0_pt) *
1170 trafo.rotate_pt(math.degrees(math.atan2(y1_pt-y0_pt, x1_pt-x0_pt))) *
1171 # one trafo to move the brace's left outer stroke to zero
1172 trafo.translate_pt(leftlength_pt, 0))
1173 # >>>
1175 def decorate(self, dp, texrunner):
1176 dp.ensurenormpath()
1177 x0_pt, y0_pt = dp.path.atbegin_pt()
1178 x1_pt, y1_pt = dp.path.atend_pt()
1179 if self.reverse:
1180 x0_pt, y0_pt, x1_pt, y1_pt = x1_pt, y1_pt, x0_pt, y0_pt
1181 if self.stretch is not None:
1182 xm, ym = 0.5*(x0_pt+x1_pt), 0.5*(y0_pt+y1_pt)
1183 x0_pt, y0_pt = xm + self.stretch*(x0_pt-xm), ym + self.stretch*(y0_pt-ym)
1184 x1_pt, y1_pt = xm + self.stretch*(x1_pt-xm), ym + self.stretch*(y1_pt-ym)
1185 if self.dist is not None:
1186 d = unit.topt(self.dist)
1187 dx, dy = dp.path.rotation_pt(dp.path.begin()).apply_pt(0, 1)
1188 x0_pt += d*dx; y0_pt += d*dy
1189 dx, dy = dp.path.rotation_pt(dp.path.end()).apply_pt(0, 1)
1190 x1_pt += d*dx; y1_pt += d*dy
1191 dp.ornaments.fill(self._bracepath(x0_pt, y0_pt, x1_pt, y1_pt), self.fillattrs)
1193 brace.clear = attr.clearclass(brace)
1195 leftbrace = brace(reverse=0, middlerelpos=0.55, innerstrokesrelheight=0.6, outerstrokesrelheight=0.7, slantstrokesangle=-10)
1196 rightbrace = brace(reverse=1, middlerelpos=0.45, innerstrokesrelheight=0.6, outerstrokesrelheight=0.7, slantstrokesangle=10)
1197 belowbrace = brace(reverse=1, middlerelpos=0.55, innerstrokesrelheight=0.7, outerstrokesrelheight=0.9, slantstrokesangle=-10)
1198 abovebrace = brace(reverse=0, middlerelpos=0.45, innerstrokesrelheight=0.7, outerstrokesrelheight=0.9, slantstrokesangle=-10)
1199 straightbrace = brace(innerstrokesrelheight=0.5, outerstrokesrelheight=0.5,
1200 innerstrokesangle=30, outerstrokesangle=30, slantstrokesangle=0,
1201 innerstrokessmoothness=1.0, outerstrokessmoothness=1.0)