~ubuntu-branches/ubuntu/trusty/pyx/trusty

« back to all changes in this revision

Viewing changes to pyx/path.py

  • Committer: Bazaar Package Importer
  • Author(s): Thomas Viehmann
  • Date: 2006-11-26 14:04:53 UTC
  • mfrom: (2.1.3 edgy)
  • Revision ID: james.westby@ubuntu.com-20061126140453-1dq3cycpspmlik2t
Tags: 0.9-3
* New maintainer. Thank you for more than three years of
  maintenance,  Graham! Closes: #400087
* Don't hard-code python 2.3 in manual/Makefile.
  Thanks to Matthias Klose for the bug report and patch.
  Closes: #392634
* Remove obsolete dh_python call from debian/rules.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
1
# -*- coding: ISO-8859-1 -*-
3
2
#
4
3
#
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>
 
4
# Copyright (C) 2002-2006 J�rg Lehmann <joergl@users.sourceforge.net>
 
5
# Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
 
6
# Copyright (C) 2002-2006 Andr� Wobst <wobsta@users.sourceforge.net>
8
7
#
9
8
# This file is part of PyX (http://pyx.sourceforge.net/).
10
9
#
20
19
#
21
20
# You should have received a copy of the GNU General Public License
22
21
# along with PyX; if not, write to the Free Software
23
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24
 
 
25
 
#       - exceptions: nocurrentpoint, paramrange
26
 
#       - correct bbox for curveto and normcurve
27
 
#         (maybe we still need the current bbox implementation (then maybe called
28
 
#          cbox = control box) for normcurve for the use during the
29
 
#          intersection of bpaths)
30
 
 
31
 
import math, bisect
32
 
from math import cos, sin, pi
 
22
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 
23
 
 
24
from __future__ import nested_scopes
 
25
 
 
26
import math
 
27
from math import cos, sin, tan, acos, pi
33
28
try:
34
29
    from math import radians, degrees
35
30
except ImportError:
36
 
    # fallback implementation for Python 2.1 and below
 
31
    # fallback implementation for Python 2.1
37
32
    def radians(x): return x*pi/180
38
33
    def degrees(x): return x*180/pi
39
 
import base, bbox, trafo, unit, helper
 
34
 
 
35
import trafo, unit
 
36
from normpath import NormpathException, normpath, normsubpath, normline_pt, normcurve_pt
 
37
import bbox as bboxmodule
 
38
 
 
39
# set is available as an external interface to the normpath.set method
 
40
from normpath import set
 
41
# normpath's invalid is available as an external interface
 
42
from normpath import invalid
40
43
 
41
44
try:
42
45
    sum([])
43
46
except NameError:
44
 
    # fallback implementation for Python 2.2. and below
 
47
    # fallback implementation for Python 2.2 and below
45
48
    def sum(list):
46
49
        return reduce(lambda x, y: x+y, list, 0)
47
50
 
48
51
try:
49
52
    enumerate([])
50
53
except NameError:
51
 
    # fallback implementation for Python 2.2. and below
 
54
    # fallback implementation for Python 2.2 and below
52
55
    def enumerate(list):
53
56
        return zip(xrange(len(list)), list)
54
57
 
55
58
# use new style classes when possible
56
59
__metaclass__ = type
57
60
 
 
61
class _marker: pass
 
62
 
58
63
################################################################################
59
64
 
60
 
# global epsilon (default precision of normsubpaths)
61
 
_epsilon = 1e-5
62
 
 
63
 
def set(epsilon=None):
64
 
    if epsilon is not None:
65
 
        _epsilon = epsilon
 
65
# specific exception for path-related problems
 
66
class PathException(Exception): pass
66
67
 
67
68
################################################################################
68
69
# Bezier helper functions
69
70
################################################################################
70
71
 
 
72
def _bezierpolyrange(x0, x1, x2, x3):
 
73
    tc = [0, 1]
 
74
 
 
75
    a = x3 - 3*x2 + 3*x1 - x0
 
76
    b = 2*x0 - 4*x1 + 2*x2
 
77
    c = x1 - x0
 
78
 
 
79
    s = b*b - 4*a*c
 
80
    if s >= 0:
 
81
        if b >= 0:
 
82
            q = -0.5*(b+math.sqrt(s))
 
83
        else:
 
84
            q = -0.5*(b-math.sqrt(s))
 
85
 
 
86
        try:
 
87
            t = q*1.0/a
 
88
        except ZeroDivisionError:
 
89
            pass
 
90
        else:
 
91
            if 0 < t < 1:
 
92
                tc.append(t)
 
93
 
 
94
        try:
 
95
            t = c*1.0/q
 
96
        except ZeroDivisionError:
 
97
            pass
 
98
        else:
 
99
            if 0 < t < 1:
 
100
                tc.append(t)
 
101
 
 
102
    p = [(((a*t + 1.5*b)*t + 3*c)*t + x0) for t in tc]
 
103
 
 
104
    return min(*p), max(*p)
 
105
 
 
106
 
71
107
def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
72
108
    """generate the best bezier curve corresponding to an arc segment"""
73
109
 
75
111
 
76
112
    if dphi==0: return None
77
113
 
78
 
    # the two endpoints should be clear 
 
114
    # the two endpoints should be clear
79
115
    x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
80
116
    x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
81
117
 
86
122
    x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
87
123
    x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
88
124
 
