Coverage for src \ pavpen \ geometry_calculator \ rounded_corner_calculator.py: 100%

79 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 

6import warnings 

7from enum import Enum 

8from typing import Annotated, Self 

9 

10from pavpen.geometry_calculator.circle_calculator import CircleCalculator 

11from pavpen.geometry_calculator.defaults import DEFAULT_TOLERANCE 

12from pavpen.geometry_calculator.faults import ( 

13 AmbiguousComputationDueToPrecisionWarning, 

14 ImpossibleOutputGeometryError, 

15) 

16from pavpen.geometry_calculator.float_vector_field_operations import FloatVectorFieldOperations 

17from pavpen.geometry_calculator.orthonormal_basis_calculator import OrthonormalBasisCalculator 

18 

19 

20class RoundedCornerCalculatorComputedValueNames(Enum): 

21 CENTER = "center" 

22 RADIUS = "radius" 

23 ARC_START = "arc_start" 

24 ARC_MIDPOINT = "arc_midpoint" 

25 ARC_END = "arc_end" 

26 

27 

28class RoundedCornerCalculator[Vector]: 

29 """Calculates the parameters of a rounded corner's circular arc 

30 

31 The non-rounded corner, for which a rounded corner arc is to be 

32 calculated is determined by *previous_vertex*, *corner_vertex*, and 

33 *next_vertex*. 

34 

35 If *x_hat*, and *y_hat* are specified, they define the coordinate 

36 system basis for outputs, such as the center of the rouded corner 

37 circle, and the start and end point of the rounded corner arc. 

38 

39 :param vector_field_operations: defines how to calculate vector 

40 operations for vectors, such as *previous_vertex*, *corner_vertex*, and 

41 *next_vertex* 

42 

43 :param radius: the radius of the rounded corner's circle 

44 """ 

45 

46 def __init__( 

47 self, 

48 vector_field_operations: FloatVectorFieldOperations[Vector], 

49 previous_vertex: Vector, 

50 corner_vertex: Vector, 

51 next_vertex: Vector, 

52 radius: float, 

53 x_hat: Vector | None = None, 

54 y_hat: Vector | None = None, 

55 float_tolerance: float = DEFAULT_TOLERANCE, 

56 ) -> None: 

57 self._vector_field_operations = vector_field_operations 

58 self._float_tolerance = float_tolerance 

59 self._previous_vertex = previous_vertex 

60 self._corner_vertex = corner_vertex 

61 self._next_vertex = next_vertex 

62 self._radius = radius 

63 self._x_hat = x_hat 

64 self._y_hat = y_hat 

65 

66 def _calculate_basis(self) -> None: 

67 """Sets ``self.x_hat``, and ``self.y_hat``""" 

68 

69 x_hat = self._x_hat 

70 y_hat = self._y_hat 

71 if x_hat is None: 

72 basis_calculator = OrthonormalBasisCalculator( 

73 vector_field_operations=self._vector_field_operations, 

74 float_tolerance=self._float_tolerance, 

75 points=[self._previous_vertex, self._corner_vertex, self._next_vertex], 

76 ).calculate() 

77 self.x_hat = basis_calculator.basis_vectors[0] 

78 else: 

79 self.x_hat = x_hat 

80 if y_hat is None: 

81 basis_calculator = OrthonormalBasisCalculator( 

82 vector_field_operations=self._vector_field_operations, 

83 float_tolerance=self._float_tolerance, 

84 points=[self._previous_vertex, self._corner_vertex, self._next_vertex], 

85 ).calculate() 

86 self.y_hat = basis_calculator.basis_vectors[1] 

87 else: 

88 self.y_hat = y_hat 

89 

90 def calculate(self) -> Self: 

91 """Calculates, and sets the folowing output parameters of the rounded 

92 corner arc as attributes of ``self``: 

93 

94 * ``center`` 

95 * ``radius`` 

96 * ``arc_start`` 

97 * ``arc_midpoint`` 

98 * ``arc_end`` 

99 """ 

100 

101 self._calculate_basis() 

102 vec = self._vector_field_operations 

103 float_tolerance = self._float_tolerance 

104 previous_vertex = self._previous_vertex 

105 corner_vertex = self._corner_vertex 

106 next_vertex = self._next_vertex 

107 radius = self._radius 

108 corner_to_previous_vertex_vector = vec.subtracted(previous_vertex, corner_vertex) 

109 corner_to_next_vertex_vector = vec.subtracted(next_vertex, corner_vertex) 

110 corner_to_previous_vertex_angle_rad = math.atan2( 

111 vec.projection_length_along(direction=self.y_hat, projected=corner_to_previous_vertex_vector), 

112 vec.projection_length_along(direction=self.x_hat, projected=corner_to_previous_vertex_vector), 

113 ) 

