Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/misc/bezierTools.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

559 statements  

1# -*- coding: utf-8 -*- 

2"""fontTools.misc.bezierTools.py -- tools for working with Bezier path segments. 

3""" 

4 

5from fontTools.misc.arrayTools import calcBounds, sectRect, rectArea 

6from fontTools.misc.transform import Identity 

7import math 

8from collections import namedtuple 

9 

10try: 

11 import cython 

12except (AttributeError, ImportError): 

13 # if cython not installed, use mock module with no-op decorators and types 

14 from fontTools.misc import cython 

15COMPILED = cython.compiled 

16 

17 

18EPSILON = 1e-9 

19 

20 

21Intersection = namedtuple("Intersection", ["pt", "t1", "t2"]) 

22 

23 

24__all__ = [ 

25 "approximateCubicArcLength", 

26 "approximateCubicArcLengthC", 

27 "approximateQuadraticArcLength", 

28 "approximateQuadraticArcLengthC", 

29 "calcCubicArcLength", 

30 "calcCubicArcLengthC", 

31 "calcQuadraticArcLength", 

32 "calcQuadraticArcLengthC", 

33 "calcCubicBounds", 

34 "calcQuadraticBounds", 

35 "splitLine", 

36 "splitQuadratic", 

37 "splitCubic", 

38 "splitQuadraticAtT", 

39 "splitCubicAtT", 

40 "splitCubicAtTC", 

41 "splitCubicIntoTwoAtTC", 

42 "solveQuadratic", 

43 "solveCubic", 

44 "quadraticPointAtT", 

45 "cubicPointAtT", 

46 "cubicPointAtTC", 

47 "linePointAtT", 

48 "segmentPointAtT", 

49 "lineLineIntersections", 

50 "curveLineIntersections", 

51 "curveCurveIntersections", 

52 "segmentSegmentIntersections", 

53] 

54 

55 

56def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): 

57 """Calculates the arc length for a cubic Bezier segment. 

58 

59 Whereas :func:`approximateCubicArcLength` approximates the length, this 

60 function calculates it by "measuring", recursively dividing the curve 

61 until the divided segments are shorter than ``tolerance``. 

62 

63 Args: 

64 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 

65 tolerance: Controls the precision of the calcuation. 

66 

67 Returns: 

68 Arc length value. 

69 """ 

70 return calcCubicArcLengthC( 

71 complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance 

72 ) 

73 

74 

75def _split_cubic_into_two(p0, p1, p2, p3): 

76 mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 

77 deriv3 = (p3 + p2 - p1 - p0) * 0.125 

78 return ( 

79 (p0, (p0 + p1) * 0.5, mid - deriv3, mid), 

80 (mid, mid + deriv3, (p2 + p3) * 0.5, p3), 

81 ) 

82 

83 

84@cython.returns(cython.double) 

85@cython.locals( 

86 p0=cython.complex, 

87 p1=cython.complex, 

88 p2=cython.complex, 

89 p3=cython.complex, 

90) 

91@cython.locals(mult=cython.double, arch=cython.double, box=cython.double) 

92def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): 

93 arch = abs(p0 - p3) 

94 box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3) 

95 if arch * mult + EPSILON >= box: 

96 return (arch + box) * 0.5 

97 else: 

98 one, two = _split_cubic_into_two(p0, p1, p2, p3) 

99 return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse( 

100 mult, *two 

101 ) 

102 

103 

104@cython.returns(cython.double) 

105@cython.locals( 

106 pt1=cython.complex, 

107 pt2=cython.complex, 

108 pt3=cython.complex, 

109 pt4=cython.complex, 

110) 

111@cython.locals( 

112 tolerance=cython.double, 

113 mult=cython.double, 

114) 

115def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): 

116 """Calculates the arc length for a cubic Bezier segment. 

117 

118 Args: 

119 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. 

120 tolerance: Controls the precision of the calcuation. 

121 

122 Returns: 

123 Arc length value. 

124 """ 

125 mult = 1.0 + 1.5 * tolerance # The 1.5 is a empirical hack; no math 

126 return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) 

127 

128 

129epsilonDigits = 6 

130epsilon = 1e-10 

131 

132 

133@cython.cfunc 

134@cython.inline 

135@cython.returns(cython.double) 

136@cython.locals(v1=cython.complex, v2=cython.complex) 

137def _dot(v1, v2): 

138 return (v1 * v2.conjugate()).real 

139 

140 

141@cython.cfunc 

142@cython.inline 

143@cython.returns(cython.double) 

144@cython.locals(x=cython.double) 

145def _intSecAtan(x): 

146 # In : sympy.integrate(sp.sec(sp.atan(x))) 

147 # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2 

148 return x * math.sqrt(x**2 + 1) / 2 + math.asinh(x) / 2 

149 

150 

151def calcQuadraticArcLength(pt1, pt2, pt3): 

152 """Calculates the arc length for a quadratic Bezier segment. 

153 

154 Args: 

155 pt1: Start point of the Bezier as 2D tuple. 

156 pt2: Handle point of the Bezier as 2D tuple. 

157 pt3: End point of the Bezier as 2D tuple. 

158 

159 Returns: 

160 Arc length value. 

161 

162 Example:: 

163 

164 >>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment 

165 0.0 

166 >>> calcQuadraticArcLength((0, 0), (50, 0), (80, 0)) # collinear points 

167 80.0 

168 >>> calcQuadraticArcLength((0, 0), (0, 50), (0, 80)) # collinear points vertical 

169 80.0 

170 >>> calcQuadraticArcLength((0, 0), (50, 20), (100, 40)) # collinear points 

171 107.70329614269008 

172 >>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0)) 

173 154.02976155645263 

174 >>> calcQuadraticArcLength((0, 0), (0, 50), (100, 0)) 

175 120.21581243984076 

176 >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50)) 

177 102.53273816445825 

178 >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside 

179 66.66666666666667 

180 >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back 

181 40.0 

182 """ 

183 return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) 

184 

185 

186@cython.returns(cython.double) 

187@cython.locals( 

188 pt1=cython.complex, 

189 pt2=cython.complex, 

190 pt3=cython.complex, 

191 d0=cython.complex, 

192 d1=cython.complex, 

193 d=cython.complex, 

194 n=cython.complex, 

195) 

196@cython.locals( 

197 scale=cython.double, 

198 origDist=cython.double, 

199 a=cython.double, 

200 b=cython.double, 

201 x0=cython.double, 

202 x1=cython.double, 

203 Len=cython.double, 

204) 

205def calcQuadraticArcLengthC(pt1, pt2, pt3): 

206 """Calculates the arc length for a quadratic Bezier segment. 

207 

208 Args: 

209 pt1: Start point of the Bezier as a complex number. 

210 pt2: Handle point of the Bezier as a complex number. 

211 pt3: End point of the Bezier as a complex number. 

212 

213 Returns: 

214 Arc length value. 

215 """ 

