"""Primitive boundary shapes."""
from __future__ import annotations
import numpy as np
from shapely import affinity
from shapely.geometry import Point, Polygon
from shapely.geometry.base import BaseGeometry
from .._validation import _validate_finite, _validate_integer, _validate_positive
from .base import Boundary, Point2D, _optional_area, _resolve_center
__all__ = [
"Rectangle",
"Square",
"RegularPolygon",
"Circle",
"Ellipse",
"Annulus",
]
[docs]
class Rectangle(Boundary):
"""Rectangle whose ``width`` and ``height`` set only the aspect ratio.
``area`` sets the size in square meters, ``None`` for a unit-size shape
to size later, and ``rotation`` turns the rectangle counterclockwise about
its center, in degrees.
"""
def __init__(
self,
width: float = 1.0,
height: float = 1.0,
*,
area: float | None = None,
center: Point2D | None = None,
rotation: float = 0.0,
):
self.width = _validate_positive("width", width)
self.height = _validate_positive("height", height)
self.target_area = _optional_area(area)
self.center, self._positioned = _resolve_center(center)
self.rotation = _validate_finite("rotation", rotation)
def _build_geometry(self) -> BaseGeometry:
hw, hh = self.width / 2.0, self.height / 2.0
poly = Polygon([(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)])
if self.rotation:
poly = affinity.rotate(poly, self.rotation, origin=(0.0, 0.0))
return poly
[docs]
class Square(Rectangle):
"""Square of a given ``area``, turned by ``rotation`` degrees."""
def __init__(
self,
*,
area: float | None = None,
center: Point2D | None = None,
rotation: float = 0.0,
):
super().__init__(
1.0,
1.0,
area=area,
center=center,
rotation=rotation,
)
[docs]
class RegularPolygon(Boundary):
"""Regular polygon with ``n_sides`` sides, at least 3, and a given ``area``.
The first vertex lies at ``rotation`` degrees counterclockwise from the
positive x axis.
"""
def __init__(
self,
n_sides: int,
*,
area: float | None = None,
center: Point2D | None = None,
rotation: float = 0.0,
):
self.n_sides = _validate_integer("n_sides", n_sides, minimum=3)
self.target_area = _optional_area(area)
self.center, self._positioned = _resolve_center(center)
self.rotation = _validate_finite("rotation", rotation)
def _build_geometry(self) -> BaseGeometry:
angles = np.linspace(0.0, 2.0 * np.pi, self.n_sides, endpoint=False)
angles = angles + np.deg2rad(self.rotation)
return Polygon(np.column_stack([np.cos(angles), np.sin(angles)]))
[docs]
class Circle(Boundary):
"""Circle of a given ``area``, drawn as a polygon of ``n_segments`` vertices.
``n_segments`` is rounded down to a multiple of four.
"""
def __init__(
self,
*,
area: float | None = None,
center: Point2D | None = None,
n_segments: int = 64,
):
self.target_area = _optional_area(area)
self.center, self._positioned = _resolve_center(center)
self.n_segments = _validate_integer("n_segments", n_segments, minimum=4)
def _build_geometry(self) -> BaseGeometry:
return Point(0.0, 0.0).buffer(1.0, quad_segs=self.n_segments // 4)
[docs]
class Ellipse(Boundary):
"""Ellipse whose ``semi_major`` and ``semi_minor`` axes set only the shape.
``area`` sets the size and ``rotation`` turns the major axis
counterclockwise from the x axis, in degrees. The outline has
``n_segments`` vertices, rounded down to a multiple of four.
"""
def __init__(
self,
semi_major: float = 1.0,
semi_minor: float = 1.0,
*,
area: float | None = None,
center: Point2D | None = None,
rotation: float = 0.0,
n_segments: int = 64,
):
self.semi_major = _validate_positive("semi_major", semi_major)
self.semi_minor = _validate_positive("semi_minor", semi_minor)
self.target_area = _optional_area(area)
self.center, self._positioned = _resolve_center(center)
self.rotation = _validate_finite("rotation", rotation)
self.n_segments = _validate_integer("n_segments", n_segments, minimum=4)
def _build_geometry(self) -> BaseGeometry:
base = Point((0.0, 0.0)).buffer(1.0, quad_segs=self.n_segments // 4)
poly = affinity.scale(
base,
xfact=self.semi_major,
yfact=self.semi_minor,
origin=(0, 0),
)
if self.rotation:
poly = affinity.rotate(poly, self.rotation, origin=(0, 0))
return poly
[docs]
class Annulus(Boundary):
"""Ring whose ``outer_radius`` and ``inner_radius`` set only the shape.
``area`` is the area of the ring itself, excluding the hole. Each circle
has ``n_segments`` vertices, rounded down to a multiple of four.
"""
def __init__(
self,
outer_radius: float = 1.0,
inner_radius: float = 0.4,
*,
area: float | None = None,
center: Point2D | None = None,
n_segments: int = 64,
):
outer_radius = _validate_positive("outer_radius", outer_radius)
inner_radius = _validate_positive("inner_radius", inner_radius)
if inner_radius >= outer_radius:
raise ValueError("inner_radius must be smaller than outer_radius")
self.outer_radius = outer_radius
self.inner_radius = inner_radius
self.target_area = _optional_area(area)
self.center, self._positioned = _resolve_center(center)
self.n_segments = _validate_integer("n_segments", n_segments, minimum=4)
def _build_geometry(self) -> BaseGeometry:
quad = self.n_segments // 4
outer = Point(0.0, 0.0).buffer(self.outer_radius, quad_segs=quad)
inner = Point(0.0, 0.0).buffer(self.inner_radius, quad_segs=quad)
return outer.difference(inner)