import math
from typing import Union, Tuple, Any
import numpy as np
[docs]
class Vector:
"""
Represents a 2D vector for use in movement, geometry, and physics.
Supports arithmetic with both other Vectors and 2D tuples.
Examples:
>>> v = Vector(1, 2)
>>> v2 = Vector(3, 4)
>>> v + v2
Vector(4.0, 6.0)
>>> v + (1, 1)
Vector(2.0, 3.0)
>>> (1, 1) + v
Vector(2.0, 3.0)
"""
def __init__(self, x: float, y: float) -> None:
self.vec = np.array([x, y], dtype=float)
@staticmethod
def _to_vector(value: Union["Vector", Tuple[float, float]]) -> "Vector":
if isinstance(value, Vector):
return value
if isinstance(value, tuple) and len(value) == 2:
return Vector(*value)
raise TypeError(f"Expected Vector or 2-tuple, got {type(value)}.")
def __getitem__(self, index: int) -> float:
return self.vec[index]
@property
def x(self) -> float:
return self.vec[0]
@x.setter
def x(self, value: float) -> None:
self.vec[0] = value
@property
def y(self) -> float:
return self.vec[1]
@y.setter
def y(self, value: float) -> None:
self.vec[1] = value
@property
def angle(self) -> float:
return self.to_direction()
[docs]
def to_position(self) -> Tuple[float, float]:
return (self.x, self.y)
[docs]
@classmethod
def from_position(cls, position: Tuple[float, float]) -> "Vector":
if not (isinstance(position, tuple) and len(position) == 2):
raise TypeError("Position must be a tuple of two float values.")
return cls(*position)
[docs]
@classmethod
def from_positions(cls, p1: Tuple[float, float], p2: Tuple[float, float]) -> "Vector":
return cls(p2[0] - p1[0], p2[1] - p1[1])
[docs]
@classmethod
def from_direction(cls, direction: Union[str, int, float, str]) -> "Vector":
"""Creates a vector from miniworlds direction."""
if direction >= 90:
x = 0 + math.sin(math.radians(direction)) * 1
y = 0 - math.cos(math.radians(direction)) * 1
else:
x = 0 + math.sin(math.radians(direction)) * 1
y = 0 - math.cos(math.radians(direction)) * 1
return cls(x, y)
[docs]
@classmethod
def from_actors(cls, t1: "actor_mod.Actor", t2: "actor_mod.Actor") -> "Vector":
"""Create a vector from two actors.
The vector describes is generated from:
actor2.center - actor1.center
"""
x = t2.center[0] - t1.center[0]
y = t2.center[1] - t1.center[1]
return cls(x, y)
[docs]
@classmethod
def from_actor_and_position(cls, t1: "actor_mod.Actor", pos) -> "Vector":
"""Create a vector from actor and position
The vector describes is generated from:
actor2.center - position
"""
x = pos[0] - t1.center[0]
y = pos[1] - t1.center[1]
return cls(x, y)
[docs]
@classmethod
def from_actor_direction(cls, actor: "actor_mod.Actor") -> "Vector":
"""Creates a vector from actor direction
Examples:
Creates rotating rectangle
.. code-block:: python
from miniworlds import *
world = World()
player = Rectangle((200,200),40, 40)
player.speed = 1
player.direction = 80
@player.register
def act(self):
v1 = Vector.from_actor_direction(self)
v1.rotate(-1)
self.direction = v1
world.run()
.. raw:: html
<video loop autoplay muted width="400">
<source src="../_static/mp4/rotating_rectangle.mp4" type="video/mp4">
<source src="../_static/rotating_rectangle.webm" type="video/webm">
Your browser does not support the video tag.
</video>
"""
return Vector.from_direction(actor.direction)
[docs]
def rotate(self, theta: float) -> "Vector":
radians = np.deg2rad(theta % 360)
rot = np.array([[math.cos(radians), -math.sin(radians)],
[math.sin(radians), math.cos(radians)]])
self.vec = np.dot(rot, self.vec)
return self
[docs]
def to_direction(self) -> float:
if self.length() == 0:
return 0.0
axis = np.array([0, -1])
unit_vector = self.vec / np.linalg.norm(self.vec)
dot = np.dot(unit_vector, axis)
angle = math.degrees(math.acos(dot))
if self.x < 0:
angle = 360 - angle
return angle
[docs]
def normalize(self) -> "Vector":
norm = np.linalg.norm(self.vec)
if norm == 0:
return self
self.vec = self.vec / norm
return self
[docs]
def length(self) -> float:
return float(np.linalg.norm(self.vec))
[docs]
def limit(self, max_length: float) -> "Vector":
if self.length() > max_length:
self.vec = self.normalize().vec * max_length
return self
[docs]
def multiply(self, other: Union[float, int, "Vector"]) -> Union["Vector", float]:
if isinstance(other, (int, float)):
return Vector(self.x * other, self.y * other)
if isinstance(other, Vector):
return self.dot(other)
raise TypeError("Unsupported operand type for multiply.")
[docs]
def add_to_position(self, position: Tuple[float, float]) -> Tuple[float, float]:
return (self.x + position[0], self.y + position[1])
[docs]
def get_normal(self) -> "Vector":
return Vector(-self.y, self.x)
[docs]
def dot(self, other: "Vector") -> float:
return float(np.dot(self.vec, other.vec))
[docs]
def distance_to(self, other: Union["Vector", Tuple[float, float]]) -> float:
"""
Calculates Euclidean distance to another vector or position.
Args:
other: A Vector or tuple.
Returns:
The distance as float.
Examples:
>>> Vector(0, 0).distance_to((3, 4))
5.0
"""
other = self._to_vector(other)
return float(np.linalg.norm(self.vec - other.vec))
[docs]
def angle_to(self, other: Union["Vector", Tuple[float, float]]) -> float:
"""
Computes the angle between this vector and another in degrees.
Args:
other: A Vector or tuple.
Returns:
Angle in degrees between 0 and 180.
Examples:
>>> Vector(1, 0).angle_to((0, 1))
90.0
"""
other = self._to_vector(other)
norm_self = self.vec / np.linalg.norm(self.vec)
norm_other = other.vec / np.linalg.norm(other.vec)
dot = np.clip(np.dot(norm_self, norm_other), -1.0, 1.0)
return math.degrees(math.acos(dot))
def __add__(self, other: Union["Vector", Tuple[float, float]]) -> "Vector":
other = self._to_vector(other)
return Vector(self.x + other.x, self.y + other.y)
def __radd__(self, other: Union["Vector", Tuple[float, float]]) -> "Vector":
return self.__add__(other)
def __sub__(self, other: Union["Vector", Tuple[float, float]]) -> "Vector":
other = self._to_vector(other)
return Vector(self.x - other.x, self.y - other.y)
def __rsub__(self, other: Union["Vector", Tuple[float, float]]) -> "Vector":
other = self._to_vector(other)
return Vector(other.x - self.x, other.y - self.y)
def __mul__(self, other: Union[float, int, "Vector", Tuple[float, float]]) -> Union["Vector", float]:
if isinstance(other, (int, float)):
return Vector(self.x * other, self.y * other)
other = self._to_vector(other)
return self.dot(other)
def __rmul__(self, other: Union[float, int, "Vector", Tuple[float, float]]) -> Union["Vector", float]:
return self.__mul__(other)
def __neg__(self) -> "Vector":
return Vector(-self.x, -self.y)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Vector):
return np.allclose(self.vec, other.vec)
if isinstance(other, tuple) and len(other) == 2:
return np.allclose(self.vec, np.array(other, dtype=float))
return False
def __str__(self) -> str:
return f"({round(self.x, 3)}, {round(self.y, 3)})"
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"