1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Optional, Tuple, Iterable, Dict
4from ezdxf.entities import factory
5from ezdxf import options
6from ezdxf.lldxf import validator
7from ezdxf.lldxf.attributes import (
8    DXFAttr, DXFAttributes, DefSubclass, RETURN_DEFAULT, group_code_mapping,
9)
10from ezdxf import colors as clr
11from ezdxf.lldxf.const import (
12    DXF12, DXF2000, DXF2004, DXF2007, DXF2013, DXFValueError, DXFKeyError,
13    DXFTableEntryError, SUBCLASS_MARKER, DXFInvalidLineType, DXFStructureError,
14)
15from ezdxf.math import OCS, Matrix44
16from ezdxf.proxygraphic import load_proxy_graphic, export_proxy_graphic
17from .dxfentity import DXFEntity, base_class, SubclassProcessor
18
19if TYPE_CHECKING:
20    from ezdxf.eztypes import (
21        Auditor, TagWriter, BaseLayout, DXFNamespace, Vertex, Drawing,
22    )
23
24__all__ = [
25    'DXFGraphic', 'acdb_entity', 'SeqEnd', 'add_entity',
26    'replace_entity', 'elevation_to_z_axis',
27]
28
29GRAPHIC_PROPERTIES = {
30    'layer', 'linetype', 'color', 'lineweight', 'ltscale', 'true_color',
31    'color_name', 'transparency',
32}
33
34acdb_entity = DefSubclass('AcDbEntity', {
35    # Layer name as string, no auto fix for invalid names!
36    'layer': DXFAttr(8, default='0', validator=validator.is_valid_layer_name),
37
38    # Linetype name as string, no auto fix for invalid names!
39    'linetype': DXFAttr(
40        6, default='BYLAYER', optional=True,
41        validator=validator.is_valid_table_name,
42    ),
43    # ACI color index, BYBLOCK=0, BYLAYER=256, BYOBJECT=257:
44    'color': DXFAttr(
45        62, default=256, optional=True,
46        validator=validator.is_valid_aci_color,
47        fixer=RETURN_DEFAULT,
48    ),
49    # modelspace=0, paperspace=1
50    'paperspace': DXFAttr(
51        67, default=0, optional=True,
52        validator=validator.is_integer_bool,
53        fixer=RETURN_DEFAULT,
54    ),
55
56    # Lineweight in mm times 100 (e.g. 0.13mm = 13). Smallest line weight is 13
57    # and biggest line weight is 200, values outside this range prevents AutoCAD
58    # from loading the file.
59    # Special values: BYLAYER=-1, BYBLOCK=-2, DEFAULT=-3
60    'lineweight': DXFAttr(
61        370, default=-1, dxfversion=DXF2000, optional=True,
62        validator=validator.is_valid_lineweight,
63        fixer=validator.fix_lineweight,
64    ),
65    'ltscale': DXFAttr(
66        48, default=1.0, dxfversion=DXF2000, optional=True,
67        validator=validator.is_positive,
68        fixer=RETURN_DEFAULT,
69    ),
70    # visible=0, invisible=1
71    'invisible': DXFAttr(60, default=0, dxfversion=DXF2000, optional=True),
72
73    # True color as 0x00RRGGBB 24-bit value
74    # True color always overrides ACI "color"!
75
76    'true_color': DXFAttr(420, dxfversion=DXF2004, optional=True),
77
78    # Color name as string. Color books are stored in .stb config files?
79    'color_name': DXFAttr(430, dxfversion=DXF2004, optional=True),
80
81    # Transparency value 0x020000TT 0 = fully transparent / 255 = opaque
82    'transparency': DXFAttr(440, dxfversion=DXF2004, optional=True),
83
84    # Shadow mode:
85    # 0 = Casts and receives shadows
86    # 1 = Casts shadows
87    # 2 = Receives shadows
88    # 3 = Ignores shadows
89    'shadow_mode': DXFAttr(284, dxfversion=DXF2007, optional=True),
90    'material_handle': DXFAttr(347, dxfversion=DXF2007, optional=True),
91    'visualstyle_handle': DXFAttr(348, dxfversion=DXF2007, optional=True),
92
93    # PlotStyleName type enum (AcDb::PlotStyleNameType). Stored and moved around
94    # as a 16-bit integer. Custom non-entity
95    'plotstyle_enum': DXFAttr(380, dxfversion=DXF2007, default=1,
96                              optional=True),
97
98    # Handle value of the PlotStyleName object, basically a hard pointer, but
99    # has a different range to make backward compatibility easier to deal with.
100    'plotstyle_handle': DXFAttr(390, dxfversion=DXF2007, optional=True),
101
102    # 92 or 160?: Number of bytes in the proxy entity graphics represented in
103    # the subsequent 310 groups, which are binary chunk records (optional)
104    # 310: Proxy entity graphics data (multiple lines; 256 characters max. per
105    # line) (optional), compiled by TagCompiler() to a DXFBinaryTag() objects
106})
107acdb_entity_group_codes = group_code_mapping(acdb_entity)
108
109
110def elevation_to_z_axis(dxf: 'DXFNamespace', names: Iterable[str]):
111    # The elevation group code (38) is only used for DXF R11 and prior and
112    # ignored for DXF R2000 and later.
113    # DXF R12 and later store the entity elevation in the z-axis of the
114    # vertices, but AutoCAD supports elevation for R12 if no z-axis is present.
115    # DXF types with legacy elevation support:
116    # SOLID, TRACE, TEXT, CIRCLE, ARC, TEXT, ATTRIB, ATTDEF, INSERT, SHAPE
117
118    # The elevation is only used for DXF R12 if no z-axis is stored in the DXF
119    # file. This is a problem because ezdxf loads the vertices always as 3D
120    # vertex including a z-axis even if no z-axis is present in DXF file.
121    if dxf.hasattr('elevation'):
122        elevation = dxf.elevation
123        # ezdxf does not export the elevation attribute for any DXF version
124        dxf.discard('elevation')
125        if elevation == 0:
126            return
127
128        for name in names:
129            v = dxf.get(name)
130            # Only use elevation value if z-axis is 0, this will not work for
131            # situations where an elevation and a z-axis=0 is present, but let's
132            # assume if the elevation group code is used the z-axis is not
133            # present if z-axis is 0.
134            if v is not None and v.z == 0:
135                dxf.set(name, v.replace(z=elevation))
136
137
138class DXFGraphic(DXFEntity):
139    """ Common base class for all graphic entities, a subclass of
140    :class:`~ezdxf.entities.dxfentity.DXFEntity`. These entities resides in
141    entity spaces like modelspace, paperspace or block.
142
143    """
144    DXFTYPE = 'DXFGFX'
145    DEFAULT_ATTRIBS = {'layer': '0'}
146    DXFATTRIBS = DXFAttributes(base_class, acdb_entity)
147
148    def load_dxf_attribs(
149            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
150        """ Adds subclass processing for 'AcDbEntity', requires previous base
151        class processing by parent class.
152
153        (internal API)
154        """
155        dxf = super().load_dxf_attribs(processor)
156        if processor is None:
157            return dxf
158        r12 = processor.r12
159        # It is valid to mix up the base class with AcDbEntity class.
160        processor.append_base_class_to_acdb_entity()
161
162        # Load proxy graphic data if requested
163        if options.load_proxy_graphics:
164            # length tag has group code 92 until DXF R2010
165            if processor.dxfversion and processor.dxfversion < DXF2013:
166                code = 92
167            else:
168                code = 160
169            self.proxy_graphic = load_proxy_graphic(
170                processor.subclasses[0 if r12 else 1],
171                length_code=code,
172            )
173        processor.fast_load_dxfattribs(dxf, acdb_entity_group_codes, 1)
174        return dxf
175
176    def post_new_hook(self):
177        """ Post processing and integrity validation after entity creation
178        (internal API)
179        """
180        if self.doc:
181            if self.dxf.linetype not in self.doc.linetypes:
182                raise DXFInvalidLineType(
183                    f'Linetype "{self.dxf.linetype}" not defined.'
184                )
185
186    @property
187    def rgb(self) -> Optional[clr.RGB]:
188        """ Returns RGB true color as (r, g, b) tuple or None if true_color is
189        not set.
190        """
191        if self.dxf.hasattr('true_color'):
192            return clr.int2rgb(self.dxf.get('true_color'))
193        else:
194            return None
195
196    @rgb.setter
197    def rgb(self, rgb: clr.RGB) -> None:
198        """ Set RGB true color as (r, g , b) tuple e.g. (12, 34, 56). """
199        self.dxf.set('true_color', clr.rgb2int(rgb))
200
201    @property
202    def transparency(self) -> float:
203        """ Get transparency as float value between 0 and 1, 0 is opaque and 1
204        is 100% transparent (invisible).
205        """
206        if self.dxf.hasattr('transparency'):
207            return clr.transparency2float(self.dxf.get('transparency'))
208        else:
209            return 0.
210
211    @transparency.setter
212    def transparency(self, transparency: float) -> None:
213        """ Set transparency as float value between 0 and 1, 0 is opaque and 1
214        is 100% transparent (invisible).
215        """
216        self.dxf.set('transparency', clr.float2transparency(transparency))
217
218    def graphic_properties(self) -> Dict:
219        """ Returns the important common properties layer, color, linetype,
220        lineweight, ltscale, true_color and color_name as `dxfattribs` dict.
221
222        """
223        attribs = dict()
224        for key in GRAPHIC_PROPERTIES:
225            if self.dxf.hasattr(key):
226                attribs[key] = self.dxf.get(key)
227        return attribs
228
229    def ocs(self) -> Optional[OCS]:
230        """ Returns object coordinate system (:ref:`ocs`) for 2D entities like
231        :class:`Text` or :class:`Circle`, returns ``None`` for entities without
232        OCS support.
233
234        """
235        # extrusion is only defined for 2D entities like Text, Circle, ...
236        if self.dxf.is_supported('extrusion'):
237            extrusion = self.dxf.get('extrusion', default=(0, 0, 1))
238            return OCS(extrusion)
239        else:
240            return None
241
242    def set_owner(self, owner: str, paperspace: int = 0) -> None:
243        """ Set owner attribute and paperspace flag. (internal API)"""
244        self.dxf.owner = owner
245        if paperspace:
246            self.dxf.paperspace = paperspace
247        else:
248            self.dxf.discard('paperspace')
249
250    def link_entity(self, entity: 'DXFEntity') -> None:
251        """ Store linked or attached entities. Same API for both types of
252        appended data, because entities with linked entities (POLYLINE, INSERT)
253        have no attached entities and vice versa.
254
255        (internal API)
256        """
257        pass
258
259    def export_entity(self, tagwriter: 'TagWriter') -> None:
260        """ Export entity specific data as DXF tags. (internal API)"""
261        # Base class export is done by parent class.
262        self.export_acdb_entity(tagwriter)
263        # XDATA and embedded objects export is also done by the parent class.
264
265    def export_acdb_entity(self, tagwriter: 'TagWriter'):
266        """ Export subclass 'AcDbEntity' as DXF tags. (internal API)"""
267        # Full control over tag order and YES, sometimes order matters
268        not_r12 = tagwriter.dxfversion > DXF12
269        if not_r12:
270            tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name)
271
272        self.dxf.export_dxf_attribs(tagwriter, [
273            'paperspace', 'layer', 'linetype', 'material_handle', 'color',
274            'lineweight', 'ltscale', 'true_color', 'color_name', 'transparency',
275            'plotstyle_enum', 'plotstyle_handle', 'shadow_mode',
276            'visualstyle_handle',
277        ])
278
279        if self.proxy_graphic and not_r12 and options.store_proxy_graphics:
280            # length tag has group code 92 until DXF R2010
281            export_proxy_graphic(
282                self.proxy_graphic,
283                tagwriter=tagwriter,
284                length_code=(92 if tagwriter.dxfversion < DXF2013 else 160)
285            )
286
287    def get_layout(self) -> Optional['BaseLayout']:
288        """ Returns the owner layout or returns ``None`` if entity is not
289        assigned to any layout.
290        """
291        if self.dxf.owner is None:  # unlinked entity
292            return None
293        try:
294            return self.doc.layouts.get_layout_by_key(self.dxf.owner)
295        except DXFKeyError:
296            pass
297        try:
298            return self.doc.blocks.get_block_layout_by_handle(self.dxf.owner)
299        except DXFTableEntryError:
300            return None
301
302    def unlink_from_layout(self) -> None:
303        """
304        Unlink entity from associated layout. Does nothing if entity is already
305        unlinked.
306
307        It is more efficient to call the
308        :meth:`~ezdxf.layouts.BaseLayout.unlink_entity` method of the associated
309        layout, especially if you have to unlink more than one entity.
310
311        """
312        if not self.is_alive:
313            raise TypeError('Can not unlink destroyed entity.')
314
315        if self.doc is None:
316            # no doc -> no layout
317            self.dxf.owner = None
318            return
319
320        layout = self.get_layout()
321        if layout:
322            layout.unlink_entity(self)
323
324    def move_to_layout(self, layout: 'BaseLayout',
325                       source: 'BaseLayout' = None) -> None:
326        """
327        Move entity from model space or a paper space layout to another layout.
328        For block layout as source, the block layout has to be specified. Moving
329        between different DXF drawings is not supported.
330
331        Args:
332            layout: any layout (model space, paper space, block)
333            source: provide source layout, faster for DXF R12, if entity is
334                    in a block layout
335
336        Raises:
337            DXFStructureError: for moving between different DXF drawings
338
339        """
340        if source is None:
341            source = self.get_layout()
342            if source is None:
343                raise DXFValueError('Source layout for entity not found.')
344        source.move_to_layout(self, layout)
345
346    def copy_to_layout(self, layout: 'BaseLayout') -> 'DXFEntity':
347        """
348        Copy entity to another `layout`, returns new created entity as
349        :class:`DXFEntity` object. Copying between different DXF drawings is
350        not supported.
351
352        Args:
353            layout: any layout (model space, paper space, block)
354
355        Raises:
356            DXFStructureError: for copying between different DXF drawings
357
358        """
359        if self.doc != layout.doc:
360            raise DXFStructureError(
361                'Copying between different DXF drawings is not supported.'
362            )
363
364        new_entity = self.copy()
365        layout.add_entity(new_entity)
366        return new_entity
367
368    def audit(self, auditor: 'Auditor') -> None:
369        """ Audit and repair graphical DXF entities.
370
371        .. important::
372
373            Do not delete entities while auditing process, because this
374            would alter the entity database while iterating, instead use::
375
376                auditor.trash(entity.dxf.handle)
377
378            to delete invalid entities after auditing automatically.
379
380        """
381        assert self.doc is auditor.doc, 'Auditor for different DXF document.'
382        if not self.is_alive:
383            return
384
385        super().audit(auditor)
386        auditor.check_owner_exist(self)
387        dxf = self.dxf
388        if dxf.hasattr('layer'):
389            auditor.check_for_valid_layer_name(self)
390        if dxf.hasattr('linetype'):
391            auditor.check_entity_linetype(self)
392        if dxf.hasattr('color'):
393            auditor.check_entity_color_index(self)
394        if dxf.hasattr('lineweight'):
395            auditor.check_entity_lineweight(self)
396        if dxf.hasattr('extrusion'):
397            auditor.check_extrusion_vector(self)
398
399    def transform(self, m: 'Matrix44') -> 'DXFGraphic':
400        """ Inplace transformation interface, returns `self`
401        (floating interface).
402
403        Args:
404             m: 4x4 transformation matrix (:class:`ezdxf.math.Matrix44`)
405
406        """
407        raise NotImplementedError()
408
409    def translate(self, dx: float, dy: float, dz: float) -> 'DXFGraphic':
410        """ Translate entity inplace about `dx` in x-axis, `dy` in y-axis and
411        `dz` in z-axis, returns `self` (floating interface).
412
413        Basic implementation uses the :meth:`transform` interface, subclasses
414        may have faster implementations.
415
416        """
417        return self.transform(Matrix44.translate(dx, dy, dz))
418
419    def scale(self, sx: float, sy: float, sz: float) -> 'DXFGraphic':
420        """ Scale entity inplace about `dx` in x-axis, `dy` in y-axis and `dz`
421        in z-axis, returns `self` (floating interface).
422
423        """
424        return self.transform(Matrix44.scale(sx, sy, sz))
425
426    def scale_uniform(self, s: float) -> 'DXFGraphic':
427        """ Scale entity inplace uniform about `s` in x-axis, y-axis and z-axis,
428        returns `self` (floating interface).
429
430        """
431        return self.transform(Matrix44.scale(s))
432
433    def rotate_axis(self, axis: 'Vertex', angle: float) -> 'DXFGraphic':
434        """ Rotate entity inplace about vector `axis`, returns `self`
435        (floating interface).
436
437        Args:
438            axis: rotation axis as tuple or :class:`Vec3`
439            angle: rotation angle in radians
440
441        """
442        return self.transform(Matrix44.axis_rotate(axis, angle))
443
444    def rotate_x(self, angle: float) -> 'DXFGraphic':
445        """ Rotate entity inplace about x-axis, returns `self`
446        (floating interface).
447
448        Args:
449            angle: rotation angle in radians
450
451        """
452        return self.transform(Matrix44.x_rotate(angle))
453
454    def rotate_y(self, angle: float) -> 'DXFGraphic':
455        """ Rotate entity inplace about y-axis, returns `self`
456        (floating interface).
457
458        Args:
459            angle: rotation angle in radians
460
461        """
462        return self.transform(Matrix44.y_rotate(angle))
463
464    def rotate_z(self, angle: float) -> 'DXFGraphic':
465        """ Rotate entity inplace about z-axis, returns `self`
466        (floating interface).
467
468        Args:
469            angle: rotation angle in radians
470
471        """
472        return self.transform(Matrix44.z_rotate(angle))
473
474    def has_hyperlink(self) -> bool:
475        """ Returns ``True`` if entity has an attached hyperlink. """
476        return bool(self.xdata) and ('PE_URL' in self.xdata)
477
478    def set_hyperlink(self, link: str, description: str = None,
479                      location: str = None):
480        """ Set hyperlink of an entity. """
481        xdata = [(1001, 'PE_URL'), (1000, str(link))]
482        if description:
483            xdata.append((1002, '{'))
484            xdata.append((1000, str(description)))
485            if location:
486                xdata.append((1000, str(location)))
487            xdata.append((1002, '}'))
488
489        self.discard_xdata('PE_URL')
490        self.set_xdata('PE_URL', xdata)
491        if self.doc and 'PE_URL' not in self.doc.appids:
492            self.doc.appids.new('PE_URL')
493        return self
494
495    def get_hyperlink(self) -> Tuple[str, str, str]:
496        """ Returns hyperlink, description and location. """
497        link = ""
498        description = ""
499        location = ""
500        if self.xdata and 'PE_URL' in self.xdata:
501            xdata = [tag.value for tag in self.get_xdata('PE_URL') if
502                     tag.code == 1000]
503            if len(xdata):
504                link = xdata[0]
505            if len(xdata) > 1:
506                description = xdata[1]
507            if len(xdata) > 2:
508                location = xdata[2]
509        return link, description, location
510
511    def remove_dependencies(self, other: 'Drawing' = None) -> None:
512        """ Remove all dependencies from current document.
513
514        (internal API)
515        """
516        if not self.is_alive:
517            return
518
519        super().remove_dependencies(other)
520        # The layer attribute is preserved because layer doesn't need a layer
521        # table entry, the layer attributes are reset to default attributes
522        # like color is 7 and linetype is CONTINUOUS
523        has_linetype = (bool(other) and self.dxf.linetype in other.linetypes)
524        if not has_linetype:
525            self.dxf.linetype = 'BYLAYER'
526        self.dxf.discard('material_handle')
527        self.dxf.discard('visualstyle_handle')
528        self.dxf.discard('plotstyle_enum')
529        self.dxf.discard('plotstyle_handle')
530
531    def _new_compound_entity(self, type_: str,
532                             dxfattribs: dict) -> 'DXFGraphic':
533        """ Create and bind  new entity with same layout settings as `self`.
534
535        Used by INSERT & POLYLINE to create appended DXF entities, don't use it
536        to create new standalone entities.
537
538        (internal API)
539        """
540        dxfattribs = dxfattribs or {}
541
542        # if layer is not deliberately set, set same layer as creator entity,
543        # at least VERTEX should have the same layer as the POLYGON entity.
544        # Don't know if that is also important for the ATTRIB & INSERT entity.
545        if 'layer' not in dxfattribs:
546            dxfattribs['layer'] = self.dxf.layer
547        if self.doc:
548            entity = factory.create_db_entry(type_, dxfattribs, self.doc)
549        else:
550            entity = factory.new(type_, dxfattribs)
551        entity.dxf.owner = self.dxf.owner
552        entity.dxf.paperspace = self.dxf.paperspace
553        return entity
554
555
556@factory.register_entity
557class SeqEnd(DXFGraphic):
558    DXFTYPE = 'SEQEND'
559
560
561def add_entity(entity: 'DXFGraphic', layout: 'BaseLayout') -> None:
562    """ Add `entity` entity to the entity database and to the given `layout`.
563    """
564    assert entity.dxf.handle is None
565    assert layout is not None
566    if layout.doc:
567        factory.bind(entity, layout.doc)
568    layout.add_entity(entity)
569
570
571def replace_entity(source: 'DXFGraphic', target: 'DXFGraphic',
572                   layout: 'BaseLayout') -> None:
573    """ Add `target` entity to the entity database and to the given `layout`
574    and replace the `source` entity by the `target` entity.
575
576    """
577    assert target.dxf.handle is None
578    assert layout is not None
579    target.dxf.handle = source.dxf.handle
580    if source in layout:
581        layout.delete_entity(source)
582        if layout.doc:
583            factory.bind(target, layout.doc)
584        layout.add_entity(target)
585    else:
586        source.destroy()
587