Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 12 additions & 26 deletions src/shapepy/bool2d/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,19 @@ def __init__(self):
def __invert__(self) -> SubSetR2:
"""Invert shape"""
result = Future.invert(self)
if Config.clean["inv"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["inv"] else result

@debug("shapepy.bool2d.base")
def __or__(self, other: SubSetR2) -> SubSetR2:
"""Union shapes"""
result = Future.unite((self, other))
if Config.clean["or"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["or"] else result

@debug("shapepy.bool2d.base")
def __and__(self, other: SubSetR2) -> SubSetR2:
"""Intersection shapes"""
result = Future.intersect((self, other))
if Config.clean["and"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["and"] else result

@abstractmethod
def __copy__(self) -> SubSetR2:
Expand All @@ -62,41 +56,31 @@ def __deepcopy__(self, memo) -> SubSetR2:
def __neg__(self) -> SubSetR2:
"""Invert the SubSetR2"""
result = Future.invert(self)
if Config.clean["neg"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["neg"] else result

@debug("shapepy.bool2d.base")
def __add__(self, other: SubSetR2):
"""Union of SubSetR2"""
result = Future.unite((self, other))
if Config.clean["add"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["add"] else result

@debug("shapepy.bool2d.base")
def __mul__(self, other: SubSetR2):
"""Intersection of SubSetR2"""
result = Future.intersect((self, other))
if Config.clean["mul"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["mul"] else result

@debug("shapepy.bool2d.base")
def __sub__(self, other: SubSetR2):
"""Subtraction of SubSetR2"""
result = Future.intersect((self, Future.invert(other)))
if Config.clean["sub"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["sub"] else result

@debug("shapepy.bool2d.base")
def __xor__(self, other: SubSetR2):
"""XOR of SubSetR2"""
result = Future.xor((self, other))
if Config.clean["xor"]:
result = Future.clean(result)
return result
return Future.clean(result) if Config.clean["xor"] else result

def __repr__(self) -> str: # pragma: no cover
return str(self)
Expand Down Expand Up @@ -347,7 +331,8 @@ def __invert__(self) -> SubSetR2:
return EmptyShape()

def __xor__(self, other: SubSetR2) -> SubSetR2:
return ~Future.convert(other)
result = ~Future.convert(other)
return Future.clean(result) if Config.clean["xor"] else result

def __contains__(self, other: SubSetR2) -> bool:
return True
Expand All @@ -356,7 +341,8 @@ def __str__(self) -> str:
return "WholeShape"

def __sub__(self, other: SubSetR2) -> SubSetR2:
return ~Future.convert(other)
result = ~Future.convert(other)
return Future.clean(result) if Config.clean["xor"] else result

def __bool__(self) -> bool:
return True
Expand Down
82 changes: 68 additions & 14 deletions src/shapepy/bool2d/boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,12 @@
from ..loggers import debug
from ..tools import CyclicContainer, Is, NotExpectedError
from . import boolalg
from .base import EmptyShape, SubSetR2, WholeShape
from .base import EmptyShape, Future, SubSetR2, WholeShape
from .config import Config
from .convert import from_any
from .curve import SingleCurve
from .lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy, is_lazy
from .point import SinglePoint
from .shape import (
ConnectedShape,
DisjointShape,
SimpleShape,
shape_from_jordans,
)
from .shape import ConnectedShape, DisjointShape, SimpleShape


@debug("shapepy.bool2d.boolean")
Expand Down Expand Up @@ -168,10 +162,9 @@ def clean_bool2d_not(subset: LazyNot) -> SubSetR2:
if Is.instance(inverted, SimpleShape):
return SimpleShape(~inverted.jordan, True)
if Is.instance(inverted, ConnectedShape):
return DisjointShape(~simple for simple in inverted.subshapes)
return DisjointShape((~s).clean() for s in inverted.subshapes)
if Is.instance(inverted, DisjointShape):
new_jordans = tuple(~jordan for jordan in inverted.jordans)
return shape_from_jordans(new_jordans)
return shape_from_jordans(~jordan for jordan in inverted.jordans)
raise NotImplementedError(f"Missing typo: {type(inverted)}")


Expand All @@ -192,8 +185,8 @@ def contains_bool2d(subseta: SubSetR2, subsetb: SubSetR2) -> bool:
bool
The result if B is inside A
"""
subseta = from_any(subseta)
subsetb = from_any(subsetb)
subseta = Future.convert(subseta)
subsetb = Future.convert(subsetb)
if Is.instance(subseta, EmptyShape) or Is.instance(subsetb, WholeShape):
return subseta is subsetb
if Is.instance(subseta, WholeShape) or Is.instance(subsetb, EmptyShape):
Expand All @@ -208,7 +201,7 @@ def contains_bool2d(subseta: SubSetR2, subsetb: SubSetR2) -> bool:
if Is.instance(subsetb, (SinglePoint, SingleCurve, SimpleShape)):
return subsetb in subseta
if Is.instance(subsetb, ConnectedShape):
return ~subseta in ~subsetb
return (~subseta).clean() in (~subsetb).clean()
if not Config.auto_clean:
raise ValueError(
f"Needs clean to evaluate: {type(subseta)}, {type(subsetb)}"
Expand Down Expand Up @@ -294,6 +287,67 @@ def expression2subset(expression: str) -> SubSetR2:
raise NotExpectedError(f"Invalid expression: {expression}")


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.jordan
for subsimple in connected:
subjordan = subsimple.jordan
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)


class FollowPath:
"""
Class responsible to compute the final jordan curve
Expand Down
13 changes: 7 additions & 6 deletions src/shapepy/bool2d/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,25 @@ class Config:

clean = {
"add": True,
"or": True,
"or": False,
"xor": True,
"and": True,
"and": False,
"sub": True,
"mul": True,
"neg": True,
"inv": True,
"inv": False,
}

auto_clean = True


@contextmanager
def disable_auto_clean():
"""Function that disables temporarily the auto clean"""
def set_auto_clean(value: bool):
"""Function that enables/disables temporarily the auto clean"""
value = bool(value)
old = Config.clean.copy()
for key in Config.clean:
Config.clean[key] = False
Config.clean[key] = value
try:
yield
finally:
Expand Down
6 changes: 2 additions & 4 deletions src/shapepy/bool2d/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ def rotate(self, angle):
return self

def density(self, center):
densities = (sub.density(center) for sub in self)
return unite_densities(tuple(densities))
return unite_densities(sub.density(center) for sub in self)


class LazyAnd(SubSetR2):
Expand Down Expand Up @@ -251,8 +250,7 @@ def rotate(self, angle):
return self

def density(self, center):
densities = (sub.density(center) for sub in self)
return intersect_densities(tuple(densities))
return intersect_densities(sub.density(center) for sub in self)


def is_lazy(subset: SubSetR2) -> bool:
Expand Down
65 changes: 2 additions & 63 deletions src/shapepy/bool2d/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def subshapes(self) -> Set[SimpleShape]:
def subshapes(self, simples: Iterable[SimpleShape]):
simples = frozenset(simples)
if not all(Is.instance(simple, SimpleShape) for simple in simples):
raise TypeError
raise TypeError(f"Invalid typos: {tuple(map(type, simples))}")
self.__subshapes = simples

def move(self, vector: Point2D) -> JordanCurve:
Expand Down Expand Up @@ -446,7 +446,7 @@ def subshapes(self, values: Iterable[SubSetR2]):
if not all(
Is.instance(sub, (SimpleShape, ConnectedShape)) for sub in values
):
raise ValueError
raise ValueError(f"Invalid typos: {tuple(map(type, values))}")
self.__subshapes = values

def move(self, vector: Point2D) -> JordanCurve:
Expand Down Expand Up @@ -492,64 +492,3 @@ def density(self, center: Point2D) -> Real:
center = To.point(center)
densities = (sub.density(center) for sub in self.subshapes)
return unite_densities(densities)


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.jordan
for subsimple in connected:
subjordan = subsimple.jordan
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)
1 change: 1 addition & 0 deletions src/shapepy/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def plot_subset(self, shape: SubSetR2, *, kwargs):
Plots a SubSetR2, which can be EmptyShape, WholeShape, Simple, etc
"""
assert Is.instance(shape, SubSetR2)
shape = shape.clean()
if Is.instance(shape, EmptyShape):
return
if Is.instance(shape, WholeShape):
Expand Down
6 changes: 2 additions & 4 deletions tests/bool2d/test_bool_finite_intersect.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ def test_or_two_rombos(self):
good_points += [(0, -1), (1, -2), (3, 0), (1, 2)]
good_shape = Primitive.polygon(good_points)

test_shape = square0 | square1
assert test_shape == good_shape
assert square0 + square1 == good_shape

@pytest.mark.order(42)
@pytest.mark.timeout(40)
Expand All @@ -60,9 +59,8 @@ def test_and_two_rombos(self):
square0 = Primitive.regular_polygon(nsides=4, radius=2, center=(-1, 0))
square1 = Primitive.regular_polygon(nsides=4, radius=2, center=(1, 0))

test = square0 & square1
good = Primitive.regular_polygon(nsides=4, radius=1, center=(0, 0))
assert test == good
assert square0 * square1 == good

@pytest.mark.order(42)
@pytest.mark.timeout(40)
Expand Down
Loading