216 # Analytical solution to the length of a quadratic bezier. 

217 # Documentation: https://github.com/fonttools/fonttools/issues/3055 

218 d0 = pt2 - pt1 

219 d1 = pt3 - pt2 

220 d = d1 - d0 

221 n = d * 1j 

222 scale = abs(n) 

223 if scale == 0.0: 

224 return abs(pt3 - pt1) 

225 origDist = _dot(n, d0) 

226 if abs(origDist) < epsilon: 

227 if _dot(d0, d1) >= 0: 

228 return abs(pt3 - pt1) 

229 a, b = abs(d0), abs(d1) 

230 return (a * a + b * b) / (a + b) 

231 x0 = _dot(d, d0) / origDist 

232 x1 = _dot(d, d1) / origDist 

233 Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0))) 

234 return Len 

235 

236 

237def approximateQuadraticArcLength(pt1, pt2, pt3): 

238 """Calculates the arc length for a quadratic Bezier segment. 

239 

240 Uses Gauss-Legendre quadrature for a branch-free approximation. 

241 See :func:`calcQuadraticArcLength` for a slower but more accurate result. 

242 

243 Args: 

244 pt1: Start point of the Bezier as 2D tuple. 

245 pt2: Handle point of the Bezier as 2D tuple. 

246 pt3: End point of the Bezier as 2D tuple. 

247 

248 Returns: 

249 Approximate arc length value. 

250 """ 

251 return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) 

252 

253 

254@cython.returns(cython.double) 

255@cython.locals( 

256 pt1=cython.complex, 

257 pt2=cython.complex, 

258 pt3=cython.complex, 

259) 

260@cython.locals( 

261 v0=cython.double, 

262 v1=cython.double, 

263 v2=cython.double, 

264) 

265def approximateQuadraticArcLengthC(pt1, pt2, pt3): 

266 """Calculates the arc length for a quadratic Bezier segment. 

267 

268 Uses Gauss-Legendre quadrature for a branch-free approximation. 

269 See :func:`calcQuadraticArcLength` for a slower but more accurate result. 

270 

271 Args: 

272 pt1: Start point of the Bezier as a complex number. 

273 pt2: Handle point of the Bezier as a complex number. 

274 pt3: End point of the Bezier as a complex number. 

275 

276 Returns: 

277 Approximate arc length value. 

278 """ 

279 # This, essentially, approximates the length-of-derivative function 

280 # to be integrated with the best-matching fifth-degree polynomial 

281 # approximation of it. 

282 # 

283 # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature 

284 

285 # abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2), 

286 # weighted 5/18, 8/18, 5/18 respectively. 

287 v0 = abs( 

288 -0.492943519233745 * pt1 + 0.430331482911935 * pt2 + 0.0626120363218102 * pt3 

289 ) 

290 v1 = abs(pt3 - pt1) * 0.4444444444444444 

291 v2 = abs( 

292 -0.0626120363218102 * pt1 - 0.430331482911935 * pt2 + 0.492943519233745 * pt3 

293 ) 

294 

295 return v0 + v1 + v2 

296 

297 

298def calcQuadraticBounds(pt1, pt2, pt3): 

299 """Calculates the bounding rectangle for a quadratic Bezier segment. 

300 

301 Args: 

302 pt1: Start point of the Bezier as a 2D tuple. 

303 pt2: Handle point of the Bezier as a 2D tuple. 

304 pt3: End point of the Bezier as a 2D tuple. 

305 

306 Returns: 

307 A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. 

308 

309 Example:: 

310 

311 >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0)) 

312 (0, 0, 100, 50.0) 

313 >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100)) 

314 (0.0, 0.0, 100, 100) 

315 """ 

316 (ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3) 

317 ax2 = ax * 2.0 

318 ay2 = ay * 2.0 

319 roots = [] 

320 if ax2 != 0: 

321 roots.append(-bx / ax2) 

322 if ay2 != 0: 

323 roots.append(-by / ay2) 

324 points = [ 

325 (ax * t * t + bx * t + cx, ay * t * t + by * t + cy) 

326 for t in roots 

327 if 0 <= t < 1 

328 ] + [pt1, pt3] 

329 return calcBounds(points) 

330 

331 

332def approximateCubicArcLength(pt1, pt2, pt3, pt4): 

333 """Approximates the arc length for a cubic Bezier segment. 

334 

335 Uses Gauss-Lobatto quadrature with n=5 points to approximate arc length. 

336 See :func:`calcCubicArcLength` for a slower but more accurate result. 

337 

338 Args: 

339 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 

340 

341 Returns: 

342 Arc length value. 

343 

344 Example:: 

345 

346 >>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0)) 

347 190.04332968932817 

348 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 50), (100, 100)) 

349 154.8852074945903 

350 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (150, 0)) # line; exact result should be 150. 

351 149.99999999999991 

352 >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (-50, 0)) # cusp; exact result should be 150. 

353 136.9267662156362 

354 >>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp 

355 154.80848416537057 

356 """ 

357 return approximateCubicArcLengthC( 

358 complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4) 

359 ) 

360 

361 

362@cython.returns(cython.double) 

363@cython.locals( 

364 pt1=cython.complex, 

365 pt2=cython.complex, 

366 pt3=cython.complex, 

367 pt4=cython.complex, 

368) 

369@cython.locals( 

370 v0=cython.double, 

371 v1=cython.double, 

372 v2=cython.double, 

373 v3=cython.double, 

374 v4=cython.double, 

375) 

376def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): 

377 """Approximates the arc length for a cubic Bezier segment. 

378 

379 Args: 

380 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. 

381 

382 Returns: 

383 Arc length value. 

384 """ 

385 # This, essentially, approximates the length-of-derivative function 

386 # to be integrated with the best-matching seventh-degree polynomial 

387 # approximation of it. 

388 # 

389 # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules 

390 

391 # abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1), 

392 # weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively. 

393 v0 = abs(pt2 - pt1) * 0.15 

394 v1 = abs( 

395 -0.558983582205757 * pt1 

396 + 0.325650248872424 * pt2 

397 + 0.208983582205757 * pt3 

398 + 0.024349751127576 * pt4 

399 ) 

400 v2 = abs(pt4 - pt1 + pt3 - pt2) * 0.26666666666666666 

401 v3 = abs( 

402 -0.024349751127576 * pt1 

403 - 0.208983582205757 * pt2 

404 - 0.325650248872424 * pt3 

405 + 0.558983582205757 * pt4 

406 ) 

407 v4 = abs(pt4 - pt3) * 0.15 

408 

409 return v0 + v1 + v2 + v3 + v4 

410 

411 

412def calcCubicBounds(pt1, pt2, pt3, pt4): 

413 """Calculates the bounding rectangle for a quadratic Bezier segment. 

414 

415 Args: 

416 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 

417 

418 Returns: 

419 A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``. 

420 

421 Example:: 

422 

423 >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0)) 

424 (0, 0, 100, 75.0) 

425 >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100)) 

426 (0.0, 0.0, 100, 100) 

427 >>> print("%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))) 

428 35.566243 0.000000 64.433757 75.000000 

429 """ 

