1#  Copyright (c) 2021, Manfred Moitzi
2#  License: MIT License
3
4import pytest
5import ezdxf
6from ezdxf.lldxf import const
7from ezdxf.math import (
8    required_fit_points, required_control_points,
9    required_knot_values, uniform_knot_vector,
10)
11from ezdxf.audit import Auditor, AuditError
12
13
14@pytest.fixture(scope='module')
15def doc():
16    return ezdxf.new()
17
18
19@pytest.fixture
20def auditor(doc):
21    return Auditor(doc)
22
23
24@pytest.fixture
25def spline(doc):
26    msp = doc.modelspace()
27    return msp.add_spline()
28
29
30@pytest.mark.parametrize('order, expected', [
31    (0, 2), (1, 2), (2, 2), (3, 3), (4, 4), (5, 5)
32])
33def test_required_fit_points_without_given_end_tangents(order, expected):
34    assert required_fit_points(order, tangents=False) == expected
35
36
37@pytest.mark.parametrize('order, expected', [
38    (0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 3)
39])
40def test_required_fit_points_with_given_end_tangents(order, expected):
41    assert required_fit_points(order, tangents=True) == expected
42
43
44@pytest.mark.parametrize('order, expected', [
45    (0, 2), (1, 2), (2, 2), (3, 3), (4, 4),
46])
47def test_required_control_points_calculation(order, expected):
48    assert required_control_points(order) == expected
49
50
51def test_any_points_present(auditor, spline):
52    spline.audit(auditor)
53    assert len(auditor.fixes) == 1
54    assert auditor.fixes[0].code == AuditError.INVALID_SPLINE_DEFINITION
55
56    # Test if invalid spline will be destroyed:
57    auditor.empty_trashcan()
58    assert spline.is_alive is False
59
60
61def test_degree_of_spline(auditor, spline):
62    # Attribute validator prevents setting an invalid degree:
63    with pytest.raises(const.DXFValueError):
64        spline.dxf.degree = 0
65
66    # But could be loaded from DXF file:
67    # Hack to test entity validator for degree < 1
68    spline.dxf.__dict__['degree'] = 0
69    spline.audit(auditor)
70    assert len(auditor.fixes) == 1
71    assert auditor.fixes[0].code == AuditError.INVALID_SPLINE_DEFINITION
72
73    # Test if invalid spline will be destroyed:
74    auditor.empty_trashcan()
75    assert spline.is_alive is False
76
77
78def add_n_fit_points(s, count: int):
79    for x in range(count):
80        s.fit_points.append([x, 0, 0])
81
82
83class TestFitPoints:
84
85    @pytest.mark.parametrize('degree', [1, 2, 3, 4])
86    def test_if_fit_point_count_is_valid(self, auditor, spline, degree):
87        order = degree + 1
88        spline.dxf.degree = degree
89        add_n_fit_points(spline, required_fit_points(order, tangents=True) - 1)
90
91        spline.audit(auditor)
92        assert len(auditor.fixes) == 1
93        assert auditor.fixes[
94                   0].code == AuditError.INVALID_SPLINE_FIT_POINT_COUNT
95
96        # Test if invalid spline will be destroyed:
97        auditor.empty_trashcan()
98        assert spline.is_alive is False
99
100    def test_remove_unused_knot_values(self, auditor, spline):
101        add_n_fit_points(spline, 4)
102        # Add arbitrary knot values -- have no meaning for splines defined
103        # by fit points:
104        spline.knots = [1, 2, 3, 4]
105        spline.audit(auditor)
106        assert len(auditor.fixes) == 1
107        assert auditor.fixes[0].code == \
108               AuditError.INVALID_SPLINE_KNOT_VALUE_COUNT
109        assert len(spline.knots) == 0
110
111        # Spline is usable, test if spline will not be destroyed:
112        auditor.empty_trashcan()
113        assert spline.is_alive is True
114
115    def test_remove_unused_weights(self, auditor, spline):
116        add_n_fit_points(spline, 4)
117        # Add arbitrary weights -- have no meaning for splines defined by fit
118        # points:
119        spline.weights = [1, 2, 2, 1]
120        spline.audit(auditor)
121        assert len(auditor.fixes) == 1
122        assert auditor.fixes[0].code == AuditError.INVALID_SPLINE_WEIGHT_COUNT
123        assert len(spline.weights) == 0
124
125        # Spline is usable, test if spline will not be destroyed:
126        auditor.empty_trashcan()
127        assert spline.is_alive is True
128
129
130def add_n_control_points(s, count: int):
131    for x in range(count):
132        s.control_points.append([x, 0, 0])
133
134
135class TestControlPoints:
136    @pytest.mark.parametrize('degree', [1, 2, 3, 4])
137    def test_auditing_control_point_count(self, auditor, spline, degree):
138        order = degree + 1
139        spline.dxf.degree = degree
140        add_n_control_points(spline, required_control_points(order) - 1)
141
142        spline.audit(auditor)
143        assert len(auditor.fixes) == 1
144        assert auditor.fixes[0].code == \
145               AuditError.INVALID_SPLINE_CONTROL_POINT_COUNT
146
147        # Test if invalid spline will be destroyed:
148        auditor.empty_trashcan()
149        assert spline.is_alive is False
150
151    @pytest.mark.parametrize('degree', [1, 2, 3, 4])
152    def test_auditing_knot_value_count(self, auditor, spline, degree):
153        order = degree + 1
154        spline.dxf.degree = degree
155        add_n_control_points(spline, required_control_points(order))
156        spline.knots = [1, 2, 3]
157
158        spline.audit(auditor)
159        assert len(auditor.fixes) == 1
160        assert auditor.fixes[0].code == \
161               AuditError.INVALID_SPLINE_KNOT_VALUE_COUNT
162
163        # Test if invalid spline will be destroyed:
164        auditor.empty_trashcan()
165        assert spline.is_alive is False
166
167    @pytest.mark.parametrize('degree', [1, 2, 3, 4])
168    def test_auditing_weight_count(self, auditor, spline, degree):
169        order = degree + 1
170        spline.dxf.degree = degree
171        count = required_control_points(order)
172        add_n_control_points(spline, count)
173        spline.knots = uniform_knot_vector(count, order)
174        spline.weights = range(count - 1)
175
176        spline.audit(auditor)
177        assert len(auditor.fixes) == 1
178        assert auditor.fixes[0].code == \
179               AuditError.INVALID_SPLINE_WEIGHT_COUNT
180
181        # Test if invalid spline will be destroyed:
182        auditor.empty_trashcan()
183        assert spline.is_alive is False
184
185
186if __name__ == '__main__':
187    pytest.main([__file__])
188