bezierTools.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497
  1. # -*- coding: utf-8 -*-
  2. """fontTools.misc.bezierTools.py -- tools for working with Bezier path segments.
  3. """
  4. from fontTools.misc.arrayTools import calcBounds, sectRect, rectArea
  5. from fontTools.misc.transform import Identity
  6. import math
  7. from collections import namedtuple
  8. try:
  9. import cython
  10. except (AttributeError, ImportError):
  11. # if cython not installed, use mock module with no-op decorators and types
  12. from fontTools.misc import cython
  13. COMPILED = cython.compiled
  14. EPSILON = 1e-9
  15. Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
  16. __all__ = [
  17. "approximateCubicArcLength",
  18. "approximateCubicArcLengthC",
  19. "approximateQuadraticArcLength",
  20. "approximateQuadraticArcLengthC",
  21. "calcCubicArcLength",
  22. "calcCubicArcLengthC",
  23. "calcQuadraticArcLength",
  24. "calcQuadraticArcLengthC",
  25. "calcCubicBounds",
  26. "calcQuadraticBounds",
  27. "splitLine",
  28. "splitQuadratic",
  29. "splitCubic",
  30. "splitQuadraticAtT",
  31. "splitCubicAtT",
  32. "splitCubicAtTC",
  33. "splitCubicIntoTwoAtTC",
  34. "solveQuadratic",
  35. "solveCubic",
  36. "quadraticPointAtT",
  37. "cubicPointAtT",
  38. "cubicPointAtTC",
  39. "linePointAtT",
  40. "segmentPointAtT",
  41. "lineLineIntersections",
  42. "curveLineIntersections",
  43. "curveCurveIntersections",
  44. "segmentSegmentIntersections",
  45. ]
  46. def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005):
  47. """Calculates the arc length for a cubic Bezier segment.
  48. Whereas :func:`approximateCubicArcLength` approximates the length, this
  49. function calculates it by "measuring", recursively dividing the curve
  50. until the divided segments are shorter than ``tolerance``.
  51. Args:
  52. pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples.
  53. tolerance: Controls the precision of the calcuation.
  54. Returns:
  55. Arc length value.
  56. """
  57. return calcCubicArcLengthC(
  58. complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance
  59. )
  60. def _split_cubic_into_two(p0, p1, p2, p3):
  61. mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
  62. deriv3 = (p3 + p2 - p1 - p0) * 0.125
  63. return (
  64. (p0, (p0 + p1) * 0.5, mid - deriv3, mid),
  65. (mid, mid + deriv3, (p2 + p3) * 0.5, p3),
  66. )
  67. @cython.returns(cython.double)
  68. @cython.locals(
  69. p0=cython.complex,
  70. p1=cython.complex,
  71. p2=cython.complex,
  72. p3=cython.complex,
  73. )
  74. @cython.locals(mult=cython.double, arch=cython.double, box=cython.double)
  75. def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
  76. arch = abs(p0 - p3)
  77. box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3)
  78. if arch * mult + EPSILON >= box:
  79. return (arch + box) * 0.5
  80. else:
  81. one, two = _split_cubic_into_two(p0, p1, p2, p3)
  82. return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(
  83. mult, *two
  84. )
  85. @cython.returns(cython.double)
  86. @cython.locals(
  87. pt1=cython.complex,
  88. pt2=cython.complex,
  89. pt3=cython.complex,
  90. pt4=cython.complex,
  91. )
  92. @cython.locals(
  93. tolerance=cython.double,
  94. mult=cython.double,
  95. )
  96. def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005):
  97. """Calculates the arc length for a cubic Bezier segment.
  98. Args:
  99. pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.
  100. tolerance: Controls the precision of the calcuation.
  101. Returns:
  102. Arc length value.
  103. """
  104. mult = 1.0 + 1.5 * tolerance # The 1.5 is a empirical hack; no math
  105. return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4)
  106. epsilonDigits = 6
  107. epsilon = 1e-10
  108. @cython.cfunc
  109. @cython.inline
  110. @cython.returns(cython.double)
  111. @cython.locals(v1=cython.complex, v2=cython.complex)
  112. def _dot(v1, v2):
  113. return (v1 * v2.conjugate()).real
  114. @cython.cfunc
  115. @cython.inline
  116. @cython.returns(cython.double)
  117. @cython.locals(x=cython.double)
  118. def _intSecAtan(x):
  119. # In : sympy.integrate(sp.sec(sp.atan(x)))
  120. # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
  121. return x * math.sqrt(x**2 + 1) / 2 + math.asinh(x) / 2
  122. def calcQuadraticArcLength(pt1, pt2, pt3):
  123. """Calculates the arc length for a quadratic Bezier segment.
  124. Args:
  125. pt1: Start point of the Bezier as 2D tuple.
  126. pt2: Handle point of the Bezier as 2D tuple.
  127. pt3: End point of the Bezier as 2D tuple.
  128. Returns:
  129. Arc length value.
  130. Example::
  131. >>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment
  132. 0.0
  133. >>> calcQuadraticArcLength((0, 0), (50, 0), (80, 0)) # collinear points
  134. 80.0
  135. >>> calcQuadraticArcLength((0, 0), (0, 50), (0, 80)) # collinear points vertical
  136. 80.0
  137. >>> calcQuadraticArcLength((0, 0), (50, 20), (100, 40)) # collinear points
  138. 107.70329614269008
  139. >>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0))
  140. 154.02976155645263
  141. >>> calcQuadraticArcLength((0, 0), (0, 50), (100, 0))
  142. 120.21581243984076
  143. >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50))
  144. 102.53273816445825
  145. >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside
  146. 66.66666666666667
  147. >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back
  148. 40.0
  149. """
  150. return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
  151. @cython.returns(cython.double)
  152. @cython.locals(
  153. pt1=cython.complex,
  154. pt2=cython.complex,
  155. pt3=cython.complex,
  156. d0=cython.complex,
  157. d1=cython.complex,
  158. d=cython.complex,
  159. n=cython.complex,
  160. )
  161. @cython.locals(
  162. scale=cython.double,
  163. origDist=cython.double,
  164. a=cython.double,
  165. b=cython.double,
  166. x0=cython.double,
  167. x1=cython.double,
  168. Len=cython.double,
  169. )
  170. def calcQuadraticArcLengthC(pt1, pt2, pt3):
  171. """Calculates the arc length for a quadratic Bezier segment.
  172. Args:
  173. pt1: Start point of the Bezier as a complex number.
  174. pt2: Handle point of the Bezier as a complex number.
  175. pt3: End point of the Bezier as a complex number.
  176. Returns:
  177. Arc length value.
  178. """
  179. # Analytical solution to the length of a quadratic bezier.
  180. # Documentation: https://github.com/fonttools/fonttools/issues/3055
  181. d0 = pt2 - pt1
  182. d1 = pt3 - pt2
  183. d = d1 - d0
  184. n = d * 1j
  185. scale = abs(n)
  186. if scale == 0.0:
  187. return abs(pt3 - pt1)
  188. origDist = _dot(n, d0)
  189. if abs(origDist) < epsilon:
  190. if _dot(d0, d1) >= 0:
  191. return abs(pt3 - pt1)
  192. a, b = abs(d0), abs(d1)
  193. return (a * a + b * b) / (a + b)
  194. x0 = _dot(d, d0) / origDist
  195. x1 = _dot(d, d1) / origDist
  196. Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0)))
  197. return Len
  198. def approximateQuadraticArcLength(pt1, pt2, pt3):
  199. """Calculates the arc length for a quadratic Bezier segment.
  200. Uses Gauss-Legendre quadrature for a branch-free approximation.
  201. See :func:`calcQuadraticArcLength` for a slower but more accurate result.
  202. Args:
  203. pt1: Start point of the Bezier as 2D tuple.
  204. pt2: Handle point of the Bezier as 2D tuple.
  205. pt3: End point of the Bezier as 2D tuple.
  206. Returns:
  207. Approximate arc length value.
  208. """
  209. return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
  210. @cython.returns(cython.double)
  211. @cython.locals(
  212. pt1=cython.complex,
  213. pt2=cython.complex,
  214. pt3=cython.complex,
  215. )
  216. @cython.locals(
  217. v0=cython.double,
  218. v1=cython.double,
  219. v2=cython.double,
  220. )
  221. def approximateQuadraticArcLengthC(pt1, pt2, pt3):
  222. """Calculates the arc length for a quadratic Bezier segment.
  223. Uses Gauss-Legendre quadrature for a branch-free approximation.
  224. See :func:`calcQuadraticArcLength` for a slower but more accurate result.
  225. Args:
  226. pt1: Start point of the Bezier as a complex number.
  227. pt2: Handle point of the Bezier as a complex number.
  228. pt3: End point of the Bezier as a complex number.
  229. Returns:
  230. Approximate arc length value.
  231. """
  232. # This, essentially, approximates the length-of-derivative function
  233. # to be integrated with the best-matching fifth-degree polynomial
  234. # approximation of it.
  235. #
  236. # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature
  237. # abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2),
  238. # weighted 5/18, 8/18, 5/18 respectively.
  239. v0 = abs(
  240. -0.492943519233745 * pt1 + 0.430331482911935 * pt2 + 0.0626120363218102 * pt3
  241. )
  242. v1 = abs(pt3 - pt1) * 0.4444444444444444
  243. v2 = abs(
  244. -0.0626120363218102 * pt1 - 0.430331482911935 * pt2 + 0.492943519233745 * pt3
  245. )
  246. return v0 + v1 + v2
  247. def calcQuadraticBounds(pt1, pt2, pt3):
  248. """Calculates the bounding rectangle for a quadratic Bezier segment.
  249. Args:
  250. pt1: Start point of the Bezier as a 2D tuple.
  251. pt2: Handle point of the Bezier as a 2D tuple.
  252. pt3: End point of the Bezier as a 2D tuple.
  253. Returns:
  254. A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``.
  255. Example::
  256. >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0))
  257. (0, 0, 100, 50.0)
  258. >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100))
  259. (0.0, 0.0, 100, 100)
  260. """
  261. (ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
  262. ax2 = ax * 2.0
  263. ay2 = ay * 2.0
  264. roots = []
  265. if ax2 != 0:
  266. roots.append(-bx / ax2)
  267. if ay2 != 0:
  268. roots.append(-by / ay2)
  269. points = [
  270. (ax * t * t + bx * t + cx, ay * t * t + by * t + cy)
  271. for t in roots
  272. if 0 <= t < 1
  273. ] + [pt1, pt3]
  274. return calcBounds(points)
  275. def approximateCubicArcLength(pt1, pt2, pt3, pt4):
  276. """Approximates the arc length for a cubic Bezier segment.
  277. Uses Gauss-Lobatto quadrature with n=5 points to approximate arc length.
  278. See :func:`calcCubicArcLength` for a slower but more accurate result.
  279. Args:
  280. pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples.
  281. Returns:
  282. Arc length value.
  283. Example::
  284. >>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0))
  285. 190.04332968932817
  286. >>> approximateCubicArcLength((0, 0), (50, 0), (100, 50), (100, 100))
  287. 154.8852074945903
  288. >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (150, 0)) # line; exact result should be 150.
  289. 149.99999999999991
  290. >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (-50, 0)) # cusp; exact result should be 150.
  291. 136.9267662156362
  292. >>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp
  293. 154.80848416537057
  294. """
  295. return approximateCubicArcLengthC(
  296. complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4)
  297. )
  298. @cython.returns(cython.double)
  299. @cython.locals(
  300. pt1=cython.complex,
  301. pt2=cython.complex,
  302. pt3=cython.complex,
  303. pt4=cython.complex,
  304. )
  305. @cython.locals(
  306. v0=cython.double,
  307. v1=cython.double,
  308. v2=cython.double,
  309. v3=cython.double,
  310. v4=cython.double,
  311. )
  312. def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
  313. """Approximates the arc length for a cubic Bezier segment.
  314. Args:
  315. pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.
  316. Returns:
  317. Arc length value.
  318. """
  319. # This, essentially, approximates the length-of-derivative function
  320. # to be integrated with the best-matching seventh-degree polynomial
  321. # approximation of it.
  322. #
  323. # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules
  324. # abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1),
  325. # weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively.
  326. v0 = abs(pt2 - pt1) * 0.15
  327. v1 = abs(
  328. -0.558983582205757 * pt1
  329. + 0.325650248872424 * pt2
  330. + 0.208983582205757 * pt3
  331. + 0.024349751127576 * pt4
  332. )
  333. v2 = abs(pt4 - pt1 + pt3 - pt2) * 0.26666666666666666
  334. v3 = abs(
  335. -0.024349751127576 * pt1
  336. - 0.208983582205757 * pt2
  337. - 0.325650248872424 * pt3
  338. + 0.558983582205757 * pt4
  339. )
  340. v4 = abs(pt4 - pt3) * 0.15
  341. return v0 + v1 + v2 + v3 + v4
  342. def calcCubicBounds(pt1, pt2, pt3, pt4):
  343. """Calculates the bounding rectangle for a quadratic Bezier segment.
  344. Args:
  345. pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples.
  346. Returns:
  347. A four-item tuple representing the bounding rectangle ``(xMin, yMin, xMax, yMax)``.
  348. Example::
  349. >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0))
  350. (0, 0, 100, 75.0)
  351. >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100))
  352. (0.0, 0.0, 100, 100)
  353. >>> print("%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0)))
  354. 35.566243 0.000000 64.433757 75.000000
  355. """
  356. (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4)
  357. # calc first derivative
  358. ax3 = ax * 3.0
  359. ay3 = ay * 3.0
  360. bx2 = bx * 2.0
  361. by2 = by * 2.0
  362. xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1]
  363. yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
  364. roots = xRoots + yRoots
  365. points = [
  366. (
  367. ax * t * t * t + bx * t * t + cx * t + dx,
  368. ay * t * t * t + by * t * t + cy * t + dy,
  369. )
  370. for t in roots
  371. ] + [pt1, pt4]
  372. return calcBounds(points)
  373. def splitLine(pt1, pt2, where, isHorizontal):
  374. """Split a line at a given coordinate.
  375. Args:
  376. pt1: Start point of line as 2D tuple.
  377. pt2: End point of line as 2D tuple.
  378. where: Position at which to split the line.
  379. isHorizontal: Direction of the ray splitting the line. If true,
  380. ``where`` is interpreted as a Y coordinate; if false, then
  381. ``where`` is interpreted as an X coordinate.
  382. Returns:
  383. A list of two line segments (each line segment being two 2D tuples)
  384. if the line was successfully split, or a list containing the original
  385. line.
  386. Example::
  387. >>> printSegments(splitLine((0, 0), (100, 100), 50, True))
  388. ((0, 0), (50, 50))
  389. ((50, 50), (100, 100))
  390. >>> printSegments(splitLine((0, 0), (100, 100), 100, True))
  391. ((0, 0), (100, 100))
  392. >>> printSegments(splitLine((0, 0), (100, 100), 0, True))
  393. ((0, 0), (0, 0))
  394. ((0, 0), (100, 100))
  395. >>> printSegments(splitLine((0, 0), (100, 100), 0, False))
  396. ((0, 0), (0, 0))
  397. ((0, 0), (100, 100))
  398. >>> printSegments(splitLine((100, 0), (0, 0), 50, False))
  399. ((100, 0), (50, 0))
  400. ((50, 0), (0, 0))
  401. >>> printSegments(splitLine((0, 100), (0, 0), 50, True))
  402. ((0, 100), (0, 50))
  403. ((0, 50), (0, 0))
  404. """
  405. pt1x, pt1y = pt1
  406. pt2x, pt2y = pt2
  407. ax = pt2x - pt1x
  408. ay = pt2y - pt1y
  409. bx = pt1x
  410. by = pt1y
  411. a = (ax, ay)[isHorizontal]
  412. if a == 0:
  413. return [(pt1, pt2)]
  414. t = (where - (bx, by)[isHorizontal]) / a
  415. if 0 <= t < 1:
  416. midPt = ax * t + bx, ay * t + by
  417. return [(pt1, midPt), (midPt, pt2)]
  418. else:
  419. return [(pt1, pt2)]
  420. def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
  421. """Split a quadratic Bezier curve at a given coordinate.
  422. Args:
  423. pt1,pt2,pt3: Control points of the Bezier as 2D tuples.
  424. where: Position at which to split the curve.
  425. isHorizontal: Direction of the ray splitting the curve. If true,
  426. ``where`` is interpreted as a Y coordinate; if false, then
  427. ``where`` is interpreted as an X coordinate.
  428. Returns:
  429. A list of two curve segments (each curve segment being three 2D tuples)
  430. if the curve was successfully split, or a list containing the original
  431. curve.
  432. Example::
  433. >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False))
  434. ((0, 0), (50, 100), (100, 0))
  435. >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False))
  436. ((0, 0), (25, 50), (50, 50))
  437. ((50, 50), (75, 50), (100, 0))
  438. >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False))
  439. ((0, 0), (12.5, 25), (25, 37.5))
  440. ((25, 37.5), (62.5, 75), (100, 0))
  441. >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True))
  442. ((0, 0), (7.32233, 14.6447), (14.6447, 25))
  443. ((14.6447, 25), (50, 75), (85.3553, 25))
  444. ((85.3553, 25), (92.6777, 14.6447), (100, -7.10543e-15))
  445. >>> # XXX I'm not at all sure if the following behavior is desirable:
  446. >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True))
  447. ((0, 0), (25, 50), (50, 50))
  448. ((50, 50), (50, 50), (50, 50))
  449. ((50, 50), (75, 50), (100, 0))
  450. """
  451. a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
  452. solutions = solveQuadratic(
  453. a[isHorizontal], b[isHorizontal], c[isHorizontal] - where
  454. )
  455. solutions = sorted(t for t in solutions if 0 <= t < 1)
  456. if not solutions:
  457. return [(pt1, pt2, pt3)]
  458. return _splitQuadraticAtT(a, b, c, *solutions)
  459. def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
  460. """Split a cubic Bezier curve at a given coordinate.
  461. Args:
  462. pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples.
  463. where: Position at which to split the curve.
  464. isHorizontal: Direction of the ray splitting the curve. If true,
  465. ``where`` is interpreted as a Y coordinate; if false, then
  466. ``where`` is interpreted as an X coordinate.
  467. Returns:
  468. A list of two curve segments (each curve segment being four 2D tuples)
  469. if the curve was successfully split, or a list containing the original
  470. curve.
  471. Example::
  472. >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False))
  473. ((0, 0), (25, 100), (75, 100), (100, 0))
  474. >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False))
  475. ((0, 0), (12.5, 50), (31.25, 75), (50, 75))
  476. ((50, 75), (68.75, 75), (87.5, 50), (100, 0))
  477. >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True))
  478. ((0, 0), (2.29379, 9.17517), (4.79804, 17.5085), (7.47414, 25))
  479. ((7.47414, 25), (31.2886, 91.6667), (68.7114, 91.6667), (92.5259, 25))
  480. ((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), (100, 1.77636e-15))
  481. """
  482. a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
  483. solutions = solveCubic(
  484. a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - where
  485. )
  486. solutions = sorted(t for t in solutions if 0 <= t < 1)
  487. if not solutions:
  488. return [(pt1, pt2, pt3, pt4)]
  489. return _splitCubicAtT(a, b, c, d, *solutions)
  490. def splitQuadraticAtT(pt1, pt2, pt3, *ts):
  491. """Split a quadratic Bezier curve at one or more values of t.
  492. Args:
  493. pt1,pt2,pt3: Control points of the Bezier as 2D tuples.
  494. *ts: Positions at which to split the curve.
  495. Returns:
  496. A list of curve segments (each curve segment being three 2D tuples).
  497. Examples::
  498. >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5))
  499. ((0, 0), (25, 50), (50, 50))
  500. ((50, 50), (75, 50), (100, 0))
  501. >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75))
  502. ((0, 0), (25, 50), (50, 50))
  503. ((50, 50), (62.5, 50), (75, 37.5))
  504. ((75, 37.5), (87.5, 25), (100, 0))
  505. """
  506. a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
  507. return _splitQuadraticAtT(a, b, c, *ts)
  508. def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
  509. """Split a cubic Bezier curve at one or more values of t.
  510. Args:
  511. pt1,pt2,pt3,pt4: Control points of the Bezier as 2D tuples.
  512. *ts: Positions at which to split the curve.
  513. Returns:
  514. A list of curve segments (each curve segment being four 2D tuples).
  515. Examples::
  516. >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5))
  517. ((0, 0), (12.5, 50), (31.25, 75), (50, 75))
  518. ((50, 75), (68.75, 75), (87.5, 50), (100, 0))
  519. >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75))
  520. ((0, 0), (12.5, 50), (31.25, 75), (50, 75))
  521. ((50, 75), (59.375, 75), (68.75, 68.75), (77.3438, 56.25))
  522. ((77.3438, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0))
  523. """
  524. a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
  525. split = _splitCubicAtT(a, b, c, d, *ts)
  526. # the split impl can introduce floating point errors; we know the first
  527. # segment should always start at pt1 and the last segment should end at pt4,
  528. # so we set those values directly before returning.
  529. split[0] = (pt1, *split[0][1:])
  530. split[-1] = (*split[-1][:-1], pt4)
  531. return split
  532. @cython.locals(
  533. pt1=cython.complex,
  534. pt2=cython.complex,
  535. pt3=cython.complex,
  536. pt4=cython.complex,
  537. a=cython.complex,
  538. b=cython.complex,
  539. c=cython.complex,
  540. d=cython.complex,
  541. )
  542. def splitCubicAtTC(pt1, pt2, pt3, pt4, *ts):
  543. """Split a cubic Bezier curve at one or more values of t.
  544. Args:
  545. pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers..
  546. *ts: Positions at which to split the curve.
  547. Yields:
  548. Curve segments (each curve segment being four complex numbers).
  549. """
  550. a, b, c, d = calcCubicParametersC(pt1, pt2, pt3, pt4)
  551. yield from _splitCubicAtTC(a, b, c, d, *ts)
  552. @cython.returns(cython.complex)
  553. @cython.locals(
  554. t=cython.double,
  555. pt1=cython.complex,
  556. pt2=cython.complex,
  557. pt3=cython.complex,
  558. pt4=cython.complex,
  559. pointAtT=cython.complex,
  560. off1=cython.complex,
  561. off2=cython.complex,
  562. )
  563. @cython.locals(
  564. t2=cython.double, _1_t=cython.double, _1_t_2=cython.double, _2_t_1_t=cython.double
  565. )
  566. def splitCubicIntoTwoAtTC(pt1, pt2, pt3, pt4, t):
  567. """Split a cubic Bezier curve at t.
  568. Args:
  569. pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.
  570. t: Position at which to split the curve.
  571. Returns:
  572. A tuple of two curve segments (each curve segment being four complex numbers).
  573. """
  574. t2 = t * t
  575. _1_t = 1 - t
  576. _1_t_2 = _1_t * _1_t
  577. _2_t_1_t = 2 * t * _1_t
  578. pointAtT = (
  579. _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
  580. )
  581. off1 = _1_t_2 * pt1 + _2_t_1_t * pt2 + t2 * pt3
  582. off2 = _1_t_2 * pt2 + _2_t_1_t * pt3 + t2 * pt4
  583. pt2 = pt1 + (pt2 - pt1) * t
  584. pt3 = pt4 + (pt3 - pt4) * _1_t
  585. return ((pt1, pt2, off1, pointAtT), (pointAtT, off2, pt3, pt4))
  586. def _splitQuadraticAtT(a, b, c, *ts):
  587. ts = list(ts)
  588. segments = []
  589. ts.insert(0, 0.0)
  590. ts.append(1.0)
  591. ax, ay = a
  592. bx, by = b
  593. cx, cy = c
  594. for i in range(len(ts) - 1):
  595. t1 = ts[i]
  596. t2 = ts[i + 1]
  597. delta = t2 - t1
  598. # calc new a, b and c
  599. delta_2 = delta * delta
  600. a1x = ax * delta_2
  601. a1y = ay * delta_2
  602. b1x = (2 * ax * t1 + bx) * delta
  603. b1y = (2 * ay * t1 + by) * delta
  604. t1_2 = t1 * t1
  605. c1x = ax * t1_2 + bx * t1 + cx
  606. c1y = ay * t1_2 + by * t1 + cy
  607. pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
  608. segments.append((pt1, pt2, pt3))
  609. return segments
  610. def _splitCubicAtT(a, b, c, d, *ts):
  611. ts = list(ts)
  612. ts.insert(0, 0.0)
  613. ts.append(1.0)
  614. segments = []
  615. ax, ay = a
  616. bx, by = b
  617. cx, cy = c
  618. dx, dy = d
  619. for i in range(len(ts) - 1):
  620. t1 = ts[i]
  621. t2 = ts[i + 1]
  622. delta = t2 - t1
  623. delta_2 = delta * delta
  624. delta_3 = delta * delta_2
  625. t1_2 = t1 * t1
  626. t1_3 = t1 * t1_2
  627. # calc new a, b, c and d
  628. a1x = ax * delta_3
  629. a1y = ay * delta_3
  630. b1x = (3 * ax * t1 + bx) * delta_2
  631. b1y = (3 * ay * t1 + by) * delta_2
  632. c1x = (2 * bx * t1 + cx + 3 * ax * t1_2) * delta
  633. c1y = (2 * by * t1 + cy + 3 * ay * t1_2) * delta
  634. d1x = ax * t1_3 + bx * t1_2 + cx * t1 + dx
  635. d1y = ay * t1_3 + by * t1_2 + cy * t1 + dy
  636. pt1, pt2, pt3, pt4 = calcCubicPoints(
  637. (a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y)
  638. )
  639. segments.append((pt1, pt2, pt3, pt4))
  640. return segments
  641. @cython.locals(
  642. a=cython.complex,
  643. b=cython.complex,
  644. c=cython.complex,
  645. d=cython.complex,
  646. t1=cython.double,
  647. t2=cython.double,
  648. delta=cython.double,
  649. delta_2=cython.double,
  650. delta_3=cython.double,
  651. a1=cython.complex,
  652. b1=cython.complex,
  653. c1=cython.complex,
  654. d1=cython.complex,
  655. )
  656. def _splitCubicAtTC(a, b, c, d, *ts):
  657. ts = list(ts)
  658. ts.insert(0, 0.0)
  659. ts.append(1.0)
  660. for i in range(len(ts) - 1):
  661. t1 = ts[i]
  662. t2 = ts[i + 1]
  663. delta = t2 - t1
  664. delta_2 = delta * delta
  665. delta_3 = delta * delta_2
  666. t1_2 = t1 * t1
  667. t1_3 = t1 * t1_2
  668. # calc new a, b, c and d
  669. a1 = a * delta_3
  670. b1 = (3 * a * t1 + b) * delta_2
  671. c1 = (2 * b * t1 + c + 3 * a * t1_2) * delta
  672. d1 = a * t1_3 + b * t1_2 + c * t1 + d
  673. pt1, pt2, pt3, pt4 = calcCubicPointsC(a1, b1, c1, d1)
  674. yield (pt1, pt2, pt3, pt4)
  675. #
  676. # Equation solvers.
  677. #
  678. from math import sqrt, acos, cos, pi
  679. def solveQuadratic(a, b, c, sqrt=sqrt):
  680. """Solve a quadratic equation.
  681. Solves *a*x*x + b*x + c = 0* where a, b and c are real.
  682. Args:
  683. a: coefficient of *x²*
  684. b: coefficient of *x*
  685. c: constant term
  686. Returns:
  687. A list of roots. Note that the returned list is neither guaranteed to
  688. be sorted nor to contain unique values!
  689. """
  690. if abs(a) < epsilon:
  691. if abs(b) < epsilon:
  692. # We have a non-equation; therefore, we have no valid solution
  693. roots = []
  694. else:
  695. # We have a linear equation with 1 root.
  696. roots = [-c / b]
  697. else:
  698. # We have a true quadratic equation. Apply the quadratic formula to find two roots.
  699. DD = b * b - 4.0 * a * c
  700. if DD >= 0.0:
  701. rDD = sqrt(DD)
  702. roots = [(-b + rDD) / 2.0 / a, (-b - rDD) / 2.0 / a]
  703. else:
  704. # complex roots, ignore
  705. roots = []
  706. return roots
  707. def solveCubic(a, b, c, d):
  708. """Solve a cubic equation.
  709. Solves *a*x*x*x + b*x*x + c*x + d = 0* where a, b, c and d are real.
  710. Args:
  711. a: coefficient of *x³*
  712. b: coefficient of *x²*
  713. c: coefficient of *x*
  714. d: constant term
  715. Returns:
  716. A list of roots. Note that the returned list is neither guaranteed to
  717. be sorted nor to contain unique values!
  718. Examples::
  719. >>> solveCubic(1, 1, -6, 0)
  720. [-3.0, -0.0, 2.0]
  721. >>> solveCubic(-10.0, -9.0, 48.0, -29.0)
  722. [-2.9, 1.0, 1.0]
  723. >>> solveCubic(-9.875, -9.0, 47.625, -28.75)
  724. [-2.911392, 1.0, 1.0]
  725. >>> solveCubic(1.0, -4.5, 6.75, -3.375)
  726. [1.5, 1.5, 1.5]
  727. >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123)
  728. [0.5, 0.5, 0.5]
  729. >>> solveCubic(
  730. ... 9.0, 0.0, 0.0, -7.62939453125e-05
  731. ... ) == [-0.0, -0.0, -0.0]
  732. True
  733. """
  734. #
  735. # adapted from:
  736. # CUBIC.C - Solve a cubic polynomial
  737. # public domain by Ross Cottrell
  738. # found at: http://www.strangecreations.com/library/snippets/Cubic.C
  739. #
  740. if abs(a) < epsilon:
  741. # don't just test for zero; for very small values of 'a' solveCubic()
  742. # returns unreliable results, so we fall back to quad.
  743. return solveQuadratic(b, c, d)
  744. a = float(a)
  745. a1 = b / a
  746. a2 = c / a
  747. a3 = d / a
  748. Q = (a1 * a1 - 3.0 * a2) / 9.0
  749. R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0
  750. R2 = R * R
  751. Q3 = Q * Q * Q
  752. R2 = 0 if R2 < epsilon else R2
  753. Q3 = 0 if abs(Q3) < epsilon else Q3
  754. R2_Q3 = R2 - Q3
  755. if R2 == 0.0 and Q3 == 0.0:
  756. x = round(-a1 / 3.0, epsilonDigits)
  757. return [x, x, x]
  758. elif R2_Q3 <= epsilon * 0.5:
  759. # The epsilon * .5 above ensures that Q3 is not zero.
  760. theta = acos(max(min(R / sqrt(Q3), 1.0), -1.0))
  761. rQ2 = -2.0 * sqrt(Q)
  762. a1_3 = a1 / 3.0
  763. x0 = rQ2 * cos(theta / 3.0) - a1_3
  764. x1 = rQ2 * cos((theta + 2.0 * pi) / 3.0) - a1_3
  765. x2 = rQ2 * cos((theta + 4.0 * pi) / 3.0) - a1_3
  766. x0, x1, x2 = sorted([x0, x1, x2])
  767. # Merge roots that are close-enough
  768. if x1 - x0 < epsilon and x2 - x1 < epsilon:
  769. x0 = x1 = x2 = round((x0 + x1 + x2) / 3.0, epsilonDigits)
  770. elif x1 - x0 < epsilon:
  771. x0 = x1 = round((x0 + x1) / 2.0, epsilonDigits)
  772. x2 = round(x2, epsilonDigits)
  773. elif x2 - x1 < epsilon:
  774. x0 = round(x0, epsilonDigits)
  775. x1 = x2 = round((x1 + x2) / 2.0, epsilonDigits)
  776. else:
  777. x0 = round(x0, epsilonDigits)
  778. x1 = round(x1, epsilonDigits)
  779. x2 = round(x2, epsilonDigits)
  780. return [x0, x1, x2]
  781. else:
  782. x = pow(sqrt(R2_Q3) + abs(R), 1 / 3.0)
  783. x = x + Q / x
  784. if R >= 0.0:
  785. x = -x
  786. x = round(x - a1 / 3.0, epsilonDigits)
  787. return [x]
  788. #
  789. # Conversion routines for points to parameters and vice versa
  790. #
  791. def calcQuadraticParameters(pt1, pt2, pt3):
  792. x2, y2 = pt2
  793. x3, y3 = pt3
  794. cx, cy = pt1
  795. bx = (x2 - cx) * 2.0
  796. by = (y2 - cy) * 2.0
  797. ax = x3 - cx - bx
  798. ay = y3 - cy - by
  799. return (ax, ay), (bx, by), (cx, cy)
  800. def calcCubicParameters(pt1, pt2, pt3, pt4):
  801. x2, y2 = pt2
  802. x3, y3 = pt3
  803. x4, y4 = pt4
  804. dx, dy = pt1
  805. cx = (x2 - dx) * 3.0
  806. cy = (y2 - dy) * 3.0
  807. bx = (x3 - x2) * 3.0 - cx
  808. by = (y3 - y2) * 3.0 - cy
  809. ax = x4 - dx - cx - bx
  810. ay = y4 - dy - cy - by
  811. return (ax, ay), (bx, by), (cx, cy), (dx, dy)
  812. @cython.cfunc
  813. @cython.inline
  814. @cython.locals(
  815. pt1=cython.complex,
  816. pt2=cython.complex,
  817. pt3=cython.complex,
  818. pt4=cython.complex,
  819. a=cython.complex,
  820. b=cython.complex,
  821. c=cython.complex,
  822. )
  823. def calcCubicParametersC(pt1, pt2, pt3, pt4):
  824. c = (pt2 - pt1) * 3.0
  825. b = (pt3 - pt2) * 3.0 - c
  826. a = pt4 - pt1 - c - b
  827. return (a, b, c, pt1)
  828. def calcQuadraticPoints(a, b, c):
  829. ax, ay = a
  830. bx, by = b
  831. cx, cy = c
  832. x1 = cx
  833. y1 = cy
  834. x2 = (bx * 0.5) + cx
  835. y2 = (by * 0.5) + cy
  836. x3 = ax + bx + cx
  837. y3 = ay + by + cy
  838. return (x1, y1), (x2, y2), (x3, y3)
  839. def calcCubicPoints(a, b, c, d):
  840. ax, ay = a
  841. bx, by = b
  842. cx, cy = c
  843. dx, dy = d
  844. x1 = dx
  845. y1 = dy
  846. x2 = (cx / 3.0) + dx
  847. y2 = (cy / 3.0) + dy
  848. x3 = (bx + cx) / 3.0 + x2
  849. y3 = (by + cy) / 3.0 + y2
  850. x4 = ax + dx + cx + bx
  851. y4 = ay + dy + cy + by
  852. return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
  853. @cython.cfunc
  854. @cython.inline
  855. @cython.locals(
  856. a=cython.complex,
  857. b=cython.complex,
  858. c=cython.complex,
  859. d=cython.complex,
  860. p2=cython.complex,
  861. p3=cython.complex,
  862. p4=cython.complex,
  863. )
  864. def calcCubicPointsC(a, b, c, d):
  865. p2 = c * (1 / 3) + d
  866. p3 = (b + c) * (1 / 3) + p2
  867. p4 = a + b + c + d
  868. return (d, p2, p3, p4)
  869. #
  870. # Point at time
  871. #
  872. def linePointAtT(pt1, pt2, t):
  873. """Finds the point at time `t` on a line.
  874. Args:
  875. pt1, pt2: Coordinates of the line as 2D tuples.
  876. t: The time along the line.
  877. Returns:
  878. A 2D tuple with the coordinates of the point.
  879. """
  880. return ((pt1[0] * (1 - t) + pt2[0] * t), (pt1[1] * (1 - t) + pt2[1] * t))
  881. def quadraticPointAtT(pt1, pt2, pt3, t):
  882. """Finds the point at time `t` on a quadratic curve.
  883. Args:
  884. pt1, pt2, pt3: Coordinates of the curve as 2D tuples.
  885. t: The time along the curve.
  886. Returns:
  887. A 2D tuple with the coordinates of the point.
  888. """
  889. x = (1 - t) * (1 - t) * pt1[0] + 2 * (1 - t) * t * pt2[0] + t * t * pt3[0]
  890. y = (1 - t) * (1 - t) * pt1[1] + 2 * (1 - t) * t * pt2[1] + t * t * pt3[1]
  891. return (x, y)
  892. def cubicPointAtT(pt1, pt2, pt3, pt4, t):
  893. """Finds the point at time `t` on a cubic curve.
  894. Args:
  895. pt1, pt2, pt3, pt4: Coordinates of the curve as 2D tuples.
  896. t: The time along the curve.
  897. Returns:
  898. A 2D tuple with the coordinates of the point.
  899. """
  900. t2 = t * t
  901. _1_t = 1 - t
  902. _1_t_2 = _1_t * _1_t
  903. x = (
  904. _1_t_2 * _1_t * pt1[0]
  905. + 3 * (_1_t_2 * t * pt2[0] + _1_t * t2 * pt3[0])
  906. + t2 * t * pt4[0]
  907. )
  908. y = (
  909. _1_t_2 * _1_t * pt1[1]
  910. + 3 * (_1_t_2 * t * pt2[1] + _1_t * t2 * pt3[1])
  911. + t2 * t * pt4[1]
  912. )
  913. return (x, y)
  914. @cython.returns(cython.complex)
  915. @cython.locals(
  916. t=cython.double,
  917. pt1=cython.complex,
  918. pt2=cython.complex,
  919. pt3=cython.complex,
  920. pt4=cython.complex,
  921. )
  922. @cython.locals(t2=cython.double, _1_t=cython.double, _1_t_2=cython.double)
  923. def cubicPointAtTC(pt1, pt2, pt3, pt4, t):
  924. """Finds the point at time `t` on a cubic curve.
  925. Args:
  926. pt1, pt2, pt3, pt4: Coordinates of the curve as complex numbers.
  927. t: The time along the curve.
  928. Returns:
  929. A complex number with the coordinates of the point.
  930. """
  931. t2 = t * t
  932. _1_t = 1 - t
  933. _1_t_2 = _1_t * _1_t
  934. return _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
  935. def segmentPointAtT(seg, t):
  936. if len(seg) == 2:
  937. return linePointAtT(*seg, t)
  938. elif len(seg) == 3:
  939. return quadraticPointAtT(*seg, t)
  940. elif len(seg) == 4:
  941. return cubicPointAtT(*seg, t)
  942. raise ValueError("Unknown curve degree")
  943. #
  944. # Intersection finders
  945. #
  946. def _line_t_of_pt(s, e, pt):
  947. sx, sy = s
  948. ex, ey = e
  949. px, py = pt
  950. if abs(sx - ex) < epsilon and abs(sy - ey) < epsilon:
  951. # Line is a point!
  952. return -1
  953. # Use the largest
  954. if abs(sx - ex) > abs(sy - ey):
  955. return (px - sx) / (ex - sx)
  956. else:
  957. return (py - sy) / (ey - sy)
  958. def _both_points_are_on_same_side_of_origin(a, b, origin):
  959. xDiff = (a[0] - origin[0]) * (b[0] - origin[0])
  960. yDiff = (a[1] - origin[1]) * (b[1] - origin[1])
  961. return not (xDiff <= 0.0 and yDiff <= 0.0)
  962. def lineLineIntersections(s1, e1, s2, e2):
  963. """Finds intersections between two line segments.
  964. Args:
  965. s1, e1: Coordinates of the first line as 2D tuples.
  966. s2, e2: Coordinates of the second line as 2D tuples.
  967. Returns:
  968. A list of ``Intersection`` objects, each object having ``pt``, ``t1``
  969. and ``t2`` attributes containing the intersection point, time on first
  970. segment and time on second segment respectively.
  971. Examples::
  972. >>> a = lineLineIntersections( (310,389), (453, 222), (289, 251), (447, 367))
  973. >>> len(a)
  974. 1
  975. >>> intersection = a[0]
  976. >>> intersection.pt
  977. (374.44882952482897, 313.73458370177315)
  978. >>> (intersection.t1, intersection.t2)
  979. (0.45069111555824465, 0.5408153767394238)
  980. """
  981. s1x, s1y = s1
  982. e1x, e1y = e1
  983. s2x, s2y = s2
  984. e2x, e2y = e2
  985. if (
  986. math.isclose(s2x, e2x) and math.isclose(s1x, e1x) and not math.isclose(s1x, s2x)
  987. ): # Parallel vertical
  988. return []
  989. if (
  990. math.isclose(s2y, e2y) and math.isclose(s1y, e1y) and not math.isclose(s1y, s2y)
  991. ): # Parallel horizontal
  992. return []
  993. if math.isclose(s2x, e2x) and math.isclose(s2y, e2y): # Line segment is tiny
  994. return []
  995. if math.isclose(s1x, e1x) and math.isclose(s1y, e1y): # Line segment is tiny
  996. return []
  997. if math.isclose(e1x, s1x):
  998. x = s1x
  999. slope34 = (e2y - s2y) / (e2x - s2x)
  1000. y = slope34 * (x - s2x) + s2y
  1001. pt = (x, y)
  1002. return [
  1003. Intersection(
  1004. pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
  1005. )
  1006. ]
  1007. if math.isclose(s2x, e2x):
  1008. x = s2x
  1009. slope12 = (e1y - s1y) / (e1x - s1x)
  1010. y = slope12 * (x - s1x) + s1y
  1011. pt = (x, y)
  1012. return [
  1013. Intersection(
  1014. pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
  1015. )
  1016. ]
  1017. slope12 = (e1y - s1y) / (e1x - s1x)
  1018. slope34 = (e2y - s2y) / (e2x - s2x)
  1019. if math.isclose(slope12, slope34):
  1020. return []
  1021. x = (slope12 * s1x - s1y - slope34 * s2x + s2y) / (slope12 - slope34)
  1022. y = slope12 * (x - s1x) + s1y
  1023. pt = (x, y)
  1024. if _both_points_are_on_same_side_of_origin(
  1025. pt, e1, s1
  1026. ) and _both_points_are_on_same_side_of_origin(pt, s2, e2):
  1027. return [
  1028. Intersection(
  1029. pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
  1030. )
  1031. ]
  1032. return []
  1033. def _alignment_transformation(segment):
  1034. # Returns a transformation which aligns a segment horizontally at the
  1035. # origin. Apply this transformation to curves and root-find to find
  1036. # intersections with the segment.
  1037. start = segment[0]
  1038. end = segment[-1]
  1039. angle = math.atan2(end[1] - start[1], end[0] - start[0])
  1040. return Identity.rotate(-angle).translate(-start[0], -start[1])
  1041. def _curve_line_intersections_t(curve, line):
  1042. aligned_curve = _alignment_transformation(line).transformPoints(curve)
  1043. if len(curve) == 3:
  1044. a, b, c = calcQuadraticParameters(*aligned_curve)
  1045. intersections = solveQuadratic(a[1], b[1], c[1])
  1046. elif len(curve) == 4:
  1047. a, b, c, d = calcCubicParameters(*aligned_curve)
  1048. intersections = solveCubic(a[1], b[1], c[1], d[1])
  1049. else:
  1050. raise ValueError("Unknown curve degree")
  1051. return sorted(i for i in intersections if 0.0 <= i <= 1)
  1052. def curveLineIntersections(curve, line):
  1053. """Finds intersections between a curve and a line.
  1054. Args:
  1055. curve: List of coordinates of the curve segment as 2D tuples.
  1056. line: List of coordinates of the line segment as 2D tuples.
  1057. Returns:
  1058. A list of ``Intersection`` objects, each object having ``pt``, ``t1``
  1059. and ``t2`` attributes containing the intersection point, time on first
  1060. segment and time on second segment respectively.
  1061. Examples::
  1062. >>> curve = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
  1063. >>> line = [ (25, 260), (230, 20) ]
  1064. >>> intersections = curveLineIntersections(curve, line)
  1065. >>> len(intersections)
  1066. 3
  1067. >>> intersections[0].pt
  1068. (84.9000930760723, 189.87306176459828)
  1069. """
  1070. if len(curve) == 3:
  1071. pointFinder = quadraticPointAtT
  1072. elif len(curve) == 4:
  1073. pointFinder = cubicPointAtT
  1074. else:
  1075. raise ValueError("Unknown curve degree")
  1076. intersections = []
  1077. for t in _curve_line_intersections_t(curve, line):
  1078. pt = pointFinder(*curve, t)
  1079. # Back-project the point onto the line, to avoid problems with
  1080. # numerical accuracy in the case of vertical and horizontal lines
  1081. line_t = _line_t_of_pt(*line, pt)
  1082. pt = linePointAtT(*line, line_t)
  1083. intersections.append(Intersection(pt=pt, t1=t, t2=line_t))
  1084. return intersections
  1085. def _curve_bounds(c):
  1086. if len(c) == 3:
  1087. return calcQuadraticBounds(*c)
  1088. elif len(c) == 4:
  1089. return calcCubicBounds(*c)
  1090. raise ValueError("Unknown curve degree")
  1091. def _split_segment_at_t(c, t):
  1092. if len(c) == 2:
  1093. s, e = c
  1094. midpoint = linePointAtT(s, e, t)
  1095. return [(s, midpoint), (midpoint, e)]
  1096. if len(c) == 3:
  1097. return splitQuadraticAtT(*c, t)
  1098. elif len(c) == 4:
  1099. return splitCubicAtT(*c, t)
  1100. raise ValueError("Unknown curve degree")
  1101. def _curve_curve_intersections_t(
  1102. curve1, curve2, precision=1e-3, range1=None, range2=None
  1103. ):
  1104. bounds1 = _curve_bounds(curve1)
  1105. bounds2 = _curve_bounds(curve2)
  1106. if not range1:
  1107. range1 = (0.0, 1.0)
  1108. if not range2:
  1109. range2 = (0.0, 1.0)
  1110. # If bounds don't intersect, go home
  1111. intersects, _ = sectRect(bounds1, bounds2)
  1112. if not intersects:
  1113. return []
  1114. def midpoint(r):
  1115. return 0.5 * (r[0] + r[1])
  1116. # If they do overlap but they're tiny, approximate
  1117. if rectArea(bounds1) < precision and rectArea(bounds2) < precision:
  1118. return [(midpoint(range1), midpoint(range2))]
  1119. c11, c12 = _split_segment_at_t(curve1, 0.5)
  1120. c11_range = (range1[0], midpoint(range1))
  1121. c12_range = (midpoint(range1), range1[1])
  1122. c21, c22 = _split_segment_at_t(curve2, 0.5)
  1123. c21_range = (range2[0], midpoint(range2))
  1124. c22_range = (midpoint(range2), range2[1])
  1125. found = []
  1126. found.extend(
  1127. _curve_curve_intersections_t(
  1128. c11, c21, precision, range1=c11_range, range2=c21_range
  1129. )
  1130. )
  1131. found.extend(
  1132. _curve_curve_intersections_t(
  1133. c12, c21, precision, range1=c12_range, range2=c21_range
  1134. )
  1135. )
  1136. found.extend(
  1137. _curve_curve_intersections_t(
  1138. c11, c22, precision, range1=c11_range, range2=c22_range
  1139. )
  1140. )
  1141. found.extend(
  1142. _curve_curve_intersections_t(
  1143. c12, c22, precision, range1=c12_range, range2=c22_range
  1144. )
  1145. )
  1146. unique_key = lambda ts: (int(ts[0] / precision), int(ts[1] / precision))
  1147. seen = set()
  1148. unique_values = []
  1149. for ts in found:
  1150. key = unique_key(ts)
  1151. if key in seen:
  1152. continue
  1153. seen.add(key)
  1154. unique_values.append(ts)
  1155. return unique_values
  1156. def _is_linelike(segment):
  1157. maybeline = _alignment_transformation(segment).transformPoints(segment)
  1158. return all(math.isclose(p[1], 0.0) for p in maybeline)
  1159. def curveCurveIntersections(curve1, curve2):
  1160. """Finds intersections between a curve and a curve.
  1161. Args:
  1162. curve1: List of coordinates of the first curve segment as 2D tuples.
  1163. curve2: List of coordinates of the second curve segment as 2D tuples.
  1164. Returns:
  1165. A list of ``Intersection`` objects, each object having ``pt``, ``t1``
  1166. and ``t2`` attributes containing the intersection point, time on first
  1167. segment and time on second segment respectively.
  1168. Examples::
  1169. >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
  1170. >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
  1171. >>> intersections = curveCurveIntersections(curve1, curve2)
  1172. >>> len(intersections)
  1173. 3
  1174. >>> intersections[0].pt
  1175. (81.7831487395506, 109.88904552375288)
  1176. """
  1177. if _is_linelike(curve1):
  1178. line1 = curve1[0], curve1[-1]
  1179. if _is_linelike(curve2):
  1180. line2 = curve2[0], curve2[-1]
  1181. return lineLineIntersections(*line1, *line2)
  1182. else:
  1183. return curveLineIntersections(curve2, line1)
  1184. elif _is_linelike(curve2):
  1185. line2 = curve2[0], curve2[-1]
  1186. return curveLineIntersections(curve1, line2)
  1187. intersection_ts = _curve_curve_intersections_t(curve1, curve2)
  1188. return [
  1189. Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1])
  1190. for ts in intersection_ts
  1191. ]
  1192. def segmentSegmentIntersections(seg1, seg2):
  1193. """Finds intersections between two segments.
  1194. Args:
  1195. seg1: List of coordinates of the first segment as 2D tuples.
  1196. seg2: List of coordinates of the second segment as 2D tuples.
  1197. Returns:
  1198. A list of ``Intersection`` objects, each object having ``pt``, ``t1``
  1199. and ``t2`` attributes containing the intersection point, time on first
  1200. segment and time on second segment respectively.
  1201. Examples::
  1202. >>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
  1203. >>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
  1204. >>> intersections = segmentSegmentIntersections(curve1, curve2)
  1205. >>> len(intersections)
  1206. 3
  1207. >>> intersections[0].pt
  1208. (81.7831487395506, 109.88904552375288)
  1209. >>> curve3 = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
  1210. >>> line = [ (25, 260), (230, 20) ]
  1211. >>> intersections = segmentSegmentIntersections(curve3, line)
  1212. >>> len(intersections)
  1213. 3
  1214. >>> intersections[0].pt
  1215. (84.9000930760723, 189.87306176459828)
  1216. """
  1217. # Arrange by degree
  1218. swapped = False
  1219. if len(seg2) > len(seg1):
  1220. seg2, seg1 = seg1, seg2
  1221. swapped = True
  1222. if len(seg1) > 2:
  1223. if len(seg2) > 2:
  1224. intersections = curveCurveIntersections(seg1, seg2)
  1225. else:
  1226. intersections = curveLineIntersections(seg1, seg2)
  1227. elif len(seg1) == 2 and len(seg2) == 2:
  1228. intersections = lineLineIntersections(*seg1, *seg2)
  1229. else:
  1230. raise ValueError("Couldn't work out which intersection function to use")
  1231. if not swapped:
  1232. return intersections
  1233. return [Intersection(pt=i.pt, t1=i.t2, t2=i.t1) for i in intersections]
  1234. def _segmentrepr(obj):
  1235. """
  1236. >>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
  1237. '(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
  1238. """
  1239. try:
  1240. it = iter(obj)
  1241. except TypeError:
  1242. return "%g" % obj
  1243. else:
  1244. return "(%s)" % ", ".join(_segmentrepr(x) for x in it)
  1245. def printSegments(segments):
  1246. """Helper for the doctests, displaying each segment in a list of
  1247. segments on a single line as a tuple.
  1248. """
  1249. for segment in segments:
  1250. print(_segmentrepr(segment))
  1251. if __name__ == "__main__":
  1252. import sys
  1253. import doctest
  1254. sys.exit(doctest.testmod().failed)