430 (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4) 

431 # calc first derivative 

432 ax3 = ax * 3.0 

433 ay3 = ay * 3.0 

434 bx2 = bx * 2.0 

435 by2 = by * 2.0 

436 xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1] 

437 yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1] 

438 roots = xRoots + yRoots 

439 

440 points = [ 

441 ( 

442 ax * t * t * t + bx * t * t + cx * t + dx, 

443 ay * t * t * t + by * t * t + cy * t + dy, 

444 ) 

445 for t in roots 

446 ] + [pt1, pt4] 

447 return calcBounds(points) 

448 

449 

450def splitLine(pt1, pt2, where, isHorizontal): 

451 """Split a line at a given coordinate. 

452 

453 Args: 

454 pt1: Start point of line as 2D tuple. 

455 pt2: End point of line as 2D tuple. 

456 where: Position at which to split the line. 

457 isHorizontal: Direction of the ray splitting the line. If true, 

458 ``where`` is interpreted as a Y coordinate; if false, then 

459 ``where`` is interpreted as an X coordinate. 

460 

461 Returns: 

462 A list of two line segments (each line segment being two 2D tuples) 

463 if the line was successfully split, or a list containing the original 

464 line. 

465 

466 Example:: 

467 

468 >>> printSegments(splitLine((0, 0), (100, 100), 50, True)) 

469 ((0, 0), (50, 50)) 

470 ((50, 50), (100, 100)) 

471 >>> printSegments(splitLine((0, 0), (100, 100), 100, True)) 

472 ((0, 0), (100, 100)) 

473 >>> printSegments(splitLine((0, 0), (100, 100), 0, True)) 

474 ((0, 0), (0, 0)) 

475 ((0, 0), (100, 100)) 

476 >>> printSegments(splitLine((0, 0), (100, 100), 0, False)) 

477 ((0, 0), (0, 0)) 

478 ((0, 0), (100, 100)) 

479 >>> printSegments(splitLine((100, 0), (0, 0), 50, False)) 

480 ((100, 0), (50, 0)) 

481 ((50, 0), (0, 0)) 

482 >>> printSegments(splitLine((0, 100), (0, 0), 50, True)) 

483 ((0, 100), (0, 50)) 

484 ((0, 50), (0, 0)) 

485 """ 

486 pt1x, pt1y = pt1 

487 pt2x, pt2y = pt2 

488 

489 ax = pt2x - pt1x 

490 ay = pt2y - pt1y 

491 

492 bx = pt1x 

493 by = pt1y 

494 

495 a = (ax, ay)[isHorizontal] 

496 

497 if a == 0: 

498 return [(pt1, pt2)] 

499 t = (where - (bx, by)[isHorizontal]) / a 

500 if 0 <= t < 1: 

501 midPt = ax * t + bx, ay * t + by 

502 return [(pt1, midPt), (midPt, pt2)] 

503 else: 

504 return [(pt1, pt2)] 

505 

506 

507def splitQuadratic(pt1, pt2, pt3, where, isHorizontal): 

508 """Split a quadratic Bezier curve at a given coordinate. 

509 

510 Args: 

511 pt1,pt2,pt3: Control points of the Bezier as 2D tuples. 

512 where: Position at which to split the curve. 

513 isHorizontal: Direction of the ray splitting the curve. If true, 

514 ``where`` is interpreted as a Y coordinate; if false, then 

515 ``where`` is interpreted as an X coordinate. 

516 

517 Returns: 

518 A list of two curve segments (each curve segment being three 2D tuples) 

519 if the curve was successfully split, or a list containing the original 

520 curve. 

521 

522 Example:: 

523 

524 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False)) 

525 ((0, 0), (50, 100), (100, 0)) 

526 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False)) 

527 ((0, 0), (25, 50), (50, 50)) 

528 ((50, 50), (75, 50), (100, 0)) 

529 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False)) 

530 ((0, 0), (12.5, 25), (25, 37.5)) 

531 ((25, 37.5), (62.5, 75), (100, 0)) 

532 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True)) 

533 ((0, 0), (7.32233, 14.6447), (14.6447, 25)) 

534 ((14.6447, 25), (50, 75), (85.3553, 25)) 

535 ((85.3553, 25), (92.6777, 14.6447), (100, -7.10543e-15)) 

536 >>> # XXX I'm not at all sure if the following behavior is desirable: 

537 >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True)) 

538 ((0, 0), (25, 50), (50, 50)) 

539 ((50, 50), (50, 50), (50, 50)) 

540 ((50, 50), (75, 50), (100, 0)) 

541 """ 

542 a, b, c = calcQuadraticParameters(pt1, pt2, pt3) 

543 solutions = solveQuadratic( 

544 a[isHorizontal], b[isHorizontal], c[isHorizontal] - where 

545 ) 

546 solutions = sorted(t for t in solutions if 0 <= t < 1) 

547 if not solutions: 

548 return [(pt1, pt2, pt3)] 

549 return _splitQuadraticAtT(a, b, c, *solutions) 

550 

551 

552def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal): 

553 """Split a cubic Bezier curve at a given coordinate. 

554 

555 Args: 

556 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 

557 where: Position at which to split the curve. 

558 isHorizontal: Direction of the ray splitting the curve. If true, 

559 ``where`` is interpreted as a Y coordinate; if false, then 

560 ``where`` is interpreted as an X coordinate. 

561 

562 Returns: 

563 A list of two curve segments (each curve segment being four 2D tuples) 

564 if the curve was successfully split, or a list containing the original 

565 curve. 

566 

567 Example:: 

568 

569 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False)) 

570 ((0, 0), (25, 100), (75, 100), (100, 0)) 

571 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False)) 

572 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 

573 ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) 

574 >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True)) 

575 ((0, 0), (2.29379, 9.17517), (4.79804, 17.5085), (7.47414, 25)) 

576 ((7.47414, 25), (31.2886, 91.6667), (68.7114, 91.6667), (92.5259, 25)) 

577 ((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), (100, 1.77636e-15)) 

578 """ 

579 a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) 

580 solutions = solveCubic( 

581 a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - where 

582 ) 

583 solutions = sorted(t for t in solutions if 0 <= t < 1) 

584 if not solutions: 

585 return [(pt1, pt2, pt3, pt4)] 

586 return _splitCubicAtT(a, b, c, d, *solutions) 

587 

588 

589def splitQuadraticAtT(pt1, pt2, pt3, *ts): 

