Skip to content

base

pleat.base

2D geometry primitives: vectors, angles, areas, intersections, and coordinate transforms.

All functions take and return numpy.ndarray of shape (..., 2) for points and angles in radians. The convention is right-handed: positive rotation is counter-clockwise; :func:signed_area is positive for CCW polygons.

unit_vector

unit_vector(alpha: ArrayLike) -> NDArray[np.float64]

Return unit vector(s) at angle alpha (radians).

Accepts a scalar or array; the trailing axis of length 2 is appended.

Source code in pleat/base.py
def unit_vector(alpha: ArrayLike) -> NDArray[np.float64]:
    """Return unit vector(s) at angle *alpha* (radians).

    Accepts a scalar or array; the trailing axis of length 2 is appended.
    """
    return np.stack([np.cos(alpha), np.sin(alpha)], axis=-1)

angle_to_axis

angle_to_axis(vectors: NDArray) -> NDArray

Return the angle (radians) each 2D vector makes with the positive x-axis.

Result is in (-pi, pi] (per :func:numpy.arctan2).

Source code in pleat/base.py
def angle_to_axis(vectors: NDArray) -> NDArray:
    """Return the angle (radians) each 2D vector makes with the positive x-axis.

    Result is in ``(-pi, pi]`` (per :func:`numpy.arctan2`).
    """
    return np.arctan2(vectors[..., 1], vectors[..., 0])

angle

angle(a: NDArray, b: NDArray, c: NDArray) -> NDArray

Return the interior angle at vertex b on the path a -> b -> c, in [0, 2*pi).

Source code in pleat/base.py
def angle(a: NDArray, b: NDArray, c: NDArray) -> NDArray:
    """Return the interior angle at vertex *b* on the path ``a -> b -> c``, in ``[0, 2*pi)``."""
    return (angle_to_axis(a - b) - angle_to_axis(c - b)) % (2 * np.pi)

in_angles

in_angles(points: NDArray) -> NDArray

Interior angles of a polygon given its ordered vertices (shape (n, 2)).

Vertices are assumed to be in CCW order; angles are returned in [0, 2*pi).

Source code in pleat/base.py
def in_angles(points: NDArray) -> NDArray:
    """Interior angles of a polygon given its ordered vertices (shape ``(n, 2)``).

    Vertices are assumed to be in CCW order; angles are returned in ``[0, 2*pi)``.
    """
    edge_vectors = np.concatenate([points[1:], points[:1]]) - points
    edge_angles = angle_to_axis(edge_vectors)
    return (np.pi + edge_angles - np.concatenate([edge_angles[1:], edge_angles[:1]])) % (2 * np.pi)

edge_lengths

edge_lengths(points: NDArray) -> NDArray

Edge lengths of a polygon given its ordered vertices (shape (n, 2)).

Source code in pleat/base.py
def edge_lengths(points: NDArray) -> NDArray:
    """Edge lengths of a polygon given its ordered vertices (shape ``(n, 2)``)."""
    edge_vectors = np.concatenate([points[1:], points[:1]]) - points
    return np.linalg.norm(edge_vectors, axis=1)

edge_lengths_and_in_angles

edge_lengths_and_in_angles(
    points: NDArray, geometry
) -> tuple[list[float], list[float]]

Return (edge_lengths, interior_angles) for a polygon under geometry.

geometry must expose distance(p, q) and angle(p, q, r) (see :mod:pleat.geometries).

Source code in pleat/base.py
def edge_lengths_and_in_angles(points: NDArray, geometry) -> tuple[list[float], list[float]]:
    """Return ``(edge_lengths, interior_angles)`` for a polygon under *geometry*.

    *geometry* must expose ``distance(p, q)`` and ``angle(p, q, r)`` (see
    :mod:`pleat.geometries`).
    """
    edge_lengths = [geometry.distance(p1, p2) for p1, p2 in zip(points, np.concatenate([points[1:], points[:1]]))]
    in_angles = [
        geometry.angle(p1, p2, p3)
        for p1, p2, p3 in zip(
            points, np.concatenate([points[1:], points[:1]]), np.concatenate([points[2:], points[:2]])
        )
    ]
    return edge_lengths, in_angles

unit_vector_to_vector

unit_vector_to_vector(
    alpha: float, vector: NDArray
) -> NDArray

Rotate direction alpha onto the line through vector[0] and vector[1].