89
 
    return normcurve(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
 
125
    return normcurve_pt(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
90
126
 
91
127
 
92
128
def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
114
150
 
115
151
    return apath
116
152
 
117
 
#
118
 
# we define one exception
119
 
#
120
 
 
121
 
class PathException(Exception): pass
122
 
 
123
 
################################################################################
124
 
# _pathcontext: context during walk along path
125
 
################################################################################
126
 
 
127
 
class _pathcontext:
128
 
 
129
 
    """context during walk along path"""
130
 
 
131
 
    __slots__ = "currentpoint", "currentsubpath"
132
 
 
133
 
    def __init__(self, currentpoint=None, currentsubpath=None):
134
 
        """ initialize context
135
 
 
136
 
        currentpoint:   position of current point
137
 
        currentsubpath: position of first point of current subpath
138
 
 
 
153
def _arcpoint(x_pt, y_pt, r_pt, angle):
 
154
    """return starting point of arc segment"""
 
155
    return x_pt+r_pt*cos(radians(angle)), y_pt+r_pt*sin(radians(angle))
 
156
 
 
157
def _arcbboxdata(x_pt, y_pt, r_pt, angle1, angle2):
 
158
    phi1 = radians(angle1)
 
159
    phi2 = radians(angle2)
 
160
 
 
161
    # starting end end point of arc segment
 
162
    sarcx_pt, sarcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle1)
 
163
    earcx_pt, earcy_pt = _arcpoint(x_pt, y_pt, r_pt, angle2)
 
164
 
 
165
    # Now, we have to determine the corners of the bbox for the
 
166
    # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
 
167
    # in the interval [phi1, phi2]. These can either be located
 
168
    # on the borders of this interval or in the interior.
 
169
 
 
170
    if phi2 < phi1:
 
171
        # guarantee that phi2>phi1
 
172
        phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
 
173
 
 
174
    # next minimum of cos(phi) looking from phi1 in counterclockwise
 
175
    # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
 
176
 
 
177
    if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
 
178
        minarcx_pt = min(sarcx_pt, earcx_pt)
 
179
    else:
 
180
        minarcx_pt = x_pt-r_pt
 
181
 
 
182
    # next minimum of sin(phi) looking from phi1 in counterclockwise
 
183
    # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
 
184
 
 
185
    if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
 
186
        minarcy_pt = min(sarcy_pt, earcy_pt)
 
187
    else:
 
188
        minarcy_pt = y_pt-r_pt
 
189
 
 
190
    # next maximum of cos(phi) looking from phi1 in counterclockwise
 
191
    # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
 
192
 
 
193
    if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
 
194
        maxarcx_pt = max(sarcx_pt, earcx_pt)
 
195
    else:
 
196
        maxarcx_pt = x_pt+r_pt
 
197
 
 
198
    # next maximum of sin(phi) looking from phi1 in counterclockwise
 
199
    # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
 
200
 
 
201
    if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
 
202
        maxarcy_pt = max(sarcy_pt, earcy_pt)
 
203
    else:
 
204
        maxarcy_pt = y_pt+r_pt
 
205
 
 
206
    return minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt
 
207
 
 
208
 
 
209
################################################################################
 
210
# path context and pathitem base class
 
211
################################################################################
 
212
 
 
213
class context:
 
214
 
 
215
    """context for pathitem"""
 
216
 
 
217
    def __init__(self, x_pt, y_pt, subfirstx_pt, subfirsty_pt):
 
218
        """initializes a context for path items
 
219
 
 
220
        x_pt, y_pt are the currentpoint. subfirstx_pt, subfirsty_pt
 
221
        are the starting point of the current subpath. There are no
 
222
        invalid contexts, i.e. all variables need to be set to integer
 
223
        or float numbers.
139
224
        """
140
 
 
141
 
        self.currentpoint = currentpoint
142
 
        self.currentsubpath = currentsubpath
143
 
 
144
 
################################################################################ 
145
 
# pathitem: element of a PS style path 
146
 
################################################################################
147
 
 
148
 
class pathitem(base.canvasitem):
 
225
        self.x_pt = x_pt
 
226
        self.y_pt = y_pt
 
227
        self.subfirstx_pt = subfirstx_pt
 
228
        self.subfirsty_pt = subfirsty_pt
 
229
 
 
230
 
 
231
class pathitem:
149
232
 
150
233
    """element of a PS style path"""
151
234
 
152
 
    def _updatecontext(self, context):
153
 
        """update context of during walk along pathitem
154
 
 
155
 
        changes context in place
156
 
        """
157
 
        pass
158
 
 
159
 
 
160
 
    def _bbox(self, context):
161
 
        """calculate bounding box of pathitem
162
 
 
163
 
        context: context of pathitem
164
 
 
165
 
        returns bounding box of pathitem (in given context)
166
 
 
167
 
        Important note: all coordinates in bbox, currentpoint, and 
168
 
        currrentsubpath have to be floats (in unit.topt)
169
 
 
170
 
        """
171
 
        pass
172
 
 
173
 
    def _normalized(self, context):
174
 
        """returns list of normalized version of pathitem
175
 
 
176
 
        context: context of pathitem
177
 
 
178
 
        Returns the path converted into a list of closepath, moveto_pt,
179
 
        normline, or normcurve instances.
180
 
 
181
 
        """
182
 
        pass
183
 
 
184
 
    def outputPS(self, file):
185
 
        """write PS code corresponding to pathitem to file"""
186
 
        pass
187
 
 
188
 
    def outputPDF(self, file):
189
 
        """write PDF code corresponding to pathitem to file"""
190
 
        pass
191
 
 
192
 
#
 
235
    def __str__(self):
 
236
        raise NotImplementedError()
 
237
 
 
238
    def createcontext(self):
 
239
        """creates a context from the current pathitem
 
240
 
 
241
        Returns a context instance. Is called, when no context has yet
 
242
        been defined, i.e. for the very first pathitem. Most of the
 
243
        pathitems do not provide this method. Note, that you should pass
 
244
        the context created by createcontext to updatebbox and updatenormpath
 
245
        of successive pathitems only; use the context-free createbbox and
 
246
        createnormpath for the first pathitem instead.
 
247
        """
 
248
        raise PathException("path must start with moveto or the like (%r)" % self)
 
249
 
 
250
    def createbbox(self):
 
251
        """creates a bbox from the current pathitem
 
252
 
 
253
        Returns a bbox instance. Is called, when a bbox has to be
 
254
        created instead of updating it, i.e. for the very first
 
255
        pathitem. Most pathitems do not provide this method.
 
256
        updatebbox must not be called for the created instance and the
 
257
        same pathitem.
 
258
        """
 
259
        raise PathException("path must start with moveto or the like (%r)" % self)
 
260
 
 
261
    def createnormpath(self, epsilon=_marker):
 
262
        """create a normpath from the current pathitem
 
263
 
 
264
        Return a normpath instance. Is called, when a normpath has to
 
265
        be created instead of updating it, i.e. for the very first
 
266
        pathitem. Most pathitems do not provide this method.
 
267
        updatenormpath must not be called for the created instance and
 
268
        the same pathitem.
 
269
        """
 
270
        raise PathException("path must start with moveto or the like (%r)" % self)
 
271
 
 
272
    def updatebbox(self, bbox, context):
 
273
        """updates the bbox to contain the pathitem for the given
 
274
        context
 
275
 
 
276
        Is called for all subsequent pathitems in a path to complete
 
277
        the bbox information. Both, the bbox and context are updated
 
278
        inplace. Does not return anything.
 
279
        """
 
280
        raise NotImplementedError()
 
281
 
 
282
    def updatenormpath(self, normpath, context):
 
283
        """update the normpath to contain the pathitem for the given
 
284
        context
 
285
 
 
286
        Is called for all subsequent pathitems in a path to complete
 
287
        the normpath. Both the normpath and the context are updated
 
288
        inplace. Most pathitem implementations will use
 
289
        normpath.normsubpath[-1].append to add normsubpathitem(s).
 
290
        Does not return anything.
 
291
        """
 
292
        raise NotImplementedError()
 
293
 
 
294
    def outputPS(self, file, writer):
 
295
        """write PS representation of pathitem to file"""
 
296
 
 
297
 
 
298
 
 
299
################################################################################
193
300
# various pathitems
194
 
#
 
301
################################################################################
195
302
# Each one comes in two variants:
196
 
#  - one which requires the coordinates to be already in pts (mainly
197
 
#    used for internal purposes)
 
303
#  - one with suffix _pt. This one requires the coordinates
 
304
#    to be already in pts (mainly used for internal purposes)
198
305
#  - another which accepts arbitrary units
199
306
 
200
 
class closepath(pathitem): 
 
307
 
 
308
class closepath(pathitem):
201
309
 
202
310
    """Connect subpath back to its starting point"""
203
311
 
204
312
    __slots__ = ()
205
313
 
206
314
    def __str__(self):
207
 
        return "closepath"
208
 
 
209
 
    def _updatecontext(self, context):
210
 
        context.currentpoint = None
211
 
        context.currentsubpath = None
212
 
 
213
 
    def _bbox(self, context):
214
 
        x0_pt, y0_pt = context.currentpoint
215
 
        x1_pt, y1_pt = context.currentsubpath
216
 
 
217
 
        return bbox.bbox_pt(min(x0_pt, x1_pt), min(y0_pt, y1_pt), 
218
 
                          max(x0_pt, x1_pt), max(y0_pt, y1_pt))
219
 
 
220
 
    def _normalized(self, context):
221
 
        return [closepath()]
222
 
 
223
 
    def outputPS(self, file):
 
315
        return "closepath()"
 
316
 
 
317
    def updatebbox(self, bbox, context):
 
318
        context.x_pt = context.subfirstx_pt
 
319
        context.y_pt = context.subfirsty_pt
 
320
 
 
321
    def updatenormpath(self, normpath, context):
 
322
        normpath.normsubpaths[-1].close()
 
323
        context.x_pt = context.subfirstx_pt
 
324
        context.y_pt = context.subfirsty_pt
 
325
 
 
326
    def outputPS(self, file, writer):
224
327
        file.write("closepath\n")
225
328
 
226
 
    def outputPDF(self, file):
227
 
        file.write("h\n")
228
 
 
229
329
 
230
330
class moveto_pt(pathitem):
231
331
 
232
 
    """Set current point to (x_pt, y_pt) (coordinates in pts)"""
 
332
    """Start a new subpath and set current point to (x_pt, y_pt) (coordinates in pts)"""
233
333
 
234
334
    __slots__ = "x_pt", "y_pt"
235
335
 
236
336
    def __init__(self, x_pt, y_pt):
237
 
         self.x_pt = x_pt
238
 
         self.y_pt = y_pt
 
337
        self.x_pt = x_pt
 
338
        self.y_pt = y_pt
239
339
 
240
340
    def __str__(self):
241
 
        return "%g %g moveto" % (self.x_pt, self.y_pt)
242
 
 
243
 
    def _updatecontext(self, context):
244
 
        context.currentpoint = self.x_pt, self.y_pt
245
 
        context.currentsubpath = self.x_pt, self.y_pt
246
 
 
247
 
    def _bbox(self, context):
248
 
        return None
249
 
 
250
 
    def _normalized(self, context):
251
 
        return [moveto_pt(self.x_pt, self.y_pt)]
252
 
 
253
 
    def outputPS(self, file):
 
341
        return "moveto_pt(%g, %g)" % (self.x_pt, self.y_pt)
 
342
 
 
343
    def createcontext(self):
 
344
        return context(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
 
345
 
 
346
    def createbbox(self):
 
347
        return bboxmodule.bbox_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)
 
348
 
 
349
    def createnormpath(self, epsilon=_marker):
 
350
        if epsilon is _marker:
 
351
            return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)])])
 
352
        else:
 
353
            return normpath([normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
 
354
                                         epsilon=epsilon)])
 
355
 
 
356
    def updatebbox(self, bbox, context):
 
357
        bbox.includepoint_pt(self.x_pt, self.y_pt)
 
358
        context.x_pt = context.subfirstx_pt = self.x_pt
 
359
        context.y_pt = context.subfirsty_pt = self.y_pt
 
360
 
 
361
    def updatenormpath(self, normpath, context):
 
362
        if normpath.normsubpaths[-1].epsilon is not None:
 
363
            normpath.append(normsubpath([normline_pt(self.x_pt, self.y_pt, self.x_pt, self.y_pt)],
 
364
                                        epsilon=normpath.normsubpaths[-1].epsilon))
 
365
        else:
 
366
            normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
 
367
        context.x_pt = context.subfirstx_pt = self.x_pt
 
368
        context.y_pt = context.subfirsty_pt = self.y_pt
 
369
 
 
370
    def outputPS(self, file, writer):
254
371
        file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
255
372
 
256
 
    def outputPDF(self, file):
257
 
        file.write("%f %f m\n" % (self.x_pt, self.y_pt) )
258
 
 
259
373
 
260
374
class lineto_pt(pathitem):
261
375
 
264
378
    __slots__ = "x_pt", "y_pt"
265
379
 
266
380
    def __init__(self, x_pt, y_pt):
267
 
         self.x_pt = x_pt
268
 
         self.y_pt = y_pt
 
381
        self.x_pt = x_pt
 
382
        self.y_pt = y_pt
269
383
 
270
384
    def __str__(self):
271
 
        return "%g %g lineto" % (self.x_pt, self.y_pt)
272
 
 
273
 
    def _updatecontext(self, context):
274
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
275
 
        context.currentpoint = self.x_pt, self.y_pt
276
 
 
277
 
    def _bbox(self, context):
278
 
        return bbox.bbox_pt(min(context.currentpoint[0], self.x_pt),
279
 
                          min(context.currentpoint[1], self.y_pt), 
280
 
                          max(context.currentpoint[0], self.x_pt),
281
 
                          max(context.currentpoint[1], self.y_pt))
282
 
 
283
 
    def _normalized(self, context):
284
 
        return [normline(context.currentpoint[0], context.currentpoint[1], self.x_pt, self.y_pt)]
285
 
 
286
 
    def outputPS(self, file):
 
385
        return "lineto_pt(%g, %g)" % (self.x_pt, self.y_pt)
 
386
 
 
387
    def updatebbox(self, bbox, context):
 
388
        bbox.includepoint_pt(self.x_pt, self.y_pt)
 
389
        context.x_pt = self.x_pt
 
390
        context.y_pt = self.y_pt
 
391
 
 
392
    def updatenormpath(self, normpath, context):
 
393
        normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
 
394
                                                     self.x_pt, self.y_pt))
 
395
        context.x_pt = self.x_pt
 
396
        context.y_pt = self.y_pt
 
397
 
 
398
    def outputPS(self, file, writer):
287
399
        file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
288
400
 
289
 
    def outputPDF(self, file):
290
 
        file.write("%f %f l\n" % (self.x_pt, self.y_pt) )
291
 
 
292
401
 
293
402
class curveto_pt(pathitem):
294
403
 
305
414
        self.y3_pt = y3_pt
306
415
 
307
416
    def __str__(self):
308
 
        return "%g %g %g %g %g %g curveto" % (self.x1_pt, self.y1_pt,
309
 
                                              self.x2_pt, self.y2_pt,
310
 
                                              self.x3_pt, self.y3_pt)
311
 
 
312
 
    def _updatecontext(self, context):
313
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
314
 
        context.currentpoint = self.x3_pt, self.y3_pt
315
 
 
316
 
    def _bbox(self, context):
317
 
        return bbox.bbox_pt(min(context.currentpoint[0], self.x1_pt, self.x2_pt, self.x3_pt),
318
 
                          min(context.currentpoint[1], self.y1_pt, self.y2_pt, self.y3_pt),
319
 
                          max(context.currentpoint[0], self.x1_pt, self.x2_pt, self.x3_pt),
320
 
                          max(context.currentpoint[1], self.y1_pt, self.y2_pt, self.y3_pt))
321
 
 
322
 
    def _normalized(self, context):
323
 
        return [normcurve(context.currentpoint[0], context.currentpoint[1],
324
 
                          self.x1_pt, self.y1_pt,
325
 
                          self.x2_pt, self.y2_pt,
326
 
                          self.x3_pt, self.y3_pt)]
327
 
 
328
 
    def outputPS(self, file):
329
 
        file.write("%g %g %g %g %g %g curveto\n" % ( self.x1_pt, self.y1_pt,
330
 
                                                     self.x2_pt, self.y2_pt,
331
 
                                                     self.x3_pt, self.y3_pt ) )
332
 
 
333
 
    def outputPDF(self, file):
334
 
        file.write("%f %f %f %f %f %f c\n" % ( self.x1_pt, self.y1_pt,
335
 
                                               self.x2_pt, self.y2_pt,
336
 
                                               self.x3_pt, self.y3_pt ) )
 
417
        return "curveto_pt(%g,%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
 
418
                                                      self.x2_pt, self.y2_pt,
 
419
                                                      self.x3_pt, self.y3_pt)
 
420
 
 
421
    def updatebbox(self, bbox, context):
 
422
        xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt, self.x1_pt, self.x2_pt, self.x3_pt)
 
423
        ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt, self.y1_pt, self.y2_pt, self.y3_pt)
 
424
        bbox.includepoint_pt(xmin_pt, ymin_pt)
 
425
        bbox.includepoint_pt(xmax_pt, ymax_pt)
 
426
        context.x_pt = self.x3_pt
 
427
        context.y_pt = self.y3_pt
 
428
 
 
429
    def updatenormpath(self, normpath, context):
 
430
        normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
 
431
                                                      self.x1_pt, self.y1_pt,
 
432
                                                      self.x2_pt, self.y2_pt,
 
433
                                                      self.x3_pt, self.y3_pt))
 
434
        context.x_pt = self.x3_pt
 
435
        context.y_pt = self.y3_pt
 
436
 
 
437
    def outputPS(self, file, writer):
 
438
        file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt,
 
439
                                                    self.x2_pt, self.y2_pt,
 
440
                                                    self.x3_pt, self.y3_pt))
337
441
 
338
442
 
339
443
class rmoveto_pt(pathitem):
346
450
         self.dx_pt = dx_pt
347
451
         self.dy_pt = dy_pt
348
452
 
349
 
    def _updatecontext(self, context):
350
 
        context.currentpoint = (context.currentpoint[0] + self.dx_pt,
351
 
                                context.currentpoint[1] + self.dy_pt)
352
 
        context.currentsubpath = context.currentpoint
353
 
 
354
 
    def _bbox(self, context):
355
 
        return None
356
 
 
357
 
    def _normalized(self, context):
358
 
        x_pt = context.currentpoint[0]+self.dx_pt
359
 
        y_pt = context.currentpoint[1]+self.dy_pt
360
 
        return [moveto_pt(x_pt, y_pt)]
361
 
 
362
 
    def outputPS(self, file):
 
453
    def __str__(self):
 
454
        return "rmoveto_pt(%g, %g)" % (self.dx_pt, self.dy_pt)
 
455
 
 
456
    def updatebbox(self, bbox, context):
 
457
        bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
 
458
        context.x_pt += self.dx_pt
 
459
        context.y_pt += self.dy_pt
 
460
        context.subfirstx_pt = context.x_pt
 
461
        context.subfirsty_pt = context.y_pt
 
462
 
 
463
    def updatenormpath(self, normpath, context):
 
464
        context.x_pt += self.dx_pt
 
465
        context.y_pt += self.dy_pt
 
466
        context.subfirstx_pt = context.x_pt
 
467
        context.subfirsty_pt = context.y_pt
 
468
        if normpath.normsubpaths[-1].epsilon is not None:
 
469
            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
 
470
                                                     context.x_pt, context.y_pt)],
 
471
                                        epsilon=normpath.normsubpaths[-1].epsilon))
 
472
        else:
 
473
            normpath.append(normsubpath(epsilon=normpath.normsubpaths[-1].epsilon))
 
474
 
 
475
    def outputPS(self, file, writer):
363
476
        file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
364
477
 
365
478
 
370
483
    __slots__ = "dx_pt", "dy_pt"
371
484
 
372
485
    def __init__(self, dx_pt, dy_pt):
373
 
         self.dx_pt = dx_pt
374
 
         self.dy_pt = dy_pt
375
 
 
376
 
    def _updatecontext(self, context):
377
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
378
 
        context.currentpoint = (context.currentpoint[0]+self.dx_pt,
379
 
                                context.currentpoint[1]+self.dy_pt)
380
 
 
381
 
    def _bbox(self, context):
382
 
        x = context.currentpoint[0] + self.dx_pt
383
 
        y = context.currentpoint[1] + self.dy_pt
384
 
        return bbox.bbox_pt(min(context.currentpoint[0], x),
385
 
                          min(context.currentpoint[1], y),
386
 
                          max(context.currentpoint[0], x),
387
 
                          max(context.currentpoint[1], y))
388
 
 
389
 
    def _normalized(self, context):
390
 
        x0_pt = context.currentpoint[0]
391
 
        y0_pt = context.currentpoint[1]
392
 
        return [normline(x0_pt, y0_pt, x0_pt+self.dx_pt, y0_pt+self.dy_pt)]
393
 
 
394
 
    def outputPS(self, file):
 
486
        self.dx_pt = dx_pt
 
487
        self.dy_pt = dy_pt
 
488
 
 
489
    def __str__(self):
 
490
        return "rlineto_pt(%g %g)" % (self.dx_pt, self.dy_pt)
 
491
 
 
492
    def updatebbox(self, bbox, context):
 
493
        bbox.includepoint_pt(context.x_pt + self.dx_pt, context.y_pt + self.dy_pt)
 
494
        context.x_pt += self.dx_pt
 
495
        context.y_pt += self.dy_pt
 
496
 
 
497
    def updatenormpath(self, normpath, context):
 
498
        normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
 
499
                                                     context.x_pt + self.dx_pt, context.y_pt + self.dy_pt))
 
500
        context.x_pt += self.dx_pt
 
501
        context.y_pt += self.dy_pt
 
502
 
 
503
    def outputPS(self, file, writer):
395
504
        file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
396
505
 
397
506
 
409
518
        self.dx3_pt = dx3_pt
410
519
        self.dy3_pt = dy3_pt
411
520
 
412
 
    def outputPS(self, file):
413
 
        file.write("%g %g %g %g %g %g rcurveto\n" % ( self.dx1_pt, self.dy1_pt,
414
 
                                                    self.dx2_pt, self.dy2_pt,
415
 
                                                    self.dx3_pt, self.dy3_pt ) )
416
 
 
417
 
    def _updatecontext(self, context):
418
 
        x3_pt = context.currentpoint[0]+self.dx3_pt
419
 
        y3_pt = context.currentpoint[1]+self.dy3_pt
420
 
 
421
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
422
 
        context.currentpoint = x3_pt, y3_pt
423
 
 
424
 
 
425
 
    def _bbox(self, context):
426
 
        x1_pt = context.currentpoint[0]+self.dx1_pt
427
 
        y1_pt = context.currentpoint[1]+self.dy1_pt
428
 
        x2_pt = context.currentpoint[0]+self.dx2_pt
429
 
        y2_pt = context.currentpoint[1]+self.dy2_pt
430
 
        x3_pt = context.currentpoint[0]+self.dx3_pt
431
 
        y3_pt = context.currentpoint[1]+self.dy3_pt
432
 
        return bbox.bbox_pt(min(context.currentpoint[0], x1_pt, x2_pt, x3_pt),
433
 
                          min(context.currentpoint[1], y1_pt, y2_pt, y3_pt),
434
 
                          max(context.currentpoint[0], x1_pt, x2_pt, x3_pt),
435
 
                          max(context.currentpoint[1], y1_pt, y2_pt, y3_pt))
436
 
 
437
 
    def _normalized(self, context):
438
 
        x0_pt = context.currentpoint[0]
439
 
        y0_pt = context.currentpoint[1]
440
 
        return [normcurve(x0_pt, y0_pt, x0_pt+self.dx1_pt, y0_pt+self.dy1_pt, x0_pt+self.dx2_pt, y0_pt+self.dy2_pt, x0_pt+self.dx3_pt, y0_pt+self.dy3_pt)]
 
521
    def __str__(self):
 
522
        return "rcurveto_pt(%g, %g, %g, %g, %g, %g)" % (self.dx1_pt, self.dy1_pt,
 
523
                                                        self.dx2_pt, self.dy2_pt,
 
524
                                                        self.dx3_pt, self.dy3_pt)
 
525
 
 
526
    def updatebbox(self, bbox, context):
 
527
        xmin_pt, xmax_pt = _bezierpolyrange(context.x_pt,
 
528
                                            context.x_pt+self.dx1_pt,
 
529
                                            context.x_pt+self.dx2_pt,
 
530
                                            context.x_pt+self.dx3_pt)
 
531
        ymin_pt, ymax_pt = _bezierpolyrange(context.y_pt,
 
532
                                            context.y_pt+self.dy1_pt,
 
533
                                            context.y_pt+self.dy2_pt,
 
534
                                            context.y_pt+self.dy3_pt)
 
535
        bbox.includepoint_pt(xmin_pt, ymin_pt)
 
536
        bbox.includepoint_pt(xmax_pt, ymax_pt)
 
537
        context.x_pt += self.dx3_pt
 
538
        context.y_pt += self.dy3_pt
 
539
 
 
540
    def updatenormpath(self, normpath, context):
 
541
        normpath.normsubpaths[-1].append(normcurve_pt(context.x_pt, context.y_pt,
 
542
                                                      context.x_pt + self.dx1_pt, context.y_pt + self.dy1_pt,
 
543
                                                      context.x_pt + self.dx2_pt, context.y_pt + self.dy2_pt,
 
544
                                                      context.x_pt + self.dx3_pt, context.y_pt + self.dy3_pt))
 
545
        context.x_pt += self.dx3_pt
 
546
        context.y_pt += self.dy3_pt
 
547
 
 
548
    def outputPS(self, file, writer):
 
549
        file.write("%g %g %g %g %g %g rcurveto\n" % (self.dx1_pt, self.dy1_pt,
 
550
                                                     self.dx2_pt, self.dy2_pt,
 
551
                                                     self.dx3_pt, self.dy3_pt))
441
552
 
442
553
 
443
554
class arc_pt(pathitem):
453
564
        self.angle1 = angle1
454
565
        self.angle2 = angle2
455
566
 
456
 
    def _sarc(self):
457
 
        """Return starting point of arc segment"""
458
 
        return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
459
 
                self.y_pt+self.r_pt*sin(radians(self.angle1)))
460
 
 
461
 
    def _earc(self):
462
 
        """Return end point of arc segment"""
463
 
        return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
464
 
                self.y_pt+self.r_pt*sin(radians(self.angle2)))
465
 
 
466
 
    def _updatecontext(self, context):
467
 
        if context.currentpoint:
468
 
            context.currentsubpath = context.currentsubpath or context.currentpoint
469
 
        else:
470
 
            # we assert that currentsubpath is also None
471
 
            context.currentsubpath = self._sarc()
472
 
 
473
 
        context.currentpoint = self._earc()
474
 
 
475
 
    def _bbox(self, context):
476
 
        phi1 = radians(self.angle1)
477
 
        phi2 = radians(self.angle2)
478
 
 
479
 
        # starting end end point of arc segment
480
 
        sarcx_pt, sarcy_pt = self._sarc()
481
 
        earcx_pt, earcy_pt = self._earc()
482
 
 
483
 
        # Now, we have to determine the corners of the bbox for the
484
 
        # arc segment, i.e. global maxima/mimima of cos(phi) and sin(phi)
485
 
        # in the interval [phi1, phi2]. These can either be located
486
 
        # on the borders of this interval or in the interior.
487
 
 
488
 
        if phi2 < phi1:
489
 
            # guarantee that phi2>phi1
490
 
            phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
491
 
 
492
 
        # next minimum of cos(phi) looking from phi1 in counterclockwise
493
 
        # direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
494
 
 
495
 
        if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
496
 
            minarcx_pt = min(sarcx_pt, earcx_pt)
497
 
        else:
498
 
            minarcx_pt = self.x_pt-self.r_pt
499
 
 
500
 
        # next minimum of sin(phi) looking from phi1 in counterclockwise
501
 
        # direction: 2*pi*floor((phi1-3*pi/2)/(2*pi)) + 7/2*pi
502
 
 
503
 
        if phi2 < (2*math.floor((phi1-3.0*pi/2)/(2*pi))+7.0/2)*pi:
504
 
            minarcy_pt = min(sarcy_pt, earcy_pt)
505
 
        else:
506
 
            minarcy_pt = self.y_pt-self.r_pt
507
 
 
508
 
        # next maximum of cos(phi) looking from phi1 in counterclockwise 
509
 
        # direction: 2*pi*floor((phi1)/(2*pi))+2*pi
510
 
 
511
 
        if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
512
 
            maxarcx_pt = max(sarcx_pt, earcx_pt)
513
 
        else:
514
 
            maxarcx_pt = self.x_pt+self.r_pt
515
 
 
516
 
        # next maximum of sin(phi) looking from phi1 in counterclockwise 
517
 
        # direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
518
 
 
519
 
        if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
520
 
            maxarcy_pt = max(sarcy_pt, earcy_pt)
521
 
        else:
522
 
            maxarcy_pt = self.y_pt+self.r_pt
523
 
 
524
 
        # Finally, we are able to construct the bbox for the arc segment.
525
 
        # Note that if there is a currentpoint defined, we also
526
 
        # have to include the straight line from this point
527
 
        # to the first point of the arc segment
528
 
 
529
 
        if context.currentpoint:
530
 
            return (bbox.bbox_pt(min(context.currentpoint[0], sarcx_pt),
531
 
                               min(context.currentpoint[1], sarcy_pt),
532
 
                               max(context.currentpoint[0], sarcx_pt),
533
 
                               max(context.currentpoint[1], sarcy_pt)) +
534
 
                    bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt)
535
 
                    )
536
 
        else:
537
 
            return  bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt)
538
 
 
539
 
    def _normalized(self, context):
540
 
        # get starting and end point of arc segment and bpath corresponding to arc
541
 
        sarcx_pt, sarcy_pt = self._sarc()
542
 
        earcx_pt, earcy_pt = self._earc()
543
 
        barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2)
544
 
 
545
 
        # convert to list of curvetos omitting movetos
546
 
        nbarc = []
547
 
 
548
 
        for bpathitem in barc:
549
 
            nbarc.append(normcurve(bpathitem.x0_pt, bpathitem.y0_pt,
550
 
                                   bpathitem.x1_pt, bpathitem.y1_pt,
551
 
                                   bpathitem.x2_pt, bpathitem.y2_pt,
552
 
                                   bpathitem.x3_pt, bpathitem.y3_pt))
553
 
 
554
 
        # Note that if there is a currentpoint defined, we also
555
 
        # have to include the straight line from this point
556
 
        # to the first point of the arc segment.
557
 
        # Otherwise, we have to add a moveto at the beginning
558
 
        if context.currentpoint:
559
 
            return [normline(context.currentpoint[0], context.currentpoint[1], sarcx_pt, sarcy_pt)] + nbarc
560
 
        else:
561
 
            return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
562
 
 
563
 
    def outputPS(self, file):
564
 
        file.write("%g %g %g %g %g arc\n" % ( self.x_pt, self.y_pt,
565
 
                                            self.r_pt,
566
 
                                            self.angle1,
567
 
                                            self.angle2 ) )
 
567
    def __str__(self):
 
568
        return "arc_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
 
569
                                               self.angle1, self.angle2)
 
570
 
 
571
    def createcontext(self):
 
572
        x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
573
        return context(x_pt, y_pt, x_pt, y_pt)
 
574
 
 
575
    def createbbox(self):
 
576
        return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
 
577
                                                self.angle1, self.angle2))
 
578
 
 
579
    def createnormpath(self, epsilon=_marker):
 
580
        if epsilon is _marker:
 
581
            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))])
 
582
        else:
 
583
            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2),
 
584
                                         epsilon=epsilon)])
 
585
 
 
586
    def updatebbox(self, bbox, context):
 
587
        minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
 
588
                                                                      self.angle1, self.angle2)
 
589
        bbox.includepoint_pt(minarcx_pt, minarcy_pt)
 
590
        bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
 
591
        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
592
 
 
593
    def updatenormpath(self, normpath, context):
 
594
        if normpath.normsubpaths[-1].closed:
 
595
            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
 
596
                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
 
597
                                        epsilon=normpath.normsubpaths[-1].epsilon))
 
598
        else:
 
599
            normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
 
600
                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
 
601
        normpath.normsubpaths[-1].extend(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle1, self.angle2))
 
602
        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
603
 
 
604
    def outputPS(self, file, writer):
 
605
        file.write("%g %g %g %g %g arc\n" % (self.x_pt, self.y_pt,
 
606
                                             self.r_pt,
 
607
                                             self.angle1,
 
608
                                             self.angle2))
568
609
 
569
610
 
570
611
class arcn_pt(pathitem):
580
621
        self.angle1 = angle1
581
622
        self.angle2 = angle2
582
623
 
583
 
    def _sarc(self):
584
 
        """Return starting point of arc segment"""
585
 
        return (self.x_pt+self.r_pt*cos(radians(self.angle1)),
586
 
                self.y_pt+self.r_pt*sin(radians(self.angle1)))
587
 
 
588
 
    def _earc(self):
589
 
        """Return end point of arc segment"""
590
 
        return (self.x_pt+self.r_pt*cos(radians(self.angle2)),
591
 
                self.y_pt+self.r_pt*sin(radians(self.angle2)))
592
 
 
593
 
    def _updatecontext(self, context):
594
 
        if context.currentpoint:
595
 
            context.currentsubpath = context.currentsubpath or context.currentpoint
596
 
        else:  # we assert that currentsubpath is also None
597
 
            context.currentsubpath = self._sarc()
598
 
 
599
 
        context.currentpoint = self._earc()
600
 
 
601
 
    def _bbox(self, context):
602
 
        # in principle, we obtain bbox of an arcn element from 
603
 
        # the bounding box of the corrsponding arc element with
604
 
        # angle1 and angle2 interchanged. Though, we have to be carefull
605
 
        # with the straight line segment, which is added if currentpoint 
606
 
        # is defined.
607
 
 
608
 
        # Hence, we first compute the bbox of the arc without this line:
609
 
 
610
 
        a = arc_pt(self.x_pt, self.y_pt, self.r_pt, 
611
 
                 self.angle2, 
612
 
                 self.angle1)
613
 
 
614
 
        sarcx_pt, sarcy_pt = self._sarc()
615
 
        arcbb = a._bbox(_pathcontext())
616
 
 
617
 
        # Then, we repeat the logic from arc.bbox, but with interchanged
618
 
        # start and end points of the arc
619
 
 
620
 
        if context.currentpoint:
621
 
            return  bbox.bbox_pt(min(context.currentpoint[0], sarcx_pt),
622
 
                               min(context.currentpoint[1], sarcy_pt),
623
 
                               max(context.currentpoint[0], sarcx_pt),
624
 
                               max(context.currentpoint[1], sarcy_pt))+ arcbb
625
 
        else:
626
 
            return arcbb
627
 
 
628
 
    def _normalized(self, context):
629
 
        # get starting and end point of arc segment and bpath corresponding to arc
630
 
        sarcx_pt, sarcy_pt = self._sarc()
631
 
        earcx_pt, earcy_pt = self._earc()
632
 
        barc = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
633
 
        barc.reverse()
634
 
 
635
 
        # convert to list of curvetos omitting movetos
636
 
        nbarc = []
637
 
 
638
 
        for bpathitem in barc:
639
 
            nbarc.append(normcurve(bpathitem.x3_pt, bpathitem.y3_pt,
640
 
                                   bpathitem.x2_pt, bpathitem.y2_pt,
641
 
                                   bpathitem.x1_pt, bpathitem.y1_pt,
642
 
                                   bpathitem.x0_pt, bpathitem.y0_pt))
643
 
 
644
 
        # Note that if there is a currentpoint defined, we also
645
 
        # have to include the straight line from this point
646
 
        # to the first point of the arc segment.
647
 
        # Otherwise, we have to add a moveto at the beginning
648
 
        if context.currentpoint:
649
 
            return [normline(context.currentpoint[0], context.currentpoint[1], sarcx_pt, sarcy_pt)] + nbarc
650
 
        else:
651
 
            return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
652
 
 
653
 
 
654
 
    def outputPS(self, file):
655
 
        file.write("%g %g %g %g %g arcn\n" % ( self.x_pt, self.y_pt,
656
 
                                               self.r_pt,
657
 
                                               self.angle1,
658
 
                                               self.angle2 ) )
 
624
    def __str__(self):
 
625
        return "arcn_pt(%g, %g, %g, %g, %g)" % (self.x_pt, self.y_pt, self.r_pt,
 
626
                                                self.angle1, self.angle2)
 
627
 
 
628
    def createcontext(self):
 
629
        x_pt, y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
630
        return context(x_pt, y_pt, x_pt, y_pt)
 
631
 
 
632
    def createbbox(self):
 
633
        return bboxmodule.bbox_pt(*_arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
 
634
                                                self.angle2, self.angle1))
 
635
 
 
636
    def createnormpath(self, epsilon=_marker):
 
637
        if epsilon is _marker:
 
638
            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1))]).reversed()
 
639
        else:
 
640
            return normpath([normsubpath(_arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1),
 
641
                                         epsilon=epsilon)]).reversed()
 
642
 
 
643
    def updatebbox(self, bbox, context):
 
644
        minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt = _arcbboxdata(self.x_pt, self.y_pt, self.r_pt,
 
645
                                                                      self.angle2, self.angle1)
 
646
        bbox.includepoint_pt(minarcx_pt, minarcy_pt)
 
647
        bbox.includepoint_pt(maxarcx_pt, maxarcy_pt)
 
648
        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
649
 
 
650
    def updatenormpath(self, normpath, context):
 
651
        if normpath.normsubpaths[-1].closed:
 
652
            normpath.append(normsubpath([normline_pt(context.x_pt, context.y_pt,
 
653
                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1))],
 
654
                                        epsilon=normpath.normsubpaths[-1].epsilon))
 
655
        else:
 
656
            normpath.normsubpaths[-1].append(normline_pt(context.x_pt, context.y_pt,
 
657
                                                         *_arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle1)))
 
658
        bpathitems = _arctobezierpath(self.x_pt, self.y_pt, self.r_pt, self.angle2, self.angle1)
 
659
        bpathitems.reverse()
 
660
        for bpathitem in bpathitems:
 
661
            normpath.normsubpaths[-1].append(bpathitem.reversed())
 
662
        context.x_pt, context.y_pt = _arcpoint(self.x_pt, self.y_pt, self.r_pt, self.angle2)
 
663
 
 
664
    def outputPS(self, file, writer):
 
665
        file.write("%g %g %g %g %g arcn\n" % (self.x_pt, self.y_pt,
 
666
                                              self.r_pt,
 
667
                                              self.angle1,
 
668
                                              self.angle2))
659
669
 
660
670
 
661
671
class arct_pt(pathitem):
671
681
        self.y2_pt = y2_pt
672
682
        self.r_pt = r_pt
673
683
 
674
 
    def _path(self, currentpoint, currentsubpath):
675
 
        """returns new currentpoint, currentsubpath and path consisting
676
 
        of arc and/or line which corresponds to arct
677
 
 
678
 
        this is a helper routine for _bbox and _normalized, which both need
679
 
        this path. Note: we don't want to calculate the bbox from a bpath
680
 
 
 
684
    def __str__(self):
 
685
        return "arct_pt(%g, %g, %g, %g, %g)" % (self.x1_pt, self.y1_pt,
 
686
                                                self.x2_pt, self.y2_pt,
 
687
                                                self.r_pt)
 
688
 
 
689
    def _pathitems(self, x_pt, y_pt):
 
690
        """return pathitems corresponding to arct for given currentpoint x_pt, y_pt.
 
691
 
 
692
        The return is a list containing line_pt, arc_pt, a arcn_pt instances.
 
693
 
 
694
        This is a helper routine for updatebbox and updatenormpath,
 
695
        which will delegate the work to the constructed pathitem.
681
696
        """
682
697
 
683
 
        # direction and length of tangent 1
684
 
        dx1_pt = currentpoint[0]-self.x1_pt
685
 
        dy1_pt = currentpoint[1]-self.y1_pt
686
 
        l1 = math.hypot(dx1_pt, dy1_pt)
687
 
 
688
 
        # direction and length of tangent 2
689
 
        dx2_pt = self.x2_pt-self.x1_pt
690
 
        dy2_pt = self.y2_pt-self.y1_pt
691
 
        l2 = math.hypot(dx2_pt, dy2_pt)
692
 
 
693
 
        # intersection angle between two tangents
694
 
        alpha = math.acos((dx1_pt*dx2_pt+dy1_pt*dy2_pt)/(l1*l2))
695
 
 
696
 
        if math.fabs(sin(alpha)) >= 1e-15 and 1.0+self.r_pt != 1.0:
697
 
            cotalpha2 = 1.0/math.tan(alpha/2)
698
 
 
 
698
        # direction of tangent 1
 
699
        dx1_pt, dy1_pt = self.x1_pt-x_pt, self.y1_pt-y_pt
 
700
        l1_pt = math.hypot(dx1_pt, dy1_pt)
 
701
        dx1, dy1 = dx1_pt/l1_pt, dy1_pt/l1_pt
 
702
 
 
703
        # direction of tangent 2
 
704
        dx2_pt, dy2_pt = self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt
 
705
        l2_pt = math.hypot(dx2_pt, dy2_pt)
 
706
        dx2, dy2 = dx2_pt/l2_pt, dy2_pt/l2_pt
 
707
 
 
708
        # intersection angle between two tangents in the range (-pi, pi).
 
709
        # We take the orientation from the sign of the vector product.
 
710
        # Negative (positive) angles alpha corresponds to a turn to the right (left)
 
711
        # as seen from currentpoint.
 
712
        if dx1*dy2-dy1*dx2 > 0:
 
713
            alpha = acos(dx1*dx2+dy1*dy2) 
 
714
        else:
 
715
            alpha = -acos(dx1*dx2+dy1*dy2) 
 
716
 
 
717
        try:
699
718
            # two tangent points
700
 
            xt1_pt = self.x1_pt + dx1_pt*self.r_pt*cotalpha2/l1
701
 
            yt1_pt = self.y1_pt + dy1_pt*self.r_pt*cotalpha2/l1
702
 
            xt2_pt = self.x1_pt + dx2_pt*self.r_pt*cotalpha2/l2
703
 
            yt2_pt = self.y1_pt + dy2_pt*self.r_pt*cotalpha2/l2
704
 
 
705
 
            # direction of center of arc 
706
 
            rx_pt = self.x1_pt - 0.5*(xt1_pt+xt2_pt)
707
 
            ry_pt = self.y1_pt - 0.5*(yt1_pt+yt2_pt)
708
 
            lr = math.hypot(rx_pt, ry_pt)
 
719
            xt1_pt = self.x1_pt - dx1*self.r_pt*tan(abs(alpha)/2)
 
720
            yt1_pt = self.y1_pt - dy1*self.r_pt*tan(abs(alpha)/2)
 
721
            xt2_pt = self.x1_pt + dx2*self.r_pt*tan(abs(alpha)/2)
 
722
            yt2_pt = self.y1_pt + dy2*self.r_pt*tan(abs(alpha)/2)
 
723
 
 
724
            # direction point 1 -> center of arc
 
725
            dmx_pt = 0.5*(xt1_pt+xt2_pt) - self.x1_pt
 
726
            dmy_pt = 0.5*(yt1_pt+yt2_pt) - self.y1_pt
 
727
            lm_pt = math.hypot(dmx_pt, dmy_pt)
 
728
            dmx, dmy = dmx_pt/lm_pt, dmy_pt/lm_pt
 
729
 
 
730
            # center of arc
 
731
            mx_pt = self.x1_pt + dmx*self.r_pt/cos(alpha/2)
 
732
            my_pt = self.y1_pt + dmy*self.r_pt/cos(alpha/2)
709
733
 
710
734
            # angle around which arc is centered
711
 
            if rx_pt >= 0:
712
 
                phi = degrees(math.atan2(ry_pt, rx_pt))
713
 
            else:
714
 
                # XXX why is rx_pt/ry_pt and not ry_pt/rx_pt used??? 
715
 
                phi = degrees(math.atan(rx_pt/ry_pt))+180
716
 
 
717
 
            # half angular width of arc 
718
 
            deltaphi = 90*(1-alpha/pi)
719
 
 
720
 
            # center position of arc
721
 
            mx_pt = self.x1_pt - rx_pt*self.r_pt/(lr*sin(alpha/2))
722
 
            my_pt = self.y1_pt - ry_pt*self.r_pt/(lr*sin(alpha/2))
723
 
 
724
 
            # now we are in the position to construct the path
725
 
            p = path(moveto_pt(*currentpoint))
726
 
 
727
 
            if phi<0:
728
 
                p.append(arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi))
729
 
            else:
730
 
                p.append(arcn_pt(mx_pt, my_pt, self.r_pt, phi+deltaphi, phi-deltaphi))
731
 
 
732
 
            return ( (xt2_pt, yt2_pt),
733
 
                     currentsubpath or (xt2_pt, yt2_pt),
734
 
                     p )
735
 
 
736
 
        else:
737
 
            # we need no arc, so just return a straight line to currentpoint to x1_pt, y1_pt
738
 
            return  ( (self.x1_pt, self.y1_pt),
739
 
                      currentsubpath or (self.x1_pt, self.y1_pt),
740
 
                      line_pt(currentpoint[0], currentpoint[1], self.x1_pt, self.y1_pt) )
741
 
 
742
 
    def _updatecontext(self, context):
743
 
        result = self._path(context.currentpoint, context.currentsubpath)
744
 
        context.currentpoint, context.currentsubpath = result[:2]
745
 
 
746
 
    def _bbox(self, context):
747
 
        return self._path(context.currentpoint, context.currentsubpath)[2].bbox()
748
 
 
749
 
    def _normalized(self, context):
750
 
        # XXX TODO NOTE
751
 
        return self._path(context.currentpoint,
752
 
                          context.currentsubpath)[2].normpath().normsubpaths[0].normsubpathitems
753
 
    def outputPS(self, file):
754
 
        file.write("%g %g %g %g %g arct\n" % ( self.x1_pt, self.y1_pt,
755
 
                                               self.x2_pt, self.y2_pt,
756
 
                                               self.r_pt ) )
 
735
            phi = degrees(math.atan2(-dmy, -dmx))
 
736
 
 
737
            # half angular width of arc
 
738
            deltaphi = degrees(alpha)/2
 
739
 
 
740
            line = lineto_pt(*_arcpoint(mx_pt, my_pt, self.r_pt, phi-deltaphi))
 
741
            if alpha > 0:
 
742
                return [line, arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
 
743
            else:
 
744
                return [line, arcn_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi)]
 
745
 
 
746
        except ZeroDivisionError:
 
747
            # in the degenerate case, we just return a line as specified by the PS 
 
748
            # language reference
 
749
            return [lineto_pt(self.x1_pt, self.y1_pt)]
 
750
 
 
751
    def updatebbox(self, bbox, context):
 
752
        for pathitem in self._pathitems(context.x_pt, context.y_pt):
 
753
            pathitem.updatebbox(bbox, context)
 
754
 
 
755
    def updatenormpath(self, normpath, context):
 
756
        for pathitem in self._pathitems(context.x_pt, context.y_pt):
 
757
            pathitem.updatenormpath(normpath, context)
 
758
 
 
759
    def outputPS(self, file, writer):
 
760
        file.write("%g %g %g %g %g arct\n" % (self.x1_pt, self.y1_pt,
 
761
                                              self.x2_pt, self.y2_pt,
 
762
                                              self.r_pt))
757
763
 
758
764
#
759
765
# now the pathitems that convert from user coordinates to pts
778
784
    def __init__(self, x, y):
779
785
        lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
780
786
 
781
 
        
 
787
 
782
788
class curveto(curveto_pt):
783
789
 
784
790
    """Append curveto"""
848
854
 
849
855
    """Append tangent arc"""
850
856
 
851
 
    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r"
 
857
    __slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
852
858
 
853
859
    def __init__(self, x1, y1, x2, y2, r):
854
860
        arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
867
873
    def __init__(self, points_pt):
868
874
        self.points_pt = points_pt
869
875
 
870
 
    def _updatecontext(self, context):
871
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
872
 
        context.currentpoint = self.points_pt[-1]
873
 
 
874
 
    def _bbox(self, context):
875
 
        xs_pt = [point[0] for point in self.points_pt]
876
 
        ys_pt = [point[1] for point in self.points_pt]
877
 
        return bbox.bbox_pt(min(context.currentpoint[0], *xs_pt),
878
 
                          min(context.currentpoint[1], *ys_pt),
879
 
                          max(context.currentpoint[0], *xs_pt),
880
 
                          max(context.currentpoint[1], *ys_pt))
881
 
 
882
 
    def _normalized(self, context):
 
876
    def __str__(self):
883
877
        result = []
884
 
        x0_pt, y0_pt = context.currentpoint
885
 
        for x_pt, y_pt in self.points_pt:
886
 
            result.append(normline(x0_pt, y0_pt, x_pt, y_pt))
887
 
            x0_pt, y0_pt = x_pt, y_pt
888
 
        return result
889
 
 
890
 
    def outputPS(self, file):
 
878
        for point_pt in self.points_pt:
 
879
            result.append("(%g, %g)" % point_pt )
 
880
        return "multilineto_pt([%s])" % (", ".join(result))
 
881
 
 
882
    def updatebbox(self, bbox, context):
 
883
        for point_pt in self.points_pt:
 
884
            bbox.includepoint_pt(*point_pt)
 
885
        if self.points_pt:
 
886
            context.x_pt, context.y_pt = self.points_pt[-1]
 
887
 
 
888
    def updatenormpath(self, normpath, context):
 
889
        x0_pt, y0_pt = context.x_pt, context.y_pt
 
890
        for point_pt in self.points_pt:
 
891
            normpath.normsubpaths[-1].append(normline_pt(x0_pt, y0_pt, *point_pt))
 
892
            x0_pt, y0_pt = point_pt
 
893
        context.x_pt, context.y_pt = x0_pt, y0_pt
 
894
 
 
895
    def outputPS(self, file, writer):
891
896
        for point_pt in self.points_pt:
892
897
            file.write("%g %g lineto\n" % point_pt )
893
898
 
894
 
    def outputPDF(self, file):
895
 
        for point_pt in self.points_pt:
896
 
            file.write("%f %f l\n" % point_pt )
897
 
 
898
899
 
899
900
class multicurveto_pt(pathitem):
900
901
 
905
906
    def __init__(self, points_pt):
906
907
        self.points_pt = points_pt
907
908
 
908
 
    def _updatecontext(self, context):
909
 
        context.currentsubpath = context.currentsubpath or context.currentpoint
910
 
        context.currentpoint = self.points_pt[-1]
911
 
 
912
 
    def _bbox(self, context):
913
 
        xs = ( [point[0] for point in self.points_pt] +
914
 
               [point[2] for point in self.points_pt] +
915
 
               [point[4] for point in self.points_pt] )
916
 
        ys = ( [point[1] for point in self.points_pt] +
917
 
               [point[3] for point in self.points_pt] +
918
 
               [point[5] for point in self.points_pt] )
919
 
        return bbox.bbox_pt(min(context.currentpoint[0], *xs_pt),
920
 
                          min(context.currentpoint[1], *ys_pt),
921
 
                          max(context.currentpoint[0], *xs_pt),
922
 
                          max(context.currentpoint[1], *ys_pt))
923
 
 
924
 
    def _normalized(self, context):
 
909
    def __str__(self):
925
910
        result = []
926
 
        x0_pt, y0_pt = context.currentpoint
927
 
        for point_pt in self.points_pt:
928
 
            result.append(normcurve(x0_pt, y0_pt, *point_pt))
 
911
        for point_pt in self.points_pt:
 
912
            result.append("(%g, %g, %g, %g, %g, %g)" % point_pt )
 
913
        return "multicurveto_pt([%s])" % (", ".join(result))
 
914
 
 
915
    def updatebbox(self, bbox, context):
 
916
        for point_pt in self.points_pt:
 
917
            bbox.includepoint_pt(*point_pt[0: 2])
 
918
            bbox.includepoint_pt(*point_pt[2: 4])
 
919
            bbox.includepoint_pt(*point_pt[4: 6])
 
920
        if self.points_pt:
 
921
            context.x_pt, context.y_pt = self.points_pt[-1][4:]
 
922
 
 
923
    def updatenormpath(self, normpath, context):
 
924
        x0_pt, y0_pt = context.x_pt, context.y_pt
 
925
        for point_pt in self.points_pt:
 
926
            normpath.normsubpaths[-1].append(normcurve_pt(x0_pt, y0_pt, *point_pt))
929
927
            x0_pt, y0_pt = point_pt[4:]
930
 
        return result
 
928
        context.x_pt, context.y_pt = x0_pt, y0_pt
931
929
 
932
 
    def outputPS(self, file):
 
930
    def outputPS(self, file, writer):
933
931
        for point_pt in self.points_pt:
934
932
            file.write("%g %g %g %g %g %g curveto\n" % point_pt)
935
 
            
936
 
    def outputPDF(self, file):
937
 
        for point_pt in self.points_pt:
938
 
            file.write("%f %f %f %f %f %f c\n" % point_pt)
939
933
 
940
934
 
941
935
################################################################################
942
936
# path: PS style path
943
937
################################################################################
944
938
 
945
 
class path(base.canvasitem):
 
939
class path:
946
940
 
947
941
    """PS style path"""
948
942
 
949
 
    __slots__ = "path"
950
 
 
951
 
    def __init__(self, *args):
952
 
        if len(args)==1 and isinstance(args[0], path):
953
 
            self.path = args[0].path
954
 
        else:
955
 
            self.path = list(args)
 
943
    __slots__ = "pathitems", "_normpath"
 
944
 
 
945
    def __init__(self, *pathitems):
 
946
        """construct a path from pathitems *args"""
 
947
 
 
948
        for apathitem in pathitems:
 
949
            assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
 
950
 
 
951
        self.pathitems = list(pathitems)
 
952
        # normpath cache (when no epsilon is set)
 
953
        self._normpath = None
956
954
 
957
955
    def __add__(self, other):
958
 
        return path(*(self.path+other.path))
 
956
        """create new path out of self and other"""
 
957
        return path(*(self.pathitems + other.path().pathitems))
959
958
 
960
959
    def __iadd__(self, other):
961
 
        self.path += other.path
 
960
        """add other inplace
 
961
 
 
962
        If other is a normpath instance, it is converted to a path before
 
963
        being added.
 
964
        """
 
965
        self.pathitems += other.path().pathitems
 
966
        self._normpath = None
962
967
        return self
963
968
 
964
969
    def __getitem__(self, i):
965
 
        return self.path[i]
 
970
        """return path item i"""
 
971
        return self.pathitems[i]
966
972
 
967
973
    def __len__(self):
968
 
        return len(self.path)
969
 
 
970
 
    def append(self, pathitem):
971
 
        self.path.append(pathitem)
 
974
        """return the number of path items"""
 
975
        return len(self.pathitems)
 
976
 
 
977
    def __str__(self):
 
978
        l = ", ".join(map(str, self.pathitems))
 
979
        return "path(%s)" % l
 
980
 
 
981
    def append(self, apathitem):
 
982
        """append a path item"""
 
983
        assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
 
984
        self.pathitems.append(apathitem)
 
985
        self._normpath = None
972
986
 
973
987
    def arclen_pt(self):
974
 
        """returns total arc length of path in pts"""
 
988
        """return arc length in pts"""
975
989
        return self.normpath().arclen_pt()
976
990
 
977
991
    def arclen(self):
978
 
        """returns total arc length of path"""
 
992
        """return arc length"""
979
993
        return self.normpath().arclen()
980
994
 
 
995
    def arclentoparam_pt(self, lengths_pt):
 
996
        """return the param(s) matching the given length(s)_pt in pts"""
 
997
        return self.normpath().arclentoparam_pt(lengths_pt)
 
998
 
981
999
    def arclentoparam(self, lengths):
982
 
        """returns the parameter value(s) matching the given length(s)"""
 
1000
        """return the param(s) matching the given length(s)"""
983
1001
        return self.normpath().arclentoparam(lengths)
984
1002
 
985
 
    def at_pt(self, param=None, arclen=None):
986
 
        """return coordinates of path in pts at either parameter value param
987
 
        or arc length arclen.
988
 
 
989
 
        At discontinuities in the path, the limit from below is returned
990
 
        """
991
 
        return self.normpath().at_pt(param, arclen)
992
 
 
993
 
    def at(self, param=None, arclen=None):
994
 
        """return coordinates of path at either parameter value param
995
 
        or arc length arclen.
996
 
 
997
 
        At discontinuities in the path, the limit from below is returned
998
 
        """
999
 
        return self.normpath().at(param, arclen)
 
1003
    def at_pt(self, params):
 
1004
        """return coordinates of path in pts at param(s) or arc length(s) in pts"""
 
1005
        return self.normpath().at_pt(params)
 
1006
 
 
1007
    def at(self, params):
 
1008
        """return coordinates of path at param(s) or arc length(s)"""
 
1009
        return self.normpath().at(params)
 
1010
 
 
1011
    def atbegin_pt(self):
 
1012
        """return coordinates of the beginning of first subpath in path in pts"""
 
1013
        return self.normpath().atbegin_pt()
 
1014
 
 
1015
    def atbegin(self):
 
1016
        """return coordinates of the beginning of first subpath in path"""
 
1017
        return self.normpath().atbegin()
 
1018
 
 
1019
    def atend_pt(self):
 
1020
        """return coordinates of the end of last subpath in path in pts"""
 
1021
        return self.normpath().atend_pt()
 
1022
 
 
1023
    def atend(self):
 
1024
        """return coordinates of the end of last subpath in path"""
 
1025
        return self.normpath().atend()
1000
1026
 
1001
1027
    def bbox(self):
1002
 
        context = _pathcontext()
1003
 
        abbox = None
1004
 
 
1005
 
        for pitem in self.path:
1006
 
            nbbox =  pitem._bbox(context)
1007
 
            pitem._updatecontext(context)
1008
 
            if abbox is None:
1009
 
                abbox = nbbox
1010
 
            elif nbbox: 
1011
 
                abbox += nbbox
1012
 
 
1013
 
        return abbox
1014
 
 
1015
 
    def begin_pt(self):
1016
 
        """return coordinates of first point of first subpath in path (in pts)"""
1017
 
        return self.normpath().begin_pt()
 
1028
        """return bbox of path"""
 
1029
        if self.pathitems:
 
1030
            bbox = self.pathitems[0].createbbox()
 
1031
            context = self.pathitems[0].createcontext()
 
1032
            for pathitem in self.pathitems[1:]:
 
1033
                pathitem.updatebbox(bbox, context)
 
1034
            return bbox
 
1035
        else:
 
1036
            return bboxmodule.empty()
1018
1037
 
1019
1038
    def begin(self):
1020
 
        """return coordinates of first point of first subpath in path"""
 
1039
        """return param corresponding of the beginning of the path"""
1021
1040
        return self.normpath().begin()
1022
1041
 
1023
 
    def curvradius_pt(self, param=None, arclen=None):
1024
 
        """Returns the curvature radius in pts (or None if infinite)
1025
 
        at parameter param or arc length arclen.  This is the inverse
1026
 
        of the curvature at this parameter
1027
 
 
1028
 
        Please note that this radius can be negative or positive,
1029
 
        depending on the sign of the curvature"""
1030
 
        return self.normpath().curvradius_pt(param, arclen)
1031
 
 
1032
 
    def curvradius(self, param=None, arclen=None):
1033
 
        """Returns the curvature radius (or None if infinite) at
1034
 
        parameter param or arc length arclen.  This is the inverse of
1035
 
        the curvature at this parameter
1036
 
 
1037
 
        Please note that this radius can be negative or positive,
1038
 
        depending on the sign of the curvature"""
1039
 
        return self.normpath().curvradius(param, arclen)
1040
 
 
1041
 
    def end_pt(self):
1042
 
        """return coordinates of last point of last subpath in path (in pts)"""
1043
 
        return self.normpath().end_pt()
 
1042
    def curveradius_pt(self, params):
 
1043
        """return the curvature radius in pts at param(s) or arc length(s) in pts
 
1044
 
 
1045
        The curvature radius is the inverse of the curvature. When the
 
1046
        curvature is 0, None is returned. Note that this radius can be negative
 
1047
        or positive, depending on the sign of the curvature."""
 
1048
        return self.normpath().curveradius_pt(params)
 
1049
 
 
1050
    def curveradius(self, params):
 
1051
        """return the curvature radius at param(s) or arc length(s)
 
1052
 
 
1053
        The curvature radius is the inverse of the curvature. When the
 
1054
        curvature is 0, None is returned. Note that this radius can be negative
 
1055
        or positive, depending on the sign of the curvature."""
 
1056
        return self.normpath().curveradius(params)
1044
1057
 
1045
1058
    def end(self):
1046
 
        """return coordinates of last point of last subpath in path"""
 
1059
        """return param corresponding of the end of the path"""
1047
1060
        return self.normpath().end()
1048
1061
 
1049
1062
    def extend(self, pathitems):
1050
 
        self.path.extend(pathitems)
 
1063
        """extend path by pathitems"""
 
1064
        for apathitem in pathitems:
 
1065
            assert isinstance(apathitem, pathitem), "only pathitem instance allowed"
 
1066
        self.pathitems.extend(pathitems)
 
1067
        self._normpath = None
 
1068
 
 
1069
    def intersect(self, other):
 
1070
        """intersect self with other path
 
1071
 
 
1072
        Returns a tuple of lists consisting of the parameter values
 
1073
        of the intersection points of the corresponding normpath.
 
1074
        """
 
1075
        return self.normpath().intersect(other)
 
1076
 
 
1077
    def join(self, other):
 
1078
        """join other path/normpath inplace
 
1079
 
 
1080
        If other is a normpath instance, it is converted to a path before
 
1081
        being joined.
 
1082
        """
 
1083
        self.pathitems = self.joined(other).path().pathitems
 
1084
        self._normpath = None
 
1085
        return self
1051
1086
 
1052
1087
    def joined(self, other):
1053
1088
        """return path consisting of self and other joined together"""
1054
 
        return self.normpath().joined(other)
 
1089
        return self.normpath().joined(other).path()
1055
1090
 
1056
1091
    # << operator also designates joining
1057
1092
    __lshift__ = joined
1058
1093
 
1059
 
    def intersect(self, other):
1060
 
        """intersect normpath corresponding to self with other path"""
1061
 
        return self.normpath().intersect(other)
1062
 
 
1063
 
    def normpath(self, epsilon=None):
1064
 
        """converts the path into a normpath"""
1065
 
        # use global epsilon if it is has not been specified
1066
 
        if epsilon is None:
1067
 
            epsilon = _epsilon
1068
 
        # split path in sub paths
1069
 
        subpaths = []
1070
 
        currentsubpathitems = []
1071
 
        context = _pathcontext()
1072
 
        for pitem in self.path:
1073
 
            for npitem in pitem._normalized(context):
1074
 
                if isinstance(npitem, moveto_pt):
1075
 
                    if currentsubpathitems:
1076
 
                        # append open sub path
1077
 
                        subpaths.append(normsubpath(currentsubpathitems, closed=0, epsilon=epsilon))
1078
 
                    # start new sub path
1079
 
                    currentsubpathitems = []
1080
 
                elif isinstance(npitem, closepath):
1081
 
                    if currentsubpathitems:
1082
 
                        # append closed sub path
1083
 
                        currentsubpathitems.append(normline(context.currentpoint[0], context.currentpoint[1],
1084
 
                                                            context.currentsubpath[0], context.currentsubpath[1]))
1085
 
                    subpaths.append(normsubpath(currentsubpathitems, closed=1, epsilon=epsilon))
1086
 
                    currentsubpathitems = []
1087
 
                else:
1088
 
                    currentsubpathitems.append(npitem)
1089
 
            pitem._updatecontext(context)
1090
 
 
1091
 
        if currentsubpathitems:
1092
 
            # append open sub path
1093
 
            subpaths.append(normsubpath(currentsubpathitems, 0, epsilon))
1094
 
        return normpath(subpaths)
1095
 
 
1096
 
    def range(self):
1097
 
        """return maximal value for parameter value t for corr. normpath"""
1098
 
        return self.normpath().range()
 
1094
    def normpath(self, epsilon=_marker):
 
1095
        """convert the path into a normpath"""
 
1096
        # use cached value if existent and epsilon is _marker
 
1097
        if self._normpath is not None and epsilon is _marker:
 
1098
            return self._normpath
 
1099
        if self.pathitems:
 
1100
            if epsilon is _marker:
 
1101
                normpath = self.pathitems[0].createnormpath()
 
1102
            else:
 
1103
                normpath = self.pathitems[0].createnormpath(epsilon)
 
1104
            context = self.pathitems[0].createcontext()
 
1105
            for pathitem in self.pathitems[1:]:
 
1106
                pathitem.updatenormpath(normpath, context)
 
1107
        else:
 
1108
            if epsilon is _marker:
 
1109
                normpath = normpath([])
 
1110
            else:
 
1111
                normpath = normpath(epsilon=epsilon)
 
1112
        if epsilon is _marker:
 
1113
            self._normpath = normpath
 
1114
        return normpath
 
1115
 
 
1116
    def paramtoarclen_pt(self, params):
 
1117
        """return arc lenght(s) in pts matching the given param(s)"""
 
1118
        return self.normpath().paramtoarclen_pt(params)
 
1119
 
 
1120
    def paramtoarclen(self, params):
 
1121
        """return arc lenght(s) matching the given param(s)"""
 
1122
        return self.normpath().paramtoarclen(params)
 
1123
 
 
1124
    def path(self):
 
1125
        """return corresponding path, i.e., self"""
 
1126
        return self
1099
1127
 
1100
1128
    def reversed(self):
1101
 
        """return reversed path"""
 
1129
        """return reversed normpath"""
 
1130
        # TODO: couldn't we try to return a path instead of converting it
 
1131
        #       to a normpath (but this might not be worth the trouble)
1102
1132
        return self.normpath().reversed()
1103
1133
 
 
1134
    def rotation_pt(self, params):
 
1135
        """return rotation at param(s) or arc length(s) in pts"""
 
1136
        return self.normpath().rotation(params)
 
1137
 
 
1138
    def rotation(self, params):
 
1139
        """return rotation at param(s) or arc length(s)"""
 
1140
        return self.normpath().rotation(params)
 
1141
 
 
1142
    def split_pt(self, params):
 
1143
        """split normpath at param(s) or arc length(s) in pts and return list of normpaths"""
 
1144
        return self.normpath().split(params)
 
1145
 
1104
1146
    def split(self, params):
1105
 
        """return corresponding normpaths split at parameter values params"""
 
1147
        """split normpath at param(s) or arc length(s) and return list of normpaths"""
1106
1148
        return self.normpath().split(params)
1107
1149
 
1108
 
    def tangent(self, param=None, arclen=None, length=None):
1109
 
        """return tangent vector of path at either parameter value param
1110
 
        or arc length arclen.
1111
 
 
1112
 
        At discontinuities in the path, the limit from below is returned.
 
1150
    def tangent_pt(self, params, length=None):
 
1151
        """return tangent vector of path at param(s) or arc length(s) in pts
 
1152
 
 
1153
        If length in pts is not None, the tangent vector will be scaled to
 
1154
        the desired length.
 
1155
        """
 
1156
        return self.normpath().tangent_pt(params, length)
 
1157
 
 
1158
    def tangent(self, params, length=None):
 
1159
        """return tangent vector of path at param(s) or arc length(s)
 
1160
 
1113
1161
        If length is not None, the tangent vector will be scaled to
1114
1162
        the desired length.
1115
1163
        """
1116
 
        return self.normpath().tangent(param, arclen, length)
1117
 
 
1118
 
    def trafo(self, param=None, arclen=None):
1119
 
        """return transformation at either parameter value param or arc length arclen"""
1120
 
        return self.normpath().trafo(param, arclen)
 
1164
        return self.normpath().tangent(params, length)
 
1165
 
 
1166
    def trafo_pt(self, params):
 
1167
        """return transformation at param(s) or arc length(s) in pts"""
 
1168
        return self.normpath().trafo(params)
 
1169
 
 
1170
    def trafo(self, params):
 
1171
        """return transformation at param(s) or arc length(s)"""
 
1172
        return self.normpath().trafo(params)
1121
1173
 
1122
1174
    def transformed(self, trafo):
1123
1175
        """return transformed path"""
1124
1176
        return self.normpath().transformed(trafo)
1125
1177
 
1126
 
    def outputPS(self, file):
1127
 
        if not (isinstance(self.path[0], moveto_pt) or
1128
 
                isinstance(self.path[0], arc_pt) or
1129
 
                isinstance(self.path[0], arcn_pt)):
1130
 
            raise PathException("first path element must be either moveto, arc, or arcn")
1131
 
        for pitem in self.path:
1132
 
            pitem.outputPS(file)
1133
 
 
1134
 
    def outputPDF(self, file):
1135
 
        if not (isinstance(self.path[0], moveto_pt) or
1136
 
                isinstance(self.path[0], arc_pt) or
1137
 
                isinstance(self.path[0], arcn_pt)):
1138
 
            raise PathException("first path element must be either moveto, arc, or arcn")
1139
 
        # PDF practically only supports normsubpathitems
1140
 
        context = _pathcontext()
1141
 
        for pitem in self.path:
1142
 
            for npitem in pitem._normalized(context):
1143
 
                npitem.outputPDF(file)
1144
 
            pitem._updatecontext(context)
1145
 
 
1146
 
################################################################################
 
1178
    def outputPS(self, file, writer):
 
1179
        """write PS code to file"""
 
1180
        for pitem in self.pathitems:
 
1181
            pitem.outputPS(file, writer)
 
1182
 
 
1183
    def outputPDF(self, file, writer):
 
1184
        """write PDF code to file"""
 
1185
        # PDF only supports normsubpathitems; we need to use a normpath
 
1186
        # with epsilon equals None to prevent failure for paths shorter
 
1187
        # than epsilon
 
1188
        self.normpath(epsilon=None).outputPDF(file, writer)
 
1189
 
 
1190
 
 
1191
#
1147
1192
# some special kinds of path, again in two variants
1148
 
################################################################################
 
1193
#
1149
1194
 
1150
1195
class line_pt(path):
1151
1196
 
1152
 
   """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) (coordinates in pts)"""
 
1197
    """straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) in pts"""
1153
1198
 
1154
 
   def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
1155
 
       path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
 
1199
    def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt):
 
1200
        path.__init__(self, moveto_pt(x1_pt, y1_pt), lineto_pt(x2_pt, y2_pt))
1156
1201
 
1157
1202
 
1158
1203
class curve_pt(path):
1159
1204
 
1160
 
   """Bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt)
