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