From bc14d909f01d0b4366316a7049662f164d769158 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 20 Sep 2025 18:45:06 +0200 Subject: [PATCH 1/3] feat: add Point2D as SubSetR2 --- src/shapepy/bool2d/point.py | 83 +++++++++++++++++++++++++++ tests/bool2d/test_point.py | 111 ++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/shapepy/bool2d/point.py create mode 100644 tests/bool2d/test_point.py diff --git a/src/shapepy/bool2d/point.py b/src/shapepy/bool2d/point.py new file mode 100644 index 00000000..0a68587e --- /dev/null +++ b/src/shapepy/bool2d/point.py @@ -0,0 +1,83 @@ +""" +Defines a SinglePoint class, that represents a SubSet of the plane +that contains only one point on the plane +""" + +from __future__ import annotations + +from copy import copy +from typing import Tuple, Union + +from ..geometry.point import Point2D +from ..loggers import debug +from ..scalar.angle import Angle +from ..scalar.reals import Real +from ..tools import Is, To +from .base import SubSetR2 +from .density import Density + + +class SinglePoint(SubSetR2): + """ + SinglePoint 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, point: Point2D): + point = To.point(point) + if Is.infinity(point.radius): + raise ValueError("Must be a finite point") + self.__point = point + + @property + def internal(self) -> Point2D: + """Gives the geometric point that defines the SinglePoint SubSetR2""" + return self.__point + + def __copy__(self) -> SinglePoint: + return SinglePoint(self.internal) + + def __deepcopy__(self, memo) -> SinglePoint: + return SinglePoint(copy(self.__point)) + + def __str__(self) -> str: # pragma: no cover # For debug + return "{" + str(self.__point) + "}" + + def __eq__(self, other: SubSetR2) -> bool: + """Compare two subsets + + 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, SinglePoint) and self.internal == other.internal + ) + + @debug("shapepy.bool2d.shape") + def __hash__(self): + return hash((self.internal.xcoord, self.internal.ycoord)) + + def move(self, vector: Point2D) -> SinglePoint: + self.__point = self.__point.move(vector) + return self + + def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> SinglePoint: + self.__point = self.__point.scale(amount) + return self + + def rotate(self, angle: Angle) -> SinglePoint: + self.__point = self.__point.rotate(angle) + return self + + def density(self, center: Point2D) -> Density: + return Density.zero diff --git a/tests/bool2d/test_point.py b/tests/bool2d/test_point.py new file mode 100644 index 00000000..854b517c --- /dev/null +++ b/tests/bool2d/test_point.py @@ -0,0 +1,111 @@ +"""File to test the functions `move`, `scale` and `rotate`""" + +from copy import copy, deepcopy + +import pytest + +from shapepy.bool2d.point import SinglePoint +from shapepy.geometry.point import cartesian, polar +from shapepy.scalar.angle import degrees +from shapepy.scalar.reals import Math + + +@pytest.mark.order(22) +@pytest.mark.dependency( + depends=[ + "tests/geometry/test_point.py::test_all", + "tests/bool2d/test_empty_whole.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_build(): + SinglePoint(cartesian(0, 0)) + SinglePoint(cartesian(1, 0)) + SinglePoint(cartesian(0, 1)) + SinglePoint(cartesian(-1, 0)) + + with pytest.raises(ValueError): + SinglePoint(polar(Math.POSINF, 0)) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_move(): + subset = SinglePoint(cartesian(0, 0)) + assert subset.move((1, 2)) == SinglePoint(cartesian(1, 2)) + + subset = SinglePoint(cartesian(-1, 2)) + assert subset.move((-3, 5)) == SinglePoint(cartesian(-4, 7)) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_scale(): + subset = SinglePoint(cartesian(0, 0)) + assert subset.scale(2) == SinglePoint(cartesian(0, 0)) + + subset = SinglePoint(cartesian(-1, 2)) + assert subset.scale(2) == SinglePoint(cartesian(-2, 4)) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_rotate(): + subset = SinglePoint(cartesian(-1, 2)) + assert subset.rotate(degrees(90)) == SinglePoint(cartesian(-2, -1)) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_density(): + subset = SinglePoint(cartesian(-1, 2)) + + assert subset.density((0, 0)) == 0 + assert subset.density((-1, 2)) == 0 + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_copy(): + subset = SinglePoint(cartesian(-1, 2)) + + other = copy(subset) + assert other == subset + assert id(other) != id(subset) + assert id(subset.internal) == id(other.internal) + + other = deepcopy(subset) + assert other == subset + assert id(other) != id(subset) + assert id(subset.internal) != id(other.internal) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_hash(): + subseta = SinglePoint(cartesian(-1, 2)) + subsetb = SinglePoint(cartesian(-1, 2)) + assert hash(subseta) == hash(subsetb) + + +@pytest.mark.order(22) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_build", + "test_move", + "test_scale", + "test_rotate", + "test_density", + "test_copy", + "test_hash", + ] +) +def test_end(): + pass From 2cf2b8d5c055386c0710fa93ab429f98a25cd682 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 20 Sep 2025 18:51:36 +0200 Subject: [PATCH 2/3] feat: add Curve as SubsetR2 --- src/shapepy/bool2d/curve.py | 79 ++++++++++++++++++++++ tests/bool2d/test_curve.py | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/shapepy/bool2d/curve.py create mode 100644 tests/bool2d/test_curve.py diff --git a/src/shapepy/bool2d/curve.py b/src/shapepy/bool2d/curve.py new file mode 100644 index 00000000..adfa4fb5 --- /dev/null +++ b/src/shapepy/bool2d/curve.py @@ -0,0 +1,79 @@ +""" +Defines a SingleCurve class, that represents a SubSet of the plane +that contains a continous set of points on the plane +""" + +from __future__ import annotations + +from copy import copy +from typing import Tuple, Union + +from ..geometry.base import IGeometricCurve +from ..geometry.point import Point2D +from ..loggers import debug +from ..scalar.angle import Angle +from ..scalar.reals import Real +from ..tools import Is +from .base import SubSetR2 +from .density import Density + + +class SingleCurve(SubSetR2): + """SingleCurve class + + It represents a subset on the plane of continous points + """ + + def __init__(self, curve: IGeometricCurve): + if not Is.instance(curve, IGeometricCurve): + raise TypeError(f"Invalid: {type(curve)} != {IGeometricCurve}") + self.__curve = curve + + @property + def internal(self) -> IGeometricCurve: + """Gives the geometric curve that defines the SingleCurve SubSetR2""" + return self.__curve + + def __copy__(self) -> SingleCurve: + return SingleCurve(self.internal) + + def __deepcopy__(self, memo) -> SingleCurve: + return SingleCurve(copy(self.__curve)) + + def __str__(self) -> str: # pragma: no cover # For debug + return "{" + str(self.__curve) + "}" + + def __eq__(self, other: SubSetR2) -> bool: + """Compare two subsets + + Parameters + ---------- + other: SubSetR2 + The subset to compare + + :raises ValueError: If ``other`` is not a SubSetR2 instance + """ + if not Is.instance(other, SubSetR2): + raise ValueError + return ( + Is.instance(other, SingleCurve) and self.internal == other.internal + ) + + @debug("shapepy.bool2d.shape") + def __hash__(self): + return hash(self.internal.length) + + def move(self, vector: Point2D) -> SingleCurve: + self.__curve = self.__curve.move(vector) + return self + + def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> SingleCurve: + self.__curve = self.__curve.scale(amount) + return self + + def rotate(self, angle: Angle) -> SingleCurve: + self.__curve = self.__curve.rotate(angle) + return self + + def density(self, center: Point2D) -> Density: + return Density.zero diff --git a/tests/bool2d/test_curve.py b/tests/bool2d/test_curve.py new file mode 100644 index 00000000..ee7ba28a --- /dev/null +++ b/tests/bool2d/test_curve.py @@ -0,0 +1,126 @@ +"""File to test the functions `move`, `scale` and `rotate`""" + +from copy import copy, deepcopy + +import pytest + +from shapepy.bool2d.curve import SingleCurve +from shapepy.geometry.factory import FactorySegment +from shapepy.scalar.angle import degrees + + +@pytest.mark.order(23) +@pytest.mark.dependency( + depends=[ + "tests/geometry/test_point.py::test_all", + "tests/geometry/test_jordan_polygon.py::test_all", + "tests/bool2d/test_empty_whole.py::test_end", + "tests/bool2d/test_point.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin"]) +def test_build(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + SingleCurve(segment) + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin", "test_build"]) +def test_move(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + curve = SingleCurve(segment) + + test = curve.move((-3, 2)) + segment = FactorySegment.bezier([(-2, 4), (-7, 3)]) + good = SingleCurve(segment) + + assert test == good + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin", "test_build"]) +def test_scale(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + curve = SingleCurve(segment) + + test = curve.scale(4) + segment = FactorySegment.bezier([(4, 8), (-16, 4)]) + good = SingleCurve(segment) + + assert test == good + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin", "test_build"]) +def test_rotate(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + curve = SingleCurve(segment) + + test = curve.rotate(degrees(90)) + segment = FactorySegment.bezier([(-2, 1), (-1, -4)]) + good = SingleCurve(segment) + + assert test == good + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin"]) +def test_density(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + subset = SingleCurve(segment) + + assert subset.density((0, 0)) == 0 + assert subset.density((-1, 2)) == 0 + assert subset.density((1, 2)) == 0 + assert subset.density((-4, 1)) == 0 + + +@pytest.mark.order(23) +@pytest.mark.dependency(depends=["test_begin"]) +def test_copy(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + subset = SingleCurve(segment) + + other = copy(subset) + assert other == subset + assert id(other) != id(subset) + assert id(subset.internal) == id(other.internal) + + other = deepcopy(subset) + assert other == subset + assert id(other) != id(subset) + assert id(subset.internal) != id(other.internal) + + +@pytest.mark.order(22) +@pytest.mark.dependency(depends=["test_begin"]) +def test_hash(): + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + subseta = SingleCurve(segment) + + segment = FactorySegment.bezier([(1, 2), (-4, 1)]) + subsetb = SingleCurve(segment) + assert hash(subseta) == hash(subsetb) + + +@pytest.mark.order(23) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_build", + "test_move", + "test_scale", + "test_rotate", + "test_density", + "test_copy", + "test_hash", + ] +) +def test_end(): + pass From d8a4efef22f6fd44fe5ec67b83f4972642f107b8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 20 Sep 2025 18:59:47 +0200 Subject: [PATCH 3/3] feat: add conversion from objects to Point or Curve --- src/shapepy/bool2d/convert.py | 13 ++++++++---- src/shapepy/bool2d/shape.py | 39 ++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/shapepy/bool2d/convert.py b/src/shapepy/bool2d/convert.py index 61b5634d..b6b89223 100644 --- a/src/shapepy/bool2d/convert.py +++ b/src/shapepy/bool2d/convert.py @@ -2,14 +2,19 @@ Functions to convert from typical data types into SubSetR2 """ -from ..tools import Is +from ..geometry.base import IGeometricCurve +from ..tools import Is, To from .base import SubSetR2 +from .curve import SingleCurve +from .point import SinglePoint def from_any(subset: SubSetR2) -> SubSetR2: """ Converts an object into a SubSetR2 """ - if not Is.instance(subset, SubSetR2): - raise TypeError - return subset + if Is.instance(subset, SubSetR2): + return subset + if Is.instance(subset, IGeometricCurve): + return SingleCurve(subset) + return SinglePoint(To.point(subset)) diff --git a/src/shapepy/bool2d/shape.py b/src/shapepy/bool2d/shape.py index b257164a..e0d809cd 100644 --- a/src/shapepy/bool2d/shape.py +++ b/src/shapepy/bool2d/shape.py @@ -19,13 +19,15 @@ from ..scalar.angle import Angle from ..scalar.reals import Real from ..tools import Is, To -from .base import EmptyShape, SubSetR2 +from .base import EmptyShape, Future, SubSetR2 +from .curve import SingleCurve from .density import ( Density, intersect_densities, lebesgue_density_jordan, unite_densities, ) +from .point import SinglePoint class SimpleShape(SubSetR2): @@ -132,24 +134,27 @@ def invert(self) -> SimpleShape: return self def __contains__(self, other: SubSetR2) -> bool: - if Is.instance(other, SubSetR2): - if Is.instance(other, SimpleShape): - return self.__contains_simple(other) - if Is.instance(other, ConnectedShape): - return ~self in ~other - if Is.instance(other, DisjointShape): - return all(o in self for o in other.subshapes) - return Is.instance(other, EmptyShape) - if Is.instance(other, JordanCurve): - return self.__contains_jordan(other) - return self.__contains_point(other) - - def __contains_point(self, point: Point2D) -> bool: - density = float(self.density(point)) + if not Is.instance(other, SubSetR2): + other = Future.convert(other) + if Is.instance(other, SinglePoint): + return self.__contains_point(other) + if Is.instance(other, SingleCurve): + return self.__contains_curve(other) + if Is.instance(other, SimpleShape): + return self.__contains_simple(other) + if Is.instance(other, ConnectedShape): + return ~self in ~other + if Is.instance(other, DisjointShape): + return all(o in self for o in other.subshapes) + return Is.instance(other, EmptyShape) + + def __contains_point(self, point: SinglePoint) -> bool: + point = Future.convert(point) + density = float(self.density(point.internal)) return density > 0 if self.boundary else density == 1 - def __contains_jordan(self, jordan: JordanCurve) -> bool: - piecewise = jordan.parametrize() + def __contains_curve(self, curve: SingleCurve) -> bool: + piecewise = curve.internal.parametrize() vertices = map(piecewise, piecewise.knots[:-1]) if not all(map(self.__contains_point, vertices)): return False