1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, List, Iterable
4import copy
5import logging
6from ezdxf.lldxf import validator
7from ezdxf.lldxf.attributes import (
8    DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT,
9    group_code_mapping,
10)
11from ezdxf.lldxf.tags import Tags, DXFTag
12from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000
13from ezdxf.math import Vec3, X_AXIS, Z_AXIS, NULLVEC
14from ezdxf.math.transformtools import transform_extrusion
15from ezdxf.explode import explode_entity
16from ezdxf.audit import AuditError
17from .dxfentity import base_class, SubclassProcessor
18from .dxfgfx import DXFGraphic, acdb_entity
19from .factory import register_entity
20from .dimension import OverrideMixin
21
22logger = logging.getLogger('ezdxf')
23
24if TYPE_CHECKING:
25    from ezdxf.eztypes import (
26        TagWriter, DXFNamespace, Vertex, Matrix44, BaseLayout, EntityQuery,
27        Auditor,
28    )
29
30__all__ = ['Leader']
31
32acdb_leader = DefSubclass('AcDbLeader', {
33    'dimstyle': DXFAttr(
34        3, default='Standard',
35        validator=validator.is_valid_table_name,
36        # no fixer!
37    ),
38
39    # Arrowhead flag: 0/1 = no/yes
40    'has_arrowhead': DXFAttr(
41        71, default=1, optional=True,
42        validator=validator.is_integer_bool,
43        fixer=RETURN_DEFAULT,
44    ),
45
46    # Leader path type:
47    # 0 = Straight line segments
48    # 1 = Spline
49    'path_type': DXFAttr(
50        72, default=0, optional=True,
51        validator=validator.is_integer_bool,
52        fixer=RETURN_DEFAULT,
53    ),
54
55    # Annotation type or leader creation flag:
56    # 0 = Created with text annotation
57    # 1 = Created with tolerance annotation;
58    # 2 = Created with block reference annotation
59    # 3 = Created without any annotation
60    'annotation_type': DXFAttr(
61        73, default=3,
62        validator=validator.is_in_integer_range(0, 4),
63        fixer=RETURN_DEFAULT,
64    ),
65
66    # Hook line direction flag:
67    # 1 = Hook line (or end of tangent for a spline leader) is the opposite
68    #     direction from the horizontal vector
69    # 0 = Hook line (or end of tangent for a spline leader) is the same
70    #     direction as horizontal vector (see code 75)
71    # DXF reference error: swapped meaning of 1/0
72    'hookline_direction': DXFAttr(
73        74, default=1, optional=True,
74        validator=validator.is_integer_bool,
75        fixer=RETURN_DEFAULT,
76    ),
77
78    # Hook line flag: 0/1 = no/yes
79    'has_hookline': DXFAttr(
80        75, default=1, optional=True,
81        validator=validator.is_integer_bool,
82        fixer=RETURN_DEFAULT,
83    ),
84
85    # Text annotation height:
86    'text_height': DXFAttr(
87        40, default=1, optional=True,
88        validator=validator.is_greater_zero,
89        fixer=RETURN_DEFAULT,
90    ),
91
92    # Text annotation width:
93    'text_width': DXFAttr(
94        41, default=1, optional=True,
95        validator=validator.is_greater_zero,
96        fixer=RETURN_DEFAULT,
97    ),
98
99    # 76: Number of vertices in leader (ignored for OPEN)
100    # 10, 20, 30: Vertex coordinates (one entry for each vertex)
101
102    # Color to use if leader's DIMCLRD = BYBLOCK
103    'block_color': DXFAttr(
104        77, default=7, optional=True,
105        validator=validator.is_valid_aci_color,
106        fixer=RETURN_DEFAULT,
107    ),
108
109    # Hard reference to associated annotation:
110    # (mtext, tolerance, or insert entity)
111    'annotation_handle': DXFAttr(340, default='0', optional=True),
112
113    'normal_vector': DXFAttr(
114        210, xtype=XType.point3d, default=Z_AXIS, optional=True,
115        validator=validator.is_not_null_vector,
116        fixer=RETURN_DEFAULT,
117    ),
118
119    # 'horizontal' direction for leader
120    'horizontal_direction': DXFAttr(
121        211, xtype=XType.point3d, default=X_AXIS, optional=True,
122        validator=validator.is_not_null_vector,
123        fixer=RETURN_DEFAULT,
124    ),
125
126    # Offset of last leader vertex from block reference insertion point
127    'leader_offset_block_ref': DXFAttr(
128        212, xtype=XType.point3d, default=NULLVEC, optional=True),
129
130    # Offset of last leader vertex from annotation placement point
131    'leader_offset_annotation_placement': DXFAttr(
132        213, xtype=XType.point3d, default=NULLVEC, optional=True),
133
134    # Xdata belonging to the application ID "ACAD" follows a leader entity if
135    # any dimension overrides have been applied to this entity. See Dimension
136    # Style Overrides.
137})
138acdb_leader_group_codes = group_code_mapping(acdb_leader)
139
140
141@register_entity
142class Leader(DXFGraphic, OverrideMixin):
143    """ DXF LEADER entity """
144    DXFTYPE = 'LEADER'
145    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_leader)
146    MIN_DXF_VERSION_FOR_EXPORT = DXF2000
147
148    def __init__(self):
149        super().__init__()
150        self.vertices: List[Vec3] = []
151
152    def _copy_data(self, entity: 'Leader') -> None:
153        """ Copy vertices. """
154        entity.vertices = copy.deepcopy(self.vertices)
155
156    def load_dxf_attribs(
157            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
158        dxf = super().load_dxf_attribs(processor)
159        if processor:
160            tags = Tags(self.load_vertices(processor.subclass_by_index(2)))
161            processor.fast_load_dxfattribs(
162                dxf, acdb_leader_group_codes, tags, recover=True)
163        return dxf
164
165    def load_vertices(self, tags: Tags) -> Iterable[DXFTag]:
166        for tag in tags:
167            if tag.code == 10:
168                self.vertices.append(tag.value)
169            elif tag.code == 76:
170                # Number of vertices in leader (ignored for OPEN)
171                pass
172            else:
173                yield tag
174
175    def preprocess_export(self, tagwriter: 'TagWriter') -> bool:
176        if len(self.vertices) < 2:
177            logger.debug(f"Invalid {str(self)}: more than 1 vertex required.")
178            return False
179        else:
180            return True
181
182    def export_entity(self, tagwriter: 'TagWriter') -> None:
183        """ Export entity specific data as DXF tags. """
184        super().export_entity(tagwriter)
185        tagwriter.write_tag2(SUBCLASS_MARKER, acdb_leader.name)
186        self.dxf.export_dxf_attribs(tagwriter, [
187            'dimstyle', 'has_arrowhead', 'path_type', 'annotation_type',
188            'hookline_direction', 'has_hookline', 'text_height', 'text_width',
189        ])
190        self.export_vertices(tagwriter)
191        self.dxf.export_dxf_attribs(tagwriter, [
192            'block_color', 'annotation_handle', 'normal_vector',
193            'horizontal_direction', 'leader_offset_block_ref',
194            'leader_offset_annotation_placement'
195        ])
196
197    def export_vertices(self, tagwriter: 'TagWriter') -> None:
198        tagwriter.write_tag2(76, len(self.vertices))
199        for vertex in self.vertices:
200            tagwriter.write_vertex(10, vertex)
201
202    def set_vertices(self, vertices: Iterable['Vertex']):
203        """ Set vertices of the leader, vertices is an iterable of
204        (x, y [,z]) tuples or :class:`~ezdxf.math.Vec3`.
205
206        """
207        self.vertices = [Vec3(v) for v in vertices]
208
209    def transform(self, m: 'Matrix44') -> 'Leader':
210        """ Transform LEADER entity by transformation matrix `m` inplace. """
211        self.vertices = list(m.transform_vertices(self.vertices))
212        self.dxf.normal_vector, _ = transform_extrusion(self.dxf.normal_vector,
213                                                        m)  # ???
214        self.dxf.horizontal_direction = m.transform_direction(
215            self.dxf.horizontal_direction)
216        return self
217
218    def virtual_entities(self) -> Iterable['DXFGraphic']:
219        """
220        Yields 'virtual' parts of LEADER as DXF primitives.
221
222        This entities are located at the original positions, but are not stored
223        in the entity database, have no handle and are not assigned to any
224        layout.
225
226        .. versionadded:: 0.14
227
228        """
229        from ezdxf.render.leader import virtual_entities
230        return virtual_entities(self)
231
232    def explode(self, target_layout: 'BaseLayout' = None) -> 'EntityQuery':
233        """
234        Explode parts of LEADER as DXF primitives into target layout, if target
235        layout is ``None``, the target layout is the layout of the LEADER.
236
237        Returns an :class:`~ezdxf.query.EntityQuery` container with all
238        DXF parts.
239
240        Args:
241            target_layout: target layout for DXF parts, ``None`` for same
242                layout as source entity.
243
244        .. versionadded:: 0.14
245
246        """
247        return explode_entity(self, target_layout)
248
249    def audit(self, auditor: 'Auditor') -> None:
250        """ Validity check. """
251        super().audit(auditor)
252        if len(self.vertices) < 2:
253            auditor.fixed_error(
254                code=AuditError.INVALID_VERTEX_COUNT,
255                message=f'Deleted entity {str(self)} with invalid vertex count '
256                        f'= {len(self.vertices)}.',
257                dxf_entity=self,
258            )
259            self.destroy()
260