905
906
def __init__(self, points_pt):
906
907
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))
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))
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])
921
context.x_pt, context.y_pt = self.points_pt[-1][4:]
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:]
928
context.x_pt, context.y_pt = x0_pt, y0_pt
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)
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
935
################################################################################
942
936
# path: PS style path
943
937
################################################################################
945
class path(base.canvasitem):
947
941
"""PS style path"""
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)
943
__slots__ = "pathitems", "_normpath"
945
def __init__(self, *pathitems):
946
"""construct a path from pathitems *args"""
948
for apathitem in pathitems:
949
assert isinstance(apathitem, pathitem), "only pathitem instances allowed"
951
self.pathitems = list(pathitems)
952
# normpath cache (when no epsilon is set)
953
self._normpath = None
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))
960
959
def __iadd__(self, other):
961
self.path += other.path
962
If other is a normpath instance, it is converted to a path before
965
self.pathitems += other.path().pathitems
966
self._normpath = None
964
969
def __getitem__(self, i):
970
"""return path item i"""
971
return self.pathitems[i]
967
973
def __len__(self):
968
return len(self.path)
970
def append(self, pathitem):
971
self.path.append(pathitem)
974
"""return the number of path items"""
975
return len(self.pathitems)
978
l = ", ".join(map(str, self.pathitems))
979
return "path(%s)" % l
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
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()
977
991
def arclen(self):
978
"""returns total arc length of path"""
992
"""return arc length"""
979
993
return self.normpath().arclen()
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)
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)
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)
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)
1007
def at(self, params):
1008
"""return coordinates of path at param(s) or arc length(s)"""
1009
return self.normpath().at(params)
1011
def atbegin_pt(self):
1012
"""return coordinates of the beginning of first subpath in path in pts"""
1013
return self.normpath().atbegin_pt()
1016
"""return coordinates of the beginning of first subpath in path"""
1017
return self.normpath().atbegin()
1020
"""return coordinates of the end of last subpath in path in pts"""
1021
return self.normpath().atend_pt()
1024
"""return coordinates of the end of last subpath in path"""
1025
return self.normpath().atend()
1001
1027
def bbox(self):
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()
1028
"""return bbox of path"""
1030
bbox = self.pathitems[0].createbbox()
1031
context = self.pathitems[0].createcontext()
1032
for pathitem in self.pathitems[1:]:
1033
pathitem.updatebbox(bbox, context)
1036
return bboxmodule.empty()
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()
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()
1042
def curveradius_pt(self, params):
1043
"""return the curvature radius in pts at param(s) or arc length(s) in pts
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)
1050
def curveradius(self, params):
1051
"""return the curvature radius at param(s) or arc length(s)
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)
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()
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
1069
def intersect(self, other):
1070
"""intersect self with other path
1072
Returns a tuple of lists consisting of the parameter values
1073
of the intersection points of the corresponding normpath.
1075
return self.normpath().intersect(other)
1077
def join(self, other):
1078
"""join other path/normpath inplace
1080
If other is a normpath instance, it is converted to a path before
1083
self.pathitems = self.joined(other).path().pathitems
1084
self._normpath = None
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()
1056
1091
# << operator also designates joining
1057
1092
__lshift__ = joined
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()
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
1100
if epsilon is _marker:
1101
normpath = self.pathitems[0].createnormpath()
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)
1108
if epsilon is _marker:
1109
normpath = normpath([])
1111
normpath = normpath(epsilon=epsilon)
1112
if epsilon is _marker:
1113
self._normpath = normpath
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)
1120
def paramtoarclen(self, params):
1121
"""return arc lenght(s) matching the given param(s)"""
1122
return self.normpath().paramtoarclen(params)
1125
"""return corresponding path, i.e., self"""
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()
1134
def rotation_pt(self, params):
1135
"""return rotation at param(s) or arc length(s) in pts"""
1136
return self.normpath().rotation(params)
1138
def rotation(self, params):
1139
"""return rotation at param(s) or arc length(s)"""
1140
return self.normpath().rotation(params)
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)
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)
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.
1150
def tangent_pt(self, params, length=None):
1151
"""return tangent vector of path at param(s) or arc length(s) in pts
1153
If length in pts is not None, the tangent vector will be scaled to
1156
return self.normpath().tangent_pt(params, length)
1158
def tangent(self, params, length=None):
1159
"""return tangent vector of path at param(s) or arc length(s)
1113
1161
If length is not None, the tangent vector will be scaled to
1114
1162
the desired length.
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)
1164
return self.normpath().tangent(params, length)
1166
def trafo_pt(self, params):
1167
"""return transformation at param(s) or arc length(s) in pts"""
1168
return self.normpath().trafo(params)
1170
def trafo(self, params):
1171
"""return transformation at param(s) or arc length(s)"""
1172
return self.normpath().trafo(params)
1122
1174
def transformed(self, trafo):
1123
1175
"""return transformed path"""
1124
1176
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
################################################################################
1178
def outputPS(self, file, writer):
1179
"""write PS code to file"""
1180
for pitem in self.pathitems:
1181
pitem.outputPS(file, writer)
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
1188
self.normpath(epsilon=None).outputPDF(file, writer)
1147
1192
# some special kinds of path, again in two variants
1148
################################################################################
1150
1195
class line_pt(path):
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"""
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))
1158
1203
class curve_pt(path):
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"""
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))
1207
def __init__(self, x0_pt, y0_pt, x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt):
1209
moveto_pt(x0_pt, y0_pt),
1210
curveto_pt(x1_pt, y1_pt, x2_pt, y2_pt, x3_pt, y3_pt))
1169
1213
class rect_pt(path):
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"""
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),
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),
1181
1225
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),
1227
"""circle with center (x_pt, y_pt) and radius_pt in pts"""
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),
1235
class ellipse_pt(path):
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"""
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)
1190
1247
class line(line_pt):
1192
"""straight line from (x1, y1) to (x2, y2)"""
1249
"""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))
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))
1200
1256
class curve(curve_pt):
1202
"""Bezier curve with control points (x0, y1),..., (x3, y3)"""
1258
"""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))
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))
1212
1267
class rect(rect_pt):
1214
"""rectangle at position (x,y) with width and height"""
1269
"""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))
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))
1222
1276
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)
1278
"""circle with center (x,y) and radius"""
1280
def __init__(self, x, y, radius, **kwargs):
1281
circle_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(radius), **kwargs)
1284
class ellipse(ellipse_pt):
1286
"""ellipse with center (x, y), the two axes (a, b),
1287
and the angle angle of the first axis"""
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)