1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, List
4from ezdxf.lldxf import validator
5from ezdxf.lldxf.attributes import (
6    DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT,
7    group_code_mapping,
8)
9from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, VERTEXNAMES
10from ezdxf.math import Matrix44, Z_AXIS, NULLVEC, Vec3
11from ezdxf.math.transformtools import OCSTransform
12from .dxfentity import base_class, SubclassProcessor
13from .dxfgfx import DXFGraphic, acdb_entity, elevation_to_z_axis
14from .factory import register_entity
15
16if TYPE_CHECKING:
17    from ezdxf.eztypes import TagWriter, DXFNamespace
18
19__all__ = ['Solid', 'Trace', 'Face3d']
20
21acdb_trace = DefSubclass('AcDbTrace', {
22    # 1. corner Solid WCS; Trace OCS
23    'vtx0': DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
24
25    # 2. corner Solid WCS; Trace OCS
26    'vtx1': DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
27
28    # 3. corner Solid WCS; Trace OCS
29    'vtx2': DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
30
31    # 4. corner Solid WCS; Trace OCS:
32    # If only three corners are entered to define the SOLID, then the fourth
33    # corner coordinate is the same as the third.
34    'vtx3': DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
35
36    # Elevation is a legacy feature from R11 and prior, do not use this
37    # attribute, store the entity elevation in the z-axis of the vertices.
38    # ezdxf does not export the elevation attribute!
39    'elevation': DXFAttr(38, default=0, optional=True),
40
41    # Thickness could be negative:
42    'thickness': DXFAttr(39, default=0, optional=True),
43    'extrusion': DXFAttr(
44        210, xtype=XType.point3d, default=Z_AXIS, optional=True,
45        validator=validator.is_not_null_vector,
46        fixer=RETURN_DEFAULT,
47    ),
48})
49acdb_trace_group_codes = group_code_mapping(acdb_trace)
50
51
52class _Base(DXFGraphic):
53    def __getitem__(self, num):
54        return self.dxf.get(VERTEXNAMES[num])
55
56    def __setitem__(self, num, value):
57        return self.dxf.set(VERTEXNAMES[num], value)
58
59
60@register_entity
61class Solid(_Base):
62    """ DXF SHAPE entity """
63    DXFTYPE = 'SOLID'
64    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_trace)
65
66    def load_dxf_attribs(
67            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
68        """ Loading interface. (internal API) """
69        dxf = super().load_dxf_attribs(processor)
70        if processor:
71            processor.fast_load_dxfattribs(
72                dxf, acdb_trace_group_codes, subclass=2, recover=True)
73            if processor.r12:
74                # Transform elevation attribute from R11 to z-axis values:
75                elevation_to_z_axis(dxf, VERTEXNAMES)
76        return dxf
77
78    def export_entity(self, tagwriter: 'TagWriter') -> None:
79        """ Export entity specific data as DXF tags. (internal API) """
80        super().export_entity(tagwriter)
81        if tagwriter.dxfversion > DXF12:
82            tagwriter.write_tag2(SUBCLASS_MARKER, acdb_trace.name)
83        if not self.dxf.hasattr('vtx3'):
84            self.dxf.vtx3 = self.dxf.vtx2
85        self.dxf.export_dxf_attribs(tagwriter, [
86            'vtx0', 'vtx1', 'vtx2', 'vtx3', 'thickness',
87            'extrusion',
88        ])
89
90    def transform(self, m: Matrix44) -> 'Solid':
91        """ Transform the SOLID/TRACE entity by transformation matrix `m` inplace.
92        """
93        # SOLID and TRACE are OCS entities.
94        dxf = self.dxf
95        ocs = OCSTransform(self.dxf.extrusion, m)
96        for name in VERTEXNAMES:
97            if dxf.hasattr(name):
98                dxf.set(name, ocs.transform_vertex(dxf.get(name)))
99        if dxf.hasattr('thickness'):
100            dxf.thickness = ocs.transform_length(
101                (0, 0, dxf.thickness), reflection=dxf.thickness)
102        dxf.extrusion = ocs.new_extrusion
103        return self
104
105    def wcs_vertices(self, close: bool = False) -> List[Vec3]:
106        """ Returns WCS vertices in correct order,
107        if argument `close` is ``True``, last vertex == first vertex.
108        Does **not** return duplicated last vertex if represents a triangle.
109
110        .. versionadded:: 0.15
111
112        """
113        ocs = self.ocs()
114        return list(ocs.points_to_wcs(self.vertices(close)))
115
116    def vertices(self, close: bool = False) -> List[Vec3]:
117        """ Returns OCS vertices in correct order,
118        if argument `close` is ``True``, last vertex == first vertex.
119        Does **not** return duplicated last vertex if represents a triangle.
120
121        .. versionadded:: 0.15
122
123        """
124        dxf = self.dxf
125        vertices = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
126        if dxf.vtx3 != dxf.vtx2:  # when the face is a triangle, vtx2 == vtx3
127            vertices.append(dxf.vtx3)
128
129        # adjust weird vertex order of SOLID and TRACE:
130        # 0, 1, 2, 3 -> 0, 1, 3, 2
131        if len(vertices) > 3:
132            vertices[2], vertices[3] = vertices[3], vertices[2]
133
134        if close and not vertices[0].isclose(vertices[-1]):
135            vertices.append(vertices[0])
136        return vertices
137
138
139@register_entity
140class Trace(Solid):
141    """ DXF TRACE entity """
142    DXFTYPE = 'TRACE'
143
144
145acdb_face = DefSubclass('AcDbFace', {
146    # 1. corner WCS:
147    'vtx0': DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
148
149    # 2. corner WCS:
150    'vtx1': DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
151
152    # 3. corner WCS:
153    'vtx2': DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
154
155    # 4. corner WCS:
156    # If only three corners are entered to define the SOLID, then the fourth
157    # corner coordinate is the same as the third.
158    'vtx3': DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
159
160    # invisible:
161    # 1 = First edge is invisible
162    # 2 = Second edge is invisible
163    # 4 = Third edge is invisible
164    # 8 = Fourth edge is invisible
165    'invisible': DXFAttr(70, default=0, optional=True),
166})
167acdb_face_group_codes = group_code_mapping(acdb_face)
168
169
170@register_entity
171class Face3d(_Base):
172    """ DXF 3DFACE entity """
173    DXFTYPE = '3DFACE'
174    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_face)
175
176    def is_invisible_edge(self, num) -> bool:
177        """ Returns True if edge `num` is an invisible edge. """
178        return bool(self.dxf.invisible & (1 << num))
179
180    def set_edge_visibility(self, num, status=False):
181        """ Set visibility of edge `num`, status `True` for visible, status
182        `False` for invisible.
183        """
184        if not status:
185            self.dxf.invisible = self.dxf.invisible | (1 << num)
186        else:
187            self.dxf.invisible = self.dxf.invisible & ~(1 << num)
188
189    def load_dxf_attribs(self,
190                         processor: SubclassProcessor = None) -> 'DXFNamespace':
191        dxf = super().load_dxf_attribs(processor)
192        if processor:
193            processor.fast_load_dxfattribs(
194                dxf, acdb_face_group_codes, subclass=2, recover=True)
195        return dxf
196
197    def export_entity(self, tagwriter: 'TagWriter') -> None:
198        super().export_entity(tagwriter)
199        if tagwriter.dxfversion > DXF12:
200            tagwriter.write_tag2(SUBCLASS_MARKER, acdb_face.name)
201        if not self.dxf.hasattr('vtx3'):
202            self.dxf.vtx3 = self.dxf.vtx2
203        self.dxf.export_dxf_attribs(tagwriter, [
204            'vtx0', 'vtx1', 'vtx2', 'vtx3', 'invisible'
205        ])
206
207    def transform(self, m: Matrix44) -> 'Face3d':
208        """ Transform the 3DFACE  entity by transformation matrix `m` inplace.
209        """
210        dxf = self.dxf
211        # 3DFACE is a real 3d entity
212        dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3 = m.transform_vertices(
213            (dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3))
214        return self
215
216    def wcs_vertices(self, close: bool = False) -> List[Vec3]:
217        """ Returns WCS vertices, if argument `close` is
218        ``True``, last vertex == first vertex.
219        Does **not** return duplicated last vertex if represents a triangle.
220
221        Compatibility interface to SOLID and TRACE. The 3DFACE entity returns
222        already WCS vertices.
223
224        """
225        dxf = self.dxf
226        vertices = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
227        if dxf.vtx3 != dxf.vtx2:  # when the face is a triangle, vtx2 == vtx3
228            vertices.append(dxf.vtx3)
229
230        if close and not vertices[0].isclose(vertices[-1]):
231            vertices.append(vertices[0])
232        return vertices
233