From 19ac3e3fcb13cb4f42cab5d401a45b1e75a03ee2 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 9 Feb 2025 10:12:47 +0100 Subject: [PATCH 01/80] WIP require at least one of the two bodies attached to constraint be dynamic --- CHANGELOG.rst | 3 +++ TODO.txt | 4 ++++ pymunk/body.py | 6 ++++++ pymunk/space.py | 11 ++++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6b5e6d5..e09321be 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ ========= Changelog ========= +.. Pymunk 7.0.0 + Breaking: At least one of the two bodies attached to constraint/join must be dynamic. + .. Pymunk 6.12????? Changes.... diff --git a/TODO.txt b/TODO.txt index 071e83c0..b4abe638 100644 --- a/TODO.txt +++ b/TODO.txt @@ -54,6 +54,10 @@ v6.x - Remove support for pyglet 1.5 in debug draw. Should be fine now that 2.x has been out for a long time. - Think about if Pymunk should assert things that Chipmunk already asserts, like if a body can sleep when calling Body.sleep()? +v7 +--- +- Require at least one body on constraint to be dynamic. + v7+ (all potentially breaking changes) --- - Think about split between pymunk.util and pymunk modules diff --git a/pymunk/body.py b/pymunk/body.py index f41724fd..9e9ef1f8 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -619,6 +619,12 @@ def is_sleeping(self) -> bool: return bool(lib.cpBodyIsSleeping(self._body)) def _set_type(self, body_type: _BodyType) -> None: + if body_type != Body.DYNAMIC: + for c in self.constraints: + assert (c.a != self and c.b.body_type == Body.DYNAMIC) or ( + c.b != self and c.a.body_type == Body.DYNAMIC + ), "Cannot set a non-dynamic body type when Body is connected to a constraint {c} with a non-dynamic other body." + lib.cpBodySetType(self._body, body_type) def _get_type(self) -> _BodyType: diff --git a/pymunk/space.py b/pymunk/space.py index a59df1d8..488cb0dc 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -138,9 +138,9 @@ def spacefree(cp_space: ffi.CData) -> None: self._space = ffi.gc(cp_space, spacefree) - self._handlers: Dict[ - Any, CollisionHandler - ] = {} # To prevent the gc to collect the callbacks. + self._handlers: Dict[Any, CollisionHandler] = ( + {} + ) # To prevent the gc to collect the callbacks. self._post_step_callbacks: Dict[Any, Callable[["Space"], None]] = {} self._removed_shapes: Dict[int, Shape] = {} @@ -450,6 +450,11 @@ def _add_constraint(self, constraint: "Constraint") -> None: """Adds a constraint to the space""" assert constraint not in self._constraints, "Constraint already added to space." + assert ( + constraint.a.body_type == Body.DYNAMIC + or constraint.b.body_type == Body.DYNAMIC + ), "At leasts one of a constraint's bodies must be DYNAMIC." + self._constraints[constraint] = None cp.cpSpaceAddConstraint(self._space, constraint._constraint) From 5993754e43e819db5f877a7db9f1218f52332658 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 14 Feb 2025 22:19:24 +0100 Subject: [PATCH 02/80] Update pygame_util to clarify Pygame-CE can be used --- pymunk/pygame_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pymunk/pygame_util.py b/pymunk/pygame_util.py index ee5d47b6..7c4de82a 100644 --- a/pymunk/pygame_util.py +++ b/pymunk/pygame_util.py @@ -22,7 +22,7 @@ # ---------------------------------------------------------------------------- """This submodule contains helper functions to help with quick prototyping -using pymunk together with pygame. +using pymunk together with pygame or pygame-ce. Intended to help with debugging and prototyping, not for actual production use in a full application. The methods contained in this module is opinionated @@ -77,6 +77,8 @@ class DrawOptions(pymunk.SpaceDebugDrawOptions): def __init__(self, surface: pygame.Surface) -> None: """Draw a pymunk.Space on a pygame.Surface object. + This class should work both with Pygame and Pygame-CE. + Typical usage:: >>> import pymunk From 7716e51575ddb00391cf03b6b25384daed53de22 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 15 Feb 2025 20:14:14 +0100 Subject: [PATCH 03/80] Require that at least ony body is alwayd DYNAMIC on constraints --- pymunk/constraints.py | 5 ++++- pymunk/tests/test_constraint.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pymunk/constraints.py b/pymunk/constraints.py index c4154e63..e729fd1f 100644 --- a/pymunk/constraints.py +++ b/pymunk/constraints.py @@ -71,12 +71,12 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union if TYPE_CHECKING: - from .body import Body from .space import Space from ._chipmunk_cffi import ffi, lib from ._pickle import PickleMixin from ._typing_attr import TypingAttrMixing +from .body import Body from .vec2d import Vec2d _TorqueFunc = Callable[["DampedRotarySpring", float], float] @@ -275,6 +275,9 @@ def post_solve( def _set_bodies(self, a: "Body", b: "Body") -> None: assert a is not b + assert ( + a.body_type == Body.DYNAMIC or b.body_type == Body.DYNAMIC + ), "At least one of the two bodies attached to a constraint must be DYNAMIC." self._a = a self._b = b a._constraints.add(self) diff --git a/pymunk/tests/test_constraint.py b/pymunk/tests/test_constraint.py index 307e5185..23e93c27 100644 --- a/pymunk/tests/test_constraint.py +++ b/pymunk/tests/test_constraint.py @@ -170,6 +170,35 @@ def testPickle(self) -> None: j2 = j.copy() + def test_body_types(self) -> None: + supported = [ + (p.Body.DYNAMIC, p.Body.DYNAMIC), + (p.Body.DYNAMIC, p.Body.KINEMATIC), + (p.Body.DYNAMIC, p.Body.STATIC), + (p.Body.KINEMATIC, p.Body.DYNAMIC), + (p.Body.STATIC, p.Body.DYNAMIC), + ] + non_supported = [ + (p.Body.KINEMATIC, p.Body.KINEMATIC), + (p.Body.KINEMATIC, p.Body.STATIC), + (p.Body.STATIC, p.Body.KINEMATIC), + (p.Body.STATIC, p.Body.STATIC), + ] + + for type1, type2 in supported: + a, b = p.Body(4, 5, body_type=type1), p.Body(10, 10, body_type=type2) + _ = PivotJoint(a, b, (1, 2)) + + for type3, type4 in non_supported: + with self.assertRaises(AssertionError): + a.body_type = type3 + a.body_type = type4 + + for type1, type2 in non_supported: + a, b = p.Body(4, 5, body_type=type1), p.Body(10, 10, body_type=type2) + with self.assertRaises(AssertionError): + _ = PivotJoint(a, b, (1, 2)) + class UnitTestPinJoint(unittest.TestCase): def testAnchor(self) -> None: From a24f93e6b4cac0206f54921f422cf226a95fe309 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 15 Feb 2025 21:33:11 +0100 Subject: [PATCH 04/80] New method ShapeFilter.rejects_collision() #271 --- CHANGELOG.rst | 2 +- pymunk/shape_filter.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3eb8188c..572842e5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Changelog ========= .. Pymunk 7.0.0 - Breaking: At least one of the two bodies attached to constraint/join must be dynamic. + Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. Pymunk 6.11.1 (2025-02-09) diff --git a/pymunk/shape_filter.py b/pymunk/shape_filter.py index a5200ce7..bca858de 100644 --- a/pymunk/shape_filter.py +++ b/pymunk/shape_filter.py @@ -107,3 +107,45 @@ def ALL_MASKS() -> int: @staticmethod def ALL_CATEGORIES() -> int: return 0xFFFFFFFF + + def rejects_collision(self, other: "ShapeFilter") -> bool: + """Return true if this ShapeFilter would reject collisions with other ShapeFilter. + + Two ShapeFilters reject the collision if: + + * They are in the same non-zero group, or + * The category doesnt match the mask of the other shape filter + + See the class documentation for a complete explanation of usecases. + + Groups:: + + >>> ShapeFilter().rejects_collision(ShapeFilter()) + False + >>> ShapeFilter(group=1).rejects_collision(ShapeFilter()) + False + >>> ShapeFilter(group=1).rejects_collision(ShapeFilter(group=1)) + True + + + >>> default = ShapeFilter() + >>> default.rejects_collision(default) + False + >>> f1 = ShapeFilter(categories=0b11, mask=0b11) + >>> f2 = ShapeFilter(categories=0b01, mask=0b11) + >>> f3 = ShapeFilter(categories=0b01, mask=0b10) + >>> f1.rejects_collision(f2) + False + >>> f1.rejects_collision(f3) + False + >>> f2.rejects_collision(f3) + True + """ + + return ( + # they are in the same non-zero group + (self.group != 0 and self.group == other.group) + # One of the category/mask combinations fails. + or (self.categories & other.mask) == 0 + or (other.categories & self.mask) == 0 + ) From f772dedbee6b312833fe183086d599a2e05c06f2 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 18 Feb 2025 18:51:06 +0100 Subject: [PATCH 05/80] improve docs --- pymunk/shape_filter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pymunk/shape_filter.py b/pymunk/shape_filter.py index bca858de..e9152f31 100644 --- a/pymunk/shape_filter.py +++ b/pymunk/shape_filter.py @@ -122,12 +122,14 @@ def rejects_collision(self, other: "ShapeFilter") -> bool: >>> ShapeFilter().rejects_collision(ShapeFilter()) False - >>> ShapeFilter(group=1).rejects_collision(ShapeFilter()) + >>> ShapeFilter(group=1).rejects_collision(ShapeFilter(group=2)) False >>> ShapeFilter(group=1).rejects_collision(ShapeFilter(group=1)) True + Categories and Masks:: + >>> default = ShapeFilter() >>> default.rejects_collision(default) False From e6be610d41eadc206eb6c856a5ea599140e0c1f0 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 20 Feb 2025 23:04:36 +0100 Subject: [PATCH 06/80] Switch Chipmunk2D to Munk2D, part1 --- .gitmodules | 6 +++--- Chipmunk2D | 1 - Munk2D | 1 + pymunk/pymunk_extension_build.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) delete mode 160000 Chipmunk2D create mode 160000 Munk2D diff --git a/.gitmodules b/.gitmodules index 4d311954..7e18cd62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "Chipmunk2D"] - path = Chipmunk2D - url = git@github.com:viblo/Chipmunk2D.git +[submodule "Munk2D"] + path = Munk2D + url = git@github.com:viblo/Munk2D.git diff --git a/Chipmunk2D b/Chipmunk2D deleted file mode 160000 index 7f091aad..00000000 --- a/Chipmunk2D +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7f091aadb1b9e6c0e0a1705c1ed71c1e375eb64d diff --git a/Munk2D b/Munk2D new file mode 160000 index 00000000..aa081e2d --- /dev/null +++ b/Munk2D @@ -0,0 +1 @@ +Subproject commit aa081e2d11f6a68f7d215cb58c3e5b9759379d1b diff --git a/pymunk/pymunk_extension_build.py b/pymunk/pymunk_extension_build.py index 7f84b9ca..04161aad 100644 --- a/pymunk/pymunk_extension_build.py +++ b/pymunk/pymunk_extension_build.py @@ -20,7 +20,7 @@ ffibuilder.cdef(f.read()) hasty_space_include = """#include "chipmunk/cpHastySpace.h" """ -source_folders = [os.path.join("Chipmunk2D", "src")] +source_folders = [os.path.join("Munk2D", "src")] sources = [] for folder in source_folders: for fn in os.listdir(folder): @@ -75,7 +75,7 @@ """, extra_compile_args=extra_compile_args, # extra_link_args=['/DEBUG:FULL'], - include_dirs=[os.path.join("Chipmunk2D", "include")], + include_dirs=[os.path.join("Munk2D", "include")], sources=sources, libraries=libraries, define_macros=[("CP_OVERRIDE_MESSAGE", None)], From 70cb1e44a85db2275c5e780d65d826d7d1c72df6 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 20 Feb 2025 23:18:38 +0100 Subject: [PATCH 07/80] Comment out update body type test until fixed --- pymunk/tests/test_body.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index 0ff1649f..6a22b6c3 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -270,6 +270,7 @@ def test_shapes(self) -> None: self.assertTrue(s2 in b1.shapes) self.assertTrue(s1 not in s.static_body.shapes) + @unittest.skip("Not supported anymore. TODO: Fix the test and reenable") def test_body_type_update(self) -> None: s = p.Space() From 875c3f7295a099871702a9c0eb300e470a465683 Mon Sep 17 00:00:00 2001 From: aatle <168398276+aatle@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:25:21 -0800 Subject: [PATCH 08/80] Make most properties use conventional syntax --- pymunk/arbiter.py | 63 ++++---- pymunk/body.py | 122 +++++++-------- pymunk/collision_handler.py | 4 +- pymunk/constraints.py | 243 +++++++++++++---------------- pymunk/shapes.py | 162 ++++++++----------- pymunk/space.py | 148 ++++++++---------- pymunk/space_debug_draw_options.py | 107 ++++++------- 7 files changed, 367 insertions(+), 482 deletions(-) diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 19882b02..48037a2b 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -38,11 +38,17 @@ def __init__(self, _arbiter: ffi.CData, space: "Space") -> None: self._arbiter = _arbiter self._space = space - def _get_contact_point_set(self) -> ContactPointSet: + @property + def contact_point_set(self) -> ContactPointSet: + """Contact point sets make getting contact information from the + Arbiter simpler. + + Return `ContactPointSet`""" _set = lib.cpArbiterGetContactPointSet(self._arbiter) return ContactPointSet._from_cp(_set) - def _set_contact_point_set(self, point_set: ContactPointSet) -> None: + @contact_point_set.setter + def contact_point_set(self, point_set: ContactPointSet) -> None: # This has to be done by fetching a new Chipmunk point set, update it # according to whats passed in and the pass that back to chipmunk due # to the fact that ContactPointSet doesnt contain a reference to the @@ -63,15 +69,6 @@ def _set_contact_point_set(self, point_set: ContactPointSet) -> None: lib.cpArbiterSetContactPointSet(self._arbiter, ffi.addressof(_set)) - contact_point_set = property( - _get_contact_point_set, - _set_contact_point_set, - doc="""Contact point sets make getting contact information from the - Arbiter simpler. - - Return `ContactPointSet`""", - ) - @property def shapes(self) -> Tuple["Shape", "Shape"]: """Get the shapes in the order that they were defined in the @@ -87,46 +84,40 @@ def shapes(self) -> Tuple["Shape", "Shape"]: assert b is not None return a, b - def _get_restitution(self) -> float: - return lib.cpArbiterGetRestitution(self._arbiter) - - def _set_restitution(self, restitution: float) -> None: - lib.cpArbiterSetRestitution(self._arbiter, restitution) - - restitution = property( - _get_restitution, - _set_restitution, - doc="""The calculated restitution (elasticity) for this collision + @property + def restitution(self) -> float: + """The calculated restitution (elasticity) for this collision pair. - + Setting the value in a pre_solve() callback will override the value calculated by the space. The default calculation multiplies the elasticity of the two shapes together. - """, - ) + """ + return lib.cpArbiterGetRestitution(self._arbiter) - def _get_friction(self) -> float: - return lib.cpArbiterGetFriction(self._arbiter) + @restitution.setter + def restitution(self, restitution: float) -> None: + lib.cpArbiterSetRestitution(self._arbiter, restitution) - def _set_friction(self, friction: float) -> None: - lib.cpArbiterSetFriction(self._arbiter, friction) + @property + def friction(self) -> float: + """The calculated friction for this collision pair. - friction = property( - _get_friction, - _set_friction, - doc="""The calculated friction for this collision pair. - Setting the value in a pre_solve() callback will override the value calculated by the space. The default calculation multiplies the friction of the two shapes together. - """, - ) + """ + return lib.cpArbiterGetFriction(self._arbiter) + + @friction.setter + def friction(self, friction: float) -> None: + lib.cpArbiterSetFriction(self._arbiter, friction) def _get_surface_velocity(self) -> Vec2d: v = lib.cpArbiterGetSurfaceVelocity(self._arbiter) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, velocity: Vec2d) -> None: + def _set_surface_velocity(self, velocity: Tuple[float, float]) -> None: lib.cpArbiterSetSurfaceVelocity(self._arbiter, velocity) surface_velocity = property( diff --git a/pymunk/body.py b/pymunk/body.py index 332b848b..eb03b172 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -252,30 +252,28 @@ def __repr__(self) -> str: else: return "Body(Body.STATIC)" - def _set_mass(self, mass: float) -> None: - lib.cpBodySetMass(self._body, mass) - - def _get_mass(self) -> float: + @property + def mass(self) -> float: + """Mass of the body.""" return lib.cpBodyGetMass(self._body) - mass = property(_get_mass, _set_mass, doc="""Mass of the body.""") + @mass.setter + def mass(self, mass: float) -> None: + lib.cpBodySetMass(self._body, mass) - def _set_moment(self, moment: float) -> None: - lib.cpBodySetMoment(self._body, moment) + @property + def moment(self) -> float: + """Moment of inertia (MoI or sometimes just moment) of the body. - def _get_moment(self) -> float: + The moment is like the rotational mass of a body. + """ return lib.cpBodyGetMoment(self._body) - moment = property( - _get_moment, - _set_moment, - doc="""Moment of inertia (MoI or sometimes just moment) of the body. - - The moment is like the rotational mass of a body. - """, - ) + @moment.setter + def moment(self, moment: float) -> None: + lib.cpBodySetMoment(self._body, moment) - def _set_position(self, pos: Union[Vec2d, Tuple[float, float]]) -> None: + def _set_position(self, pos: Tuple[float, float]) -> None: assert len(pos) == 2 lib.cpBodySetPosition(self._body, pos) @@ -344,16 +342,9 @@ def _get_force(self) -> Vec2d: force applied manually from the apply force functions.""", ) - def _set_angle(self, angle: float) -> None: - lib.cpBodySetAngle(self._body, angle) - - def _get_angle(self) -> float: - return lib.cpBodyGetAngle(self._body) - - angle = property( - _get_angle, - _set_angle, - doc="""Rotation of the body in radians. + @property + def angle(self) -> float: + """Rotation of the body in radians. When changing the rotation you may also want to call :py:func:`Space.reindex_shapes_for_body` to update the collision @@ -365,43 +356,39 @@ def _get_angle(self) -> float: If you get small/no changes to the angle when for example a ball is "rolling" down a slope it might be because the Circle shape attached to the body or the slope shape does not have any friction - set.""", - ) + set.""" + return lib.cpBodyGetAngle(self._body) - def _set_angular_velocity(self, w: float) -> None: - lib.cpBodySetAngularVelocity(self._body, w) + @angle.setter + def angle(self, angle: float) -> None: + lib.cpBodySetAngle(self._body, angle) - def _get_angular_velocity(self) -> float: + @property + def angular_velocity(self) -> float: + """The angular velocity of the body in radians per second.""" return lib.cpBodyGetAngularVelocity(self._body) - angular_velocity = property( - _get_angular_velocity, - _set_angular_velocity, - doc="""The angular velocity of the body in radians per second.""", - ) + @angular_velocity.setter + def angular_velocity(self, w: float) -> None: + lib.cpBodySetAngularVelocity(self._body, w) - def _set_torque(self, t: float) -> None: - lib.cpBodySetTorque(self._body, t) + @property + def torque(self) -> float: + """The torque applied to the body. - def _get_torque(self) -> float: + This value is reset for every time step.""" return lib.cpBodyGetTorque(self._body) - torque = property( - _get_torque, - _set_torque, - doc="""The torque applied to the body. - - This value is reset for every time step.""", - ) + @torque.setter + def torque(self, t: float) -> None: + lib.cpBodySetTorque(self._body, t) - def _get_rotation_vector(self) -> Vec2d: + @property + def rotation_vector(self) -> Vec2d: + """The rotation vector for the body.""" v = lib.cpBodyGetRotation(self._body) return Vec2d(v.x, v.y) - rotation_vector = property( - _get_rotation_vector, doc="""The rotation vector for the body.""" - ) - @property def space(self) -> Optional["Space"]: """Get the :py:class:`Space` that the body has been added to (or @@ -609,7 +596,20 @@ def is_sleeping(self) -> bool: """Returns true if the body is sleeping.""" return bool(lib.cpBodyIsSleeping(self._body)) - def _set_type(self, body_type: _BodyType) -> None: + @property + def body_type(self) -> _BodyType: + """The type of a body (:py:const:`Body.DYNAMIC`, + :py:const:`Body.KINEMATIC` or :py:const:`Body.STATIC`). + + When changing an body to a dynamic body, the mass and moment of + inertia are recalculated from the shapes added to the body. Custom + calculated moments of inertia are not preserved when changing types. + This function cannot be called directly in a collision callback. + """ + return lib.cpBodyGetType(self._body) + + @body_type.setter + def body_type(self, body_type: _BodyType) -> None: if body_type != Body.DYNAMIC: for c in self.constraints: assert (c.a != self and c.b.body_type == Body.DYNAMIC) or ( @@ -618,22 +618,6 @@ def _set_type(self, body_type: _BodyType) -> None: lib.cpBodySetType(self._body, body_type) - def _get_type(self) -> _BodyType: - return lib.cpBodyGetType(self._body) - - body_type = property( - _get_type, - _set_type, - doc="""The type of a body (:py:const:`Body.DYNAMIC`, - :py:const:`Body.KINEMATIC` or :py:const:`Body.STATIC`). - - When changing an body to a dynamic body, the mass and moment of - inertia are recalculated from the shapes added to the body. Custom - calculated moments of inertia are not preserved when changing types. - This function cannot be called directly in a collision callback. - """, - ) - def each_arbiter( self, func: Callable[..., None], # TODO: Fix me once PEP 612 is ready diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index 6f47c54c..9090e76a 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -72,7 +72,7 @@ def data(self) -> Dict[Any, Any]: """ return self._data - def _set_begin(self, func: Callable[[Arbiter, "Space", Any], bool]) -> None: + def _set_begin(self, func: _CollisionCallbackBool) -> None: self._begin = func self._handler.beginFunc = lib.ext_cpCollisionBeginFunc @@ -98,7 +98,7 @@ def _set_pre_solve(self, func: _CollisionCallbackBool) -> None: self._pre_solve = func self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc - def _get_pre_solve(self) -> Optional[Callable[[Arbiter, "Space", Any], bool]]: + def _get_pre_solve(self) -> Optional[_CollisionCallbackBool]: return self._pre_solve pre_solve = property( diff --git a/pymunk/constraints.py b/pymunk/constraints.py index e729fd1f..1812704f 100644 --- a/pymunk/constraints.py +++ b/pymunk/constraints.py @@ -120,32 +120,22 @@ def constraintfree(cp_constraint: ffi.CData) -> None: self._data_handle = d # to prevent gc to collect the handle lib.cpConstraintSetUserData(self._constraint, d) - def _get_max_force(self) -> float: - return lib.cpConstraintGetMaxForce(self._constraint) - - def _set_max_force(self, f: float) -> None: - lib.cpConstraintSetMaxForce(self._constraint, f) - - max_force = property( - _get_max_force, - _set_max_force, - doc="""The maximum force that the constraint can use to act on the two + @property + def max_force(self) -> float: + """The maximum force that the constraint can use to act on the two bodies. Defaults to infinity - """, - ) - - def _get_error_bias(self) -> float: - return lib.cpConstraintGetErrorBias(self._constraint) + """ + return lib.cpConstraintGetMaxForce(self._constraint) - def _set_error_bias(self, error_bias: float) -> None: - lib.cpConstraintSetErrorBias(self._constraint, error_bias) + @max_force.setter + def max_force(self, f: float) -> None: + lib.cpConstraintSetMaxForce(self._constraint, f) - error_bias = property( - _get_error_bias, - _set_error_bias, - doc="""The percentage of joint error that remains unfixed after a + @property + def error_bias(self) -> float: + """The percentage of joint error that remains unfixed after a second. This works exactly the same as the collision bias property of a space, @@ -154,42 +144,40 @@ def _set_error_bias(self, error_bias: float) -> None: Defaults to pow(1.0 - 0.1, 60.0) meaning that it will correct 10% of the error every 1/60th of a second. - """, - ) - - def _get_max_bias(self) -> float: - return lib.cpConstraintGetMaxBias(self._constraint) + """ + return lib.cpConstraintGetErrorBias(self._constraint) - def _set_max_bias(self, max_bias: float) -> None: - lib.cpConstraintSetMaxBias(self._constraint, max_bias) + @error_bias.setter + def error_bias(self, error_bias: float) -> None: + lib.cpConstraintSetErrorBias(self._constraint, error_bias) - max_bias = property( - _get_max_bias, - _set_max_bias, - doc="""The maximum speed at which the constraint can apply error + @property + def max_bias(self) -> float: + """The maximum speed at which the constraint can apply error correction. Defaults to infinity - """, - ) - - def _get_collide_bodies(self) -> bool: - return lib.cpConstraintGetCollideBodies(self._constraint) + """ + return lib.cpConstraintGetMaxBias(self._constraint) - def _set_collide_bodies(self, collide_bodies: bool) -> None: - lib.cpConstraintSetCollideBodies(self._constraint, collide_bodies) + @max_bias.setter + def max_bias(self, max_bias: float) -> None: + lib.cpConstraintSetMaxBias(self._constraint, max_bias) - collide_bodies = property( - _get_collide_bodies, - _set_collide_bodies, - doc="""Constraints can be used for filtering collisions too. + @property + def collide_bodies(self) -> bool: + """Constraints can be used for filtering collisions too. When two bodies collide, Pymunk ignores the collisions if this property is set to False on any constraint that connects the two bodies. Defaults to True. This can be used to create a chain that self collides, but adjacent links in the chain do not collide. - """, - ) + """ + return lib.cpConstraintGetCollideBodies(self._constraint) + + @collide_bodies.setter + def collide_bodies(self, collide_bodies: bool) -> None: + lib.cpConstraintSetCollideBodies(self._constraint, collide_bodies) @property def impulse(self) -> float: @@ -358,14 +346,14 @@ def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: anchor_b = property(_get_anchor_b, _set_anchor_b) - def _get_distance(self) -> float: + @property + def distance(self) -> float: return lib.cpPinJointGetDist(self._constraint) - def _set_distance(self, distance: float) -> None: + @distance.setter + def distance(self, distance: float) -> None: lib.cpPinJointSetDist(self._constraint, distance) - distance = property(_get_distance, _set_distance) - class SlideJoint(Constraint): """SlideJoint is like a PinJoint, but have a minimum and maximum distance. @@ -421,22 +409,22 @@ def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: anchor_b = property(_get_anchor_b, _set_anchor_b) - def _get_min(self) -> float: + @property + def min(self) -> float: return lib.cpSlideJointGetMin(self._constraint) - def _set_min(self, min: float) -> None: + @min.setter + def min(self, min: float) -> None: lib.cpSlideJointSetMin(self._constraint, min) - min = property(_get_min, _set_min) - - def _get_max(self) -> float: + @property + def max(self) -> float: return lib.cpSlideJointGetMax(self._constraint) - def _set_max(self, max: float) -> None: + @max.setter + def max(self, max: float) -> None: lib.cpSlideJointSetMax(self._constraint, max) - max = property(_get_max, _set_max) - class PivotJoint(Constraint): """PivotJoint allow two objects to pivot about a single point. @@ -642,40 +630,33 @@ def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: anchor_b = property(_get_anchor_b, _set_anchor_b) - def _get_rest_length(self) -> float: + @property + def rest_length(self) -> float: + """The distance the spring wants to be.""" return lib.cpDampedSpringGetRestLength(self._constraint) - def _set_rest_length(self, rest_length: float) -> None: + @rest_length.setter + def rest_length(self, rest_length: float) -> None: lib.cpDampedSpringSetRestLength(self._constraint, rest_length) - rest_length = property( - _get_rest_length, - _set_rest_length, - doc="""The distance the spring wants to be.""", - ) - - def _get_stiffness(self) -> float: + @property + def stiffness(self) -> float: + """The spring constant (Young's modulus).""" return lib.cpDampedSpringGetStiffness(self._constraint) - def _set_stiffness(self, stiffness: float) -> None: + @stiffness.setter + def stiffness(self, stiffness: float) -> None: lib.cpDampedSpringSetStiffness(self._constraint, stiffness) - stiffness = property( - _get_stiffness, _set_stiffness, doc="""The spring constant (Young's modulus).""" - ) - - def _get_damping(self) -> float: + @property + def damping(self) -> float: + """How soft to make the damping of the spring.""" return lib.cpDampedSpringGetDamping(self._constraint) - def _set_damping(self, damping: float) -> None: + @damping.setter + def damping(self, damping: float) -> None: lib.cpDampedSpringSetDamping(self._constraint, damping) - damping = property( - _get_damping, - _set_damping, - doc="""How soft to make the damping of the spring.""", - ) - @staticmethod def spring_force(spring: "DampedSpring", dist: float) -> float: """Default damped spring force function.""" @@ -737,40 +718,33 @@ def __init__( ) self._init(a, b, _constraint) - def _get_rest_angle(self) -> float: + @property + def rest_angle(self) -> float: + """The relative angle in radians that the bodies want to have""" return lib.cpDampedRotarySpringGetRestAngle(self._constraint) - def _set_rest_angle(self, rest_angle: float) -> None: + @rest_angle.setter + def rest_angle(self, rest_angle: float) -> None: lib.cpDampedRotarySpringSetRestAngle(self._constraint, rest_angle) - rest_angle = property( - _get_rest_angle, - _set_rest_angle, - doc="""The relative angle in radians that the bodies want to have""", - ) - - def _get_stiffness(self) -> float: + @property + def stiffness(self) -> float: + """The spring constant (Young's modulus).""" return lib.cpDampedRotarySpringGetStiffness(self._constraint) - def _set_stiffness(self, stiffness: float) -> None: + @stiffness.setter + def stiffness(self, stiffness: float) -> None: lib.cpDampedRotarySpringSetStiffness(self._constraint, stiffness) - stiffness = property( - _get_stiffness, _set_stiffness, doc="""The spring constant (Young's modulus).""" - ) - - def _get_damping(self) -> float: + @property + def damping(self) -> float: + """How soft to make the damping of the spring.""" return lib.cpDampedRotarySpringGetDamping(self._constraint) - def _set_damping(self, damping: float) -> None: + @damping.setter + def damping(self, damping: float) -> None: lib.cpDampedRotarySpringSetDamping(self._constraint, damping) - damping = property( - _get_damping, - _set_damping, - doc="""How soft to make the damping of the spring.""", - ) - @staticmethod def spring_torque(spring: "DampedRotarySpring", relative_angle: float) -> float: """Default damped rotary spring torque function.""" @@ -819,22 +793,22 @@ def __init__(self, a: "Body", b: "Body", min: float, max: float) -> None: _constraint = lib.cpRotaryLimitJointNew(a._body, b._body, min, max) self._init(a, b, _constraint) - def _get_min(self) -> float: + @property + def min(self) -> float: return lib.cpRotaryLimitJointGetMin(self._constraint) - def _set_min(self, min: float) -> None: + @min.setter + def min(self, min: float) -> None: lib.cpRotaryLimitJointSetMin(self._constraint, min) - min = property(_get_min, _set_min) - - def _get_max(self) -> float: + @property + def max(self) -> float: return lib.cpRotaryLimitJointGetMax(self._constraint) - def _set_max(self, max: float) -> None: + @max.setter + def max(self, max: float) -> None: lib.cpRotaryLimitJointSetMax(self._constraint, max) - max = property(_get_max, _set_max) - class RatchetJoint(Constraint): """RatchetJoint is a rotary ratchet, it works like a socket wrench.""" @@ -850,30 +824,30 @@ def __init__(self, a: "Body", b: "Body", phase: float, ratchet: float) -> None: _constraint = lib.cpRatchetJointNew(a._body, b._body, phase, ratchet) self._init(a, b, _constraint) - def _get_angle(self) -> float: + @property + def angle(self) -> float: return lib.cpRatchetJointGetAngle(self._constraint) - def _set_angle(self, angle: float) -> None: + @angle.setter + def angle(self, angle: float) -> None: lib.cpRatchetJointSetAngle(self._constraint, angle) - angle = property(_get_angle, _set_angle) - - def _get_phase(self) -> float: + @property + def phase(self) -> float: return lib.cpRatchetJointGetPhase(self._constraint) - def _set_phase(self, phase: float) -> None: + @phase.setter + def phase(self, phase: float) -> None: lib.cpRatchetJointSetPhase(self._constraint, phase) - phase = property(_get_phase, _set_phase) - - def _get_ratchet(self) -> float: + @property + def ratchet(self) -> float: return lib.cpRatchetJointGetRatchet(self._constraint) - def _set_ratchet(self, ratchet: float) -> None: + @ratchet.setter + def ratchet(self, ratchet: float) -> None: lib.cpRatchetJointSetRatchet(self._constraint, ratchet) - ratchet = property(_get_ratchet, _set_ratchet) - class GearJoint(Constraint): """GearJoint keeps the angular velocity ratio of a pair of bodies constant.""" @@ -890,22 +864,22 @@ def __init__(self, a: "Body", b: "Body", phase: float, ratio: float): _constraint = lib.cpGearJointNew(a._body, b._body, phase, ratio) self._init(a, b, _constraint) - def _get_phase(self) -> float: + @property + def phase(self) -> float: return lib.cpGearJointGetPhase(self._constraint) - def _set_phase(self, phase: float) -> None: + @phase.setter + def phase(self, phase: float) -> None: lib.cpGearJointSetPhase(self._constraint, phase) - phase = property(_get_phase, _set_phase) - - def _get_ratio(self) -> float: + @property + def ratio(self) -> float: return lib.cpGearJointGetRatio(self._constraint) - def _set_ratio(self, ratio: float) -> None: + @ratio.setter + def ratio(self, ratio: float) -> None: lib.cpGearJointSetRatio(self._constraint, ratio) - ratio = property(_get_ratio, _set_ratio) - class SimpleMotor(Constraint): """SimpleMotor keeps the relative angular velocity constant.""" @@ -922,12 +896,11 @@ def __init__(self, a: "Body", b: "Body", rate: float) -> None: _constraint = lib.cpSimpleMotorNew(a._body, b._body, rate) self._init(a, b, _constraint) - def _get_rate(self) -> float: + @property + def rate(self) -> float: + """The desired relative angular velocity""" return lib.cpSimpleMotorGetRate(self._constraint) - def _set_rate(self, rate: float) -> None: + @rate.setter + def rate(self, rate: float) -> None: lib.cpSimpleMotorSetRate(self._constraint, rate) - - rate = property( - _get_rate, _set_rate, doc="""The desired relative angular velocity""" - ) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 5ef4dd27..a30c2951 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -82,40 +82,34 @@ def _set_id(self) -> None: cp.cpShapeSetUserData(self._shape, ffi.cast("cpDataPointer", Shape._id_counter)) Shape._id_counter += 1 - def _get_mass(self) -> float: + @property + def mass(self) -> float: + """The mass of this shape. + + This is useful when you let Pymunk calculate the total mass and inertia + of a body from the shapes attached to it. (Instead of setting the body + mass and inertia directly) + """ return cp.cpShapeGetMass(self._shape) - def _set_mass(self, mass: float) -> None: + @mass.setter + def mass(self, mass: float) -> None: cp.cpShapeSetMass(self._shape, mass) - mass = property( - _get_mass, - _set_mass, - doc="""The mass of this shape. + @property + def density(self) -> float: + """The density of this shape. This is useful when you let Pymunk calculate the total mass and inertia of a body from the shapes attached to it. (Instead of setting the body mass and inertia directly) - """, - ) - - def _get_density(self) -> float: + """ return cp.cpShapeGetDensity(self._shape) - def _set_density(self, density: float) -> None: + @density.setter + def density(self, density: float) -> None: cp.cpShapeSetDensity(self._shape, density) - density = property( - _get_density, - _set_density, - doc="""The density of this shape. - - This is useful when you let Pymunk calculate the total mass and inertia - of a body from the shapes attached to it. (Instead of setting the body - mass and inertia directly) - """, - ) - @property def moment(self) -> float: """The calculated moment of this shape.""" @@ -132,79 +126,60 @@ def center_of_gravity(self) -> Vec2d: v = cp.cpShapeGetCenterOfGravity(self._shape) return Vec2d(v.x, v.y) - def _get_sensor(self) -> bool: - return bool(cp.cpShapeGetSensor(self._shape)) - - def _set_sensor(self, is_sensor: bool) -> None: - cp.cpShapeSetSensor(self._shape, is_sensor) - - sensor = property( - _get_sensor, - _set_sensor, - doc="""A boolean value if this shape is a sensor or not. + @property + def sensor(self) -> bool: + """A boolean value if this shape is a sensor or not. Sensors only call collision callbacks, and never generate real collisions. - """, - ) - - def _get_collision_type(self) -> int: - return cp.cpShapeGetCollisionType(self._shape) + """ + return bool(cp.cpShapeGetSensor(self._shape)) - def _set_collision_type(self, t: int) -> None: - cp.cpShapeSetCollisionType(self._shape, t) + @sensor.setter + def sensor(self, is_sensor: bool) -> None: + cp.cpShapeSetSensor(self._shape, is_sensor) - collision_type = property( - _get_collision_type, - _set_collision_type, - doc="""User defined collision type for the shape. + @property + def collision_type(self) -> int: + """User defined collision type for the shape. See :py:meth:`Space.add_collision_handler` function for more information on when to use this property. - """, - ) + """ + return cp.cpShapeGetCollisionType(self._shape) + + @collision_type.setter + def collision_type(self, t: int) -> None: + cp.cpShapeSetCollisionType(self._shape, t) - def _get_filter(self) -> ShapeFilter: + @property + def filter(self) -> ShapeFilter: + """Set the collision :py:class:`ShapeFilter` for this shape. + """ f = cp.cpShapeGetFilter(self._shape) return ShapeFilter(f.group, f.categories, f.mask) - def _set_filter(self, f: ShapeFilter) -> None: + @filter.setter + def filter(self, f: ShapeFilter) -> None: cp.cpShapeSetFilter(self._shape, f) - filter = property( - _get_filter, - _set_filter, - doc="""Set the collision :py:class:`ShapeFilter` for this shape. - """, - ) - - def _get_elasticity(self) -> float: - return cp.cpShapeGetElasticity(self._shape) - - def _set_elasticity(self, e: float) -> None: - cp.cpShapeSetElasticity(self._shape, e) - - elasticity = property( - _get_elasticity, - _set_elasticity, - doc="""Elasticity of the shape. + @property + def elasticity(self) -> float: + """Elasticity of the shape. A value of 0.0 gives no bounce, while a value of 1.0 will give a 'perfect' bounce. However due to inaccuracies in the simulation using 1.0 or greater is not recommended. - """, - ) - - def _get_friction(self) -> float: - return cp.cpShapeGetFriction(self._shape) + """ + return cp.cpShapeGetElasticity(self._shape) - def _set_friction(self, u: float) -> None: - cp.cpShapeSetFriction(self._shape, u) + @elasticity.setter + def elasticity(self, e: float) -> None: + cp.cpShapeSetElasticity(self._shape, e) - friction = property( - _get_friction, - _set_friction, - doc="""Friction coefficient. + @property + def friction(self) -> float: + """Friction coefficient. Pymunk uses the Coulomb friction model, a value of 0.0 is frictionless. @@ -234,14 +209,18 @@ def _set_friction(self, u: float) -> None: Teflon (PTFE) Teflon 0.04 Wood Wood 0.4 ============== ====== ======== - """, - ) + """ + return cp.cpShapeGetFriction(self._shape) + + @friction.setter + def friction(self, u: float) -> None: + cp.cpShapeSetFriction(self._shape, u) def _get_surface_velocity(self) -> Vec2d: v = cp.cpShapeGetSurfaceVelocity(self._shape) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, surface_v: Vec2d) -> None: + def _set_surface_velocity(self, surface_v: Tuple[float, float]) -> None: assert len(surface_v) == 2 cp.cpShapeSetSurfaceVelocity(self._shape, surface_v) @@ -256,10 +235,14 @@ def _set_surface_velocity(self, surface_v: Vec2d) -> None: """, ) - def _get_body(self) -> Optional["Body"]: + @property + def body(self) -> Optional["Body"]: + """The body this shape is attached to. Can be set to None to + indicate that this shape doesnt belong to a body.""" return self._body - def _set_body(self, body: Optional["Body"]) -> None: + @body.setter + def body(self, body: Optional["Body"]) -> None: if self._body is not None: self._body._shapes.remove(self) body_body = ffi.NULL if body is None else body._body @@ -268,13 +251,6 @@ def _set_body(self, body: Optional["Body"]) -> None: body._shapes.add(self) self._body = body - body = property( - _get_body, - _set_body, - doc="""The body this shape is attached to. Can be set to None to - indicate that this shape doesnt belong to a body.""", - ) - def update(self, transform: Transform) -> BB: """Update, cache and return the bounding box of a shape with an explicit transformation. @@ -492,18 +468,18 @@ def __init__( _shape = cp.cpSegmentShapeNew(body_body, a, b, radius) self._init(body, _shape) - def _get_a(self) -> Vec2d: + @property + def a(self) -> Vec2d: + """The first of the two endpoints for this segment""" v = cp.cpSegmentShapeGetA(self._shape) return Vec2d(v.x, v.y) - a = property(_get_a, doc="""The first of the two endpoints for this segment""") - - def _get_b(self) -> Vec2d: + @property + def b(self) -> Vec2d: + """The second of the two endpoints for this segment""" v = cp.cpSegmentShapeGetB(self._shape) return Vec2d(v.x, v.y) - b = property(_get_b, doc="""The second of the two endpoints for this segment""") - def unsafe_set_endpoints( self, a: Tuple[float, float], b: Tuple[float, float] ) -> None: diff --git a/pymunk/space.py b/pymunk/space.py index 8d8a3987..fc43cf56 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -187,16 +187,9 @@ def static_body(self) -> Body: # assert self._static_body is not None return self._static_body - def _set_iterations(self, value: int) -> None: - cp.cpSpaceSetIterations(self._space, value) - - def _get_iterations(self) -> int: - return cp.cpSpaceGetIterations(self._space) - - iterations = property( - _get_iterations, - _set_iterations, - doc="""Iterations allow you to control the accuracy of the solver. + @property + def iterations(self) -> int: + """Iterations allow you to control the accuracy of the solver. Defaults to 10. @@ -212,8 +205,12 @@ def _get_iterations(self) -> int: the number of iterations lets you balance between CPU usage and the accuracy of the physics. Pymunk's default of 10 iterations is sufficient for most simple games. - """, - ) + """ + return cp.cpSpaceGetIterations(self._space) + + @iterations.setter + def iterations(self, value: int) -> None: + cp.cpSpaceSetIterations(self._space, value) def _set_gravity(self, gravity_vector: Tuple[float, float]) -> None: assert len(gravity_vector) == 2 @@ -234,81 +231,62 @@ def _get_gravity(self) -> Vec2d: """, ) - def _set_damping(self, damping: float) -> None: - cp.cpSpaceSetDamping(self._space, damping) - - def _get_damping(self) -> float: - return cp.cpSpaceGetDamping(self._space) - - damping = property( - _get_damping, - _set_damping, - doc="""Amount of simple damping to apply to the space. + @property + def damping(self) -> float: + """Amount of simple damping to apply to the space. A value of 0.9 means that each body will lose 10% of its velocity per second. Defaults to 1. Like gravity, it can be overridden on a per body basis. - """, - ) - - def _set_idle_speed_threshold(self, idle_speed_threshold: float) -> None: - cp.cpSpaceSetIdleSpeedThreshold(self._space, idle_speed_threshold) + """ + return cp.cpSpaceGetDamping(self._space) - def _get_idle_speed_threshold(self) -> float: - return cp.cpSpaceGetIdleSpeedThreshold(self._space) + @damping.setter + def damping(self, damping: float) -> None: + cp.cpSpaceSetDamping(self._space, damping) - idle_speed_threshold = property( - _get_idle_speed_threshold, - _set_idle_speed_threshold, - doc="""Speed threshold for a body to be considered idle. + @property + def idle_speed_threshold(self) -> float: + """Speed threshold for a body to be considered idle. The default value of 0 means the space estimates a good threshold based on gravity. - """, - ) - - def _set_sleep_time_threshold(self, sleep_time_threshold: float) -> None: - cp.cpSpaceSetSleepTimeThreshold(self._space, sleep_time_threshold) + """ + return cp.cpSpaceGetIdleSpeedThreshold(self._space) - def _get_sleep_time_threshold(self) -> float: - return cp.cpSpaceGetSleepTimeThreshold(self._space) + @idle_speed_threshold.setter + def idle_speed_threshold(self, idle_speed_threshold: float) -> None: + cp.cpSpaceSetIdleSpeedThreshold(self._space, idle_speed_threshold) - sleep_time_threshold = property( - _get_sleep_time_threshold, - _set_sleep_time_threshold, - doc="""Time a group of bodies must remain idle in order to fall + @property + def sleep_time_threshold(self) -> float: + """Time a group of bodies must remain idle in order to fall asleep. The default value of `inf` disables the sleeping algorithm. - """, - ) - - def _set_collision_slop(self, collision_slop: float) -> None: - cp.cpSpaceSetCollisionSlop(self._space, collision_slop) + """ + return cp.cpSpaceGetSleepTimeThreshold(self._space) - def _get_collision_slop(self) -> float: - return cp.cpSpaceGetCollisionSlop(self._space) + @sleep_time_threshold.setter + def sleep_time_threshold(self, sleep_time_threshold: float) -> None: + cp.cpSpaceSetSleepTimeThreshold(self._space, sleep_time_threshold) - collision_slop = property( - _get_collision_slop, - _set_collision_slop, - doc="""Amount of overlap between shapes that is allowed. + @property + def collision_slop(self) -> float: + """Amount of overlap between shapes that is allowed. To improve stability, set this as high as you can without noticeable overlapping. It defaults to 0.1. - """, - ) - - def _set_collision_bias(self, collision_bias: float) -> None: - cp.cpSpaceSetCollisionBias(self._space, collision_bias) + """ + return cp.cpSpaceGetCollisionSlop(self._space) - def _get_collision_bias(self) -> float: - return cp.cpSpaceGetCollisionBias(self._space) + @collision_slop.setter + def collision_slop(self, collision_slop: float) -> None: + cp.cpSpaceSetCollisionSlop(self._space, collision_slop) - collision_bias = property( - _get_collision_bias, - _set_collision_bias, - doc="""Determines how fast overlapping shapes are pushed apart. + @property + def collision_bias(self) -> float: + """Determines how fast overlapping shapes are pushed apart. Pymunk allows fast moving objects to overlap, then fixes the overlap over time. Overlapping objects are unavoidable even if swept @@ -322,19 +300,16 @@ def _get_collision_bias(self) -> float: ..Note:: Very very few games will need to change this value. - """, - ) - - def _set_collision_persistence(self, collision_persistence: float) -> None: - cp.cpSpaceSetCollisionPersistence(self._space, collision_persistence) + """ + return cp.cpSpaceGetCollisionBias(self._space) - def _get_collision_persistence(self) -> float: - return cp.cpSpaceGetCollisionPersistence(self._space) + @collision_bias.setter + def collision_bias(self, collision_bias: float) -> None: + cp.cpSpaceSetCollisionBias(self._space, collision_bias) - collision_persistence = property( - _get_collision_persistence, - _set_collision_persistence, - doc="""The number of frames the space keeps collision solutions + @property + def collision_persistence(self) -> float: + """The number of frames the space keeps collision solutions around for. Helps prevent jittering contacts from getting worse. This defaults @@ -342,19 +317,20 @@ def _get_collision_persistence(self) -> float: ..Note:: Very very few games will need to change this value. - """, - ) + """ + return cp.cpSpaceGetCollisionPersistence(self._space) - def _get_current_time_step(self) -> float: - return cp.cpSpaceGetCurrentTimeStep(self._space) + @collision_persistence.setter + def collision_persistence(self, collision_persistence: float) -> None: + cp.cpSpaceSetCollisionPersistence(self._space, collision_persistence) - current_time_step = property( - _get_current_time_step, - doc="""Retrieves the current (if you are in a callback from + @property + def current_time_step(self) -> float: + """Retrieves the current (if you are in a callback from Space.step()) or most recent (outside of a Space.step() call) timestep. - """, - ) + """ + return cp.cpSpaceGetCurrentTimeStep(self._space) def add(self, *objs: _AddableObjects) -> None: """Add one or many shapes, bodies or constraints (joints) to the space diff --git a/pymunk/space_debug_draw_options.py b/pymunk/space_debug_draw_options.py index 92374c21..a76c16c7 100644 --- a/pymunk/space_debug_draw_options.py +++ b/pymunk/space_debug_draw_options.py @@ -98,16 +98,9 @@ def __init__(self) -> None: | SpaceDebugDrawOptions.DRAW_COLLISION_POINTS ) - def _get_shape_outline_color(self) -> SpaceDebugColor: - return self._c(self._options.shapeOutlineColor) - - def _set_shape_outline_color(self, c: SpaceDebugColor) -> None: - self._options.shapeOutlineColor = c - - shape_outline_color = property( - _get_shape_outline_color, - _set_shape_outline_color, - doc="""The outline color of shapes. + @property + def shape_outline_color(self) -> SpaceDebugColor: + """The outline color of shapes. Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). @@ -124,22 +117,19 @@ def _set_shape_outline_color(self, c: SpaceDebugColor) -> None: >>> s.debug_draw(options) draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=10.0, g=20.0, b=30.0, a=40.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) - """, - ) - - def _get_constraint_color(self) -> SpaceDebugColor: - return self._c(self._options.constraintColor) + """ + return self._c(self._options.shapeOutlineColor) - def _set_constraint_color(self, c: SpaceDebugColor) -> None: - self._options.constraintColor = c + @shape_outline_color.setter + def shape_outline_color(self, c: SpaceDebugColor) -> None: + self._options.shapeOutlineColor = c - constraint_color = property( - _get_constraint_color, - _set_constraint_color, - doc="""The color of constraints. + @property + def constraint_color(self) -> SpaceDebugColor: + """The color of constraints. Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). - + Example: >>> import pymunk @@ -156,19 +146,16 @@ def _set_constraint_color(self, c: SpaceDebugColor) -> None: draw_dot (5.0, Vec2d(0.0, 0.0), SpaceDebugColor(r=10.0, g=20.0, b=30.0, a=40.0)) draw_dot (5.0, Vec2d(0.0, 0.0), SpaceDebugColor(r=10.0, g=20.0, b=30.0, a=40.0)) - """, - ) - - def _get_collision_point_color(self) -> SpaceDebugColor: - return self._c(self._options.collisionPointColor) + """ + return self._c(self._options.constraintColor) - def _set_collision_point_color(self, c: SpaceDebugColor) -> None: - self._options.collisionPointColor = c + @constraint_color.setter + def constraint_color(self, c: SpaceDebugColor) -> None: + self._options.constraintColor = c - collision_point_color = property( - _get_collision_point_color, - _set_collision_point_color, - doc="""The color of collisions. + @property + def collision_point_color(self) -> SpaceDebugColor: + """The color of collisions. Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). @@ -191,8 +178,12 @@ def _set_collision_point_color(self, c: SpaceDebugColor) -> None: draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=52.0, g=152.0, b=219.0, a=255.0)) draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) draw_segment (Vec2d(8.0, 0.0), Vec2d(-8.0, 0.0), SpaceDebugColor(r=10.0, g=20.0, b=30.0, a=40.0)) - """, - ) + """ + return self._c(self._options.collisionPointColor) + + @collision_point_color.setter + def collision_point_color(self, c: SpaceDebugColor) -> None: + self._options.collisionPointColor = c def __enter__(self) -> None: pass @@ -208,16 +199,9 @@ def __exit__( def _c(self, color: ffi.CData) -> SpaceDebugColor: return SpaceDebugColor(color.r, color.g, color.b, color.a) - def _get_flags(self) -> _DrawFlags: - return self._options.flags - - def _set_flags(self, f: _DrawFlags) -> None: - self._options.flags = f - - flags = property( - _get_flags, - _set_flags, - doc="""Bit flags which of shapes, joints and collisions should be drawn. + @property + def flags(self) -> _DrawFlags: + """Bit flags which of shapes, joints and collisions should be drawn. By default all 3 flags are set, meaning shapes, joints and collisions will be drawn. @@ -235,7 +219,7 @@ def _set_flags(self, f: _DrawFlags) -> None: >>> s.add(pymunk.Circle(s.static_body, 3)) >>> s.step(0.01) >>> options = pymunk.SpaceDebugDrawOptions() - + >>> # Only draw the shapes, nothing else: >>> options.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES >>> s.debug_draw(options) @@ -249,21 +233,17 @@ def _set_flags(self, f: _DrawFlags) -> None: draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=52.0, g=152.0, b=219.0, a=255.0)) draw_circle (Vec2d(0.0, 0.0), 0.0, 3.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) draw_segment (Vec2d(1.0, 0.0), Vec2d(-8.0, 0.0), SpaceDebugColor(r=231.0, g=76.0, b=60.0, a=255.0)) - - """, - ) - def _get_transform(self) -> Transform: - t = self._options.transform - return Transform(t.a, t.b, t.c, t.d, t.tx, t.ty) + """ + return self._options.flags - def _set_transform(self, t: Transform) -> None: - self._options.transform = t + @flags.setter + def flags(self, f: _DrawFlags) -> None: + self._options.flags = f - transform = property( - _get_transform, - _set_transform, - doc="""The transform is applied before drawing, e.g for scaling or + @property + def transform(self) -> Transform: + """The transform is applied before drawing, e.g for scaling or translation. Example: @@ -281,13 +261,18 @@ def _set_transform(self, t: Transform) -> None: >>> options.transform = pymunk.Transform.translation(2,3) >>> s.debug_draw(options) draw_circle (Vec2d(2.0, 3.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) - + .. Note:: Not all tranformations are supported by the debug drawing logic. Uniform scaling and translation are supported, but not rotation, linear stretching or shearing. - """, - ) + """ + t = self._options.transform + return Transform(t.a, t.b, t.c, t.d, t.tx, t.ty) + + @transform.setter + def transform(self, t: Transform) -> None: + self._options.transform = t def draw_circle( self, From cb2853bcd3da0086cf4a02b1564b3f9d5189935f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 21 Feb 2025 21:42:42 +0100 Subject: [PATCH 09/80] Add __bool__ to Vec2d to allow testing if 0 --- pymunk/vec2d.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 23dd45bf..19747fc8 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -244,6 +244,18 @@ def __abs__(self) -> float: """ return self.length + def __bool__(self) -> bool: + """Return true if both x and y are nonzero. + + >>> bool(Vec2d(1, 0)) + True + >>> bool(Vec2d(-1, -2)) + True + >>> bool(Vec2d(0, 0)) + False + """ + return self.x != 0 or self.y != 0 + # vectory functions def get_length_sqrd(self) -> float: """Get the squared length of the vector. From 949a367cc71fd614030859e82cafc1990b991b06 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 21 Feb 2025 21:50:12 +0100 Subject: [PATCH 10/80] Return floats if possible in Vec2d and optimize Vec2d.angle #274 --- CHANGELOG.rst | 3 +++ pymunk/vec2d.py | 28 +++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 572842e5..922b2195 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,9 @@ Changelog ========= .. Pymunk 7.0.0 Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. + New feature: ShapeFilter.rejects_collision() method + New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) + Optimized Vec2d.angle and Vec2d.angle_degrees Pymunk 6.11.1 (2025-02-09) diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 19747fc8..3066c5df 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -330,10 +330,8 @@ def angle(self) -> float: >>> '%.2f' % Vec2d(-1, 0).angle '3.14' >>> Vec2d(0, 0).angle - 0 + 0.0 """ - if self.get_length_sqrd() == 0: - return 0 return math.atan2(self.y, self.x) @property @@ -383,12 +381,12 @@ def normalized(self) -> "Vec2d": >>> Vec2d(3, 4).normalized() Vec2d(0.6, 0.8) >>> Vec2d(0, 0).normalized() - Vec2d(0, 0) + Vec2d(0.0, 0.0) """ length = self.length if length != 0: return self / length - return Vec2d(0, 0) + return Vec2d(0.0, 0.0) def normalized_and_length(self) -> Tuple["Vec2d", float]: """Normalize the vector and return its length before the normalization. @@ -398,12 +396,12 @@ def normalized_and_length(self) -> Tuple["Vec2d", float]: >>> Vec2d(3, 4).normalized_and_length() (Vec2d(0.6, 0.8), 5.0) >>> Vec2d(0, 0).normalized_and_length() - (Vec2d(0, 0), 0) + (Vec2d(0.0, 0.0), 0.0) """ length = self.length if length != 0: return self / length, length - return Vec2d(0, 0), 0 + return Vec2d(0.0, 0.0), 0.0 def perpendicular(self) -> "Vec2d": """Get a vertical vector rotated 90 degrees counterclockwise from the original vector. @@ -479,12 +477,12 @@ def projection(self, other: Tuple[float, float]) -> "Vec2d": >>> Vec2d(10, 1).projection((10, 5)) Vec2d(8.4, 4.2) >>> Vec2d(10, 1).projection((0, 0)) - Vec2d(0, 0) + Vec2d(0.0, 0.0) """ assert len(other) == 2 other_length_sqrd = other[0] * other[0] + other[1] * other[1] if other_length_sqrd == 0.0: - return Vec2d(0, 0) + return Vec2d(0.0, 0.0) # projected_length_times_other_length = self.dot(other) # new_length = projected_length_times_other_length / other_length_sqrd new_length = self.dot(other) / other_length_sqrd @@ -541,27 +539,27 @@ def zero() -> "Vec2d": """A vector of zero length. >>> Vec2d.zero() - Vec2d(0, 0) + Vec2d(0.0, 0.0) """ - return Vec2d(0, 0) + return Vec2d(0.0, 0.0) @staticmethod def unit() -> "Vec2d": """A unit vector pointing up. >>> Vec2d.unit() - Vec2d(0, 1) + Vec2d(0.0, 1.0) """ - return Vec2d(0, 1) + return Vec2d(0.0, 1.0) @staticmethod def ones() -> "Vec2d": """A vector where both x and y is 1. >>> Vec2d.ones() - Vec2d(1, 1) + Vec2d(1.0, 1.0) """ - return Vec2d(1, 1) + return Vec2d(1.0, 1.0) @staticmethod def from_polar(length: float, angle: float) -> "Vec2d": From a7afc5b1432fdf9bece1d6c0b24d8e588f0941c6 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 21 Feb 2025 21:56:34 +0100 Subject: [PATCH 11/80] Improve docs of Vec2d #274 --- CHANGELOG.rst | 7 +++++-- pymunk/vec2d.py | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 922b2195..828d4419 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,9 +3,12 @@ Changelog ========= .. Pymunk 7.0.0 Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. - New feature: ShapeFilter.rejects_collision() method - New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) + + Extra thanks for aetle for a number of suggestions for improvements: + New feature: ShapeFilter.rejects_collision() + New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Optimized Vec2d.angle and Vec2d.angle_degrees + Improved vec2d documentation Pymunk 6.11.1 (2025-02-09) diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 3066c5df..a1d910db 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -287,12 +287,19 @@ def length(self) -> float: def scale_to_length(self, length: float) -> "Vec2d": """Return a copy of this vector scaled to the given length. + Note that a zero length Vec2d cannot be scaled but will raise an + exception. + >>> Vec2d(1, 0).scale_to_length(10) Vec2d(10.0, 0.0) >>> '%.2f, %.2f' % Vec2d(10, 20).scale_to_length(20) '8.94, 17.89' >>> Vec2d(1, 0).scale_to_length(0) Vec2d(0.0, 0.0) + >>> Vec2d(0, 0).scale_to_length(1) + Traceback (most recent call last): + ... + ZeroDivisionError: float division by zero """ old_length = self.length return Vec2d(self.x * length / old_length, self.y * length / old_length) @@ -374,7 +381,7 @@ def get_angle_degrees_between(self, other: "Vec2d") -> float: def normalized(self) -> "Vec2d": """Get a normalized copy of the vector. - Note: This function will return 0 if the length of the vector is 0. + Note: This function will return a Vec2d(0.0, 0.0) if the length of the vector is 0. >>> Vec2d(3, 0).normalized() Vec2d(1.0, 0.0) From ba40182f60a2bb60745a6ef91d48b4e630dec9b3 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 22 Feb 2025 21:39:31 +0100 Subject: [PATCH 12/80] Add Vec2d.length_squared, deprecated Vec2d.get_length_sqrd #274 --- CHANGELOG.rst | 7 +++++-- pymunk/examples/tank.py | 2 +- pymunk/vec2d.py | 33 ++++++++++++++++++++++++++++----- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 828d4419..2dd37797 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,12 +3,15 @@ Changelog ========= .. Pymunk 7.0.0 Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. + New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. + Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() - Extra thanks for aetle for a number of suggestions for improvements: + New feature: ShapeFilter.rejects_collision() - New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Optimized Vec2d.angle and Vec2d.angle_degrees Improved vec2d documentation + + Extra thanks for aetle for a number of suggestions for improvements in this pymunk release Pymunk 6.11.1 (2025-02-09) diff --git a/pymunk/examples/tank.py b/pymunk/examples/tank.py index 39b127e1..2e7866c9 100644 --- a/pymunk/examples/tank.py +++ b/pymunk/examples/tank.py @@ -23,7 +23,7 @@ def update(space, dt, surface): tank_control_body.angle = tank_body.angle - turn # drive the tank towards the mouse - if (mouse_pos - tank_body.position).get_length_sqrd() < 30 ** 2: + if (mouse_pos - tank_body.position).length_squared < 30**2: tank_control_body.velocity = 0, 0 else: if mouse_delta.dot(tank_body.rotation_vector) > 0.0: diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index a1d910db..469a7750 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -108,6 +108,7 @@ import math import numbers import operator +import warnings from typing import NamedTuple, Tuple __all__ = ["Vec2d"] @@ -256,12 +257,29 @@ def __bool__(self) -> bool: """ return self.x != 0 or self.y != 0 + @property + def length_squared(self) -> float: + """Get the squared length of the vector. + If the squared length is enough, it is more efficient to use this method + instead of first access .length and then do a x**2. + + >>> v = Vec2d(3, 4) + >>> v.length_squared == v.length**2 + True + >>> Vec2d(0, 0).length_squared + 0 + """ + return self.x**2 + self.y**2 + # vectory functions + def get_length_sqrd(self) -> float: """Get the squared length of the vector. If the squared length is enough, it is more efficient to use this method - instead of first calling get_length() or access .length and then do a - x**2. + instead of first accessing .length and then do a x**2. + + .. deprecated:: 7.0.0 + Please use :py:attr:`length_squared` instead. >>> v = Vec2d(3, 4) >>> v.get_length_sqrd() == v.length**2 @@ -269,6 +287,11 @@ def get_length_sqrd(self) -> float: >>> Vec2d(0, 0).get_length_sqrd() 0 """ + warnings.warn( + "Vec2d.get_length_sqrd() is deprecated. Use Vec2d.length_squared instead.", + category=DeprecationWarning, + stacklevel=2, + ) return self.x**2 + self.y**2 @property @@ -527,14 +550,14 @@ def convert_to_basis( """ assert len(x_vector) == 2 assert len(y_vector) == 2 - x = self.dot(x_vector) / Vec2d(*x_vector).get_length_sqrd() - y = self.dot(y_vector) / Vec2d(*y_vector).get_length_sqrd() + x = self.dot(x_vector) / Vec2d(*x_vector).length_squared + y = self.dot(y_vector) / Vec2d(*y_vector).length_squared return Vec2d(x, y) @property def int_tuple(self) -> Tuple[int, int]: """The x and y values of this vector as a tuple of ints. - Use `round()` to round to closest int. + Uses `round()` to round to closest int. >>> Vec2d(0.9, 2.4).int_tuple (1, 2) From ab9f69940c03aa13098af62c93a72cac49fed31f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 22 Feb 2025 21:45:49 +0100 Subject: [PATCH 13/80] New Vec2d.polar_tuple property #274 --- CHANGELOG.rst | 2 ++ pymunk/vec2d.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2dd37797..571c3cbd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,9 +8,11 @@ Changelog New feature: ShapeFilter.rejects_collision() + New feature: Added Vec2d.polar_tuple Optimized Vec2d.angle and Vec2d.angle_degrees Improved vec2d documentation + Extra thanks for aetle for a number of suggestions for improvements in this pymunk release diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 469a7750..548019c0 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -564,6 +564,22 @@ def int_tuple(self) -> Tuple[int, int]: """ return round(self.x), round(self.y) + @property + def polar_tuple(self) -> Tuple[float, float]: + """Return this vector as polar coordinates (length, angle) + + See Vec2d.from_polar() for the inverse. + + >>> Vec2d(2, 0).polar_tuple + (2.0, 0.0) + >>> Vec2d(2, 0).rotated(0.5).polar_tuple + (2.0, 0.5) + >>> Vec2d.from_polar(2, 0.5).polar_tuple + (2.0, 0.5) + + """ + return self.length, self.angle + @staticmethod def zero() -> "Vec2d": """A vector of zero length. @@ -595,6 +611,8 @@ def ones() -> "Vec2d": def from_polar(length: float, angle: float) -> "Vec2d": """Create a new Vec2d from a length and an angle (in radians). + See Vec2d.polar_tuple for the inverse. + >>> Vec2d.from_polar(2, 0) Vec2d(2.0, 0.0) >>> Vec2d(2, 0).rotated(0.5) == Vec2d.from_polar(2, 0.5) From eafa77c6c1228a754268c2dc77e2f4df83808a7b Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 22 Feb 2025 22:28:24 +0100 Subject: [PATCH 14/80] Added Vec2d.get_distance_squared, deprecated get_dist_sqrd #274 --- CHANGELOG.rst | 2 +- pymunk/examples/planet.py | 2 +- pymunk/vec2d.py | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 571c3cbd..38b3b3d2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() - + Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() New feature: ShapeFilter.rejects_collision() New feature: Added Vec2d.polar_tuple diff --git a/pymunk/examples/planet.py b/pymunk/examples/planet.py index da2ae878..dd29fb26 100644 --- a/pymunk/examples/planet.py +++ b/pymunk/examples/planet.py @@ -40,7 +40,7 @@ def planet_gravity(body, gravity, damping, dt): # distance, and directed toward the origin. The central planet is assumed # to be massive enough that it affects the satellites but not vice versa. p = body.position - sq_dist = p.get_dist_sqrd(center) + sq_dist = p.get_distance_squared(center) g = (p - center) * -gravityStrength / (sq_dist * math.sqrt(sq_dist)) # body.velocity += g * dt # setting velocity directly like would be slower diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 548019c0..202dc860 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -484,11 +484,29 @@ def get_distance(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return math.sqrt((self.x - other[0]) ** 2 + (self.y - other[1]) ** 2) + def get_distance_squared(self, other: Tuple[float, float]) -> float: + """The squared distance between the vector and other vector. + It is more efficent to use this method than to call get_distance() + first and then do a square() on the result. + + >>> Vec2d(1, 0).get_distance_squared((1, 10)) + 100 + >>> Vec2d(1, 2).get_distance_squared((10, 11)) + 162 + >>> Vec2d(1, 2).get_distance((10, 11))**2 + 162.0 + """ + assert len(other) == 2 + return (self.x - other[0]) ** 2 + (self.y - other[1]) ** 2 + def get_dist_sqrd(self, other: Tuple[float, float]) -> float: """The squared distance between the vector and other vector. It is more efficent to use this method than to call get_distance() first and then do a square() on the result. + .. deprecated:: 7.0.0 + Please use :py:func:`get_distance_squared` instead. + >>> Vec2d(1, 0).get_dist_sqrd((1, 10)) 100 >>> Vec2d(1, 2).get_dist_sqrd((10, 11)) @@ -496,6 +514,11 @@ def get_dist_sqrd(self, other: Tuple[float, float]) -> float: >>> Vec2d(1, 2).get_distance((10, 11))**2 162.0 """ + warnings.warn( + "get_dist_sqrd() is deprecated. Use get_distance_squared() instead.", + category=DeprecationWarning, + stacklevel=2, + ) assert len(other) == 2 return (self.x - other[0]) ** 2 + (self.y - other[1]) ** 2 From de52ceb01cd36cb0bab20670213bfda8d756964c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 24 Feb 2025 20:48:58 +0100 Subject: [PATCH 15/80] Note about how angle of vec2d is calculated #274 --- CHANGELOG.rst | 2 +- pymunk/vec2d.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 38b3b3d2..f3226b1c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,7 +9,7 @@ Changelog New feature: ShapeFilter.rejects_collision() New feature: Added Vec2d.polar_tuple - Optimized Vec2d.angle and Vec2d.angle_degrees + Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) Improved vec2d documentation diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 202dc860..0c7b8e66 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -357,6 +357,8 @@ def rotated_degrees(self, angle_degrees: float) -> "Vec2d": def angle(self) -> float: """The angle (in radians) of the vector. + Angle calculated with atan2(y, x). + >>> '%.2f' % Vec2d(-1, 0).angle '3.14' >>> Vec2d(0, 0).angle From bc90bf6636ba4c8f001adb4f3156288b9b7630f8 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 25 Feb 2025 22:25:19 +0100 Subject: [PATCH 16/80] Add default CollisionHandler callback functions instead of None to better align API #272 --- CHANGELOG.rst | 3 + pymunk/collision_handler.py | 158 ++++++++++++++++++++-------------- pymunk/tests/test_batch.py | 3 + pymunk/tests/test_shape.py | 3 +- pymunk/tests/test_space.py | 51 ++++++++--- pymunk_cffi/extensions.c | 3 + pymunk_cffi/extensions_cdef.h | 5 +- 7 files changed, 144 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f3226b1c..66feb572 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,9 @@ Changelog Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() + Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. + If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. + New feature: ShapeFilter.rejects_collision() New feature: Added Vec2d.polar_tuple Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index 9090e76a..46a6ad3b 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict if TYPE_CHECKING: from .space import Space @@ -8,8 +8,8 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallbackBool = Callable[[Arbiter, "Space", Any], bool] -_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Any], None] +_CollisionCallbackBool = Callable[[Arbiter, "Space", Dict[Any, Any]], bool] +_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Dict[Any, Any]], None] class CollisionHandler(object): @@ -42,25 +42,13 @@ def __init__(self, _handler: Any, space: "Space") -> None: self._handler.userData = self._userData self._space = space - self._begin: Optional[_CollisionCallbackBool] = None - self._pre_solve: Optional[_CollisionCallbackBool] = None - self._post_solve: Optional[_CollisionCallbackNoReturn] = None - self._separate: Optional[_CollisionCallbackNoReturn] = None + self._begin: _CollisionCallbackBool = CollisionHandler.always_collide + self._pre_solve: _CollisionCallbackBool = CollisionHandler.always_collide + self._post_solve: _CollisionCallbackNoReturn = CollisionHandler.do_nothing + self._separate: _CollisionCallbackNoReturn = CollisionHandler.do_nothing self._data: Dict[Any, Any] = {} - def _reset(self) -> None: - def allways_collide(arb: Arbiter, space: "Space", data: Any) -> bool: - return True - - def do_nothing(arb: Arbiter, space: "Space", data: Any) -> None: - return - - self.begin = allways_collide - self.pre_solve = allways_collide - self.post_solve = do_nothing - self.separate = do_nothing - @property def data(self) -> Dict[Any, Any]: """Data property that get passed on into the @@ -72,17 +60,9 @@ def data(self) -> Dict[Any, Any]: """ return self._data - def _set_begin(self, func: _CollisionCallbackBool) -> None: - self._begin = func - self._handler.beginFunc = lib.ext_cpCollisionBeginFunc - - def _get_begin(self) -> Optional[_CollisionCallbackBool]: - return self._begin - - begin = property( - _get_begin, - _set_begin, - doc="""Two shapes just started touching for the first time this step. + @property + def begin(self) -> _CollisionCallbackBool: + """Two shapes just started touching for the first time this step. ``func(arbiter, space, data) -> bool`` @@ -91,20 +71,24 @@ def _get_begin(self) -> Optional[_CollisionCallbackBool]: false, the `pre_solve` and `post_solve` callbacks will never be run, but you will still recieve a separate event when the shapes stop overlapping. - """, - ) + """ + return self._begin - def _set_pre_solve(self, func: _CollisionCallbackBool) -> None: - self._pre_solve = func - self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc + @begin.setter + def begin(self, func: _CollisionCallbackBool) -> None: + assert ( + func is not None + ), "To reset the begin callback, set handler.begin = CollisionHandler.always_collide" + self._begin = func - def _get_pre_solve(self) -> Optional[_CollisionCallbackBool]: - return self._pre_solve + if self._begin == CollisionHandler.always_collide: + self._handler.beginFunc = ffi.addressof(lib, "AlwaysCollide") + else: + self._handler.beginFunc = lib.ext_cpCollisionBeginFunc - pre_solve = property( - _get_pre_solve, - _set_pre_solve, - doc="""Two shapes are touching during this step. + @property + def pre_solve(self) -> _CollisionCallbackBool: + """Two shapes are touching during this step. ``func(arbiter, space, data) -> bool`` @@ -113,21 +97,24 @@ def _get_pre_solve(self) -> Optional[_CollisionCallbackBool]: override collision values using Arbiter.friction, Arbiter.elasticity or Arbiter.surfaceVelocity to provide custom friction, elasticity, or surface velocity values. See Arbiter for more info. - """, - ) - - def _set_post_solve(self, func: _CollisionCallbackNoReturn) -> None: + """ + return self._pre_solve - self._post_solve = func - self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc + @pre_solve.setter + def pre_solve(self, func: _CollisionCallbackBool) -> None: + assert ( + func is not None + ), "To reset the pre_solve callback, set handler.pre_solve = CollisionHandler.always_collide" + self._pre_solve = func - def _get_post_solve(self) -> Optional[_CollisionCallbackNoReturn]: - return self._post_solve + if self._pre_solve == CollisionHandler.always_collide: + self._handler.preSolveFunc = ffi.addressof(lib, "AlwaysCollide") + else: + self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc - post_solve = property( - _get_post_solve, - _set_post_solve, - doc="""Two shapes are touching and their collision response has been + @property + def post_solve(self) -> _CollisionCallbackNoReturn: + """Two shapes are touching and their collision response has been processed. ``func(arbiter, space, data)`` @@ -135,20 +122,26 @@ def _get_post_solve(self) -> Optional[_CollisionCallbackNoReturn]: You can retrieve the collision impulse or kinetic energy at this time if you want to use it to calculate sound volumes or damage amounts. See Arbiter for more info. - """, - ) + """ + return self._post_solve - def _set_separate(self, func: _CollisionCallbackNoReturn) -> None: - self._separate = func - self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc + @post_solve.setter + def post_solve(self, func: _CollisionCallbackNoReturn) -> None: + assert ( + func is not None + ), "To reset the post_solve callback, set handler.post_solve = CollisionHandler.do_nothing" + self._post_solve = func - def _get_separate(self) -> Optional[_CollisionCallbackNoReturn]: - return self._separate + if self._post_solve == CollisionHandler.do_nothing: + self._handler.postSolveFunc = ffi.addressof(lib, "DoNothing") + else: + self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc - separate = property( - _get_separate, - _set_separate, - doc="""Two shapes have just stopped touching for the first time this + self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc + + @property + def separate(self) -> _CollisionCallbackNoReturn: + """Two shapes have just stopped touching for the first time this step. ``func(arbiter, space, data)`` @@ -156,5 +149,38 @@ def _get_separate(self) -> Optional[_CollisionCallbackNoReturn]: To ensure that begin()/separate() are always called in balanced pairs, it will also be called when removing a shape while its in contact with something or when de-allocating the space. - """, - ) + """ + return self._separate + + @separate.setter + def separate(self, func: _CollisionCallbackNoReturn) -> None: + assert ( + func is not None + ), "To reset the separate callback, set handler.separate = CollisionHandler.do_nothing" + self._separate = func + + if self._separate == CollisionHandler.do_nothing: + self._handler.separateFunc = ffi.addressof(lib, "DoNothing") + else: + self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc + + @staticmethod + def do_nothing(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> None: + """The default do nothing method used for the post_solve and seprate + callbacks. + + Note that its more efficient to set this method than to define your own + do nothing method. + """ + return + + @staticmethod + def always_collide(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> bool: + """The default method used for the begin and pre_solve callbacks. + + It will always return True, meaning the collision should not be ignored. + + Note that its more efficient to set this method than to define your own + return True method. + """ + return True diff --git a/pymunk/tests/test_batch.py b/pymunk/tests/test_batch.py index 6969bf11..66233aa9 100644 --- a/pymunk/tests/test_batch.py +++ b/pymunk/tests/test_batch.py @@ -201,6 +201,9 @@ def test_get_arbiters(self) -> None: ints = memoryview(data.int_buf()).cast("P") def check_arb_data(arb: pymunk.Arbiter) -> None: + assert arb.shapes[0].body is not None + assert arb.shapes[1].body is not None + a_id = arb.shapes[0].body.id b_id = arb.shapes[1].body.id diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index a328db1f..6fc95948 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -195,8 +195,9 @@ def testPickle(self) -> None: self.assertEqual(c.surface_velocity, c2.surface_velocity) self.assertEqual(c.density, c2.density) self.assertEqual(c.mass, c2.mass) + assert c.body is not None self.assertEqual(c.body.mass, c2.body.mass) - + self.assertIsNotNone c = p.Circle(None, 1) c.density = 3 diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 7698fba0..d3d7f305 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -73,7 +73,7 @@ def testProperties(self) -> None: s.step(0.1) self.assertEqual(s.current_time_step, 0.1) - self.assertTrue(s.static_body != None) + self.assertTrue(s.static_body is not None) self.assertEqual(s.static_body.body_type, p.Body.STATIC) self.assertEqual(s.threads, 1) @@ -196,7 +196,7 @@ def testPointQueryNearestWithShapeFilter(self) -> None: s1.filter = f1 hit = s.point_query_nearest((0, 0), 0, f2) self.assertEqual( - hit != None, + hit is not None, test["hit"], "Got {}!=None, expected {} for test: {}".format(hit, test["hit"], test), ) @@ -647,6 +647,29 @@ def separate(*_: Any) -> None: s.step(1) s.remove(c1) + def testCollisionHandlerDefaultCallbacks(self) -> None: + s = p.Space() + + b1 = p.Body(1, 1) + c1 = p.Circle(b1, 10) + b1.position = 9, 11 + + b2 = p.Body(body_type=p.Body.STATIC) + c2 = p.Circle(b2, 10) + b2.position = 0, 0 + + s.add(b1, c1, b2, c2) + s.gravity = 0, -100 + + h = s.add_default_collision_handler() + h.begin = h.always_collide + h.pre_solve = h.always_collide + h.post_solve = h.do_nothing + h.separate = h.do_nothing + + for _ in range(10): + s.step(0.1) + @unittest.skip("Existing bug in Pymunk. TODO: Fix bug and enable test") def testRemoveInSeparate(self) -> None: s = p.Space() @@ -1057,26 +1080,26 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: # Assert collision handlers h2 = s2.add_default_collision_handler() self.assertIsNotNone(h2.begin) - self.assertIsNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) + self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) + self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_wildcard_collision_handler(1) - self.assertIsNone(h2.begin) + self.assertEqual(h2.begin, p.CollisionHandler.always_collide) self.assertIsNotNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) + self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_collision_handler(1, 2) - self.assertIsNone(h2.begin) - self.assertIsNone(h2.pre_solve) + self.assertEqual(h2.begin, p.CollisionHandler.always_collide) + self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) self.assertIsNotNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_collision_handler(3, 4) - self.assertIsNone(h2.begin) - self.assertIsNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) + self.assertEqual(h2.begin, p.CollisionHandler.always_collide) + self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) + self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.separate) def testPickleCachedArbiters(self) -> None: diff --git a/pymunk_cffi/extensions.c b/pymunk_cffi/extensions.c index 1249d56c..cf6a2ebd 100644 --- a/pymunk_cffi/extensions.c +++ b/pymunk_cffi/extensions.c @@ -488,3 +488,6 @@ void cpMessage(const char *condition, const char *file, int line, int isError, i snprintf(formattedMessage, sizeof(formattedMessage), "\tSource: %s:%d", file, line); ext_pyLog(formattedMessage); } + +static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data){return cpTrue;} +static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data){} \ No newline at end of file diff --git a/pymunk_cffi/extensions_cdef.h b/pymunk_cffi/extensions_cdef.h index 860328e0..48ed8652 100644 --- a/pymunk_cffi/extensions_cdef.h +++ b/pymunk_cffi/extensions_cdef.h @@ -102,4 +102,7 @@ cpContact *cpContactArrAlloc(int count); cpFloat defaultSpringForce(cpDampedSpring *spring, cpFloat dist); -cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle); \ No newline at end of file +cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle); + +static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data); +static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data); \ No newline at end of file From 72bbe3b06594f382d79895eb6b1b156cbb2b4577 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 25 Feb 2025 22:47:15 +0100 Subject: [PATCH 17/80] Add example in Poly docs of convex hull #257 --- CHANGELOG.rst | 2 +- pymunk/shapes.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 66feb572..67646dde 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Changelog New feature: Added Vec2d.polar_tuple Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) Improved vec2d documentation - + Improved Poly documentation Extra thanks for aetle for a number of suggestions for improvements in this pymunk release diff --git a/pymunk/shapes.py b/pymunk/shapes.py index a30c2951..5e7407ae 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -154,8 +154,7 @@ def collision_type(self, t: int) -> None: @property def filter(self) -> ShapeFilter: - """Set the collision :py:class:`ShapeFilter` for this shape. - """ + """Set the collision :py:class:`ShapeFilter` for this shape.""" f = cp.cpShapeGetFilter(self._shape) return ShapeFilter(f.group, f.categories, f.mask) @@ -547,7 +546,11 @@ def __init__( A convex hull will be calculated from the vertexes automatically. Note that concave ones will be converted to a convex hull using the Quickhull - algorithm. + algorithm:: + + >>> poly = Poly(None, [(-1,0), (0, -0.5), (1, 0), (0,-1)]) + >>> poly.get_vertices() + [Vec2d(-1.0, 0.0), Vec2d(0.0, -1.0), Vec2d(1.0, 0.0)] Adding a small radius will bevel the corners and can significantly reduce problems where the poly gets stuck on seams in your geometry. @@ -562,23 +565,21 @@ def __init__( Either directly place the vertices like the below example: - >>> import pymunk >>> w, h = 10, 20 >>> vs = [(-w/2,-h/2), (w/2,-h/2), (w/2,h/2), (-w/2,h/2)] - >>> poly_good = pymunk.Poly(None, vs) + >>> poly_good = Poly(None, vs) >>> print(poly_good.center_of_gravity) Vec2d(0.0, 0.0) Or use a transform to move them: - >>> import pymunk >>> width, height = 10, 20 >>> vs = [(0, 0), (width, 0), (width, height), (0, height)] - >>> poly_bad = pymunk.Poly(None, vs) + >>> poly_bad = Poly(None, vs) >>> print(poly_bad.center_of_gravity) Vec2d(5.0, 10.0) - >>> t = pymunk.Transform(tx=-width/2, ty=-height/2) - >>> poly_good = pymunk.Poly(None, vs, transform=t) + >>> t = Transform(tx=-width/2, ty=-height/2) + >>> poly_good = Poly(None, vs, transform=t) >>> print(poly_good.center_of_gravity) Vec2d(0.0, 0.0) From c1d222a5c8eef8388218a41e4ce1c4df15f2e8b5 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 9 Mar 2025 23:08:55 +0100 Subject: [PATCH 18/80] Made it required to have mass > 0 on dynamic bodies when stepping #268 --- CHANGELOG.rst | 1 + pymunk/body.py | 26 ++++++++++++++------------ pymunk/space.py | 11 +++++++++++ pymunk/tests/test_common.py | 6 +++--- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67646dde..ac72312d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ Changelog New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() + A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/pymunk/body.py b/pymunk/body.py index eb03b172..0479ce6b 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -254,11 +254,18 @@ def __repr__(self) -> str: @property def mass(self) -> float: - """Mass of the body.""" + """Mass of the body. + + Note that dynamic bodies must have mass > 0 if they are attached to a + Space. + """ return lib.cpBodyGetMass(self._body) @mass.setter def mass(self, mass: float) -> None: + assert ( + self._space is None or mass > 0 + ), "Dynamic bodies must have mass > 0 if they are attached to a Space." lib.cpBodySetMass(self._body, mass) @property @@ -347,9 +354,9 @@ def angle(self) -> float: """Rotation of the body in radians. When changing the rotation you may also want to call - :py:func:`Space.reindex_shapes_for_body` to update the collision - detection information for the attached shapes if plan to make any - queries against the space. A body rotates around its center of gravity, + :py:func:`Space.reindex_shapes_for_body` to update the collision + detection information for the attached shapes if plan to make any + queries against the space. A body rotates around its center of gravity, not its position. .. Note:: @@ -393,7 +400,7 @@ def rotation_vector(self) -> Vec2d: def space(self) -> Optional["Space"]: """Get the :py:class:`Space` that the body has been added to (or None).""" - assert hasattr(self, "_space"), ( + assert hasattr(self, "_space"), ( # TODO: When can this happen? "_space not set. This can mean there's a direct or indirect" " circular reference between the Body and the Space. Circular" " references are not supported when using pickle or copy and" @@ -482,12 +489,7 @@ def _set_position_func(self, func: _PositionFunc) -> None: @property def kinetic_energy(self) -> float: """Get the kinetic energy of a body.""" - # todo: use ffi method - # return lib._cpBodyKineticEnergy(self._body) - - vsq: float = self.velocity.dot(self.velocity) - wsq: float = self.angular_velocity * self.angular_velocity - return (vsq * self.mass if vsq else 0.0) + (wsq * self.moment if wsq else 0.0) + return lib.cpBodyKineticEnergy(self._body) @staticmethod def update_velocity( @@ -598,7 +600,7 @@ def is_sleeping(self) -> bool: @property def body_type(self) -> _BodyType: - """The type of a body (:py:const:`Body.DYNAMIC`, + """The type of a body (:py:const:`Body.DYNAMIC`, :py:const:`Body.KINEMATIC` or :py:const:`Body.STATIC`). When changing an body to a dynamic body, the mass and moment of diff --git a/pymunk/space.py b/pymunk/space.py index fc43cf56..211bd73f 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -143,6 +143,7 @@ def spacefree(cp_space: ffi.CData) -> None: self._add_later: Set[_AddableObjects] = set() self._remove_later: Set[_AddableObjects] = set() + self._bodies_to_check: Set[Body] = set() def _get_self(self) -> "Space": return self @@ -409,6 +410,7 @@ def _add_body(self, body: "Body") -> None: body._space = weakref.proxy(self) self._bodies[body] = None + self._bodies_to_check.add(body) cp.cpSpaceAddBody(self._space, body._body) def _add_constraint(self, constraint: "Constraint") -> None: @@ -437,6 +439,8 @@ def _remove_body(self, body: "Body") -> None: """Removes a body from the space""" assert body in self._bodies, "body not in space, already removed?" body._space = None + if body in self._bodies_to_check: + self._bodies_to_check.remove(body) # During GC at program exit sometimes the shape might already be removed. Then skip this step. if cp.cpSpaceContainsBody(self._space, body._body): cp.cpSpaceRemoveBody(self._space, body._body) @@ -546,6 +550,13 @@ def step(self, dt: float) -> None: :param dt: Time step length """ + + for b in self._bodies_to_check: + assert b.body_type != Body.DYNAMIC or ( + b.mass > 0 and b.mass < math.inf + ), f"Dynamic bodies must have a mass > 0 and < inf. {b} has mass {b.mass}." + self._bodies_to_check.clear() + try: self._locked = True if self.threaded: diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index f2e68203..0f0ca79e 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -134,14 +134,14 @@ def remove_first(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: def testX(self) -> None: space = p.Space() - b1 = p.Body() + b1 = p.Body(1) c1 = p.Circle(b1, 10) c1.collision_type = 2 - b2 = p.Body() + b2 = p.Body(1) c2 = p.Circle(b2, 10) - b3 = p.Body() + b3 = p.Body(1) c3 = p.Circle(b3, 10) # b1.position = 0, 0 From daaaee902953c3150733e6f4401af2089b92b757 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 9 Mar 2025 23:20:28 +0100 Subject: [PATCH 19/80] Add test for dynamic 0 mass bodies --- pymunk/space.py | 2 +- pymunk/tests/test_body.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pymunk/space.py b/pymunk/space.py index 211bd73f..a5bbe309 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -660,7 +660,7 @@ def add_post_step_callback( self, callback_function: Callable[ ..., None - ], # TODO: Fix me once PEP-612 is implemented + ], # TODO: Fix me once PEP-612 is implemented (py 3.10) key: Hashable, *args: Any, **kwargs: Any, diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index 6a22b6c3..3e394a10 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -171,6 +171,35 @@ def test_static(self) -> None: b = p.Body(body_type=p.Body.STATIC) self.assertEqual(b.body_type, p.Body.STATIC) + def test_mass(self) -> None: + s = p.Space() + b = p.Body() + + b.mass = 2 + s.add(b) + + # Cant set 0 mass on Body in Space + with self.assertRaises(AssertionError): + b.mass = 0 + + s.remove(b) + b.mass = 0 + s.add(b) + # Cant add 0 mass Body to Space and run step + with self.assertRaises(AssertionError): + s.step(1) + + c = p.Circle(b, 1) + s.add(c) + + # Same with a Shape + with self.assertRaises(AssertionError): + s.step(1) + + # Setting the Shape mass or density should fix it + c.density = 10 + s.step(1) + def test_mass_moment_from_shape(self) -> None: s = p.Space() From 3f549e1783a338d8f84ba051a92917884037ea84 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 9 Mar 2025 23:41:10 +0100 Subject: [PATCH 20/80] Deprecated matplotlib_util --- CHANGELOG.rst | 3 ++- TODO.txt | 1 - pymunk/matplotlib_util.py | 22 +++++++++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ac72312d..b29a2d12 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,7 +7,8 @@ Changelog Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. - + Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track + Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/TODO.txt b/TODO.txt index b4abe638..f0b261c9 100644 --- a/TODO.txt +++ b/TODO.txt @@ -44,7 +44,6 @@ v6.x - Add Canvas util module (after pyodide) - Use https://diataxis.fr/ method of organizing docs (tutorials, how-to, explanation, reference) - Make sure cffi extensions included in wheel and zip! -- Catch error when NaN in debugdraw, suggest doublecheck mass=0. https://github.com/viblo/pymunk/issues/226 and https://github.com/viblo/pymunk/issues/267 - Make benchmark between pymunk and pybullet. 2d bullet inspiration: https://github.com/bulletphysics/bullet3/blob/2.83/examples/Planar2D/Planar2D.cpp - After a couple of versions / time passed, re-evaluate if batch api should be separate or merged in to space. - Think about the copyright notice in some files. Keep / put everywere / remove? diff --git a/pymunk/matplotlib_util.py b/pymunk/matplotlib_util.py index 1c50cfdd..89b7e2e0 100644 --- a/pymunk/matplotlib_util.py +++ b/pymunk/matplotlib_util.py @@ -1,13 +1,14 @@ -"""This submodule contains helper functions to help with quick prototyping +"""This submodule contains helper functions to help with quick prototyping using pymunk together with matplotlib. Intended to help with debugging and prototyping, not for actual production use -in a full application. The methods contained in this module is opinionated -about your coordinate system and not very optimized. +in a full application. The methods contained in this module is opinionated +about your coordinate system and not very optimized. """ __docformat__ = "reStructuredText" +import warnings from typing import TYPE_CHECKING, Any, Sequence import matplotlib.pyplot as plt # type: ignore @@ -43,11 +44,26 @@ def __init__(self, ax: Any) -> None: See matplotlib_util.demo.py for a full example + .. deprecated:: 7.0.0 + If you find this class useful, please open an issue on the pymunk + issue tracker at https://github.com/viblo/pymunk/issues Otherwise + this is likely to be completely removed in a future Pymunk version. + + :Param: ax: matplotlib.Axes A matplotlib Axes object. """ + warnings.warn( + "matplotlib_util.DrawOptions is deprecated. If you find it useful, " + "please open an issue on the pymunk issue tracker at " + "https://github.com/viblo/pymunk/issues Otherwise this is likely " + "to be completely removed in a future Pymunk version.", + category=DeprecationWarning, + stacklevel=2, + ) + super(DrawOptions, self).__init__() self.ax = ax From df54445fa3e13c89e0fd2bb449ea8325529fde3d Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 14 Mar 2025 21:39:13 +0100 Subject: [PATCH 21/80] Change Space.shapes internally to behave as Space.bodies --- pymunk/shapes.py | 25 ++++++------------------- pymunk/space.py | 34 ++++++++++++---------------------- pymunk/tests/test_shape.py | 4 ---- 3 files changed, 18 insertions(+), 45 deletions(-) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 5e7407ae..7d9f6f61 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -66,21 +66,8 @@ def shapefree(cp_shape: ffi.CData) -> None: cp.cpShapeFree(cp_shape) self._shape = ffi.gc(_shape, shapefree) - self._set_id() - - @property - def _id(self) -> int: - """Unique id of the Shape. - - .. note:: - Experimental API. Likely to change in future major, minor orpoint - releases. - """ - return int(ffi.cast("int", cp.cpShapeGetUserData(self._shape))) - - def _set_id(self) -> None: - cp.cpShapeSetUserData(self._shape, ffi.cast("cpDataPointer", Shape._id_counter)) - Shape._id_counter += 1 + self._h = ffi.new_handle(self) # to prevent GC of the handle + cp.cpShapeSetUserData(self._shape, self._h) @property def mass(self) -> float: @@ -290,8 +277,8 @@ def point_query(self, p: Tuple[float, float]) -> PointQueryInfo: info = ffi.new("cpPointQueryInfo *") _ = cp.cpShapePointQuery(self._shape, p, info) - ud = int(ffi.cast("int", cp.cpShapeGetUserData(info.shape))) - assert ud == self._id + shape = ffi.from_handle(cp.cpShapeGetUserData(info.shape)) + assert shape == self, "This is a bug in Pymunk. Please report it." return PointQueryInfo( self, Vec2d(info.point.x, info.point.y), @@ -311,8 +298,8 @@ def segment_query( info = ffi.new("cpSegmentQueryInfo *") r = cp.cpShapeSegmentQuery(self._shape, start, end, radius, info) if r: - ud = int(ffi.cast("int", cp.cpShapeGetUserData(info.shape))) - assert ud == self._id + shape = ffi.from_handle(cp.cpShapeGetUserData(info.shape)) + assert shape == self, "This is a bug in Pymunk. Please report it." return SegmentQueryInfo( self, Vec2d(info.point.x, info.point.y), diff --git a/pymunk/space.py b/pymunk/space.py index a5bbe309..694fe62f 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -132,9 +132,9 @@ def spacefree(cp_space: ffi.CData) -> None: ) # To prevent the gc to collect the callbacks. self._post_step_callbacks: Dict[Any, Callable[["Space"], None]] = {} - self._removed_shapes: Dict[int, Shape] = {} + self._removed_shapes: Dict[Shape, None] = {} - self._shapes: Dict[int, Shape] = {} + self._shapes: Dict[Shape, None] = {} self._bodies: Dict[Body, None] = {} self._static_body: Optional[Body] = None self._constraints: Dict[Constraint, None] = {} @@ -154,7 +154,7 @@ def shapes(self) -> List[Shape]: (includes both static and non-static) """ - return list(self._shapes.values()) + return list(self._shapes) @property def bodies(self) -> List[Body]: @@ -389,8 +389,7 @@ def remove(self, *objs: _AddableObjects) -> None: def _add_shape(self, shape: "Shape") -> None: """Adds a shape to the space""" - # print("addshape", self._space, shape) - assert shape._id not in self._shapes, "Shape already added to space." + assert shape not in self._shapes, "Shape already added to space." assert ( shape.space == None ), "Shape already added to another space. A shape can only be in one space at a time." @@ -400,7 +399,7 @@ def _add_shape(self, shape: "Shape") -> None: ), "The shape's body must be added to the space before (or at the same time) as the shape." shape._space = weakref.proxy(self) - self._shapes[shape._id] = shape + self._shapes[shape] = None cp.cpSpaceAddShape(self._space, shape._shape) def _add_body(self, body: "Body") -> None: @@ -427,13 +426,13 @@ def _add_constraint(self, constraint: "Constraint") -> None: def _remove_shape(self, shape: "Shape") -> None: """Removes a shape from the space""" - assert shape._id in self._shapes, "shape not in space, already removed?" - self._removed_shapes[shape._id] = shape + assert shape in self._shapes, "shape not in space, already removed?" + self._removed_shapes[shape] = None shape._space = None # During GC at program exit sometimes the shape might already be removed. Then skip this step. if cp.cpSpaceContainsShape(self._space, shape._shape): cp.cpSpaceRemoveShape(self._space, shape._shape) - del self._shapes[shape._id] + del self._shapes[shape] def _remove_body(self, body: "Body") -> None: """Removes a body from the space""" @@ -517,7 +516,7 @@ def use_spatial_hash(self, dim: float, count: int) -> None: the shape to be inserted into many cells, setting it too low will cause too many objects into the same hash slot. - count is the suggested minimum number of cells in the hash table. If + count is the minimum number of cells in the hash table. If there are too few cells, the spatial hash will return many false positives. Too many cells will be hard on the cache and waste memory. Setting count to ~10x the number of objects in the space is probably a @@ -563,7 +562,7 @@ def step(self, dt: float) -> None: cp.cpHastySpaceStep(self._space, dt) else: cp.cpSpaceStep(self._space, dt) - self._removed_shapes = {} + self._removed_shapes.clear() finally: self._locked = False self.add(*self._add_later) @@ -575,7 +574,7 @@ def step(self, dt: float) -> None: for key in self._post_step_callbacks: self._post_step_callbacks[key](self) - self._post_step_callbacks = {} + self._post_step_callbacks.clear() def add_collision_handler( self, collision_type_a: int, collision_type_b: int @@ -745,16 +744,7 @@ def point_query( def _get_shape(self, _shape: Any) -> Optional[Shape]: if not bool(_shape): return None - - shapeid = int(ffi.cast("int", cp.cpShapeGetUserData(_shape))) - # return self._shapes[hashid_private] - - if shapeid in self._shapes: - return self._shapes[shapeid] - elif shapeid in self._removed_shapes: - return self._removed_shapes[shapeid] - else: - return None + return ffi.from_handle(cp.cpShapeGetUserData(_shape)) def point_query_nearest( self, point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index 6fc95948..ab7b4769 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -6,10 +6,6 @@ class UnitTestShape(unittest.TestCase): - def testId(self) -> None: - c = p.Circle(None, 4) - self.assertGreater(c._id, 0) - def testPointQuery(self) -> None: b = p.Body(10, 10) c = p.Circle(b, 5) From 59a525197002fed62556dc6ad5e0126e7db5885a Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 16 Mar 2025 09:01:16 +0100 Subject: [PATCH 22/80] Improve docs for collision_type #277 --- pymunk/shapes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 7d9f6f61..86625e26 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -130,7 +130,9 @@ def sensor(self, is_sensor: bool) -> None: def collision_type(self) -> int: """User defined collision type for the shape. - See :py:meth:`Space.add_collision_handler` function for more + Defaults to 0. + + See the :py:meth:`Space.add_collision_handler` function for more information on when to use this property. """ return cp.cpShapeGetCollisionType(self._shape) From df11ae6ebd6a60793da978a716079901014aa0bc Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 16 Mar 2025 21:06:52 +0100 Subject: [PATCH 23/80] Update body weakref to space to ref, and fix issue with del space. #278 --- pymunk/_callbacks.py | 4 ++-- pymunk/body.py | 22 ++++++++++------------ pymunk/space.py | 6 +++--- pymunk/tests/test_space.py | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index 04397d75..e70acef4 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -290,8 +290,8 @@ def ext_cpBodyArbiterIteratorFunc( _body: ffi.CData, _arbiter: ffi.CData, data: ffi.CData ) -> None: body, func, args, kwargs = ffi.from_handle(data) - assert body._space is not None - arbiter = Arbiter(_arbiter, body._space) + assert body.space is not None + arbiter = Arbiter(_arbiter, body.space) func(arbiter, *args, **kwargs) diff --git a/pymunk/body.py b/pymunk/body.py index 0479ce6b..fda7caef 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -1,5 +1,6 @@ __docformat__ = "reStructuredText" +import weakref from typing import ( # Literal, TYPE_CHECKING, Any, @@ -106,12 +107,13 @@ class Body(PickleMixin, TypingAttrMixing, object): "is_sleeping", "_velocity_func", "_position_func", + # "_space", ] _position_func: Optional[_PositionFunc] = None _velocity_func: Optional[_VelocityFunc] = None - _id_counter = 1 + _dead_ref = weakref.ref(set()) def __init__( self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC @@ -217,9 +219,7 @@ def freebody(cp_body: ffi.CData) -> None: elif body_type == Body.STATIC: self._body = ffi.gc(lib.cpBodyNewStatic(), freebody) - self._space: Optional["Space"] = ( - None # Weak ref to the space holding this body (if any) - ) + self._space: weakref.ref = Body._dead_ref self._constraints: WeakSet["Constraint"] = ( WeakSet() @@ -264,7 +264,7 @@ def mass(self) -> float: @mass.setter def mass(self, mass: float) -> None: assert ( - self._space is None or mass > 0 + self.space is None or mass > 0 ), "Dynamic bodies must have mass > 0 if they are attached to a Space." lib.cpBodySetMass(self._body, mass) @@ -400,16 +400,14 @@ def rotation_vector(self) -> Vec2d: def space(self) -> Optional["Space"]: """Get the :py:class:`Space` that the body has been added to (or None).""" - assert hasattr(self, "_space"), ( # TODO: When can this happen? + # This assert is tested in test_pickle_circular_ref + assert hasattr(self, "_space"), ( "_space not set. This can mean there's a direct or indirect" " circular reference between the Body and the Space. Circular" " references are not supported when using pickle or copy and" " might crash." ) - if self._space is not None: - return self._space._get_self() # ugly hack because of weakref - else: - return None + return self._space() def _set_velocity_func(self, func: _VelocityFunc) -> None: if func == Body.update_velocity: @@ -571,7 +569,7 @@ def sleep(self) -> None: Cannot be called from a callback. """ - if self._space == None: + if self.space == None: raise Exception("Body not added to space") lib.cpBodySleep(self._body) @@ -589,7 +587,7 @@ def sleep_with_group(self, body: "Body") -> None: to initialize levels and start stacks of objects in a pre-sleeping state. """ - if self._space == None: + if self.space == None: raise Exception("Body not added to space") lib.cpBodySleepWithGroup(self._body, body._body) diff --git a/pymunk/space.py b/pymunk/space.py index 694fe62f..dcee92cd 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -167,7 +167,7 @@ def constraints(self) -> List[Constraint]: return list(self._constraints) def _setup_static_body(self, static_body: Body) -> None: - static_body._space = weakref.proxy(self) + static_body._space = weakref.ref(self) cp.cpSpaceAddBody(self._space, static_body._body) @property @@ -407,7 +407,7 @@ def _add_body(self, body: "Body") -> None: assert body not in self._bodies, "Body already added to this space." assert body.space == None, "Body already added to another space." - body._space = weakref.proxy(self) + body._space = weakref.ref(self) self._bodies[body] = None self._bodies_to_check.add(body) cp.cpSpaceAddBody(self._space, body._body) @@ -437,7 +437,7 @@ def _remove_shape(self, shape: "Shape") -> None: def _remove_body(self, body: "Body") -> None: """Removes a body from the space""" assert body in self._bodies, "body not in space, already removed?" - body._space = None + body._space = body._dead_ref if body in self._bodies_to_check: self._bodies_to_check.remove(body) # During GC at program exit sometimes the shape might already be removed. Then skip this step. diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index d3d7f305..41922de8 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1168,6 +1168,24 @@ def testPickleCachedArbiters(self) -> None: self.assertAlmostEqual(s.bodies[0].position.x, s_copy.bodies[0].position.x) self.assertAlmostEqual(s.bodies[0].position.y, s_copy.bodies[0].position.y) + def testDeleteSpaceWithObjects(self) -> None: + s = p.Space() + + b = p.Body(1) + + c = p.Circle(b, 10) + + j = p.PinJoint(b, s.static_body) + + s.add(b, c, j) + + del s + + self.assertIsNone(b.space) + self.assertIsNone(c.space) + self.assertEqual(j.a, b) + self.assertEqual(j.b.body_type, p.Body.STATIC) + def f1(*args: Any, **kwargs: Any) -> None: pass From fe322bb9af486fc704f183e5cfbd8d29aefdad7a Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 16 Mar 2025 21:23:13 +0100 Subject: [PATCH 24/80] Reverse dependency between shape and body, so shape has the weak ref #275 --- pymunk/body.py | 7 ++----- pymunk/shapes.py | 36 ++++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/pymunk/body.py b/pymunk/body.py index fda7caef..f488aa55 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -224,7 +224,7 @@ def freebody(cp_body: ffi.CData) -> None: self._constraints: WeakSet["Constraint"] = ( WeakSet() ) # weak refs to any constraints attached - self._shapes: WeakSet["Shape"] = WeakSet() # weak refs to any shapes attached + self._shapes: dict["Shape", None] = {} d = ffi.new_handle(self) self._data_handle = d # to prevent gc to collect the handle @@ -660,10 +660,7 @@ def constraints(self) -> Set["Constraint"]: @property def shapes(self) -> Set["Shape"]: - """Get the shapes attached to this body. - - The body only keeps a weak reference to the shapes and a live - body wont prevent GC of the attached shapes""" + """Get the shapes attached to this body.""" return set(self._shapes) def local_to_world(self, v: Tuple[float, float]) -> Vec2d: diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 86625e26..5ee8fe34 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,5 +1,6 @@ __docformat__ = "reStructuredText" +import weakref from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple if TYPE_CHECKING: @@ -42,18 +43,19 @@ class Shape(PickleMixin, TypingAttrMixing, object): _pickle_attrs_skip = PickleMixin._pickle_attrs_skip + ["mass", "density"] _space = None # Weak ref to the space holding this body (if any) - - _id_counter = 1 + _dead_ref = weakref.ref(set()) def __init__(self, shape: "Shape") -> None: self._shape = shape - self._body: Optional["Body"] = shape.body + self._body: weakref.ref = weakref.ref(shape.body) def _init(self, body: Optional["Body"], _shape: ffi.CData) -> None: - self._body = body if body is not None: - body._shapes.add(self) + self._body = weakref.ref(body) + body._shapes[self] = None + else: + self._body = Shape._dead_ref def shapefree(cp_shape: ffi.CData) -> None: cp_space = cp.cpShapeGetSpace(cp_shape) @@ -225,19 +227,25 @@ def _set_surface_velocity(self, surface_v: Tuple[float, float]) -> None: @property def body(self) -> Optional["Body"]: - """The body this shape is attached to. Can be set to None to - indicate that this shape doesnt belong to a body.""" - return self._body + """The body this shape is attached to. + + Can be set to None to indicate that this shape doesnt belong to a body. + The shape only holds a weakref to the Body, meaning it wont prevent it + from being GCed. + """ + return self._body() @body.setter def body(self, body: Optional["Body"]) -> None: - if self._body is not None: - self._body._shapes.remove(self) - body_body = ffi.NULL if body is None else body._body - cp.cpShapeSetBody(self._shape, body_body) + if self.body is not None: + del self.body._shapes[self] + cp_body = ffi.NULL if body is None else body._body + cp.cpShapeSetBody(self._shape, cp_body) if body is not None: - body._shapes.add(self) - self._body = body + body._shapes[self] = None + self._body = weakref.ref(body) + else: + self._body = Shape._dead_ref def update(self, transform: Transform) -> BB: """Update, cache and return the bounding box of a shape with an From 83ffa01321471f9b4adf0b3e7025db0ba1f4639f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 16 Mar 2025 23:19:10 +0100 Subject: [PATCH 25/80] Add latest changes to changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b29a2d12..80f91489 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,9 @@ Changelog Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) Improved vec2d documentation Improved Poly documentation + Improved Shape documentation + Removed unused code + Fix issue with accessing body.space after space is deleted and GCed. Extra thanks for aetle for a number of suggestions for improvements in this pymunk release From 2d6e031bca7303600ee35ab35b9512b784c3ce0b Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 17 Mar 2025 21:31:00 +0100 Subject: [PATCH 26/80] Fix test of deleted space --- pymunk/tests/test_space.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 41922de8..74db2177 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1170,17 +1170,20 @@ def testPickleCachedArbiters(self) -> None: def testDeleteSpaceWithObjects(self) -> None: s = p.Space() - b = p.Body(1) - c = p.Circle(b, 10) - - j = p.PinJoint(b, s.static_body) + static_body = s.static_body # to stop it from GC + j = p.PinJoint(b, static_body) s.add(b, c, j) del s + # needed for pypy + import gc + + gc.collect() + self.assertIsNone(b.space) self.assertIsNone(c.space) self.assertEqual(j.a, b) From f6bf84963778f2412f850d6835cdb773ede43285 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 17 Mar 2025 21:46:07 +0100 Subject: [PATCH 27/80] Drop support for Python 3.8 --- .github/workflows/wheels.yml | 9 +++++---- README.rst | 3 ++- pymunk/body.py | 13 ++++++++++++- pyproject.toml | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d15503f5..56246759 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,7 +8,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-14] + os: + [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 @@ -21,10 +22,10 @@ jobs: python3 -c "import pathlib,glob;pathlib.Path('GITHUB_ENV').write_text('SDIST_PATH' + glob.glob('dist/*.tar.gz')[0])" - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BUILD: - "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* pp39-* pp310-*" + "cp39-* cp310-* cp311-* cp312-* cp313-* pp39-* pp310-* pp311-*" # "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" CIBW_TEST_COMMAND: "python -m pymunk.tests" # CIBW_BUILD_VERBOSITY: 3 @@ -56,7 +57,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - uses: pypa/cibuildwheel@v2.22.0 + - uses: pypa/cibuildwheel@v2.23.1 env: CIBW_PLATFORM: pyodide PYMUNK_BUILD_SLIM: 1 diff --git a/README.rst b/README.rst index 054893d6..75698516 100644 --- a/README.rst +++ b/README.rst @@ -152,7 +152,8 @@ Older Pythons - Support for Python 2 (and Python 3.0 - 3.5) was dropped with Pymunk 6.0. - Support for Python 3.6 was dropped with Pymunk 6.5.2. -- Support for Python 3.7 was dropped with Pymunk 6.9.0 +- Support for Python 3.7 was dropped with Pymunk 6.9.0. +- Support for Python 3.8 was dropped with Pymunk 7.0.0. If you use any of these legacy versions of Python, please use an older Pymunk version. (It might work on newer Pymunks as well, but it's not tested, diff --git a/pymunk/body.py b/pymunk/body.py index f488aa55..077eed3a 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -660,7 +660,18 @@ def constraints(self) -> Set["Constraint"]: @property def shapes(self) -> Set["Shape"]: - """Get the shapes attached to this body.""" + """Get the shapes attached to this body. + + In case you only have a single shape attached to the body you can + unpack it out easily: + + >>> from pymunk import Circle + >>> b = Body(1) + >>> circle = Circle(b, 2) + >>> [shape] = b.shapes + >>> shape == circle + True + """ return set(self._shapes) def local_to_world(self, v: Tuple[float, float]) -> Vec2d: diff --git a/pyproject.toml b/pyproject.toml index 1941dfda..56fce9ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", ] -requires-python = ">=3.8" +requires-python = ">=3.9" [project.optional-dependencies] dev = ["pyglet", "pygame", "sphinx", "aafigure", "wheel", "matplotlib", "numpy"] From 34735327cf0cfbfed1a7b3688423c97c18e20059 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 23 Mar 2025 22:39:15 +0100 Subject: [PATCH 28/80] Changed Body.constraints to return a KeysView of the constraints attaced. #275 --- CHANGELOG.rst | 5 +++++ pymunk/_weakkeysview.py | 25 +++++++++++++++++++++++++ pymunk/body.py | 17 ++++++++--------- pymunk/constraints.py | 13 ++++++++----- pymunk/tests/test_common.py | 31 +++++++++++++++++++++++++++++++ pymunk/tests/test_constraint.py | 12 ++++++++++++ 6 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 pymunk/_weakkeysview.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80f91489..f4bfecce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,10 @@ Changelog Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track + Dropped support for Python 3.8 + Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. + Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. + Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. @@ -20,6 +24,7 @@ Changelog Improved Shape documentation Removed unused code Fix issue with accessing body.space after space is deleted and GCed. + Build wheels for Linux ARM and Pypy 3.11 Extra thanks for aetle for a number of suggestions for improvements in this pymunk release diff --git a/pymunk/_weakkeysview.py b/pymunk/_weakkeysview.py new file mode 100644 index 00000000..4faa013c --- /dev/null +++ b/pymunk/_weakkeysview.py @@ -0,0 +1,25 @@ +from collections.abc import Iterator, KeysView +from typing import TYPE_CHECKING, TypeVar +from weakref import WeakKeyDictionary + +# Can be simplified in Python 3.12, PEP 695 +KT = TypeVar("KT") +VT = TypeVar("VT") + + +class WeakKeysView(KeysView[KT]): + def __init__(self, weak_dict: WeakKeyDictionary[KT, VT]) -> None: + self._weak_dict = weak_dict + + def __iter__(self) -> Iterator[KT]: + return iter(self._weak_dict.keys()) + + def __len__(self) -> int: + return len(self._weak_dict) + + def __contains__(self, key: KT) -> bool: + return key in self._weak_dict + + def __repr__(self) -> str: + # Provide a string representation of the keys view + return f"{self.__class__.__name__}({list(self._weak_dict.keys())})" diff --git a/pymunk/body.py b/pymunk/body.py index 077eed3a..79e39eef 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -1,6 +1,7 @@ __docformat__ = "reStructuredText" import weakref +from collections.abc import KeysView from typing import ( # Literal, TYPE_CHECKING, Any, @@ -9,9 +10,8 @@ Optional, Set, Tuple, - Union, ) -from weakref import WeakSet +from weakref import WeakKeyDictionary if TYPE_CHECKING: from .space import Space @@ -21,6 +21,7 @@ from ._chipmunk_cffi import ffi, lib from ._pickle import PickleMixin, _State from ._typing_attr import TypingAttrMixing +from ._weakkeysview import WeakKeysView from .vec2d import Vec2d _BodyType = int @@ -113,7 +114,7 @@ class Body(PickleMixin, TypingAttrMixing, object): _position_func: Optional[_PositionFunc] = None _velocity_func: Optional[_VelocityFunc] = None - _dead_ref = weakref.ref(set()) + _dead_ref: weakref.ref = weakref.ref(set()) def __init__( self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC @@ -221,9 +222,7 @@ def freebody(cp_body: ffi.CData) -> None: self._space: weakref.ref = Body._dead_ref - self._constraints: WeakSet["Constraint"] = ( - WeakSet() - ) # weak refs to any constraints attached + self._constraints: WeakKeyDictionary = WeakKeyDictionary() self._shapes: dict["Shape", None] = {} d = ffi.new_handle(self) @@ -620,7 +619,7 @@ def body_type(self, body_type: _BodyType) -> None: def each_arbiter( self, - func: Callable[..., None], # TODO: Fix me once PEP 612 is ready + func: Callable[..., None], # TODO: Fix me once PEP 612 is ready (Python 3.10) *args: Any, **kwargs: Any, ) -> None: @@ -645,7 +644,7 @@ def each_arbiter( lib.cpBodyEachArbiter(self._body, lib.ext_cpBodyArbiterIteratorFunc, data) @property - def constraints(self) -> Set["Constraint"]: + def constraints(self) -> KeysView["Constraint"]: """Get the constraints this body is attached to. It is not possible to detach a body from a constraint. The only way is @@ -656,7 +655,7 @@ def constraints(self) -> Set["Constraint"]: collected it will automatically be removed from this collection as well. """ - return set(self._constraints) + return WeakKeysView(self._constraints) @property def shapes(self) -> Set["Shape"]: diff --git a/pymunk/constraints.py b/pymunk/constraints.py index 1812704f..ce401a92 100644 --- a/pymunk/constraints.py +++ b/pymunk/constraints.py @@ -28,7 +28,7 @@ This submodule contain all the constraints that are supported by Pymunk. -All the constraints support copy and pickle from the standard library. Custom +All the constraints support copy and pickle from the standard library. Custom properties set on a constraint will also be copied/pickled. Chipmunk has a good overview of the different constraint on youtube which @@ -102,6 +102,9 @@ class Constraint(PickleMixin, TypingAttrMixing, object): _pre_solve_func: Optional[Callable[["Constraint", "Space"], None]] = None _post_solve_func: Optional[Callable[["Constraint", "Space"], None]] = None + _a: "Body" + _b: "Body" + def __init__(self, constraint: ffi.CData) -> None: self._constraint = constraint @@ -201,8 +204,8 @@ def b(self) -> "Body": def activate_bodies(self) -> None: """Activate the bodies this constraint is attached to""" - self._a.activate() - self._b.activate() + self.a.activate() + self.b.activate() @property def pre_solve(self) -> Optional[Callable[["Constraint", "Space"], None]]: @@ -268,8 +271,8 @@ def _set_bodies(self, a: "Body", b: "Body") -> None: ), "At least one of the two bodies attached to a constraint must be DYNAMIC." self._a = a self._b = b - a._constraints.add(self) - b._constraints.add(self) + a._constraints[self] = None + b._constraints[self] = None def __getstate__(self) -> Dict[str, List[Tuple[str, Any]]]: """Return the state of this object diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index 0f0ca79e..ab2f5e88 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -1,7 +1,10 @@ +import gc import unittest +import weakref from typing import Any, List import pymunk as p +from pymunk._weakkeysview import WeakKeysView from pymunk.vec2d import Vec2d @@ -170,3 +173,31 @@ def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: b2.position = 22, 0 space.step(1) # print(3) + + def testWeakKeysView(self) -> None: + x1, x2, x3 = p.Body(1), p.Body(2), p.Body(3) + + d = weakref.WeakKeyDictionary() + + d[x1] = 1 + d[x2] = 2 + + keys1 = WeakKeysView(d) + keys2 = WeakKeysView(d) + + del x2 + d[x3] = 3 + + iterations = 0 + for x in keys2: + iterations += 1 + del x3 + gc.collect() + + self.assertEqual(iterations, 1) + gc.collect() + + self.assertEqual(len(d), 1) + self.assertEqual(list(keys1), [x1]) + self.assertEqual(list(keys2), [x1]) + self.assertEqual(list(d.keys()), [x1]) diff --git a/pymunk/tests/test_constraint.py b/pymunk/tests/test_constraint.py index 23e93c27..a5186ea7 100644 --- a/pymunk/tests/test_constraint.py +++ b/pymunk/tests/test_constraint.py @@ -1,3 +1,4 @@ +import gc import pickle import unittest @@ -199,6 +200,17 @@ def test_body_types(self) -> None: with self.assertRaises(AssertionError): _ = PivotJoint(a, b, (1, 2)) + def test_delete(self) -> None: + a, b = p.Body(1), p.Body(2) + j1 = PivotJoint(a, b, (0, 0)) + j2 = PivotJoint(a, b, (0, 0)) + + del j1 + gc.collect() + + self.assertListEqual(list(a.constraints), [j2]) + self.assertListEqual(list(b.constraints), [j2]) + class UnitTestPinJoint(unittest.TestCase): def testAnchor(self) -> None: From 7e7b63d27cb1053e52030931cfa58aabf9204cd9 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 23 Mar 2025 22:42:25 +0100 Subject: [PATCH 29/80] Changed Body.Shapes to return a KeysView instead of set #275 --- CHANGELOG.rst | 2 +- pymunk/body.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4bfecce..4283c7e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,7 @@ Changelog Dropped support for Python 3.8 Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. - + Changed body.shapes to return a KeysView instead of a set of the shapes. Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/pymunk/body.py b/pymunk/body.py index 79e39eef..1f644576 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -658,7 +658,7 @@ def constraints(self) -> KeysView["Constraint"]: return WeakKeysView(self._constraints) @property - def shapes(self) -> Set["Shape"]: + def shapes(self) -> KeysView["Shape"]: """Get the shapes attached to this body. In case you only have a single shape attached to the body you can @@ -671,7 +671,7 @@ def shapes(self) -> Set["Shape"]: >>> shape == circle True """ - return set(self._shapes) + return self._shapes.keys() def local_to_world(self, v: Tuple[float, float]) -> Vec2d: """Convert body local coordinates to world space coordinates From 7fda2795170c4de4986047afe9040737fe22d1ba Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 23 Mar 2025 22:53:20 +0100 Subject: [PATCH 30/80] Improved and fixed some types --- pymunk/_weakkeysview.py | 3 +-- pymunk/body.py | 6 +++--- pymunk/shapes.py | 7 ++++--- pymunk/space.py | 1 + pymunk/tests/test_arbiter.py | 6 ++---- pymunk/tests/test_common.py | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pymunk/_weakkeysview.py b/pymunk/_weakkeysview.py index 4faa013c..1171fd54 100644 --- a/pymunk/_weakkeysview.py +++ b/pymunk/_weakkeysview.py @@ -17,9 +17,8 @@ def __iter__(self) -> Iterator[KT]: def __len__(self) -> int: return len(self._weak_dict) - def __contains__(self, key: KT) -> bool: + def __contains__(self, key: object) -> bool: return key in self._weak_dict def __repr__(self) -> str: - # Provide a string representation of the keys view return f"{self.__class__.__name__}({list(self._weak_dict.keys())})" diff --git a/pymunk/body.py b/pymunk/body.py index 1f644576..8be312f7 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -114,7 +114,7 @@ class Body(PickleMixin, TypingAttrMixing, object): _position_func: Optional[_PositionFunc] = None _velocity_func: Optional[_VelocityFunc] = None - _dead_ref: weakref.ref = weakref.ref(set()) + _dead_ref: weakref.ref[Any] = weakref.ref(set()) def __init__( self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC @@ -220,9 +220,9 @@ def freebody(cp_body: ffi.CData) -> None: elif body_type == Body.STATIC: self._body = ffi.gc(lib.cpBodyNewStatic(), freebody) - self._space: weakref.ref = Body._dead_ref + self._space: weakref.ref["Space"] = Body._dead_ref - self._constraints: WeakKeyDictionary = WeakKeyDictionary() + self._constraints: WeakKeyDictionary["Constraint", None] = WeakKeyDictionary() self._shapes: dict["Shape", None] = {} d = ffi.new_handle(self) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 5ee8fe34..b7528ed6 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,7 +1,7 @@ __docformat__ = "reStructuredText" import weakref -from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple if TYPE_CHECKING: from .body import Body @@ -43,11 +43,12 @@ class Shape(PickleMixin, TypingAttrMixing, object): _pickle_attrs_skip = PickleMixin._pickle_attrs_skip + ["mass", "density"] _space = None # Weak ref to the space holding this body (if any) - _dead_ref = weakref.ref(set()) + _dead_ref: weakref.ref[Any] = weakref.ref(set()) def __init__(self, shape: "Shape") -> None: self._shape = shape - self._body: weakref.ref = weakref.ref(shape.body) + assert shape.body != None + self._body: weakref.ref["Body"] = weakref.ref(shape.body) def _init(self, body: Optional["Body"], _shape: ffi.CData) -> None: diff --git a/pymunk/space.py b/pymunk/space.py index dcee92cd..72be91e5 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -1,5 +1,6 @@ __docformat__ = "reStructuredText" +import math import platform import weakref from typing import ( diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index efccff94..4c0b9892 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -197,9 +197,8 @@ def testTotalKE(self) -> None: s.add(b1, c1, b2, c2) - def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.total_ke, 43.438914027) - return True s.add_collision_handler(1, 2).post_solve = post_solve @@ -292,10 +291,9 @@ def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called2 = False - def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called2 = True self.assertTrue(arb.is_removal) - return True s.add_collision_handler(1, 2).separate = separate2 s.remove(b1, c1) diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index ab2f5e88..a395721f 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -177,7 +177,7 @@ def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: def testWeakKeysView(self) -> None: x1, x2, x3 = p.Body(1), p.Body(2), p.Body(3) - d = weakref.WeakKeyDictionary() + d: weakref.WeakKeyDictionary[p.Body, int] = weakref.WeakKeyDictionary() d[x1] = 1 d[x2] = 2 From 39f562a846d0803974b7000393aada5567f77c1d Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 24 Mar 2025 21:32:01 +0100 Subject: [PATCH 31/80] Refactored body.space weakref implementation --- pymunk/_util.py | 4 ++++ pymunk/body.py | 16 +++------------- pymunk/examples/colors.py | 5 ++--- pymunk/shapes.py | 18 ++++++------------ pymunk/space.py | 10 ++++------ 5 files changed, 19 insertions(+), 34 deletions(-) create mode 100644 pymunk/_util.py diff --git a/pymunk/_util.py b/pymunk/_util.py new file mode 100644 index 00000000..0128dddf --- /dev/null +++ b/pymunk/_util.py @@ -0,0 +1,4 @@ +import weakref +from typing import Any + +_dead_ref: weakref.ref[Any] = weakref.ref(set()) diff --git a/pymunk/body.py b/pymunk/body.py index 8be312f7..e42d222e 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -2,15 +2,7 @@ import weakref from collections.abc import KeysView -from typing import ( # Literal, - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Optional, - Set, - Tuple, -) +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple # Literal, from weakref import WeakKeyDictionary if TYPE_CHECKING: @@ -21,6 +13,7 @@ from ._chipmunk_cffi import ffi, lib from ._pickle import PickleMixin, _State from ._typing_attr import TypingAttrMixing +from ._util import _dead_ref from ._weakkeysview import WeakKeysView from .vec2d import Vec2d @@ -114,8 +107,6 @@ class Body(PickleMixin, TypingAttrMixing, object): _position_func: Optional[_PositionFunc] = None _velocity_func: Optional[_VelocityFunc] = None - _dead_ref: weakref.ref[Any] = weakref.ref(set()) - def __init__( self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC ) -> None: @@ -220,8 +211,7 @@ def freebody(cp_body: ffi.CData) -> None: elif body_type == Body.STATIC: self._body = ffi.gc(lib.cpBodyNewStatic(), freebody) - self._space: weakref.ref["Space"] = Body._dead_ref - + self._space: weakref.ref["Space"] = _dead_ref self._constraints: WeakKeyDictionary["Constraint", None] = WeakKeyDictionary() self._shapes: dict["Shape", None] = {} diff --git a/pymunk/examples/colors.py b/pymunk/examples/colors.py index 6398a21e..66ca94eb 100644 --- a/pymunk/examples/colors.py +++ b/pymunk/examples/colors.py @@ -1,10 +1,9 @@ """ -An example of the determinism of pymunk by coloring balls according to their -position, and then respawning them to verify each ball ends up in the same +An example of the determinism of pymunk by coloring balls according to their +position, and then respawning them to verify each ball ends up in the same place. Inspired by Pymunk user Nam Dao. """ - import random import pygame diff --git a/pymunk/shapes.py b/pymunk/shapes.py index b7528ed6..128dbb58 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,7 +1,7 @@ __docformat__ = "reStructuredText" import weakref -from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence, Tuple if TYPE_CHECKING: from .body import Body @@ -11,6 +11,7 @@ from ._chipmunk_cffi import lib as cp from ._pickle import PickleMixin, _State from ._typing_attr import TypingAttrMixing +from ._util import _dead_ref from .bb import BB from .contact_point_set import ContactPointSet from .query_info import PointQueryInfo, SegmentQueryInfo @@ -42,8 +43,7 @@ class Shape(PickleMixin, TypingAttrMixing, object): ] _pickle_attrs_skip = PickleMixin._pickle_attrs_skip + ["mass", "density"] - _space = None # Weak ref to the space holding this body (if any) - _dead_ref: weakref.ref[Any] = weakref.ref(set()) + _space: weakref.ref["Space"] = _dead_ref def __init__(self, shape: "Shape") -> None: self._shape = shape @@ -56,7 +56,7 @@ def _init(self, body: Optional["Body"], _shape: ffi.CData) -> None: self._body = weakref.ref(body) body._shapes[self] = None else: - self._body = Shape._dead_ref + self._body = _dead_ref def shapefree(cp_shape: ffi.CData) -> None: cp_space = cp.cpShapeGetSpace(cp_shape) @@ -246,7 +246,7 @@ def body(self, body: Optional["Body"]) -> None: body._shapes[self] = None self._body = weakref.ref(body) else: - self._body = Shape._dead_ref + self._body = _dead_ref def update(self, transform: Transform) -> BB: """Update, cache and return the bounding box of a shape with an @@ -338,13 +338,7 @@ def space(self) -> Optional["Space"]: """Get the :py:class:`Space` that shape has been added to (or None). """ - if self._space is not None: - try: - return self._space._get_self() # ugly hack because of weakref - except ReferenceError: - return None - else: - return None + return self._space() @property def _hashid(self) -> int: diff --git a/pymunk/space.py b/pymunk/space.py index 72be91e5..33c81be7 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -27,6 +27,7 @@ cp = lib from ._pickle import PickleMixin, _State +from ._util import _dead_ref from .arbiter import _arbiter_from_dict, _arbiter_to_dict from .body import Body from .collision_handler import CollisionHandler @@ -146,9 +147,6 @@ def spacefree(cp_space: ffi.CData) -> None: self._remove_later: Set[_AddableObjects] = set() self._bodies_to_check: Set[Body] = set() - def _get_self(self) -> "Space": - return self - @property def shapes(self) -> List[Shape]: """A list of all the shapes added to this space @@ -399,7 +397,7 @@ def _add_shape(self, shape: "Shape") -> None: shape.body.space == self ), "The shape's body must be added to the space before (or at the same time) as the shape." - shape._space = weakref.proxy(self) + shape._space = weakref.ref(self) self._shapes[shape] = None cp.cpSpaceAddShape(self._space, shape._shape) @@ -429,7 +427,7 @@ def _remove_shape(self, shape: "Shape") -> None: """Removes a shape from the space""" assert shape in self._shapes, "shape not in space, already removed?" self._removed_shapes[shape] = None - shape._space = None + shape._space = _dead_ref # During GC at program exit sometimes the shape might already be removed. Then skip this step. if cp.cpSpaceContainsShape(self._space, shape._shape): cp.cpSpaceRemoveShape(self._space, shape._shape) @@ -438,7 +436,7 @@ def _remove_shape(self, shape: "Shape") -> None: def _remove_body(self, body: "Body") -> None: """Removes a body from the space""" assert body in self._bodies, "body not in space, already removed?" - body._space = body._dead_ref + body._space = _dead_ref if body in self._bodies_to_check: self._bodies_to_check.remove(body) # During GC at program exit sometimes the shape might already be removed. Then skip this step. From d77494046cecfe4d56ccd5a7b004ada32deb2c79 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 24 Mar 2025 21:48:10 +0100 Subject: [PATCH 32/80] Removed unused imports --- CHANGELOG.rst | 1 + pymunk/contact_point_set.py | 2 +- pymunk/pygame_util.py | 8 ++++---- pymunk/shapes.py | 2 +- pymunk/space_debug_draw_options.py | 26 ++++++++++++-------------- pymunk/transform.py | 2 +- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4283c7e4..f4102691 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,7 @@ Changelog Removed unused code Fix issue with accessing body.space after space is deleted and GCed. Build wheels for Linux ARM and Pypy 3.11 + Minor internal refactorings for easier and cleaner code Extra thanks for aetle for a number of suggestions for improvements in this pymunk release diff --git a/pymunk/contact_point_set.py b/pymunk/contact_point_set.py index fdb4967d..e3b89fe2 100644 --- a/pymunk/contact_point_set.py +++ b/pymunk/contact_point_set.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING, List if TYPE_CHECKING: from ._chipmunk_cffi import ffi diff --git a/pymunk/pygame_util.py b/pymunk/pygame_util.py index 7c4de82a..bb8f0aae 100644 --- a/pymunk/pygame_util.py +++ b/pymunk/pygame_util.py @@ -21,12 +21,12 @@ # SOFTWARE. # ---------------------------------------------------------------------------- -"""This submodule contains helper functions to help with quick prototyping +"""This submodule contains helper functions to help with quick prototyping using pymunk together with pygame or pygame-ce. Intended to help with debugging and prototyping, not for actual production use -in a full application. The methods contained in this module is opinionated -about your coordinate system and not in any way optimized. +in a full application. The methods contained in this module is opinionated +about your coordinate system and not in any way optimized. """ __docformat__ = "reStructuredText" @@ -39,7 +39,7 @@ "positive_y_is_up", ] -from typing import List, Sequence, Tuple +from typing import Sequence, Tuple import pygame diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 128dbb58..f093731a 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,7 +1,7 @@ __docformat__ = "reStructuredText" import weakref -from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple if TYPE_CHECKING: from .body import Body diff --git a/pymunk/space_debug_draw_options.py b/pymunk/space_debug_draw_options.py index a76c16c7..c30c5416 100644 --- a/pymunk/space_debug_draw_options.py +++ b/pymunk/space_debug_draw_options.py @@ -6,8 +6,6 @@ from .shapes import Shape from types import TracebackType -import math - from ._chipmunk_cffi import ffi, lib from .body import Body from .transform import Transform @@ -101,7 +99,7 @@ def __init__(self) -> None: @property def shape_outline_color(self) -> SpaceDebugColor: """The outline color of shapes. - + Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). Example: @@ -158,7 +156,7 @@ def collision_point_color(self) -> SpaceDebugColor: """The color of collisions. Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). - + Example: >>> import pymunk @@ -203,11 +201,11 @@ def _c(self, color: ffi.CData) -> SpaceDebugColor: def flags(self) -> _DrawFlags: """Bit flags which of shapes, joints and collisions should be drawn. - By default all 3 flags are set, meaning shapes, joints and collisions + By default all 3 flags are set, meaning shapes, joints and collisions will be drawn. Example using the basic text only DebugDraw implementation (normally - you would the desired backend instead, such as + you would the desired backend instead, such as `pygame_util.DrawOptions` or `pyglet_util.DrawOptions`): >>> import pymunk @@ -218,11 +216,11 @@ def flags(self) -> _DrawFlags: >>> s.add(b, c) >>> s.add(pymunk.Circle(s.static_body, 3)) >>> s.step(0.01) - >>> options = pymunk.SpaceDebugDrawOptions() + >>> options = pymunk.SpaceDebugDrawOptions() >>> # Only draw the shapes, nothing else: >>> options.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES - >>> s.debug_draw(options) + >>> s.debug_draw(options) draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=52.0, g=152.0, b=219.0, a=255.0)) draw_circle (Vec2d(0.0, 0.0), 0.0, 3.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) @@ -243,17 +241,17 @@ def flags(self, f: _DrawFlags) -> None: @property def transform(self) -> Transform: - """The transform is applied before drawing, e.g for scaling or + """The transform is applied before drawing, e.g for scaling or translation. - Example: + Example: >>> import pymunk >>> s = pymunk.Space() >>> c = pymunk.Circle(s.static_body, 10) >>> s.add(c) - >>> options = pymunk.SpaceDebugDrawOptions() - >>> s.debug_draw(options) + >>> options = pymunk.SpaceDebugDrawOptions() + >>> s.debug_draw(options) draw_circle (Vec2d(0.0, 0.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) >>> options.transform = pymunk.Transform.scaling(2) >>> s.debug_draw(options) @@ -263,9 +261,9 @@ def transform(self) -> Transform: draw_circle (Vec2d(2.0, 3.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=149.0, g=165.0, b=166.0, a=255.0)) .. Note:: - Not all tranformations are supported by the debug drawing logic. + Not all tranformations are supported by the debug drawing logic. Uniform scaling and translation are supported, but not rotation, - linear stretching or shearing. + linear stretching or shearing. """ t = self._options.transform return Transform(t.a, t.b, t.c, t.d, t.tx, t.ty) diff --git a/pymunk/transform.py b/pymunk/transform.py index 7db941e8..192f6e62 100644 --- a/pymunk/transform.py +++ b/pymunk/transform.py @@ -1,5 +1,5 @@ import math -from typing import NamedTuple, Tuple, Union, cast, overload +from typing import NamedTuple, Tuple, Union, overload from .vec2d import Vec2d From bd0295a6799de88047a7a38bb1dd8efd20e64318 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 24 Mar 2025 21:56:53 +0100 Subject: [PATCH 33/80] Minor cleanup --- pymunk/autogeometry.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pymunk/autogeometry.py b/pymunk/autogeometry.py index 2924f199..34d697dc 100644 --- a/pymunk/autogeometry.py +++ b/pymunk/autogeometry.py @@ -1,4 +1,4 @@ -"""This module contain functions for automatic generation of geometry, for +"""This module contain functions for automatic generation of geometry, for example from an image. Example:: @@ -23,7 +23,7 @@ >>> print(len(pl_set)) 2 -The information in segments can now be used to create geometry, for example as +The information in segments can now be used to create geometry, for example as a Pymunk Poly or Segment:: >>> s = pymunk.Space() @@ -31,11 +31,12 @@ ... for i in range(len(poly_line) - 1): ... a = poly_line[i] ... b = poly_line[i + 1] - ... segment = pymunk.Segment(s.static_body, a, b, 1) + ... segment = pymunk.Segment(s.static_body, a, b, 1) ... s.add(segment) """ + __docformat__ = "reStructuredText" from typing import TYPE_CHECKING, Callable, List, Sequence, Tuple, Union, overload @@ -47,7 +48,6 @@ from ._chipmunk_cffi import ffi, lib from .vec2d import Vec2d -_SegmentFunc = Callable[[Tuple[float, float], Tuple[float, float]], None] _SampleFunc = Callable[[Tuple[float, float]], float] _Polyline = Union[List[Tuple[float, float]], List[Vec2d]] @@ -201,19 +201,19 @@ def __len__(self) -> int: return self._set.count @overload - def __getitem__(self, index: int) -> List[Vec2d]: - ... + def __getitem__(self, index: int) -> List[Vec2d]: ... @overload - def __getitem__(self, index: slice) -> "PolylineSet": - ... + def __getitem__(self, index: slice) -> "PolylineSet": ... - def __getitem__(self, key: Union[int, slice]) -> Union[List[Vec2d], "PolylineSet"]: - assert not isinstance(key, slice), "Slice indexing not supported" - if key >= self._set.count: + def __getitem__( + self, index: Union[int, slice] + ) -> Union[List[Vec2d], "PolylineSet"]: + assert not isinstance(index, slice), "Slice indexing not supported" + if index >= self._set.count: raise IndexError line = [] - l = self._set.lines[key] + l = self._set.lines[index] for i in range(l.count): line.append(Vec2d(l.verts[i].x, l.verts[i].y)) return line From 5e99b08423d3e3e8d835785c62efb697568533c4 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 25 Mar 2025 22:03:55 +0100 Subject: [PATCH 34/80] Refactor how to get a shape from a cp_shape --- pymunk/_callbacks.py | 15 ++++++++------- pymunk/arbiter.py | 22 +++++++++++----------- pymunk/shapes.py | 7 +++++++ pymunk/space.py | 9 ++------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index e70acef4..0ce3f474 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -6,6 +6,7 @@ from .arbiter import Arbiter from .contact_point_set import ContactPointSet from .query_info import PointQueryInfo, SegmentQueryInfo, ShapeQueryInfo +from .shapes import Shape from .vec2d import Vec2d _logger = logging.getLogger(__name__) @@ -22,7 +23,7 @@ def ext_cpSpacePointQueryFunc( data: ffi.CData, ) -> None: self, query_hits = ffi.from_handle(data) - shape = self._get_shape(_shape) + shape = Shape._from_cp_shape(_shape) p = PointQueryInfo( shape, Vec2d(point.x, point.y), distance, Vec2d(gradient.x, gradient.y) ) @@ -38,7 +39,7 @@ def ext_cpSpaceSegmentQueryFunc( data: ffi.CData, ) -> None: self, query_hits = ffi.from_handle(data) - shape = self._get_shape(_shape) + shape = Shape._from_cp_shape(_shape) p = SegmentQueryInfo( shape, Vec2d(point.x, point.y), Vec2d(normal.x, normal.y), alpha ) @@ -47,8 +48,8 @@ def ext_cpSpaceSegmentQueryFunc( @ffi.def_extern() def ext_cpSpaceBBQueryFunc(_shape: ffi.CData, data: ffi.CData) -> None: - self, query_hits = ffi.from_handle(data) - shape = self._get_shape(_shape) + _, query_hits = ffi.from_handle(data) + shape = Shape._from_cp_shape(_shape) assert shape is not None query_hits.append(shape) @@ -58,7 +59,7 @@ def ext_cpSpaceShapeQueryFunc( _shape: ffi.CData, _points: ffi.CData, data: ffi.CData ) -> None: self, query_hits = ffi.from_handle(data) - found_shape = self._get_shape(_shape) + found_shape = Shape._from_cp_shape(_shape) point_set = ContactPointSet._from_cp(_points) info = ShapeQueryInfo(found_shape, point_set) query_hits.append(info) @@ -171,8 +172,8 @@ def ext_cpSpaceDebugDrawDotImpl( @ffi.def_extern() def ext_cpSpaceDebugDrawColorForShapeImpl(_shape: ffi.CData, data: ffi.CData) -> None: - options, space = ffi.from_handle(data) - shape = space._get_shape(_shape) + options, _ = ffi.from_handle(data) + shape = Shape._from_cp_shape(_shape) return options.color_for_shape(shape) diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 48037a2b..8ad2b77a 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -1,14 +1,14 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Tuple, Dict, List, Any, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple if TYPE_CHECKING: from .space import Space - from .shapes import Shape from ._chipmunk_cffi import ffi, lib from .contact_point_set import ContactPointSet +from .shapes import Shape from .vec2d import Vec2d @@ -40,7 +40,7 @@ def __init__(self, _arbiter: ffi.CData, space: "Space") -> None: @property def contact_point_set(self) -> ContactPointSet: - """Contact point sets make getting contact information from the + """Contact point sets make getting contact information from the Arbiter simpler. Return `ContactPointSet`""" @@ -79,18 +79,18 @@ def shapes(self) -> Tuple["Shape", "Shape"]: lib.cpArbiterGetShapes(self._arbiter, shapeA_p, shapeB_p) - a, b = self._space._get_shape(shapeA_p[0]), self._space._get_shape(shapeB_p[0]) + a, b = Shape._from_cp_shape(shapeA_p[0]), Shape._from_cp_shape(shapeB_p[0]) assert a is not None assert b is not None return a, b @property def restitution(self) -> float: - """The calculated restitution (elasticity) for this collision - pair. + """The calculated restitution (elasticity) for this collision + pair. - Setting the value in a pre_solve() callback will override the value - calculated by the space. The default calculation multiplies the + Setting the value in a pre_solve() callback will override the value + calculated by the space. The default calculation multiplies the elasticity of the two shapes together. """ return lib.cpArbiterGetRestitution(self._arbiter) @@ -101,10 +101,10 @@ def restitution(self, restitution: float) -> None: @property def friction(self) -> float: - """The calculated friction for this collision pair. + """The calculated friction for this collision pair. - Setting the value in a pre_solve() callback will override the value - calculated by the space. The default calculation multiplies the + Setting the value in a pre_solve() callback will override the value + calculated by the space. The default calculation multiplies the friction of the two shapes together. """ return lib.cpArbiterGetFriction(self._arbiter) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index f093731a..6a328c14 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -348,6 +348,13 @@ def _hashid(self) -> int: def _hashid(self, v: int) -> None: cp.cpShapeSetHashID(self._shape, v) + @staticmethod + def _from_cp_shape(cp_shape: ffi.CData) -> Optional["Shape"]: + """Get Pymunk Shape from a Chipmunk Shape pointer""" + if not bool(cp_shape): + return None + return ffi.from_handle(cp.cpShapeGetUserData(cp_shape)) + def __getstate__(self) -> _State: """Return the state of this object. diff --git a/pymunk/space.py b/pymunk/space.py index 33c81be7..a5e39c02 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -740,11 +740,6 @@ def point_query( ) return query_hits - def _get_shape(self, _shape: Any) -> Optional[Shape]: - if not bool(_shape): - return None - return ffi.from_handle(cp.cpShapeGetUserData(_shape)) - def point_query_nearest( self, point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter ) -> Optional[PointQueryInfo]: @@ -777,7 +772,7 @@ def point_query_nearest( self._space, point, max_distance, shape_filter, info ) - shape = self._get_shape(_shape) + shape = shape = Shape._from_cp_shape(_shape) if shape != None: return PointQueryInfo( @@ -862,7 +857,7 @@ def segment_query_first( self._space, start, end, radius, shape_filter, info ) - shape = self._get_shape(_shape) + shape = shape = Shape._from_cp_shape(_shape) if shape != None: return SegmentQueryInfo( shape, From e6b45065bf67be0ea3994d59dd1e585826319f3f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 25 Mar 2025 22:29:01 +0100 Subject: [PATCH 35/80] Changed Query info shape properties to never have a None. #279 --- CHANGELOG.rst | 2 ++ pymunk/_callbacks.py | 9 ++++++--- pymunk/query_info.py | 14 +++++--------- pymunk/shapes.py | 11 ++++------- pymunk/space.py | 4 ++-- pymunk/tests/test_shape.py | 7 +++---- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4102691..83df0bb1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,10 +12,12 @@ Changelog Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. Changed body.shapes to return a KeysView instead of a set of the shapes. + Changed Space.segment_query to return None in case the query did not hit the shape. Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. + Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. New feature: ShapeFilter.rejects_collision() New feature: Added Vec2d.polar_tuple Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index 0ce3f474..e079132c 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -22,8 +22,9 @@ def ext_cpSpacePointQueryFunc( gradient: ffi.CData, data: ffi.CData, ) -> None: - self, query_hits = ffi.from_handle(data) + _, query_hits = ffi.from_handle(data) shape = Shape._from_cp_shape(_shape) + assert shape != None p = PointQueryInfo( shape, Vec2d(point.x, point.y), distance, Vec2d(gradient.x, gradient.y) ) @@ -38,8 +39,9 @@ def ext_cpSpaceSegmentQueryFunc( alpha: float, data: ffi.CData, ) -> None: - self, query_hits = ffi.from_handle(data) + _, query_hits = ffi.from_handle(data) shape = Shape._from_cp_shape(_shape) + assert shape != None p = SegmentQueryInfo( shape, Vec2d(point.x, point.y), Vec2d(normal.x, normal.y), alpha ) @@ -58,8 +60,9 @@ def ext_cpSpaceBBQueryFunc(_shape: ffi.CData, data: ffi.CData) -> None: def ext_cpSpaceShapeQueryFunc( _shape: ffi.CData, _points: ffi.CData, data: ffi.CData ) -> None: - self, query_hits = ffi.from_handle(data) + _, query_hits = ffi.from_handle(data) found_shape = Shape._from_cp_shape(_shape) + assert found_shape != None point_set = ContactPointSet._from_cp(_points) info = ShapeQueryInfo(found_shape, point_set) query_hits.append(info) diff --git a/pymunk/query_info.py b/pymunk/query_info.py index 7ef3c41f..def7a010 100644 --- a/pymunk/query_info.py +++ b/pymunk/query_info.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, NamedTuple, Optional +from typing import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from .contact_point_set import ContactPointSet @@ -13,8 +13,8 @@ class PointQueryInfo(NamedTuple): Space. """ - shape: Optional["Shape"] - """The nearest shape, None if no shape was within range.""" + shape: "Shape" + """The nearest shape""" point: "Vec2d" """The closest point on the shape's surface. (in world space @@ -40,18 +40,14 @@ class SegmentQueryInfo(NamedTuple): they also return where a shape was hit and it's surface normal at the hit point. This object hold that information. - To test if the query hit something, check if - SegmentQueryInfo.shape == None or not. - Segment queries are like ray casting, but because not all spatial indexes allow processing infinitely long ray queries it is limited to segments. In practice this is still very fast and you don't need to worry too much about the performance as long as you aren't using extremely long segments for your queries. - """ - shape: Optional["Shape"] + shape: "Shape" """Shape that was hit, or None if no collision occured""" point: "Vec2d" @@ -69,7 +65,7 @@ class ShapeQueryInfo(NamedTuple): they also return where a shape was hit. This object hold that information. """ - shape: Optional["Shape"] + shape: "Shape" """Shape that was hit, or None if no collision occured""" contact_point_set: "ContactPointSet" diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 6a328c14..e829afd4 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -299,9 +299,11 @@ def point_query(self, p: Tuple[float, float]) -> PointQueryInfo: def segment_query( self, start: Tuple[float, float], end: Tuple[float, float], radius: float = 0 - ) -> SegmentQueryInfo: + ) -> Optional[SegmentQueryInfo]: """Check if the line segment from start to end intersects the shape. + Returns None if it does not intersect + :rtype: :py:class:`SegmentQueryInfo` """ assert len(start) == 2 @@ -318,12 +320,7 @@ def segment_query( info.alpha, ) else: - return SegmentQueryInfo( - None, - Vec2d(info.point.x, info.point.y), - Vec2d(info.normal.x, info.normal.y), - info.alpha, - ) + return None def shapes_collide(self, b: "Shape") -> ContactPointSet: """Get contact information about this shape and shape b. diff --git a/pymunk/space.py b/pymunk/space.py index a5e39c02..1f0fafe2 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -772,7 +772,7 @@ def point_query_nearest( self._space, point, max_distance, shape_filter, info ) - shape = shape = Shape._from_cp_shape(_shape) + shape = Shape._from_cp_shape(_shape) if shape != None: return PointQueryInfo( @@ -857,7 +857,7 @@ def segment_query_first( self._space, start, end, radius, shape_filter, info ) - shape = shape = Shape._from_cp_shape(_shape) + shape = Shape._from_cp_shape(_shape) if shape != None: return SegmentQueryInfo( shape, diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index ab7b4769..c9e14b68 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -32,15 +32,14 @@ def testSegmentQuery(self) -> None: c.cache_bb() info = c.segment_query((10, -50), (10, 50)) - self.assertEqual(info.shape, None) - self.assertEqual(info.point, (10, 50)) - self.assertEqual(info.normal, (0, 0)) - self.assertEqual(info.alpha, 1.0) + self.assertEqual(info, None) info = c.segment_query((10, -50), (10, 50), 6) + assert info != None self.assertEqual(info.shape, c) info = c.segment_query((0, -50), (0, 50)) + assert info != None self.assertEqual(info.shape, c) self.assertAlmostEqual(info.point.x, 0) self.assertAlmostEqual(info.point.y, -5) From 50984d7c6405671ac2d019ab3a7fe7bd2abc5076 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 25 Mar 2025 23:10:11 +0100 Subject: [PATCH 36/80] Fix point_query.py example --- pymunk/examples/point_query.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pymunk/examples/point_query.py b/pymunk/examples/point_query.py index ed41fb33..ee00dfe8 100644 --- a/pymunk/examples/point_query.py +++ b/pymunk/examples/point_query.py @@ -1,4 +1,4 @@ -"""This example showcase point queries by highlighting the shape under the +"""This example showcase point queries by highlighting the shape under the mouse pointer. """ @@ -48,13 +48,12 @@ def main(): ticks_to_next_ball -= 1 if ticks_to_next_ball <= 0: ticks_to_next_ball = 100 - mass = 10 radius = 25 - inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0)) - body = pymunk.Body(mass, inertia) + body = pymunk.Body() x = random.randint(115, 350) body.position = x, 200 - shape = pymunk.Circle(body, radius, Vec2d(0, 0)) + shape = pymunk.Circle(body, radius) + shape.density = 10 shape.color = pygame.Color("lightgrey") space.add(body, shape) balls.append(shape) @@ -75,13 +74,11 @@ def main(): balls.remove(ball) mouse_pos = pymunk.pygame_util.get_mouse_pos(screen) - - shape = space.point_query_nearest( - mouse_pos, float("inf"), pymunk.ShapeFilter() - ).shape - if shape is not None and isinstance(shape, pymunk.Circle): - r = shape.radius + 4 - p = pymunk.pygame_util.to_pygame(shape.body.position, screen) + max_distance = 50 + info = space.point_query_nearest(mouse_pos, max_distance, pymunk.ShapeFilter()) + if info is not None and isinstance(info.shape, pymunk.Circle): + r = info.shape.radius + 4 + p = pymunk.pygame_util.to_pygame(info.shape.body.position, screen) pygame.draw.circle(screen, pygame.Color("red"), p, int(r), 2) ### Update physics From 143db86c0f368d078ad2cc5660ff08c7c5330e2f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 30 Mar 2025 21:00:01 +0200 Subject: [PATCH 37/80] Temp disable building of arm linux wheels --- .github/workflows/wheels.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 56246759..6fdf0de0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,8 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: - [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + os: [ubuntu-latest, windows-latest, macos-13, macos-14] # ubuntu-24.04-arm, steps: - uses: actions/checkout@v4 From 5650984c0dafee1dff13d589e3ee6a0d51095dc4 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 30 Mar 2025 21:51:47 +0200 Subject: [PATCH 38/80] Temp disable some os in build --- .github/workflows/wheels.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6fdf0de0..348c434e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,7 +8,9 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-14] # ubuntu-24.04-arm, + os: + [windows-latest] + #[ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 From 86e5d042096a33cdd8a7d01aee8640fe0505033c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Wed, 2 Apr 2025 20:57:19 +0200 Subject: [PATCH 39/80] Temp disable some os in build --- .github/workflows/wheels.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 348c434e..287ce9ee 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,9 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: - [windows-latest] - #[ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + os: [ubuntu-latest] #, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 From add4fe4f4433e8f294d2de050d7df4364adf59d9 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 4 Apr 2025 22:34:53 +0200 Subject: [PATCH 40/80] test fix for seg fault --- pymunk/shapes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index e829afd4..128c06eb 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -63,9 +63,9 @@ def shapefree(cp_shape: ffi.CData) -> None: if cp_space != ffi.NULL: cp.cpSpaceRemoveShape(cp_space, cp_shape) - cp_body = cp.cpShapeGetBody(cp_shape) - if cp_body != ffi.NULL: - cp.cpShapeSetBody(cp_shape, ffi.NULL) + # cp_body = cp.cpShapeGetBody(cp_shape) + # if cp_body != ffi.NULL: + # cp.cpShapeSetBody(cp_shape, ffi.NULL) cp.cpShapeFree(cp_shape) self._shape = ffi.gc(_shape, shapefree) From cbe41d9a306f691558fe030219685ec8b23c3d68 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 4 Apr 2025 22:53:35 +0200 Subject: [PATCH 41/80] re-enable build for all platforms --- .github/workflows/wheels.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 287ce9ee..56246759 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,7 +8,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] #, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + os: + [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 From e1682c2acfd870d961de3482fab9db832c944d9f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 5 Apr 2025 12:13:27 +0200 Subject: [PATCH 42/80] Change ContactPointSet.points to tuple #279 --- CHANGELOG.rst | 3 ++- pymunk/contact_point_set.py | 38 ++++++++++++++++++++---------------- pymunk/tests/test_arbiter.py | 2 +- pymunk/tests/test_space.py | 8 ++++++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83df0bb1..b707953f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,8 @@ Changelog Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. Changed body.shapes to return a KeysView instead of a set of the shapes. - Changed Space.segment_query to return None in case the query did not hit the shape. + Changed Shape.segment_query to return None in case the query did not hit the shape. + Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/pymunk/contact_point_set.py b/pymunk/contact_point_set.py index e3b89fe2..cd6042e4 100644 --- a/pymunk/contact_point_set.py +++ b/pymunk/contact_point_set.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Tuple if TYPE_CHECKING: from ._chipmunk_cffi import ffi @@ -31,8 +31,6 @@ def __init__( point_b: Vec2d, distance: float, ) -> None: - assert len(point_a) == 2 - assert len(point_b) == 2 self.point_a = point_a self.point_b = point_b self.distance = distance @@ -52,12 +50,11 @@ class ContactPointSet(object): """ normal: Vec2d - points: List[ContactPoint] + points: Tuple[ContactPoint, ...] __slots__ = ("normal", "points") - def __init__(self, normal: Vec2d, points: List[ContactPoint]) -> None: - assert len(normal) == 2 + def __init__(self, normal: Vec2d, points: Tuple[ContactPoint, ...]) -> None: self.normal = normal self.points = points @@ -68,14 +65,21 @@ def __repr__(self) -> str: def _from_cp(cls, _points: "ffi.CData") -> "ContactPointSet": normal = Vec2d(_points.normal.x, _points.normal.y) - points = [] - for i in range(_points.count): - _p = _points.points[i] - p = ContactPoint( - Vec2d(_p.pointA.x, _p.pointA.y), - Vec2d(_p.pointB.x, _p.pointB.y), - _p.distance, - ) - points.append(p) - - return cls(normal, points) + assert _points.count in (1, 2), "This is likely a bug in Pymunk, please report." + + _p1 = _points.points[0] + p1 = ContactPoint( + Vec2d(_p1.pointA.x, _p1.pointA.y), + Vec2d(_p1.pointB.x, _p1.pointB.y), + _p1.distance, + ) + if _points.count == 1: + return cls(normal, (p1,)) + + _p2 = _points.points[1] + p2 = ContactPoint( + Vec2d(_p2.pointA.x, _p2.pointA.y), + Vec2d(_p2.pointB.x, _p2.pointB.y), + _p2.distance, + ) + return cls(normal, (p1, p2)) diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 4c0b9892..ac47e2d2 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -137,7 +137,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: self.assertAlmostEqual(p1.distance, -11) # check for length of points - ps2.points = [] + ps2.points = () def f() -> None: arb.contact_point_set = ps2 diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 74db2177..21ead0cf 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1045,10 +1045,10 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: s.add(j1, j2) h = s.add_default_collision_handler() - h.begin = f1 + h.begin = f2 h = s.add_wildcard_collision_handler(1) - h.pre_solve = f1 + h.pre_solve = f2 h = s.add_collision_handler(1, 2) h.post_solve = f1 @@ -1192,3 +1192,7 @@ def testDeleteSpaceWithObjects(self) -> None: def f1(*args: Any, **kwargs: Any) -> None: pass + + +def f2(*args: Any, **kwargs: Any) -> bool: + return True From 929f95e129195efc3ef98d38804191b0087550f7 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 5 Apr 2025 13:23:54 +0200 Subject: [PATCH 43/80] New shorthand to get bodies from a Arbiter. #279 --- CHANGELOG.rst | 1 + pymunk/arbiter.py | 19 +++++++++++++++++++ pymunk/tests/test_arbiter.py | 4 +++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b707953f..29ff25b3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,7 @@ Changelog Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. + New feature: Arbiter.bodies shorthand to ge the shapes' bodies in the Arbiter Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. New feature: ShapeFilter.rejects_collision() New feature: Added Vec2d.polar_tuple diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 8ad2b77a..6cc3c698 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -5,6 +5,7 @@ if TYPE_CHECKING: from .space import Space + from .body import Body from ._chipmunk_cffi import ffi, lib from .contact_point_set import ContactPointSet @@ -69,6 +70,24 @@ def contact_point_set(self, point_set: ContactPointSet) -> None: lib.cpArbiterSetContactPointSet(self._arbiter, ffi.addressof(_set)) + @property + def bodies(self) -> Tuple["Body", "Body"]: + """The the bodies in the order their corresponding shapes were defined + in the collision handler associated with this arbiter. + + This is a shorthand to get the bodes:: + + arb.bodies == arb.shapes[0].body, arb.shapes[1].body . + """ + a, b = self.shapes + assert ( + a.body != None + ), "Shape should have a body. Could be a bug in Pymunk, please report" + assert ( + b.body != None + ), "Shape should have a body. Could be a bug in Pymunk, please report" + return a.body, b.body + @property def shapes(self) -> Tuple["Shape", "Shape"]: """Get the shapes in the order that they were defined in the diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index ac47e2d2..bd966e60 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -300,7 +300,7 @@ def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(self.called2) - def testShapes(self) -> None: + def testShapesAndBodies(self) -> None: s = p.Space() s.gravity = 0, -100 @@ -324,6 +324,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: self.assertEqual(len(arb.shapes), 2) self.assertEqual(arb.shapes[0], c1) self.assertEqual(arb.shapes[1], c2) + self.assertEqual(arb.bodies[0], arb.shapes[0].body) + self.assertEqual(arb.bodies[1], arb.shapes[1].body) return True s.add_collision_handler(1, 2).pre_solve = pre_solve From 9b8076a1001f70bedf2ecd9ce7bde02f62c8bec7 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 5 Apr 2025 13:26:04 +0200 Subject: [PATCH 44/80] Update showcase articles --- docs/src/showcase.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/showcase.rst b/docs/src/showcase.rst index f51fd39d..ad7ee6a3 100644 --- a/docs/src/showcase.rst +++ b/docs/src/showcase.rst @@ -350,6 +350,14 @@ List of papers which has used or mentioned Pymunk: .. (list made using "Chicago" style citation) +#. Liu, Li, Qiang-hong Zhang, Meng-zi Li, Rui-tong Li, Zhiming He, Arnaud Dechesne, Barth F. Smets, and Guo-ping Sheng. + "Single-cell analysis reveals antibiotic affects conjugative transfer by modulating bacterial growth rather than conjugation efficiency." + Environment International (2025): 109385. + +#. Davison, Andrew. + "Simulating Mechanical Curve Drawing using Pymunk." + (2025). + #. Liu, Daochang, Junyu Zhang, Anh-Dung Dinh, Eunbyung Park, Shichao Zhang, and Chang Xu. "Generative Physical AI in Vision: A Survey." arXiv preprint arXiv:2501.10928 (2025). From e1a9c519862380fb8d2c0a6b50a991094a56e90c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 5 Apr 2025 13:29:54 +0200 Subject: [PATCH 45/80] Update cibuildwheel --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 56246759..de57a91d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - uses: pypa/cibuildwheel@v2.23.1 + - uses: pypa/cibuildwheel@v2.23.2 env: CIBW_PLATFORM: pyodide PYMUNK_BUILD_SLIM: 1 From e6bc763da8e74ad81db8e3b6ee04d583e47e611c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 6 Apr 2025 11:01:00 +0200 Subject: [PATCH 46/80] Changed Space.shapes/bodies/constraints to return a KeysView #275 --- CHANGELOG.rst | 4 ++- pymunk/space.py | 52 ++++++++++++++++++++++++++++---------- pymunk/tests/test_space.py | 31 ++++++++++++----------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29ff25b3..1e01cef9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,8 @@ Changelog ========= .. Pymunk 7.0.0 + Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means its no longer a copy. To get the old behavior, do list(space.shapes) etc. + Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() @@ -14,7 +16,7 @@ Changelog Changed body.shapes to return a KeysView instead of a set of the shapes. Changed Shape.segment_query to return None in case the query did not hit the shape. Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. - + Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/pymunk/space.py b/pymunk/space.py index 1f0fafe2..0e7749db 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -3,6 +3,7 @@ import math import platform import weakref +from collections.abc import KeysView from typing import ( TYPE_CHECKING, Any, @@ -148,22 +149,47 @@ def spacefree(cp_space: ffi.CData) -> None: self._bodies_to_check: Set[Body] = set() @property - def shapes(self) -> List[Shape]: - """A list of all the shapes added to this space + def shapes(self) -> KeysView[Shape]: + """The shapes added to this space returned as a KeysView. - (includes both static and non-static) + Since its a view that is returned it will update as shapes are added:: + + >>> import pymunk + >>> s = pymunk.Space() + >>> s.add(pymunk.Circle(s.static_body, 1)) + >>> shapes_view = s.shapes + >>> len(shapes_view) + 1 + >>> s.add(pymunk.Circle(s.static_body, 2)) + >>> len(shapes_view) + 2 """ - return list(self._shapes) + return self._shapes.keys() @property - def bodies(self) -> List[Body]: - """A list of the bodies added to this space""" - return list(self._bodies) + def bodies(self) -> KeysView[Body]: + """The bodies added to this space returned as a KeysView. + + This includes both static and non-static bodies added to the Space. + + Since its a view that is returned it will update as bodies are added:: + + >>> import pymunk + >>> s = pymunk.Space() + >>> s.add(pymunk.Body()) + >>> bodies_view = s.bodies + >>> len(bodies_view) + 1 + >>> s.add(pymunk.Body()) + >>> len(bodies_view) + 2 + """ + return self._bodies.keys() @property - def constraints(self) -> List[Constraint]: - """A list of the constraints added to this space""" - return list(self._constraints) + def constraints(self) -> KeysView[Constraint]: + """The constraints added to this space as a KeysView.""" + return self._constraints.keys() def _setup_static_body(self, static_body: Body) -> None: static_body._space = weakref.ref(self) @@ -968,13 +994,13 @@ def __getstate__(self) -> _State: d["special"].append(("pymunk_version", _version.version)) # bodies needs to be added to the state before their shapes. - d["special"].append(("bodies", self.bodies)) + d["special"].append(("bodies", list(self.bodies))) if self._static_body != None: # print("getstate", self._static_body) d["special"].append(("_static_body", self._static_body)) - d["special"].append(("shapes", self.shapes)) - d["special"].append(("constraints", self.constraints)) + d["special"].append(("shapes", list(self.shapes))) + d["special"].append(("constraints", list(self.constraints))) handlers = [] for k, v in self._handlers.items(): diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 21ead0cf..1c9fd03c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -102,18 +102,18 @@ def testSpatialHash(self) -> None: def testAddRemove(self) -> None: s = p.Space() - self.assertEqual(s.bodies, []) - self.assertEqual(s.shapes, []) + self.assertTrue(len(s.bodies) == 0) + self.assertTrue(len(s.shapes) == 0) b = p.Body(1, 2) s.add(b) - self.assertEqual(s.bodies, [b]) - self.assertEqual(s.shapes, []) + self.assertEqual(list(s.bodies), [b]) + self.assertTrue(len(s.shapes) == 0) c1 = p.Circle(b, 10) s.add(c1) - self.assertEqual(s.bodies, [b]) - self.assertEqual(s.shapes, [c1]) + self.assertEqual(list(s.bodies), [b]) + self.assertEqual(list(s.shapes), [c1]) c2 = p.Circle(b, 15) s.add(c2) @@ -122,16 +122,16 @@ def testAddRemove(self) -> None: self.assertTrue(c2 in s.shapes) s.remove(c1) - self.assertEqual(s.shapes, [c2]) + self.assertEqual(list(s.shapes), [c2]) s.remove(c2, b) - self.assertEqual(s.bodies, []) - self.assertEqual(s.shapes, []) + self.assertEqual(len(s.bodies), 0) + self.assertEqual(len(s.shapes), 0) # note that shape is before the body, which is something to test s.add(c2, b) - self.assertEqual(s.bodies, [b]) - self.assertEqual(s.shapes, [c2]) + self.assertEqual(list(s.bodies), [b]) + self.assertEqual(list(s.shapes), [c2]) def testAddShapeAsserts(self) -> None: s1 = p.Space() @@ -937,7 +937,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: ch = s.add_collision_handler(0, 0).pre_solve = pre_solve s.step(0.1) - self.assertEqual([], s.shapes) + self.assertEqual(len(s.shapes), 0) self.assertEqual(self.calls, 1) s.step(0.1) @@ -1164,9 +1164,10 @@ def testPickleCachedArbiters(self) -> None: # TODO: to assert that everything is working as it should all # properties on the cached the arbiters should be asserted. - - self.assertAlmostEqual(s.bodies[0].position.x, s_copy.bodies[0].position.x) - self.assertAlmostEqual(s.bodies[0].position.y, s_copy.bodies[0].position.y) + bodies = list(s.bodies) + copy_bodies = list(s_copy.bodies) + self.assertAlmostEqual(bodies[0].position.x, copy_bodies[0].position.x) + self.assertAlmostEqual(bodies[0].position.y, copy_bodies[0].position.y) def testDeleteSpaceWithObjects(self) -> None: s = p.Space() From e7fe4f545ef4f36ec89ab48d3c72a81d5a66e911 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 8 Apr 2025 22:28:00 +0200 Subject: [PATCH 47/80] Use Munk2D 1.0 --- CHANGELOG.rst | 3 ++- Munk2D | 2 +- README.rst | 8 ++++---- pymunk/_version.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e01cef9..e7531896 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,7 +16,8 @@ Changelog Changed body.shapes to return a KeysView instead of a set of the shapes. Changed Shape.segment_query to return None in case the query did not hit the shape. Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. - + Switch from using Chipmunk to the new Munk2D fork of Chipmunk. + Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. diff --git a/Munk2D b/Munk2D index aa081e2d..9c3026ad 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit aa081e2d11f6a68f7d215cb58c3e5b9759379d1b +Subproject commit 9c3026aded65c439d64d1cb4fa537544fa8a3989 diff --git a/README.rst b/README.rst index 75698516..da67a75b 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,8 @@ Pymunk Pymunk is an easy-to-use pythonic 2D physics library that can be used whenever you need 2D rigid body physics from Python. Perfect when you need 2D physics -in your game, demo or simulation! It is built on top of the very -capable 2D physics library `Chipmunk2D `_. +in your game, demo or simulation! It is built on top of Munk2D, a fork of the +very capable 2D physics library `Chipmunk2D `_. The first version was released in 2007 and Pymunk is still actively developed and maintained today, more than 15 years of active development! @@ -18,8 +18,8 @@ the Pymunk webpage for some examples. 2007 - 2025, Victor Blomqvist - vb@viblo.se, MIT License -This release is based on the latest Pymunk release (6.11.1), -using Chipmunk2D 7 rev dfc2fb8ca023ce6376fa2cf4a7f91c92ee08a970. +This release is based on the latest Pymunk release (7.0.0), +using Munk2D 1.0 rev 9c3026aded65c439d64d1cb4fa537544fa8a3989. Installation diff --git a/pymunk/_version.py b/pymunk/_version.py index e387bfe9..ee33cfd7 100644 --- a/pymunk/_version.py +++ b/pymunk/_version.py @@ -22,7 +22,7 @@ # ---------------------------------------------------------------------------- """ -Internal module used to make it possible to import the pymunk verison number +Internal module used to make it possible to import the pymunk verison number from more places than __init__. """ @@ -36,5 +36,5 @@ chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), - "dfc2fb8ca023ce6376fa2cf4a7f91c92ee08a970", + "9c3026aded65c439d64d1cb4fa537544fa8a3989", ) From e431bdacd35c1fd7b37adc93eb63c0a3fd8857d9 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Wed, 9 Apr 2025 00:11:38 +0200 Subject: [PATCH 48/80] wip readme --- CHANGELOG.rst | 76 ++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e7531896..59e1e73f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,40 +1,48 @@ ========= Changelog ========= -.. Pymunk 7.0.0 - Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means its no longer a copy. To get the old behavior, do list(space.shapes) etc. - - Breaking: At least one of the two bodies attached to constraint/joint must be dynamic. - New feature: Vec2d supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. - Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() - Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() - A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. - Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track - Dropped support for Python 3.8 - Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. - Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. - Changed body.shapes to return a KeysView instead of a set of the shapes. - Changed Shape.segment_query to return None in case the query did not hit the shape. - Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. - Switch from using Chipmunk to the new Munk2D fork of Chipmunk. - - Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. - If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. - - New feature: Arbiter.bodies shorthand to ge the shapes' bodies in the Arbiter - Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. - New feature: ShapeFilter.rejects_collision() - New feature: Added Vec2d.polar_tuple - Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.) - Improved vec2d documentation - Improved Poly documentation - Improved Shape documentation - Removed unused code - Fix issue with accessing body.space after space is deleted and GCed. - Build wheels for Linux ARM and Pypy 3.11 - Minor internal refactorings for easier and cleaner code - - Extra thanks for aetle for a number of suggestions for improvements in this pymunk release +.. Pymunk 7.0.0 (2025-04-16?) + + **Many improvements release** + + This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! + + Extra thanks for Github user aetle for a number of suggestions and feedback for this pymunk release + + Changes: + + Breaking changes + + - Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, do list(space.shapes) etc. + - At least one of the two bodies attached to constraint/joint must be dynamic. + - Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. + - Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() + - Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() + - A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. + - Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track + - Dropped support for Python 3.8 + - Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. + - Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. + - Changed body.shapes to return a KeysView instead of a set of the shapes. + - Changed Shape.segment_query to return None in case the query did not hit the shape. + - Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. + - Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. + + New non-breaking features + - Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see https://github.com/viblo/Munk2D for details). + - Added Arbiter.bodies shorthand to get the shapes' bodies in the Arbiter + - New method ShapeFilter.rejects_collision() that checks if the filter would reject a collision with another filter. + - New method Vec2d.polar_tuple that return the vector as polar coordinates + - Build and publish wheels for Linux ARM and Pypy 3.11 + - Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. + + Other improvements + - Optimized Vec2d.angle and Vec2d.angle_degrees. Note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently. + - Fixed issue with accessing Body.space after space is deleted and GCed. + - Improved documentation in many places (Vec2d, Poly, Shape and more) + - Internal cleanup of code + + Extra thanks for aetle for a number of suggestions for improvements in this pymunk release Pymunk 6.11.1 (2025-02-09) From eba16743f706785a0100ab19d05ab80db41a3ec9 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 10 Apr 2025 18:18:43 +0200 Subject: [PATCH 49/80] fix space docs --- pymunk/space.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymunk/space.py b/pymunk/space.py index 0e7749db..9773f3ac 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -152,7 +152,8 @@ def spacefree(cp_space: ffi.CData) -> None: def shapes(self) -> KeysView[Shape]: """The shapes added to this space returned as a KeysView. - Since its a view that is returned it will update as shapes are added:: + Since its a view that is returned it will update as shapes are + added. >>> import pymunk >>> s = pymunk.Space() @@ -171,8 +172,7 @@ def bodies(self) -> KeysView[Body]: """The bodies added to this space returned as a KeysView. This includes both static and non-static bodies added to the Space. - - Since its a view that is returned it will update as bodies are added:: + Since its a view that is returned it will update as bodies are added: >>> import pymunk >>> s = pymunk.Space() From a6a7c4ce67a593a550cc5a9934a5b6385ac9f02c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 19 Apr 2025 21:36:41 +0200 Subject: [PATCH 50/80] minor adjustments to changelog and docs --- CHANGELOG.rst | 80 ++++++++++++++++++++++++++----------------------- pymunk/batch.py | 18 +++++------ 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 59e1e73f..d51f5cbf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,48 +1,50 @@ ========= Changelog ========= -.. Pymunk 7.0.0 (2025-04-16?) - - **Many improvements release** - - This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! - - Extra thanks for Github user aetle for a number of suggestions and feedback for this pymunk release +Pymunk 7.0.0 (2025-04-10) +------------------------- - Changes: +**Many improvements, with some breaking changes!** - Breaking changes +This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! - - Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, do list(space.shapes) etc. - - At least one of the two bodies attached to constraint/joint must be dynamic. - - Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. - - Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() - - Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() - - A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. - - Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track - - Dropped support for Python 3.8 - - Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. - - Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. - - Changed body.shapes to return a KeysView instead of a set of the shapes. - - Changed Shape.segment_query to return None in case the query did not hit the shape. - - Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. - - Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. +Extra thanks for Github user aetle for a number of suggestions and feedback for this Pymunk release! - New non-breaking features - - Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see https://github.com/viblo/Munk2D for details). - - Added Arbiter.bodies shorthand to get the shapes' bodies in the Arbiter - - New method ShapeFilter.rejects_collision() that checks if the filter would reject a collision with another filter. - - New method Vec2d.polar_tuple that return the vector as polar coordinates - - Build and publish wheels for Linux ARM and Pypy 3.11 - - Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. - Other improvements - - Optimized Vec2d.angle and Vec2d.angle_degrees. Note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently. - - Fixed issue with accessing Body.space after space is deleted and GCed. - - Improved documentation in many places (Vec2d, Poly, Shape and more) - - Internal cleanup of code +Changes: - Extra thanks for aetle for a number of suggestions for improvements in this pymunk release +Breaking changes + +- Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, you can convert to list manually, like list(space.shapes). +- At least one of the two bodies attached to constraint/joint must be dynamic. +- Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. +- Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() +- Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() +- A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. +- Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track +- Dropped support for Python 3.8 +- Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. +- Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. +- Changed body.shapes to return a KeysView instead of a set of the shapes. +- Changed Shape.segment_query to return None in case the query did not hit the shape. +- Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. +- Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. + +New non-breaking features +- Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see https://github.com/viblo/Munk2D for details). +- Added Arbiter.bodies shorthand to get the shapes' bodies in the Arbiter +- New method ShapeFilter.rejects_collision() that checks if the filter would reject a collision with another filter. +- New method Vec2d.polar_tuple that return the vector as polar coordinates +- Build and publish wheels for Linux ARM and Pypy 3.11 +- Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. + +Other improvements +- Optimized Vec2d.angle and Vec2d.angle_degrees. Note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently. +- Fixed issue with accessing Body.space after space is deleted and GCed. +- Improved documentation in many places (Vec2d, Poly, Shape and more) +- Internal cleanup of code + +Extra thanks for aetle for a number of suggestions for improvements in this pymunk release Pymunk 6.11.1 (2025-02-09) @@ -54,6 +56,7 @@ This is a patch update to Pymunk that removes debug logging. This should an issue with GC on Python 3.13. Changes: + - Remove debug logging @@ -68,8 +71,9 @@ does not work anymore. The release also adds back pre-built wheels for Pypy with a workaround until Pypy make a new release. Changes: - - Support Pyglet 2.1.x (this means Pyglet 2.0.x wont work) - - Readded Pypy pre-built wheels + +- Support Pyglet 2.1.x (this means Pyglet 2.0.x wont work) +- Readded Pypy pre-built wheels Pymunk 6.10.0 (2024-12-22) diff --git a/pymunk/batch.py b/pymunk/batch.py index d0bf6e16..b4a789a6 100644 --- a/pymunk/batch.py +++ b/pymunk/batch.py @@ -1,9 +1,9 @@ """The batch module contain functions to efficiently get and set space data in - a batch insteaf of one by one. + a batch instead of one by one. -.. note:: - This module is highly experimental and will likely change in future Pymunk - verisons including major, minor and patch verisons! +.. note:: + This module is highly experimental and will likely change in future Pymunk + verisons including major, minor and patch verisons! To get data out @@ -21,7 +21,7 @@ >>> s.add(b2, pymunk.Circle(b2, 4)) To get data out first create a Buffer holder object, which is used to reuse -the underlying arrays between calls. Then call the batch method. Note that +the underlying arrays between calls. Then call the batch method. Note that the fields on the body to extract need to be specified explicitly. >>> data = pymunk.batch.Buffer() @@ -31,9 +31,9 @@ ... data, ... ) -The data is available in the Buffer object as cffi buffers. One that -contains any int data, and one that contains floating point data. You can -either use it directly like here, but also pass it in to 3rd parties that +The data is available in the Buffer object as cffi buffers. One that +contains any int data, and one that contains floating point data. You can +either use it directly like here, but also pass it in to 3rd parties that implements the buffer protocol like numpy arrays. >>> list(memoryview(data.int_buf()).cast("P")) == [b1.id, b2.id] @@ -42,7 +42,7 @@ [1.0, 2.0, 3.0, 4.0] Its also possible to get the arbiters with collision data. Note that we need -to call step to run the simulation, and clear the data data buffers first so +to call step to run the simulation, and clear the data data buffers first so they can be reused: >>> s.step(1) From b0d747f8558ae33adfa97e074a4b4e14785ffe37 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 19 Apr 2025 21:44:29 +0200 Subject: [PATCH 51/80] Fix strange None assert in callbacks, and callback changelog #280 --- CHANGELOG.rst | 8 ++++---- pymunk/collision_handler.py | 12 ------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d51f5cbf..71cd0c28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,19 +16,19 @@ Changes: Breaking changes - Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, you can convert to list manually, like list(space.shapes). -- At least one of the two bodies attached to constraint/joint must be dynamic. +- At least one of the two bodies attached to constraint/joint must be dynamic. - Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. -- Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd() +- Added Vec2d.length_squared, and deprecated Vec2d.get_length_sqrd() - Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() - A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. -- Deprecated matplotlib_util. If you think this is a useful module and you use it, please create an issue on the Pymunk issue track +- Deprecated matplotlib_util. If you think this is a useful module, and you use it, please create an issue on the Pymunk issue track - Dropped support for Python 3.8 - Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. - Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. - Changed body.shapes to return a KeysView instead of a set of the shapes. - Changed Shape.segment_query to return None in case the query did not hit the shape. - Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. -- Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc. +- Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. New non-breaking features - Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see https://github.com/viblo/Munk2D for details). diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index 46a6ad3b..37b4e098 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -76,9 +76,6 @@ def begin(self) -> _CollisionCallbackBool: @begin.setter def begin(self, func: _CollisionCallbackBool) -> None: - assert ( - func is not None - ), "To reset the begin callback, set handler.begin = CollisionHandler.always_collide" self._begin = func if self._begin == CollisionHandler.always_collide: @@ -102,9 +99,6 @@ def pre_solve(self) -> _CollisionCallbackBool: @pre_solve.setter def pre_solve(self, func: _CollisionCallbackBool) -> None: - assert ( - func is not None - ), "To reset the pre_solve callback, set handler.pre_solve = CollisionHandler.always_collide" self._pre_solve = func if self._pre_solve == CollisionHandler.always_collide: @@ -127,9 +121,6 @@ def post_solve(self) -> _CollisionCallbackNoReturn: @post_solve.setter def post_solve(self, func: _CollisionCallbackNoReturn) -> None: - assert ( - func is not None - ), "To reset the post_solve callback, set handler.post_solve = CollisionHandler.do_nothing" self._post_solve = func if self._post_solve == CollisionHandler.do_nothing: @@ -154,9 +145,6 @@ def separate(self) -> _CollisionCallbackNoReturn: @separate.setter def separate(self, func: _CollisionCallbackNoReturn) -> None: - assert ( - func is not None - ), "To reset the separate callback, set handler.separate = CollisionHandler.do_nothing" self._separate = func if self._separate == CollisionHandler.do_nothing: From dfb716bc7d7f2a1a9acb00ec90a79f01b6a0f33d Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 19 Apr 2025 22:00:06 +0200 Subject: [PATCH 52/80] Fix pickle of default callback funcs #280 --- pymunk/space.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pymunk/space.py b/pymunk/space.py index 9773f3ac..cf9c1513 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -1005,13 +1005,13 @@ def __getstate__(self) -> _State: handlers = [] for k, v in self._handlers.items(): h: Dict[str, Any] = {} - if v._begin is not None: + if v._begin != CollisionHandler.always_collide: h["_begin"] = v._begin - if v._pre_solve is not None: + if v._pre_solve != CollisionHandler.always_collide: h["_pre_solve"] = v._pre_solve - if v._post_solve is not None: + if v._post_solve != CollisionHandler.do_nothing: h["_post_solve"] = v._post_solve - if v._separate is not None: + if v._separate != CollisionHandler.do_nothing: h["_separate"] = v._separate handlers.append((k, h)) From 23fff819a997c1877446671a5b06c565541c7d28 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 20 Apr 2025 11:45:22 +0200 Subject: [PATCH 53/80] Update license spec in pyproject.toml to new format --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56fce9ac..350964eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,10 +21,11 @@ authors = [{ name = "Victor Blomqvist", email = "vb@viblo.se" }] readme = "README.rst" description = "Pymunk is a easy-to-use pythonic 2D physics library" keywords = ["pygame", "2d", "physics", "rigid body"] +license = "MIT" +license-files = ["LICENSE.txt"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Games/Entertainment", "Topic :: Software Development :: Libraries", From d9f6de87fb1ed2a82963fc231688bada68ebd9d6 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 8 May 2025 23:58:36 +0200 Subject: [PATCH 54/80] Reworked collision handlers wip #280 --- CHANGELOG.rst | 5 ++ Munk2D | 2 +- benchmarks/pymunk-collision-callback.py | 2 +- pymunk/_callbacks.py | 43 +--------- pymunk/arbiter.py | 20 +++++ pymunk/collision_handler.py | 55 +++++-------- pymunk/space.py | 78 +++++++++++++----- pymunk/tests/test_arbiter.py | 20 ++--- pymunk/tests/test_shape.py | 5 +- pymunk/tests/test_space.py | 100 ++++++------------------ pymunk_cffi/callbacks_cdef.h | 4 +- pymunk_cffi/chipmunk_cdef.h | 47 ++++------- pymunk_cffi/extensions.c | 17 ++-- pymunk_cffi/extensions_cdef.h | 1 - 14 files changed, 162 insertions(+), 237 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71cd0c28..2f306089 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Changelog Pymunk 7.0.0 (2025-04-10) ------------------------- +TODO: Add note of new collision handler logic! + **Many improvements, with some breaking changes!** This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! @@ -47,6 +49,9 @@ Other improvements Extra thanks for aetle for a number of suggestions for improvements in this pymunk release + + + Pymunk 6.11.1 (2025-02-09) -------------------------- diff --git a/Munk2D b/Munk2D index 9c3026ad..d5899e8e 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit 9c3026aded65c439d64d1cb4fa537544fa8a3989 +Subproject commit d5899e8eed00cac26dc469b5fa22bf50f1266a7e diff --git a/benchmarks/pymunk-collision-callback.py b/benchmarks/pymunk-collision-callback.py index 66c0c320..9c834eb1 100644 --- a/benchmarks/pymunk-collision-callback.py +++ b/benchmarks/pymunk-collision-callback.py @@ -8,7 +8,7 @@ b = pymunk.Body(1,10) c = pymunk.Circle(b, 5) s.add(b, c) -h = s.add_default_collision_handler() +h = s.add_global_collision_handler() def f(arb, s, data): return False h.pre_solve = f diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index e079132c..575846c8 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -1,6 +1,5 @@ import logging import math -import warnings from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter @@ -202,51 +201,17 @@ def ext_cpMarchSampleFunc(point: ffi.CData, data: ffi.CData) -> float: @ffi.def_extern() def ext_cpCollisionBeginFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData -) -> bool: +) -> None: handler = ffi.from_handle(data) - x = handler._begin(Arbiter(_arb, handler._space), handler._space, handler.data) - if isinstance(x, bool): - return x - - func_name = handler._begin.__code__.co_name - filename = handler._begin.__code__.co_filename - lineno = handler._begin.__code__.co_firstlineno - - warnings.warn_explicit( - "Function '" + func_name + "' should return a bool to" - " indicate if the collision should be processed or not when" - " used as 'begin' or 'pre_solve' collision callback.", - UserWarning, - filename, - lineno, - handler._begin.__module__, - ) - return True + handler._begin(Arbiter(_arb, handler._space), handler._space, handler.data) @ffi.def_extern() def ext_cpCollisionPreSolveFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData -) -> bool: +) -> None: handler = ffi.from_handle(data) - x = handler._pre_solve(Arbiter(_arb, handler._space), handler._space, handler.data) - if isinstance(x, bool): - return x - - func_name = handler._pre_solve.__code__.co_name - filename = handler._pre_solve.__code__.co_filename - lineno = handler._pre_solve.__code__.co_firstlineno - - warnings.warn_explicit( - "Function '" + func_name + "' should return a bool to" - " indicate if the collision should be processed or not when" - " used as 'begin' or 'pre_solve' collision callback.", - UserWarning, - filename, - lineno, - handler._pre_solve.__module__, - ) - return True + handler._pre_solve(Arbiter(_arb, handler._space), handler._space, handler.data) @ffi.def_extern() diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 6cc3c698..cbd5d0db 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -39,6 +39,26 @@ def __init__(self, _arbiter: ffi.CData, space: "Space") -> None: self._arbiter = _arbiter self._space = space + @property + def process_collision(self) -> bool: + """Decides if the collision should be processed or rejected. + + Set this during a begin() or pre_solve() callback to override + the default (True) value. + + Set this to true to process the collision normally or + false to cause pymunk to ignore the collision entirely. If you set it to + false from a `begin` callback, the `pre_solve` and `post_solve` callbacks will never be run, + but you will still recieve a separate event when the shapes stop + overlapping. + + """ + return lib.cpArbiterGetProcessCollision(self._arbiter) + + @process_collision.setter + def process_collision(self, v: bool) -> None: + lib.cpArbiterSetProcessCollision(self._arbiter, v) + @property def contact_point_set(self) -> ContactPointSet: """Contact point sets make getting contact information from the diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index 37b4e098..1849915c 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -8,8 +8,7 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallbackBool = Callable[[Arbiter, "Space", Dict[Any, Any]], bool] -_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Dict[Any, Any]], None] +_CollisionCallback = Callable[[Arbiter, "Space", Dict[Any, Any]], None] class CollisionHandler(object): @@ -42,10 +41,10 @@ def __init__(self, _handler: Any, space: "Space") -> None: self._handler.userData = self._userData self._space = space - self._begin: _CollisionCallbackBool = CollisionHandler.always_collide - self._pre_solve: _CollisionCallbackBool = CollisionHandler.always_collide - self._post_solve: _CollisionCallbackNoReturn = CollisionHandler.do_nothing - self._separate: _CollisionCallbackNoReturn = CollisionHandler.do_nothing + self._begin: _CollisionCallback = CollisionHandler.do_nothing + self._pre_solve: _CollisionCallback = CollisionHandler.do_nothing + self._post_solve: _CollisionCallback = CollisionHandler.do_nothing + self._separate: _CollisionCallback = CollisionHandler.do_nothing self._data: Dict[Any, Any] = {} @@ -61,10 +60,10 @@ def data(self) -> Dict[Any, Any]: return self._data @property - def begin(self) -> _CollisionCallbackBool: + def begin(self) -> _CollisionCallback: """Two shapes just started touching for the first time this step. - ``func(arbiter, space, data) -> bool`` + ``func(arbiter, space, data)`` Return true from the callback to process the collision normally or false to cause pymunk to ignore the collision entirely. If you return @@ -75,22 +74,21 @@ def begin(self) -> _CollisionCallbackBool: return self._begin @begin.setter - def begin(self, func: _CollisionCallbackBool) -> None: + def begin(self, func: _CollisionCallback) -> None: self._begin = func - if self._begin == CollisionHandler.always_collide: - self._handler.beginFunc = ffi.addressof(lib, "AlwaysCollide") + if self._begin == CollisionHandler.do_nothing: + self._handler.beginFunc = ffi.addressof(lib, "DoNothing") else: self._handler.beginFunc = lib.ext_cpCollisionBeginFunc @property - def pre_solve(self) -> _CollisionCallbackBool: + def pre_solve(self) -> _CollisionCallback: """Two shapes are touching during this step. - ``func(arbiter, space, data) -> bool`` + ``func(arbiter, space, data)`` - Return false from the callback to make pymunk ignore the collision - this step or true to process it normally. Additionally, you may + Additionally, you may override collision values using Arbiter.friction, Arbiter.elasticity or Arbiter.surfaceVelocity to provide custom friction, elasticity, or surface velocity values. See Arbiter for more info. @@ -98,16 +96,16 @@ def pre_solve(self) -> _CollisionCallbackBool: return self._pre_solve @pre_solve.setter - def pre_solve(self, func: _CollisionCallbackBool) -> None: + def pre_solve(self, func: _CollisionCallback) -> None: self._pre_solve = func - if self._pre_solve == CollisionHandler.always_collide: - self._handler.preSolveFunc = ffi.addressof(lib, "AlwaysCollide") + if self._pre_solve == CollisionHandler.do_nothing: + self._handler.preSolveFunc = ffi.addressof(lib, "DoNothing") else: self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc @property - def post_solve(self) -> _CollisionCallbackNoReturn: + def post_solve(self) -> _CollisionCallback: """Two shapes are touching and their collision response has been processed. @@ -120,7 +118,7 @@ def post_solve(self) -> _CollisionCallbackNoReturn: return self._post_solve @post_solve.setter - def post_solve(self, func: _CollisionCallbackNoReturn) -> None: + def post_solve(self, func: _CollisionCallback) -> None: self._post_solve = func if self._post_solve == CollisionHandler.do_nothing: @@ -128,10 +126,8 @@ def post_solve(self, func: _CollisionCallbackNoReturn) -> None: else: self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc - self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc - @property - def separate(self) -> _CollisionCallbackNoReturn: + def separate(self) -> _CollisionCallback: """Two shapes have just stopped touching for the first time this step. @@ -144,7 +140,7 @@ def separate(self) -> _CollisionCallbackNoReturn: return self._separate @separate.setter - def separate(self, func: _CollisionCallbackNoReturn) -> None: + def separate(self, func: _CollisionCallback) -> None: self._separate = func if self._separate == CollisionHandler.do_nothing: @@ -161,14 +157,3 @@ def do_nothing(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> None: do nothing method. """ return - - @staticmethod - def always_collide(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> bool: - """The default method used for the begin and pre_solve callbacks. - - It will always return True, meaning the collision should not be ignored. - - Note that its more efficient to set this method than to define your own - return True method. - """ - return True diff --git a/pymunk/space.py b/pymunk/space.py index cf9c1513..57c09ae4 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -3,13 +3,14 @@ import math import platform import weakref -from collections.abc import KeysView +from collections.abc import KeysView, Mapping from typing import ( TYPE_CHECKING, Any, Callable, Dict, Hashable, + Iterator, List, Optional, Set, @@ -42,6 +43,36 @@ _AddableObjects = Union[Body, Shape, Constraint] +class Handlers(Mapping[Union[None, int, Tuple[int, int]], CollisionHandler]): + + def __init__(self, space: "Space") -> None: + self.space = space + + _handlers: Dict[Union[None, int, Tuple[int, int]], CollisionHandler] = {} + + def __getitem__(self, key: Union[None, int, Tuple[int, int]]) -> CollisionHandler: + if key in self._handlers: + return self._handlers[key] + if key == None: + self._handlers[None] = self.space.add_global_collision_handler() + return self._handlers[None] + elif isinstance(key, int): + self._handlers[key] = self.space.add_wildcard_collision_handler(key) + return self._handlers[key] + elif isinstance(key, tuple): + assert isinstance(key, tuple) + self._handlers[key] = self.space.add_collision_handler(key[0], key[1]) + return self._handlers[key] + else: + raise ValueError() + + def __len__(self) -> int: + return len(self._handlers) + + def __iter__(self) -> Iterator[Union[None, int, Tuple[int, int]]]: + return iter(self._handlers) + + class Space(PickleMixin, object): """Spaces are the basic unit of simulation. You add rigid bodies, shapes and joints to it and then step them all forward together through time. @@ -148,6 +179,8 @@ def spacefree(cp_space: ffi.CData) -> None: self._remove_later: Set[_AddableObjects] = set() self._bodies_to_check: Set[Body] = set() + self._collision_handlers = Handlers(self) + @property def shapes(self) -> KeysView[Shape]: """The shapes added to this space returned as a KeysView. @@ -497,28 +530,25 @@ def reindex_static(self) -> None: """ cp.cpSpaceReindexStatic(self._space) - def _get_threads(self) -> int: + @property + def threads(self) -> int: + """The number of threads to use for running the step function. + + Only valid when the Space was created with threaded=True. Currently the + max limit is 2, setting a higher value wont have any effect. The + default is 1 regardless if the Space was created with threaded=True, + to keep determinism in the simulation. Note that Windows does not + support the threaded solver. + """ if self.threaded: return int(cp.cpHastySpaceGetThreads(self._space)) return 1 - def _set_threads(self, n: int) -> None: + @threads.setter + def threads(self, n: int) -> None: if self.threaded: cp.cpHastySpaceSetThreads(self._space, n) - threads = property( - _get_threads, - _set_threads, - doc="""The number of threads to use for running the step function. - - Only valid when the Space was created with threaded=True. Currently the - max limit is 2, setting a higher value wont have any effect. The - default is 1 regardless if the Space was created with threaded=True, - to keep determinism in the simulation. Note that Windows does not - support the threaded solver. - """, - ) - def use_spatial_hash(self, dim: float, count: int) -> None: """Switch the space to use a spatial hash instead of the bounding box tree. @@ -601,6 +631,12 @@ def step(self, dt: float) -> None: self._post_step_callbacks.clear() + @property + def collision_handlers( + self, + ) -> Mapping[Union[None, int, Tuple[int, int]], CollisionHandler]: + return self.collision_handlers + def add_collision_handler( self, collision_type_a: int, collision_type_b: int ) -> CollisionHandler: @@ -663,7 +699,7 @@ def add_wildcard_collision_handler(self, collision_type_a: int) -> CollisionHand self._handlers[collision_type_a] = ch return ch - def add_default_collision_handler(self) -> CollisionHandler: + def add_global_collision_handler(self) -> CollisionHandler: """Return a reference to the default collision handler or that is used to process all collisions that don't have a more specific handler. @@ -675,7 +711,7 @@ def add_default_collision_handler(self) -> CollisionHandler: if None in self._handlers: return self._handlers[None] - _h = cp.cpSpaceAddDefaultCollisionHandler(self._space) + _h = cp.cpSpaceAddGlobalCollisionHandler(self._space) h = CollisionHandler(_h, self) self._handlers[None] = h return h @@ -1005,9 +1041,9 @@ def __getstate__(self) -> _State: handlers = [] for k, v in self._handlers.items(): h: Dict[str, Any] = {} - if v._begin != CollisionHandler.always_collide: + if v._begin != CollisionHandler.do_nothing: h["_begin"] = v._begin - if v._pre_solve != CollisionHandler.always_collide: + if v._pre_solve != CollisionHandler.do_nothing: h["_pre_solve"] = v._pre_solve if v._post_solve != CollisionHandler.do_nothing: h["_post_solve"] = v._post_solve @@ -1065,7 +1101,7 @@ def __setstate__(self, state: _State) -> None: elif k == "_handlers": for k2, hd in v: if k2 == None: - h = self.add_default_collision_handler() + h = self.add_global_collision_handler() elif isinstance(k2, tuple): h = self.add_collision_handler(k2[0], k2[1]) else: diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index bd966e60..54244c85 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -23,10 +23,9 @@ def testRestitution(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 - return True s.add_collision_handler(1, 2).pre_solve = pre_solve @@ -52,10 +51,9 @@ def testFriction(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): self.assertEqual(arb.friction, 0.18) arb.friction = 1 - return True s.add_collision_handler(1, 2).pre_solve = pre_solve @@ -81,13 +79,12 @@ def testSurfaceVelocity(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): self.assertAlmostEqual(arb.surface_velocity.x, 1.38461538462) self.assertAlmostEqual(arb.surface_velocity.y, -0.923076923077) arb.surface_velocity = (10, 10) # TODO: add assert check that setting surface_velocity has any effect - return True s.add_collision_handler(1, 2).pre_solve = pre_solve for x in range(5): @@ -108,7 +105,7 @@ def testContactPointSet(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): # check inital values ps: p.ContactPointSet = arb.contact_point_set self.assertEqual(len(ps.points), 1) @@ -144,9 +141,7 @@ def f() -> None: self.assertRaises(Exception, f) - return True - - s.add_default_collision_handler().pre_solve = pre_solve + s.add_global_collision_handler().pre_solve = pre_solve s.step(0.1) @@ -248,12 +243,11 @@ def testNormal(self) -> None: s.add(b1, c1, c2) - def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any): self.assertAlmostEqual(arb.normal.x, 0.44721359) self.assertAlmostEqual(arb.normal.y, 0.89442719) - return True - s.add_default_collision_handler().pre_solve = pre_solve1 + s.add_global_collision_handler().pre_solve = pre_solve1 s.step(0.1) diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index c9e14b68..602326dd 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -298,11 +298,10 @@ def testSegmentSegmentCollision(self) -> None: self.num_of_begins = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def begin(arb: p.Arbiter, space: p.Space, data: Any): self.num_of_begins += 1 - return True - s.add_default_collision_handler().begin = begin + s.add_global_collision_handler().begin = begin s.step(0.1) self.assertEqual(1, self.num_of_begins) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 1c9fd03c..cfed9fa7 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -509,9 +509,8 @@ def testCollisionHandlerBegin(self) -> None: self.hits = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def begin(arb: p.Arbiter, space: p.Space, data: Any): self.hits += h.data["test"] - return True h = s.add_collision_handler(0, 0) h.data["test"] = 1 @@ -522,27 +521,6 @@ def begin(arb: p.Arbiter, space: p.Space, data: Any) -> bool: self.assertEqual(self.hits, 1) - def testCollisionHandlerBeginNoReturn(self) -> None: - s = p.Space() - b1 = p.Body(1, 1) - c1 = p.Circle(b1, 10) - b2 = p.Body(1, 1) - c2 = p.Circle(b2, 10) - s.add(b1, c1, b2, c2) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - def begin(arb: p.Arbiter, space: p.Space, data: Any) -> bool: - return # type: ignore - - s.add_collision_handler(0, 0).begin = begin - s.step(0.1) - - assert w is not None - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, UserWarning)) - def testCollisionHandlerPreSolve(self) -> None: s = p.Space() b1 = p.Body(1, 1) @@ -554,11 +532,10 @@ def testCollisionHandlerPreSolve(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): d["shapes"] = arb.shapes d["space"] = space # type: ignore d["test"] = data["test"] - return True h = s.add_collision_handler(0, 1) h.data["test"] = 1 @@ -569,27 +546,6 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: self.assertEqual(s, d["space"]) self.assertEqual(1, d["test"]) - def testCollisionHandlerPreSolveNoReturn(self) -> None: - s = p.Space() - b1 = p.Body(1, 1) - c1 = p.Circle(b1, 10) - b2 = p.Body(1, 1) - c2 = p.Circle(b2, 10) - s.add(b1, c1, b2, c2) - - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: - return # type: ignore - - s.add_collision_handler(0, 0).pre_solve = pre_solve - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - s.step(0.1) - assert w is not None - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, UserWarning)) - def testCollisionHandlerPostSolve(self) -> None: self._setUp() self.hit = 0 @@ -642,7 +598,7 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.add_default_collision_handler().separate = separate + s.add_global_collision_handler().separate = separate s.step(1) s.remove(c1) @@ -661,9 +617,9 @@ def testCollisionHandlerDefaultCallbacks(self) -> None: s.add(b1, c1, b2, c2) s.gravity = 0, -100 - h = s.add_default_collision_handler() - h.begin = h.always_collide - h.pre_solve = h.always_collide + h = s.add_global_collision_handler() + h.begin = h.do_nothing + h.pre_solve = h.do_nothing h.post_solve = h.do_nothing h.separate = h.do_nothing @@ -805,19 +761,17 @@ def testCollisionHandlerAddRemoveInStep(self) -> None: b = p.Body(1, 2) c = p.Circle(b, 2) - def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any): space.add(b, c) space.add(c, b) self.assertTrue(b not in s.bodies) self.assertTrue(c not in s.shapes) - return True - def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any): space.remove(b, c) space.remove(c, b) self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - return True s.add_collision_handler(0, 0).pre_solve = pre_solve_add @@ -837,9 +791,8 @@ def testCollisionHandlerRemoveInStep(self) -> None: self._setUp() s = self.s - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): space.remove(*arb.shapes) - return True s.add_collision_handler(0, 0).pre_solve = pre_solve @@ -866,10 +819,9 @@ def testWildcardCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): d["shapes"] = arb.shapes d["space"] = space # type: ignore - return True s.add_wildcard_collision_handler(1).pre_solve = pre_solve s.step(0.1) @@ -895,12 +847,11 @@ def testDefaultCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): d["shapes"] = arb.shapes d["space"] = space # type: ignore - return True - s.add_default_collision_handler().pre_solve = pre_solve + s.add_global_collision_handler().pre_solve = pre_solve s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -928,11 +879,10 @@ def callback( s.remove(shape) test_self.calls += 1 - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): # note that we dont pass on the whole arbiters object, instead # we take only the shapes. space.add_post_step_callback(callback, 0, arb.shapes, test_self=self) - return True ch = s.add_collision_handler(0, 0).pre_solve = pre_solve @@ -1044,11 +994,11 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: j2 = PinJoint(s.static_body, b2) s.add(j1, j2) - h = s.add_default_collision_handler() - h.begin = f2 + h = s.add_global_collision_handler() + h.begin = f1 h = s.add_wildcard_collision_handler(1) - h.pre_solve = f2 + h.pre_solve = f1 h = s.add_collision_handler(1, 2) h.post_solve = f1 @@ -1078,27 +1028,27 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: self.assertIn(s2.static_body, ja) # Assert collision handlers - h2 = s2.add_default_collision_handler() + h2 = s2.add_global_collision_handler() self.assertIsNotNone(h2.begin) - self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) + self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_wildcard_collision_handler(1) - self.assertEqual(h2.begin, p.CollisionHandler.always_collide) + self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.pre_solve) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_collision_handler(1, 2) - self.assertEqual(h2.begin, p.CollisionHandler.always_collide) - self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) + self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) + self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.post_solve) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) h2 = s2.add_collision_handler(3, 4) - self.assertEqual(h2.begin, p.CollisionHandler.always_collide) - self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide) + self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) + self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.separate) @@ -1193,7 +1143,3 @@ def testDeleteSpaceWithObjects(self) -> None: def f1(*args: Any, **kwargs: Any) -> None: pass - - -def f2(*args: Any, **kwargs: Any) -> bool: - return True diff --git a/pymunk_cffi/callbacks_cdef.h b/pymunk_cffi/callbacks_cdef.h index cfa4d185..aff59ccb 100644 --- a/pymunk_cffi/callbacks_cdef.h +++ b/pymunk_cffi/callbacks_cdef.h @@ -18,8 +18,8 @@ extern "Python" { // cpSpace.h - cpBool ext_cpCollisionBeginFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); - cpBool ext_cpCollisionPreSolveFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); + void ext_cpCollisionBeginFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); + void ext_cpCollisionPreSolveFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); void ext_cpCollisionPostSolveFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); void ext_cpCollisionSeparateFunc(cpArbiter *arb, cpSpace *space, cpDataPointer userData); diff --git a/pymunk_cffi/chipmunk_cdef.h b/pymunk_cffi/chipmunk_cdef.h index 3e00ecd0..8cf1788d 100644 --- a/pymunk_cffi/chipmunk_cdef.h +++ b/pymunk_cffi/chipmunk_cdef.h @@ -64,7 +64,7 @@ enum cpArbiterState // Arbiter is active and its not the first collision. CP_ARBITER_STATE_NORMAL, // Collision has been explicitly ignored. - // Either by returning false from a begin collision handler or calling cpArbiterIgnore(). + // Can be set/unset from begin or preStep callback functions. CP_ARBITER_STATE_IGNORE, // Collison is no longer active. A space will cache an arbiter for up to cpSpace.collisionPersistence more steps. CP_ARBITER_STATE_CACHED, @@ -175,9 +175,15 @@ cpVect cpArbiterTotalImpulse(const cpArbiter *arb); /// This function should only be called from a post-solve, post-step or cpBodyEachArbiter callback. cpFloat cpArbiterTotalKE(const cpArbiter *arb); -/// Mark a collision pair to be ignored until the two objects separate. -/// Pre-solve and post-solve callbacks will not be called, but the separate callback will be called. -cpBool cpArbiterIgnore(cpArbiter *arb); +/// Return if the collision was set to be ignored by cpArbiterSetProcessCollision earlier. +/// Note that the collision can be ignored for different reasons, even when this is true. +cpBool cpArbiterGetProcessCollision(const cpArbiter *arb); +/// Set processCollision to false to not process the collision. +/// Setting this to false from a begin callback causes the collision to be ignored until +/// the the separate callback is called when the objects stop colliding. +/// Returning false from a pre-step callback causes the collision to be ignored until the next step. +/// It should not be called from post-solve or separate callbacks. +void cpArbiterSetProcessCollision(cpArbiter *arb, cpBool processCollision); /// Return the colliding shapes involved for this arbiter. /// The order of their cpSpace.collision_type values will match @@ -232,30 +238,6 @@ cpVect cpArbiterGetPointB(const cpArbiter *arb, int i); /// Get the depth of the @c ith contact point. cpFloat cpArbiterGetDepth(const cpArbiter *arb, int i); -/// If you want a custom callback to invoke the wildcard callback for the first collision type, you must call this function explicitly. -/// You must decide how to handle the wildcard's return value since it may disagree with the other wildcard handler's return value or your own. -cpBool cpArbiterCallWildcardBeginA(cpArbiter *arb, cpSpace *space); -/// If you want a custom callback to invoke the wildcard callback for the second collision type, you must call this function explicitly. -/// You must decide how to handle the wildcard's return value since it may disagree with the other wildcard handler's return value or your own. -cpBool cpArbiterCallWildcardBeginB(cpArbiter *arb, cpSpace *space); - -/// If you want a custom callback to invoke the wildcard callback for the first collision type, you must call this function explicitly. -/// You must decide how to handle the wildcard's return value since it may disagree with the other wildcard handler's return value or your own. -cpBool cpArbiterCallWildcardPreSolveA(cpArbiter *arb, cpSpace *space); -/// If you want a custom callback to invoke the wildcard callback for the second collision type, you must call this function explicitly. -/// You must decide how to handle the wildcard's return value since it may disagree with the other wildcard handler's return value or your own. -cpBool cpArbiterCallWildcardPreSolveB(cpArbiter *arb, cpSpace *space); - -/// If you want a custom callback to invoke the wildcard callback for the first collision type, you must call this function explicitly. -void cpArbiterCallWildcardPostSolveA(cpArbiter *arb, cpSpace *space); -/// If you want a custom callback to invoke the wildcard callback for the second collision type, you must call this function explicitly. -void cpArbiterCallWildcardPostSolveB(cpArbiter *arb, cpSpace *space); - -/// If you want a custom callback to invoke the wildcard callback for the first collision type, you must call this function explicitly. -void cpArbiterCallWildcardSeparateA(cpArbiter *arb, cpSpace *space); -/// If you want a custom callback to invoke the wildcard callback for the second collision type, you must call this function explicitly. -void cpArbiterCallWildcardSeparateB(cpArbiter *arb, cpSpace *space); - /////////////////////////////////////////// // cpBody.h /////////////////////////////////////////// @@ -1018,10 +1000,10 @@ void cpSimpleMotorSetRate(cpConstraint *constraint, cpFloat rate); /// Collision begin event function callback type. /// Returning false from a begin callback causes the collision to be ignored until /// the the separate callback is called when the objects stop colliding. -typedef cpBool (*cpCollisionBeginFunc)(cpArbiter *arb, cpSpace *space, cpDataPointer userData); +typedef void (*cpCollisionBeginFunc)(cpArbiter *arb, cpSpace *space, cpDataPointer userData); /// Collision pre-solve event function callback type. /// Returning false from a pre-step callback causes the collision to be ignored until the next step. -typedef cpBool (*cpCollisionPreSolveFunc)(cpArbiter *arb, cpSpace *space, cpDataPointer userData); +typedef void (*cpCollisionPreSolveFunc)(cpArbiter *arb, cpSpace *space, cpDataPointer userData); /// Collision post-solve event function callback type. typedef void (*cpCollisionPostSolveFunc)(cpArbiter *arb, cpSpace *space, cpDataPointer userData); /// Collision separate event function callback type. @@ -1132,10 +1114,9 @@ cpBool cpSpaceIsLocked(cpSpace *space); // MARK: Collision Handlers -/// Create or return the existing collision handler that is called for all collisions that are not handled by a more specific collision handler. -cpCollisionHandler *cpSpaceAddDefaultCollisionHandler(cpSpace *space); +/// Create or return the existing collision handler that is called for all collisions +cpCollisionHandler *cpSpaceAddGlobalCollisionHandler(cpSpace *space); /// Create or return the existing collision handler for the specified pair of collision types. -/// If wildcard handlers are used with either of the collision types, it's the responibility of the custom handler to invoke the wildcard handlers. cpCollisionHandler *cpSpaceAddCollisionHandler(cpSpace *space, cpCollisionType a, cpCollisionType b); /// Create or return the existing wildcard collision handler for the specified type. cpCollisionHandler *cpSpaceAddWildcardHandler(cpSpace *space, cpCollisionType type); diff --git a/pymunk_cffi/extensions.c b/pymunk_cffi/extensions.c index cf6a2ebd..7970422e 100644 --- a/pymunk_cffi/extensions.c +++ b/pymunk_cffi/extensions.c @@ -394,11 +394,11 @@ void cpSpaceEachCachedArbiter(cpSpace *space, cpArbiterIteratorFunc func, void * } static inline cpCollisionHandler * -cpSpaceLookupHandler(cpSpace *space, cpCollisionType a, cpCollisionType b, cpCollisionHandler *defaultValue) +cpSpaceLookupHandler(cpSpace *space, cpCollisionType a, cpCollisionType b) { cpCollisionType types[] = {a, b}; cpCollisionHandler *handler = (cpCollisionHandler *)cpHashSetFind(space->collisionHandlers, CP_HASH_PAIR(a, b), types); - return (handler ? handler : defaultValue); + return (handler ? handler : &cpCollisionHandlerDoNothing); } void cpSpaceAddCachedArbiter(cpSpace *space, cpArbiter *arb) @@ -422,18 +422,14 @@ void cpSpaceAddCachedArbiter(cpSpace *space, cpArbiter *arb) // Set handlers to their defaults cpCollisionType typeA = a->type, typeB = b->type; - cpCollisionHandler *defaultHandler = &space->defaultHandler; - cpCollisionHandler *handler = arb->handler = cpSpaceLookupHandler(space, typeA, typeB, defaultHandler); + cpCollisionHandler *handler = arb->handler = cpSpaceLookupHandler(space, typeA, typeB); // Check if the types match, but don't swap for a default handler which use the wildcard for type A. cpBool swapped = arb->swapped = (typeA != handler->typeA && handler->typeA != CP_WILDCARD_COLLISION_TYPE); - if (handler != defaultHandler || space->usesWildcards) - { - // The order of the main handler swaps the wildcard handlers too. Uffda. - arb->handlerA = cpSpaceLookupHandler(space, (swapped ? typeB : typeA), CP_WILDCARD_COLLISION_TYPE, &cpCollisionHandlerDoNothing); - arb->handlerB = cpSpaceLookupHandler(space, (swapped ? typeA : typeB), CP_WILDCARD_COLLISION_TYPE, &cpCollisionHandlerDoNothing); - } + // The order of the main handler swaps the wildcard handlers too. Uffda. + arb->handlerA = cpSpaceLookupHandler(space, (swapped ? typeB : typeA), CP_WILDCARD_COLLISION_TYPE); + arb->handlerB = cpSpaceLookupHandler(space, (swapped ? typeA : typeB), CP_WILDCARD_COLLISION_TYPE); // Update the arbiter's state cpArrayPush(space->arbiters, arb); @@ -489,5 +485,4 @@ void cpMessage(const char *condition, const char *file, int line, int isError, i ext_pyLog(formattedMessage); } -static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data){return cpTrue;} static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data){} \ No newline at end of file diff --git a/pymunk_cffi/extensions_cdef.h b/pymunk_cffi/extensions_cdef.h index 48ed8652..3b4dd441 100644 --- a/pymunk_cffi/extensions_cdef.h +++ b/pymunk_cffi/extensions_cdef.h @@ -104,5 +104,4 @@ cpFloat defaultSpringForce(cpDampedSpring *spring, cpFloat dist); cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle); -static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data); static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data); \ No newline at end of file From 498a57f95191d4e2efa8191885576c94f6cbe6a0 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 9 May 2025 21:31:07 +0200 Subject: [PATCH 55/80] Fix cpArbiter mem issue --- pymunk_cffi/chipmunk_cdef.h | 1 + 1 file changed, 1 insertion(+) diff --git a/pymunk_cffi/chipmunk_cdef.h b/pymunk_cffi/chipmunk_cdef.h index 8cf1788d..a88bf705 100644 --- a/pymunk_cffi/chipmunk_cdef.h +++ b/pymunk_cffi/chipmunk_cdef.h @@ -112,6 +112,7 @@ struct cpArbiter cpTimestamp stamp; enum cpArbiterState state; + cpBool processCollision; }; struct cpArray From feb6dd0ff0adb94860ba01e29ba3a30874943661 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 9 May 2025 22:48:24 +0200 Subject: [PATCH 56/80] Use list/tuple instead of typing.Tuple etc --- pymunk/__init__.py | 22 +++---- pymunk/_pickle.py | 12 ++-- pymunk/_pyglet15_util.py | 12 ++-- pymunk/_types.py | 6 +- pymunk/arbiter.py | 18 +++--- pymunk/autogeometry.py | 30 ++++----- pymunk/bb.py | 14 ++-- pymunk/body.py | 28 ++++---- pymunk/collision_handler.py | 10 +-- pymunk/constraints.py | 48 +++++++------- pymunk/contact_point_set.py | 6 +- pymunk/examples/arrows.py | 7 +- pymunk/examples/bouncing_balls.py | 5 +- pymunk/examples/using_sprites.py | 9 ++- pymunk/examples/using_sprites_pyglet.py | 7 +- pymunk/pygame_util.py | 16 ++--- pymunk/pyglet_util.py | 12 ++-- pymunk/pymunk_extension_build.py | 3 +- pymunk/shapes.py | 2 +- pymunk/space.py | 86 +++++++++++-------------- pymunk/space_debug_draw_options.py | 12 ++-- pymunk/tests/__init__.py | 22 +++---- pymunk/tests/doctests.py | 3 +- pymunk/tests/test_autogeometry.py | 21 +++--- pymunk/tests/test_body.py | 5 +- pymunk/tests/test_common.py | 4 +- pymunk/transform.py | 10 +-- pymunk/vec2d.py | 50 +++++++------- 28 files changed, 230 insertions(+), 250 deletions(-) diff --git a/pymunk/__init__.py b/pymunk/__init__.py index 59359ac3..b206e2bc 100644 --- a/pymunk/__init__.py +++ b/pymunk/__init__.py @@ -30,10 +30,10 @@ This is the main containing module of Pymunk. It contains among other things the very central Space, Body and Shape classes. -Pymunk uses the standard logging module to log helpful information. It does +Pymunk uses the standard logging module to log helpful information. It does that under the "pymunk" name. If you do not do anything setup, it will print -WARNING and higher messages to stderr. (Note that you most likely do not want -to set logLevel to DEBUG, since Pymunk might log a lot of debug level +WARNING and higher messages to stderr. (Note that you most likely do not want +to set logLevel to DEBUG, since Pymunk might log a lot of debug level messages mostly useful during development of Pymunk itself.) """ @@ -68,7 +68,7 @@ "Vec2d", ] -from typing import Sequence, Tuple, cast +from typing import Sequence, cast from . import _chipmunk_cffi @@ -119,7 +119,7 @@ def moment_for_circle( mass: float, inner_radius: float, outer_radius: float, - offset: Tuple[float, float] = (0, 0), + offset: tuple[float, float] = (0, 0), ) -> float: """Calculate the moment of inertia for a hollow circle @@ -131,7 +131,7 @@ def moment_for_circle( def moment_for_segment( - mass: float, a: Tuple[float, float], b: Tuple[float, float], radius: float + mass: float, a: tuple[float, float], b: tuple[float, float], radius: float ) -> float: """Calculate the moment of inertia for a line segment @@ -143,7 +143,7 @@ def moment_for_segment( return cp.cpMomentForSegment(mass, a, b, radius) -def moment_for_box(mass: float, size: Tuple[float, float]) -> float: +def moment_for_box(mass: float, size: tuple[float, float]) -> float: """Calculate the moment of inertia for a solid box centered on the body. size should be a tuple of (width, height) @@ -154,8 +154,8 @@ def moment_for_box(mass: float, size: Tuple[float, float]) -> float: def moment_for_poly( mass: float, - vertices: Sequence[Tuple[float, float]], - offset: Tuple[float, float] = (0, 0), + vertices: Sequence[tuple[float, float]], + offset: tuple[float, float] = (0, 0), radius: float = 0, ) -> float: """Calculate the moment of inertia for a solid polygon shape. @@ -174,7 +174,7 @@ def area_for_circle(inner_radius: float, outer_radius: float) -> float: def area_for_segment( - a: Tuple[float, float], b: Tuple[float, float], radius: float + a: tuple[float, float], b: tuple[float, float], radius: float ) -> float: """Area of a beveled segment. @@ -186,7 +186,7 @@ def area_for_segment( return cp.cpAreaForSegment(a, b, radius) -def area_for_poly(vertices: Sequence[Tuple[float, float]], radius: float = 0) -> float: +def area_for_poly(vertices: Sequence[tuple[float, float]], radius: float = 0) -> float: """Signed area of a polygon shape. Returns a negative number for polygons with a clockwise winding. diff --git a/pymunk/_pickle.py b/pymunk/_pickle.py index 99bc30a7..bac05374 100644 --- a/pymunk/_pickle.py +++ b/pymunk/_pickle.py @@ -1,8 +1,8 @@ import copy -from typing import Any, ClassVar, Dict, List, Tuple, TypeVar +from typing import Any, ClassVar, TypeVar T = TypeVar("T", bound="PickleMixin") -_State = Dict[str, List[Tuple[str, Any]]] +_State = dict[str, list[tuple[str, Any]]] class PickleMixin: @@ -10,9 +10,9 @@ class PickleMixin: and copy. """ - _pickle_attrs_init: ClassVar[List[str]] = [] - _pickle_attrs_general: ClassVar[List[str]] = [] - _pickle_attrs_skip: ClassVar[List[str]] = [] + _pickle_attrs_init: ClassVar[list[str]] = [] + _pickle_attrs_general: ClassVar[list[str]] = [] + _pickle_attrs_skip: ClassVar[list[str]] = [] def __getstate__(self) -> _State: """Return the state of this object @@ -46,7 +46,7 @@ def __setstate__(self, state: _State) -> None: modules with this class. """ - init_attrs: List[str] = [] + init_attrs: list[str] = [] init_args = [v for k, v in state["init"]] self.__init__(*init_args) # type: ignore diff --git a/pymunk/_pyglet15_util.py b/pymunk/_pyglet15_util.py index a2be9f92..d2da9044 100644 --- a/pymunk/_pyglet15_util.py +++ b/pymunk/_pyglet15_util.py @@ -21,20 +21,20 @@ # SOFTWARE. # ---------------------------------------------------------------------------- -"""This submodule contains helper functions to help with quick prototyping +"""This submodule contains helper functions to help with quick prototyping using pymunk together with pyglet. Intended to help with debugging and prototyping, not for actual production use -in a full application. The methods contained in this module is opinionated -about your coordinate system and not very optimized (they use batched -drawing, but there is probably room for optimizations still). +in a full application. The methods contained in this module is opinionated +about your coordinate system and not very optimized (they use batched +drawing, but there is probably room for optimizations still). """ __docformat__ = "reStructuredText" import math import warnings -from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Type +from typing import TYPE_CHECKING, Any, Optional, Sequence, Type import pyglet # type: ignore @@ -92,7 +92,7 @@ def __init__(self, **kwargs: Any) -> None: """ self.new_batch = False - self.draw_shapes: List[Any] = [] + self.draw_shapes: list[Any] = [] if "batch" not in kwargs: self.new_batch = True diff --git a/pymunk/_types.py b/pymunk/_types.py index 6f64a28a..ac68e478 100644 --- a/pymunk/_types.py +++ b/pymunk/_types.py @@ -1,7 +1,7 @@ -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: from .vec2d import Vec2d -_Vec2dOrTuple = Union[Tuple[float, float], List[float], "Vec2d"] -_Vec2dOrFloat = Union[Tuple[float, float], List[float], "Vec2d", float] +_Vec2dOrTuple = Union[tuple[float, float], list[float], "Vec2d"] +_Vec2dOrFloat = Union[tuple[float, float], list[float], "Vec2d", float] diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index cbd5d0db..795f83d2 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -1,7 +1,7 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Sequence if TYPE_CHECKING: from .space import Space @@ -91,7 +91,7 @@ def contact_point_set(self, point_set: ContactPointSet) -> None: lib.cpArbiterSetContactPointSet(self._arbiter, ffi.addressof(_set)) @property - def bodies(self) -> Tuple["Body", "Body"]: + def bodies(self) -> tuple["Body", "Body"]: """The the bodies in the order their corresponding shapes were defined in the collision handler associated with this arbiter. @@ -109,7 +109,7 @@ def bodies(self) -> Tuple["Body", "Body"]: return a.body, b.body @property - def shapes(self) -> Tuple["Shape", "Shape"]: + def shapes(self) -> tuple["Shape", "Shape"]: """Get the shapes in the order that they were defined in the collision handler associated with this arbiter """ @@ -156,7 +156,7 @@ def _get_surface_velocity(self) -> Vec2d: v = lib.cpArbiterGetSurfaceVelocity(self._arbiter) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, velocity: Tuple[float, float]) -> None: + def _set_surface_velocity(self, velocity: tuple[float, float]) -> None: lib.cpArbiterSetSurfaceVelocity(self._arbiter, velocity) surface_velocity = property( @@ -223,14 +223,14 @@ def normal(self) -> Vec2d: def _contacts_to_dicts( _contacts: Sequence[ffi.CData], count: int -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: res = [] for i in range(count): res.append(_contact_to_dict(_contacts[i])) return res -def _contact_to_dict(_contact: ffi.CData) -> Dict[str, Any]: +def _contact_to_dict(_contact: ffi.CData) -> dict[str, Any]: d = {} d["r1"] = _contact.r1.x, _contact.r1.y d["r2"] = _contact.r2.x, _contact.r2.y @@ -245,7 +245,7 @@ def _contact_to_dict(_contact: ffi.CData) -> Dict[str, Any]: return d -def _contacts_from_dicts(ds: Sequence[Dict[str, Any]]) -> List[ffi.CData]: +def _contacts_from_dicts(ds: Sequence[dict[str, Any]]) -> dist[ffi.CData]: _contacts = lib.cpContactArrAlloc(len(ds)) for i in range(len(ds)): _contact = _contacts[i] @@ -265,7 +265,7 @@ def _contacts_from_dicts(ds: Sequence[Dict[str, Any]]) -> List[ffi.CData]: return _contacts -def _arbiter_from_dict(d: Dict[str, Any], space: "Space") -> ffi.CData: +def _arbiter_from_dict(d: dict[str, Any], space: "Space") -> ffi.CData: _arb = lib.cpArbiterNew( d["a"]._shape, d["b"]._shape ) # this will also set the bodies @@ -285,7 +285,7 @@ def _arbiter_from_dict(d: Dict[str, Any], space: "Space") -> ffi.CData: return _arb -def _arbiter_to_dict(_arbiter: ffi.CData, space: "Space") -> Dict[str, Any]: +def _arbiter_to_dict(_arbiter: ffi.CData, space: "Space") -> dict[str, Any]: d = {} d["e"] = _arbiter.e d["u"] = _arbiter.u diff --git a/pymunk/autogeometry.py b/pymunk/autogeometry.py index 34d697dc..bf8603a0 100644 --- a/pymunk/autogeometry.py +++ b/pymunk/autogeometry.py @@ -39,7 +39,7 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Callable, List, Sequence, Tuple, Union, overload +from typing import TYPE_CHECKING, Callable, Sequence, Union, overload if TYPE_CHECKING: from .bb import BB @@ -48,11 +48,11 @@ from ._chipmunk_cffi import ffi, lib from .vec2d import Vec2d -_SampleFunc = Callable[[Tuple[float, float]], float] +_SampleFunc = Callable[[tuple[float, float]], float] -_Polyline = Union[List[Tuple[float, float]], List[Vec2d]] +_Polyline = Union[list[tuple[float, float]], list[Vec2d]] # Union is needed since List is invariant -# and Sequence cant be used since CFFI requires a List (or Tuple) +# and Sequence cant be used since CFFI requires a list (or tuple) def _to_chipmunk(polyline: _Polyline) -> ffi.CData: @@ -64,7 +64,7 @@ def _to_chipmunk(polyline: _Polyline) -> ffi.CData: return _line -def _from_polyline_set(_set: ffi.CData) -> List[List[Vec2d]]: +def _from_polyline_set(_set: ffi.CData) -> list[list[Vec2d]]: lines = [] for i in range(_set.count): line = [] @@ -85,7 +85,7 @@ def is_closed(polyline: _Polyline) -> bool: return bool(lib.cpPolylineIsClosed(_to_chipmunk(polyline))) -def simplify_curves(polyline: _Polyline, tolerance: float) -> List[Vec2d]: +def simplify_curves(polyline: _Polyline, tolerance: float) -> list[Vec2d]: """Returns a copy of a polyline simplified by using the Douglas-Peucker algorithm. @@ -105,7 +105,7 @@ def simplify_curves(polyline: _Polyline, tolerance: float) -> List[Vec2d]: return simplified -def simplify_vertexes(polyline: _Polyline, tolerance: float) -> List[Vec2d]: +def simplify_vertexes(polyline: _Polyline, tolerance: float) -> list[Vec2d]: """Returns a copy of a polyline simplified by discarding "flat" vertexes. This works well on straight edged or angular shapes, not as well on smooth @@ -123,7 +123,7 @@ def simplify_vertexes(polyline: _Polyline, tolerance: float) -> List[Vec2d]: return simplified -def to_convex_hull(polyline: _Polyline, tolerance: float) -> List[Vec2d]: +def to_convex_hull(polyline: _Polyline, tolerance: float) -> list[Vec2d]: """Get the convex hull of a polyline as a looped polyline. :param polyline: Polyline to simplify. @@ -138,7 +138,7 @@ def to_convex_hull(polyline: _Polyline, tolerance: float) -> List[Vec2d]: return hull -def convex_decomposition(polyline: _Polyline, tolerance: float) -> List[List[Vec2d]]: +def convex_decomposition(polyline: _Polyline, tolerance: float) -> list[list[Vec2d]]: """Get an approximate convex decomposition from a polyline. Returns a list of convex hulls that match the original shape to within @@ -164,7 +164,7 @@ def convex_decomposition(polyline: _Polyline, tolerance: float) -> List[List[Vec return _from_polyline_set(_set) -class PolylineSet(Sequence[List[Vec2d]]): +class PolylineSet(Sequence[list[Vec2d]]): """A set of Polylines. Mainly intended to be used for its :py:meth:`collect_segment` function @@ -180,7 +180,7 @@ def free(_set: ffi.CData) -> None: self._set = ffi.gc(lib.cpPolylineSetNew(), free) - def collect_segment(self, v0: Tuple[float, float], v1: Tuple[float, float]) -> None: + def collect_segment(self, v0: tuple[float, float], v1: tuple[float, float]) -> None: """Add a line segment to a polyline set. A segment will either start a new polyline, join two others, or add to @@ -201,14 +201,14 @@ def __len__(self) -> int: return self._set.count @overload - def __getitem__(self, index: int) -> List[Vec2d]: ... + def __getitem__(self, index: int) -> list[Vec2d]: ... @overload def __getitem__(self, index: slice) -> "PolylineSet": ... def __getitem__( self, index: Union[int, slice] - ) -> Union[List[Vec2d], "PolylineSet"]: + ) -> Union[list[Vec2d], "PolylineSet"]: assert not isinstance(index, slice), "Slice indexing not supported" if index >= self._set.count: raise IndexError @@ -238,7 +238,7 @@ def march_soft( :param sample_func: The sample function will be called for x_samples * y_samples spread across the bounding box area, and should return a float. - :type sample_func: ``func(point: Tuple[float, float]) -> float`` + :type sample_func: ``func(point: tuple[float, float]) -> float`` :return: PolylineSet with the polylines found. """ pl_set = PolylineSet() @@ -278,7 +278,7 @@ def march_hard( :param sample_func: The sample function will be called for x_samples * y_samples spread across the bounding box area, and should return a float. - :type sample_func: ``func(point: Tuple[float, float]) -> float`` + :type sample_func: ``func(point: tuple[float, float]) -> float`` :return: PolylineSet with the polylines found. """ diff --git a/pymunk/bb.py b/pymunk/bb.py index 960a5ee9..60a2d439 100644 --- a/pymunk/bb.py +++ b/pymunk/bb.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import NamedTuple, Tuple +from typing import NamedTuple from . import _chipmunk_cffi @@ -29,7 +29,7 @@ class BB(NamedTuple): top: float = 0 @staticmethod - def newForCircle(p: Tuple[float, float], r: float) -> "BB": + def newForCircle(p: tuple[float, float], r: float) -> "BB": """Convenience constructor for making a BB fitting a circle at position p with radius r. """ @@ -42,7 +42,7 @@ def intersects(self, other: "BB") -> bool: return bool(lib.cpBBIntersects(self, other)) def intersects_segment( - self, a: Tuple[float, float], b: Tuple[float, float] + self, a: tuple[float, float], b: tuple[float, float] ) -> bool: """Returns true if the segment defined by endpoints a and b intersect this bb.""" @@ -54,7 +54,7 @@ def contains(self, other: "BB") -> bool: """Returns true if bb completley contains the other bb""" return bool(lib.cpBBContainsBB(self, other)) - def contains_vect(self, v: Tuple[float, float]) -> bool: + def contains_vect(self, v: tuple[float, float]) -> bool: """Returns true if this bb contains the vector v""" assert len(v) == 2 return bool(lib.cpBBContainsVect(self, v)) @@ -66,7 +66,7 @@ def merge(self, other: "BB") -> "BB": cp_bb = lib.cpBBMerge(self, other) return BB(cp_bb.l, cp_bb.b, cp_bb.r, cp_bb.t) - def expand(self, v: Tuple[float, float]) -> "BB": + def expand(self, v: tuple[float, float]) -> "BB": """Return the minimal bounding box that contans both this bounding box and the vector v """ @@ -88,7 +88,7 @@ def merged_area(self, other: "BB") -> float: """ return lib.cpBBMergedArea(self, other) - def segment_query(self, a: Tuple[float, float], b: Tuple[float, float]) -> float: + def segment_query(self, a: tuple[float, float], b: tuple[float, float]) -> float: """Returns the fraction along the segment query the BB is hit. Returns infinity if it doesnt hit @@ -97,7 +97,7 @@ def segment_query(self, a: Tuple[float, float], b: Tuple[float, float]) -> float assert len(b) == 2 return lib.cpBBSegmentQuery(self, a, b) - def clamp_vect(self, v: Tuple[float, float]) -> Vec2d: + def clamp_vect(self, v: tuple[float, float]) -> Vec2d: """Returns a copy of the vector v clamped to the bounding box""" assert len(v) == 2 v2 = lib.cpBBClampVect(self, v) diff --git a/pymunk/body.py b/pymunk/body.py index e42d222e..8f7135a4 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -2,7 +2,7 @@ import weakref from collections.abc import KeysView -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple # Literal, +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional # Literal, from weakref import WeakKeyDictionary if TYPE_CHECKING: @@ -269,7 +269,7 @@ def moment(self) -> float: def moment(self, moment: float) -> None: lib.cpBodySetMoment(self._body, moment) - def _set_position(self, pos: Tuple[float, float]) -> None: + def _set_position(self, pos: tuple[float, float]) -> None: assert len(pos) == 2 lib.cpBodySetPosition(self._body, pos) @@ -288,7 +288,7 @@ def _get_position(self) -> Vec2d: queries against the space.""", ) - def _set_center_of_gravity(self, cog: Tuple[float, float]) -> None: + def _set_center_of_gravity(self, cog: tuple[float, float]) -> None: assert len(cog) == 2 lib.cpBodySetCenterOfGravity(self._body, cog) @@ -306,7 +306,7 @@ def _get_center_of_gravity(self) -> Vec2d: """, ) - def _set_velocity(self, vel: Tuple[float, float]) -> None: + def _set_velocity(self, vel: tuple[float, float]) -> None: assert len(vel) == 2 lib.cpBodySetVelocity(self._body, vel) @@ -320,7 +320,7 @@ def _get_velocity(self) -> Vec2d: doc="""Linear velocity of the center of gravity of the body.""", ) - def _set_force(self, f: Tuple[float, float]) -> None: + def _set_force(self, f: tuple[float, float]) -> None: assert len(f) == 2 lib.cpBodySetForce(self._body, f) @@ -480,7 +480,7 @@ def kinetic_energy(self) -> float: @staticmethod def update_velocity( - body: "Body", gravity: Tuple[float, float], damping: float, dt: float + body: "Body", gravity: tuple[float, float], damping: float, dt: float ) -> None: """Default rigid body velocity integration function. @@ -502,7 +502,7 @@ def update_position(body: "Body", dt: float) -> None: lib.cpBodyUpdatePosition(body._body, dt) def apply_force_at_world_point( - self, force: Tuple[float, float], point: Tuple[float, float] + self, force: tuple[float, float], point: tuple[float, float] ) -> None: """Add the force force to body as if applied from the world point. @@ -519,7 +519,7 @@ def apply_force_at_world_point( lib.cpBodyApplyForceAtWorldPoint(self._body, force, point) def apply_force_at_local_point( - self, force: Tuple[float, float], point: Tuple[float, float] = (0, 0) + self, force: tuple[float, float], point: tuple[float, float] = (0, 0) ) -> None: """Add the local force force to body as if applied from the body local point. @@ -529,7 +529,7 @@ def apply_force_at_local_point( lib.cpBodyApplyForceAtLocalPoint(self._body, force, point) def apply_impulse_at_world_point( - self, impulse: Tuple[float, float], point: Tuple[float, float] + self, impulse: tuple[float, float], point: tuple[float, float] ) -> None: """Add the impulse impulse to body as if applied from the world point.""" assert len(impulse) == 2 @@ -537,7 +537,7 @@ def apply_impulse_at_world_point( lib.cpBodyApplyImpulseAtWorldPoint(self._body, impulse, point) def apply_impulse_at_local_point( - self, impulse: Tuple[float, float], point: Tuple[float, float] = (0, 0) + self, impulse: tuple[float, float], point: tuple[float, float] = (0, 0) ) -> None: """Add the local impulse impulse to body as if applied from the body local point. @@ -663,7 +663,7 @@ def shapes(self) -> KeysView["Shape"]: """ return self._shapes.keys() - def local_to_world(self, v: Tuple[float, float]) -> Vec2d: + def local_to_world(self, v: tuple[float, float]) -> Vec2d: """Convert body local coordinates to world space coordinates Many things are defined in coordinates local to a body meaning that @@ -676,7 +676,7 @@ def local_to_world(self, v: Tuple[float, float]) -> Vec2d: v2 = lib.cpBodyLocalToWorld(self._body, v) return Vec2d(v2.x, v2.y) - def world_to_local(self, v: Tuple[float, float]) -> Vec2d: + def world_to_local(self, v: tuple[float, float]) -> Vec2d: """Convert world space coordinates to body local coordinates :param v: Vector in world space coordinates @@ -685,7 +685,7 @@ def world_to_local(self, v: Tuple[float, float]) -> Vec2d: v2 = lib.cpBodyWorldToLocal(self._body, v) return Vec2d(v2.x, v2.y) - def velocity_at_world_point(self, point: Tuple[float, float]) -> Vec2d: + def velocity_at_world_point(self, point: tuple[float, float]) -> Vec2d: """Get the absolute velocity of the rigid body at the given world point @@ -697,7 +697,7 @@ def velocity_at_world_point(self, point: Tuple[float, float]) -> Vec2d: v = lib.cpBodyGetVelocityAtWorldPoint(self._body, point) return Vec2d(v.x, v.y) - def velocity_at_local_point(self, point: Tuple[float, float]) -> Vec2d: + def velocity_at_local_point(self, point: tuple[float, float]) -> Vec2d: """Get the absolute velocity of the rigid body at the given body local point """ diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index 1849915c..f4c276de 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Any, Callable, Dict +from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from .space import Space @@ -8,7 +8,7 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallback = Callable[[Arbiter, "Space", Dict[Any, Any]], None] +_CollisionCallback = Callable[[Arbiter, "Space", dict[Any, Any]], None] class CollisionHandler(object): @@ -46,10 +46,10 @@ def __init__(self, _handler: Any, space: "Space") -> None: self._post_solve: _CollisionCallback = CollisionHandler.do_nothing self._separate: _CollisionCallback = CollisionHandler.do_nothing - self._data: Dict[Any, Any] = {} + self._data: dict[Any, Any] = {} @property - def data(self) -> Dict[Any, Any]: + def data(self) -> dict[Any, Any]: """Data property that get passed on into the callbacks. @@ -149,7 +149,7 @@ def separate(self, func: _CollisionCallback) -> None: self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc @staticmethod - def do_nothing(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> None: + def do_nothing(arbiter: Arbiter, space: "Space", data: dict[Any, Any]) -> None: """The default do nothing method used for the post_solve and seprate callbacks. diff --git a/pymunk/constraints.py b/pymunk/constraints.py index ce401a92..858874ee 100644 --- a/pymunk/constraints.py +++ b/pymunk/constraints.py @@ -68,7 +68,7 @@ "SimpleMotor", ] -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Optional, Union if TYPE_CHECKING: from .space import Space @@ -274,7 +274,7 @@ def _set_bodies(self, a: "Body", b: "Body") -> None: a._constraints[self] = None b._constraints[self] = None - def __getstate__(self) -> Dict[str, List[Tuple[str, Any]]]: + def __getstate__(self) -> dict[str, list[tuple[str, Any]]]: """Return the state of this object This method allows the usage of the :mod:`copy` and :mod:`pickle` @@ -287,7 +287,7 @@ def __getstate__(self) -> Dict[str, List[Tuple[str, Any]]]: return d - def __setstate__(self, state: Dict[str, List[Tuple[str, Any]]]) -> None: + def __setstate__(self, state: dict[str, list[tuple[str, Any]]]) -> None: """Unpack this object from a saved state. This method allows the usage of the :mod:`copy` and :mod:`pickle` @@ -314,8 +314,8 @@ def __init__( self, a: "Body", b: "Body", - anchor_a: Tuple[float, float] = (0, 0), - anchor_b: Tuple[float, float] = (0, 0), + anchor_a: tuple[float, float] = (0, 0), + anchor_b: tuple[float, float] = (0, 0), ) -> None: """a and b are the two bodies to connect, and anchor_a and anchor_b are the anchor points on those bodies. @@ -333,7 +333,7 @@ def _get_anchor_a(self) -> Vec2d: v = lib.cpPinJointGetAnchorA(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_a(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPinJointSetAnchorA(self._constraint, anchor) @@ -343,7 +343,7 @@ def _get_anchor_b(self) -> Vec2d: v = lib.cpPinJointGetAnchorB(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPinJointSetAnchorB(self._constraint, anchor) @@ -376,8 +376,8 @@ def __init__( self, a: "Body", b: "Body", - anchor_a: Tuple[float, float], - anchor_b: Tuple[float, float], + anchor_a: tuple[float, float], + anchor_b: tuple[float, float], min: float, max: float, ) -> None: @@ -396,7 +396,7 @@ def _get_anchor_a(self) -> Vec2d: v = lib.cpSlideJointGetAnchorA(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_a(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpSlideJointSetAnchorA(self._constraint, anchor) @@ -406,7 +406,7 @@ def _get_anchor_b(self) -> Vec2d: v = lib.cpSlideJointGetAnchorB(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpSlideJointSetAnchorB(self._constraint, anchor) @@ -442,7 +442,7 @@ def __init__( a: "Body", b: "Body", *args: Union[ - Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]] + tuple[float, float], tuple[tuple[float, float], tuple[float, float]] ], ) -> None: """a and b are the two bodies to connect, and pivot is the point in @@ -480,7 +480,7 @@ def _get_anchor_a(self) -> Vec2d: v = lib.cpPivotJointGetAnchorA(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_a(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPivotJointSetAnchorA(self._constraint, anchor) @@ -490,7 +490,7 @@ def _get_anchor_b(self) -> Vec2d: v = lib.cpPivotJointGetAnchorB(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPivotJointSetAnchorB(self._constraint, anchor) @@ -513,9 +513,9 @@ def __init__( self, a: "Body", b: "Body", - groove_a: Tuple[float, float], - groove_b: Tuple[float, float], - anchor_b: Tuple[float, float], + groove_a: tuple[float, float], + groove_b: tuple[float, float], + anchor_b: tuple[float, float], ) -> None: """The groove goes from groove_a to groove_b on body a, and the pivot is attached to anchor_b on body b. @@ -534,7 +534,7 @@ def _get_anchor_b(self) -> Vec2d: v = lib.cpGrooveJointGetAnchorB(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpGrooveJointSetAnchorB(self._constraint, anchor) @@ -544,7 +544,7 @@ def _get_groove_a(self) -> Vec2d: v = lib.cpGrooveJointGetGrooveA(self._constraint) return Vec2d(v.x, v.y) - def _set_groove_a(self, groove: Tuple[float, float]) -> None: + def _set_groove_a(self, groove: tuple[float, float]) -> None: assert len(groove) == 2 lib.cpGrooveJointSetGrooveA(self._constraint, groove) @@ -554,7 +554,7 @@ def _get_groove_b(self) -> Vec2d: v = lib.cpGrooveJointGetGrooveB(self._constraint) return Vec2d(v.x, v.y) - def _set_groove_b(self, groove: Tuple[float, float]) -> None: + def _set_groove_b(self, groove: tuple[float, float]) -> None: assert len(groove) == 2 lib.cpGrooveJointSetGrooveB(self._constraint, groove) @@ -581,8 +581,8 @@ def __init__( self, a: "Body", b: "Body", - anchor_a: Tuple[float, float], - anchor_b: Tuple[float, float], + anchor_a: tuple[float, float], + anchor_b: tuple[float, float], rest_length: float, stiffness: float, damping: float, @@ -617,7 +617,7 @@ def _get_anchor_a(self) -> Vec2d: v = lib.cpDampedSpringGetAnchorA(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_a(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpDampedSpringSetAnchorA(self._constraint, anchor) @@ -627,7 +627,7 @@ def _get_anchor_b(self) -> Vec2d: v = lib.cpDampedSpringGetAnchorB(self._constraint) return Vec2d(v.x, v.y) - def _set_anchor_b(self, anchor: Tuple[float, float]) -> None: + def _set_anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpDampedSpringSetAnchorB(self._constraint, anchor) diff --git a/pymunk/contact_point_set.py b/pymunk/contact_point_set.py index cd6042e4..4c0297bd 100644 --- a/pymunk/contact_point_set.py +++ b/pymunk/contact_point_set.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING if TYPE_CHECKING: from ._chipmunk_cffi import ffi @@ -50,11 +50,11 @@ class ContactPointSet(object): """ normal: Vec2d - points: Tuple[ContactPoint, ...] + points: tuple[ContactPoint, ...] __slots__ = ("normal", "points") - def __init__(self, normal: Vec2d, points: Tuple[ContactPoint, ...]) -> None: + def __init__(self, normal: Vec2d, points: tuple[ContactPoint, ...]) -> None: self.normal = normal self.points = points diff --git a/pymunk/examples/arrows.py b/pymunk/examples/arrows.py index beb3a188..3cc0a10e 100644 --- a/pymunk/examples/arrows.py +++ b/pymunk/examples/arrows.py @@ -1,9 +1,8 @@ -"""Showcase of flying arrows that can stick to objects in a somewhat +"""Showcase of flying arrows that can stick to objects in a somewhat realistic looking way. """ import sys -from typing import List import pygame @@ -71,7 +70,7 @@ def main(): draw_options = pymunk.pygame_util.DrawOptions(screen) # walls - the left-top-right walls - static: List[pymunk.Shape] = [ + static: list[pymunk.Shape] = [ pymunk.Segment(space.static_body, (50, 550), (50, 50), 5), pymunk.Segment(space.static_body, (50, 50), (650, 50), 5), pymunk.Segment(space.static_body, (650, 50), (650, 550), 5), @@ -98,7 +97,7 @@ def main(): arrow_body, arrow_shape = create_arrow() space.add(arrow_body, arrow_shape) - flying_arrows: List[pymunk.Body] = [] + flying_arrows: list[pymunk.Body] = [] handler = space.add_collision_handler(0, 1) handler.data["flying_arrows"] = flying_arrows handler.post_solve = post_solve_arrow_hit diff --git a/pymunk/examples/bouncing_balls.py b/pymunk/examples/bouncing_balls.py index d9b49af5..38413f1a 100644 --- a/pymunk/examples/bouncing_balls.py +++ b/pymunk/examples/bouncing_balls.py @@ -1,4 +1,4 @@ -"""This example spawns (bouncing) balls randomly on a L-shape constructed of +"""This example spawns (bouncing) balls randomly on a L-shape constructed of two segment shapes. Not interactive. """ @@ -6,7 +6,6 @@ # Python imports import random -from typing import List # Library imports import pygame @@ -44,7 +43,7 @@ def __init__(self) -> None: self._add_static_scenery() # Balls that exist in the world - self._balls: List[pymunk.Circle] = [] + self._balls: list[pymunk.Circle] = [] # Execution control and time until the next ball spawns self._running = True diff --git a/pymunk/examples/using_sprites.py b/pymunk/examples/using_sprites.py index 1c4f219e..2a50bde6 100644 --- a/pymunk/examples/using_sprites.py +++ b/pymunk/examples/using_sprites.py @@ -1,6 +1,6 @@ -"""Very basic example of using a sprite image to draw a shape more similar -how you would do it in a real game instead of the simple line drawings used -by the other examples. +"""Very basic example of using a sprite image to draw a shape more similar +how you would do it in a real game instead of the simple line drawings used +by the other examples. """ __docformat__ = "reStructuredText" @@ -8,7 +8,6 @@ import math import os.path import random -from typing import List import pygame @@ -38,7 +37,7 @@ def main(): os.path.dirname(os.path.abspath(__file__)), "pymunk_logo_googlecode.png" ) ) - logos: List[pymunk.Shape] = [] + logos: list[pymunk.Shape] = [] ### Static line static_lines = [ diff --git a/pymunk/examples/using_sprites_pyglet.py b/pymunk/examples/using_sprites_pyglet.py index f26e0ace..3632cb40 100644 --- a/pymunk/examples/using_sprites_pyglet.py +++ b/pymunk/examples/using_sprites_pyglet.py @@ -1,12 +1,11 @@ -"""This example is a clone of the using_sprites example with the difference -that it uses pyglet instead of pygame to showcase sprite drawing. +"""This example is a clone of the using_sprites example with the difference +that it uses pyglet instead of pygame to showcase sprite drawing. """ __docformat__ = "reStructuredText" import math import random -from typing import List import pyglet @@ -21,7 +20,7 @@ logo_img = pyglet.resource.image("pymunk_logo_googlecode.png") logo_img.anchor_x = logo_img.width / 2 logo_img.anchor_y = logo_img.height / 2 -logos: List[pyglet.sprite.Sprite] = [] +logos: list[pyglet.sprite.Sprite] = [] batch = pyglet.graphics.Batch() ### Physics stuff diff --git a/pymunk/pygame_util.py b/pymunk/pygame_util.py index bb8f0aae..1dc400ef 100644 --- a/pymunk/pygame_util.py +++ b/pymunk/pygame_util.py @@ -39,7 +39,7 @@ "positive_y_is_up", ] -from typing import Sequence, Tuple +from typing import Sequence import pygame @@ -150,8 +150,8 @@ def draw_segment(self, a: Vec2d, b: Vec2d, color: SpaceDebugColor) -> None: def draw_fat_segment( self, - a: Tuple[float, float], - b: Tuple[float, float], + a: tuple[float, float], + b: tuple[float, float], radius: float, outline_color: SpaceDebugColor, fill_color: SpaceDebugColor, @@ -190,7 +190,7 @@ def draw_fat_segment( def draw_polygon( self, - verts: Sequence[Tuple[float, float]], + verts: Sequence[tuple[float, float]], radius: float, outline_color: SpaceDebugColor, fill_color: SpaceDebugColor, @@ -206,19 +206,19 @@ def draw_polygon( self.draw_fat_segment(a, b, radius, outline_color, outline_color) def draw_dot( - self, size: float, pos: Tuple[float, float], color: SpaceDebugColor + self, size: float, pos: tuple[float, float], color: SpaceDebugColor ) -> None: p = to_pygame(pos, self.surface) pygame.draw.circle(self.surface, color.as_int(), p, round(size), 0) -def get_mouse_pos(surface: pygame.Surface) -> Tuple[int, int]: +def get_mouse_pos(surface: pygame.Surface) -> tuple[int, int]: """Get position of the mouse pointer in pymunk coordinates.""" p = pygame.mouse.get_pos() return from_pygame(p, surface) -def to_pygame(p: Tuple[float, float], surface: pygame.Surface) -> Tuple[int, int]: +def to_pygame(p: tuple[float, float], surface: pygame.Surface) -> tuple[int, int]: """Convenience method to convert pymunk coordinates to pygame surface local coordinates. @@ -231,7 +231,7 @@ def to_pygame(p: Tuple[float, float], surface: pygame.Surface) -> Tuple[int, int return round(p[0]), round(p[1]) -def from_pygame(p: Tuple[float, float], surface: pygame.Surface) -> Tuple[int, int]: +def from_pygame(p: tuple[float, float], surface: pygame.Surface) -> tuple[int, int]: """Convenience method to convert pygame surface local coordinates to pymunk coordinates """ diff --git a/pymunk/pyglet_util.py b/pymunk/pyglet_util.py index f70f0493..387ea306 100644 --- a/pymunk/pyglet_util.py +++ b/pymunk/pyglet_util.py @@ -21,19 +21,19 @@ # SOFTWARE. # ---------------------------------------------------------------------------- -"""This submodule contains helper functions to help with quick prototyping +"""This submodule contains helper functions to help with quick prototyping using pymunk together with pyglet. Intended to help with debugging and prototyping, not for actual production use -in a full application. The methods contained in this module is opinionated -about your coordinate system and not very optimized (they use batched -drawing, but there is probably room for optimizations still). +in a full application. The methods contained in this module is opinionated +about your coordinate system and not very optimized (they use batched +drawing, but there is probably room for optimizations still). """ __docformat__ = "reStructuredText" import math -from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Type +from typing import TYPE_CHECKING, Any, Optional, Sequence, Type import pyglet @@ -83,7 +83,7 @@ def __init__(self, **kwargs: Any) -> None: """ self.new_batch = False - self.draw_shapes: List[Any] = [] + self.draw_shapes: list[Any] = [] if "batch" not in kwargs: self.new_batch = True diff --git a/pymunk/pymunk_extension_build.py b/pymunk/pymunk_extension_build.py index 04161aad..0608bb66 100644 --- a/pymunk/pymunk_extension_build.py +++ b/pymunk/pymunk_extension_build.py @@ -1,7 +1,6 @@ import os import os.path import platform -from typing import List from cffi import FFI # type: ignore @@ -38,7 +37,7 @@ elif fn[-1] == "o": os.remove(fn_path) -libraries: List[str] = [] +libraries: list[str] = [] # if os == linux: # libraries.append('m') diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 128c06eb..848e787a 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,7 +1,7 @@ __docformat__ = "reStructuredText" import weakref -from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Optional, Sequence if TYPE_CHECKING: from .body import Body diff --git a/pymunk/space.py b/pymunk/space.py index 57c09ae4..f21d0eb9 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -4,19 +4,7 @@ import platform import weakref from collections.abc import KeysView, Mapping -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Hashable, - Iterator, - List, - Optional, - Set, - Tuple, - Union, -) +from typing import TYPE_CHECKING, Any, Callable, Hashable, Iterator, Optional, Union from pymunk.constraints import Constraint from pymunk.shape_filter import ShapeFilter @@ -43,14 +31,14 @@ _AddableObjects = Union[Body, Shape, Constraint] -class Handlers(Mapping[Union[None, int, Tuple[int, int]], CollisionHandler]): +class Handlers(Mapping[Union[None, int, tuple[int, int]], CollisionHandler]): def __init__(self, space: "Space") -> None: self.space = space - _handlers: Dict[Union[None, int, Tuple[int, int]], CollisionHandler] = {} + _handlers: dict[Union[None, int, tuple[int, int]], CollisionHandler] = {} - def __getitem__(self, key: Union[None, int, Tuple[int, int]]) -> CollisionHandler: + def __getitem__(self, key: Union[None, int, tuple[int, int]]) -> CollisionHandler: if key in self._handlers: return self._handlers[key] if key == None: @@ -69,7 +57,7 @@ def __getitem__(self, key: Union[None, int, Tuple[int, int]]) -> CollisionHandle def __len__(self) -> int: return len(self._handlers) - def __iter__(self) -> Iterator[Union[None, int, Tuple[int, int]]]: + def __iter__(self) -> Iterator[Union[None, int, tuple[int, int]]]: return iter(self._handlers) @@ -131,7 +119,7 @@ def __init__(self, threaded: bool = False) -> None: freefunc = cp.cpSpaceFree def spacefree(cp_space: ffi.CData) -> None: - cp_shapes: List[Shape] = [] + cp_shapes: list[Shape] = [] cp_shapes_h = ffi.new_handle(cp_shapes) cp.cpSpaceEachShape(cp_space, lib.ext_cpSpaceShapeIteratorFunc, cp_shapes_h) @@ -141,7 +129,7 @@ def spacefree(cp_space: ffi.CData) -> None: lib.cpSpaceRemoveShape(cp_space, cp_shape) lib.cpShapeSetBody(cp_shape, ffi.NULL) - cp_constraints: List[Constraint] = [] + cp_constraints: list[Constraint] = [] cp_constraints_h = ffi.new_handle(cp_constraints) cp.cpSpaceEachConstraint( cp_space, lib.ext_cpSpaceConstraintIteratorFunc, cp_constraints_h @@ -150,7 +138,7 @@ def spacefree(cp_space: ffi.CData) -> None: cp_space = lib.cpConstraintGetSpace(cp_constraint) lib.cpSpaceRemoveConstraint(cp_space, cp_constraint) - cp_bodys: List[Body] = [] + cp_bodys: list[Body] = [] cp_bodys_h = ffi.new_handle(cp_bodys) cp.cpSpaceEachBody(cp_space, lib.ext_cpSpaceBodyIteratorFunc, cp_bodys_h) for cp_body in cp_bodys: @@ -161,23 +149,23 @@ def spacefree(cp_space: ffi.CData) -> None: self._space = ffi.gc(cp_space, spacefree) - self._handlers: Dict[Any, CollisionHandler] = ( + self._handlers: dict[Any, CollisionHandler] = ( {} ) # To prevent the gc to collect the callbacks. - self._post_step_callbacks: Dict[Any, Callable[["Space"], None]] = {} - self._removed_shapes: Dict[Shape, None] = {} + self._post_step_callbacks: dict[Any, Callable[["Space"], None]] = {} + self._removed_shapes: dict[Shape, None] = {} - self._shapes: Dict[Shape, None] = {} - self._bodies: Dict[Body, None] = {} + self._shapes: dict[Shape, None] = {} + self._bodies: dict[Body, None] = {} self._static_body: Optional[Body] = None - self._constraints: Dict[Constraint, None] = {} + self._constraints: dict[Constraint, None] = {} self._locked = False - self._add_later: Set[_AddableObjects] = set() - self._remove_later: Set[_AddableObjects] = set() - self._bodies_to_check: Set[Body] = set() + self._add_later: set[_AddableObjects] = set() + self._remove_later: set[_AddableObjects] = set() + self._bodies_to_check: set[Body] = set() self._collision_handlers = Handlers(self) @@ -271,7 +259,7 @@ def iterations(self) -> int: def iterations(self, value: int) -> None: cp.cpSpaceSetIterations(self._space, value) - def _set_gravity(self, gravity_vector: Tuple[float, float]) -> None: + def _set_gravity(self, gravity_vector: tuple[float, float]) -> None: assert len(gravity_vector) == 2 cp.cpSpaceSetGravity(self._space, gravity_vector) @@ -634,7 +622,7 @@ def step(self, dt: float) -> None: @property def collision_handlers( self, - ) -> Mapping[Union[None, int, Tuple[int, int]], CollisionHandler]: + ) -> Mapping[Union[None, int, tuple[int, int]], CollisionHandler]: return self.collision_handlers def add_collision_handler( @@ -699,7 +687,7 @@ def add_wildcard_collision_handler(self, collision_type_a: int) -> CollisionHand self._handlers[collision_type_a] = ch return ch - def add_global_collision_handler(self) -> CollisionHandler: + def add_all_collision_handler(self) -> CollisionHandler: """Return a reference to the default collision handler or that is used to process all collisions that don't have a more specific handler. @@ -764,8 +752,8 @@ def f(x: "Space") -> None: return True def point_query( - self, point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter - ) -> List[PointQueryInfo]: + self, point: tuple[float, float], max_distance: float, shape_filter: ShapeFilter + ) -> list[PointQueryInfo]: """Query space at point for shapes within the given distance range. The filter is applied to the query and follows the same rules as the @@ -789,7 +777,7 @@ def point_query( :rtype: [:py:class:`PointQueryInfo`] """ assert len(point) == 2 - query_hits: List[PointQueryInfo] = [] + query_hits: list[PointQueryInfo] = [] d = (self, query_hits) data = ffi.new_handle(d) cp.cpSpacePointQuery( @@ -803,7 +791,7 @@ def point_query( return query_hits def point_query_nearest( - self, point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter + self, point: tuple[float, float], max_distance: float, shape_filter: ShapeFilter ) -> Optional[PointQueryInfo]: """Query space at point the nearest shape within the given distance range. @@ -847,11 +835,11 @@ def point_query_nearest( def segment_query( self, - start: Tuple[float, float], - end: Tuple[float, float], + start: tuple[float, float], + end: tuple[float, float], radius: float, shape_filter: ShapeFilter, - ) -> List[SegmentQueryInfo]: + ) -> list[SegmentQueryInfo]: """Query space along the line segment from start to end with the given radius. @@ -874,7 +862,7 @@ def segment_query( """ assert len(start) == 2 assert len(end) == 2 - query_hits: List[SegmentQueryInfo] = [] + query_hits: list[SegmentQueryInfo] = [] d = (self, query_hits) data = ffi.new_handle(d) @@ -892,8 +880,8 @@ def segment_query( def segment_query_first( self, - start: Tuple[float, float], - end: Tuple[float, float], + start: tuple[float, float], + end: tuple[float, float], radius: float, shape_filter: ShapeFilter, ) -> Optional[SegmentQueryInfo]: @@ -929,7 +917,7 @@ def segment_query_first( ) return None - def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> List[Shape]: + def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: """Query space to find all shapes near bb. The filter is applied to the query and follows the same rules as the @@ -944,7 +932,7 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> List[Shape]: :rtype: [:py:class:`Shape`] """ - query_hits: List[Shape] = [] + query_hits: list[Shape] = [] d = (self, query_hits) data = ffi.new_handle(d) @@ -954,7 +942,7 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> List[Shape]: ) return query_hits - def shape_query(self, shape: Shape) -> List[ShapeQueryInfo]: + def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]: """Query a space for any shapes overlapping the given shape .. Note:: @@ -966,7 +954,7 @@ def shape_query(self, shape: Shape) -> List[ShapeQueryInfo]: :rtype: [:py:class:`ShapeQueryInfo`] """ - query_hits: List[ShapeQueryInfo] = [] + query_hits: list[ShapeQueryInfo] = [] d = (self, query_hits) data = ffi.new_handle(d) @@ -1014,8 +1002,8 @@ def debug_draw(self, options: SpaceDebugDrawOptions) -> None: # """ # pass - def _get_arbiters(self) -> List[ffi.CData]: - _arbiters: List[ffi.CData] = [] + def _get_arbiters(self) -> list[ffi.CData]: + _arbiters: list[ffi.CData] = [] data = ffi.new_handle(_arbiters) cp.cpSpaceEachCachedArbiter(self._space, cp.ext_cpArbiterIteratorFunc, data) return _arbiters @@ -1040,7 +1028,7 @@ def __getstate__(self) -> _State: handlers = [] for k, v in self._handlers.items(): - h: Dict[str, Any] = {} + h: dict[str, Any] = {} if v._begin != CollisionHandler.do_nothing: h["_begin"] = v._begin if v._pre_solve != CollisionHandler.do_nothing: diff --git a/pymunk/space_debug_draw_options.py b/pymunk/space_debug_draw_options.py index c30c5416..9190bbef 100644 --- a/pymunk/space_debug_draw_options.py +++ b/pymunk/space_debug_draw_options.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence, Tuple, Type +from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence, Type if TYPE_CHECKING: from .shapes import Shape @@ -22,7 +22,7 @@ class SpaceDebugColor(NamedTuple): b: float a: float - def as_int(self) -> Tuple[int, int, int, int]: + def as_int(self) -> tuple[int, int, int, int]: """Return the color as a tuple of ints, where each value is rounded. >>> SpaceDebugColor(0, 51.1, 101.9, 255).as_int() @@ -30,7 +30,7 @@ def as_int(self) -> Tuple[int, int, int, int]: """ return round(self[0]), round(self[1]), round(self[2]), round(self[3]) - def as_float(self) -> Tuple[float, float, float, float]: + def as_float(self) -> tuple[float, float, float, float]: """Return the color as a tuple of floats, each value divided by 255. >>> SpaceDebugColor(0, 51, 102, 255).as_float() @@ -58,9 +58,9 @@ class SpaceDebugDrawOptions(object): Use on the flags property to control if constraints should be drawn or not. """ - DRAW_COLLISION_POINTS: ClassVar[ - _DrawFlags - ] = lib.CP_SPACE_DEBUG_DRAW_COLLISION_POINTS + DRAW_COLLISION_POINTS: ClassVar[_DrawFlags] = ( + lib.CP_SPACE_DEBUG_DRAW_COLLISION_POINTS + ) """Draw collision points. Use on the flags property to control if collision points should be drawn or diff --git a/pymunk/tests/__init__.py b/pymunk/tests/__init__.py index 71750792..26cf1d3e 100644 --- a/pymunk/tests/__init__.py +++ b/pymunk/tests/__init__.py @@ -1,9 +1,9 @@ """ The Pymunk test suite. -The tests cover most of Pymunk and is quick to run. However, some parts -requires an additional dependency to tests, e.g. to test pygame the pygame -library must be installed. +The tests cover most of Pymunk and is quick to run. However, some parts +requires an additional dependency to tests, e.g. to test pygame the pygame +library must be installed. Tests can be run both by running the module from a shell:: @@ -14,25 +14,25 @@ > from pymunk.tests import run_tests > run_tests() -Some arguments are allowed to the tests. You can show them with the --help +Some arguments are allowed to the tests. You can show them with the --help flag:: $> python pymunk.tests --help -It is possible to filter out tests with the filter parameter. Tests containing -the filter will be run, the others skipped. A special case is doctests, which +It is possible to filter out tests with the filter parameter. Tests containing +the filter will be run, the others skipped. A special case is doctests, which can be matched against the filter doctest:: $> python -m pymunk.tests -f testTransform $> python -m pymynk.tests -f doctest -By default all tests will run except those with an additional dependency. To +By default all tests will run except those with an additional dependency. To run tests with dependencies, specify them with the -d parameter:: $> python -m pymunk.tests -d pygame -Note that the tests covers most/all of Pymunk, but does not test the -underlying Chipmunk library in a significant way except as a side effect of +Note that the tests covers most/all of Pymunk, but does not test the +underlying Chipmunk library in a significant way except as a side effect of testing Pymunk features. """ @@ -44,12 +44,12 @@ import platform import sys import unittest -from typing import Any, Iterator, List +from typing import Any, Iterator from . import doctests -def run_tests(filter: str = "", with_dependencies: List[str] = []) -> bool: +def run_tests(filter: str = "", with_dependencies: list[str] = []) -> bool: """Run the Pymunk test suite.""" faulthandler.enable() diff --git a/pymunk/tests/doctests.py b/pymunk/tests/doctests.py index f66ddfd2..a36e3283 100644 --- a/pymunk/tests/doctests.py +++ b/pymunk/tests/doctests.py @@ -2,7 +2,6 @@ import pkgutil import sys import unittest -from typing import Any, List import pymunk @@ -11,7 +10,7 @@ def load_tests( - tests: unittest.TestSuite, dependencies: List[str] = [] + tests: unittest.TestSuite, dependencies: list[str] = [] ) -> unittest.TestSuite: for importer, modname, ispkg in pkgutil.iter_modules(pymunk.__path__): # try: diff --git a/pymunk/tests/test_autogeometry.py b/pymunk/tests/test_autogeometry.py index 08cfbd76..b2ca98f1 100644 --- a/pymunk/tests/test_autogeometry.py +++ b/pymunk/tests/test_autogeometry.py @@ -1,5 +1,4 @@ import unittest -from typing import List, Tuple import pymunk.autogeometry as a from pymunk.bb import BB @@ -26,32 +25,32 @@ def test_collect_segment(self) -> None: class UnitTestAutoGeometry(unittest.TestCase): def test_is_closed(self) -> None: - not_closed: List[Tuple[float, float]] = [(0, 0), (1, 1), (0, 1)] - closed: List[Tuple[float, float]] = [(0, 0), (1, 1), (0, 1), (0, 0)] + not_closed: list[tuple[float, float]] = [(0, 0), (1, 1), (0, 1)] + closed: list[tuple[float, float]] = [(0, 0), (1, 1), (0, 1), (0, 0)] self.assertFalse(a.is_closed(not_closed)) self.assertTrue(a.is_closed(closed)) def test_simplify_curves(self) -> None: - p1: List[Tuple[float, float]] = [(0, 0), (0, 10), (5, 11), (10, 10), (0, 10)] + p1: list[tuple[float, float]] = [(0, 0), (0, 10), (5, 11), (10, 10), (0, 10)] expected = [(0, 0), (0, 10), (10, 10), (0, 10)] actual = a.simplify_curves(p1, 1) self.assertEqual(actual, expected) def test_simplify_vertexes(self) -> None: - p1: List[Tuple[float, float]] = [(0, 0), (0, 10), (5, 11), (10, 10), (0, 10)] + p1: list[tuple[float, float]] = [(0, 0), (0, 10), (5, 11), (10, 10), (0, 10)] expected = [(0, 0), (0, 10), (10, 10), (0, 10)] actual = a.simplify_vertexes(p1, 1) self.assertEqual(actual, expected) def test_to_convex_hull(self) -> None: - p1: List[Tuple[float, float]] = [(0, 0), (0, 10), (5, 5), (10, 10), (10, 0)] + p1: list[tuple[float, float]] = [(0, 0), (0, 10), (5, 5), (10, 10), (10, 0)] expected = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)] actual = a.to_convex_hull(p1, 1) self.assertEqual(actual, expected) def test_convex_decomposition(self) -> None: # TODO: Use a more complicated polygon as test case - p1: List[Tuple[float, float]] = [ + p1: list[tuple[float, float]] = [ (0, 0), (5, 0), (10, 10), @@ -72,11 +71,11 @@ def test_convex_decomposition(self) -> None: # environments, so we cant have this assert here. # self.assertEqual(actual, expected) - open_poly: List[Tuple[float, float]] = [(0, 0), (10, 0), (10, 10)] + open_poly: list[tuple[float, float]] = [(0, 0), (10, 0), (10, 10)] with self.assertRaises(Exception): a.convex_decomposition(open_poly, 0.1) - wrong_winding: List[Tuple[float, float]] = [(0, 0), (10, 10), (10, 0), (0, 0)] + wrong_winding: list[tuple[float, float]] = [(0, 0), (10, 10), (10, 0), (0, 0)] with self.assertRaises(Exception): a.convex_decomposition(wrong_winding, 0.1) @@ -91,7 +90,7 @@ def test_march_soft(self) -> None: " xxxxx", ] - def sample_func(point: Tuple[float, float]) -> float: + def sample_func(point: tuple[float, float]) -> float: x = int(point[0]) y = int(point[1]) if img[y][x] == "x": @@ -134,7 +133,7 @@ def test_march_hard(self) -> None: " xxxxx", ] - def sample_func(point: Tuple[float, float]) -> float: + def sample_func(point: tuple[float, float]) -> float: x = int(point[0]) y = int(point[1]) if img[y][x] == "x": diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index 3e394a10..2fcf824e 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -1,6 +1,5 @@ import pickle import unittest -from typing import List, Tuple import pymunk as p from pymunk.arbiter import Arbiter @@ -266,9 +265,9 @@ def test_each_arbiters(self) -> None: s.add(b1, b2, c1, c2) s.step(1) - shapes: List[Shape] = [] + shapes: list[Shape] = [] - def f(arbiter: Arbiter, shapes: List[Shape]) -> None: + def f(arbiter: Arbiter, shapes: list[Shape]) -> None: shapes += arbiter.shapes b1.each_arbiter(f, shapes) diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index a395721f..528d7285 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -1,7 +1,7 @@ import gc import unittest import weakref -from typing import Any, List +from typing import Any import pymunk as p from pymunk._weakkeysview import WeakKeysView @@ -45,7 +45,7 @@ def testGC(self) -> None: _logger = logging.getLogger(__name__) - def make() -> List[Any]: + def make() -> list[Any]: s = p.Space() b1 = p.Body(1, 2) c1 = p.Circle(b1, 2) diff --git a/pymunk/transform.py b/pymunk/transform.py index 192f6e62..6a6feb6c 100644 --- a/pymunk/transform.py +++ b/pymunk/transform.py @@ -1,5 +1,5 @@ import math -from typing import NamedTuple, Tuple, Union, overload +from typing import NamedTuple, Union, overload from .vec2d import Vec2d @@ -50,20 +50,20 @@ class Transform(NamedTuple): ty: float = 0 @overload - def __matmul__(self, other: Tuple[float, float]) -> Vec2d: ... + def __matmul__(self, other: tuple[float, float]) -> Vec2d: ... @overload def __matmul__( - self, other: Tuple[float, float, float, float, float, float] + self, other: tuple[float, float, float, float, float, float] ) -> "Transform": ... def __matmul__( self, other: Union[ - Tuple[float, float], Tuple[float, float, float, float, float, float] + tuple[float, float], tuple[float, float, float, float, float, float] ], ) -> Union[Vec2d, "Transform"]: - """Multiply this transform with a Transform, Vec2d or Tuple of size 2 + """Multiply this transform with a Transform, Vec2d or tuple of size 2 or 6. diff --git a/pymunk/vec2d.py b/pymunk/vec2d.py index 0c7b8e66..0337a1aa 100644 --- a/pymunk/vec2d.py +++ b/pymunk/vec2d.py @@ -52,7 +52,7 @@ >>> len(Vec2d(1, 2)) 2 -The Vec2d supports many common opertions, for example addition and +The Vec2d supports many common opertions, for example addition and multiplication:: >>> Vec2d(7.3, 4.2) + Vec2d(1, 2) @@ -60,7 +60,7 @@ >>> Vec2d(7.3, 4.2) * 2 Vec2d(14.6, 8.4) -Vec2ds are immutable, meaning you cannot update them. But you can replace +Vec2ds are immutable, meaning you cannot update them. But you can replace them:: >>> v = Vec2d(1, 2) @@ -109,7 +109,7 @@ import numbers import operator import warnings -from typing import NamedTuple, Tuple +from typing import NamedTuple __all__ = ["Vec2d"] @@ -131,8 +131,8 @@ def __repr__(self) -> str: return "Vec2d(%s, %s)" % (self.x, self.y) # Addition - def __add__(self, other: Tuple[float, float]) -> "Vec2d": # type: ignore[override] - """Add a Vec2d with another Vec2d or Tuple of size 2. + def __add__(self, other: tuple[float, float]) -> "Vec2d": # type: ignore[override] + """Add a Vec2d with another Vec2d or tuple of size 2. >>> Vec2d(3, 4) + Vec2d(1, 2) Vec2d(4, 6) @@ -145,8 +145,8 @@ def __add__(self, other: Tuple[float, float]) -> "Vec2d": # type: ignore[overri return Vec2d(self.x + other[0], self.y + other[1]) - def __radd__(self, other: Tuple[float, float]) -> "Vec2d": - """Add a Tuple of size 2 with a Vec2d. + def __radd__(self, other: tuple[float, float]) -> "Vec2d": + """Add a tuple of size 2 with a Vec2d. >>> (1, 2) + Vec2d(3, 4) Vec2d(4, 6) @@ -154,8 +154,8 @@ def __radd__(self, other: Tuple[float, float]) -> "Vec2d": return self.__add__(other) # Subtraction - def __sub__(self, other: Tuple[float, float]) -> "Vec2d": - """Subtract a Vec2d with another Vec2d or Tuple of size 2. + def __sub__(self, other: tuple[float, float]) -> "Vec2d": + """Subtract a Vec2d with another Vec2d or tuple of size 2. >>> Vec2d(3, 4) - Vec2d(1, 2) Vec2d(2, 2) @@ -164,8 +164,8 @@ def __sub__(self, other: Tuple[float, float]) -> "Vec2d": """ return Vec2d(self.x - other[0], self.y - other[1]) - def __rsub__(self, other: Tuple[float, float]) -> "Vec2d": - """Subtract a Tuple of size 2 with a Vec2d. + def __rsub__(self, other: tuple[float, float]) -> "Vec2d": + """Subtract a tuple of size 2 with a Vec2d. >>> (1, 2) - Vec2d(3, 4) Vec2d(-2, -2) @@ -377,7 +377,7 @@ def angle_degrees(self) -> float: """ return math.degrees(self.angle) - def get_angle_between(self, other: Tuple[float, float]) -> float: + def get_angle_between(self, other: tuple[float, float]) -> float: """Get the angle between the vector and the other in radians. >>> '%.2f' % Vec2d(3, 0).get_angle_between(Vec2d(-1, 0)) @@ -420,7 +420,7 @@ def normalized(self) -> "Vec2d": return self / length return Vec2d(0.0, 0.0) - def normalized_and_length(self) -> Tuple["Vec2d", float]: + def normalized_and_length(self) -> tuple["Vec2d", float]: """Normalize the vector and return its length before the normalization. >>> Vec2d(3, 0).normalized_and_length() @@ -462,7 +462,7 @@ def perpendicular_normal(self) -> "Vec2d": return Vec2d(-self.y / length, self.x / length) return Vec2d(self.x, self.y) - def dot(self, other: Tuple[float, float]) -> float: + def dot(self, other: tuple[float, float]) -> float: """The dot product between the vector and other vector. v1.dot(v2) -> v1.x*v2.x + v1.y*v2.y @@ -474,7 +474,7 @@ def dot(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return float(self.x * other[0] + self.y * other[1]) - def get_distance(self, other: Tuple[float, float]) -> float: + def get_distance(self, other: tuple[float, float]) -> float: """The distance between the vector and other vector. >>> Vec2d(0, 2).get_distance((0, -3)) @@ -486,7 +486,7 @@ def get_distance(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return math.sqrt((self.x - other[0]) ** 2 + (self.y - other[1]) ** 2) - def get_distance_squared(self, other: Tuple[float, float]) -> float: + def get_distance_squared(self, other: tuple[float, float]) -> float: """The squared distance between the vector and other vector. It is more efficent to use this method than to call get_distance() first and then do a square() on the result. @@ -501,7 +501,7 @@ def get_distance_squared(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return (self.x - other[0]) ** 2 + (self.y - other[1]) ** 2 - def get_dist_sqrd(self, other: Tuple[float, float]) -> float: + def get_dist_sqrd(self, other: tuple[float, float]) -> float: """The squared distance between the vector and other vector. It is more efficent to use this method than to call get_distance() first and then do a square() on the result. @@ -524,7 +524,7 @@ def get_dist_sqrd(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return (self.x - other[0]) ** 2 + (self.y - other[1]) ** 2 - def projection(self, other: Tuple[float, float]) -> "Vec2d": + def projection(self, other: tuple[float, float]) -> "Vec2d": """Project this vector on top of other vector. >>> Vec2d(10, 1).projection((5.0, 0)) @@ -543,7 +543,7 @@ def projection(self, other: Tuple[float, float]) -> "Vec2d": new_length = self.dot(other) / other_length_sqrd return Vec2d(other[0] * new_length, other[1] * new_length) - def cross(self, other: Tuple[float, float]) -> float: + def cross(self, other: tuple[float, float]) -> float: """The cross product between the vector and the other. v1.cross(v2) -> v1.x*v2.y - v1.y*v2.x @@ -554,7 +554,7 @@ def cross(self, other: Tuple[float, float]) -> float: assert len(other) == 2 return self.x * other[1] - self.y * other[0] - def interpolate_to(self, other: Tuple[float, float], range: float) -> "Vec2d": + def interpolate_to(self, other: tuple[float, float], range: float) -> "Vec2d": """Vector interpolation between the current vector and another vector. >>> Vec2d(10,20).interpolate_to((20,-20), 0.1) @@ -566,7 +566,7 @@ def interpolate_to(self, other: Tuple[float, float], range: float) -> "Vec2d": ) def convert_to_basis( - self, x_vector: Tuple[float, float], y_vector: Tuple[float, float] + self, x_vector: tuple[float, float], y_vector: tuple[float, float] ) -> "Vec2d": """Convert the vector to a new basis defined by the given x and y vectors. @@ -580,7 +580,7 @@ def convert_to_basis( return Vec2d(x, y) @property - def int_tuple(self) -> Tuple[int, int]: + def int_tuple(self) -> tuple[int, int]: """The x and y values of this vector as a tuple of ints. Uses `round()` to round to closest int. @@ -590,7 +590,7 @@ def int_tuple(self) -> Tuple[int, int]: return round(self.x), round(self.y) @property - def polar_tuple(self) -> Tuple[float, float]: + def polar_tuple(self) -> tuple[float, float]: """Return this vector as polar coordinates (length, angle) See Vec2d.from_polar() for the inverse. @@ -649,14 +649,14 @@ def from_polar(length: float, angle: float) -> "Vec2d": return Vec2d(math.cos(angle) * length, math.sin(angle) * length) # Extra functions, mainly for chipmunk - def cpvrotate(self, other: Tuple[float, float]) -> "Vec2d": + def cpvrotate(self, other: tuple[float, float]) -> "Vec2d": """Use complex multiplication to rotate this vector by the other.""" assert len(other) == 2 return Vec2d( self.x * other[0] - self.y * other[1], self.x * other[1] + self.y * other[0] ) - def cpvunrotate(self, other: Tuple[float, float]) -> "Vec2d": + def cpvunrotate(self, other: tuple[float, float]) -> "Vec2d": """The inverse of cpvrotate.""" assert len(other) == 2 return Vec2d( From b557366268392c3dd41911b1b645cb10c47b290c Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 9 May 2025 22:52:06 +0200 Subject: [PATCH 57/80] Use type instead of typing.Type etc --- pymunk/_pyglet15_util.py | 4 ++-- pymunk/pyglet_util.py | 4 ++-- pymunk/space_debug_draw_options.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pymunk/_pyglet15_util.py b/pymunk/_pyglet15_util.py index d2da9044..c1d76580 100644 --- a/pymunk/_pyglet15_util.py +++ b/pymunk/_pyglet15_util.py @@ -34,7 +34,7 @@ import math import warnings -from typing import TYPE_CHECKING, Any, Optional, Sequence, Type +from typing import TYPE_CHECKING, Any, Optional, Sequence import pyglet # type: ignore @@ -108,7 +108,7 @@ def __enter__(self) -> None: def __exit__( self, - type: Optional[Type[BaseException]], + type: Optional[type[BaseException]], value: Optional[BaseException], traceback: Optional["TracebackType"], ) -> None: diff --git a/pymunk/pyglet_util.py b/pymunk/pyglet_util.py index 387ea306..7752b95f 100644 --- a/pymunk/pyglet_util.py +++ b/pymunk/pyglet_util.py @@ -33,7 +33,7 @@ __docformat__ = "reStructuredText" import math -from typing import TYPE_CHECKING, Any, Optional, Sequence, Type +from typing import TYPE_CHECKING, Any, Optional, Sequence import pyglet @@ -99,7 +99,7 @@ def __enter__(self) -> None: def __exit__( self, - type: Optional[Type[BaseException]], + type: Optional[type[BaseException]], value: Optional[BaseException], traceback: Optional["TracebackType"], ) -> None: diff --git a/pymunk/space_debug_draw_options.py b/pymunk/space_debug_draw_options.py index 9190bbef..a689b4d3 100644 --- a/pymunk/space_debug_draw_options.py +++ b/pymunk/space_debug_draw_options.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence, Type +from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence if TYPE_CHECKING: from .shapes import Shape @@ -188,7 +188,7 @@ def __enter__(self) -> None: def __exit__( self, - type: Optional[Type[BaseException]], + type: Optional[type[BaseException]], value: Optional[BaseException], traceback: Optional["TracebackType"], ) -> None: From b052012f9f3e8d139df637c67b6c874cfb1bbc22 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 9 May 2025 23:19:43 +0200 Subject: [PATCH 58/80] skip building for PyPy 3.9 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index de57a91d..c1481c6c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -25,7 +25,7 @@ jobs: uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BUILD: - "cp39-* cp310-* cp311-* cp312-* cp313-* pp39-* pp310-* pp311-*" + "cp39-* cp310-* cp311-* cp312-* cp313-* pp310-* pp311-*" # "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" CIBW_TEST_COMMAND: "python -m pymunk.tests" # CIBW_BUILD_VERBOSITY: 3 From df66852d23639eeadcba4ee83a344563286c8831 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Fri, 9 May 2025 23:39:59 +0200 Subject: [PATCH 59/80] Minor type cleanup --- pymunk/arbiter.py | 2 +- pymunk/shapes.py | 28 ++++++++++++++-------------- pymunk/space.py | 2 +- pymunk/tests/test_arbiter.py | 19 ++++++++----------- pymunk/tests/test_body.py | 2 +- pymunk/tests/test_shape.py | 2 +- pymunk/tests/test_space.py | 16 ++++++++-------- 7 files changed, 34 insertions(+), 37 deletions(-) diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 795f83d2..add66346 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -245,7 +245,7 @@ def _contact_to_dict(_contact: ffi.CData) -> dict[str, Any]: return d -def _contacts_from_dicts(ds: Sequence[dict[str, Any]]) -> dist[ffi.CData]: +def _contacts_from_dicts(ds: Sequence[dict[str, Any]]) -> ffi.CData: _contacts = lib.cpContactArrAlloc(len(ds)) for i in range(len(ds)): _contact = _contacts[i] diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 848e787a..516f6146 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -211,7 +211,7 @@ def _get_surface_velocity(self) -> Vec2d: v = cp.cpShapeGetSurfaceVelocity(self._shape) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, surface_v: Tuple[float, float]) -> None: + def _set_surface_velocity(self, surface_v: tuple[float, float]) -> None: assert len(surface_v) == 2 cp.cpShapeSetSurfaceVelocity(self._shape, surface_v) @@ -276,12 +276,12 @@ def bb(self) -> BB: _bb = cp.cpShapeGetBB(self._shape) return BB(_bb.l, _bb.b, _bb.r, _bb.t) - def point_query(self, p: Tuple[float, float]) -> PointQueryInfo: + def point_query(self, p: tuple[float, float]) -> PointQueryInfo: """Check if the given point lies within the shape. A negative distance means the point is within the shape. - :return: Tuple of (distance, info) + :return: tuple of (distance, info) :rtype: (float, :py:class:`PointQueryInfo`) """ assert len(p) == 2 @@ -298,7 +298,7 @@ def point_query(self, p: Tuple[float, float]) -> PointQueryInfo: ) def segment_query( - self, start: Tuple[float, float], end: Tuple[float, float], radius: float = 0 + self, start: tuple[float, float], end: tuple[float, float], radius: float = 0 ) -> Optional[SegmentQueryInfo]: """Check if the line segment from start to end intersects the shape. @@ -380,7 +380,7 @@ def __init__( self, body: Optional["Body"], radius: float, - offset: Tuple[float, float] = (0, 0), + offset: tuple[float, float] = (0, 0), ) -> None: """body is the body attach the circle to, offset is the offset from the body's center of gravity in body local coordinates. @@ -410,7 +410,7 @@ def radius(self) -> float: """The Radius of the circle.""" return cp.cpCircleShapeGetRadius(self._shape) - def unsafe_set_offset(self, o: Tuple[float, float]) -> None: + def unsafe_set_offset(self, o: tuple[float, float]) -> None: """Unsafe set the offset of the circle. .. note:: @@ -441,8 +441,8 @@ class Segment(Shape): def __init__( self, body: Optional["Body"], - a: Tuple[float, float], - b: Tuple[float, float], + a: tuple[float, float], + b: tuple[float, float], radius: float, ) -> None: """Create a Segment. @@ -476,7 +476,7 @@ def b(self) -> Vec2d: return Vec2d(v.x, v.y) def unsafe_set_endpoints( - self, a: Tuple[float, float], b: Tuple[float, float] + self, a: tuple[float, float], b: tuple[float, float] ) -> None: """Set the two endpoints for this segment. @@ -513,7 +513,7 @@ def radius(self) -> float: return cp.cpSegmentShapeGetRadius(self._shape) def set_neighbors( - self, prev: Tuple[float, float], next: Tuple[float, float] + self, prev: tuple[float, float], next: tuple[float, float] ) -> None: """When you have a number of segment shapes that are all joined together, things can still collide with the "cracks" between the @@ -534,7 +534,7 @@ class Poly(Shape): def __init__( self, body: Optional["Body"], - vertices: Sequence[Tuple[float, float]], + vertices: Sequence[tuple[float, float]], transform: Optional[Transform] = None, radius: float = 0, ) -> None: @@ -616,7 +616,7 @@ def radius(self) -> float: @staticmethod def create_box( - body: Optional["Body"], size: Tuple[float, float] = (10, 10), radius: float = 0 + body: Optional["Body"], size: tuple[float, float] = (10, 10), radius: float = 0 ) -> "Poly": """Convenience function to create a box with given width and height. @@ -665,7 +665,7 @@ def create_box_bb(body: Optional["Body"], bb: BB, radius: float = 0) -> "Poly": return self - def get_vertices(self) -> List[Vec2d]: + def get_vertices(self) -> list[Vec2d]: """Get the vertices in local coordinates for the polygon. If you need the vertices in world coordinates then the vertices can be @@ -696,7 +696,7 @@ def get_vertices(self) -> List[Vec2d]: def unsafe_set_vertices( self, - vertices: Sequence[Tuple[float, float]], + vertices: Sequence[tuple[float, float]], transform: Optional[Transform] = None, ) -> None: """Unsafe set the vertices of the poly. diff --git a/pymunk/space.py b/pymunk/space.py index f21d0eb9..d2459157 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -687,7 +687,7 @@ def add_wildcard_collision_handler(self, collision_type_a: int) -> CollisionHand self._handlers[collision_type_a] = ch return ch - def add_all_collision_handler(self) -> CollisionHandler: + def add_global_collision_handler(self) -> CollisionHandler: """Return a reference to the default collision handler or that is used to process all collisions that don't have a more specific handler. diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 54244c85..125589df 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -23,7 +23,7 @@ def testRestitution(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 @@ -51,7 +51,7 @@ def testFriction(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.friction, 0.18) arb.friction = 1 @@ -79,7 +79,7 @@ def testSurfaceVelocity(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.surface_velocity.x, 1.38461538462) self.assertAlmostEqual(arb.surface_velocity.y, -0.923076923077) @@ -105,7 +105,7 @@ def testContactPointSet(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # check inital values ps: p.ContactPointSet = arb.contact_point_set self.assertEqual(len(ps.points), 1) @@ -216,17 +216,15 @@ def testIsFirstContact(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(arb.is_first_contact) - return True s.add_collision_handler(1, 2).pre_solve = pre_solve1 s.step(0.1) - def pre_solve2(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertFalse(arb.is_first_contact) - return True s.add_collision_handler(1, 2).pre_solve = pre_solve2 @@ -243,7 +241,7 @@ def testNormal(self) -> None: s.add(b1, c1, c2) - def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.normal.x, 0.44721359) self.assertAlmostEqual(arb.normal.y, 0.89442719) @@ -313,14 +311,13 @@ def testShapesAndBodies(self) -> None: self.called = False - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called = True self.assertEqual(len(arb.shapes), 2) self.assertEqual(arb.shapes[0], c1) self.assertEqual(arb.shapes[1], c2) self.assertEqual(arb.bodies[0], arb.shapes[0].body) self.assertEqual(arb.bodies[1], arb.shapes[1].body) - return True s.add_collision_handler(1, 2).pre_solve = pre_solve diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index 2fcf824e..20b6ab10 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -398,5 +398,5 @@ def pf(body: p.Body, dt: float) -> None: body.pf = True -def vf(body: p.Body, gravity: Tuple[float, float], damping: float, dt: float) -> None: +def vf(body: p.Body, gravity: tuple[float, float], damping: float, dt: float) -> None: body.vf = True diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index 602326dd..fa9023ea 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -298,7 +298,7 @@ def testSegmentSegmentCollision(self) -> None: self.num_of_begins = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any): + def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.num_of_begins += 1 s.add_global_collision_handler().begin = begin diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index cfed9fa7..949ef3b2 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -509,7 +509,7 @@ def testCollisionHandlerBegin(self) -> None: self.hits = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any): + def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hits += h.data["test"] h = s.add_collision_handler(0, 0) @@ -532,7 +532,7 @@ def testCollisionHandlerPreSolve(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore d["test"] = data["test"] @@ -761,13 +761,13 @@ def testCollisionHandlerAddRemoveInStep(self) -> None: b = p.Body(1, 2) c = p.Circle(b, 2) - def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.add(b, c) space.add(c, b) self.assertTrue(b not in s.bodies) self.assertTrue(c not in s.shapes) - def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.remove(b, c) space.remove(c, b) self.assertTrue(b in s.bodies) @@ -791,7 +791,7 @@ def testCollisionHandlerRemoveInStep(self) -> None: self._setUp() s = self.s - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.remove(*arb.shapes) s.add_collision_handler(0, 0).pre_solve = pre_solve @@ -819,7 +819,7 @@ def testWildcardCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore @@ -847,7 +847,7 @@ def testDefaultCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore @@ -879,7 +879,7 @@ def callback( s.remove(shape) test_self.calls += 1 - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any): + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # note that we dont pass on the whole arbiters object, instead # we take only the shapes. space.add_post_step_callback(callback, 0, arb.shapes, test_self=self) From ff22c4704132ca3d34d6cf45cddb779529713271 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 13 May 2025 21:11:19 +0200 Subject: [PATCH 60/80] Fix in MANIFEST --- MANIFEST.in | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 1aacd420..5150f6ab 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,9 +19,9 @@ prune additional_examples/dist recursive-include pymunk/examples *.png recursive-include pymunk/examples *.wav #chipmunk src -recursive-include Chipmunk2D/src * -recursive-include Chipmunk2D/include/chipmunk * -include Chipmunk2D/*.txt +recursive-include Munk2D/src * +recursive-include Munk2D/include/chipmunk * +include Munk2D/*.txt recursive-include pymunk_cffi * #tools recursive-include tools *.py diff --git a/pyproject.toml b/pyproject.toml index 350964eb..e37fe765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ "setuptools", - "setuptools<74; platform_system=='Windows' and implementation_name=='pypy'", + # "setuptools<74; platform_system=='Windows' and implementation_name=='pypy'", "wheel", "cffi >= 1.17.1; platform_system != 'Emscripten'", "cffi > 1.14.0; platform_system == 'Emscripten'", From 89c757fa6d6ac14fa603950d75cf3e10e4d33cbc Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 18 May 2025 10:58:10 +0200 Subject: [PATCH 61/80] Include sensor shapes in result from all Space.query methods. #280 --- CHANGELOG.rst | 5 +++-- CITATION.cff | 2 +- Munk2D | 2 +- pymunk/_version.py | 2 +- pymunk/space.py | 18 +++++------------- pymunk/tests/test_space.py | 36 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 7 files changed, 48 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f306089..e56c668e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,13 +10,14 @@ TODO: Add note of new collision handler logic! This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! -Extra thanks for Github user aetle for a number of suggestions and feedback for this Pymunk release! +Extra thanks for Github user aatle for a number of suggestions and feedback for this Pymunk release! Changes: Breaking changes +- Unified all Space.query methods to include sensor shapes. Previsouly the nearest methods filtered them out. - Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, you can convert to list manually, like list(space.shapes). - At least one of the two bodies attached to constraint/joint must be dynamic. - Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. @@ -46,7 +47,7 @@ Other improvements - Improved documentation in many places (Vec2d, Poly, Shape and more) - Internal cleanup of code -Extra thanks for aetle for a number of suggestions for improvements in this pymunk release +Extra thanks for aatle for a number of suggestions for improvements in this Pymunk release diff --git a/CITATION.cff b/CITATION.cff index 257706bf..afb2cc18 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,6 +5,6 @@ authors: given-names: "Victor" title: "Pymunk" abstract: "A easy-to-use pythonic rigid body 2d physics library" -version: 6.11.1 +version: 7.0.0 date-released: 2025-02-09 url: "https://pymunk.org" diff --git a/Munk2D b/Munk2D index d5899e8e..bdc25b0c 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit d5899e8eed00cac26dc469b5fa22bf50f1266a7e +Subproject commit bdc25b0c4778d2001e261dfae68a07b33c3a0e40 diff --git a/pymunk/_version.py b/pymunk/_version.py index ee33cfd7..1d22d1f4 100644 --- a/pymunk/_version.py +++ b/pymunk/_version.py @@ -32,7 +32,7 @@ cp = _chipmunk_cffi.lib ffi = _chipmunk_cffi.ffi -version = "6.11.1" +version = "7.0.0" chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), diff --git a/pymunk/space.py b/pymunk/space.py index d2459157..93b54c46 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -762,13 +762,11 @@ def point_query( the point must be a under a certain depth within a shape to be considered a match. + Sensor shapes are included in the result. + See :py:class:`ShapeFilter` for details about how the shape_filter parameter can be used. - .. Note:: - Sensor shapes are included in the result (In - :py:meth:`Space.point_query_nearest` they are not) - :param point: Where to check for collision in the Space :type point: :py:class:`~vec2d.Vec2d` or (float,float) :param float max_distance: Match only within this distance @@ -806,8 +804,7 @@ def point_query_nearest( parameter can be used. .. Note:: - Sensor shapes are not included in the result (In - :py:meth:`Space.point_query` they are) + Sensor shapes are included in the result. (Changed in Pymunk 7.0) :param point: Where to check for collision in the Space :type point: :py:class:`~vec2d.Vec2d` or (float,float) @@ -849,9 +846,7 @@ def segment_query( See :py:class:`ShapeFilter` for details about how the shape_filter parameter can be used. - .. Note:: - Sensor shapes are included in the result (In - :py:meth:`Space.segment_query_first` they are not) + Sensor shapes are included in the result. :param start: Starting point :param end: End point @@ -892,8 +887,7 @@ def segment_query_first( collision detection. .. Note:: - Sensor shapes are not included in the result (In - :py:meth:`Space.segment_query` they are) + Sensor shapes are included in the result. (Changed in Pymunk 7.0) See :py:class:`ShapeFilter` for details about how the shape_filter parameter can be used. @@ -923,7 +917,6 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: The filter is applied to the query and follows the same rules as the collision detection. - .. Note:: Sensor shapes are included in the result :param bb: Bounding box @@ -945,7 +938,6 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]: """Query a space for any shapes overlapping the given shape - .. Note:: Sensor shapes are included in the result :param shape: Shape to query with diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 949ef3b2..fd3f8a6c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -339,6 +339,42 @@ def testStaticPointQueries(self) -> None: self.assertEqual(hits[0].shape, c) self._tearDown() + def testSensorQueries(self) -> None: + s = p.Space() + + b1 = p.Body(1, 1) + b1.position = 3, 0 + s1 = p.Circle(b1, 1) + s1.sensor = True + s.add(b1, s1) + + b2 = p.Body(1, 1) + b2.position = 6, 0 + s2 = p.Circle(b2, 1) + s.add(b2, s2) + + print() + print("Shapes", s1, s2) + r = s.bb_query(p.BB(0, 0, 10, 10), p.ShapeFilter()) + assert len(r), 2 + + r = s.point_query((0, 0), 10, p.ShapeFilter()) + assert len(r), 2 + + r = s.point_query_nearest((0, 0), 10, p.ShapeFilter()) + assert r != None + self.assertEqual(r.shape, s1) + + r = s.shape_query(p.Circle(p.Body(body_type=p.Body.KINEMATIC), 10)) + assert len(r), 2 + + r = s.segment_query((0, 0), (10, 0), 1, p.ShapeFilter()) + assert len(r), 2 + + r = s.segment_query_first((0, 0), (10, 0), 1, p.ShapeFilter()) + assert r != None + self.assertEqual(r.shape, s1) + def testReindexShape(self) -> None: s = p.Space() diff --git a/pyproject.toml b/pyproject.toml index e37fe765..598a0c1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "pymunk" -version = "6.11.1" # remember to change me for new versions! +version = "7.0.0" # remember to change me for new versions! # Require cffi >1.14.0 since that (and older) has problem with returing structs from functions. # Require cffi >= 1.17.1 since older cant work with latest setuptools version dependencies = [ From cf198b9f656667e4139bbd1e176ae9f0a5cbd83a Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 18 May 2025 17:59:45 +0200 Subject: [PATCH 62/80] wip collision handler rework --- .readthedocs.yaml | 10 +-- benchmarks/pymunk-collision-callback.py | 2 +- pymunk/space.py | 83 +++++++------------ pymunk/tests/test_arbiter.py | 20 +++-- pymunk/tests/test_shape.py | 2 +- pymunk/tests/test_space.py | 103 ++++++++++++++++++++---- pymunk_cffi/chipmunk_cdef.h | 2 +- pymunk_cffi/extensions.c | 19 ++++- 8 files changed, 150 insertions(+), 91 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3e80d603..bf333b78 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,17 +12,17 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/src/conf.py + configuration: docs/src/conf.py # Optionally build your docs in additional formats such as PDF formats: - - pdf + - pdf # Optionally set the version of Python and requirements required to build your docs python: - install: - - requirements: docs/requirements.txt + install: + - requirements: docs/requirements.txt # Do not include the Chipmunk2D submodule. It it not needed for docs. submodules: - exclude: all + exclude: all diff --git a/benchmarks/pymunk-collision-callback.py b/benchmarks/pymunk-collision-callback.py index 9c834eb1..07ed04c6 100644 --- a/benchmarks/pymunk-collision-callback.py +++ b/benchmarks/pymunk-collision-callback.py @@ -8,7 +8,7 @@ b = pymunk.Body(1,10) c = pymunk.Circle(b, 5) s.add(b, c) -h = s.add_global_collision_handler() +h = s.add_collision_handler(None, None) def f(arb, s, data): return False h.pre_solve = f diff --git a/pymunk/space.py b/pymunk/space.py index 93b54c46..bbd634d7 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -42,10 +42,10 @@ def __getitem__(self, key: Union[None, int, tuple[int, int]]) -> CollisionHandle if key in self._handlers: return self._handlers[key] if key == None: - self._handlers[None] = self.space.add_global_collision_handler() + self._handlers[None] = self.space.add_collision_handler(None, None) return self._handlers[None] elif isinstance(key, int): - self._handlers[key] = self.space.add_wildcard_collision_handler(key) + self._handlers[key] = self.space.add_collision_handler(key, None) return self._handlers[key] elif isinstance(key, tuple): assert isinstance(key, tuple) @@ -626,11 +626,13 @@ def collision_handlers( return self.collision_handlers def add_collision_handler( - self, collision_type_a: int, collision_type_b: int + self, collision_type_a: Optional[int], collision_type_b: Optional[int] ) -> CollisionHandler: """Return the :py:class:`CollisionHandler` for collisions between objects of type collision_type_a and collision_type_b. + Use None to indicate any collision_type. + Fill the desired collision callback functions, for details see the :py:class:`CollisionHandler` object. @@ -645,12 +647,27 @@ def add_collision_handler( :rtype: :py:class:`CollisionHandler` """ - key = min(collision_type_a, collision_type_b), max( - collision_type_a, collision_type_b - ) + # key = min(collision_type_a, collision_type_b), max( + # collision_type_a, collision_type_b + # ) + + if collision_type_a == None and collision_type_b != None: + collision_type_b, collision_type_a = collision_type_a, collision_type_b + + key = collision_type_a, collision_type_b if key in self._handlers: return self._handlers[key] + # if collision_type_a == None and collision_type_b == None: + # return self.add_global_collision_handler() + # CP_WILDCARD_COLLISION_TYPE + wildcard = int(ffi.cast("uintptr_t", ~0)) + if collision_type_a == None: + collision_type_a = wildcard + + if collision_type_b == None: + collision_type_b = wildcard + h = cp.cpSpaceAddCollisionHandler( self._space, collision_type_a, collision_type_b ) @@ -658,52 +675,6 @@ def add_collision_handler( self._handlers[key] = ch return ch - def add_wildcard_collision_handler(self, collision_type_a: int) -> CollisionHandler: - """Add a wildcard collision handler for given collision type. - - This handler will be used any time an object with this type collides - with another object, regardless of its type. A good example is a - projectile that should be destroyed the first time it hits anything. - There may be a specific collision handler and two wildcard handlers. - It's up to the specific handler to decide if and when to call the - wildcard handlers and what to do with their return values. - - When a new wildcard handler is created, the callbacks will all be - set to builtin callbacks that perform the default behavior. (accept - all collisions in :py:func:`~CollisionHandler.begin` and - :py:func:`~CollisionHandler.pre_solve`, or do nothing for - :py:func:`~CollisionHandler.post_solve` and - :py:func:`~CollisionHandler.separate`. - - :param int collision_type_a: Collision type - :rtype: :py:class:`CollisionHandler` - """ - - if collision_type_a in self._handlers: - return self._handlers[collision_type_a] - - h = cp.cpSpaceAddWildcardHandler(self._space, collision_type_a) - ch = CollisionHandler(h, self) - self._handlers[collision_type_a] = ch - return ch - - def add_global_collision_handler(self) -> CollisionHandler: - """Return a reference to the default collision handler or that is - used to process all collisions that don't have a more specific - handler. - - The default behavior for each of the callbacks is to call - the wildcard handlers, ANDing their return values together if - applicable. - """ - if None in self._handlers: - return self._handlers[None] - - _h = cp.cpSpaceAddGlobalCollisionHandler(self._space) - h = CollisionHandler(_h, self) - self._handlers[None] = h - return h - def add_post_step_callback( self, callback_function: Callable[ @@ -917,7 +888,7 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: The filter is applied to the query and follows the same rules as the collision detection. - Sensor shapes are included in the result + Sensor shapes are included in the result :param bb: Bounding box :param shape_filter: Shape filter @@ -938,7 +909,7 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]: """Query a space for any shapes overlapping the given shape - Sensor shapes are included in the result + Sensor shapes are included in the result :param shape: Shape to query with :type shape: :py:class:`Circle`, :py:class:`Poly` or :py:class:`Segment` @@ -1081,11 +1052,11 @@ def __setstate__(self, state: _State) -> None: elif k == "_handlers": for k2, hd in v: if k2 == None: - h = self.add_global_collision_handler() + h = self.add_collision_handler(None, None) elif isinstance(k2, tuple): h = self.add_collision_handler(k2[0], k2[1]) else: - h = self.add_wildcard_collision_handler(k2) + h = self.add_collision_handler(k2, None) if "_begin" in hd: h.begin = hd["_begin"] if "_pre_solve" in hd: diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 125589df..f08ec114 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -107,7 +107,7 @@ def testContactPointSet(self) -> None: def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # check inital values - ps: p.ContactPointSet = arb.contact_point_set + ps = arb.contact_point_set self.assertEqual(len(ps.points), 1) self.assertAlmostEqual(ps.normal.x, 0.8574929257) self.assertAlmostEqual(ps.normal.y, 0.5144957554) @@ -141,7 +141,7 @@ def f() -> None: self.assertRaises(Exception, f) - s.add_global_collision_handler().pre_solve = pre_solve + s.add_collision_handler(2, 1).pre_solve = pre_solve s.step(0.1) @@ -191,14 +191,17 @@ def testTotalKE(self) -> None: c2.friction = 0.8 s.add(b1, c1, b2, c2) + r = {} def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.assertAlmostEqual(arb.total_ke, 43.438914027) + r["ke"] = arb.total_ke s.add_collision_handler(1, 2).post_solve = post_solve s.step(0.1) + self.assertAlmostEqual(r["ke"], 43.438914027) + def testIsFirstContact(self) -> None: s = p.Space() s.gravity = 0, -100 @@ -237,18 +240,23 @@ def testNormal(self) -> None: b1 = p.Body(1, 30) b1.position = 5, 10 c1 = p.Circle(b1, 10) + c1.collision_type = 1 c2 = p.Circle(s.static_body, 10) + c2.collision_type = 2 s.add(b1, c1, c2) + r = {} def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.assertAlmostEqual(arb.normal.x, 0.44721359) - self.assertAlmostEqual(arb.normal.y, 0.89442719) + r["n"] = Vec2d(*arb.normal) - s.add_global_collision_handler().pre_solve = pre_solve1 + s.add_collision_handler(1, 2).pre_solve = pre_solve1 s.step(0.1) + self.assertAlmostEqual(r["n"].x, -0.44721359) + self.assertAlmostEqual(r["n"].y, -0.89442719) + def testIsRemoval(self) -> None: s = p.Space() s.gravity = 0, -100 diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index fa9023ea..1d11f9fe 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -301,7 +301,7 @@ def testSegmentSegmentCollision(self) -> None: def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.num_of_begins += 1 - s.add_global_collision_handler().begin = begin + s.add_collision_handler(None, None).begin = begin s.step(0.1) self.assertEqual(1, self.num_of_begins) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index fd3f8a6c..a1937b7c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1,6 +1,7 @@ from __future__ import with_statement import copy +import functools import io import pickle import sys @@ -634,7 +635,7 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.add_global_collision_handler().separate = separate + s.add_collision_handler(None, None).separate = separate s.step(1) s.remove(c1) @@ -653,7 +654,7 @@ def testCollisionHandlerDefaultCallbacks(self) -> None: s.add(b1, c1, b2, c2) s.gravity = 0, -100 - h = s.add_global_collision_handler() + h = s.add_collision_handler(None, None) h.begin = h.do_nothing h.pre_solve = h.do_nothing h.post_solve = h.do_nothing @@ -827,7 +828,7 @@ def testCollisionHandlerRemoveInStep(self) -> None: self._setUp() s = self.s - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: dict[Any, Any]) -> None: space.remove(*arb.shapes) s.add_collision_handler(0, 0).pre_solve = pre_solve @@ -838,12 +839,75 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(self.s2 not in s.shapes) self._tearDown() - def testCollisionHandlerKeyOrder(self) -> None: + def testCollisionHandlerOrder(self) -> None: s = p.Space() - h1 = s.add_collision_handler(1, 2) - h2 = s.add_collision_handler(2, 1) - self.assertEqual(h1, h2) + callback_calls = [] + + def callback( + name: str, + types: tuple[int, int], + arb: p.Arbiter, + space: p.Space, + data: dict[Any, Any], + ) -> None: + callback_calls.append((name, types)) + + handler_order = [ + (1, 2), + (2, 1), + (1, None), + (2, None), + (None, None), + ] + + for t1, t2 in handler_order: + h = s.add_collision_handler(t1, t2) + h.begin = functools.partial(callback, "begin", (t1, t2)) + h.pre_solve = functools.partial(callback, "pre_solve", (t1, t2)) + h.post_solve = functools.partial(callback, "post_solve", (t1, t2)) + h.separate = functools.partial(callback, "separate", (t1, t2)) + + b1 = p.Body(1, 30) + c1 = p.Circle(b1, 10) + b1.position = 5, 3 + c1.collision_type = 2 + c1.friction = 0.5 + + b2 = p.Body(body_type=p.Body.STATIC) + c2 = p.Circle(b2, 10) + c2.collision_type = 1 + c2.friction = 0.8 + + s.add(b1, c1, b2, c2) + + s.step(0.1) + b1.position = 100, 100 + s.step(0.1) + + expected_calls = [ + ("begin", (1, 2)), + ("begin", (2, 1)), + ("begin", (1, None)), + ("begin", (2, None)), + ("begin", (None, None)), + ("pre_solve", (1, 2)), + ("pre_solve", (2, 1)), + ("pre_solve", (1, None)), + ("pre_solve", (2, None)), + ("pre_solve", (None, None)), + ("post_solve", (1, 2)), + ("post_solve", (2, 1)), + ("post_solve", (1, None)), + ("post_solve", (2, None)), + ("post_solve", (None, None)), + ("separate", (1, 2)), + ("separate", (2, 1)), + ("separate", (1, None)), + ("separate", (2, None)), + ("separate", (None, None)), + ] + self.assertListEqual(callback_calls, expected_calls) def testWildcardCollisionHandler(self) -> None: s = p.Space() @@ -859,7 +923,12 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.add_wildcard_collision_handler(1).pre_solve = pre_solve + h1 = s.add_collision_handler(1, None) + h2 = s.add_collision_handler(None, 1) + + self.assertEqual(h1, h2) + h1.pre_solve = pre_solve + s.step(0.1) self.assertEqual({}, d) @@ -867,8 +936,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: c1.collision_type = 1 s.step(0.1) - self.assertEqual(c1, d["shapes"][0]) - self.assertEqual(c2, d["shapes"][1]) + self.assertEqual(c1, d["shapes"][1]) + self.assertEqual(c2, d["shapes"][0]) self.assertEqual(s, d["space"]) def testDefaultCollisionHandler(self) -> None: @@ -887,11 +956,11 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.add_global_collision_handler().pre_solve = pre_solve + s.add_collision_handler(None, None).pre_solve = pre_solve s.step(0.1) - self.assertEqual(c1, d["shapes"][1]) - self.assertEqual(c2, d["shapes"][0]) + self.assertEqual(c1, d["shapes"][0]) + self.assertEqual(c2, d["shapes"][1]) self.assertEqual(s, d["space"]) def testPostStepCallback(self) -> None: @@ -1030,10 +1099,10 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: j2 = PinJoint(s.static_body, b2) s.add(j1, j2) - h = s.add_global_collision_handler() + h = s.add_collision_handler(None, None) h.begin = f1 - h = s.add_wildcard_collision_handler(1) + h = s.add_collision_handler(1, None) h.pre_solve = f1 h = s.add_collision_handler(1, 2) @@ -1064,13 +1133,13 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: self.assertIn(s2.static_body, ja) # Assert collision handlers - h2 = s2.add_global_collision_handler() + h2 = s2.add_collision_handler(None, None) self.assertIsNotNone(h2.begin) self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) - h2 = s2.add_wildcard_collision_handler(1) + h2 = s2.add_collision_handler(1, None) self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.pre_solve) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) diff --git a/pymunk_cffi/chipmunk_cdef.h b/pymunk_cffi/chipmunk_cdef.h index a88bf705..b2a91029 100644 --- a/pymunk_cffi/chipmunk_cdef.h +++ b/pymunk_cffi/chipmunk_cdef.h @@ -107,7 +107,7 @@ struct cpArbiter cpVect n; // Regular, wildcard A and wildcard B collision handlers. - cpCollisionHandler *handler, *handlerA, *handlerB; + cpCollisionHandler *handlerAB, *handlerBA, *handlerA, *handlerB; cpBool swapped; cpTimestamp stamp; diff --git a/pymunk_cffi/extensions.c b/pymunk_cffi/extensions.c index 7970422e..a8b8d379 100644 --- a/pymunk_cffi/extensions.c +++ b/pymunk_cffi/extensions.c @@ -422,14 +422,25 @@ void cpSpaceAddCachedArbiter(cpSpace *space, cpArbiter *arb) // Set handlers to their defaults cpCollisionType typeA = a->type, typeB = b->type; - cpCollisionHandler *handler = arb->handler = cpSpaceLookupHandler(space, typeA, typeB); + //cpCollisionHandler *handler = arb->handler = cpSpaceLookupHandler(space, typeA, typeB); + arb->handlerAB = cpSpaceLookupHandler(space, typeA, typeB); + arb->handlerA = cpSpaceLookupHandler(space, typeA, CP_WILDCARD_COLLISION_TYPE); + + if (typeA != typeB){ + arb->handlerBA = cpSpaceLookupHandler(space, typeB, typeA); + arb->handlerB = cpSpaceLookupHandler(space, typeB, CP_WILDCARD_COLLISION_TYPE); + } else{ + arb->handlerBA = &cpCollisionHandlerDoNothing; + arb->handlerB = &cpCollisionHandlerDoNothing; + } + arb->swapped = (typeA != arb->handlerAB->typeA); // Check if the types match, but don't swap for a default handler which use the wildcard for type A. - cpBool swapped = arb->swapped = (typeA != handler->typeA && handler->typeA != CP_WILDCARD_COLLISION_TYPE); + //cpBool swapped = arb->swapped = (typeA != handler->typeA && handler->typeA != CP_WILDCARD_COLLISION_TYPE); // The order of the main handler swaps the wildcard handlers too. Uffda. - arb->handlerA = cpSpaceLookupHandler(space, (swapped ? typeB : typeA), CP_WILDCARD_COLLISION_TYPE); - arb->handlerB = cpSpaceLookupHandler(space, (swapped ? typeA : typeB), CP_WILDCARD_COLLISION_TYPE); + //arb->handlerA = cpSpaceLookupHandler(space, (swapped ? typeB : typeA), CP_WILDCARD_COLLISION_TYPE); + //arb->handlerB = cpSpaceLookupHandler(space, (swapped ? typeA : typeB), CP_WILDCARD_COLLISION_TYPE); // Update the arbiter's state cpArrayPush(space->arbiters, arb); From fe6393f3e2fb0f0a0f6659028a85940d77ec8305 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 18 May 2025 18:40:02 +0200 Subject: [PATCH 63/80] WIP fixing collision handler order --- Munk2D | 2 +- pymunk/tests/test_arbiter.py | 8 ++-- pymunk/tests/test_space.py | 78 +++++++++++++++++++----------------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/Munk2D b/Munk2D index bdc25b0c..d6a13d35 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit bdc25b0c4778d2001e261dfae68a07b33c3a0e40 +Subproject commit d6a13d352222ffe11e7707825613cb2bad87f4f9 diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index f08ec114..e64fc9c8 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -165,8 +165,8 @@ def testImpulse(self) -> None: self.post_solve_done = False def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.assertAlmostEqual(arb.total_impulse.x, 3.3936651583) - self.assertAlmostEqual(arb.total_impulse.y, 4.3438914027) + self.assertAlmostEqual(arb.total_impulse.x, -3.3936651583) + self.assertAlmostEqual(arb.total_impulse.y, -4.3438914027) self.post_solve_done = True s.add_collision_handler(1, 2).post_solve = post_solve @@ -254,8 +254,8 @@ def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: s.step(0.1) - self.assertAlmostEqual(r["n"].x, -0.44721359) - self.assertAlmostEqual(r["n"].y, -0.89442719) + self.assertAlmostEqual(r["n"].x, 0.44721359) + self.assertAlmostEqual(r["n"].y, 0.89442719) def testIsRemoval(self) -> None: s = p.Space() diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index a1937b7c..53894faa 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -354,27 +354,25 @@ def testSensorQueries(self) -> None: s2 = p.Circle(b2, 1) s.add(b2, s2) - print() - print("Shapes", s1, s2) - r = s.bb_query(p.BB(0, 0, 10, 10), p.ShapeFilter()) - assert len(r), 2 + r1 = s.bb_query(p.BB(0, 0, 10, 10), p.ShapeFilter()) + assert len(r1), 2 - r = s.point_query((0, 0), 10, p.ShapeFilter()) - assert len(r), 2 + r2 = s.point_query((0, 0), 10, p.ShapeFilter()) + assert len(r2), 2 - r = s.point_query_nearest((0, 0), 10, p.ShapeFilter()) - assert r != None - self.assertEqual(r.shape, s1) + r3 = s.point_query_nearest((0, 0), 10, p.ShapeFilter()) + assert r3 != None + self.assertEqual(r3.shape, s1) - r = s.shape_query(p.Circle(p.Body(body_type=p.Body.KINEMATIC), 10)) - assert len(r), 2 + r4 = s.shape_query(p.Circle(p.Body(body_type=p.Body.KINEMATIC), 10)) + assert len(r4), 2 - r = s.segment_query((0, 0), (10, 0), 1, p.ShapeFilter()) - assert len(r), 2 + r5 = s.segment_query((0, 0), (10, 0), 1, p.ShapeFilter()) + assert len(r5), 2 - r = s.segment_query_first((0, 0), (10, 0), 1, p.ShapeFilter()) - assert r != None - self.assertEqual(r.shape, s1) + r6 = s.segment_query_first((0, 0), (10, 0), 1, p.ShapeFilter()) + assert r6 != None + self.assertEqual(r6.shape, s1) def testReindexShape(self) -> None: s = p.Space() @@ -851,7 +849,13 @@ def callback( space: p.Space, data: dict[Any, Any], ) -> None: - callback_calls.append((name, types)) + callback_calls.append( + ( + name, + types, + (arb.shapes[0].collision_type, arb.shapes[1].collision_type), + ) + ) handler_order = [ (1, 2), @@ -886,26 +890,26 @@ def callback( s.step(0.1) expected_calls = [ - ("begin", (1, 2)), - ("begin", (2, 1)), - ("begin", (1, None)), - ("begin", (2, None)), - ("begin", (None, None)), - ("pre_solve", (1, 2)), - ("pre_solve", (2, 1)), - ("pre_solve", (1, None)), - ("pre_solve", (2, None)), - ("pre_solve", (None, None)), - ("post_solve", (1, 2)), - ("post_solve", (2, 1)), - ("post_solve", (1, None)), - ("post_solve", (2, None)), - ("post_solve", (None, None)), - ("separate", (1, 2)), - ("separate", (2, 1)), - ("separate", (1, None)), - ("separate", (2, None)), - ("separate", (None, None)), + ("begin", (1, 2), (1, 2)), + ("begin", (2, 1), (2, 1)), + ("begin", (1, None), (1, 2)), + ("begin", (2, None), (2, 1)), + ("begin", (None, None), (1, 2)), + ("pre_solve", (1, 2), (1, 2)), + ("pre_solve", (2, 1), (2, 1)), + ("pre_solve", (1, None), (1, 2)), + ("pre_solve", (2, None), (2, 1)), + ("pre_solve", (None, None), (1, 2)), + ("post_solve", (1, 2), (1, 2)), + ("post_solve", (2, 1), (2, 1)), + ("post_solve", (1, None), (1, 2)), + ("post_solve", (2, None), (2, 1)), + ("post_solve", (None, None), (1, 2)), + ("separate", (1, 2), (1, 2)), + ("separate", (2, 1), (2, 1)), + ("separate", (1, None), (1, 2)), + ("separate", (2, None), (2, 1)), + ("separate", (None, None), (1, 2)), ] self.assertListEqual(callback_calls, expected_calls) From 23061921884c3478f8a0e92e1eb4f623ab1cdb7a Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 18 May 2025 22:51:13 +0200 Subject: [PATCH 64/80] fix tests --- Munk2D | 2 +- pymunk/tests/test_arbiter.py | 10 +++++----- pymunk/tests/test_space.py | 24 ++++-------------------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/Munk2D b/Munk2D index d6a13d35..6dc890f3 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit d6a13d352222ffe11e7707825613cb2bad87f4f9 +Subproject commit 6dc890f3bba72cf24d3953ffa8762a4456403416 diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index e64fc9c8..bd174960 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -165,8 +165,8 @@ def testImpulse(self) -> None: self.post_solve_done = False def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.assertAlmostEqual(arb.total_impulse.x, -3.3936651583) - self.assertAlmostEqual(arb.total_impulse.y, -4.3438914027) + self.assertAlmostEqual(arb.total_impulse.x, 3.3936651583) + self.assertAlmostEqual(arb.total_impulse.y, 4.3438914027) self.post_solve_done = True s.add_collision_handler(1, 2).post_solve = post_solve @@ -254,8 +254,8 @@ def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: s.step(0.1) - self.assertAlmostEqual(r["n"].x, 0.44721359) - self.assertAlmostEqual(r["n"].y, 0.89442719) + self.assertAlmostEqual(r["n"].x, -0.44721359) + self.assertAlmostEqual(r["n"].y, -0.89442719) def testIsRemoval(self) -> None: s = p.Space() @@ -327,7 +327,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.bodies[0], arb.shapes[0].body) self.assertEqual(arb.bodies[1], arb.shapes[1].body) - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.add_collision_handler(1, 2).post_solve = pre_solve s.step(0.1) self.assertTrue(self.called) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 53894faa..e7afe89c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -269,14 +269,6 @@ def testPointQueryNearest(self) -> None: self.assertEqual(hit.distance, 1) self.assertEqual(hit.gradient, (1, 0)) - def testPointQueryNearestSensor(self) -> None: - s = p.Space() - c = p.Circle(s.static_body, 10) - c.sensor = True - s.add(c) - hit = s.point_query_nearest((0, 0), 100, p.ShapeFilter()) - self.assertEqual(hit, None) - def testBBQuery(self) -> None: s = p.Space() @@ -511,14 +503,6 @@ def testSegmentQueryFirst(self) -> None: hit = s.segment_query_first((-13, 50), (131, 50), 0, p.ShapeFilter()) self.assertEqual(hit, None) - def testSegmentQueryFirstSensor(self) -> None: - s = p.Space() - c = p.Circle(s.static_body, 10) - c.sensor = True - s.add(c) - hit = s.segment_query_first((-20, 0), (20, 0), 1, p.ShapeFilter()) - self.assertIsNone(hit) - def testStaticSegmentQueries(self) -> None: self._setUp() b = p.Body(body_type=p.Body.KINEMATIC) @@ -940,8 +924,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: c1.collision_type = 1 s.step(0.1) - self.assertEqual(c1, d["shapes"][1]) - self.assertEqual(c2, d["shapes"][0]) + self.assertEqual(c1, d["shapes"][0]) + self.assertEqual(c2, d["shapes"][1]) self.assertEqual(s, d["space"]) def testDefaultCollisionHandler(self) -> None: @@ -963,8 +947,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: s.add_collision_handler(None, None).pre_solve = pre_solve s.step(0.1) - self.assertEqual(c1, d["shapes"][0]) - self.assertEqual(c2, d["shapes"][1]) + self.assertEqual(c1, d["shapes"][1]) + self.assertEqual(c2, d["shapes"][0]) self.assertEqual(s, d["space"]) def testPostStepCallback(self) -> None: From 36fed33aafd6337a673300c56a6d0a5a6e27b620 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Wed, 21 May 2025 23:33:32 +0200 Subject: [PATCH 65/80] Fix processCollision --- Munk2D | 2 +- pymunk/tests/test_arbiter.py | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/Munk2D b/Munk2D index 6dc890f3..adabb581 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit 6dc890f3bba72cf24d3953ffa8762a4456403416 +Subproject commit adabb581793a0612837b9f22fbea85cd4702172e diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index bd174960..31799f6f 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -1,3 +1,4 @@ +import functools import unittest from typing import Any @@ -332,6 +333,101 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: s.step(0.1) self.assertTrue(self.called) + def testProcessCollision(self) -> None: + + def setup(): + s = p.Space() + + b1 = p.Body(1, 30) + c1 = p.Circle(b1, 10) + b1.position = 5, 3 + c1.collision_type = 1 + c1.friction = 0.5 + + b2 = p.Body(body_type=p.Body.STATIC) + c2 = p.Circle(b2, 10) + c2.collision_type = 2 + c2.friction = 0.8 + + s.add(b1, c1, b2, c2) + return s + + def callback( + name: str, + arb: p.Arbiter, + space: p.Space, + data: dict[Any, Any], + ) -> None: + # print("callback", name) # , arb.shapes) + expected_name, expected_process_collision = h.data["expected"].pop(0) + process_collision = h.data["process_values"].pop(0) + correct_call = ( + expected_process_collision == arb.process_collision + and expected_name == name + ) + if not correct_call: + print( + " Unexpected call:", + expected_name, + name, + expected_process_collision, + arb.process_collision, + ) + + h.data["result"].append(correct_call) + arb.process_collision = process_collision + # print(" arb.process_collision", process_collision) + + # test matrix: + # ("process_collision values to set in the callbacks", [callback name, process_collision value, callback name, ...]) + # 1: True, 0: False, _: not called. + + test_matrix = [ + ("111111", ["b", 1, "p", 1, "t", 1, "p", 1, "t", 1, "s", 1]), + ("111110", ["b", 1, "p", 1, "t", 1, "p", 1, "t", 1, "s", 1]), + ("111100", ["b", 1, "p", 1, "t", 1, "p", 1, "t", 1, "s", 0]), + ("11100_", ["b", 1, "p", 1, "t", 1, "p", 1, "s", 0]), + ("11000_", ["b", 1, "p", 1, "t", 1, "p", 0, "s", 0]), + ("1000__", ["b", 1, "p", 1, "p", 0, "s", 0]), + ("0000__", ["b", 1, "p", 0, "p", 0, "s", 0]), + ("011111", ["b", 1, "p", 0, "t", 1, "p", 1, "t", 1, "s", 1]), + ("00111_", ["b", 1, "p", 0, "p", 0, "t", 1, "s", 1]), + ("0001__", ["b", 1, "p", 0, "p", 0, "s", 0]), + ("0000__", ["b", 1, "p", 0, "p", 0, "s", 0]), + ("10100_", ["b", 1, "p", 1, "p", 0, "t", 1, "s", 0]), + ("010101", ["b", 1, "p", 0, "t", 1, "p", 0, "t", 1, "s", 0]), + ] + # print() + for process_values, expected_calls in test_matrix: + process_values = [ + bit == "1" for bit in process_values if bit in "01" + ] # will crash if bit is not 0 or 1. + + expected_calls.append(None) + expected_calls = list(zip(expected_calls[::2], expected_calls[1::2])) + # print("process_values, expected calls", process_values, expected_calls) + + s = setup() + h = s.add_collision_handler(1, 2) + h.data["process_values"] = process_values + h.data["expected"] = expected_calls + h.data["result"] = [] + + h.begin = functools.partial(callback, "b") + h.pre_solve = functools.partial(callback, "p") + h.post_solve = functools.partial(callback, "t") + h.separate = functools.partial(callback, "s") + + s.step(0.1) + s.step(0.1) + next(iter(s.bodies)).position = 100, 100 + s.step(0.1) + + # print(h.data) + # print(all(h.data["result"])) + self.assertTrue(all(h.data["result"])) + # print("done") + if __name__ == "__main__": print("testing pymunk version " + p.version) From aa5a7c01c9148a7a2a009d40243b2e586c6e5291 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Wed, 21 May 2025 23:36:17 +0200 Subject: [PATCH 66/80] fix processCollision in cpHastySpace --- Munk2D | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Munk2D b/Munk2D index adabb581..fc7ecea1 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit adabb581793a0612837b9f22fbea85cd4702172e +Subproject commit fc7ecea12aad22df30f89f7cfc0b6aa271f864ee From 3686ff60e4cbd8aa770f46768718d14e2e03b02d Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 22 May 2025 17:56:36 +0200 Subject: [PATCH 67/80] internal refactor - use lib instead of cp for cffi --- pymunk/space.py | 159 +++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 77 deletions(-) diff --git a/pymunk/space.py b/pymunk/space.py index bbd634d7..76cd054f 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -13,14 +13,11 @@ from . import _version from ._callbacks import * from ._chipmunk_cffi import ffi, lib - -cp = lib - from ._pickle import PickleMixin, _State from ._util import _dead_ref from .arbiter import _arbiter_from_dict, _arbiter_to_dict from .body import Body -from .collision_handler import CollisionHandler +from .collision_handler import CollisionHandler, _CollisionCallback from .query_info import PointQueryInfo, SegmentQueryInfo, ShapeQueryInfo from .shapes import Shape from .vec2d import Vec2d @@ -112,16 +109,18 @@ def __init__(self, threaded: bool = False) -> None: self.threaded = threaded and platform.system() != "Windows" if self.threaded: - cp_space = cp.cpHastySpaceNew() - freefunc = cp.cpHastySpaceFree + cp_space = lib.cpHastySpaceNew() + freefunc = lib.cpHastySpaceFree else: - cp_space = cp.cpSpaceNew() - freefunc = cp.cpSpaceFree + cp_space = lib.cpSpaceNew() + freefunc = lib.cpSpaceFree def spacefree(cp_space: ffi.CData) -> None: cp_shapes: list[Shape] = [] cp_shapes_h = ffi.new_handle(cp_shapes) - cp.cpSpaceEachShape(cp_space, lib.ext_cpSpaceShapeIteratorFunc, cp_shapes_h) + lib.cpSpaceEachShape( + cp_space, lib.ext_cpSpaceShapeIteratorFunc, cp_shapes_h + ) for cp_shape in cp_shapes: cp_space = lib.cpShapeGetSpace(cp_shape) @@ -131,7 +130,7 @@ def spacefree(cp_space: ffi.CData) -> None: cp_constraints: list[Constraint] = [] cp_constraints_h = ffi.new_handle(cp_constraints) - cp.cpSpaceEachConstraint( + lib.cpSpaceEachConstraint( cp_space, lib.ext_cpSpaceConstraintIteratorFunc, cp_constraints_h ) for cp_constraint in cp_constraints: @@ -140,7 +139,7 @@ def spacefree(cp_space: ffi.CData) -> None: cp_bodys: list[Body] = [] cp_bodys_h = ffi.new_handle(cp_bodys) - cp.cpSpaceEachBody(cp_space, lib.ext_cpSpaceBodyIteratorFunc, cp_bodys_h) + lib.cpSpaceEachBody(cp_space, lib.ext_cpSpaceBodyIteratorFunc, cp_bodys_h) for cp_body in cp_bodys: cp_space = lib.cpBodyGetSpace(cp_body) lib.cpSpaceRemoveBody(cp_space, cp_body) @@ -214,7 +213,7 @@ def constraints(self) -> KeysView[Constraint]: def _setup_static_body(self, static_body: Body) -> None: static_body._space = weakref.ref(self) - cp.cpSpaceAddBody(self._space, static_body._body) + lib.cpSpaceAddBody(self._space, static_body._body) @property def static_body(self) -> Body: @@ -228,7 +227,7 @@ def static_body(self) -> Body: self._setup_static_body(self._static_body) # self.add(self._static_body) - # b = cp.cpSpaceGetStaticBody(self._space) + # b = lib.cpSpaceGetStaticBody(self._space) # self._static_body = Body._init_with_body(b) # self._static_body._space = self # assert self._static_body is not None @@ -253,18 +252,18 @@ def iterations(self) -> int: accuracy of the physics. Pymunk's default of 10 iterations is sufficient for most simple games. """ - return cp.cpSpaceGetIterations(self._space) + return lib.cpSpaceGetIterations(self._space) @iterations.setter def iterations(self, value: int) -> None: - cp.cpSpaceSetIterations(self._space, value) + lib.cpSpaceSetIterations(self._space, value) def _set_gravity(self, gravity_vector: tuple[float, float]) -> None: assert len(gravity_vector) == 2 - cp.cpSpaceSetGravity(self._space, gravity_vector) + lib.cpSpaceSetGravity(self._space, gravity_vector) def _get_gravity(self) -> Vec2d: - v = cp.cpSpaceGetGravity(self._space) + v = lib.cpSpaceGetGravity(self._space) return Vec2d(v.x, v.y) gravity = property( @@ -286,11 +285,11 @@ def damping(self) -> float: second. Defaults to 1. Like gravity, it can be overridden on a per body basis. """ - return cp.cpSpaceGetDamping(self._space) + return lib.cpSpaceGetDamping(self._space) @damping.setter def damping(self, damping: float) -> None: - cp.cpSpaceSetDamping(self._space, damping) + lib.cpSpaceSetDamping(self._space, damping) @property def idle_speed_threshold(self) -> float: @@ -299,11 +298,11 @@ def idle_speed_threshold(self) -> float: The default value of 0 means the space estimates a good threshold based on gravity. """ - return cp.cpSpaceGetIdleSpeedThreshold(self._space) + return lib.cpSpaceGetIdleSpeedThreshold(self._space) @idle_speed_threshold.setter def idle_speed_threshold(self, idle_speed_threshold: float) -> None: - cp.cpSpaceSetIdleSpeedThreshold(self._space, idle_speed_threshold) + lib.cpSpaceSetIdleSpeedThreshold(self._space, idle_speed_threshold) @property def sleep_time_threshold(self) -> float: @@ -312,11 +311,11 @@ def sleep_time_threshold(self) -> float: The default value of `inf` disables the sleeping algorithm. """ - return cp.cpSpaceGetSleepTimeThreshold(self._space) + return lib.cpSpaceGetSleepTimeThreshold(self._space) @sleep_time_threshold.setter def sleep_time_threshold(self, sleep_time_threshold: float) -> None: - cp.cpSpaceSetSleepTimeThreshold(self._space, sleep_time_threshold) + lib.cpSpaceSetSleepTimeThreshold(self._space, sleep_time_threshold) @property def collision_slop(self) -> float: @@ -325,11 +324,11 @@ def collision_slop(self) -> float: To improve stability, set this as high as you can without noticeable overlapping. It defaults to 0.1. """ - return cp.cpSpaceGetCollisionSlop(self._space) + return lib.cpSpaceGetCollisionSlop(self._space) @collision_slop.setter def collision_slop(self, collision_slop: float) -> None: - cp.cpSpaceSetCollisionSlop(self._space, collision_slop) + lib.cpSpaceSetCollisionSlop(self._space, collision_slop) @property def collision_bias(self) -> float: @@ -348,11 +347,11 @@ def collision_bias(self) -> float: ..Note:: Very very few games will need to change this value. """ - return cp.cpSpaceGetCollisionBias(self._space) + return lib.cpSpaceGetCollisionBias(self._space) @collision_bias.setter def collision_bias(self, collision_bias: float) -> None: - cp.cpSpaceSetCollisionBias(self._space, collision_bias) + lib.cpSpaceSetCollisionBias(self._space, collision_bias) @property def collision_persistence(self) -> float: @@ -365,11 +364,11 @@ def collision_persistence(self) -> float: ..Note:: Very very few games will need to change this value. """ - return cp.cpSpaceGetCollisionPersistence(self._space) + return lib.cpSpaceGetCollisionPersistence(self._space) @collision_persistence.setter def collision_persistence(self, collision_persistence: float) -> None: - cp.cpSpaceSetCollisionPersistence(self._space, collision_persistence) + lib.cpSpaceSetCollisionPersistence(self._space, collision_persistence) @property def current_time_step(self) -> float: @@ -377,7 +376,7 @@ def current_time_step(self) -> float: Space.step()) or most recent (outside of a Space.step() call) timestep. """ - return cp.cpSpaceGetCurrentTimeStep(self._space) + return lib.cpSpaceGetCurrentTimeStep(self._space) def add(self, *objs: _AddableObjects) -> None: """Add one or many shapes, bodies or constraints (joints) to the space @@ -446,7 +445,7 @@ def _add_shape(self, shape: "Shape") -> None: shape._space = weakref.ref(self) self._shapes[shape] = None - cp.cpSpaceAddShape(self._space, shape._shape) + lib.cpSpaceAddShape(self._space, shape._shape) def _add_body(self, body: "Body") -> None: """Adds a body to the space""" @@ -456,7 +455,7 @@ def _add_body(self, body: "Body") -> None: body._space = weakref.ref(self) self._bodies[body] = None self._bodies_to_check.add(body) - cp.cpSpaceAddBody(self._space, body._body) + lib.cpSpaceAddBody(self._space, body._body) def _add_constraint(self, constraint: "Constraint") -> None: """Adds a constraint to the space""" @@ -468,7 +467,7 @@ def _add_constraint(self, constraint: "Constraint") -> None: ), "At leasts one of a constraint's bodies must be DYNAMIC." self._constraints[constraint] = None - cp.cpSpaceAddConstraint(self._space, constraint._constraint) + lib.cpSpaceAddConstraint(self._space, constraint._constraint) def _remove_shape(self, shape: "Shape") -> None: """Removes a shape from the space""" @@ -476,8 +475,8 @@ def _remove_shape(self, shape: "Shape") -> None: self._removed_shapes[shape] = None shape._space = _dead_ref # During GC at program exit sometimes the shape might already be removed. Then skip this step. - if cp.cpSpaceContainsShape(self._space, shape._shape): - cp.cpSpaceRemoveShape(self._space, shape._shape) + if lib.cpSpaceContainsShape(self._space, shape._shape): + lib.cpSpaceRemoveShape(self._space, shape._shape) del self._shapes[shape] def _remove_body(self, body: "Body") -> None: @@ -487,8 +486,8 @@ def _remove_body(self, body: "Body") -> None: if body in self._bodies_to_check: self._bodies_to_check.remove(body) # During GC at program exit sometimes the shape might already be removed. Then skip this step. - if cp.cpSpaceContainsBody(self._space, body._body): - cp.cpSpaceRemoveBody(self._space, body._body) + if lib.cpSpaceContainsBody(self._space, body._body): + lib.cpSpaceRemoveBody(self._space, body._body) del self._bodies[body] def _remove_constraint(self, constraint: "Constraint") -> None: @@ -498,25 +497,25 @@ def _remove_constraint(self, constraint: "Constraint") -> None: ), "constraint not in space, already removed?" # print("remove", constraint, constraint._constraint, self._constraints) # During GC at program exit sometimes the constraint might already be removed. Then skip this steip. - if cp.cpSpaceContainsConstraint(self._space, constraint._constraint): - cp.cpSpaceRemoveConstraint(self._space, constraint._constraint) + if lib.cpSpaceContainsConstraint(self._space, constraint._constraint): + lib.cpSpaceRemoveConstraint(self._space, constraint._constraint) del self._constraints[constraint] def reindex_shape(self, shape: Shape) -> None: """Update the collision detection data for a specific shape in the space. """ - cp.cpSpaceReindexShape(self._space, shape._shape) + lib.cpSpaceReindexShape(self._space, shape._shape) def reindex_shapes_for_body(self, body: Body) -> None: """Reindex all the shapes for a certain body.""" - cp.cpSpaceReindexShapesForBody(self._space, body._body) + lib.cpSpaceReindexShapesForBody(self._space, body._body) def reindex_static(self) -> None: """Update the collision detection info for the static shapes in the space. You only need to call this if you move one of the static shapes. """ - cp.cpSpaceReindexStatic(self._space) + lib.cpSpaceReindexStatic(self._space) @property def threads(self) -> int: @@ -529,13 +528,13 @@ def threads(self) -> int: support the threaded solver. """ if self.threaded: - return int(cp.cpHastySpaceGetThreads(self._space)) + return int(lib.cpHastySpaceGetThreads(self._space)) return 1 @threads.setter def threads(self, n: int) -> None: if self.threaded: - cp.cpHastySpaceSetThreads(self._space, n) + lib.cpHastySpaceSetThreads(self._space, n) def use_spatial_hash(self, dim: float, count: int) -> None: """Switch the space to use a spatial hash instead of the bounding box @@ -568,7 +567,7 @@ def use_spatial_hash(self, dim: float, count: int) -> None: :param dim: the size of the hash cells :param count: the suggested minimum number of cells in the hash table """ - cp.cpSpaceUseSpatialHash(self._space, dim, count) + lib.cpSpaceUseSpatialHash(self._space, dim, count) def step(self, dt: float) -> None: """Update the space for the given time step. @@ -602,9 +601,9 @@ def step(self, dt: float) -> None: try: self._locked = True if self.threaded: - cp.cpHastySpaceStep(self._space, dt) + lib.cpHastySpaceStep(self._space, dt) else: - cp.cpSpaceStep(self._space, dt) + lib.cpSpaceStep(self._space, dt) self._removed_shapes.clear() finally: self._locked = False @@ -626,7 +625,14 @@ def collision_handlers( return self.collision_handlers def add_collision_handler( - self, collision_type_a: Optional[int], collision_type_b: Optional[int] + self, + collision_type_a: Optional[int] = None, + collision_type_b: Optional[int] = None, + begin: Optional[_CollisionCallback] = None, + pre_step: Optional[_CollisionCallback] = None, + post_step: Optional[_CollisionCallback] = None, + separate: Optional[_CollisionCallback] = None, + data: Optional[Mapping] = None, ) -> CollisionHandler: """Return the :py:class:`CollisionHandler` for collisions between objects of type collision_type_a and collision_type_b. @@ -638,9 +644,11 @@ def add_collision_handler( Whenever shapes with collision types (:py:attr:`Shape.collision_type`) a and b collide, this handler will be used to process the collision - events. When a new collision handler is created, the callbacks will all be - set to builtin callbacks that perform the default behavior (call the - wildcard handlers, and accept all collisions). + events. If no handler is set, the default is to process collision as + normally. + + If multiple handlers match the collision, the order will be that the + most specific handler is called first. :param int collision_type_a: Collision type a :param int collision_type_b: Collision type b @@ -650,7 +658,6 @@ def add_collision_handler( # key = min(collision_type_a, collision_type_b), max( # collision_type_a, collision_type_b # ) - if collision_type_a == None and collision_type_b != None: collision_type_b, collision_type_a = collision_type_a, collision_type_b @@ -658,8 +665,6 @@ def add_collision_handler( if key in self._handlers: return self._handlers[key] - # if collision_type_a == None and collision_type_b == None: - # return self.add_global_collision_handler() # CP_WILDCARD_COLLISION_TYPE wildcard = int(ffi.cast("uintptr_t", ~0)) if collision_type_a == None: @@ -668,7 +673,7 @@ def add_collision_handler( if collision_type_b == None: collision_type_b = wildcard - h = cp.cpSpaceAddCollisionHandler( + h = lib.cpSpaceAddCollisionHandler( self._space, collision_type_a, collision_type_b ) ch = CollisionHandler(h, self) @@ -749,12 +754,12 @@ def point_query( query_hits: list[PointQueryInfo] = [] d = (self, query_hits) data = ffi.new_handle(d) - cp.cpSpacePointQuery( + lib.cpSpacePointQuery( self._space, point, max_distance, shape_filter, - cp.ext_cpSpacePointQueryFunc, + lib.ext_cpSpacePointQueryFunc, data, ) return query_hits @@ -786,7 +791,7 @@ def point_query_nearest( """ assert len(point) == 2 info = ffi.new("cpPointQueryInfo *") - _shape = cp.cpSpacePointQueryNearest( + _shape = lib.cpSpacePointQueryNearest( self._space, point, max_distance, shape_filter, info ) @@ -833,13 +838,13 @@ def segment_query( d = (self, query_hits) data = ffi.new_handle(d) - cp.cpSpaceSegmentQuery( + lib.cpSpaceSegmentQuery( self._space, start, end, radius, shape_filter, - cp.ext_cpSpaceSegmentQueryFunc, + lib.ext_cpSpaceSegmentQueryFunc, data, ) return query_hits @@ -868,7 +873,7 @@ def segment_query_first( assert len(start) == 2 assert len(end) == 2 info = ffi.new("cpSegmentQueryInfo *") - _shape = cp.cpSpaceSegmentQueryFirst( + _shape = lib.cpSpaceSegmentQueryFirst( self._space, start, end, radius, shape_filter, info ) @@ -901,8 +906,8 @@ def bb_query(self, bb: "BB", shape_filter: ShapeFilter) -> list[Shape]: d = (self, query_hits) data = ffi.new_handle(d) - cp.cpSpaceBBQuery( - self._space, bb, shape_filter, cp.ext_cpSpaceBBQueryFunc, data + lib.cpSpaceBBQuery( + self._space, bb, shape_filter, lib.ext_cpSpaceBBQueryFunc, data ) return query_hits @@ -921,8 +926,8 @@ def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]: d = (self, query_hits) data = ffi.new_handle(d) - cp.cpSpaceShapeQuery( - self._space, shape._shape, cp.ext_cpSpaceShapeQueryFunc, data + lib.cpSpaceShapeQuery( + self._space, shape._shape, lib.ext_cpSpaceShapeQueryFunc, data ) return query_hits @@ -951,7 +956,7 @@ def debug_draw(self, options: SpaceDebugDrawOptions) -> None: options._options.data = h with options: - cp.cpSpaceDebugDraw(self._space, options._options) + lib.cpSpaceDebugDraw(self._space, options._options) else: for shape in self.shapes: options.draw_shape(shape) @@ -968,7 +973,7 @@ def debug_draw(self, options: SpaceDebugDrawOptions) -> None: def _get_arbiters(self) -> list[ffi.CData]: _arbiters: list[ffi.CData] = [] data = ffi.new_handle(_arbiters) - cp.cpSpaceEachCachedArbiter(self._space, cp.ext_cpArbiterIteratorFunc, data) + lib.cpSpaceEachCachedArbiter(self._space, lib.ext_cpArbiterIteratorFunc, data) return _arbiters def __getstate__(self) -> _State: @@ -1005,11 +1010,11 @@ def __getstate__(self) -> _State: d["special"].append(("_handlers", handlers)) d["special"].append( - ("shapeIDCounter", cp.cpSpaceGetShapeIDCounter(self._space)) + ("shapeIDCounter", lib.cpSpaceGetShapeIDCounter(self._space)) ) - d["special"].append(("stamp", cp.cpSpaceGetTimestamp(self._space))) + d["special"].append(("stamp", lib.cpSpaceGetTimestamp(self._space))) d["special"].append( - ("currentTimeStep", cp.cpSpaceGetCurrentTimeStep(self._space)) + ("currentTimeStep", lib.cpSpaceGetCurrentTimeStep(self._space)) ) _arbs = self._get_arbiters() @@ -1034,14 +1039,14 @@ def __setstate__(self, state: _State) -> None: elif k == "bodies": self.add(*v) elif k == "_static_body": - # _ = cp.cpSpaceSetStaticBody(self._space, v._body) + # _ = lib.cpSpaceSetStaticBody(self._space, v._body) # v._space = self # self._static_body = v # print("setstate", v, self._static_body) self._static_body = v self._setup_static_body(v) # self._static_body._space = weakref.proxy(self) - # cp.cpSpaceAddBody(self._space, v._body) + # lib.cpSpaceAddBody(self._space, v._body) # self.add(v) elif k == "shapes": @@ -1066,13 +1071,13 @@ def __setstate__(self, state: _State) -> None: if "_separate" in hd: h.separate = hd["_separate"] elif k == "stamp": - cp.cpSpaceSetTimestamp(self._space, v) + lib.cpSpaceSetTimestamp(self._space, v) elif k == "shapeIDCounter": - cp.cpSpaceSetShapeIDCounter(self._space, v) + lib.cpSpaceSetShapeIDCounter(self._space, v) elif k == "currentTimeStep": - cp.cpSpaceSetCurrentTimeStep(self._space, v) + lib.cpSpaceSetCurrentTimeStep(self._space, v) elif k == "arbiters": for d in v: - # cp.cpSpaceTest(self._space) + # lib.cpSpaceTest(self._space) _arbiter = _arbiter_from_dict(d, self) - cp.cpSpaceAddCachedArbiter(self._space, _arbiter) + lib.cpSpaceAddCachedArbiter(self._space, _arbiter) From 1b4b88e50e3c58685dfbb4322c9902089a4f3ae1 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Thu, 22 May 2025 23:07:57 +0200 Subject: [PATCH 68/80] WIP Removed data from coll callbacks, made CollisionHandler private --- pymunk/_callbacks.py | 8 +-- pymunk/collision_handler.py | 4 +- pymunk/space.py | 69 ++++++++++++++------------ pymunk/tests/test_arbiter.py | 58 +++++++++++----------- pymunk/tests/test_space.py | 95 ++++++++++++++++-------------------- 5 files changed, 114 insertions(+), 120 deletions(-) diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index 575846c8..86434ea9 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -203,7 +203,7 @@ def ext_cpCollisionBeginFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._begin(Arbiter(_arb, handler._space), handler._space, handler.data) + handler._begin(Arbiter(_arb, handler._space), handler._space) @ffi.def_extern() @@ -211,7 +211,7 @@ def ext_cpCollisionPreSolveFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._pre_solve(Arbiter(_arb, handler._space), handler._space, handler.data) + handler._pre_solve(Arbiter(_arb, handler._space), handler._space) @ffi.def_extern() @@ -219,7 +219,7 @@ def ext_cpCollisionPostSolveFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._post_solve(Arbiter(_arb, handler._space), handler._space, handler.data) + handler._post_solve(Arbiter(_arb, handler._space), handler._space) @ffi.def_extern() @@ -234,7 +234,7 @@ def ext_cpCollisionSeparateFunc( # this try is needed since a separate callback will be called # if a colliding object is removed, regardless if its in a # step or not. Meaning the unlock must succeed - handler._separate(Arbiter(_arb, handler._space), handler._space, handler.data) + handler._separate(Arbiter(_arb, handler._space), handler._space) finally: handler._space._locked = orig_locked diff --git a/pymunk/collision_handler.py b/pymunk/collision_handler.py index f4c276de..8cee5ca5 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/collision_handler.py @@ -8,7 +8,7 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallback = Callable[[Arbiter, "Space", dict[Any, Any]], None] +_CollisionCallback = Callable[[Arbiter, "Space"], None] class CollisionHandler(object): @@ -149,7 +149,7 @@ def separate(self, func: _CollisionCallback) -> None: self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc @staticmethod - def do_nothing(arbiter: Arbiter, space: "Space", data: dict[Any, Any]) -> None: + def do_nothing(arbiter: Arbiter, space: "Space") -> None: """The default do nothing method used for the post_solve and seprate callbacks. diff --git a/pymunk/space.py b/pymunk/space.py index 76cd054f..9d682722 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -28,14 +28,15 @@ _AddableObjects = Union[Body, Shape, Constraint] -class Handlers(Mapping[Union[None, int, tuple[int, int]], CollisionHandler]): +class HandlersMapping(Mapping[Union[None, int, tuple[int, int]], CollisionHandler]): def __init__(self, space: "Space") -> None: self.space = space - _handlers: dict[Union[None, int, tuple[int, int]], CollisionHandler] = {} + self._handlers: dict[Union[None, int, tuple[int, int]], CollisionHandler] = {} def __getitem__(self, key: Union[None, int, tuple[int, int]]) -> CollisionHandler: + if key in self._handlers: return self._handlers[key] if key == None: @@ -44,7 +45,7 @@ def __getitem__(self, key: Union[None, int, tuple[int, int]]) -> CollisionHandle elif isinstance(key, int): self._handlers[key] = self.space.add_collision_handler(key, None) return self._handlers[key] - elif isinstance(key, tuple): + elif isinstance(key, tuple) and len(key) == 2: assert isinstance(key, tuple) self._handlers[key] = self.space.add_collision_handler(key[0], key[1]) return self._handlers[key] @@ -166,7 +167,7 @@ def spacefree(cp_space: ffi.CData) -> None: self._remove_later: set[_AddableObjects] = set() self._bodies_to_check: set[Body] = set() - self._collision_handlers = Handlers(self) + self._collision_handlers = HandlersMapping(self) @property def shapes(self) -> KeysView[Shape]: @@ -618,24 +619,16 @@ def step(self, dt: float) -> None: self._post_step_callbacks.clear() - @property - def collision_handlers( - self, - ) -> Mapping[Union[None, int, tuple[int, int]], CollisionHandler]: - return self.collision_handlers - - def add_collision_handler( + def set_collision_callbacks( self, collision_type_a: Optional[int] = None, collision_type_b: Optional[int] = None, begin: Optional[_CollisionCallback] = None, - pre_step: Optional[_CollisionCallback] = None, - post_step: Optional[_CollisionCallback] = None, + pre_solve: Optional[_CollisionCallback] = None, + post_solve: Optional[_CollisionCallback] = None, separate: Optional[_CollisionCallback] = None, - data: Optional[Mapping] = None, - ) -> CollisionHandler: - """Return the :py:class:`CollisionHandler` for collisions between - objects of type collision_type_a and collision_type_b. + ): + """Set callbacks that will be called during the 4 phases of collision handling. Use None to indicate any collision_type. @@ -650,6 +643,9 @@ def add_collision_handler( If multiple handlers match the collision, the order will be that the most specific handler is called first. + Note that if a handler already exist for the a,b pair, that existing + handler will be returned. + :param int collision_type_a: Collision type a :param int collision_type_b: Collision type b @@ -662,23 +658,32 @@ def add_collision_handler( collision_type_b, collision_type_a = collision_type_a, collision_type_b key = collision_type_a, collision_type_b - if key in self._handlers: - return self._handlers[key] - - # CP_WILDCARD_COLLISION_TYPE - wildcard = int(ffi.cast("uintptr_t", ~0)) - if collision_type_a == None: - collision_type_a = wildcard + if key not in self._handlers: + # CP_WILDCARD_COLLISION_TYPE + wildcard = int(ffi.cast("uintptr_t", ~0)) + if collision_type_a == None: + collision_type_a = wildcard - if collision_type_b == None: - collision_type_b = wildcard + if collision_type_b == None: + collision_type_b = wildcard - h = lib.cpSpaceAddCollisionHandler( - self._space, collision_type_a, collision_type_b - ) - ch = CollisionHandler(h, self) - self._handlers[key] = ch - return ch + h = lib.cpSpaceAddCollisionHandler( + self._space, collision_type_a, collision_type_b + ) + ch = CollisionHandler(h, self) + self._handlers[key] = ch + else: + ch = self._handlers[key] + + if begin != None: + ch.begin = begin + if pre_solve != None: + ch.pre_solve = pre_solve + if post_solve != None: + ch.post_solve = post_solve + if separate != None: + ch.separate = separate + return def add_post_step_callback( self, diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 31799f6f..67d6a66c 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -24,11 +24,11 @@ def testRestitution(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.set_collision_callbacks(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -52,11 +52,11 @@ def testFriction(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: self.assertEqual(arb.friction, 0.18) arb.friction = 1 - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.set_collision_callbacks(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -80,14 +80,14 @@ def testSurfaceVelocity(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: self.assertAlmostEqual(arb.surface_velocity.x, 1.38461538462) self.assertAlmostEqual(arb.surface_velocity.y, -0.923076923077) arb.surface_velocity = (10, 10) # TODO: add assert check that setting surface_velocity has any effect - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.set_collision_callbacks(1, 2, pre_solve=pre_solve) for x in range(5): s.step(0.1) @@ -106,7 +106,7 @@ def testContactPointSet(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: # check inital values ps = arb.contact_point_set self.assertEqual(len(ps.points), 1) @@ -142,7 +142,7 @@ def f() -> None: self.assertRaises(Exception, f) - s.add_collision_handler(2, 1).pre_solve = pre_solve + s.set_collision_callbacks(2, 1, pre_solve=pre_solve) s.step(0.1) @@ -165,12 +165,12 @@ def testImpulse(self) -> None: self.post_solve_done = False - def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def post_solve(arb: p.Arbiter, space: p.Space) -> None: self.assertAlmostEqual(arb.total_impulse.x, 3.3936651583) self.assertAlmostEqual(arb.total_impulse.y, 4.3438914027) self.post_solve_done = True - s.add_collision_handler(1, 2).post_solve = post_solve + s.set_collision_callbacks(1, 2, post_solve=post_solve) s.step(0.1) @@ -194,10 +194,10 @@ def testTotalKE(self) -> None: s.add(b1, c1, b2, c2) r = {} - def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def post_solve(arb: p.Arbiter, space: p.Space) -> None: r["ke"] = arb.total_ke - s.add_collision_handler(1, 2).post_solve = post_solve + s.set_collision_callbacks(1, 2, post_solve=post_solve) s.step(0.1) @@ -220,17 +220,17 @@ def testIsFirstContact(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve1(arb: p.Arbiter, space: p.Space) -> None: self.assertTrue(arb.is_first_contact) - s.add_collision_handler(1, 2).pre_solve = pre_solve1 + s.set_collision_callbacks(1, 2, pre_solve=pre_solve1) s.step(0.1) - def pre_solve2(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve2(arb: p.Arbiter, space: p.Space) -> None: self.assertFalse(arb.is_first_contact) - s.add_collision_handler(1, 2).pre_solve = pre_solve2 + s.set_collision_callbacks(1, 2, pre_solve=pre_solve2) s.step(0.1) @@ -248,10 +248,10 @@ def testNormal(self) -> None: s.add(b1, c1, c2) r = {} - def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve1(arb: p.Arbiter, space: p.Space) -> None: r["n"] = Vec2d(*arb.normal) - s.add_collision_handler(1, 2).pre_solve = pre_solve1 + s.set_collision_callbacks(1, 2, pre_solve=pre_solve1) s.step(0.1) @@ -277,11 +277,11 @@ def testIsRemoval(self) -> None: self.called1 = False - def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def separate1(arb: p.Arbiter, space: p.Space) -> None: self.called1 = True self.assertFalse(arb.is_removal) - s.add_collision_handler(1, 2).separate = separate1 + s.set_collision_callbacks(1, 2, separate=separate1) for x in range(10): s.step(0.1) @@ -292,11 +292,11 @@ def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called2 = False - def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def separate2(arb: p.Arbiter, space: p.Space) -> None: self.called2 = True self.assertTrue(arb.is_removal) - s.add_collision_handler(1, 2).separate = separate2 + s.set_collision_callbacks(1, 2, separate=separate2) s.remove(b1, c1) self.assertTrue(self.called2) @@ -320,7 +320,7 @@ def testShapesAndBodies(self) -> None: self.called = False - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: self.called = True self.assertEqual(len(arb.shapes), 2) self.assertEqual(arb.shapes[0], c1) @@ -328,7 +328,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.bodies[0], arb.shapes[0].body) self.assertEqual(arb.bodies[1], arb.shapes[1].body) - s.add_collision_handler(1, 2).post_solve = pre_solve + s.set_collision_callbacks(1, 2, post_solve=pre_solve) s.step(0.1) self.assertTrue(self.called) @@ -408,15 +408,15 @@ def callback( # print("process_values, expected calls", process_values, expected_calls) s = setup() - h = s.add_collision_handler(1, 2) + h = s.set_collision_callbacks(1, 2) h.data["process_values"] = process_values h.data["expected"] = expected_calls h.data["result"] = [] - h.begin = functools.partial(callback, "b") - h.pre_solve = functools.partial(callback, "p") - h.post_solve = functools.partial(callback, "t") - h.separate = functools.partial(callback, "s") + begin = functools.partial(callback, "b") + pre_solve = functools.partial(callback, "p") + post_solve = functools.partial(callback, "t") + separate = functools.partial(callback, "s") s.step(0.1) s.step(0.1) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index e7afe89c..6695dd9d 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -528,12 +528,10 @@ def testCollisionHandlerBegin(self) -> None: self.hits = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.hits += h.data["test"] + def begin(arb: p.Arbiter, space: p.Space) -> None: + self.hits += 1 - h = s.add_collision_handler(0, 0) - h.data["test"] = 1 - h.begin = begin + s.set_collision_callbacks(0, 0, begin=begin) for x in range(10): s.step(0.1) @@ -551,12 +549,12 @@ def testCollisionHandlerPreSolve(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore d["test"] = data["test"] - h = s.add_collision_handler(0, 1) + h = s.set_collision_callbacks(0, 1) h.data["test"] = 1 h.pre_solve = pre_solve s.step(0.1) @@ -569,10 +567,10 @@ def testCollisionHandlerPostSolve(self) -> None: self._setUp() self.hit = 0 - def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def post_solve(arb: p.Arbiter, space: p.Space) -> None: self.hit += 1 - self.s.add_collision_handler(0, 0).post_solve = post_solve + self.s.set_collision_callbacks(0, 0, post_solve=post_solve) self.s.step(0.1) self.assertEqual(self.hit, 1) self._tearDown() @@ -593,12 +591,10 @@ def testCollisionHandlerSeparate(self) -> None: self.separated = False - def separate(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.separated = data["test"] + def separate(arb: p.Arbiter, space: p.Space) -> None: + self.separated = True - h = s.add_collision_handler(0, 0) - h.data["test"] = True - h.separate = separate + s.set_collision_callbacks(0, 0, separate=separate) for x in range(10): s.step(0.1) @@ -617,7 +613,7 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.add_collision_handler(None, None).separate = separate + s.set_collision_callbacks(separate=separate) s.step(1) s.remove(c1) @@ -636,7 +632,7 @@ def testCollisionHandlerDefaultCallbacks(self) -> None: s.add(b1, c1, b2, c2) s.gravity = 0, -100 - h = s.add_collision_handler(None, None) + h = s.set_collision_callbacks(None, None) h.begin = h.do_nothing h.pre_solve = h.do_nothing h.post_solve = h.do_nothing @@ -673,15 +669,15 @@ def remove2(*_: Any) -> None: print("remove2") s.remove(shape2) - # s.add_collision_handler(1, 0).separate = remove2 - s.add_collision_handler(1, 0).separate = remove1 + # s.set_collision_callbacks(1, 0).separate = remove2 + s.set_collision_callbacks(1, 0, separate=remove1) s.step(0.001) # trigger separate with shape2 and shape3, shape1 will be removed 2x s.remove(shape1) - s.add_collision_handler(1, 0).separate = remove2 + s.set_collision_callbacks(1, 0, separate=remove2) s.add(shape1) s.step(1) @@ -711,7 +707,7 @@ def separate(*_: Any) -> None: pass s.step(1) - s.add_wildcard_collision_handler(0).separate = separate + s.set_collision_callbacks(0, separate=separate) s.remove(shape1) @@ -738,18 +734,18 @@ def testCollisionHandlerRemoveAfterSeparate(self) -> None: space.add(shape1, body2, shape2, shape3, body3) print("START", shape1, shape2, shape3) - def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: + def separate(arbiter: p.Arbiter, space: p.Space) -> None: print("SEP", arbiter.shapes) self.separate_occurred = True - def post_solve(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: + def post_solve(arbiter: p.Arbiter, space: p.Space) -> None: print("POST", arbiter.shapes) if self.separate_occurred: print("POST REMOVE", arbiter.shapes) space.remove(*arbiter.shapes) - space.add_collision_handler(1, 2).post_solve = post_solve - space.add_collision_handler(3, 2).separate = separate + space.set_collision_callbacks(1, 2, post_solve=post_solve) + space.set_collision_callbacks(3, 2, separate=separate) print(1) self.separate_occurred = False @@ -780,26 +776,26 @@ def testCollisionHandlerAddRemoveInStep(self) -> None: b = p.Body(1, 2) c = p.Circle(b, 2) - def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve_add(arb: p.Arbiter, space: p.Space) -> None: space.add(b, c) space.add(c, b) self.assertTrue(b not in s.bodies) self.assertTrue(c not in s.shapes) - def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve_remove(arb: p.Arbiter, space: p.Space) -> None: space.remove(b, c) space.remove(c, b) self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.add_collision_handler(0, 0).pre_solve = pre_solve_add + s.set_collision_callbacks(0, 0, pre_solve=pre_solve_add) s.step(0.1) return self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.add_collision_handler(0, 0).pre_solve = pre_solve_remove + s.set_collision_callbacks(0, 0).pre_solve = pre_solve_remove s.step(0.1) @@ -810,10 +806,10 @@ def testCollisionHandlerRemoveInStep(self) -> None: self._setUp() s = self.s - def pre_solve(arb: p.Arbiter, space: p.Space, data: dict[Any, Any]) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: space.remove(*arb.shapes) - s.add_collision_handler(0, 0).pre_solve = pre_solve + s.set_collision_callbacks(0, 0, pre_solve=pre_solve) s.step(0.1) @@ -850,7 +846,7 @@ def callback( ] for t1, t2 in handler_order: - h = s.add_collision_handler(t1, t2) + h = s.set_collision_callbacks(t1, t2) h.begin = functools.partial(callback, "begin", (t1, t2)) h.pre_solve = functools.partial(callback, "pre_solve", (t1, t2)) h.post_solve = functools.partial(callback, "post_solve", (t1, t2)) @@ -907,15 +903,12 @@ def testWildcardCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - h1 = s.add_collision_handler(1, None) - h2 = s.add_collision_handler(None, 1) - - self.assertEqual(h1, h2) - h1.pre_solve = pre_solve + s.set_collision_callbacks(1, None, pre_solve=pre_solve) + s.set_collision_callbacks(None, 1, pre_solve=pre_solve) s.step(0.1) @@ -940,11 +933,11 @@ def testDefaultCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.add_collision_handler(None, None).pre_solve = pre_solve + s.set_collision_callbacks(pre_solve=pre_solve) s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -972,12 +965,12 @@ def callback( s.remove(shape) test_self.calls += 1 - def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space) -> None: # note that we dont pass on the whole arbiters object, instead # we take only the shapes. space.add_post_step_callback(callback, 0, arb.shapes, test_self=self) - ch = s.add_collision_handler(0, 0).pre_solve = pre_solve + s.set_collision_callbacks(0, 0, pre_solve=pre_solve) s.step(0.1) self.assertEqual(len(s.shapes), 0) @@ -1087,17 +1080,13 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: j2 = PinJoint(s.static_body, b2) s.add(j1, j2) - h = s.add_collision_handler(None, None) - h.begin = f1 + s.set_collision_callbacks(begin=f1) - h = s.add_collision_handler(1, None) - h.pre_solve = f1 + s.set_collision_callbacks(1, pre_solve=f1) - h = s.add_collision_handler(1, 2) - h.post_solve = f1 + s.set_collision_callbacks(1, 2, post_solve=f1) - h = s.add_collision_handler(3, 4) - h.separate = f1 + s.set_collision_callbacks(3, 4, separate=f1) s2 = copy_func(s) @@ -1121,25 +1110,25 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: self.assertIn(s2.static_body, ja) # Assert collision handlers - h2 = s2.add_collision_handler(None, None) + h2 = s2.set_collision_callbacks(None, None) self.assertIsNotNone(h2.begin) self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) - h2 = s2.add_collision_handler(1, None) + h2 = s2.set_collision_callbacks(1, None) self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.pre_solve) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) - h2 = s2.add_collision_handler(1, 2) + h2 = s2.set_collision_callbacks(1, 2) self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertIsNotNone(h2.post_solve) self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) - h2 = s2.add_collision_handler(3, 4) + h2 = s2.set_collision_callbacks(3, 4) self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) From 359151756505e4c1beeca526ee5cc37b3696dd89 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sat, 24 May 2025 23:05:13 +0200 Subject: [PATCH 69/80] Readd data to collision callbacks, fix tests --- pymunk/__init__.py | 2 - pymunk/_callbacks.py | 14 +- ...ision_handler.py => _collision_handler.py} | 46 +++--- pymunk/examples/arrows.py | 8 +- pymunk/examples/balls_and_lines.py | 11 +- pymunk/examples/breakout.py | 26 ++-- pymunk/examples/collisions.py | 34 ++--- pymunk/examples/contact_and_no_flipy.py | 13 +- pymunk/examples/contact_with_friction.py | 15 +- pymunk/examples/deformable.py | 9 +- pymunk/examples/platformer.py | 14 +- pymunk/examples/playground.py | 5 +- pymunk/space.py | 127 +++++++++-------- pymunk/tests/test_arbiter.py | 84 +++++------ pymunk/tests/test_common.py | 4 +- pymunk/tests/test_shape.py | 2 +- pymunk/tests/test_space.py | 134 +++++++++--------- 17 files changed, 280 insertions(+), 268 deletions(-) rename pymunk/{collision_handler.py => _collision_handler.py} (76%) diff --git a/pymunk/__init__.py b/pymunk/__init__.py index b206e2bc..01355392 100644 --- a/pymunk/__init__.py +++ b/pymunk/__init__.py @@ -58,7 +58,6 @@ "ContactPoint", "ContactPointSet", "Arbiter", - "CollisionHandler", "BB", "ShapeFilter", "Transform", @@ -80,7 +79,6 @@ from .arbiter import Arbiter from .bb import BB from .body import Body -from .collision_handler import CollisionHandler from .constraints import * from .contact_point_set import ContactPoint, ContactPointSet from .query_info import PointQueryInfo, SegmentQueryInfo, ShapeQueryInfo diff --git a/pymunk/_callbacks.py b/pymunk/_callbacks.py index 86434ea9..a34c71c1 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -203,7 +203,7 @@ def ext_cpCollisionBeginFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._begin(Arbiter(_arb, handler._space), handler._space) + handler._begin(Arbiter(_arb, handler._space), handler._space, handler.data["begin"]) @ffi.def_extern() @@ -211,7 +211,9 @@ def ext_cpCollisionPreSolveFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._pre_solve(Arbiter(_arb, handler._space), handler._space) + handler._pre_solve( + Arbiter(_arb, handler._space), handler._space, handler.data["pre_solve"] + ) @ffi.def_extern() @@ -219,7 +221,9 @@ def ext_cpCollisionPostSolveFunc( _arb: ffi.CData, _space: ffi.CData, data: ffi.CData ) -> None: handler = ffi.from_handle(data) - handler._post_solve(Arbiter(_arb, handler._space), handler._space) + handler._post_solve( + Arbiter(_arb, handler._space), handler._space, handler.data["post_solve"] + ) @ffi.def_extern() @@ -234,7 +238,9 @@ def ext_cpCollisionSeparateFunc( # this try is needed since a separate callback will be called # if a colliding object is removed, regardless if its in a # step or not. Meaning the unlock must succeed - handler._separate(Arbiter(_arb, handler._space), handler._space) + handler._separate( + Arbiter(_arb, handler._space), handler._space, handler.data["separate"] + ) finally: handler._space._locked = orig_locked diff --git a/pymunk/collision_handler.py b/pymunk/_collision_handler.py similarity index 76% rename from pymunk/collision_handler.py rename to pymunk/_collision_handler.py index 8cee5ca5..1a31835b 100644 --- a/pymunk/collision_handler.py +++ b/pymunk/_collision_handler.py @@ -1,6 +1,6 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Optional if TYPE_CHECKING: from .space import Space @@ -8,7 +8,7 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallback = Callable[[Arbiter, "Space"], None] +_CollisionCallback = Callable[[Arbiter, "Space", Any], None] class CollisionHandler(object): @@ -41,10 +41,10 @@ def __init__(self, _handler: Any, space: "Space") -> None: self._handler.userData = self._userData self._space = space - self._begin: _CollisionCallback = CollisionHandler.do_nothing - self._pre_solve: _CollisionCallback = CollisionHandler.do_nothing - self._post_solve: _CollisionCallback = CollisionHandler.do_nothing - self._separate: _CollisionCallback = CollisionHandler.do_nothing + self._begin: Optional[_CollisionCallback] = None + self._pre_solve: Optional[_CollisionCallback] = None + self._post_solve: Optional[_CollisionCallback] = None + self._separate: Optional[_CollisionCallback] = None self._data: dict[Any, Any] = {} @@ -60,7 +60,7 @@ def data(self) -> dict[Any, Any]: return self._data @property - def begin(self) -> _CollisionCallback: + def begin(self) -> Optional[_CollisionCallback]: """Two shapes just started touching for the first time this step. ``func(arbiter, space, data)`` @@ -74,16 +74,16 @@ def begin(self) -> _CollisionCallback: return self._begin @begin.setter - def begin(self, func: _CollisionCallback) -> None: + def begin(self, func: Optional[_CollisionCallback]) -> None: self._begin = func - if self._begin == CollisionHandler.do_nothing: + if self._begin == None: self._handler.beginFunc = ffi.addressof(lib, "DoNothing") else: self._handler.beginFunc = lib.ext_cpCollisionBeginFunc @property - def pre_solve(self) -> _CollisionCallback: + def pre_solve(self) -> Optional[_CollisionCallback]: """Two shapes are touching during this step. ``func(arbiter, space, data)`` @@ -96,16 +96,16 @@ def pre_solve(self) -> _CollisionCallback: return self._pre_solve @pre_solve.setter - def pre_solve(self, func: _CollisionCallback) -> None: + def pre_solve(self, func: Optional[_CollisionCallback]) -> None: self._pre_solve = func - if self._pre_solve == CollisionHandler.do_nothing: + if self._pre_solve == None: self._handler.preSolveFunc = ffi.addressof(lib, "DoNothing") else: self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc @property - def post_solve(self) -> _CollisionCallback: + def post_solve(self) -> Optional[_CollisionCallback]: """Two shapes are touching and their collision response has been processed. @@ -118,16 +118,16 @@ def post_solve(self) -> _CollisionCallback: return self._post_solve @post_solve.setter - def post_solve(self, func: _CollisionCallback) -> None: + def post_solve(self, func: Optional[_CollisionCallback]) -> None: self._post_solve = func - if self._post_solve == CollisionHandler.do_nothing: + if self._post_solve == None: self._handler.postSolveFunc = ffi.addressof(lib, "DoNothing") else: self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc @property - def separate(self) -> _CollisionCallback: + def separate(self) -> Optional[_CollisionCallback]: """Two shapes have just stopped touching for the first time this step. @@ -140,20 +140,10 @@ def separate(self) -> _CollisionCallback: return self._separate @separate.setter - def separate(self, func: _CollisionCallback) -> None: + def separate(self, func: Optional[_CollisionCallback]) -> None: self._separate = func - if self._separate == CollisionHandler.do_nothing: + if self._separate == None: self._handler.separateFunc = ffi.addressof(lib, "DoNothing") else: self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc - - @staticmethod - def do_nothing(arbiter: Arbiter, space: "Space") -> None: - """The default do nothing method used for the post_solve and seprate - callbacks. - - Note that its more efficient to set this method than to define your own - do nothing method. - """ - return diff --git a/pymunk/examples/arrows.py b/pymunk/examples/arrows.py index 3cc0a10e..32d84caa 100644 --- a/pymunk/examples/arrows.py +++ b/pymunk/examples/arrows.py @@ -49,7 +49,7 @@ def post_solve_arrow_hit(arbiter, space, data): arrow_body, other_body, position, - data["flying_arrows"], + data, ) @@ -98,9 +98,9 @@ def main(): space.add(arrow_body, arrow_shape) flying_arrows: list[pymunk.Body] = [] - handler = space.add_collision_handler(0, 1) - handler.data["flying_arrows"] = flying_arrows - handler.post_solve = post_solve_arrow_hit + space.set_collision_callback( + 0, 1, post_solve=post_solve_arrow_hit, data=flying_arrows + ) start_time = 0 while running: diff --git a/pymunk/examples/balls_and_lines.py b/pymunk/examples/balls_and_lines.py index 25ada068..92e4f763 100644 --- a/pymunk/examples/balls_and_lines.py +++ b/pymunk/examples/balls_and_lines.py @@ -1,6 +1,5 @@ -"""This example lets you dynamically create static walls and dynamic balls +"""This example lets you dynamically create static walls and dynamic balls""" -""" __docformat__ = "reStructuredText" import pygame @@ -24,7 +23,7 @@ def mouse_coll_func(arbiter, space, data): """Simple callback that increases the radius of circles touching the mouse""" s1, s2 = arbiter.shapes s2.unsafe_set_radius(s2.radius + 0.15) - return False + arbiter.process_collision = False def main(): @@ -47,9 +46,9 @@ def main(): mouse_shape.collision_type = COLLTYPE_MOUSE space.add(mouse_body, mouse_shape) - space.add_collision_handler( - COLLTYPE_MOUSE, COLLTYPE_BALL - ).pre_solve = mouse_coll_func + space.set_collision_callback( + COLLTYPE_MOUSE, COLLTYPE_BALL, pre_solve=mouse_coll_func + ) ### Static line line_point1 = None diff --git a/pymunk/examples/breakout.py b/pymunk/examples/breakout.py index 61497e63..340e8e63 100644 --- a/pymunk/examples/breakout.py +++ b/pymunk/examples/breakout.py @@ -1,7 +1,7 @@ -"""Very simple breakout clone. A circle shape serves as the paddle, then -breakable bricks constructed of Poly-shapes. +"""Very simple breakout clone. A circle shape serves as the paddle, then +breakable bricks constructed of Poly-shapes. -The code showcases several pymunk concepts such as elasitcity, impulses, +The code showcases several pymunk concepts such as elasitcity, impulses, constant object speed, joints, collision handlers and post step callbacks. """ @@ -48,7 +48,7 @@ def constant_velocity(body, gravity, damping, dt): def setup_level(space, player_body): # Remove balls and bricks - for s in space.shapes[:]: + for s in list(space.shapes): if s.body.body_type == pymunk.Body.DYNAMIC and s.body not in [player_body]: space.remove(s.body, s) @@ -76,8 +76,9 @@ def remove_brick(arbiter, space, data): brick_shape = arbiter.shapes[0] space.remove(brick_shape, brick_shape.body) - h = space.add_collision_handler(collision_types["brick"], collision_types["ball"]) - h.separate = remove_brick + space.set_collision_callback( + collision_types["brick"], collision_types["ball"], separate=remove_brick + ) def main(): @@ -114,11 +115,10 @@ def main(): def remove_first(arbiter, space, data): ball_shape = arbiter.shapes[0] space.remove(ball_shape, ball_shape.body) - return True - h = space.add_collision_handler(collision_types["ball"], collision_types["bottom"]) - h.begin = remove_first - space.add(bottom) + space.set_collision_callback( + collision_types["ball"], collision_types["bottom"], begin=remove_first + ) ### Player ship player_body = pymunk.Body(500, float("inf")) @@ -142,10 +142,10 @@ def pre_solve(arbiter, space, data): set_.normal = normal set_.points[0].distance = 0 arbiter.contact_point_set = set_ - return True - h = space.add_collision_handler(collision_types["player"], collision_types["ball"]) - h.pre_solve = pre_solve + space.set_collision_callback( + collision_types["player"], collision_types["ball"], pre_solve=pre_solve + ) # restrict movement of player to a straigt line move_joint = pymunk.GrooveJoint( diff --git a/pymunk/examples/collisions.py b/pymunk/examples/collisions.py index bc6ea824..23237711 100644 --- a/pymunk/examples/collisions.py +++ b/pymunk/examples/collisions.py @@ -1,6 +1,6 @@ -"""This example attempts to display collision points, and the callbacks -""" +"""This example attempts to display collision points, and the callbacks""" +import functools import math import random import sys @@ -20,8 +20,6 @@ def begin(arbiter, space, data): "separate": 0, } - return True - def pre_solve(arbiter: pymunk.Arbiter, space, data): data["log"]["pre_solve"] += 1 @@ -54,8 +52,6 @@ def pre_solve(arbiter: pymunk.Arbiter, space, data): (p.point_a.interpolate_to(p.point_b, 0.5)), ) - return True - def post_solve(arbiter, space, data): # Will not be called, since the shapes are kinematic sensors @@ -64,7 +60,6 @@ def post_solve(arbiter, space, data): def separate(arbiter, space, data): data["log"]["separate"] += 1 - pass def main(): @@ -113,14 +108,19 @@ def main(): selected_shape_idx = 0 space.add(shapes[selected_shape_idx]) - h = space.add_collision_handler(0, 1) - h.data["screen"] = screen - h.data["log"] = {"begin": 0, "pre_solve": 0, "post_solve": 0, "separate": 0} - h.data["font"] = font - h.begin = begin - h.pre_solve = pre_solve - h.post_solve = post_solve - h.separate = separate + data = {} + h = space.set_collision_callback( + 0, + 1, + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + data=data, + ) + data["screen"] = screen + data["log"] = {"begin": 0, "pre_solve": 0, "post_solve": 0, "separate": 0} + data["font"] = font while True: for event in pygame.event.get(): @@ -155,10 +155,10 @@ def main(): ) y = 30 - for k in h.data["log"]: + for k in data["log"]: screen.blit( font.render( - f"{k}: {h.data['log'][k]}", + f"{k}: {data['log'][k]}", True, pygame.Color("black"), ), diff --git a/pymunk/examples/contact_and_no_flipy.py b/pymunk/examples/contact_and_no_flipy.py index e80f9948..f64987a8 100644 --- a/pymunk/examples/contact_and_no_flipy.py +++ b/pymunk/examples/contact_and_no_flipy.py @@ -1,5 +1,5 @@ -"""This example spawns (bouncing) balls randomly on a L-shape constructed of -two segment shapes. For each collision it draws a red circle with size +"""This example spawns (bouncing) balls randomly on a L-shape constructed of +two segment shapes. For each collision it draws a red circle with size depending on collision strength. Not interactive. """ @@ -9,15 +9,14 @@ import pygame import pymunk as pm -from pymunk import Vec2d -def draw_collision(arbiter, space, data): +def draw_collision(arbiter, space, surface): for c in arbiter.contact_point_set.points: r = max(3, abs(c.distance * 5)) r = int(r) p = tuple(map(int, c.point_a)) - pygame.draw.circle(data["surface"], pygame.Color("red"), p, r, 0) + pygame.draw.circle(surface, pygame.Color("red"), p, r, 0) def main(): @@ -43,9 +42,7 @@ def main(): ticks_to_next_ball = 10 - ch = space.add_collision_handler(0, 0) - ch.data["surface"] = screen - ch.post_solve = draw_collision + space.set_collision_callback(0, 0, post_solve=draw_collision, data=screen) while running: for event in pygame.event.get(): diff --git a/pymunk/examples/contact_with_friction.py b/pymunk/examples/contact_with_friction.py index 4efbe61d..bf9d8170 100644 --- a/pymunk/examples/contact_with_friction.py +++ b/pymunk/examples/contact_with_friction.py @@ -1,5 +1,5 @@ -"""This example spawns (bouncing) balls randomly on a L-shape constructed of -two segment shapes. Displays collsion strength and rotating balls thanks to +"""This example spawns (bouncing) balls randomly on a L-shape constructed of +two segment shapes. Displays collsion strength and rotating balls thanks to friction. Not interactive. """ @@ -10,18 +10,17 @@ import pymunk import pymunk.pygame_util -from pymunk import Vec2d pymunk.pygame_util.positive_y_is_up = True -def draw_collision(arbiter, space, data): +def draw_collision(arbiter, space, surface): for c in arbiter.contact_point_set.points: r = max(3, abs(c.distance * 5)) r = int(r) - p = pymunk.pygame_util.to_pygame(c.point_a, data["surface"]) - pygame.draw.circle(data["surface"], pygame.Color("black"), p, r, 1) + p = pymunk.pygame_util.to_pygame(c.point_a, surface) + pygame.draw.circle(surface, pygame.Color("black"), p, r, 1) def main(): @@ -53,9 +52,7 @@ def main(): ticks_to_next_ball = 10 - ch = space.add_collision_handler(0, 0) - ch.data["surface"] = screen - ch.post_solve = draw_collision + space.set_collision_callback(0, 0, post_solve=draw_collision, data=screen) while running: for event in pygame.event.get(): diff --git a/pymunk/examples/deformable.py b/pymunk/examples/deformable.py index ace96b02..3feb4bac 100644 --- a/pymunk/examples/deformable.py +++ b/pymunk/examples/deformable.py @@ -1,6 +1,7 @@ -"""This is an example on how the autogeometry can be used for deformable +"""This is an example on how the autogeometry can be used for deformable terrain. """ + __docformat__ = "reStructuredText" import sys @@ -29,7 +30,7 @@ def draw_helptext(screen): def generate_geometry(surface, space): - for s in space.shapes: + for s in list(space.shapes): if hasattr(s, "generated") and s.generated: space.remove(s) @@ -79,9 +80,9 @@ def main(): def pre_solve(arb, space, data): s = arb.shapes[0] space.remove(s.body, s) - return False + arb.process_collision = False - space.add_collision_handler(0, 1).pre_solve = pre_solve + space.set_collision_callback(0, 1, pre_solve=pre_solve) terrain_surface = pygame.Surface((600, 600)) terrain_surface.fill(pygame.Color("white")) diff --git a/pymunk/examples/platformer.py b/pymunk/examples/platformer.py index e7003a5e..9f949eff 100644 --- a/pymunk/examples/platformer.py +++ b/pymunk/examples/platformer.py @@ -3,15 +3,15 @@ The red girl sprite is taken from Sithjester's RMXP Resources: http://untamed.wild-refuge.net/rmxpresources.php?characters -.. note:: The code of this example is a bit messy. If you adapt this to your +.. note:: The code of this example is a bit messy. If you adapt this to your own code you might want to structure it a bit differently. """ __docformat__ = "reStructuredText" import math -import sys import os.path +import sys import pygame @@ -133,13 +133,13 @@ def main(): passthrough.filter = pymunk.ShapeFilter(categories=0b1000) space.add(passthrough) - def passthrough_handler(arbiter, space, data): - if arbiter.shapes[0].body.velocity.y < 0: - return True + def passthrough_handler(arbiter: pymunk.Arbiter, space: pymunk.Space, data): + if arbiter.bodies[0].velocity.y < 0: + arbiter.process_collision = True else: - return False + arbiter.process_collision = False - space.add_collision_handler(1, 2).begin = passthrough_handler + space.set_collision_callback(1, 2, begin=passthrough_handler) # player body = pymunk.Body(5, float("inf")) diff --git a/pymunk/examples/playground.py b/pymunk/examples/playground.py index 12212414..c299e6a3 100644 --- a/pymunk/examples/playground.py +++ b/pymunk/examples/playground.py @@ -1,7 +1,8 @@ -"""A basic playground. Most interesting function is draw a shape, basically -move the mouse as you want and pymunk will approximate a Poly shape from the +"""A basic playground. Most interesting function is draw a shape, basically +move the mouse as you want and pymunk will approximate a Poly shape from the drawing. """ + __docformat__ = "reStructuredText" import pygame diff --git a/pymunk/space.py b/pymunk/space.py index 9d682722..2240b169 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -3,8 +3,8 @@ import math import platform import weakref -from collections.abc import KeysView, Mapping -from typing import TYPE_CHECKING, Any, Callable, Hashable, Iterator, Optional, Union +from collections.abc import KeysView +from typing import TYPE_CHECKING, Any, Callable, Hashable, Optional, Union from pymunk.constraints import Constraint from pymunk.shape_filter import ShapeFilter @@ -13,11 +13,11 @@ from . import _version from ._callbacks import * from ._chipmunk_cffi import ffi, lib +from ._collision_handler import CollisionHandler, _CollisionCallback from ._pickle import PickleMixin, _State from ._util import _dead_ref -from .arbiter import _arbiter_from_dict, _arbiter_to_dict +from .arbiter import Arbiter, _arbiter_from_dict, _arbiter_to_dict from .body import Body -from .collision_handler import CollisionHandler, _CollisionCallback from .query_info import PointQueryInfo, SegmentQueryInfo, ShapeQueryInfo from .shapes import Shape from .vec2d import Vec2d @@ -28,37 +28,6 @@ _AddableObjects = Union[Body, Shape, Constraint] -class HandlersMapping(Mapping[Union[None, int, tuple[int, int]], CollisionHandler]): - - def __init__(self, space: "Space") -> None: - self.space = space - - self._handlers: dict[Union[None, int, tuple[int, int]], CollisionHandler] = {} - - def __getitem__(self, key: Union[None, int, tuple[int, int]]) -> CollisionHandler: - - if key in self._handlers: - return self._handlers[key] - if key == None: - self._handlers[None] = self.space.add_collision_handler(None, None) - return self._handlers[None] - elif isinstance(key, int): - self._handlers[key] = self.space.add_collision_handler(key, None) - return self._handlers[key] - elif isinstance(key, tuple) and len(key) == 2: - assert isinstance(key, tuple) - self._handlers[key] = self.space.add_collision_handler(key[0], key[1]) - return self._handlers[key] - else: - raise ValueError() - - def __len__(self) -> int: - return len(self._handlers) - - def __iter__(self) -> Iterator[Union[None, int, tuple[int, int]]]: - return iter(self._handlers) - - class Space(PickleMixin, object): """Spaces are the basic unit of simulation. You add rigid bodies, shapes and joints to it and then step them all forward together through time. @@ -167,8 +136,6 @@ def spacefree(cp_space: ffi.CData) -> None: self._remove_later: set[_AddableObjects] = set() self._bodies_to_check: set[Body] = set() - self._collision_handlers = HandlersMapping(self) - @property def shapes(self) -> KeysView[Shape]: """The shapes added to this space returned as a KeysView. @@ -619,7 +586,7 @@ def step(self, dt: float) -> None: self._post_step_callbacks.clear() - def set_collision_callbacks( + def set_collision_callback( self, collision_type_a: Optional[int] = None, collision_type_b: Optional[int] = None, @@ -627,7 +594,8 @@ def set_collision_callbacks( pre_solve: Optional[_CollisionCallback] = None, post_solve: Optional[_CollisionCallback] = None, separate: Optional[_CollisionCallback] = None, - ): + data: Any = None, + ) -> None: """Set callbacks that will be called during the 4 phases of collision handling. Use None to indicate any collision_type. @@ -675,14 +643,36 @@ def set_collision_callbacks( else: ch = self._handlers[key] - if begin != None: + if begin == Space.do_nothing: + ch.begin = None + elif begin != None: ch.begin = begin - if pre_solve != None: + ch.data["begin"] = data + if pre_solve == Space.do_nothing: + ch.pre_solve = None + elif pre_solve != None: ch.pre_solve = pre_solve - if post_solve != None: + ch.data["pre_solve"] = data + if post_solve == Space.do_nothing: + ch.post_solve = None + elif post_solve != None: ch.post_solve = post_solve - if separate != None: + ch.data["post_solve"] = data + if separate == Space.do_nothing: + ch.separate = None + elif separate != None: ch.separate = separate + ch.data["separate"] = data + return + + @staticmethod + def do_nothing(arbiter: Arbiter, space: "Space", data: Any) -> None: + """The default do nothing method used for the collision callbacks. + + Can be used to reset a collsion callback to its original do nothing + function. Note that its more efficient to use this method than to + define your own do nothing method. + """ return def add_post_step_callback( @@ -1002,13 +992,13 @@ def __getstate__(self) -> _State: handlers = [] for k, v in self._handlers.items(): h: dict[str, Any] = {} - if v._begin != CollisionHandler.do_nothing: + if v._begin != Space.do_nothing: h["_begin"] = v._begin - if v._pre_solve != CollisionHandler.do_nothing: + if v._pre_solve != Space.do_nothing: h["_pre_solve"] = v._pre_solve - if v._post_solve != CollisionHandler.do_nothing: + if v._post_solve != Space.do_nothing: h["_post_solve"] = v._post_solve - if v._separate != CollisionHandler.do_nothing: + if v._separate != Space.do_nothing: h["_separate"] = v._separate handlers.append((k, h)) @@ -1061,20 +1051,43 @@ def __setstate__(self, state: _State) -> None: self.add(*v) elif k == "_handlers": for k2, hd in v: - if k2 == None: - h = self.add_collision_handler(None, None) - elif isinstance(k2, tuple): - h = self.add_collision_handler(k2[0], k2[1]) - else: - h = self.add_collision_handler(k2, None) + begin = pre_solve = post_solve = separate = None if "_begin" in hd: - h.begin = hd["_begin"] + begin = hd["_begin"] if "_pre_solve" in hd: - h.pre_solve = hd["_pre_solve"] + pre_solve = hd["_pre_solve"] if "_post_solve" in hd: - h.post_solve = hd["_post_solve"] + post_solve = hd["_post_solve"] if "_separate" in hd: - h.separate = hd["_separate"] + separate = hd["_separate"] + if k2 == None: + self.set_collision_callback( + None, + None, + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + ) + elif isinstance(k2, tuple): + self.set_collision_callback( + k2[0], + k2[1], + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + ) + else: + self.set_collision_callback( + k2, + None, + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + ) + elif k == "stamp": lib.cpSpaceSetTimestamp(self._space, v) elif k == "shapeIDCounter": diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 67d6a66c..06d0669d 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -24,11 +24,11 @@ def testRestitution(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 - s.set_collision_callbacks(1, 2, pre_solve=pre_solve) + s.set_collision_callback(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -52,11 +52,11 @@ def testFriction(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.friction, 0.18) arb.friction = 1 - s.set_collision_callbacks(1, 2, pre_solve=pre_solve) + s.set_collision_callback(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -80,14 +80,14 @@ def testSurfaceVelocity(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.surface_velocity.x, 1.38461538462) self.assertAlmostEqual(arb.surface_velocity.y, -0.923076923077) arb.surface_velocity = (10, 10) # TODO: add assert check that setting surface_velocity has any effect - s.set_collision_callbacks(1, 2, pre_solve=pre_solve) + s.set_collision_callback(1, 2, pre_solve=pre_solve) for x in range(5): s.step(0.1) @@ -106,7 +106,7 @@ def testContactPointSet(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # check inital values ps = arb.contact_point_set self.assertEqual(len(ps.points), 1) @@ -142,7 +142,7 @@ def f() -> None: self.assertRaises(Exception, f) - s.set_collision_callbacks(2, 1, pre_solve=pre_solve) + s.set_collision_callback(2, 1, pre_solve=pre_solve) s.step(0.1) @@ -165,12 +165,12 @@ def testImpulse(self) -> None: self.post_solve_done = False - def post_solve(arb: p.Arbiter, space: p.Space) -> None: + def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.total_impulse.x, 3.3936651583) self.assertAlmostEqual(arb.total_impulse.y, 4.3438914027) self.post_solve_done = True - s.set_collision_callbacks(1, 2, post_solve=post_solve) + s.set_collision_callback(1, 2, post_solve=post_solve) s.step(0.1) @@ -194,10 +194,10 @@ def testTotalKE(self) -> None: s.add(b1, c1, b2, c2) r = {} - def post_solve(arb: p.Arbiter, space: p.Space) -> None: + def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: r["ke"] = arb.total_ke - s.set_collision_callbacks(1, 2, post_solve=post_solve) + s.set_collision_callback(1, 2, post_solve=post_solve) s.step(0.1) @@ -220,17 +220,17 @@ def testIsFirstContact(self) -> None: s.add(b1, c1, b2, c2) - def pre_solve1(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(arb.is_first_contact) - s.set_collision_callbacks(1, 2, pre_solve=pre_solve1) + s.set_collision_callback(1, 2, pre_solve=pre_solve1) s.step(0.1) - def pre_solve2(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertFalse(arb.is_first_contact) - s.set_collision_callbacks(1, 2, pre_solve=pre_solve2) + s.set_collision_callback(1, 2, pre_solve=pre_solve2) s.step(0.1) @@ -248,10 +248,10 @@ def testNormal(self) -> None: s.add(b1, c1, c2) r = {} - def pre_solve1(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: r["n"] = Vec2d(*arb.normal) - s.set_collision_callbacks(1, 2, pre_solve=pre_solve1) + s.set_collision_callback(1, 2, pre_solve=pre_solve1) s.step(0.1) @@ -277,11 +277,11 @@ def testIsRemoval(self) -> None: self.called1 = False - def separate1(arb: p.Arbiter, space: p.Space) -> None: + def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called1 = True self.assertFalse(arb.is_removal) - s.set_collision_callbacks(1, 2, separate=separate1) + s.set_collision_callback(1, 2, separate=separate1) for x in range(10): s.step(0.1) @@ -292,11 +292,11 @@ def separate1(arb: p.Arbiter, space: p.Space) -> None: self.called2 = False - def separate2(arb: p.Arbiter, space: p.Space) -> None: + def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called2 = True self.assertTrue(arb.is_removal) - s.set_collision_callbacks(1, 2, separate=separate2) + s.set_collision_callback(1, 2, separate=separate2) s.remove(b1, c1) self.assertTrue(self.called2) @@ -320,7 +320,7 @@ def testShapesAndBodies(self) -> None: self.called = False - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called = True self.assertEqual(len(arb.shapes), 2) self.assertEqual(arb.shapes[0], c1) @@ -328,14 +328,14 @@ def pre_solve(arb: p.Arbiter, space: p.Space) -> None: self.assertEqual(arb.bodies[0], arb.shapes[0].body) self.assertEqual(arb.bodies[1], arb.shapes[1].body) - s.set_collision_callbacks(1, 2, post_solve=pre_solve) + s.set_collision_callback(1, 2, post_solve=pre_solve) s.step(0.1) self.assertTrue(self.called) def testProcessCollision(self) -> None: - def setup(): + def setup() -> p.Space: s = p.Space() b1 = p.Body(1, 30) @@ -359,8 +359,8 @@ def callback( data: dict[Any, Any], ) -> None: # print("callback", name) # , arb.shapes) - expected_name, expected_process_collision = h.data["expected"].pop(0) - process_collision = h.data["process_values"].pop(0) + expected_name, expected_process_collision = data["expected"].pop(0) + process_collision = data["process_values"].pop(0) correct_call = ( expected_process_collision == arb.process_collision and expected_name == name @@ -374,7 +374,7 @@ def callback( arb.process_collision, ) - h.data["result"].append(correct_call) + data["result"].append(correct_call) arb.process_collision = process_collision # print(" arb.process_collision", process_collision) @@ -398,9 +398,9 @@ def callback( ("010101", ["b", 1, "p", 0, "t", 1, "p", 0, "t", 1, "s", 0]), ] # print() - for process_values, expected_calls in test_matrix: + for process_values_str, expected_calls in test_matrix: process_values = [ - bit == "1" for bit in process_values if bit in "01" + bit == "1" for bit in process_values_str if bit in "01" ] # will crash if bit is not 0 or 1. expected_calls.append(None) @@ -408,15 +408,19 @@ def callback( # print("process_values, expected calls", process_values, expected_calls) s = setup() - h = s.set_collision_callbacks(1, 2) - h.data["process_values"] = process_values - h.data["expected"] = expected_calls - h.data["result"] = [] - - begin = functools.partial(callback, "b") - pre_solve = functools.partial(callback, "p") - post_solve = functools.partial(callback, "t") - separate = functools.partial(callback, "s") + hdata = {} + hdata["process_values"] = process_values + hdata["expected"] = expected_calls + hdata["result"] = [] + s.set_collision_callback( + 1, + 2, + begin=functools.partial(callback, "b"), + pre_solve=functools.partial(callback, "p"), + post_solve=functools.partial(callback, "t"), + separate=functools.partial(callback, "s"), + data=hdata, + ) s.step(0.1) s.step(0.1) @@ -425,7 +429,7 @@ def callback( # print(h.data) # print(all(h.data["result"])) - self.assertTrue(all(h.data["result"])) + self.assertTrue(all(hdata["result"])) # print("done") diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index 528d7285..2257f94d 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -126,7 +126,7 @@ def remove_first(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: # space.add_post_step_callback(space.remove, first_shape, first_shape.body) # space.remove(c1) - space.add_collision_handler(2, 0).separate = remove_first + space.set_collision_callback(2, 0, separate=remove_first) # print(1) space.step(1.0 / 60) # print(2) @@ -166,7 +166,7 @@ def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: # space.add_post_step_callback(space.remove, first_shape, first_shape.body) # space.remove(c1) - space.add_collision_handler(2, 0).separate = separate + space.set_collision_callback(2, 0, separate=separate) # print(1) space.step(1) # print(2) diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index 1d11f9fe..77b2ba9f 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -301,7 +301,7 @@ def testSegmentSegmentCollision(self) -> None: def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.num_of_begins += 1 - s.add_collision_handler(None, None).begin = begin + s.set_collision_callback(begin=begin) s.step(0.1) self.assertEqual(1, self.num_of_begins) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 6695dd9d..765c0a5c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -7,7 +7,7 @@ import sys import unittest import warnings -from typing import Any, Callable, Sequence, cast +from typing import Any, Callable, Optional, Sequence, cast import pymunk as p from pymunk import * @@ -528,10 +528,10 @@ def testCollisionHandlerBegin(self) -> None: self.hits = 0 - def begin(arb: p.Arbiter, space: p.Space) -> None: + def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hits += 1 - s.set_collision_callbacks(0, 0, begin=begin) + s.set_collision_callback(0, 0, begin=begin) for x in range(10): s.step(0.1) @@ -549,14 +549,14 @@ def testCollisionHandlerPreSolve(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore d["test"] = data["test"] - h = s.set_collision_callbacks(0, 1) - h.data["test"] = 1 - h.pre_solve = pre_solve + data = {"test": 1} + s.set_collision_callback(0, 1, pre_solve=pre_solve, data=data) + s.step(0.1) self.assertEqual(c1, d["shapes"][1]) self.assertEqual(c2, d["shapes"][0]) @@ -567,10 +567,10 @@ def testCollisionHandlerPostSolve(self) -> None: self._setUp() self.hit = 0 - def post_solve(arb: p.Arbiter, space: p.Space) -> None: + def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hit += 1 - self.s.set_collision_callbacks(0, 0, post_solve=post_solve) + self.s.set_collision_callback(0, 0, post_solve=post_solve) self.s.step(0.1) self.assertEqual(self.hit, 1) self._tearDown() @@ -591,10 +591,10 @@ def testCollisionHandlerSeparate(self) -> None: self.separated = False - def separate(arb: p.Arbiter, space: p.Space) -> None: + def separate(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.separated = True - s.set_collision_callbacks(0, 0, separate=separate) + s.set_collision_callback(0, 0, separate=separate) for x in range(10): s.step(0.1) @@ -613,7 +613,7 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.set_collision_callbacks(separate=separate) + s.set_collision_callback(separate=separate) s.step(1) s.remove(c1) @@ -632,11 +632,14 @@ def testCollisionHandlerDefaultCallbacks(self) -> None: s.add(b1, c1, b2, c2) s.gravity = 0, -100 - h = s.set_collision_callbacks(None, None) - h.begin = h.do_nothing - h.pre_solve = h.do_nothing - h.post_solve = h.do_nothing - h.separate = h.do_nothing + s.set_collision_callback( + None, + None, + begin=Space.do_nothing, + pre_solve=Space.do_nothing, + post_solve=Space.do_nothing, + separate=Space.do_nothing, + ) for _ in range(10): s.step(0.1) @@ -669,15 +672,15 @@ def remove2(*_: Any) -> None: print("remove2") s.remove(shape2) - # s.set_collision_callbacks(1, 0).separate = remove2 - s.set_collision_callbacks(1, 0, separate=remove1) + # s.set_collision_callback(1, 0).separate = remove2 + s.set_collision_callback(1, 0, separate=remove1) s.step(0.001) # trigger separate with shape2 and shape3, shape1 will be removed 2x s.remove(shape1) - s.set_collision_callbacks(1, 0, separate=remove2) + s.set_collision_callback(1, 0, separate=remove2) s.add(shape1) s.step(1) @@ -707,7 +710,7 @@ def separate(*_: Any) -> None: pass s.step(1) - s.set_collision_callbacks(0, separate=separate) + s.set_collision_callback(0, separate=separate) s.remove(shape1) @@ -734,18 +737,18 @@ def testCollisionHandlerRemoveAfterSeparate(self) -> None: space.add(shape1, body2, shape2, shape3, body3) print("START", shape1, shape2, shape3) - def separate(arbiter: p.Arbiter, space: p.Space) -> None: + def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: print("SEP", arbiter.shapes) self.separate_occurred = True - def post_solve(arbiter: p.Arbiter, space: p.Space) -> None: + def post_solve(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: print("POST", arbiter.shapes) if self.separate_occurred: print("POST REMOVE", arbiter.shapes) space.remove(*arbiter.shapes) - space.set_collision_callbacks(1, 2, post_solve=post_solve) - space.set_collision_callbacks(3, 2, separate=separate) + space.set_collision_callback(1, 2, post_solve=post_solve) + space.set_collision_callback(3, 2, separate=separate) print(1) self.separate_occurred = False @@ -776,26 +779,26 @@ def testCollisionHandlerAddRemoveInStep(self) -> None: b = p.Body(1, 2) c = p.Circle(b, 2) - def pre_solve_add(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve_add(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.add(b, c) space.add(c, b) self.assertTrue(b not in s.bodies) self.assertTrue(c not in s.shapes) - def pre_solve_remove(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.remove(b, c) space.remove(c, b) self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.set_collision_callbacks(0, 0, pre_solve=pre_solve_add) + s.set_collision_callback(0, 0, pre_solve=pre_solve_add) s.step(0.1) return self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.set_collision_callbacks(0, 0).pre_solve = pre_solve_remove + s.set_collision_callback(0, 0).pre_solve = pre_solve_remove s.step(0.1) @@ -806,10 +809,10 @@ def testCollisionHandlerRemoveInStep(self) -> None: self._setUp() s = self.s - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.remove(*arb.shapes) - s.set_collision_callbacks(0, 0, pre_solve=pre_solve) + s.set_collision_callback(0, 0, pre_solve=pre_solve) s.step(0.1) @@ -824,7 +827,7 @@ def testCollisionHandlerOrder(self) -> None: def callback( name: str, - types: tuple[int, int], + types: tuple[Optional[int], Optional[int]], arb: p.Arbiter, space: p.Space, data: dict[Any, Any], @@ -846,11 +849,14 @@ def callback( ] for t1, t2 in handler_order: - h = s.set_collision_callbacks(t1, t2) - h.begin = functools.partial(callback, "begin", (t1, t2)) - h.pre_solve = functools.partial(callback, "pre_solve", (t1, t2)) - h.post_solve = functools.partial(callback, "post_solve", (t1, t2)) - h.separate = functools.partial(callback, "separate", (t1, t2)) + s.set_collision_callback( + t1, + t2, + begin=functools.partial(callback, "begin", (t1, t2)), + pre_solve=functools.partial(callback, "pre_solve", (t1, t2)), + post_solve=functools.partial(callback, "post_solve", (t1, t2)), + separate=functools.partial(callback, "separate", (t1, t2)), + ) b1 = p.Body(1, 30) c1 = p.Circle(b1, 10) @@ -903,12 +909,12 @@ def testWildcardCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.set_collision_callbacks(1, None, pre_solve=pre_solve) - s.set_collision_callbacks(None, 1, pre_solve=pre_solve) + s.set_collision_callback(1, None, pre_solve=pre_solve) + s.set_collision_callback(None, 1, pre_solve=pre_solve) s.step(0.1) @@ -933,11 +939,11 @@ def testDefaultCollisionHandler(self) -> None: d = {} - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.set_collision_callbacks(pre_solve=pre_solve) + s.set_collision_callback(pre_solve=pre_solve) s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -965,12 +971,12 @@ def callback( s.remove(shape) test_self.calls += 1 - def pre_solve(arb: p.Arbiter, space: p.Space) -> None: + def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # note that we dont pass on the whole arbiters object, instead # we take only the shapes. space.add_post_step_callback(callback, 0, arb.shapes, test_self=self) - s.set_collision_callbacks(0, 0, pre_solve=pre_solve) + s.set_collision_callback(0, 0, pre_solve=pre_solve) s.step(0.1) self.assertEqual(len(s.shapes), 0) @@ -1080,13 +1086,13 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: j2 = PinJoint(s.static_body, b2) s.add(j1, j2) - s.set_collision_callbacks(begin=f1) + s.set_collision_callback(begin=f1) - s.set_collision_callbacks(1, pre_solve=f1) + s.set_collision_callback(1, pre_solve=f1) - s.set_collision_callbacks(1, 2, post_solve=f1) + s.set_collision_callback(1, 2, post_solve=f1) - s.set_collision_callbacks(3, 4, separate=f1) + s.set_collision_callback(3, 4, separate=f1) s2 = copy_func(s) @@ -1110,28 +1116,28 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: self.assertIn(s2.static_body, ja) # Assert collision handlers - h2 = s2.set_collision_callbacks(None, None) + h2 = s2._handlers[(None, None)] self.assertIsNotNone(h2.begin) - self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) - self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) - self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) + self.assertEqual(h2.pre_solve, p.Space.do_nothing) + self.assertEqual(h2.post_solve, p.Space.do_nothing) + self.assertEqual(h2.separate, p.Space.do_nothing) - h2 = s2.set_collision_callbacks(1, None) - self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) + h2 = s2._handlers[(1, None)] + self.assertEqual(h2.begin, p.Space.do_nothing) self.assertIsNotNone(h2.pre_solve) - self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) - self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) + self.assertEqual(h2.post_solve, p.Space.do_nothing) + self.assertEqual(h2.separate, p.Space.do_nothing) - h2 = s2.set_collision_callbacks(1, 2) - self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) - self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) + h2 = s2._handlers[(1, 2)] + self.assertEqual(h2.begin, p.Space.do_nothing) + self.assertEqual(h2.pre_solve, p.Space.do_nothing) self.assertIsNotNone(h2.post_solve) - self.assertEqual(h2.separate, p.CollisionHandler.do_nothing) + self.assertEqual(h2.separate, p.Space.do_nothing) - h2 = s2.set_collision_callbacks(3, 4) - self.assertEqual(h2.begin, p.CollisionHandler.do_nothing) - self.assertEqual(h2.pre_solve, p.CollisionHandler.do_nothing) - self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing) + h2 = s2._handlers[(3, 4)] + self.assertEqual(h2.begin, p.Space.do_nothing) + self.assertEqual(h2.pre_solve, p.Space.do_nothing) + self.assertEqual(h2.post_solve, p.Space.do_nothing) self.assertIsNotNone(h2.separate) def testPickleCachedArbiters(self) -> None: From b16b4c453216f9739451ede4ce57627cb0fe95d5 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 25 May 2025 11:07:31 +0200 Subject: [PATCH 70/80] Renamed coll callback to on_collision, moved do_nothing to Pymunk.empty_callback --- pymunk/__init__.py | 10 +++ pymunk/examples/arrows.py | 4 +- pymunk/examples/balls_and_lines.py | 4 +- pymunk/examples/breakout.py | 6 +- pymunk/examples/collisions.py | 2 +- pymunk/examples/contact_and_no_flipy.py | 2 +- pymunk/examples/contact_with_friction.py | 2 +- pymunk/examples/deformable.py | 2 +- pymunk/examples/platformer.py | 2 +- pymunk/space.py | 40 ++++++------ pymunk/tests/test_arbiter.py | 26 ++++---- pymunk/tests/test_common.py | 4 +- pymunk/tests/test_shape.py | 2 +- pymunk/tests/test_space.py | 80 ++++++++++++------------ 14 files changed, 94 insertions(+), 92 deletions(-) diff --git a/pymunk/__init__.py b/pymunk/__init__.py index 01355392..def21270 100644 --- a/pymunk/__init__.py +++ b/pymunk/__init__.py @@ -193,4 +193,14 @@ def area_for_poly(vertices: Sequence[tuple[float, float]], radius: float = 0) -> return cp.cpAreaForPoly(len(vs), vs, radius) +def empty_callback(*args, **kwargs) -> None: + """A default empty callback. + + Can be used to reset a collsion callback to its original empty + function. Note that its more efficient to use this method than to + define your own empty/do nothing method. + """ + return + + # del cp, ct, u diff --git a/pymunk/examples/arrows.py b/pymunk/examples/arrows.py index 32d84caa..bfe704d2 100644 --- a/pymunk/examples/arrows.py +++ b/pymunk/examples/arrows.py @@ -98,9 +98,7 @@ def main(): space.add(arrow_body, arrow_shape) flying_arrows: list[pymunk.Body] = [] - space.set_collision_callback( - 0, 1, post_solve=post_solve_arrow_hit, data=flying_arrows - ) + space.on_collision(0, 1, post_solve=post_solve_arrow_hit, data=flying_arrows) start_time = 0 while running: diff --git a/pymunk/examples/balls_and_lines.py b/pymunk/examples/balls_and_lines.py index 92e4f763..74d92239 100644 --- a/pymunk/examples/balls_and_lines.py +++ b/pymunk/examples/balls_and_lines.py @@ -46,9 +46,7 @@ def main(): mouse_shape.collision_type = COLLTYPE_MOUSE space.add(mouse_body, mouse_shape) - space.set_collision_callback( - COLLTYPE_MOUSE, COLLTYPE_BALL, pre_solve=mouse_coll_func - ) + space.on_collision(COLLTYPE_MOUSE, COLLTYPE_BALL, pre_solve=mouse_coll_func) ### Static line line_point1 = None diff --git a/pymunk/examples/breakout.py b/pymunk/examples/breakout.py index 340e8e63..5a335a4b 100644 --- a/pymunk/examples/breakout.py +++ b/pymunk/examples/breakout.py @@ -76,7 +76,7 @@ def remove_brick(arbiter, space, data): brick_shape = arbiter.shapes[0] space.remove(brick_shape, brick_shape.body) - space.set_collision_callback( + space.on_collision( collision_types["brick"], collision_types["ball"], separate=remove_brick ) @@ -116,7 +116,7 @@ def remove_first(arbiter, space, data): ball_shape = arbiter.shapes[0] space.remove(ball_shape, ball_shape.body) - space.set_collision_callback( + space.on_collision( collision_types["ball"], collision_types["bottom"], begin=remove_first ) @@ -143,7 +143,7 @@ def pre_solve(arbiter, space, data): set_.points[0].distance = 0 arbiter.contact_point_set = set_ - space.set_collision_callback( + space.on_collision( collision_types["player"], collision_types["ball"], pre_solve=pre_solve ) diff --git a/pymunk/examples/collisions.py b/pymunk/examples/collisions.py index 23237711..9328c69b 100644 --- a/pymunk/examples/collisions.py +++ b/pymunk/examples/collisions.py @@ -109,7 +109,7 @@ def main(): space.add(shapes[selected_shape_idx]) data = {} - h = space.set_collision_callback( + h = space.on_collision( 0, 1, begin=begin, diff --git a/pymunk/examples/contact_and_no_flipy.py b/pymunk/examples/contact_and_no_flipy.py index f64987a8..e7500bfd 100644 --- a/pymunk/examples/contact_and_no_flipy.py +++ b/pymunk/examples/contact_and_no_flipy.py @@ -42,7 +42,7 @@ def main(): ticks_to_next_ball = 10 - space.set_collision_callback(0, 0, post_solve=draw_collision, data=screen) + space.on_collision(0, 0, post_solve=draw_collision, data=screen) while running: for event in pygame.event.get(): diff --git a/pymunk/examples/contact_with_friction.py b/pymunk/examples/contact_with_friction.py index bf9d8170..d79cad9c 100644 --- a/pymunk/examples/contact_with_friction.py +++ b/pymunk/examples/contact_with_friction.py @@ -52,7 +52,7 @@ def main(): ticks_to_next_ball = 10 - space.set_collision_callback(0, 0, post_solve=draw_collision, data=screen) + space.on_collision(0, 0, post_solve=draw_collision, data=screen) while running: for event in pygame.event.get(): diff --git a/pymunk/examples/deformable.py b/pymunk/examples/deformable.py index 3feb4bac..6a667a2c 100644 --- a/pymunk/examples/deformable.py +++ b/pymunk/examples/deformable.py @@ -82,7 +82,7 @@ def pre_solve(arb, space, data): space.remove(s.body, s) arb.process_collision = False - space.set_collision_callback(0, 1, pre_solve=pre_solve) + space.on_collision(0, 1, pre_solve=pre_solve) terrain_surface = pygame.Surface((600, 600)) terrain_surface.fill(pygame.Color("white")) diff --git a/pymunk/examples/platformer.py b/pymunk/examples/platformer.py index 9f949eff..50b25006 100644 --- a/pymunk/examples/platformer.py +++ b/pymunk/examples/platformer.py @@ -139,7 +139,7 @@ def passthrough_handler(arbiter: pymunk.Arbiter, space: pymunk.Space, data): else: arbiter.process_collision = False - space.set_collision_callback(1, 2, begin=passthrough_handler) + space.on_collision(1, 2, begin=passthrough_handler) # player body = pymunk.Body(5, float("inf")) diff --git a/pymunk/space.py b/pymunk/space.py index 2240b169..857f5c11 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -586,7 +586,7 @@ def step(self, dt: float) -> None: self._post_step_callbacks.clear() - def set_collision_callback( + def on_collision( self, collision_type_a: Optional[int] = None, collision_type_b: Optional[int] = None, @@ -643,38 +643,31 @@ def set_collision_callback( else: ch = self._handlers[key] - if begin == Space.do_nothing: + # to avoid circular dep + from . import empty_callback + + if begin == empty_callback: ch.begin = None elif begin != None: ch.begin = begin ch.data["begin"] = data - if pre_solve == Space.do_nothing: + if pre_solve == empty_callback: ch.pre_solve = None elif pre_solve != None: ch.pre_solve = pre_solve ch.data["pre_solve"] = data - if post_solve == Space.do_nothing: + if post_solve == empty_callback: ch.post_solve = None elif post_solve != None: ch.post_solve = post_solve ch.data["post_solve"] = data - if separate == Space.do_nothing: + if separate == empty_callback: ch.separate = None elif separate != None: ch.separate = separate ch.data["separate"] = data return - @staticmethod - def do_nothing(arbiter: Arbiter, space: "Space", data: Any) -> None: - """The default do nothing method used for the collision callbacks. - - Can be used to reset a collsion callback to its original do nothing - function. Note that its more efficient to use this method than to - define your own do nothing method. - """ - return - def add_post_step_callback( self, callback_function: Callable[ @@ -989,16 +982,19 @@ def __getstate__(self) -> _State: d["special"].append(("shapes", list(self.shapes))) d["special"].append(("constraints", list(self.constraints))) + # to avoid circular dep + from . import empty_callback + handlers = [] for k, v in self._handlers.items(): h: dict[str, Any] = {} - if v._begin != Space.do_nothing: + if v._begin != empty_callback: h["_begin"] = v._begin - if v._pre_solve != Space.do_nothing: + if v._pre_solve != empty_callback: h["_pre_solve"] = v._pre_solve - if v._post_solve != Space.do_nothing: + if v._post_solve != empty_callback: h["_post_solve"] = v._post_solve - if v._separate != Space.do_nothing: + if v._separate != empty_callback: h["_separate"] = v._separate handlers.append((k, h)) @@ -1061,7 +1057,7 @@ def __setstate__(self, state: _State) -> None: if "_separate" in hd: separate = hd["_separate"] if k2 == None: - self.set_collision_callback( + self.on_collision( None, None, begin=begin, @@ -1070,7 +1066,7 @@ def __setstate__(self, state: _State) -> None: separate=separate, ) elif isinstance(k2, tuple): - self.set_collision_callback( + self.on_collision( k2[0], k2[1], begin=begin, @@ -1079,7 +1075,7 @@ def __setstate__(self, state: _State) -> None: separate=separate, ) else: - self.set_collision_callback( + self.on_collision( k2, None, begin=begin, diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index 06d0669d..e9943be7 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -28,7 +28,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 - s.set_collision_callback(1, 2, pre_solve=pre_solve) + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -56,7 +56,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.friction, 0.18) arb.friction = 1 - s.set_collision_callback(1, 2, pre_solve=pre_solve) + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -87,7 +87,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: arb.surface_velocity = (10, 10) # TODO: add assert check that setting surface_velocity has any effect - s.set_collision_callback(1, 2, pre_solve=pre_solve) + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(5): s.step(0.1) @@ -142,7 +142,7 @@ def f() -> None: self.assertRaises(Exception, f) - s.set_collision_callback(2, 1, pre_solve=pre_solve) + s.on_collision(2, 1, pre_solve=pre_solve) s.step(0.1) @@ -170,7 +170,7 @@ def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertAlmostEqual(arb.total_impulse.y, 4.3438914027) self.post_solve_done = True - s.set_collision_callback(1, 2, post_solve=post_solve) + s.on_collision(1, 2, post_solve=post_solve) s.step(0.1) @@ -197,7 +197,7 @@ def testTotalKE(self) -> None: def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: r["ke"] = arb.total_ke - s.set_collision_callback(1, 2, post_solve=post_solve) + s.on_collision(1, 2, post_solve=post_solve) s.step(0.1) @@ -223,14 +223,14 @@ def testIsFirstContact(self) -> None: def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(arb.is_first_contact) - s.set_collision_callback(1, 2, pre_solve=pre_solve1) + s.on_collision(1, 2, pre_solve=pre_solve1) s.step(0.1) def pre_solve2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertFalse(arb.is_first_contact) - s.set_collision_callback(1, 2, pre_solve=pre_solve2) + s.on_collision(1, 2, pre_solve=pre_solve2) s.step(0.1) @@ -251,7 +251,7 @@ def testNormal(self) -> None: def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: r["n"] = Vec2d(*arb.normal) - s.set_collision_callback(1, 2, pre_solve=pre_solve1) + s.on_collision(1, 2, pre_solve=pre_solve1) s.step(0.1) @@ -281,7 +281,7 @@ def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called1 = True self.assertFalse(arb.is_removal) - s.set_collision_callback(1, 2, separate=separate1) + s.on_collision(1, 2, separate=separate1) for x in range(10): s.step(0.1) @@ -296,7 +296,7 @@ def separate2(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called2 = True self.assertTrue(arb.is_removal) - s.set_collision_callback(1, 2, separate=separate2) + s.on_collision(1, 2, separate=separate2) s.remove(b1, c1) self.assertTrue(self.called2) @@ -328,7 +328,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertEqual(arb.bodies[0], arb.shapes[0].body) self.assertEqual(arb.bodies[1], arb.shapes[1].body) - s.set_collision_callback(1, 2, post_solve=pre_solve) + s.on_collision(1, 2, post_solve=pre_solve) s.step(0.1) self.assertTrue(self.called) @@ -412,7 +412,7 @@ def callback( hdata["process_values"] = process_values hdata["expected"] = expected_calls hdata["result"] = [] - s.set_collision_callback( + s.on_collision( 1, 2, begin=functools.partial(callback, "b"), diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index 2257f94d..160591eb 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -126,7 +126,7 @@ def remove_first(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: # space.add_post_step_callback(space.remove, first_shape, first_shape.body) # space.remove(c1) - space.set_collision_callback(2, 0, separate=remove_first) + space.on_collision(2, 0, separate=remove_first) # print(1) space.step(1.0 / 60) # print(2) @@ -166,7 +166,7 @@ def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: # space.add_post_step_callback(space.remove, first_shape, first_shape.body) # space.remove(c1) - space.set_collision_callback(2, 0, separate=separate) + space.on_collision(2, 0, separate=separate) # print(1) space.step(1) # print(2) diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index 77b2ba9f..20bb4283 100644 --- a/pymunk/tests/test_shape.py +++ b/pymunk/tests/test_shape.py @@ -301,7 +301,7 @@ def testSegmentSegmentCollision(self) -> None: def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.num_of_begins += 1 - s.set_collision_callback(begin=begin) + s.on_collision(begin=begin) s.step(0.1) self.assertEqual(1, self.num_of_begins) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 765c0a5c..7badde9c 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -531,7 +531,7 @@ def testCollisionHandlerBegin(self) -> None: def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hits += 1 - s.set_collision_callback(0, 0, begin=begin) + s.on_collision(0, 0, begin=begin) for x in range(10): s.step(0.1) @@ -555,7 +555,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["test"] = data["test"] data = {"test": 1} - s.set_collision_callback(0, 1, pre_solve=pre_solve, data=data) + s.on_collision(0, 1, pre_solve=pre_solve, data=data) s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -570,7 +570,7 @@ def testCollisionHandlerPostSolve(self) -> None: def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hit += 1 - self.s.set_collision_callback(0, 0, post_solve=post_solve) + self.s.on_collision(0, 0, post_solve=post_solve) self.s.step(0.1) self.assertEqual(self.hit, 1) self._tearDown() @@ -594,7 +594,7 @@ def testCollisionHandlerSeparate(self) -> None: def separate(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.separated = True - s.set_collision_callback(0, 0, separate=separate) + s.on_collision(0, 0, separate=separate) for x in range(10): s.step(0.1) @@ -613,7 +613,7 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.set_collision_callback(separate=separate) + s.on_collision(separate=separate) s.step(1) s.remove(c1) @@ -632,13 +632,13 @@ def testCollisionHandlerDefaultCallbacks(self) -> None: s.add(b1, c1, b2, c2) s.gravity = 0, -100 - s.set_collision_callback( + s.on_collision( None, None, - begin=Space.do_nothing, - pre_solve=Space.do_nothing, - post_solve=Space.do_nothing, - separate=Space.do_nothing, + begin=p.empty_callback, + pre_solve=p.empty_callback, + post_solve=p.empty_callback, + separate=p.empty_callback, ) for _ in range(10): @@ -672,15 +672,15 @@ def remove2(*_: Any) -> None: print("remove2") s.remove(shape2) - # s.set_collision_callback(1, 0).separate = remove2 - s.set_collision_callback(1, 0, separate=remove1) + # s.on_collision(1, 0).separate = remove2 + s.on_collision(1, 0, separate=remove1) s.step(0.001) # trigger separate with shape2 and shape3, shape1 will be removed 2x s.remove(shape1) - s.set_collision_callback(1, 0, separate=remove2) + s.on_collision(1, 0, separate=remove2) s.add(shape1) s.step(1) @@ -710,7 +710,7 @@ def separate(*_: Any) -> None: pass s.step(1) - s.set_collision_callback(0, separate=separate) + s.on_collision(0, separate=separate) s.remove(shape1) @@ -747,8 +747,8 @@ def post_solve(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: print("POST REMOVE", arbiter.shapes) space.remove(*arbiter.shapes) - space.set_collision_callback(1, 2, post_solve=post_solve) - space.set_collision_callback(3, 2, separate=separate) + space.on_collision(1, 2, post_solve=post_solve) + space.on_collision(3, 2, separate=separate) print(1) self.separate_occurred = False @@ -791,14 +791,14 @@ def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.set_collision_callback(0, 0, pre_solve=pre_solve_add) + s.on_collision(0, 0, pre_solve=pre_solve_add) s.step(0.1) return self.assertTrue(b in s.bodies) self.assertTrue(c in s.shapes) - s.set_collision_callback(0, 0).pre_solve = pre_solve_remove + s.on_collision(0, 0).pre_solve = pre_solve_remove s.step(0.1) @@ -812,7 +812,7 @@ def testCollisionHandlerRemoveInStep(self) -> None: def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: space.remove(*arb.shapes) - s.set_collision_callback(0, 0, pre_solve=pre_solve) + s.on_collision(0, 0, pre_solve=pre_solve) s.step(0.1) @@ -849,7 +849,7 @@ def callback( ] for t1, t2 in handler_order: - s.set_collision_callback( + s.on_collision( t1, t2, begin=functools.partial(callback, "begin", (t1, t2)), @@ -913,8 +913,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.set_collision_callback(1, None, pre_solve=pre_solve) - s.set_collision_callback(None, 1, pre_solve=pre_solve) + s.on_collision(1, None, pre_solve=pre_solve) + s.on_collision(None, 1, pre_solve=pre_solve) s.step(0.1) @@ -943,7 +943,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - s.set_collision_callback(pre_solve=pre_solve) + s.on_collision(pre_solve=pre_solve) s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -976,7 +976,7 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: # we take only the shapes. space.add_post_step_callback(callback, 0, arb.shapes, test_self=self) - s.set_collision_callback(0, 0, pre_solve=pre_solve) + s.on_collision(0, 0, pre_solve=pre_solve) s.step(0.1) self.assertEqual(len(s.shapes), 0) @@ -1086,13 +1086,13 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: j2 = PinJoint(s.static_body, b2) s.add(j1, j2) - s.set_collision_callback(begin=f1) + s.on_collision(begin=f1) - s.set_collision_callback(1, pre_solve=f1) + s.on_collision(1, pre_solve=f1) - s.set_collision_callback(1, 2, post_solve=f1) + s.on_collision(1, 2, post_solve=f1) - s.set_collision_callback(3, 4, separate=f1) + s.on_collision(3, 4, separate=f1) s2 = copy_func(s) @@ -1118,26 +1118,26 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: # Assert collision handlers h2 = s2._handlers[(None, None)] self.assertIsNotNone(h2.begin) - self.assertEqual(h2.pre_solve, p.Space.do_nothing) - self.assertEqual(h2.post_solve, p.Space.do_nothing) - self.assertEqual(h2.separate, p.Space.do_nothing) + self.assertEqual(h2.pre_solve, p.empty_callback) + self.assertEqual(h2.post_solve, p.empty_callback) + self.assertEqual(h2.separate, p.empty_callback) h2 = s2._handlers[(1, None)] - self.assertEqual(h2.begin, p.Space.do_nothing) + self.assertEqual(h2.begin, p.empty_callback) self.assertIsNotNone(h2.pre_solve) - self.assertEqual(h2.post_solve, p.Space.do_nothing) - self.assertEqual(h2.separate, p.Space.do_nothing) + self.assertEqual(h2.post_solve, p.empty_callback) + self.assertEqual(h2.separate, p.empty_callback) h2 = s2._handlers[(1, 2)] - self.assertEqual(h2.begin, p.Space.do_nothing) - self.assertEqual(h2.pre_solve, p.Space.do_nothing) + self.assertEqual(h2.begin, p.empty_callback) + self.assertEqual(h2.pre_solve, p.empty_callback) self.assertIsNotNone(h2.post_solve) - self.assertEqual(h2.separate, p.Space.do_nothing) + self.assertEqual(h2.separate, p.empty_callback) h2 = s2._handlers[(3, 4)] - self.assertEqual(h2.begin, p.Space.do_nothing) - self.assertEqual(h2.pre_solve, p.Space.do_nothing) - self.assertEqual(h2.post_solve, p.Space.do_nothing) + self.assertEqual(h2.begin, p.empty_callback) + self.assertEqual(h2.pre_solve, p.empty_callback) + self.assertEqual(h2.post_solve, p.empty_callback) self.assertIsNotNone(h2.separate) def testPickleCachedArbiters(self) -> None: From 5395577052daf5cb3936a88fdd5d6ecc43fde979 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 25 May 2025 21:53:35 +0200 Subject: [PATCH 71/80] fix tests --- pymunk/tests/test_space.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 7badde9c..14d50264 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1118,26 +1118,26 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None: # Assert collision handlers h2 = s2._handlers[(None, None)] self.assertIsNotNone(h2.begin) - self.assertEqual(h2.pre_solve, p.empty_callback) - self.assertEqual(h2.post_solve, p.empty_callback) - self.assertEqual(h2.separate, p.empty_callback) + self.assertEqual(h2.pre_solve, None) + self.assertEqual(h2.post_solve, None) + self.assertEqual(h2.separate, None) h2 = s2._handlers[(1, None)] - self.assertEqual(h2.begin, p.empty_callback) + self.assertEqual(h2.begin, None) self.assertIsNotNone(h2.pre_solve) - self.assertEqual(h2.post_solve, p.empty_callback) - self.assertEqual(h2.separate, p.empty_callback) + self.assertEqual(h2.post_solve, None) + self.assertEqual(h2.separate, None) h2 = s2._handlers[(1, 2)] - self.assertEqual(h2.begin, p.empty_callback) - self.assertEqual(h2.pre_solve, p.empty_callback) + self.assertEqual(h2.begin, None) + self.assertEqual(h2.pre_solve, None) self.assertIsNotNone(h2.post_solve) - self.assertEqual(h2.separate, p.empty_callback) + self.assertEqual(h2.separate, None) h2 = s2._handlers[(3, 4)] - self.assertEqual(h2.begin, p.empty_callback) - self.assertEqual(h2.pre_solve, p.empty_callback) - self.assertEqual(h2.post_solve, p.empty_callback) + self.assertEqual(h2.begin, None) + self.assertEqual(h2.pre_solve, None) + self.assertEqual(h2.post_solve, None) self.assertIsNotNone(h2.separate) def testPickleCachedArbiters(self) -> None: From 030328e779cbae15abc2b783c429695fc6fdabe0 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 25 May 2025 21:56:42 +0200 Subject: [PATCH 72/80] Minor type fixes --- pymunk/__init__.py | 4 ++-- pymunk/tests/test_arbiter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pymunk/__init__.py b/pymunk/__init__.py index def21270..2bd405d8 100644 --- a/pymunk/__init__.py +++ b/pymunk/__init__.py @@ -67,7 +67,7 @@ "Vec2d", ] -from typing import Sequence, cast +from typing import Any, Sequence, cast from . import _chipmunk_cffi @@ -193,7 +193,7 @@ def area_for_poly(vertices: Sequence[tuple[float, float]], radius: float = 0) -> return cp.cpAreaForPoly(len(vs), vs, radius) -def empty_callback(*args, **kwargs) -> None: +def empty_callback(*args: Any, **kwargs: Any) -> None: """A default empty callback. Can be used to reset a collsion callback to its original empty diff --git a/pymunk/tests/test_arbiter.py b/pymunk/tests/test_arbiter.py index e9943be7..2241f741 100644 --- a/pymunk/tests/test_arbiter.py +++ b/pymunk/tests/test_arbiter.py @@ -408,7 +408,7 @@ def callback( # print("process_values, expected calls", process_values, expected_calls) s = setup() - hdata = {} + hdata: dict[str, Any] = {} hdata["process_values"] = process_values hdata["expected"] = expected_calls hdata["result"] = [] From 6471d3de3bd80bd2a6bffc18012c805cbf9b222d Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 25 May 2025 22:39:06 +0200 Subject: [PATCH 73/80] fix tutorial, changelog --- CHANGELOG.rst | 128 ++++++++++++++++-------- docs/src/tutorials/SlideAndPinJoint.rst | 115 +++++++++++---------- 2 files changed, 143 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e56c668e..218bb7f2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,53 +4,97 @@ Changelog Pymunk 7.0.0 (2025-04-10) ------------------------- -TODO: Add note of new collision handler logic! - -**Many improvements, with some breaking changes!** - -This is a big cleanup release with several breaking changes. If you upgrade from an older version, make sure to pay attention, especially the Space.bodies, Space.shapes and shape.constraints updates can break silently! - -Extra thanks for Github user aatle for a number of suggestions and feedback for this Pymunk release! - - -Changes: - -Breaking changes - -- Unified all Space.query methods to include sensor shapes. Previsouly the nearest methods filtered them out. -- Changed Space.shapes, Space.bodies and Space.constraints to return a KeysView of instead of a list of the items. Note that this means the returned collection is no longer a copy. To get the old behavior, you can convert to list manually, like list(space.shapes). -- At least one of the two bodies attached to constraint/joint must be dynamic. -- Vec2d now supports bool to test if zero. (bool(Vec2d(2,3) == True) Note this is a breaking change. -- Added Vec2d.length_squared, and deprecated Vec2d.get_length_sqrd() -- Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd() -- A dynamic body must have non-zero mass when calling Space.step (either from Body.mass, or by setting mass or density on a Shape attached to the Body). Its not valid to set mass to 0 on a dynamic body attached to a space. -- Deprecated matplotlib_util. If you think this is a useful module, and you use it, please create an issue on the Pymunk issue track +**Many improvements and breaking changes!** + +This is a big cleanup release with several breaking changes. If you upgrade +from an older version, make sure to pay attention, especially the +``Space.bodies``, ``Space.shapes`` and ``Space.constraints`` updates can break silently! + +Extra thanks for Github user aatle for a number of suggestions and feedback +for this Pymunk release! + + +The biggest changes relates to collision handlers: + +- The ``begin``, ``pre_step`` and ``separate`` methods will now always be + called. ``post_solve`` will continue to only be called if the collision were + actually processed. +- You no longer return a ``bool`` from ``begin`` or ``pre_step`` to control if the + collision should be processed. Instead, there is a property on the ``Arbiter``, + ``process_collision``, that can be set to ``False``. +- The ``process_collision`` property is stable but updatable, e.g if set to + ``False`` in a ``begin`` callback it will be ``False`` in the ``pre_step``, + which in turn can toggle it back to ``True``. +- The ``CollisionHandler`` class itself is no longer public. The ``Space`` has + a new ``on_collision()`` method that takes the callbacks as arguments without + returning anything. + +In addition to the above (which will be easy to spot), there is also a more +subtle change: + +- ``Space.shapes``, ``Space.bodies`` and ``Space.constraints`` now return a + ``KeysView``. That means that the returned collection is no longer a copy, + instead if you hold a ref to it and if you for example add an object to the + ``Space`` it will update. To get the old behavior, you can convert to a list + manually, e.g ``list(Space.shapes)``. + +Additional breaking changes: + +- Unified all ``Space.query`` methods to include sensor shapes. Previously + the nearest methods filtered them out. +- At least one of the two bodies attached to a constraint/joint must be + dynamic. +- ``Vec2d`` now supports ``bool`` to test if zero. ( + ``bool(Vec2d(2,3) == True``). +- Added ``Vec2d.length_squared``, and deprecated ``Vec2d.get_length_sqrd()`` +- Added ``Vec2d.get_distance_squared()``, and deprecated + ``Vec2d.get_dist_sqrd()`` +- A dynamic body must have non-zero mass when calling ``Space.step()`` (either + from ``Body.mass``, or by setting ``mass`` or ``density`` on a Shape + attached to the ``Body``). It is not valid to set ``mass`` to ``0`` on a + dynamic body attached to a ``Space``. +- Deprecated ``matplotlib_util``. If you think this is a useful module, and + you use it, please create an issue on the Pymunk issue tracker at + https://github.com/viblo/pymunk/issues - Dropped support for Python 3.8 -- Changed body.constraints to return a KeysView of the Constraints attached to the body. Note that its still weak references to the Constraints. -- Reversed the dependency between bodies and shapes. Now the Body owns the connection, and the Shape only keeps a weak ref to the Body. That means that if you remove a Body, then any shapes not referenced anywhere else will also be removed. -- Changed body.shapes to return a KeysView instead of a set of the shapes. -- Changed Shape.segment_query to return None in case the query did not hit the shape. -- Changed ContactPointSet.points to be a tuple and not list to make it clear its length is fixed. -- Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks. - -New non-breaking features -- Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see https://github.com/viblo/Munk2D for details). -- Added Arbiter.bodies shorthand to get the shapes' bodies in the Arbiter -- New method ShapeFilter.rejects_collision() that checks if the filter would reject a collision with another filter. -- New method Vec2d.polar_tuple that return the vector as polar coordinates +- Changed ``Body.constraints`` to return a ``KeysView`` of the Constraints + attached to the ``Body``. Note that its still weak references to the + Constraints. +- Reversed the dependency between bodies and shapes. Now the ``Body`` owns the + connection, and the ``Shape`` only keeps a weak ref to the ``Body``. That + means that if you remove a ``Body``, then any shapes not referenced + anywhere else will also be removed. +- Changed ``Body.shapes`` to return a ``KeysView`` instead of a set of the shapes. +- Changed ``Shape.segment_query`` to return None in case the query did not + hit the shape. +- Changed ``ContactPointSet.points`` to be a ``tuple`` and not ``list`` to + make it clear its length is fixed. +- Added default ``empty_callback``, that can be used to efficiently reset + any callback to default. + +New non-breaking features: + +- Switched from using Chipmunk to the new Munk2D fork of Chipmunk (see + https://github.com/viblo/Munk2D for details). +- Added ``Arbiter.bodies`` shorthand to get the shapes' bodies in the ``Arbiter``. +- New method ``ShapeFilter.rejects_collision()`` that checks if the filter + would reject a collision with another filter. +- New method ``Vec2d.polar_tuple`` that return the vector as polar coordinates +- Changed type of ``PointQueryInfo.shape``, ``SegmentQueryInfo.shape`` and + ``ShapeQueryInfo.shape`` to not be ``Optional``, they will always have a shape. - Build and publish wheels for Linux ARM and Pypy 3.11 -- Changed type of PointQueryInfo.shape, SegmentQueryInfo.shape and ShapeQueryInfo.shape to not be Optional, they will always have a shape. - -Other improvements -- Optimized Vec2d.angle and Vec2d.angle_degrees. Note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently. -- Fixed issue with accessing Body.space after space is deleted and GCed. -- Improved documentation in many places (Vec2d, Poly, Shape and more) -- Internal cleanup of code - -Extra thanks for aatle for a number of suggestions for improvements in this Pymunk release +Other improvements: +- Optimized ``Vec2d.angle`` and ``Vec2d.angle_degrees``. Note that the + optimized versions treat ``0`` length vectors with ``x`` and/or ``y`` equal + to ``-0`` slightly differently. +- Fixed issue with accessing ``Body.space`` after the ``space`` is deleted and + GCed. +- Improved documentation in many places (``Vec2d``, ``Poly``, ``Shape`` and + more) +- Internal cleanup of code Pymunk 6.11.1 (2025-02-09) diff --git a/docs/src/tutorials/SlideAndPinJoint.rst b/docs/src/tutorials/SlideAndPinJoint.rst index 54c91df9..93aa1f05 100644 --- a/docs/src/tutorials/SlideAndPinJoint.rst +++ b/docs/src/tutorials/SlideAndPinJoint.rst @@ -2,10 +2,10 @@ Slide and Pin Joint Demo Step by Step ************************************* -This is a step by step tutorial explaining the demo slide_and_pinjoint.py -included in pymunk. You will find a screenshot of it in the list of +This is a step-by-step tutorial explaining the demo slide_and_pinjoint.py +included in Pymunk. You will find a screenshot of it in the list of :ref:`examples `. -It is probably a good idea to have the file near by if I +It is probably a good idea to have the file nearby if I miss something in the tutorial or something is unclear. .. image :: /_static/tutorials/slide_and_pinjoint.png @@ -20,18 +20,18 @@ For this tutorial you will need: * Pymunk Pygame is required for this tutorial and some of the included demos, but it -is not required to run just pymunk. Pymunk should work just fine with other +is not required to run just Pymunk. Pymunk should work just fine with other similar libraries as well, for example you could easily translate this tutorial to use Pyglet instead. -Pymunk is built on top of the 2d physics library Chipmunk. Chipmunk itself +Pymunk is built on top of the 2d physics library Munk2D. Munk2D itself is written in C meaning Pymunk need to call into the c code. The Cffi library helps with this, however if you are on a platform that I haven't been able to compile it on you might have to do it yourself. The good news is that -it is very easy to do, in fact if you got Pymunk by Pip install its arelady +it is very easy to do, in fact if you got Pymunk by Pip install it's already done! -When you have pymunk installed, try to import it from the python prompt to +When you have Pymunk installed, try to import it from the python prompt to make sure it works and can be imported:: >>> import pymunk @@ -39,7 +39,7 @@ make sure it works and can be imported:: More information on installation can be found here: :ref:`Installation ` -If it doesnt work or you have some kind of problem, feel free to write a post +If it doesn't work or you have some kind of problem, feel free to write a post in the chipmunk forum, contact me directly or add your problem to the issue tracker: :ref:`Contact & Support ` @@ -47,8 +47,8 @@ An empty simulation ======================= Ok, lets start. -Chipmunk (and therefore Pymunk) has a couple of central concepts, which is -explained pretty good in this citation from the Chipmunk docs: +Munk2D (and therefore Pymunk) has a couple of central concepts, which is +explained pretty good in this citation from the Munk2D docs: Rigid bodies A rigid body holds the physical properties of an object. (mass, position, @@ -65,11 +65,10 @@ Constraints/joints You can attach joints between two bodies to constrain their behavior. Spaces - Spaces are the basic simulation unit in Chipmunk. You add bodies, shapes + Spaces are the basic simulation unit in Munk2D. You add bodies, shapes and joints to a space, and then update the space as a whole. -The documentation for Chipmunk can be found here: -http://chipmunk-physics.net/release/ChipmunkLatest-Docs/ +The documentation for Munk2D can be found here: https://viblo.github.io/Munk2D/ It is for the c-library but is a good complement to the Pymunk documentation as the concepts are the same, just that Pymunk is more pythonic to use. @@ -84,7 +83,7 @@ Anyway, we are now ready to write some code:: def main(): pygame.init() screen = pygame.display.set_mode((600, 600)) - pygame.display.set_caption("Joints. Just wait and the L will tip over") + pygame.display.set_caption("Joints. Just wait, and the L will tip over") clock = pygame.time.Clock() space = pymunk.Space() #2 @@ -110,11 +109,11 @@ Anyway, we are now ready to write some code:: The code will display a blank window, and will run a physics simulation of an empty space. -1. We need to import pymunk in order to use it... +1. We need to import Pymunk in order to use it... 2. We then create a space and set its gravity to something good. Remember that what is important is what looks good on screen, not what the real - world value is. 900 will make a good looking simulation, but feel free + world value is. 900 will make a good-looking simulation, but feel free to experiment when you have the full code ready. 3. In our game loop we call the step() function on our space. The step @@ -122,16 +121,16 @@ empty space. .. Note:: It is best to keep the step size constant and not adjust it depending on the - framerate. The physic simulation will work much better with a constant step + frame rate. The physic simulation will work much better with a constant step size. Falling balls ============= -The easiest shape to handle (and draw) is the circle. Therefore our next -step is to make a ball spawn once in while. In many of the example demos all +The easiest shape to handle (and draw) is the circle. Therefore, our next +step is to make a ball spawn once in a while. In many of the example demos all code is in one big pile in the main() function as they are so small and easy, -but I will extract some methods in this tutorial to make it more easy to +but I will extract some methods in this tutorial to make it easier to follow. First, a function to add a ball to a space:: def add_ball(space): @@ -159,22 +158,22 @@ follow. First, a function to add a ball to a space:: easiest to let Pymunk handle calculation from shapes. So we set the mass of each shape, and then when added to space the body will automatically get a proper mass and moment set. Another option is to set the density of each - shape, or its also possible to set the values directly on the body (or - even adjust them afterwards). + shape, or it's also possible to set the values directly on the body (or + even adjust them afterward). 5. To make the balls roll we set friction on the shape. (By default its 0). -6. Finally we add the body and shape to the space to include it in our +6. Finally, we add the body and shape to the space to include it in our simulation. Note that the body must always be added to the space before or at the same time as any shapes attached to it. Now that we can create balls we want to display them. Either we can use the -built in pymunk_util package do draw the whole space directly, or we can do it -manually. The debug drawing functions included with Pymunk are good for putting -something together easy and quickly, while for example a polished game most -probably will want to make its own drawing code. +built-in ``pymunk_util`` package do draw the whole space directly, or we can +do it manually. The debug drawing functions included with Pymunk are good for +putting something together easy and quickly, while for example a polished game +most probably will want to make its own drawing code. -If we want to draw manually, our draw function could look something like this:: +If we want to draw manually, our draw function could look something like this:: def draw_ball(screen, ball): p = int(ball.body.position.x), int(ball.body.position.y) @@ -186,9 +185,9 @@ called balls):: for ball in balls: draw_ball(screen, ball) -However, as we use pygame in this example we can instead use the debug_draw +However, as we use Pygame in this example we can instead use the ``debug_draw`` method already included in Pymunk to simplify a bit. It first needs to be -imported, and next we have to create a DrawOptions object with the options +imported, and next we have to create a ``DrawOptions`` object with the options (what surface to draw on in the case of Pygame):: import pymunk.pygame_util @@ -198,7 +197,7 @@ imported, and next we have to create a DrawOptions object with the options And after that when we want to draw all our shapes we would just do it in this way:: - space.debug_draw(draw_options) + space.debug_draw(draw_options) Most of the examples included with Pymunk uses this way of drawing. @@ -218,7 +217,7 @@ balls you should see a couple of balls falling. Yay! def main(): pygame.init() screen = pygame.display.set_mode((600, 600)) - pygame.display.set_caption("Joints. Just wait and the L will tip over") + pygame.display.set_caption("Joints. Just wait, and the L will tip over") clock = pygame.time.Clock() space = pymunk.Space() @@ -244,21 +243,21 @@ balls you should see a couple of balls falling. Yay! space.step(1/50.0) - screen.fill((255,255,255)) + screen.fill((255,255,255)) space.debug_draw(draw_options) pygame.display.flip() clock.tick(50) if __name__ == '__main__': - main() + main() A static L ========== Falling balls are quite boring. We don't see any physics simulation except basic gravity, and everyone can do gravity without help from a physics library. -So lets add something the balls can land on, two static lines forming an L. As +So let's add something the balls can land on, two static lines forming an L. As with the balls we start with a function to add an L to the space:: def add_static_L(space): @@ -267,23 +266,23 @@ with the balls we start with a function to add an L to the space:: l1 = pymunk.Segment(body, (-150, 0), (255, 0), 5) # 2 l2 = pymunk.Segment(body, (-150, 0), (-150, -50), 5) l1.friction = 1 # 3 - l2.friction = 1 + l2.friction = 1 space.add(body, l1, l2) # 4 return l1,l2 1. We create a "static" body. The important step is to never add it to the space like the dynamic ball bodies. Note how static bodies are created by - setting the body_type of the body. Many times its easier to use the + setting the body_type of the body. Many times it's easier to use the already existing static body in the space (`space.static_body`), but we will make the L shape dynamic in just a little bit. 2. A line shaped shape is created here. 3. Set the friction. 4. Again, we only add the segments, not the body to the space. -Since we use Space.debug_draw to draw the space we dont need to do any special -draw code for the Segments, but I still include a possible draw function here -just to show what it could look like:: +Since we use ``Space.debug_draw`` to draw the space we dont need to do any +special draw code for the Segments, but I still include a possible draw +function here just to show what it could look like:: def draw_lines(screen, lines): for line in lines: @@ -295,18 +294,18 @@ just to show what it could look like:: pygame.draw.lines(screen, THECOLORS["lightgray"], False, [p1,p2]) 1. In order to get the position with the line rotation we use this calculation. - line.a is the first endpoint of the line, line.b the second. At the moment - the lines are static, and not rotated so we don't really have to do this - extra calculation, but we will soon make them move and rotate. + ``line.a`` is the first endpoint of the line, ``line.b`` the second. At the + moment the lines are static, and not rotated so we don't really have to do + this extra calculation, but we will soon make them move and rotate. -2. This is a little function to convert coordinates from pymunk to pygame - world. Now that we have it we can use it in the draw_ball() function as +2. This is a little function to convert coordinates from Pymunk to Pygame + world. Now that we have it we can use it in the ``draw_ball()`` function as well. :: def to_pygame(p): - """Small helper to convert pymunk vec2d to pygame integers""" + """Small helper to convert Pymunk vec2d to Pygame integers""" return round(p.x), round(p.y) @@ -354,7 +353,7 @@ an inverted L shape in the middle will balls spawning and hitting the shape. space.step(1/50.0) - screen.fill((255,255,255)) + screen.fill((255,255,255)) space.debug_draw(draw_options) pygame.display.flip() @@ -367,7 +366,7 @@ an inverted L shape in the middle will balls spawning and hitting the shape. Joints (1) ============== -A static L shape is pretty boring. So lets make it a bit more exciting by +A static L shape is pretty boring. So let's make it a bit more exciting by adding two joints, one that it can rotate around, and one that prevents it from rotating too much. In this part we only add the rotation joint, and in the next we constrain it. As our static L shape won't be static anymore we also rename @@ -393,7 +392,7 @@ the function to add_L(). :: return l1, l2 1. This is the rotation center body. Its only purpose is to act as a static - point in the joint so the line can rotate around it. As you see we never add + point in the joint, so the line can rotate around it. As you see we never add any shapes to it. 2. The L shape will now be moving in the world, and therefor it can no longer @@ -408,7 +407,7 @@ the function to add_L(). :: Joints (2) ============== -In the previous part we added a pin joint, and now its time to constrain the +In the previous part we added a pin joint, and now it's time to constrain the rotating L shape to create a more interesting simulation. In order to do this we modify the add_L() function:: @@ -446,7 +445,7 @@ Ending ====== You might notice that we never delete balls. This will make the simulation -require more and more memory and use more and more cpu, and this is of course +require more and more memory and use more and more CPU, and this is of course not what we want. So in the final step we add some code to remove balls from the simulation when they are bellow the screen. :: @@ -459,21 +458,21 @@ the simulation when they are bellow the screen. :: space.remove(ball, ball.body) # 3 balls.remove(ball) # 4 -1. Loop the balls and check if the body.position is less than 0. +1. Loop the balls and check if the ``body.position`` is less than ``0``. 2. If that is the case, we add it to our list of balls to remove. 3. To remove an object from the space, we need to remove its shape and its body. 4. And then we remove it from our list of balls. And now, done! You should have an inverted L shape in the middle of the screen -being filled will balls, tipping over releasing them, tipping back and start -over. You can check slide_and_pinjoint.py included in pymunk, but it +being filled with balls, tipping over releasing them, tipping back and start +over. You can check slide_and_pinjoint.py included in Pymunk, but it doesn't follow this tutorial exactly as I factored out a couple of blocks to functions to make it easier to follow in tutorial form. -If anything is unclear, not working feel free to raise an issue on github. If +If anything is unclear, not working feel free to raise an issue on Github. If you have an idea for another tutorial you want to read, or some example code -you want to see included in pymunk, please write it somewhere (like in the +you want to see included in Pymunk, please write it somewhere (like in the chipmunk forum) The full code for this tutorial is:: @@ -488,11 +487,11 @@ The full code for this tutorial is:: """Add a ball to the given space at a random position""" mass = 3 radius = 25 - inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) - body = pymunk.Body(mass, inertia) + body = pymunk.Body() x = random.randint(120,300) body.position = x, 50 shape = pymunk.Circle(body, radius, (0,0)) + shape.mass shape.friction = 1 space.add(body, shape) return shape From b4a0690335d4dd1c9f33e0bb723360f029dd1466 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Sun, 25 May 2025 23:10:15 +0200 Subject: [PATCH 74/80] Minor fixes to docs of new features --- README.rst | 2 +- pymunk/__init__.py | 4 ++++ pymunk/_version.py | 4 ++-- pymunk/arbiter.py | 19 ++++++++++-------- pymunk/space.py | 50 ++++++++++++++++++++++++++++++++-------------- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index da67a75b..d6d4acf0 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ the Pymunk webpage for some examples. 2007 - 2025, Victor Blomqvist - vb@viblo.se, MIT License This release is based on the latest Pymunk release (7.0.0), -using Munk2D 1.0 rev 9c3026aded65c439d64d1cb4fa537544fa8a3989. +using Munk2D 1.0 rev fc7ecea12aad22df30f89f7cfc0b6aa271f864ee. Installation diff --git a/pymunk/__init__.py b/pymunk/__init__.py index 2bd405d8..29288e06 100644 --- a/pymunk/__init__.py +++ b/pymunk/__init__.py @@ -54,6 +54,10 @@ "moment_for_poly", "moment_for_segment", "moment_for_box", + "area_for_circle", + "area_for_segment", + "area_for_poly", + "empty_callback", "SegmentQueryInfo", "ContactPoint", "ContactPointSet", diff --git a/pymunk/_version.py b/pymunk/_version.py index 1d22d1f4..ff186655 100644 --- a/pymunk/_version.py +++ b/pymunk/_version.py @@ -32,9 +32,9 @@ cp = _chipmunk_cffi.lib ffi = _chipmunk_cffi.ffi -version = "7.0.0" +version = "1.0.0" chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), - "9c3026aded65c439d64d1cb4fa537544fa8a3989", + "fc7ecea12aad22df30f89f7cfc0b6aa271f864ee", ) diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index add66346..a78708ef 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -43,14 +43,17 @@ def __init__(self, _arbiter: ffi.CData, space: "Space") -> None: def process_collision(self) -> bool: """Decides if the collision should be processed or rejected. - Set this during a begin() or pre_solve() callback to override - the default (True) value. - - Set this to true to process the collision normally or - false to cause pymunk to ignore the collision entirely. If you set it to - false from a `begin` callback, the `pre_solve` and `post_solve` callbacks will never be run, - but you will still recieve a separate event when the shapes stop - overlapping. + Set this during a `begin()` or `pre_solve()` callback to override + the default (`True`) value. + + Set this to `true` to process the collision normally or + `false` to cause Pymunk to ignore the collision entirely. Note that + while `post_solve` might be skipped if this is `false`, `separate` + will always be called when the shapes stop overlapping. + + .. note:: + No collision will be processed for a sensor Shape, or a Shape + attached to a STATIC or KINEMATIC Body. """ return lib.cpArbiterGetProcessCollision(self._arbiter) diff --git a/pymunk/space.py b/pymunk/space.py index 857f5c11..a5b4955a 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -596,28 +596,48 @@ def on_collision( separate: Optional[_CollisionCallback] = None, data: Any = None, ) -> None: - """Set callbacks that will be called during the 4 phases of collision handling. - - Use None to indicate any collision_type. - - Fill the desired collision callback functions, for details see the - :py:class:`CollisionHandler` object. + """Set callbacks that will be called during the 4 phases of collision + handling. Whenever shapes with collision types (:py:attr:`Shape.collision_type`) - a and b collide, this handler will be used to process the collision - events. If no handler is set, the default is to process collision as + a and b collide, the callback will be used to process the collision + events. If no callback is set, the default is to process collision normally. - If multiple handlers match the collision, the order will be that the - most specific handler is called first. + Its possible to pass in None for one or both of the collision types. + None matches any collision type on a Shape. - Note that if a handler already exist for the a,b pair, that existing - handler will be returned. + If you call this multiple times with the same combination of + collision_type_a and collision_type_b, then the last call will + overwrite the earlier. + + If multiple callbacks match the collision, the order will be that the + most specific handler is called first. - :param int collision_type_a: Collision type a - :param int collision_type_b: Collision type b + Callback phases: + + - **begin**: Two shapes just started touching for the first time + this step. + - **pre_solve**: Two shapes are touching during this step, before + collision resolution. You may override collision values using + Arbiter.friction, Arbiter.elasticity or Arbiter.surfaceVelocity + to provide custom friction, elasticity, or surface velocity + values. See Arbiter for more info. + - **post_solve**: Two shapes are touching and their collision response + has been processed. You can retrieve the collision impulse or + kinetic energy at this time if you want to use it to calculate + sound volumes or damage amounts. See Arbiter for more info. + - **separate**: Two shapes have just stopped touching for the + first time this step. To ensure that begin()/separate() are + always called in balanced pairs, it will also be called when + removing a shape while its in contact with something or when + de-allocating the space. + + From each callback you can set process_collision on the Arbiter, + which decides if the collision should be processed or not. + + data will be passed in to the callback function unchanged. - :rtype: :py:class:`CollisionHandler` """ # key = min(collision_type_a, collision_type_b), max( # collision_type_a, collision_type_b From ef9e787c72aeb22c981743e72b3cb5a6b23d4e5f Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 26 May 2025 21:31:22 +0200 Subject: [PATCH 75/80] Dont allow on_collision(None, 1). Better assert for non-inf mass. --- CHANGELOG.rst | 1 + Munk2D | 2 +- benchmarks/pymunk-collision-callback.py | 2 +- pymunk/body.py | 4 ++-- pymunk/space.py | 8 +++++--- pymunk/tests/test_body.py | 3 +++ pymunk/tests/test_space.py | 3 ++- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 218bb7f2..27f2bae7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -94,6 +94,7 @@ Other improvements: GCed. - Improved documentation in many places (``Vec2d``, ``Poly``, ``Shape`` and more) +- Improved assert for ``body.mass`` sanity check. - Internal cleanup of code diff --git a/Munk2D b/Munk2D index fc7ecea1..0cdb2591 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit fc7ecea12aad22df30f89f7cfc0b6aa271f864ee +Subproject commit 0cdb25912be93b0b944ada998e85f32b58b3c889 diff --git a/benchmarks/pymunk-collision-callback.py b/benchmarks/pymunk-collision-callback.py index 07ed04c6..9724ca23 100644 --- a/benchmarks/pymunk-collision-callback.py +++ b/benchmarks/pymunk-collision-callback.py @@ -8,7 +8,7 @@ b = pymunk.Body(1,10) c = pymunk.Circle(b, 5) s.add(b, c) -h = s.add_collision_handler(None, None) +h = s.on_collision(None, None) def f(arb, s, data): return False h.pre_solve = f diff --git a/pymunk/body.py b/pymunk/body.py index 8f7135a4..320a8dc1 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -252,8 +252,8 @@ def mass(self) -> float: @mass.setter def mass(self, mass: float) -> None: - assert ( - self.space is None or mass > 0 + assert self.space is None or 0 < mass < float( + "inf" ), "Dynamic bodies must have mass > 0 if they are attached to a Space." lib.cpBodySetMass(self._body, mass) diff --git a/pymunk/space.py b/pymunk/space.py index a5b4955a..63111542 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -605,7 +605,8 @@ def on_collision( normally. Its possible to pass in None for one or both of the collision types. - None matches any collision type on a Shape. + None matches any collision type on a Shape. However, if + collision_type_a is None, then collision_type_b must also be None. If you call this multiple times with the same combination of collision_type_a and collision_type_b, then the last call will @@ -642,8 +643,9 @@ def on_collision( # key = min(collision_type_a, collision_type_b), max( # collision_type_a, collision_type_b # ) - if collision_type_a == None and collision_type_b != None: - collision_type_b, collision_type_a = collision_type_a, collision_type_b + assert ( + collision_type_a != None or collision_type_b == None + ), "collision_type_a can not be None if collision_type_b is not None. Please swap them." key = collision_type_a, collision_type_b if key not in self._handlers: diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index 20b6ab10..b7e5b4fa 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -181,6 +181,9 @@ def test_mass(self) -> None: with self.assertRaises(AssertionError): b.mass = 0 + with self.assertRaises(AssertionError): + b.mass = float("inf") + s.remove(b) b.mass = 0 s.add(b) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 14d50264..9b146957 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -914,7 +914,8 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: d["space"] = space # type: ignore s.on_collision(1, None, pre_solve=pre_solve) - s.on_collision(None, 1, pre_solve=pre_solve) + with self.assertRaises(AssertionError): + s.on_collision(None, 1, pre_solve=pre_solve) s.step(0.1) From a675cfca33f6f631fd34f9ba20d1286dde0d9a4e Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 26 May 2025 21:44:47 +0200 Subject: [PATCH 76/80] Assert body.moment > 0 when added to space --- CHANGELOG.rst | 5 ++++- pymunk/body.py | 12 +++++++++--- pymunk/space.py | 4 ++-- pymunk/tests/test_body.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 27f2bae7..7b6b43a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -94,7 +94,10 @@ Other improvements: GCed. - Improved documentation in many places (``Vec2d``, ``Poly``, ``Shape`` and more) -- Improved assert for ``body.mass`` sanity check. +- Improved assert for ``body.mass`` sanity check (``0 < mass < math.inf``) for + dynamic bodies added to a space. +- Improved assert for ``body.moment`` sanity check (``0 < moment``) for dynamic + bodies added to a space. - Internal cleanup of code diff --git a/pymunk/body.py b/pymunk/body.py index 320a8dc1..18281529 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -1,5 +1,6 @@ __docformat__ = "reStructuredText" +import math import weakref from collections.abc import KeysView from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional # Literal, @@ -252,8 +253,8 @@ def mass(self) -> float: @mass.setter def mass(self, mass: float) -> None: - assert self.space is None or 0 < mass < float( - "inf" + assert ( + self.space is None or 0 < mass < math.inf ), "Dynamic bodies must have mass > 0 if they are attached to a Space." lib.cpBodySetMass(self._body, mass) @@ -261,12 +262,17 @@ def mass(self, mass: float) -> None: def moment(self) -> float: """Moment of inertia (MoI or sometimes just moment) of the body. - The moment is like the rotational mass of a body. + The moment is like the rotational mass of a body. Note that it is + valid to set moment to float('inf'). This will make a body that cannot + rotate. """ return lib.cpBodyGetMoment(self._body) @moment.setter def moment(self, moment: float) -> None: + assert ( + self.space is None or moment > 0 + ), "Dynamic bodies must have moment > 0 if they are attached to a Space" lib.cpBodySetMoment(self._body, moment) def _set_position(self, pos: tuple[float, float]) -> None: diff --git a/pymunk/space.py b/pymunk/space.py index 63111542..185ec0ed 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -562,8 +562,8 @@ def step(self, dt: float) -> None: for b in self._bodies_to_check: assert b.body_type != Body.DYNAMIC or ( - b.mass > 0 and b.mass < math.inf - ), f"Dynamic bodies must have a mass > 0 and < inf. {b} has mass {b.mass}." + b.mass > 0 and b.mass < math.inf and b.moment > 0 + ), f"Dynamic bodies must have a mass > 0 and < inf and moment > 0. {b} has mass {b.mass}, moment {b.moment}." self._bodies_to_check.clear() try: diff --git a/pymunk/tests/test_body.py b/pymunk/tests/test_body.py index b7e5b4fa..c3ad57e5 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -1,3 +1,4 @@ +import math import pickle import unittest @@ -182,7 +183,7 @@ def test_mass(self) -> None: b.mass = 0 with self.assertRaises(AssertionError): - b.mass = float("inf") + b.mass = math.inf s.remove(b) b.mass = 0 @@ -202,6 +203,39 @@ def test_mass(self) -> None: c.density = 10 s.step(1) + def test_moment(self) -> None: + s = p.Space() + b = p.Body() + + b.mass = 2 + b.moment = 3 + s.add(b) + + # Cant set 0 moment on Body in Space + with self.assertRaises(AssertionError): + b.moment = 0 + + # inf moment is fine + b.moment = math.inf + + s.remove(b) + b.moment = 0 + s.add(b) + # Cant add 0 moment Body to Space and run step + with self.assertRaises(AssertionError): + s.step(1) + + c = p.Circle(b, 1) + s.add(c) + + # Same with a Shape + with self.assertRaises(AssertionError): + s.step(1) + + # Setting the Shape density should fix it + c.density = 10 + s.step(1) + def test_mass_moment_from_shape(self) -> None: s = p.Space() From ee6fce4ec48c75d526ff25b383731abf7c554fb5 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 26 May 2025 21:49:50 +0200 Subject: [PATCH 77/80] minor doc fixes --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7b6b43a8..98af5f8f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ ========= Changelog ========= -Pymunk 7.0.0 (2025-04-10) +Pymunk 7.0.0 (2025-05-26) ------------------------- **Many improvements and breaking changes!** From abdaab3d25a65e5d47d6aee01c9c62779c4558d8 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Mon, 26 May 2025 21:58:11 +0200 Subject: [PATCH 78/80] fix callback benchmark --- benchmarks/pymunk-collision-callback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmarks/pymunk-collision-callback.py b/benchmarks/pymunk-collision-callback.py index 9724ca23..327cebe4 100644 --- a/benchmarks/pymunk-collision-callback.py +++ b/benchmarks/pymunk-collision-callback.py @@ -8,10 +8,9 @@ b = pymunk.Body(1,10) c = pymunk.Circle(b, 5) s.add(b, c) -h = s.on_collision(None, None) def f(arb, s, data): return False -h.pre_solve = f +s.on_collision(pre_solve=f) """ print(min(timeit.repeat("s.step(0.01)", setup=s, repeat=10))) From 6af8524e917c61b77f005ba978d14890e68b41a5 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 27 May 2025 21:17:48 +0200 Subject: [PATCH 79/80] fix broken test --- pymunk/tests/test_common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pymunk/tests/test_common.py b/pymunk/tests/test_common.py index 160591eb..f427374b 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -137,14 +137,14 @@ def remove_first(arbiter: p.Arbiter, space: p.Space, data: Any) -> None: def testX(self) -> None: space = p.Space() - b1 = p.Body(1) + b1 = p.Body(1, 1) c1 = p.Circle(b1, 10) c1.collision_type = 2 - b2 = p.Body(1) + b2 = p.Body(1, 2) c2 = p.Circle(b2, 10) - b3 = p.Body(1) + b3 = p.Body(1, 3) c3 = p.Circle(b3, 10) # b1.position = 0, 0 From 92f6e5cd7535aad0da25df48cda88b510aaa7ba4 Mon Sep 17 00:00:00 2001 From: Victor Blomqvist Date: Tue, 27 May 2025 22:13:07 +0200 Subject: [PATCH 80/80] Prep 7.0.0 --- CHANGELOG.rst | 3 ++- CITATION.cff | 2 +- Munk2D | 2 +- README.rst | 2 +- pymunk/_version.py | 4 ++-- pyproject.toml | 1 - 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 98af5f8f..4b071a00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,8 @@ ========= Changelog ========= -Pymunk 7.0.0 (2025-05-26) + +Pymunk 7.0.0 (2025-05-28) ------------------------- **Many improvements and breaking changes!** diff --git a/CITATION.cff b/CITATION.cff index afb2cc18..cf6ef19b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,5 +6,5 @@ authors: title: "Pymunk" abstract: "A easy-to-use pythonic rigid body 2d physics library" version: 7.0.0 -date-released: 2025-02-09 +date-released: 2025-05-28 url: "https://pymunk.org" diff --git a/Munk2D b/Munk2D index 0cdb2591..5ef74989 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit 0cdb25912be93b0b944ada998e85f32b58b3c889 +Subproject commit 5ef7498946f0e956f294cb3fea283626921e4128 diff --git a/README.rst b/README.rst index d6d4acf0..2657230e 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ the Pymunk webpage for some examples. 2007 - 2025, Victor Blomqvist - vb@viblo.se, MIT License This release is based on the latest Pymunk release (7.0.0), -using Munk2D 1.0 rev fc7ecea12aad22df30f89f7cfc0b6aa271f864ee. +using Munk2D 2.0 rev 5ef7498946f0e956f294cb3fea283626921e4128. Installation diff --git a/pymunk/_version.py b/pymunk/_version.py index ff186655..ef814cd8 100644 --- a/pymunk/_version.py +++ b/pymunk/_version.py @@ -32,9 +32,9 @@ cp = _chipmunk_cffi.lib ffi = _chipmunk_cffi.ffi -version = "1.0.0" +version = "2.0.0" chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), - "fc7ecea12aad22df30f89f7cfc0b6aa271f864ee", + "5ef7498946f0e956f294cb3fea283626921e4128", ) diff --git a/pyproject.toml b/pyproject.toml index 598a0c1d..af743e02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools", - # "setuptools<74; platform_system=='Windows' and implementation_name=='pypy'", "wheel", "cffi >= 1.17.1; platform_system != 'Emscripten'", "cffi > 1.14.0; platform_system == 'Emscripten'",