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

import abc
from copy import copy
from typing import Optional, Tuple, Union

import numpy as np
import rbool

from ..geometry.box import Box
from ..geometry.integral import IntegrateJordan
from ..geometry.jordancurve import JordanCurve
from ..geometry.point import Point2D
from ..scalar.angle import Angle
from ..scalar.reals import Real
from ..tools import Is, To
from .base import EmptyShape, SubSetR2


# pylint: disable=no-member
class DefinedShape(SubSetR2):
    """
    DefinedShape is the base class for SimpleShape,
    ConnectedShape and DisjointShape

    """

    def __copy__(self) -> DefinedShape:
        return self.__deepcopy__(None)

    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 jordan in self.jordans:
            box |= jordan.box()
        return box

    def __contains__(
        self, other: Union[Point2D, JordanCurve, SubSetR2]
    ) -> bool:
        if Is.instance(other, DefinedShape):
            return self.contains_shape(other)
        if Is.instance(other, SubSetR2):
            return Is.instance(other, EmptyShape)
        if Is.instance(other, JordanCurve):
            return self.contains_jordan(other)
        point = To.point(other)
        return self.contains_point(point)

    def contains_point(
        self, point: Point2D, boundary: Optional[bool] = True
    ) -> bool:
        """
        Checks if given point is inside the shape

        Parameters
        ----------

        point : Point2D
            The point to verify if is inside
        boundary : bool, default = True
            The flag to decide if a boundary point is considered
            inside or outside.
            If ``True``, then a boundary point is considered inside.

        :return: Whether the point is inside or not
        :rtype: bool


        Example use
        -----------
        >>> from shapepy import Primitive
        >>> square = Primitive.square()
        >>> square.contains_point((0, 0))
        True
        >>> square.contains_point((0.5, 0), True)
        True
        >>> square.contains_point((0.5, 0), False)
        False

        """
        point = To.point(point)
        assert Is.bool(boundary)
        return self._contains_point(point, boundary)

    def contains_jordan(
        self, jordan: JordanCurve, boundary: Optional[bool] = True
    ) -> bool:
        """
        Checks if the all points of jordan are inside the shape

        Parameters
        ----------

        jordan : JordanCurve
            The jordan curve to verify
        boundary : bool, default = True
            The flag to check if jordan is inside a closed (True)
            or open (False) set

        :return: Whether the jordan is inside or not
        :rtype: bool


        Example use
        -----------
        >>> from shapepy import Primitive
        >>> square = Primitive.square()
        >>> jordan = small_square.jordans[0]
        >>> square.contains_jordan(jordan)
        True

        """
        assert Is.jordan(jordan)
        assert Is.bool(boundary)
        return self._contains_jordan(jordan, boundary)

    def contains_shape(self, other: SubSetR2) -> bool:
        """
        Checks if the all points of given shape are inside the shape

        Mathematically speaking, checks if ``other`` is a subset of ``self``

        Parameters
        ----------

        other : SubSetR2
            The shape to be verified if is inside

        :return: Whether the ``other`` shape is inside or not
        :rtype: bool

        Example use
        -----------
        >>> from shapepy import Primitive
        >>> square = Primitive.regular_polygon(4)
        >>> circle = Primitive.circle()
        >>> circle.contains_shape(square)
        True

        """
        assert Is.instance(other, DefinedShape)
        return self._contains_shape(other)

    @abc.abstractmethod
    def _contains_point(self, point: Point2D, boundary: Optional[bool] = True):
        pass

    @abc.abstractmethod
    def _contains_jordan(
        self, jordan: JordanCurve, boundary: Optional[bool] = True
    ):
        pass

    @abc.abstractmethod
    def _contains_shape(self, other: SubSetR2):
        pass