114 corner_to_next_vertex_angle_rad = math.atan2( 

115 vec.projection_length_along(direction=self.y_hat, projected=corner_to_next_vertex_vector), 

116 vec.projection_length_along(direction=self.x_hat, projected=corner_to_next_vertex_vector), 

117 ) 

118 

119 edge_to_edge_angle_rad = math.fabs(corner_to_previous_vertex_angle_rad - corner_to_next_vertex_angle_rad) 

120 corner_to_center_angle_rad = (corner_to_previous_vertex_angle_rad + corner_to_next_vertex_angle_rad) / 2.0 

121 difference_from_alternate_solution = math.fabs(edge_to_edge_angle_rad - math.pi) 

122 if difference_from_alternate_solution < float_tolerance: 

123 warnings.warn( 

124 AmbiguousComputationDueToPrecisionWarning( 

125 ambiguous_value_names=[ 

126 f"{self.__class__.__name__}.{RoundedCornerCalculatorComputedValueNames.CENTER.value}" 

127 ], 

128 tolerance=float_tolerance, 

129 difference_from_alternate_solution=difference_from_alternate_solution, 

130 ), 

131 stacklevel=1, 

132 ) 

133 if edge_to_edge_angle_rad > math.pi: 

134 corner_to_center_angle_rad += math.pi 

135 edge_to_edge_angle_rad = 2.0 * math.pi - edge_to_edge_angle_rad 

136 center_to_edge_angle_rad = edge_to_edge_angle_rad / 2.0 

137 corner_to_center_dir = vec.added( 

138 vec.scaled(self.x_hat, math.cos(corner_to_center_angle_rad)), 

139 vec.scaled(self.y_hat, math.sin(corner_to_center_angle_rad)), 

140 ) 

141 corner_to_center_distance = radius / math.sin(center_to_edge_angle_rad) 

142 center = vec.added(self._corner_vertex, vec.scaled(corner_to_center_dir, corner_to_center_distance)) 

143 circle_calculator = CircleCalculator( 

144 vector_field_operations=vec, 

145 center=center, 

146 radius=radius, 

147 x_hat=self.x_hat, 

148 y_hat=self.y_hat, 

149 ) 

150 

151 edge_to_corner_shortening: Annotated[ 

152 float, 

153 """Distance cut off by rounding the corner along each edge that 

154 would end at the corner vertex, if there was no rounding""", 

155 ] = corner_to_center_distance * math.cos(center_to_edge_angle_rad) 

156 

157 corner_to_previous_vertex_vector_norm = vec.norm(corner_to_previous_vertex_vector) 

158 if corner_to_previous_vertex_vector_norm < edge_to_corner_shortening: 

159 message = ( 

160 f"Rounded corner previous vertex is within the corner part " 

161 f"cut off by rounding the corner. Values: " 

162 f"previous_vertex: {previous_vertex}, " 

163 f"corner_vertex: {corner_vertex}, " 

164 f"next_vertex: {next_vertex}, radius: {radius}; " 

165 f"corner_to_previous_vertex_vector norm: {corner_to_previous_vertex_vector_norm} < " 

166 f"edge_to_corner_shortening: {edge_to_corner_shortening}" 

167 ) 

168 raise ImpossibleOutputGeometryError(message) 

169 arc_start = vec.added( 

170 corner_vertex, 

171 vec.scaled( 

172 vec.normalized(corner_to_previous_vertex_vector), 

173 edge_to_corner_shortening, 

174 ), 

175 ) 

176 

177 corner_to_next_vertex_vector_norm = vec.norm(corner_to_next_vertex_vector) 

178 if corner_to_next_vertex_vector_norm < edge_to_corner_shortening: 

179 message = ( 

180 f"Rounded corner next vertex is within the corner part " 

181 f"cut off by rounding the corner. Values: " 

182 f"previous_vertex: {previous_vertex}, " 

183 f"corner_vertex: {corner_vertex}, " 

184 f"next_vertex: {next_vertex}, radius: {radius}; " 

185 f"corner_to_next_vertex_vector norm: {corner_to_next_vertex_vector_norm} < " 

186 f"edge_to_corner_shortening: {edge_to_corner_shortening}" 

187 ) 

188 raise ImpossibleOutputGeometryError(message) 

189 arc_end = vec.added( 

190 corner_vertex, 

191 vec.scaled( 

192 vec.normalized(corner_to_next_vertex_vector), 

193 edge_to_corner_shortening, 

194 ), 

195 ) 

196 arc_midpoint = circle_calculator.point_at_angle_rad(corner_to_center_angle_rad + math.pi) 

197 

198 self.center = center 

199 self.radius = radius 

200 self.arc_start = arc_start 

201 self.arc_midpoint = arc_midpoint 

202 self.arc_end = arc_end 

203 

204 return self