1# -*- encoding: utf-8 -*-
2#
3#
4# Copyright (C) 2002-2006 Jörg Lehmann <joerg@pyx-project.org>
5# Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6# Copyright (C) 2002-2011 André Wobst <wobsta@pyx-project.org>
7#
8# This file is part of PyX (https://pyx-project.org/).
9#
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.
14#
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.
19#
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
23
24import math
25from math import cos, sin, tan, acos, pi, radians, degrees
26from . import trafo, unit
27from .normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
28from . import bbox as bboxmodule
29
30# set is available as an external interface to the normpath.set method
31from .normpath import set
32
33
34class _marker: pass
35
36################################################################################
37
38# specific exception for path-related problems
39class PathException(Exception): pass
40
41################################################################################
42# Bezier helper functions
43################################################################################
44
45def _bezierpolyrange(x0, x1, x2, x3):
46    tc = [0, 1]
47
48    a = x3 - 3*x2 + 3*x1 - x0
49    b = 2*x0 - 4*x1 + 2*x2
50    c = x1 - x0
51
52    s = b*b - 4*a*c
53    if s >= 0:
54        if b >= 0:
55            q = -0.5*(b+math.sqrt(s))
56        else:
57            q = -0.5*(b-math.sqrt(s))
58
59        if 0 < q < a or a < q < 0:
60            # 0 < q/a < 1
61            tc.append(q/a)
62
63        if 0 < c < q or q < c < 0:
64            # 0 < c/q < 1
65            tc.append(c/q)
66
67    p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
68
69    return min(*p), max(*p)
70
71
72def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
73    """generate the best bezier curve corresponding to an arc segment"""
74
75    dphi = phi2-phi1
76
77    if dphi==0: return None
78
79    # the two endpoints should be clear
80    x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
81    x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
82
83    # optimal relative distance along tangent for second and third
84    # control point
85    l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
86
87    x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
88    x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
89
90    return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
91
92
93def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
94    apath = []
95
96    phi1 = radians(phi1)
97    phi2 = radians(phi2)
98    dphimax = radians(dphimax)
99
100    if phi2<phi1:
101        # guarantee that phi2>phi1 ...
102        phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
103    elif phi2>phi1+2*pi:
104        # ... or remove unnecessary multiples of 2*pi
105        phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
106
107    if r_pt == 0 or phi1-phi2 == 0: return []
108
109    subdivisions = int((phi2-phi1)/dphimax)+1
110
111    dphi = (phi2-phi1)/subdivisions
112
113    for i in range(subdivisions):
114        apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
115
116    return apath
117
118def _arcpoint(x_pt, y_pt, r_pt, angle):
119    """return starting point of arc segment"""
120    return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
121
122def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
123    phi1 = radians(angle1)
124    phi2 = radians(angle2)
125
126    # starting end end point of arc segment
127    sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
128    earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
129
130    # Now, we have to determine the corners of the bbox for the
131    # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
132    # in the interval [phi1, phi2]. These can either be located
133    # on the borders of this interval or in the interior.
134
135    if phi2 < phi1:
136        # guarantee that phi2>phi1
137        phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
138
139    # next minimum of cos(phi) looking from phi1 in counterclockwise
140    # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
141
142    if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
143        minarcx_pt = min(sarcx_pt, earcx_pt)
144    else:
145        minarcx_pt = x_pt-r_pt
146
147    # next minimum of sin(phi) looking from phi1 in counterclockwise
148    # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
149
150    if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
151        minarcy_pt = min(sarcy_pt, earcy_pt)
152    else:
153        minarcy_pt = y_pt-r_pt
154
155    # next maximum of cos(phi) looking from phi1 in counterclockwise
156    # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
157
158    if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
159        maxarcx_pt = max(sarcx_pt, earcx_pt)
160    else:
161        maxarcx_pt = x_pt+r_pt
162
163    # next maximum of sin(phi) looking from phi1 in counterclockwise
164    # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
165
166    if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
167        maxarcy_pt = max(sarcy_pt, earcy_pt)
168    else:
169        maxarcy_pt = y_pt+r_pt
170
171    return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
172
173
174################################################################################
175# path context and pathitem base class
176################################################################################
177
178class context:
179
180    """context for pathitem"""
181
182    def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
183        """initializes a context for path items
184
185        x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
186        are the starting point of the current subpath. There are no
187        invalid contexts, i.e. all variables need to be set to integer
188        or float numbers.
189        """
190        self.x_pt = x_pt
191        self.y_pt = y_pt
192        self.subfirstx_pt = subfirstx_pt
193        self.subfirsty_pt = subfirsty_pt
194
195
196class pathitem:
197
198    """element of a PS style path"""
199
200    def __str__(self):
201        raise NotImplementedError()
202
203    def createcontext(self):
204        """creates a context from the current pathitem
205
206        Returns a context instance. Is called, when no context has yet
207        been defined, i.e. for the very first pathitem. Most of the
208        pathitems do not provide this method. Note, that you should pass
209        the context created by createcontext to updatebbox and updatenormpath
210        of successive pathitems only; use the context-free createbbox and
211        createnormpath for the first pathitem instead.
212        """
213        raise PathException("path must start with moveto or the like (%r)" % self)
214
215    def createbbox(self):
216        """creates a bbox from the current pathitem
217
218        Returns a bbox instance. Is called, when a bbox has to be
219        created instead of updating it, i.e. for the very first
220        pathitem. Most pathitems do not provide this method.
221        updatebbox must not be called for the created instance and the
222        same pathitem.
223        """
224        raise PathException("path must start with moveto or the like (%r)" % self)
225
226    def createnormpath(self, epsilon=_marker):
227        """create a normpath from the current pathitem
228
229        Return a normpath instance. Is called, when a normpath has to
230        be created instead of updating it, i.e. for the very first
231        pathitem. Most pathitems do not provide this method.
232        updatenormpath must not be called for the created instance and
233        the same pathitem.
234        """
235        raise PathException("path must start with moveto or the like (%r)" % self)
236
237    def updatebbox(self, bbox, context):
238        """updates the bbox to contain the pathitem for the given
239        context
240
241        Is called for all subsequent pathitems in a path to complete
242        the bbox information. Both, the bbox and context are updated
243        inplace. Does not return anything.
244        """
245        raise NotImplementedError(self)
246
247    def updatenormpath(self, normpath, context):
248        """update the normpath to contain the pathitem for the given
249        context
250
251        Is called for all subsequent pathitems in a path to complete
252        the normpath. Both the normpath and the context are updated
253        inplace. Most pathitem implementations will use
254        normpath.normsubpath[-1].append to add normsubpathitem(s).
255        Does not return anything.
256        """
257        raise NotImplementedError(self)
258
259    def outputPS(self, file, writer):
260        """write PS representation of pathitem to file"""
261        raise NotImplementedError(self)
262
263    def returnSVGdata(self, inverse_y, first, context):
264        """return SVG representation of pathitem
265
266        :param bool inverse_y: reverts y coordinate as SVG uses a
267            different y direction, but when creating font paths no
268            y inversion is needed.
269        :param bool first: :class:`arc` and :class:`arcn` need to
270            know whether it is first in the path to prepend a line
271            or a move. Note that it can't tell from the context as
272            it is not stored in the context whether it is first.
273        :param context: :class:`arct` need the currentpoint and
274            closepath needs the startingpoint of the last subpath
275            to update the currentpoint
276        :type context: :class:`context`
277        :rtype: string
278
279        """
280        raise NotImplementedError(self)
281
282
283
284################################################################################
285# various pathitems
286################################################################################
287# Each one comes in two variants:
288#  - one with suffix _pt. This one requires the coordinates
289#    to be already in pts (mainly used for internal purposes)
290#  - another which accepts arbitrary units
291
292
293class closepath(pathitem):
294
295    """Connect subpath back to its starting point"""
296
297    __slots__ = ()
298
299    def __str__(self):
300        return "closepath()"
301
302    def updatebbox(self, bbox, context):
303        context.x_pt = context.subfirstx_pt
304        context.y_pt = context.subfirsty_pt
305
306    def updatenormpath(self, normpath, context):
307        normpath.normsubpaths[-1].close()
308        context.x_pt = context.subfirstx_pt
309        context.y_pt = context.subfirsty_pt
310
311    def outputPS(self, file, writer):
312        file.write("closepath\n")
313
314    def returnSVGdata(self, inverse_y, first, context):
315        return "Z"
316
317
318class pdfmoveto_pt(normline_pt):
319
320    def outputPDF(self, file, writer):
321        pass
322
323
324class moveto_pt(pathitem):
325
326    """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
327
328    __slots__ = "x_pt", "y_pt"
329
330    def __init__(self, x_pt, y_pt):
331        self.x_pt = x_pt
332        self.y_pt = y_pt
333
334    def __str__(self):
335        return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
336
337    def createcontext(self):
338        return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
339
340    def createbbox(self):
341        return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
342
343    def createnormpath(self, epsilon=_marker):
344        if epsilon is _marker:
345            return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
346        elif epsilon is None:
347            return normpath([normsubpath([pdfmoveto_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
348                                         epsilon=epsilon)])
349        else:
350            return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
351                                         epsilon=epsilon)])
352
353    def updatebbox(self, bbox, context):
354        bbox.includepoint_pt(self.x_pt, self.y_pt)
355        context.x_pt = context.subfirstx_pt = self.x_pt
356        context.y_pt = context.subfirsty_pt = self.y_pt
357
358    def updatenormpath(self, normpath, context):
359        if normpath.normsubpaths[-1].epsilon is not None:
360            normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
361                                        epsilon=normpath.normsubpaths[-1].epsilon))
362        else:
363            normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
364        context.x_pt = context.subfirstx_pt = self.x_pt
365        context.y_pt = context.subfirsty_pt = self.y_pt
366
367    def outputPS(self, file, writer):
368        file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
369
370    def returnSVGdata(self, inverse_y, first, context):
371        context.x_pt = context.subfirstx_pt = self.x_pt
372        context.y_pt = context.subfirsty_pt = self.y_pt
373        if inverse_y:
374            return "M%g %g" % (self.x_pt, -self.y_pt)
375        return "M%g %g" % (self.x_pt, self.y_pt)
376
377
378class lineto_pt(pathitem):
379
380    """Append straight line to (x_pt, y_pt) (coordinates in pts)"""
381
382    __slots__ = "x_pt", "y_pt"
383
384    def __init__(self, x_pt, y_pt):
385        self.x_pt = x_pt
386        self.y_pt = y_pt
387
388    def __str__(self):
389        return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
390
391    def updatebbox(self, bbox, context):
392        bbox.includepoint_pt(self.x_pt, self.y_pt)
393        context.x_pt = self.x_pt
394        context.y_pt = self.y_pt
395
396    def updatenormpath(self, normpath, context):
397        normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
398                                                     self.x_pt, self.y_pt))
399        context.x_pt = self.x_pt
400        context.y_pt = self.y_pt
401
402    def outputPS(self, file, writer):
403        file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
404
405    def returnSVGdata(self, inverse_y, first, context):
406        context.x_pt = self.x_pt
407        context.y_pt = self.y_pt
408        if inverse_y:
409            return "L%g %g" % (self.x_pt, -self.y_pt)
410        return "L%g %g" % (self.x_pt, self.y_pt)
411
412
413class curveto_pt(pathitem):
414
415    """Append curveto (coordinates in pts)"""
416
417    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
418
419    def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
420        self.x1_pt = x1_pt
421        self.y1_pt = y1_pt
422        self.x2_pt = x2_pt
423        self.y2_pt = y2_pt
424        self.x3_pt = x3_pt
425        self.y3_pt = y3_pt
426
427    def __str__(self):
428        return "curveto_pt(%g, %g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
429                                                       self.x2_pt, self.y2_pt,
430                                                       self.x3_pt, self.y3_pt)
431
432    def updatebbox(self, bbox, context):
433        xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
434        ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
435        bbox.includepoint_pt(xmin_pt, ymin_pt)
436        bbox.includepoint_pt(xmax_pt, ymax_pt)
437        context.x_pt = self.x3_pt
438        context.y_pt = self.y3_pt
439
440    def updatenormpath(self, normpath, context):
441        normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
442                                                      self.x1_pt, self.y1_pt,
443                                                      self.x2_pt, self.y2_pt,
444                                                      self.x3_pt, self.y3_pt))
445        context.x_pt = self.x3_pt
446        context.y_pt = self.y3_pt
447
448    def outputPS(self, file, writer):
449        file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
450                                                    self.x2_pt, self.y2_pt,
451                                                    self.x3_pt, self.y3_pt))
452
453    def returnSVGdata(self, inverse_y, first, context):
454        context.x_pt = self.x3_pt
455        context.y_pt = self.y3_pt
456        if inverse_y:
457            return "C%g %g %g %g %g %g" % (self.x1_pt, -self.y1_pt, self.x2_pt, -self.y2_pt, self.x3_pt, -self.y3_pt)
458        return "C%g %g %g %g %g %g" % (self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
459
460
461class rmoveto_pt(pathitem):
462
463    """Perform relative moveto (coordinates in pts)"""
464
465    __slots__ = "dx_pt", "dy_pt"
466
467    def __init__(self, dx_pt, dy_pt):
468         self.dx_pt = dx_pt
469         self.dy_pt = dy_pt
470
471    def __str__(self):
472        return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
473
474    def updatebbox(self, bbox, context):
475        bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
476        context.x_pt += self.dx_pt
477        context.y_pt += self.dy_pt
478        context.subfirstx_pt = context.x_pt
479        context.subfirsty_pt = context.y_pt
480
481    def updatenormpath(self, normpath, context):
482        context.x_pt += self.dx_pt
483        context.y_pt += self.dy_pt
484        context.subfirstx_pt = context.x_pt
485        context.subfirsty_pt = context.y_pt
486        if normpath.normsubpaths[-1].epsilon is not None:
487            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
488                                                     context.x_pt, context.y_pt)],
489                                        epsilon=normpath.normsubpaths[-1].epsilon))
490        else:
491            normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
492
493    def outputPS(self, file, writer):
494        file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
495
496    def returnSVGdata(self, inverse_y, first, context):
497        context.x_pt += self.dx_pt
498        context.y_pt += self.dy_pt
499        context.subfirstx_pt = context.x_pt
500        context.subfirsty_pt = context.y_pt
501        if inverse_y:
502            return "m%g %g" % (self.dx_pt, -self.dy_pt)
503        return "m%g %g" % (self.dx_pt, self.dy_pt)
504
505
506class rlineto_pt(pathitem):
507
508    """Perform relative lineto (coordinates in pts)"""
509
510    __slots__ = "dx_pt", "dy_pt"
511
512    def __init__(self, dx_pt, dy_pt):
513        self.dx_pt = dx_pt
514        self.dy_pt = dy_pt
515
516    def __str__(self):
517        return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
518
519    def updatebbox(self, bbox, context):
520        bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
521        context.x_pt += self.dx_pt
522        context.y_pt += self.dy_pt
523
524    def updatenormpath(self, normpath, context):
525        normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
526                                                     context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
527        context.x_pt += self.dx_pt
528        context.y_pt += self.dy_pt
529
530    def outputPS(self, file, writer):
531        file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
532
533    def returnSVGdata(self, inverse_y, first, context):
534        context.x_pt += self.dx_pt
535        context.y_pt += self.dy_pt
536        if inverse_y:
537            return "l%g %g" % (self.dx_pt, -self.dy_pt)
538        return "l%g %g" % (self.dx_pt, self.dy_pt)
539
540
541class rcurveto_pt(pathitem):
542
543    """Append rcurveto (coordinates in pts)"""
544
545    __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
546
547    def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
548        self.dx1_pt = dx1_pt
549        self.dy1_pt = dy1_pt
550        self.dx2_pt = dx2_pt
551        self.dy2_pt = dy2_pt
552        self.dx3_pt = dx3_pt
553        self.dy3_pt = dy3_pt
554
555    def __str__(self):
556        return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
557                                                        self.dx2_pt, self.dy2_pt,
558                                                        self.dx3_pt, self.dy3_pt)
559
560    def updatebbox(self, bbox, context):
561        xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
562                                            context.x_pt+self.dx1_pt,
563                                            context.x_pt+self.dx2_pt,
564                                            context.x_pt+self.dx3_pt)
565        ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
566                                            context.y_pt+self.dy1_pt,
567                                            context.y_pt+self.dy2_pt,
568                                            context.y_pt+self.dy3_pt)
569        bbox.includepoint_pt(xmin_pt, ymin_pt)
570        bbox.includepoint_pt(xmax_pt, ymax_pt)
571        context.x_pt += self.dx3_pt
572        context.y_pt += self.dy3_pt
573
574    def updatenormpath(self, normpath, context):
575        normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
576                                                      context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
577                                                      context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
578                                                      context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
579        context.x_pt += self.dx3_pt
580        context.y_pt += self.dy3_pt
581
582    def outputPS(self, file, writer):
583        file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
584                                                     self.dx2_pt, self.dy2_pt,
585                                                     self.dx3_pt, self.dy3_pt))
586
587    def returnSVGdata(self, inverse_y, first, context):
588        context.x_pt += self.dx3_pt
589        context.y_pt += self.dy3_pt
590        if inverse_y:
591            return "c%g %g %g %g %g %g" % (self.dx1_pt, -self.dy1_pt, self.dx2_pt, -self.dy2_pt, self.dx3_pt, -self.dy3_pt)
592        return "c%g %g %g %g %g %g" % (self.dx1_pt, self.dy1_pt, self.dx2_pt, self.dy2_pt, self.dx3_pt, self.dy3_pt)
593
594
595class arc_pt(pathitem):
596
597    """Append counterclockwise arc (coordinates in pts)"""
598
599    __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
600
601    sweep = 0
602
603    def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
604        self.x_pt = x_pt
605        self.y_pt = y_pt
606        self.r_pt = r_pt
607        self.angle1 = angle1
608        self.angle2 = angle2
609
610    def __str__(self):
611        return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
612                                               self.angle1, self.angle2)
613
614    def createcontext(self):
615        x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
616        x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
617        return context(x2_pt, y2_pt, x1_pt, y1_pt)
618
619    def createbbox(self):
620        return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
621                                                self.angle1, self.angle2))
622
623    def createnormpath(self, epsilon=_marker):
624        if epsilon is _marker:
625            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
626        else:
627            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
628                                         epsilon=epsilon)])
629
630    def updatebbox(self, bbox, context):
631        minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
632                                                                      self.angle1, self.angle2)
633        bbox.includepoint_pt(minarcx_pt, minarcy_pt)
634        bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
635        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
636
637    def updatenormpath(self, normpath, context):
638        if normpath.normsubpaths[-1].closed:
639            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
640                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
641                                        epsilon=normpath.normsubpaths[-1].epsilon))
642        else:
643            normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
644                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
645        normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
646        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
647
648    def outputPS(self, file, writer):
649        file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
650                                             self.r_pt,
651                                             self.angle1,
652                                             self.angle2))
653
654    def returnSVGdata(self, inverse_y, first, context):
655        # move or line to the start point
656        x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
657        if inverse_y:
658            y_pt = -y_pt
659        if first:
660            data = ["M%g %g" % (x_pt, y_pt)]
661        else:
662            data = ["L%g %g" % (x_pt, y_pt)]
663
664        angle1 = self.angle1
665        angle2 = self.angle2
666
667        # make 0 < angle2-angle1 < 2*360
668        if angle2 < angle1:
669            angle2 += (math.floor((angle1-angle2)/360)+1)*360
670        elif angle2 > angle1 + 360:
671            angle2 -= (math.floor((angle2-angle1)/360)-1)*360
672        # svg arcs become unstable when close to 360 degree and cannot
673        # express more than 360 degree at all, so we might need to split.
674        subdivisions = int((angle2-angle1)/350)+1
675
676        # we equal split by subdivisions
677        large = "1" if (angle2-angle1)/subdivisions > 180 else "0"
678        for i in range(subdivisions):
679            x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, angle1 + (i+1)*(angle2-angle1)/subdivisions)
680            if inverse_y:
681                y_pt = -y_pt
682            data.append("A%g %g 0 %s 0 %g %g" % (self.r_pt, self.r_pt, large, x_pt, y_pt))
683
684        context.x_pt = x_pt
685        context.y_pt = y_pt
686        return "".join(data)
687
688
689class arcn_pt(pathitem):
690
691    """Append clockwise arc (coordinates in pts)"""
692
693    __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
694
695    sweep = 1
696
697    def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
698        self.x_pt = x_pt
699        self.y_pt = y_pt
700        self.r_pt = r_pt
701        self.angle1 = angle1
702        self.angle2 = angle2
703
704    def __str__(self):
705        return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
706                                                self.angle1, self.angle2)
707
708    def createcontext(self):
709        x1_pt, y1_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
710        x2_pt, y2_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
711        return context(x2_pt, y2_pt, x1_pt, y1_pt)
712
713    def createbbox(self):
714        return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
715                                                self.angle2, self.angle1))
716
717    def createnormpath(self, epsilon=_marker):
718        if epsilon is _marker:
719            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
720        else:
721            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
722                                         epsilon=epsilon)]).reversed()
723
724    def updatebbox(self, bbox, context):
725        minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
726                                                                      self.angle2, self.angle1)
727        bbox.includepoint_pt(minarcx_pt, minarcy_pt)
728        bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
729        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
730
731    def updatenormpath(self, normpath, context):
732        if normpath.normsubpaths[-1].closed:
733            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
734                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
735                                        epsilon=normpath.normsubpaths[-1].epsilon))
736        else:
737            normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
738                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
739        bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
740        bpathitems.reverse()
741        for bpathitem in bpathitems:
742            normpath.normsubpaths[-1].append(bpathitem.reversed())
743        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
744
745    def outputPS(self, file, writer):
746        file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
747                                              self.r_pt,
748                                              self.angle1,
749                                              self.angle2))
750
751    def returnSVGdata(self, inverse_y, first, context):
752        # move or line to the start point
753        x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)
754        if inverse_y:
755            y_pt = -y_pt
756        if first:
757            data = ["M%g %g" % (x_pt, y_pt)]
758        else:
759            data = ["L%g %g" % (x_pt, y_pt)]
760
761        angle1 = self.angle1
762        angle2 = self.angle2
763
764        # make 0 < angle1-angle2 < 2*360
765        if angle1 < angle2:
766            angle1 += (math.floor((angle2-angle1)/360)+1)*360
767        elif angle1 > angle2 + 360:
768            angle1 -= (math.floor((angle1-angle2)/360)-1)*360
769        # svg arcs become unstable when close to 360 degree and cannot
770        # express more than 360 degree at all, so we might need to split.
771        subdivisions = int((angle1-angle2)/350)+1
772
773        # we equal split by subdivisions
774        large = "1" if (angle1-angle2)/subdivisions > 180 else "0"
775        for i in range(subdivisions):
776            x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, angle1 + (i+1)*(angle2-angle1)/subdivisions)
777            if inverse_y:
778                y_pt = -y_pt
779            data.append("A%g %g 0 %s 1 %g %g" % (self.r_pt, self.r_pt, large, x_pt, y_pt))
780
781        context.x_pt = x_pt
782        context.y_pt = y_pt
783        return "".join(data)
784
785
786class arct_pt(pathitem):
787
788    """Append tangent arc (coordinates in pts)"""
789
790    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
791
792    def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
793        self.x1_pt = x1_pt
794        self.y1_pt = y1_pt
795        self.x2_pt = x2_pt
796        self.y2_pt = y2_pt
797        self.r_pt = r_pt
798
799    def __str__(self):
800        return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
801                                                self.x2_pt, self.y2_pt,
802                                                self.r_pt)
803
804    def _pathitems(self, x_pt, y_pt):
805        """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
806
807        The return is a list containing line_pt, arc_pt, a arcn_pt instances.
808
809        This is a helper routine for updatebbox and updatenormpath,
810        which will delegate the work to the constructed pathitem.
811        """
812
813        # direction of tangent 1
814        dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
815        l1_pt = math.hypot(dx1_pt, dy1_pt)
816        dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
817
818        # direction of tangent 2
819        dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
820        l2_pt = math.hypot(dx2_pt, dy2_pt)
821        dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
822
823        # intersection angle between two tangents in the range (-pi, pi).
824        # We take the orientation from the sign of the vector product.
825        # Negative (positive) angles alpha corresponds to a turn to the right (left)
826        # as seen from currentpoint.
827        if dx1*dy2-dy1*dx2 > 0:
828            alpha = acos(dx1*dx2+dy1*dy2)
829        else:
830            alpha = -acos(dx1*dx2+dy1*dy2)
831
832        try:
833            # two tangent points
834            xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
835            yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
836            xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
837            yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
838
839            # direction point 1 -> center of arc
840            dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
841            dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
842            lm_pt = math.hypot(dmx_pt, dmy_pt)
843            dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
844
845            # center of arc
846            mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
847            my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
848
849            # angle around which arc is centered
850            phi = degrees(math.atan2(-dmy, -dmx))
851
852            # half angular width of arc
853            deltaphi = degrees(alpha)/2
854
855            line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
856            if alpha > 0:
857                return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
858            else:
859                return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
860
861        except ZeroDivisionError:
862            # in the degenerate case, we just return a line as specified by the PS
863            # language reference
864            return [lineto_pt(self.x1_pt, self.y1_pt)]
865
866    def updatebbox(self, bbox, context):
867        for pathitem in self._pathitems(context.x_pt, context.y_pt):
868            pathitem.updatebbox(bbox, context)
869
870    def updatenormpath(self, normpath, context):
871        for pathitem in self._pathitems(context.x_pt, context.y_pt):
872            pathitem.updatenormpath(normpath, context)
873
874    def outputPS(self, file, writer):
875        file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
876                                              self.x2_pt, self.y2_pt,
877                                              self.r_pt))
878
879    def returnSVGdata(self, inverse_y, first, context):
880        # first is always False as arct cannot be first, it has no createcontext method
881        return "".join(pathitem.returnSVGdata(inverse_y, first, context) for pathitem in self._pathitems(context.x_pt, context.y_pt))
882
883#
884# now the pathitems that convert from user coordinates to pts
885#
886
887class moveto(moveto_pt):
888
889    """Set current point to (x, y)"""
890
891    __slots__ = "x_pt", "y_pt"
892
893    def __init__(self, x, y):
894        moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
895
896
897class lineto(lineto_pt):
898
899    """Append straight line to (x, y)"""
900
901    __slots__ = "x_pt", "y_pt"
902
903    def __init__(self, x, y):
904        lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
905
906
907class curveto(curveto_pt):
908
909    """Append curveto"""
910
911    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
912
913    def __init__(self, x1, y1, x2, y2, x3, y3):
914        curveto_pt.__init__(self,
915                            unit.topt(x1), unit.topt(y1),
916                            unit.topt(x2), unit.topt(y2),
917                            unit.topt(x3), unit.topt(y3))
918
919class rmoveto(rmoveto_pt):
920
921    """Perform relative moveto"""
922
923    __slots__ = "dx_pt", "dy_pt"
924
925    def __init__(self, dx, dy):
926        rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
927
928
929class rlineto(rlineto_pt):
930
931    """Perform relative lineto"""
932
933    __slots__ = "dx_pt", "dy_pt"
934
935    def __init__(self, dx, dy):
936        rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
937
938
939class rcurveto(rcurveto_pt):
940
941    """Append rcurveto"""
942
943    __slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
944
945    def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
946        rcurveto_pt.__init__(self,
947                             unit.topt(dx1), unit.topt(dy1),
948                             unit.topt(dx2), unit.topt(dy2),
949                             unit.topt(dx3), unit.topt(dy3))
950
951
952class arcn(arcn_pt):
953
954    """Append clockwise arc"""
955
956    __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
957
958    def __init__(self, x, y, r, angle1, angle2):
959        arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
960
961
962class arc(arc_pt):
963
964    """Append counterclockwise arc"""
965
966    __slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
967
968    def __init__(self, x, y, r, angle1, angle2):
969        arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
970
971
972class arct(arct_pt):
973
974    """Append tangent arc"""
975
976    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
977
978    def __init__(self, x1, y1, x2, y2, r):
979        arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
980                         unit.topt(x2), unit.topt(y2), unit.topt(r))
981
982#
983# "combined" pathitems provided for performance reasons
984#
985
986class multilineto_pt(pathitem):
987
988    """Perform multiple linetos (coordinates in pts)"""
989
990    __slots__ = "points_pt"
991
992    def __init__(self, points_pt):
993        self.points_pt = points_pt
994
995    def __str__(self):
996        result = []
997        for point_pt in self.points_pt:
998            result.append("(%g, %g)" % point_pt )
999        return "multilineto_pt([%s])" % (", ".join(result))
1000
1001    def updatebbox(self, bbox, context):
1002        for point_pt in self.points_pt:
1003            bbox.includepoint_pt(*point_pt)
1004        if self.points_pt:
1005            context.x_pt, context.y_pt = self.points_pt[-1]
1006
1007    def updatenormpath(self, normpath, context):
1008        x0_pt, y0_pt = context.x_pt, context.y_pt
1009        for point_pt in self.points_pt:
1010            normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
1011            x0_pt, y0_pt = point_pt
1012        context.x_pt, context.y_pt = x0_pt, y0_pt
1013
1014    def outputPS(self, file, writer):
1015        for point_pt in self.points_pt:
1016            file.write("%g %g lineto\n" % point_pt )
1017
1018    def returnSVGdata(self, inverse_y, first, context):
1019        if self.points_pt:
1020            context.x_pt, context.y_pt = self.points_pt[-1]
1021        if inverse_y:
1022            return "".join("L%g %g" % (x_pt, -y_pt) for x_pt, y_pt in self.points_pt)
1023        return "".join("L%g %g" % point_pt for point_pt in self.points_pt)
1024
1025
1026class multicurveto_pt(pathitem):
1027
1028    """Perform multiple curvetos (coordinates in pts)"""
1029
1030    __slots__ = "points_pt"
1031
1032    def __init__(self, points_pt):
1033        self.points_pt = points_pt
1034
1035    def __str__(self):
1036        result = []
1037        for point_pt in self.points_pt:
1038            result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
1039        return "multicurveto_pt([%s])" % (", ".join(result))
1040
1041    def updatebbox(self, bbox, context):
1042        for point_pt in self.points_pt:
1043            xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, point_pt[0], point_pt[2], point_pt[4])
1044            ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, point_pt[1], point_pt[3], point_pt[5])
1045            bbox.includepoint_pt(xmin_pt, ymin_pt)
1046            bbox.includepoint_pt(xmax_pt, ymax_pt)
1047            context.x_pt, context.y_pt = point_pt[4:]
1048
1049    def updatenormpath(self, normpath, context):
1050        x0_pt, y0_pt = context.x_pt, context.y_pt
1051        for point_pt in self.points_pt:
1052            normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
1053            x0_pt, y0_pt = point_pt[4:]
1054        context.x_pt, context.y_pt = x0_pt, y0_pt
1055
1056    def outputPS(self, file, writer):
1057        for point_pt in self.points_pt:
1058            file.write("%g %g %g %g %g %g curveto\n" % point_pt)
1059
1060    def returnSVGdata(self, inverse_y, first, context):
1061        if self.points_pt:
1062            context.x_pt, context.y_pt = self.points_pt[-1][4:]
1063        if inverse_y:
1064            return "".join("C%g %g %g %g %g %g" % (x1_pt, -y1_pt, x2_pt, -y2_pt, x3_pt, -y3_pt)
1065                           for x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt in self.points_pt)
1066        return "".join("C%g %g %g %g %g %g" % point_pt for point_pt in self.points_pt)
1067
1068
1069################################################################################
1070# path: PS style path
1071################################################################################
1072
1073class path:
1074
1075    """PS style path"""
1076
1077    __slots__ = "pathitems", "_normpath"
1078
1079    def __init__(self, *pathitems):
1080        """construct a path from pathitems *args"""
1081
1082        for apathitem in pathitems:
1083            assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
1084
1085        self.pathitems = list(pathitems)
1086        # normpath cache (when no epsilon is set)
1087        self._normpath = None
1088
1089    def __add__(self, other):
1090        """create new path out of self and other"""
1091        return path(*(self.pathitems + other.path().pathitems))
1092
1093    def __iadd__(self, other):
1094        """add other inplace
1095
1096        If other is a normpath instance, it is converted to a path before
1097        being added.
1098        """
1099        self.pathitems += other.path().pathitems
1100        self._normpath = None
1101        return self
1102
1103    def __getitem__(self, i):
1104        """return path item i"""
1105        return self.pathitems[i]
1106
1107    def __len__(self):
1108        """return the number of path items"""
1109        return len(self.pathitems)
1110
1111    def __str__(self):
1112        l = ", ".join(map(str, self.pathitems))
1113        return "path(%s)" % l
1114
1115    def append(self, apathitem):
1116        """append a path item"""
1117        assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1118        self.pathitems.append(apathitem)
1119        self._normpath = None
1120
1121    def arclen_pt(self):
1122        """return arc length in pts"""
1123        return self.normpath().arclen_pt()
1124
1125    def arclen(self):
1126        """return arc length"""
1127        return self.normpath().arclen()
1128
1129    def arclentoparam_pt(self, lengths_pt):
1130        """return the param(s) matching the given length(s)_pt in pts"""
1131        return self.normpath().arclentoparam_pt(lengths_pt)
1132
1133    def arclentoparam(self, lengths):
1134        """return the param(s) matching the given length(s)"""
1135        return self.normpath().arclentoparam(lengths)
1136
1137    def at_pt(self, params):
1138        """return coordinates of path in pts at param(s) or arc length(s) in pts"""
1139        return self.normpath().at_pt(params)
1140
1141    def at(self, params):
1142        """return coordinates of path at param(s) or arc length(s)"""
1143        return self.normpath().at(params)
1144
1145    def atbegin_pt(self):
1146        """return coordinates of the beginning of first subpath in path in pts"""
1147        return self.normpath().atbegin_pt()
1148
1149    def atbegin(self):
1150        """return coordinates of the beginning of first subpath in path"""
1151        return self.normpath().atbegin()
1152
1153    def atend_pt(self):
1154        """return coordinates of the end of last subpath in path in pts"""
1155        return self.normpath().atend_pt()
1156
1157    def atend(self):
1158        """return coordinates of the end of last subpath in path"""
1159        return self.normpath().atend()
1160
1161    def bbox(self):
1162        """return bbox of path"""
1163        if self.pathitems:
1164            bbox = self.pathitems[0].createbbox()
1165            context = self.pathitems[0].createcontext()
1166            for pathitem in self.pathitems[1:]:
1167                pathitem.updatebbox(bbox, context)
1168            return bbox
1169        else:
1170            return bboxmodule.empty()
1171
1172    def begin(self):
1173        """return param corresponding of the beginning of the path"""
1174        return self.normpath().begin()
1175
1176    def curvature_pt(self, params):
1177        """return the curvature in 1/pts at param(s) or arc length(s) in pts"""
1178        return self.normpath().curvature_pt(params)
1179
1180    def end(self):
1181        """return param corresponding of the end of the path"""
1182        return self.normpath().end()
1183
1184    def extend(self, pathitems):
1185        """extend path by pathitems"""
1186        for apathitem in pathitems:
1187            assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
1188        self.pathitems.extend(pathitems)
1189        self._normpath = None
1190
1191    def intersect(self, other):
1192        """intersect self with other path
1193
1194        Returns a tuple of lists consisting of the parameter values
1195        of the intersection points of the corresponding normpath.
1196        """
1197        return self.normpath().intersect(other)
1198
1199    def join(self, other):
1200        """join other path/normpath inplace
1201
1202        If other is a normpath instance, it is converted to a path before
1203        being joined.
1204        """
1205        self.pathitems = self.joined(other).path().pathitems
1206        self._normpath = None
1207        return self
1208
1209    def joined(self, other):
1210        """return path consisting of self and other joined together"""
1211        return self.normpath().joined(other).path()
1212
1213    # << operator also designates joining
1214    __lshift__ = joined
1215
1216    def normpath(self, epsilon=_marker):
1217        """convert the path into a normpath"""
1218        # use cached value if existent and epsilon is _marker
1219        if self._normpath is not None and epsilon is _marker:
1220            return self._normpath
1221        if self.pathitems:
1222            if epsilon is _marker:
1223                np = self.pathitems[0].createnormpath()
1224            else:
1225                np = self.pathitems[0].createnormpath(epsilon)
1226            context = self.pathitems[0].createcontext()
1227            for pathitem in self.pathitems[1:]:
1228                pathitem.updatenormpath(np, context)
1229        else:
1230            np = normpath()
1231        if epsilon is _marker:
1232            self._normpath = np
1233        return np
1234
1235    def paramtoarclen_pt(self, params):
1236        """return arc lenght(s) in pts matching the given param(s)"""
1237        return self.normpath().paramtoarclen_pt(params)
1238
1239    def paramtoarclen(self, params):
1240        """return arc lenght(s) matching the given param(s)"""
1241        return self.normpath().paramtoarclen(params)
1242
1243    def path(self):
1244        """return corresponding path, i.e., self"""
1245        return self
1246
1247    def reversed(self):
1248        """return reversed normpath"""
1249        # TODO: couldn't we try to return a path instead of converting it
1250        #       to a normpath (but this might not be worth the trouble)
1251        return self.normpath().reversed()
1252
1253    def rotation_pt(self, params):
1254        """return rotation at param(s) or arc length(s) in pts"""
1255        return self.normpath().rotation(params)
1256
1257    def rotation(self, params):
1258        """return rotation at param(s) or arc length(s)"""
1259        return self.normpath().rotation(params)
1260
1261    def split_pt(self, params):
1262        """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
1263        return self.normpath().split_pt(params)
1264
1265    def split(self, params):
1266        """split normpath at param(s) or arc length(s) and return list of normpaths"""
1267        return self.normpath().split(params)
1268
1269    def tangent_pt(self, params, length):
1270        """return tangent vector of path at param(s) or arc length(s) in pts
1271
1272        If length in pts is not None, the tangent vector will be scaled to
1273        the desired length.
1274        """
1275        return self.normpath().tangent_pt(params, length)
1276
1277    def tangent(self, params, length=1):
1278        """return tangent vector of path at param(s) or arc length(s)
1279
1280        If length is not None, the tangent vector will be scaled to
1281        the desired length.
1282        """
1283        return self.normpath().tangent(params, length)
1284
1285    def trafo_pt(self, params):
1286        """return transformation at param(s) or arc length(s) in pts"""
1287        return self.normpath().trafo(params)
1288
1289    def trafo(self, params):
1290        """return transformation at param(s) or arc length(s)"""
1291        return self.normpath().trafo(params)
1292
1293    def transformed(self, trafo):
1294        """return transformed path"""
1295        return self.normpath().transformed(trafo)
1296
1297    def outputPS(self, file, writer):
1298        """write PS code to file"""
1299        for pitem in self.pathitems:
1300            pitem.outputPS(file, writer)
1301
1302    def outputPDF(self, file, writer):
1303        """write PDF code to file"""
1304        # PDF only supports normsubpathitems; we need to use a normpath
1305        # with epsilon equals None to prevent failure for paths shorter
1306        # than epsilon
1307        self.normpath(epsilon=None).outputPDF(file, writer)
1308
1309    def returnSVGdata(self, inverse_y=True):
1310        """return SVG code"""
1311        if not self.pathitems:
1312            return ""
1313        context = self.pathitems[0].createcontext()
1314        return "".join(pitem.returnSVGdata(inverse_y, not i, context) for i, pitem in enumerate(self.pathitems))
1315
1316
1317#
1318# some special kinds of path, again in two variants
1319#
1320
1321class line_pt(path):
1322
1323    """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1324
1325    def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1326        path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1327
1328
1329class curve_pt(path):
1330
1331    """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1332
1333    def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1334        path.__init__(self,
1335                      moveto_pt(x0_pt, y0_pt),
1336                      curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1337
1338
1339class rect_pt(path):
1340
1341    """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1342
1343    def __init__(self, x_pt, y_pt, width_pt, height_pt):
1344        path.__init__(self, moveto_pt(x_pt, y_pt),
1345                            lineto_pt(x_pt+width_pt, y_pt),
1346                            lineto_pt(x_pt+width_pt, y_pt+height_pt),
1347                            lineto_pt(x_pt, y_pt+height_pt),
1348                            closepath())
1349
1350
1351class circle_pt(path):
1352
1353    """circle with center (x_pt, y_pt) and radius_pt in pts"""
1354
1355    def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
1356        path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
1357                            arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
1358                            closepath())
1359
1360
1361class ellipse_pt(path):
1362
1363    """ellipse with center (x_pt, y_pt) in pts,
1364    the two axes (a_pt, b_pt) in pts,
1365    and the angle angle of the first axis"""
1366
1367    def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
1368        t = trafo.scale(a_pt, b_pt).rotated(angle).translated_pt(x_pt, y_pt)
1369        p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
1370        path.__init__(self, *p.pathitems)
1371
1372
1373class line(line_pt):
1374
1375    """straight line from (x1, y1) to (x2, y2)"""
1376
1377    def __init__(self, x1, y1, x2, y2):
1378        line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
1379                               unit.topt(x2), unit.topt(y2))
1380
1381
1382class curve(curve_pt):
1383
1384    """bezier curve with control points (x0, y1),..., (x3, y3)"""
1385
1386    def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1387        curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
1388                                unit.topt(x1), unit.topt(y1),
1389                                unit.topt(x2), unit.topt(y2),
1390                                unit.topt(x3), unit.topt(y3))
1391
1392
1393class rect(rect_pt):
1394
1395    """rectangle at position (x,y) with width and height"""
1396
1397    def __init__(self, x, y, width, height):
1398        rect_pt.__init__(self, unit.topt(x), unit.topt(y),
1399                               unit.topt(width), unit.topt(height))
1400
1401
1402class circle(circle_pt):
1403
1404    """circle with center (x,y) and radius"""
1405
1406    def __init__(self, x, y, radius, **kwargs):
1407        circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1408
1409
1410class ellipse(ellipse_pt):
1411
1412    """ellipse with center (x, y), the two axes (a, b),
1413    and the angle angle of the first axis"""
1414
1415    def __init__(self, x, y, a, b, angle, **kwargs):
1416        ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)
1417