[docs] class SimpleShape(DefinedShape): """ 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): assert Is.jordan(jordancurve) self.__jordancurve = jordancurve def __deepcopy__(self, memo) -> DefinedShape: return SimpleShape(copy(self.__jordancurve)) def __str__(self) -> str: area = float(self.area) vertices = tuple(map(tuple, self.jordans[0].vertices)) vertices = np.array(vertices, dtype="float64") return f"Simple Shape of area {area:.2f} with vertices:\n{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.__jordancurve == other.jordans[0] ) def __invert__(self) -> SimpleShape: return self.__class__(~self.__jordancurve) @property def area(self) -> Real: """The internal area that is enclosed by the shape""" return self.__jordancurve.area @property def jordans(self) -> Tuple[JordanCurve]: """ The jordans curve that define the SimpleShape It has only one jordan curve inside it """ return (self.__jordancurve,)
[docs] def invert(self) -> SimpleShape: """ Inverts the region of simple shape. Parameters ---------- :return: The same instance :rtype: SimpleShape Example use ----------- >>> from shapepy import Primitive >>> square = Primitive.square() >>> print(square) Simple Shape of area 1.00 with vertices: [[ 0.5 0.5] [-0.5 0.5] [-0.5 -0.5] [ 0.5 -0.5]] >>> square.invert() Simple Shape of area -1.00 with vertices: [[ 0.5 0.5] [ 0.5 -0.5] [-0.5 -0.5] [-0.5 0.5]] """ self.__jordancurve.invert() return self
def _contains_point( self, point: Point2D, boundary: Optional[bool] = True ) -> bool: jordan = self.jordans[0] wind = IntegrateJordan.winding_number(jordan, center=point) return ( (wind > 0 if boundary else wind == 1) if jordan.area > 0 else wind > -1 if boundary else wind == 0 ) def _contains_jordan( self, jordan: JordanCurve, boundary: Optional[bool] = True ) -> bool: piecewise = jordan.parametrize() vertices = map(piecewise, piecewise.knots) if not all(map(self.contains_point, vertices)): return False inters = piecewise & self.jordans[0] inters.evaluate() if inters.all_subsets[id(piecewise)] is not rbool.Empty(): 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)) def _contains_shape(self, other: DefinedShape) -> bool: assert Is.instance(other, DefinedShape) if Is.instance(other, SimpleShape): return self.__contains_simple(other) if Is.instance(other, ConnectedShape): # cap S_i in S_j = any_i (bar S_j in bar S_i) contains = False self.invert() for subshape in other.subshapes: subshape.invert() if self in subshape: contains = True subshape.invert() if contains: break self.invert() return contains # Disjoint shape for subshape in other.subshapes: if subshape not in self: return False return True # pylint: disable=chained-comparison def __contains_simple(self, other: SimpleShape) -> bool: assert Is.instance(other, SimpleShape) areaa = other.area areab = self.area jordana = other.jordans[0] jordanb = self.jordans[0] 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) -> JordanCurve: self.__jordancurve = self.__jordancurve.move(vector) return self
[docs] def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> JordanCurve: self.__jordancurve = self.__jordancurve.scale(amount) return self
[docs] def rotate(self, angle: Angle) -> JordanCurve: self.__jordancurve = self.__jordancurve.rotate(angle) return self
[docs] class ConnectedShape(DefinedShape): """ ConnectedShape Class A shape defined by intersection of two or more SimpleShapes """ def __init__(self, subshapes: Tuple[SimpleShape]): self.subshapes = subshapes def __deepcopy__(self, memo): simples = tuple(map(copy, self.subshapes)) return ConnectedShape(simples) @property def area(self) -> Real: """The internal area that is enclosed by the shape""" return sum(simple.area for simple in self.subshapes) def __str__(self) -> str: 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 abs(self.area - other.area) < 1e-6 ) def __invert__(self) -> DisjointShape: simples = [~simple for simple in self.subshapes] return DisjointShape(simples) @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.jordans[0] for shape in self.subshapes) @property def subshapes(self) -> Tuple[SimpleShape]: """ Subshapes that defines the connected shape :getter: Subshapes that defines connected shape :setter: Subshapes that defines connected shape :type: tuple[SimpleShape] Example use ----------- >>> from shapepy import Primitive >>> big_square = Primitive.square(side = 2) >>> small_square = Primitive.square(side = 1) >>> shape = big_square - small_square >>> for subshape in shape.subshapes: print(subshape) Simple Shape of area 4.00 with vertices: [[ 1. 1.] [-1. 1.] [-1. -1.] [ 1. -1.]] Simple Shape of area -1.00 with vertices: [[ 0.5 0.5] [ 0.5 -0.5] [-0.5 -0.5] [-0.5 0.5]] """ return self.__subshapes @subshapes.setter def subshapes(self, simples: Tuple[SimpleShape]): if not all(Is.instance(simple, SimpleShape) for simple in simples): raise TypeError areas = (simple.area for simple in simples) def algori(pair): return pair[0] simples = sorted(zip(areas, simples), key=algori, reverse=True) simples = tuple(val[1] for val in simples) self.__subshapes = tuple(simples) def _contains_point( self, point: Point2D, boundary: Optional[bool] = True ) -> bool: for subshape in self.subshapes: if not subshape.contains_point(point, boundary): return False return True def _contains_jordan( self, jordan: JordanCurve, boundary: Optional[bool] = True ) -> bool: for subshape in self.subshapes: if not subshape.contains_jordan(jordan, boundary): return False return True def _contains_shape(self, other: DefinedShape) -> bool: for subshape in self.subshapes: if not subshape.contains_shape(other): return False return True
[docs] def move(self, vector: Point2D) -> JordanCurve: vector = To.point(vector) for subshape in self.subshapes: subshape.move(vector) return self
[docs] def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> JordanCurve: for subshape in self.subshapes: subshape.scale(amount) return self
[docs] def rotate(self, angle: Angle) -> JordanCurve: angle = To.angle(angle) for subshape in self.subshapes: subshape.rotate(angle) return self
[docs] class DisjointShape(DefinedShape): """ DisjointShape Class A shape defined by the union of some SimpleShape instances and ConnectedShape instances """ def __new__(cls, subshapes: Tuple[ConnectedShape]): subshapes = list(subshapes) while EmptyShape() in subshapes: subshapes.remove(EmptyShape()) if len(subshapes) == 0: return EmptyShape() for subshape in subshapes: assert Is.instance(subshape, (SimpleShape, ConnectedShape)) if len(subshapes) == 1: return copy(subshapes[0]) instance = super(DisjointShape, cls).__new__(cls) instance.subshapes = subshapes return instance def __init__(self, _: Tuple[ConnectedShape]): pass def __deepcopy__(self, memo): subshapes = tuple(map(copy, self.subshapes)) return DisjointShape(subshapes) def __invert__(self): new_jordans = tuple(~jordan for jordan in self.jordans) return shape_from_jordans(new_jordans) @property def area(self) -> Real: """The internal area that is enclosed by the shape""" return sum(sub.area for sub in self.subshapes) def __eq__(self, other: SubSetR2): assert Is.instance(other, SubSetR2) if not Is.instance(other, DisjointShape): return False if self.area != other.area: return False self_subshapes = list(self.subshapes) othe_subshapes = list(other.subshapes) # Compare if a curve is inside another while len(self_subshapes) != 0 and len(othe_subshapes) != 0: for j, osbshape in enumerate(othe_subshapes): if osbshape == self_subshapes[0]: self_subshapes.pop(0) othe_subshapes.pop(j) break else: return False return not (len(self_subshapes) or len(othe_subshapes)) def __str__(self) -> str: msg = f"Disjoint shape with total area {self.area} and " msg += f"{len(self.subshapes)} subshapes" return msg def _contains_point( self, point: Point2D, boundary: Optional[bool] = True ) -> bool: for subshape in self.subshapes: if subshape.contains_point(point, boundary): return True return False def _contains_jordan( self, jordan: JordanCurve, boundary: Optional[bool] = True ) -> bool: for subshape in self.subshapes: if subshape.contains_jordan(jordan, boundary): return True return False def _contains_shape(self, other: DefinedShape) -> bool: assert Is.instance(other, DefinedShape) if Is.instance(other, (SimpleShape, ConnectedShape)): for subshape in self.subshapes: if other in subshape: return True return False if Is.instance(other, DisjointShape): for subshape in other.subshapes: if subshape not in self: return False return True raise NotImplementedError @property def jordans(self) -> Tuple[JordanCurve]: """Jordan curves that defines the shape :getter: Returns a set of jordan curves :type: tuple[JordanCurve] """ lista = [] for subshape in self.subshapes: lista += list(subshape.jordans) return tuple(lista) @property def subshapes(self) -> Tuple[Union[SimpleShape, ConnectedShape]]: """ Subshapes that defines the disjoint shape :getter: Subshapes that defines disjoint shape :setter: Subshapes that defines disjoint shape :type: tuple[SimpleShape | ConnectedShape] Example use ----------- >>> from shapepy import Primitive >>> left = Primitive.square(center=(-2, 0)) >>> right = Primitive.square(center = (2, 0)) >>> shape = left | right >>> for subshape in shape.subshapes: print(subshape) Simple Shape of area 1.00 with vertices: [[-1.5 0.5] [-2.5 0.5] [-2.5 -0.5] [-1.5 -0.5]] Simple Shape of area 1.00 with vertices: [[ 2.5 0.5] [ 1.5 0.5] [ 1.5 -0.5] [ 2.5 -0.5]] """ return self.__subshapes @subshapes.setter def subshapes(self, values: Tuple[SubSetR2]): if not all( Is.instance(sub, (SimpleShape, ConnectedShape)) for sub in values ): raise ValueError areas = tuple(sub.area for sub in values) lenghts = tuple(sub.jordans[0].length for sub in values) def algori(triple): return triple[:2] values = sorted(zip(areas, lenghts, values), key=algori, reverse=True) values = tuple(val[2] for val in values) self.__subshapes = tuple(values)
[docs] def move(self, vector: Point2D) -> JordanCurve: vector = To.point(vector) for subshape in self.subshapes: subshape.move(vector) return self
[docs] def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> JordanCurve: for subshape in self.subshapes: subshape.scale(amount) return self
[docs] def rotate(self, angle: Angle) -> JordanCurve: angle = To.angle(angle) for subshape in self.subshapes: subshape.rotate(angle) return self
def divide_connecteds( simples: Tuple[SimpleShape], ) -> Tuple[Union[SimpleShape, ConnectedShape]]: """ Divides the simples in groups of connected shapes The idea is get the simple shape with maximum abs area, this is the biggest shape of all we start from it. We them separate all shapes in inside and outside """ if len(simples) == 0: return tuple() externals = [] connected = [] simples = list(simples) while len(simples) != 0: areas = (s.area for s in simples) absareas = tuple(map(abs, areas)) index = absareas.index(max(absareas)) connected.append(simples.pop(index)) internal = [] while len(simples) != 0: # Divide in two groups simple = simples.pop(0) jordan = simple.jordans[0] for subsimple in connected: subjordan = subsimple.jordans[0] if jordan not in subsimple or subjordan not in simple: externals.append(simple) break else: internal.append(simple) simples = internal if len(connected) == 1: connected = connected[0] else: connected = ConnectedShape(connected) return (connected,) + divide_connecteds(externals) def shape_from_jordans(jordans: Tuple[JordanCurve]) -> SubSetR2: """Returns the correspondent shape This function don't do entry validation as verify if one shape is inside other Example ---------- >>> shape_from_jordans([]) EmptyShape """ assert len(jordans) != 0 simples = tuple(map(SimpleShape, jordans)) if len(simples) == 1: return simples[0] connecteds = divide_connecteds(simples) if len(connecteds) == 1: return connecteds[0] return DisjointShape(connecteds)