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

1# SPDX-FileCopyrightText: 2026-present Pavel M. Penev <pavpen@gmail.com> 

2# 

3# SPDX-License-Identifier: MIT 

4 

5import math 

6from dataclasses import dataclass 

7from typing import cast 

8 

9import pytest 

10 

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) 

19 

20 

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 

33 

34 

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 ) 

100 

101 # Act 

102 result = calculator.calculate() 

103 

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) 

112 

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 ) 

126 

127 # Act 

128 with pytest.warns(AmbiguousComputationDueToPrecisionWarning) as warnings: 

129 result = calculator.calculate() 

130 

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)) 

141 

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 

148 

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 """ 

161 

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 ) 

174 

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() 

184 

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 """ 

198 

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 ) 

211 

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() 

221 

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 ) 

233 

234 # Act 

235 result = calculator.calculate() 

236 

237 # Verify 

238 assert result.x_hat == pytest.approx((1, 0)) 

239 assert result.y_hat == pytest.approx((0, 1)) 

240 

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))