590 """Split a quadratic Bezier curve at one or more values of t. 

591 

592 Args: 

593 pt1,pt2,pt3: Control points of the Bezier as 2D tuples. 

594 *ts: Positions at which to split the curve. 

595 

596 Returns: 

597 A list of curve segments (each curve segment being three 2D tuples). 

598 

599 Examples:: 

600 

601 >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5)) 

602 ((0, 0), (25, 50), (50, 50)) 

603 ((50, 50), (75, 50), (100, 0)) 

604 >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75)) 

605 ((0, 0), (25, 50), (50, 50)) 

606 ((50, 50), (62.5, 50), (75, 37.5)) 

607 ((75, 37.5), (87.5, 25), (100, 0)) 

608 """ 

609 a, b, c = calcQuadraticParameters(pt1, pt2, pt3) 

610 return _splitQuadraticAtT(a, b, c, *ts) 

611 

612 

613def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): 

614 """Split a cubic Bezier curve at one or more values of t. 

615 

616 Args: 

617 pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples. 

618 *ts: Positions at which to split the curve. 

619 

620 Returns: 

621 A list of curve segments (each curve segment being four 2D tuples). 

622 

623 Examples:: 

624 

625 >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5)) 

626 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 

627 ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) 

628 >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75)) 

629 ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) 

630 ((50, 75), (59.375, 75), (68.75, 68.75), (77.3438, 56.25)) 

631 ((77.3438, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0)) 

632 """ 

633 a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) 

634 split = _splitCubicAtT(a, b, c, d, *ts) 

635 

636 # the split impl can introduce floating point errors; we know the first 

637 # segment should always start at pt1 and the last segment should end at pt4, 

638 # so we set those values directly before returning. 

639 split[0] = (pt1, *split[0][1:]) 

640 split[-1] = (*split[-1][:-1], pt4) 

641 return split 

642 

643 

644@cython.locals( 

645 pt1=cython.complex, 

646 pt2=cython.complex, 

647 pt3=cython.complex, 

648 pt4=cython.complex, 

649 a=cython.complex, 

650 b=cython.complex, 

651 c=cython.complex, 

652 d=cython.complex, 

653) 

654def splitCubicAtTC(pt1, pt2, pt3, pt4, *ts): 

655 """Split a cubic Bezier curve at one or more values of t. 

656 

657 Args: 

658 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.. 

659 *ts: Positions at which to split the curve. 

660 

661 Yields: 

662 Curve segments (each curve segment being four complex numbers). 

663 """ 

664 a, b, c, d = calcCubicParametersC(pt1, pt2, pt3, pt4) 

665 yield from _splitCubicAtTC(a, b, c, d, *ts) 

666 

667 

668@cython.returns(cython.complex) 

669@cython.locals( 

670 t=cython.double, 

671 pt1=cython.complex, 

672 pt2=cython.complex, 

673 pt3=cython.complex, 

674 pt4=cython.complex, 

675 pointAtT=cython.complex, 

676 off1=cython.complex, 

677 off2=cython.complex, 

678) 

679@cython.locals( 

680 t2=cython.double, _1_t=cython.double, _1_t_2=cython.double, _2_t_1_t=cython.double 

681) 

682def splitCubicIntoTwoAtTC(pt1, pt2, pt3, pt4, t): 

683 """Split a cubic Bezier curve at t. 

684 

685 Args: 

686 pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers. 

687 t: Position at which to split the curve. 

688 

689 Returns: 

690 A tuple of two curve segments (each curve segment being four complex numbers). 

691 """ 

692 t2 = t * t 

693 _1_t = 1 - t 

694 _1_t_2 = _1_t * _1_t 

695 _2_t_1_t = 2 * t * _1_t 

696 pointAtT = ( 

697 _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4 

698 ) 

699 off1 = _1_t_2 * pt1 + _2_t_1_t * pt2 + t2 * pt3 

700 off2 = _1_t_2 * pt2 + _2_t_1_t * pt3 + t2 * pt4 

701 

702 pt2 = pt1 + (pt2 - pt1) * t 

703 pt3 = pt4 + (pt3 - pt4) * _1_t 

704 

705 return ((pt1, pt2, off1, pointAtT), (pointAtT, off2, pt3, pt4)) 

706 

707 

708def _splitQuadraticAtT(a, b, c, *ts): 

709 ts = list(ts) 

710 segments = [] 

711 ts.insert(0, 0.0) 

712 ts.append(1.0) 

713 ax, ay = a 

714 bx, by = b 

715 cx, cy = c 

716 for i in range(len(ts) - 1): 

717 t1 = ts[i] 

718 t2 = ts[i + 1] 

719 delta = t2 - t1 

720 # calc new a, b and c 

721 delta_2 = delta * delta 

722 a1x = ax * delta_2 

723 a1y = ay * delta_2 

724 b1x = (2 * ax * t1 + bx) * delta 

725 b1y = (2 * ay * t1 + by) * delta 

726 t1_2 = t1 * t1 

727 c1x = ax * t1_2 + bx * t1 + cx 

728 c1y = ay * t1_2 + by * t1 + cy 

729 

730 pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y)) 

731 segments.append((pt1, pt2, pt3)) 

732 return segments 

733 

734 

735def _splitCubicAtT(a, b, c, d, *ts): 

736 ts = list(ts) 

737 ts.insert(0, 0.0) 

738 ts.append(1.0) 

739 segments = [] 

740 ax, ay = a 

741 bx, by = b 

742 cx, cy = c 

743 dx, dy = d 

744 for i in range(len(ts) - 1): 

745 t1 = ts[i] 

746 t2 = ts[i + 1] 

747 delta = t2 - t1 

748 

749 delta_2 = delta * delta 

750 delta_3 = delta * delta_2 

751 t1_2 = t1 * t1 

752 t1_3 = t1 * t1_2 

753 

754 # calc new a, b, c and d 

755 a1x = ax * delta_3 

756 a1y = ay * delta_3 

757 b1x = (3 * ax * t1 + bx) * delta_2 

758 b1y = (3 * ay * t1 + by) * delta_2 

759 c1x = (2 * bx * t1 + cx + 3 * ax * t1_2) * delta 

760 c1y = (2 * by * t1 + cy + 3 * ay * t1_2) * delta 

761 d1x = ax * t1_3 + bx * t1_2 + cx * t1 + dx 

762 d1y = ay * t1_3 + by * t1_2 + cy * t1 + dy 

763 pt1, pt2, pt3, pt4 = calcCubicPoints( 

764 (a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y) 

765 ) 

766 segments.append((pt1, pt2, pt3, pt4)) 

767 return segments 

768 

769 

770@cython.locals( 

771 a=cython.complex, 

772 b=cython.complex, 

773 c=cython.complex, 

774 d=cython.complex, 

775 t1=cython.double, 

776 t2=cython.double, 

777 delta=cython.double, 

778 delta_2=cython.double, 

779 delta_3=cython.double, 

780 a1=cython.complex, 

781 b1=cython.complex, 

782 c1=cython.complex, 

783 d1=cython.complex, 

784) 

785def _splitCubicAtTC(a, b, c, d, *ts): 

786 ts = list(ts) 

787 ts.insert(0, 0.0) 

788 ts.append(1.0) 

789 for i in range(len(ts) - 1): 