1161
 
   (coordinates in pts)"""
 
1205
    """bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt) in pts"""
1162
1206
 
1163
 
   def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1164
 
       path.__init__(self,
1165
 
                     moveto_pt(x0_pt, y0_pt),
1166
 
                     curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
 
1207
    def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
 
1208
        path.__init__(self,
 
1209
                      moveto_pt(x0_pt, y0_pt),
 
1210
                      curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1167
1211
 
1168
1212
 
1169
1213
class rect_pt(path):
1170
1214
 
1171
 
   """rectangle at position (x,y) with width and height (coordinates in pts)"""
 
1215
    """rectangle at position (x_pt, y_pt) with width_pt and height_pt in pts"""
1172
1216
 
1173
 
   def __init__(self, x, y, width, height):
1174
 
       path.__init__(self, moveto_pt(x, y),
1175
 
                           lineto_pt(x+width, y),
1176
 
                           lineto_pt(x+width, y+height),
1177
 
                           lineto_pt(x, y+height),
1178
 
                           closepath())
 
1217
    def __init__(self, x_pt, y_pt, width_pt, height_pt):
 
1218
        path.__init__(self, moveto_pt(x_pt, y_pt),
 
1219
                            lineto_pt(x_pt+width_pt, y_pt),
 
1220
                            lineto_pt(x_pt+width_pt, y_pt+height_pt),
 
1221
                            lineto_pt(x_pt, y_pt+height_pt),
 
1222
                            closepath())
1179
1223
 
1180
1224
 
1181
1225
class circle_pt(path):
1182
1226
 
1183
 
   """circle with center (x,y) and radius"""
1184
 
 
1185
 
   def __init__(self, x, y, radius):
1186
 
       path.__init__(self, arc_pt(x, y, radius, 0, 360),
1187
 
                     closepath())
 
1227
    """circle with center (x_pt, y_pt) and radius_pt in pts"""
 
1228
 
 
1229
    def __init__(self, x_pt, y_pt, radius_pt, arcepsilon=0.1):
 
1230
        path.__init__(self, moveto_pt(x_pt+radius_pt, y_pt),
 
1231
                            arc_pt(x_pt, y_pt, radius_pt, arcepsilon, 360-arcepsilon),
 
1232
                            closepath())
 
1233
 
 
1234
 
 
1235
class ellipse_pt(path):
 
1236
 
 
1237
    """ellipse with center (x_pt, y_pt) in pts,
 
