dxfgroups.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. # Created: 22.03.2011
  2. # Copyright (c) 2011-2018, Manfred Moitzi
  3. # License: MIT-License
  4. from typing import TYPE_CHECKING, Iterable, Union, cast, List
  5. from contextlib import contextmanager
  6. from ezdxf.lldxf.types import DXFTag
  7. from ezdxf.lldxf.extendedtags import ExtendedTags
  8. from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass
  9. from ezdxf.dxfentity import DXFEntity
  10. from ezdxf.lldxf.const import DXFValueError
  11. from .dxfobjects import none_subclass
  12. from .object_manager import ObjectManager
  13. if TYPE_CHECKING:
  14. from ezdxf.eztypes import Tags, Drawing
  15. _GROUP_TPL = """0
  16. GROUP
  17. 5
  18. 0
  19. 330
  20. 0
  21. 100
  22. AcDbGroup
  23. 300
  24. 70
  25. 1
  26. 71
  27. 1
  28. """
  29. GROUP_ITEM_CODE = 340
  30. class DXFGroup(DXFEntity):
  31. __slots__ = ()
  32. # groups are not allowed in block definitions
  33. TEMPLATE = ExtendedTags.from_text(_GROUP_TPL)
  34. DXFATTRIBS = DXFAttributes(
  35. none_subclass,
  36. DefSubclass('AcDbGroup', {
  37. 'description': DXFAttr(300),
  38. 'unnamed': DXFAttr(70),
  39. 'selectable': DXFAttr(71),
  40. }),
  41. )
  42. @property
  43. def AcDbGroup(self) -> 'Tags':
  44. return self.tags.subclasses[1]
  45. def __iter__(self) -> Iterable[DXFEntity]:
  46. """ Yields all DXF entities of this group as wrapped DXFEntity (LINE, CIRCLE, ...) objects.
  47. """
  48. wrap = self.dxffactory.wrap_handle
  49. for handle in self.handles():
  50. try:
  51. entity = wrap(handle)
  52. yield entity
  53. except KeyError: # handle not in entity database, maybe entity were deleted; internal exception
  54. pass
  55. def __len__(self) -> int:
  56. return sum(1 for tag in self.AcDbGroup if tag.code == GROUP_ITEM_CODE)
  57. def __contains__(self, item: Union[str, DXFEntity]) -> bool:
  58. handle = item if isinstance(item, str) else item.dxf.handle
  59. return handle in set(self.handles())
  60. def handles(self) -> Iterable[str]:
  61. return (tag.value for tag in self.AcDbGroup if tag.code == GROUP_ITEM_CODE)
  62. def get_name(self):
  63. group_table = cast('DXFDictionary', self.dxffactory.wrap_handle(self.dxf.owner))
  64. my_handle = self.dxf.handle
  65. for name, handle in group_table.items():
  66. if handle == my_handle:
  67. return name
  68. return None
  69. @contextmanager
  70. def edit_data(self) -> List['DXFEntity']:
  71. data = list(self)
  72. yield data
  73. self.set_data(data)
  74. def set_data(self, entities: List['DXFEntity']) -> None:
  75. entities = list(entities) # for generators
  76. if not all_entities_on_same_layout(entities):
  77. raise DXFValueError(
  78. "All entities have to be on the same layout (model space or any paper layout but not block).")
  79. self.clear()
  80. self.AcDbGroup.extend(DXFTag(GROUP_ITEM_CODE, entity.dxf.handle) for entity in entities)
  81. def extend(self, entities: Iterable['DXFEntity']) -> None:
  82. """
  83. Add `entities` to group.
  84. Args:
  85. entities: iterable of DXFEntity
  86. """
  87. with self.edit_data() as e:
  88. e.extend(entities)
  89. def clear(self) -> None:
  90. """
  91. Remove all entity references, does not delete any drawing entities referenced by this group.
  92. """
  93. self.AcDbGroup.remove_tags((GROUP_ITEM_CODE,))
  94. def remove_invalid_handles(self) -> None:
  95. """
  96. Remove invalid handles from group.
  97. Invalid handles are: deleted entities, entities in a block layout
  98. """
  99. def handle_not_in_block_definition(handle: str) -> bool:
  100. wrap = self.dxffactory.wrap_handle # shortcut
  101. # owner block_record.layout is 0 if entity is in a block definition
  102. owner_handle = wrap(handle).dxf.owner
  103. return wrap(owner_handle).dxf.layout != 0
  104. db = self.entitydb # faster local var
  105. valid_handles = [handle for handle in self.handles() if handle in db]
  106. self.clear()
  107. # If one entity is in a block layout remove all entities, because they have to be on the same layout
  108. if len(valid_handles) and handle_not_in_block_definition(valid_handles[0]):
  109. self.AcDbGroup.extend(DXFTag(GROUP_ITEM_CODE, handle) for handle in valid_handles)
  110. def all_entities_on_same_layout(entities: Iterable['DXFEntity']):
  111. """
  112. Check if all entities are on the same layout (model space or any paper layout but not block).
  113. """
  114. owners = set(entity.dxf.owner for entity in entities)
  115. return len(owners) < 2 # 0 for no entities; 1 for all entities on the same layout
  116. class GroupManager(ObjectManager):
  117. def __init__(self, drawing: 'Drawing'):
  118. super().__init__(drawing, dict_name='ACAD_GROUP', object_type='GROUP')
  119. self._next_unnamed_number = 0
  120. def groups(self) -> Iterable[DXFGroup]:
  121. for name, group in self:
  122. yield group
  123. def next_name(self) -> str:
  124. name = self._next_name()
  125. while name in self:
  126. name = self._next_name()
  127. return name
  128. def _next_name(self) -> str:
  129. self._next_unnamed_number += 1
  130. return "*A{}".format(self._next_unnamed_number)
  131. def new(self, name: str = None, description: str = "", selectable: int = 1) -> DXFGroup:
  132. if name in self:
  133. raise DXFValueError("GROUP '{}' already exists.".format(name))
  134. if name is None:
  135. name = self.next_name()
  136. unnamed = 1
  137. else:
  138. unnamed = 0
  139. # The group name isn't stored in the group entity itself.
  140. dxfattribs = {
  141. 'description': description,
  142. 'unnamed': unnamed,
  143. 'selectable': selectable,
  144. }
  145. return cast(DXFGroup, self._new(name, dxfattribs))
  146. def delete(self, group: DXFGroup) -> None:
  147. """
  148. Delete GROUP by name or Group() object.
  149. """
  150. if isinstance(group, str): # delete group by name
  151. super(GroupManager, self).delete(group)
  152. else: # group should be a DXFEntity
  153. group_handle = group.dxf.handle
  154. for name, _group in self:
  155. if group_handle == _group.dxf.handle:
  156. super(GroupManager, self).delete(name)
  157. return
  158. raise DXFValueError("GROUP not in group table registered.")
  159. def cleanup(self) -> None:
  160. """
  161. Removes invalid handles in all groups and removes empty groups.
  162. """
  163. empty_groups = []
  164. for name, group in self:
  165. group.remove_invalid_handles()
  166. if not len(group): # remove empty group
  167. # do not delete groups while iterating over groups!
  168. empty_groups.append(name)
  169. # now delete empty groups
  170. for name in empty_groups:
  171. self.delete(name)