import math from .Direction import * from .Vector import * class Polar: """! A polar 2D vector """ def __init__(self, distance: float, angle: Angle): """! Create a new polar vector @param distance The length of the vector @param angle The direction of the angle """ self.distance: float = distance ## The length of the vector self.direction: Angle = angle ## The direction of the vector ## Normalizing such that distance >= 0 if self.distance < 0: self.distance = -self.distance self.direction = self.direction.Inverse() elif self.distance == 0: self.direction = Angle.zero def Degrees(distance: float, degrees: float): """! Create a polar vector without using the Angle type. All given angles are in degrees @param distance The distance in meters @param horizontal The angle in degrees @return The polar vector """ angle: Angle = Angle.Degrees(degrees) r: Polar = Polar(distance, angle) return r def Radians(distance: float, radians: float): """! Create polar vector without using the Angle type. All given angles are in radians @param distance The distance in meters @param horizontal The horizontal angle in radians @param vertical The vertical angle in radians @return The polar vector """ angle: Angle = Angle.Radians(radians) r: Polar = Polar(distance, angle) return r @staticmethod def FromVector2(v: Vector2): """! Create a polar coordinate from a Vector2 coordinate @param v The vector coordinate @return The polar coordinate """ distance = v.Magnitude() if distance == 0: return Polar(0, Angle.zero) angle = Angle.Radians(math.atan2(v.right, v.up)) return Polar(distance, angle) def ToVector2(self) -> Vector2: """! Convert the polar coordinate to a Vector2 coordinate @return The vector coordinate """ horizontalRad = self.direction.InRadians() cosHorizontal = math.cos(horizontalRad) sinHorizontal = math.sin(horizontalRad) right = self.distance * sinHorizontal up = self.distance * cosHorizontal return Vector2(right, up) def __eq__(self, other) -> bool: """! Check if this vector is equal to the given vector @param v The vector to check against @return true if it is identical to the given vector @note This uses float comparison to check equality which may have strange effects. Equality on floats should be avoided. """ return ( self.distance == other.distance and self.direction == other.direction ) def isclose(self, other, rel_tol=1e-9, abs_tol=1e-8): return ( math.isclose(self.distance, other.distance, rel_tol=rel_tol, abs_tol=abs_tol) and self.direction.isclose(other.direction, rel_tol, abs_tol) ) def Magnitude(self) -> float: return math.fabs(self.distance) def Normalized(self): if self.distance == 0: return Polar(0, self.direction) return Polar(1, self.direction) def __neg__(self): """! Negate the vector @return The negated vector This will negate the direction. Distance will stay the same. """ return Polar(self.distance, self.direction.Inverse()) def __sub__(self, other): """! Subtract a polar vector from this vector @param other The vector to subtract @return The result of the subtraction """ # v1 = self.ToVector2() # v2 = other.ToVector2() # r = v1 - v2 # return Polar.FromVector2(r) r = self + (-other) return r def __add__(self, other): """! Add a polar vector to this vector @param other The vector to add @return The result of the addition """ # v1 = self.ToVector2() # v2 = other.ToVector2() # r = v1 - v2 # return Polar.FromVector2(r) if other.distance == 0: return Polar(self.distance, self.direction) if self.distance == 0: return other deltaAngle: float = (other.direction - self.direction).InDegrees(); if deltaAngle < 0: rotation = 180 + deltaAngle else: rotation = 180 - deltaAngle if rotation == 180 and other.distance > 0: # angle is too small, take this angle and add the distances return Polar(self.distance + other.distance, self.direction) newDistance: float = Angle.CosineRuleSide(other.distance, self.distance, Angle.Degrees(rotation)) angle: float = Angle.CosineRuleAngle(newDistance, self.distance, other.distance).InDegrees() if deltaAngle < 0: new_angle: float = self.direction.InDegrees() - angle else: new_angle: float = self.direction.InDegrees() + angle new_angle_a: Angle = Angle.Degrees(new_angle) vector = Polar(newDistance, new_angle_a) return vector def __mul__(self, factor): """! Scale the vector uniformly up @param factor The scaling factor @return The scaled vector @remark This operation will scale the distance of the vector. The angle will be unaffected. """ return Polar( self.distance * factor, self.direction ) def __truediv__(self, factor): """! Scale the vector uniformly down @param factor The scaling factor @return The scaled vector @remark This operation will scale the distance of the vector. The angle will be unaffected. """ return Polar( self.distance / factor, self.direction ) @staticmethod def Distance(v1, v2) -> float: """! Calculate the distance between two spherical coordinates @param v1 The first coordinate @param v2 The second coordinate @return The distance between the coordinates in meters """ v1 = v1.ToVector2() v2 = v2.ToVector2() distance: float = Vector2.Distance(v1, v2) return distance @staticmethod def Angle(v1, v2) -> Angle: """! Calculate the unsigned angle between two spherical vectors @param v1 The first vector @param v2 The second vector @return The unsigned angle between the vectors [0..179.9999999..., -180] @remark the strange range is caused by the 2s complement signed values which has range [minvalue..maxvalue). This is a hardware limitation, not something we can change. """ angle: Angle = Angle.Abs(v1.direction - v2.direction) return angle @staticmethod def SignedAngle(v1, v2) -> Angle: """! Calculate the unsigned angle between two spherical vectors @param v1 The first vector @param v2 The second vector @return The unsigned angle between the vectors [0..179.9999999..., -180] @remark the strange range is caused by the 2s complement signed values which has range [minvalue..maxvalue). This is a hardware limitation, not something we can change. """ angle: Angle = v2.direction - v1.direction return angle def Lerp(v1, v2, f: float): """! Lerp (linear interpolation) between two vectors @param v1 The starting vector @param v2 The ending vector @param f The interpolation distance @return The lerped vector @remark The factor f is unclamped. Value 0 matches the vector *v1*, Value 1 matches vector *v2*. Value -1 is vector *v1* minus the difference between *v1* and *v2* etc. """ return v1 + (v2 - v1) * f # return v1 * (1 - f) + v2 * f Polar.zero = Polar(0, Angle.zero) class Spherical(Polar): """! A spherical 3D vector """ def __init__(self, distance: float, direction: Direction): """! Create a new spherical vector @param distance The length of the vector @param direction The direction of the vector """ self.distance: float = distance ## The length of the vector self.direction: Direction = direction ## The direction of the vector ## Normalizing such that distance >= 0 if self.distance < 0: self.distance = -self.distance self.direction = -self.direction elif self.distance == 0: self.direction = Direction.zero def Degrees(distance: float, horizontal: float, vertical: float): """! Create sperical vector without using the Direction type. All given angles are in degrees @param distance The distance in meters @param horizontal The horizontal angle in degrees @param vertical The vertical angle in degrees @return The spherical vector """ direction: Direction = Direction.Degrees(horizontal, vertical) r: Spherical = Spherical(distance, direction) return r def Radians(distance: float, horizontal: float, vertical: float): """! Create sperical vector without using the Direction type. All given angles are in radians @param distance The distance in meters @param horizontal The horizontal angle in radians @param vertical The vertical angle in radians @return The spherical vector """ direction: Direction = Direction.Radians(horizontal, vertical) r: Spherical = Spherical(distance, direction) return r @staticmethod def FromVector3(v: Vector3): """! Create a Spherical coordinate from a Vector3 coordinate @param v The vector coordinate @return The spherical coordinate """ distance = v.Magnitude() if distance == 0: return Spherical(0, Angle(), Angle()) verticalAngle = Angle.Radians((math.pi / 2 - math.acos(v.up / distance))) horizontalAngle = Angle.Radians(math.atan2(v.right, v.forward)) return Spherical(distance, Direction(horizontalAngle, verticalAngle)) def ToVector3(self) -> Vector3: """! Convert the spherical coordinate to a Vector3 coordinate @return The vector coordinate """ verticalRad = (math.pi / 2) - self.direction.vertical.InRadians() horizontalRad = self.direction.horizontal.InRadians() cosVertical = math.cos(verticalRad) sinVertical = math.sin(verticalRad) cosHorizontal = math.cos(horizontalRad) sinHorizontal = math.sin(horizontalRad) right = self.distance * sinVertical * sinHorizontal up = self.distance * cosVertical forward = self.distance * sinVertical * cosHorizontal return Vector3(right, up, forward) def __eq__(self, other) -> bool: """! Check if this vector is equal to the given vector @param v The vector to check against @return true if it is identical to the given vector @note This uses float comparison to check equality which may have strange effects. Equality on floats should be avoided. """ return ( self.distance == other.distance and self.direction == other.direction ) def isclose(self, other, rel_tol=1e-9, abs_tol=1e-8): return ( math.isclose(self.distance, other.distance, rel_tol=rel_tol, abs_tol=abs_tol) and self.direction.isclose(other.direction, rel_tol, abs_tol) ) def Normalized(self) -> float: if self.distance == 0: return Spherical(0, self.direction) return Spherical(1, self.direction) def __neg__(self): """! Negate the vector @return The negated vector This will negate the direction. Distance will stay the same. """ return Spherical(self.distance, self.direction.Inverse()) def __sub__(self, other): """! Subtract a spherical vector from this vector @param other The vector to subtract @return The result of the subtraction """ v1 = self.ToVector3() v2 = other.ToVector3() r = v1 - v2 return Spherical.FromVector3(r) def __add__(self, other): """! Add a spherical vector to this vector @param other The vector to add @return The result of the addition """ v1 = self.ToVector3() v2 = other.ToVector3() r = v1 + v2 return Spherical.FromVector3(r) def __mul__(self, factor): """! Scale the vector uniformly up @param factor The scaling factor @return The scaled vector @remark This operation will scale the distance of the vector. The angle will be unaffected. """ return Spherical( self.distance * factor, self.direction ) def __truediv__(self, factor): """! Scale the vector uniformly down @param factor The scaling factor @return The scaled vector @remark This operation will scale the distance of the vector. The angle will be unaffected. """ return Spherical( self.distance / factor, self.direction ) @staticmethod def Distance(v1, v2) -> float: """! Calculate the distance between two spherical coordinates @param s1 The first coordinate @param s2 The second coordinate @return The distance between the coordinates in meters """ v1: Vector3 = v1.ToVector3() v2: Vector3 = v2.ToVector3() distance: float = Vector3.Distance(v1, v2) return distance @staticmethod def Angle(s1, s2) -> Angle: """! Calculate the unsigned angle between two spherical vectors @param s1 The first vector @param s2 The second vector @return The unsigned angle between the vectors [0..179.9999999..., -180] @remark the strange range is caused by the 2s complement signed values which has range [minvalue..maxvalue). This is a hardware limitation, not something we can change. """ v1: Vector3 = s1.ToVector3() v2: Vector3 = s2.ToVector3() angle: Angle = Vector3.Angle(v1, v2) return angle def SignedAngle(s1, s2, axis) -> Angle: """! Calculate the signed angle between two spherical vectors @param s1 The first vector @param s2 The second vector @param axis The axis around which the angle is calculated @return The signed angle between the vectors """ v1 = s1.ToVector3() v2 = s2.ToVector3() v_axis = axis.ToVector3() angle = Vector3.SignedAngle(v1, v2, v_axis) return angle @staticmethod def Rotate(s, horizontal: Angle, vertical: Angle): """! Rotate a spherical vector @param s The vector to rotate @param horizontal The horizontal rotation angle in local space @param vertical The vertical rotation angle in local space @return The rotated vector """ direction: Direction = Direction( s.direction.horizontal + horizontal, s.direction.vertical + vertical ) r = Spherical(s.distance, direction) return r def __repr__(self): return f"Spherical(r={self.distance}, horizontal={self.direction.horizontal}, phi={self.direction.vertical})" Spherical.zero = Spherical(0, Direction.zero)