1238
    the two axes (a_pt, b_pt) in pts,
 
1239
    and the angle angle of the first axis"""
 
1240
 
 
1241
    def __init__(self, x_pt, y_pt, a_pt, b_pt, angle, **kwargs):
 
1242
        t = trafo.scale(a_pt, b_pt, epsilon=None).rotated(angle).translated_pt(x_pt, y_pt)
 
1243
        p = circle_pt(0, 0, 1, **kwargs).normpath(epsilon=None).transformed(t).path()
 
1244
        path.__init__(self, *p.pathitems)
1188
1245
 
1189
1246
 
1190
1247
class line(line_pt):
1191
1248
 
1192
 
   """straight line from (x1, y1) to (x2, y2)"""
 
1249
    """straight line from (x1, y1) to (x2, y2)"""
1193
1250
 
1194
 
   def __init__(self, x1, y1, x2, y2):
1195
 
       line_pt.__init__(self,
1196
 
                        unit.topt(x1), unit.topt(y1),
1197
 
                        unit.topt(x2), unit.topt(y2))
 
1251
    def __init__(self, x1, y1, x2, y2):
 
1252
        line_pt.__init__(self, unit.topt(x1), unit.topt(y1),
 
1253
                               unit.topt(x2), unit.topt(y2))
1198
1254
 
1199
1255
 
1200
1256
class curve(curve_pt):
1201
1257
 
1202
 
   """Bezier curve with control points (x0, y1),..., (x3, y3)"""
 
1258
    """bezier curve with control points (x0, y1),..., (x3, y3)"""
1203
1259
 
1204
 
   def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
1205
 
       curve_pt.__init__(self,
1206
 
                         unit.topt(x0), unit.topt(y0),
1207
 
                         unit.topt(x1), unit.topt(y1),
1208
 
                         unit.topt(x2), unit.topt(y2),
1209
 
                         unit.topt(x3), unit.topt(y3))
 
1260
    def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
 
1261
        curve_pt.__init__(self, unit.topt(x0), unit.topt(y0),
 
1262
                                unit.topt(x1), unit.topt(y1),
 
1263
                                unit.topt(x2), unit.topt(y2),
 
1264
                                unit.topt(x3), unit.topt(y3))
1210
1265
 
1211
1266
 
1212
1267
class rect(rect_pt):
1213
1268
 
1214
 
   """rectangle at position (x,y) with width and height"""
 
1269
    """rectangle at position (x,y) with width and height"""
1215
1270
 
1216
 
   def __init__(self, x, y, width, height):
1217
 
       rect_pt.__init__(self,
1218
 
                        unit.topt(x), unit.topt(y),
1219
 
                        unit.topt(width), unit.topt(height))
 
1271
    def __init__(self, x, y, width, height):
 
1272
        rect_pt.__init__(self, unit.topt(x), unit.topt(y),
 
1273
                               unit.topt(width), unit.topt(height))
1220
1274
 
1221
1275
 
1222
1276
class circle(circle_pt):
1223
1277
 
1224
 
   """circle with center (x,y) and radius"""
1225
 
 
1226
 
   def __init__(self, x, y, radius):
1227
 
       circle_pt.__init__(self,
1228
 
                        unit.topt(x), unit.topt(y),
1229
 
                        unit.topt(radius))
1230
 
 
1231
 
################################################################################
1232
 
# normpath and corresponding classes
1233
 
################################################################################
1234
 
 
1235
 
# two helper functions for the intersection of normsubpathitems
1236
 
 
1237
 
def _intersectnormcurves(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
1238
 
    """intersect two bpathitems
