pdfwriter
[PyX/mjg.git] / pyx / deco.py
blob488d421059827855d76443ae3c89897256eed6bd
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 # TODO:
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
29 import sys, math
30 import attr, base, canvas, color, helper, path, style, trafo, unit
33 # Decorated path
36 class decoratedpath(base.PSCmd):
37 """Decorated path
39 The main purpose of this class is during the drawing
40 (stroking/filling) of a path. It collects attributes for the
41 stroke and/or fill operations.
42 """
44 def __init__(self, path, strokepath=None, fillpath=None,
45 styles=None, strokestyles=None, fillstyles=None,
46 subcanvas=None):
48 self.path = path
50 # path to be stroked or filled (or None)
51 self.strokepath = strokepath
52 self.fillpath = fillpath
54 # global style for stroking and filling and subdps
55 self.styles = helper.ensurelist(styles)
57 # styles which apply only for stroking and filling
58 self.strokestyles = helper.ensurelist(strokestyles)
59 self.fillstyles = helper.ensurelist(fillstyles)
61 # the canvas can contain additional elements of the path, e.g.,
62 # arrowheads,
63 if subcanvas is None:
64 self.subcanvas = canvas.canvas()
65 else:
66 self.subcanvas = subcanvas
69 def bbox(self):
70 scbbox = self.subcanvas.bbox()
71 pbbox = self.path.bbox()
72 if scbbox is not None:
73 return scbbox+pbbox
74 else:
75 return pbbox
77 def prolog(self):
78 result = []
79 for style in list(self.styles) + list(self.fillstyles) + list(self.strokestyles):
80 result.extend(style.prolog())
81 result.extend(self.subcanvas.prolog())
82 return result
84 def outputPS(self, file):
85 # draw (stroke and/or fill) the decoratedpath on the canvas
86 # while trying to produce an efficient output, e.g., by
87 # not writing one path two times
89 # small helper
90 def _writestyles(styles, file=file):
91 for style in styles:
92 style.outputPS(file)
94 # apply global styles
95 if self.styles:
96 file.write("gsave\n")
97 _writestyles(self.styles)
99 if self.fillpath is not None:
100 file.write("newpath\n")
101 self.fillpath.outputPS(file)
103 if self.strokepath==self.fillpath:
104 # do efficient stroking + filling
105 file.write("gsave\n")
107 if self.fillstyles:
108 _writestyles(self.fillstyles)
110 file.write("fill\n")
111 file.write("grestore\n")
113 if self.strokestyles:
114 file.write("gsave\n")
115 _writestyles(self.strokestyles)
117 file.write("stroke\n")
119 if self.strokestyles:
120 file.write("grestore\n")
121 else:
122 # only fill fillpath - for the moment
123 if self.fillstyles:
124 file.write("gsave\n")
125 _writestyles(self.fillstyles)
127 file.write("fill\n")
129 if self.fillstyles:
130 file.write("grestore\n")
132 if self.strokepath is not None and self.strokepath!=self.fillpath:
133 # this is the only relevant case still left
134 # Note that a possible stroking has already been done.
136 if self.strokestyles:
137 file.write("gsave\n")
138 _writestyles(self.strokestyles)
140 file.write("newpath\n")
141 self.strokepath.outputPS(file)
142 file.write("stroke\n")
144 if self.strokestyles:
145 file.write("grestore\n")
147 if self.strokepath is None and self.fillpath is None:
148 raise RuntimeError("Path neither to be stroked nor filled")
150 # now, draw additional elements of decoratedpath
151 self.subcanvas.outputPS(file)
153 # restore global styles
154 if self.styles:
155 file.write("grestore\n")
157 def outputPDF(self, file):
158 # draw (stroke and/or fill) the decoratedpath on the canvas
160 def _writestyles(styles, file=file):
161 for style in styles:
162 style.outputPDF(file)
164 def _writestrokestyles(strokestyles, file=file):
165 for style in strokestyles:
166 if isinstance(style, color.color):
167 style.outputPDF(file, fillattr=0)
168 else:
169 style.outputPDF(file)
171 def _writefillstyles(fillstyles, file=file):
172 for style in fillstyles:
173 if isinstance(style, color.color):
174 style.outputPDF(file, strokeattr=0)
175 else:
176 style.outputPDF(file)
178 # apply global styles
179 if self.styles:
180 file.write("q\n") # gsave
181 _writestyles(self.styles)
183 if self.fillpath is not None:
184 self.fillpath.outputPDF(file)
186 if self.strokepath==self.fillpath:
187 # do efficient stroking + filling
188 file.write("q\n") # gsave
190 if self.fillstyles:
191 _writefillstyles(self.fillstyles)
192 if self.strokestyles:
193 _writestrokestyles(self.strokestyles)
195 file.write("B\n") # both stroke and fill
196 file.write("Q\n") # grestore
197 else:
198 # only fill fillpath - for the moment
199 if self.fillstyles:
200 file.write("q\n") # gsave
201 _writefillstyles(self.fillstyles)
203 file.write("f\n") # fill
205 if self.fillstyles:
206 file.write("Q\n") # grestore
208 if self.strokepath is not None and self.strokepath!=self.fillpath:
209 # this is the only relevant case still left
210 # Note that a possible stroking has already been done.
212 if self.strokestyles:
213 file.write("q\n") # gsave
214 _writestrokestyles(self.strokestyles)
216 self.strokepath.outputPDF(file)
217 file.write("S\n") # stroke
219 if self.strokestyles:
220 file.write("Q\n") # grestore
222 if self.strokepath is None and self.fillpath is None:
223 raise RuntimeError("Path neither to be stroked nor filled")
225 # now, draw additional elements of decoratedpath
226 self.subcanvas.outputPDF(file)
228 # restore global styles
229 if self.styles:
230 file.write("Q\n") # grestore
233 # Path decorators
236 class deco:
238 """decorators
240 In contrast to path styles, path decorators depend on the concrete
241 path to which they are applied. In particular, they don't make
242 sense without any path and can thus not be used in canvas.set!
246 def decorate(self, dp):
247 """apply a style to a given decoratedpath object dp
249 decorate accepts a decoratedpath object dp, applies PathStyle
250 by modifying dp in place and returning the new dp.
253 pass
256 # stroked and filled: basic decos which stroked and fill,
257 # respectively the path
260 class _stroked(deco, attr.exclusiveattr):
262 """stroked is a decorator, which draws the outline of the path"""
264 def __init__(self, styles=[]):
265 attr.exclusiveattr.__init__(self, _stroked)
266 self.styles = attr.mergeattrs(styles)
267 attr.checkattrs(self.styles, [style.strokestyle])
269 def __call__(self, styles=[]):
270 # XXX or should we also merge self.styles
271 return _stroked(styles)
273 def decorate(self, dp):
274 dp.strokepath = dp.path
275 dp.strokestyles = self.styles
276 return dp
278 stroked = _stroked()
279 stroked.clear = attr.clearclass(_stroked)
282 class _filled(deco, attr.exclusiveattr):
284 """filled is a decorator, which fills the interior of the path"""
286 def __init__(self, styles=[]):
287 attr.exclusiveattr.__init__(self, _filled)
288 self.styles = attr.mergeattrs(styles)
289 attr.checkattrs(self.styles, [style.fillstyle])
291 def __call__(self, styles=[]):
292 # XXX or should we also merge self.styles
293 return _filled(styles)
295 def decorate(self, dp):
296 dp.fillpath = dp.path
297 dp.fillstyles = self.styles
298 return dp
300 filled = _filled()
301 filled.clear = attr.clearclass(_filled)
304 # Arrows
307 # two helper functions which construct the arrowhead and return its size, respectively
309 def _arrowheadtemplatelength(anormpath, size):
310 "calculate length of arrowhead template (in parametrisation of anormpath)"
311 # get tip (tx, ty)
312 tx, ty = anormpath.begin()
314 # obtain arrow template by using path up to first intersection
315 # with circle around tip (as suggested by Michael Schindler)
316 ipar = anormpath.intersect(path.circle(tx, ty, size))
317 if ipar[0]:
318 alen = ipar[0][0]
319 else:
320 # if this doesn't work, use first order conversion from pts to
321 # the bezier curve's parametrization
322 tvec = anormpath.tangent(0)
323 tlen = tvec.arclen_pt()
324 try:
325 alen = unit.topt(size)/tlen
326 except ArithmeticError:
327 # take maximum, we can get
328 alen = anormpath.range()
329 if alen > anormpath.range(): alen = anormpath.range()
331 return alen
334 def _arrowhead(anormpath, size, angle, constriction):
336 """helper routine, which returns an arrowhead for a normpath
338 returns arrowhead at begin of anormpath with size,
339 opening angle and relative constriction
342 alen = _arrowheadtemplatelength(anormpath, size)
343 tx, ty = anormpath.begin()
345 # now we construct the template for our arrow but cutting
346 # the path a the corresponding length
347 arrowtemplate = anormpath.split([alen])[0]
349 # from this template, we construct the two outer curves
350 # of the arrow
351 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
352 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
354 # now come the joining backward parts
355 if constriction:
356 # arrow with constriction
358 # constriction point (cx, cy) lies on path
359 cx, cy = anormpath.at(constriction*alen)
361 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
363 arrow = arrowl.reversed() << arrowr << arrowcr
364 arrow.append(path.closepath())
365 else:
366 # arrow without constriction
367 arrow = arrowl.reversed() << arrowr
368 arrow.append(path.closepath())
370 return arrow
373 _base = 6 * unit.v_pt
375 class arrow(deco, attr.attr):
377 """arrow is a decorator which adds an arrow to either side of the path"""
379 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
380 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
381 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
382 self.position = position
383 self.size = size
384 self.angle = angle
385 self.constriction = constriction
387 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=None):
388 if attrs is None:
389 attrs = self.attrs
390 if position is None:
391 position = self.position
392 if size is None:
393 size = self.size
394 if angle is None:
395 angle = self.angle
396 if constriction is None:
397 constriction = self.constriction
398 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
400 def decorate(self, dp):
401 # XXX raise exception error, when strokepath is not defined
403 # convert to normpath if necessary
404 if isinstance(dp.strokepath, path.normpath):
405 anormpath = dp.strokepath
406 else:
407 anormpath = path.normpath(dp.path)
408 if self.position:
409 anormpath = anormpath.reversed()
411 # add arrowhead to decoratedpath
412 dp.subcanvas.draw(_arrowhead(anormpath, self.size, self.angle, self.constriction),
413 self.attrs)
415 # calculate new strokepath
416 alen = _arrowheadtemplatelength(anormpath, self.size)
417 if self.constriction:
418 ilen = alen*self.constriction
419 else:
420 ilen = alen
422 # correct somewhat for rotation of arrow segments
423 ilen = ilen*math.cos(math.pi*self.angle/360.0)
425 # this is the rest of the path, we have to draw
426 anormpath = anormpath.split([ilen])[1]
428 # go back to original orientation, if necessary
429 if self.position:
430 anormpath.reverse()
432 # set the new (shortened) strokepath
433 dp.strokepath = anormpath
435 return dp
437 arrow.clear = attr.clearclass(arrow)
439 # arrows at begin of path
440 barrow = arrow(position=0)
441 barrow.SMALL = barrow(size=_base/math.sqrt(64))
442 barrow.SMALl = barrow(size=_base/math.sqrt(32))
443 barrow.SMAll = barrow(size=_base/math.sqrt(16))
444 barrow.SMall = barrow(size=_base/math.sqrt(8))
445 barrow.Small = barrow(size=_base/math.sqrt(4))
446 barrow.small = barrow(size=_base/math.sqrt(2))
447 barrow.normal = barrow(size=_base)
448 barrow.large = barrow(size=_base*math.sqrt(2))
449 barrow.Large = barrow(size=_base*math.sqrt(4))
450 barrow.LArge = barrow(size=_base*math.sqrt(8))
451 barrow.LARge = barrow(size=_base*math.sqrt(16))
452 barrow.LARGe = barrow(size=_base*math.sqrt(32))
453 barrow.LARGE = barrow(size=_base*math.sqrt(64))
455 # arrows at end of path
456 earrow = arrow(position=1)
457 earrow.SMALL = earrow(size=_base/math.sqrt(64))
458 earrow.SMALl = earrow(size=_base/math.sqrt(32))
459 earrow.SMAll = earrow(size=_base/math.sqrt(16))
460 earrow.SMall = earrow(size=_base/math.sqrt(8))
461 earrow.Small = earrow(size=_base/math.sqrt(4))
462 earrow.small = earrow(size=_base/math.sqrt(2))
463 earrow.normal = earrow(size=_base)
464 earrow.large = earrow(size=_base*math.sqrt(2))
465 earrow.Large = earrow(size=_base*math.sqrt(4))
466 earrow.LArge = earrow(size=_base*math.sqrt(8))
467 earrow.LARge = earrow(size=_base*math.sqrt(16))
468 earrow.LARGe = earrow(size=_base*math.sqrt(32))
469 earrow.LARGE = earrow(size=_base*math.sqrt(64))
472 class cycloid(deco, attr.attr):
473 """Wraps a cycloid around a path.
475 The outcome looks like a metal spring with the originalpath as the axis.
478 def __init__(self, radius=0.5*unit.t_cm, loops=10, skipfirst=1*unit.t_cm, skiplast=1*unit.t_cm, curvesperloop=2, left=1):
479 self.skipfirst = skipfirst
480 self.skiplast = skiplast
481 self.radius = radius
482 self.halfloops = 2 * int(loops) + 1
483 self.curvesperhloop = int(0.5 * curvesperloop)
484 self.sign = left and 1 or -1
486 def __call__(self, radius=None, loops=None, skipfirst=None, skiplast=None, curvesperloop=None, left=None):
487 if radius is None:
488 radius = self.radius
489 if loops is None:
490 loops = int(0.5 * self.halfloops)
491 if skipfirst is None:
492 skipfirst = self.skipfirst
493 if skiplast is None:
494 skiplast = self.skiplast
495 if curvesperloop is None:
496 curvesperloop = self.curvesperloop
497 if left is None:
498 left = self.left
499 return cycloid(radius=radius, loops=loop, skipfirst=skipfirst, skiplast=skiplast,
500 curvesperloop=curvesperloop, left=left)
502 def decorate(self, dp):
503 # XXX: is this the correct way to select the basepath???!!!
504 if isinstance(dp.strokepath, path.normpath):
505 basepath = dp.strokepath
506 elif dp.strokepath is not None:
507 basepath = path.normpath(dp.strokepath)
508 elif isinstance(dp.path, path.normpath):
509 basepath = dp.path
510 else:
511 basepath = path.normpath(dp.path)
513 skipfirst = abs(unit.topt(self.skipfirst))
514 skiplast = abs(unit.topt(self.skiplast))
515 radius = abs(unit.topt(self.radius))
517 # make list of the lengths and parameters at points on basepath where we will add cycloid-points
518 totlength = basepath.arclen_pt()
519 if totlength < skipfirst + skiplast + radius:
520 raise RuntimeError("Path is too short for decoration with cycloid")
522 # differences in length, angle ... between two basepoints
523 # and between basepoints and controlpoints
524 Dphi = math.pi / self.curvesperhloop
525 Dlength = (totlength - skipfirst - skiplast - 2*radius) * 1.0 / (self.halfloops * self.curvesperhloop)
526 # from path._arctobcurve:
527 # optimal relative distance along tangent for second and third control point
528 l = 4 * (1 - math.cos(Dphi/2)) / (3 * math.sin(Dphi/2))
529 controlDphi = math.atan2(l, 1.0)
530 controlDlength = Dlength * controlDphi / Dphi
532 # for every point on the cycloid we need the basepoint and two controlpoints
533 lengths = [skipfirst + radius + i * Dlength for i in range(self.halfloops * self.curvesperhloop + 1)]
534 lengths[0] = skipfirst
535 lengths[-1] = totlength - skiplast
536 phis = [i * Dphi for i in range(self.halfloops * self.curvesperhloop + 1)]
537 params = basepath.arclentoparam_pt(lengths)
539 #for param in params:
540 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.blue])
541 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.blue])
542 # dp.subcanvas.fill(path.circle_pt(*(basepath.at_pt(param) + (0.3,))), [color.rgb.red])
544 # get the positions of the splitpoints in the cycloid
545 points = []
546 for phi, param, length in zip(phis, params, lengths):
547 # the cycloid is a circle that is stretched along the basepath
548 # here are the points of that circle
549 basetrafo = basepath.trafo(param)
550 basex, basey = -radius * math.cos(phi), radius * math.sin(phi)
551 preex, preey = basex - l * basey, basey + l * basex
552 postx, posty = basex + l * basey, basey - l * basex
553 # and put everything at the proper place
554 preex = preex - controlDlength
555 postx = postx + controlDlength
556 if length is lengths[0]:
557 postx += radius
558 if length is lengths[-1]:
559 basex, preex = basex - radius, preex - radius
560 points.append(basetrafo._apply(preex, self.sign * preey) +
561 basetrafo._apply(basex, self.sign * basey) +
562 basetrafo._apply(postx, self.sign * posty))
564 cycloidpath = basepath.split([params[0]])[0]
565 if len(points) > 1:
566 cycloidpath.append(path.multicurveto_pt(
567 [(points[i][4:6] + points[i+1][0:4]) for i in range(len(points)-1)]))
568 else:
569 raise RuntimeError("Not enough points while decorating with cycloid")
570 cycloidpath.joined(basepath.split([params[-1]])[-1])
572 # store cycloid path
573 # XXX bbox of dp.path is wrong
574 dp.strokepath = cycloidpath
575 return dp
578 class smoothed(deco, attr.attr):
580 """Bends corners in a path.
582 This decorator replaces corners in a path with bezier curves. There are two cases:
583 - If the corner lies between two lines, _two_ bezier curves will be used
584 that are highly optimized to look good (their curvature is to be zero at the ends
585 and has to have zero derivative in the middle).
586 Additionally, it can controlled by the softness-parameter.
587 - If the corner lies between curves then _one_ bezier is used that is (except in some
588 special cases) uniquely determined by the tangents and curvatures at its end-points.
589 In some cases it is necessary to use only the absolute value of the curvature to avoid a
590 cusp-shaped connection of the new bezier to the old path. In this case the use of
591 "strict=0" allows the sign of the curvature to switch.
592 - The radius argument gives the arclength-distance of the corner to the points where the
593 old path is cut and the beziers are inserted.
594 - Path elements that are too short (shorter than the radius) are skipped
597 def __init__(self, radius, softness=1, strict=0):
598 self.radius = radius
599 self.softness = softness
600 self.strict = strict
602 def __call__(self, radius=None, softness=None, strict=None):
603 if radius is None:
604 radius = self.radius
605 if softness is None:
606 softness = self.softness
607 if strict is None:
608 strict = self.strict
609 return smoothed(radius=radius, softness=softness, strict=strict)
611 def _twobeziersbetweentolines(self, B, tangent1, tangent2, r1, r2, softness=1):
612 # Takes the corner B
613 # and two tangent vectors heading to and from B
614 # and two radii r1 and r2:
615 # All arguments must be in Points
616 # Returns the seven control points of the two bezier curves:
617 # - start d1
618 # - control points g1 and f1
619 # - midpoint e
620 # - control points f2 and g2
621 # - endpoint d2
623 # make direction vectors d1: from B to A
624 # d2: from B to C
625 d1 = -tangent1[0] / math.hypot(*tangent1), -tangent1[1] / math.hypot(*tangent1)
626 d2 = tangent2[0] / math.hypot(*tangent2), tangent2[1] / math.hypot(*tangent2)
628 # 0.3192 has turned out to be the maximum softness available
629 # for straight lines ;-)
630 f = 0.3192 * softness
631 g = (15.0 * f + math.sqrt(-15.0*f*f + 24.0*f))/12.0
633 # make the control points
634 f1 = B[0] + f * r1 * d1[0], B[1] + f * r1 * d1[1]
635 f2 = B[0] + f * r2 * d2[0], B[1] + f * r2 * d2[1]
636 g1 = B[0] + g * r1 * d1[0], B[1] + g * r1 * d1[1]
637 g2 = B[0] + g * r2 * d2[0], B[1] + g * r2 * d2[1]
638 d1 = B[0] + r1 * d1[0], B[1] + r1 * d1[1]
639 d2 = B[0] + r2 * d2[0], B[1] + r2 * d2[1]
640 e = 0.5 * (f1[0] + f2[0]), 0.5 * (f1[1] + f2[1])
642 return [d1, g1, f1, e, f2, g2, d2]
644 def _onebezierbetweentwopathels(self, A, B, tangentA, tangentB, curvA, curvB, strict=0):
645 # connects points A and B with a bezier curve that has
646 # prescribed tangents dirA, dirB and curvatures curA, curB.
647 # If strict, the sign of the curvature will be forced which may invert
648 # the sign of the tangents. If not strict, the sign of the curvature may
649 # be switched but the tangent may not.
651 def sign(x):
652 try: return abs(x) / x
653 except ZeroDivisionError: return 0
655 # normalise the tangent vectors
656 dirA = (tangentA[0] / math.hypot(*tangentA), tangentA[1] / math.hypot(*tangentA))
657 dirB = (tangentB[0] / math.hypot(*tangentB), tangentB[1] / math.hypot(*tangentB))
658 # some shortcuts
659 T = dirA[0] * dirB[1] - dirA[1] * dirB[0]
660 D = 3 * (dirA[0] * (B[1]-A[1]) - dirA[1] * (B[0]-A[0]))
661 E = 3 * (dirB[0] * (B[1]-A[1]) - dirB[1] * (B[0]-A[0]))
662 # the variables: \dot X(0) = a * dirA
663 # \dot X(1) = b * dirB
664 a, b = None, None
666 # ask for some special cases:
667 # Newton iteration is likely to fail if T==0 or curvA,curvB==0
668 if abs(T) < 1e-10:
669 try:
670 a = 2.0 * D / curvA
671 a = math.sqrt(abs(a)) * sign(a)
672 b = -2.0 * E / curvB
673 b = math.sqrt(abs(b)) * sign(b)
674 except ZeroDivisionError:
675 sys.stderr.write("*** PyX Warning: The connecting bezier is not uniquely determined. "
676 "The simple heuristic solution may not be optimal.\n")
677 a = b = 1.5 * math.hypot(A[0] - B[0], A[1] - B[1])
678 else:
679 if abs(curvA) < 1.0e-4:
680 b = D / T
681 a = - (E + b*abs(b)*curvB*0.5) / T
682 elif abs(curvB) < 1.0e-4:
683 a = -E / T
684 b = (D - a*abs(a)*curvA*0.5) / T
685 else:
686 a, b = None, None
688 # do the general case: Newton iteration
689 if a is None:
690 # solve the coupled system
691 # 0 = Ga(a,b) = 0.5 a |a| curvA + b * T - D
692 # 0 = Gb(a,b) = 0.5 b |b| curvB + a * T + E
693 # this system is equivalent to the geometric contraints:
694 # the curvature and the normal tangent vectors
695 # at parameters 0 and 1 are to be continuous
696 # the system is solved by 2-dim Newton-Iteration
697 # (a,b)^{i+1} = (a,b)^i - (DG)^{-1} (Ga(a^i,b^i), Gb(a^i,b^i))
698 a = b = 0
699 Ga = Gb = 1
700 while max(abs(Ga),abs(Gb)) > 1.0e-5:
701 detDG = abs(a*b) * curvA*curvB - T*T
702 invDG = [[curvB*abs(b)/detDG, -T/detDG], [-T/detDG, curvA*abs(a)/detDG]]
704 Ga = a*abs(a)*curvA*0.5 + b*T - D
705 Gb = b*abs(b)*curvB*0.5 + a*T + E
707 a, b = a - 0.5*invDG[0][0]*Ga - 0.5*invDG[0][1]*Gb, b - 0.5*invDG[1][0]*Ga - 0.5*invDG[1][1]*Gb
709 # the curvature may change its sign if we would get a cusp
710 # in the optimal case we have a>0 and b>0
711 if not strict:
712 a, b = abs(a), abs(b)
714 return [A, (A[0] + a * dirA[0] / 3.0, A[1] + a * dirA[1] / 3.0),
715 (B[0] - b * dirB[0] / 3.0, B[1] - b * dirB[1] / 3.0), B]
718 def decorate(self, dp):
719 radius = unit.topt(self.radius)
720 # XXX: is this the correct way to select the basepath???!!!
721 # compare to wriggle()
722 if isinstance(dp.strokepath, path.normpath):
723 basepath = dp.strokepath
724 elif dp.strokepath is not None:
725 basepath = path.normpath(dp.strokepath)
726 elif isinstance(dp.path, path.normpath):
727 basepath = dp.path
728 else:
729 basepath = path.normpath(dp.path)
731 newpath = path.path()
732 for normsubpath in basepath.subpaths:
733 npels = normsubpath.normpathels
734 arclens = [npel.arclen_pt() for npel in npels]
736 # 1. Build up a list of all relevant normpathels
737 # and the lengths where they will be cut (length with respect to the normsubpath)
738 npelnumbers = []
739 cumalen = 0
740 for no in range(len(arclens)):
741 alen = arclens[no]
742 # a first selection criterion for skipping too short normpathels
743 # the rest will queeze the radius
744 if alen > radius:
745 npelnumbers.append(no)
746 else:
747 sys.stderr.write("*** PyX Warning: smoothed is skipping a normpathel that is too short\n")
748 cumalen += alen
749 # XXX: what happens, if 0 or -1 is skipped and path not closed?
751 # 2. Find the parameters, points,
752 # and calculate tangents and curvatures
753 params, tangents, curvatures, points = [], [], [], []
754 for no in npelnumbers:
755 npel = npels[no]
756 alen = arclens[no]
758 # find the parameter(s): either one or two
759 if no is npelnumbers[0] and not normsubpath.closed:
760 pars = npel._arclentoparam_pt([max(0, alen - radius)])[0]
761 elif alen > 2 * radius:
762 pars = npel._arclentoparam_pt([radius, alen - radius])[0]
763 else:
764 pars = npel._arclentoparam_pt([0.5 * alen])[0]
766 # find points, tangents and curvatures
767 ts,cs,ps = [],[],[]
768 for par in pars:
769 # XXX: there is no trafo method for normpathels?
770 thetrafo = normsubpath.trafo(par + no)
771 p = thetrafo._apply(0,0)
772 t = thetrafo._apply(1,0)
773 ps.append(p)
774 ts.append((t[0]-p[0], t[1]-p[1]))
775 c = npel.curvradius_pt(par)
776 if c is None: cs.append(0)
777 else: cs.append(1.0/c)
779 params.append(pars)
780 points.append(ps)
781 tangents.append(ts)
782 curvatures.append(cs)
784 do_moveto = 1 # we do not know yet where to moveto
785 # 3. First part of extra handling of closed paths
786 if not normsubpath.closed:
787 bpart = npels[npelnumbers[0]].split(params[0])[0]
788 if do_moveto:
789 newpath.append(path.moveto_pt(*bpart.begin_pt()))
790 do_moveto = 0
791 if isinstance(bpart, path.normline):
792 newpath.append(path.lineto_pt(*bpart.end_pt()))
793 elif isinstance(bpart, path.normcurve):
794 newpath.append(path.curveto_pt(bpart.x1_pt, bpart.y1_pt, bpart.x2_pt, bpart.y2_pt, bpart.x3_pt, bpart.y3_pt))
795 do_moveto = 0
797 # 4. Do the splitting for the first to the last element,
798 # a closed path must be closed later
799 for i in range(len(npelnumbers)-1+(normsubpath.closed==1)):
800 this = npelnumbers[i]
801 next = npelnumbers[(i+1) % len(npelnumbers)]
802 thisnpel, nextnpel = npels[this], npels[next]
804 # split thisnpel apart and take the middle peace
805 if len(points[this]) == 2:
806 mpart = thisnpel.split(params[this])[1]
807 if do_moveto:
808 newpath.append(path.moveto_pt(*mpart.begin_pt()))
809 do_moveto = 0
810 if isinstance(mpart, path.normline):
811 newpath.append(path.lineto_pt(*mpart.end_pt()))
812 elif isinstance(mpart, path.normcurve):
813 newpath.append(path.curveto_pt(mpart.x1_pt, mpart.y1_pt, mpart.x2_pt, mpart.y2_pt, mpart.x3_pt, mpart.y3_pt))
815 # add the curve(s) replacing the corner
816 if isinstance(thisnpel, path.normline) and isinstance(nextnpel, path.normline) \
817 and (next-this == 1 or (this==0 and next==len(npels)-1)):
818 d1,g1,f1,e,f2,g2,d2 = self._twobeziersbetweentolines(
819 thisnpel.end_pt(), tangents[this][-1], tangents[next][0],
820 math.hypot(points[this][-1][0] - thisnpel.end_pt()[0], points[this][-1][1] - thisnpel.end_pt()[1]),
821 math.hypot(points[next][0][0] - nextnpel.begin_pt()[0], points[next][0][1] - nextnpel.begin_pt()[1]),
822 softness=self.softness)
823 if do_moveto:
824 newpath.append(path.moveto_pt(*d1))
825 do_moveto = 0
826 newpath.append(path.curveto_pt(*(g1 + f1 + e)))
827 newpath.append(path.curveto_pt(*(f2 + g2 + d2)))
828 #for X in [d1,g1,f1,e,f2,g2,d2]:
829 # dp.subcanvas.fill(path.circle_pt(X[0], X[1], 1.0))
830 else:
831 if not self.strict:
832 # the curvature may have the wrong sign -- produce a heuristic for the sign:
833 vx, vy = thisnpel.end_pt()[0] - points[this][-1][0], thisnpel.end_pt()[1] - points[this][-1][1]
834 wx, wy = points[next][0][0] - thisnpel.end_pt()[0], points[next][0][1] - thisnpel.end_pt()[1]
835 sign = vx * wy - vy * wx
836 sign = sign / abs(sign)
837 curvatures[this][-1] = sign * abs(curvatures[this][-1])
838 curvatures[next][0] = sign * abs(curvatures[next][0])
839 A,B,C,D = self._onebezierbetweentwopathels(
840 points[this][-1], points[next][0], tangents[this][-1], tangents[next][0],
841 curvatures[this][-1], curvatures[next][0], strict=self.strict)
842 if do_moveto:
843 newpath.append(path.moveto_pt(*A))
844 do_moveto = 0
845 newpath.append(path.curveto_pt(*(B + C + D)))
846 #for X in [A,B,C,D]:
847 # dp.subcanvas.fill(path.circle_pt(X[0], X[1], 1.0))
849 # 5. Second part of extra handling of closed paths
850 if normsubpath.closed:
851 if do_moveto:
852 newpath.append(path.moveto_pt(*dp.strokepath.begin()))
853 sys.stderr.write("*** PyXWarning: The whole path has been smoothed away -- sorry\n")
854 newpath.append(path.closepath())
855 else:
856 epart = npels[npelnumbers[-1]].split([params[-1][0]])[-1]
857 if do_moveto:
858 newpath.append(path.moveto_pt(*epart.begin_pt()))
859 do_moveto = 0
860 if isinstance(epart, path.normline):
861 newpath.append(path.lineto_pt(*epart.end_pt()))
862 elif isinstance(epart, path.normcurve):
863 newpath.append(path.curveto_pt(epart.x1_pt, epart.y1_pt, epart.x2_pt, epart.y2_pt, epart.x3_pt, epart.y3_pt))
865 dp.strokepath = newpath
866 return dp
868 smoothed.clear = attr.clearclass(smoothed)
870 _base = unit.v_cm
871 smoothed.SHARP = smoothed(radius=_base/math.sqrt(64))
872 smoothed.SHARp = smoothed(radius=_base/math.sqrt(32))
873 smoothed.SHArp = smoothed(radius=_base/math.sqrt(16))
874 smoothed.SHarp = smoothed(radius=_base/math.sqrt(8))
875 smoothed.Sharp = smoothed(radius=_base/math.sqrt(4))
876 smoothed.sharp = smoothed(radius=_base/math.sqrt(2))
877 smoothed.normal = smoothed(radius=_base)
878 smoothed.round = smoothed(radius=_base*math.sqrt(2))
879 smoothed.Round = smoothed(radius=_base*math.sqrt(4))
880 smoothed.ROund = smoothed(radius=_base*math.sqrt(8))
881 smoothed.ROUnd = smoothed(radius=_base*math.sqrt(16))
882 smoothed.ROUNd = smoothed(radius=_base*math.sqrt(32))
883 smoothed.ROUND = smoothed(radius=_base*math.sqrt(64))