Returns a length-2 segment starting at vector[0] and pointing in the direction obtained by rotating vector[1] - vector[0] by alpha.

Source code in pleat/base.py
def unit_vector_to_vector(alpha: float, vector: NDArray) -> NDArray:
    """Rotate direction *alpha* onto the line through ``vector[0]`` and ``vector[1]``.

    Returns a length-2 segment starting at ``vector[0]`` and pointing in the
    direction obtained by rotating ``vector[1] - vector[0]`` by *alpha*.
    """
    return np.array([vector[0], vector[0] + unit_vector(angle_to_axis(vector[1] - vector[0]) + alpha)])

tri_grid_point

tri_grid_point(i: int, j: int) -> NDArray

Return the point at integer coordinates (i, j) on the standard triangular grid.

Source code in pleat/base.py
def tri_grid_point(i: int, j: int) -> NDArray:
    """Return the point at integer coordinates ``(i, j)`` on the standard triangular grid."""
    return unit_vector(0) * i + unit_vector(np.pi / 3) * j

regular_poly_points

regular_poly_points(n: int) -> NDArray

Vertices of a unit-edge-length regular n-gon centered at the origin.

Source code in pleat/base.py
def regular_poly_points(n: int) -> NDArray:
    """Vertices of a unit-edge-length regular *n*-gon centered at the origin."""
    return unit_vector(np.linspace(0, tau, n, endpoint=False)) / (2 * np.sin(pi / n))

apply_affine

apply_affine(vectors: NDArray, matrix: NDArray) -> NDArray

Apply a 2D affine transform to an array of 2D points.

Parameters:

Name Type Description Default
vectors NDArray

Points of shape (..., 2).

required
matrix NDArray

A (3, 2) matrix where rows 0-1 are the linear part and row 2 is the translation.

required
Source code in pleat/base.py
def apply_affine(vectors: NDArray, matrix: NDArray) -> NDArray:
    """Apply a 2D affine transform to an array of 2D points.

    Args:
        vectors: Points of shape ``(..., 2)``.
        matrix: A ``(3, 2)`` matrix where rows 0-1 are the linear part and row 2 is the translation.
    """
    return np.dot(
        np.concatenate([vectors, np.ones_like(vectors[..., :1])], axis=-1),
        matrix,
    )

rotation_matrix

rotation_matrix(alpha: float) -> NDArray

Return a 2x2 rotation matrix.

Use as point @ R to rotate point CCW by alpha.

Source code in pleat/base.py
def rotation_matrix(alpha: float) -> NDArray:
    """Return a 2x2 rotation matrix.

    Use as ``point @ R`` to rotate *point* CCW by *alpha*.
    """
    s, c = np.sin(alpha), np.cos(alpha)
    return np.array([[c, s], [-s, c]])

find_affine

find_affine(line0: NDArray, line1: NDArray) -> NDArray

Affine transform ((3, 2) matrix) mapping directed segment line0 to line1.

The transform is rotation + uniform scaling + translation; in particular, it is conformal (no shear).

Source code in pleat/base.py
def find_affine(line0: NDArray, line1: NDArray) -> NDArray:
    """Affine transform (``(3, 2)`` matrix) mapping directed segment *line0* to *line1*.

    The transform is rotation + uniform scaling + translation; in particular,
    it is conformal (no shear).
    """
    lines = np.stack([line0, line1])
    relative = lines[:, 1] - lines[:, 0]
    lengths = np.linalg.norm(relative, axis=1)
    scale = lengths[1] / lengths[0]
    angles = angle_to_axis(relative)
    angle = angles[1] - angles[0]
    linear = scale * rotation_matrix(angle)
    offset = lines[1, 1] - lines[0, 1].dot(linear)
    return np.concatenate([linear, offset[None]])

nearest_neighbor

nearest_neighbor(
    data: NDArray, query: NDArray, return_index: bool = True
)

Return the nearest point in data to query (brute-force).

Parameters:

Name Type Description Default
data NDArray

Points of shape (N, D).

required
query NDArray

A single point of shape (D,).

required
return_index bool

If True, return (point, index); otherwise return the point.

