curves.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # Purpose: curve objects
  2. # Created: 26.03.2010, 2018 adapted for ezdxf
  3. # Copyright (c) 2010-2018, Manfred Moitzi
  4. # License: MIT License
  5. from typing import TYPE_CHECKING, Iterable, List, Tuple, Optional
  6. from ezdxf.math.vector import Vector
  7. from ezdxf.math.bspline import bspline_control_frame
  8. from ezdxf.math.bspline import BSpline, BSplineU, BSplineClosed
  9. from ezdxf.math.bezier4p import Bezier4P
  10. from ezdxf.math.eulerspiral import EulerSpiral as _EulerSpiral
  11. if TYPE_CHECKING:
  12. from ezdxf.eztypes import Vertex, GenericLayoutType, Matrix44
  13. class Bezier:
  14. """
  15. Bezier 2d/3d curve.
  16. The Bezier() class is implemented with multiple segments, each segment is an optimized 4 point bezier curve, the
  17. 4 control points of the curve are: the start point (1) and the end point (4), point (2) is start point + start vector
  18. and point (3) is end point + end vector. Each segment has its own approximation count.
  19. """
  20. class Segment:
  21. def __init__(self, start: 'Vertex', end: 'Vertex', start_tangent: 'Vertex', end_tangent: 'Vertex',
  22. segments: int):
  23. self.start = Vector(start)
  24. self.end = Vector(end)
  25. self.start_tangent = Vector(start_tangent) # as vector, from start point
  26. self.end_tangent = Vector(end_tangent) # as vector, from end point
  27. self.segments = segments
  28. def approximate(self) -> Iterable[Vector]:
  29. control_points = [
  30. self.start,
  31. self.start + self.start_tangent,
  32. self.end + self.end_tangent,
  33. self.end,
  34. ]
  35. bezier = Bezier4P(control_points)
  36. return bezier.approximate(self.segments)
  37. def __init__(self):
  38. # fit point, first control vector, second control vector, segment count
  39. self.points = [] # type: List[Tuple[Vector, Optional[Vector], Optional[Vector], Optional[int]]]
  40. def start(self, point: 'Vertex', tangent: 'Vertex') -> None:
  41. """
  42. Set start point and start tangent.
  43. Args:
  44. point: start point
  45. tangent: start tangent as vector, example: (5, 0, 0) means a
  46. horizontal tangent with a length of 5 drawing units
  47. """
  48. self.points.append((point, None, tangent, None))
  49. def append(self, point: 'Vertex', tangent1: 'Vertex', tangent2: 'Vertex' = None, segments: int = 20):
  50. """
  51. Append a control point with two control tangents.
  52. Args:
  53. point: the control point
  54. tangent1: first control tangent as vector *left* of point
  55. tangent2: second control tangent as vector *right* of point, if omitted tangent2 = -tangent1
  56. segments: count of line segments for polyline approximation, count of line segments from previous
  57. control point to this point.
  58. """
  59. tangent1 = Vector(tangent1)
  60. if tangent2 is None:
  61. tangent2 = -tangent1
  62. else:
  63. tangent2 = Vector(tangent2)
  64. self.points.append((point, tangent1, tangent2, int(segments)))
  65. def _build_bezier_segments(self) -> Iterable[Segment]:
  66. if len(self.points) > 1:
  67. for from_point, to_point in zip(self.points[:-1], self.points[1:]):
  68. start_point = from_point[0]
  69. start_tangent = from_point[2] # tangent2
  70. end_point = to_point[0]
  71. end_tangent = to_point[1] # tangent1
  72. count = to_point[3]
  73. yield Bezier.Segment(start_point, end_point,
  74. start_tangent, end_tangent, count)
  75. else:
  76. raise ValueError('Two or more points needed!')
  77. def render(self, layout: 'GenericLayoutType', force3d: bool = False, dxfattribs: dict = None) -> None:
  78. """
  79. Render curve as DXF POLYLINE entity.
  80. Args:
  81. layout: ezdxf layout object
  82. force3d: force 3d polyline rendering
  83. dxfattribs: DXF attributes for base DXF entity (POLYLINE/LWPOLYLINE)
  84. """
  85. points = []
  86. for segment in self._build_bezier_segments():
  87. points.extend(segment.approximate())
  88. if force3d or any(p[2] for p in points):
  89. layout.add_polyline3d(points, dxfattribs=dxfattribs)
  90. else:
  91. layout.add_polyline2d(points, dxfattribs=dxfattribs)
  92. class Spline:
  93. def __init__(self, points: Iterable['Vertex'] = None, segments: int = 100):
  94. if points is None:
  95. points = []
  96. self.points = points
  97. self.segments = int(segments)
  98. def subdivide(self, segments: int = 4) -> None:
  99. """
  100. Calculate overall segment count, where segments is the sub-segment count, segments=4, means 4 line segments
  101. between two definition points e.g. 4 definition points and 4 segments = 12 overall segments, useful for fit
  102. point rendering.
  103. Args:
  104. segments: sub-segments count between two definition points
  105. """
  106. self.segments = (len(self.points) - 1) * segments
  107. def render_as_fit_points(self, layout: 'GenericLayoutType', degree: int = 3, method: str = 'distance',
  108. power: float = .5, dxfattribs: dict = None) -> None:
  109. """
  110. Render a B-spline as 2d/3d polyline, where the definition points are fit points.
  111. - 2d points in -> add_polyline2d()
  112. - 3d points in -> add_polyline3d()
  113. To get vertices at fit points, use method='uniform' and use Spline.subdivide(count), where
  114. count is the sub-segment count, count=4, means 4 line segments between two definition points.
  115. Args:
  116. layout: ezdxf layout
  117. degree: degree of B-spline
  118. method: 'uniform', 'distance' or 'centripetal', calculation method for parameter t
  119. power: power for 'centripetal', default is distance ^ .5
  120. dxfattribs: DXF attributes for POLYLINE
  121. """
  122. spline = bspline_control_frame(self.points, degree=degree, method=method, power=power)
  123. vertices = list(spline.approximate(self.segments))
  124. if any(vertex.z != 0. for vertex in vertices):
  125. layout.add_polyline3d(vertices, dxfattribs=dxfattribs)
  126. else:
  127. layout.add_polyline2d(vertices, dxfattribs=dxfattribs)
  128. render = render_as_fit_points
  129. def render_open_bspline(self, layout: 'GenericLayoutType', degree: int = 3, dxfattribs: dict = None) -> None:
  130. """
  131. Render an open uniform BSpline as 3d polyline. Definition points are control points.
  132. Args:
  133. layout: ezdxf layout
  134. degree: B-spline degree (order = degree + 1)
  135. dxfattribs: DXF attributes for POLYLINE
  136. """
  137. spline = BSpline(self.points, order=degree + 1)
  138. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  139. def render_uniform_bspline(self, layout: 'GenericLayoutType', degree: int = 3, dxfattribs: dict = None) -> None:
  140. """
  141. Render a uniform BSpline as 3d polyline. Definition points are control points.
  142. Args:
  143. layout: ezdxf layout
  144. degree: B-spline degree (order = degree + 1)
  145. dxfattribs: DXF attributes for POLYLINE
  146. """
  147. spline = BSplineU(self.points, order=degree + 1)
  148. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  149. def render_closed_bspline(self, layout: 'GenericLayoutType', degree: int = 3, dxfattribs: dict = None) -> None:
  150. """
  151. Render a closed uniform BSpline as 3d polyline. Definition points are control points.
  152. Args:
  153. layout: ezdxf layout
  154. degree: B-spline degree (order = degree + 1)
  155. dxfattribs: DXF attributes for POLYLINE
  156. """
  157. spline = BSplineClosed(self.points, order=degree + 1)
  158. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  159. def render_open_rbspline(self, layout: 'GenericLayoutType', weights: Iterable[float], degree: int = 3,
  160. dxfattribs: dict = None) -> None:
  161. """
  162. Render a rational open uniform BSpline as 3d polyline.
  163. Args:
  164. layout: ezdxf layout
  165. weights: list of weights, requires a weight value for each defpoint.
  166. degree: B-spline degree (order = degree + 1)
  167. dxfattribs: DXF attributes for POLYLINE
  168. """
  169. spline = BSpline(self.points, order=degree + 1, weights=weights)
  170. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  171. def render_uniform_rbspline(self, layout: 'GenericLayoutType', weights: Iterable[float], degree: int = 3,
  172. dxfattribs: dict = None) -> None:
  173. """
  174. Render a rational uniform BSpline as 3d polyline.
  175. Args:
  176. layout: ezdxf layout
  177. weights: list of weights, requires a weight value for each defpoint.
  178. degree: B-spline degree (order = degree + 1)
  179. dxfattribs: DXF attributes for POLYLINE
  180. """
  181. spline = BSplineU(self.points, order=degree + 1, weights=weights)
  182. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  183. def render_closed_rbspline(self, layout: 'GenericLayoutType', weights: Iterable[float], degree: int = 3,
  184. dxfattribs: dict = None) -> None:
  185. """
  186. Render a rational BSpline as 3d polyline.
  187. Args:
  188. layout: ezdxf layout
  189. weights: list of weights, requires a weight value for each defpoint.
  190. degree: B-spline degree (order = degree + 1)
  191. dxfattribs: DXF attributes for POLYLINE
  192. """
  193. spline = BSplineClosed(self.points, order=degree + 1, weights=weights)
  194. layout.add_polyline3d(list(spline.approximate(self.segments)), dxfattribs=dxfattribs)
  195. class EulerSpiral:
  196. """
  197. Euler spiral (clothoid) for *curvature* (Radius of curvature).
  198. This is a parametric curve, which always starts at the origin.
  199. """
  200. def __init__(self, curvature: float = 1):
  201. self.spiral = _EulerSpiral(float(curvature))
  202. def render_polyline(self, layout: 'GenericLayoutType', length: float = 1, segments: int = 100,
  203. matrix: 'Matrix44' = None, dxfattribs: dict = None):
  204. """
  205. Render curve as polyline.
  206. Args:
  207. layout: ezdxf layout
  208. length: length measured along the spiral curve from its initial position
  209. segments: count of line segments to use, vertex count is segments+1
  210. matrix: transformation matrix as ezdxf.math.Matrix44
  211. dxfattribs: DXF attributes for POLYLINE
  212. Returns: DXF Polyline entity
  213. """
  214. points = self.spiral.approximate(length, segments)
  215. if matrix is not None:
  216. points = matrix.transform_vectors(points)
  217. return layout.add_polyline3d(list(points), dxfattribs=dxfattribs)
  218. def render_spline(self, layout: 'GenericLayoutType', length: float = 1, fit_points: int = 10, degree: int = 3,
  219. matrix: 'Matrix44' = None, dxfattribs: dict = None):
  220. """
  221. Render curve as B-spline.
  222. Args:
  223. layout: ezdxf layout
  224. length: length measured along the spiral curve from its initial position
  225. fit_points: count of spline fit points to use
  226. degree: degree of B-spline
  227. matrix: transformation matrix as ezdxf.math.Matrix44
  228. dxfattribs: DXF attributes for POLYLINE
  229. Returns: DXF Spline entity
  230. """
  231. spline = self.spiral.bspline(length, fit_points, degree=degree)
  232. points = spline.control_points
  233. if matrix is not None:
  234. points = matrix.transform_vectors(points)
  235. return layout.add_open_spline(
  236. control_points=points,
  237. degree=spline.degree,
  238. knots=spline.knot_values(),
  239. dxfattribs=dxfattribs,
  240. )