1#  Copyright (c) 2020, Manfred Moitzi
2#  License: MIT License
3from typing import TYPE_CHECKING, List, cast
4from itertools import chain
5from ezdxf.entities import factory
6from ezdxf.math import Vec3, OCS
7import logging
8
9if TYPE_CHECKING:
10    from ezdxf.entities import MLine, DXFGraphic, Hatch, LWPolyline
11
12__all__ = ['virtual_entities']
13
14logger = logging.getLogger('ezdxf')
15
16
17# The MLINE geometry stored in vertices, is the final geometry,
18# scaling factor, justification and MLineStyle settings are already
19# applied.
20
21def _dxfattribs(mline):
22    attribs = mline.graphic_properties()
23    # True color value of MLINE is ignored by CAD applications:
24    if 'true_color' in attribs:
25        del attribs['true_color']
26    return attribs
27
28
29def virtual_entities(mline: 'MLine') -> List['DXFGraphic']:
30    """ Yields 'virtual' parts of MLINE as LINE, ARC and HATCH entities.
31
32    This entities are located at the original positions, but are not stored
33    in the entity database, have no handle and are not assigned to any
34    layout.
35    """
36
37    def filling():
38        attribs = _dxfattribs(mline)
39        attribs['color'] = style.dxf.fill_color
40        attribs['elevation'] = ocs.from_wcs(bottom_border[0]).replace(x=0, y=0)
41        attribs['extrusion'] = mline.dxf.extrusion
42        hatch = cast('Hatch', factory.new('HATCH', dxfattribs=attribs, doc=doc))
43        bulges: List[float] = [0.0] * (len(bottom_border) * 2)
44        points = chain(
45            ocs.points_from_wcs(bottom_border),
46            ocs.points_from_wcs(reversed(top_border))
47        )
48        if not closed:
49            if style.get_flag_state(style.END_ROUND):
50                bulges[len(bottom_border) - 1] = 1.0
51            if style.get_flag_state(style.START_ROUND):
52                bulges[-1] = 1.0
53        lwpoints = ((v.x, v.y, bulge) for v, bulge in zip(points, bulges))
54        hatch.paths.add_polyline_path(lwpoints, is_closed=True)
55        return hatch
56
57    def start_cap():
58        entities = []
59        if style.get_flag_state(style.START_SQUARE):
60            entities.extend(create_miter(miter_points[0]))
61        if style.get_flag_state(style.START_ROUND):
62            entities.extend(round_caps(0, top_index, bottom_index))
63        if style.get_flag_state(style.START_INNER_ARC) and \
64                len(style.elements) > 3:
65            start_index = ordered_indices[-2]
66            end_index = ordered_indices[1]
67            entities.extend(round_caps(0, start_index, end_index))
68        return entities
69
70    def end_cap():
71        entities = []
72        if style.get_flag_state(style.END_SQUARE):
73            entities.extend(create_miter(miter_points[-1]))
74        if style.get_flag_state(style.END_ROUND):
75            entities.extend(round_caps(-1, bottom_index, top_index))
76        if style.get_flag_state(style.END_INNER_ARC) and \
77                len(style.elements) > 3:
78            start_index = ordered_indices[1]
79            end_index = ordered_indices[-2]
80            entities.extend(round_caps(-1, start_index, end_index))
81        return entities
82
83    def round_caps(miter_index: int, start_index: int, end_index:int):
84        color1 = style.elements[start_index].color
85        color2 = style.elements[end_index].color
86        start = ocs.from_wcs(miter_points[miter_index][start_index])
87        end = ocs.from_wcs(miter_points[miter_index][end_index])
88        return _arc_caps(start, end, color1, color2)
89
90    def _arc_caps(start: Vec3, end: Vec3, color1: int, color2: int):
91        attribs = _dxfattribs(mline)
92        center = start.lerp(end)
93        radius = (end - start).magnitude / 2.0
94        angle = (start - center).angle_deg
95        attribs['center'] = center
96        attribs['radius'] = radius
97        attribs['color'] = color1
98        attribs['start_angle'] = angle
99        attribs['end_angle'] = angle + (180 if color1 == color2 else 90)
100        arc1 = factory.new('ARC', dxfattribs=attribs, doc=doc)
101        if color1 == color2:
102            return arc1,
103        attribs['start_angle'] = angle + 90
104        attribs['end_angle'] = angle + 180
105        attribs['color'] = color2
106        arc2 = factory.new('ARC', dxfattribs=attribs, doc=doc)
107        return arc1, arc2
108
109    def lines():
110        prev = None
111        _lines = []
112        attribs = _dxfattribs(mline)
113
114        for miter in miter_points:
115            if prev is not None:
116                for index, element in enumerate(style.elements):
117                    attribs['start'] = prev[index]
118                    attribs['end'] = miter[index]
119                    attribs['color'] = element.color
120                    attribs['linetype'] = element.linetype
121                    _lines.append(factory.new(
122                        'LINE', dxfattribs=attribs, doc=doc))
123            prev = miter
124        return _lines
125
126    def display_miter():
127        _lines = []
128        skip = set()
129        skip.add(len(miter_points) - 1)
130        if not closed:
131            skip.add(0)
132        for index, miter in enumerate(miter_points):
133            if index not in skip:
134                _lines.extend(create_miter(miter))
135        return _lines
136
137    def create_miter(miter):
138        _lines = []
139        attribs = _dxfattribs(mline)
140        top = miter[top_index]
141        bottom = miter[bottom_index]
142        zero = bottom.lerp(top)
143        element = style.elements[top_index]
144        attribs['start'] = top
145        attribs['end'] = zero
146        attribs['color'] = element.color
147        attribs['linetype'] = element.linetype
148        _lines.append(factory.new(
149            'LINE', dxfattribs=attribs, doc=doc))
150        element = style.elements[bottom_index]
151        attribs['start'] = bottom
152        attribs['end'] = zero
153        attribs['color'] = element.color
154        attribs['linetype'] = element.linetype
155        _lines.append(factory.new(
156            'LINE', dxfattribs=attribs, doc=doc))
157        return _lines
158
159    entities = []
160    if not mline.is_alive or mline.doc is None or len(mline.vertices) < 2:
161        return entities
162
163    style = mline.style
164    if style is None:
165        return entities
166
167    doc = mline.doc
168    ocs = OCS(mline.dxf.extrusion)
169    element_count = len(style.elements)
170    closed = mline.is_closed
171    ordered_indices = style.ordered_indices()
172    bottom_index = ordered_indices[0]
173    top_index = ordered_indices[-1]
174    bottom_border: List[Vec3] = []
175    top_border: List[Vec3] = []
176    miter_points: List[List[Vec3]] = []
177
178    for vertex in mline.vertices:
179        offsets = vertex.line_params
180        if len(offsets) != element_count:
181            logger.debug(
182                f'Invalid line parametrization for vertex {len(miter_points)} '
183                f'in {str(mline)}.'
184            )
185            return entities
186        location = vertex.location
187        miter_direction = vertex.miter_direction
188        miter = []
189        for offset in offsets:
190            try:
191                length = offset[0]
192            except IndexError:  # DXFStructureError?
193                length = 0
194            miter.append(location + miter_direction * length)
195        miter_points.append(miter)
196        top_border.append(miter[top_index])
197        bottom_border.append(miter[bottom_index])
198
199    if closed:
200        miter_points.append(miter_points[0])
201        top_border.append(top_border[0])
202        bottom_border.append(bottom_border[0])
203
204    if not closed:
205        entities.extend(start_cap())
206
207    entities.extend(lines())
208
209    if style.get_flag_state(style.MITER):
210        entities.extend(display_miter())
211
212    if not closed:
213        entities.extend(end_cap())
214
215    if style.get_flag_state(style.FILL):
216        entities.insert(0, filling())
217
218    return entities
219