1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3from typing import (
4    TYPE_CHECKING, Iterable, Union, List, cast, Tuple, Sequence, Dict,
5)
6from itertools import chain
7from ezdxf.lldxf import validator
8from ezdxf.lldxf.attributes import (
9    DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT,
10    group_code_mapping
11)
12from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, VERTEXNAMES
13from ezdxf.lldxf import const
14from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS
15from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError
16from ezdxf.render.polyline import virtual_polyline_entities
17from ezdxf.explode import explode_entity
18from ezdxf.query import EntityQuery
19from ezdxf.entities import factory
20from ezdxf.audit import AuditError
21from .dxfentity import base_class, SubclassProcessor
22from .dxfgfx import DXFGraphic, acdb_entity
23from .lwpolyline import FORMAT_CODES
24from .subentity import LinkedEntities
25
26if TYPE_CHECKING:
27    from ezdxf.eztypes import (
28        TagWriter, Vertex, FaceType, DXFNamespace, Line, Arc, Face3d,
29        BaseLayout, Auditor,
30    )
31
32__all__ = ['Polyline', 'Polyface', 'Polymesh']
33
34acdb_polyline = DefSubclass('AcDbPolylineDummy', {
35    # AcDbPolylineDummy is a temporary solution while loading
36    # Group code 66 is obsolete - Vertices follow flag
37
38    # Elevation is a "dummy" point. The x and y values are always 0,
39    # and the Z value is the polyline elevation:
40    'elevation': DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
41
42    # Polyline flags (bit-coded):
43    # 1 = closed POLYLINE or a POLYMESH closed in the M direction
44    # 2 = Curve-fit vertices have been added
45    # 4 = Spline-fit vertices have been added
46    # 8 = 3D POLYLINE
47    # 16 = POLYMESH
48    # 32 = POLYMESH is closed in the N direction
49    # 64 = POLYFACE
50    # 128 = linetype pattern is generated continuously around the vertices
51    'flags': DXFAttr(70, default=0),
52    'default_start_width': DXFAttr(40, default=0, optional=True),
53    'default_end_width': DXFAttr(41, default=0, optional=True),
54    'm_count': DXFAttr(
55        71, default=0, optional=True,
56        validator=validator.is_greater_or_equal_zero,
57        fixer=RETURN_DEFAULT,
58    ),
59    'n_count': DXFAttr(
60        72, default=0, optional=True,
61        validator=validator.is_greater_or_equal_zero,
62        fixer=RETURN_DEFAULT,
63    ),
64    'm_smooth_density': DXFAttr(73, default=0, optional=True),
65    'n_smooth_density': DXFAttr(74, default=0, optional=True),
66
67    # Curves and smooth surface type:
68    # 0 = No smooth surface fitted
69    # 5 = Quadratic B-spline surface
70    # 6 = Cubic B-spline surface
71    # 8 = Bezier surface
72    'smooth_type': DXFAttr(
73        75, default=0, optional=True,
74        validator=validator.is_one_of({0, 5, 6, 8}),
75        fixer=RETURN_DEFAULT,
76    ),
77    'thickness': DXFAttr(39, default=0, optional=True),
78    'extrusion': DXFAttr(
79        210, xtype=XType.point3d, default=Z_AXIS, optional=True,
80        validator=validator.is_not_null_vector,
81        fixer=RETURN_DEFAULT,
82    ),
83})
84acdb_polyline_group_codes = group_code_mapping(acdb_polyline, ignore=(66, ))
85
86# Notes to SEQEND:
87# todo: A loaded entity should have a valid SEQEND, a POLYLINE without vertices
88#  makes no sense - has to be tested
89#
90# A virtual POLYLINE does not need a SEQEND, because it can not be exported,
91# therefore the SEQEND entity should not be created in the
92# DXFEntity.post_new_hook() method.
93#
94# A bounded POLYLINE needs a SEQEND to valid at export, therefore the
95# LinkedEntities.post_bind_hook() method creates a new SEQEND after binding
96# the entity to a document if needed.
97
98
99@factory.register_entity
100class Polyline(LinkedEntities):
101    """ DXF POLYLINE entity """
102    DXFTYPE = 'POLYLINE'
103    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_polyline)
104    # polyline flags (70)
105    CLOSED = 1
106    MESH_CLOSED_M_DIRECTION = CLOSED
107    CURVE_FIT_VERTICES_ADDED = 2
108    SPLINE_FIT_VERTICES_ADDED = 4
109    POLYLINE_3D = 8
110    POLYMESH = 16
111    MESH_CLOSED_N_DIRECTION = 32
112    POLYFACE = 64
113    GENERATE_LINETYPE_PATTERN = 128
114    # polymesh smooth type (75)
115    NO_SMOOTH = 0
116    QUADRATIC_BSPLINE = 5
117    CUBIC_BSPLINE = 6
118    BEZIER_SURFACE = 8
119    ANY3D = POLYLINE_3D | POLYMESH | POLYFACE
120
121    @property
122    def vertices(self):
123        return self._sub_entities
124
125    def load_dxf_attribs(
126            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
127        dxf = super().load_dxf_attribs(processor)
128        if processor:
129            processor.fast_load_dxfattribs(
130                dxf, acdb_polyline_group_codes, subclass=2, recover=True)
131        return dxf
132
133    def export_dxf(self, tagwriter: 'TagWriter'):
134        """ Export POLYLINE entity and all linked entities: VERTEX, SEQEND.
135        """
136        super().export_dxf(tagwriter)
137        # export sub-entities
138        self.process_sub_entities(lambda e: e.export_dxf(tagwriter))
139
140    def export_entity(self, tagwriter: 'TagWriter') -> None:
141        """ Export POLYLINE specific data as DXF tags. """
142        super().export_entity(tagwriter)
143        if tagwriter.dxfversion > DXF12:
144            tagwriter.write_tag2(SUBCLASS_MARKER, self.get_mode())
145
146        tagwriter.write_tag2(66, 1)  # Vertices follow
147        self.dxf.export_dxf_attribs(tagwriter, [
148            'elevation', 'flags', 'default_start_width', 'default_end_width',
149            'm_count', 'n_count', 'm_smooth_density', 'n_smooth_density',
150            'smooth_type', 'thickness', 'extrusion',
151        ])
152
153    def on_layer_change(self, layer: str):
154        """ Event handler for layer change. Changes also the layer of all vertices.
155
156        Args:
157            layer: new layer as string
158
159        """
160        for v in self.vertices:
161            v.dxf.layer = layer
162
163    def on_linetype_change(self, linetype: str):
164        """ Event handler for linetype change. Changes also the linetype of all
165        vertices.
166
167        Args:
168            linetype: new linetype as string
169
170        """
171        for v in self.vertices:
172            v.dxf.linetype = linetype
173
174    def get_vertex_flags(self) -> int:
175        return const.VERTEX_FLAGS[self.get_mode()]
176
177    def get_mode(self) -> str:
178        """ Returns POLYLINE type as string:
179
180            - 'AcDb2dPolyline'
181            - 'AcDb3dPolyline'
182            - 'AcDbPolygonMesh'
183            - 'AcDbPolyFaceMesh'
184
185        """
186        if self.is_3d_polyline:
187            return 'AcDb3dPolyline'
188        elif self.is_polygon_mesh:
189            return 'AcDbPolygonMesh'
190        elif self.is_poly_face_mesh:
191            return 'AcDbPolyFaceMesh'
192        else:
193            return 'AcDb2dPolyline'
194
195    @property
196    def is_2d_polyline(self) -> bool:
197        """ ``True`` if POLYLINE is a 2D polyline. """
198        return self.dxf.flags & self.ANY3D == 0
199
200    @property
201    def is_3d_polyline(self) -> bool:
202        """ ``True`` if POLYLINE is a 3D polyline. """
203        return bool(self.dxf.flags & self.POLYLINE_3D)
204
205    @property
206    def is_polygon_mesh(self) -> bool:
207        """ ``True`` if POLYLINE is a polygon mesh, see :class:`Polymesh` """
208        return bool(self.dxf.flags & self.POLYMESH)
209
210    @property
211    def is_poly_face_mesh(self) -> bool:
212        """ ``True`` if POLYLINE is a poly face mesh, see :class:`Polyface` """
213        return bool(self.dxf.flags & self.POLYFACE)
214
215    @property
216    def is_closed(self) -> bool:
217        """ ``True`` if POLYLINE is closed. """
218        return bool(self.dxf.flags & self.CLOSED)
219
220    @property
221    def is_m_closed(self) -> bool:
222        """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in m
223        direction.
224        """
225        return bool(self.dxf.flags & self.MESH_CLOSED_M_DIRECTION)
226
227    @property
228    def is_n_closed(self) -> bool:
229        """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in n
230        direction.
231        """
232        return bool(self.dxf.flags & self.MESH_CLOSED_N_DIRECTION)
233
234    @property
235    def has_arc(self) -> bool:
236        """ Returns ``True`` if 2D POLYLINE has an arc segment. """
237        if self.is_2d_polyline:
238            return any(
239                v.dxf.hasattr('bulge') and bool(v.dxf.bulge) for v in
240                self.vertices
241            )
242        else:
243            return False
244
245    @property
246    def has_width(self) -> bool:
247        """ Returns ``True`` if 2D POLYLINE has default width values or any
248        segment with width attributes.
249
250        .. versionadded:: 0.14
251
252        """
253        if self.is_2d_polyline:
254            if self.dxf.hasattr('default_start_width') and bool(
255                    self.dxf.default_start_width):
256                return True
257            if self.dxf.hasattr('default_end_width') and bool(
258                    self.dxf.default_end_width):
259                return True
260            for v in self.vertices:
261                if v.dxf.hasattr('start_width') and bool(v.dxf.start_width):
262                    return True
263                if v.dxf.hasattr('end_width') and bool(v.dxf.end_width):
264                    return True
265        return False
266
267    def m_close(self, status=True) -> None:
268        """ Close POLYMESH in m direction if `status` is ``True`` (also closes
269        POLYLINE), clears closed state if `status` is ``False``.
270        """
271        self.set_flag_state(self.MESH_CLOSED_M_DIRECTION, status, name='flags')
272
273    def n_close(self, status=True) -> None:
274        """ Close POLYMESH in n direction if `status` is ``True``, clears closed
275        state if `status` is ``False``.
276        """
277        self.set_flag_state(self.MESH_CLOSED_N_DIRECTION, status, name='flags')
278
279    def close(self, m_close=True, n_close=False) -> None:
280        """ Set closed state of POLYMESH and POLYLINE in m direction and n
281        direction. ``True`` set closed flag, ``False`` clears closed flag.
282        """
283        self.m_close(m_close)
284        self.n_close(n_close)
285
286    def __len__(self) -> int:
287        """ Returns count of :class:`Vertex` entities. """
288        return len(self.vertices)
289
290    def __getitem__(self, pos) -> 'DXFVertex':
291        """ Get :class:`Vertex` entity at position `pos`, supports ``list``
292        slicing.
293        """
294        return self.vertices[pos]
295
296    def points(self) -> Iterable[Vec3]:
297        """ Returns iterable of all polyline vertices as ``(x, y, z)`` tuples,
298        not as :class:`Vertex` objects.
299        """
300        return (vertex.dxf.location for vertex in self.vertices)
301
302    def _append_vertex(self, vertex: 'DXFVertex') -> None:
303        self.vertices.append(vertex)
304
305    def append_vertices(self, points: Iterable['Vertex'],
306                        dxfattribs: Dict = None) -> None:
307        """ Append multiple :class:`Vertex` entities at location `points`.
308
309        Args:
310            points: iterable of ``(x, y[, z])`` tuples
311            dxfattribs: dict of DXF attributes for :class:`Vertex` class
312
313        """
314        dxfattribs = dxfattribs or {}
315        for vertex in self._build_dxf_vertices(points, dxfattribs):
316            self._append_vertex(vertex)
317
318    def append_formatted_vertices(self, points: Iterable['Vertex'],
319                                  format: str = 'xy',
320                                  dxfattribs: Dict = None) -> None:
321        """ Append multiple :class:`Vertex` entities at location `points`.
322
323        Args:
324            points: iterable of (x, y, [start_width, [end_width, [bulge]]])
325                    tuple
326            format: format string, default is ``'xy'``, see: :ref:`format codes`
327            dxfattribs: dict of DXF attributes for :class:`Vertex` class
328
329        """
330        dxfattribs = dxfattribs or {}
331        dxfattribs['flags'] = (
332                dxfattribs.get('flags', 0) | self.get_vertex_flags()
333        )
334
335        # same DXF attributes for VERTEX entities as for POLYLINE
336        dxfattribs['owner'] = self.dxf.owner
337        dxfattribs['layer'] = self.dxf.layer
338        if self.dxf.hasattr('linetype'):
339            dxfattribs['linetype'] = self.dxf.linetype
340
341        for point in points:
342            attribs = vertex_attribs(point, format)
343            attribs.update(dxfattribs)
344            vertex = self._new_compound_entity('VERTEX', attribs)
345            self._append_vertex(vertex)
346
347    def append_vertex(self, point: 'Vertex', dxfattribs: dict = None) -> None:
348        """ Append a single :class:`Vertex` entity at location `point`.
349
350        Args:
351            point: as ``(x, y[, z])`` tuple
352            dxfattribs: dict of DXF attributes for :class:`Vertex` class
353
354        """
355        dxfattribs = dxfattribs or {}
356        for vertex in self._build_dxf_vertices([point], dxfattribs):
357            self._append_vertex(vertex)
358
359    def insert_vertices(self, pos: int, points: Iterable['Vertex'],
360                        dxfattribs: dict = None) -> None:
361        """
362        Insert vertices `points` into :attr:`Polyline.vertices` list
363        at insertion location `pos` .
364
365        Args:
366            pos: insertion position of list :attr:`Polyline.vertices`
367            points: list of ``(x, y[, z])`` tuples
368            dxfattribs: dict of DXF attributes for :class:`Vertex` class
369
370        """
371        dxfattribs = dxfattribs or {}
372        self.vertices[pos:pos] = list(
373            self._build_dxf_vertices(points, dxfattribs))
374
375    def _build_dxf_vertices(self, points: Iterable['Vertex'],
376                            dxfattribs: dict) -> List['DXFVertex']:
377        """ Converts point (x, y, z)-tuples into DXFVertex objects.
378
379        Args:
380            points: list of (x, y, z)-tuples
381            dxfattribs: dict of DXF attributes
382        """
383        dxfattribs['flags'] = (
384                dxfattribs.get('flags', 0) | self.get_vertex_flags()
385        )
386
387        # same DXF attributes for VERTEX entities as for POLYLINE
388        dxfattribs['owner'] = self.dxf.owner
389        dxfattribs['layer'] = self.dxf.layer
390        if self.dxf.hasattr('linetype'):
391            dxfattribs['linetype'] = self.dxf.linetype
392        for point in points:
393            dxfattribs['location'] = Vec3(point)
394            yield self._new_compound_entity('VERTEX', dxfattribs)
395
396    def cast(self) -> Union['Polyline', 'Polymesh', 'Polyface']:
397        mode = self.get_mode()
398        if mode == 'AcDbPolyFaceMesh':
399            return Polyface.from_polyline(self)
400        elif mode == 'AcDbPolygonMesh':
401            return Polymesh.from_polyline(self)
402        else:
403            return self
404
405    def transform(self, m: Matrix44) -> 'Polyline':
406        """ Transform the POLYLINE entity by transformation matrix `m` inplace.
407        """
408
409        def _ocs_locations(elevation):
410            for vertex in self.vertices:
411                location = vertex.dxf.location
412                if elevation is not None:
413                    # Older DXF version may not have written the z-axis, which
414                    # is now 0 by default in ezdxf, so replace existing z-axis
415                    # by elevation value.
416                    location = location.replace(z=elevation)
417                yield location
418
419        if self.is_2d_polyline:
420            dxf = self.dxf
421            ocs = OCSTransform(self.dxf.extrusion, m)
422            if not ocs.scale_uniform and self.has_arc:
423                # Parent function has to catch this Exception and explode this
424                # 2D POLYLINE into LINE and ELLIPSE entities.
425                raise NonUniformScalingError(
426                    '2D POLYLINE with arcs does not support non uniform scaling'
427                )
428
429            if dxf.hasattr('elevation'):
430                z_axis = dxf.elevation.z
431            else:
432                z_axis = None
433
434            vertices = [
435                ocs.transform_vertex(vertex) for vertex in
436                _ocs_locations(z_axis)
437            ]
438
439            # All vertices of a 2D polyline have the same z-axis:
440            if vertices:
441                dxf.elevation = vertices[0].replace(x=0, y=0)
442
443            for vertex, location in zip(self.vertices, vertices):
444                vertex.dxf.location = location
445
446            if dxf.hasattr('thickness'):
447                dxf.thickness = ocs.transform_length((0, 0, dxf.thickness))
448
449            dxf.extrusion = ocs.new_extrusion
450        else:
451            for vertex in self.vertices:
452                vertex.transform(m)
453        return self
454
455    def explode(self, target_layout: 'BaseLayout' = None) -> 'EntityQuery':
456        """ Explode POLYLINE as DXF LINE, ARC or 3DFACE primitives into target
457        layout, if the target layout is ``None``, the target layout is the
458        layout of the POLYLINE entity .
459        Returns an :class:`~ezdxf.query.EntityQuery` container including all
460        DXF primitives.
461
462        Args:
463            target_layout: target layout for DXF primitives, ``None`` for same
464            layout as source entity.
465
466        """
467        return explode_entity(self, target_layout)
468
469    def virtual_entities(self) -> Iterable[Union['Line', 'Arc', 'Face3d']]:
470        """  Yields 'virtual' parts of POLYLINE as LINE, ARC or 3DFACE
471        primitives.
472
473        This entities are located at the original positions, but are not stored
474        in the entity database, have no handle and are not assigned to any
475        layout.
476
477        """
478        return virtual_polyline_entities(self)
479
480    def audit(self, auditor: 'Auditor') -> None:
481        """ Audit and repair POLYLINE entity. """
482
483        def audit_sub_entity(entity):
484            entity.doc = doc  # grant same document
485            dxf = entity.dxf
486            if dxf.owner != owner:
487                dxf.owner = owner
488            if dxf.layer != layer:
489                dxf.layer = layer
490
491        doc = self.doc
492        owner = self.dxf.handle
493        layer = self.dxf.layer
494        for vertex in self.vertices:
495            audit_sub_entity(vertex)
496
497        seqend = self.seqend
498        if seqend:
499            audit_sub_entity(seqend)
500        elif doc:
501            self.new_seqend()
502            auditor.fixed_error(
503                code=AuditError.MISSING_REQUIRED_SEQEND,
504                message=f'Create required SEQEND entity for {str(self)}.',
505                dxf_entity=self,
506            )
507
508
509class Polyface(Polyline):
510    """
511    PolyFace structure:
512
513    POLYLINE
514      AcDbEntity
515      AcDbPolyFaceMesh
516    VERTEX - Vertex
517      AcDbEntity
518      AcDbVertex
519      AcDbPolyFaceMeshVertex
520    VERTEX - Face
521      AcDbEntity
522      AcDbFaceRecord
523    SEQEND
524
525    Order of mesh_vertices and face_records is important (DXF R2010):
526
527        1. mesh_vertices: the polyface mesh vertex locations
528        2. face_records: indices of the face forming vertices
529
530    """
531
532    @classmethod
533    def from_polyline(cls, polyline: Polyline) -> 'Polyface':
534        polyface = cls.shallow_copy(polyline)
535        polyface._sub_entities = polyline._sub_entities
536        polyface.seqend = polyline.seqend
537        # do not destroy polyline - all data would be lost
538        return polyface
539
540    def append_face(self, face: 'FaceType', dxfattribs: Dict = None) -> None:
541        """
542        Append a single face. A `face` is a list of ``(x, y, z)`` tuples.
543
544        Args:
545            face: List[``(x, y, z)`` tuples]
546            dxfattribs: dict of DXF attributes for :class:`Vertex` entity
547
548        """
549        self.append_faces([face], dxfattribs)
550
551    def _points_to_dxf_vertices(self, points: Iterable['Vertex'],
552                                dxfattribs: Dict) -> List['DXFVertex']:
553        """ Converts point (x,y, z)-tuples into DXFVertex objects.
554
555        Args:
556            points: List[``(x, y, z)`` tuples]
557            dxfattribs: dict of DXF attributes for :class:`Vertex` entity
558
559        """
560        dxfattribs['flags'] = (
561                dxfattribs.get('flags', 0) | self.get_vertex_flags()
562        )
563
564        # All vertices have to be on the same layer as the POLYLINE entity:
565        dxfattribs['layer'] = self.get_dxf_attrib('layer', '0')
566        vertices: List[DXFVertex] = []
567        for point in points:
568            dxfattribs['location'] = point
569            vertices.append(cast(
570                'DXFVertex',
571                self._new_compound_entity('VERTEX', dxfattribs)
572            ))
573        return vertices
574
575    def append_faces(self, faces: Iterable['FaceType'],
576                     dxfattribs: Dict = None) -> None:
577        """
578        Append multiple `faces`. `faces` is a list of single faces and a single
579        face is a list of ``(x, y, z)`` tuples.
580
581        Args:
582            faces: list of List[``(x, y, z)`` tuples]
583            dxfattribs: dict of DXF attributes for :class:`Vertex` entity
584
585        """
586
587        def new_face_record() -> 'DXFVertex':
588            dxfattribs['flags'] = const.VTX_3D_POLYFACE_MESH_VERTEX
589            # location of face record vertex is always (0, 0, 0)
590            dxfattribs['location'] = Vec3()
591            return self._new_compound_entity('VERTEX', dxfattribs)
592
593        dxfattribs = dxfattribs or {}
594
595        existing_vertices, existing_faces = self.indexed_faces()
596        new_faces: List[FaceProxy] = []
597        for face in faces:
598            face_mesh_vertices = self._points_to_dxf_vertices(face, {})
599            # Index of first new vertex
600            index = len(existing_vertices)
601            existing_vertices.extend(face_mesh_vertices)
602            face_record = FaceProxy(new_face_record(), existing_vertices)
603
604            # Set VERTEX indices:
605            face_record.indices = tuple(
606                range(index, index + len(face_mesh_vertices))
607            )
608            new_faces.append(face_record)
609        self._rebuild(chain(existing_faces, new_faces))
610
611    def _rebuild(self, faces: Iterable['FaceProxy'],
612                 precision: int = 6) -> None:
613        """
614        Build a valid POLYFACE structure from `faces`.
615
616        Args:
617            faces: iterable of FaceProxy objects.
618
619        """
620        polyface_builder = PolyfaceBuilder(faces, precision=precision)
621        self._sub_entities = []
622        self._sub_entities = polyface_builder.get_vertices()
623        self.update_count(polyface_builder.nvertices, polyface_builder.nfaces)
624
625    def update_count(self, nvertices: int, nfaces: int) -> None:
626        self.dxf.m_count = nvertices
627        self.dxf.n_count = nfaces
628
629    def optimize(self, precision: int = 6) -> None:
630        """
631        Rebuilds :class:`Polyface` including vertex optimization by merging
632        vertices with nearly same vertex locations.
633
634        Args:
635            precision: floating point precision for determining identical
636                       vertex locations
637
638        """
639        vertices, faces = self.indexed_faces()
640        self._rebuild(faces, precision)
641
642    def faces(self) -> Iterable[List['DXFVertex']]:
643        """
644        Iterable of all faces, a face is a tuple of vertices.
645
646        Returns:
647             list: [vertex, vertex, vertex, [vertex,] face_record]
648
649        """
650        _, faces = self.indexed_faces()
651        for face in faces:
652            face_vertices = list(face)
653            face_vertices.append(face.face_record)
654            yield face_vertices
655
656    def indexed_faces(self) -> Tuple[List['DXFVertex'], Iterable['FaceProxy']]:
657        """
658        Returns a list of all vertices and a generator of FaceProxy() objects.
659
660        (internal API)
661        """
662        vertices = []
663        face_records = []
664        for vertex in self.vertices:  # type: DXFVertex
665            (
666                vertices if vertex.is_poly_face_mesh_vertex else face_records).append(
667                vertex)
668
669        faces = (FaceProxy(face_record, vertices) for face_record in
670                 face_records)
671        return vertices, faces
672
673
674class FaceProxy:
675    """
676    Represents a single face of a polyface structure. (internal class)
677
678    vertices:
679
680        List of all polyface vertices.
681
682    face_record:
683
684        The face forming vertex of type ``AcDbFaceRecord``, contains the indices
685        to the face building vertices. Indices of the DXF structure are 1-based
686        and a negative index indicates the beginning of an invisible edge.
687        Face.face_record.dxf.color determines the color of the face.
688
689    indices:
690
691        Indices to the face building vertices as tuple. This indices are 0-base
692        and are used to get vertices from the list `Face.vertices`.
693
694    """
695    __slots__ = ('vertices', 'face_record', 'indices')
696
697    def __init__(self, face_record: 'DXFVertex',
698                 vertices: Sequence['DXFVertex']):
699        """ Returns iterable of all face vertices as :class:`Vertex` entities.
700        """
701        self.vertices: Sequence[DXFVertex] = vertices
702        self.face_record: DXFVertex = face_record
703        self.indices: Sequence[int] = self._indices()
704
705    def __len__(self) -> int:
706        """ Returns count of face vertices (without face_record). """
707        return len(self.indices)
708
709    def __getitem__(self, pos: int) -> 'DXFVertex':
710        """ Returns :class:`Vertex` at position `pos`.
711
712        Args:
713            pos: vertex position 0-based
714
715        """
716        return self.vertices[self.indices[pos]]
717
718    def __iter__(self) -> Iterable['DXFVertex']:
719        return (self.vertices[index] for index in self.indices)
720
721    def points(self) -> Iterable['Vertex']:
722        """ Returns iterable of all face vertex locations as (x, y, z)-tuples.
723        """
724        return (vertex.dxf.location for vertex in self)
725
726    def _raw_indices(self) -> Iterable[int]:
727        return (self.face_record.get_dxf_attrib(name, 0) for name in
728                const.VERTEXNAMES)
729
730    def _indices(self) -> Sequence[int]:
731        return tuple(
732            abs(index) - 1 for index in self._raw_indices() if index != 0)
733
734    def is_edge_visible(self, pos: int) -> bool:
735        """ Returns ``True`` if edge starting at vertex `pos` is visible.
736
737        Args:
738            pos: vertex position 0-based
739
740        """
741        name = const.VERTEXNAMES[pos]
742        return self.face_record.get_dxf_attrib(name) > 0
743
744
745class PolyfaceBuilder:
746    """ Optimized POLYFACE builder. (internal class) """
747
748    def __init__(self, faces: Iterable['FaceProxy'], precision: int = 6):
749        self.precision: int = precision
750        self.faces: List[DXFVertex] = []
751        self.vertices: List[DXFVertex] = []
752        self.index_mapping: Dict[Tuple[float, ...], int] = {}
753        self.build(faces)
754
755    @property
756    def nvertices(self) -> int:
757        return len(self.vertices)
758
759    @property
760    def nfaces(self) -> int:
761        return len(self.faces)
762
763    def get_vertices(self) -> List['DXFVertex']:
764        vertices = self.vertices[:]
765        vertices.extend(self.faces)
766        return vertices
767
768    def build(self, faces: Iterable['FaceProxy']) -> None:
769        for face in faces:
770            face_record = face.face_record
771            for vertex, name in zip(face, VERTEXNAMES):
772                index = self.add(vertex)
773                # preserve sign of old index value
774                sign = -1 if face_record.dxf.get(name, 0) < 0 else +1
775                face_record.dxf.set(name, (index + 1) * sign)
776            self.faces.append(face_record)
777
778    def add(self, vertex: 'DXFVertex') -> int:
779        def key(point):
780            return tuple((round(coord, self.precision) for coord in point))
781
782        location = key(vertex.dxf.location)
783        try:
784            return self.index_mapping[location]
785        except KeyError:
786            index = len(self.vertices)
787            self.index_mapping[location] = index
788            self.vertices.append(vertex)
789            return index
790
791
792class Polymesh(Polyline):
793    """
794    PolyMesh structure:
795
796    POLYLINE
797      AcDbEntity
798      AcDbPolygonMesh
799    VERTEX
800      AcDbEntity
801      AcDbVertex
802      AcDbPolygonMeshVertex
803    """
804
805    @classmethod
806    def from_polyline(cls, polyline: Polyline) -> 'Polymesh':
807        polymesh = cls.shallow_copy(polyline)
808        polymesh._sub_entities = polyline._sub_entities
809        polymesh.seqend = polyline.seqend
810        return polymesh
811
812    def set_mesh_vertex(self, pos: Tuple[int, int], point: 'Vertex',
813                        dxfattribs: dict = None):
814        """
815        Set location and DXF attributes of a single mesh vertex.
816
817        Args:
818            pos: 0-based (row, col)-tuple, position of mesh vertex
819            point: (x, y, z)-tuple, new 3D coordinates of the mesh vertex
820            dxfattribs: dict of DXF attributes
821
822        """
823        dxfattribs = dxfattribs or {}
824        dxfattribs['location'] = point
825        vertex = self.get_mesh_vertex(pos)
826        vertex.update_dxf_attribs(dxfattribs)
827
828    def get_mesh_vertex(self, pos: Tuple[int, int]) -> 'DXFVertex':
829        """
830        Get location of a single mesh vertex.
831
832        Args:
833            pos: 0-based ``(row, col)`` tuple, position of mesh vertex
834
835        """
836        m_count = self.dxf.m_count
837        n_count = self.dxf.n_count
838        m, n = pos
839        if 0 <= m < m_count and 0 <= n < n_count:
840            pos = m * n_count + n
841            return self.vertices[pos]
842        else:
843            raise const.DXFIndexError(repr(pos))
844
845    def get_mesh_vertex_cache(self) -> 'MeshVertexCache':
846        """
847        Get a :class:`MeshVertexCache` object for this POLYMESH.
848        The caching object provides fast access to the :attr:`location`
849        attribute of mesh vertices.
850
851        """
852        return MeshVertexCache(self)
853
854
855class MeshVertexCache:
856    """ Cache mesh vertices in a dict, keys are 0-based (row, col)-tuples.
857
858    vertices:
859        Dict of mesh vertices, keys are 0-based (row, col)-tuples. Writing to
860        this dict doesn't change the DXF entity.
861
862    """
863    __slots__ = ('vertices',)
864
865    def __init__(self, mesh: 'Polyline'):
866        self.vertices: Dict[Tuple[int, int], DXFVertex] = self._setup(
867            mesh, mesh.dxf.m_count, mesh.dxf.n_count
868        )
869
870    def _setup(self, mesh: 'Polyline', m_count: int, n_count: int) -> dict:
871        cache: Dict[Tuple[int, int], DXFVertex] = {}
872        vertices = iter(mesh.vertices)
873        for m in range(m_count):
874            for n in range(n_count):
875                cache[(m, n)] = next(vertices)
876        return cache
877
878    def __getitem__(self, pos: Tuple[int, int]) -> 'Vertex':
879        """
880        Get mesh vertex location as (x, y, z)-tuple.
881
882        Args:
883            pos: 0-based (row, col)-tuple.
884
885        """
886        try:
887            return self.vertices[pos].dxf.location
888        except KeyError:
889            raise const.DXFIndexError(repr(pos))
890
891    def __setitem__(self, pos: Tuple[int, int], location: 'Vertex') -> None:
892        """
893        Get mesh vertex location as (x, y, z)-tuple.
894
895        Args:
896            pos: 0-based (row, col)-tuple.
897            location: (x, y, z)-tuple
898
899        """
900        try:
901            self.vertices[pos].dxf.location = location
902        except KeyError:
903            raise const.DXFIndexError(repr(pos))
904
905
906acdb_vertex = DefSubclass('AcDbVertex', {  # last subclass index -1
907    # Location point in OCS if 2D, and WCS if 3D
908    'location': DXFAttr(10, xtype=XType.point3d),
909    'start_width': DXFAttr(40, default=0, optional=True),
910    'end_width': DXFAttr(41, default=0, optional=True),
911
912    # Bulge (optional; default is 0). The bulge is the tangent of one fourth
913    # the included angle for an arc segment, made negative if the arc goes
914    # clockwise from the start point to the endpoint. A bulge of 0 indicates
915    # a straight segment, and a bulge of 1 is a semicircle.
916    'bulge': DXFAttr(42, default=0, optional=True),
917    'flags': DXFAttr(70, default=0),
918    # Curve fit tangent direction (in degrees)
919    'tangent': DXFAttr(50, optional=True),
920    'vtx0': DXFAttr(71, optional=True),
921    'vtx1': DXFAttr(72, optional=True),
922    'vtx2': DXFAttr(73, optional=True),
923    'vtx3': DXFAttr(74, optional=True),
924    'vertex_identifier': DXFAttr(91, optional=True),
925})
926acdb_vertex_group_codes = group_code_mapping(acdb_vertex)
927
928
929@factory.register_entity
930class DXFVertex(DXFGraphic):
931    """ DXF VERTEX entity """
932    DXFTYPE = 'VERTEX'
933
934    DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_vertex)
935    # Extra vertex created by curve-fitting:
936    EXTRA_VERTEX_CREATED = 1
937
938    # Curve-fit tangent defined for this vertex. A curve-fit tangent direction
939    # of 0 may be omitted from the DXF output, but is significant if this bit
940    # is set:
941    CURVE_FIT_TANGENT = 2
942
943    # 4 = unused, never set in dxf files
944    # Spline vertex created by spline-fitting
945    SPLINE_VERTEX_CREATED = 8
946    SPLINE_FRAME_CONTROL_POINT = 16
947    POLYLINE_3D_VERTEX = 32
948    POLYGON_MESH_VERTEX = 64
949    POLYFACE_MESH_VERTEX = 128
950    FACE_FLAGS = POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
951    VTX3D = POLYLINE_3D_VERTEX + POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
952
953    def load_dxf_attribs(
954            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
955        dxf = super().load_dxf_attribs(processor)
956        if processor:
957            # VERTEX can have 3 subclasses if representing a `face record` or
958            # 4 subclasses if representing a vertex location, just the last
959            # subclass contains data:
960            processor.fast_load_dxfattribs(
961                dxf, acdb_vertex_group_codes, subclass=-1, recover=True)
962        return dxf
963
964    def export_entity(self, tagwriter: 'TagWriter') -> None:
965        """ Export entity specific data as DXF tags. """
966        super().export_entity(tagwriter)
967        if tagwriter.dxfversion > DXF12:
968            if self.is_face_record:
969                tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbFaceRecord')
970            else:
971                tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbVertex')
972                if self.is_3d_polyline_vertex:
973                    tagwriter.write_tag2(
974                        SUBCLASS_MARKER, 'AcDb3dPolylineVertex'
975                    )
976                elif self.is_poly_face_mesh_vertex:
977                    tagwriter.write_tag2(
978                        SUBCLASS_MARKER, 'AcDbPolyFaceMeshVertex'
979                    )
980                elif self.is_polygon_mesh_vertex:
981                    tagwriter.write_tag2(
982                        SUBCLASS_MARKER, 'AcDbPolygonMeshVertex'
983                    )
984                else:
985                    tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDb2dVertex')
986
987        self.dxf.export_dxf_attribs(tagwriter, [
988            'location', 'start_width', 'end_width', 'bulge', 'flags', 'tangent',
989            'vtx0', 'vtx1', 'vtx2', 'vtx3', 'vertex_identifier'
990        ])
991
992    @property
993    def is_2d_polyline_vertex(self) -> bool:
994        return self.dxf.flags & self.VTX3D == 0
995
996    @property
997    def is_3d_polyline_vertex(self) -> bool:
998        return self.dxf.flags & self.POLYLINE_3D_VERTEX
999
1000    @property
1001    def is_polygon_mesh_vertex(self) -> bool:
1002        return self.dxf.flags & self.POLYGON_MESH_VERTEX
1003
1004    @property
1005    def is_poly_face_mesh_vertex(self) -> bool:
1006        return self.dxf.flags & self.FACE_FLAGS == self.FACE_FLAGS
1007
1008    @property
1009    def is_face_record(self) -> bool:
1010        return (self.dxf.flags & self.FACE_FLAGS) == self.POLYFACE_MESH_VERTEX
1011
1012    def transform(self, m: 'Matrix44') -> 'DXFVertex':
1013        """ Transform the VERTEX entity by transformation matrix `m` inplace.
1014        """
1015        if self.is_face_record:
1016            return self
1017        self.dxf.location = m.transform(self.dxf.location)
1018        return self
1019
1020    def format(self, format='xyz') -> Sequence:
1021        """ Return formatted vertex components as tuple.
1022
1023        Format codes:
1024
1025            - ``x`` = x-coordinate
1026            - ``y`` = y-coordinate
1027            - ``z`` = z-coordinate
1028            - ``s`` = start width
1029            - ``e`` = end width
1030            - ``b`` = bulge value
1031            - ``v`` = (x, y, z) as tuple
1032
1033        Args:
1034            format: format string, default is "xyz"
1035
1036        .. versionadded:: 0.14
1037
1038        """
1039        dxf = self.dxf
1040        v = Vec3(dxf.location)
1041        x, y, z = v.xyz
1042        b = dxf.bulge
1043        s = dxf.start_width
1044        e = dxf.end_width
1045        vars = locals()
1046        return tuple(vars[code] for code in format.lower())
1047
1048
1049def vertex_attribs(data: Sequence[float], format='xyseb') -> dict:
1050    """
1051    Create VERTEX attributes from input data.
1052
1053    Format codes:
1054
1055        - ``x`` = x-coordinate
1056        - ``y`` = y-coordinate
1057        - ``s`` = start width
1058        - ``e`` = end width
1059        - ``b`` = bulge value
1060        - ``v`` = (x, y [,z]) tuple (z-axis is ignored)
1061
1062    Args:
1063        data: list or tuple of point components
1064        format: format string, default is 'xyseb'
1065
1066    Returns:
1067       dict with keys: 'location', 'bulge', 'start_width', 'end_width'
1068
1069    """
1070    attribs = dict()
1071    format = [code for code in format.lower() if code in FORMAT_CODES]
1072    location = Vec3()
1073    for code, value in zip(format, data):
1074        if code not in FORMAT_CODES:
1075            continue
1076        if code == 'v':
1077            location = Vec3(value)
1078        elif code == 'b':
1079            attribs['bulge'] = value
1080        elif code == 's':
1081            attribs['start_width'] = value
1082        elif code == 'e':
1083            attribs['end_width'] = value
1084        elif code == 'x':
1085            location = location.replace(x=value)
1086        elif code == 'y':
1087            location = location.replace(y=value)
1088    attribs['location'] = location
1089    return attribs
1090