1239
 
 
1240
 
    a and b are bpathitems with parameter ranges [a_t0, a_t1],
1241
 
    respectively [b_t0, b_t1].
1242
 
    epsilon determines when the bpathitems are assumed to be straight
1243
 
 
1244
 
    """
1245
 
 
1246
 
    # intersection of bboxes is a necessary criterium for intersection
1247
 
    if not a.bbox().intersects(b.bbox()): return []
1248
 
 
1249
 
    if not a.isstraight(epsilon):
1250
 
        (aa, ab) = a.midpointsplit()
1251
 
        a_tm = 0.5*(a_t0+a_t1)
1252
 
 
1253
 
        if not b.isstraight(epsilon):
1254
 
            (ba, bb) = b.midpointsplit()
1255
 
            b_tm = 0.5*(b_t0+b_t1)
1256
 
 
1257
 
            return ( _intersectnormcurves(aa, a_t0, a_tm,
1258
 
                                       ba, b_t0, b_tm, epsilon) + 
1259
 
                     _intersectnormcurves(ab, a_tm, a_t1,
1260
 
                                       ba, b_t0, b_tm, epsilon) + 
1261
 
                     _intersectnormcurves(aa, a_t0, a_tm,
1262
 
                                       bb, b_tm, b_t1, epsilon) +
1263
 
                     _intersectnormcurves(ab, a_tm, a_t1,
1264
 
                                       bb, b_tm, b_t1, epsilon) )
1265
 
        else:
1266
 
            return ( _intersectnormcurves(aa, a_t0, a_tm,
1267
 
                                       b, b_t0, b_t1, epsilon) +
1268
 
                     _intersectnormcurves(ab, a_tm, a_t1,
1269
 
                                       b, b_t0, b_t1, epsilon) )
1270
 
    else:
1271
 
        if not b.isstraight(epsilon):
1272
 
            (ba, bb) = b.midpointsplit()
1273
 
            b_tm = 0.5*(b_t0+b_t1)
1274
 
 
1275
 
            return  ( _intersectnormcurves(a, a_t0, a_t1,
1276
 
                                       ba, b_t0, b_tm, epsilon) +
1277
 
                      _intersectnormcurves(a, a_t0, a_t1,
1278
 
                                       bb, b_tm, b_t1, epsilon) )
1279
 
        else:
1280
 
            # no more subdivisions of either a or b
1281
 
            # => try to intersect a and b as straight line segments
1282
 
 
1283
 
            a_deltax = a.x3_pt - a.x0_pt
1284
 
            a_deltay = a.y3_pt - a.y0_pt
1285
 
            b_deltax = b.x3_pt - b.x0_pt
1286
 
            b_deltay = b.y3_pt - b.y0_pt
1287
 
 
1288
 
            det = b_deltax*a_deltay - b_deltay*a_deltax
1289
 
 
1290
 
            ba_deltax0_pt = b.x0_pt - a.x0_pt
1291
 
            ba_deltay0_pt = b.y0_pt - a.y0_pt
1292
 
 
1293
 
            try:
1294
 
                a_t = ( b_deltax*ba_deltay0_pt - b_deltay*ba_deltax0_pt)/det
1295
 
                b_t = ( a_deltax*ba_deltay0_pt - a_deltay*ba_deltax0_pt)/det
1296
 
            except ArithmeticError:
1297
 
                return []
1298
 
 
1299
 
            # check for intersections out of bound
1300
 
            if not (0<=a_t<=1 and 0<=b_t<=1): return []
1301
 
 
1302
 
            # return rescaled parameters of the intersection
1303
 
            return [ ( a_t0 + a_t * (a_t1 - a_t0),
1304
 
                       b_t0 + b_t * (b_t1 - b_t0) ) ]
1305
 
 
1306
 
 
1307
 
def _intersectnormlines(a, b):
1308
 
    """return one-element list constisting either of tuple of
1309
 
    parameters of the intersection point of the two normlines a and b
1310
 
    or empty list if both normlines do not intersect each other"""
1311
 
 
1312
 
    a_deltax_pt = a.x1_pt - a.x0_pt
1313
 
    a_deltay_pt = a.y1_pt - a.y0_pt
1314
 
    b_deltax_pt = b.x1_pt - b.x0_pt
1315
 
    b_deltay_pt = b.y1_pt - b.y0_pt
1316
 
 
1317
 
    det = 1.0*(b_deltax_pt * a_deltay_pt - b_deltay_pt * a_deltax_pt)
1318
 
 
1319
 
    ba_deltax0_pt = b.x0_pt - a.x0_pt
1320
 
    ba_deltay0_pt = b.y0_pt - a.y0_pt
1321
 
 
1322
 
    try:
1323
 
        a_t = ( b_deltax_pt * ba_deltay0_pt - b_deltay_pt * ba_deltax0_pt)/det
1324
 
        b_t = ( a_deltax_pt * ba_deltay0_pt - a_deltay_pt * ba_deltax0_pt)/det
1325
 
    except ArithmeticError:
1326
 
        return []
1327
 
 
1328
 
    # check for intersections out of bound
1329
 
    if not (0<=a_t<=1 and 0<=b_t<=1): return []
1330
 
 
1331
 
    # return parameters of the intersection
1332
 
    return [( a_t, b_t)]
1333
 
 
1334
 
#
1335
 
# normsubpathitem: normalized element
1336
 
#
1337
 
 
1338
 
class normsubpathitem:
1339
 
 
1340
 
    """element of a normalized sub path"""
1341
 
 
1342
 
    def at_pt(self, t):
1343
 
        """returns coordinates of point in pts at parameter t (0<=t<=1) """
1344
 
        pass
1345
 
 
1346
 
    def arclen_pt(self, epsilon=1e-5):
1347
 
        """returns arc length of normsubpathitem in pts with given accuracy epsilon"""
1348
 
        pass
1349
 
 
1350
 
    def _arclentoparam_pt(self, lengths, epsilon=1e-5):
1351
 
        """returns tuple (t,l) with
1352
 
          t the parameter where the arclen of normsubpathitem is length and
1353
 
          l the total arclen
1354
 
 
1355
 
        length:  length (in pts) to find the parameter for
1356
 
        epsilon: epsilon controls the accuracy for calculation of the
1357
 
                 length of the Bezier elements
1358
 
        """
1359
 
        # Note: _arclentoparam returns both, parameters and total lengths
1360
 
        # while  arclentoparam returns only parameters
1361
 
        pass
1362
 
 
1363
 
    def bbox(self):
1364
 
        """return bounding box of normsubpathitem"""
1365
 
        pass
1366
 
 
1367
 
    def curvradius_pt(self, param):
1368
 
        """Returns the curvature radius in pts at parameter param.
1369
 
        This is the inverse of the curvature at this parameter
1370
 
 
1371
 
        Please note that this radius can be negative or positive,
1372
 
        depending on the sign of the curvature"""
1373
 
        pass
1374
 
 
1375
 
    def intersect(self, other, epsilon=1e-5):
1376
 
        """intersect self with other normsubpathitem"""
1377
 
        pass
1378
 
 
1379
 
    def modified(self, xs_pt=None, ys_pt=None, xe_pt=None, ye_pt=None):
1380
 
        """returns a (new) modified normpath with different start and
1381
 
        end points as provided"""
1382
 
        pass
1383
 
 
1384
 
    def reversed(self):
1385
 
        """return reversed normsubpathitem"""
1386
 
        pass
1387
 
 
1388
 
    def split(self, parameters):
1389
 
        """splits normsubpathitem
1390
 
 
1391
 
        parameters: list of parameter values (0<=t<=1) at which to split
1392
 
 
1393
 
        returns None or list of tuple of normsubpathitems corresponding to 
1394
 
        the orginal normsubpathitem.
1395
 
 
1396
 
        """
1397
 
        pass
1398
 
 
1399
 
    def tangentvector_pt(self, t):
1400
 
        """returns tangent vector of normsubpathitem in pts at parameter t (0<=t<=1)"""
1401
 
        pass
1402
 
 
1403
 
    def transformed(self, trafo):
1404
 
        """return transformed normsubpathitem according to trafo"""
1405
 
        pass
1406
 
 
1407
 
    def outputPS(self, file):
1408
 
        """write PS code corresponding to normsubpathitem to file"""
1409
 
        pass
1410
 
 
1411
 
    def outputPS(self, file):
1412
 
        """write PDF code corresponding to normsubpathitem to file"""
1413
 
        pass
1414
 
 
1415
 
#
1416
 
# there are only two normsubpathitems: normline and normcurve
1417
 
#
1418
 
 
1419
 
class normline(normsubpathitem):
1420
 
 
1421
 
    """Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1422
 
 
1423
 
    __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1424
 
 
1425
 
    def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt):
1426
 
         self.x0_pt = x0_pt
1427
 
         self.y0_pt = y0_pt
1428
 
         self.x1_pt = x1_pt
1429
 
         self.y1_pt = y1_pt
1430
 
 
1431
 
    def __str__(self):
1432
 
        return "normline(%g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt)
1433
 
 
1434
 
    def _arclentoparam_pt(self, lengths, epsilon=1e-5):
1435
 
        l = self.arclen_pt(epsilon)
1436
 
        return ([max(min(1.0 * length / l, 1), 0) for length in lengths], l)
1437
 
 
1438
 
    def _normcurve(self):
1439
 
        """ return self as equivalent normcurve """
1440
 
        xa_pt = self.x0_pt+(self.x1_pt-self.x0_pt)/3.0
1441
 
        ya_pt = self.y0_pt+(self.y1_pt-self.y0_pt)/3.0
1442
 
        xb_pt = self.x0_pt+2.0*(self.x1_pt-self.x0_pt)/3.0
1443
 
        yb_pt = self.y0_pt+2.0*(self.y1_pt-self.y0_pt)/3.0
1444
 
        return normcurve(self.x0_pt, self.y0_pt, xa_pt, ya_pt, xb_pt, yb_pt, self.x1_pt, self.y1_pt)
1445
 
 
1446
 
    def arclen_pt(self,  epsilon=1e-5):
1447
 
        return math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1448
 
 
1449
 
    def at_pt(self, t):
1450
 
        return self.x0_pt+(self.x1_pt-self.x0_pt)*t, self.y0_pt+(self.y1_pt-self.y0_pt)*t
1451
 
 
1452
 
    def at(self, t):
1453
 
        return (self.x0_pt+(self.x1_pt-self.x0_pt)*t) * unit.t_pt, (self.y0_pt+(self.y1_pt-self.y0_pt)*t) * unit.t_pt
1454
 
 
1455
 
    def bbox(self):
1456
 
        return bbox.bbox_pt(min(self.x0_pt, self.x1_pt), min(self.y0_pt, self.y1_pt), 
1457
 
                          max(self.x0_pt, self.x1_pt), max(self.y0_pt, self.y1_pt))
1458
 
 
1459
 
    def begin_pt(self):
1460
 
        return self.x0_pt, self.y0_pt
1461
 
 
1462
 
    def begin(self):
1463
 
        return self.x0_pt * unit.t_pt, self.y0_pt * unit.t_pt
1464
 
 
1465
 
    def curvradius_pt(self, param):
1466
 
        return None
1467
 
 
1468
 
    def end_pt(self):
1469
 
        return self.x1_pt, self.y1_pt
1470
 
 
1471
 
    def end(self):
1472
 
        return self.x1_pt * unit.t_pt, self.y1_pt * unit.t_pt
1473
 
 
1474
 
    def intersect(self, other, epsilon=1e-5):
1475
 
        if isinstance(other, normline):
1476
 
            return _intersectnormlines(self, other)
1477
 
        else:
1478
 
            return  _intersectnormcurves(self._normcurve(), 0, 1, other, 0, 1, epsilon)
1479
 
 
1480
 
    def isstraight(self, epsilon):
1481
 
        return 1
1482
 
 
1483
 
    def modified(self, xs_pt=None, ys_pt=None, xe_pt=None, ye_pt=None):
1484
 
        if xs_pt is None:
1485
 
            xs_pt = self.x0_pt
1486
 
        if ys_pt is None:
1487
 
            ys_pt = self.y0_pt
1488
 
        if xe_pt is None:
1489
 
            xe_pt = self.x1_pt
1490
 
        if ye_pt is None:
1491
 
            ye_pt = self.y1_pt
1492
 
        return normline(xs_pt, ys_pt, xe_pt, ye_pt)
1493
 
 
1494
 
    def reverse(self):
1495
 
        self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt = self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt
1496
 
 
1497
 
    def reversed(self):
1498
 
        return normline(self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt)
1499
 
 
1500
 
    def split(self, params):
1501
 
        # just for performance reasons
1502
 
        x0_pt, y0_pt = self.x0_pt, self.y0_pt
1503
 
        x1_pt, y1_pt = self.x1_pt, self.y1_pt
1504
 
 
1505
 
        result = []
1506
 
 
1507
 
        xl_pt, yl_pt = x0_pt, y0_pt
1508
 
        for t in params + [1]:
1509
 
            xr_pt, yr_pt = x0_pt + (x1_pt-x0_pt)*t, y0_pt + (y1_pt-y0_pt)*t
1510
 
            result.append(normline(xl_pt, yl_pt, xr_pt, yr_pt))
1511
 
            xl_pt, yl_pt = xr_pt, yr_pt
1512
 
 
1513
 
        return result
1514
 
 
1515
 
    def tangentvector_pt(self, param):
1516
 
        return self.x1_pt-self.x0_pt, self.y1_pt-self.y0_pt
