"""
Module that defines the basic geometric classes used for the package
They are used to encapsulate some commands
"""
from __future__ import annotations
from typing import Tuple, Union
from ..scalar.angle import Angle
from ..scalar.reals import Math, Real
from ..tools import Is, To
TOLERANCE = 1e-9
def cartesian(xcoord: Real, ycoord: Real) -> Point2D:
"""
Creates a Point with cartesian coordinates
"""
xcoord = To.real(xcoord)
ycoord = To.real(ycoord)
return Point2D(xcoord, ycoord, None, None)
def polar(radius: Real, angle: Angle) -> Point2D:
"""
Creates a Point with polar coordinates
"""
radius = To.real(radius)
angle = Angle.degrees(0) if radius == 0 else To.angle(angle)
return Point2D(None, None, radius, angle)
[docs]
class Point2D:
"""
Defines a Point in the plane,
It can be described in cartesian way: (x, y)
Or also in a polar way: (radius:angle)
"""
def __init__(
self,
xcoord: Real = None,
ycoord: Real = None,
radius: Real = None,
angle: Angle = None,
):
self.__xcoord = xcoord
self.__ycoord = ycoord
self.__radius = radius
self.__angle = angle
@property
def xcoord(self) -> Real:
"""The horizontal coordinate of the point"""
if self.__xcoord is None:
cos = self.__angle.cos()
self.__xcoord = self.__radius * cos if cos != 0 else To.finite(0)
return self.__xcoord
@property
def ycoord(self) -> Real:
"""The vertical coordinate of the point"""
if self.__ycoord is None:
sin = self.__angle.sin()
self.__ycoord = self.__radius * sin if sin != 0 else To.finite(0)
return self.__ycoord
@property
def radius(self) -> Real:
"""The norm L2 of the point: sqrt(x*x + y*y)"""
if self.__radius is None:
self.__radius = Math.sqrt(self.__xcoord**2 + self.__ycoord**2)
return self.__radius
@property
def angle(self) -> Angle:
"""The angle the point (x, y) forms with respect to the horizontal"""
if self.__angle is None:
self.__angle = Angle.arg(self.__xcoord, self.__ycoord)
return self.__angle
def __copy__(self) -> Point2D:
return +self
def __iter__(self):
yield self.__xcoord
yield self.__ycoord
def __getitem__(self, index: int) -> Real:
return self.__xcoord if index == 0 else self.__ycoord
def __str__(self) -> str:
return (
f"({self.xcoord}, {self.ycoord})"
if Is.finite(self.radius)
else f"({self.radius}:{self.angle})"
)
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other: Point2D) -> bool:
return (
abs(self[0] - other[0]) < TOLERANCE
and abs(self[1] - other[1]) < TOLERANCE
)
def __neg__(self) -> Point2D:
return self.__class__(
-self.xcoord,
-self.ycoord,
self.radius,
self.angle + Angle.degrees(180),
)
def __pos__(self) -> Point2D:
return self.__class__(
self.xcoord, self.ycoord, self.radius, self.angle
)
def __iadd__(self, other: Point2D) -> Point2D:
return self.move(other)
def __add__(self, other: Point2D) -> Point2D:
other = To.point(other)
return cartesian(self[0] + other[0], self[1] + other[1])
def __sub__(self, other: Point2D) -> Point2D:
other = To.point(other)
return cartesian(self[0] - other[0], self[1] - other[1])
def __mul__(self, other: float) -> Point2D:
if Is.point(other):
return inner(self, other)
if not Is.finite(other):
raise TypeError(f"Multiplication with non-real number: {other}")
return cartesian(self[0] * other, self[1] * other)
def __rmul__(self, other: float) -> Point2D:
return self.__mul__(other)
def __abs__(self) -> float:
"""Returns the norm of the point, the distance to the origin"""
return self.radius
[docs]
def move(self, vector: tuple[Real, Real]) -> Point2D:
"""
Moves the point by the given deltas
Parameters
----------
dx : float
The delta to move the x coordinate
dy : float
The delta to move the y coordinate
Returns
-------
Point2D
The moved point
"""
vector = To.point(vector)
self.__xcoord += vector[0]
self.__ycoord += vector[1]
return self
[docs]
def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> Point2D:
"""
Scales the point by the given factors
Parameters
----------
xscale : float
The factor to scale the x coordinate
yscale : float
The factor to scale the y coordinate
Returns
-------
Point2D
The scaled point
"""
xscale, yscale = (amount, amount) if Is.real(amount) else amount
self.__xcoord *= xscale
self.__ycoord *= yscale
return self
[docs]
def rotate(self, angle: Angle) -> Point2D:
"""
Rotates the point around the origin by the given angle
Parameters
----------
angle : float
The angle in radians to rotate the point
Returns
-------
Point2D
The rotated point
"""
angle = To.angle(angle)
cos_angle = angle.cos()
sin_angle = angle.sin()
x_new = self[0] * cos_angle - self[1] * sin_angle
y_new = self[0] * sin_angle + self[1] * cos_angle
self.__xcoord = x_new
self.__ycoord = y_new
return self
def inner(pointa: Point2D, pointb: Point2D) -> Real:
"""Compute the cross product between two points"""
return pointa.xcoord * pointb.xcoord + pointa.ycoord * pointb.ycoord
def cross(pointa: Point2D, pointb: Point2D) -> Real:
"""Compute the cross product between two points"""
return pointa.xcoord * pointb.ycoord - pointa.ycoord * pointb.xcoord
def to_point(point: Point2D | tuple[Real, Real]) -> Point2D:
"""
Converts a point to a Point2D object
Parameters
----------
point : Point2D or tuple of two reals
The point to be converted
Returns
-------
Point2D
The converted point
"""
if Is.instance(point, Point2D):
return point
xcoord = To.finite(point[0])
ycoord = To.finite(point[1])
return cartesian(xcoord, ycoord)
def is_point(point: Point2D | tuple[Real, Real]) -> bool:
"""
Checks if the given point is a Point2D object or a tuple of two reals
Parameters
----------
point : Point2D or tuple of two reals
The point to be checked
Returns
-------
bool
True if the point is a Point2D or a tuple of two reals, False otherwise
"""
return Is.instance(point, Point2D) or (
Is.instance(point, tuple)
and len(point) == 2
and all(Is.finite(coord) for coord in point)
)
To.point = to_point
Is.point = is_point