Coverage for src \ pavpen \ geometry_calculator \ circle_calculator.py: 100%
16 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 logging
6import math
8from pavpen.geometry_calculator.float_vector_field_operations import FloatVectorFieldOperations
10logger = logging.getLogger(__name__)
13class CircleCalculator[Vector]:
14 """Calculates the location of points on a circle (or an ellipse, if the
15 norms of :emphasis:`x_hat`, and :emphasis:`y_hat` are different)
17 Example:
19 >>> import math
20 >>> from pavpen.geometry_calculator import CircleCalculator
21 >>> from pavpen.geometry_calculator.vector_field_implementations import (
22 ... TupleFloatVectorFieldOperations,
23 ... )
24 >>>
25 >>> (x, y) = CircleCalculator(
26 ... vector_field_operations=TupleFloatVectorFieldOperations.for_2d(),
27 ... center=(0, 0),
28 ... radius=1,
29 ... x_hat=(1, 0),
30 ... y_hat=(0, 1),
31 ... ).point_at_angle_rad(math.pi / 4)
32 >>> print(f"({x:.4f}, {y:.4f})")
33 (0.7071, 0.7071)
34 """
36 def __init__(
37 self,
38 vector_field_operations: FloatVectorFieldOperations[Vector],
39 center: Vector,
40 radius: float,
41 x_hat: Vector | None = None,
42 y_hat: Vector | None = None,
43 ):
44 self.vector_field_operations = vector_field_operations
45 self.center = center
46 self.radius = radius
47 self._x_hat = vector_field_operations.additive_identity if x_hat is None else x_hat
48 self._y_hat = vector_field_operations.additive_identity if y_hat is None else y_hat
50 def point_at_angle_rad(self, angle_rad: float) -> Vector:
51 """Location of the point at angle *angle_rad* in the right
52 direction from ``x_hat`` the on this circle
54 The value of the ``x_hat`` argument is defined when calling the
55 :py:meth:`constructor <CircleCalculator>`.
56 """
58 vec = self.vector_field_operations
59 x_offset = vec.scaled(self._x_hat, self.radius * math.cos(angle_rad))
60 y_offset = vec.scaled(self._y_hat, self.radius * math.sin(angle_rad))
62 return vec.summed(self.center, x_offset, y_offset)