Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

556 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 

12 

13 COMPILED = cython.compiled 

14except (AttributeError, ImportError): 

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

16 from fontTools.misc import cython 

17 

18 COMPILED = False 

19 

20 

21EPSILON = 1e-9 

22 

23 

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

25 

26 

27__all__ = [ 

28 "approximateCubicArcLength", 

29 "approximateCubicArcLengthC", 

30 "approximateQuadraticArcLength", 

31 "approximateQuadraticArcLengthC", 

32 "calcCubicArcLength", 

33 "calcCubicArcLengthC", 

34 "calcQuadraticArcLength", 

35 "calcQuadraticArcLengthC", 

36 "calcCubicBounds", 

37 "calcQuadraticBounds", 

38 "splitLine", 

39 "splitQuadratic", 

40 "splitCubic", 

41 "splitQuadraticAtT", 

42 "splitCubicAtT", 

43 "splitCubicAtTC", 

44 "splitCubicIntoTwoAtTC", 

45 "solveQuadratic", 

46 "solveCubic", 

47 "quadraticPointAtT", 

48 "cubicPointAtT", 

49 "cubicPointAtTC", 

50 "linePointAtT", 

51 "segmentPointAtT", 

52 "lineLineIntersections", 

53 "curveLineIntersections", 

54 "curveCurveIntersections", 

55 "segmentSegmentIntersections", 

56] 

57 

58 

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

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

61 

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

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

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

65 

66 Args: 

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

68 tolerance: Controls the precision of the calcuation. 

69 

70 Returns: 

71 Arc length value. 

72 """ 

73 return calcCubicArcLengthC( 

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

75 ) 

76 

77 

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

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

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

81 return ( 

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

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

84 ) 

85 

86 

87@cython.returns(cython.double) 

88@cython.locals( 

89 p0=cython.complex, 

90 p1=cython.complex, 

91 p2=cython.complex, 

92 p3=cython.complex, 

93) 

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

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

96 arch = abs(p0 - p3) 

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

98 if arch * mult + EPSILON >= box: 

99 return (arch + box) * 0.5 

100 else: 

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

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

103 mult, *two 

104 ) 

105 

106 

107@cython.returns(cython.double) 

108@cython.locals( 

109 pt1=cython.complex, 

110 pt2=cython.complex, 

111 pt3=cython.complex, 

112 pt4=cython.complex, 

113) 

114@cython.locals( 

115 tolerance=cython.double, 

116 mult=cython.double, 

117) 

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

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

120 

121 Args: 

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

123 tolerance: Controls the precision of the calcuation. 

124 

125 Returns: 

126 Arc length value. 

127 """ 

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

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

130 

131 

132epsilonDigits = 6 

133epsilon = 1e-10 

134 

135 

136@cython.cfunc 

137@cython.inline 

138@cython.returns(cython.double) 

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

140def _dot(v1, v2): 

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

142 

143 

144@cython.cfunc 

145@cython.inline 

146@cython.returns(cython.double) 

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

148def _intSecAtan(x): 

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

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

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

152 

153 

154def calcQuadraticArcLength(pt1, pt2, pt3): 

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

156 

157 Args: 

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

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

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

161 

162 Returns: 

163 Arc length value. 

164 

165 Example:: 

166 

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

168 0.0 

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

170 80.0 

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

172 80.0 

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

174 107.70329614269008 

175 >>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0)) 

176 154.02976155645263 

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

178 120.21581243984076 

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

180 102.53273816445825 

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

182 66.66666666666667 

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

184 40.0 

185 """ 

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

187 

188 

189@cython.returns(cython.double) 

190@cython.locals( 

191 pt1=cython.complex, 

192 pt2=cython.complex, 

193 pt3=cython.complex, 

194 d0=cython.complex, 

195 d1=cython.complex, 

196 d=cython.complex, 

197 n=cython.complex, 

198) 

199@cython.locals( 

200 scale=cython.double, 

201 origDist=cython.double, 

202 a=cython.double, 

203 b=cython.double, 

204 x0=cython.double, 

205 x1=cython.double, 

206 Len=cython.double, 

207) 

208def calcQuadraticArcLengthC(pt1, pt2, pt3): 

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

210 

211 Args: 

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

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

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

215 

216 Returns: 

217 Arc length value. 

218 """ 

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

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