790 t1 = ts[i] 

791 t2 = ts[i + 1] 

792 delta = t2 - t1 

793 

794 delta_2 = delta * delta 

795 delta_3 = delta * delta_2 

796 t1_2 = t1 * t1 

797 t1_3 = t1 * t1_2 

798 

799 # calc new a, b, c and d 

800 a1 = a * delta_3 

801 b1 = (3 * a * t1 + b) * delta_2 

802 c1 = (2 * b * t1 + c + 3 * a * t1_2) * delta 

803 d1 = a * t1_3 + b * t1_2 + c * t1 + d 

804 pt1, pt2, pt3, pt4 = calcCubicPointsC(a1, b1, c1, d1) 

805 yield (pt1, pt2, pt3, pt4) 

806 

807 

808# 

809# Equation solvers. 

810# 

811 

812from math import sqrt, acos, cos, pi 

813 

814 

815def solveQuadratic(a, b, c, sqrt=sqrt): 

816 """Solve a quadratic equation. 

817 

818 Solves *a*x*x + b*x + c = 0* where a, b and c are real. 

819 

820 Args: 

821 a: coefficient of *x²* 

822 b: coefficient of *x* 

823 c: constant term 

824 

825 Returns: 

826 A list of roots. Note that the returned list is neither guaranteed to 

827 be sorted nor to contain unique values! 

828 """ 

829 if abs(a) < epsilon: 

830 if abs(b) < epsilon: 

831 # We have a non-equation; therefore, we have no valid solution 

832 roots = [] 

833 else: 

834 # We have a linear equation with 1 root. 

835 roots = [-c / b] 

836 else: 

837 # We have a true quadratic equation. Apply the quadratic formula to find two roots. 

838 DD = b * b - 4.0 * a * c 

839 if DD >= 0.0: 

840 rDD = sqrt(DD) 

841 roots = [(-b + rDD) / 2.0 / a, (-b - rDD) / 2.0 / a] 

842 else: 

843 # complex roots, ignore 

844 roots = [] 

845 return roots 

846 

847 

848def solveCubic(a, b, c, d): 

849 """Solve a cubic equation. 

850 

851 Solves *a*x*x*x + b*x*x + c*x + d = 0* where a, b, c and d are real. 

852 

853 Args: 

854 a: coefficient of *x³* 

855 b: coefficient of *x²* 

856 c: coefficient of *x* 

857 d: constant term 

858 

859 Returns: 

860 A list of roots. Note that the returned list is neither guaranteed to 

861 be sorted nor to contain unique values! 

862 

863 Examples:: 

864 

865 >>> solveCubic(1, 1, -6, 0) 

866 [-3.0, -0.0, 2.0] 

867 >>> solveCubic(-10.0, -9.0, 48.0, -29.0) 

868 [-2.9, 1.0, 1.0] 

869 >>> solveCubic(-9.875, -9.0, 47.625, -28.75) 

870 [-2.911392, 1.0, 1.0] 

871 >>> solveCubic(1.0, -4.5, 6.75, -3.375) 

872 [1.5, 1.5, 1.5] 

873 >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123) 

874 [0.5, 0.5, 0.5] 

875 >>> solveCubic( 

876 ... 9.0, 0.0, 0.0, -7.62939453125e-05 

877 ... ) == [-0.0, -0.0, -0.0] 

878 True 

879 """ 

880 # 

881 # adapted from: 

882 # CUBIC.C - Solve a cubic polynomial 

883 # public domain by Ross Cottrell 

884 # found at: http://www.strangecreations.com/library/snippets/Cubic.C 

885 # 

886 if abs(a) < epsilon: 

887 # don't just test for zero; for very small values of 'a' solveCubic() 

888 # returns unreliable results, so we fall back to quad. 

889 return solveQuadratic(b, c, d) 

890 a = float(a) 

891 a1 = b / a 

892 a2 = c / a 

893 a3 = d / a 

894 

895 Q = (a1 * a1 - 3.0 * a2) / 9.0 

896 R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0 

897 

898 R2 = R * R 

899 Q3 = Q * Q * Q 

900 R2 = 0 if R2 < epsilon else R2 

901 Q3 = 0 if abs(Q3) < epsilon else Q3 

902 

903 R2_Q3 = R2 - Q3 

904 

905 if R2 == 0.0 and Q3 == 0.0: 

906 x = round(-a1 / 3.0, epsilonDigits) 

907 return [x, x, x] 

908 elif R2_Q3 <= epsilon * 0.5: 

909 # The epsilon * .5 above ensures that Q3 is not zero. 

910 theta = acos(max(min(R / sqrt(Q3), 1.0), -1.0)) 

911 rQ2 = -2.0 * sqrt(Q) 

912 a1_3 = a1 / 3.0 

913 x0 = rQ2 * cos(theta / 3.0) - a1_3 

914 x1 = rQ2 * cos((theta + 2.0 * pi) / 3.0) - a1_3 

915 x2 = rQ2 * cos((theta + 4.0 * pi) / 3.0) - a1_3 

916 x0, x1, x2 = sorted([x0, x1, x2]) 

917 # Merge roots that are close-enough 

918 if x1 - x0 < epsilon and x2 - x1 < epsilon: 

919 x0 = x1 = x2 = round((x0 + x1 + x2) / 3.0, epsilonDigits) 

920 elif x1 - x0 < epsilon: 

921 x0 = x1 = round((x0 + x1) / 2.0, epsilonDigits) 

922 x2 = round(x2, epsilonDigits) 

923 elif x2 - x1 < epsilon: 

924 x0 = round(x0, epsilonDigits) 

925 x1 = x2 = round((x1 + x2) / 2.0, epsilonDigits) 

926 else: 

927 x0 = round(x0, epsilonDigits) 

928 x1 = round(x1, epsilonDigits) 

929 x2 = round(x2, epsilonDigits) 

930 return [x0, x1, x2] 

931 else: 

932 x = pow(sqrt(R2_Q3) + abs(R), 1 / 3.0) 

933 x = x + Q / x 

934 if R >= 0.0: 

935 x = -x 

936 x = round(x - a1 / 3.0, epsilonDigits) 

937 return [x] 

938 

939 

940# 

941# Conversion routines for points to parameters and vice versa 

942# 

943 

944 

945def calcQuadraticParameters(pt1, pt2, pt3): 

946 x2, y2 = pt2 

947 x3, y3 = pt3 

948 cx, cy = pt1 

949 bx = (x2 - cx) * 2.0 

950 by = (y2 - cy) * 2.0 

951 ax = x3 - cx - bx 

952 ay = y3 - cy - by 

953 return (ax, ay), (bx, by), (cx, cy) 

954 

955 

956def calcCubicParameters(pt1, pt2, pt3, pt4): 

957 x2, y2 = pt2 

958 x3, y3 = pt3 

959 x4, y4 = pt4 

960 dx, dy = pt1 

961 cx = (x2 - dx) * 3.0 

