1# Copyright (c) 2020, Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Iterable, Callable, List, Optional 4 5from ezdxf.entities import factory, DXFGraphic, SeqEnd, DXFEntity 6from ezdxf.lldxf import const 7 8if TYPE_CHECKING: 9 from ezdxf.eztypes import DXFEntity, EntityDB, Drawing 10 11__all__ = ['entity_linker', 'LinkedEntities'] 12 13 14class LinkedEntities(DXFGraphic): 15 """ Super class for common features of the INSERT and the POLYLINE entity. 16 Both have linked entities like the VERTEX or ATTRIB entity and a 17 SEQEND entity. 18 19 """ 20 21 def __init__(self): 22 super().__init__() 23 self._sub_entities: List[DXFGraphic] = [] 24 self.seqend: Optional['SeqEnd'] = None 25 26 def _copy_data(self, entity: 'LinkedEntities') -> None: 27 """ Copy all sub-entities ands SEQEND. (internal API) """ 28 entity._sub_entities = [e.copy() for e in self._sub_entities] 29 if self.seqend: 30 entity.seqend = self.seqend.copy() 31 32 def link_entity(self, entity: 'DXFGraphic') -> None: 33 """ Link VERTEX ot ATTRIB entities. """ 34 entity.set_owner(self.dxf.owner, self.dxf.paperspace) 35 self._sub_entities.append(entity) 36 37 def link_seqend(self, seqend: 'DXFEntity') -> None: 38 """ Link SEQEND entity. (internal API) """ 39 seqend.dxf.owner = self.dxf.owner 40 self.seqend = seqend 41 42 def post_bind_hook(self): 43 """ Create always a SEQEND entity. """ 44 if self.seqend is None: 45 self.new_seqend() 46 47 def all_sub_entities(self) -> Iterable['DXFEntity']: 48 """ Yields all sub-entities ans SEQEND. (internal API) """ 49 yield from self._sub_entities 50 if self.seqend: 51 yield self.seqend 52 53 def process_sub_entities(self, func: Callable[['DXFEntity'], None]): 54 """ Call `func` for all sub-entities and SEQEND. (internal API) 55 """ 56 for entity in self.all_sub_entities(): 57 if entity.is_alive: 58 func(entity) 59 60 def add_sub_entities_to_entitydb(self, db: 'EntityDB') -> None: 61 """ Add sub-entities (VERTEX, ATTRIB, SEQEND) to entity database `db`, 62 called from EntityDB. (internal API) 63 """ 64 65 def add(entity: 'DXFEntity'): 66 entity.doc = self.doc # grant same document 67 db.add(entity) 68 69 if not self.seqend or not self.seqend.is_alive: 70 self.new_seqend() 71 self.process_sub_entities(add) 72 73 def new_seqend(self): 74 """ Create and bind new SEQEND. (internal API) """ 75 attribs = {'layer': self.dxf.layer} 76 if self.doc: 77 seqend = factory.create_db_entry('SEQEND', attribs, self.doc) 78 else: 79 seqend = factory.new('SEQEND', attribs) 80 self.link_seqend(seqend) 81 82 def set_owner(self, owner: str, paperspace: int = 0): 83 """ Set owner of all sub-entities and SEQEND. (internal API) """ 84 # Loading from file: POLYLINE/INSERT will be added to layout before 85 # vertices/attrib entities are linked, so set_owner() of POLYLINE does 86 # not set owner of vertices at loading time. 87 super().set_owner(owner, paperspace) 88 89 def set_owner(entity): 90 if isinstance(entity, DXFGraphic): 91 entity.set_owner(owner, paperspace) 92 else: # SEQEND 93 entity.dxf.owner = owner 94 95 self.process_sub_entities(set_owner) 96 97 def remove_dependencies(self, other: 'Drawing' = None): 98 """ Remove all dependencies from current document to bind entity to 99 `other` document. (internal API) 100 """ 101 self.process_sub_entities(lambda e: e.remove_dependencies(other)) 102 super().remove_dependencies(other) 103 104 def destroy(self) -> None: 105 """ Destroy all data and references. """ 106 if not self.is_alive: 107 return 108 109 self.process_sub_entities(func=lambda e: e.destroy()) 110 del self._sub_entities 111 del self.seqend 112 super().destroy() 113 114 115# This attached MTEXT is a limited MTEXT entity, starting with (0, 'MTEXT') 116# therefore separated entity, but without the base class: no handle, no owner 117# nor AppData, and a limited AcDbEntity subclass. 118# Detect attached entities (more than MTEXT?) by required but missing handle and 119# owner tags use DXFEntity.link_entity() for linking to preceding entity, 120# INSERT & POLYLINE do not have attached entities, so reuse of API for 121# ATTRIB & ATTDEF should be safe. 122 123LINKED_ENTITIES = { 124 'INSERT': 'ATTRIB', 125 'POLYLINE': 'VERTEX' 126} 127 128 129def entity_linker() -> Callable[[DXFEntity], bool]: 130 """ Create an DXF entities linker. """ 131 main_entity: Optional[DXFEntity] = None 132 prev: Optional[DXFEntity] = None 133 expected_dxftype = "" 134 135 def entity_linker_(entity: DXFEntity) -> bool: 136 """ Collect and link entities which are linked to a parent entity: 137 138 - VERTEX -> POLYLINE 139 - ATTRIB -> INSERT 140 - attached MTEXT entity 141 142 Args: 143 entity: examined DXF entity 144 145 Returns: 146 True if `entity` is linked to a parent entity 147 148 """ 149 nonlocal main_entity, expected_dxftype, prev 150 dxftype: str = entity.dxftype() 151 # INSERT & POLYLINE are not linked entities, they are stored in the 152 # entity space. 153 are_linked_entities = False 154 if main_entity is not None: 155 # VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in 156 # the entity space. 157 are_linked_entities = True 158 if dxftype == 'SEQEND': 159 main_entity.link_seqend(entity) 160 # Marks also the end of the main entity 161 main_entity = None 162 # Check for valid DXF structure: 163 # VERTEX follows POLYLINE 164 # ATTRIB follows INSERT 165 elif dxftype == expected_dxftype: 166 main_entity.link_entity(entity) 167 else: 168 raise const.DXFStructureError( 169 f"Expected DXF entity {dxftype} or SEQEND" 170 ) 171 172 elif dxftype in LINKED_ENTITIES: 173 # Only INSERT and POLYLINE have a linked entities structure: 174 if dxftype == 'INSERT' and not entity.dxf.get('attribs_follow', 0): 175 # INSERT must not have following ATTRIBS, ATTRIB can be a stand 176 # alone entity: 177 # 178 # INSERT with no ATTRIBS, attribs_follow == 0 179 # ATTRIB as stand alone entity 180 # .... 181 # INSERT with ATTRIBS, attribs_follow == 1 182 # ATTRIB as connected entity 183 # SEQEND 184 # 185 # Therefore a ATTRIB following an INSERT doesn't mean that 186 # these entities are linked. 187 pass 188 else: 189 main_entity = entity 190 expected_dxftype = LINKED_ENTITIES[dxftype] 191 192 # Attached MTEXT entity: 193 elif (dxftype == 'MTEXT') and (entity.dxf.handle is None): 194 if prev: 195 prev.link_entity(entity) 196 are_linked_entities = True 197 else: 198 raise const.DXFStructureError( 199 "Found attached MTEXT entity without a preceding entity." 200 ) 201 prev = entity 202 return are_linked_entities 203 204 return entity_linker_ 205