dxfentity.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. # Created: 11.03.2011
  2. # Copyright (c) 2011-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Any, Iterable, Mapping, List, cast, Tuple
  5. from ezdxf.lldxf.const import DXFStructureError, DXFAttributeError, DXFInvalidLayerName, DXFValueError, DXFXDataError
  6. from ezdxf.lldxf.validator import is_valid_layer_name
  7. from ezdxf.lldxf.tags import Tags
  8. from ezdxf.lldxf.extendedtags import ExtendedTags
  9. from ezdxf.tools import set_flag_state
  10. from ezdxf.math import OCS
  11. if TYPE_CHECKING: # import forward dependencies
  12. from ezdxf.eztypes import TagValue, IterableTags, DXFAttr, DXFDictionary
  13. from ezdxf.eztypes import Drawing, EntityDB, DXFFactoryType, GenericLayoutType
  14. ACAD_REACTORS = '{ACAD_REACTORS'
  15. ACAD_XDICTIONARY = '{ACAD_XDICTIONARY'
  16. class NotFoundException(Exception):
  17. pass
  18. class DXFNamespace:
  19. """
  20. Provides the dxf namespace for GenericWrapper.
  21. """
  22. __slots__ = ('_wrapper',)
  23. def __init__(self, wrapper: 'DXFEntity'):
  24. # DXFNamespace.__setattr__ can not set _wrapper
  25. super(DXFNamespace, self).__setattr__('_wrapper', wrapper)
  26. def __getattr__(self, attrib: str):
  27. """
  28. Returns value of DXF attribute *attrib*. usage: value = DXFEntity.dxf.attrib
  29. """
  30. return self._wrapper.get_dxf_attrib(attrib)
  31. def __setattr__(self, attrib: str, value) -> None:
  32. """
  33. Set DXF attribute *attrib* to *value. usage: DXFEntity.dxf.attrib = value
  34. """
  35. return self._wrapper.set_dxf_attrib(attrib, value)
  36. def __delattr__(self, attrib: str) -> None:
  37. """
  38. Remove DXF attribute *attrib*. usage: del DXFEntity.dxf.attrib
  39. """
  40. return self._wrapper.del_dxf_attrib(attrib)
  41. class DXFEntity:
  42. __slots__ = ('tags', 'dxf', 'drawing')
  43. TEMPLATE = None
  44. CLASS = None
  45. DXFATTRIBS = {}
  46. def __init__(self, tags: ExtendedTags, drawing: 'Drawing' = None):
  47. self.tags = tags # DXF tags stored as DXFTag (and inherited) in an ExtendedTags container
  48. self.dxf = DXFNamespace(self) # type: Any # dynamic DXF attribute dispatching, e.g. DXFEntity.dxf.layer
  49. self.drawing = drawing
  50. def __str__(self) -> str:
  51. """
  52. Returns a simple string representation.
  53. """
  54. return "{}(#{})".format(self.dxftype(), self.dxf.handle)
  55. def __repr__(self) -> str:
  56. """
  57. Returns a simple string representation including the class.
  58. """
  59. return str(self.__class__) + " " + str(self)
  60. @property
  61. def dxffactory(self) -> 'DXFFactoryType':
  62. return self.drawing.dxffactory
  63. @property
  64. def dxfversion(self) -> str:
  65. return self.drawing.dxfversion
  66. @property
  67. def entitydb(self) -> 'EntityDB':
  68. return self.drawing.entitydb
  69. @classmethod
  70. def new(cls, handle: str, dxfattribs: dict = None, drawing: 'Drawing' = None) -> 'DXFEntity':
  71. if cls.TEMPLATE is None:
  72. raise NotImplementedError("new() for type %s not implemented." % cls.__name__)
  73. entity = cls(cls.TEMPLATE.clone(), drawing)
  74. entity.dxf.handle = handle
  75. if dxfattribs is not None:
  76. if 'layer' in dxfattribs:
  77. layer_name = dxfattribs['layer']
  78. if not is_valid_layer_name(layer_name):
  79. raise DXFInvalidLayerName("Invalid layer name '{}'".format(layer_name))
  80. entity.update_dxf_attribs(dxfattribs)
  81. entity.post_new_hook()
  82. return entity
  83. def post_new_hook(self) -> None:
  84. """
  85. Called after entity creation.
  86. """
  87. pass
  88. def _new_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity':
  89. """
  90. Create new entity with same layout settings as *self*.
  91. Used by INSERT & POLYLINE to create appended DXF entities, don't use it to create new standalone entities.
  92. """
  93. entity = self.dxffactory.create_db_entry(type_, dxfattribs)
  94. self.dxffactory.copy_layout(self, entity)
  95. return entity
  96. def __copy__(self) -> 'DXFEntity':
  97. """
  98. Deep copy of DXFEntity with new handle and duplicated linked entities (VERTEX, ATTRIB, SEQEND).
  99. The new entity is not included in any layout space, so the owner tag is set to '0' for undefined owner/layout.
  100. Use Layout.add_entity(new_entity) to add the duplicated entity to a layout, layout can be the model space,
  101. a paper space layout or a block layout.
  102. It is not called __deepcopy__, because this is not a deep copy in the meaning of Python, because handle, link
  103. and owner is changed.
  104. """
  105. new_tags = self.entitydb.duplicate_tags(self.tags)
  106. entity = self.dxffactory.wrap_entity(new_tags)
  107. if self.supports_dxf_attrib('owner'): # R2000+
  108. entity.dxf.owner = '0' # reset ownership/layout
  109. return entity
  110. copy = __copy__
  111. def linked_entities(self) -> Iterable['DXFEntity']:
  112. """
  113. Iterate over all linked entities, only POLYLINE and INSERT has linked entities (VERTEX, ATTRIB, SEQEND)
  114. Yields: DXFEntity() objects
  115. """
  116. link = self.tags.link # type: str
  117. wrap = self.dxffactory.wrap_handle
  118. while link is not None:
  119. entity = wrap(link)
  120. yield entity
  121. link = entity.tags.link
  122. def copy_to_layout(self, layout: 'GenericLayoutType') -> 'DXFEntity':
  123. """
  124. Copy entity to another layout.
  125. Args:
  126. layout: any layout (model space, paper space, block)
  127. Returns: new created entity as DXFEntity() object
  128. """
  129. new_entity = self.copy()
  130. layout.add_entity(new_entity)
  131. return new_entity
  132. def move_to_layout(self, layout: 'GenericLayoutType', source: 'GenericLayoutType' = None) -> None:
  133. """
  134. Move entity from model space or a paper space layout to another layout. For block layout as source, the
  135. block layout has to be specified.
  136. Args:
  137. layout: any layout (model space, paper space, block)
  138. source: provide source layout, faster for DXF R12, if entity is in a block layout
  139. """
  140. if source is None:
  141. source = self.get_layout()
  142. if source is None:
  143. raise DXFValueError('Source layout for entity not found.')
  144. source.move_to_layout(self, layout)
  145. def dxftype(self) -> str:
  146. return self.tags.noclass[0].value
  147. def _get_dxfattr_definition(self, key: str) -> 'DXFAttr':
  148. try:
  149. return self.DXFATTRIBS[key]
  150. except KeyError:
  151. raise DXFAttributeError(key)
  152. def get_dxf_attrib(self, key: str, default: Any = DXFValueError) -> 'TagValue':
  153. dxfattr = self._get_dxfattr_definition(key)
  154. return dxfattr.get_attrib(self, key, default)
  155. def set_dxf_attrib(self, key: str, value: 'TagValue') -> None:
  156. dxfattr = self._get_dxfattr_definition(key)
  157. dxfattr.set_attrib(self, key, value)
  158. def del_dxf_attrib(self, key: str) -> None:
  159. dxfattr = self._get_dxfattr_definition(key)
  160. dxfattr.del_attrib(self)
  161. def supports_dxf_attrib(self, key: str) -> bool:
  162. """
  163. Returns True if DXF attribute key is supported else False. Does not grant that attribute key really exists.
  164. """
  165. dxfattr = self.DXFATTRIBS.get(key, None)
  166. if dxfattr is None:
  167. return False
  168. if dxfattr.dxfversion is None:
  169. return True
  170. return self.drawing.dxfversion >= dxfattr.dxfversion
  171. def dxf_attrib_exists(self, key: str) -> bool:
  172. """
  173. Returns True if DXF attrib key really exists else False. Raises AttributeError if key isn't supported.
  174. """
  175. # attributes with default values don't raise an exception!
  176. return self.get_dxf_attrib(key, default=None) is not None
  177. def valid_dxf_attrib_names(self) -> Iterable[str]:
  178. """
  179. Returns a list of supported DXF attribute names.
  180. """
  181. return [key for key, attrib in self.DXFATTRIBS.items() if
  182. attrib.dxfversion is None or (attrib.dxfversion <= self.drawing.dxfversion)]
  183. def get_dxf_default_value(self, key: str) -> 'TagValue':
  184. """
  185. Returns the default value as defined in the DXF standard.
  186. """
  187. return self._get_dxfattr_definition(key).default
  188. def has_dxf_default_value(self, key: str) -> bool:
  189. """
  190. Returns True if the DXF attribute key has a DXF standard default value.
  191. """
  192. return self._get_dxfattr_definition(key).default is not None
  193. def dxfattribs(self) -> dict:
  194. """
  195. Clones defined and existing DXF attributes as dict.
  196. """
  197. dxfattribs = {}
  198. for key in self.DXFATTRIBS.keys():
  199. value = self.get_dxf_attrib(key, default=None)
  200. if value is not None:
  201. dxfattribs[key] = value
  202. return dxfattribs
  203. clone_dxf_attribs = dxfattribs
  204. def update_dxf_attribs(self, dxfattribs: Mapping) -> None:
  205. for key, value in dxfattribs.items():
  206. self.set_dxf_attrib(key, value)
  207. def set_flag_state(self, flag: int, state: bool = True, name: str = 'flags') -> None:
  208. flags = self.get_dxf_attrib(name, 0)
  209. self.set_dxf_attrib(name, set_flag_state(flags, flag, state=state))
  210. def get_flag_state(self, flag: int, name: str = 'flags') -> bool:
  211. return bool(self.get_dxf_attrib(name, 0) & flag)
  212. def ocs(self) -> OCS:
  213. extrusion = self.get_dxf_attrib('extrusion', default=(0, 0, 1))
  214. return OCS(extrusion)
  215. def destroy(self) -> None:
  216. if self.has_extension_dict():
  217. xdict = self.get_extension_dict()
  218. self.drawing.objects.delete_entity(xdict)
  219. def has_app_data(self, appid: str) -> bool:
  220. return self.tags.has_app_data(appid)
  221. def get_app_data(self, appid: str) -> Tags:
  222. return self.tags.get_app_data_content(appid)
  223. def set_app_data(self, appid: str, app_data_tags: 'IterableTags') -> None:
  224. if self.tags.has_app_data(appid):
  225. self.tags.set_app_data_content(appid, app_data_tags)
  226. else:
  227. self.tags.new_app_data(appid, app_data_tags)
  228. def has_xdata(self, appid: str) -> bool:
  229. return self.tags.has_xdata(appid)
  230. def get_xdata(self, appid: str) -> Tags:
  231. return Tags(self.tags.get_xdata(appid)[1:]) # without app id tag
  232. def set_xdata(self, appid: str, xdata_tags: 'IterableTags') -> None:
  233. if self.tags.has_xdata(appid):
  234. self.tags.set_xdata(appid, xdata_tags)
  235. else:
  236. self.tags.new_xdata(appid, xdata_tags)
  237. def has_xdata_list(self, appid: str, name: str) -> bool:
  238. """
  239. Returns if list `name` from XDATA `appid` exists.
  240. Args:
  241. appid: APPID
  242. name: list name
  243. """
  244. try:
  245. self.get_xdata_list(appid, name)
  246. except DXFValueError:
  247. return False
  248. else:
  249. return True
  250. def get_xdata_list(self, appid: str, name: str) -> List[Tuple]:
  251. """
  252. Get list `name` from XDATA `appid`.
  253. Args:
  254. appid: APPID
  255. name: list name
  256. Returns: list of DXFTags including list name and curly braces '{' '}' tags
  257. Raises:
  258. DXFValueError: XDATA `appid` do not exist or list `name` do not exist
  259. """
  260. xdata = self.get_xdata(appid)
  261. try:
  262. return get_named_list_from_xdata(name, xdata)
  263. except NotFoundException:
  264. raise DXFValueError('No data list "{}" not found for APPID "{}"'.format(name, appid))
  265. def set_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None:
  266. """
  267. Create new list `name` of XDATA `appid` with `xdata_tags` and replaces list `name` if already exists.
  268. Args:
  269. appid: APPID
  270. name: list name
  271. xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will
  272. be added
  273. """
  274. if not self.tags.has_xdata(appid):
  275. self.tags.new_xdata(appid, xdata_list(name, xdata_tags))
  276. else:
  277. self.replace_xdata_list(appid, name, xdata_tags)
  278. def discard_xdata_list(self, appid: str, name: str) -> None:
  279. """
  280. Deletes list `name` from XDATA `appid`. Ignores silently if XDATA `appid` or list `name` not exists.
  281. Args:
  282. appid: APPID
  283. name: list name
  284. """
  285. try:
  286. xdata = self.get_xdata(appid)
  287. except DXFValueError:
  288. pass
  289. else:
  290. try:
  291. tags = remove_named_list_from_xdata(name, xdata)
  292. except NotFoundException:
  293. pass
  294. else:
  295. self.set_xdata(appid, tags)
  296. def replace_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None:
  297. """
  298. Replaces list `name` of existing XDATA `appid` with `xdata_tags`. Appends new list if list `name` do not exist,
  299. but raises `DXFValueError` if XDATA `appid` do not exist.
  300. Low level interface, if not sure use `set_xdata_list()` instead.
  301. Args:
  302. appid: APPID
  303. name: list name
  304. xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will
  305. be added
  306. Raises:
  307. DXFValueError: XDATA `appid` do not exist
  308. """
  309. xdata = self.get_xdata(appid)
  310. try:
  311. tags = remove_named_list_from_xdata(name, xdata)
  312. except NotFoundException:
  313. tags = xdata
  314. tags.extend(xdata_list(name, xdata_tags))
  315. self.tags.set_xdata(appid, tags)
  316. def has_reactors(self) -> bool:
  317. return self.has_app_data(ACAD_REACTORS)
  318. def get_reactors(self) -> List[str]:
  319. reactor_tags = self.get_app_data(ACAD_REACTORS)
  320. return [tag.value for tag in reactor_tags]
  321. def set_reactors(self, reactor_handles: Iterable[str]) -> None:
  322. reactor_tags = [(330, handle) for handle in reactor_handles]
  323. self.set_app_data(ACAD_REACTORS, reactor_tags)
  324. def append_reactor_handle(self, handle: str) -> None:
  325. reactors = set(self.get_reactors())
  326. reactors.add(handle)
  327. self.set_reactors(sorted(reactors, key=lambda x: int(x, base=16)))
  328. def remove_reactor_handle(self, handle: str) -> None:
  329. reactors = set(self.get_reactors())
  330. reactors.discard(handle)
  331. self.set_reactors(reactors)
  332. def has_extension_dict(self) -> bool:
  333. return self.has_app_data(ACAD_XDICTIONARY)
  334. def get_extension_dict(self) -> 'DXFDictionary':
  335. """
  336. Get associated extension dictionary as DXFDictionary() object.
  337. """
  338. app_data = self.get_app_data(ACAD_XDICTIONARY)
  339. if len(app_data) == 0 or app_data[0].code != 360:
  340. raise DXFStructureError("XDICTIONARY error in entity: " + str(self))
  341. # are more than one XDICTIONARY possible?
  342. xdict_handle = app_data[0].value
  343. return cast('DXFDictionary', self.dxffactory.wrap_handle(xdict_handle))
  344. def new_extension_dict(self) -> 'DXFDictionary':
  345. """
  346. Creates and assigns a new extensions dictionary. Link to an existing extension dictionary will be lost.
  347. """
  348. xdict = self.drawing.objects.add_dictionary(owner=self.dxf.handle)
  349. self.set_app_data(ACAD_XDICTIONARY, [(360, xdict.dxf.handle)])
  350. return xdict
  351. def get_layout(self) -> 'GenericLayoutType':
  352. return self.dxffactory.get_layout_for_entity(self)
  353. def audit(self, auditor):
  354. """
  355. Audit entity for errors.
  356. Args:
  357. auditor: Audit() object
  358. """
  359. pass
  360. def has_embedded_objects(self) -> bool:
  361. return any(tags.has_embedded_objects() for tags in self.tags.subclasses)
  362. OPEN_LIST = (1002, '{')
  363. CLOSE_LIST = (1002, '}')
  364. def xdata_list(name: str, xdata_tags: 'IterableTags') -> List[Tuple]:
  365. tags = []
  366. if name:
  367. tags.append((1000, name))
  368. tags.append(OPEN_LIST)
  369. tags.extend(xdata_tags)
  370. tags.append(CLOSE_LIST)
  371. return tags
  372. def remove_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]:
  373. start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
  374. del tags[start: end]
  375. return tags
  376. def get_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]:
  377. start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
  378. return tags[start: end]
  379. def get_start_and_end_of_named_list_in_xdata(name: str, tags: List[Tuple]) -> Tuple[int, int]:
  380. start = None
  381. end = None
  382. level = 0
  383. for index in range(len(tags)):
  384. tag = tags[index]
  385. if start is None and tag == (1000, name):
  386. next_tag = tags[index + 1]
  387. if next_tag == OPEN_LIST:
  388. start = index
  389. continue
  390. if start is not None:
  391. if tag == OPEN_LIST:
  392. level += 1
  393. elif tag == CLOSE_LIST:
  394. level -= 1
  395. if level == 0:
  396. end = index
  397. break
  398. if start is None:
  399. raise NotFoundException
  400. if end is None:
  401. raise DXFXDataError('Invalid XDATA structure: missing (1002, "}").')
  402. return start, end + 1