962 cy = (y2 - dy) * 3.0 

963 bx = (x3 - x2) * 3.0 - cx 

964 by = (y3 - y2) * 3.0 - cy 

965 ax = x4 - dx - cx - bx 

966 ay = y4 - dy - cy - by 

967 return (ax, ay), (bx, by), (cx, cy), (dx, dy) 

968 

969 

970@cython.cfunc 

971@cython.inline 

972@cython.locals( 

973 pt1=cython.complex, 

974 pt2=cython.complex, 

975 pt3=cython.complex, 

976 pt4=cython.complex, 

977 a=cython.complex, 

978 b=cython.complex, 

979 c=cython.complex, 

980) 

981def calcCubicParametersC(pt1, pt2, pt3, pt4): 

982 c = (pt2 - pt1) * 3.0 

983 b = (pt3 - pt2) * 3.0 - c 

984 a = pt4 - pt1 - c - b 

985 return (a, b, c, pt1) 

986 

987 

988def calcQuadraticPoints(a, b, c): 

989 ax, ay = a 

990 bx, by = b 

991 cx, cy = c 

992 x1 = cx 

993 y1 = cy 

994 x2 = (bx * 0.5) + cx 

995 y2 = (by * 0.5) + cy 

996 x3 = ax + bx + cx 

997 y3 = ay + by + cy 

998 return (x1, y1), (x2, y2), (x3, y3) 

999 

1000 

1001def calcCubicPoints(a, b, c, d): 

1002 ax, ay = a 

1003 bx, by = b 

1004 cx, cy = c 

1005 dx, dy = d 

1006 x1 = dx 

1007 y1 = dy 

1008 x2 = (cx / 3.0) + dx 

1009 y2 = (cy / 3.0) + dy 

1010 x3 = (bx + cx) / 3.0 + x2 

1011 y3 = (by + cy) / 3.0 + y2 

1012 x4 = ax + dx + cx + bx 

1013 y4 = ay + dy + cy + by 

1014 return (x1, y1), (x2, y2), (x3, y3), (x4, y4) 

1015 

1016 

1017@cython.cfunc 

1018@cython.inline 

1019@cython.locals( 

1020 a=cython.complex, 

1021 b=cython.complex, 

1022 c=cython.complex, 

1023 d=cython.complex, 

1024 p2=cython.complex, 

1025 p3=cython.complex, 

1026 p4=cython.complex, 

1027) 

1028def calcCubicPointsC(a, b, c, d): 

1029 p2 = c * (1 / 3) + d 

1030 p3 = (b + c) * (1 / 3) + p2 

1031 p4 = a + b + c + d 

1032 return (d, p2, p3, p4) 

1033 

1034 

1035# 

1036# Point at time 

1037# 

1038 

1039 

1040def linePointAtT(pt1, pt2, t): 

1041 """Finds the point at time `t` on a line. 

1042 

1043 Args: 

1044 pt1, pt2: Coordinates of the line as 2D tuples. 

1045 t: The time along the line. 

1046 

1047 Returns: 

1048 A 2D tuple with the coordinates of the point. 

1049 """ 

1050 return ((pt1[0] * (1 - t) + pt2[0] * t), (pt1[1] * (1 - t) + pt2[1] * t)) 

1051 

1052 

1053def quadraticPointAtT(pt1, pt2, pt3, t): 

1054 """Finds the point at time `t` on a quadratic curve. 

1055 

1056 Args: 

1057 pt1, pt2, pt3: Coordinates of the curve as 2D tuples. 

1058 t: The time along the curve. 

1059 

1060 Returns: 

1061 A 2D tuple with the coordinates of the point. 

1062 """ 

1063 x = (1 - t) * (1 - t) * pt1[0] + 2 * (1 - t) * t * pt2[0] + t * t * pt3[0] 

1064 y = (1 - t) * (1 - t) * pt1[1] + 2 * (1 - t) * t * pt2[1] + t * t * pt3[1] 

1065 return (x, y) 

1066 

1067 

1068def cubicPointAtT(pt1, pt2, pt3, pt4, t): 

1069 """Finds the point at time `t` on a cubic curve. 

1070 

1071 Args: 

1072 pt1, pt2, pt3, pt4: Coordinates of the curve as 2D tuples. 

1073 t: The time along the curve. 

1074 

1075 Returns: 

1076 A 2D tuple with the coordinates of the point. 

1077 """ 

1078 t2 = t * t 

1079 _1_t = 1 - t 

1080 _1_t_2 = _1_t * _1_t 

1081 x = ( 

1082 _1_t_2 * _1_t * pt1[0] 

1083 + 3 * (_1_t_2 * t * pt2[0] + _1_t * t2 * pt3[0]) 

1084 + t2 * t * pt4[0] 

1085 ) 

1086 y = ( 

1087 _1_t_2 * _1_t * pt1[1] 

1088 + 3 * (_1_t_2 * t * pt2[1] + _1_t * t2 * pt3[1]) 

1089 + t2 * t * pt4[1] 

1090 ) 

1091 return (x, y) 

1092 

1093 

1094@cython.returns(cython.complex) 

1095@cython.locals( 

1096 t=cython.double, 

1097 pt1=cython.complex, 

1098 pt2=cython.complex, 

1099 pt3=cython.complex, 

1100 pt4=cython.complex, 

1101) 

1102@cython.locals(t2=cython.double, _1_t=cython.double, _1_t_2=cython.double) 

1103def cubicPointAtTC(pt1, pt2, pt3, pt4, t): 

1104 """Finds the point at time `t` on a cubic curve. 

1105 

1106 Args: 

1107 pt1, pt2, pt3, pt4: Coordinates of the curve as complex numbers. 

1108 t: The time along the curve. 

1109 

1110 Returns: 

1111 A complex number with the coordinates of the point. 

1112 """ 

1113 t2 = t * t 

1114 _1_t = 1 - t 

1115 _1_t_2 = _1_t * _1_t 

1116 return _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4 

1117 

1118 

1119def segmentPointAtT(seg, t): 

1120 if len(seg) == 2: 

1121 return linePointAtT(*seg, t) 

1122 elif len(seg) == 3: 

1123 return quadraticPointAtT(*seg, t) 

1124 elif len(seg) == 4: 

1125 return cubicPointAtT(*seg, t) 

1126 raise ValueError("Unknown curve degree") 

1127 

1128 

1129# 

1130# Intersection finders 

1131# 

1132 

1133 

1134def _line_t_of_pt(s, e, pt): 

1135 sx, sy = s 

1136 ex, ey = e 

1137 px, py = pt 

1138 if abs(sx - ex) < epsilon and abs(sy - ey) < epsilon: 

1139 # Line is a point! 

1140 return -1 

1141 # Use the largest 

1142 if abs(sx - ex) > abs(sy - ey): 

1143 return (px - sx) / (ex - sx) 

1144 else: 

1145 return (py - sy) / (ey - sy) 

