spline.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # Created: 24.05.2015
  2. # Copyright (c) 2015-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import Iterable, TYPE_CHECKING, cast, Sequence
  5. from contextlib import contextmanager
  6. from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
  7. from ezdxf.lldxf.types import DXFTag
  8. from ezdxf.lldxf.extendedtags import ExtendedTags
  9. from ezdxf.lldxf.const import DXFValueError
  10. from ezdxf.lldxf.packedtags import TagArray, VertexArray
  11. from ezdxf.math.bspline import knot_uniform, knot_open_uniform
  12. from ezdxf.lldxf import loader
  13. from .graphics import none_subclass, entity_subclass, ModernGraphicEntity
  14. if TYPE_CHECKING:
  15. from ezdxf.eztypes import Tags, Vertex
  16. import array
  17. class KnotTags(TagArray):
  18. code = -40 # compatible with DXFTag.code
  19. VALUE_CODE = 40
  20. DTYPE = 'f'
  21. def dxftags(self) -> Iterable[DXFTag]:
  22. # knot value count
  23. yield DXFTag(72, len(self.value))
  24. # Python 2.7 compatible
  25. for t in super(KnotTags, self).dxftags():
  26. yield t
  27. class WeightTags(TagArray):
  28. code = -41 # compatible with DXFTag.code
  29. VALUE_CODE = 41
  30. DTYPE = 'f'
  31. class ControlPoints(VertexArray):
  32. code = -10 # compatible with DXFTag.code
  33. VERTEX_CODE = 10
  34. VERTEX_SIZE = 3
  35. def dxftags(self) -> Iterable[DXFTag]:
  36. # control point count
  37. yield DXFTag(73, len(self))
  38. # Python 2.7 compatible
  39. for t in super(ControlPoints, self).dxftags():
  40. yield t
  41. class FitPoints(VertexArray):
  42. code = -11 # compatible with DXFTag.code
  43. VERTEX_CODE = 11
  44. VERTEX_SIZE = 3
  45. def dxftags(self) -> Iterable[DXFTag]:
  46. # fit point count
  47. yield DXFTag(74, len(self))
  48. # Python 2.7 compatible
  49. for t in super(FitPoints, self).dxftags():
  50. yield t
  51. REMOVE_CODES = (ControlPoints.VERTEX_CODE, FitPoints.VERTEX_CODE, KnotTags.VALUE_CODE, WeightTags.VALUE_CODE) + (
  52. 72, 73, 74)
  53. @loader.register('SPLINE', legacy=False)
  54. def tag_processor(tags: ExtendedTags) -> ExtendedTags:
  55. spline_tags = tags.get_subclass('AcDbSpline')
  56. control_points = ControlPoints.from_tags(spline_tags)
  57. fit_points = FitPoints.from_tags(spline_tags)
  58. knots = KnotTags.from_tags(spline_tags)
  59. weights = WeightTags.from_tags(spline_tags)
  60. spline_tags.remove_tags(codes=REMOVE_CODES)
  61. spline_tags.extend((knots, weights, control_points, fit_points))
  62. return tags
  63. _SPLINE_TPL = """0
  64. SPLINE
  65. 5
  66. 0
  67. 330
  68. 0
  69. 100
  70. AcDbEntity
  71. 8
  72. 0
  73. 100
  74. AcDbSpline
  75. 70
  76. 0
  77. 71
  78. 3
  79. 72
  80. 0
  81. 73
  82. 0
  83. 74
  84. 0
  85. """
  86. spline_subclass = DefSubclass('AcDbSpline', {
  87. 'flags': DXFAttr(70, default=0),
  88. 'degree': DXFAttr(71),
  89. 'n_knots': DXFAttr(72, xtype=XType.callback, getter='knot_value_count'),
  90. 'n_control_points': DXFAttr(73, xtype=XType.callback, getter='control_point_count'),
  91. 'n_fit_points': DXFAttr(74, xtype=XType.callback, getter='fit_point_count'),
  92. 'knot_tolerance': DXFAttr(42, default=1e-10),
  93. 'control_point_tolerance': DXFAttr(43, default=1e-10),
  94. 'fit_tolerance': DXFAttr(44, default=1e-10),
  95. 'start_tangent': DXFAttr(12, xtype=XType.point3d),
  96. 'end_tangent': DXFAttr(13, xtype=XType.point3d),
  97. 'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
  98. # 10: Control points (in WCS); one entry per control point
  99. # 11: Fit points (in WCS); one entry per fit point
  100. # 40: Knot value (one entry per knot)
  101. # 41: Weight (if not 1); with multiple group pairs, they are present if all are not 1
  102. })
  103. class Spline(ModernGraphicEntity):
  104. __slots__ = ()
  105. TEMPLATE = tag_processor(ExtendedTags.from_text(_SPLINE_TPL))
  106. DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, spline_subclass)
  107. CLOSED = 1 # closed b-spline
  108. PERIODIC = 2 # uniform b-spline
  109. RATIONAL = 4 # rational b-spline
  110. PLANAR = 8 # all spline points in a plane, don't read or set this bit, just ignore like AutoCAD
  111. LINEAR = 16 # always set with PLANAR, don't read or set this bit, just ignore like AutoCAD
  112. @property
  113. def AcDbSpline(self) -> 'Tags':
  114. return self.tags.subclasses[2]
  115. @property
  116. def closed(self) -> bool:
  117. return self.get_flag_state(self.CLOSED, name='flags')
  118. @closed.setter
  119. def closed(self, status: bool) -> None:
  120. self.set_flag_state(self.CLOSED, state=status, name='flags')
  121. @property
  122. def knot_values(self) -> 'array.array': # group code 40
  123. """
  124. Returns spline knot values as array.array('f').
  125. """
  126. return cast('array.array', self.AcDbSpline.get_first_tag(KnotTags.code).value)
  127. def knot_value_count(self) -> int: # DXF callback attribute Spline.dxf.n_knots
  128. return len(self.knot_values)
  129. def get_knot_values(self) -> 'array.array': # deprecated
  130. return self.knot_values
  131. def set_knot_values(self, knot_values: Iterable[float]) -> None:
  132. knots = cast(KnotTags, self.AcDbSpline.get_first_tag(KnotTags.code))
  133. knots.set_values(knot_values)
  134. @property
  135. def weights(self) -> 'array.array': # group code 41
  136. """
  137. Returns spline control point weights as array.array('f').
  138. """
  139. return cast('array.array', self.AcDbSpline.get_first_tag(WeightTags.code).value)
  140. def get_weights(self): # deprecated
  141. return self.weights
  142. def set_weights(self, values):
  143. weights = cast(WeightTags, self.AcDbSpline.get_first_tag(WeightTags.code))
  144. weights.set_values(values)
  145. @property
  146. def control_points(self) -> ControlPoints: # group code 10
  147. """
  148. Returns spline control points as ControlPoints() object.
  149. """
  150. return self.AcDbSpline.get_first_tag(ControlPoints.code)
  151. def control_point_count(self) -> int: # DXF callback attribute Spline.dxf.n_control_points
  152. return len(self.control_points)
  153. def get_control_points(self) -> ControlPoints: # deprecated
  154. return self.control_points
  155. def set_control_points(self, points: Iterable['Vertex']) -> None:
  156. vertices = self.control_points
  157. vertices.clear()
  158. vertices.extend(points)
  159. @property
  160. def fit_points(self) -> FitPoints: # group code 11
  161. """
  162. Returns spline fit points as FitPoints() object.
  163. """
  164. return self.AcDbSpline.get_first_tag(FitPoints.code)
  165. def fit_point_count(self) -> int: # DXF callback attribute Spline.dxf.n_fit_points
  166. return len(self.fit_points)
  167. def get_fit_points(self) -> FitPoints: # deprecated
  168. return self.fit_points
  169. def set_fit_points(self, points: Iterable['Vertex']) -> None:
  170. vertices = self.fit_points
  171. vertices.clear()
  172. vertices.extend(points)
  173. def set_open_uniform(self, control_points: Sequence['Vertex'], degree: int = 3) -> None:
  174. """
  175. Open B-spline with uniform knot vector, start and end at your first and last control points.
  176. """
  177. self.dxf.flags = 0 # clear all flags
  178. self.dxf.degree = degree
  179. self.set_control_points(control_points)
  180. self.set_knot_values(knot_open_uniform(len(control_points), degree + 1))
  181. def set_uniform(self, control_points: Sequence['Vertex'], degree: int = 3) -> None:
  182. """
  183. B-spline with uniform knot vector, does NOT start and end at your first and last control points.
  184. """
  185. self.dxf.flags = 0 # clear all flags
  186. self.dxf.degree = degree
  187. self.set_control_points(control_points)
  188. self.set_knot_values(knot_uniform(len(control_points), degree + 1))
  189. def set_periodic(self, control_points: Sequence['Vertex'], degree=3) -> None:
  190. """
  191. Closed B-spline with uniform knot vector, start and end at your first control point.
  192. """
  193. self.dxf.flags = self.PERIODIC | self.CLOSED
  194. self.dxf.degree = degree
  195. self.set_control_points(control_points)
  196. # AutoDesk Developer Docs:
  197. # If the spline is periodic, the length of knot vector will be greater than length of the control array by 1.
  198. self.set_knot_values(list(range(len(control_points) + 1)))
  199. def set_open_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float], degree: int = 3) -> None:
  200. """
  201. Open rational B-spline with uniform knot vector, start and end at your first and last control points, and has
  202. additional control possibilities by weighting each control point.
  203. """
  204. self.set_open_uniform(control_points, degree=degree)
  205. self.dxf.flags = self.dxf.flags | self.RATIONAL
  206. if len(weights) != len(control_points):
  207. raise DXFValueError('Control point count must be equal to weights count.')
  208. self.set_weights(weights)
  209. def set_uniform_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float],
  210. degree: int = 3) -> None:
  211. """
  212. Rational B-spline with uniform knot vector, deos NOT start and end at your first and last control points, and
  213. has additional control possibilities by weighting each control point.
  214. """
  215. self.set_uniform(control_points, degree=degree)
  216. self.dxf.flags = self.dxf.flags | self.RATIONAL
  217. if len(weights) != len(control_points):
  218. raise DXFValueError('Control point count must be equal to weights count.')
  219. self.set_weights(weights)
  220. def set_periodic_rational(self, control_points: Sequence['Vertex'], weights: Sequence[float],
  221. degree: int = 3) -> None:
  222. """
  223. Closed rational B-spline with uniform knot vector, start and end at your first control point, and has
  224. additional control possibilities by weighting each control point.
  225. """
  226. self.set_periodic(control_points, degree=degree)
  227. self.dxf.flags = self.dxf.flags | self.RATIONAL
  228. if len(weights) != len(control_points):
  229. raise DXFValueError('Control point count must be equal to weights count.')
  230. self.set_weights(weights)
  231. @contextmanager
  232. def edit_data(self) -> 'SplineData':
  233. """
  234. Edit spline data by context manager, usage::
  235. with spline.edit_data() as data:
  236. # set uniform knot vector
  237. data.knots_values = list(range(spline.dxf.n_control_points+spline.dxf.degree+1))
  238. Yields: SplineData()
  239. """
  240. data = SplineData(self)
  241. yield data
  242. if data.fit_points is not self.fit_points:
  243. self.set_fit_points(data.fit_points)
  244. if data.control_points is not self.control_points:
  245. self.set_control_points(data.control_points)
  246. if data.knot_values is not self.knot_values:
  247. self.set_knot_values(data.knot_values)
  248. if data.weights is not self.weights:
  249. self.set_weights(data.weights)
  250. class SplineData:
  251. def __init__(self, spline: 'Spline'):
  252. self.fit_points = spline.fit_points
  253. self.control_points = spline.control_points
  254. self.knot_values = spline.knot_values
  255. self.weights = spline.weights