# Created: 11.03.2011 # Copyright (c) 2011-2018, Manfred Moitzi # License: MIT License from typing import TYPE_CHECKING, Any, Iterable, Mapping, List, cast, Tuple from ezdxf.lldxf.const import DXFStructureError, DXFAttributeError, DXFInvalidLayerName, DXFValueError, DXFXDataError from ezdxf.lldxf.validator import is_valid_layer_name from ezdxf.lldxf.tags import Tags from ezdxf.lldxf.extendedtags import ExtendedTags from ezdxf.tools import set_flag_state from ezdxf.math import OCS if TYPE_CHECKING: # import forward dependencies from ezdxf.eztypes import TagValue, IterableTags, DXFAttr, DXFDictionary from ezdxf.eztypes import Drawing, EntityDB, DXFFactoryType, GenericLayoutType ACAD_REACTORS = '{ACAD_REACTORS' ACAD_XDICTIONARY = '{ACAD_XDICTIONARY' class NotFoundException(Exception): pass class DXFNamespace: """ Provides the dxf namespace for GenericWrapper. """ __slots__ = ('_wrapper',) def __init__(self, wrapper: 'DXFEntity'): # DXFNamespace.__setattr__ can not set _wrapper super(DXFNamespace, self).__setattr__('_wrapper', wrapper) def __getattr__(self, attrib: str): """ Returns value of DXF attribute *attrib*. usage: value = DXFEntity.dxf.attrib """ return self._wrapper.get_dxf_attrib(attrib) def __setattr__(self, attrib: str, value) -> None: """ Set DXF attribute *attrib* to *value. usage: DXFEntity.dxf.attrib = value """ return self._wrapper.set_dxf_attrib(attrib, value) def __delattr__(self, attrib: str) -> None: """ Remove DXF attribute *attrib*. usage: del DXFEntity.dxf.attrib """ return self._wrapper.del_dxf_attrib(attrib) class DXFEntity: __slots__ = ('tags', 'dxf', 'drawing') TEMPLATE = None CLASS = None DXFATTRIBS = {} def __init__(self, tags: ExtendedTags, drawing: 'Drawing' = None): self.tags = tags # DXF tags stored as DXFTag (and inherited) in an ExtendedTags container self.dxf = DXFNamespace(self) # type: Any # dynamic DXF attribute dispatching, e.g. DXFEntity.dxf.layer self.drawing = drawing def __str__(self) -> str: """ Returns a simple string representation. """ return "{}(#{})".format(self.dxftype(), self.dxf.handle) def __repr__(self) -> str: """ Returns a simple string representation including the class. """ return str(self.__class__) + " " + str(self) @property def dxffactory(self) -> 'DXFFactoryType': return self.drawing.dxffactory @property def dxfversion(self) -> str: return self.drawing.dxfversion @property def entitydb(self) -> 'EntityDB': return self.drawing.entitydb @classmethod def new(cls, handle: str, dxfattribs: dict = None, drawing: 'Drawing' = None) -> 'DXFEntity': if cls.TEMPLATE is None: raise NotImplementedError("new() for type %s not implemented." % cls.__name__) entity = cls(cls.TEMPLATE.clone(), drawing) entity.dxf.handle = handle if dxfattribs is not None: if 'layer' in dxfattribs: layer_name = dxfattribs['layer'] if not is_valid_layer_name(layer_name): raise DXFInvalidLayerName("Invalid layer name '{}'".format(layer_name)) entity.update_dxf_attribs(dxfattribs) entity.post_new_hook() return entity def post_new_hook(self) -> None: """ Called after entity creation. """ pass def _new_entity(self, type_: str, dxfattribs: dict) -> 'DXFEntity': """ Create new entity with same layout settings as *self*. Used by INSERT & POLYLINE to create appended DXF entities, don't use it to create new standalone entities. """ entity = self.dxffactory.create_db_entry(type_, dxfattribs) self.dxffactory.copy_layout(self, entity) return entity def __copy__(self) -> 'DXFEntity': """ Deep copy of DXFEntity with new handle and duplicated linked entities (VERTEX, ATTRIB, SEQEND). The new entity is not included in any layout space, so the owner tag is set to '0' for undefined owner/layout. Use Layout.add_entity(new_entity) to add the duplicated entity to a layout, layout can be the model space, a paper space layout or a block layout. It is not called __deepcopy__, because this is not a deep copy in the meaning of Python, because handle, link and owner is changed. """ new_tags = self.entitydb.duplicate_tags(self.tags) entity = self.dxffactory.wrap_entity(new_tags) if self.supports_dxf_attrib('owner'): # R2000+ entity.dxf.owner = '0' # reset ownership/layout return entity copy = __copy__ def linked_entities(self) -> Iterable['DXFEntity']: """ Iterate over all linked entities, only POLYLINE and INSERT has linked entities (VERTEX, ATTRIB, SEQEND) Yields: DXFEntity() objects """ link = self.tags.link # type: str wrap = self.dxffactory.wrap_handle while link is not None: entity = wrap(link) yield entity link = entity.tags.link def copy_to_layout(self, layout: 'GenericLayoutType') -> 'DXFEntity': """ Copy entity to another layout. Args: layout: any layout (model space, paper space, block) Returns: new created entity as DXFEntity() object """ new_entity = self.copy() layout.add_entity(new_entity) return new_entity def move_to_layout(self, layout: 'GenericLayoutType', source: 'GenericLayoutType' = None) -> None: """ Move entity from model space or a paper space layout to another layout. For block layout as source, the block layout has to be specified. Args: layout: any layout (model space, paper space, block) source: provide source layout, faster for DXF R12, if entity is in a block layout """ if source is None: source = self.get_layout() if source is None: raise DXFValueError('Source layout for entity not found.') source.move_to_layout(self, layout) def dxftype(self) -> str: return self.tags.noclass[0].value def _get_dxfattr_definition(self, key: str) -> 'DXFAttr': try: return self.DXFATTRIBS[key] except KeyError: raise DXFAttributeError(key) def get_dxf_attrib(self, key: str, default: Any = DXFValueError) -> 'TagValue': dxfattr = self._get_dxfattr_definition(key) return dxfattr.get_attrib(self, key, default) def set_dxf_attrib(self, key: str, value: 'TagValue') -> None: dxfattr = self._get_dxfattr_definition(key) dxfattr.set_attrib(self, key, value) def del_dxf_attrib(self, key: str) -> None: dxfattr = self._get_dxfattr_definition(key) dxfattr.del_attrib(self) def supports_dxf_attrib(self, key: str) -> bool: """ Returns True if DXF attribute key is supported else False. Does not grant that attribute key really exists. """ dxfattr = self.DXFATTRIBS.get(key, None) if dxfattr is None: return False if dxfattr.dxfversion is None: return True return self.drawing.dxfversion >= dxfattr.dxfversion def dxf_attrib_exists(self, key: str) -> bool: """ Returns True if DXF attrib key really exists else False. Raises AttributeError if key isn't supported. """ # attributes with default values don't raise an exception! return self.get_dxf_attrib(key, default=None) is not None def valid_dxf_attrib_names(self) -> Iterable[str]: """ Returns a list of supported DXF attribute names. """ return [key for key, attrib in self.DXFATTRIBS.items() if attrib.dxfversion is None or (attrib.dxfversion <= self.drawing.dxfversion)] def get_dxf_default_value(self, key: str) -> 'TagValue': """ Returns the default value as defined in the DXF standard. """ return self._get_dxfattr_definition(key).default def has_dxf_default_value(self, key: str) -> bool: """ Returns True if the DXF attribute key has a DXF standard default value. """ return self._get_dxfattr_definition(key).default is not None def dxfattribs(self) -> dict: """ Clones defined and existing DXF attributes as dict. """ dxfattribs = {} for key in self.DXFATTRIBS.keys(): value = self.get_dxf_attrib(key, default=None) if value is not None: dxfattribs[key] = value return dxfattribs clone_dxf_attribs = dxfattribs def update_dxf_attribs(self, dxfattribs: Mapping) -> None: for key, value in dxfattribs.items(): self.set_dxf_attrib(key, value) def set_flag_state(self, flag: int, state: bool = True, name: str = 'flags') -> None: flags = self.get_dxf_attrib(name, 0) self.set_dxf_attrib(name, set_flag_state(flags, flag, state=state)) def get_flag_state(self, flag: int, name: str = 'flags') -> bool: return bool(self.get_dxf_attrib(name, 0) & flag) def ocs(self) -> OCS: extrusion = self.get_dxf_attrib('extrusion', default=(0, 0, 1)) return OCS(extrusion) def destroy(self) -> None: if self.has_extension_dict(): xdict = self.get_extension_dict() self.drawing.objects.delete_entity(xdict) def has_app_data(self, appid: str) -> bool: return self.tags.has_app_data(appid) def get_app_data(self, appid: str) -> Tags: return self.tags.get_app_data_content(appid) def set_app_data(self, appid: str, app_data_tags: 'IterableTags') -> None: if self.tags.has_app_data(appid): self.tags.set_app_data_content(appid, app_data_tags) else: self.tags.new_app_data(appid, app_data_tags) def has_xdata(self, appid: str) -> bool: return self.tags.has_xdata(appid) def get_xdata(self, appid: str) -> Tags: return Tags(self.tags.get_xdata(appid)[1:]) # without app id tag def set_xdata(self, appid: str, xdata_tags: 'IterableTags') -> None: if self.tags.has_xdata(appid): self.tags.set_xdata(appid, xdata_tags) else: self.tags.new_xdata(appid, xdata_tags) def has_xdata_list(self, appid: str, name: str) -> bool: """ Returns if list `name` from XDATA `appid` exists. Args: appid: APPID name: list name """ try: self.get_xdata_list(appid, name) except DXFValueError: return False else: return True def get_xdata_list(self, appid: str, name: str) -> List[Tuple]: """ Get list `name` from XDATA `appid`. Args: appid: APPID name: list name Returns: list of DXFTags including list name and curly braces '{' '}' tags Raises: DXFValueError: XDATA `appid` do not exist or list `name` do not exist """ xdata = self.get_xdata(appid) try: return get_named_list_from_xdata(name, xdata) except NotFoundException: raise DXFValueError('No data list "{}" not found for APPID "{}"'.format(name, appid)) def set_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None: """ Create new list `name` of XDATA `appid` with `xdata_tags` and replaces list `name` if already exists. Args: appid: APPID name: list name xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will be added """ if not self.tags.has_xdata(appid): self.tags.new_xdata(appid, xdata_list(name, xdata_tags)) else: self.replace_xdata_list(appid, name, xdata_tags) def discard_xdata_list(self, appid: str, name: str) -> None: """ Deletes list `name` from XDATA `appid`. Ignores silently if XDATA `appid` or list `name` not exists. Args: appid: APPID name: list name """ try: xdata = self.get_xdata(appid) except DXFValueError: pass else: try: tags = remove_named_list_from_xdata(name, xdata) except NotFoundException: pass else: self.set_xdata(appid, tags) def replace_xdata_list(self, appid: str, name: str, xdata_tags: 'IterableTags') -> None: """ Replaces list `name` of existing XDATA `appid` with `xdata_tags`. Appends new list if list `name` do not exist, but raises `DXFValueError` if XDATA `appid` do not exist. Low level interface, if not sure use `set_xdata_list()` instead. Args: appid: APPID name: list name xdata_tags: list content as DXFTags or (code, value) tuples, list name and curly braces '{' '}' tags will be added Raises: DXFValueError: XDATA `appid` do not exist """ xdata = self.get_xdata(appid) try: tags = remove_named_list_from_xdata(name, xdata) except NotFoundException: tags = xdata tags.extend(xdata_list(name, xdata_tags)) self.tags.set_xdata(appid, tags) def has_reactors(self) -> bool: return self.has_app_data(ACAD_REACTORS) def get_reactors(self) -> List[str]: reactor_tags = self.get_app_data(ACAD_REACTORS) return [tag.value for tag in reactor_tags] def set_reactors(self, reactor_handles: Iterable[str]) -> None: reactor_tags = [(330, handle) for handle in reactor_handles] self.set_app_data(ACAD_REACTORS, reactor_tags) def append_reactor_handle(self, handle: str) -> None: reactors = set(self.get_reactors()) reactors.add(handle) self.set_reactors(sorted(reactors, key=lambda x: int(x, base=16))) def remove_reactor_handle(self, handle: str) -> None: reactors = set(self.get_reactors()) reactors.discard(handle) self.set_reactors(reactors) def has_extension_dict(self) -> bool: return self.has_app_data(ACAD_XDICTIONARY) def get_extension_dict(self) -> 'DXFDictionary': """ Get associated extension dictionary as DXFDictionary() object. """ app_data = self.get_app_data(ACAD_XDICTIONARY) if len(app_data) == 0 or app_data[0].code != 360: raise DXFStructureError("XDICTIONARY error in entity: " + str(self)) # are more than one XDICTIONARY possible? xdict_handle = app_data[0].value return cast('DXFDictionary', self.dxffactory.wrap_handle(xdict_handle)) def new_extension_dict(self) -> 'DXFDictionary': """ Creates and assigns a new extensions dictionary. Link to an existing extension dictionary will be lost. """ xdict = self.drawing.objects.add_dictionary(owner=self.dxf.handle) self.set_app_data(ACAD_XDICTIONARY, [(360, xdict.dxf.handle)]) return xdict def get_layout(self) -> 'GenericLayoutType': return self.dxffactory.get_layout_for_entity(self) def audit(self, auditor): """ Audit entity for errors. Args: auditor: Audit() object """ pass def has_embedded_objects(self) -> bool: return any(tags.has_embedded_objects() for tags in self.tags.subclasses) OPEN_LIST = (1002, '{') CLOSE_LIST = (1002, '}') def xdata_list(name: str, xdata_tags: 'IterableTags') -> List[Tuple]: tags = [] if name: tags.append((1000, name)) tags.append(OPEN_LIST) tags.extend(xdata_tags) tags.append(CLOSE_LIST) return tags def remove_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]: start, end = get_start_and_end_of_named_list_in_xdata(name, tags) del tags[start: end] return tags def get_named_list_from_xdata(name: str, tags: Tags) -> List[Tuple]: start, end = get_start_and_end_of_named_list_in_xdata(name, tags) return tags[start: end] def get_start_and_end_of_named_list_in_xdata(name: str, tags: List[Tuple]) -> Tuple[int, int]: start = None end = None level = 0 for index in range(len(tags)): tag = tags[index] if start is None and tag == (1000, name): next_tag = tags[index + 1] if next_tag == OPEN_LIST: start = index continue if start is not None: if tag == OPEN_LIST: level += 1 elif tag == CLOSE_LIST: level -= 1 if level == 0: end = index break if start is None: raise NotFoundException if end is None: raise DXFXDataError('Invalid XDATA structure: missing (1002, "}").') return start, end + 1