1146 

1147 

1148def _both_points_are_on_same_side_of_origin(a, b, origin): 

1149 xDiff = (a[0] - origin[0]) * (b[0] - origin[0]) 

1150 yDiff = (a[1] - origin[1]) * (b[1] - origin[1]) 

1151 return not (xDiff <= 0.0 and yDiff <= 0.0) 

1152 

1153 

1154def lineLineIntersections(s1, e1, s2, e2): 

1155 """Finds intersections between two line segments. 

1156 

1157 Args: 

1158 s1, e1: Coordinates of the first line as 2D tuples. 

1159 s2, e2: Coordinates of the second line as 2D tuples. 

1160 

1161 Returns: 

1162 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 

1163 and ``t2`` attributes containing the intersection point, time on first 

1164 segment and time on second segment respectively. 

1165 

1166 Examples:: 

1167 

1168 >>> a = lineLineIntersections( (310,389), (453, 222), (289, 251), (447, 367)) 

1169 >>> len(a) 

1170 1 

1171 >>> intersection = a[0] 

1172 >>> intersection.pt 

1173 (374.44882952482897, 313.73458370177315) 

1174 >>> (intersection.t1, intersection.t2) 

1175 (0.45069111555824465, 0.5408153767394238) 

1176 """ 

1177 s1x, s1y = s1 

1178 e1x, e1y = e1 

1179 s2x, s2y = s2 

1180 e2x, e2y = e2 

1181 if ( 

1182 math.isclose(s2x, e2x) and math.isclose(s1x, e1x) and not math.isclose(s1x, s2x) 

1183 ): # Parallel vertical 

1184 return [] 

1185 if ( 

1186 math.isclose(s2y, e2y) and math.isclose(s1y, e1y) and not math.isclose(s1y, s2y) 

1187 ): # Parallel horizontal 

1188 return [] 

1189 if math.isclose(s2x, e2x) and math.isclose(s2y, e2y): # Line segment is tiny 

1190 return [] 

1191 if math.isclose(s1x, e1x) and math.isclose(s1y, e1y): # Line segment is tiny 

1192 return [] 

1193 if math.isclose(e1x, s1x): 

1194 x = s1x 

1195 slope34 = (e2y - s2y) / (e2x - s2x) 

1196 y = slope34 * (x - s2x) + s2y 

1197 pt = (x, y) 

1198 return [ 

1199 Intersection( 

1200 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 

1201 ) 

1202 ] 

1203 if math.isclose(s2x, e2x): 

1204 x = s2x 

1205 slope12 = (e1y - s1y) / (e1x - s1x) 

1206 y = slope12 * (x - s1x) + s1y 

1207 pt = (x, y) 

1208 return [ 

1209 Intersection( 

1210 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 

1211 ) 

1212 ] 

1213 

1214 slope12 = (e1y - s1y) / (e1x - s1x) 

1215 slope34 = (e2y - s2y) / (e2x - s2x) 

1216 if math.isclose(slope12, slope34): 

1217 return [] 

1218 x = (slope12 * s1x - s1y - slope34 * s2x + s2y) / (slope12 - slope34) 

1219 y = slope12 * (x - s1x) + s1y 

1220 pt = (x, y) 

1221 if _both_points_are_on_same_side_of_origin( 

1222 pt, e1, s1 

1223 ) and _both_points_are_on_same_side_of_origin(pt, s2, e2): 

1224 return [ 

1225 Intersection( 

1226 pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt) 

1227 ) 

1228 ] 

1229 return [] 

1230 

1231 

1232def _alignment_transformation(segment): 

1233 # Returns a transformation which aligns a segment horizontally at the 

1234 # origin. Apply this transformation to curves and root-find to find 

1235 # intersections with the segment. 

1236 start = segment[0] 

1237 end = segment[-1] 

1238 angle = math.atan2(end[1] - start[1], end[0] - start[0]) 

1239 return Identity.rotate(-angle).translate(-start[0], -start[1]) 

1240 

1241 

1242def _curve_line_intersections_t(curve, line): 

1243 aligned_curve = _alignment_transformation(line).transformPoints(curve) 

1244 if len(curve) == 3: 

1245 a, b, c = calcQuadraticParameters(*aligned_curve) 

1246 intersections = solveQuadratic(a[1], b[1], c[1]) 

1247 elif len(curve) == 4: 

1248 a, b, c, d = calcCubicParameters(*aligned_curve) 

1249 intersections = solveCubic(a[1], b[1], c[1], d[1]) 

1250 else: 

1251 raise ValueError("Unknown curve degree") 

1252 return sorted(i for i in intersections if 0.0 <= i <= 1) 

1253 

1254 

1255def curveLineIntersections(curve, line): 

1256 """Finds intersections between a curve and a line. 

1257 

1258 Args: 

1259 curve: List of coordinates of the curve segment as 2D tuples. 

1260 line: List of coordinates of the line segment as 2D tuples. 

1261 

1262 Returns: 

1263 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 

1264 and ``t2`` attributes containing the intersection point, time on first 

1265 segment and time on second segment respectively. 

1266 

1267 Examples:: 

1268 >>> curve = [ (100, 240), (30, 60), (210, 230), (160, 30) ] 

1269 >>> line = [ (25, 260), (230, 20) ] 

1270 >>> intersections = curveLineIntersections(curve, line) 

1271 >>> len(intersections) 

1272 3 

1273 >>> intersections[0].pt 

1274 (84.9000930760723, 189.87306176459828) 

1275 """ 

1276 if len(curve) == 3: 

1277 pointFinder = quadraticPointAtT 

1278 elif len(curve) == 4: 

1279 pointFinder = cubicPointAtT 

1280 else: 

1281 raise ValueError("Unknown curve degree") 

1282 intersections = [] 

1283 for t in _curve_line_intersections_t(curve, line): 

1284 pt = pointFinder(*curve, t) 

1285 # Back-project the point onto the line, to avoid problems with 

1286 # numerical accuracy in the case of vertical and horizontal lines 

1287 line_t = _line_t_of_pt(*line, pt) 

1288 pt = linePointAtT(*line, line_t) 

1289 intersections.append(Intersection(pt=pt, t1=t, t2=line_t)) 

1290 return intersections 

1291 

1292 

1293def _curve_bounds(c): 

1294 if len(c) == 3: 

1295 return calcQuadraticBounds(*c) 

1296 elif len(c) == 4: 

1297 return calcCubicBounds(*c) 

1298 raise ValueError("Unknown curve degree") 

1299 

1300 

1301def _split_segment_at_t(c, t): 

1302 if len(c) == 2: 

1303 s, e = c 

1304 midpoint = linePointAtT(s, e, t) 

1305 return [(s, midpoint), (midpoint, e)] 

1306 if len(c) == 3: 

1307 return splitQuadraticAtT(*c, t) 

1308 elif len(c) == 4: 

1309 return splitCubicAtT(*c, t) 

1310 raise ValueError("Unknown curve degree") 