221 d0 = pt2 - pt1 

222 d1 = pt3 - pt2 

223 d = d1 - d0 

224 n = d * 1j 

225 scale = abs(n) 

226 if scale == 0.0: 

227 return abs(pt3 - pt1) 

228 origDist = _dot(n, d0) 

229 if abs(origDist) < epsilon: 

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

231 return abs(pt3 - pt1) 

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

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

234 x0 = _dot(d, d0) / origDist 

235 x1 = _dot(d, d1) / origDist 

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

237 return Len 

238 

239 

240def approximateQuadraticArcLength(pt1, pt2, pt3): 

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

242 

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

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

245 

246 Args: 

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

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

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

250 

251 Returns: 

252 Approximate arc length value. 

253 """ 

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

255 

256 

257@cython.returns(cython.double) 

258@cython.locals( 

259 pt1=cython.complex, 

260 pt2=cython.complex, 

261 pt3=cython.complex, 

262) 

263@cython.locals( 

264 v0=cython.double, 

265 v1=cython.double, 

266 v2=cython.double, 

267) 

268def approximateQuadraticArcLengthC(pt1, pt2, pt3): 

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

270 

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

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

273 

274 Args: 

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

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

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

278 

279 Returns: 

280 Approximate arc length value. 

281 """ 

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

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

284 # approximation of it. 

285 # 

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

287 

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

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

290 v0 = abs( 

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

292 ) 

293 v1 = abs(pt3 - pt1) * 0.4444444444444444 

294 v2 = abs( 

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

296 ) 

297 

298 return v0 + v1 + v2 

299 

300 

301def calcQuadraticBounds(pt1, pt2, pt3): 

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

303 

304 Args: 

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

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

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

308 

309 Returns: 

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

311 

312 Example:: 

313 

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

315 (0, 0, 100, 50.0) 

316 >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100)) 

317 (0.0, 0.0, 100, 100) 

318 """ 

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

320 ax2 = ax * 2.0 

321 ay2 = ay * 2.0 

322 roots = [] 

323 if ax2 != 0: 

324 roots.append(-bx / ax2) 

325 if ay2 != 0: 

326 roots.append(-by / ay2) 

327 points = [ 

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

329 for t in roots 

330 if 0 <= t < 1 

331 ] + [pt1, pt3] 

332 return calcBounds(points) 

333 

334 

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

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

337 

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

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

340 

341 Args: 

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

343 

344 Returns: 

345 Arc length value. 

346 

347 Example:: 

348 

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

350 190.04332968932817 

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

352 154.8852074945903 

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

354 149.99999999999991 

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

356 136.9267662156362 

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

358 154.80848416537057 

359 """ 

360 return approximateCubicArcLengthC( 

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

362 ) 

363 

364 

365@cython.returns(cython.double) 

366@cython.locals( 

367 pt1=cython.complex, 

368 pt2=cython.complex, 

369 pt3=cython.complex, 

370 pt4=cython.complex, 

371) 

372@cython.locals( 

373 v0=cython.double, 

374 v1=cython.double, 

375 v2=cython.double, 

376 v3=cython.double, 

377 v4=cython.double, 

378) 

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

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

381 

382 Args: 

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

384 

385 Returns: 

386 Arc length value. 

