diff --git a/src/shapepy/bool2d/__init__.py b/src/shapepy/bool2d/__init__.py index f156f890..4cef3f5d 100644 --- a/src/shapepy/bool2d/__init__.py +++ b/src/shapepy/bool2d/__init__.py @@ -3,8 +3,23 @@ operations between subsets """ +from ..tools import Is from .base import Future -from .boolean import intersect, unite +from .boolean import ( + clean_bool2d, + intersect_bool2d, + invert_bool2d, + unite_bool2d, + xor_bool2d, +) +from .convert import from_any +from .lazy import is_lazy -Future.unite = unite -Future.intersect = intersect +Future.invert = invert_bool2d +Future.unite = unite_bool2d +Future.intersect = intersect_bool2d +Future.clean = clean_bool2d +Future.convert = from_any +Future.xor = xor_bool2d + +Is.lazy = is_lazy diff --git a/src/shapepy/bool2d/base.py b/src/shapepy/bool2d/base.py index 53ca0feb..a8ed764a 100644 --- a/src/shapepy/bool2d/base.py +++ b/src/shapepy/bool2d/base.py @@ -9,13 +9,13 @@ from __future__ import annotations from abc import abstractmethod -from copy import copy from typing import Iterable, Tuple, Union from ..geometry.point import Point2D from ..loggers import debug from ..scalar.angle import Angle from ..scalar.reals import Real +from .config import Config from .density import Density @@ -27,19 +27,29 @@ class SubSetR2: def __init__(self): pass - @abstractmethod + @debug("shapepy.bool2d.base") def __invert__(self) -> SubSetR2: """Invert shape""" + result = Future.invert(self) + if Config.clean["inv"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") def __or__(self, other: SubSetR2) -> SubSetR2: """Union shapes""" - return Future.unite((self, other)) + result = Future.unite((self, other)) + if Config.clean["or"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") def __and__(self, other: SubSetR2) -> SubSetR2: """Intersection shapes""" - return Future.intersect((self, other)) + result = Future.intersect((self, other)) + if Config.clean["and"]: + result = Future.clean(result) + return result @abstractmethod def __copy__(self) -> SubSetR2: @@ -51,27 +61,42 @@ def __deepcopy__(self, memo) -> SubSetR2: def __neg__(self) -> SubSetR2: """Invert the SubSetR2""" - return ~self + result = Future.invert(self) + if Config.clean["neg"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") def __add__(self, other: SubSetR2): """Union of SubSetR2""" - return self | other + result = Future.unite((self, other)) + if Config.clean["add"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") - def __mul__(self, value: SubSetR2): + def __mul__(self, other: SubSetR2): """Intersection of SubSetR2""" - return self & value + result = Future.intersect((self, other)) + if Config.clean["mul"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") - def __sub__(self, value: SubSetR2): + def __sub__(self, other: SubSetR2): """Subtraction of SubSetR2""" - return self & (~value) + result = Future.intersect((self, Future.invert(other))) + if Config.clean["sub"]: + result = Future.clean(result) + return result @debug("shapepy.bool2d.base") def __xor__(self, other: SubSetR2): """XOR of SubSetR2""" - return (self - other) | (other - self) + result = Future.xor((self, other)) + if Config.clean["xor"]: + result = Future.clean(result) + return result def __repr__(self) -> str: # pragma: no cover return str(self) @@ -80,6 +105,23 @@ def __repr__(self) -> str: # pragma: no cover def __hash__(self): raise NotImplementedError + def clean(self) -> SubSetR2: + """ + Cleans the subset, changing its representation into a simpler form + + Parameters + ---------- + :return: The same instance + :rtype: SubSetR2 + + Example use + ----------- + >>> from shapepy import Primitive + >>> circle = Primitive.circle() + >>> circle.clean() + """ + return Future.clean(self) + @abstractmethod def move(self, vector: Point2D) -> SubSetR2: """ @@ -206,17 +248,29 @@ def __deepcopy__(self, memo) -> EmptyShape: return self def __or__(self, other: SubSetR2) -> SubSetR2: - return copy(other) + return Future.convert(other) - def __and__(self, other: SubSetR2) -> SubSetR2: + def __add__(self, other: SubSetR2) -> SubSetR2: + return Future.convert(other) + + def __and__(self, _: SubSetR2) -> SubSetR2: return self - def __sub__(self, other: SubSetR2) -> SubSetR2: + def __mul__(self, _: SubSetR2) -> SubSetR2: + return self + + def __sub__(self, _: SubSetR2) -> SubSetR2: return self + def __neg__(self) -> SubSetR2: + return WholeShape() + def __invert__(self) -> SubSetR2: return WholeShape() + def __xor__(self, other: SubSetR2) -> SubSetR2: + return Future.convert(other) + def __contains__(self, other: SubSetR2) -> bool: return self is other @@ -273,12 +327,24 @@ def __deepcopy__(self, memo) -> WholeShape: def __or__(self, other: SubSetR2) -> WholeShape: return self + def __add__(self, _: SubSetR2) -> WholeShape: + return self + def __and__(self, other: SubSetR2) -> SubSetR2: - return copy(other) + return Future.convert(other) + + def __mul__(self, other: SubSetR2) -> WholeShape: + return Future.convert(other) + + def __neg__(self) -> SubSetR2: + return EmptyShape() def __invert__(self) -> SubSetR2: return EmptyShape() + def __xor__(self, other: SubSetR2) -> SubSetR2: + return ~Future.convert(other) + def __contains__(self, other: SubSetR2) -> bool: return True @@ -286,7 +352,7 @@ def __str__(self) -> str: return "WholeShape" def __sub__(self, other: SubSetR2) -> SubSetR2: - return ~other + return ~Future.convert(other) def __bool__(self) -> bool: return True @@ -332,6 +398,13 @@ class Future: a circular import. """ + @staticmethod + def convert(subset: SubSetR2) -> SubSetR2: + """ + Converts an object into a SubSetR2 + """ + raise NotImplementedError + @staticmethod def unite(subsets: Iterable[SubSetR2]) -> SubSetR2: """ @@ -351,3 +424,33 @@ def intersect(subsets: Iterable[SubSetR2]) -> SubSetR2: in the `shapepy.bool2d.boolean.py` file """ raise NotImplementedError + + @staticmethod + def invert(subset: SubSetR2) -> SubSetR2: + """ + Computes the complementar set of given SubSetR2 instance + + This function is overrided by a function defined + in the `shapepy.bool2d.boolean.py` file + """ + raise NotImplementedError + + @staticmethod + def xor(subset: SubSetR2) -> SubSetR2: + """ + Computes the exclusive union of given subsets + + This function is overrided by a function defined + in the `shapepy.bool2d.boolean.py` file + """ + raise NotImplementedError + + @staticmethod + def clean(subset: SubSetR2) -> SubSetR2: + """ + Cleans and simplifies given subset + + This function is overrided by a function defined + in the `shapepy.bool2d.boolean.py` file + """ + raise NotImplementedError diff --git a/src/shapepy/bool2d/boolean.py b/src/shapepy/bool2d/boolean.py index d4f0bc36..4a6ea1d6 100644 --- a/src/shapepy/bool2d/boolean.py +++ b/src/shapepy/bool2d/boolean.py @@ -7,15 +7,17 @@ from copy import copy from fractions import Fraction -from typing import Iterable, Tuple, Union +from typing import Dict, Iterable, Tuple, Union from shapepy.geometry.jordancurve import JordanCurve from ..geometry.intersection import GeometricIntersectionCurves from ..geometry.unparam import USegment from ..loggers import debug -from ..tools import CyclicContainer, Is +from ..tools import CyclicContainer, Is, NotExpectedError +from . import boolalg from .base import EmptyShape, SubSetR2, WholeShape +from .lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy, is_lazy from .shape import ( ConnectedShape, DisjointShape, @@ -25,7 +27,25 @@ @debug("shapepy.bool2d.boolean") -def unite(subsets: Iterable[SubSetR2]) -> SubSetR2: +def invert_bool2d(subset: SubSetR2) -> SubSetR2: + """ + Computes the complementar set of given SubSetR2 instance + + Parameters + ---------- + subsets: SubSetR2 + The subset to be inverted + + Return + ------ + SubSetR2 + The complementar subset + """ + return Boolalg.clean(RecipeLazy.invert(subset)) + + +@debug("shapepy.bool2d.boolean") +def unite_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: """ Computes the union of given subsets @@ -39,26 +59,12 @@ def unite(subsets: Iterable[SubSetR2]) -> SubSetR2: SubSetR2 The united subset """ - subsets = tuple(subsets) - assert len(subsets) == 2 - assert Is.instance(subsets[0], SubSetR2) - assert Is.instance(subsets[1], SubSetR2) - if Is.instance(subsets[1], WholeShape): - return WholeShape() - if Is.instance(subsets[1], EmptyShape): - return copy(subsets[0]) - if subsets[1] in subsets[0]: - return copy(subsets[0]) - if subsets[0] in subsets[1]: - return copy(subsets[1]) - new_jordans = FollowPath.or_shapes(subsets[0], subsets[1]) - if len(new_jordans) == 0: - return WholeShape() - return shape_from_jordans(new_jordans) + union = RecipeLazy.unite(subsets) + return Boolalg.clean(union) @debug("shapepy.bool2d.boolean") -def intersect(subsets: Iterable[SubSetR2]) -> SubSetR2: +def intersect_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: """ Computes the intersection of given subsets @@ -72,22 +78,170 @@ def intersect(subsets: Iterable[SubSetR2]) -> SubSetR2: SubSetR2 The intersection subset """ - subsets = tuple(subsets) + intersection = RecipeLazy.intersect(subsets) + return Boolalg.clean(intersection) + + +@debug("shapepy.bool2d.boolean") +def xor_bool2d(subsets: Iterable[SubSetR2]) -> SubSetR2: + """ + Computes the xor of given subsets + + Parameters + ---------- + subsets: SubSetR2 + The subsets to compute the xor + + Return + ------ + SubSetR2 + The intersection subset + """ + subset = RecipeLazy.xor(subsets) + return Boolalg.clean(subset) + + +# pylint: disable=too-many-return-statements +@debug("shapepy.bool2d.boolean") +def clean_bool2d(subset: SubSetR2) -> SubSetR2: + """ + Computes the intersection of given subsets + + Parameters + ---------- + subset: SubSetR2 + The subset to be cleaned + + Return + ------ + SubSetR2 + The intersection subset + """ + subset = Boolalg.clean(subset) + if not Is.lazy(subset): + return subset + if Is.instance(subset, LazyNot): + return clean_bool2d_not(subset) + subsets = tuple(subset) assert len(subsets) == 2 - assert Is.instance(subsets[0], SubSetR2) - assert Is.instance(subsets[1], SubSetR2) - if Is.instance(subsets[1], WholeShape): - return copy(subsets[0]) - if Is.instance(subsets[1], EmptyShape): - return EmptyShape() - if subsets[1] in subsets[0]: - return copy(subsets[1]) - if subsets[0] in subsets[1]: - return copy(subsets[0]) - new_jordans = FollowPath.and_shapes(subsets[0], subsets[1]) - if len(new_jordans) == 0: - return EmptyShape() - return shape_from_jordans(new_jordans) + shapea, shapeb = subsets + shapea = clean_bool2d(shapea) + shapeb = clean_bool2d(shapeb) + if Is.instance(subset, LazyAnd): + if shapeb in shapea: + return copy(shapeb) + if shapea in shapeb: + return copy(shapea) + jordans = FollowPath.and_shapes(shapea, shapeb) + elif Is.instance(subset, LazyOr): + if shapeb in shapea: + return copy(shapea) + if shapea in shapeb: + return copy(shapeb) + jordans = FollowPath.or_shapes(shapea, shapeb) + if len(jordans) == 0: + return EmptyShape() if Is.instance(subset, LazyAnd) else WholeShape() + return shape_from_jordans(jordans) + + +@debug("shapepy.bool2d.boolean") +def clean_bool2d_not(subset: LazyNot) -> SubSetR2: + """ + Cleans complementar of given subset + + Parameters + ---------- + subset: SubSetR2 + The subset to be cleaned + + Return + ------ + SubSetR2 + The cleaned subset + """ + assert Is.instance(subset, LazyNot) + inverted = ~subset + if Is.instance(inverted, SimpleShape): + return SimpleShape(~inverted.jordan, True) + if Is.instance(inverted, ConnectedShape): + return DisjointShape(~simple for simple in inverted.subshapes) + if Is.instance(inverted, DisjointShape): + new_jordans = tuple(~jordan for jordan in inverted.jordans) + return shape_from_jordans(new_jordans) + raise NotImplementedError(f"Missing typo: {type(inverted)}") + + +class Boolalg: + """Static methods to clean a SubSetR2 using algebraic simplifier""" + + alphabet = "abcdefghijklmnop" + sub2var: Dict[SubSetR2, str] = {} + + @staticmethod + def clean(subset: SubSetR2) -> SubSetR2: + """Simplifies the subset""" + + if not Is.lazy(subset): + return subset + Boolalg.sub2var.clear() + original = Boolalg.subset2expression(subset) + simplified = boolalg.simplify(original) + if simplified != original: + subset = Boolalg.expression2subset(simplified) + Boolalg.sub2var.clear() + return subset + + @staticmethod + def get_variable(subset: SubSetR2) -> str: + """Gets the variable represeting the subset""" + if subset not in Boolalg.sub2var: + index = len(Boolalg.sub2var) + if index > len(Boolalg.alphabet): + raise ValueError("Too many variables") + Boolalg.sub2var[subset] = Boolalg.alphabet[index] + return Boolalg.sub2var[subset] + + @staticmethod + def subset2expression(subset: SubSetR2) -> str: + """Converts a SubSetR2 into a boolean expression""" + if not is_lazy(subset): + if Is.instance(subset, (EmptyShape, WholeShape)): + raise NotExpectedError("Lazy does not contain these") + return Boolalg.get_variable(subset) + if Is.instance(subset, LazyNot): + return boolalg.Formatter.invert_str( + Boolalg.subset2expression(~subset) + ) + internals = map(Boolalg.subset2expression, subset) + if Is.instance(subset, LazyAnd): + return boolalg.Formatter.mult_strs(internals, boolalg.AND) + if Is.instance(subset, LazyOr): + return boolalg.Formatter.mult_strs(internals, boolalg.OR) + raise NotExpectedError + + @staticmethod + def expression2subset(expression: str) -> SubSetR2: + """Converts a boolean expression into a SubSetR2""" + if expression == boolalg.TRUE: + return WholeShape() + if expression == boolalg.FALSE: + return EmptyShape() + for subset, variable in Boolalg.sub2var.items(): + if expression == variable: + return subset + expression = boolalg.remove_parentesis(expression) + operator = boolalg.find_operator(expression) + if operator == boolalg.NOT: + subexpr = boolalg.extract(expression, boolalg.NOT) + inverted = Boolalg.expression2subset(subexpr) + return RecipeLazy.invert(inverted) + subexprs = boolalg.extract(expression, operator) + subsets = map(Boolalg.expression2subset, subexprs) + if operator == boolalg.OR: + return RecipeLazy.unite(subsets) + if operator == boolalg.AND: + return RecipeLazy.intersect(subsets) + raise NotExpectedError(f"Invalid expression: {expression}") class FollowPath: @@ -254,8 +408,12 @@ def or_shapes(shapea: SubSetR2, shapeb: SubSetR2) -> Tuple[JordanCurve]: Computes the set of jordan curves that defines the boundary of the union between the two base shapes """ - assert Is.instance(shapea, SubSetR2) - assert Is.instance(shapeb, SubSetR2) + assert Is.instance( + shapea, (SimpleShape, ConnectedShape, DisjointShape) + ) + assert Is.instance( + shapeb, (SimpleShape, ConnectedShape, DisjointShape) + ) FollowPath.split_on_intersection([shapea.jordans, shapeb.jordans]) indexs = FollowPath.midpoints_shapes( shapea, shapeb, closed=True, inside=False @@ -270,8 +428,12 @@ def and_shapes(shapea: SubSetR2, shapeb: SubSetR2) -> Tuple[JordanCurve]: Computes the set of jordan curves that defines the boundary of the intersection between the two base shapes """ - assert Is.instance(shapea, SubSetR2) - assert Is.instance(shapeb, SubSetR2) + assert Is.instance( + shapea, (SimpleShape, ConnectedShape, DisjointShape) + ) + assert Is.instance( + shapeb, (SimpleShape, ConnectedShape, DisjointShape) + ) FollowPath.split_on_intersection([shapea.jordans, shapeb.jordans]) indexs = FollowPath.midpoints_shapes( shapea, shapeb, closed=False, inside=True diff --git a/src/shapepy/bool2d/config.py b/src/shapepy/bool2d/config.py new file mode 100644 index 00000000..4a05ec24 --- /dev/null +++ b/src/shapepy/bool2d/config.py @@ -0,0 +1,32 @@ +"""Configuration file for the bool2d package""" + +from contextlib import contextmanager + + +# pylint: disable=too-few-public-methods +class Config: + """Static class that contains flags""" + + clean = { + "add": True, + "or": True, + "xor": True, + "and": True, + "sub": True, + "mul": True, + "neg": True, + "inv": True, + } + + +@contextmanager +def disable_auto_clean(): + """Function that disables temporarily the auto clean""" + old = Config.clean.copy() + for key in Config.clean: + Config.clean[key] = False + try: + yield + finally: + for key in Config.clean: + Config.clean[key] = old[key] diff --git a/src/shapepy/bool2d/convert.py b/src/shapepy/bool2d/convert.py new file mode 100644 index 00000000..61b5634d --- /dev/null +++ b/src/shapepy/bool2d/convert.py @@ -0,0 +1,15 @@ +""" +Functions to convert from typical data types into SubSetR2 +""" + +from ..tools import Is +from .base import SubSetR2 + + +def from_any(subset: SubSetR2) -> SubSetR2: + """ + Converts an object into a SubSetR2 + """ + if not Is.instance(subset, SubSetR2): + raise TypeError + return subset diff --git a/src/shapepy/bool2d/density.py b/src/shapepy/bool2d/density.py index 2a52a47e..54b19dc9 100644 --- a/src/shapepy/bool2d/density.py +++ b/src/shapepy/bool2d/density.py @@ -115,6 +115,9 @@ def __float__(self) -> float: value = subset_length(self.subset) return float(value) + def __invert__(self) -> Density: + return Density(create_interval(0, 1) - self.subset) + def __or__(self, value: Density): if not Is.instance(value, Density): raise TypeError diff --git a/src/shapepy/bool2d/lazy.py b/src/shapepy/bool2d/lazy.py new file mode 100644 index 00000000..0dd78355 --- /dev/null +++ b/src/shapepy/bool2d/lazy.py @@ -0,0 +1,260 @@ +"""Defines containers, or also named Lazy Evaluators""" + +from __future__ import annotations + +from collections import Counter +from copy import deepcopy +from typing import Iterable, Iterator, Type + +from ..loggers import debug +from ..tools import Is +from .base import EmptyShape, SubSetR2, WholeShape +from .density import intersect_densities, unite_densities + + +class RecipeLazy: + """Contains static methods that gives lazy recipes""" + + @staticmethod + def flatten(subsets: Iterable[SubSetR2], typo: Type) -> Iterator[SubSetR2]: + """Flattens the subsets""" + for subset in subsets: + if Is.instance(subset, typo): + yield from subset + else: + yield subset + + @staticmethod + @debug("shapepy.bool2d.lazy") + def invert(subset: SubSetR2) -> SubSetR2: + """Gives the complementar of the given subset""" + if Is.instance(subset, (EmptyShape, WholeShape, LazyNot)): + return -subset + return LazyNot(subset) + + @staticmethod + @debug("shapepy.bool2d.lazy") + def intersect(subsets: Iterable[SubSetR2]) -> SubSetR2: + """Gives the recipe for the intersection of given subsets""" + subsets = RecipeLazy.flatten(subsets, LazyAnd) + subsets = frozenset( + s for s in subsets if not Is.instance(s, WholeShape) + ) + if len(subsets) == 0: + return WholeShape() + if any(Is.instance(s, EmptyShape) for s in subsets): + return EmptyShape() + if len(subsets) == 1: + return tuple(subsets)[0] + return LazyAnd(subsets) + + @staticmethod + @debug("shapepy.bool2d.contain") + def unite(subsets: Iterable[SubSetR2]) -> SubSetR2: + """Gives the recipe for the union of given subsets""" + subsets = RecipeLazy.flatten(subsets, LazyOr) + subsets = frozenset( + s for s in subsets if not Is.instance(s, EmptyShape) + ) + if len(subsets) == 0: + return EmptyShape() + if any(Is.instance(s, WholeShape) for s in subsets): + return WholeShape() + if len(subsets) == 1: + return tuple(subsets)[0] + return LazyOr(subsets) + + @staticmethod + @debug("shapepy.bool2d.contain") + def xor(subsets: Iterable[SubSetR2]) -> SubSetR2: + """Gives the exclusive or of the given subsets""" + subsets = tuple(subsets) + dictids = dict(Counter(map(id, subsets))) + subsets = tuple(s for s in subsets if dictids[id(s)] % 2) + length = len(subsets) + if length == 0: + return EmptyShape() + if length == 1: + return subsets[0] + mid = length // 2 + aset = RecipeLazy.xor(subsets[:mid]) + bset = RecipeLazy.xor(subsets[mid:]) + left = RecipeLazy.intersect((aset, RecipeLazy.invert(bset))) + righ = RecipeLazy.intersect((RecipeLazy.invert(aset), bset)) + return RecipeLazy.unite((left, righ)) + + +class LazyNot(SubSetR2): + """A Lazy evaluator that stores the complementar of given subset""" + + def __init__(self, subset: SubSetR2): + if not Is.instance(subset, SubSetR2): + raise TypeError(f"Invalid typo: {type(subset)}: {subset}") + if Is.instance(subset, LazyNot): + raise TypeError("Subset cannot be LazyNot") + self.__internal = subset + + @debug("shapepy.bool2d.lazy") + def __hash__(self): + return -hash(self.__internal) + + def __str__(self): + return f"NOT[{str(self.__internal)}]" + + def __repr__(self): + return f"NOT[{repr(self.__internal)}]" + + def __invert__(self): + return self.__internal + + def __neg__(self): + return self.__internal + + def __copy__(self): + return LazyNot(self.__internal) + + def __deepcopy__(self, memo): + return LazyNot(deepcopy(self.__internal)) + + def __eq__(self, other): + return ( + Is.instance(other, LazyNot) + and hash(self) == hash(other) + and (~self == ~other) + ) + + def move(self, vector): + self.__internal.move(vector) + return self + + def scale(self, amount): + self.__internal.scale(amount) + return self + + def rotate(self, angle): + self.__internal.rotate(angle) + return self + + def density(self, center): + return ~self.__internal.density(center) + + +class LazyOr(SubSetR2): + """A Lazy evaluator that stores the union of given subsets""" + + def __init__(self, subsets: Iterable[SubSetR2]): + subsets = (s for s in subsets if not Is.instance(s, EmptyShape)) + subsets = frozenset(subsets) + if any(Is.instance(s, LazyOr) for s in subsets): + raise TypeError + if any(Is.instance(s, WholeShape) for s in subsets): + subsets = frozenset() + self.__subsets = subsets + + def __iter__(self) -> Iterator[SubSetR2]: + yield from self.__subsets + + def __str__(self): + return f"OR[{', '.join(map(str, self))}]" + + def __repr__(self): + return f"OR[{', '.join(map(repr, self))}]" + + @debug("shapepy.bool2d.lazy") + def __hash__(self): + return hash(tuple(map(hash, self.__subsets))) + + def __copy__(self): + return LazyOr(self.__subsets) + + def __deepcopy__(self, memo): + return LazyOr(map(deepcopy, self)) + + def __eq__(self, other): + return ( + Is.instance(other, LazyOr) + and hash(self) == hash(other) + and frozenset(self) == frozenset(other) + ) + + def move(self, vector): + for subset in self: + subset.move(vector) + return self + + def scale(self, amount): + for subset in self: + subset.scale(amount) + return self + + def rotate(self, angle): + for subset in self: + subset.rotate(angle) + return self + + def density(self, center): + densities = (sub.density(center) for sub in self) + return unite_densities(tuple(densities)) + + +class LazyAnd(SubSetR2): + """A Lazy evaluator that stores the union of given subsets""" + + def __init__(self, subsets: Iterable[SubSetR2]): + subsets = (s for s in subsets if not Is.instance(s, EmptyShape)) + subsets = frozenset(subsets) + if any(Is.instance(s, LazyAnd) for s in subsets): + raise TypeError + if any(Is.instance(s, WholeShape) for s in subsets): + subsets = frozenset() + self.__subsets = subsets + + def __iter__(self) -> Iterator[SubSetR2]: + yield from self.__subsets + + def __str__(self): + return f"AND[{', '.join(map(str, self))}]" + + def __repr__(self): + return f"AND[{', '.join(map(repr, self))}]" + + @debug("shapepy.bool2d.lazy") + def __hash__(self): + return -hash(tuple(-hash(sub) for sub in self)) + + def __copy__(self): + return LazyAnd(self.__subsets) + + def __deepcopy__(self, memo): + return LazyAnd(map(deepcopy, self)) + + def __eq__(self, other): + return ( + Is.instance(other, LazyAnd) + and hash(self) == hash(other) + and frozenset(self) == frozenset(other) + ) + + def move(self, vector): + for subset in self: + subset.move(vector) + return self + + def scale(self, amount): + for subset in self: + subset.scale(amount) + return self + + def rotate(self, angle): + for subset in self: + subset.rotate(angle) + return self + + def density(self, center): + densities = (sub.density(center) for sub in self) + return intersect_densities(tuple(densities)) + + +def is_lazy(subset: SubSetR2) -> bool: + """Tells if the given subset is a Lazy evaluated instance""" + return Is.instance(subset, (LazyAnd, LazyNot, LazyOr)) diff --git a/src/shapepy/bool2d/shape.py b/src/shapepy/bool2d/shape.py index d750c40e..b257164a 100644 --- a/src/shapepy/bool2d/shape.py +++ b/src/shapepy/bool2d/shape.py @@ -15,6 +15,7 @@ from ..geometry.box import Box from ..geometry.jordancurve import JordanCurve from ..geometry.point import Point2D +from ..loggers import debug from ..scalar.angle import Angle from ..scalar.reals import Real from ..tools import Is, To @@ -71,9 +72,6 @@ def __eq__(self, other: SubSetR2) -> bool: and self.jordan == other.jordan ) - def __invert__(self) -> SimpleShape: - return self.__class__(~self.jordan) - @property def boundary(self) -> bool: """The flag that informs if the boundary is inside the Shape""" @@ -98,6 +96,7 @@ 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) @@ -253,9 +252,7 @@ def __eq__(self, other: SubSetR2) -> bool: and self.subshapes == other.subshapes ) - def __invert__(self) -> DisjointShape: - return DisjointShape(~simple for simple in self.subshapes) - + @debug("shapepy.bool2d.shape") def __hash__(self): return hash(self.area) @@ -374,10 +371,6 @@ 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) - def __contains__(self, other: SubSetR2) -> bool: if Is.instance(other, DisjointShape): return all(o in self for o in other.subshapes) @@ -414,6 +407,7 @@ def __str__(self) -> str: # pragma: no cover # For debug msg += f"{len(self.subshapes)} subshapes" return msg + @debug("shapepy.bool2d.shape") def __hash__(self): return hash(self.area) diff --git a/src/shapepy/loggers.py b/src/shapepy/loggers.py index a920ba72..52c20eb0 100644 --- a/src/shapepy/loggers.py +++ b/src/shapepy/loggers.py @@ -122,7 +122,7 @@ def indent(): @contextmanager -def enable_logger(base: str, /, *, level: logging._Level): +def enable_logger(base: str, /, *, level: logging._Level = "DEBUG"): """Enables temporarily the given logger""" current_enable = LogConfiguration.log_enabled current_levels = {} diff --git a/tests/bool2d/test_bool_finite_intersect.py b/tests/bool2d/test_bool_finite_intersect.py index 6a38bc17..1ee0af76 100644 --- a/tests/bool2d/test_bool_finite_intersect.py +++ b/tests/bool2d/test_bool_finite_intersect.py @@ -10,12 +10,13 @@ from shapepy.geometry.factory import FactoryJordan -@pytest.mark.order(32) +@pytest.mark.order(42) @pytest.mark.dependency( depends=[ "tests/bool2d/test_primitive.py::test_end", "tests/bool2d/test_contains.py::test_end", "tests/bool2d/test_empty_whole.py::test_end", + "tests/bool2d/test_lazy.py::test_all", "tests/bool2d/test_bool_no_intersect.py::test_end", ], scope="session", @@ -30,12 +31,12 @@ class TestIntersectionSimple: of intersection points """ - @pytest.mark.order(32) + @pytest.mark.order(42) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(32) + @pytest.mark.order(42) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestIntersectionSimple::test_begin"]) def test_or_two_rombos(self): @@ -52,7 +53,7 @@ def test_or_two_rombos(self): test_shape = square0 | square1 assert test_shape == good_shape - @pytest.mark.order(32) + @pytest.mark.order(42) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestIntersectionSimple::test_begin"]) def test_and_two_rombos(self): @@ -63,7 +64,7 @@ def test_and_two_rombos(self): good = Primitive.regular_polygon(nsides=4, radius=1, center=(0, 0)) assert test == good - @pytest.mark.order(32) + @pytest.mark.order(42) @pytest.mark.timeout(40) @pytest.mark.dependency( depends=[ @@ -85,7 +86,7 @@ def test_sub_two_rombos(self): assert square0 - square1 == left_shape assert square1 - square0 == right_shape - @pytest.mark.order(32) + @pytest.mark.order(42) @pytest.mark.dependency( depends=[ "TestIntersectionSimple::test_begin", @@ -98,7 +99,7 @@ def test_end(self): pass -@pytest.mark.order(32) +@pytest.mark.order(42) @pytest.mark.dependency( depends=[ "TestIntersectionSimple::test_end", diff --git a/tests/bool2d/test_bool_infinite_intersect.py b/tests/bool2d/test_bool_infinite_intersect.py deleted file mode 100644 index d834c099..00000000 --- a/tests/bool2d/test_bool_infinite_intersect.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -This module tests when two shapes have common edges/segments -""" - -import pytest - -from shapepy.bool2d.primitive import Primitive - - -@pytest.mark.order(33) -@pytest.mark.dependency( - depends=[ - "tests/bool2d/test_primitive.py::test_end", - "tests/bool2d/test_contains.py::test_end", - "tests/bool2d/test_empty_whole.py::test_end", - "tests/bool2d/test_shape.py::test_end", - "tests/bool2d/test_bool_no_intersect.py::test_end", - "tests/bool2d/test_bool_finite_intersect.py::test_end", - ], - scope="session", -) -def test_begin(): - pass - - -class TestTriangle: - @pytest.mark.order(33) - @pytest.mark.dependency( - depends=[ - "test_begin", - ] - ) - def test_begin(self): - pass - - @pytest.mark.order(33) - @pytest.mark.timeout(40) - @pytest.mark.dependency(depends=["TestTriangle::test_begin"]) - def test_or_triangles(self): - vertices0 = [(0, 0), (1, 0), (0, 1)] - vertices1 = [(0, 0), (0, 1), (-1, 0)] - triangle0 = Primitive.polygon(vertices0) - triangle1 = Primitive.polygon(vertices1) - test = triangle0 | triangle1 - - vertices = [(1, 0), (0, 1), (-1, 0)] - good = Primitive.polygon(vertices) - assert test == good - - @pytest.mark.order(33) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestTriangle::test_begin", - "TestTriangle::test_or_triangles", - ] - ) - def test_and_triangles(self): - vertices0 = [(0, 0), (2, 0), (0, 2)] - vertices1 = [(0, 0), (1, 0), (0, 1)] - triangle0 = Primitive.polygon(vertices0) - triangle1 = Primitive.polygon(vertices1) - test = triangle0 & triangle1 - - vertices = [(0, 0), (1, 0), (0, 1)] - good = Primitive.polygon(vertices) - assert test == good - - @pytest.mark.order(33) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestTriangle::test_begin", - "TestTriangle::test_or_triangles", - "TestTriangle::test_and_triangles", - ] - ) - def test_sub_triangles(self): - vertices0 = [(0, 0), (2, 0), (0, 2)] - vertices1 = [(0, 0), (1, 0), (0, 1)] - triangle0 = Primitive.polygon(vertices0) - triangle1 = Primitive.polygon(vertices1) - test = triangle0 - triangle1 - - vertices = [(1, 0), (2, 0), (0, 2), (0, 1)] - good = Primitive.polygon(vertices) - - assert test == good - - @pytest.mark.order(33) - @pytest.mark.dependency( - depends=[ - "TestTriangle::test_begin", - "TestTriangle::test_or_triangles", - "TestTriangle::test_and_triangles", - "TestTriangle::test_sub_triangles", - ] - ) - def test_end(self): - pass - - -@pytest.mark.order(33) -@pytest.mark.dependency( - depends=[ - "TestTriangle::test_end", - ] -) -def test_end(): - pass diff --git a/tests/bool2d/test_bool_no_intersect.py b/tests/bool2d/test_bool_no_intersect.py index 464cd83d..5673fe30 100644 --- a/tests/bool2d/test_bool_no_intersect.py +++ b/tests/bool2d/test_bool_no_intersect.py @@ -10,12 +10,13 @@ from shapepy.bool2d.shape import ConnectedShape, DisjointShape -@pytest.mark.order(31) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "tests/bool2d/test_primitive.py::test_end", "tests/bool2d/test_contains.py::test_end", "tests/bool2d/test_empty_whole.py::test_end", + "tests/bool2d/test_lazy.py::test_all", ], scope="session", ) @@ -23,88 +24,6 @@ def test_begin(): pass -class TestEqualSquare: - """ - Make tests of boolean operations between the same shape (a square) - """ - - @pytest.mark.order(31) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency(depends=["TestEqualSquare::test_begin"]) - def test_or(self): - square = Primitive.square(side=1, center=(0, 0)) - assert square.area > 0 - assert square | square == square - assert square | (~square) is WholeShape() - assert (~square) | square is WholeShape() - assert (~square) | (~square) == ~square - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=["TestEqualSquare::test_begin", "TestEqualSquare::test_or"] - ) - def test_and(self): - square = Primitive.square(side=1, center=(0, 0)) - assert square.area > 0 - assert square & square == square - assert square & (~square) is EmptyShape() - assert (~square) & square is EmptyShape() - assert (~square) & (~square) == ~square - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestEqualSquare::test_begin", - "TestEqualSquare::test_and", - ] - ) - def test_sub(self): - square = Primitive.square(side=1, center=(0, 0)) - assert square.area > 0 - assert square - square is EmptyShape() - assert square - (~square) == square - assert (~square) - square == ~square - assert (~square) - (~square) is EmptyShape() - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestEqualSquare::test_begin", - "TestEqualSquare::test_or", - "TestEqualSquare::test_and", - "TestEqualSquare::test_sub", - ] - ) - def test_xor(self): - square = Primitive.square(side=1, center=(0, 0)) - assert square.area > 0 - assert square ^ square is EmptyShape() - assert square ^ (~square) is WholeShape() - assert (~square) ^ square is WholeShape() - assert (~square) ^ (~square) is EmptyShape() - - @pytest.mark.order(31) - @pytest.mark.dependency( - depends=[ - "TestEqualSquare::test_begin", - "TestEqualSquare::test_or", - "TestEqualSquare::test_and", - "TestEqualSquare::test_sub", - "TestEqualSquare::test_xor", - ] - ) - def test_end(self): - pass - - class TestTwoCenteredSquares: """ Make tests when a shape is completely inside another shape @@ -113,14 +32,12 @@ class TestTwoCenteredSquares: which is tested in other file """ - @pytest.mark.order(31) - @pytest.mark.dependency( - depends=["test_begin", "TestEqualSquare::test_end"] - ) + @pytest.mark.order(41) + @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoCenteredSquares::test_begin"]) def test_or(self): @@ -138,7 +55,7 @@ def test_or(self): assert (~square1) | (~square2) == ~square1 assert (~square2) | (~square1) == ~square1 - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoCenteredSquares::test_begin"]) def test_and(self): @@ -156,7 +73,7 @@ def test_and(self): assert (~square1) & (~square2) == ~square2 assert (~square2) & (~square1) == ~square2 - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoCenteredSquares::test_begin"]) def test_sub(self): @@ -174,7 +91,7 @@ def test_sub(self): assert (~square1) - (~square2) == ConnectedShape([square2, ~square1]) assert (~square2) - (~square1) is EmptyShape() - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoCenteredSquares::test_begin"]) def test_xor(self): @@ -192,14 +109,12 @@ def test_xor(self): assert (~square1) ^ (~square2) == ConnectedShape([square2, ~square1]) assert (~square2) ^ (~square1) == ConnectedShape([square2, ~square1]) - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestTwoCenteredSquares::test_begin", "TestTwoCenteredSquares::test_or", "TestTwoCenteredSquares::test_and", - "TestTwoCenteredSquares::test_sub", - "TestTwoCenteredSquares::test_xor", ] ) def test_end(self): @@ -214,17 +129,16 @@ class TestTwoDisjointSquares: which is tested in other file """ - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "test_begin", - "TestEqualSquare::test_end", ] ) def test_begin(self): pass - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjointSquares::test_begin"]) def test_or(self): @@ -242,7 +156,7 @@ def test_or(self): assert (~left) | (~right) is WholeShape() assert (~right) | (~left) is WholeShape() - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjointSquares::test_begin"]) def test_and(self): @@ -260,7 +174,7 @@ def test_and(self): assert (~left) & (~right) == ConnectedShape([~left, ~right]) assert (~right) & (~left) == ConnectedShape([~left, ~right]) - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjointSquares::test_begin"]) def test_sub(self): @@ -278,7 +192,7 @@ def test_sub(self): assert (~left) - (~right) == right assert (~right) - (~left) == left - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjointSquares::test_begin"]) def test_xor(self): @@ -296,107 +210,12 @@ def test_xor(self): assert (~left) ^ (~right) == DisjointShape([left, right]) assert (~right) ^ (~left) == DisjointShape([left, right]) - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestTwoDisjointSquares::test_begin", "TestTwoDisjointSquares::test_or", "TestTwoDisjointSquares::test_and", - "TestTwoDisjointSquares::test_sub", - "TestTwoDisjointSquares::test_xor", - ] - ) - def test_end(self): - pass - - -class TestEqualHollowSquare: - """ - Make tests of boolean operations between the same shape (a square) - """ - - @pytest.mark.order(31) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency(depends=["TestEqualHollowSquare::test_begin"]) - def test_or(self): - big = Primitive.square(side=2, center=(0, 0)) - small = Primitive.square(side=1, center=(0, 0)) - square = big - small - assert square.area > 0 - assert square | square == square - assert square | (~square) is WholeShape() - assert (~square) | square is WholeShape() - assert (~square) | (~square) == ~square - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestEqualHollowSquare::test_begin", - "TestEqualHollowSquare::test_or", - ] - ) - def test_and(self): - big = Primitive.square(side=2, center=(0, 0)) - small = Primitive.square(side=1, center=(0, 0)) - square = big - small - assert square.area > 0 - assert square & square == square - assert square & (~square) is EmptyShape() - assert (~square) & square is EmptyShape() - assert (~square) & (~square) == ~square - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestEqualHollowSquare::test_begin", - "TestEqualHollowSquare::test_and", - ] - ) - def test_sub(self): - big = Primitive.square(side=2, center=(0, 0)) - small = Primitive.square(side=1, center=(0, 0)) - square = big - small - assert square.area > 0 - assert square - square is EmptyShape() - assert square - (~square) == square - assert (~square) - square == ~square - assert (~square) - (~square) is EmptyShape() - - @pytest.mark.order(31) - @pytest.mark.timeout(40) - @pytest.mark.dependency( - depends=[ - "TestEqualHollowSquare::test_begin", - "TestEqualHollowSquare::test_or", - "TestEqualHollowSquare::test_and", - "TestEqualHollowSquare::test_sub", - ] - ) - def test_xor(self): - big = Primitive.square(side=2, center=(0, 0)) - small = Primitive.square(side=1, center=(0, 0)) - square = big - small - assert square.area > 0 - assert square ^ square is EmptyShape() - assert square ^ (~square) is WholeShape() - assert (~square) ^ square is WholeShape() - assert (~square) ^ (~square) is EmptyShape() - - @pytest.mark.order(31) - @pytest.mark.dependency( - depends=[ - "TestEqualHollowSquare::test_begin", - "TestEqualHollowSquare::test_or", - "TestEqualHollowSquare::test_and", - "TestEqualHollowSquare::test_sub", - "TestEqualHollowSquare::test_xor", ] ) def test_end(self): @@ -411,20 +230,18 @@ class TestTwoDisjHollowSquares: which is tested in other file """ - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "test_begin", - "TestEqualSquare::test_end", "TestTwoCenteredSquares::test_end", "TestTwoDisjointSquares::test_end", - "TestEqualHollowSquare::test_end", ] ) def test_begin(self): pass - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjHollowSquares::test_begin"]) def test_or(self): @@ -446,7 +263,7 @@ def test_or(self): assert (~left) | (~right) is WholeShape() assert (~right) | (~left) is WholeShape() - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjHollowSquares::test_begin"]) def test_and(self): @@ -470,7 +287,7 @@ def test_and(self): assert (~left) & (~right) == good assert (~right) & (~left) == good - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjHollowSquares::test_begin"]) def test_sub(self): @@ -494,7 +311,7 @@ def test_sub(self): assert (~left) - (~right) == right assert (~right) - (~left) == left - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.timeout(40) @pytest.mark.dependency(depends=["TestTwoDisjHollowSquares::test_begin"]) def test_xor(self): @@ -518,27 +335,23 @@ def test_xor(self): assert (~left) ^ (~right) == DisjointShape([left, right]) assert (~right) ^ (~left) == DisjointShape([left, right]) - @pytest.mark.order(31) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestTwoDisjHollowSquares::test_begin", "TestTwoDisjHollowSquares::test_or", "TestTwoDisjHollowSquares::test_and", - "TestTwoDisjHollowSquares::test_sub", - "TestTwoDisjHollowSquares::test_xor", ] ) def test_end(self): pass -@pytest.mark.order(31) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "TestEqualSquare::test_end", "TestTwoCenteredSquares::test_end", "TestTwoDisjointSquares::test_end", - "TestEqualHollowSquare::test_end", "TestTwoDisjHollowSquares::test_end", ] ) diff --git a/tests/bool2d/test_bool_overlap.py b/tests/bool2d/test_bool_overlap.py new file mode 100644 index 00000000..68969547 --- /dev/null +++ b/tests/bool2d/test_bool_overlap.py @@ -0,0 +1,404 @@ +""" +This module tests when two shapes have common edges/segments +""" + +import pytest + +from shapepy.bool2d.base import EmptyShape, WholeShape +from shapepy.bool2d.config import disable_auto_clean +from shapepy.bool2d.primitive import Primitive + + +@pytest.mark.order(43) +@pytest.mark.dependency( + depends=[ + "tests/bool2d/test_primitive.py::test_end", + "tests/bool2d/test_contains.py::test_end", + "tests/bool2d/test_empty_whole.py::test_end", + "tests/bool2d/test_shape.py::test_end", + "tests/bool2d/test_lazy.py::test_all", + "tests/bool2d/test_bool_no_intersect.py::test_end", + "tests/bool2d/test_bool_finite_intersect.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +class TestEqualSquare: + """ + Make tests of boolean operations between the same shape (a square) + """ + + @pytest.mark.order(43) + @pytest.mark.dependency(depends=["test_begin"]) + def test_begin(self): + pass + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency(depends=["TestEqualSquare::test_begin"]) + def test_or(self): + square = Primitive.square(side=1, center=(0, 0)) + assert square.area > 0 + assert square | square == square + assert square | (~square) is WholeShape() + assert (~square) | square is WholeShape() + assert (~square) | (~square) == ~square + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=["TestEqualSquare::test_begin", "TestEqualSquare::test_or"] + ) + def test_and(self): + square = Primitive.square(side=1, center=(0, 0)) + assert square.area > 0 + assert square & square == square + assert square & (~square) is EmptyShape() + assert (~square) & square is EmptyShape() + assert (~square) & (~square) == ~square + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestEqualSquare::test_begin", + "TestEqualSquare::test_and", + ] + ) + def test_sub(self): + square = Primitive.square(side=1, center=(0, 0)) + assert square.area > 0 + assert square - square is EmptyShape() + assert square - (~square) == square + assert (~square) - square == ~square + assert (~square) - (~square) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestEqualSquare::test_begin", + "TestEqualSquare::test_or", + "TestEqualSquare::test_and", + "TestEqualSquare::test_sub", + ] + ) + def test_xor(self): + square = Primitive.square(side=1, center=(0, 0)) + assert square.area > 0 + assert square ^ square is EmptyShape() + assert square ^ (~square) is WholeShape() + assert (~square) ^ square is WholeShape() + assert (~square) ^ (~square) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "TestEqualSquare::test_begin", + "TestEqualSquare::test_or", + "TestEqualSquare::test_and", + "TestEqualSquare::test_sub", + "TestEqualSquare::test_xor", + ] + ) + def test_end(self): + pass + + +class TestEqualHollowSquare: + """ + Make tests of boolean operations between the same shape (a square) + """ + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "test_begin", + "TestEqualSquare::test_end", + ] + ) + def test_begin(self): + pass + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency(depends=["TestEqualHollowSquare::test_begin"]) + def test_or(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + square = big - small + assert square.area > 0 + assert square | square == square + assert square | (~square) is WholeShape() + assert (~square) | square is WholeShape() + assert (~square) | (~square) == ~square + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestEqualHollowSquare::test_begin", + "TestEqualHollowSquare::test_or", + ] + ) + def test_and(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + square = big - small + assert square.area > 0 + assert square & square == square + assert square & (~square) is EmptyShape() + assert (~square) & square is EmptyShape() + assert (~square) & (~square) == ~square + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestEqualHollowSquare::test_begin", + "TestEqualHollowSquare::test_and", + ] + ) + def test_sub(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + square = big - small + assert square.area > 0 + assert square - square is EmptyShape() + assert square - (~square) == square + assert (~square) - square == ~square + assert (~square) - (~square) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestEqualHollowSquare::test_begin", + "TestEqualHollowSquare::test_or", + "TestEqualHollowSquare::test_and", + "TestEqualHollowSquare::test_sub", + ] + ) + def test_xor(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + square = big - small + assert square.area > 0 + assert square ^ square is EmptyShape() + assert square ^ (~square) is WholeShape() + assert (~square) ^ square is WholeShape() + assert (~square) ^ (~square) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "TestEqualHollowSquare::test_begin", + "TestEqualHollowSquare::test_or", + "TestEqualHollowSquare::test_and", + "TestEqualHollowSquare::test_sub", + "TestEqualHollowSquare::test_xor", + ] + ) + def test_end(self): + pass + + +class TestTriangle: + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "test_begin", + ] + ) + def test_begin(self): + pass + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency(depends=["TestTriangle::test_begin"]) + def test_or_triangles(self): + vertices0 = [(0, 0), (1, 0), (0, 1)] + vertices1 = [(0, 0), (0, 1), (-1, 0)] + triangle0 = Primitive.polygon(vertices0) + triangle1 = Primitive.polygon(vertices1) + test = triangle0 | triangle1 + + vertices = [(1, 0), (0, 1), (-1, 0)] + good = Primitive.polygon(vertices) + assert test == good + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestTriangle::test_begin", + "TestTriangle::test_or_triangles", + ] + ) + def test_and_triangles(self): + vertices0 = [(0, 0), (2, 0), (0, 2)] + vertices1 = [(0, 0), (1, 0), (0, 1)] + triangle0 = Primitive.polygon(vertices0) + triangle1 = Primitive.polygon(vertices1) + test = triangle0 & triangle1 + + vertices = [(0, 0), (1, 0), (0, 1)] + good = Primitive.polygon(vertices) + assert test == good + + @pytest.mark.order(43) + @pytest.mark.skip(reason="Fails due to float precision on py3.11") + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestTriangle::test_begin", + "TestTriangle::test_or_triangles", + "TestTriangle::test_and_triangles", + ] + ) + def test_sub_triangles(self): + vertices0 = [(0, 0), (2, 0), (0, 2)] + vertices1 = [(0, 0), (1, 0), (0, 1)] + triangle0 = Primitive.polygon(vertices0) + triangle1 = Primitive.polygon(vertices1) + test = triangle0 - triangle1 + + vertices = [(1, 0), (2, 0), (0, 2), (0, 1)] + good = Primitive.polygon(vertices) + + assert test == good + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "TestTriangle::test_begin", + "TestTriangle::test_or_triangles", + "TestTriangle::test_and_triangles", + ] + ) + def test_end(self): + pass + + +class TestDisabledClean: + """ + Make tests of boolean operations between the same shape (a square) + """ + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "test_begin", + "TestEqualSquare::test_end", + "TestEqualHollowSquare::test_end", + "TestTriangle::test_end", + ] + ) + def test_begin(self): + pass + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency(depends=["TestDisabledClean::test_begin"]) + def test_or(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + left = Primitive.circle(radius=3, center=(-10, 0)) + right = Primitive.circle(radius=3, center=(10, 0)) + with disable_auto_clean(): + shape = big - small | left ^ right + assert shape | shape == shape + assert shape | (~shape) is WholeShape() + assert (~shape) | shape is WholeShape() + assert (~shape) | (~shape) == ~shape + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestDisabledClean::test_begin", + "TestDisabledClean::test_or", + ] + ) + def test_and(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + left = Primitive.circle(radius=3, center=(-10, 0)) + right = Primitive.circle(radius=3, center=(10, 0)) + with disable_auto_clean(): + shape = big - small | left ^ right + assert shape & shape == shape + assert shape & (~shape) is EmptyShape() + assert (~shape) & shape is EmptyShape() + assert (~shape) & (~shape) == ~shape + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestDisabledClean::test_begin", + "TestDisabledClean::test_and", + ] + ) + def test_sub(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + left = Primitive.circle(radius=3, center=(-10, 0)) + right = Primitive.circle(radius=3, center=(10, 0)) + with disable_auto_clean(): + shape = big - small | left ^ right + assert shape - shape is EmptyShape() + assert shape - (~shape) == shape + assert (~shape) - shape == ~shape + assert (~shape) - (~shape) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.timeout(40) + @pytest.mark.dependency( + depends=[ + "TestDisabledClean::test_begin", + "TestDisabledClean::test_or", + "TestDisabledClean::test_and", + "TestDisabledClean::test_sub", + ] + ) + def test_xor(self): + big = Primitive.square(side=2, center=(0, 0)) + small = Primitive.square(side=1, center=(0, 0)) + left = Primitive.circle(radius=3, center=(-10, 0)) + right = Primitive.circle(radius=3, center=(10, 0)) + with disable_auto_clean(): + shape = big - small | left ^ right + assert shape ^ shape is EmptyShape() + assert shape ^ (~shape) is WholeShape() + assert (~shape) ^ shape is WholeShape() + assert (~shape) ^ (~shape) is EmptyShape() + + @pytest.mark.order(43) + @pytest.mark.dependency( + depends=[ + "TestDisabledClean::test_begin", + "TestDisabledClean::test_or", + "TestDisabledClean::test_and", + "TestDisabledClean::test_sub", + "TestDisabledClean::test_xor", + ] + ) + def test_end(self): + pass + + +@pytest.mark.order(43) +@pytest.mark.dependency( + depends=[ + "TestEqualSquare::test_end", + "TestEqualHollowSquare::test_end", + "TestTriangle::test_end", + "TestDisabledClean::test_end", + ] +) +def test_end(): + pass diff --git a/tests/bool2d/test_lazy.py b/tests/bool2d/test_lazy.py new file mode 100644 index 00000000..e5df05c6 --- /dev/null +++ b/tests/bool2d/test_lazy.py @@ -0,0 +1,415 @@ +""" +This module tests when two shapes have common edges/segments +""" + +from copy import copy, deepcopy + +import pytest + +from shapepy.bool2d.base import EmptyShape, WholeShape +from shapepy.bool2d.lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy +from shapepy.bool2d.primitive import Primitive +from shapepy.scalar.angle import degrees + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "tests/bool2d/test_primitive.py::test_end", + "tests/bool2d/test_contains.py::test_end", + "tests/bool2d/test_empty_whole.py::test_end", + "tests/bool2d/test_shape.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(33) +@pytest.mark.dependency(depends=["test_begin"]) +def test_invert(): + empty = EmptyShape() + whole = WholeShape() + square = Primitive.square() + circle = Primitive.circle() + + assert RecipeLazy.invert(empty) is whole + assert RecipeLazy.invert(whole) is empty + + for shape in (square, circle): + invshape = RecipeLazy.invert(shape) + assert RecipeLazy.invert(invshape) is shape + + LazyNot(empty) + LazyNot(whole) + + +@pytest.mark.order(33) +@pytest.mark.dependency(depends=["test_begin", "test_invert"]) +def test_unite(): + empty = EmptyShape() + whole = WholeShape() + square = Primitive.square() + circle = Primitive.circle() + + assert RecipeLazy.unite((empty, empty, empty)) is empty + assert RecipeLazy.unite((whole, whole, whole)) is whole + + for shape in (square, circle): + assert RecipeLazy.unite((shape, shape, shape)) is shape + + LazyOr((empty, empty)) + LazyOr((empty, whole)) + LazyOr((whole, whole)) + LazyOr((whole, empty)) + + +@pytest.mark.order(33) +@pytest.mark.dependency(depends=["test_begin", "test_invert", "test_unite"]) +def test_intersect(): + empty = EmptyShape() + whole = WholeShape() + square = Primitive.square() + circle = Primitive.circle() + + assert RecipeLazy.intersect((empty, empty, empty)) is empty + assert RecipeLazy.intersect((whole, whole, whole)) is whole + + for shape in (square, circle): + assert RecipeLazy.intersect((shape, shape, shape)) is shape + + LazyAnd((empty, empty)) + LazyAnd((empty, whole)) + LazyAnd((whole, whole)) + LazyAnd((whole, empty)) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=["test_begin", "test_invert", "test_unite", "test_intersect"] +) +def test_hash(): + square = Primitive.square() + circle = Primitive.circle() + + lazyNot = LazyNot(circle) + assert hash(lazyNot) + hash(circle) == 0 + + lazyOr = LazyOr((square, circle)) + assert hash(lazyOr) == hash((square, circle)) + + lazyAnd = LazyAnd((square, circle)) + hash(lazyAnd) + # assert hash(lazyAnd) == -hash((-hash(square), hash(circle))) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + ] +) +def test_xor(): + empty = EmptyShape() + whole = WholeShape() + square = Primitive.square() + circle = Primitive.circle() + + assert RecipeLazy.xor((empty,)) is empty + assert RecipeLazy.xor((whole,)) is whole + assert RecipeLazy.xor((empty, empty)) is empty + assert RecipeLazy.xor((whole, whole)) is empty + assert RecipeLazy.xor((empty, empty, empty)) is empty + assert RecipeLazy.xor((whole, whole, whole)) is whole + + for shape in (square, circle): + assert RecipeLazy.xor((shape,)) is shape + assert RecipeLazy.xor((shape, shape)) is empty + assert RecipeLazy.xor((shape, shape, shape)) is shape + assert RecipeLazy.xor((shape, shape, shape, shape)) is empty + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + ] +) +def test_transformation_move(): + square0 = Primitive.square() + square1 = Primitive.square(center=(1, 2)) + notsquare0 = RecipeLazy.invert(square0) + notsquare1 = RecipeLazy.invert(square1) + assert notsquare0.move((1, 2)) == notsquare1 + + square = Primitive.square() + circle = Primitive.circle() + lazyNot = LazyNot(square) + lazyNot.move((3, -4)) + lazyOr = LazyOr((square, circle)) + lazyOr.move((3, -4)) + lazyAnd = LazyAnd((square, circle)) + lazyAnd.move((3, -4)) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + ] +) +def test_transformation_scale(): + square0 = Primitive.square(side=1) + square1 = Primitive.square(side=2) + notsquare0 = RecipeLazy.invert(square0) + notsquare1 = RecipeLazy.invert(square1) + assert notsquare0.scale(2) == notsquare1 + + square = Primitive.square() + circle = Primitive.circle() + lazyNot = LazyNot(square) + lazyNot.scale(2) + lazyOr = LazyOr((square, circle)) + lazyOr.scale(3) + lazyAnd = LazyAnd((square, circle)) + lazyAnd.scale(2) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + ] +) +def test_transformation_rotate(): + square0 = Primitive.square() + square1 = Primitive.square() + notsquare0 = RecipeLazy.invert(square0) + notsquare1 = RecipeLazy.invert(square1) + assert notsquare0.rotate(degrees(90)) == notsquare1 + + square = Primitive.square() + circle = Primitive.circle() + lazyNot = LazyNot(square) + lazyNot.rotate(degrees(60)) + lazyOr = LazyOr((square, circle)) + lazyOr.rotate(degrees(45)) + lazyAnd = LazyAnd((square, circle)) + lazyAnd.rotate(degrees(30)) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + ] +) +def test_printing(): + square = Primitive.square() + circle = Primitive.circle() + + lazyNot = LazyNot(square) + str(lazyNot) + repr(lazyNot) + + lazyOr = LazyOr((square, circle)) + str(lazyOr) + repr(lazyOr) + + lazyAnd = LazyAnd((square, circle)) + str(lazyAnd) + repr(lazyAnd) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + "test_printing", + ] +) +def test_copy(): + square = Primitive.square() + circle = Primitive.circle() + + lazyNot = LazyNot(square) + copyLazyNot = copy(lazyNot) + assert copyLazyNot == lazyNot + assert id(copyLazyNot) != id(lazyNot) + assert id(~copyLazyNot) == id(~lazyNot) + deepLazyNot = deepcopy(lazyNot) + assert deepLazyNot == lazyNot + assert id(deepLazyNot) != id(lazyNot) + assert ~deepLazyNot == ~lazyNot + assert id(~deepLazyNot) != id(~lazyNot) + + lazyOr = LazyOr((square, circle)) + copyLazyOr = copy(lazyOr) + assert copyLazyOr == lazyOr + assert id(copyLazyOr) != id(lazyOr) + deepLazyOr = deepcopy(lazyOr) + assert deepLazyOr == lazyOr + assert id(deepLazyOr) != id(lazyOr) + + lazyAnd = LazyAnd((square, circle)) + copyLazyAnd = copy(lazyAnd) + assert copyLazyAnd == lazyAnd + assert id(copyLazyAnd) != id(lazyAnd) + deepLazyAnd = deepcopy(lazyAnd) + assert deepLazyAnd == lazyAnd + assert id(deepLazyAnd) != id(lazyAnd) + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + "test_printing", + ] +) +def test_clean(): + square = Primitive.square(center=(-3, 0)) + circle = Primitive.circle(center=(3, 0)) + + lazyNot = LazyNot(square) + assert lazyNot.clean() == (-square).clean() + + lazyOr = LazyOr((square, circle)) + assert lazyOr.clean() == (square + circle).clean() + + lazyAnd = LazyAnd((square, circle)) + assert lazyAnd.clean() == (square * circle).clean() + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + "test_printing", + "test_clean", + ] +) +def test_density(): + square = Primitive.square(side=2) + circle = Primitive.circle() + + lazyNot = LazyNot(square) + densities = { + (0, 0): 0, + (-1, 0): 0.5, + (1, 0): 0.5, + (0, 1): 0.5, + (0, -1): 0.5, + (-1, 1): 0.75, + (-1, -1): 0.75, + (1, 1): 0.75, + (1, -1): 0.75, + } + for point, value in densities.items(): + density = lazyNot.density(point) + assert float(density) == value + + lazyOr = LazyOr((square, circle)) + densities = { + (0, 0): 1, + (-1, 0): 0.5, + (1, 0): 0.5, + (0, 1): 0.5, + (0, -1): 0.5, + (-1, 1): 0.25, + (-1, -1): 0.25, + (1, 1): 0.25, + (1, -1): 0.25, + } + for point, value in densities.items(): + density = lazyOr.density(point) + assert abs(float(density) - value) < 1e-9 + + lazyAnd = LazyAnd((square, circle)) + densities = { + (0, 0): 1, + (-1, 0): 0.5, + (1, 0): 0.5, + (0, 1): 0.5, + (0, -1): 0.5, + (-1, 1): 0, + (-1, -1): 0, + (1, 1): 0, + (1, -1): 0, + } + for point, value in densities.items(): + density = lazyAnd.density(point) + assert abs(float(density) - value) < 1e-9 + + +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + "test_printing", + "test_hash", + "test_copy", + "test_density", + ] +) +def test_all(): + pass