"""
This modules contains all the geometry need to make any kind of shape.
You can use directly the function ```regular_polygon``` and the transformations
like ```move```, ```rotate``` and ```scale``` to model your desired curve.
You can assemble JordanCurves to create shapes with holes,
or even unconnected shapes.
"""
from __future__ import annotations
from copy import copy
from typing import Iterable, Iterator, Tuple, Union
from ..geometry.box import Box
from ..geometry.jordancurve import JordanCurve
from ..geometry.point import Point2D
from ..geometry.transform import move, rotate, scale
from ..loggers import debug
from ..scalar.angle import Angle
from ..scalar.reals import Real
from ..tools import Is, To
from .base import Future, SubSetR2
from .curve import SingleCurve
from .density import (
Density,
intersect_densities,
lebesgue_density_jordan,
unite_densities,
)
from .point import SinglePoint
[docs]
class SimpleShape(SubSetR2):
"""
SimpleShape class
Is a shape which is defined by only one jordan curve.
It represents the interior/exterior region of the jordan curve
if the jordan curve is counter-clockwise/clockwise
"""
def __init__(self, jordancurve: JordanCurve, boundary: bool = True):
if not Is.instance(jordancurve, JordanCurve):
raise TypeError
self.__jordancurve = jordancurve
self.__boundary = bool(boundary)
def __copy__(self) -> SimpleShape:
return self.__deepcopy__(None)
def __deepcopy__(self, memo) -> SimpleShape:
return SimpleShape(copy(self.__jordancurve))
def __str__(self) -> str: # pragma: no cover # For debug
area = float(self.area)
vertices = tuple(map(tuple, self.jordan.vertices()))
return f"SimpleShape[{area:.2f}]:[{vertices}]"
def __eq__(self, other: SubSetR2) -> bool:
"""Compare two shapes
Parameters
----------
other: SubSetR2
The shape to compare
:raises ValueError: If ``other`` is not a SubSetR2 instance
"""
if not Is.instance(other, SubSetR2):
raise ValueError
return (
Is.instance(other, SimpleShape)
and self.area == other.area
and self.jordan == other.jordan
)
@property
def boundary(self) -> bool:
"""The flag that informs if the boundary is inside the Shape"""
return self.__boundary
@property
def jordan(self) -> JordanCurve:
"""Gives the jordan curve that defines the boundary"""
return self.__jordancurve
@property
def jordans(self) -> Tuple[JordanCurve]:
"""Gives the jordan curve that defines the boundary"""
return (self.__jordancurve,)
@property
def area(self) -> Real:
"""The internal area that is enclosed by the shape"""
return self.__jordancurve.area
@debug("shapepy.bool2d.shape")
def __hash__(self):
return hash(self.area)
@debug("shapepy.bool2d.shape")
def __contains__(self, other: SubSetR2) -> bool:
if Is.instance(other, SinglePoint):
return self.__contains_point(other)
if Is.instance(other, SingleCurve):
return self.__contains_curve(other)
if Is.instance(other, SimpleShape):
return self.__contains_simple(other)
return super().__contains__(other)
def __contains_point(self, point: SinglePoint) -> bool:
point = Future.convert(point)
density = float(self.density(point.internal))
return density > 0 if self.boundary else density == 1
def __contains_curve(self, curve: SingleCurve) -> bool:
piecewise = curve.internal.parametrize()
vertices = map(piecewise, piecewise.knots[:-1])
if not all(map(self.__contains_point, vertices)):
return False
inters = piecewise & self.jordan
if not inters:
return True
knots = sorted(inters.all_knots[id(piecewise)])
midknots = ((k0 + k1) / 2 for k0, k1 in zip(knots, knots[1:]))
midpoints = map(piecewise, midknots)
return all(map(self.__contains_point, midpoints))
# pylint: disable=chained-comparison
def __contains_simple(self, other: SimpleShape) -> bool:
assert Is.instance(other, SimpleShape)
areaa = other.area
areab = self.area
jordana = other.jordan
jordanb = self.jordan
if areaa < 0 and areab > 0:
return False
if not self.box() & other.box():
return areaa > 0 and areab < 0
if areaa > 0 and areab < 0:
return jordana in self and jordanb not in other
if areaa > areab or jordana not in self:
return False
if areaa > 0:
return True
# If simple shape is not a square
# may happens error here
return True
[docs]
def move(self, vector: Point2D) -> SimpleShape:
return SimpleShape(move(self.jordan, vector), self.boundary)
[docs]
def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> SimpleShape:
return SimpleShape(scale(self.jordan, amount), self.boundary)
[docs]
def rotate(self, angle: Angle) -> SimpleShape:
return SimpleShape(rotate(self.jordan, angle), self.boundary)
[docs]
def box(self) -> Box:
"""
Box that encloses all jordan curves
Parameters
----------
:return: The box that encloses all
:rtype: Box
Example use
-----------
>>> from shapepy import Primitive, IntegrateShape
>>> circle = Primitive.circle(radius = 1)
>>> circle.box()
Box with vertices (-1.0, -1.0) and (1., 1.0)
"""
return self.jordan.box()
[docs]
def density(self, center: Point2D) -> Density:
return lebesgue_density_jordan(self.jordan, center)
[docs]
class ConnectedShape(SubSetR2):
"""
ConnectedShape Class
A shape defined by intersection of two or more SimpleShapes
"""
def __init__(self, subshapes: Iterable[SimpleShape]):
subshapes = frozenset(subshapes)
if not all(Is.instance(simple, SimpleShape) for simple in subshapes):
raise TypeError(f"Invalid typos: {tuple(map(type, subshapes))}")
self.__subshapes = subshapes
def __copy__(self) -> ConnectedShape:
return self.__deepcopy__(None)
def __deepcopy__(self, memo) -> ConnectedShape:
return ConnectedShape(map(copy, self))
@property
def area(self) -> Real:
"""The internal area that is enclosed by the shape"""
return sum(simple.area for simple in self)
def __str__(self) -> str: # pragma: no cover # For debug
return f"Connected shape total area {self.area}"
def __eq__(self, other: SubSetR2) -> bool:
assert Is.instance(other, SubSetR2)
return (
Is.instance(other, ConnectedShape)
and hash(self) == hash(other)
and self.area == other.area
and frozenset(self) == frozenset(other)
)
@debug("shapepy.bool2d.shape")
def __hash__(self):
return hash(self.area)
def __iter__(self) -> Iterator[SimpleShape]:
yield from self.__subshapes
@property
def jordans(self) -> Tuple[JordanCurve, ...]:
"""Jordan curves that defines the shape
:getter: Returns a set of jordan curves
:type: tuple[JordanCurve]
"""
return tuple(shape.jordan for shape in self)
[docs]
def move(self, vector: Point2D) -> ConnectedShape:
vector = To.point(vector)
return ConnectedShape(sub.move(vector) for sub in self)
[docs]
def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> ConnectedShape:
return ConnectedShape(sub.scale(amount) for sub in self)
[docs]
def rotate(self, angle: Angle) -> ConnectedShape:
angle = To.angle(angle)
return ConnectedShape(sub.rotate(angle) for sub in self)
[docs]
def box(self) -> Box:
"""
Box that encloses all jordan curves
Parameters
----------
:return: The box that encloses all
:rtype: Box
Example use
-----------
>>> from shapepy import Primitive, IntegrateShape
>>> circle = Primitive.circle(radius = 1)
>>> circle.box()
Box with vertices (-1.0, -1.0) and (1., 1.0)
"""
box = None
for sub in self:
box |= sub.jordan.box()
return box
[docs]
def density(self, center: Point2D) -> Density:
center = To.point(center)
densities = (sub.density(center) for sub in self)
return intersect_densities(densities)
[docs]
class DisjointShape(SubSetR2):
"""
DisjointShape Class
A shape defined by the union of some SimpleShape instances and
ConnectedShape instances
"""
def __init__(
self, subshapes: Iterable[Union[SimpleShape, ConnectedShape]]
):
subshapes = frozenset(subshapes)
if not all(
Is.instance(s, (SimpleShape, ConnectedShape)) for s in subshapes
):
raise ValueError(f"Invalid typos: {tuple(map(type, subshapes))}")
self.__subshapes = subshapes
def __copy__(self) -> ConnectedShape:
return self.__deepcopy__(None)
def __deepcopy__(self, memo):
return DisjointShape(map(copy, self))
def __iter__(self) -> Iterator[Union[SimpleShape, ConnectedShape]]:
yield from self.__subshapes
@property
def area(self) -> Real:
"""The internal area that is enclosed by the shape"""
return sum(sub.area for sub in self)
@property
def jordans(self) -> Tuple[JordanCurve, ...]:
"""Jordan curves that defines the shape
:getter: Returns a set of jordan curves
:type: tuple[JordanCurve]
"""
jordans = []
for subshape in self:
jordans += list(subshape.jordans)
return tuple(jordans)
def __eq__(self, other: SubSetR2):
assert Is.instance(other, SubSetR2)
return (
Is.instance(other, DisjointShape)
and hash(self) == hash(other)
and self.area == other.area
and frozenset(self) == frozenset(other)
)
def __str__(self) -> str: # pragma: no cover # For debug
msg = f"Disjoint shape with total area {self.area} and "
msg += f"{len(self.__subshapes)} subshapes"
return msg
@debug("shapepy.bool2d.shape")
def __hash__(self):
return hash(self.area)
[docs]
def move(self, vector: Point2D) -> DisjointShape:
vector = To.point(vector)
return DisjointShape(sub.move(vector) for sub in self)
[docs]
def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> DisjointShape:
return DisjointShape(sub.scale(amount) for sub in self)
[docs]
def rotate(self, angle: Angle) -> DisjointShape:
angle = To.angle(angle)
return DisjointShape(sub.rotate(angle) for sub in self)
[docs]
def box(self) -> Box:
"""
Box that encloses all jordan curves
Parameters
----------
:return: The box that encloses all
:rtype: Box
Example use
-----------
>>> from shapepy import Primitive, IntegrateShape
>>> circle = Primitive.circle(radius = 1)
>>> circle.box()
Box with vertices (-1.0, -1.0) and (1., 1.0)
"""
box = None
for sub in self:
box |= sub.box()
return box
[docs]
def density(self, center: Point2D) -> Real:
center = To.point(center)
return unite_densities((sub.density(center) for sub in self))