387 """ 

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

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

390 # approximation of it. 

391 # 

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

393 

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

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

396 v0 = abs(pt2 - pt1) * 0.15 

397 v1 = abs( 

398 -0.558983582205757 * pt1 

399 + 0.325650248872424 * pt2 

400 + 0.208983582205757 * pt3 

401 + 0.024349751127576 * pt4 

402 ) 

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

404 v3 = abs( 

405 -0.024349751127576 * pt1 

406 - 0.208983582205757 * pt2 

407 - 0.325650248872424 * pt3 

408 + 0.558983582205757 * pt4 

409 ) 

410 v4 = abs(pt4 - pt3) * 0.15 

411 

412 return v0 + v1 + v2 + v3 + v4 

413 

414 

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

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

417 

418 Args: 

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

420 

421 Returns: 

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

423 

424 Example:: 

425 

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

427 (0, 0, 100, 75.0) 

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

429 (0.0, 0.0, 100, 100) 

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

431 35.566243 0.000000 64.433757 75.000000 

432 """ 

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

434 # calc first derivative 

435 ax3 = ax * 3.0 

436 ay3 = ay * 3.0 

437 bx2 = bx * 2.0 

438 by2 = by * 2.0 

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

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

441 roots = xRoots + yRoots 

442 

443 points = [ 

444 ( 

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

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

447 ) 

448 for t in roots 

449 ] + [pt1, pt4] 

450 return calcBounds(points) 

451 

452 

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

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

455 

456 Args: 

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

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

459 where: Position at which to split the line. 

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

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

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

463 

464 Returns: 

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

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

467 line. 

468 

469 Example:: 

470 

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

472 ((0, 0), (50, 50)) 

473 ((50, 50), (100, 100)) 

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

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

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

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

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

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

480 ((0, 0), (0, 0)) 

481 ((0, 0), (100, 100)) 

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

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

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

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

486 ((0, 100), (0, 50)) 

487 ((0, 50), (0, 0)) 

488 """ 

489 pt1x, pt1y = pt1 

490 pt2x, pt2y = pt2 

491 

492 ax = pt2x - pt1x 

493 ay = pt2y - pt1y 

494 

495 bx = pt1x 

496 by = pt1y 

497 

498 a = (ax, ay)[isHorizontal] 

499 

500 if a == 0: 

501 return [(pt1, pt2)] 

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

503 if 0 <= t < 1: 

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

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

506 else: 

507 return [(pt1, pt2)] 

508 

509 

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

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

512 

513 Args: 

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

515 where: Position at which to split the curve. 

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

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

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

519 

520 Returns: 

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

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

523 curve. 

524 

525 Example:: 

526 

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

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

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

530 ((0, 0), (25, 50), (50, 50)) 

531 ((50, 50), (75, 50), (100, 0)) 

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

533 ((0, 0), (12.5, 25), (25, 37.5)) 

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

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

536 ((0, 0), (7.32233, 14.6447), (14.6447, 25)) 

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

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

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

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

541 ((0, 0), (25, 50), (50, 50)) 

542 ((50, 50), (50, 50), (50, 50)) 

543 ((50, 50), (75, 50), (100, 0)) 

544 """ 

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

546 solutions = solveQuadratic( 

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

548 ) 

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

550 if not solutions: 

551 return [(pt1, pt2, pt3)] 

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

553 

554 

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

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

557 

558 Args: 

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

560 where: Position at which to split the curve. 

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

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

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

564 

565 Returns: 

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

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

568 curve. 

569 

570 Example:: 

571 

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

573 ((0, 0), (25, 100), (75, 100), (100, 0)) 

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

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

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

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

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

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

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

581 """ 

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

583 solutions = solveCubic( 

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

585 ) 

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

587 if not solutions: 

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

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

590 

591 

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

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

594 

595 Args: 

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

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

598 

599 Returns: 

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

601 

602 Examples:: 

603 

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

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

606 ((50, 50), (75, 50), (100, 0)) 

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

608 ((0, 0), (25, 50), (50, 50)) 

609 ((50, 50), (62.5, 50), (75, 37.5)) 

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

611 """ 

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

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

614 

615 

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

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

618 

619 Args: 

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

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

622 

623 Returns: 

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

625 

626 Examples:: 

627 

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

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

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

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

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

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

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

