lwpolyline.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. # Created: 25.03.2011
  2. # Copyright (c) 2011-2018, Manfred Moitzi
  3. # License: MIT License
  4. from contextlib import contextmanager
  5. import array
  6. from ezdxf.lldxf.types import DXFTag, DXFVertex
  7. from ezdxf.lldxf.packedtags import VertexArray, replace_tags
  8. from ezdxf.lldxf import loader
  9. from .graphics import ExtendedTags, DXFAttr, DefSubclass, DXFAttributes, XType
  10. from .graphics import none_subclass, entity_subclass, ModernGraphicEntity
  11. from typing import TYPE_CHECKING, Tuple, Iterable, cast, Sequence, List
  12. if TYPE_CHECKING:
  13. from ezdxf.eztypes import Tags, Vertex
  14. LWPointType = Tuple[float, float, float, float, float]
  15. FORMAT_CODES = frozenset('xysebv')
  16. DEFAULT_FORMAT = 'xyseb'
  17. _LWPOLYLINE_TPL = """0
  18. LWPOLYLINE
  19. 5
  20. 0
  21. 330
  22. 0
  23. 100
  24. AcDbEntity
  25. 8
  26. 0
  27. 100
  28. AcDbPolyline
  29. 90
  30. 0
  31. 70
  32. 0
  33. 43
  34. 0.0
  35. """
  36. # Order doesn't matter, not valid for AutoCAD:
  37. # If tag 90 is not the first TAG, AutoCAD does not close the polyline when the `close` flag is set.
  38. lwpolyline_subclass = DefSubclass('AcDbPolyline', {
  39. 'count': DXFAttr(90, xtype=XType.callback, getter='__len__', setter='_set_count'),
  40. # always return actual length and set tag 90
  41. '_count': DXFAttr(90), # special attribute to update length
  42. 'elevation': DXFAttr(38, default=0.0),
  43. 'thickness': DXFAttr(39, default=0.0),
  44. 'flags': DXFAttr(70, default=0),
  45. 'const_width': DXFAttr(43, default=0.0),
  46. 'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
  47. })
  48. LWPOINTCODES = (10, 20, 40, 41, 42)
  49. class LWPolylinePoints(VertexArray):
  50. code = -10 # compatible with DXFTag.code
  51. VERTEX_CODE = 10
  52. START_WIDTH_CODE = 40
  53. END_WIDTH_CODE = 41
  54. BULGE_CODE = 42
  55. VERTEX_SIZE = 5
  56. __slots__ = ('value',)
  57. @classmethod
  58. def from_tags(cls, tags: ExtendedTags) -> 'LWPolylinePoints':
  59. """
  60. Setup point array from extended tags.
  61. Args:
  62. tags: ExtendedTags() object
  63. """
  64. subclass = tags.get_subclass('AcDbPolyline')
  65. def get_vertex() -> LWPointType:
  66. point.append(attribs.get(cls.START_WIDTH_CODE, 0))
  67. point.append(attribs.get(cls.END_WIDTH_CODE, 0))
  68. point.append(attribs.get(cls.BULGE_CODE, 0))
  69. return tuple(point)
  70. data = []
  71. point = None
  72. attribs = {}
  73. for tag in subclass:
  74. if tag.code in LWPOINTCODES:
  75. if tag.code == 10:
  76. if point is not None:
  77. data.extend(get_vertex())
  78. point = list(tag.value[0:2]) # just use x, y coordinates, z is invalid but you never know!
  79. attribs = {}
  80. else:
  81. attribs[tag.code] = tag.value
  82. if point is not None:
  83. data.extend(get_vertex())
  84. return cls(data=data)
  85. def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
  86. super(LWPolylinePoints, self).append(compile_array(point, format=format))
  87. def dxftags(self) -> Iterable[DXFTag]:
  88. for point in self:
  89. x, y, start_width, end_width, bulge = point
  90. yield DXFVertex(self.VERTEX_CODE, (x, y))
  91. if start_width:
  92. yield DXFTag(self.START_WIDTH_CODE, start_width)
  93. if end_width:
  94. yield DXFTag(self.END_WIDTH_CODE, end_width)
  95. if bulge:
  96. yield DXFTag(self.BULGE_CODE, bulge)
  97. @loader.register('LWPOLYLINE', legacy=False)
  98. def tag_processor(tags: ExtendedTags) -> ExtendedTags:
  99. points = LWPolylinePoints.from_tags(tags)
  100. subclass = tags.get_subclass('AcDbPolyline')
  101. replace_tags(subclass, codes=LWPOINTCODES, packed_data=points)
  102. return tags
  103. class LWPolyline(ModernGraphicEntity):
  104. __slots__ = ()
  105. TEMPLATE = tag_processor(ExtendedTags.from_text(_LWPOLYLINE_TPL))
  106. DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, lwpolyline_subclass)
  107. CLOSED = 1
  108. PLINEGEN = 128
  109. @property
  110. def AcDbPolyline(self) -> 'Tags':
  111. return self.tags.subclasses[2]
  112. @property
  113. def lwpoints(self) -> LWPolylinePoints:
  114. return cast(LWPolylinePoints, self.AcDbPolyline.get_first_tag(LWPolylinePoints.code))
  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, status, name='flags')
  121. # same as POLYLINE
  122. def close(self, state=True) -> None:
  123. self.closed = state
  124. def __len__(self) -> int:
  125. return len(self.lwpoints)
  126. def __iter__(self) -> Iterable[LWPointType]:
  127. """
  128. Yielding tuples of (x, y, start_width, end_width, bulge).
  129. """
  130. return iter(self.lwpoints)
  131. def __getitem__(self, index: int) -> LWPointType:
  132. """
  133. Returns polyline point at position index as (x, y, start_width, end_width, bulge) tuple.
  134. """
  135. return self.lwpoints[index]
  136. def __setitem__(self, index: int, value: Sequence[float]) -> None:
  137. """
  138. Set polyline point at position index. Point format is fixed as 'xyseb'.
  139. Args:
  140. index: point index
  141. value: point value as (x, y, [start_width, [end_width, [bulge]]]) tuple
  142. """
  143. self.lwpoints[index] = compile_array(value)
  144. def __delitem__(self, index: int) -> None:
  145. del self.lwpoints[index]
  146. self.update_count()
  147. def vertices(self) -> Iterable[Tuple[float, float]]:
  148. """
  149. Yields all points as (x, y) tuples.
  150. """
  151. for point in self:
  152. yield point[0], point[1]
  153. def vertices_in_wcs(self) -> Iterable['Vertex']:
  154. """
  155. Yields all points as (x, y, z) tuples in WCS.
  156. """
  157. ocs = self.ocs()
  158. elevation = self.get_dxf_attrib('elevation', default=0.)
  159. for x, y in self.vertices():
  160. yield ocs.to_wcs((x, y, elevation))
  161. def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
  162. """
  163. Append point to polyline, format specifies a user defined point format.
  164. Args:
  165. point: (x, y, [start_width, [end_width, [bulge]]]) tuple
  166. format: format string, default is 'xyseb'
  167. x = x coordinate
  168. y = y coordinate
  169. s = start width
  170. e = end width
  171. b = bulge value
  172. v = (x, y) as tuple
  173. """
  174. self.lwpoints.append(point, format=format)
  175. self.update_count()
  176. def insert(self, pos: int, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
  177. """
  178. Insert new point in front of positions pos, format specifies a user defined point format.
  179. Args:
  180. pos: insert position
  181. point: point data
  182. format: format string, default is 'xyseb'
  183. x = x coordinate
  184. y = y coordinate
  185. s = start width
  186. e = end width
  187. b = bulge value
  188. v = (x, y) as tuple
  189. """
  190. data = compile_array(point, format=format)
  191. self.lwpoints.insert(pos, data)
  192. self.update_count()
  193. def append_points(self, points: Iterable[Sequence[float]], format: str = DEFAULT_FORMAT) -> None:
  194. """
  195. Append new points to polyline, format specifies a user defined point format.
  196. Args:
  197. points: iterable of point, point is (x, y, [start_width, [end_width, [bulge]]]) tuple
  198. format: format string, default is 'xyseb'
  199. x = x coordinate
  200. y = y coordinate
  201. s = start width
  202. e = end width
  203. b = bulge value
  204. v = (x, y) as tuple
  205. """
  206. for point in points:
  207. self.lwpoints.append(point, format=format)
  208. self.update_count()
  209. @contextmanager
  210. def points(self, format: str = DEFAULT_FORMAT) -> List[Sequence[float]]:
  211. points = self.get_points(format=format)
  212. yield points
  213. self.set_points(points, format=format)
  214. def get_points(self, format: str = DEFAULT_FORMAT) -> List[Sequence[float]]:
  215. """
  216. Returns all points as list of tuples, format specifies a user defined point format.
  217. Args:
  218. format: format string, default is 'xyseb'
  219. x = x coordinate
  220. y = y coordinate
  221. s = start width
  222. e = end width
  223. b = bulge value
  224. v = (x, y) as tuple
  225. """
  226. return [format_point(p, format=format) for p in self.lwpoints]
  227. def set_points(self, points: List[Sequence[float]], format: str = DEFAULT_FORMAT) -> None:
  228. """
  229. Remove all points and append new points.
  230. Args:
  231. points: iterable of point, point is (x, y, [start_width, [end_width, [bulge]]]) tuple
  232. format: format string, default is 'xyseb'
  233. x = x coordinate
  234. y = y coordinate
  235. s = start width
  236. e = end width
  237. b = bulge value
  238. v = (x, y) as tuple
  239. """
  240. self.lwpoints.clear()
  241. self.append_points(points, format=format)
  242. def clear(self) -> None:
  243. self.lwpoints.clear()
  244. self.update_count()
  245. def _set_count(self, value: int) -> None:
  246. # use special DXF attribute, because 'count' is a callback attribute
  247. self.set_dxf_attrib('_count', value)
  248. def update_count(self):
  249. self._set_count(len(self.lwpoints))
  250. def format_point(point: Sequence[float], format: str = 'xyseb') -> Sequence[float]:
  251. """
  252. Reformat point components.
  253. Args:
  254. point: list or tuple of (x, y, start_width, end_width, bulge)
  255. format: format string, default is 'xyseb'
  256. x = x coordinate
  257. y = y coordinate
  258. s = start width
  259. e = end width
  260. b = bulge value
  261. v = (x, y) as tuple
  262. Returns: tuple of selected components
  263. """
  264. x, y, s, e, b = point
  265. v = (x, y)
  266. vars = locals()
  267. return tuple(vars[code] for code in format.lower() if code in FORMAT_CODES)
  268. def compile_array(data: Sequence[float], format='xyseb'):
  269. """
  270. Gather point components from input data.
  271. Args:
  272. data: list or tuple of point components
  273. format: format string, default is 'xyseb'
  274. x = x coordinate
  275. y = y coordinate
  276. s = start width
  277. e = end width
  278. b = bulge value
  279. v = (x, y) as tuple
  280. Returns: array.array('d', (x, y, start_width, end_width, bulge))
  281. """
  282. a = array.array('d', (0., 0., 0., 0., 0.))
  283. format = [code for code in format.lower() if code in FORMAT_CODES]
  284. for code, value in zip(format, data):
  285. if code not in FORMAT_CODES:
  286. continue
  287. if code == 'v':
  288. value = cast('Vertex', value)
  289. a[0] = value[0]
  290. a[1] = value[1]
  291. else:
  292. a['xyseb'.index(code)] = value
  293. return a