1517
 
 
1518
 
    def trafo(self, param):
1519
 
        tx_pt, ty_pt = self.at_pt(param)
1520
 
        tdx_pt, tdy_pt = self.x1_pt-self.x0_pt, self.y1_pt-self.y0_pt
1521
 
        return trafo.translate_pt(tx_pt, ty_pt)*trafo.rotate(degrees(math.atan2(tdy_pt, tdx_pt)))
1522
 
 
1523
 
    def transformed(self, trafo):
1524
 
        return normline(*(trafo._apply(self.x0_pt, self.y0_pt) + trafo._apply(self.x1_pt, self.y1_pt)))
1525
 
 
1526
 
    def outputPS(self, file):
1527
 
        file.write("%g %g lineto\n" % (self.x1_pt, self.y1_pt))
1528
 
 
1529
 
    def outputPDF(self, file):
1530
 
        file.write("%f %f l\n" % (self.x1_pt, self.y1_pt))
1531
 
 
1532
 
 
1533
 
class normcurve(normsubpathitem):
1534
 
 
1535
 
    """Bezier curve with control points x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt (coordinates in pts)"""
1536
 
 
1537
 
    __slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1538
 
 
1539
 
    def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1540
 
        self.x0_pt = x0_pt
1541
 
        self.y0_pt = y0_pt
1542
 
        self.x1_pt = x1_pt
1543
 
        self.y1_pt = y1_pt
1544
 
        self.x2_pt = x2_pt
1545
 
        self.y2_pt = y2_pt
1546
 
        self.x3_pt = x3_pt
1547
 
        self.y3_pt = y3_pt
1548
 
 
1549
 
    def __str__(self):
1550
 
        return "normcurve(%g, %g, %g, %g, %g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt,
1551
 
                                                              self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt)
1552
 
 
1553
 
    def _arclentoparam_pt(self, lengths, epsilon=1e-5):
1554
 
        """computes the parameters [t] of bpathitem where the given lengths (in pts) are assumed
1555
 
        returns ( [parameters], total arclen)
1556
 
        A negative length gives a parameter 0"""
1557
 
 
1558
 
        # create the list of accumulated lengths
1559
 
        # and the length of the parameters
1560
 
        seg = self.seglengths(1, epsilon)
1561
 
        arclens = [seg[i][0] for i in range(len(seg))]
1562
 
        Dparams = [seg[i][1] for i in range(len(seg))]
1563
 
        l = len(arclens)
1564
 
        for i in range(1,l):
1565
 
            arclens[i] += arclens[i-1]
1566
 
 
1567
 
        # create the list of parameters to be returned
1568
 
        params = []
1569
 
        for length in lengths:
1570
 
            # find the last index that is smaller than length
1571
 
            try:
1572
 
                lindex = bisect.bisect_left(arclens, length)
1573
 
            except: # workaround for python 2.0
1574
 
                lindex = bisect.bisect(arclens, length)
1575
 
                while lindex and (lindex >= len(arclens) or
1576
 
                                  arclens[lindex] >= length):
1577
 
                    lindex -= 1
1578
 
            if lindex == 0:
1579
 
                param = Dparams[0] * length * 1.0 / arclens[0]
1580
 
            elif lindex < l-1:
1581
 
                param = Dparams[lindex+1] * (length - arclens[lindex]) * 1.0 / (arclens[lindex+1] - arclens[lindex])
1582
 
                for i in range(lindex+1):
1583
 
                    param += Dparams[i]
1584
 
            else:
1585
 
                param = 1 + Dparams[-1] * (length - arclens[-1]) * 1.0 / (arclens[-1] - arclens[-2])
1586
 
 
1587
 
            param = max(min(param,1),0)
1588
 
            params.append(param)
1589
 
        return (params, arclens[-1])
1590
 
 
1591
 
    def arclen_pt(self, epsilon=1e-5):
1592
 
        """computes arclen of bpathitem in pts using successive midpoint split"""
1593
 
        if self.isstraight(epsilon):
1594
 
            return math.hypot(self.x3_pt-self.x0_pt, self.y3_pt-self.y0_pt)
1595
 
        else:
1596
 
            a, b = self.midpointsplit()
1597
 
            return a.arclen_pt(epsilon) + b.arclen_pt(epsilon)
1598
 
 
1599
 
    def at_pt(self, t):
