# -*- coding: utf-8 -*-
"""
Basic geometry functions
------------------------
Overview
^^^^^^^^
The :py:mod:`.geometry` module provides basic geometry functions for
computing 2D transformations and distances.
The following functions are available:
* :py:func:`.translate`
* :py:func:`.scale`
* :py:func:`.rotate`
* :py:func:`.colvector`
* :py:func:`.vector_norm`
* :py:func:`.vector_projection`
* :py:func:`.vector_angle`
* :py:func:`.compute_center`
* :py:func:`.compute_rect_size`
* :py:func:`.compute_distance`
* :py:func:`.compute_angle`
Reference
^^^^^^^^^
.. autofunction:: translate
.. autofunction:: scale
.. autofunction:: rotate
.. autofunction:: colvector
.. autofunction:: vector_norm
.. autofunction:: vector_projection
.. autofunction:: vector_angle
.. autofunction:: compute_center
.. autofunction:: compute_rect_size
.. autofunction:: compute_distance
.. autofunction:: compute_angle
"""
# pylint: disable=C0103
from __future__ import annotations
import numpy as np
# ===============================================================================
# Transform matrix functions
# ===============================================================================
[docs]
def translate(tx: float, ty: float) -> np.ndarray:
"""Return translation matrix
Args:
tx: Translation along X-axis
ty: Translation along Y-axis
Returns:
Translation matrix
"""
return np.array([[1, 0, tx], [0, 1, ty], [0, 0, 1]], float)
[docs]
def scale(sx: float, sy: float) -> np.ndarray:
"""Return scale matrix
Args:
sx: Scale along X-axis
sy: Scale along Y-axis
Returns:
Scale matrix
"""
return np.array([[sx, 0, 0], [0, sy, 0], [0, 0, 1]], float)
[docs]
def rotate(alpha: float) -> np.ndarray:
"""Return rotation matrix
Args:
alpha: Rotation angle (in radians)
Returns:
Rotation matrix
"""
return np.array(
[
[np.cos(alpha), -np.sin(alpha), 0],
[np.sin(alpha), np.cos(alpha), 0],
[0, 0, 1],
],
float,
)
[docs]
def colvector(x: float, y: float) -> np.ndarray:
"""Return vector from coordinates
Args:
x: x-coordinate
y: y-coordinate
Returns:
Vector
"""
return np.array([x, y, 1]).T
# ===============================================================================
# Operations on vectors (from coordinates)
# ===============================================================================
[docs]
def vector_norm(xa: float, ya: float, xb: float, yb: float) -> float:
"""Return vector norm
Args:
xa: x-coordinate of first point
ya: y-coordinate of first point
xb: x-coordinate of second point
yb: y-coordinate of second point
Returns:
Norm of vector (xa, xb)-->(ya, yb)
"""
return np.linalg.norm(np.array((xb - xa, yb - ya)))
[docs]
def vector_projection(
dv: np.ndarray, xa: float, ya: float, xb: float, yb: float
) -> np.ndarray:
"""Return vector projection
Args:
dv: vector to project
xa: x-coordinate of first point
ya: y-coordinate of first point
xb: x-coordinate of second point
yb: y-coordinate of second point
Returns:
Projection of *dv* on vector (xa, xb)-->(ya, yb)
"""
assert dv.shape == (2,)
v_ab = np.array((xb - xa, yb - ya))
norm = np.linalg.norm(v_ab)
if norm == 0:
# Points A and B are identical: projection direction is undefined.
# Return point B (= point A) rather than raising on 0/0 division.
return np.array((xb, yb))
u_ab = v_ab / norm
return np.vdot(u_ab, dv) * u_ab + np.array((xb, yb))
def vector_rotation(theta: float, dx: float, dy: float) -> tuple[float, float]:
"""Compute theta-rotation on vector
Args:
theta: Rotation angle
dx: x-coordinate of vector
dy: y-coordinate of vector
Returns:
Tuple of (x, y) coordinates of rotated vector
"""
return (rotate(theta) @ colvector(dx, dy)).ravel()[:2]
[docs]
def vector_angle(dx: float, dy: float) -> float:
"""Return vector angle with X-axis
Args:
dx: x-coordinate of vector
dy: y-coordinate of vector
Returns:
Angle between vector and X-axis (in radians)
"""
# sign(dy) == 1 --> return Arccos()
# sign(dy) == 0 --> return 0 if sign(dx) == 1
# sign(dy) == 0 --> return pi if sign(dx) == -1
# sign(dy) == -1 --> return 2pi-Arccos()
if dx == 0 and dy == 0:
return 0.0
else:
sx, sy = np.sign(dx), np.sign(dy)
acos = np.arccos(dx / np.sqrt(dx**2 + dy**2))
return sy * (np.pi * (sy - 1) + acos) + np.pi * (1 - sy**2) * (1 - sx) * 0.5
# ===============================================================================
# Misc.
# ===============================================================================
[docs]
def compute_center(x1: float, y1: float, x2: float, y2: float) -> tuple[float, float]:
"""Compute center of rectangle
Args:
x1: x-coordinate of top-left corner
y1: y-coordinate of top-left corner
x2: x-coordinate of bottom-right corner
y2: y-coordinate of bottom-right corner
Returns:
Tuple of (x, y) coordinates of center
"""
return 0.5 * (x1 + x2), 0.5 * (y1 + y2)
[docs]
def compute_rect_size(
x1: float, y1: float, x2: float, y2: float
) -> tuple[float, float]:
"""Compute rectangle size
Args:
x1: x-coordinate of top-left corner
y1: y-coordinate of top-left corner
x2: x-coordinate of bottom-right corner
y2: y-coordinate of bottom-right corner
Returns:
Tuple of (width, height)
"""
return x2 - x1, np.fabs(y2 - y1)
[docs]
def compute_distance(x1: float, y1: float, x2: float, y2: float) -> float:
"""Compute distance between two points
Args:
x1: x-coordinate of first point
y1: y-coordinate of first point
x2: x-coordinate of second point
y2: y-coordinate of second point
Returns:
Distance between points
"""
return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
[docs]
def compute_angle(
x1: float, y1: float, x2: float, y2: float, reverse: bool = False
) -> float:
"""Compute angle between two points
Args:
x1: x-coordinate of first point
y1: y-coordinate of first point
x2: x-coordinate of second point
y2: y-coordinate of second point
reverse: If True, return the angle in the opposite direction
Returns:
Angle between points (in degrees)
"""
sign = -1 if reverse else 1
if x2 == x1:
return 0.0
else:
return np.arctan(-sign * (y2 - y1) / (x2 - x1)) * 180.0 / np.pi