Source code for oowl.boundaries.primitives

"""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)