Esempio n. 1
0
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)
Esempio n. 2
0
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)
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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',
        ])
Esempio n. 6
0
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)
Esempio n. 7
0
File: mesh.py Progetto: mbway/ezdxf
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
Esempio n. 8
0
class Field(DXFObject):
    """ DXF FIELD entity """
    DXFTYPE = 'FIELD'
    DXFATTRIBS = DXFAttributes(base_class, acdb_field)
Esempio n. 9
0
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()
Esempio n. 10
0
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
Esempio n. 11
0
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
Esempio n. 12
0
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'
        ])
Esempio n. 13
0
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())
Esempio n. 14
0
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,
            )
Esempio n. 15
0
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)
Esempio n. 16
0
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)
Esempio n. 17
0
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
Esempio n. 18
0
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
Esempio n. 19
0
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)
Esempio n. 20
0
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
Esempio n. 21
0
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'
Esempio n. 22
0
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()
Esempio n. 23
0
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
Esempio n. 24
0
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()
Esempio n. 25
0
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)
Esempio n. 26
0
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
Esempio n. 27
0
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))
Esempio n. 28
0
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
Esempio n. 29
0
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
Esempio n. 30
0
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