blocks.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. # Purpose: blocks section
  2. # Created: 14.03.2011
  3. # Copyright (c) 2011-2018, Manfred Moitzi
  4. # License: MIT License
  5. from typing import TYPE_CHECKING, Iterable, Union, Iterator, Sequence
  6. import logging
  7. from ezdxf.lldxf.const import DXFStructureError, DXFAttributeError, DXFBlockInUseError
  8. from ezdxf.lldxf import const
  9. from ezdxf.lldxf.extendedtags import get_xtags_linker
  10. logger = logging.getLogger('ezdxf')
  11. if TYPE_CHECKING:
  12. from ezdxf.eztypes import Drawing, BlockLayout, ExtendedTags, DXFFactoryType, EntityDB, TagWriter
  13. class BlocksSection:
  14. """
  15. Manages BLOCK definitions in a dict(). Since v0.8.5 ezdxf uses a lower case key. 'Test' == 'TEST', to behave
  16. like AutoCAD.
  17. """
  18. name = 'BLOCKS'
  19. def __init__(self, entities: Iterable['ExtendedTags'], drawing: 'Drawing'):
  20. # Mapping of BlockLayouts, for dict() order of blocks is random,
  21. # if turns out later, that blocks order is important: use an OrderedDict().
  22. self._block_layouts = dict()
  23. self.drawing = drawing
  24. if entities is not None:
  25. self._build(iter(entities))
  26. self._anonymous_block_counter = 0
  27. def __len__(self):
  28. return len(self._block_layouts)
  29. @staticmethod
  30. def key(entity: Union[str, 'BlockLayout']) -> str:
  31. if not isinstance(entity, str):
  32. entity = entity.name
  33. return entity.lower() # block key is lower case
  34. @property
  35. def entitydb(self) -> 'EntityDB':
  36. return self.drawing.entitydb
  37. @property
  38. def dxffactory(self) -> 'DXFFactoryType':
  39. return self.drawing.dxffactory
  40. def _build(self, entities: Iterator['ExtendedTags']) -> None:
  41. def build_block_layout(handles: Sequence[str]) -> 'BlockLayout':
  42. block = self.dxffactory.new_block_layout(
  43. block_handle=handles[0],
  44. endblk_handle=handles[-1],
  45. )
  46. for handle in handles[1:-1]:
  47. block.add_handle(handle)
  48. return block
  49. def link_entities() -> Iterable['ExtendedTags']:
  50. linked_tags = get_xtags_linker()
  51. for entity in entities:
  52. if not linked_tags(entity): # don't store linked entities (VERTEX, ATTRIB, SEQEND) in block layout
  53. yield entity
  54. section_head = next(entities)
  55. if section_head[0] != (0, 'SECTION') or section_head[1] != (2, 'BLOCKS'):
  56. raise DXFStructureError("Critical structure error in BLOCKS section.")
  57. handles = []
  58. for xtags in link_entities():
  59. handles.append(xtags.get_handle())
  60. if xtags.dxftype() == 'ENDBLK':
  61. block_layout = build_block_layout(handles)
  62. try:
  63. name = block_layout.name
  64. except DXFAttributeError:
  65. raise
  66. if block_layout.name in self:
  67. logger.warning(
  68. 'Warning! Multiple block definitions with name "{}", replacing previous definition'.format(
  69. block_layout.name))
  70. self.add(block_layout)
  71. handles = []
  72. def add(self, block_layout: 'BlockLayout') -> None:
  73. """
  74. Add or replace a block object.
  75. Args:
  76. block_layout: BlockLayout() object
  77. """
  78. self._block_layouts[self.key(block_layout.name)] = block_layout
  79. def __iter__(self) -> Iterable['BlockLayout']:
  80. return iter(self._block_layouts.values())
  81. def __contains__(self, name: str) -> bool:
  82. return self.key(name) in self._block_layouts
  83. def __getitem__(self, name: str) -> 'BlockLayout':
  84. return self._block_layouts[self.key(name)]
  85. def __delitem__(self, name: str) -> None:
  86. del self._block_layouts[self.key(name)]
  87. def get(self, name: str, default=None) -> 'BlockLayout':
  88. try:
  89. return self.__getitem__(name)
  90. except KeyError: # internal exception
  91. return default
  92. def new(self, name: str, base_point: Sequence[float] = (0, 0), dxfattribs: dict = None) -> 'BlockLayout':
  93. """
  94. Create a new named block.
  95. """
  96. dxfattribs = dxfattribs or {}
  97. dxfattribs['name'] = name
  98. dxfattribs['name2'] = name
  99. dxfattribs['base_point'] = base_point
  100. head = self.dxffactory.create_db_entry('BLOCK', dxfattribs)
  101. tail = self.dxffactory.create_db_entry('ENDBLK', {})
  102. block_layout = self.dxffactory.new_block_layout(head.dxf.handle, tail.dxf.handle)
  103. self.dxffactory.create_block_entry_in_block_records_table(block_layout)
  104. self.add(block_layout)
  105. return block_layout
  106. def new_anonymous_block(self, type_char: str = 'U', base_point: Sequence[float] = (0, 0)) -> 'BlockLayout':
  107. blockname = self.anonymous_blockname(type_char)
  108. block = self.new(blockname, base_point, {'flags': const.BLK_ANONYMOUS})
  109. return block
  110. def anonymous_blockname(self, type_char: str) -> str:
  111. """
  112. Create name for an anonymous block.
  113. Args:
  114. type_char: letter
  115. U = *U### anonymous blocks
  116. E = *E### anonymous non-uniformly scaled blocks
  117. X = *X### anonymous hatches
  118. D = *D### anonymous dimensions
  119. A = *A### anonymous groups
  120. T = *T### anonymous ACAD_TABLE content
  121. """
  122. while True:
  123. self._anonymous_block_counter += 1
  124. blockname = "*%s%d" % (type_char, self._anonymous_block_counter)
  125. if not self.__contains__(blockname):
  126. return blockname
  127. def rename_block(self, old_name: str, new_name: str) -> None:
  128. """
  129. Renames the block and the associated block record.
  130. """
  131. block_layout = self.get(old_name) # block key is lower case
  132. block_layout.name = new_name
  133. if self.drawing.dxfversion > 'AC1009':
  134. block_record = self.drawing.block_records.get(old_name)
  135. block_record.dxf.name = new_name
  136. self.__delitem__(old_name)
  137. self.add(block_layout) # add new dict entry
  138. def delete_block(self, name: str, safe: bool = True) -> None:
  139. """
  140. Delete block. If save is True, check if block is still referenced.
  141. Args:
  142. name: block name (case insensitive)
  143. safe: check if block is still referenced
  144. Raises:
  145. DXFKeyError() if block not exists
  146. DXFValueError() if block is still referenced, and save is True
  147. """
  148. if safe:
  149. block_refs = self.drawing.query("INSERT[name=='{}']i".format(name)) # ignore case
  150. if len(block_refs):
  151. raise DXFBlockInUseError(
  152. 'Block "{}" is still in use and can not deleted. (Hint: block name is case insensitive!)'.format(
  153. name))
  154. block_layout = self[name]
  155. block_layout.destroy()
  156. self.__delitem__(name)
  157. def delete_all_blocks(self, safe: bool = True) -> None:
  158. """
  159. Delete all blocks except layout blocks (model space or paper space).
  160. Args:
  161. safe: check if block is still referenced and ignore them if so
  162. """
  163. if safe:
  164. # block names are case insensitive
  165. references = set(entity.dxf.name.lower() for entity in self.drawing.query('INSERT'))
  166. def is_save(name: str) -> bool:
  167. return name.lower() not in references if safe else True
  168. # do not delete blocks defined for layouts
  169. if self.drawing.dxfversion > 'AC1009':
  170. layout_keys = set(layout.layout_key for layout in self.drawing.layouts)
  171. for block in list(self):
  172. name = block.name
  173. if block.block_record_handle not in layout_keys and is_save(name):
  174. # safety check is already done
  175. self.delete_block(name, safe=False)
  176. else:
  177. for block_name in list(self._block_layouts.keys()):
  178. if block_name not in ('$model_space', '$paper_space') and is_save(block_name):
  179. # safety check is already done
  180. self.delete_block(block_name, safe=False)
  181. def write(self, tagwriter: 'TagWriter') -> None:
  182. tagwriter.write_str(" 0\nSECTION\n 2\nBLOCKS\n")
  183. for block in self._block_layouts.values():
  184. block.write(tagwriter)
  185. tagwriter.write_tag2(0, "ENDSEC")
  186. def new_layout_block(self) -> 'BlockLayout':
  187. def block_name(_count):
  188. return "*Paper_Space%d" % _count
  189. count = 0
  190. while block_name(count) in self:
  191. count += 1
  192. block_layout = self.new(block_name(count))
  193. return block_layout