Source code for shapepy.bool2d.shape

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