635 """ 

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

637 return _splitCubicAtT(a, b, c, d, *ts) 

638 

639 

640@cython.locals( 

641 pt1=cython.complex, 

642 pt2=cython.complex, 

643 pt3=cython.complex, 

644 pt4=cython.complex, 

645 a=cython.complex, 

646 b=cython.complex, 

647 c=cython.complex, 

648 d=cython.complex, 

649) 

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

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

652 

653 Args: 

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

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

656 

657 Yields: 

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

659 """ 

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

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

662 

663 

664@cython.returns(cython.complex) 

665@cython.locals( 

666 t=cython.double, 

667 pt1=cython.complex, 

668 pt2=cython.complex, 

669 pt3=cython.complex, 

670 pt4=cython.complex, 

671 pointAtT=cython.complex, 

672 off1=cython.complex, 

673 off2=cython.complex, 

674) 

675@cython.locals( 

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

677) 

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

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

680 

681 Args: 

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

683 t: Position at which to split the curve. 

684 

685 Returns: 

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

687 """ 

688 t2 = t * t 

689 _1_t = 1 - t 

690 _1_t_2 = _1_t * _1_t 

691 _2_t_1_t = 2 * t * _1_t 

692 pointAtT = ( 

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

694 ) 

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

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

697 

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

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

700 

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

702 

703 

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

705 ts = list(ts) 

706 segments = [] 

707 ts.insert(0, 0.0) 

708 ts.append(1.0) 

709 ax, ay = a 

710 bx, by = b 

711 cx, cy = c 

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

713 t1 = ts[i] 

714 t2 = ts[i + 1] 

715 delta = t2 - t1 

716 # calc new a, b and c 

717 delta_2 = delta * delta 

718 a1x = ax * delta_2 

719 a1y = ay * delta_2 

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

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

722 t1_2 = t1 * t1 

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

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

725 

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

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

728 return segments 

729 

730 

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

732 ts = list(ts) 

733 ts.insert(0, 0.0) 

734 ts.append(1.0) 

735 segments = [] 

736 ax, ay = a 

737 bx, by = b 

738 cx, cy = c 

739 dx, dy = d 

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

741 t1 = ts[i] 

742 t2 = ts[i + 1] 

743 delta = t2 - t1 

744 

745 delta_2 = delta * delta 

746 delta_3 = delta * delta_2 

747 t1_2 = t1 * t1 

748 t1_3 = t1 * t1_2 

749 

750 # calc new a, b, c and d 

751 a1x = ax * delta_3 

752 a1y = ay * delta_3 

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

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

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

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

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

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

759 pt1, pt2, pt3, pt4 = calcCubicPoints( 

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

761 ) 

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

763 return segments 

764 

765 

766@cython.locals( 

767 a=cython.complex, 

768 b=cython.complex, 

769 c=cython.complex, 

770 d=cython.complex, 

771 t1=cython.double, 

772 t2=cython.double, 

773 delta=cython.double, 

774 delta_2=cython.double, 

775 delta_3=cython.double, 

776 a1=cython.complex, 

777 b1=cython.complex, 

778 c1=cython.complex, 

779 d1=cython.complex, 

780) 

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

782 ts = list(ts) 

783 ts.insert(0, 0.0) 

784 ts.append(1.0) 

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

786 t1 = ts[i] 

787 t2 = ts[i + 1] 

788 delta = t2 - t1 

789 

790 delta_2 = delta * delta 

791 delta_3 = delta * delta_2 

792 t1_2 = t1 * t1 

793 t1_3 = t1 * t1_2 

794 

795 # calc new a, b, c and d 

796 a1 = a * delta_3 

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

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

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

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

801 yield (pt1, pt2, pt3, pt4) 

802 

803 

804# 

805# Equation solvers. 

806# 

807 

808from math import sqrt, acos, cos, pi 

809 

810 

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

812 """Solve a quadratic equation. 

813 

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

815 

816 Args: 

817 a: coefficient of *x²* 

