bezier4p.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. # Purpose: Bezier Curve optimized for 4 control points
  2. # Created: 26.03.2010
  3. # Copyright (c) 2010-2018 Manfred Moitzi
  4. # License: MIT License
  5. from typing import List, TYPE_CHECKING, Iterable
  6. if TYPE_CHECKING:
  7. from ezdxf.eztypes import Vertex
  8. def check_if_in_valid_range(t: float):
  9. if not (0 <= t <= 1.):
  10. raise ValueError("t not in range [0 to 1]")
  11. class Bezier4P:
  12. """
  13. Implements an optimized Cubic Bezier Curve with 4 control points.
  14. Special behavior: 2d in -> 2d out and 3d in -> 3d out!
  15. """
  16. def __init__(self, control_points: List['Vertex']):
  17. if len(control_points) == 4:
  18. is3d = any(len(p) > 2 for p in control_points)
  19. self.math = D3D if is3d else D2D
  20. self._cpoints = [self.math.tovector(vector) for vector in control_points]
  21. else:
  22. raise ValueError("Four control points required.")
  23. @property
  24. def control_points(self) -> List['Vertex']:
  25. return self._cpoints
  26. def tangent(self, t: float) -> 'Vertex':
  27. """
  28. Calculate tangent at parameter t [0, 1].
  29. Args:
  30. t: curve position in the range [0, 1]
  31. Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.
  32. """
  33. check_if_in_valid_range(t)
  34. return self._get_curve_tangent(t)
  35. def point(self, t: float) -> 'Vertex':
  36. """
  37. Calculate curve point at parameter t [0, 1].
  38. Args:
  39. t: curve position in the range [0, 1]
  40. Returns: (x, y, z) tuple
  41. """
  42. check_if_in_valid_range(t)
  43. return self._get_curve_point(t)
  44. def approximate(self, segments: int) -> Iterable['Vertex']:
  45. delta_t = 1. / segments
  46. yield self._cpoints[0]
  47. for segment in range(1, segments):
  48. yield self.point(delta_t * segment)
  49. yield self._cpoints[3]
  50. def _get_curve_point(self, t: float) -> 'Vertex':
  51. """
  52. Calculate curve point at parameter t [0, 1].
  53. Returns: (x, y, z) tuple
  54. """
  55. b1, b2, b3, b4 = self._cpoints
  56. one_minus_t = 1. - t
  57. m = self.math
  58. point = m.vmul_scalar(b1, one_minus_t ** 3)
  59. point = m.vadd(point, m.vmul_scalar(b2, 3. * one_minus_t ** 2 * t))
  60. point = m.vadd(point, m.vmul_scalar(b3, 3. * one_minus_t * t ** 2))
  61. point = m.vadd(point, m.vmul_scalar(b4, t ** 3))
  62. return tuple(point)
  63. def _get_curve_tangent(self, t: float) -> 'Vertex':
  64. """
  65. Calculate tangent at parameter t [0, 1]. Implementation optimized for 4 control points.
  66. Returns: (x, y, z) tuple, a vector which defines the direction of the tangent.
  67. """
  68. b1, b2, b3, b4 = self._cpoints
  69. m = self.math
  70. tangent = m.vmul_scalar(b1, -3. * (1. - t) ** 2)
  71. tangent = m.vadd(tangent, m.vmul_scalar(b2, 3. * (1. - 4. * t + 3. * t ** 2)))
  72. tangent = m.vadd(tangent, m.vmul_scalar(b3, 3. * t * (2. - 3. * t)))
  73. tangent = m.vadd(tangent, m.vmul_scalar(b4, 3. * t ** 2))
  74. return tuple(tangent)
  75. def approximated_length(self, segments: int = 100) -> float:
  76. length = 0.
  77. point_gen = self.approximate(segments)
  78. prev_point = next(point_gen)
  79. distance = self.math.distance
  80. for point in point_gen:
  81. length += distance(prev_point, point)
  82. prev_point = point
  83. return length
  84. class D2D:
  85. @staticmethod
  86. def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
  87. """ Add vectors """
  88. return vector1[0] + vector2[0], vector1[1] + vector2[1]
  89. @staticmethod
  90. def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
  91. """ Mul vector with scalar """
  92. return vector[0] * scalar, vector[1] * scalar
  93. @staticmethod
  94. def tovector(vector: 'Vertex') -> 'Vertex':
  95. """ Return a 2d point """
  96. return float(vector[0]), float(vector[1])
  97. @staticmethod
  98. def distance(point1: 'Vertex', point2: 'Vertex') -> float:
  99. """ calc distance between two 2d points """
  100. return ((point1[0] - point2[0]) ** 2 +
  101. (point1[1] - point2[1]) ** 2) ** 0.5
  102. class D3D:
  103. @staticmethod
  104. def vadd(vector1: 'Vertex', vector2: 'Vertex') -> 'Vertex':
  105. """ Add vectors """
  106. return vector1[0] + vector2[0], vector1[1] + vector2[1], vector1[2] + vector2[2]
  107. @staticmethod
  108. def vmul_scalar(vector: 'Vertex', scalar: float) -> 'Vertex':
  109. """ Mul vector with scalar """
  110. return vector[0] * scalar, vector[1] * scalar, vector[2] * scalar
  111. @staticmethod
  112. def tovector(vector: 'Vertex') -> 'Vertex':
  113. """ Return a 3d point """
  114. try:
  115. z = float(vector[2])
  116. except IndexError:
  117. z = 0.
  118. return float(vector[0]), float(vector[1]), z
  119. @staticmethod
  120. def distance(point1: 'Vertex', point2: 'Vertex') -> float:
  121. """Calc distance between two 3d points """
  122. return ((point1[0] - point2[0]) ** 2 +
  123. (point1[1] - point2[1]) ** 2 +
  124. (point1[2] - point2[2]) ** 2) ** 0.5