Coverage for tests \ test_rounded_corner_calculator.py: 100%
64 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-27 03:59 -0400
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-27 03:59 -0400
1# SPDX-FileCopyrightText: 2026-present Pavel M. Penev <pavpen@gmail.com>
2#
3# SPDX-License-Identifier: MIT
5import math
6from dataclasses import dataclass
7from typing import cast
9import pytest
11from pavpen.geometry_calculator.faults import (
12 AmbiguousComputationDueToPrecisionWarning,
13 ImpossibleOutputGeometryError,
14)
15from pavpen.geometry_calculator.rounded_corner_calculator import RoundedCornerCalculator
16from pavpen.geometry_calculator.vector_field_implementations.tuple_float_vector_field_operations import (
17 TupleFloatVectorFieldOperations,
18)
21@dataclass
22class CalculateTestCase:
23 id: str
24 vertices: tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
25 radius: float
26 expected_center: tuple[float, float]
27 expected_arc_start: tuple[float, float]
28 expected_arc_midpoint: tuple[float, float]
29 expected_arc_end: tuple[float, float]
30 x_hat: tuple[float, float] = (1, 0)
31 y_hat: tuple[float, float] = (0, 1)
32 float_tolerance: float = 1e-8
35class TestRoundedCornerCalculator:
36 @pytest.mark.parametrize(
37 "test_case",
38 [
39 CalculateTestCase(
40 id="┘-corner, quadrant 2 arc",
41 vertices=((0, 0), (4, 0), (4, 4)),
42 radius=1,
43 expected_center=(3, 1),
44 expected_arc_start=(3, 0),
45 expected_arc_end=(4, 1),
46 expected_arc_midpoint=(3 + 1 / math.sqrt(2), 1 - 1 / math.sqrt(2)),
47 ),
48 CalculateTestCase(
49 id="┐-corner, quadrant 3 arc",
50 vertices=((4, 0), (4, 4), (0, 4)),
51 radius=1,
52 expected_center=(3, 3),
53 expected_arc_start=(4, 3),
54 expected_arc_end=(3, 4),
55 expected_arc_midpoint=(3 + 1 / math.sqrt(2), 3 + 1 / math.sqrt(2)),
56 ),
57 CalculateTestCase(
58 id="└-corner, quadrant 1 arc",
59 vertices=((0, 4), (0, 0), (4, 0)),
60 radius=1,
61 expected_center=(1, 1),
62 expected_arc_start=(0, 1),
63 expected_arc_end=(1, 0),
64 expected_arc_midpoint=(1 - 1 / math.sqrt(2), 1 - 1 / math.sqrt(2)),
65 ),
66 CalculateTestCase(
67 id="┌-corner, quadrant 4 arc",
68 vertices=((0, 0), (0, 4), (4, 4)),
69 radius=1,
70 expected_center=(1, 3),
71 expected_arc_start=(0, 3),
72 expected_arc_end=(1, 4),
73 expected_arc_midpoint=(1 - 1 / math.sqrt(2), 3 + 1 / math.sqrt(2)),
74 ),
75 CalculateTestCase(
76 id="Clockwise ┘-corner arc",
77 vertices=((4, 4), (4, 0), (0, 0)),
78 radius=1,
79 expected_center=(3, 1),
80 expected_arc_start=(4, 1),
81 expected_arc_end=(3, 0),
82 expected_arc_midpoint=(3 + 1 / math.sqrt(2), 1 - 1 / math.sqrt(2)),
83 ),
84 ],
85 ids=lambda test_case: test_case.id,
86 )
87 def test_calculate_result_matches_a_test_case(self, test_case: CalculateTestCase) -> None:
88 # Setup
89 vector_field_operations = TupleFloatVectorFieldOperations.for_2d()
90 calculator = RoundedCornerCalculator(
91 vector_field_operations=vector_field_operations,
92 float_tolerance=test_case.float_tolerance,
93 previous_vertex=test_case.vertices[0],
94 corner_vertex=test_case.vertices[1],
95 next_vertex=test_case.vertices[2],
96 radius=test_case.radius,
97 x_hat=test_case.x_hat,
98 y_hat=test_case.y_hat,
99 )
101 # Act
102 result = calculator.calculate()
104 # Verify
105 assert result.x_hat == pytest.approx(test_case.x_hat)
106 assert result.y_hat == pytest.approx(test_case.y_hat)
107 assert result.center == pytest.approx(test_case.expected_center)
108 assert result.radius == pytest.approx(test_case.radius)
109 assert result.arc_start == pytest.approx(test_case.expected_arc_start)
110 assert result.arc_midpoint == pytest.approx(test_case.expected_arc_midpoint)
111 assert result.arc_end == pytest.approx(test_case.expected_arc_end)
113 def test_calculate_reports_a_warning_on_ambiguous_center_location(self) -> None:
114 # Setup
115 vector_field_operations = TupleFloatVectorFieldOperations.for_2d()
116 calculator = RoundedCornerCalculator(
117 vector_field_operations=vector_field_operations,
118 float_tolerance=1e-8,
119 previous_vertex=(0, 0),
120 corner_vertex=(0, 4),
121 next_vertex=(0, 8),
122 radius=1,
123 x_hat=(1, 0),
124 y_hat=(0, 1),
125 )
127 # Act
128 with pytest.warns(AmbiguousComputationDueToPrecisionWarning) as warnings:
129 result = calculator.calculate()
131 # Verify
132 assert result.x_hat == pytest.approx((1, 0))
133 assert result.y_hat == pytest.approx((0, 1))
134 # This is an unstable solution. The center could easily be at (-1, 4)
135 # as well:
136 assert result.center == pytest.approx((1, 4))
137 assert result.radius == pytest.approx(1)
138 assert result.arc_start == pytest.approx((0, 4))
139 assert result.arc_midpoint == pytest.approx((0, 4))
140 assert result.arc_end == pytest.approx((0, 4))
142 assert len(warnings) == 1
143 warning = cast("AmbiguousComputationDueToPrecisionWarning", warnings[0].message)
144 assert warning.ambiguous_value_names == ["RoundedCornerCalculator.center"]
145 assert warning.tolerance == pytest.approx(1e-8)
146 assert warning.difference_from_alternate_solution == pytest.approx(0)
147 assert warning.causing_exception is None
149 def test_calculate_throws_on_previous_vertex_inside_arc_circle(self) -> None:
150 """
151 ```
152 ^
153 |
154 | * (4, 4) previous_vertex
155 |
156 *---+---*-->
157 (-4, 0) (4, 0)
158 next_vertex corner_vertex
159 ```
160 """
162 # Setup
163 vector_field_operations = TupleFloatVectorFieldOperations.for_2d()
164 calculator = RoundedCornerCalculator(
165 vector_field_operations=vector_field_operations,
166 float_tolerance=1e-8,
167 previous_vertex=(4, 4),
168 corner_vertex=(4, 0),
169 next_vertex=(-4, 0),
170 radius=5,
171 x_hat=(1, 0),
172 y_hat=(0, 1),
173 )
175 # Act and verify
176 with pytest.raises(
177 ImpossibleOutputGeometryError,
178 match=(
179 r"Rounded corner previous vertex is within the corner part "
180 "cut off by rounding the corner."
181 ),
182 ):
183 calculator.calculate()
185 def test_calculate_throws_on_next_vertex_inside_arc_circle(self) -> None:
186 """
187 ```
188 ^
189 |
190 | * (4, 8) previous_vertex
191 |
192 |
193 *---*-->
194 (0, 0) (4, 0)
195 next_vertex corner_vertex
196 ```
197 """
199 # Setup
200 vector_field_operations = TupleFloatVectorFieldOperations.for_2d()
201 calculator = RoundedCornerCalculator(
202 vector_field_operations=vector_field_operations,
203 float_tolerance=1e-8,
204 previous_vertex=(4, 8),
205 corner_vertex=(4, 0),
206 next_vertex=(0, 0),
207 radius=5,
208 x_hat=(1, 0),
209 y_hat=(0, 1),
210 )
212 # Act and verify
213 with pytest.raises(
214 ImpossibleOutputGeometryError,
215 match=(
216 r"Rounded corner next vertex is within the corner part "
217 "cut off by rounding the corner."
218 ),
219 ):
220 calculator.calculate()
222 def test_calculate_chooses_a_plane_basis_if_unspecified(self) -> None:
223 # Setup
224 vector_field_operations = TupleFloatVectorFieldOperations.for_2d()
225 calculator = RoundedCornerCalculator(
226 vector_field_operations=vector_field_operations,
227 float_tolerance=1e-8,
228 previous_vertex=(0, 0),
229 corner_vertex=(4, 0),
230 next_vertex=(4, 4),
231 radius=1,
232 )
234 # Act
235 result = calculator.calculate()
237 # Verify
238 assert result.x_hat == pytest.approx((1, 0))
239 assert result.y_hat == pytest.approx((0, 1))
241 assert result.center == pytest.approx((3, 1))
242 assert result.radius == pytest.approx(1)
243 assert result.arc_start == pytest.approx((3, 0))
244 assert result.arc_midpoint == pytest.approx((3 + math.sqrt(2) / 2, 1 - math.sqrt(2) / 2))
245 assert result.arc_end == pytest.approx((4, 1))