class DimStyle(DXFEntity): __slots__ = () TEMPLATE = ExtendedTags.from_text(_DIMSTYLETEMPLATE) DXFATTRIBS = DXFAttributes( DefSubclass( None, { 'handle': DXFAttr(105), 'name': DXFAttr(2), 'flags': DXFAttr(70), 'dimpost': DXFAttr(3), 'dimapost': DXFAttr(4), 'dimblk': DXFAttr(5), 'dimblk1': DXFAttr(6), 'dimblk2': DXFAttr(7), 'dimscale': DXFAttr(40), 'dimasz': DXFAttr(41), 'dimexo': DXFAttr(42), 'dimdli': DXFAttr(43), 'dimexe': DXFAttr(44), 'dimrnd': DXFAttr(45), 'dimdle': DXFAttr(46), 'dimtp': DXFAttr(47), 'dimtm': DXFAttr(48), 'dimtxt': DXFAttr(140), 'dimcen': DXFAttr(141), 'dimtsz': DXFAttr(142), 'dimaltf': DXFAttr(143, default=1.), 'dimlfac': DXFAttr(144), 'dimtvp': DXFAttr(145), 'dimtfac': DXFAttr(146), 'dimgap': DXFAttr(147), 'dimtol': DXFAttr(71), 'dimlim': DXFAttr(72), 'dimtih': DXFAttr(73), 'dimtoh': DXFAttr(74), 'dimse1': DXFAttr(75), 'dimse2': DXFAttr(76), 'dimtad': DXFAttr(77), # 0 center, 1 above, 4 below dimline 'dimzin': DXFAttr(78), 'dimalt': DXFAttr(170), 'dimaltd': DXFAttr(171), 'dimtofl': DXFAttr(172), 'dimsah': DXFAttr(173), 'dimtix': DXFAttr(174), 'dimsoxd': DXFAttr(175), 'dimclrd': DXFAttr(176), 'dimclre': DXFAttr(177), 'dimclrt': DXFAttr(178), })) CODE_TO_DXF_ATTRIB = dict(DXFATTRIBS.build_group_code_items(dim_filter)) def dim_attribs(self) -> Iterable[Tuple[str, DXFAttr]]: return ((name, attrib) for name, attrib in self.DXFATTRIBS.items() if name.startswith('dim')) def print_dim_attribs(self) -> None: for name, attrib in self.dim_attribs(): code = attrib.code value = self.get_dxf_attrib(name, None) if value is not None: print("{name} ({code}) = {value}".format(name=name, value=value, code=code)) def copy_to_header(self, dwg): attribs = self.dxfattribs() header = dwg.header header['$DIMSTYLE'] = self.dxf.name for name, value in attribs.items(): if name.startswith('dim'): header_var = '$' + name.upper() try: header[header_var] = value except DXFKeyError: logger.debug( 'Unsupported header variable: {}.'.format(header_var)) def set_arrows(self, blk: str = '', blk1: str = '', blk2: str = '') -> None: """ Set arrows by block names or AutoCAD standard arrow names, set dimtsz = 0 which disables tick. Args: blk: block/arrow name for both arrows, if dimsah == 0 blk1: block/arrow name for first arrow, if dimsah == 1 blk2: block/arrow name for second arrow, if dimsah == 1 """ self.set_dxf_attrib('dimblk', blk) self.set_dxf_attrib('dimblk1', blk1) self.set_dxf_attrib('dimblk2', blk2) self.set_dxf_attrib('dimtsz', 0) # use blocks # only existing BLOCK definitions allowed if self.drawing: blocks = self.drawing.blocks for b in (blk, blk1, blk2): if ARROWS.is_acad_arrow(b): # not real blocks continue if b and b not in blocks: raise DXFValueError( 'BLOCK "{}" does not exist.'.format(blk)) def set_tick(self, size: float = 1) -> None: """ Use oblique stroke as tick, disables arrows. Args: size: arrow size in daring units """ self.set_dxf_attrib('dimtsz', size) def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None: """ Set measurement text alignment, `halign` defines the horizontal alignment (requires DXFR2000+), `valign` defines the vertical alignment, `above1` and `above2` means above extension line 1 or 2 and aligned with extension line. Args: halign: `left`, `right`, `center`, `above1`, `above2`, requires DXF R2000+ valign: `above`, `center`, `below` vshift: vertical text shift, if `valign` is `center`; >0 shift upward, <0 shift downwards """ if valign: valign = valign.lower() self.set_dxf_attrib('dimtad', DIMTAD[valign]) if valign == 'center' and vshift is not None: self.set_dxf_attrib('dimtvp', vshift) try: if halign: self.set_dxf_attrib('dimjust', DIMJUST[halign.lower()]) except const.DXFAttributeError: raise DXFVersionError('DIMJUST require DXF R2000+') def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None, leading_zeros: bool = True, trailing_zeros: bool = True): """ Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places. Args: prefix: Dimension text prefix text as string postfix: Dimension text postfix text as string rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to 0.25, all distances round to the nearest 0.25 unit. If you set DIMRND to 1.0, all distances round to the nearest integer. dec: Sets the number of decimal places displayed for the primary units of a dimension. requires DXF R2000+ sep: "." or "," as decimal separator requires DXF R2000+ leading_zeros: suppress leading zeros for decimal dimensions if False trailing_zeros: suppress trailing zeros for decimal dimensions if False """ if prefix or postfix: self.dxf.dimpost = prefix + '<>' + postfix if rnd is not None: self.dxf.dimrnd = rnd # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimzin = 0 if leading_zeros is False: dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimzin = dimzin try: if dec is not None: self.dxf.dimdec = dec if sep is not None: self.dxf.dimdsep = ord(sep) except const.DXFAttributeError: raise DXFVersionError('DIMDSEP and DIMDEC require DXF R2000+') def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None, extension: float = None, disable1: bool = None, disable2: bool = None): """ Set dimension line properties Args: color: color index linetype: linetype as string, requires DXF R2007+ lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm, requires DXF R2000+ extension: extension length disable1: True to suppress first part of dimension line, requires DXF R2000+ disable2: True to suppress second part of dimension line, requires DXF R2000+ """ if color is not None: self.dxf.dimclrd = color if extension is not None: self.dxf.dimdle = extension try: if lineweight is not None: self.dxf.dimlwd = lineweight if disable1 is not None: self.dxf.dimsd1 = disable1 if disable2 is not None: self.dxf.dimsd2 = disable2 except const.DXFAttributeError: raise DXFVersionError( 'DIMLWD, DIMSD1 and DIMSD2 requires DXF R2000+') try: if linetype is not None: self.dxf.dimltype = linetype except const.DXFAttributeError: raise DXFVersionError('DIMLTYPE requires DXF R2007+') def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None, offset: float = None, fixed_length: float = None): """ Set common extension line attributes. Args: color: color index lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm extension: extension length above dimension line offset: offset from measurement point fixed_length: set fixed length extension line, length below the dimension line """ if color is not None: self.dxf.dimclre = color if extension is not None: self.dxf.dimexe = extension if offset is not None: self.dxf.dimexo = offset try: if lineweight is not None: self.dxf.dimlwe = lineweight except const.DXFAttributeError: raise DXFVersionError('DIMLWE requires DXF R2000+') try: if fixed_length is not None: self.dxf.dimfxlon = 1 self.dxf.dimfxl = fixed_length except const.DXFAttributeError: raise DXFVersionError('DIMFXL requires DXF R2007+') def set_extline1(self, linetype: str = None, disable=False): """ Set extension line 1 attributes. Args: linetype: linetype for extension line 1, requires DXF R2007+ disable: disable extension line 1 if True """ if disable: self.dxf.dimse1 = 1 try: if linetype is not None: self.dxf.dimltex1 = linetype except const.DXFAttributeError: raise DXFVersionError('DIMLTEX1 requires DXF R2007+') def set_extline2(self, linetype: str = None, disable=False): """ Set extension line 2 attributes. Args: linetype: linetype for extension line 2, requires DXF R2007+ disable: disable extension line 2 if True """ if disable: self.dxf.dimse2 = 1 try: if linetype is not None: self.dxf.dimltex2 = linetype except const.DXFAttributeError: raise DXFVersionError('DIMLTEX2 requires DXF R2007+') def set_tolerance(self, upper: float, lower: float = None, hfactor: float = 1.0, align: str = None, dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None: """ Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and trailing zero suppression. Args: upper: upper tolerance value lower: lower tolerance value, if None same as upper hfactor: tolerance text height factor in relation to the dimension text height align: tolerance text alignment "TOP", "MIDDLE", "BOTTOM", required DXF R2000+ dec: Sets the number of decimal places displayed, required DXF R2000+ leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+ trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+ """ # exclusive tolerances self.dxf.dimtol = 1 self.dxf.dimlim = 0 self.dxf.dimtp = float(upper) if lower is not None: self.dxf.dimtm = float(lower) else: self.dxf.dimtm = float(upper) if hfactor is not None: self.dxf.dimtfac = float(hfactor) if self.dxfversion > 'AC1009': # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimtzin = 0 if leading_zeros is False: dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimtzin = dimtzin if align is not None: self.dxf.dimtolj = const.MTEXT_INLINE_ALIGN[align.upper()] if dec is not None: self.dxf.dimtdec = int(dec) def set_limits(self, upper: float, lower: float, hfactor: float = 1.0, dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None: """ Set limits text format, upper and lower limit values, text height factor, number of decimal places or leading and trailing zero suppression. Args: upper: upper limit value added to measurement value lower: lower lower value subtracted from measurement value hfactor: limit text height factor in relation to the dimension text height dec: Sets the number of decimal places displayed, required DXF R2000+ leading_zeros: suppress leading zeros for decimal dimensions if False, required DXF R2000+ trailing_zeros: suppress trailing zeros for decimal dimensions if False, required DXF R2000+ """ # exclusive limits self.dxf.dimlim = 1 self.dxf.dimtol = 0 self.dxf.dimtp = float(upper) self.dxf.dimtm = float(lower) self.dxf.dimtfac = float(hfactor) if self.dxfversion > 'AC1009': # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimtzin = 0 if leading_zeros is False: dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimtzin = dimtzin self.dxf.dimtolj = 0 # set bottom as default if dec is not None: self.dxf.dimtdec = int(dec)
class DXFGraphic(DXFEntity): """ Common base class for all graphic entities, a subclass of :class:`~ezdxf.entities.dxfentity.DXFEntity`. This entities resides in entity spaces like modelspace, any paperspace or blocks. """ DXFTYPE = 'DXFGFX' DEFAULT_ATTRIBS = {'layer': '0'} DXFATTRIBS = DXFAttributes(base_class, acdb_entity) # DXF attribute definitions def load_dxf_attribs(self, processor: SubclassProcessor = None) -> 'DXFNamespace': """ Adds subclass processing for 'AcDbEntity', requires previous base class processing by parent class. (internal API) """ dxf = super().load_dxf_attribs(processor) if processor is None: return dxf tags = processor.load_dxfattribs_into_namespace(dxf, acdb_entity) if len(tags) and not processor.r12: processor.log_unprocessed_tags(tags, subclass=acdb_entity.name) return dxf def post_new_hook(self): """ Post processing and integrity validation after entity creation (internal API) """ ns = self.dxf if not is_valid_layer_name(ns.layer): raise DXFInvalidLayerName(ns.layer) if ns.hasattr('linetype'): if ns.linetype not in self.doc.linetypes: raise DXFInvalidLineType('Linetype "{}" not defined.'.format(ns.linetype)) @property def rgb(self) -> Optional[Tuple[int, int, int]]: """ Returns RGB true color as (r, g, b) tuple or None if true_color is not set. """ if self.dxf.hasattr('true_color'): return int2rgb(self.dxf.get('true_color')) else: return None @rgb.setter def rgb(self, rgb: Tuple[int, int, int]) -> None: """ Set RGB true color as (r, g , b) tuple e.g. (12, 34, 56). """ self.dxf.set('true_color', rgb2int(rgb)) @property def transparency(self) -> float: """ Get transparency as float value between 0 and 1, 0 is opaque and 1 is 100% transparent (invisible). """ if self.dxf.hasattr('transparency'): return transparency2float(self.dxf.get('transparency')) else: return 0. @transparency.setter def transparency(self, transparency: float) -> None: """ Set transparency as float value between 0 and 1, 0 is opaque and 1 is 100% transparent (invisible). """ self.dxf.set('transparency', float2transparency(transparency)) def ocs(self) -> Optional[OCS]: """ Returns object coordinate system (:ref:`ocs`) for 2D entities like :class:`Text` or :class:`Circle`, returns ``None`` for entities without OCS support. """ # extrusion is only defined for 2D entities like Text, Circle, ... if self.dxf.is_supported('extrusion'): extrusion = self.dxf.get('extrusion', default=(0, 0, 1)) return OCS(extrusion) else: return None def set_owner(self, owner: str, paperspace: int = 0) -> None: """ Set owner attribute and paperspace flag. (internal API)""" self.dxf.owner = owner if paperspace: self.dxf.paperspace = paperspace else: self.dxf.discard('paperspace') for e in self.linked_entities(): # type: DXFGraphic e.set_owner(owner, paperspace) def linked_entities(self) -> Iterable['DXFEntity']: """ Yield linked entities: VERTEX or ATTRIB, different handling than attached entities. (internal API)""" return [] def attached_entities(self) -> Iterable['DXFEntity']: """ Yield attached entities: MTEXT, different handling than linked entities. (internal API)""" return [] def link_entity(self, entity: 'DXFEntity') -> None: """ Store linked or attached entities. Same API for both types of appended data, because entities with linked entities (POLYLINE, INSERT) have no attached entities and vice versa. (internal API) """ pass @property def zorder(self): """ Inverted priority order (lowest value first) """ return -self.priority @zorder.setter def zorder(self, value): self.priority = -value def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. (internal API)""" # base class (handle, appid, reactors, xdict, owner) export is done by parent class self.export_acdb_entity(tagwriter) # xdata and embedded objects export is also done by parent def export_acdb_entity(self, tagwriter: 'TagWriter'): """ Export subclass 'AcDbEntity' as DXF tags. (internal API)""" # Full control over tag order and YES, sometimes order matters dxfversion = tagwriter.dxfversion if dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name) self.dxf.export_dxf_attribs(tagwriter, [ 'paperspace', 'layer', 'linetype', 'material_handle', 'color', 'lineweight', 'ltscale', 'true_color', 'color_name', 'transparency', 'plotstyle_enum', 'plotstyle_handle', 'shadow_mode', 'visualstyle_handle', ]) def get_layout(self) -> Optional['BaseLayout']: """ Returns the owner layout or returns ``None`` if entity is not assigned to any layout. """ if self.dxf.owner is None: # unlinked entity return None try: return self.doc.layouts.get_layout_by_key(self.dxf.owner) except DXFKeyError: pass try: return self.doc.blocks.get_block_layout_by_handle(self.dxf.owner) except DXFTableEntryError: return None def move_to_layout(self, layout: 'BaseLayout', source: 'BaseLayout' = 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. Moving between different DXF drawings is not supported. Args: layout: any layout (model space, paper space, block) source: provide source layout, faster for DXF R12, if entity is in a block layout Raises: DXFStructureError: for moving between different DXF drawings """ 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 copy_to_layout(self, layout: 'BaseLayout') -> 'DXFEntity': """ Copy entity to another `layout`, returns new created entity as :class:`DXFEntity` object. Copying between different DXF drawings not supported. Args: layout: any layout (model space, paper space, block) Raises: DXFStructureError: for copying between different DXF drawings """ if self.doc != layout.doc: raise DXFStructureError('Copying between different DXF drawings not supported.') new_entity = self.copy() self.entitydb.add(new_entity) layout.add_entity(new_entity) return new_entity def audit(self, auditor: 'Auditor') -> None: """ Validity check. (internal API) """ super().audit(auditor) auditor.check_for_valid_layer_name(self) auditor.check_if_linetype_exists(self) auditor.check_for_valid_color_index(self) auditor.check_pointer_target_exist(self, zero_pointer_valid=False) def _ucs_and_ocs_transformation(self, ucs: UCS, vector_names: Sequence, angle_names: Sequence = None) -> None: """ Transforms entity for given `ucs` to the parent coordinate system (most likely the WCS). Transforms the entity vectors and angles attributes from `ucs` to the parent coordinate system. Takes established OCS by the extrusion vector :attr:`dxf.extrusion` into account. """ extrusion = self.dxf.extrusion vectors = (self.dxf.get_default(name) for name in vector_names) ocs_vectors = ucs.ocs_points_to_ocs(vectors, extrusion=extrusion) for name, value in zip(vector_names, ocs_vectors): self.dxf.set(name, value) if angle_names is not None: angles = (self.dxf.get_default(name) for name in angle_names) ocs_angles = ucs.ocs_angles_to_ocs_deg(angles=angles, extrusion=extrusion) for name, value in zip(angle_names, ocs_angles): self.dxf.set(name, value) self.dxf.extrusion = ucs.direction_to_wcs(extrusion)
class Block(DXFEntity): """ DXF BLOCK entity """ DXFTYPE = 'BLOCK' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_block_begin) # block entity flags # This is an anonymous block generated by hatching, associative dimensioning, # other internal operations, or an application ANONYMOUS = 1 # This block has non-constant attribute definitions (this bit is not set if the block has # any attribute definitions that are constant, or has no attribute definitions at all) NON_CONSTANT_ATTRIBUTES = 2 XREF = 4 # This block is an external reference (xref) XREF_OVERLAY = 8 # This block is an xref overlay EXTERNAL = 16 # This block is externally dependent RESOLVED = 32 # This is a resolved external reference, or dependent of an external reference (ignored on input) REFERENCED = 64 # This definition is a referenced external reference (ignored on input) def load_dxf_attribs(self, processor: SubclassProcessor = None) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf processor.load_dxfattribs_into_namespace(dxf, acdb_entity) processor.load_dxfattribs_into_namespace(dxf, acdb_block_begin) if processor.r12: if dxf.name.lower() == MODEL_SPACE_R12_LOWER: dxf.name = MODEL_SPACE_R2000 dxf.name2 = MODEL_SPACE_R2000 elif dxf.name.lower() == PAPER_SPACE_R12_LOWER: dxf.name = PAPER_SPACE_R2000 dxf.name2 = PAPER_SPACE_R2000 return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name) if self.dxf.hasattr('paperspace'): tagwriter.write_tag2(67, 1) # set paper space flag self.dxf.export_dxf_attribs(tagwriter, 'layer') if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_block_begin.name) name = self.dxf.name if tagwriter.dxfversion == DXF12: # export modelspace and paperspace with leading '$' instead of '*' if name.lower() == MODEL_SPACE_R2000_LOWER: name = MODEL_SPACE_R12 elif name.lower() == PAPER_SPACE_R2000_LOWER: name = PAPER_SPACE_R12 tagwriter.write_tag2(2, name) self.dxf.export_dxf_attribs(tagwriter, ['flags', 'base_point']) tagwriter.write_tag2(3, name) self.dxf.export_dxf_attribs(tagwriter, ['xref_path', 'description']) # xdata and embedded objects export will be done by parent class @property def is_layout_block(self) -> bool: """ Returns ``True`` if this is a :class:`~ezdxf.layouts.Modelspace` or :class:`~ezdxf.layouts.Paperspace` block definition. """ name = self.dxf.name.lower() return name.startswith('*model_space') or name.startswith('*paper_space') @property def is_anonymous(self) -> bool: """ Returns ``True`` if this is an anonymous block generated by hatching, associative dimensioning, other internal operations, or an application. .. versionadded:: 0.12 """ return self.get_flag_state(Block.ANONYMOUS) @property def is_xref(self) -> bool: """ Returns ``True`` if bock is an external referenced file. (XREF) .. versionadded:: 0.12 """ return self.get_flag_state(Block.XREF) @property def is_xref_overlay(self) -> bool: """ Returns ``True`` if bock is an external referenced overlay file. (XREF) .. versionadded:: 0.12 """ return self.get_flag_state(Block.XREF_OVERLAY)
class MText(ModernGraphicEntity ): # MTEXT will be extended in DXF version AC1021 (ACAD 2007) __slots__ = () TEMPLATE = ExtendedTags.from_text(_MTEXT_TPL) DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, mtext_subclass) def get_text(self) -> str: tags = self.tags.get_subclass('AcDbMText') tail = "" parts = [] for tag in tags: if tag.code == 1: tail = tag.value if tag.code == 3: parts.append(tag.value) parts.append(tail) return "".join(parts) def set_text(self, text: str) -> 'MText': tags = self.tags.get_subclass('AcDbMText') tags.remove_tags(codes=(1, 3)) str_chunks = split_string_in_chunks(text, size=250) if len(str_chunks) == 0: str_chunks.append("") while len(str_chunks) > 1: tags.append(DXFTag(3, str_chunks.pop(0))) tags.append(DXFTag(1, str_chunks[0])) return self def get_rotation(self) -> float: try: vector = self.dxf.text_direction except DXFValueError: rotation = self.get_dxf_attrib('rotation', 0.0) else: radians = math.atan2(vector[1], vector[0]) # ignores z-axis rotation = math.degrees(radians) return rotation def set_rotation(self, angle: float) -> 'MText': del self.dxf.text_direction # *text_direction* has higher priority than *rotation*, therefore delete it self.dxf.rotation = angle return self def set_location(self, insert: 'Vertex', rotation: float = None, attachment_point: int = None) -> 'MText': self.dxf.insert = Vector(insert) if rotation is not None: self.set_rotation(rotation) if attachment_point is not None: self.dxf.attachment_point = attachment_point return self def set_bg_color(self, color: Union[int, str, Tuple[int, int, int], None], scale: float = 1.5): self.dxf.box_fill_scale = scale if color is None: self.del_dxf_attrib('bg_fill') self.del_dxf_attrib('box_fill_scale') self.del_dxf_attrib('bg_fill_color') self.del_dxf_attrib('bg_fill_true_color') self.del_dxf_attrib('bg_fill_color_name') elif color == 'canvas': # special case for use background color self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR self.dxf.bg_fill_color = 0 # required but ignored else: self.dxf.bg_fill = const.MTEXT_BG_COLOR if isinstance(color, int): self.dxf.bg_fill_color = color elif isinstance(color, str): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_color_name = color elif isinstance(color, tuple): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_true_color = rgb2int(color) return self # fluent interface @contextmanager def edit_data(self) -> 'MTextData': buffer = MTextData(self.get_text()) yield buffer self.set_text(buffer.text) buffer = edit_data # alias
class Material(DXFObject): DXFTYPE = 'MATERIAL' DEFAULT_ATTRIBS = { 'diffuse_color_method': 1, 'diffuse_color_value': -1023410177, } DXFATTRIBS = DXFAttributes(base_class, acdb_material) def __init__(self): super().__init__() self.diffuse_mapper_matrix: Optional[Matrix44] = None # code 43 self.specular_mapper_matrix: Optional[Matrix44] = None # code 47 self.reflexion_mapper_matrix: Optional[Matrix44] = None # code 49 self.opacity_mapper_matrix: Optional[Matrix44] = None # group 142 self.bump_mapper_matrix: Optional[Matrix44] = None # group 144 self.refraction_mapper_matrix: Optional[Matrix44] = None # code 147 self.normal_mapper_matrix: Optional[Matrix44] = None # code 43 ??? def _copy_data(self, entity: 'Material') -> None: """ Copy material mapper matrices """ def copy(matrix): return None if matrix is None else matrix.copy() entity.diffuse_mapper_matrix = copy(self.diffuse_mapper_matrix) entity.specular_mapper_matrix = copy(self.specular_mapper_matrix) entity.reflexion_mapper_matrix = copy(self.reflexion_mapper_matrix) entity.opacity_mapper_matrix = copy(self.opacity_mapper_matrix) entity.bump_mapper_matrix = copy(self.bump_mapper_matrix) entity.refraction_mapper_matrix = copy(self.refraction_mapper_matrix) entity.normal_mapper_matrix = copy(self.normal_mapper_matrix) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace(dxf, acdb_material) self.load_matrices(tags) return dxf def load_matrices(self, tags): tags, matrix = fetch_matrix(tags, 43) if matrix: self.diffuse_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 47) if matrix: self.specular_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 49) if matrix: self.reflexion_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 142) if matrix: self.opacity_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 144) if matrix: self.bump_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 147) if matrix: self.refraction_mapper_matrix = matrix tags, matrix = fetch_matrix(tags, 43) if matrix: self.normal_mapper_matrix = matrix def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_material.name) self.dxf.export_dxf_attribs(tagwriter, [ 'name', 'description', 'ambient_color_method', 'ambient_color_factor', 'ambient_color_value', 'diffuse_color_method', 'diffuse_color_factor', 'diffuse_color_value', 'diffuse_map_blend_factor', 'diffuse_map_source', 'diffuse_map_file_name', 'diffuse_map_projection_method', 'diffuse_map_tiling_method', 'diffuse_map_auto_transform_method', ]) export_matrix(tagwriter, 43, self.diffuse_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'specular_gloss_factor', 'specular_color_method', 'specular_color_factor', 'specular_color_value', 'specular_map_blend_factor', 'specular_map_source', 'specular_map_file_name', 'specular_map_projection_method', 'specular_map_tiling_method', 'specular_map_auto_transform_method', ]) export_matrix(tagwriter, 47, self.specular_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'reflection_map_blend_factor', 'reflection_map_source', 'reflection_map_file_name', 'reflection_map_projection_method', 'reflection_map_tiling_method', 'reflection_map_auto_transform_method', ]) export_matrix(tagwriter, 49, self.reflexion_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'opacity', 'opacity_map_blend_factor', 'opacity_map_source', 'opacity_map_file_name', 'opacity_map_projection_method', 'opacity_map_tiling_method', 'opacity_map_auto_transform_method', ]) export_matrix(tagwriter, 142, self.opacity_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'bump_map_blend_factor', 'bump_map_source', 'bump_map_file_name', 'bump_map_projection_method', 'bump_map_tiling_method', 'bump_map_auto_transform_method', ]) export_matrix(tagwriter, 144, self.bump_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'refraction_index', 'refraction_map_blend_factor', 'refraction_map_source', 'refraction_map_file_name', 'refraction_map_projection_method', 'refraction_map_tiling_method', 'refraction_map_auto_transform_method', ]) export_matrix(tagwriter, 147, self.refraction_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'normal_map_method', 'normal_map_strength', 'normal_map_blend_factor', 'normal_map_source', 'normal_map_file_name', 'normal_map_projection_method', 'normal_map_tiling_method', 'normal_map_auto_transform_method', ]) export_matrix(tagwriter, 43, self.normal_mapper_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'color_bleed_scale', 'indirect_dump_scale', 'reflectance_scale', 'transmittance_scale', 'two_sided_material', 'luminance', 'luminance_mode', 'materials_anonymous', 'global_illumination_mode', 'final_gather_mode', 'gen_proc_name', 'gen_proc_val_bool', 'gen_proc_val_int', 'gen_proc_val_real', 'gen_proc_val_text', 'gen_proc_table_end', 'gen_proc_val_color_index', 'gen_proc_val_color_rgb', 'gen_proc_val_color_name', 'map_utile', 'translucence', 'self_illumination', 'reflectivity', 'illumination_model', 'channel_flags', ])
class DimStyle(DXFEntity): """ DXF BLOCK_RECORD table entity """ DXFTYPE = 'DIMSTYLE' DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_dimstyle) CODE_TO_DXF_ATTRIB = dict(DXFATTRIBS.build_group_code_items(dim_filter)) @property def dxfversion(self): return self.doc.dxfversion def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace(dxf, acdb_dimstyle) if len(tags) and not processor.r12: processor.log_unprocessed_tags(tags, subclass=acdb_dimstyle.name) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: super().export_entity(tagwriter) # AcDbEntity export is done by parent class if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dimstyle.name) if tagwriter.dxfversion > DXF12: # set handles from dimblk names self.set_handles() # for all DXF versions if tagwriter.dxfversion == DXF12: attribs = EXPORT_MAP_R12 elif tagwriter.dxfversion < DXF2007: attribs = EXPORT_MAP_R2000 else: attribs = EXPORT_MAP_R2007 self.dxf.export_dxf_attribs(tagwriter, attribs) def set_handles(self): style = self.dxf.get('dimtxsty', None) if style is not None: self.dxf.dimtxsty_handle = self.doc.styles.get(style).dxf.handle for blk_name in ('dimblk', 'dimblk1', 'dimblk2', 'dimldrblk'): name = self.dxf.get(blk_name) if name is not None: self.set_blk_handle(blk_name + '_handle', name) for ltype_name in ('dimltype', 'dimltex1', 'dimltex2'): get_linetype = self.doc.linetypes.get name = self.dxf.get(ltype_name, None) if name is not None: handle = get_linetype(name).dxf.handle self.dxf.set(ltype_name + '_handle', handle) def discard_handles(self): for attr in ('dimblk', 'dimblk1', 'dimblk2', 'dimldrblk', 'dimltype', 'dimltex1', 'dimltex2', 'dimtxsty'): self.dxf.discard(attr + '_handle') def set_blk_handle(self, attr: str, arrow_name: str) -> None: if arrow_name == ARROWS.closed_filled: # special arrow, no handle needed (is '0' if set) # do not create block by default, this will be done if arrow is used # and block record handle is not needed here self.dxf.discard(attr) return blocks = self.doc.blocks if ARROWS.is_acad_arrow(arrow_name): # create block, because need block record handle is needed here block_name = ARROWS.create_block(blocks, arrow_name) else: block_name = arrow_name blk = blocks.get(block_name) if blk is not None: self.set_dxf_attrib(attr, blk.block_record_handle) else: raise DXFValueError('Block {} does not exist.'.format(arrow_name)) def get_arrow_block_name(self, name: str) -> str: handle = self.get_dxf_attrib(name, None) if handle in (None, '0'): # unset handle or handle '0' is default closed filled arrow return ARROWS.closed_filled else: block_name = get_block_name_by_handle(handle, self.doc) return ARROWS.arrow_name( block_name ) # if arrow return standard arrow name else just the block name def set_linetypes(self, dimline=None, ext1=None, ext2=None) -> None: if self.dxfversion < DXF2007: logger.debug('Linetype support requires DXF R2007 or later.') if dimline is not None: self.dxf.dimltype = dimline if ext1 is not None: self.dxf.dimltex1 = ext1 if ext2 is not None: self.dxf.dimltex2 = ext2 # -- legacy -- def dim_attribs(self) -> Iterable[Tuple[str, DXFAttr]]: return ((name, attrib) for name, attrib in self.DXFATTRIBS.items() if name.startswith('dim')) def print_dim_attribs(self) -> None: for name, attrib in self.dim_attribs(): code = attrib.code value = self.get_dxf_attrib(name, None) if value is not None: print("{name} ({code}) = {value}".format(name=name, value=value, code=code)) def copy_to_header(self, dwg: 'Drawing'): """ Copy all dimension style variables to HEADER section of `dwg`. """ attribs = self.dxfattribs() header = dwg.header header['$DIMSTYLE'] = self.dxf.name for name, value in attribs.items(): if name.startswith('dim'): header_var = '$' + name.upper() try: header[header_var] = value except DXFKeyError: logger.debug( 'Unsupported header variable: {}.'.format(header_var)) def set_arrows(self, blk: str = '', blk1: str = '', blk2: str = '', ldrblk: str = '') -> None: """ Set arrows by block names or AutoCAD standard arrow names, set DIMTSZ to ``0`` which disables tick. Args: blk: block/arrow name for both arrows, if DIMSAH is ``0`` blk1: block/arrow name for first arrow, if DIMSAH is ``1`` blk2: block/arrow name for second arrow, if DIMSAH is ``1`` ldrblk: block/arrow name for leader """ self.set_dxf_attrib('dimblk', blk) self.set_dxf_attrib('dimblk1', blk1) self.set_dxf_attrib('dimblk2', blk2) self.set_dxf_attrib('dimldrblk', ldrblk) self.set_dxf_attrib('dimtsz', 0) # use blocks # only existing BLOCK definitions allowed if self.doc: blocks = self.doc.blocks for b in (blk, blk1, blk2, ldrblk): if ARROWS.is_acad_arrow(b): # not real blocks # todo: create default arrow blocks!!! ARROWS.create_block(blocks, b) continue if b and b not in blocks: raise DXFValueError( 'BLOCK "{}" does not exist.'.format(blk)) def set_tick(self, size: float = 1) -> None: """ Set tick `size`, which also disables arrows, a tick is just an oblique stroke as marker. Args: size: arrow size in drawing units """ self.set_dxf_attrib('dimtsz', size) def set_text_align(self, halign: str = None, valign: str = None, vshift: float = None) -> None: """ Set measurement text alignment, `halign` defines the horizontal alignment (requires DXF R2000), `valign` defines the vertical alignment, `above1` and `above2` means above extension line 1 or 2 and aligned with extension line. Args: halign: ``left``, ``right``, ``center`, `above1``, ``above2`` (requires DXF R2000) valign: ``above``, ``center``, ``below`` vshift: vertical text shift, if `valign` is ``center``; >0 shift upward, <0 shift downwards """ if valign: valign = valign.lower() self.set_dxf_attrib('dimtad', const.DIMTAD[valign]) if valign == 'center' and vshift is not None: self.set_dxf_attrib('dimtvp', vshift) if halign: self.set_dxf_attrib('dimjust', const.DIMJUST[halign.lower()]) def set_text_format(self, prefix: str = '', postfix: str = '', rnd: float = None, dec: int = None, sep: str = None, leading_zeros: bool = True, trailing_zeros: bool = True): """ Set dimension text format, like prefix and postfix string, rounding rule and number of decimal places. Args: prefix: Dimension text prefix text as string postfix: Dimension text postfix text as string rnd: Rounds all dimensioning distances to the specified value, for instance, if DIMRND is set to ``0.25``, all distances round to the nearest ``0.25`` unit. If you set DIMRND to ``1.0``, all distances round to the nearest integer. dec: Sets the number of decimal places displayed for the primary units of a dimension. (requires DXF R2000) sep: ``'.'`` or ``','`` as decimal separator (requires DXF R2000) leading_zeros: suppress leading zeros for decimal dimensions if ``False`` trailing_zeros: suppress trailing zeros for decimal dimensions if ``False`` """ if prefix or postfix: self.dxf.dimpost = prefix + '<>' + postfix if rnd is not None: self.dxf.dimrnd = rnd # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimzin = 0 if leading_zeros is False: dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimzin = dimzin if dec is not None: self.dxf.dimdec = dec if sep is not None: self.dxf.dimdsep = ord(sep) def set_dimline_format(self, color: int = None, linetype: str = None, lineweight: int = None, extension: float = None, disable1: bool = None, disable2: bool = None): """ Set dimension line properties Args: color: color index linetype: linetype as string (requires DXF R2007) lineweight: line weight as int, ``13`` = 0.13mm, ``200`` = 2.00mm (requires DXF R2000) extension: extension length disable1: ``True`` to suppress first part of dimension line (requires DXF R2000) disable2: ``True`` to suppress second part of dimension line (requires DXF R2000) """ if color is not None: self.dxf.dimclrd = color if extension is not None: self.dxf.dimdle = extension if lineweight is not None: self.dxf.dimlwd = lineweight if disable1 is not None: self.dxf.dimsd1 = disable1 if disable2 is not None: self.dxf.dimsd2 = disable2 if linetype is not None: self.dxf.dimltype = linetype def set_extline_format(self, color: int = None, lineweight: int = None, extension: float = None, offset: float = None, fixed_length: float = None): """ Set common extension line attributes. Args: color: color index lineweight: line weight as int, ``13`` = 0.13mm, ``200`` = 2.00mm extension: extension length above dimension line offset: offset from measurement point fixed_length: set fixed length extension line, length below the dimension line """ if color is not None: self.dxf.dimclre = color if extension is not None: self.dxf.dimexe = extension if offset is not None: self.dxf.dimexo = offset if lineweight is not None: self.dxf.dimlwe = lineweight if fixed_length is not None: self.dxf.dimfxlon = 1 self.dxf.dimfxl = fixed_length def set_extline1(self, linetype: str = None, disable=False): """ Set extension line 1 attributes. Args: linetype: linetype for extension line 1 (requires DXF R2007) disable: disable extension line 1 if ``True`` """ if disable: self.dxf.dimse1 = 1 if linetype is not None: self.dxf.dimltex1 = linetype def set_extline2(self, linetype: str = None, disable=False): """ Set extension line 2 attributes. Args: linetype: linetype for extension line 2 (requires DXF R2007) disable: disable extension line 2 if ``True`` """ if disable: self.dxf.dimse2 = 1 if linetype is not None: self.dxf.dimltex2 = linetype def set_tolerance(self, upper: float, lower: float = None, hfactor: float = 1.0, align: str = None, dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None: """ Set tolerance text format, upper and lower value, text height factor, number of decimal places or leading and trailing zero suppression. Args: upper: upper tolerance value lower: lower tolerance value, if ``None`` same as upper hfactor: tolerance text height factor in relation to the dimension text height align: tolerance text alignment ``'TOP'``, ``'MIDDLE'``, ``'BOTTOM'`` (requires DXF R2000) dec: Sets the number of decimal places displayed (requires DXF R2000) leading_zeros: suppress leading zeros for decimal dimensions if ``False`` (requires DXF R2000) trailing_zeros: suppress trailing zeros for decimal dimensions if ``False`` (requires DXF R2000) """ # exclusive tolerances self.dxf.dimtol = 1 self.dxf.dimlim = 0 self.dxf.dimtp = float(upper) if lower is not None: self.dxf.dimtm = float(lower) else: self.dxf.dimtm = float(upper) if hfactor is not None: self.dxf.dimtfac = float(hfactor) # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimtzin = 0 if leading_zeros is False: dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimtzin = dimtzin if align is not None: self.dxf.dimtolj = const.MTEXT_INLINE_ALIGN[align.upper()] if dec is not None: self.dxf.dimtdec = int(dec) def set_limits(self, upper: float, lower: float, hfactor: float = 1.0, dec: int = None, leading_zeros: bool = None, trailing_zeros: bool = None) -> None: """ Set limits text format, upper and lower limit values, text height factor, number of decimal places or leading and trailing zero suppression. Args: upper: upper limit value added to measurement value lower: lower lower value subtracted from measurement value hfactor: limit text height factor in relation to the dimension text height dec: Sets the number of decimal places displayed (requires DXF R2000) leading_zeros: suppress leading zeros for decimal dimensions if ``False`` (requires DXF R2000) trailing_zeros: suppress trailing zeros for decimal dimensions if ``False`` (requires DXF R2000) """ # exclusive limits self.dxf.dimlim = 1 self.dxf.dimtol = 0 self.dxf.dimtp = float(upper) self.dxf.dimtm = float(lower) self.dxf.dimtfac = float(hfactor) # works only with decimal dimensions not inch and feet, US user set dimzin directly if leading_zeros is not None or trailing_zeros is not None: dimtzin = 0 if leading_zeros is False: dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS if trailing_zeros is False: dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS self.dxf.dimtzin = dimtzin self.dxf.dimtolj = 0 # set bottom as default if dec is not None: self.dxf.dimtdec = int(dec)
class Mesh(DXFGraphic): """ DXF MESH entity """ DXFTYPE = 'MESH' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mesh) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 def __init__(self, doc: 'Drawing' = None): super().__init__(doc) self._vertices = VertexArray() # vertices stored as array.array('d') self._faces = FaceList() # face lists data self._edges = EdgeArray() # edge indices stored as array.array('L') self._creases = array.array('f') # creases stored as array.array('f') def _copy_data(self, entity: 'Mesh') -> None: """ Copy data: vertices, faces, edges, creases. """ entity._vertices = copy.deepcopy(self._vertices) entity._faces = copy.deepcopy(self._faces) entity._edges = copy.deepcopy(self._edges) entity._creases = copy.deepcopy(self._creases) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.find_subclass(acdb_mesh.name) # load spline data (fit points, control points, weights, knots) and remove their tags from subclass self.load_mesh_data(tags, dxf.handle) # load remaining data into name space tags = processor.load_dxfattribs_into_namespace(dxf, acdb_mesh) if len(tags): # override data processor.log_unprocessed_tags(tags, subclass=acdb_mesh.name) return dxf def load_mesh_data(self, mesh_tags: 'Tags', handle: str) -> None: def process_vertices(): try: vertex_count_index = mesh_tags.tag_index(92) except DXFValueError: raise DXFStructureError( COUNT_ERROR_MSG.format(handle, 'vertex')) vertices = create_vertex_array(mesh_tags, vertex_count_index + 1) # remove vertex count tag and all vertex tags end_index = vertex_count_index + 1 + len(vertices) del mesh_tags[vertex_count_index:end_index] return vertices def process_faces(): try: face_count_index = mesh_tags.tag_index(93) except DXFValueError: raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'face')) else: # remove face count tag and all face tags faces = create_face_list(mesh_tags, face_count_index + 1) end_index = face_count_index + 1 + faces.tag_count() del mesh_tags[face_count_index:end_index] return faces def process_edges(): try: edge_count_index = mesh_tags.tag_index(94) except DXFValueError: raise DXFStructureError(COUNT_ERROR_MSG.format(handle, 'edge')) else: edges = create_edge_array(mesh_tags, edge_count_index + 1) # remove edge count tag and all edge tags end_index = edge_count_index + 1 + len(edges.values) del mesh_tags[edge_count_index:end_index] return edges def process_creases(): try: crease_count_index = mesh_tags.tag_index(95) except DXFValueError: raise DXFStructureError( COUNT_ERROR_MSG.format(handle, 'crease')) else: creases = create_crease_array(mesh_tags, crease_count_index + 1) # remove crease count tag and all crease tags end_index = crease_count_index + 1 + len(creases) del mesh_tags[crease_count_index:end_index] return creases self._vertices = process_vertices() self._faces = process_faces() self._edges = process_edges() self._creases = process_creases() def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) # AcDbEntity export is done by parent class tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mesh.name) self.dxf.export_dxf_attribs( tagwriter, ['version', 'blend_crease', 'subdivision_levels']) self.export_mesh_data(tagwriter) self.export_override_data(tagwriter) def export_mesh_data(self, tagwriter: 'TagWriter'): tagwriter.write_tag2(92, len(self.vertices)) self._vertices.export_dxf(tagwriter, code=10) self._faces.export_dxf(tagwriter) self._edges.export_dxf(tagwriter) tagwriter.write_tag2(95, len(self.creases)) for crease_value in self.creases: tagwriter.write_tag2(140, crease_value) def export_override_data(self, tagwriter: 'TagWriter'): tagwriter.write_tag2(90, 0) @property def creases(self) -> 'array.array': # group code 40 """ Creases as :class:`array.array`. (read/write)""" return self._creases @creases.setter def creases(self, values: Iterable[float]) -> None: self._creases = array.array('f', values) @property def vertices(self): """ Vertices as list like :class:`~ezdxf.lldxf.packedtags.VertexArray`. (read/write)""" return self._vertices @vertices.setter def vertices(self, points: Iterable['Vertex']) -> None: self._vertices = VertexArray(chain.from_iterable(points)) @property def edges(self): """ Edges as list like :class:`~ezdxf.lldxf.packedtags.TagArray`. (read/write)""" return self._edges @edges.setter def edges(self, edges: Iterable[Tuple[int, int]]) -> None: self._edges.set_data(edges) @property def faces(self): """ Faces as list like :class:`~ezdxf.lldxf.packedtags.TagList`. (read/write)""" return self._faces @faces.setter def faces(self, faces: Iterable[Sequence[int]]) -> None: self._faces.set_data(faces) def get_data(self) -> 'MeshData': return MeshData(self) def set_data(self, data: 'MeshData') -> None: self.vertices = data.vertices self._faces.set_data(data.faces) self._edges.set_data(data.edges) self.creases = data.edge_crease_values @contextmanager def edit_data(self) -> 'MeshData': """ Context manager various mesh data, returns :class:`MeshData`. Despite that vertices, edge and faces since `ezdxf` v0.8.9 are accessible as packed data types, the usage of :class:`MeshData` by context manager :meth:`edit_data` is still recommended. """ data = self.get_data() yield data self.set_data(data) def transform(self, m: 'Matrix44') -> 'Mesh': """ Transform MESH entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ self._vertices.transform(m) return self
class Field(DXFObject): """ DXF FIELD entity """ DXFTYPE = 'FIELD' DXFATTRIBS = DXFAttributes(base_class, acdb_field)
class Dictionary(DXFObject): """ AutoCAD maintains items such as mline styles and group definitions as objects in dictionaries. Other applications are free to create and use their own dictionaries as they see fit. The prefix "ACAD_" is reserved for use by AutoCAD applications. Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string, because at loading time not all objects are already stored in the EntityDB, and have to acquired later. """ DXFTYPE = 'DICTIONARY' DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary) def __init__(self, doc: 'Drawing' = None): super().__init__(doc) self._data = dict() # type: Dict[str, Union[str, DXFEntity]] self._value_code = VALUE_CODE # some dict have 360 handles for values def _copy_data(self, entity: 'Dictionary') -> None: """ Copy hard owned entities but do not store the copies in the entity database, this is a second step, this is just real copying. """ # todo: what about reactors of cloned DXF objects? entity._value_code = self._value_code if self.dxf.hard_owned: entity._data = {key: entity.copy() for key, entity in self.items()} else: entity._data = {key: entity for key, entity in self.items()} def _add_data_to_db(self) -> None: """ Add hard owned and therefore copied entities into database and the objects section. """ # todo: don't know how to proceed with reactors of cloned objects? if self.dxf.hard_owned: my_handle = self.dxf.handle for _, entity in self.items(): entity.dxf.owner = my_handle entity.dxf.handle = None self.entitydb.add(entity) # add it also to the objects section, else it wouldn't exported to DXF file self.doc.objects.add_object(entity) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace( dxf, acdb_dictionary) self.load_dict(tags) return dxf def load_dict(self, tags): entry_handle = None dict_key = None value_code = VALUE_CODE for code, value in tags: if code in SEARCH_CODES: # first store handles, because at this point, not all objects are stored in the EntityDB, # at access convert the handle to DXFEntity value_code = code entry_handle = value elif code == KEY_CODE: dict_key = value if dict_key and entry_handle: try: entity = self.entitydb[entry_handle] except KeyError: entity = entry_handle # store entity as handle string self._data[dict_key] = entity entry_handle = None dict_key = None self._value_code = value_code # use same value code as loaded def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name) self.dxf.export_dxf_attribs(tagwriter, ['hard_owned', 'cloning']) self.export_dict(tagwriter) def export_dict(self, tagwriter: 'TagWriter'): # key: dict key string # value: DXFEntity or handle as string # Ignore invalid handles at export, because removing can create an empty dictionary, which is more a problem for # AutoCAD than invalid handles, and removing the whole dictionary is also a problem maybe. for key, value in self._data.items(): tagwriter.write_tag2(KEY_CODE, key) # value can be a handle string or a DXFEntity if isinstance(value, DXFEntity): if value.is_alive: value = value.dxf.handle else: # entry was deleted externally logger.debug( f'Key "{key}" references (external) deleted entry in {str(self)}' ) value = '0' tagwriter.write_tag2(self._value_code, value) # use same value code as loaded @property def is_hard_owner(self) -> bool: """ ``True`` if :class:`Dictionary` is hard owner of entities. Hard owned entities will be deleted by deleting the dictionary. """ return bool(self.dxf.hard_owned) def keys(self) -> KeysView: """ Returns :class:`KeysView` of all dictionary keys. """ return self._data.keys() def items(self) -> ItemsView: """ Returns :class:`ItemsView` for all dictionary entries as (:attr:`key`, :class:`DXFEntity`) pairs. """ for key in self.keys(): yield key, self.get(key) # maybe handle -> DXFEntity def __getitem__(self, key: str) -> 'DXFEntity': """ Return the value for `key`, raises a :class:`DXFKeyError` if `key` does not exist. """ return self.get(key) def __setitem__(self, key: str, value: 'DXFEntity') -> None: """ Add item as ``(key, value)`` pair to dictionary. """ return self.add(key, value) def __delitem__(self, key: str) -> None: """ Delete entry `key` from the dictionary, raises :class:`DXFKeyError` if key does not exist. """ return self.remove(key) def __contains__(self, key: str) -> bool: """ Returns ``True`` if `key` exist. """ return key in self._data def __len__(self) -> int: """ Returns count of items. """ return len(self._data) count = __len__ def get(self, key: str, default: Any = DXFKeyError) -> 'DXFEntity': """ Returns :class:`DXFEntity` for `key`, if `key` exist, else `default` or raises a :class:`DXFKeyError` for `default` = :class:`DXFKeyError`. """ try: entity = self._data[key] except KeyError: if default is DXFKeyError: raise DXFKeyError("KeyError: '{}'".format(key)) else: return default else: if isinstance(entity, str): # entity is still a handle, get DXFEntity from EntityDB entity = self.entitydb[entity] # and store DXFEntity in the dict self._data[key] = entity return entity def add(self, key: str, value: 'DXFEntity') -> None: """ Add entry ``(key, value)``. """ if isinstance(value, str): try: value = self.entitydb[value] except KeyError: raise DXFKeyError( 'Invalid entity handle #{} for key {}'.format(value, key)) self._data[key] = value def remove(self, key: str) -> None: """ Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not exist. Deletes also hard owned DXF objects from OBJECTS section. """ data = self._data if key not in data: raise DXFKeyError(key) if self.is_hard_owner: entity = self.get(key) # Presumption: hard owned DXF objects always reside in the OBJECTS section self.doc.objects.delete_entity(entity) del data[key] def discard(self, key: str) -> None: """ Delete entry `key` if exists. Does NOT raise an exception if `key` not exist and does not delete hard owned DXF objects. """ try: del self._data[key] except KeyError: pass def clear(self) -> None: """ Delete all entries from DXFDictionary, deletes hard owned DXF objects from OBJECTS section. """ if self.is_hard_owner: self._delete_hard_owned_entries() self._data.clear() def _delete_hard_owned_entries(self) -> None: # Presumption: hard owned DXF objects always reside in the OBJECTS section objects = self.doc.objects for key, entity in self.items(): objects.delete_entity(entity) def add_new_dict(self, key: str, hard_owned: bool = False) -> 'Dictionary': """ Create a new sub :class:`Dictionary`. Args: key: name of the sub dictionary hard_owned: entries of the new dictionary are hard owned """ dxf_dict = self.doc.objects.add_dictionary(owner=self.dxf.handle, hard_owned=hard_owned) self.add(key, dxf_dict) return dxf_dict def add_dict_var(self, key: str, value: str) -> 'DictionaryVar': """ Add new :class:`DictionaryVar`. Args: key: entry name as string value: entry value as string """ new_var = self.doc.objects.add_dictionary_var(owner=self.dxf.handle, value=value) self.add(key, new_var) return new_var def get_required_dict(self, key: str) -> 'Dictionary': """ Get entry `key` or create a new :class:`Dictionary`, if `Key` not exit. """ try: dxf_dict = self.get(key) except DXFKeyError: dxf_dict = self.add_new_dict(key) return dxf_dict def audit(self, auditor: 'Auditor') -> None: super().audit(auditor) self._check_invalid_entries(auditor) def _check_invalid_entries(self, auditor: 'Auditor'): keys_to_discard = [] # do not delete content while iterating append = keys_to_discard.append db = self.entitydb for key, entry in self._data.items(): if isinstance(entry, str): # entry is a handle as string if entry not in db: append(key) elif entry.is_alive: # entry is a DXFEntity and alive if entry.dxf.handle not in db: append(key) else: # entry is a DXFEntity and was deleted externally append(key) for key in keys_to_discard: del self._data[key] auditor.fixed_error( code=AuditError.INVALID_DICTIONARY_ENTRY, message= f'Removed entry "{key}" with invalid handle in {str(self)}', dxf_entity=self, data=key, ) def destroy(self) -> None: if self.is_hard_owner: self._delete_hard_owned_entries()
class Body(DXFGraphic): """ DXF BODY entity - container entity for embedded ACIS data. """ DXFTYPE = 'BODY' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_modeler_geometry) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 def __init__(self): super().__init__() self._acis_data: List[Union[str, bytes]] = [] @property def acis_data(self) -> List[Union[str, bytes]]: """ Get ACIS text data as list of strings for DXF R2000 to DXF R2010 and binary encoded ACIS data for DXF R2013 and later as list of bytes. """ if self.has_binary_data: return self.doc.acdsdata.get_acis_data(self.dxf.handle) else: return self._acis_data @acis_data.setter def acis_data(self, lines: Iterable[str]): """ Set ACIS data as list of strings for DXF R2000 to DXF R2010. In case of DXF R2013 and later, setting ACIS data as binary data is not supported. """ if self.has_binary_data: raise DXFTypeError( 'Setting ACIS data not supported for DXF R2013 and later.') else: self._acis_data = list(lines) @property def has_binary_data(self): """ Returns ``True`` if ACIS data is of type ``List[bytes]``, ``False`` if data is of type ``List[str]``. """ if self.doc: return self.doc.dxfversion >= DXF2013 else: return False def copy(self): """ Prevent copying. (internal interface)""" raise DXFTypeError('Copying of ACIS data not supported.') def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': """ Loading interface. (internal API)""" dxf = super().load_dxf_attribs(processor) if processor: # TODO: fast loader? processor.fast_load_dxfattribs(dxf, acdb_modeler_geometry_group_codes, 2, log=False) if not self.has_binary_data: self.load_acis_data(processor.subclasses[2]) return dxf def load_acis_data(self, tags: Tags): """ Loading interface. (internal API)""" text_lines = tags2textlines(tag for tag in tags if tag.code in (1, 3)) self.acis_data = crypt.decode(text_lines) def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. (internal API)""" super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_modeler_geometry.name) if tagwriter.dxfversion >= DXF2013: # ACIS data stored in the ACDSDATA section as binary encoded # information. if self.dxf.hasattr('version'): tagwriter.write_tag2(70, self.dxf.version) self.dxf.export_dxf_attribs(tagwriter, ['flags', 'uid']) else: # DXF R2000 - R2013 stores ACIS data as text in entity self.dxf.export_dxf_attribs(tagwriter, 'version') self.export_acis_data(tagwriter) def export_acis_data(self, tagwriter: 'TagWriter') -> None: """ Export ACIS data as DXF tags. (internal API)""" def cleanup(lines): for line in lines: yield line.rstrip().replace('\n', '') tags = Tags(textlines2tags(crypt.encode(cleanup(self.acis_data)))) tagwriter.write_tags(tags) def set_text(self, text: str, sep: str = '\n') -> None: """ Set ACIS data from one string. """ self.acis_data = text.split(sep) def tostring(self) -> str: """ Returns ACIS data as one string for DXF R2000 to R2010. """ if self.has_binary_data: return "" else: return "\n".join(self.acis_data) def tobytes(self) -> bytes: """ Returns ACIS data as joined bytes for DXF R2013 and later. """ if self.has_binary_data: return b"".join(self.acis_data) else: return b"" def get_acis_data(self): """ Get the ACIS source code as a list of strings. """ # for backward compatibility return self.acis_data def set_acis_data(self, text_lines: Iterable[str]) -> None: """ Set the ACIS source code as a list of strings **without** line endings. """ # for backward compatibility self.acis_data = text_lines @contextmanager def edit_data(self) -> 'ModelerGeometry': # for backward compatibility data = ModelerGeometry(self) yield data self.acis_data = data.text_lines
class SortEntsTable(DXFObject): """ DXF VBA_PROJECT entity """ # should work with AC1015/R2000 but causes problems with TrueView/AutoCAD LT 2019: "expected was-a-zombie-flag" # No problems with AC1018/R2004 and later # # If the header variable $SORTENTS Regen flag (bit-code value 16) is set, AutoCAD regenerates entities in ascending # handle order. # # When the DRAWORDER command is used, a SORTENTSTABLE object is attached to the *Model_Space or *Paper_Space block's # extension dictionary under the name ACAD_SORTENTS. The SORTENTSTABLE object related to this dictionary associates # a different handle with each entity, which redefines the order in which the entities are regenerated. # # $SORTENTS (280): Controls the object sorting methods (bitcode): # 0 = Disables SORTENTS # 1 = Sorts for object selection # 2 = Sorts for object snap # 4 = Sorts for redraws; obsolete # 8 = Sorts for MSLIDE command slide creation; obsolete # 16 = Sorts for REGEN commands # 32 = Sorts for plotting # 64 = Sorts for PostScript output; obsolete DXFTYPE = 'SORTENTSTABLE' DXFATTRIBS = DXFAttributes(base_class, acdb_sort_ents_table) def __init__(self, doc: 'Drawing' = None): super().__init__(doc) self.table = dict() # type: Dict[str, str] def _copy_data(self, entity: 'SortEntsTable') -> None: entity.tags = dict(entity.table) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace( dxf, acdb_sort_ents_table) try: self.load_table(tags) # Mayfail 房山.dxf except: pass return dxf def load_table(self, tags: 'Tags') -> None: for handle, sort_handle in take2(tags): if handle.code != 331: raise DXFStructureError( 'Invalid handle code {}, expected 331'.format(handle.code)) if sort_handle.code != 5: raise DXFStructureError( 'Invalid sort handle code {}, expected 5'.format( handle.code)) self.table[handle.value] = sort_handle.value def export_entity(self, tagwriter: 'TagWriter') -> None: # base class export is done by parent class super().export_entity(tagwriter) # AcDbEntity export is done by parent class tagwriter.write_tag2(SUBCLASS_MARKER, acdb_sort_ents_table.name) tagwriter.write_tag2(330, self.dxf.block_record_handle) self.export_table(tagwriter) def export_table(self, tagwriter: 'TagWriter'): for handle, sort_handle in self.table.items(): tagwriter.write_tag2(331, handle) tagwriter.write_tag2(5, sort_handle) def __len__(self) -> int: return len(self.table) def __iter__(self) -> Iterable: """ Yields all redraw associations as (object_handle, sort_handle) tuples. """ return iter(self.table.items()) def append(self, handle: str, sort_handle: str) -> None: """ Append redraw association (handle, sort_handle). Args: handle: DXF entity handle (uppercase hex value without leading '0x') sort_handle: sort handle (uppercase hex value without leading '0x') """ self.table[handle] = sort_handle def clear(self): """ Remove all handles from redraw order table. """ self.table = dict() def set_handles(self, handles: Iterable[Tuple[str, str]]) -> None: """ Set all redraw associations from iterable `handles`, after removing all existing associations. Args: handles: iterable yielding (object_handle, sort_handle) tuples """ # The sort_handle doesn't have to be unique, same or all handles can share the same sort_handle and sort_handles # can use existing handles too. # # The '0' handle can be used, but this sort_handle will be drawn as latest (on top of all other entities) and # not as first as expected. Invalid entity handles will be ignored by AutoCAD. self.table = dict(handles) def remove_invalid_handles(self) -> None: """ Remove all handles which do not exists in the drawing database. """ entitydb = self.doc.entitydb self.table = { handle: sort_handle for handle, sort_handle in self.table.items() if handle in entitydb } def remove_handle(self, handle: str) -> None: """ Remove handle of DXF entity from redraw order table. Args: handle: DXF entity handle (uppercase hex value without leading '0x') """ try: del self.table[handle] except KeyError: pass
class SweptSurface(Surface): """ DXF SWEPTSURFACE entity - container entity for embedded ACIS data. """ DXFTYPE = 'SWEPTSURFACE' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_modeler_geometry, acdb_surface, acdb_swept_surface) def __init__(self): super().__init__() self.transformation_matrix_sweep_entity = Matrix44() self.transformation_matrix_path_entity = Matrix44() self.sweep_entity_transformation_matrix = Matrix44() self.path_entity_transformation_matrix = Matrix44() def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: processor.fast_load_dxfattribs(dxf, acdb_swept_surface_group_codes, 4, log=False) self.load_matrices(processor.subclasses[4]) return dxf def load_matrices(self, tags: Tags): self.transformation_matrix_sweep_entity = load_matrix(tags, code=40) self.transformation_matrix_path_entity = load_matrix(tags, code=41) self.sweep_entity_transformation_matrix = load_matrix(tags, code=46) self.path_entity_transformation_matrix = load_matrix(tags, code=47) def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) # AcDbEntity export is done by parent class # AcDbModelerGeometry export is done by parent class tagwriter.write_tag2(SUBCLASS_MARKER, acdb_swept_surface.name) self.dxf.export_dxf_attribs(tagwriter, [ 'swept_entity_id', 'path_entity_id', ]) export_matrix(tagwriter, code=40, matrix=self.transformation_matrix_sweep_entity) export_matrix(tagwriter, code=41, matrix=self.transformation_matrix_path_entity) self.dxf.export_dxf_attribs(tagwriter, [ 'draft_angle', 'draft_start_distance', 'draft_end_distance', 'twist_angle', 'scale_factor', 'align_angle' ]) export_matrix(tagwriter, code=46, matrix=self.sweep_entity_transformation_matrix) export_matrix(tagwriter, code=47, matrix=self.path_entity_transformation_matrix) self.dxf.export_dxf_attribs(tagwriter, [ 'solid', 'sweep_alignment', 'unknown1', 'align_start', 'bank', 'base_point_set', 'sweep_entity_transform_computed', 'path_entity_transform_computed', 'reference_vector_for_controlling_twist' ])
class DXFVertex(DXFGraphic): """ DXF VERTEX entity """ DXFTYPE = 'VERTEX' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_vertex) # Extra vertex created by curve-fitting: EXTRA_VERTEX_CREATED = 1 # Curve-fit tangent defined for this vertex. A curve-fit tangent direction # of 0 may be omitted from the DXF output, but is significant if this bit # is set: CURVE_FIT_TANGENT = 2 # 4 = unused, never set in dxf files # Spline vertex created by spline-fitting SPLINE_VERTEX_CREATED = 8 SPLINE_FRAME_CONTROL_POINT = 16 POLYLINE_3D_VERTEX = 32 POLYGON_MESH_VERTEX = 64 POLYFACE_MESH_VERTEX = 128 FACE_FLAGS = POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX VTX3D = POLYLINE_3D_VERTEX + POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf # VERTEX can have 3 subclasses if representing a `face record` or # 4 subclasses if representing a vertex location, just the last # subclass contains data: processor.load_and_recover_dxfattribs(dxf, acdb_vertex, index=-1) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) if tagwriter.dxfversion > DXF12: if self.is_face_record: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbFaceRecord') else: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbVertex') if self.is_3d_polyline_vertex: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDb3dPolylineVertex') elif self.is_poly_face_mesh_vertex: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbPolyFaceMeshVertex') elif self.is_polygon_mesh_vertex: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbPolygonMeshVertex') else: tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDb2dVertex') self.dxf.export_dxf_attribs(tagwriter, [ 'location', 'start_width', 'end_width', 'bulge', 'flags', 'tangent', 'vtx0', 'vtx1', 'vtx2', 'vtx3', 'vertex_identifier' ]) @property def is_2d_polyline_vertex(self) -> bool: return self.dxf.flags & self.VTX3D == 0 @property def is_3d_polyline_vertex(self) -> bool: return self.dxf.flags & self.POLYLINE_3D_VERTEX @property def is_polygon_mesh_vertex(self) -> bool: return self.dxf.flags & self.POLYGON_MESH_VERTEX @property def is_poly_face_mesh_vertex(self) -> bool: return self.dxf.flags & self.FACE_FLAGS == self.FACE_FLAGS @property def is_face_record(self) -> bool: return (self.dxf.flags & self.FACE_FLAGS) == self.POLYFACE_MESH_VERTEX def transform(self, m: 'Matrix44') -> 'DXFVertex': """ Transform VERTEX entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ if self.is_face_record: return self self.dxf.location = m.transform(self.dxf.location) return self def format(self, format='xyz') -> Sequence: """ Return formatted vertex components as tuple. Format codes: - ``x`` = x-coordinate - ``y`` = y-coordinate - ``z`` = z-coordinate - ``s`` = start width - ``e`` = end width - ``b`` = bulge value - ``v`` = (x, y, z) as tuple Args: format: format string, default is "xyz" .. versionadded:: 0.14 """ dxf = self.dxf v = Vector(dxf.location) x, y, z = v.xyz b = dxf.bulge s = dxf.start_width e = dxf.end_width vars = locals() return tuple(vars[code] for code in format.lower())
class Polyline(LinkedEntities): """ DXF POLYLINE entity """ DXFTYPE = 'POLYLINE' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_polyline) # polyline flags (70) CLOSED = 1 MESH_CLOSED_M_DIRECTION = CLOSED CURVE_FIT_VERTICES_ADDED = 2 SPLINE_FIT_VERTICES_ADDED = 4 POLYLINE_3D = 8 POLYMESH = 16 MESH_CLOSED_N_DIRECTION = 32 POLYFACE = 64 GENERATE_LINETYPE_PATTERN = 128 # polymesh smooth type (75) NO_SMOOTH = 0 QUADRATIC_BSPLINE = 5 CUBIC_BSPLINE = 6 BEZIER_SURFACE = 8 ANY3D = POLYLINE_3D | POLYMESH | POLYFACE @property def vertices(self): return self._sub_entities def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf if processor.r12: processor.load_dxfattribs_into_namespace( dxf, subclass_definition=acdb_polyline, index=0) else: tags = processor.load_dxfattribs_into_namespace( dxf, subclass_definition=acdb_polyline, index=2) name = processor.subclasses[2][0].value if len(tags): tags = processor.recover_graphic_attributes(tags, dxf) if len(tags): # do not log group code 66: attribs follow, not required processor.log_unprocessed_tags( unprocessed_tags=tags.filter((66, )), subclass=name) return dxf def export_dxf(self, tagwriter: 'TagWriter'): """ Export POLYLINE entity and all linked entities: VERTEX, SEQEND. """ super().export_dxf(tagwriter) # export sub-entities self.process_sub_entities(lambda e: e.export_dxf(tagwriter)) def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export POLYLINE specific data as DXF tags. """ super().export_entity(tagwriter) if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, self.get_mode()) tagwriter.write_tag2(66, 1) # Vertices follow self.dxf.export_dxf_attribs(tagwriter, [ 'elevation', 'flags', 'default_start_width', 'default_end_width', 'm_count', 'n_count', 'm_smooth_density', 'n_smooth_density', 'smooth_type', 'thickness', 'extrusion', ]) def on_layer_change(self, layer: str): """ Event handler for layer change. Changes also the layer of all vertices. Args: layer: new layer as string """ for v in self.vertices: v.dxf.layer = layer def on_linetype_change(self, linetype: str): """ Event handler for linetype change. Changes also the linetype of all vertices. Args: linetype: new linetype as string """ for v in self.vertices: v.dxf.linetype = linetype def get_vertex_flags(self) -> int: return const.VERTEX_FLAGS[self.get_mode()] def get_mode(self) -> str: """ Returns POLYLINE type as string: - 'AcDb2dPolyline' - 'AcDb3dPolyline' - 'AcDbPolygonMesh' - 'AcDbPolyFaceMesh' """ if self.is_3d_polyline: return 'AcDb3dPolyline' elif self.is_polygon_mesh: return 'AcDbPolygonMesh' elif self.is_poly_face_mesh: return 'AcDbPolyFaceMesh' else: return 'AcDb2dPolyline' @property def is_2d_polyline(self) -> bool: """ ``True`` if POLYLINE is a 2D polyline. """ return self.dxf.flags & self.ANY3D == 0 @property def is_3d_polyline(self) -> bool: """ ``True`` if POLYLINE is a 3D polyline. """ return bool(self.dxf.flags & self.POLYLINE_3D) @property def is_polygon_mesh(self) -> bool: """ ``True`` if POLYLINE is a polygon mesh, see :class:`Polymesh` """ return bool(self.dxf.flags & self.POLYMESH) @property def is_poly_face_mesh(self) -> bool: """ ``True`` if POLYLINE is a poly face mesh, see :class:`Polyface` """ return bool(self.dxf.flags & self.POLYFACE) @property def is_closed(self) -> bool: """ ``True`` if POLYLINE is closed. """ return bool(self.dxf.flags & self.CLOSED) @property def is_m_closed(self) -> bool: """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in m direction. """ return bool(self.dxf.flags & self.MESH_CLOSED_M_DIRECTION) @property def is_n_closed(self) -> bool: """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in n direction. """ return bool(self.dxf.flags & self.MESH_CLOSED_N_DIRECTION) @property def has_arc(self) -> bool: """ Returns ``True`` if 2D POLYLINE has an arc segment. """ if self.is_2d_polyline: return any( v.dxf.hasattr('bulge') and bool(v.dxf.bulge) for v in self.vertices) else: return False @property def has_width(self) -> bool: """ Returns ``True`` if 2D POLYLINE has default width values or any segment with width attributes. .. versionadded:: 0.14 """ if self.is_2d_polyline: if self.dxf.hasattr('default_start_width') and bool( self.dxf.default_start_width): return True if self.dxf.hasattr('default_end_width') and bool( self.dxf.default_end_width): return True for v in self.vertices: if v.dxf.hasattr('start_width') and bool(v.dxf.start_width): return True if v.dxf.hasattr('end_width') and bool(v.dxf.end_width): return True return False def m_close(self, status=True) -> None: """ Close POLYMESH in m direction if `status` is ``True`` (also closes POLYLINE), clears closed state if `status` is ``False``. """ self.set_flag_state(self.MESH_CLOSED_M_DIRECTION, status, name='flags') def n_close(self, status=True) -> None: """ Close POLYMESH in n direction if `status` is ``True``, clears closed state if `status` is ``False``. """ self.set_flag_state(self.MESH_CLOSED_N_DIRECTION, status, name='flags') def close(self, m_close=True, n_close=False) -> None: """ Set closed state of POLYMESH and POLYLINE in m direction and n direction. ``True`` set closed flag, ``False`` clears closed flag. """ self.m_close(m_close) self.n_close(n_close) def __len__(self) -> int: """ Returns count of :class:`Vertex` entities. """ return len(self.vertices) def __getitem__(self, pos) -> 'DXFVertex': """ Get :class:`Vertex` entity at position `pos`, supports ``list`` slicing. """ return self.vertices[pos] def points(self) -> Iterable[Vector]: """ Returns iterable of all polyline vertices as ``(x, y, z)`` tuples, not as :class:`Vertex` objects. """ return (vertex.dxf.location for vertex in self.vertices) def _append_vertex(self, vertex: 'DXFVertex') -> None: self.vertices.append(vertex) def append_vertices(self, points: Iterable['Vertex'], dxfattribs: Dict = None) -> None: """ Append multiple :class:`Vertex` entities at location `points`. Args: points: iterable of ``(x, y[, z])`` tuples dxfattribs: dict of DXF attributes for :class:`Vertex` class """ dxfattribs = dxfattribs or {} for vertex in self._build_dxf_vertices(points, dxfattribs): self._append_vertex(vertex) def append_formatted_vertices(self, points: Iterable['Vertex'], format: str = 'xy', dxfattribs: Dict = None) -> None: """ Append multiple :class:`Vertex` entities at location `points`. Args: points: iterable of (x, y, [start_width, [end_width, [bulge]]]) tuple format: format string, default is ``'xy'``, see: :ref:`format codes` dxfattribs: dict of DXF attributes for :class:`Vertex` class """ dxfattribs = dxfattribs or {} dxfattribs['flags'] = (dxfattribs.get('flags', 0) | self.get_vertex_flags()) # same DXF attributes for VERTEX entities as for POLYLINE dxfattribs['owner'] = self.dxf.owner dxfattribs['layer'] = self.dxf.layer if self.dxf.hasattr('linetype'): dxfattribs['linetype'] = self.dxf.linetype for point in points: attribs = vertex_attribs(point, format) attribs.update(dxfattribs) vertex = self._new_compound_entity('VERTEX', attribs) self._append_vertex(vertex) def append_vertex(self, point: 'Vertex', dxfattribs: dict = None) -> None: """ Append a single :class:`Vertex` entity at location `point`. Args: point: as ``(x, y[, z])`` tuple dxfattribs: dict of DXF attributes for :class:`Vertex` class """ dxfattribs = dxfattribs or {} for vertex in self._build_dxf_vertices([point], dxfattribs): self._append_vertex(vertex) def insert_vertices(self, pos: int, points: Iterable['Vertex'], dxfattribs: dict = None) -> None: """ Insert vertices `points` into :attr:`Polyline.vertices` list at insertion location `pos` . Args: pos: insertion position of list :attr:`Polyline.vertices` points: list of ``(x, y[, z])`` tuples dxfattribs: dict of DXF attributes for :class:`Vertex` class """ dxfattribs = dxfattribs or {} self.vertices[pos:pos] = list( self._build_dxf_vertices(points, dxfattribs)) def _build_dxf_vertices(self, points: Iterable['Vertex'], dxfattribs: dict) -> List['DXFVertex']: """ Converts point (x, y, z)-tuples into DXFVertex objects. Args: points: list of (x, y, z)-tuples dxfattribs: dict of DXF attributes """ dxfattribs['flags'] = (dxfattribs.get('flags', 0) | self.get_vertex_flags()) # same DXF attributes for VERTEX entities as for POLYLINE dxfattribs['owner'] = self.dxf.owner dxfattribs['layer'] = self.dxf.layer if self.dxf.hasattr('linetype'): dxfattribs['linetype'] = self.dxf.linetype for point in points: dxfattribs['location'] = Vector(point) yield self._new_compound_entity('VERTEX', dxfattribs) def cast(self) -> Union['Polyline', 'Polymesh', 'Polyface']: mode = self.get_mode() if mode == 'AcDbPolyFaceMesh': return Polyface.from_polyline(self) elif mode == 'AcDbPolygonMesh': return Polymesh.from_polyline(self) else: return self def transform(self, m: Matrix44) -> 'Polyline': """ Transform POLYLINE entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ def _ocs_locations(elevation): for vertex in self.vertices: location = vertex.dxf.location if elevation is not None: # Older DXF version may not have written the z-axis, which # is now 0 by default in ezdxf, so replace existing z-axis # by elevation value. location = location.replace(z=elevation) yield location if self.is_2d_polyline: dxf = self.dxf ocs = OCSTransform(self.dxf.extrusion, m) if not ocs.scale_uniform and self.has_arc: # Parent function has to catch this Exception and explode this # 2D POLYLINE into LINE and ELLIPSE entities. raise NonUniformScalingError( '2D POLYLINE with arcs does not support non uniform scaling' ) if dxf.hasattr('elevation'): z_axis = dxf.elevation.z else: z_axis = None vertices = [ ocs.transform_vertex(vertex) for vertex in _ocs_locations(z_axis) ] # All vertices of a 2D polyline have the same z-axis: if vertices: dxf.elevation = vertices[0].replace(x=0, y=0) for vertex, location in zip(self.vertices, vertices): vertex.dxf.location = location if dxf.hasattr('thickness'): dxf.thickness = ocs.transform_length((0, 0, dxf.thickness)) dxf.extrusion = ocs.new_extrusion else: for vertex in self.vertices: vertex.transform(m) return self def explode(self, target_layout: 'BaseLayout' = None) -> 'EntityQuery': """ Explode POLYLINE as DXF LINE, ARC or 3DFACE primitives into target layout, if the target layout is ``None``, the target layout is the layout of the POLYLINE entity . Returns an :class:`~ezdxf.query.EntityQuery` container including all DXF primitives. Args: target_layout: target layout for DXF primitives, ``None`` for same layout as source entity. """ return explode_entity(self, target_layout) def virtual_entities(self) -> Iterable[Union['Line', 'Arc', 'Face3d']]: """ Yields 'virtual' parts of POLYLINE as LINE, ARC or 3DFACE primitives. This entities are located at the original positions, but are not stored in the entity database, have no handle and are not assigned to any layout. """ return virtual_polyline_entities(self) def audit(self, auditor: 'Auditor') -> None: """ Audit and repair POLYLINE entity. """ def audit_sub_entity(entity): entity.doc = doc # grant same document dxf = entity.dxf if dxf.owner != owner: dxf.owner = owner if dxf.layer != layer: dxf.layer = layer doc = self.doc owner = self.dxf.handle layer = self.dxf.layer for vertex in self.vertices: audit_sub_entity(vertex) seqend = self.seqend if seqend: audit_sub_entity(seqend) elif doc: self.new_seqend() auditor.fixed_error( code=AuditError.MISSING_REQUIRED_SEQEND, message=f'Create required SEQEND entity for {str(self)}.', dxf_entity=self, )
class MText(DXFGraphic): """ DXF MTEXT entity """ DXFTYPE = 'MTEXT' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mtext) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 UNDERLINE_START = r'\L' UNDERLINE_STOP = r'\l' UNDERLINE = UNDERLINE_START + '%s' + UNDERLINE_STOP OVERSTRIKE_START = r'\O' OVERSTRIKE_STOP = r'\o' OVERSTRIKE = OVERSTRIKE_START + '%s' + OVERSTRIKE_STOP STRIKE_START = r'\K' STRIKE_STOP = r'\k' STRIKE = STRIKE_START + '%s' + STRIKE_STOP NEW_LINE = r'\P' GROUP_START = '{' GROUP_END = '}' GROUP = GROUP_START + '%s' + GROUP_END NBSP = r'\~' # non breaking space def __init__(self): """ Default constructor """ super().__init__() self.text: str = "" def _copy_data(self, entity: 'DXFEntity') -> None: """ Copy entity data: text """ entity.text = self.text def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: self.load_mtext(processor.subclasses[2]) processor.load_and_recover_dxfattribs(dxf, acdb_mtext) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mtext.name) self.dxf.export_dxf_attribs(tagwriter, [ 'insert', 'char_height', 'width', 'defined_height', 'attachment_point', 'flow_direction', ]) self.export_mtext(tagwriter) self.dxf.export_dxf_attribs(tagwriter, [ 'style', 'extrusion', 'text_direction', 'rect_width', 'rect_height', 'rotation', 'line_spacing_style', 'line_spacing_factor', 'box_fill_scale', 'bg_fill', 'bg_fill_color', 'bg_fill_true_color', 'bg_fill_color_name', 'bg_fill_transparency', ]) def load_mtext(self, tags: Tags) -> None: tail = "" parts = [] for tag in tags: if tag.code == 1: tail = tag.value if tag.code == 3: parts.append(tag.value) parts.append(tail) self.text = _dxf_escape_line_endings(caret_decode("".join(parts))) tags.remove_tags((1, 3)) def export_mtext(self, tagwriter: 'TagWriter') -> None: txt = _dxf_escape_line_endings(self.text) str_chunks = split_mtext_string(txt, size=250) if len(str_chunks) == 0: str_chunks.append("") while len(str_chunks) > 1: tagwriter.write_tag2(3, str_chunks.pop(0)) tagwriter.write_tag2(1, str_chunks[0]) def get_rotation(self) -> float: """ Get text rotation in degrees, independent if it is defined by :attr:`dxf.rotation` or :attr:`dxf.text_direction`. """ if self.dxf.hasattr('text_direction'): vector = self.dxf.text_direction radians = math.atan2(vector[1], vector[0]) # ignores z-axis rotation = math.degrees(radians) else: rotation = self.dxf.get('rotation', 0) return rotation def set_rotation(self, angle: float) -> 'MText': """ Set attribute :attr:`rotation` to `angle` (in degrees) and deletes :attr:`dxf.text_direction` if present. """ # text_direction has higher priority than rotation, therefore delete it self.dxf.discard('text_direction') self.dxf.rotation = angle return self # fluent interface def set_location(self, insert: 'Vertex', rotation: float = None, attachment_point: int = None) -> 'MText': """ Set attributes :attr:`dxf.insert`, :attr:`dxf.rotation` and :attr:`dxf.attachment_point`, ``None`` for :attr:`dxf.rotation` or :attr:`dxf.attachment_point` preserves the existing value. """ self.dxf.insert = Vector(insert) if rotation is not None: self.set_rotation(rotation) if attachment_point is not None: self.dxf.attachment_point = attachment_point return self # fluent interface def set_bg_color(self, color: Union[int, str, Tuple[int, int, int], None], scale: float = 1.5): """ Set background color as :ref:`ACI` value or as name string or as RGB tuple ``(r, g, b)``. Use special color name ``canvas``, to set background color to canvas background color. Args: color: color as :ref:`ACI`, string or RGB tuple scale: determines how much border there is around the text, the value is based on the text height, and should be in the range of [1, 5], where 1 fits exact the MText entity. """ if 1 <= scale <= 5: self.dxf.box_fill_scale = scale else: raise ValueError('argument scale has to be in range from 1 to 5.') if color is None: self.dxf.discard('bg_fill') self.dxf.discard('box_fill_scale') self.dxf.discard('bg_fill_color') self.dxf.discard('bg_fill_true_color') self.dxf.discard('bg_fill_color_name') elif color == 'canvas': # special case for use background color self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR self.dxf.bg_fill_color = 0 # required but ignored else: self.dxf.bg_fill = const.MTEXT_BG_COLOR if isinstance(color, int): self.dxf.bg_fill_color = color elif isinstance(color, str): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_color_name = color elif isinstance(color, tuple): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_true_color = rgb2int(color) return self # fluent interface def __iadd__(self, text: str) -> 'MText': """ Append `text` to existing content (:attr:`.text` attribute). """ self.text += text return self append = __iadd__ def set_font(self, name: str, bold: bool = False, italic: bool = False, codepage: int = 1252, pitch: int = 0) -> None: """ Append font change (e.g. ``'\\Fkroeger|b0|i0|c238|p10'`` ) to existing content (:attr:`.text` attribute). Args: name: font name bold: flag italic: flag codepage: character codepage pitch: font size """ s = rf"\F{name}|b{int(bold)}|i{int(italic)}|c{codepage}|p{pitch};" self.append(s) def set_color(self, color_name: str) -> None: """ Append text color change to existing content, `color_name` as ``red``, ``yellow``, ``green``, ``cyan``, ``blue``, ``magenta`` or ``white``. """ self.append(r"\C%d" % const.MTEXT_COLOR_INDEX[color_name.lower()]) def add_stacked_text(self, upr: str, lwr: str, t: str = '^') -> None: r""" Add stacked text `upr` over `lwr`, `t` defines the kind of stacking: .. code-block:: none "^": vertical stacked without divider line, e.g. \SA^B: A B "/": vertical stacked with divider line, e.g. \SX/Y: X - Y "#": diagonal stacked, with slanting divider line, e.g. \S1#4: 1/4 """ # space ' ' in front of {lwr} is important self.append(r'\S{upr}{t} {lwr};'.format(upr=upr, lwr=lwr, t=t)) def transform(self, m: Matrix44) -> 'MText': """ Transform MTEXT entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ dxf = self.dxf old_extrusion = Vector(dxf.extrusion) new_extrusion, _ = transform_extrusion(old_extrusion, m) if dxf.hasattr('rotation') and not dxf.hasattr('text_direction'): # MTEXT is not an OCS entity, but I don't know how else to convert # a rotation angle for an entity just defined by an extrusion vector. # It's correct for the most common case: extrusion=(0, 0, 1) ocs = OCS(old_extrusion) dxf.text_direction = ocs.to_wcs(Vector.from_deg_angle( dxf.rotation)) dxf.discard('rotation') old_text_direction = Vector(dxf.text_direction) new_text_direction = m.transform_direction(old_text_direction) old_char_height_vec = old_extrusion.cross( old_text_direction).normalize(dxf.char_height) new_char_height_vec = m.transform_direction(old_char_height_vec) oblique = new_text_direction.angle_between(new_char_height_vec) dxf.char_height = new_char_height_vec.magnitude * math.sin(oblique) if dxf.hasattr('width'): width_vec = old_text_direction.normalize(dxf.width) dxf.width = m.transform_direction(width_vec).magnitude dxf.insert = m.transform(dxf.insert) dxf.text_direction = new_text_direction dxf.extrusion = new_extrusion return self def plain_text(self, split=False) -> Union[List[str], str]: """ Returns text content without formatting codes. Args: split: returns list of strings splitted at line breaks if ``True`` else returns a single string. """ return plain_mtext(self.text, split=split) def audit(self, auditor: 'Auditor'): """ Validity check. """ super().audit(auditor) auditor.check_text_style(self)
class DXFEntity: """ Common super class for all DXF entities. """ DXFTYPE = 'DXFENTITY' # storing as class var needs less memory DXFATTRIBS = DXFAttributes(base_class) # DXF attribute definitions # Default DXF attributes are set at instantiating a new object, the the # difference to attribute default values is, that this attributes are # really set, this means there is an real object in the dxf namespace # defined, where default attribute values get returned on access without # an existing object in the dxf namespace. DEFAULT_ATTRIBS: Dict = {} MIN_DXF_VERSION_FOR_EXPORT = const.DXF12 def __init__(self): """ Default constructor. (internal API)""" # Public attributes for package users self.doc: Optional[Drawing] = None self.dxf: DXFNamespace = DXFNamespace(entity=self) # None public attributes for package users # create extended data only if needed: self.appdata: Optional[AppData] = None self.reactors: Optional[Reactors] = None self.extension_dict: Optional[ExtensionDict] = None self.xdata: Optional[XData] = None # TODO: remove embedded_objects - no need to waste memory for every entity, # this is a seldom used feature (ATTRIB, ATTDEF), and this entities have to # manage the embedded objects by itself at loading stage and DXF export. # Removing is possible if ATTRIB and ATTDEF have explicit # support for embedded MTEXT objects self.embedded_objects: Optional[EmbeddedObjects] = None self.proxy_graphic: Optional[bytes] = None # self._uuid # uuid generated at first request @property def uuid(self) -> uuid.UUID: """ Returns an UUID, which allows to distinguish even virtual entities without a handle. This UUID will be created at the first request. """ uuid_ = getattr(self, '_uuid', None) if uuid_ is None: uuid_ = uuid.uuid4() self._uuid = uuid_ return uuid_ @classmethod def new(cls: Type[T], handle: str = None, owner: str = None, dxfattribs: Dict = None, doc: 'Drawing' = None) -> T: """ Constructor for building new entities from scratch by ezdxf. NEW process: This is a trusted environment where everything is under control of ezdxf respectively the package-user, it is okay to raise exception to show implementation errors in ezdxf or usage errors of the package-user. The :attr:`Drawing.is_loading` flag can be checked to distinguish the NEW and the LOAD process. Args: handle: unique DXF entity handle or None owner: owner handle if entity has an owner else None or '0' dxfattribs: DXF attributes doc: DXF document (internal API) """ entity = cls() entity.doc = doc entity.dxf.handle = handle entity.dxf.owner = owner attribs = dict(cls.DEFAULT_ATTRIBS) attribs.update(dxfattribs or {}) entity.update_dxf_attribs(attribs) # Only this method triggers the post_new_hook() entity.post_new_hook() return entity def post_new_hook(self): """ Post processing and integrity validation after entity creation. Called only if created by ezdxf (see :meth:`DXFEntity.new`), not if loaded from an external source. (internal API) """ pass def post_bind_hook(self): """ Post processing and integrity validation after binding entity to a DXF Document. This method is triggered by the :func:`factory.bind` function only when the entity was created by ezdxf. If the entity was loaded in the 1st loading stage, the :func:`factory.load` functions also calls the :func:`factory.bind` to bind entities to the loaded document, but not all entities are loaded at this time. To avoid problems this method will not be called when loading content from DXF file, but :meth:`post_load_hook` will be triggered for loaded entities at a later and safer point in time. (internal API) """ pass @classmethod def load(cls: Type[T], tags: ExtendedTags, doc: 'Drawing' = None) -> T: """ Constructor to generate entities loaded from an external source. LOAD process: This is an untrusted environment where valid structure are not guaranteed and errors should be fixed, because the package-user is not responsible for the problems and also can't fix them, raising exceptions should only be done for unrecoverable issues. Log fixes for debugging! Be more like BricsCAD and not as mean as AutoCAD! The :attr:`Drawing.is_loading` flag can be checked to distinguish the NEW and the LOAD process. Args: tags: DXF tags as :class:`ExtendedTags` doc: DXF Document (internal API) """ # This method does not trigger the post_new_hook() entity = cls() entity.doc = doc dxfversion = doc.dxfversion if doc else None entity.load_tags(tags, dxfversion=dxfversion) return entity def load_tags(self, tags: ExtendedTags, dxfversion: str = None) -> None: """ Generic tag loading interface, called if DXF document is loaded from external sources. 1. Loading stage which set the basic DXF attributes, additional resources (DXF objects) are not loaded yet. References to these resources have to be stored as handles and can be resolved in the 2. Loading stage: :meth:`post_load_hook`. (internal API) """ if tags: if len(tags.appdata): self.setup_app_data(tags.appdata) if len(tags.xdata): self.xdata = XData(tags.xdata) if tags.embedded_objects: # TODO: remove self.embedded_objects = EmbeddedObjects( tags.embedded_objects) processor = SubclassProcessor(tags, dxfversion=dxfversion) self.dxf = self.load_dxf_attribs(processor) def load_dxf_attribs( self, processor: SubclassProcessor = None) -> DXFNamespace: """ Load DXF attributes into DXF namespace. """ return DXFNamespace(processor, self) def post_load_hook(self, doc: 'Drawing') -> Optional[Callable]: """ The 2nd loading stage when loading DXF documents from an external source, for the 1st loading stage see :meth:`load_tags`. This stage is meant to convert resource handles into :class:`DXFEntity` objects. This is an untrusted environment where valid structure are not guaranteed, raise exceptions only for unrecoverable structure errors and fix everything else. Log fixes for debugging! Some fixes can not be applied at this stage, because some structures like the OBJECTS section are not initialized, in this case return a callable, which will be executed after the DXF document is fully initialized, for an example see :class:`Image`. Triggered in method: :meth:`Drawing._2nd_loading_stage` Examples for two stage loading: Image, Underlay, DXFGroup, Dictionary, Dimstyle, MText """ if self.extension_dict is not None: self.extension_dict.load_resources(doc) return None @classmethod def from_text(cls: Type[T], text: str, doc: 'Drawing' = None) -> T: """ Load constructor from text for testing. (internal API)""" return cls.load(ExtendedTags.from_text(text), doc) @classmethod def shallow_copy(cls: Type[T], other: 'DXFEntity') -> T: """ Copy constructor for type casting e.g. Polyface and Polymesh. (internal API) """ entity = cls() entity.doc = other.doc entity.dxf = other.dxf entity.extension_dict = other.extension_dict entity.reactors = other.reactors entity.appdata = other.appdata entity.xdata = other.xdata entity.embedded_objects = other.embedded_objects # todo: remove entity.proxy_graphic = other.proxy_graphic entity.dxf.rewire(entity) return entity def copy(self: T) -> T: """ Returns a copy of `self` but without handle, owner and reactors. This copy is NOT stored in the entity database and does NOT reside in any layout, block, table or objects section! Extension dictionary and reactors are not copied. Don't use this function to duplicate DXF entities in drawing, use :meth:`EntityDB.duplicate_entity` instead for this task. Copying is not trivial, because of linked resources and the lack of documentation how to handle this linked resources: extension dictionary, handles in appdata, xdata or embedded objects. (internal API) """ entity = self.__class__() entity.doc = self.doc # copy and bind dxf namespace to new entity entity.dxf = self.dxf.copy(entity) entity.dxf.reset_handles() # Do not copy extension dict: if the extension dict should be copied # in the future - a deep copy is maybe required! entity.extension_dict = None # Do not copy reactors: entity.reactors = None entity.proxy_graphic = self.proxy_graphic # immutable bytes # if appdata contains handles, they are treated as shared resources entity.appdata = copy.deepcopy(self.appdata) # if xdata contains handles, they are treated as shared resources entity.xdata = copy.deepcopy(self.xdata) # if embedded objects contains handles, they are treated as shared resources entity.embedded_objects = copy.deepcopy(self.embedded_objects) # todo: remove self._copy_data(entity) return entity def _copy_data(self, entity: 'DXFEntity') -> None: """ Copy entity data like vertices or attribs and store the copies into the entity database. (internal API) """ pass def __deepcopy__(self, memodict: Dict = None): """ Some entities maybe linked by more than one entity, to be safe use `memodict` for bookkeeping. (internal API) """ memodict = memodict or {} try: return memodict[id(self)] except KeyError: copy = self.copy() memodict[id(self)] = copy return copy def update_dxf_attribs(self, dxfattribs: Dict) -> None: """ Set DXF attributes by a ``dict`` like :code:`{'layer': 'test', 'color': 4}`. """ setter = self.dxf.set for key, value in dxfattribs.items(): setter(key, value) def setup_app_data(self, appdata: List[Tags]) -> None: """ Setup data structures from APP data. (internal API) """ for data in appdata: code, appid = data[0] if appid == const.ACAD_REACTORS: self.reactors = Reactors.from_tags(data) elif appid == const.ACAD_XDICTIONARY: self.extension_dict = ExtensionDict.from_tags(data) else: self.set_app_data(appid, data) def update_handle(self, handle: str) -> None: """ Update entity handle. (internal API) """ self.dxf.handle = handle if self.extension_dict: self.extension_dict.update_owner(handle) @property def is_alive(self): """ Returns ``False`` if entity has been deleted. """ return hasattr(self, 'dxf') @property def is_virtual(self): """ Returns ``True`` if entity is a virtual entity. """ return self.doc is None or self.dxf.handle is None @property def is_bound(self): """ Returns ``True`` if entity is bound to DXF document. """ if self.is_alive and not self.is_virtual: return factory.is_bound(self, self.doc) return False def get_dxf_attrib(self, key: str, default: Any = None) -> Any: """ Get DXF attribute `key`, returns `default` if key doesn't exist, or raise :class:`DXFValueError` if `default` is :class:`DXFValueError` and no DXF default value is defined:: layer = entity.get_dxf_attrib("layer") # same as layer = entity.dxf.layer Raises :class:`DXFAttributeError` if `key` is not an supported DXF attribute. """ return self.dxf.get(key, default) def set_dxf_attrib(self, key: str, value: Any) -> None: """ Set new `value` for DXF attribute `key`:: entity.set_dxf_attrib("layer", "MyLayer") # same as entity.dxf.layer = "MyLayer" Raises :class:`DXFAttributeError` if `key` is not an supported DXF attribute. """ self.dxf.set(key, value) def del_dxf_attrib(self, key: str) -> None: """ Delete DXF attribute `key`, does not raise an error if attribute is supported but not present. Raises :class:`DXFAttributeError` if `key` is not an supported DXF attribute. """ self.dxf.discard(key) def has_dxf_attrib(self, key: str) -> bool: """ Returns ``True`` if DXF attribute `key` really exist. Raises :class:`DXFAttributeError` if `key` is not an supported DXF attribute. """ return self.dxf.hasattr(key) dxf_attrib_exists = has_dxf_attrib def is_supported_dxf_attrib(self, key: str) -> bool: """ Returns ``True`` if DXF attrib `key` is supported by this entity. Does not grant that attribute `key` really exist. """ if key in self.DXFATTRIBS: if self.doc: return self.doc.dxfversion >= self.DXFATTRIBS.get( key).dxfversion else: return True else: return False def dxftype(self) -> str: """ Get DXF type as string, like ``LINE`` for the line entity. """ return self.DXFTYPE 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) def dxfattribs(self, drop: Set[str] = None) -> Dict: """ Returns a ``dict`` with all existing DXF attributes and their values and exclude all DXF attributes listed in set `drop`. """ all_attribs = self.dxf.all_existing_dxf_attribs() if drop: return {k: v for k, v in all_attribs.items() if k not in drop} else: return all_attribs def set_flag_state(self, flag: int, state: bool = True, name: str = 'flags') -> None: """ Set binary coded `flag` of DXF attribute `name` to ``1`` (on) if `state` is ``True``, set `flag` to ``0`` (off) if `state` is ``False``. """ flags = self.dxf.get(name, 0) self.dxf.set(name, set_flag_state(flags, flag, state=state)) def get_flag_state(self, flag: int, name: str = 'flags') -> bool: """ Returns ``True`` if any `flag` of DXF attribute is ``1`` (on), else ``False``. Always check only one flag state at the time. """ return bool(self.dxf.get(name, 0) & flag) def remove_dependencies(self, other: 'Drawing' = None): """ Remove all dependencies from current document. Intended usage is to remove dependencies from the current document to move or copy the entity to `other` DXF document. An error free call of this method does NOT guarantee that this entity can be moved/copied to the `other` document, some entities like DIMENSION have too much dependencies to a document to move or copy them, but to check this is not the domain of this method! (internal API) """ if self.is_alive: self.dxf.owner = None self.dxf.handle = None self.reactors = None self.extension_dict = None self.appdata = None self.xdata = None self.embedded_objects = None # todo: remove def destroy(self) -> None: """ Delete all data and references. Does not delete entity from structures like layouts or groups. Starting with `ezdxf` v0.14 this method could be used to delete entities. (internal API) """ if not self.is_alive: return if self.extension_dict is not None: self.extension_dict.destroy() del self.extension_dict del self.appdata del self.reactors del self.xdata del self.embedded_objects # todo: remove del self.doc del self.dxf # check mark for is_alive def preprocess_export(self, tagwriter: 'TagWriter') -> bool: """ Pre requirement check and pre processing for export. Returns False if entity should not be exported at all. (internal API) """ return True def export_dxf(self, tagwriter: 'TagWriter') -> None: """ Export DXF entity by `tagwriter`. This is the first key method for exporting DXF entities: - has to know the group codes for each attribute - has to add subclass tags in correct order - has to integrate extended data: ExtensionDict, Reactors, AppData - has to maintain the correct tag order (because sometimes order matters) (internal API) """ if tagwriter.dxfversion < self.MIN_DXF_VERSION_FOR_EXPORT: return if not self.preprocess_export(tagwriter): return # ! first step ! # write handle, AppData, Reactors, ExtensionDict, owner self.export_base_class(tagwriter) # this is the entity specific part self.export_entity(tagwriter) # ! Last step ! # write xdata, embedded objects self.export_embedded_objects(tagwriter) self.export_xdata(tagwriter) def export_base_class(self, tagwriter: 'TagWriter') -> None: """ Export base class DXF attributes and structures. (internal API) """ dxftype = self.DXFTYPE _handle_code = 105 if dxftype == 'DIMSTYLE' else 5 # 1. tag: (0, DXFTYPE) tagwriter.write_tag2(const.STRUCTURE_MARKER, dxftype) if tagwriter.dxfversion >= const.DXF2000: tagwriter.write_tag2(_handle_code, self.dxf.handle) if self.appdata: self.appdata.export_dxf(tagwriter) if self.has_extension_dict: self.extension_dict.export_dxf(tagwriter) if self.reactors: self.reactors.export_dxf(tagwriter) tagwriter.write_tag2(const.OWNER_CODE, self.dxf.owner) else: # DXF R12 if tagwriter.write_handles: tagwriter.write_tag2(_handle_code, self.dxf.handle) # do not write owner handle - not supported by DXF R12 def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export DXF entity specific data by `tagwriter`. This is the second key method for exporting DXF entities: - has to know the group codes for each attribute - has to add subclass tags in correct order - has to maintain the correct tag order (because sometimes order matters) (internal API) """ # base class (handle, appid, reactors, xdict, owner) export is done by parent class pass # xdata and embedded objects export is also done by parent def export_xdata(self, tagwriter: 'TagWriter') -> None: """ Export DXF XDATA by `tagwriter`. (internal API)""" if self.xdata: self.xdata.export_dxf(tagwriter) def export_embedded_objects(self, tagwriter: 'TagWriter') -> None: """ Export embedded objects by `tagwriter`. (internal API)""" if self.embedded_objects: # todo: remove self.embedded_objects.export_dxf(tagwriter) # todo: remove def audit(self, auditor: 'Auditor') -> None: """ Validity check. (internal API) """ # Important: do not check owner handle! -> DXFGraphic(), DXFObject() # check app data # check reactors # check extension dict # check XDATA @property def has_extension_dict(self) -> bool: """ Returns ``True`` if entity has an attached :class:`~ezdxf.entities.xdict.ExtensionDict`. """ xdict = self.extension_dict # Don't use None check: bool(xdict) for an empty extension dict is False if xdict is not None and xdict.is_alive: # Check the associated Dictionary object dictionary = xdict.dictionary if isinstance(dictionary, str): # just a handle string - SUT return True else: return dictionary.is_alive return False def get_extension_dict(self) -> 'ExtensionDict': """ Returns the existing :class:`~ezdxf.entities.xdict.ExtensionDict`. Raises: AttributeError: extension dict does not exist """ if self.has_extension_dict: return self.extension_dict else: raise AttributeError('Entity has no extension dictionary.') def new_extension_dict(self) -> 'ExtensionDict': self.extension_dict = ExtensionDict.new(self.dxf.handle, self.doc) return self.extension_dict def has_app_data(self, appid: str) -> bool: """ Returns ``True`` if application defined data for `appid` exist. """ if self.appdata: return appid in self.appdata else: return False def get_app_data(self, appid: str) -> Tags: """ Returns application defined data for `appid`. Args: appid: application name as defined in the APPID table. Raises: DXFValueError: no data for `appid` found """ if self.appdata: return self.appdata.get(appid)[1:-1] else: raise const.DXFValueError(appid) def set_app_data(self, appid: str, tags: Iterable) -> None: """ Set application defined data for `appid` as iterable of tags. Args: appid: application name as defined in the APPID table. tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` """ if self.appdata is None: self.appdata = AppData() self.appdata.add(appid, tags) def discard_app_data(self, appid: str): """ Discard application defined data for `appid`. Does not raise an exception if no data for `appid` exist. """ if self.appdata: self.appdata.discard(appid) def has_xdata(self, appid: str) -> bool: """ Returns ``True`` if extended data for `appid` exist. """ if self.xdata: return appid in self.xdata else: return False def get_xdata(self, appid: str) -> Tags: """ Returns extended data for `appid`. Args: appid: application name as defined in the APPID table. Raises: DXFValueError: no extended data for `appid` found """ if self.xdata: return Tags(self.xdata.get(appid)[1:]) else: raise const.DXFValueError(appid) def set_xdata(self, appid: str, tags: Iterable) -> None: """ Set extended data for `appid` as iterable of tags. Args: appid: application name as defined in the APPID table. tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` """ if self.xdata is None: self.xdata = XData() self.xdata.add(appid, tags) def discard_xdata(self, appid: str) -> None: """ Discard extended data for `appid`. Does not raise an exception if no extended data for `appid` exist. """ if self.xdata: self.xdata.discard(appid) def has_xdata_list(self, appid: str, name: str) -> bool: """ Returns ``True`` if a tag list `name` for extended data `appid` exist. """ if self.has_xdata(appid): return self.xdata.has_xlist(appid, name) else: return False def get_xdata_list(self, appid: str, name: str) -> Tags: """ Returns tag list `name` for extended data `appid`. Args: appid: application name as defined in the APPID table. name: extended data list name Raises: DXFValueError: no extended data for `appid` found or no data list `name` not found """ if self.xdata: return Tags(self.xdata.get_xlist(appid, name)) else: raise const.DXFValueError(appid) def set_xdata_list(self, appid: str, name: str, tags: Iterable) -> None: """ Set tag list `name` for extended data `appid` as iterable of tags. Args: appid: application name as defined in the APPID table. name: extended data list name tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` """ if self.xdata is None: self.xdata = XData() self.xdata.set_xlist(appid, name, tags) def discard_xdata_list(self, appid: str, name: str) -> None: """ Discard tag list `name` for extended data `appid`. Does not raise an exception if no extended data for `appid` or no tag list `name` exist. """ if self.xdata: self.xdata.discard_xlist(appid, name) def replace_xdata_list(self, appid: str, name: str, tags: Iterable) -> None: """ Replaces tag list `name` for existing extended data `appid` by `tags`. Appends new list if tag list `name` do not exist, but raises :class:`DXFValueError` if extended data `appid` do not exist. Args: appid: application name as defined in the APPID table. name: extended data list name tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` Raises: DXFValueError: no extended data for `appid` found """ self.xdata.replace_xlist(appid, name, tags) def has_reactors(self) -> bool: """ Returns ``True`` if entity has reactors. """ return bool(self.reactors) def get_reactors(self) -> List[str]: """ Returns associated reactors as list of handles. """ return self.reactors.get() if self.reactors else [] def set_reactors(self, handles: Iterable[str]) -> None: """ Set reactors as list of handles. """ if self.reactors is None: self.reactors = Reactors() self.reactors.set(handles) def append_reactor_handle(self, handle: str) -> None: """ Append `handle` to reactors. """ if self.reactors is None: self.reactors = Reactors() self.reactors.add(handle) def discard_reactor_handle(self, handle: str) -> None: """ Discard `handle` from reactors. Does not raise an exception if `handle` does not exist. """ if self.reactors: self.reactors.discard(handle)
class Ellipse(DXFGraphic): """ DXF ELLIPSE entity """ DXFTYPE = 'ELLIPSE' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_ellipse) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace(dxf, acdb_ellipse) if len(tags): processor.log_unprocessed_tags(tags, subclass=acdb_ellipse.name) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) # AcDbEntity export is done by parent class tagwriter.write_tag2(SUBCLASS_MARKER, acdb_ellipse.name) # AutoCAD does not accept a ratio < 1e-6 -> invalid DXF file self.dxf.ratio = max(self.dxf.ratio, 1e-6) self.dxf.export_dxf_attribs(tagwriter, [ 'center', 'major_axis', 'extrusion', 'ratio', 'start_param', 'end_param', ]) @property def minor_axis(self) -> Vector: dxf = self.dxf return ellipse.minor_axis(Vector(dxf.major_axis), Vector(dxf.extrusion), dxf.ratio) @property def start_point(self) -> 'Vector': return list(self.vertices([self.dxf.start_param]))[0] @property def end_point(self) -> 'Vector': return list(self.vertices([self.dxf.end_param]))[0] def construction_tool(self) -> ConstructionEllipse: """ Returns construction tool :class:`ezdxf.math.ConstructionEllipse`. .. versionadded:: 0.13 """ dxf = self.dxf return ConstructionEllipse( dxf.center, dxf.major_axis, dxf.extrusion, dxf.ratio, dxf.start_param, dxf.end_param, ) def apply_construction_tool(self, e: ConstructionEllipse) -> None: """ Set ELLIPSE data from construction tool :class:`ezdxf.math.ConstructionEllipse`. .. versionadded:: 0.13 """ self.update_dxf_attribs(e.dxfattribs()) def params(self, num: int) -> Iterable[float]: """ Returns `num` params from start- to end param in counter clockwise order. All params are normalized in the range from [0, 2pi). """ start = self.dxf.start_param % math.tau end = self.dxf.end_param % math.tau yield from ellipse.get_params(start, end, num) def vertices(self, params: Iterable[float]) -> Iterable[Vector]: """ Yields vertices on ellipse for iterable `params` in WCS. Args: params: param values in the range from ``0`` to ``2*pi`` in radians, param goes counter clockwise around the extrusion vector, major_axis = local x-axis = 0 rad. .. versionadded:: 0.11 """ yield from self.construction_tool().vertices(params) def swap_axis(self): """ Swap axis and adjust start- and end parameter. """ e = self.construction_tool() e.swap_axis() self.update_dxf_attribs(e.dxfattribs()) @classmethod def from_arc(cls, entity: 'DXFGraphic') -> 'Ellipse': """ Create a new ELLIPSE entity from ARC or CIRCLE entity. The new SPLINE entity has no owner, no handle, is not stored in the entity database nor assigned to any layout! .. versionadded:: 0.13 """ assert entity.dxftype() in {'ARC', 'CIRCLE'} attribs = entity.dxfattribs(drop={'owner', 'handle', 'thickness'}) e = ellipse.ConstructionEllipse.from_arc( center=attribs.get('center', NULLVEC), radius=attribs.pop('radius', 1.0), # not an ELLIPSE attribute extrusion=attribs.get('extrusion', Z_AXIS), start_angle=attribs.pop('start_angle', 0), # not an ELLIPSE attribute end_angle=attribs.pop('end_angle', 360) # not an ELLIPSE attribute ) attribs.update(e.dxfattribs()) return Ellipse.new(dxfattribs=attribs, doc=entity.doc) def transform(self, m: Matrix44) -> 'Ellipse': """ Transform ELLIPSE entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ e = self.construction_tool() e.transform(m) self.update_dxf_attribs(e.dxfattribs()) return self def translate(self, dx: float, dy: float, dz: float) -> 'Ellipse': """ Optimized ELLIPSE translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ self.dxf.center = Vector(dx, dy, dz) + self.dxf.center return self def to_spline(self, replace=True) -> 'Spline': """ Convert ELLIPSE to a :class:`~ezdxf.entities.Spline` entity. Adds the new SPLINE entity to the entity database and to the same layout as the source entity. Args: replace: replace (delete) source entity by SPLINE entity if ``True`` .. versionadded:: 0.13 """ from ezdxf.entities import Spline spline = Spline.from_arc(self) if replace: replace_entity(self, spline) else: add_entity(self, spline) return spline
class Arc(Circle): """DXF ARC entity""" DXFTYPE = "ARC" DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_circle, acdb_arc) MERGED_GROUP_CODES = merged_arc_group_codes def export_entity(self, tagwriter: "TagWriter") -> None: """Export entity specific data as DXF tags.""" super().export_entity(tagwriter) # AcDbEntity export is done by parent class # AcDbCircle export is done by parent class if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_arc.name) self.dxf.export_dxf_attribs(tagwriter, ["start_angle", "end_angle"]) @property def start_point(self) -> "Vec3": """Returns the start point of the arc in WCS, takes OCS into account.""" v = list(self.vertices([self.dxf.start_angle])) return v[0] @property def end_point(self) -> "Vec3": """Returns the end point of the arc in WCS, takes OCS into account.""" v = list(self.vertices([self.dxf.end_angle])) return v[0] def angles(self, num: int) -> Iterable[float]: """Returns `num` angles from start- to end angle in degrees in counter clockwise order. All angles are normalized in the range from [0, 360). """ if num < 2: raise ValueError("num >= 2") start = self.dxf.start_angle % 360 stop = self.dxf.end_angle % 360 if stop <= start: stop += 360 for angle in linspace(start, stop, num=num, endpoint=True): yield angle % 360 def flattening(self, sagitta: float) -> Iterable[Vertex]: """Approximate the arc by vertices in WCS, argument `segment` is the max. distance from the center of an arc segment to the center of its chord. Yields :class:`~ezdxf.math.Vec2` objects for 2D arcs and :class:`~ezdxf.math.Vec3` objects for 3D arcs. .. versionadded:: 0.15 """ arc = self.construction_tool() ocs = self.ocs() if ocs.transform: to_wcs = ocs.points_to_wcs else: to_wcs = Vec3.generate yield from to_wcs(arc.flattening(sagitta)) def transform(self, m: Matrix44) -> "Arc": """Transform ARC entity by transformation matrix `m` inplace. Raises ``NonUniformScalingError()`` for non uniform scaling. """ ocs = OCSTransform(self.dxf.extrusion, m) super()._transform(ocs) s: float = self.dxf.start_angle e: float = self.dxf.end_angle if not math.isclose(arc_angle_span_deg(s, e), 360.0): ( self.dxf.start_angle, self.dxf.end_angle, ) = ocs.transform_ccw_arc_angles_deg(s, e) self.post_transform(m) return self def construction_tool(self) -> ConstructionArc: """Returns 2D construction tool :class:`ezdxf.math.ConstructionArc`, ignoring the extrusion vector. """ dxf = self.dxf return ConstructionArc( dxf.center, dxf.radius, dxf.start_angle, dxf.end_angle, ) def apply_construction_tool(self, arc: ConstructionArc) -> "Arc": """Set ARC data from construction tool :class:`ezdxf.math.ConstructionArc`, will not change the extrusion vector. """ dxf = self.dxf dxf.center = Vec3(arc.center) dxf.radius = arc.radius dxf.start_angle = arc.start_angle dxf.end_angle = arc.end_angle return self # floating interface
class MultiLeader(DXFGraphic): DXFTYPE = 'MULTILEADER' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mleader) MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000 def __init__(self): super().__init__() self.context = MultiLeaderContext() self.arrow_heads: List[ArrowHeadData] = [] self.block_attribs: List[AttribData] = [] def _copy_data(self, entity: 'MultiLeader') -> None: """ Copy leaders """ entity.context = copy.deepcopy(self.context) entity.arrow_heads = copy.deepcopy(self.arrow_heads) entity.block_attribs = copy.deepcopy(self.block_attribs) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf tags = processor.subclass_by_index(2) context = self.extract_context_data(tags) if context: try: self.context = self.load_context(context) except const.DXFStructureError: logger.info( f'Context structure error in entity MULTILEADER(#{dxf.handle})' ) self.arrow_heads = self.extract_arrow_heads(tags) self.block_attribs = self.extract_block_attribs(tags) processor.fast_load_dxfattribs(dxf, acdb_mleader_group_codes, subclass=tags, recover=True) return dxf @staticmethod def extract_context_data(tags: Tags) -> List['DXFTag']: start, end = None, None context_data = [] for index, tag in enumerate(tags): if tag.code == START_CONTEXT_DATA: start = index elif tag.code == END_CONTEXT_DATA: end = index + 1 if start and end: context_data = tags[start:end] # Remove context data! del tags[start:end] return context_data @staticmethod def load_context(data: List['DXFTag']) -> 'MultiLeaderContext': try: context = compile_context_tags(data, END_CONTEXT_DATA) except StopIteration: raise const.DXFStructureError else: return MultiLeaderContext.load(context) @staticmethod def extract_arrow_heads(data: Tags) -> List[ArrowHeadData]: def store_head(): heads.append( ArrowHeadData( collector.get(94, 0), # arrow head index collector.get(345, '0'), # arrow head handle )) collector.clear() heads = [] try: start = data.tag_index(94) except const.DXFValueError: return heads end = start collector = dict() for code, value in data.collect_consecutive_tags({94, 345}, start): end += 1 collector[code] = value if code == 345: store_head() # Remove processed tags: del data[start:end] return heads @staticmethod def extract_block_attribs(data: Tags) -> List[AttribData]: def store_attrib(): attribs.append( AttribData( collector.get(330, '0'), # ATTDEF handle collector.get(177, 0), # ATTDEF index collector.get(44, 1.0), # ATTDEF width collector.get(302, ''), # ATTDEF text (content) )) collector.clear() attribs = [] try: start = data.tag_index(330) except const.DXFValueError: return attribs end = start collector = dict() for code, value in data.collect_consecutive_tags({330, 177, 44, 302}, start): end += 1 if code == 330 and len(collector): store_attrib() collector[code] = value if len(collector): store_attrib() # Remove processed tags: del data[start:end] return attribs def preprocess_export(self, tagwriter: 'TagWriter') -> bool: if self.context.is_valid: return True else: logger.debug( f'Ignore {str(self)} at DXF export, invalid context data.') return False def export_entity(self, tagwriter: 'TagWriter') -> None: def write_handle_if_exist(code: int, name: str): handle = dxf.get(name) if handle is not None: write_tag2(code, handle) super().export_entity(tagwriter) dxf = self.dxf version = tagwriter.dxfversion write_tag2 = tagwriter.write_tag2 write_tag2(100, acdb_mleader.name) write_tag2(270, dxf.version) self.context.export_dxf(tagwriter) # Export common MLEADER tags: # Don't use dxf.export_dxf_attribs() - all attributes should be written # even if equal to the default value: write_tag2(340, dxf.style_handle) write_tag2(90, dxf.property_override_flags) write_tag2(170, dxf.leader_type) write_tag2(91, dxf.leader_line_color) write_tag2(341, dxf.leader_linetype_handle) write_tag2(171, dxf.leader_lineweight) write_tag2(290, dxf.has_landing) write_tag2(291, dxf.has_dogleg) write_tag2(41, dxf.dogleg_length) # arrow_head_handle is None for default arrow 'closed filled': write_handle_if_exist(342, 'arrow_head_handle') write_tag2(42, dxf.arrow_head_size) write_tag2(172, dxf.content_type) write_tag2(343, dxf.text_style_handle) # mandatory! write_tag2(173, dxf.text_left_attachment_type) write_tag2(95, dxf.text_right_attachment_type) write_tag2(174, dxf.text_angle_type) write_tag2(175, dxf.text_alignment_type) write_tag2(92, dxf.text_color) write_tag2(292, dxf.has_frame_text) write_handle_if_exist(344, 'block_record_handle') write_tag2(93, dxf.block_color) tagwriter.write_vertex(10, dxf.block_scale_vector) write_tag2(43, dxf.block_rotation) write_tag2(176, dxf.block_connection_type) write_tag2(293, dxf.is_annotative) if version >= const.DXF2007: self.export_arrow_heads(tagwriter) self.export_block_attribs(tagwriter) write_tag2(294, dxf.is_text_direction_negative) write_tag2(178, dxf.text_IPE_align) write_tag2(179, dxf.text_attachment_point) write_tag2(45, dxf.scale) if version >= const.DXF2010: write_tag2(271, dxf.text_attachment_direction) write_tag2(272, dxf.text_bottom_attachment_direction) write_tag2(273, dxf.text_top_attachment_direction) if version >= const.DXF2013: write_tag2(295, dxf.leader_extend_to_text) def export_arrow_heads(self, tagwriter: 'TagWriter') -> None: for index, handle in self.arrow_heads: tagwriter.write_tag2(94, index) tagwriter.write_tag2(345, handle) def export_block_attribs(self, tagwriter: 'TagWriter') -> None: for attrib in self.block_attribs: tagwriter.write_tag2(330, attrib.handle) tagwriter.write_tag2(177, attrib.index) tagwriter.write_tag2(44, attrib.width) tagwriter.write_tag2(302, attrib.text)
class Linetype(DXFEntity): """ DXF LTYPE entity """ DXFTYPE = 'LTYPE' DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_linetype) def __init__(self): """ Default constructor """ super().__init__() self.pattern_tags = LinetypePattern(Tags()) def _copy_data(self, entity: 'Linetype') -> None: """ Copy pattern_tags. """ entity.pattern_tags = deepcopy(self.pattern_tags) @classmethod def new(cls, handle: str = None, owner: str = None, dxfattribs: dict = None, doc: 'Drawing' = None) -> 'DXFEntity': """ Constructor for building new entities from scratch by ezdxf (trusted environment). Args: handle: unique DXF entity handle or None owner: owner handle iof entity has an owner else None or '0' dxfattribs: DXF attributes to initialize doc: DXF document """ dxfattribs = dxfattribs or {} pattern = dxfattribs.pop('pattern', [0.0]) length = dxfattribs.pop('length', 0) # required for complex types ltype: 'LineType' = super().new(handle, owner, dxfattribs, doc) ltype._setup_pattern(pattern, length) return ltype def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace(dxf, acdb_linetype) self.pattern_tags = LinetypePattern(tags) return dxf def preprocess_export(self, tagwriter: 'TagWriter'): if len(self.pattern_tags) == 0: return False # do not export complex linetypes for DXF12 if tagwriter.dxfversion == DXF12: return not self.pattern_tags.is_complex_type() return True def export_entity(self, tagwriter: 'TagWriter') -> None: super().export_entity(tagwriter) # AcDbEntity export is done by parent class if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_linetype.name) self.dxf.export_dxf_attribs(tagwriter, ['name', 'flags', 'description']) if self.pattern_tags: self.pattern_tags.export_dxf(tagwriter) def _setup_pattern(self, pattern: Union[Iterable[float], str], length: float) -> None: complex_line_type = True if isinstance(pattern, str) else False if complex_line_type: # a .lin like line type definition string tags = self._setup_complex_pattern(pattern, length) else: # pattern: [2.0, 1.25, -0.25, 0.25, -0.25] - 1. element is total # pattern length pattern elements: >0 line, <0 gap, =0 point tags = Tags([ DXFTag(72, 65), # letter 'A' DXFTag(73, len(pattern) - 1), DXFTag(40, float(pattern[0])), ]) for element in pattern[1:]: tags.append(DXFTag(49, float(element))) tags.append(DXFTag(74, 0)) self.pattern_tags = LinetypePattern(tags) def _setup_complex_pattern(self, pattern: str, length: float) -> Tags: tokens = lin_compiler(pattern) tags = Tags([ DXFTag(72, 65), # letter 'A' ]) tags2 = [DXFTag(73, 0), DXFTag(40, length)] # temp length of 0 count = 0 for token in tokens: if isinstance(token, DXFTag): if tags2[-1].code == 49: # useless 74 only after 49 :)) tags2.append(DXFTag(74, 0)) tags2.append(token) count += 1 else: # TEXT or SHAPE tags2.extend( cast('ComplexLineTypePart', token).complex_ltype_tags(self.doc)) tags2.append(DXFTag(74, 0)) # useless 74 at the end :)) tags2[0] = DXFTag(73, count) tags.extend(tags2) return tags
class Ray(XLine): """ DXF Ray entity """ DXFTYPE = 'RAY' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_xline) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 XLINE_SUBCLASS = 'AcDbRay'
class Leader(DXFGraphic, OverrideMixin): """ DXF LEADER entity """ DXFTYPE = 'LEADER' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_leader) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 def __init__(self): super().__init__() self.vertices: List[Vector] = [] def _copy_data(self, entity: 'Leader') -> None: """ Copy vertices. """ entity.vertices = copy.deepcopy(self.vertices) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace(dxf, acdb_leader) tags = Tags(self.load_vertices(tags)) if len(tags): tags = processor.recover_graphic_attributes(tags, dxf) if len(tags): # 76: Number of vertices in leader (ignored for OPEN) processor.log_unprocessed_tags(tags.filter((76, )), subclass=acdb_leader.name) return dxf def load_vertices(self, tags: Tags) -> Iterable[DXFTag]: for tag in tags: if tag.code == 10: self.vertices.append(tag.value) else: yield tag def preprocess_export(self, tagwriter: 'TagWriter') -> bool: if len(self.vertices) < 2: logger.debug(f"Invalid {str(self)}: more than 1 vertex required.") return False else: return True def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_leader.name) self.dxf.export_dxf_attribs(tagwriter, [ 'dimstyle', 'has_arrowhead', 'path_type', 'annotation_type', 'hookline_direction', 'has_hookline', 'text_height', 'text_width', ]) self.export_vertices(tagwriter) self.dxf.export_dxf_attribs(tagwriter, [ 'block_color', 'annotation_handle', 'normal_vector', 'horizontal_direction', 'leader_offset_block_ref', 'leader_offset_annotation_placement' ]) def export_vertices(self, tagwriter: 'TagWriter') -> None: tagwriter.write_tag2(76, len(self.vertices)) for vertex in self.vertices: tagwriter.write_vertex(10, vertex) def set_vertices(self, vertices: Iterable['Vertex']): """ Set vertices of the leader, vertices is an iterable of ``(x, y [,z])`` tuples or :class:`~ezdxf.math.Vector`. """ self.vertices = [Vector(v) for v in vertices] def transform(self, m: 'Matrix44') -> 'Leader': """ Transform LEADER entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ self.vertices = list(m.transform_vertices(self.vertices)) self.dxf.normal_vector, _ = transform_extrusion( self.dxf.normal_vector, m) # ??? self.dxf.horizontal_direction = m.transform_direction( self.dxf.horizontal_direction) return self def virtual_entities(self) -> Iterable['DXFGraphic']: """ Yields 'virtual' parts of LEADER as DXF primitives. This entities are located at the original positions, but are not stored in the entity database, have no handle and are not assigned to any layout. .. versionadded:: 0.14 """ from ezdxf.render.leader import virtual_entities return virtual_entities(self) def explode(self, target_layout: 'BaseLayout' = None) -> 'EntityQuery': """ Explode parts of LEADER as DXF primitives into target layout, if target layout is ``None``, the target layout is the layout of the LEADER. Returns an :class:`~ezdxf.query.EntityQuery` container with all DXF parts. Args: target_layout: target layout for DXF parts, ``None`` for same layout as source entity. .. versionadded:: 0.14 """ return explode_entity(self, target_layout) def audit(self, auditor: 'Auditor') -> None: """ Validity check. """ super().audit(auditor) if len(self.vertices) < 2: auditor.fixed_error( code=AuditError.INVALID_VERTEX_COUNT, message=f'Deleted entity {str(self)} with invalid vertex count ' f'= {len(self.vertices)}.', dxf_entity=self, ) self.destroy()
class DXFGraphic(DXFEntity): """ Common base class for all graphic entities, a subclass of :class:`~ezdxf.entities.dxfentity.DXFEntity`. These entities resides in entity spaces like modelspace, paperspace or block. """ DXFTYPE = 'DXFGFX' DEFAULT_ATTRIBS = {'layer': '0'} DXFATTRIBS = DXFAttributes(base_class, acdb_entity) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': """ Adds subclass processing for 'AcDbEntity', requires previous base class processing by parent class. (internal API) """ dxf = super().load_dxf_attribs(processor) if processor is None: return dxf r12 = processor.r12 # It is valid to mix up the base class with AcDbEntity class. processor.append_base_class_to_acdb_entity() # Load proxy graphic data if requested if options.load_proxy_graphics: # length tag has group code 92 until DXF R2010 if processor.dxfversion and processor.dxfversion < DXF2013: code = 92 else: code = 160 self.proxy_graphic = load_proxy_graphic( processor.subclasses[0 if r12 else 1], length_code=code, ) # Load common AcDbEntity attributes into dxf namespace tags = processor.load_dxfattribs_into_namespace(dxf, acdb_entity, index=1) if len(tags) and not r12: processor.log_unprocessed_tags(tags, subclass=acdb_entity.name) return dxf def post_new_hook(self): """ Post processing and integrity validation after entity creation (internal API) """ if self.doc: if self.dxf.linetype not in self.doc.linetypes: raise DXFInvalidLineType( f'Linetype "{self.dxf.linetype}" not defined.') @property def rgb(self) -> Optional[Tuple[int, int, int]]: """ Returns RGB true color as (r, g, b) tuple or None if true_color is not set. """ if self.dxf.hasattr('true_color'): return int2rgb(self.dxf.get('true_color')) else: return None @rgb.setter def rgb(self, rgb: Tuple[int, int, int]) -> None: """ Set RGB true color as (r, g , b) tuple e.g. (12, 34, 56). """ self.dxf.set('true_color', rgb2int(rgb)) @property def transparency(self) -> float: """ Get transparency as float value between 0 and 1, 0 is opaque and 1 is 100% transparent (invisible). """ if self.dxf.hasattr('transparency'): return transparency2float(self.dxf.get('transparency')) else: return 0. @transparency.setter def transparency(self, transparency: float) -> None: """ Set transparency as float value between 0 and 1, 0 is opaque and 1 is 100% transparent (invisible). """ self.dxf.set('transparency', float2transparency(transparency)) def graphic_properties(self) -> Dict: """ Returns the important common properties layer, color, linetype, lineweight, ltscale, true_color and color_name as `dxfattribs` dict. .. versionadded:: 0.12 """ attribs = dict() for key in GRAPHIC_PROPERTIES: if self.dxf.hasattr(key): attribs[key] = self.dxf.get(key) return attribs def ocs(self) -> Optional[OCS]: """ Returns object coordinate system (:ref:`ocs`) for 2D entities like :class:`Text` or :class:`Circle`, returns ``None`` for entities without OCS support. """ # extrusion is only defined for 2D entities like Text, Circle, ... if self.dxf.is_supported('extrusion'): extrusion = self.dxf.get('extrusion', default=(0, 0, 1)) return OCS(extrusion) else: return None def set_owner(self, owner: str, paperspace: int = 0) -> None: """ Set owner attribute and paperspace flag. (internal API)""" self.dxf.owner = owner if paperspace: self.dxf.paperspace = paperspace else: self.dxf.discard('paperspace') def linked_entities(self) -> Iterable['DXFEntity']: """ Yield linked entities: VERTEX or ATTRIB, different handling than attached entities. (internal API) """ return [] def link_entity(self, entity: 'DXFEntity') -> None: """ Store linked or attached entities. Same API for both types of appended data, because entities with linked entities (POLYLINE, INSERT) have no attached entities and vice versa. (internal API) """ pass def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. (internal API)""" # Base class export is done by parent class. self.export_acdb_entity(tagwriter) # XDATA and embedded objects export is also done by the parent class. def export_acdb_entity(self, tagwriter: 'TagWriter'): """ Export subclass 'AcDbEntity' as DXF tags. (internal API)""" # Full control over tag order and YES, sometimes order matters not_r12 = tagwriter.dxfversion > DXF12 if not_r12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name) self.dxf.export_dxf_attribs(tagwriter, [ 'paperspace', 'layer', 'linetype', 'material_handle', 'color', 'lineweight', 'ltscale', 'true_color', 'color_name', 'transparency', 'plotstyle_enum', 'plotstyle_handle', 'shadow_mode', 'visualstyle_handle', ]) if self.proxy_graphic and not_r12 and options.store_proxy_graphics: # length tag has group code 92 until DXF R2010 export_proxy_graphic( self.proxy_graphic, tagwriter=tagwriter, length_code=(92 if tagwriter.dxfversion < DXF2013 else 160)) def get_layout(self) -> Optional['BaseLayout']: """ Returns the owner layout or returns ``None`` if entity is not assigned to any layout. """ if self.dxf.owner is None: # unlinked entity return None try: return self.doc.layouts.get_layout_by_key(self.dxf.owner) except DXFKeyError: pass try: return self.doc.blocks.get_block_layout_by_handle(self.dxf.owner) except DXFTableEntryError: return None def unlink_from_layout(self) -> None: """ Unlink entity from associated layout. Does nothing if entity is already unlinked. It is more efficient to call the :meth:`~ezdxf.layouts.BaseLayout.unlink_entity` method of the associated layout, especially if you have to unlink more than one entity. .. versionadded:: 0.13 """ if not self.is_alive: raise TypeError('Can not unlink destroyed entity.') if self.doc is None: # no doc -> no layout self.dxf.owner = None return layout = self.get_layout() if layout: layout.unlink_entity(self) def move_to_layout(self, layout: 'BaseLayout', source: 'BaseLayout' = 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. Moving between different DXF drawings is not supported. Args: layout: any layout (model space, paper space, block) source: provide source layout, faster for DXF R12, if entity is in a block layout Raises: DXFStructureError: for moving between different DXF drawings """ 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 copy_to_layout(self, layout: 'BaseLayout') -> 'DXFEntity': """ Copy entity to another `layout`, returns new created entity as :class:`DXFEntity` object. Copying between different DXF drawings is not supported. Args: layout: any layout (model space, paper space, block) Raises: DXFStructureError: for copying between different DXF drawings """ if self.doc != layout.doc: raise DXFStructureError( 'Copying between different DXF drawings is not supported.') new_entity = self.copy() layout.add_entity(new_entity) return new_entity def audit(self, auditor: 'Auditor') -> None: """ Audit and repair graphical DXF entities. .. important:: Do not delete entities while auditing process, because this would alter the entity database while iterating, instead use:: auditor.trash(entity.dxf.handle) to delete invalid entities after auditing automatically. """ assert self.doc is auditor.doc, 'Auditor for different DXF document.' if not self.is_alive: return super().audit(auditor) auditor.check_owner_exist(self) dxf = self.dxf if dxf.hasattr('layer'): auditor.check_for_valid_layer_name(self) if dxf.hasattr('linetype'): auditor.check_entity_linetype(self) if dxf.hasattr('color'): auditor.check_entity_color_index(self) if dxf.hasattr('lineweight'): auditor.check_entity_lineweight(self) if dxf.hasattr('extrusion'): auditor.check_extrusion_vector(self) def transform(self, m: 'Matrix44') -> 'DXFGraphic': """ Inplace transformation interface, returns `self` (floating interface). Args: m: 4x4 transformation matrix (:class:`ezdxf.math.Matrix44`) .. versionadded:: 0.13 """ raise NotImplementedError() def translate(self, dx: float, dy: float, dz: float) -> 'DXFGraphic': """ Translate entity inplace about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). Basic implementation uses the :meth:`transform` interface, subclasses may have faster implementations. .. versionadded:: 0.13 """ return self.transform(Matrix44.translate(dx, dy, dz)) def scale(self, sx: float, sy: float, sz: float) -> 'DXFGraphic': """ Scale entity inplace about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ return self.transform(Matrix44.scale(sx, sy, sz)) def scale_uniform(self, s: float) -> 'DXFGraphic': """ Scale entity inplace uniform about `s` in x-axis, y-axis and z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ return self.transform(Matrix44.scale(s)) def rotate_axis(self, axis: 'Vertex', angle: float) -> 'DXFGraphic': """ Rotate entity inplace about vector `axis`, returns `self` (floating interface). Args: axis: rotation axis as tuple or :class:`Vector` angle: rotation angle in radians .. versionadded:: 0.13 """ return self.transform(Matrix44.axis_rotate(axis, angle)) def rotate_x(self, angle: float) -> 'DXFGraphic': """ Rotate entity inplace about x-axis, returns `self` (floating interface). Args: angle: rotation angle in radians .. versionadded:: 0.13 """ return self.transform(Matrix44.x_rotate(angle)) def rotate_y(self, angle: float) -> 'DXFGraphic': """ Rotate entity inplace about y-axis, returns `self` (floating interface). Args: angle: rotation angle in radians .. versionadded:: 0.13 """ return self.transform(Matrix44.y_rotate(angle)) def rotate_z(self, angle: float) -> 'DXFGraphic': """ Rotate entity inplace about z-axis, returns `self` (floating interface). Args: angle: rotation angle in radians .. versionadded:: 0.13 """ return self.transform(Matrix44.z_rotate(angle)) def has_hyperlink(self) -> bool: """ Returns ``True`` if entity has an attached hyperlink. """ return bool(self.xdata) and ('PE_URL' in self.xdata) def set_hyperlink(self, link: str, description: str = None, location: str = None): """ Set hyperlink of an entity. """ xdata = [(1001, 'PE_URL'), (1000, str(link))] if description: xdata.append((1002, '{')) xdata.append((1000, str(description))) if location: xdata.append((1000, str(location))) xdata.append((1002, '}')) self.discard_xdata('PE_URL') self.set_xdata('PE_URL', xdata) if self.doc and 'PE_URL' not in self.doc.appids: self.doc.appids.new('PE_URL') return self def get_hyperlink(self) -> Tuple[str, str, str]: """ Returns hyperlink, description and location. """ link = "" description = "" location = "" if self.xdata and 'PE_URL' in self.xdata: xdata = [ tag.value for tag in self.get_xdata('PE_URL') if tag.code == 1000 ] if len(xdata): link = xdata[0] if len(xdata) > 1: description = xdata[1] if len(xdata) > 2: location = xdata[2] return link, description, location def remove_dependencies(self, other: 'Drawing' = None) -> None: """ Remove all dependencies from actual document. (internal API) """ if not self.is_alive: return super().remove_dependencies(other) # The layer attribute is preserved because layer doesn't need a layer # table entry, the layer attributes are reset to default attributes # like color is 7 and linetype is CONTINUOUS has_linetype = (bool(other) and self.dxf.linetype in other.linetypes) if not has_linetype: self.dxf.linetype = 'BYLAYER' self.dxf.discard('material_handle') self.dxf.discard('visualstyle_handle') self.dxf.discard('plotstyle_enum') self.dxf.discard('plotstyle_handle') def _new_compound_entity(self, type_: str, dxfattribs: dict) -> 'DXFGraphic': """ Create and bind 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. (internal API) """ dxfattribs = dxfattribs or {} # if layer is not deliberately set, set same layer as creator entity, # at least VERTEX should have the same layer as the POLYGON entity. # Don't know if that is also important for the ATTRIB & INSERT entity. if 'layer' not in dxfattribs: dxfattribs['layer'] = self.dxf.layer if self.doc: entity = factory.create_db_entry(type_, dxfattribs, self.doc) else: entity = factory.new(type_, dxfattribs) entity.dxf.owner = self.dxf.owner entity.dxf.paperspace = self.dxf.paperspace return entity
class Dictionary(DXFObject): """ AutoCAD maintains items such as mline styles and group definitions as objects in dictionaries. Other applications are free to create and use their own dictionaries as they see fit. The prefix "ACAD_" is reserved for use by AutoCAD applications. Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string, because at loading time not all objects are already stored in the EntityDB, and have to acquired later. """ DXFTYPE = 'DICTIONARY' DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary) def __init__(self): super().__init__() self._data: Dict[str, Union[str, DXFEntity]] = dict() self._value_code = VALUE_CODE def _copy_data(self, entity: 'Dictionary') -> None: """ Copy hard owned entities but do not store the copies in the entity database, this is a second step, this is just real copying. """ entity._value_code = self._value_code if self.dxf.hard_owned: # Reactors are removed from the cloned DXF objects. entity._data = {key: entity.copy() for key, entity in self.items()} else: entity._data = {key: entity for key, entity in self.items()} def post_bind_hook(self) -> None: """ Called by binding a new or copied dictionary to the document, bind hard owned sub-entities to the same document and add them to the objects section. """ if not self.dxf.hard_owned: return # copied or new dictionary: doc = self.doc owner_handle = self.dxf.handle for _, entity in self.items(): entity.dxf.owner = owner_handle factory.bind(entity, doc) # For a correct DXF export add entities to the objects section: doc.objects.add_object(entity) def load_dxf_attribs(self, processor: SubclassProcessor = None) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: tags = processor.load_dxfattribs_into_namespace( dxf, acdb_dictionary ) self.load_dict(tags) return dxf def load_dict(self, tags): entry_handle = None dict_key = None value_code = VALUE_CODE for code, value in tags: if code in SEARCH_CODES: # First store handles, because at this point, NOT all objects # are stored in the EntityDB, at first access convert the handle # to a DXFEntity object. value_code = code entry_handle = value elif code == KEY_CODE: dict_key = value if dict_key and entry_handle: # Store entity as handle string: self._data[dict_key] = entry_handle entry_handle = None dict_key = None # Use same value code as loaded: self._value_code = value_code def post_load_hook(self, doc: 'Drawing') -> None: super().post_load_hook(doc) db = doc.entitydb def items(): for key, handle in self.items(): entity = db.get(handle) if entity is not None and entity.is_alive: yield key, entity if len(self): for k, v in list(items()): self.__setitem__(k, v) def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name) self.dxf.export_dxf_attribs(tagwriter, ['hard_owned', 'cloning']) self.export_dict(tagwriter) def export_dict(self, tagwriter: 'TagWriter'): # key: dict key string # value: DXFEntity or handle as string # Ignore invalid handles at export, because removing can create an empty # dictionary, which is more a problem for AutoCAD than invalid handles, # and removing the whole dictionary is maybe also a problem. for key, value in self._data.items(): tagwriter.write_tag2(KEY_CODE, key) # Value can be a handle string or a DXFEntity object: if isinstance(value, DXFEntity): if value.is_alive: value = value.dxf.handle else: logger.debug( f'Key "{key}" points to a destroyed entity ' f'in {str(self)}, target replaced by "0" handle.' ) value = '0' # Use same value code as loaded: tagwriter.write_tag2(self._value_code, value) @property def is_hard_owner(self) -> bool: """ Returns ``True`` if :class:`Dictionary` is hard owner of entities. Hard owned entities will be destroyed by deleting the dictionary. """ return bool(self.dxf.hard_owned) def keys(self) -> KeysView: """ Returns :class:`KeysView` of all dictionary keys. """ return self._data.keys() def items(self) -> ItemsView: """ Returns :class:`ItemsView` for all dictionary entries as (:attr:`key`, :class:`DXFEntity`) pairs. """ for key in self.keys(): yield key, self.get(key) # maybe handle -> DXFEntity def __getitem__(self, key: str) -> 'DXFEntity': """ Return the value for `key`, raises a :class:`DXFKeyError` if `key` does not exist. """ return self.get(key) def __setitem__(self, key: str, value: 'DXFEntity') -> None: """ Add item as ``(key, value)`` pair to dictionary. """ return self.add(key, value) def __delitem__(self, key: str) -> None: """ Delete entry `key` from the dictionary, raises :class:`DXFKeyError` if key does not exist. """ return self.remove(key) def __contains__(self, key: str) -> bool: """ Returns ``True`` if `key` exist. """ return key in self._data def __len__(self) -> int: """ Returns count of items. """ return len(self._data) count = __len__ def get(self, key: str, default: Any = DXFKeyError) -> 'DXFEntity': """ Returns :class:`DXFEntity` for `key`, if `key` exist, else `default` or raises a :class:`DXFKeyError` for `default` = :class:`DXFKeyError`. """ try: return self._data[key] except KeyError: if default is DXFKeyError: raise DXFKeyError(f"KeyError: '{key}'") else: return default def add(self, key: str, value: 'DXFEntity') -> None: """ Add entry ``(key, value)``. """ if isinstance(value, str): if not is_valid_handle(value): raise DXFValueError( f'Invalid entity handle #{value} for key {key}') self._data[key] = value def remove(self, key: str) -> None: """ Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not exist. Deletes also hard owned DXF objects from OBJECTS section. """ data = self._data if key not in data: raise DXFKeyError(key) if self.is_hard_owner: entity = self.get(key) # Presumption: hard owned DXF objects always reside in the OBJECTS # section. self.doc.objects.delete_entity(entity) del data[key] def discard(self, key: str) -> None: """ Delete entry `key` if exists. Does NOT raise an exception if `key` not exist and does not delete hard owned DXF objects. """ try: del self._data[key] except KeyError: pass def clear(self) -> None: """ Delete all entries from :class:`Dictionary`, deletes hard owned DXF objects from OBJECTS section. """ if self.is_hard_owner: self._delete_hard_owned_entries() self._data.clear() def _delete_hard_owned_entries(self) -> None: # Presumption: hard owned DXF objects always reside in the OBJECTS section objects = self.doc.objects for key, entity in self.items(): objects.delete_entity(entity) def add_new_dict(self, key: str, hard_owned: bool = False) -> 'Dictionary': """ Create a new sub :class:`Dictionary`. Args: key: name of the sub dictionary hard_owned: entries of the new dictionary are hard owned """ dxf_dict = self.doc.objects.add_dictionary(owner=self.dxf.handle, hard_owned=hard_owned) self.add(key, dxf_dict) return dxf_dict def add_dict_var(self, key: str, value: str) -> 'DictionaryVar': """ Add new :class:`DictionaryVar`. Args: key: entry name as string value: entry value as string """ new_var = self.doc.objects.add_dictionary_var( owner=self.dxf.handle, value=value ) self.add(key, new_var) return new_var def set_or_add_dict_var(self, key: str, value: str) -> 'DictionaryVar': """ Set or add new :class:`DictionaryVar`. Args: key: entry name as string value: entry value as string """ if key not in self: dict_var = self.doc.objects.add_dictionary_var( owner=self.dxf.handle, value=value ) self.add(key, dict_var) else: dict_var = self.get(key) dict_var.dxf.value = str(value) return dict_var def get_required_dict(self, key: str) -> 'Dictionary': """ Get entry `key` or create a new :class:`Dictionary`, if `Key` not exist. """ try: dxf_dict = self.get(key) except DXFKeyError: dxf_dict = self.add_new_dict(key) return dxf_dict def audit(self, auditor: 'Auditor') -> None: super().audit(auditor) self._check_invalid_entries(auditor) def _check_invalid_entries(self, auditor: 'Auditor'): trash = [] # do not delete content while iterating append = trash.append db = auditor.entitydb for key, entry in self._data.items(): if isinstance(entry, str): if entry not in db: append(key) elif entry.is_alive: if entry.dxf.handle not in db: append(key) else: # entry is destroyed append(key) for key in trash: del self._data[key] auditor.fixed_error( code=AuditError.INVALID_DICTIONARY_ENTRY, message=f'Removed entry "{key}" with invalid handle in {str(self)}', dxf_entity=self, data=key, ) def destroy(self) -> None: if not self.is_alive: return if self.is_hard_owner: self._delete_hard_owned_entries() super().destroy()
class Image(ImageBase): """ DXF IMAGE entity """ DXFTYPE = 'IMAGE' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_image) _CLS_GROUP_CODES = acdb_image_group_codes _SUBCLASS_NAME = acdb_image.name DEFAULT_ATTRIBS = {'layer': '0', 'flags': 3} def __init__(self): super().__init__() self._boundary_path: List[Vec2] = [] self._image_def: Optional[ImageDef] = None self._image_def_reactor: Optional[ImageDefReactor] = None @classmethod def new(cls: 'Image', handle: str = None, owner: str = None, dxfattribs: Dict = None, doc: 'Drawing' = None) -> 'Image': dxfattribs = dxfattribs or {} # 'image_def' is not a real DXF attribute (image_def_handle) image_def = dxfattribs.pop('image_def', None) image_size = (1, 1) if image_def and image_def.is_alive: image_size = image_def.dxf.image_size dxfattribs.setdefault('image_size', image_size) image = cast('Image', super().new(handle, owner, dxfattribs, doc)) image.image_def = image_def return image def copy(self) -> 'Image': image_copy = cast('Image', super().copy()) # Each Image has its own ImageDefReactor object, # which will be created by binding the copy to the # document. image_copy.dxf.discard('image_def_reactor_handle') image_copy._image_def_reactor = None image_copy._image_def = self._image_def return image_copy def post_bind_hook(self) -> None: # Document in LOAD process -> post_load_hook() if self.doc.is_loading: return if self._image_def_reactor: # ImageDefReactor already exist return # The new Image was created by ezdxf and the ImageDefReactor # object does not exist: self._create_image_def_reactor() def post_load_hook(self, doc: 'Drawing') -> Optional[Callable]: super().post_load_hook(doc) db = doc.entitydb self._image_def = db.get(self.dxf.get('image_def_handle', None)) if self._image_def is None: # unrecoverable structure error self.destroy() return self._image_def_reactor = db.get( self.dxf.get('image_def_reactor_handle', None)) if self._image_def_reactor is None: # Image and ImageDef exist - this is recoverable by creating # an ImageDefReactor, but the objects section does not exist yet! # Return a post init command: return self._fix_missing_image_def_reactor def _fix_missing_image_def_reactor(self): try: self._create_image_def_reactor() except Exception as e: logger.exception( f'An exception occurred while executing fixing command for ' f'{str(self)}, destroying entity.', exc_info=e, ) self.destroy() return logger.debug(f'Created missing ImageDefReactor for {str(self)}') def _create_image_def_reactor(self): # ImageDef -> ImageDefReactor -> Image image_def_reactor = self.doc.objects.add_image_def_reactor( self.dxf.handle) reactor_handle = image_def_reactor.dxf.handle # Link Image to ImageDefReactor: self.dxf.image_def_reactor_handle = reactor_handle self._image_def_reactor = image_def_reactor # Link ImageDef to ImageDefReactor: self._image_def.append_reactor_handle(reactor_handle) @property def image_def(self) -> 'ImageDef': """ Returns the associated IMAGEDEF entity, see :class:`ImageDef`.""" return self._image_def @image_def.setter def image_def(self, image_def: 'ImageDef') -> None: if image_def and image_def.is_alive: self.dxf.image_def_handle = image_def.dxf.handle self._image_def = image_def else: self.dxf.discard('image_def_handle') self._image_def = None def destroy(self) -> None: if not self.is_alive: return reactor = self._image_def_reactor if reactor and reactor.is_alive: image_def = self.image_def if image_def and image_def.is_alive: image_def.discard_reactor_handle(reactor.dxf.handle) reactor.destroy() super().destroy() def audit(self, auditor: 'Auditor') -> None: super().audit(auditor)
class Face3d(_Base): """ DXF 3DFACE entity """ DXFTYPE = '3DFACE' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_face) def is_invisible_edge(self, num) -> bool: """ Returns True if edge `num` is an invisible edge. """ return bool(self.dxf.invisible & (1 << num)) def set_edge_visibility(self, num, status=False): """ Set visibility of edge `num`, status `True` for visible, status `False` for invisible. """ if not status: self.dxf.invisible = self.dxf.invisible | (1 << num) else: self.dxf.invisible = self.dxf.invisible & ~(1 << num) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: processor.fast_load_dxfattribs(dxf, acdb_face_group_codes, subclass=2, recover=True) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: super().export_entity(tagwriter) if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_face.name) if not self.dxf.hasattr('vtx3'): self.dxf.vtx3 = self.dxf.vtx2 self.dxf.export_dxf_attribs( tagwriter, ['vtx0', 'vtx1', 'vtx2', 'vtx3', 'invisible']) def transform(self, m: Matrix44) -> 'Face3d': """ Transform the 3DFACE entity by transformation matrix `m` inplace. """ dxf = self.dxf # 3DFACE is a real 3d entity dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3 = m.transform_vertices( (dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3)) return self def wcs_vertices(self, close: bool = False) -> List[Vec3]: """ Returns WCS vertices, if argument `close` is ``True``, last vertex == first vertex. Does **not** return duplicated last vertex if represents a triangle. Compatibility interface to SOLID and TRACE. The 3DFACE entity returns already WCS vertices. """ dxf = self.dxf vertices = [dxf.vtx0, dxf.vtx1, dxf.vtx2] if dxf.vtx3 != dxf.vtx2: # when the face is a triangle, vtx2 == vtx3 vertices.append(dxf.vtx3) if close and not vertices[0].isclose(vertices[-1]): vertices.append(vertices[0]) return vertices
class Layer(DXFEntity): """ DXF LAYER entity """ DXFTYPE = 'LAYER' DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_layer_table_record) DEFAULT_ATTRIBS = {'name': '0'} FROZEN = 0b00000001 THAW = 0b11111110 LOCK = 0b00000100 UNLOCK = 0b11111011 def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf tags = processor.load_dxfattribs_into_namespace( dxf, acdb_layer_table_record) if len(tags) and not processor.r12: processor.log_unprocessed_tags( tags, subclass=acdb_layer_table_record.name) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: super().export_entity(tagwriter) # AcDbEntity export is done by parent class if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_layer_table_record.name) self.dxf.export_dxf_attribs(tagwriter, [ 'name', 'flags', 'color', 'true_color', 'linetype', 'plot', 'lineweight', 'plotstyle_handle', 'material_handle', 'unknown1', ]) def post_new_hook(self) -> None: if not is_valid_layer_name(self.dxf.name): raise DXFInvalidLayerName("Invalid layer name '{}'".format( self.dxf.name)) def set_required_attributes(self): if not self.dxf.hasattr('material'): self.dxf.material_handle = self.doc.materials['Global'].dxf.handle if not self.dxf.hasattr('plotstyle_handle'): self.dxf.plotstyle_handle = self.doc.plotstyles[ 'Normal'].dxf.handle def is_frozen(self) -> bool: """ Returns ``True`` if layer is frozen. """ return self.dxf.flags & Layer.FROZEN > 0 def freeze(self) -> None: """ Freeze layer. """ self.dxf.flags = self.dxf.flags | Layer.FROZEN def thaw(self) -> None: """ Thaw layer.""" self.dxf.flags = self.dxf.flags & Layer.THAW def is_locked(self) -> bool: """ Returns ``True`` if layer is locked. """ return self.dxf.flags & Layer.LOCK > 0 def lock(self) -> None: """ Lock layer, entities on this layer are not editable - just important in CAD applications. """ self.dxf.flags = self.dxf.flags | Layer.LOCK def unlock(self) -> None: """ Unlock layer, entities on this layer are editable - just important in CAD applications. """ self.dxf.flags = self.dxf.flags & Layer.UNLOCK def is_off(self) -> bool: """ Returns ``True`` if layer is off. """ return self.dxf.color < 0 def is_on(self) -> bool: """ Returns ``True`` if layer is on. """ return not self.is_off() def on(self) -> None: """ Switch layer `on` (visible).""" self.dxf.color = abs(self.dxf.color) def off(self) -> None: """ Switch layer `off` (invisible). """ self.dxf.color = -abs(self.dxf.color) def get_color(self) -> int: """ Get layer color, safe method for getting the layer color, because dxf.color is negative for layer status `off`. """ return abs(self.dxf.color) def set_color(self, color: int) -> None: """ Set layer color, safe method for setting the layer color, because dxf.color is negative for layer status `off`. """ color = abs(color) if self.is_on() else -abs(color) self.dxf.color = color @property def rgb(self) -> Optional[Tuple[int, int, int]]: """ Returns RGB true color as (r, g, b)-tuple or None if attribute dxf.true_color is not set. """ if self.dxf.hasattr('true_color'): return int2rgb(self.dxf.get('true_color')) else: return None @rgb.setter def rgb(self, rgb: Tuple[int, int, int]) -> None: """ Set RGB true color as (r, g, b)-tuple e.g. (12, 34, 56). """ self.dxf.set('true_color', rgb2int(rgb)) @property def color(self) -> int: """ Get layer color, safe method for getting the layer color, because dxf.color is negative for layer status `off`. """ return self.get_color() @color.setter def color(self, value: int) -> None: """ Set layer color, safe method for setting the layer color, because dxf.color is negative for layer status `off`. """ self.set_color(value) @property def description(self) -> str: try: xdata = self.get_xdata(AcAecLayerStandard) except DXFValueError: return "" else: return xdata[1].value @description.setter def description(self, value: str) -> None: # create AppID table entry if not present if self.doc and AcAecLayerStandard not in self.doc.appids: self.doc.appids.new(AcAecLayerStandard) self.discard_xdata(AcAecLayerStandard) self.set_xdata(AcAecLayerStandard, [(1000, ''), (1000, value)]) @property def transparency(self) -> float: try: xdata = self.get_xdata(AcCmTransparency) except DXFValueError: return 0 else: return transparency2float(xdata[0].value) @transparency.setter def transparency(self, value: float) -> None: # create AppID table entry if not present if self.doc and AcCmTransparency not in self.doc.appids: self.doc.appids.new(AcCmTransparency) if 0 <= value <= 1: self.discard_xdata(AcCmTransparency) self.set_xdata(AcCmTransparency, [(1071, float2transparency(value))]) else: raise ValueError('Value out of range (0 .. 1).') def rename(self, name: str) -> None: """ Rename layer and all known (documented) references to this layer. .. warning:: Renaming layers may damage the DXF file in some circumstances! Args: name: new layer name Raises: ValueError: `name` contains invalid characters: <>/\\":;?*|=` ValueError: layer `name` already exist ValueError: renaming of layers ``'0'`` and ``'DEFPOINTS'`` not possible """ if not is_valid_layer_name(name): raise ValueError('Name contains invalid characters: {}.'.format( INVALID_NAME_CHARACTERS)) layers = self.doc.layers if self.dxf.name.lower() in ('0', 'defpoints'): raise ValueError('Can not rename layer "{}".'.format( self.dxf.name)) if layers.has_entry(name): raise ValueError('Layer "{}" already exist.'.format(name)) old = self.dxf.name self.dxf.name = name layers.replace(old, self) self._rename_layer_references(old, name) def _rename_layer_references(self, old_name: str, new_name: str) -> None: key = self.doc.layers.key old_key = key(old_name) for e in self.doc.entitydb.values(): if e.dxf.hasattr('layer') and key(e.dxf.layer) == old_key: e.dxf.layer = new_name entity_type = e.dxftype() if entity_type == 'VIEWPORT': e.rename_frozen_layer(old_name, new_name) elif entity_type == 'LAYER_FILTER': # todo: if LAYER_FILTER implemented, add support for renaming layers logger.debug( 'renaming layer "{}" - document contains LAYER_FILTER'. format(old_name)) elif entity_type == 'LAYER_INDEX': # todo: if LAYER_INDEX implemented, add support for renaming layers logger.debug( 'renaming layer "{}" - document contains LAYER_INDEX'. format(old_name))
class Solid(_Base): """ DXF SHAPE entity """ DXFTYPE = 'SOLID' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_trace) def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': """ Loading interface. (internal API) """ dxf = super().load_dxf_attribs(processor) if processor: processor.fast_load_dxfattribs(dxf, acdb_trace_group_codes, subclass=2, recover=True) if processor.r12: # Transform elevation attribute from R11 to z-axis values: elevation_to_z_axis(dxf, VERTEXNAMES) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. (internal API) """ super().export_entity(tagwriter) if tagwriter.dxfversion > DXF12: tagwriter.write_tag2(SUBCLASS_MARKER, acdb_trace.name) if not self.dxf.hasattr('vtx3'): self.dxf.vtx3 = self.dxf.vtx2 self.dxf.export_dxf_attribs(tagwriter, [ 'vtx0', 'vtx1', 'vtx2', 'vtx3', 'thickness', 'extrusion', ]) def transform(self, m: Matrix44) -> 'Solid': """ Transform the SOLID/TRACE entity by transformation matrix `m` inplace. """ # SOLID and TRACE are OCS entities. dxf = self.dxf ocs = OCSTransform(self.dxf.extrusion, m) for name in VERTEXNAMES: if dxf.hasattr(name): dxf.set(name, ocs.transform_vertex(dxf.get(name))) if dxf.hasattr('thickness'): dxf.thickness = ocs.transform_length((0, 0, dxf.thickness), reflection=dxf.thickness) dxf.extrusion = ocs.new_extrusion return self def wcs_vertices(self, close: bool = False) -> List[Vec3]: """ Returns WCS vertices in correct order, if argument `close` is ``True``, last vertex == first vertex. Does **not** return duplicated last vertex if represents a triangle. .. versionadded:: 0.15 """ ocs = self.ocs() return list(ocs.points_to_wcs(self.vertices(close))) def vertices(self, close: bool = False) -> List[Vec3]: """ Returns OCS vertices in correct order, if argument `close` is ``True``, last vertex == first vertex. Does **not** return duplicated last vertex if represents a triangle. .. versionadded:: 0.15 """ dxf = self.dxf vertices = [dxf.vtx0, dxf.vtx1, dxf.vtx2] if dxf.vtx3 != dxf.vtx2: # when the face is a triangle, vtx2 == vtx3 vertices.append(dxf.vtx3) # adjust weird vertex order of SOLID and TRACE: # 0, 1, 2, 3 -> 0, 1, 3, 2 if len(vertices) > 3: vertices[2], vertices[3] = vertices[3], vertices[2] if close and not vertices[0].isclose(vertices[-1]): vertices.append(vertices[0]) return vertices
class MText(DXFGraphic): """ DXF MTEXT entity """ DXFTYPE = 'MTEXT' DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mtext) MIN_DXF_VERSION_FOR_EXPORT = DXF2000 UNDERLINE_START = r'\L' UNDERLINE_STOP = r'\l' UNDERLINE = UNDERLINE_START + '%s' + UNDERLINE_STOP OVERSTRIKE_START = r'\O' OVERSTRIKE_STOP = r'\o' OVERSTRIKE = OVERSTRIKE_START + '%s' + OVERSTRIKE_STOP STRIKE_START = r'\K' STRIKE_STOP = r'\k' STRIKE = STRIKE_START + '%s' + STRIKE_STOP NEW_LINE = r'\P' GROUP_START = '{' GROUP_END = '}' GROUP = GROUP_START + '%s' + GROUP_END NBSP = r'\~' # none breaking space def __init__(self, doc: 'Drawing' = None): """ Default constructor """ super().__init__(doc) self.text = "" # type: str def _copy_data(self, entity: 'DXFEntity') -> None: """ Copy entity data: text """ entity.text = self.text def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor: self.load_mtext(processor.subclasses[2]) tags = processor.load_dxfattribs_into_namespace(dxf, acdb_mtext) if len(tags): processor.log_unprocessed_tags(tags, subclass=acdb_mtext.name) return dxf def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ # base class export is done by parent class super().export_entity(tagwriter) # AcDbEntity export is done by parent class tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mtext.name) self.dxf.export_dxf_attribs(tagwriter, [ 'insert', 'char_height', 'width', 'defined_height', 'attachment_point', 'flow_direction' ]) self.export_mtext(tagwriter) self.dxf.export_dxf_attribs(tagwriter, [ 'style', 'extrusion', 'text_direction', 'rect_width', 'rect_height', 'rotation', 'line_spacing_style', 'line_spacing_factor', 'box_fill_scale', 'bg_fill', 'bg_fill_color', 'bg_fill_true_color', 'bg_fill_color_name', 'bg_fill_transparency' ]) # xdata and embedded_object export by parent class def load_mtext(self, tags: Tags) -> None: tail = "" parts = [] for tag in tags: if tag.code == 1: tail = tag.value if tag.code == 3: parts.append(tag.value) parts.append(tail) self.text = "".join(parts) tags.remove_tags((1, 3)) def export_mtext(self, tagwriter: 'TagWriter') -> None: # replacing '\n' by '\P' is required, else an invalid DXF file would be created txt = self.text.replace('\n', '\\P') str_chunks = split_mtext_string(txt, size=250) if len(str_chunks) == 0: str_chunks.append("") while len(str_chunks) > 1: tagwriter.write_tag2(3, str_chunks.pop(0)) tagwriter.write_tag2(1, str_chunks[0]) def get_rotation(self) -> float: """ Get text rotation in degrees, independent if it is defined by :attr:`dxf.rotation` or :attr:`dxf.text_direction`. """ if self.dxf.hasattr('text_direction'): vector = self.dxf.text_direction radians = math.atan2(vector[1], vector[0]) # ignores z-axis rotation = math.degrees(radians) else: rotation = self.dxf.get('rotation', 0) return rotation def set_rotation(self, angle: float) -> 'MText': """ Set attribute :attr:`rotation` to `angle` (in degrees) and deletes :attr:`dxf.text_direction` if present. """ # text_direction has higher priority than rotation, therefore delete it self.dxf.discard('text_direction') self.dxf.rotation = angle return self # fluent interface def set_location(self, insert: 'Vertex', rotation: float = None, attachment_point: int = None) -> 'MText': """ Set attributes :attr:`dxf.insert`, :attr:`dxf.rotation` and :attr:`dxf.attachment_point`, ``None`` for :attr:`dxf.rotation` or :attr:`dxf.attachment_point` preserves the existing value. """ self.dxf.insert = Vector(insert) if rotation is not None: self.set_rotation(rotation) if attachment_point is not None: self.dxf.attachment_point = attachment_point return self # fluent interface def set_bg_color(self, color: Union[int, str, Tuple[int, int, int], None], scale: float = 1.5): """ Set background color as :ref:`ACI` value or as name string or as RGB tuple ``(r, g, b)``. Use special color name ``canvas``, to set background color to canvas background color. Args: color: color as :ref:`ACI`, string or RGB tuple scale: determines how much border there is around the text, the value is based on the text height, and should be in the range of ``1`` - ``5``, where ``1`` fits exact the MText entity. """ if 1 <= scale <= 5: self.dxf.box_fill_scale = scale else: raise ValueError('argument scale has to be in range from 1 to 5.') if color is None: self.dxf.discard('bg_fill') self.dxf.discard('box_fill_scale') self.dxf.discard('bg_fill_color') self.dxf.discard('bg_fill_true_color') self.dxf.discard('bg_fill_color_name') elif color == 'canvas': # special case for use background color self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR self.dxf.bg_fill_color = 0 # required but ignored else: self.dxf.bg_fill = const.MTEXT_BG_COLOR if isinstance(color, int): self.dxf.bg_fill_color = color elif isinstance(color, str): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_color_name = color elif isinstance(color, tuple): self.dxf.bg_fill_color = 0 # required but ignored self.dxf.bg_fill_true_color = rgb2int(color) return self # fluent interface def __iadd__(self, text: str) -> 'MText': """ Append `text` to existing content (:attr:`.text` attribute). """ self.text += text return self append = __iadd__ def set_font(self, name: str, bold: bool = False, italic: bool = False, codepage: int = 1252, pitch: int = 0) -> None: """ Append font change (e.g. ``'\\Fkroeger|b0|i0|c238|p10'`` ) to existing content (:attr:`.text` attribute). Args: name: font name bold: flag italic: flag codepage: character codepage pitch: font size """ bold_flag = 1 if bold else 0 italic_flag = 1 if italic else 0 s = r"\F{}|b{}|i{}|c{}|p{};".format(name, bold_flag, italic_flag, codepage, pitch) self.append(s) def set_color(self, color_name: str) -> None: """ Append text color change to existing content, `color_name` as ``red``, ``yellow``, ``green``, ``cyan``, ``blue``, ``magenta`` or ``white``. """ self.append(r"\C%d" % const.MTEXT_COLOR_INDEX[color_name.lower()]) def add_stacked_text(self, upr: str, lwr: str, t: str = '^') -> None: r""" Add stacked text `upr` over `lwr`, `t` defines the kind of stacking: .. code-block:: none "^": vertical stacked without divider line, e.g. \SA^B: A B "/": vertical stacked with divider line, e.g. \SX/Y: X - Y "#": diagonal stacked, with slanting divider line, e.g. \S1#4: 1/4 """ # space ' ' in front of {lwr} is important self.append(r'\S{upr}{t} {lwr};'.format(upr=upr, lwr=lwr, t=t)) def transform_to_wcs(self, ucs: 'UCS') -> 'MText': """ Transform MTEXT entity from local :class:`~ezdxf.math.UCS` coordinates to :ref:`WCS` coordinates. .. versionadded:: 0.11 """ if self.dxf.hasattr('rotation'): self.dxf.text_direction = Vector.from_deg_angle(self.dxf.rotation) self.dxf.discard('rotation') self.dxf.insert = ucs.to_wcs(self.dxf.insert) self.dxf.text_direction = ucs.direction_to_wcs(self.dxf.text_direction) self.dxf.extrusion = ucs.direction_to_wcs(self.dxf.extrusion) return self
class DXFGroup(DXFObject): """ Groups are not allowed in block definitions, and each entity can only reside in one group, so cloning of groups creates also new entities. """ DXFTYPE = 'GROUP' DXFATTRIBS = DXFAttributes(base_class, acdb_group) def __init__(self, doc: 'Drawing' = None): super().__init__(doc) self._data = list() # type: List[Union[str, DXFGraphic]] def copy(self): raise DXFTypeError('Copying of GROUP not supported.') def load_dxf_attribs(self, processor: SubclassProcessor = None) -> 'DXFNamespace': dxf = super().load_dxf_attribs(processor) if processor is None: return dxf tags = processor.load_dxfattribs_into_namespace(dxf, acdb_group) self.load_group(tags) return dxf def load_group(self, tags): for code, value in tags: if code == GROUP_ITEM_CODE: # First store handles, because at this point, not all objects # are stored in the EntityDB, at access convert the handle to # DXFEntity: try: entity = self.entitydb[value] except KeyError: # Store entity as handle string entity = value self._data.append(entity) def export_entity(self, tagwriter: 'TagWriter') -> None: """ Export entity specific data as DXF tags. """ super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_group.name) self.dxf.export_dxf_attribs(tagwriter, [ 'description', 'unnamed', 'selectable']) self.export_group(tagwriter) def export_group(self, tagwriter: 'TagWriter'): for entity in self._data: if isinstance(entity, str): handle = entity else: handle = entity.dxf.handle tagwriter.write_tag2(GROUP_ITEM_CODE, handle) def __iter__(self) -> Iterable['DXFGraphic']: """ Iterate over all DXF entities in :class:`DXFGroup` as instances of :class:`DXFGraphic` or inherited (LINE, CIRCLE, ...). """ for index, entity in enumerate(self._data): if isinstance(entity, str): # replace handle string by DXFEntity entity = self.entitydb[entity] self._data[index] = entity yield entity def __len__(self) -> int: """ Returns the count of DXF entities in :class:`DXFGroup`. """ return len(self._data) def __getitem__(self, item): """ Returns entities by standard Python indexing and slicing. """ return self._data[item] def __contains__(self, item: Union[str, 'DXFGraphic']) -> bool: """ Returns ``True`` if item is in :class:`DXFGroup`. `item` has to be a handle string or an object of type :class:`DXFGraphic` or inherited. """ handle = item if isinstance(item, str) else item.dxf.handle return handle in set(self.handles()) def handles(self) -> Iterable[str]: """ Iterable of handles of all DXF entities in :class:`DXFGroup`. """ return (entity.dxf.handle for entity in self) def get_name(self) -> str: """ Get name of :class:`DXFGroup`. """ group_table = cast('Dictionary', self.entitydb[self.dxf.owner]) for name, entity in group_table.items(): if entity is self: return name @contextmanager def edit_data(self) -> List['DXFGraphic']: """ Context manager which yields all the group entities as standard Python list:: with group.edit_data() as data: # add new entities to a group data.append(modelspace.add_line((0, 0), (3, 0))) # remove last entity from a group data.pop() """ data = list(self) yield data self.set_data(data) def set_data(self, entities: Iterable['DXFGraphic']) -> None: """ Set `entities` as new group content, entities should be an iterable :class:`DXFGraphic` or inherited (LINE, CIRCLE, ...). Raises :class:`DXFValueError` if not all entities be on the same layout (modelspace or any paperspace layout but not block) """ entities = list(entities) if not all_entities_on_same_layout(entities): raise DXFValueError( "All entities have to be on the same layout (modelspace or any " "paperspace layout but not a block)." ) self.clear() self._data = entities def extend(self, entities: Iterable['DXFGraphic']) -> None: """ Add `entities` to :class:`DXFGroup`. """ self._data.extend(entities) def clear(self) -> None: """ Remove all entities from :class:`DXFGroup`, does not delete any drawing entities referenced by this group. """ self._data = [] def audit(self, auditor: 'Auditor') -> None: """ Remove invalid handles from :class:`DXFGroup`. Invalid handles are: deleted entities, not all entities in the same layout or entities in a block layout. """ # Remove destroyed or invalid entities: self._data = list(self.filter_invalid_entities()) if len(self._data) == 0: return if not all_entities_on_same_layout(self._data): auditor.fixed_error( code=AuditError.GROUP_ENTITIES_IN_DIFFERENT_LAYOUTS, message=f'Cleared {str(self)}, not all entities are located in ' f'the same layout.', ) self.clear() def has_valid_owner(self, entity) -> bool: # no owner -> no layout association if entity.dxf.owner is None: return False owner = self.entitydb.get(entity.dxf.owner) # owner does not exist or is destroyed -> no layout association if owner is None or not owner.is_alive: return False # owner block_record.layout is 0 if entity is in a block definition, # which is not allowed: valid = owner.dxf.layout != '0' if not valid: logger.debug( f'Removed {str(entity)} from {str(self)}, because entity is ' f'located in a block layout.') return valid def filter_invalid_entities(self) -> Iterable['DXFGraphic']: db = self.entitydb for e in self._data: if e is None: continue if isinstance(e, str): e = db.get(e) # returns None for not existing entities if isinstance(e, DXFGraphic) and \ e.is_alive and \ self.has_valid_owner(e): yield e