1# Copyright (c) 2018-2020 Manfred Moitzi
2# License: MIT License
3from typing import cast
4import pytest
5import math
6from unittest.mock import MagicMock
7
8import ezdxf
9
10from ezdxf.audit import Auditor
11from ezdxf.lldxf import const
12from ezdxf.lldxf.tagwriter import TagCollector
13from ezdxf.lldxf.tags import Tags
14from ezdxf.entities.mline import MLineVertex, MLine, MLineStyle
15from ezdxf.math import Matrix44, Vec3
16
17
18# noinspection PyUnresolvedReferences
19class TestMLine:
20    @pytest.fixture(scope='class')
21    def msp(self):
22        return ezdxf.new().modelspace()
23
24    @pytest.fixture
25    def mline_mock_update_geometry(self):
26        mline = MLine()
27        mline.update_geometry = MagicMock()
28        return mline
29
30    def test_unbounded_mline(self):
31        mline = MLine()
32        assert mline.dxf.style_handle is None
33        assert mline.dxf.style_name == 'Standard'
34        assert mline.style is None
35
36    def test_generic_mline(self, msp):
37        mline = msp.add_mline()
38        assert mline.dxftype() == 'MLINE'
39        assert mline.dxf.style_name == 'Standard'
40        assert mline.dxf.count == 0
41        assert mline.dxf.start_location == (0, 0, 0)
42
43    def test_set_justification(self, mline_mock_update_geometry):
44        mline = mline_mock_update_geometry
45        mline.set_justification(mline.BOTTOM)
46        assert mline.dxf.justification == mline.BOTTOM
47        mline.update_geometry.assert_called_once()
48
49    def test_set_scale_factor(self, mline_mock_update_geometry):
50        mline = mline_mock_update_geometry
51        mline.set_scale_factor(17)
52        assert mline.dxf.scale_factor == 17
53        mline.update_geometry.assert_called_once()
54
55    def test_close_state(self, mline_mock_update_geometry):
56        mline = mline_mock_update_geometry
57        assert mline.is_closed is False
58        mline.close(True)
59        assert mline.is_closed is True
60        mline.update_geometry.assert_called_once()
61
62    def test_point_count_management(self):
63        mline = MLine()
64        mline.load_vertices(Tags.from_text(VTX_2))
65        assert len(mline.vertices) == 2
66        assert len(mline) == 2
67        assert mline.dxf.count == 2, 'should be a callback to __len__()'
68
69    def test_add_first_vertex(self):
70        mline = MLine()
71        mline.extend([(0, 0, 0)])
72        assert mline.start_location() == (0, 0, 0)
73        assert len(mline) == 1
74
75    def test_add_two_vertices(self, msp):
76        # MLineStyle is required
77        mline = msp.add_mline([(0, 0), (10, 0)])
78        assert mline.start_location() == (0, 0, 0)
79        assert len(mline) == 2
80        assert mline.vertices[0].line_direction.isclose((1, 0))
81        assert mline.vertices[0].miter_direction.isclose((0, 1))
82        assert mline.vertices[1].line_direction.isclose((1, 0)), \
83            'continue last line segment'
84        assert mline.vertices[1].miter_direction.isclose((0, 1))
85
86    def test_x_rotation(self, msp):
87        mline = msp.add_mline([(0, 5), (10, 5)])
88        m = Matrix44.x_rotate(math.pi / 2)
89        mline.transform(m)
90        assert mline.start_location().isclose((0, 0, 5))
91        assert mline.dxf.extrusion.isclose((0, -1, 0))
92        assert mline.dxf.scale_factor == 1
93
94    def test_translate(self, msp):
95        mline = msp.add_mline([(0, 5), (10, 5)])
96        m = Matrix44.translate(1, 1, 1)
97        mline.transform(m)
98        assert mline.start_location().isclose((1, 6, 1))
99        assert mline.dxf.scale_factor == 1
100
101    def test_uniform_scale(self, msp):
102        mline = msp.add_mline([(0, 5), (10, 5)])
103        m = Matrix44.scale(2, 2, 2)
104        mline.transform(m)
105        assert mline.start_location().isclose((0, 10, 0))
106        assert mline.dxf.scale_factor == 2
107
108    def test_non_uniform_scale(self, msp):
109        mline = msp.add_mline([(1, 2, 3), (3, 4, 3)])
110        m = Matrix44.scale(2, 1, 3)
111        mline.transform(m)
112        assert mline.start_location().isclose((2, 2, 9))
113        assert mline.dxf.scale_factor == 1, 'ignore non-uniform scaling'
114
115
116class TestMLineStyle:
117    @pytest.fixture(scope='class')
118    def doc(self):
119        return ezdxf.new()
120
121    def test_standard_mline_style(self, doc):
122        mline_style = cast('MLineStyle', doc.mline_styles.get('Standard'))
123        assert mline_style.dxftype() == 'MLINESTYLE'
124
125        elements = mline_style.elements
126        assert len(elements) == 2
127        assert elements[0].offset == 0.5
128        assert elements[0].color == 256
129        assert elements[0].linetype == 'BYLAYER'
130        assert elements[1].offset == -0.5
131        assert elements[1].color == 256
132        assert elements[1].linetype == 'BYLAYER'
133
134    def test_set_defined_style(self, doc):
135        style = doc.mline_styles.new('DefinedStyle')
136        mline = doc.modelspace().add_mline()
137        mline.set_style('DefinedStyle')
138        assert mline.dxf.style_name == 'DefinedStyle'
139        assert mline.dxf.style_handle == style.dxf.handle
140
141    def test_set_undefined_style(self, doc):
142        mline = doc.modelspace().add_mline()
143        with pytest.raises(const.DXFValueError):
144            mline.set_style('UndefinedStyle')
145
146    def test_ordered_indices(self):
147        style = MLineStyle()
148        style.elements.append(5)  # top order
149        style.elements.append(-5)  # bottom border
150        style.elements.append(0)
151        style.elements.append(1)
152        assert style.ordered_indices() == [1, 2, 3, 0]
153
154    def test_invalid_element_count(self, doc):
155        style = doc.mline_styles.new('InvalidMLineStyle')
156        assert len(style.elements) == 0
157        auditor = Auditor(doc)
158        style.audit(auditor)
159        assert auditor.has_errors is True, 'invalid element count'
160
161
162class TestMLineVertex:
163    def test_load_tags(self):
164        tags = Tags.from_text(VTX_1)
165        vtx = MLineVertex.load(tags)
166        assert isinstance(vtx.location, Vec3)
167        assert vtx.location == (0, 0, 0)
168        assert vtx.line_direction == (1, 0, 0)
169        assert vtx.miter_direction == (0, 1, 0)
170        assert len(vtx.line_params) == 3
171        p1, p2, p3 = vtx.line_params
172        assert p1 == (0.5, 0.0)
173        assert p2 == (0.0, 0.0)
174        assert p3 == (-0.5, 0.0)
175        assert len(vtx.fill_params) == 3
176        assert sum(len(p) for p in vtx.fill_params) == 0
177
178    def test_new(self):
179        vtx = MLineVertex.new(
180            (1, 1), (1, 0), (0, 1),
181            [(0.5, 0), (0, 0)],
182            [tuple(), tuple()],
183        )
184        assert vtx.location == (1, 1, 0)
185        assert vtx.line_direction == (1, 0, 0)
186        assert vtx.miter_direction == (0, 1, 0)
187        assert len(vtx.line_params) == 2
188        p1, p2 = vtx.line_params
189        assert p1 == (0.5, 0)
190        assert p2 == (0, 0)
191        assert len(vtx.fill_params) == 2
192        assert sum(len(p) for p in vtx.fill_params) == 0
193
194    def test_export_dxf(self):
195        t = Tags.from_text(VTX_1)
196        vtx = MLineVertex.load(t)
197        collector = TagCollector()
198        vtx.export_dxf(collector)
199
200        tags = Tags(collector.tags)
201        assert tags[0] == (11, vtx.location[0])
202        assert tags[1] == (21, vtx.location[1])
203        assert tags[2] == (31, vtx.location[2])
204
205        assert tags[3] == (12, vtx.line_direction[0])
206        assert tags[4] == (22, vtx.line_direction[1])
207        assert tags[5] == (32, vtx.line_direction[2])
208
209        assert tags[6] == (13, vtx.miter_direction[0])
210        assert tags[7] == (23, vtx.miter_direction[1])
211        assert tags[8] == (33, vtx.miter_direction[2])
212
213        # line- and fill parameters
214        assert tags[9:] == t[3:]
215
216
217class TestMLineAudit:
218    @pytest.fixture(scope='class')
219    def doc(self):
220        d = ezdxf.new()
221        new_style = d.mline_styles.new('NewStyle1')
222        new_style.elements.append(0.5)
223        new_style.elements.append(0)
224        return d
225
226    @pytest.fixture(scope='class')
227    def msp(self, doc):
228        return doc.modelspace()
229
230    @pytest.fixture
231    def auditor(self, doc):
232        return Auditor(doc)
233
234    @pytest.fixture
235    def mline1(self, msp):
236        return msp.add_mline([(0, 0), (1, 1)])
237
238    def test_valid_mline(self, mline1, auditor):
239        mline1.audit(auditor)
240        assert auditor.has_errors is False
241        assert auditor.has_fixes is False
242
243    def test_fix_invalid_style_name(self, mline1, auditor):
244        mline1.dxf.style_name = 'test'
245        mline1.audit(auditor)
246        assert mline1.dxf.style_name == 'Standard'
247        assert auditor.has_fixes is False, 'silent fix'
248
249    def test_fix_invalid_style_handle(self, mline1, auditor):
250        mline1.dxf.style_name = 'test'
251        mline1.dxf.style_handle = '0'
252        mline1.audit(auditor)
253        assert mline1.dxf.style_name == 'Standard'
254        assert mline1.dxf.style_handle == auditor.doc.mline_styles[
255            'Standard'].dxf.handle
256        assert auditor.has_fixes is True
257
258    def test_fix_invalid_style_handle_by_name(self, mline1, doc, auditor):
259        new_style = doc.mline_styles.get('NewStyle1')
260        mline1.dxf.style_name = 'NewStyle1'
261        mline1.dxf.style_handle = '0'
262        mline1.audit(auditor)
263        assert mline1.dxf.style_name == new_style.dxf.name
264        assert mline1.dxf.style_handle == new_style.dxf.handle
265        assert auditor.has_fixes is True
266
267    def test_fix_invalid_line_direction(self, mline1, auditor):
268        mline1.vertices[0].line_direction = (0, 0, 0)
269        mline1.audit(auditor)
270        assert auditor.has_fixes is True
271
272    def test_fix_invalid_miter_direction(self, mline1, auditor):
273        mline1.vertices[0].miter_direction = (0, 0, 0)
274        mline1.audit(auditor)
275        assert auditor.has_fixes is True
276
277    def test_fix_invalid_line_parameters(self, mline1, auditor):
278        mline1.vertices[0].line_params = []
279        mline1.audit(auditor)
280        assert auditor.has_fixes is True
281
282
283VTX_1 = """11
2840.0
28521
2860.0
28731
2880.0
28912
2901.0
29122
2920.0
29332
2940.0
29513
2960.0
29723
2981.0
29933
3000.0
30174
3022
30341
3040.5
30541
3060.0
30775
3080
30974
3102
31141
3120.0
31341
3140.0
31575
3160
31774
3182
31941
320-0.5
32141
3220.0
32375
3240
325"""
326
327VTX_2 = """11
3280.0
32921
3300.0
33131
3320.0
33311
33410.0
33521
3360.0
33731
3380.0
339"""
340