818 b: coefficient of *x* 

819 c: constant term 

820 

821 Returns: 

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

823 be sorted nor to contain unique values! 

824 """ 

825 if abs(a) < epsilon: 

826 if abs(b) < epsilon: 

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

828 roots = [] 

829 else: 

830 # We have a linear equation with 1 root. 

831 roots = [-c / b] 

832 else: 

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

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

835 if DD >= 0.0: 

836 rDD = sqrt(DD) 

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

838 else: 

839 # complex roots, ignore 

840 roots = [] 

841 return roots 

842 

843 

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

845 """Solve a cubic equation. 

846 

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

848 

849 Args: 

850 a: coefficient of *x³* 

851 b: coefficient of *x²* 

852 c: coefficient of *x* 

853 d: constant term 

854 

855 Returns: 

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

857 be sorted nor to contain unique values! 

858 

859 Examples:: 

860 

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

862 [-3.0, -0.0, 2.0] 

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

864 [-2.9, 1.0, 1.0] 

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

866 [-2.911392, 1.0, 1.0] 

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

868 [1.5, 1.5, 1.5] 

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

870 [0.5, 0.5, 0.5] 

871 >>> solveCubic( 

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

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

874 True 

875 """ 

876 # 

877 # adapted from: 

878 # CUBIC.C - Solve a cubic polynomial 

879 # public domain by Ross Cottrell 

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

881 # 

882 if abs(a) < epsilon: 

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

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

885 return solveQuadratic(b, c, d) 

886 a = float(a) 

887 a1 = b / a 

888 a2 = c / a 

889 a3 = d / a 

890 

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

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

893 

894 R2 = R * R 

895 Q3 = Q * Q * Q 

896 R2 = 0 if R2 < epsilon else R2 

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

898 

899 R2_Q3 = R2 - Q3 

900 

901 if R2 == 0.0 and Q3 == 0.0: 

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

903 return [x, x, x] 

904 elif R2_Q3 <= epsilon * 0.5: 

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

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

907 rQ2 = -2.0 * sqrt(Q) 

908 a1_3 = a1 / 3.0 

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

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

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

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

913 # Merge roots that are close-enough 

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

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

916 elif x1 - x0 < epsilon: 

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

918 x2 = round(x2, epsilonDigits) 

919 elif x2 - x1 < epsilon: 

920 x0 = round(x0, epsilonDigits) 

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

922 else: 

923 x0 = round(x0, epsilonDigits) 

924 x1 = round(x1, epsilonDigits) 

925 x2 = round(x2, epsilonDigits) 

926 return [x0, x1, x2] 

927 else: 

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

929 x = x + Q / x 

930 if R >= 0.0: 

931 x = -x 

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

933 return [x] 

934 

935 

936# 

937# Conversion routines for points to parameters and vice versa 

938# 

939 

940 

941def calcQuadraticParameters(pt1, pt2, pt3): 

942 x2, y2 = pt2 

943 x3, y3 = pt3 

944 cx, cy = pt1 

945 bx = (x2 - cx) * 2.0 

946 by = (y2 - cy) * 2.0 

947 ax = x3 - cx - bx 

948 ay = y3 - cy - by 

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

950 

951 

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

953 x2, y2 = pt2 

954 x3, y3 = pt3 

955 x4, y4 = pt4 

956 dx, dy = pt1 

957 cx = (x2 - dx) * 3.0 

958 cy = (y2 - dy) * 3.0 

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

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

961 ax = x4 - dx - cx - bx 

962 ay = y4 - dy - cy - by 

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

964 

965 

966@cython.cfunc 

967@cython.inline 

968@cython.locals( 

969 pt1=cython.complex, 

970 pt2=cython.complex, 

971 pt3=cython.complex, 

972 pt4=cython.complex, 

973 a=cython.complex, 

974 b=cython.complex, 

975 c=cython.complex, 

976) 

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

978 c = (pt2 - pt1) * 3.0 

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

980 a = pt4 - pt1 - c - b 

981 return (a, b, c, pt1) 

982 

983 

984def calcQuadraticPoints(a, b, c): 

985 ax, ay = a 

986 bx, by = b 

987 cx, cy = c 

988 x1 = cx 

989 y1 = cy 

990 x2 = (bx * 0.5) + cx 

991 y2 = (by * 0.5) + cy 

992 x3 = ax + bx + cx 

993 y3 = ay + by + cy 

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

995 

996 

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

998 ax, ay = a 

999 bx, by = b 

1000 cx, cy = c 

1001 dx, dy = d 

1002 x1 = dx 

1003 y1 = dy 

1004 x2 = (cx / 3.0) + dx 

1005 y2 = (cy / 3.0) + dy 

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

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

1008 x4 = ax + dx + cx + bx 

1009 y4 = ay + dy + cy + by 

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

1011 

1012 

1013@cython.cfunc 

1014@cython.inline 

1015@cython.locals( 

1016 a=cython.complex, 

1017 b=cython.complex, 

1018 c=cython.complex, 

1019 d=cython.complex, 

1020 p2=cython.complex, 

1021 p3=cython.complex, 

1022 p4=cython.complex, 

1023) 

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

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

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

1027 p4 = a + b + c + d 

1028 return (d, p2, p3, p4) 

1029 

1030 

1031# 

1032# Point at time 

1033# 

1034 

1035 

1036def linePointAtT(pt1, pt2, t): 

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

1038 

1039 Args: 

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

1041 t: The time along the line. 

1042 

1043 Returns: 

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

1045 """ 

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

1047 

1048 

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

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

1051 

1052 Args: 

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

1054 t: The time along the curve. 

1055 

1056 Returns: 

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

1058 """ 

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

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

1061 return (x, y) 

1062 

1063 

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

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

1066 

1067 Args: 

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

1069 t: The time along the curve. 

1070 

1071 Returns: 

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

1073 """ 

1074 t2 = t * t 

1075 _1_t = 1 - t 

1076 _1_t_2 = _1_t * _1_t 

1077 x = ( 

1078 _1_t_2 * _1_t * pt1[0] 

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

1080 + t2 * t * pt4[0] 

1081 ) 

1082 y = ( 

1083 _1_t_2 * _1_t * pt1[1] 

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

1085 + t2 * t * pt4[1] 

1086 ) 

1087 return (x, y) 

1088 

1089 

1090@cython.returns(cython.complex) 

1091@cython.locals( 

1092 t=cython.double, 

1093 pt1=cython.complex, 

1094 pt2=cython.complex, 

1095 pt3=cython.complex, 

1096 pt4=cython.complex, 

1097) 

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

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

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

1101 

1102 Args: 

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

1104 t: The time along the curve. 

1105 

1106 Returns: 

1107 A complex number with the coordinates of the point. 

1108 """ 

1109 t2 = t * t 

1110 _1_t = 1 - t 

1111 _1_t_2 = _1_t * _1_t 

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

1113 

1114 

1115def segmentPointAtT(seg, t): 

1116 if len(seg) == 2: 

1117 return linePointAtT(*seg, t) 

1118 elif len(seg) == 3: 

1119 return quadraticPointAtT(*seg, t) 

1120 elif len(seg) == 4: 

1121 return cubicPointAtT(*seg, t) 

1122 raise ValueError("Unknown curve degree") 

1123 

1124 

1125# 

1126# Intersection finders 

1127# 

1128 

1129 

1130def _line_t_of_pt(s, e, pt): 

1131 sx, sy = s 

1132 ex, ey = e 

1133 px, py = pt 

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

1135 # Line is a point! 

1136 return -1 

1137 # Use the largest 

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

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

1140 else: 

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

1142 

1143 

1144def _both_points_are_on_same_side_of_origin(a, b, origin): 

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

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

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

1148 

1149 

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

1151 """Finds intersections between two line segments. 

1152 

1153 Args: 

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

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

1156 

1157 Returns: 

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

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

1160 segment and time on second segment respectively. 

1161 

1162 Examples:: 

1163 

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

1165 >>> len(a) 

1166 1 

1167 >>> intersection = a[0] 

1168 >>> intersection.pt 

1169 (374.44882952482897, 313.73458370177315) 

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

1171 (0.45069111555824465, 0.5408153767394238) 

1172 """ 

1173 s1x, s1y = s1 

1174 e1x, e1y = e1 

1175 s2x, s2y = s2 

1176 e2x, e2y = e2 

1177 if ( 

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

1179 ): # Parallel vertical 

1180 return [] 

1181 if ( 

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

1183 ): # Parallel horizontal 

1184 return [] 

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

1186 return [] 

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

1188 return [] 

1189 if math.isclose(e1x, s1x): 

1190 x = s1x 

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

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

1193 pt = (x, y) 

1194 return [ 

1195 Intersection( 

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

1197 ) 

1198 ] 

1199 if math.isclose(s2x, e2x): 

1200 x = s2x 

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

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

1203 pt = (x, y) 

1204 return [ 

1205 Intersection( 

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

1207 ) 

1208 ] 

1209 

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

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

1212 if math.isclose(slope12, slope34): 

1213 return [] 

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

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

1216 pt = (x, y) 

1217 if _both_points_are_on_same_side_of_origin( 

1218 pt, e1, s1 

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

1220 return [ 

1221 Intersection( 

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

1223 ) 

1224 ] 

1225 return [] 

1226 

1227 

1228def _alignment_transformation(segment): 

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

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

1231 # intersections with the segment. 

1232 start = segment[0] 

1233 end = segment[-1] 

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

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

1236 

1237 

1238def _curve_line_intersections_t(curve, line): 

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

1240 if len(curve) == 3: 

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

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

1243 elif len(curve) == 4: 

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

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

1246 else: 

1247 raise ValueError("Unknown curve degree") 

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

1249 

1250 

1251def curveLineIntersections(curve, line): 

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

1253 

1254 Args: 

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

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

1257 

1258 Returns: 

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

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

1261 segment and time on second segment respectively. 

1262 

1263 Examples:: 

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

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

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

1267 >>> len(intersections) 

1268 3 

1269 >>> intersections[0].pt 

1270 (84.9000930760723, 189.87306176459828) 

1271 """ 

1272 if len(curve) == 3: 

1273 pointFinder = quadraticPointAtT 

1274 elif len(curve) == 4: 

1275 pointFinder = cubicPointAtT 

1276 else: 

1277 raise ValueError("Unknown curve degree") 

1278 intersections = [] 

1279 for t in _curve_line_intersections_t(curve, line): 

1280 pt = pointFinder(*curve, t) 

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

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

1283 line_t = _line_t_of_pt(*line, pt) 

1284 pt = linePointAtT(*line, line_t) 

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

1286 return intersections 

1287 

1288 

1289def _curve_bounds(c): 

1290 if len(c) == 3: 

1291 return calcQuadraticBounds(*c) 

1292 elif len(c) == 4: 

1293 return calcCubicBounds(*c) 

1294 raise ValueError("Unknown curve degree") 

1295 

1296 

1297def _split_segment_at_t(c, t): 

1298 if len(c) == 2: 

1299 s, e = c 

1300 midpoint = linePointAtT(s, e, t) 

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

1302 if len(c) == 3: 

1303 return splitQuadraticAtT(*c, t) 

1304 elif len(c) == 4: 

1305 return splitCubicAtT(*c, t) 

1306 raise ValueError("Unknown curve degree") 

1307 

1308 

1309def _curve_curve_intersections_t( 

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

1311): 

1312 bounds1 = _curve_bounds(curve1) 

1313 bounds2 = _curve_bounds(curve2) 

1314 

1315 if not range1: 

1316 range1 = (0.0, 1.0) 

1317 if not range2: 

1318 range2 = (0.0, 1.0) 

1319 

1320 # If bounds don't intersect, go home 

1321 intersects, _ = sectRect(bounds1, bounds2) 

1322 if not intersects: 

1323 return [] 

1324 

1325 def midpoint(r): 

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

1327 

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

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

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

1331 

1332 c11, c12 = _split_segment_at_t(curve1, 0.5) 

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

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

1335 

1336 c21, c22 = _split_segment_at_t(curve2, 0.5) 

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

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

1339 

1340 found = [] 

1341 found.extend( 

1342 _curve_curve_intersections_t( 

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

1344 ) 

1345 ) 

1346 found.extend( 

1347 _curve_curve_intersections_t( 

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

1349 ) 

1350 ) 

1351 found.extend( 

1352 _curve_curve_intersections_t( 

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

1354 ) 

1355 ) 

1356 found.extend( 

1357 _curve_curve_intersections_t( 

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

1359 ) 

1360 ) 

1361 

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

1363 seen = set() 

1364 unique_values = [] 

1365 

1366 for ts in found: 

1367 key = unique_key(ts) 

1368 if key in seen: 

1369 continue 

1370 seen.add(key) 

1371 unique_values.append(ts) 

1372 

1373 return unique_values 

1374 

1375 

1376def _is_linelike(segment): 

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

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

1379 

1380 

1381def curveCurveIntersections(curve1, curve2): 

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

1383 

1384 Args: 

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

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

1387 

1388 Returns: 

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

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

1391 segment and time on second segment respectively. 

1392 

1393 Examples:: 

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

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

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

1397 >>> len(intersections) 

1398 3 

1399 >>> intersections[0].pt 

1400 (81.7831487395506, 109.88904552375288) 

1401 """ 

1402 if _is_linelike(curve1): 

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

1404 if _is_linelike(curve2): 

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

1406 return lineLineIntersections(*line1, *line2) 

1407 else: 

1408 return curveLineIntersections(curve2, line1) 

1409 elif _is_linelike(curve2): 

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

1411 return curveLineIntersections(curve1, line2) 

1412 

1413 intersection_ts = _curve_curve_intersections_t(curve1, curve2) 

1414 return [ 

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

1416 for ts in intersection_ts 

1417 ] 

1418 

1419 

1420def segmentSegmentIntersections(seg1, seg2): 

1421 """Finds intersections between two segments. 

1422 

1423 Args: 

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

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

1426 

1427 Returns: 

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

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

1430 segment and time on second segment respectively. 

1431 

1432 Examples:: 

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

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

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

1436 >>> len(intersections) 

1437 3 

1438 >>> intersections[0].pt 

1439 (81.7831487395506, 109.88904552375288) 

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

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

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

1443 >>> len(intersections) 

1444 3 

1445 >>> intersections[0].pt 

1446 (84.9000930760723, 189.87306176459828) 

1447 

1448 """ 

1449 # Arrange by degree 

1450 swapped = False 

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

1452 seg2, seg1 = seg1, seg2 

1453 swapped = True 

1454 if len(seg1) > 2: 

1455 if len(seg2) > 2: 

1456 intersections = curveCurveIntersections(seg1, seg2) 

1457 else: 

1458 intersections = curveLineIntersections(seg1, seg2) 

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

1460 intersections = lineLineIntersections(*seg1, *seg2) 

1461 else: 

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

1463 if not swapped: 

1464 return intersections 

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

1466 

1467 

1468def _segmentrepr(obj): 

1469 """ 

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

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

1472 """ 

1473 try: 

1474 it = iter(obj) 

1475 except TypeError: 

1476 return "%g" % obj 

1477 else: 

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

1479 

1480 

1481def printSegments(segments): 

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

1483 segments on a single line as a tuple. 

1484 """ 

1485 for segment in segments: 

1486 print(_segmentrepr(segment)) 

1487 

1488 

1489if __name__ == "__main__": 

1490 import sys 

1491 import doctest 

1492 

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