True
Source code in pleat/base.py
def nearest_neighbor(data: NDArray, query: NDArray, return_index: bool = True):
    """Return the nearest point in *data* to *query* (brute-force).

    Args:
        data: Points of shape ``(N, D)``.
        query: A single point of shape ``(D,)``.
        return_index: If True, return ``(point, index)``; otherwise return the point.
    """
    if len(data.shape) > len(query.shape):
        query = query[None]
    index = np.argmin(np.linalg.norm(data - query, axis=-1))
    return data[index], index if return_index else data[index]

signed_area

signed_area(pts)

Signed area of a polygon (positive = CCW). Numba-accelerated.

pts must have shape (n, 2); the polygon is closed implicitly.

Source code in pleat/base.py
@jit(nopython=True)
def signed_area(pts):
    """Signed area of a polygon (positive = CCW). Numba-accelerated.

    *pts* must have shape ``(n, 2)``; the polygon is closed implicitly.
    """
    assert pts.shape[1] == 2
    pts_rot = np.concatenate((pts[1:], pts[:1]))
    return np.sum(pts[:, 0] * pts_rot[:, 1] - pts[:, 1] * pts_rot[:, 0]) / 2.0

orientation

orientation(pts, eps=0)

Return +1 (CCW), -1 (CW), or 0 (degenerate) for a polygon.

Numba-accelerated. eps is the absolute-area threshold for degeneracy.

Source code in pleat/base.py
@jit(nopython=True)
def orientation(pts, eps=0):
    """Return ``+1`` (CCW), ``-1`` (CW), or ``0`` (degenerate) for a polygon.

    Numba-accelerated.  *eps* is the absolute-area threshold for degeneracy.
    """
    area = signed_area(pts)
    if abs(area) <= eps:
        return 0
    return 2 * int(area > 0) - 1

euclidean_to_barycentric_map

euclidean_to_barycentric_map(
    tri: NDArray,
) -> Callable[[NDArray], NDArray]

Return a function converting Euclidean 2D points to barycentric coords w.r.t. tri.

tri is a (3, 2) array of triangle vertices.

Source code in pleat/base.py
def euclidean_to_barycentric_map(tri: NDArray) -> Callable[[NDArray], NDArray]:
    """Return a function converting Euclidean 2D points to barycentric coords w.r.t. *tri*.

    *tri* is a ``(3, 2)`` array of triangle vertices.
    """
    tri = np.array(tri, dtype=np.float32)

    def inner(point):
        mat = np.repeat(tri[None, :], 3, axis=0)
        mat[np.eye(3, dtype=bool)] = point
        coords = np.array([signed_area(pts) for pts in mat], dtype=np.float32)
        return coords / np.sum(coords)

    return inner

barycentric_to_euclidean_map

barycentric_to_euclidean_map(
    tri: NDArray,
) -> Callable[[NDArray], NDArray]

Return a function converting barycentric coords back to 2D points w.r.t. tri.

Source code in pleat/base.py
def barycentric_to_euclidean_map(tri: NDArray) -> Callable[[NDArray], NDArray]:
    """Return a function converting barycentric coords back to 2D points w.r.t. *tri*."""

    def inner(barycentric_coords):
        return tri.T @ barycentric_coords

    return inner

project_to_line

project_to_line(line: NDArray, points: NDArray) -> NDArray

Orthogonally project points onto the infinite line through line[0] and line[1].

Source code in pleat/base.py
def project_to_line(line: NDArray, points: NDArray) -> NDArray:
    """Orthogonally project *points* onto the infinite line through ``line[0]`` and ``line[1]``."""
    v = line[1] - line[0]
    v /= np.linalg.norm(v)
    return np.sum((points - line[0]) * v, axis=-1, keepdims=True) * v + line[0]

line_intersection

line_intersection(
    line1: NDArray, line2: NDArray
) -> NDArray

Return the intersection point of two infinite lines (each given as two points).

Raises:

Type Description
ValueError

if the lines are parallel or coincident.

Source code in pleat/base.py
def line_intersection(line1: NDArray, line2: NDArray) -> NDArray:
    """Return the intersection point of two infinite lines (each given as two points).

    Raises:
        ValueError: if the lines are parallel or coincident.
    """
    diff = np.stack([line[0] - line[1] for line in (line1, line2)])

    div = np.linalg.det(diff)
    if div == 0:
        raise ValueError("lines do not intersect (parallel or coincident)")

    d = np.array([np.linalg.det(line) for line in (line1, line2)])
    return np.array([np.linalg.det(np.stack([d, dif])) for dif in diff.T]) / div