2
# -*- coding: ISO-8859-1 -*-
5
# Copyright (C) 2002-2004 J�rg Lehmann <joergl@users.sourceforge.net>
6
# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7
# Copyright (C) 2002-2004 Andr� Wobst <wobsta@users.sourceforge.net>
9
# This file is part of PyX (http://pyx.sourceforge.net/).
11
# PyX is free software; you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation; either version 2 of the License, or
14
# (at your option) any later version.
16
# PyX is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
# GNU General Public License for more details.
21
# You should have received a copy of the GNU General Public License
22
# along with PyX; if not, write to the Free Software
23
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
# - 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)
32
from math import cos, sin, pi
34
from math import radians, degrees
36
# fallback implementation for Python 2.1 and below
37
def radians(x): return x*pi/180
38
def degrees(x): return x*180/pi
39
import base, bbox, trafo, unit, helper
44
# fallback implementation for Python 2.2. and below
46
return reduce(lambda x, y: x+y, list, 0)
51
# fallback implementation for Python 2.2. and below
53
return zip(xrange(len(list)), list)
55
# use new style classes when possible
58
################################################################################
60
# global epsilon (default precision of normsubpaths)
63
def set(epsilon=None):
64
if epsilon is not None:
67
################################################################################
68
# Bezier helper functions
69
################################################################################
71
def _arctobcurve(x_pt, y_pt, r_pt, phi1, phi2):
72
"""generate the best bezier curve corresponding to an arc segment"""
76
if dphi==0: return None
78
# the two endpoints should be clear
79
x0_pt, y0_pt = x_pt+r_pt*cos(phi1), y_pt+r_pt*sin(phi1)
80
x3_pt, y3_pt = x_pt+r_pt*cos(phi2), y_pt+r_pt*sin(phi2)
82
# optimal relative distance along tangent for second and third
84
l = r_pt*4*(1-cos(dphi/2))/(3*sin(dphi/2))
86
x1_pt, y1_pt = x0_pt-l*sin(phi1), y0_pt+l*cos(phi1)
87
x2_pt, y2_pt = x3_pt+l*sin(phi2), y3_pt-l*cos(phi2)
89
return normcurve(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt)
92
def _arctobezierpath(x_pt, y_pt, r_pt, phi1, phi2, dphimax=45):
97
dphimax = radians(dphimax)
100
# guarantee that phi2>phi1 ...
101
phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
103
# ... or remove unnecessary multiples of 2*pi
104
phi2 = phi2 - (math.floor((phi2-phi1)/(2*pi))-1)*2*pi
106
if r_pt == 0 or phi1-phi2 == 0: return []
108
subdivisions = abs(int((1.0*(phi1-phi2))/dphimax))+1
110
dphi = (1.0*(phi2-phi1))/subdivisions
112
for i in range(subdivisions):
113
apath.append(_arctobcurve(x_pt, y_pt, r_pt, phi1+i*dphi, phi1+(i+1)*dphi))
118
# we define one exception
121
class PathException(Exception): pass
123
################################################################################
124
# _pathcontext: context during walk along path
125
################################################################################
129
"""context during walk along path"""
131
__slots__ = "currentpoint", "currentsubpath"
133
def __init__(self, currentpoint=None, currentsubpath=None):
134
""" initialize context
136
currentpoint: position of current point
137
currentsubpath: position of first point of current subpath
141
self.currentpoint = currentpoint
142
self.currentsubpath = currentsubpath
144
################################################################################
145
# pathitem: element of a PS style path
146
################################################################################
148
class pathitem(base.canvasitem):
150
"""element of a PS style path"""
152
def _updatecontext(self, context):
153
"""update context of during walk along pathitem
155
changes context in place
160
def _bbox(self, context):
161
"""calculate bounding box of pathitem
163
context: context of pathitem
165
returns bounding box of pathitem (in given context)
167
Important note: all coordinates in bbox, currentpoint, and
168
currrentsubpath have to be floats (in unit.topt)
173
def _normalized(self, context):
174
"""returns list of normalized version of pathitem
176
context: context of pathitem
178
Returns the path converted into a list of closepath, moveto_pt,
179
normline, or normcurve instances.
184
def outputPS(self, file):
185
"""write PS code corresponding to pathitem to file"""
188
def outputPDF(self, file):
189
"""write PDF code corresponding to pathitem to file"""
195
# Each one comes in two variants:
196
# - one which requires the coordinates to be already in pts (mainly
197
# used for internal purposes)
198
# - another which accepts arbitrary units
200
class closepath(pathitem):
202
"""Connect subpath back to its starting point"""
209
def _updatecontext(self, context):
210
context.currentpoint = None
211
context.currentsubpath = None
213
def _bbox(self, context):
214
x0_pt, y0_pt = context.currentpoint
215
x1_pt, y1_pt = context.currentsubpath
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))
220
def _normalized(self, context):
223
def outputPS(self, file):
224
file.write("closepath\n")
226
def outputPDF(self, file):
230
class moveto_pt(pathitem):
232
"""Set current point to (x_pt, y_pt) (coordinates in pts)"""
234
__slots__ = "x_pt", "y_pt"
236
def __init__(self, x_pt, y_pt):
241
return "%g %g moveto" % (self.x_pt, self.y_pt)
243
def _updatecontext(self, context):
244
context.currentpoint = self.x_pt, self.y_pt
245
context.currentsubpath = self.x_pt, self.y_pt
247
def _bbox(self, context):
250
def _normalized(self, context):
251
return [moveto_pt(self.x_pt, self.y_pt)]
253
def outputPS(self, file):
254
file.write("%g %g moveto\n" % (self.x_pt, self.y_pt) )
256
def outputPDF(self, file):
257
file.write("%f %f m\n" % (self.x_pt, self.y_pt) )
260
class lineto_pt(pathitem):
262
"""Append straight line to (x_pt, y_pt) (coordinates in pts)"""
264
__slots__ = "x_pt", "y_pt"
266
def __init__(self, x_pt, y_pt):
271
return "%g %g lineto" % (self.x_pt, self.y_pt)
273
def _updatecontext(self, context):
274
context.currentsubpath = context.currentsubpath or context.currentpoint
275
context.currentpoint = self.x_pt, self.y_pt
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))
283
def _normalized(self, context):
284
return [normline(context.currentpoint[0], context.currentpoint[1], self.x_pt, self.y_pt)]
286
def outputPS(self, file):
287
file.write("%g %g lineto\n" % (self.x_pt, self.y_pt) )
289
def outputPDF(self, file):
290
file.write("%f %f l\n" % (self.x_pt, self.y_pt) )
293
class curveto_pt(pathitem):
295
"""Append curveto (coordinates in pts)"""
297
__slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
299
def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
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)
312
def _updatecontext(self, context):
313
context.currentsubpath = context.currentsubpath or context.currentpoint
314
context.currentpoint = self.x3_pt, self.y3_pt
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))
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)]
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 ) )
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 ) )
339
class rmoveto_pt(pathitem):
341
"""Perform relative moveto (coordinates in pts)"""
343
__slots__ = "dx_pt", "dy_pt"
345
def __init__(self, dx_pt, dy_pt):
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
354
def _bbox(self, context):
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)]
362
def outputPS(self, file):
363
file.write("%g %g rmoveto\n" % (self.dx_pt, self.dy_pt) )
366
class rlineto_pt(pathitem):
368
"""Perform relative lineto (coordinates in pts)"""
370
__slots__ = "dx_pt", "dy_pt"
372
def __init__(self, dx_pt, dy_pt):
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)
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))
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)]
394
def outputPS(self, file):
395
file.write("%g %g rlineto\n" % (self.dx_pt, self.dy_pt) )
398
class rcurveto_pt(pathitem):
400
"""Append rcurveto (coordinates in pts)"""
402
__slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
404
def __init__(self, dx1_pt, dy1_pt, dx2_pt, dy2_pt, dx3_pt, dy3_pt):
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 ) )
417
def _updatecontext(self, context):
418
x3_pt = context.currentpoint[0]+self.dx3_pt
419
y3_pt = context.currentpoint[1]+self.dy3_pt
421
context.currentsubpath = context.currentsubpath or context.currentpoint
422
context.currentpoint = x3_pt, y3_pt
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))
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)]
443
class arc_pt(pathitem):
445
"""Append counterclockwise arc (coordinates in pts)"""
447
__slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
449
def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
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)))
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)))
466
def _updatecontext(self, context):
467
if context.currentpoint:
468
context.currentsubpath = context.currentsubpath or context.currentpoint
470
# we assert that currentsubpath is also None
471
context.currentsubpath = self._sarc()
473
context.currentpoint = self._earc()
475
def _bbox(self, context):
476
phi1 = radians(self.angle1)
477
phi2 = radians(self.angle2)
479
# starting end end point of arc segment
480
sarcx_pt, sarcy_pt = self._sarc()
481
earcx_pt, earcy_pt = self._earc()
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.
489
# guarantee that phi2>phi1
490
phi2 = phi2 + (math.floor((phi1-phi2)/(2*pi))+1)*2*pi
492
# next minimum of cos(phi) looking from phi1 in counterclockwise
493
# direction: 2*pi*floor((phi1-pi)/(2*pi)) + 3*pi
495
if phi2 < (2*math.floor((phi1-pi)/(2*pi))+3)*pi:
496
minarcx_pt = min(sarcx_pt, earcx_pt)
498
minarcx_pt = self.x_pt-self.r_pt
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
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)
506
minarcy_pt = self.y_pt-self.r_pt
508
# next maximum of cos(phi) looking from phi1 in counterclockwise
509
# direction: 2*pi*floor((phi1)/(2*pi))+2*pi
511
if phi2 < (2*math.floor((phi1)/(2*pi))+2)*pi:
512
maxarcx_pt = max(sarcx_pt, earcx_pt)
514
maxarcx_pt = self.x_pt+self.r_pt
516
# next maximum of sin(phi) looking from phi1 in counterclockwise
517
# direction: 2*pi*floor((phi1-pi/2)/(2*pi)) + 1/2*pi
519
if phi2 < (2*math.floor((phi1-pi/2)/(2*pi))+5.0/2)*pi:
520
maxarcy_pt = max(sarcy_pt, earcy_pt)
522
maxarcy_pt = self.y_pt+self.r_pt
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
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)
537
return bbox.bbox_pt(minarcx_pt, minarcy_pt, maxarcx_pt, maxarcy_pt)
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)
545
# convert to list of curvetos omitting movetos
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))
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
561
return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
563
def outputPS(self, file):
564
file.write("%g %g %g %g %g arc\n" % ( self.x_pt, self.y_pt,
570
class arcn_pt(pathitem):
572
"""Append clockwise arc (coordinates in pts)"""
574
__slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
576
def __init__(self, x_pt, y_pt, r_pt, angle1, angle2):
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)))
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)))
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()
599
context.currentpoint = self._earc()
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
608
# Hence, we first compute the bbox of the arc without this line:
610
a = arc_pt(self.x_pt, self.y_pt, self.r_pt,
614
sarcx_pt, sarcy_pt = self._sarc()
615
arcbb = a._bbox(_pathcontext())
617
# Then, we repeat the logic from arc.bbox, but with interchanged
618
# start and end points of the arc
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
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)
635
# convert to list of curvetos omitting movetos
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))
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
651
return [moveto_pt(sarcx_pt, sarcy_pt)] + nbarc
654
def outputPS(self, file):
655
file.write("%g %g %g %g %g arcn\n" % ( self.x_pt, self.y_pt,
661
class arct_pt(pathitem):
663
"""Append tangent arc (coordinates in pts)"""
665
__slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r_pt"
667
def __init__(self, x1_pt, y1_pt, x2_pt, y2_pt, r_pt):
674
def _path(self, currentpoint, currentsubpath):
675
"""returns new currentpoint, currentsubpath and path consisting
676
of arc and/or line which corresponds to arct
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
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)
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)
693
# intersection angle between two tangents
694
alpha = math.acos((dx1_pt*dx2_pt+dy1_pt*dy2_pt)/(l1*l2))
696
if math.fabs(sin(alpha)) >= 1e-15 and 1.0+self.r_pt != 1.0:
697
cotalpha2 = 1.0/math.tan(alpha/2)
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
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)
710
# angle around which arc is centered
712
phi = degrees(math.atan2(ry_pt, rx_pt))
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
717
# half angular width of arc
718
deltaphi = 90*(1-alpha/pi)
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))
724
# now we are in the position to construct the path
725
p = path(moveto_pt(*currentpoint))
728
p.append(arc_pt(mx_pt, my_pt, self.r_pt, phi-deltaphi, phi+deltaphi))
730
p.append(arcn_pt(mx_pt, my_pt, self.r_pt, phi+deltaphi, phi-deltaphi))
732
return ( (xt2_pt, yt2_pt),
733
currentsubpath or (xt2_pt, yt2_pt),
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) )
742
def _updatecontext(self, context):
743
result = self._path(context.currentpoint, context.currentsubpath)
744
context.currentpoint, context.currentsubpath = result[:2]
746
def _bbox(self, context):
747
return self._path(context.currentpoint, context.currentsubpath)[2].bbox()
749
def _normalized(self, context):
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,
759
# now the pathitems that convert from user coordinates to pts
762
class moveto(moveto_pt):
764
"""Set current point to (x, y)"""
766
__slots__ = "x_pt", "y_pt"
768
def __init__(self, x, y):
769
moveto_pt.__init__(self, unit.topt(x), unit.topt(y))
772
class lineto(lineto_pt):
774
"""Append straight line to (x, y)"""
776
__slots__ = "x_pt", "y_pt"
778
def __init__(self, x, y):
779
lineto_pt.__init__(self, unit.topt(x), unit.topt(y))
782
class curveto(curveto_pt):
786
__slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
788
def __init__(self, x1, y1, x2, y2, x3, y3):
789
curveto_pt.__init__(self,
790
unit.topt(x1), unit.topt(y1),
791
unit.topt(x2), unit.topt(y2),
792
unit.topt(x3), unit.topt(y3))
794
class rmoveto(rmoveto_pt):
796
"""Perform relative moveto"""
798
__slots__ = "dx_pt", "dy_pt"
800
def __init__(self, dx, dy):
801
rmoveto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
804
class rlineto(rlineto_pt):
806
"""Perform relative lineto"""
808
__slots__ = "dx_pt", "dy_pt"
810
def __init__(self, dx, dy):
811
rlineto_pt.__init__(self, unit.topt(dx), unit.topt(dy))
814
class rcurveto(rcurveto_pt):
816
"""Append rcurveto"""
818
__slots__ = "dx1_pt", "dy1_pt", "dx2_pt", "dy2_pt", "dx3_pt", "dy3_pt"
820
def __init__(self, dx1, dy1, dx2, dy2, dx3, dy3):
821
rcurveto_pt.__init__(self,
822
unit.topt(dx1), unit.topt(dy1),
823
unit.topt(dx2), unit.topt(dy2),
824
unit.topt(dx3), unit.topt(dy3))
829
"""Append clockwise arc"""
831
__slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
833
def __init__(self, x, y, r, angle1, angle2):
834
arcn_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
839
"""Append counterclockwise arc"""
841
__slots__ = "x_pt", "y_pt", "r_pt", "angle1", "angle2"
843
def __init__(self, x, y, r, angle1, angle2):
844
arc_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(r), angle1, angle2)
849
"""Append tangent arc"""
851
__slots__ = "x1_pt", "y1_pt", "x2_pt", "y2_pt", "r"
853
def __init__(self, x1, y1, x2, y2, r):
854
arct_pt.__init__(self, unit.topt(x1), unit.topt(y1),
855
unit.topt(x2), unit.topt(y2), unit.topt(r))
858
# "combined" pathitems provided for performance reasons
861
class multilineto_pt(pathitem):
863
"""Perform multiple linetos (coordinates in pts)"""
865
__slots__ = "points_pt"
867
def __init__(self, points_pt):
868
self.points_pt = points_pt
870
def _updatecontext(self, context):
871
context.currentsubpath = context.currentsubpath or context.currentpoint
872
context.currentpoint = self.points_pt[-1]
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))
882
def _normalized(self, context):
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
890
def outputPS(self, file):
891
for point_pt in self.points_pt:
892
file.write("%g %g lineto\n" % point_pt )
894
def outputPDF(self, file):
895
for point_pt in self.points_pt:
896
file.write("%f %f l\n" % point_pt )
899
class multicurveto_pt(pathitem):
901
"""Perform multiple curvetos (coordinates in pts)"""
903
__slots__ = "points_pt"
905
def __init__(self, points_pt):
906
self.points_pt = points_pt
908
def _updatecontext(self, context):
909
context.currentsubpath = context.currentsubpath or context.currentpoint
910
context.currentpoint = self.points_pt[-1]
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))
924
def _normalized(self, context):
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))
929
x0_pt, y0_pt = point_pt[4:]
932
def outputPS(self, file):
933
for point_pt in self.points_pt:
934
file.write("%g %g %g %g %g %g curveto\n" % point_pt)
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)
941
################################################################################
942
# path: PS style path
943
################################################################################
945
class path(base.canvasitem):
951
def __init__(self, *args):
952
if len(args)==1 and isinstance(args[0], path):
953
self.path = args[0].path
955
self.path = list(args)
957
def __add__(self, other):
958
return path(*(self.path+other.path))
960
def __iadd__(self, other):
961
self.path += other.path
964
def __getitem__(self, i):
968
return len(self.path)
970
def append(self, pathitem):
971
self.path.append(pathitem)
974
"""returns total arc length of path in pts"""
975
return self.normpath().arclen_pt()
978
"""returns total arc length of path"""
979
return self.normpath().arclen()
981
def arclentoparam(self, lengths):
982
"""returns the parameter value(s) matching the given length(s)"""
983
return self.normpath().arclentoparam(lengths)
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.
989
At discontinuities in the path, the limit from below is returned
991
return self.normpath().at_pt(param, arclen)
993
def at(self, param=None, arclen=None):
994
"""return coordinates of path at either parameter value param
995
or arc length arclen.
997
At discontinuities in the path, the limit from below is returned
999
return self.normpath().at(param, arclen)
1002
context = _pathcontext()
1005
for pitem in self.path:
1006
nbbox = pitem._bbox(context)
1007
pitem._updatecontext(context)
1016
"""return coordinates of first point of first subpath in path (in pts)"""
1017
return self.normpath().begin_pt()
1020
"""return coordinates of first point of first subpath in path"""
1021
return self.normpath().begin()
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
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)
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
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)
1042
"""return coordinates of last point of last subpath in path (in pts)"""
1043
return self.normpath().end_pt()
1046
"""return coordinates of last point of last subpath in path"""
1047
return self.normpath().end()
1049
def extend(self, pathitems):
1050
self.path.extend(pathitems)
1052
def joined(self, other):
1053
"""return path consisting of self and other joined together"""
1054
return self.normpath().joined(other)
1056
# << operator also designates joining
1059
def intersect(self, other):
1060
"""intersect normpath corresponding to self with other path"""
1061
return self.normpath().intersect(other)
1063
def normpath(self, epsilon=None):
1064
"""converts the path into a normpath"""
1065
# use global epsilon if it is has not been specified
1068
# split path in sub paths
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 = []
1088
currentsubpathitems.append(npitem)
1089
pitem._updatecontext(context)
1091
if currentsubpathitems:
1092
# append open sub path
1093
subpaths.append(normsubpath(currentsubpathitems, 0, epsilon))
1094
return normpath(subpaths)
1097
"""return maximal value for parameter value t for corr. normpath"""
1098
return self.normpath().range()
1101
"""return reversed path"""
1102
return self.normpath().reversed()
1104
def split(self, params):
1105
"""return corresponding normpaths split at parameter values params"""
1106
return self.normpath().split(params)
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.
1112
At discontinuities in the path, the limit from below is returned.
1113
If length is not None, the tangent vector will be scaled to
1116
return self.normpath().tangent(param, arclen, length)
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)
1122
def transformed(self, trafo):
1123
"""return transformed path"""
1124
return self.normpath().transformed(trafo)
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)
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)
1146
################################################################################
1147
# some special kinds of path, again in two variants
1148
################################################################################
1150
class line_pt(path):
1152
"""straight line from (x1_pt, y1_pt) to (x2_pt, y2_pt) (coordinates in pts)"""
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))
1158
class curve_pt(path):
1160
"""Bezier curve with control points (x0_pt, y1_pt),..., (x3_pt, y3_pt)
1161
(coordinates in pts)"""
1163
def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1165
moveto_pt(x0_pt, y0_pt),
1166
curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1169
class rect_pt(path):
1171
"""rectangle at position (x,y) with width and height (coordinates in pts)"""
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),
1181
class circle_pt(path):
1183
"""circle with center (x,y) and radius"""
1185
def __init__(self, x, y, radius):
1186
path.__init__(self, arc_pt(x, y, radius, 0, 360),
1190
class line(line_pt):
1192
"""straight line from (x1, y1) to (x2, y2)"""
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))
1200
class curve(curve_pt):
1202
"""Bezier curve with control points (x0, y1),..., (x3, y3)"""
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))
1212
class rect(rect_pt):
1214
"""rectangle at position (x,y) with width and height"""
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))
1222
class circle(circle_pt):
1224
"""circle with center (x,y) and radius"""
1226
def __init__(self, x, y, radius):
1227
circle_pt.__init__(self,
1228
unit.topt(x), unit.topt(y),
1231
################################################################################
1232
# normpath and corresponding classes
1233
################################################################################
1235
# two helper functions for the intersection of normsubpathitems
1237
def _intersectnormcurves(a, a_t0, a_t1, b, b_t0, b_t1, epsilon=1e-5):
1238
"""intersect two bpathitems
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
1246
# intersection of bboxes is a necessary criterium for intersection
1247
if not a.bbox().intersects(b.bbox()): return []
1249
if not a.isstraight(epsilon):
1250
(aa, ab) = a.midpointsplit()
1251
a_tm = 0.5*(a_t0+a_t1)
1253
if not b.isstraight(epsilon):
1254
(ba, bb) = b.midpointsplit()
1255
b_tm = 0.5*(b_t0+b_t1)
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) )
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) )
1271
if not b.isstraight(epsilon):
1272
(ba, bb) = b.midpointsplit()
1273
b_tm = 0.5*(b_t0+b_t1)
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) )
1280
# no more subdivisions of either a or b
1281
# => try to intersect a and b as straight line segments
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
1288
det = b_deltax*a_deltay - b_deltay*a_deltax
1290
ba_deltax0_pt = b.x0_pt - a.x0_pt
1291
ba_deltay0_pt = b.y0_pt - a.y0_pt
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:
1299
# check for intersections out of bound
1300
if not (0<=a_t<=1 and 0<=b_t<=1): return []
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) ) ]
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"""
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
1317
det = 1.0*(b_deltax_pt * a_deltay_pt - b_deltay_pt * a_deltax_pt)
1319
ba_deltax0_pt = b.x0_pt - a.x0_pt
1320
ba_deltay0_pt = b.y0_pt - a.y0_pt
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:
1328
# check for intersections out of bound
1329
if not (0<=a_t<=1 and 0<=b_t<=1): return []
1331
# return parameters of the intersection
1332
return [( a_t, b_t)]
1335
# normsubpathitem: normalized element
1338
class normsubpathitem:
1340
"""element of a normalized sub path"""
1343
"""returns coordinates of point in pts at parameter t (0<=t<=1) """
1346
def arclen_pt(self, epsilon=1e-5):
1347
"""returns arc length of normsubpathitem in pts with given accuracy epsilon"""
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
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
1359
# Note: _arclentoparam returns both, parameters and total lengths
1360
# while arclentoparam returns only parameters
1364
"""return bounding box of normsubpathitem"""
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
1371
Please note that this radius can be negative or positive,
1372
depending on the sign of the curvature"""
1375
def intersect(self, other, epsilon=1e-5):
1376
"""intersect self with other normsubpathitem"""
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"""
1385
"""return reversed normsubpathitem"""
1388
def split(self, parameters):
1389
"""splits normsubpathitem
1391
parameters: list of parameter values (0<=t<=1) at which to split
1393
returns None or list of tuple of normsubpathitems corresponding to
1394
the orginal normsubpathitem.
1399
def tangentvector_pt(self, t):
1400
"""returns tangent vector of normsubpathitem in pts at parameter t (0<=t<=1)"""
1403
def transformed(self, trafo):
1404
"""return transformed normsubpathitem according to trafo"""
1407
def outputPS(self, file):
1408
"""write PS code corresponding to normsubpathitem to file"""
1411
def outputPS(self, file):
1412
"""write PDF code corresponding to normsubpathitem to file"""
1416
# there are only two normsubpathitems: normline and normcurve
1419
class normline(normsubpathitem):
1421
"""Straight line from (x0_pt, y0_pt) to (x1_pt, y1_pt) (coordinates in pts)"""
1423
__slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt"
1425
def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt):
1432
return "normline(%g, %g, %g, %g)" % (self.x0_pt, self.y0_pt, self.x1_pt, self.y1_pt)
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)
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)
1446
def arclen_pt(self, epsilon=1e-5):
1447
return math.hypot(self.x0_pt-self.x1_pt, self.y0_pt-self.y1_pt)
1450
return self.x0_pt+(self.x1_pt-self.x0_pt)*t, self.y0_pt+(self.y1_pt-self.y0_pt)*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
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))
1460
return self.x0_pt, self.y0_pt
1463
return self.x0_pt * unit.t_pt, self.y0_pt * unit.t_pt
1465
def curvradius_pt(self, param):
1469
return self.x1_pt, self.y1_pt
1472
return self.x1_pt * unit.t_pt, self.y1_pt * unit.t_pt
1474
def intersect(self, other, epsilon=1e-5):
1475
if isinstance(other, normline):
1476
return _intersectnormlines(self, other)
1478
return _intersectnormcurves(self._normcurve(), 0, 1, other, 0, 1, epsilon)
1480
def isstraight(self, epsilon):
1483
def modified(self, xs_pt=None, ys_pt=None, xe_pt=None, ye_pt=None):
1492
return normline(xs_pt, ys_pt, xe_pt, ye_pt)
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
1498
return normline(self.x1_pt, self.y1_pt, self.x0_pt, self.y0_pt)
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
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
1515
def tangentvector_pt(self, param):
1516
return self.x1_pt-self.x0_pt, self.y1_pt-self.y0_pt
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)))
1523
def transformed(self, trafo):
1524
return normline(*(trafo._apply(self.x0_pt, self.y0_pt) + trafo._apply(self.x1_pt, self.y1_pt)))
1526
def outputPS(self, file):
1527
file.write("%g %g lineto\n" % (self.x1_pt, self.y1_pt))
1529
def outputPDF(self, file):
1530
file.write("%f %f l\n" % (self.x1_pt, self.y1_pt))
1533
class normcurve(normsubpathitem):
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)"""
1537
__slots__ = "x0_pt", "y0_pt", "x1_pt", "y1_pt", "x2_pt", "y2_pt", "x3_pt", "y3_pt"
1539
def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
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)
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"""
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))]
1564
for i in range(1,l):
1565
arclens[i] += arclens[i-1]
1567
# create the list of parameters to be returned
1569
for length in lengths:
1570
# find the last index that is smaller than length
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):
1579
param = Dparams[0] * length * 1.0 / arclens[0]
1581
param = Dparams[lindex+1] * (length - arclens[lindex]) * 1.0 / (arclens[lindex+1] - arclens[lindex])
1582
for i in range(lindex+1):
1585
param = 1 + Dparams[-1] * (length - arclens[-1]) * 1.0 / (arclens[-1] - arclens[-2])
1587
param = max(min(param,1),0)
1588
params.append(param)
1589
return (params, arclens[-1])
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)
1596
a, b = self.midpointsplit()
1597
return a.arclen_pt(epsilon) + b.arclen_pt(epsilon)
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 +
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 +
1611
xt_pt, yt_pt = self.at_pt(t)
1612
return xt_pt * unit.t_pt, yt_pt * unit.t_pt
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))
1621
return self.x0_pt, self.y0_pt
1624
return self.x0_pt * unit.t_pt, self.y0_pt * unit.t_pt
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)
1640
return self.x3_pt, self.y3_pt
1643
return self.x3_pt * unit.t_pt, self.y3_pt * unit.t_pt
1645
def intersect(self, other, epsilon=1e-5):
1646
if isinstance(other, normline):
1647
return _intersectnormcurves(self, 0, 1, other._normcurve(), 0, 1, epsilon)
1649
return _intersectnormcurves(self, 0, 1, other, 0, 1, epsilon)
1651
def isstraight(self, epsilon=1e-5):
1652
"""check wheter the normcurve is approximately straight"""
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
1664
def midpointsplit(self):
1665
"""splits bpathitem at midpoint returning bpath with two bpathitems"""
1667
# for efficiency reason, we do not use self.split(0.5)!
1669
# first, we have to calculate the midpoints between adjacent
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)
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)
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)
1689
return (normcurve(self.x0_pt, self.y0_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,
1696
self.x3_pt, self.y3_pt))
1698
def modified(self, xs_pt=None, ys_pt=None, xe_pt=None, ye_pt=None):
1707
return normcurve(xs_pt, ys_pt,
1708
self.x1_pt, self.y1_pt,
1709
self.x2_pt, self.y2_pt,
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
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)
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"""
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) )
1729
# instead of isstraight method:
1730
if abs(upperlen-lowerlen)<epsilon:
1731
return [( 0.5*(upperlen+lowerlen), paraminterval )]
1733
a, b = self.midpointsplit()
1734
return a.seglengths(0.5*paraminterval, epsilon) + b.seglengths(0.5*paraminterval, epsilon)
1736
def split(self, params):
1737
"""return list of normcurves corresponding to split at parameters"""
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
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
1751
params = [0] + params + [1]
1754
for i in range(len(params)-1):
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
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 +
1770
# from this values we obtain the new control points by inversion
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
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
1785
result.append(normcurve(x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
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)
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)))
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)
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)))
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))
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))
1822
# normpaths are made up of normsubpaths, which represent connected line segments
1827
"""sub path of a normalized path
1829
A subpath consists of a list of normsubpathitems, i.e., lines and bcurves
1830
and can either be closed or not.
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.
1842
__slots__ = "normsubpathitems", "closed", "epsilon", "skippedline"
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
1852
self.normsubpathitems = []
1855
# a test (might be temporary)
1856
for anormsubpathitem in normsubpathitems:
1857
assert isinstance(anormsubpathitem, normsubpathitem), "only list of normsubpathitem instances allowed"
1859
self.extend(normsubpathitems)
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)
1870
def __getitem__(self, i):
1871
return self.normsubpathitems[i]
1873
def __iadd__(self, other):
1875
raise PathException("Cannot extend normsubpath by closed normsubpath")
1876
self.extend(other.normsubpathitems)
1880
return len(self.normsubpathitems)
1883
return "subpath(%s, [%s])" % (self.closed and "closed" or "open",
1884
", ".join(map(str, self.normsubpathitems)))
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.
1897
raise PathException("Cannot select parameters for a short normsubpath")
1901
for index, normsubpathitem in enumerate(self.normsubpathitems[:-1]):
1902
oldparamindex = paramindex
1903
while paramindex < len(params) and params[paramindex] < index + 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:]]))
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")
1919
if index > len(self.normsubpathitems) - 1:
1920
index = len(self.normsubpathitems) - 1
1923
return self.normsubpathitems[index], param - index
1925
def append(self, anormsubpathitem):
1926
assert isinstance(anormsubpathitem, normsubpathitem), "only normsubpathitem instances allowed"
1929
raise PathException("Cannot append to closed normsubpath")
1931
if self.skippedline:
1932
xs_pt, ys_pt = self.skippedline.begin_pt()
1934
xs_pt, ys_pt = anormsubpathitem.begin_pt()
1935
xe_pt, ye_pt = anormsubpathitem.end_pt()
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
1944
self.skippedline = normline(xs_pt, ys_pt, xe_pt, ye_pt)
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])
1951
"""returns total arc length of normsubpath"""
1952
return self.arclen_pt() * unit.t_pt
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"""
1961
allparams = [0] * len(lengths)
1964
for pitem in self.normsubpathitems:
1965
params, arclen = pitem._arclentoparam_pt(rests, self.epsilon)
1967
for i in range(len(rests)):
1970
allparams[i] += params[i]
1972
return (allparams, allarclen)
1974
def arclentoparam_pt(self, lengths):
1976
return self._arclentoparam_pt(lengths)[0][0]
1978
return self._arclentoparam_pt(lengths)[0]
1980
def arclentoparam(self, lengths):
1981
"""returns the parameter value(s) matching the given length(s)
1983
all given lengths must be positive.
1984
A length greater than the total arclength will give self.range()
1986
l = [unit.topt(length) for length in helper.ensuresequence(lengths)]
1987
return self.arclentoparam_pt(l)
1989
def at_pt(self, param):
1990
"""return coordinates in pts of sub path at parameter value param
1992
The parameter param must be smaller or equal to the number of
1993
segments in the normpath, otherwise None is returned.
1995
normsubpathitem, itemparam = self._findnormsubpathitem(param)
1996
return normsubpathitem.at_pt(itemparam)
1998
def at(self, param):
1999
"""return coordinates of sub path at parameter value param
2001
The parameter param must be smaller or equal to the number of
2002
segments in the normpath, otherwise None is returned.
2004
normsubpathitem, itemparam = self._findnormsubpathitem(param)
2005
return normsubpathitem.at(itemparam)
2008
if self.normsubpathitems:
2009
abbox = self.normsubpathitems[0].bbox()
2010
for anormpathitem in self.normsubpathitems[1:]:
2011
abbox += anormpathitem.bbox()
2017
if not self.normsubpathitems and self.skippedline:
2018
return self.skippedline.begin_pt()
2019
return self.normsubpathitems[0].begin_pt()
2022
if not self.normsubpathitems and self.skippedline:
2023
return self.skippedline.begin()
2024
return self.normsubpathitems[0].begin()
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")
2033
raise PathException("Normsubpath too short, cannot be closed")
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))
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
2048
def curvradius_pt(self, param):
2049
normsubpathitem, itemparam = self._findnormsubpathitem(param)
2050
return normsubpathitem.curvradius_pt(itemparam)
2053
if self.skippedline:
2054
return self.skippedline.end_pt()
2055
return self.normsubpathitems[-1].end_pt()
2058
if self.skippedline:
2059
return self.skippedline.end()
2060
return self.normsubpathitems[-1].end()
2062
def extend(self, normsubpathitems):
2063
for normsubpathitem in normsubpathitems:
2064
self.append(normsubpathitem)
2066
def intersect(self, other):
2067
"""intersect self with other normsubpath
2069
returns a tuple of lists consisting of the parameter values
2070
of the intersection points of the corresponding normsubpath
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)
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]
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()
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])
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
2106
while i < len(split):
2107
splitnormsubpath = split[i]
2109
while splitnormsubpath.isshort():
2110
ip1, ip2 = intersections[i-1][1], intersections[j][1]
2112
result.append((ip1, ip2))
2114
result.append((ip2, ip1))
2119
splitnormsubpath = splitnormsubpath.joined(split[j])
2125
while i < len(split)-1:
2126
splitnormsubpath = split[i]
2128
while splitnormsubpath.isshort():
2129
ip1, ip2 = intersections[i-1][1], intersections[j][1]
2131
result.append((ip1, ip2))
2133
result.append((ip2, ip1))
2135
if j < len(split)-1:
2136
splitnormsubpath.join(split[j])
2142
closepoints_a = closepoints(self, intersections_a)
2143
closepoints_b = closepoints(other, intersections_b)
2145
# map intersection point to lowest point which is equivalent to the
2147
equivalentpoints = list(range(len(intersections_a)))
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]
2156
# determine the remaining intersection points
2157
intersectionpoints = {}
2158
for point in equivalentpoints:
2159
intersectionpoints[point] = 1
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
2176
return [x for x, y in result], [y for x, y in result]
2179
"""return whether the subnormpath is shorter than epsilon"""
2180
return not self.normsubpathitems
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)
2188
def joined(self, other):
2189
result = normsubpath(self.normsubpathitems, self.closed, self.epsilon)
2190
result.skippedline = self.skippedline
2195
"""return maximal parameter value, i.e. number of line/curve segments"""
2196
return len(self.normsubpathitems)
2199
self.normsubpathitems.reverse()
2200
for npitem in self.normsubpathitems:
2205
for i in range(len(self.normsubpathitems)):
2206
nnormpathitems.append(self.normsubpathitems[-(i+1)].reversed())
2207
return normsubpath(nnormpathitems, self.closed)
2209
def split(self, params):
2210
"""split normsubpath at list of parameter values params and return list
2213
The parameter list params has to be sorted. Note that each element of
2214
the resulting list is an open normsubpath.
2217
result = [normsubpath(epsilon=self.epsilon)]
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:]])
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]
2230
# otherwise just close the copied path again
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)
2242
return line_pt(tx_pt, ty_pt, tx_pt+tdx_pt, ty_pt+tdy_pt)
2244
def trafo(self, param):
2245
normsubpathitem, itemparam = self._findnormsubpathitem(param)
2246
return normsubpathitem.trafo(itemparam)
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 = []
2258
self.skippedline = None
2259
for pitem in normsubpathitems:
2260
self.append(pitem.transformed(trafo))
2263
elif skippedline is not None:
2264
self.append(skippedline.transformed(trafo))
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))
2272
nnormsubpath.close()
2273
elif self.skippedline is not None:
2274
nnormsubpath.append(skippedline.transformed(trafo))
2277
def outputPS(self, file):
2278
# if the normsubpath is closed, we must not output a normline at
2280
if not self.normsubpathitems:
2282
if self.closed and isinstance(self.normsubpathitems[-1], normline):
2283
normsubpathitems = self.normsubpathitems[:-1]
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)
2291
file.write("closepath\n")
2293
def outputPDF(self, file):
2294
# if the normsubpath is closed, we must not output a normline at
2296
if not self.normsubpathitems:
2298
if self.closed and isinstance(self.normsubpathitems[-1], normline):
2299
normsubpathitems = self.normsubpathitems[:-1]
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)
2310
# the normpath class
2313
class normpath(base.canvasitem):
2317
A normalized path consists of a list of normalized sub paths.
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.
2326
if normsubpaths is None:
2327
self.normsubpaths = []
2329
self.normsubpaths = normsubpaths
2330
for subpath in normsubpaths:
2331
assert isinstance(subpath, normsubpath), "only list of normsubpath instances allowed"
2333
def __add__(self, other):
2335
result.normsubpaths = self.normsubpaths + other.normpath().normsubpaths
2338
def __getitem__(self, i):
2339
return self.normsubpaths[i]
2341
def __iadd__(self, other):
2342
self.normsubpaths += other.normpath().normsubpaths
2346
return len(self.normsubpaths)
2349
return "normpath(%s)" % ", ".join(map(str, self.normsubpaths))
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.
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")
2360
if param is not None:
2362
subpath, param = param
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
2373
return self.normsubpaths[subpath], param
2375
raise PathException("subpath index out of range")
2377
# we have been passed an arclen (or a tuple (subpath, arclen))
2379
subpath, arclen = arclen
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
2391
normsubpath = self.normsubpaths[subpath]
2393
raise PathException("subpath index out of range")
2394
return normsubpath, normsubpath.arclentoparam(arclen)
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)
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
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)]))
2412
self.normsubpaths[-1].append(pathitem)
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])
2419
"""returns total arc length of normpath"""
2420
return self.arclen_pt() * unit.t_pt
2422
def arclentoparam_pt(self, lengths):
2424
allparams = [0] * len(lengths)
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)):
2435
allparams[i] += params[i]
2438
if finis == len(rests): break
2440
if len(lengths) == 1: allparams = allparams[0]
2443
def arclentoparam(self, lengths):
2444
"""returns the parameter value(s) matching the given length(s)
2446
all given lengths must be positive.
2447
A length greater than the total arclength will give self.range()
2449
l = [unit.topt(length) for length in helper.ensuresequence(lengths)]
2450
return self.arclentoparam_pt(l)
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.
2456
At discontinuities in the path, the limit from below is returned.
2458
normsubpath, param = self._findsubpath(param, arclen)
2459
return normsubpath.at_pt(param)
2461
def at(self, param=None, arclen=None):
2462
"""return coordinates of path at either parameter value param
2463
or arc length arclen.
2465
At discontinuities in the path, the limit from below is returned
2467
normsubpath, param = self._findsubpath(param, arclen)
2468
return normsubpath.at(param)
2472
for normsubpath in self.normsubpaths:
2473
nbbox = normsubpath.bbox()
2481
"""return coordinates of first point of first subpath in path (in pts)"""
2482
if self.normsubpaths:
2483
return self.normsubpaths[0].begin_pt()
2485
raise PathException("cannot return first point of empty path")
2488
"""return coordinates of first point of first subpath in path"""
2489
if self.normsubpaths:
2490
return self.normsubpaths[0].begin()
2492
raise PathException("cannot return first point of empty path")
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
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)
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
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
2517
"""return coordinates of last point of last subpath in path (in pts)"""
2518
if self.normsubpaths:
2519
return self.normsubpaths[-1].end_pt()
2521
raise PathException("cannot return last point of empty path")
2524
"""return coordinates of last point of last subpath in path"""
2525
if self.normsubpaths:
2526
return self.normsubpaths[-1].end()
2528
raise PathException("cannot return last point of empty path")
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)
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")
2544
self.normsubpaths[-1].normsubpathitems += other.normsubpaths[0].normsubpathitems
2545
self.normsubpaths += other.normsubpaths[1:]
2547
def joined(self, other):
2548
# NOTE we skip a deep copy for performance reasons
2549
result = normpath(self.normsubpaths)
2553
# << operator also designates joining
2556
def intersect(self, other):
2557
"""intersect self with other path
2559
returns a tuple of lists consisting of the parameter values
2560
of the intersection points of the corresponding normpath
2563
other = other.normpath()
2565
# here we build up the result
2566
intersections = ([], [])
2568
# Intersect all normsubpaths of self with the normsubpaths of
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
2581
"""return maximal value for parameter value param"""
2582
return sum([normsubpath.range() for normsubpath in self.normsubpaths])
2586
self.normsubpaths.reverse()
2587
for normsubpath in self.normsubpaths:
2588
normsubpath.reverse()
2591
"""return reversed path"""
2592
nnormpath = normpath()
2593
for i in range(len(self.normsubpaths)):
2594
nnormpath.normsubpaths.append(self.normsubpaths[-(i+1)].reversed())
2597
def split(self, params):
2598
"""split path at parameter values params
2600
Note that the parameter list has to be sorted.
2604
# check whether parameter list is really sorted
2605
sortedparams = list(params)
2607
if sortedparams != list(params):
2608
raise ValueError("split parameter list params has to be sorted")
2612
for param in params:
2613
tparams.append(self._findsubpath(param, None))
2615
# we construct this list of normpaths
2618
# the currently built up normpath
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:]:
2626
np = normpath([normsubpath])
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.
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
2639
normsubpath, param = self._findsubpath(param, arclen)
2640
return normsubpath.tangent(param, length)
2642
def transform(self, trafo):
2643
"""transform path according to trafo"""
2644
for normsubpath in self.normsubpaths:
2645
normsubpath.transform(trafo)
2647
def transformed(self, trafo):
2648
"""return path transformed according to trafo"""
2649
return normpath([normsubpath.transformed(trafo) for normsubpath in self.normsubpaths])
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)
2656
def outputPS(self, file):
2657
for normsubpath in self.normsubpaths:
2658
normsubpath.outputPS(file)
2660
def outputPDF(self, file):
2661
for normsubpath in self.normsubpaths:
2662
normsubpath.outputPDF(file)