From 18134550ee692e21ec1b13fc8d4b58ef4bbcb12c Mon Sep 17 00:00:00 2001 From: gresm <78505251+gresm@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:24:53 +0200 Subject: [PATCH 1/5] Update CONTRIBUTING.md The repository was moved, so the links should be updated. --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c77bf1bd..aa5cb481 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ See below to see how to build pygame_geometry from source. If you need help, tal 2. Install [Visual Studio Community 2022](https://visualstudio.microsoft.com/vs/community/) or [Visual Studio Build Tools 2017](https://aka.ms/vs/15/release/vs_buildtools.exe) and make sure you mark `MSVC v140 - VS 2015 C++ build tools (v14.00)` with the installation 3. Run `python -m pip install setuptools -U` 4. Install the latest version of [git](https://gitforwindows.org/) -5. Run `git clone https://github.com/novialriptide/pygame_geometry.git` +5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 6. Run `cd pygame_geometry; python -m pip install .` **If you are having trouble re-compiling, try deleting the `build` folder from the root directory if it exists** @@ -32,7 +32,7 @@ See below to see how to build pygame_geometry from source. If you need help, tal brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf pkg-config ``` 4. Run `python3 -m pip install setuptools -U` -5. Run `git clone https://github.com/novialriptide/pygame_geometry.git` +5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 6. Run `cd pygame_geometry; python3 -m pip install .` ## Linux (Debian-based Distributions) @@ -43,5 +43,5 @@ sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev ``` 3. Install git by `sudo apt install git -y` 4. Run `python3 -m pip install setuptools -U` -5. Run `git clone https://github.com/novialriptide/pygame_geometry.git` +5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 6. Run `cd pygame_geometry; python3 -m pip install .` From 6b4d556745dad82d26894af2dce2b23f2d09dff7 Mon Sep 17 00:00:00 2001 From: gresm <78505251+gresm@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:36:16 +0200 Subject: [PATCH 2/5] Update CONTRIBUTING.md --- examples/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CONTRIBUTING.md b/examples/CONTRIBUTING.md index 323d1786..ac5917cc 100644 --- a/examples/CONTRIBUTING.md +++ b/examples/CONTRIBUTING.md @@ -1,6 +1,6 @@ ## Contributing Guidelines -1. See [Contributing Guidelines](https://github.com/novialriptide/pygame_geometry/blob/main/CONTRIBUTING.md) for general contributing guidelines. +1. See [Contributing Guidelines](https://github.com/pygame-community/pygame_geometry/blob/main/CONTRIBUTING.md) for general contributing guidelines. 2. Use this boilerplate to get started ```python From 3bd562bbd1a9ab32c0c4d824b19b52d453f6eda6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:47:08 +0000 Subject: [PATCH 3/5] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/black.yml | 2 +- .github/workflows/cppcheck.yml | 2 +- .github/workflows/macos_test.yml | 2 +- .github/workflows/ubuntu_test.yml | 2 +- .github/workflows/windows_test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index eb4d4307..81ce51a8 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -6,5 +6,5 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: psf/black@stable diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml index 54b86e1d..c176a74d 100644 --- a/.github/workflows/cppcheck.yml +++ b/.github/workflows/cppcheck.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install deps run: | diff --git a/.github/workflows/macos_test.yml b/.github/workflows/macos_test.yml index 0422ed89..a8bea868 100644 --- a/.github/workflows/macos_test.yml +++ b/.github/workflows/macos_test.yml @@ -14,7 +14,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, '3.10', 3.11] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index a4c03835..4d0d6eed 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -15,7 +15,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, '3.10', 3.11] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Python run: | # Add the deadsnakes PPA to install python diff --git a/.github/workflows/windows_test.yml b/.github/workflows/windows_test.yml index 4204b732..80e32e34 100644 --- a/.github/workflows/windows_test.yml +++ b/.github/workflows/windows_test.yml @@ -14,7 +14,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, '3.10', 3.11] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install packages run: | py -${{ matrix.python-version }} -m pip install --upgrade pip From db9b7cf365f82dacd7cecfa7c90b8de4dc2ad9e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:43:10 +0000 Subject: [PATCH 4/5] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/macos_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos_test.yml b/.github/workflows/macos_test.yml index a8bea868..cfa5bee7 100644 --- a/.github/workflows/macos_test.yml +++ b/.github/workflows/macos_test.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From ce7958dbb3cf12fdad04cf5e8f2b7c3a4cb2d439 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r Date: Tue, 2 Jan 2024 18:33:07 +0100 Subject: [PATCH 5/5] Add Circle.rotate/ip --- docs/circle.rst | 23 ++++++ docs/geometry.rst | 4 + geometry.pyi | 6 ++ src_c/circle.c | 95 ++++++++++++++++++++++ test/test_circle.py | 188 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+) diff --git a/docs/circle.rst b/docs/circle.rst index d980971b..ab561983 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -326,6 +326,29 @@ Circle Methods .. ## Circle.contains ## + .. method:: rotate + | :sl:`rotates the circle` + | :sg:`rotate(angle, rotation_point=Circle.center) -> None` + + Returns a new `Circle` that is rotated by the specified angle around a point. + A positive angle rotates the circle clockwise, while a negative angle rotates it counter-clockwise. + The rotation point can be a `tuple`, `list`, or `Vector2`. + If no rotation point is given, the circle will be rotated around its center. + + .. ## Circle.rotate ## + + .. method:: rotate_ip + | :sl:`rotates the circle in place` + | :sg:`rotate_ip(angle, rotation_point=Circle.center) -> None` + + This method rotates the circle by a specified angle around a point. + A positive angle rotates the circle clockwise, while a negative angle rotates it counter-clockwise. + The rotation point can be a `tuple`, `list`, or `Vector2`. + + If no rotation point is given, the circle will be rotated around its center. + + .. ## Circle.rotate_ip ## + .. method:: copy | :sl:`returns a copy of the circle` diff --git a/docs/geometry.rst b/docs/geometry.rst index 528f7bcb..de01e5d2 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -60,6 +60,10 @@ performing transformations and checking for collisions with other objects. contains: Checks if the circle fully contains the given object. + rotate: Rotates the circle by the given amount. + + rotate_ip: Rotates the circle by the given amount in place. + as_rect: Returns the smallest rectangle that contains the circle. Additionally to these, the circle shape can also be used as a collider for the ``geometry.raycast`` function. diff --git a/geometry.pyi b/geometry.pyi index 02eca2d7..86c7d8ed 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -200,6 +200,12 @@ class Circle: ) -> bool: ... @overload def collidepolygon(self, *coords, only_edges: bool = False) -> bool: ... + def rotate( + self, angle: float, rotation_point: Coordinate = Circle.center + ) -> Circle: ... + def rotate_ip( + self, angle: float, rotation_point: Coordinate = Circle.center + ) -> None: ... class Polygon: vertices: List[Coordinate] diff --git a/src_c/circle.c b/src_c/circle.c index c94a3085..7b93772d 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -498,6 +498,99 @@ pg_circle_collidepolygon(pgCircleObject *self, PyObject *const *args, return PyBool_FromLong(result); } +static void +_pg_rotate_circle_helper(pgCircleBase *circle, double angle, double rx, + double ry) +{ + if (angle == 0.0 || fmod(angle, 360.0) == 0.0) { + return; + } + + double x = circle->x - rx; + double y = circle->y - ry; + + const double angle_rad = DEG_TO_RAD(angle); + + double cos_theta = cos(angle_rad); + double sin_theta = sin(angle_rad); + + circle->x = rx + x * cos_theta - y * sin_theta; + circle->y = ry + x * sin_theta + y * cos_theta; +} + +static PyObject * +pg_circle_rotate(pgCircleObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (!nargs || nargs > 2) { + return RAISE(PyExc_TypeError, "rotate requires 1 or 2 arguments"); + } + + pgCircleBase *circle = &self->circle; + double angle, rx, ry; + + rx = circle->x; + ry = circle->y; + + if (!pg_DoubleFromObj(args[0], &angle)) { + return RAISE(PyExc_TypeError, + "Invalid angle argument, must be numeric"); + } + + if (nargs != 2) { + return _pg_circle_subtype_new(Py_TYPE(self), circle); + } + + if (!pg_TwoDoublesFromObj(args[1], &rx, &ry)) { + return RAISE(PyExc_TypeError, + "Invalid rotation point argument, must be a sequence of " + "2 numbers"); + } + + PyObject *circle_obj = _pg_circle_subtype_new(Py_TYPE(self), circle); + if (!circle_obj) { + return NULL; + } + + _pg_rotate_circle_helper(&pgCircle_AsCircle(circle_obj), angle, rx, ry); + + return circle_obj; +} + +static PyObject * +pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args, + Py_ssize_t nargs) +{ + if (!nargs || nargs > 2) { + return RAISE(PyExc_TypeError, "rotate requires 1 or 2 arguments"); + } + + pgCircleBase *circle = &self->circle; + double angle, rx, ry; + + rx = circle->x; + ry = circle->y; + + if (!pg_DoubleFromObj(args[0], &angle)) { + return RAISE(PyExc_TypeError, + "Invalid angle argument, must be numeric"); + } + + if (nargs != 2) { + /* just return None */ + Py_RETURN_NONE; + } + + if (!pg_TwoDoublesFromObj(args[1], &rx, &ry)) { + return RAISE(PyExc_TypeError, + "Invalid rotation point argument, must be a sequence " + "of 2 numbers"); + } + + _pg_rotate_circle_helper(circle, angle, rx, ry); + + Py_RETURN_NONE; +} + static struct PyMethodDef pg_circle_methods[] = { {"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL, NULL}, @@ -514,6 +607,8 @@ static struct PyMethodDef pg_circle_methods[] = { {"contains", (PyCFunction)pg_circle_contains, METH_O, NULL}, {"__copy__", (PyCFunction)pg_circle_copy, METH_NOARGS, NULL}, {"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, NULL}, + {"rotate", (PyCFunction)pg_circle_rotate, METH_FASTCALL, NULL}, + {"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL, NULL}, {NULL, NULL, 0, NULL}}; /* numeric functions */ diff --git a/test/test_circle.py b/test/test_circle.py index 4d011430..5d494ed9 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -12,6 +12,15 @@ E_F = "Expected False, " +def float_range(a, b, step): + result = [] + current_value = a + while current_value < b: + result.append(current_value) + current_value += step + return result + + class CircleTypeTest(unittest.TestCase): def testConstruction_invalid_type(self): """Checks whether passing wrong types to the constructor @@ -1186,6 +1195,185 @@ def test_collidepolygon_no_invalidation(self): self.assertEqual(poly.centerx, poly_copy.centerx) self.assertEqual(poly.centery, poly_copy.centery) + def test_meth_rotate_ip_invalid_argnum(self): + """Ensures that the rotate_ip method correctly deals with invalid numbers of arguments.""" + c = Circle(0, 0, 1) + + with self.assertRaises(TypeError): + c.rotate_ip() + + invalid_args = [ + (1, (2, 2), 2), + (1, (2, 2), 2, 2), + (1, (2, 2), 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2, 2, 2), + ] + + for args in invalid_args: + with self.assertRaises(TypeError): + c.rotate_ip(*args) + + def test_meth_rotate_ip_invalid_argtype(self): + """Ensures that the rotate_ip method correctly deals with invalid argument types.""" + c = Circle(0, 0, 1) + + invalid_args = [ + ("a",), # angle str + (None,), # angle str + ((1, 2)), # angle tuple + ([1, 2]), # angle list + (1, "a"), # origin str + (1, None), # origin None + (1, True), # origin True + (1, False), # origin False + (1, (1, 2, 3)), # origin tuple + (1, [1, 2, 3]), # origin list + (1, (1, "a")), # origin str + (1, ("a", 1)), # origin str + (1, (1, None)), # origin None + (1, (None, 1)), # origin None + (1, (1, (1, 2))), # origin tuple + (1, (1, [1, 2])), # origin list + ] + + for value in invalid_args: + with self.assertRaises(TypeError): + c.rotate_ip(*value) + + def test_meth_rotate_ip_return(self): + """Ensures that the rotate_ip method always returns None.""" + c = Circle(0, 0, 1) + + for angle in float_range(-360, 360, 1): + self.assertIsNone(c.rotate_ip(angle)) + self.assertIsInstance(c.rotate_ip(angle), type(None)) + + def test_meth_rotate_invalid_argnum(self): + """Ensures that the rotate method correctly deals with invalid numbers of arguments.""" + c = Circle(0, 0, 1) + + with self.assertRaises(TypeError): + c.rotate() + + invalid_args = [ + (1, (2, 2), 2), + (1, (2, 2), 2, 2), + (1, (2, 2), 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2, 2), + (1, (2, 2), 2, 2, 2, 2, 2, 2), + ] + + for args in invalid_args: + with self.assertRaises(TypeError): + c.rotate(*args) + + def test_meth_rotate_invalid_argtype(self): + """Ensures that the rotate method correctly deals with invalid argument types.""" + c = Circle(0, 0, 1) + + invalid_args = [ + ("a",), # angle str + (None,), # angle str + ((1, 2)), # angle tuple + ([1, 2]), # angle list + (1, "a"), # origin str + (1, None), # origin None + (1, True), # origin True + (1, False), # origin False + (1, (1, 2, 3)), # origin tuple + (1, [1, 2, 3]), # origin list + (1, (1, "a")), # origin str + (1, ("a", 1)), # origin str + (1, (1, None)), # origin None + (1, (None, 1)), # origin None + (1, (1, (1, 2))), # origin tuple + (1, (1, [1, 2])), # origin list + ] + + for value in invalid_args: + with self.assertRaises(TypeError): + c.rotate(*value) + + def test_meth_rotate_return(self): + """Ensures that the rotate method always returns a Line.""" + c = Circle(0, 0, 1) + + class CircleSubclass(Circle): + pass + + cs = CircleSubclass(0, 0, 1) + + for angle in float_range(-360, 360, 1): + self.assertIsInstance(c.rotate(angle), Circle) + self.assertIsInstance(cs.rotate(angle), CircleSubclass) + + def test_meth_rotate(self): + """Ensures the Circle.rotate() method rotates the Circle correctly.""" + + def rotate_circle(circle: Circle, angle, center): + def rotate_point(x, y, rang, cx, cy): + x -= cx + y -= cy + x_new = x * math.cos(rang) - y * math.sin(rang) + y_new = x * math.sin(rang) + y * math.cos(rang) + return x_new + cx, y_new + cy + + angle = math.radians(angle) + cx, cy = center if center is not None else circle.center + x, y = rotate_point(circle.x, circle.y, angle, cx, cy) + return Circle(x, y, circle.r) + + def assert_approx_equal(circle1, circle2, eps=1e-12): + self.assertAlmostEqual(circle1.x, circle2.x, delta=eps) + self.assertAlmostEqual(circle1.y, circle2.y, delta=eps) + self.assertAlmostEqual(circle1.r, circle2.r, delta=eps) + + c = Circle(0, 0, 1) + angles = float_range(-360, 360, 0.5) + centers = [(a, b) for a in range(-10, 10) for b in range(-10, 10)] + for angle in angles: + assert_approx_equal(c.rotate(angle), rotate_circle(c, angle, None)) + for center in centers: + assert_approx_equal( + c.rotate(angle, center), rotate_circle(c, angle, center) + ) + + def test_meth_rotate_ip(self): + """Ensures the Circle.rotate_ip() method rotates the Circle correctly.""" + + def rotate_circle(circle: Circle, angle, center): + def rotate_point(x, y, rang, cx, cy): + x -= cx + y -= cy + x_new = x * math.cos(rang) - y * math.sin(rang) + y_new = x * math.sin(rang) + y * math.cos(rang) + return x_new + cx, y_new + cy + + angle = math.radians(angle) + cx, cy = center if center is not None else circle.center + x, y = rotate_point(circle.x, circle.y, angle, cx, cy) + circle.x = x + circle.y = y + return circle + + def assert_approx_equal(circle1, circle2, eps=1e-12): + self.assertAlmostEqual(circle1.x, circle2.x, delta=eps) + self.assertAlmostEqual(circle1.y, circle2.y, delta=eps) + self.assertAlmostEqual(circle1.r, circle2.r, delta=eps) + + c = Circle(0, 0, 1) + angles = float_range(-360, 360, 0.5) + centers = [(a, b) for a in range(-10, 10) for b in range(-10, 10)] + for angle in angles: + c.rotate_ip(angle) + assert_approx_equal(c, rotate_circle(c, angle, None)) + for center in centers: + c.rotate_ip(angle, center) + assert_approx_equal(c, rotate_circle(c, angle, center)) + if __name__ == "__main__": unittest.main()