1600
 
        xt_pt = ( (-self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*t*t*t +
1601
 
                  (3*self.x0_pt-6*self.x1_pt+3*self.x2_pt          )*t*t +
1602
 
                  (-3*self.x0_pt+3*self.x1_pt                      )*t +
1603
 
                  self.x0_pt )
1604
 
        yt_pt = ( (-self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*t*t*t +
1605
 
                  (3*self.y0_pt-6*self.y1_pt+3*self.y2_pt          )*t*t +
1606
 
                  (-3*self.y0_pt+3*self.y1_pt                      )*t +
1607
 
                  self.y0_pt )
1608
 
        return xt_pt, yt_pt
1609
 
 
1610
 
    def at(self, t):
1611
 
        xt_pt, yt_pt = self.at_pt(t)
1612
 
        return xt_pt * unit.t_pt, yt_pt * unit.t_pt
1613
 
 
1614
 
    def bbox(self):
1615
 
        return bbox.bbox_pt(min(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1616
 
                          min(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt),
1617
 
                          max(self.x0_pt, self.x1_pt, self.x2_pt, self.x3_pt),
1618
 
                          max(self.y0_pt, self.y1_pt, self.y2_pt, self.y3_pt))
1619
 
 
1620
 
    def begin_pt(self):
1621
 
        return self.x0_pt, self.y0_pt
1622
 
 
1623
 
    def begin(self):
1624
 
        return self.x0_pt * unit.t_pt, self.y0_pt * unit.t_pt
1625
 
 
1626
 
    def curvradius_pt(self, param):
1627
 
        xdot = ( 3 * (1-param)*(1-param) * (-self.x0_pt + self.x1_pt) +
1628
 
                 6 * (1-param)*param * (-self.x1_pt + self.x2_pt) +
1629
 
                 3 * param*param * (-self.x2_pt + self.x3_pt) )
1630
 
        ydot = ( 3 * (1-param)*(1-param) * (-self.y0_pt + self.y1_pt) +
1631
 
                 6 * (1-param)*param * (-self.y1_pt + self.y2_pt) +
1632
 
                 3 * param*param * (-self.y2_pt + self.y3_pt) )
1633
 
        xddot = ( 6 * (1-param) * (self.x0_pt - 2*self.x1_pt + self.x2_pt) +
1634
 
                  6 * param * (self.x1_pt - 2*self.x2_pt + self.x3_pt) )
1635
 
        yddot = ( 6 * (1-param) * (self.y0_pt - 2*self.y1_pt + self.y2_pt) +
1636
 
                  6 * param * (self.y1_pt - 2*self.y2_pt + self.y3_pt) )
1637
 
        return (xdot**2 + ydot**2)**1.5 / (xdot*yddot - ydot*xddot)
1638
 
 
1639
 
    def end_pt(self):
1640
 
        return self.x3_pt, self.y3_pt
1641
 
 
1642
 
    def end(self):
1643
 
        return self.x3_pt * unit.t_pt, self.y3_pt * unit.t_pt
1644
 
 
1645
 
    def intersect(self, other, epsilon=1e-5):
1646
 
        if isinstance(other, normline):
1647
 
            return  _intersectnormcurves(self, 0, 1, other._normcurve(), 0, 1, epsilon)
1648
 
        else:
1649
 
            return  _intersectnormcurves(self, 0, 1, other, 0, 1, epsilon)
1650
 
 
1651
 
    def isstraight(self, epsilon=1e-5):
1652
 
        """check wheter the normcurve is approximately straight"""
1653
 
 
1654
 
        # just check, whether the modulus of the difference between
1655
 
        # the length of the control polygon
1656
 
        # (i.e. |P1-P0|+|P2-P1|+|P3-P2|) and the length of the
1657
 
        # straight line between starting and ending point of the
1658
 
        # normcurve (i.e. |P3-P1|) is smaller the epsilon
1659
 
        return abs(math.hypot(self.x1_pt-self.x0_pt, self.y1_pt-self.y0_pt)+
1660
 
                   math.hypot(self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt)+
1661
 
                   math.hypot(self.x3_pt-self.x2_pt, self.y3_pt-self.y2_pt)-
1662
 
                   math.hypot(self.x3_pt-self.x0_pt, self.y3_pt-self.y0_pt))<epsilon
1663
 
 
1664
 
    def midpointsplit(self):
1665
 
        """splits bpathitem at midpoint returning bpath with two bpathitems"""
1666
 
 
1667
 
        # for efficiency reason, we do not use self.split(0.5)!
1668
 
 
1669
 
        # first, we have to calculate the  midpoints between adjacent
1670
 
        # control points
1671
 
        x01_pt = 0.5*(self.x0_pt + self.x1_pt)
1672
 
        y01_pt = 0.5*(self.y0_pt + self.y1_pt)
1673
 
        x12_pt = 0.5*(self.x1_pt + self.x2_pt)
1674
 
        y12_pt = 0.5*(self.y1_pt + self.y2_pt)
1675
 
        x23_pt = 0.5*(self.x2_pt + self.x3_pt)
1676
 
        y23_pt = 0.5*(self.y2_pt + self.y3_pt)
1677
 
 
1678
 
        # In the next iterative step, we need the midpoints between 01 and 12
1679
 
        # and between 12 and 23 
1680
 
        x01_12_pt = 0.5*(x01_pt + x12_pt)
1681
 
        y01_12_pt = 0.5*(y01_pt + y12_pt)
1682
 
        x12_23_pt = 0.5*(x12_pt + x23_pt)
1683
 
        y12_23_pt = 0.5*(y12_pt + y23_pt)
1684
 
 
1685
 
        # Finally the midpoint is given by
1686
 
        xmidpoint_pt = 0.5*(x01_12_pt + x12_23_pt)
1687
 
        ymidpoint_pt = 0.5*(y01_12_pt + y12_23_pt)
1688
 
 
1689
 
        return (normcurve(self.x0_pt, self.y0_pt,
1690
 
                          x01_pt, y01_pt,
1691
 
                          x01_12_pt, y01_12_pt,
1692
 
                          xmidpoint_pt, ymidpoint_pt),
1693
 
                normcurve(xmidpoint_pt, ymidpoint_pt,
1694
 
                          x12_23_pt, y12_23_pt,
1695
 
                          x23_pt, y23_pt,
1696
 
                          self.x3_pt, self.y3_pt))
1697
 
 
1698
 
    def modified(self, xs_pt=None, ys_pt=None, xe_pt=None, ye_pt=None):
1699
 
        if xs_pt is None:
1700
 
            xs_pt = self.x0_pt
1701
 
        if ys_pt is None:
1702
 
            ys_pt = self.y0_pt
1703
 
        if xe_pt is None:
1704
 
            xe_pt = self.x3_pt
1705
 
        if ye_pt is None:
1706
 
            ye_pt = self.y3_pt
1707
 
        return normcurve(xs_pt, ys_pt,
1708
 
                         self.x1_pt, self.y1_pt,
1709
 
                         self.x2_pt, self.y2_pt,
1710
 
                         xe_pt, ye_pt)
1711
 
 
1712
 
    def reverse(self):
1713
 
        self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt = \
1714
 
        self.x3_pt, self.y3_pt, self.x2_pt, self.y2_pt, self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt
1715
 
 
1716
 
    def reversed(self):
1717
 
        return normcurve(self.x3_pt, self.y3_pt, self.x2_pt, self.y2_pt, self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt)
1718
 
 
1719
 
    def seglengths(self, paraminterval, epsilon=1e-5):
1720
 
        """returns the list of segment line lengths (in pts) of the normcurve
1721
 
           together with the length of the parameterinterval"""
1722
 
 
1723
 
        # lower and upper bounds for the arclen
1724
 
        lowerlen = math.hypot(self.x3_pt-self.x0_pt, self.y3_pt-self.y0_pt)
1725
 
        upperlen = ( math.hypot(self.x1_pt-self.x0_pt, self.y1_pt-self.y0_pt) +
1726
 
                     math.hypot(self.x2_pt-self.x1_pt, self.y2_pt-self.y1_pt) +
1727
 
                     math.hypot(self.x3_pt-self.x2_pt, self.y3_pt-self.y2_pt) )
1728
 
 
1729
 
        # instead of isstraight method:
1730
 
        if abs(upperlen-lowerlen)<epsilon:
1731
 
            return [( 0.5*(upperlen+lowerlen), paraminterval )]
1732
 
        else:
1733
 
            a, b = self.midpointsplit()
1734
 
            return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
1735
 
 
1736
 
    def split(self, params):
1737
 
        """return list of normcurves corresponding to split at parameters"""
1738
 
 
1739
 
        # first, we calculate the coefficients corresponding to our
1740
 
        # original bezier curve. These represent a useful starting
1741
 
        # point for the following change of the polynomial parameter
1742
 
        a0x_pt = self.x0_pt
1743
 
        a0y_pt = self.y0_pt
1744
 
        a1x_pt = 3*(-self.x0_pt+self.x1_pt)
1745
 
        a1y_pt = 3*(-self.y0_pt+self.y1_pt)
1746
 
        a2x_pt = 3*(self.x0_pt-2*self.x1_pt+self.x2_pt)
1747
 
        a2y_pt = 3*(self.y0_pt-2*self.y1_pt+self.y2_pt)
1748
 
        a3x_pt = -self.x0_pt+3*(self.x1_pt-self.x2_pt)+self.x3_pt
1749
 
        a3y_pt = -self.y0_pt+3*(self.y1_pt-self.y2_pt)+self.y3_pt
1750
 
 
1751
 
        params = [0] + params + [1]
1752
 
        result = []
1753
 
 
1754
 
        for i in range(len(params)-1):
1755
 
            t1 = params[i]
1756
 
            dt = params[i+1]-t1
1757
 
 
1758
 
            # [t1,t2] part
1759
 
            #
1760
 
            # the new coefficients of the [t1,t1+dt] part of the bezier curve
1761
 
            # are then given by expanding
1762
 
            #  a0 + a1*(t1+dt*u) + a2*(t1+dt*u)**2 +
1763
 
            #  a3*(t1+dt*u)**3 in u, yielding
1764
 
            #
1765
 
            #   a0 + a1*t1 + a2*t1**2 + a3*t1**3        +
1766
 
            #   ( a1 + 2*a2 + 3*a3*t1**2 )*dt    * u    + 
1767
 
            #   ( a2 + 3*a3*t1 )*dt**2           * u**2 +
1768
 
            #   a3*dt**3                         * u**3
1769
 
            #
1770
 
            # from this values we obtain the new control points by inversion
1771
 
            #
1772
 
            # XXX: we could do this more efficiently by reusing for
1773
 
            # (x0_pt, y0_pt) the control point (x3_pt, y3_pt) from the previous
1774
 
            # Bezier curve
1775
 
 
1776
 
            x0_pt = a0x_pt + a1x_pt*t1 + a2x_pt*t1*t1 + a3x_pt*t1*t1*t1 
1777
 
            y0_pt = a0y_pt + a1y_pt*t1 + a2y_pt*t1*t1 + a3y_pt*t1*t1*t1 
1778
 
            x1_pt = (a1x_pt+2*a2x_pt*t1+3*a3x_pt*t1*t1)*dt/3.0 + x0_pt
1779
 
            y1_pt = (a1y_pt+2*a2y_pt*t1+3*a3y_pt*t1*t1)*dt/3.0 + y0_pt
1780
 
            x2_pt = (a2x_pt+3*a3x_pt*t1)*dt*dt/3.0 - x0_pt + 2*x1_pt
1781
 
            y2_pt = (a2y_pt+3*a3y_pt*t1)*dt*dt/3.0 - y0_pt + 2*y1_pt
1782
 
            x3_pt = a3x_pt*dt*dt*dt + x0_pt - 3*x1_pt + 3*x2_pt
1783
 
            y3_pt = a3y_pt*dt*dt*dt + y0_pt - 3*y1_pt + 3*y2_pt
1784
 
 
1785
 
            result.append(normcurve(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1786
 
 
1787
 
        return result
1788
 
 
1789
 
    def tangentvector_pt(self, param):
1790
 
        tvectx = (3*(  -self.x0_pt+3*self.x1_pt-3*self.x2_pt+self.x3_pt)*param*param +
1791
 
                  2*( 3*self.x0_pt-6*self.x1_pt+3*self.x2_pt        )*param +
1792
 
                    (-3*self.x0_pt+3*self.x1_pt                  ))
1793
 
        tvecty = (3*(  -self.y0_pt+3*self.y1_pt-3*self.y2_pt+self.y3_pt)*param*param +
1794
 
                  2*( 3*self.y0_pt-6*self.y1_pt+3*self.y2_pt        )*param +
1795
 
                    (-3*self.y0_pt+3*self.y1_pt                  ))
1796
 
        return (tvectx, tvecty)
1797
 
 
1798
 
    def trafo(self, param):
1799
 
        tx_pt, ty_pt = self.at_pt(param)
1800
 
        tdx_pt, tdy_pt = self.tangentvector_pt(param)
1801
 
        return trafo.translate_pt(tx_pt, ty_pt)*trafo.rotate(degrees(math.atan2(tdy_pt, tdx_pt)))
1802
 
 
1803
 
    def transform(self, trafo):
1804
 
        self.x0_pt, self.y0_pt = trafo._apply(self.x0_pt, self.y0_pt)
1805
 
        self.x1_pt, self.y1_pt = trafo._apply(self.x1_pt, self.y1_pt)
1806
 
        self.x2_pt, self.y2_pt = trafo._apply(self.x2_pt, self.y2_pt)
1807
 
        self.x3_pt, self.y3_pt = trafo._apply(self.x3_pt, self.y3_pt)
1808
 
 
1809
 
    def transformed(self, trafo):
1810
 
        return normcurve(*(trafo._apply(self.x0_pt, self.y0_pt)+
1811
 
                           trafo._apply(self.x1_pt, self.y1_pt)+
1812
 
                           trafo._apply(self.x2_pt, self.y2_pt)+
1813
 
                           trafo._apply(self.x3_pt, self.y3_pt)))
1814
 
 
1815
 
    def outputPS(self, file):
1816
 
        file.write("%g %g %g %g %g %g curveto\n" % (self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt))
1817
 
 
1818
 
    def outputPDF(self, file):
1819
 
        file.write("%f %f %f %f %f %f c\n" % (self.x1_pt, self.y1_pt, self.x2_pt, self.y2_pt, self.x3_pt, self.y3_pt))
1820
 
 
1821
 
#
1822
 
# normpaths are made up of normsubpaths, which represent connected line segments
1823
 
#
1824
 
 
1825
 
class normsubpath:
1826
 
 
1827
 
    """sub path of a normalized path
1828
 
 
1829
 
    A subpath consists of a list of normsubpathitems, i.e., lines and bcurves
1830
 
    and can either be closed or not.
1831
 
 
1832
 
    Some invariants, which have to be obeyed:
1833
 
    - All normsubpathitems have to be longer than epsilon pts.
1834
 
    - At the end there may be a normline (stored in self.skippedline) whose
1835
 
      length is shorter than epsilon
1836
 
    - The last point of a normsubpathitem and the first point of the next
1837
 
    element have to be equal.
1838
 
    - When the path is closed, the last point of last normsubpathitem has
1839
 
      to be equal to the first point of the first normsubpathitem.
1840
 
    """
1841
 
 
1842
 
    __slots__ = "normsubpathitems", "closed", "epsilon", "skippedline"
1843
 
 
1844
 
    def __init__(self, normsubpathitems=[], closed=0, epsilon=1e-5):
1845
 
        self.epsilon = epsilon
1846
 
        # If one or more items appended to the normsubpath have been
1847
 
        # skipped (because their total length was shorter than
1848
 
        # epsilon), we remember this fact by a line because we have to
1849
 
        # take it properly into account when appending further subnormpathitems
1850
 
        self.skippedline = None
1851
 
 
1852
 
        self.normsubpathitems = []
1853
 
        self.closed = 0
1854
 
 
1855
 
        # a test (might be temporary)
1856
 
        for anormsubpathitem in normsubpathitems:
1857
 
            assert isinstance(anormsubpathitem, normsubpathitem), "only list of normsubpathitem instances allowed"
1858
 
 
1859
 
        self.extend(normsubpathitems)
1860
 
 
1861
 
        if closed:
1862
 
            self.close()
1863
 
 
1864
 
    def __add__(self, other):
1865
 
        # we take self.epsilon as accuracy for the resulting subnormpath
1866
 
        result = subnormpath(self.normpathitems, self.closed, self.epsilon)
1867
 
        result += other
1868
 
        return result
1869
 
 
1870
 
    def __getitem__(self, i):
1871
 
        return self.normsubpathitems[i]
1872
 
 
1873
 
    def __iadd__(self, other):
1874
 
        if other.closed:
1875
 
            raise PathException("Cannot extend normsubpath by closed normsubpath")
1876
 
        self.extend(other.normsubpathitems)
1877
 
        return self
1878
 
 
1879
 
    def __len__(self):
1880
 
        return len(self.normsubpathitems)
1881
 
 
1882
 
    def __str__(self):
1883
 
        return "subpath(%s, [%s])" % (self.closed and "closed" or "open",
1884
 
                                    ", ".join(map(str, self.normsubpathitems)))
1885
 
 
1886
 
    def _distributeparams(self, params):
1887
 
        """Creates a list tuples (normsubpathitem, itemparams),
1888
 
        where itemparams are the parameter values corresponding
1889
 
        to params in normsubpathitem. For the first normsubpathitem
1890
 
        itemparams fulfil param < 1, for the last normsubpathitem
1891
 
        itemparams fulfil 0 <= param, and for all other
1892
 
        normsubpathitems itemparams fulfil 0 <= param < 1.
1893
 
        Note that params have to be sorted.
1894
 
        """
1895
 
        if self.isshort():
1896
 
            if params:
1897
 
                raise PathException("Cannot select parameters for a short normsubpath")
1898
 
            return []
1899
 
        result = []
1900
 
        paramindex = 0
1901
 
        for index, normsubpathitem in enumerate(self.normsubpathitems[:-1]):
1902
 
            oldparamindex = paramindex
1903
 
            while paramindex < len(params) and params[paramindex] < index + 1:
1904
 
                paramindex += 1
1905
 
            result.append((normsubpathitem, [param - index for param in params[oldparamindex: paramindex]]))
1906
 
        result.append((self.normsubpathitems[-1],
1907
 
                       [param - len(self.normsubpathitems) + 1 for param in params[paramindex:]]))
1908
 
        return result
1909
 
 
1910
 
    def _findnormsubpathitem(self, param):
1911
 
        """Finds the normsubpathitem for the given parameter and
1912
 
        returns a tuple containing this item and the parameter
1913
 
        converted to the range of the item. An out of bound parameter
1914
 
        is handled like in _distributeparams."""
1915
 
        if not self.normsubpathitems:
1916
 
            raise PathException("Cannot select parameters for a short normsubpath")
1917
 
        if param > 0:
1918
 
            index = int(param)
1919
 
            if index > len(self.normsubpathitems) - 1:
1920
 
                index = len(self.normsubpathitems) - 1
1921
 
        else:
1922
 
            index = 0
1923
 
        return self.normsubpathitems[index], param - index
1924
 
 
1925
 
    def append(self, anormsubpathitem):
1926
 
        assert isinstance(anormsubpathitem, normsubpathitem), "only normsubpathitem instances allowed"
1927
 
 
1928
 
        if self.closed:
1929
 
            raise PathException("Cannot append to closed normsubpath")
1930
 
 
1931
 
        if self.skippedline:
1932
 
            xs_pt, ys_pt = self.skippedline.begin_pt()
1933
 
        else:
1934
 
            xs_pt, ys_pt = anormsubpathitem.begin_pt()
1935
 
        xe_pt, ye_pt = anormsubpathitem.end_pt()
1936
 
 
1937
 
        if (math.hypot(xe_pt-xs_pt, ye_pt-ys_pt) >= self.epsilon or
1938
 
            anormsubpathitem.arclen_pt(self.epsilon) >= self.epsilon):
1939
 
            if self.skippedline:
1940
 
                anormsubpathitem = anormsubpathitem.modified(xs_pt=xs_pt, ys_pt=ys_pt)
1941
 
            self.normsubpathitems.append(anormsubpathitem)
1942
 
            self.skippedline = None
1943
 
        else:
1944
 
            self.skippedline = normline(xs_pt, ys_pt, xe_pt, ye_pt)
1945
 
 
1946
 
    def arclen_pt(self):
1947
 
        """returns total arc length of normsubpath in pts with accuracy epsilon"""
1948
 
        return sum([npitem.arclen_pt(self.epsilon) for npitem in self.normsubpathitems])
1949
 
 
1950
 
    def arclen(self):
1951
 
        """returns total arc length of normsubpath"""
1952
 
        return self.arclen_pt() * unit.t_pt
1953
 
 
1954
 
    def _arclentoparam_pt(self, lengths):
1955
 
        """returns [t, l] where t are parameter value(s) matching given length(s)
1956
 
        and l is the total length of the normsubpath
1957
 
        The parameters are with respect to the normsubpath: t in [0, self.range()]
1958
 
        lengths that are < 0 give parameter 0"""
1959
 
 
1960
 
        allarclen = 0
1961
 
        allparams = [0] * len(lengths)
1962
 
        rests = lengths[:]
1963
 
 
1964
 
        for pitem in self.normsubpathitems:
1965
 
            params, arclen = pitem._arclentoparam_pt(rests, self.epsilon)
1966
 
            allarclen += arclen
1967
 
            for i in range(len(rests)):
1968
 
                if rests[i] >= 0:
1969
 
                    rests[i] -= arclen
1970
 
                    allparams[i] += params[i]
1971
 
 
1972
 
        return (allparams, allarclen)
1973
 
 
1974
 
    def arclentoparam_pt(self, lengths):
1975
 
        if len(lengths)==1:
1976
 
            return self._arclentoparam_pt(lengths)[0][0]
1977
 
        else:
1978
 
            return self._arclentoparam_pt(lengths)[0]
1979
 
 
1980
 
    def arclentoparam(self, lengths):
1981
 
        """returns the parameter value(s) matching the given length(s)
1982
 
 
1983
 
        all given lengths must be positive.
1984
 
        A length greater than the total arclength will give self.range()
1985
 
        """
1986
 
        l = [unit.topt(length) for length in helper.ensuresequence(lengths)]
1987
 
        return self.arclentoparam_pt(l)
1988
 
 
1989
 
    def at_pt(self, param):
1990
 
        """return coordinates in pts of sub path at parameter value param
1991
 
 
1992
 
        The parameter param must be smaller or equal to the number of
1993
 
        segments in the normpath, otherwise None is returned.
1994
 
        """
1995
 
        normsubpathitem, itemparam = self._findnormsubpathitem(param)
1996
 
        return normsubpathitem.at_pt(itemparam)
1997
 
 
1998
 
    def at(self, param):
1999
 
        """return coordinates of sub path at parameter value param
2000
 
 
2001
 
        The parameter param must be smaller or equal to the number of
2002
 
        segments in the normpath, otherwise None is returned.
2003
 
        """
2004
 
        normsubpathitem, itemparam = self._findnormsubpathitem(param)
2005
 
        return normsubpathitem.at(itemparam)
2006
 
 
2007
 
    def bbox(self):
2008
 
        if self.normsubpathitems:
2009
 
            abbox = self.normsubpathitems[0].bbox()
2010
 
            for anormpathitem in self.normsubpathitems[1:]:
2011
 
                abbox += anormpathitem.bbox()
2012
 
            return abbox
2013
 
        else:
2014
 
            return None
2015
 
 
2016
 
    def begin_pt(self):
2017
 
        if not self.normsubpathitems and self.skippedline:
2018
 
            return self.skippedline.begin_pt()
2019
 
        return self.normsubpathitems[0].begin_pt()
2020
 
 
2021
 
    def begin(self):
2022
 
        if not self.normsubpathitems and self.skippedline:
2023
 
            return self.skippedline.begin()
2024
 
        return self.normsubpathitems[0].begin()
2025
 
 
2026
 
    def close(self):
2027
 
        if self.closed:
2028
 
            raise PathException("Cannot close already closed normsubpath")
2029
 
        if not self.normsubpathitems:
2030
 
            if self.skippedline is None:
2031
 
                raise PathException("Cannot close empty normsubpath")
2032
 
            else:
2033
 
                raise PathException("Normsubpath too short, cannot be closed")
2034
 
 
2035
 
        xs_pt, ys_pt = self.normsubpathitems[-1].end_pt()
2036
 
        xe_pt, ye_pt = self.normsubpathitems[0].begin_pt()
2037
 
        self.append(normline(xs_pt, ys_pt, xe_pt, ye_pt))
2038
 
 
2039
 
        # the append might have left a skippedline, which we have to remove
2040
 
        # from the end of the closed path
2041
 
        if self.skippedline:
2042
 
            self.normsubpathitems[-1] = self.normsubpathitems[-1].modified(xe_pt=self.skippedline.x1_pt,
2043
 
                                                                           ye_pt=self.skippedline.y1_pt)
2044
 
            self.skippedline = None
2045
 
 
2046
 
        self.closed = 1
2047
 
 
2048
 
    def curvradius_pt(self, param):
2049
 
        normsubpathitem, itemparam = self._findnormsubpathitem(param)
2050
 
        return normsubpathitem.curvradius_pt(itemparam)
2051
 
 
2052
 
    def end_pt(self):
2053
 
        if self.skippedline:
2054
 
            return self.skippedline.end_pt()
2055
 
        return self.normsubpathitems[-1].end_pt()
2056
 
 
2057
 
    def end(self):
2058
 
        if self.skippedline:
2059
 
            return self.skippedline.end()
2060
 
        return self.normsubpathitems[-1].end()
2061
 
 
2062
 
    def extend(self, normsubpathitems):
2063
 
        for normsubpathitem in normsubpathitems:
2064
 
            self.append(normsubpathitem)
2065
 
 
2066
 
    def intersect(self, other):
2067
 
        """intersect self with other normsubpath
2068
 
 
2069
 
        returns a tuple of lists consisting of the parameter values
2070
 
        of the intersection points of the corresponding normsubpath
2071
 
 
2072
 
        """
2073
 
        intersections_a = []
2074
 
        intersections_b = []
2075
 
        epsilon = min(self.epsilon, other.epsilon)
2076
 
        # Intersect all subpaths of self with the subpaths of other, possibly including
2077
 
        # one intersection point several times
2078
 
        for t_a, pitem_a  in enumerate(self.normsubpathitems):
2079
 
            for t_b, pitem_b in enumerate(other.normsubpathitems):
2080
 
                for intersection_a, intersection_b in pitem_a.intersect(pitem_b, epsilon):
2081
 
                    intersections_a.append(intersection_a + t_a)
2082
 
                    intersections_b.append(intersection_b + t_b)
2083
 
 
2084
 
        # although intersectipns_a are sorted for the different normsubpathitems,
2085
 
        # within a normsubpathitem, the ordering has to be ensured separately:
2086
 
        intersections = zip(intersections_a, intersections_b)
2087
 
        intersections.sort()
2088
 
        intersections_a = [a for a, b in intersections]
2089
 
        intersections_b = [b for a, b in intersections]
2090
 
 
2091
 
        # for symmetry reasons we enumerate intersections_a as well, although
2092
 
        # they are already sorted (note we do not need to sort intersections_a)
2093
 
        intersections_a = zip(intersections_a, range(len(intersections_a)))
2094
 
        intersections_b = zip(intersections_b, range(len(intersections_b)))
2095
 
        intersections_b.sort()
2096
 
 
2097
 
        # now we search for intersections points which are closer together than epsilon
2098
 
        # This task is handled by the following function
2099
 
        def closepoints(normsubpath, intersections):
2100
 
            split = normsubpath.split([intersection for intersection, index in intersections])
2101
 
            result = []
2102
 
            if normsubpath.closed:
2103
 
                # note that the number of segments of a closed path is off by one
2104
 
                # compared to an open path
2105
 
                i = 0
2106
 
                while i < len(split):
2107
 
                    splitnormsubpath = split[i]
2108
 
                    j = i
2109
 
                    while splitnormsubpath.isshort():
2110
 
                        ip1, ip2 = intersections[i-1][1], intersections[j][1]
2111
 
                        if ip1<ip2:
2112
 
                            result.append((ip1, ip2))
2113
 
                        else:
2114
 
                            result.append((ip2, ip1))
2115
 
                        j += 1
2116
 
                        if j == len(split):
2117
 
                            j = 0
2118
 
                        if j < len(split):
2119
 
                            splitnormsubpath = splitnormsubpath.joined(split[j])
2120
 
                        else:
2121
 
                            break
2122
 
                    i += 1
2123
 
            else:
2124
 
                i = 1
2125
 
                while i < len(split)-1:
2126
 
                    splitnormsubpath = split[i]
2127
 
                    j = i
2128
 
                    while splitnormsubpath.isshort():
2129
 
                        ip1, ip2 = intersections[i-1][1], intersections[j][1]
2130
 
                        if ip1<ip2:
2131
 
                            result.append((ip1, ip2))
2132
 
                        else:
2133
 
                            result.append((ip2, ip1))
2134
 
                        j += 1
2135
 
                        if j < len(split)-1:
2136
 
                            splitnormsubpath.join(split[j])
2137
 
                        else:
2138
 
                            break
2139
 
                    i += 1
2140
 
            return result
2141
 
 
2142
 
        closepoints_a = closepoints(self, intersections_a)
2143
 
        closepoints_b = closepoints(other, intersections_b)
2144
 
 
2145
 
        # map intersection point to lowest point which is equivalent to the
2146
 
        # point
2147
 
        equivalentpoints = list(range(len(intersections_a)))
2148
 
 
2149
 
        for closepoint_a in closepoints_a:
2150
 
            for closepoint_b in closepoints_b:
2151
 
                if closepoint_a == closepoint_b:
2152
 
                    for i in range(closepoint_a[1], len(equivalentpoints)):
2153
 
                        if equivalentpoints[i] == closepoint_a[1]:
2154
 
                            equivalentpoints[i] = closepoint_a[0]
2155
 
 
2156
 
        # determine the remaining intersection points
2157
 
        intersectionpoints = {}
2158
 
        for point in equivalentpoints:
2159
 
            intersectionpoints[point] = 1
2160
 
 
2161
 
        # build result
2162
 
        result = []
2163
 
        intersectionpointskeys = intersectionpoints.keys()
2164
 
        intersectionpointskeys.sort()
2165
 
        for point in intersectionpointskeys:
2166
 
            for intersection_a, index_a in intersections_a:
2167
 
                if index_a == point:
2168
 
                    result_a = intersection_a
2169
 
            for intersection_b, index_b in intersections_b:
2170
 
                if index_b == point:
2171
 
                    result_b = intersection_b
2172
 
            result.append((result_a, result_b))
2173
 
        # note that the result is sorted in a, since we sorted
2174
 
        # intersections_a in the very beginning
2175
 
 
2176
 
        return [x for x, y in result], [y for x, y in result]
2177
 
 
2178
 
    def isshort(self):
2179
 
        """return whether the subnormpath is shorter than epsilon"""
2180
 
        return not self.normsubpathitems
2181
 
 
2182
 
    def join(self, other):
2183
 
        for othernormpathitem in other.normsubpathitems:
2184
 
            self.append(othernormpathitem)
2185
 
        if other.skippedline is not None:
2186
 
            self.append(other.skippedline)
2187
 
 
2188
 
    def joined(self, other):
2189
 
        result = normsubpath(self.normsubpathitems, self.closed, self.epsilon)
2190
 
        result.skippedline = self.skippedline
2191
 
        result.join(other)
2192
 
        return result
2193
 
 
2194
 
    def range(self):
2195
 
        """return maximal parameter value, i.e. number of line/curve segments"""
2196
 
        return len(self.normsubpathitems)
2197
 
 
2198
 
    def reverse(self):
2199
 
        self.normsubpathitems.reverse()
2200
 
        for npitem in self.normsubpathitems:
2201
 
            npitem.reverse()
2202
 
 
2203
 
    def reversed(self):
2204
 
        nnormpathitems = []
2205
 
        for i in range(len(self.normsubpathitems)):
2206
 
            nnormpathitems.append(self.normsubpathitems[-(i+1)].reversed())
2207
 
        return normsubpath(nnormpathitems, self.closed)
2208
 
 
2209
 
    def split(self, params):
2210
 
        """split normsubpath at list of parameter values params and return list
2211
 
        of normsubpaths
2212
 
 
2213
 
        The parameter list params has to be sorted. Note that each element of
2214
 
        the resulting list is an open normsubpath.
2215
 
        """
2216
 
 
2217
 
        result = [normsubpath(epsilon=self.epsilon)]
2218
 
 
2219
 
        for normsubpathitem, itemparams in self._distributeparams(params):
2220
 
            splititems = normsubpathitem.split(itemparams)
2221
 
            result[-1].append(splititems[0])
2222
 
            result.extend([normsubpath([splititem], epsilon=self.epsilon) for splititem in splititems[1:]])
2223
 
 
2224
 
        if self.closed:
2225
 
            if params:
2226
 
                # join last and first segment together if the normsubpath was originally closed and it has been split
2227
 
                result[-1].normsubpathitems.extend(result[0].normsubpathitems)
2228
 
                result = result[-1:] + result[1:-1]
2229
 
            else:
2230
 
                # otherwise just close the copied path again
2231
 
                result[0].close()
2232
 
        return result
2233
 
 
2234
 
    def tangent(self, param, length=None):
2235
 
        normsubpathitem, itemparam = self._findnormsubpathitem(param)
2236
 
        tx_pt, ty_pt = normsubpathitem.at_pt(itemparam)
2237
 
        tdx_pt, tdy_pt = normsubpathitem.tangentvector_pt(itemparam)
2238
 
        if length is not None:
2239
 
            sfactor = unit.topt(length)/math.hypot(tdx_pt, tdy_pt)
2240
 
            tdx_pt *= sfactor
2241
 
            tdy_pt *= sfactor
2242
 
        return line_pt(tx_pt, ty_pt, tx_pt+tdx_pt, ty_pt+tdy_pt)
2243
 
 
2244
 
    def trafo(self, param):
2245
 
        normsubpathitem, itemparam = self._findnormsubpathitem(param)
2246
 
        return normsubpathitem.trafo(itemparam)
2247
 
 
2248
 
    def transform(self, trafo):
2249
 
        """transform sub path according to trafo"""
2250
 
        # note that we have to rebuild the path again since normsubpathitems
2251
 
        # may become shorter than epsilon and/or skippedline may become
2252
 
        # longer than epsilon
2253
 
        normsubpathitems = self.normsubpathitems
2254
 
        closed = self.closed
2255
 
        skippedline = self.skippedline
2256
 
        self.normsubpathitems = []
2257
 
        self.closed = 0
2258
 
        self.skippedline = None
2259
 
        for pitem in normsubpathitems:
2260
 
            self.append(pitem.transformed(trafo))
2261
 
        if closed:
2262
 
            self.close()
2263
 
        elif skippedline is not None:
2264
 
            self.append(skippedline.transformed(trafo))
2265
 
 
2266
 
    def transformed(self, trafo):
2267
 
        """return sub path transformed according to trafo"""
2268
 
        nnormsubpath = normsubpath(epsilon=self.epsilon)
2269
 
        for pitem in self.normsubpathitems:
2270
 
            nnormsubpath.append(pitem.transformed(trafo))
2271
 
        if self.closed:
2272
 
            nnormsubpath.close()
2273
 
        elif self.skippedline is not None:
2274
 
            nnormsubpath.append(skippedline.transformed(trafo))
2275
 
        return nnormsubpath
2276
 
 
2277
 
    def outputPS(self, file):
2278
 
        # if the normsubpath is closed, we must not output a normline at
2279
 
        # the end
2280
 
        if not self.normsubpathitems:
2281
 
            return
2282
 
        if self.closed and isinstance(self.normsubpathitems[-1], normline):
2283
 
            normsubpathitems = self.normsubpathitems[:-1]
2284
 
        else:
2285
 
            normsubpathitems = self.normsubpathitems
2286
 
        if normsubpathitems:
2287
 
            file.write("%g %g moveto\n" % self.begin_pt())
2288
 
            for anormpathitem in normsubpathitems:
2289
 
                anormpathitem.outputPS(file)
2290
 
        if self.closed:
2291
 
            file.write("closepath\n")
2292
 
 
2293
 
    def outputPDF(self, file):
2294
 
        # if the normsubpath is closed, we must not output a normline at
2295
 
        # the end
2296
 
        if not self.normsubpathitems:
2297
 
            return
2298
 
        if self.closed and isinstance(self.normsubpathitems[-1], normline):
2299
 
            normsubpathitems = self.normsubpathitems[:-1]
2300
 
        else:
2301
 
            normsubpathitems = self.normsubpathitems
2302
 
        if normsubpathitems:
2303
 
            file.write("%f %f m\n" % self.begin_pt())
2304
 
            for anormpathitem in normsubpathitems:
2305
 
                anormpathitem.outputPDF(file)
2306
 
        if self.closed:
2307
 
            file.write("h\n")
2308
 
 
2309
 
#
2310
 
# the normpath class
2311
 
#
2312
 
 
2313
 
class normpath(base.canvasitem):
2314
 
 
2315
 
    """normalized path
2316
 
 
2317
 
    A normalized path consists of a list of normalized sub paths.
2318
 
 
2319
 
    """
2320
 
 
2321
 
    def __init__(self, normsubpaths=None):
2322
 
        """ construct a normpath from another normpath passed as arg,
2323
 
        a path or a list of normsubpaths. An accuracy of epsilon pts
2324
 
        is used for numerical calculations.
2325
 
        """
2326
 
        if normsubpaths is None:
2327
 
            self.normsubpaths = []
2328
 
        else:
2329
 
            self.normsubpaths = normsubpaths
2330
 
            for subpath in normsubpaths:
2331
 
                assert isinstance(subpath, normsubpath), "only list of normsubpath instances allowed"
2332
 
 
2333
 
    def __add__(self, other):
2334
 
        result = normpath()
2335
 
        result.normsubpaths = self.normsubpaths + other.normpath().normsubpaths
2336
 
        return result
2337
 
 
2338
 
    def __getitem__(self, i):
2339
 
        return self.normsubpaths[i]
2340
 
 
2341
 
    def __iadd__(self, other):
2342
 
        self.normsubpaths += other.normpath().normsubpaths
2343
 
        return self
2344
 
 
2345
 
    def __len__(self):
2346
 
        return len(self.normsubpaths)
2347
 
 
2348
 
    def __str__(self):
2349
 
        return "normpath(%s)" % ", ".join(map(str, self.normsubpaths))
2350
 
 
2351
 
    def _findsubpath(self, param, arclen):
2352
 
        """return a tuple (subpath, rparam), where subpath is the subpath
2353
 
        containing the position specified by either param or arclen and rparam
2354
 
        is the corresponding parameter value in this subpath. 
2355
 
        """
2356
 
 
2357
 
        if param is not None and arclen is not None:
2358
 
            raise PathException("either param or arclen has to be specified, but not both")
2359
 
 
2360
 
        if param is not None:
2361
 
            try:
2362
 
                subpath, param = param
2363
 
            except TypeError:
2364
 
                # determine subpath from param 
2365
 
                normsubpathindex = 0
2366
 
                for normsubpath in self.normsubpaths[:-1]:
2367
 
                    normsubpathrange = normsubpath.range()
2368
 
                    if param < normsubpathrange+normsubpathindex:
2369
 
                        return normsubpath, param-normsubpathindex
2370
 
                    normsubpathindex += normsubpathrange
2371
 
                return self.normsubpaths[-1], param-normsubpathindex
2372
 
            try:
2373
 
                return self.normsubpaths[subpath], param
2374
 
            except IndexError:
2375
 
                raise PathException("subpath index out of range")
2376
 
 
2377
 
        # we have been passed an arclen (or a tuple (subpath, arclen))
2378
 
        try:
2379
 
            subpath, arclen = arclen
2380
 
        except:
2381
 
            # determine subpath from arclen
2382
 
            param = self.arclentoparam(arclen)
2383
 
            for normsubpath in self.normsubpaths[:-1]:
2384
 
                normsubpathrange = normsubpath.range()
2385
 
                if param <= normsubpathrange+normsubpathindex:
2386
 
                    return normsubpath, param-normsubpathindex
2387
 
                normsubpathindex += normsubpathrange
2388
 
            return self.normsubpaths[-1], param-normsubpathindex
2389
 
 
2390
 
        try:
2391
 
            normsubpath = self.normsubpaths[subpath]
2392
 
        except IndexError:
2393
 
            raise PathException("subpath index out of range")
2394
 
        return normsubpath, normsubpath.arclentoparam(arclen)
2395
 
 
2396
 
    def append(self, anormsubpath):
2397
 
        if isinstance(anormsubpath, normsubpath):
2398
 
            # the normsubpaths list can be appended by a normsubpath only
2399
 
            self.normsubpaths.append(anormsubpath)
2400
 
        else:
2401
 
            # ... but we are kind and allow for regular path items as well
2402
 
            # in order to make a normpath to behave more like a regular path
2403
 
 
2404
 
            for pathitem in anormsubpath._normalized(_pathcontext(self.normsubpaths[-1].begin_pt(),
2405
 
                                                                  self.normsubpaths[-1].end_pt())):
2406
 
                if isinstance(pathitem, closepath):
2407
 
                    self.normsubpaths[-1].close()
2408
 
                elif isinstance(pathitem, moveto_pt):
2409
 
                    self.normsubpaths.append(normsubpath([normline(pathitem.x_pt, pathitem.y_pt,
2410
 
                                                                   pathitem.x_pt, pathitem.y_pt)]))
2411
 
                else:
2412
 
                    self.normsubpaths[-1].append(pathitem)
2413
 
 
2414
 
    def arclen_pt(self):
2415
 
        """returns total arc length of normpath in pts"""
2416
 
        return sum([normsubpath.arclen_pt() for normsubpath in self.normsubpaths])
2417
 
 
2418
 
    def arclen(self):
2419
 
        """returns total arc length of normpath"""
2420
 
        return self.arclen_pt() * unit.t_pt
2421
 
 
2422
 
    def arclentoparam_pt(self, lengths):
2423
 
        rests = lengths[:]
2424
 
        allparams = [0] * len(lengths)
2425
 
 
2426
 
        for normsubpath in self.normsubpaths:
2427
 
            # we need arclen for knowing when all the parameters are done
2428
 
            # for lengths that are done: rests[i] is negative
2429
 
            # normsubpath._arclentoparam has to ignore such lengths
2430
 
            params, arclen = normsubpath._arclentoparam_pt(rests)
2431
 
            finis = 0 # number of lengths that are done
2432
 
            for i in range(len(rests)):
2433
 
                if rests[i] >= 0:
2434
 
                  rests[i] -= arclen
2435
 
                  allparams[i] += params[i]
2436
 
                else:
2437
 
                    finis += 1
2438
 
            if finis == len(rests): break
2439
 
 
2440
 
        if len(lengths) == 1: allparams = allparams[0]
2441
 
        return allparams
2442
 
 
2443
 
    def arclentoparam(self, lengths):
2444
 
        """returns the parameter value(s) matching the given length(s)
2445
 
 
2446
 
        all given lengths must be positive.
2447
 
        A length greater than the total arclength will give self.range()
2448
 
        """
2449
 
        l = [unit.topt(length) for length in helper.ensuresequence(lengths)]
2450
 
        return self.arclentoparam_pt(l)
2451
 
 
2452
 
    def at_pt(self, param=None, arclen=None):
2453
 
        """return coordinates in pts of path at either parameter value param
2454
 
        or arc length arclen.
2455
 
 
2456
 
        At discontinuities in the path, the limit from below is returned.
2457
 
        """
2458
 
        normsubpath, param = self._findsubpath(param, arclen)
2459
 
        return normsubpath.at_pt(param)
2460
 
 
2461
 
    def at(self, param=None, arclen=None):
2462
 
        """return coordinates of path at either parameter value param
2463
 
        or arc length arclen.
2464
 
 
2465
 
        At discontinuities in the path, the limit from below is returned
2466
 
        """
2467
 
        normsubpath, param = self._findsubpath(param, arclen)
2468
 
        return normsubpath.at(param)
2469
 
 
2470
 
    def bbox(self):
2471
 
        abbox = None
2472
 
        for normsubpath in self.normsubpaths:
2473
 
            nbbox =  normsubpath.bbox()
2474
 
            if abbox is None:
2475
 
                abbox = nbbox
2476
 
            elif nbbox:
2477
 
                abbox += nbbox
2478
 
        return abbox
2479
 
 
2480
 
    def begin_pt(self):
2481
 
        """return coordinates of first point of first subpath in path (in pts)"""
2482
 
        if self.normsubpaths:
2483
 
            return self.normsubpaths[0].begin_pt()
2484
 
        else:
2485
 
            raise PathException("cannot return first point of empty path")
2486
 
 
2487
 
    def begin(self):
2488
 
        """return coordinates of first point of first subpath in path"""
2489
 
        if self.normsubpaths:
2490
 
            return self.normsubpaths[0].begin()
2491
 
        else:
2492
 
            raise PathException("cannot return first point of empty path")
2493
 
 
2494
 
    def curvradius_pt(self, param=None, arclen=None):
2495
 
        """Returns the curvature radius in pts (or None if infinite)
2496
 
        at parameter param or arc length arclen.  This is the inverse
2497
 
        of the curvature at this parameter
2498
 
 
2499
 
        Please note that this radius can be negative or positive,
2500
 
        depending on the sign of the curvature"""
2501
 
        normsubpath, param = self._findsubpath(param, arclen)
2502
 
        return normsubpath.curvradius_pt(param)
2503
 
 
2504
 
    def curvradius(self, param=None, arclen=None):
2505
 
        """Returns the curvature radius (or None if infinite) at
2506
 
        parameter param or arc length arclen.  This is the inverse of
2507
 
        the curvature at this parameter
2508
 
 
2509
 
        Please note that this radius can be negative or positive,
2510
 
        depending on the sign of the curvature"""
2511
 
        radius = self.curvradius_pt(param, arclen)
2512
 
        if radius is not None:
2513
 
            radius = radius * unit.t_pt
2514
 
        return radius
2515
 
 
2516
 
    def end_pt(self):
2517
 
        """return coordinates of last point of last subpath in path (in pts)"""
2518
 
        if self.normsubpaths:
2519
 
            return self.normsubpaths[-1].end_pt()
2520
 
        else:
2521
 
            raise PathException("cannot return last point of empty path")
2522
 
 
2523
 
    def end(self):
2524
 
        """return coordinates of last point of last subpath in path"""
2525
 
        if self.normsubpaths:
2526
 
            return self.normsubpaths[-1].end()
2527
 
        else:
2528
 
            raise PathException("cannot return last point of empty path")
2529
 
 
2530
 
    def extend(self, normsubpaths):
2531
 
        for anormsubpath in normsubpaths:
2532
 
            # use append to properly handle regular path items as well as normsubpaths
2533
 
            self.append(anormsubpath)
2534
 
 
2535
 
    def join(self, other):
2536
 
        if not self.normsubpaths:
2537
 
            raise PathException("cannot join to end of empty path")
2538
 
        if self.normsubpaths[-1].closed:
2539
 
            raise PathException("cannot join to end of closed sub path")
2540
 
        other = other.normpath()
2541
 
        if not other.normsubpaths:
2542
 
            raise PathException("cannot join empty path")
2543
 
 
2544
 
        self.normsubpaths[-1].normsubpathitems += other.normsubpaths[0].normsubpathitems
2545
 
        self.normsubpaths += other.normsubpaths[1:]
2546
 
 
2547
 
    def joined(self, other):
2548
 
        # NOTE we skip a deep copy for performance reasons
2549
 
        result = normpath(self.normsubpaths)
2550
 
        result.join(other)
2551
 
        return result
2552
 
 
2553
 
    # << operator also designates joining
2554
 
    __lshift__ = joined
2555
 
 
2556
 
    def intersect(self, other):
2557
 
        """intersect self with other path
2558
 
 
2559
 
        returns a tuple of lists consisting of the parameter values
2560
 
        of the intersection points of the corresponding normpath
2561
 
 
2562
 
        """
2563
 
        other = other.normpath()
2564
 
        
2565
 
        # here we build up the result
2566
 
        intersections = ([], [])
2567
 
 
2568
 
        # Intersect all normsubpaths of self with the normsubpaths of
2569
 
        # other.
2570
 
        for ia, normsubpath_a in enumerate(self.normsubpaths):
2571
 
            for ib, normsubpath_b in enumerate(other.normsubpaths):
2572
 
                for intersection in zip(*normsubpath_a.intersect(normsubpath_b)):
2573
 
                    intersections[0].append((ia, intersection[0]))
2574
 
                    intersections[1].append((ib, intersection[1]))
2575
 
        return intersections
2576
 
 
2577
 
    def normpath(self):
2578
 
        return self
2579
 
 
2580
 
    def range(self):
2581
 
        """return maximal value for parameter value param"""
2582
 
        return sum([normsubpath.range() for normsubpath in self.normsubpaths])
2583
 
 
2584
 
    def reverse(self):
2585
 
        """reverse path"""
2586
 
        self.normsubpaths.reverse()
2587
 
        for normsubpath in self.normsubpaths:
2588
 
            normsubpath.reverse()
2589
 
 
2590
 
    def reversed(self):
2591
 
        """return reversed path"""
2592
 
        nnormpath = normpath()
2593
 
        for i in range(len(self.normsubpaths)):
2594
 
            nnormpath.normsubpaths.append(self.normsubpaths[-(i+1)].reversed())
2595
 
        return nnormpath
2596
 
 
2597
 
    def split(self, params):
2598
 
        """split path at parameter values params
2599
 
 
2600
 
        Note that the parameter list has to be sorted.
2601
 
 
2602
 
        """
2603
 
 
2604
 
        # check whether parameter list is really sorted
2605
 
        sortedparams = list(params)
2606
 
        sortedparams.sort()
2607
 
        if sortedparams != list(params):
2608
 
            raise ValueError("split parameter list params has to be sorted")
2609
 
 
2610
 
        # convert to tuple 
2611
 
        tparams = []
2612
 
        for param in params:
2613
 
            tparams.append(self._findsubpath(param, None))
2614
 
 
2615
 
        # we construct this list of normpaths
2616
 
        result = []
2617
 
 
2618
 
        # the currently built up normpath
2619
 
        np = normpath()
2620
 
 
2621
 
        for subpath in self.normsubpaths:
2622
 
            splitnormsubpaths = subpath.split([param for normsubpath, param in tparams if normsubpath is subpath])
2623
 
            np.normsubpaths.append(splitnormsubpaths[0])
2624
 
            for normsubpath in splitnormsubpaths[1:]:
2625
 
                result.append(np)
2626
 
                np = normpath([normsubpath])
2627
 
 
2628
 
        result.append(np)
2629
 
        return result
2630
 
 
2631
 
    def tangent(self, param=None, arclen=None, length=None):
2632
 
        """return tangent vector of path at either parameter value param
2633
 
        or arc length arclen.
2634
 
 
2635
 
        At discontinuities in the path, the limit from below is returned.
2636
 
        If length is not None, the tangent vector will be scaled to
2637
 
        the desired length.
2638
 
        """
2639
 
        normsubpath, param = self._findsubpath(param, arclen)
2640
 
        return normsubpath.tangent(param, length)
2641
 
 
2642
 
    def transform(self, trafo):
2643
 
        """transform path according to trafo"""
2644
 
        for normsubpath in self.normsubpaths:
2645
 
            normsubpath.transform(trafo)
2646
 
 
2647
 
    def transformed(self, trafo):
2648
 
        """return path transformed according to trafo"""
2649
 
        return normpath([normsubpath.transformed(trafo) for normsubpath in self.normsubpaths])
2650
 
 
2651
 
    def trafo(self, param=None, arclen=None):
2652
 
        """return transformation at either parameter value param or arc length arclen"""
2653
 
        normsubpath, param = self._findsubpath(param, arclen)
2654
 
        return normsubpath.trafo(param)
2655
 
 
2656
 
    def outputPS(self, file):
2657
 
        for normsubpath in self.normsubpaths:
2658
 
            normsubpath.outputPS(file)
2659
 
 
2660
 
    def outputPDF(self, file):
2661
 
        for normsubpath in self.normsubpaths:
2662
 
            normsubpath.outputPDF(file)
2663
 
 
 
1278
    """circle with center (x,y) and radius"""
 
1279
 
 
1280
    def __init__(self, x, y, radius, **kwargs):
 
1281
        circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
 
1282
 
 
1283
 
 
1284
class ellipse(ellipse_pt):
 
1285
 
 
1286
    """ellipse with center (x, y), the two axes (a, b),
 
1287
    and the angle angle of the first axis"""
 
1288
 
 
1289
    def __init__(self, x, y, a, b, angle, **kwargs):
 
1290
        ellipse_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(a), unit.topt(b), angle, **kwargs)