1311 

1312 

1313def _curve_curve_intersections_t( 

1314 curve1, curve2, precision=1e-3, range1=None, range2=None 

1315): 

1316 bounds1 = _curve_bounds(curve1) 

1317 bounds2 = _curve_bounds(curve2) 

1318 

1319 if not range1: 

1320 range1 = (0.0, 1.0) 

1321 if not range2: 

1322 range2 = (0.0, 1.0) 

1323 

1324 # If bounds don't intersect, go home 

1325 intersects, _ = sectRect(bounds1, bounds2) 

1326 if not intersects: 

1327 return [] 

1328 

1329 def midpoint(r): 

1330 return 0.5 * (r[0] + r[1]) 

1331 

1332 # If they do overlap but they're tiny, approximate 

1333 if rectArea(bounds1) < precision and rectArea(bounds2) < precision: 

1334 return [(midpoint(range1), midpoint(range2))] 

1335 

1336 c11, c12 = _split_segment_at_t(curve1, 0.5) 

1337 c11_range = (range1[0], midpoint(range1)) 

1338 c12_range = (midpoint(range1), range1[1]) 

1339 

1340 c21, c22 = _split_segment_at_t(curve2, 0.5) 

1341 c21_range = (range2[0], midpoint(range2)) 

1342 c22_range = (midpoint(range2), range2[1]) 

1343 

1344 found = [] 

1345 found.extend( 

1346 _curve_curve_intersections_t( 

1347 c11, c21, precision, range1=c11_range, range2=c21_range 

1348 ) 

1349 ) 

1350 found.extend( 

1351 _curve_curve_intersections_t( 

1352 c12, c21, precision, range1=c12_range, range2=c21_range 

1353 ) 

1354 ) 

1355 found.extend( 

1356 _curve_curve_intersections_t( 

1357 c11, c22, precision, range1=c11_range, range2=c22_range 

1358 ) 

1359 ) 

1360 found.extend( 

1361 _curve_curve_intersections_t( 

1362 c12, c22, precision, range1=c12_range, range2=c22_range 

1363 ) 

1364 ) 

1365 

1366 unique_key = lambda ts: (int(ts[0] / precision), int(ts[1] / precision)) 

1367 seen = set() 

1368 unique_values = [] 

1369 

1370 for ts in found: 

1371 key = unique_key(ts) 

1372 if key in seen: 

1373 continue 

1374 seen.add(key) 

1375 unique_values.append(ts) 

1376 

1377 return unique_values 

1378 

1379 

1380def _is_linelike(segment): 

1381 maybeline = _alignment_transformation(segment).transformPoints(segment) 

1382 return all(math.isclose(p[1], 0.0) for p in maybeline) 

1383 

1384 

1385def curveCurveIntersections(curve1, curve2): 

1386 """Finds intersections between a curve and a curve. 

1387 

1388 Args: 

1389 curve1: List of coordinates of the first curve segment as 2D tuples. 

1390 curve2: List of coordinates of the second curve segment as 2D tuples. 

1391 

1392 Returns: 

1393 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 

1394 and ``t2`` attributes containing the intersection point, time on first 

1395 segment and time on second segment respectively. 

1396 

1397 Examples:: 

1398 >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ] 

1399 >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ] 

1400 >>> intersections = curveCurveIntersections(curve1, curve2) 

1401 >>> len(intersections) 

1402 3 

1403 >>> intersections[0].pt 

1404 (81.7831487395506, 109.88904552375288) 

1405 """ 

1406 if _is_linelike(curve1): 

1407 line1 = curve1[0], curve1[-1] 

1408 if _is_linelike(curve2): 

1409 line2 = curve2[0], curve2[-1] 

1410 return lineLineIntersections(*line1, *line2) 

1411 else: 

1412 return curveLineIntersections(curve2, line1) 

1413 elif _is_linelike(curve2): 

1414 line2 = curve2[0], curve2[-1] 

1415 return curveLineIntersections(curve1, line2) 

1416 

1417 intersection_ts = _curve_curve_intersections_t(curve1, curve2) 

1418 return [ 

1419 Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1]) 

1420 for ts in intersection_ts 

1421 ] 

1422 

1423 

1424def segmentSegmentIntersections(seg1, seg2): 

1425 """Finds intersections between two segments. 

1426 

1427 Args: 

1428 seg1: List of coordinates of the first segment as 2D tuples. 

1429 seg2: List of coordinates of the second segment as 2D tuples. 

1430 

1431 Returns: 

1432 A list of ``Intersection`` objects, each object having ``pt``, ``t1`` 

1433 and ``t2`` attributes containing the intersection point, time on first 

1434 segment and time on second segment respectively. 

1435 

1436 Examples:: 

1437 >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ] 

1438 >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ] 

1439 >>> intersections = segmentSegmentIntersections(curve1, curve2) 

1440 >>> len(intersections) 

1441 3 

1442 >>> intersections[0].pt 

1443 (81.7831487395506, 109.88904552375288) 

1444 >>> curve3 = [ (100, 240), (30, 60), (210, 230), (160, 30) ] 

1445 >>> line = [ (25, 260), (230, 20) ] 

1446 >>> intersections = segmentSegmentIntersections(curve3, line) 

1447 >>> len(intersections) 

1448 3 

1449 >>> intersections[0].pt 

1450 (84.9000930760723, 189.87306176459828) 

1451 

1452 """ 

1453 # Arrange by degree 

1454 swapped = False 

1455 if len(seg2) > len(seg1): 

1456 seg2, seg1 = seg1, seg2 

1457 swapped = True 

1458 if len(seg1) > 2: 

1459 if len(seg2) > 2: 

1460 intersections = curveCurveIntersections(seg1, seg2) 

1461 else: 

1462 intersections = curveLineIntersections(seg1, seg2) 

1463 elif len(seg1) == 2 and len(seg2) == 2: 

1464 intersections = lineLineIntersections(*seg1, *seg2) 

1465 else: 

1466 raise ValueError("Couldn't work out which intersection function to use") 

1467 if not swapped: 

1468 return intersections 

1469 return [Intersection(pt=i.pt, t1=i.t2, t2=i.t1) for i in intersections] 

1470 

1471 

1472def _segmentrepr(obj): 

1473 """ 

1474 >>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]]) 

1475 '(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))' 

1476 """ 

1477 try: 

1478 it = iter(obj) 

1479 except TypeError: 

1480 return "%g" % obj 

1481 else: 

1482 return "(%s)" % ", ".join(_segmentrepr(x) for x in it) 

1483 

1484 

1485def printSegments(segments): 

1486 """Helper for the doctests, displaying each segment in a list of 

1487 segments on a single line as a tuple. 

1488 """ 

1489 for segment in segments: 

1490 print(_segmentrepr(segment)) 

1491 

1492 

1493if __name__ == "__main__": 

1494 import sys 

1495 import doctest 

1496 

1497 sys.exit(doctest.testmod().failed)