diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d15503f5..c1481c6c 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-* 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.2 env: CIBW_PLATFORM: pyodide PYMUNK_BUILD_SLIM: 1 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/.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/CHANGELOG.rst b/CHANGELOG.rst index 8ffcc1a6..4b071a00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,106 @@ Changelog ========= +Pymunk 7.0.0 (2025-05-28) +------------------------- + +**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 ``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 + + +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) +- 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 + + Pymunk 6.11.1 (2025-02-09) -------------------------- @@ -11,6 +111,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 @@ -25,8 +126,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/CITATION.cff b/CITATION.cff index 257706bf..cf6ef19b 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 -date-released: 2025-02-09 +version: 7.0.0 +date-released: 2025-05-28 url: "https://pymunk.org" 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/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/Munk2D b/Munk2D new file mode 160000 index 00000000..5ef74989 --- /dev/null +++ b/Munk2D @@ -0,0 +1 @@ +Subproject commit 5ef7498946f0e956f294cb3fea283626921e4128 diff --git a/README.rst b/README.rst index 054893d6..2657230e 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 2.0 rev 5ef7498946f0e956f294cb3fea283626921e4128. Installation @@ -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/TODO.txt b/TODO.txt index 071e83c0..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? @@ -54,6 +53,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/benchmarks/pymunk-collision-callback.py b/benchmarks/pymunk-collision-callback.py index 66c0c320..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.add_default_collision_handler() 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))) 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). 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 diff --git a/pymunk/__init__.py b/pymunk/__init__.py index 59359ac3..29288e06 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.) """ @@ -54,11 +54,14 @@ "moment_for_poly", "moment_for_segment", "moment_for_box", + "area_for_circle", + "area_for_segment", + "area_for_poly", + "empty_callback", "SegmentQueryInfo", "ContactPoint", "ContactPointSet", "Arbiter", - "CollisionHandler", "BB", "ShapeFilter", "Transform", @@ -68,7 +71,7 @@ "Vec2d", ] -from typing import Sequence, Tuple, cast +from typing import Any, Sequence, cast from . import _chipmunk_cffi @@ -80,7 +83,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 @@ -119,7 +121,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 +133,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 +145,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 +156,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 +176,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 +188,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. @@ -195,4 +197,14 @@ def area_for_poly(vertices: Sequence[Tuple[float, float]], radius: float = 0) -> return cp.cpAreaForPoly(len(vs), vs, radius) +def empty_callback(*args: Any, **kwargs: Any) -> 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/_callbacks.py b/pymunk/_callbacks.py index 04397d75..a34c71c1 100644 --- a/pymunk/_callbacks.py +++ b/pymunk/_callbacks.py @@ -1,11 +1,11 @@ import logging import math -import warnings from ._chipmunk_cffi import ffi, lib 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__) @@ -21,8 +21,9 @@ def ext_cpSpacePointQueryFunc( gradient: 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 != None p = PointQueryInfo( shape, Vec2d(point.x, point.y), distance, Vec2d(gradient.x, gradient.y) ) @@ -37,8 +38,9 @@ def ext_cpSpaceSegmentQueryFunc( alpha: float, 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 != None p = SegmentQueryInfo( shape, Vec2d(point.x, point.y), Vec2d(normal.x, normal.y), alpha ) @@ -47,8 +49,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) @@ -57,8 +59,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) - found_shape = self._get_shape(_shape) + _, 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) @@ -171,8 +174,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) @@ -198,51 +201,19 @@ 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["begin"]) @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__, + handler._pre_solve( + Arbiter(_arb, handler._space), handler._space, handler.data["pre_solve"] ) - return True @ffi.def_extern() @@ -250,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.data) + handler._post_solve( + Arbiter(_arb, handler._space), handler._space, handler.data["post_solve"] + ) @ffi.def_extern() @@ -265,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.data) + handler._separate( + Arbiter(_arb, handler._space), handler._space, handler.data["separate"] + ) finally: handler._space._locked = orig_locked @@ -290,8 +265,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/collision_handler.py b/pymunk/_collision_handler.py similarity index 53% rename from pymunk/collision_handler.py rename to pymunk/_collision_handler.py index 6f47c54c..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, Dict, Optional +from typing import TYPE_CHECKING, Any, Callable, Optional if TYPE_CHECKING: from .space import Space @@ -8,8 +8,7 @@ from ._chipmunk_cffi import ffi, lib from .arbiter import Arbiter -_CollisionCallbackBool = Callable[[Arbiter, "Space", Any], bool] -_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Any], None] +_CollisionCallback = Callable[[Arbiter, "Space", Any], None] class CollisionHandler(object): @@ -42,27 +41,15 @@ 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: 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] = {} - - 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 + 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. @@ -72,62 +59,54 @@ def data(self) -> Dict[Any, Any]: """ return self._data - def _set_begin(self, func: Callable[[Arbiter, "Space", Any], bool]) -> 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) -> Optional[_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 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: Optional[_CollisionCallback]) -> None: + self._begin = func - def _get_pre_solve(self) -> Optional[Callable[[Arbiter, "Space", Any], bool]]: - return self._pre_solve + if self._begin == None: + self._handler.beginFunc = ffi.addressof(lib, "DoNothing") + 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) -> Optional[_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. - """, - ) - - 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: Optional[_CollisionCallback]) -> None: + self._pre_solve = func - def _get_post_solve(self) -> Optional[_CollisionCallbackNoReturn]: - return self._post_solve + if self._pre_solve == None: + self._handler.preSolveFunc = ffi.addressof(lib, "DoNothing") + 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) -> Optional[_CollisionCallback]: + """Two shapes are touching and their collision response has been processed. ``func(arbiter, space, data)`` @@ -135,20 +114,21 @@ 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: Optional[_CollisionCallback]) -> None: + self._post_solve = func - def _get_separate(self) -> Optional[_CollisionCallbackNoReturn]: - return self._separate + if self._post_solve == None: + 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 + @property + def separate(self) -> Optional[_CollisionCallback]: + """Two shapes have just stopped touching for the first time this step. ``func(arbiter, space, data)`` @@ -156,5 +136,14 @@ 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: Optional[_CollisionCallback]) -> None: + self._separate = func + + if self._separate == None: + self._handler.separateFunc = ffi.addressof(lib, "DoNothing") + else: + self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc 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..c1d76580 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 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 @@ -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/_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/_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/_version.py b/pymunk/_version.py index e387bfe9..ef814cd8 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__. """ @@ -32,9 +32,9 @@ cp = _chipmunk_cffi.lib ffi = _chipmunk_cffi.ffi -version = "6.11.1" +version = "2.0.0" chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), - "dfc2fb8ca023ce6376fa2cf4a7f91c92ee08a970", + "5ef7498946f0e956f294cb3fea283626921e4128", ) diff --git a/pymunk/_weakkeysview.py b/pymunk/_weakkeysview.py new file mode 100644 index 00000000..1171fd54 --- /dev/null +++ b/pymunk/_weakkeysview.py @@ -0,0 +1,24 @@ +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: object) -> bool: + return key in self._weak_dict + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({list(self._weak_dict.keys())})" diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index 19882b02..a78708ef 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -1,14 +1,15 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, Tuple, Dict, List, Any, Iterable, Sequence +from typing import TYPE_CHECKING, Any, Sequence if TYPE_CHECKING: from .space import Space - from .shapes import Shape + from .body import Body from ._chipmunk_cffi import ffi, lib from .contact_point_set import ContactPointSet +from .shapes import Shape from .vec2d import Vec2d @@ -38,11 +39,40 @@ def __init__(self, _arbiter: ffi.CData, space: "Space") -> None: self._arbiter = _arbiter self._space = space - def _get_contact_point_set(self) -> ContactPointSet: + @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. 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) + + @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 + 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,17 +93,26 @@ 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 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"]: + def shapes(self) -> tuple["Shape", "Shape"]: """Get the shapes in the order that they were defined in the collision handler associated with this arbiter """ @@ -82,51 +121,45 @@ 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 - def _get_restitution(self) -> float: + @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 _set_restitution(self, restitution: float) -> None: + @restitution.setter + def restitution(self, restitution: float) -> None: lib.cpArbiterSetRestitution(self._arbiter, restitution) - restitution = property( - _get_restitution, - _set_restitution, - doc="""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. - """, - ) + @property + def friction(self) -> float: + """The calculated friction for this collision pair. - def _get_friction(self) -> float: + 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) - def _set_friction(self, friction: float) -> None: + @friction.setter + def friction(self, friction: float) -> None: lib.cpArbiterSetFriction(self._arbiter, friction) - 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. - """, - ) - 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( @@ -193,14 +226,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 @@ -215,7 +248,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]]) -> ffi.CData: _contacts = lib.cpContactArrAlloc(len(ds)) for i in range(len(ds)): _contact = _contacts[i] @@ -235,7 +268,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 @@ -255,7 +288,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 2924f199..bf8603a0 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,14 +31,15 @@ ... 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 +from typing import TYPE_CHECKING, Callable, Sequence, Union, overload if TYPE_CHECKING: from .bb import BB @@ -47,12 +48,11 @@ 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] +_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,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 @@ -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/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) 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 76d2927c..18281529 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -1,16 +1,10 @@ __docformat__ = "reStructuredText" -from typing import ( # Literal, - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Optional, - Set, - Tuple, - Union, -) -from weakref import WeakSet +import math +import weakref +from collections.abc import KeysView +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional # Literal, +from weakref import WeakKeyDictionary if TYPE_CHECKING: from .space import Space @@ -20,6 +14,8 @@ 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 _BodyType = int @@ -106,13 +102,12 @@ 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 - def __init__( self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC ) -> None: @@ -217,14 +212,9 @@ 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._constraints: WeakSet["Constraint"] = ( - WeakSet() - ) # weak refs to any constraints attached - self._shapes: WeakSet["Shape"] = WeakSet() # weak refs to any shapes attached + self._space: weakref.ref["Space"] = _dead_ref + self._constraints: WeakKeyDictionary["Constraint", None] = WeakKeyDictionary() + self._shapes: dict["Shape", None] = {} d = ffi.new_handle(self) self._data_handle = d # to prevent gc to collect the handle @@ -252,30 +242,40 @@ def __repr__(self) -> str: else: return "Body(Body.STATIC)" - def _set_mass(self, mass: float) -> None: - lib.cpBodySetMass(self._body, mass) + @property + def mass(self) -> float: + """Mass of the body. - def _get_mass(self) -> float: + Note that dynamic bodies must have mass > 0 if they are attached to a + Space. + """ return lib.cpBodyGetMass(self._body) - mass = property(_get_mass, _set_mass, doc="""Mass of the body.""") + @mass.setter + def mass(self, mass: float) -> None: + 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) - 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. 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 = 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: + 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: Union[Vec2d, Tuple[float, float]]) -> None: + def _set_position(self, pos: tuple[float, float]) -> None: assert len(pos) == 2 lib.cpBodySetPosition(self._body, pos) @@ -294,7 +294,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) @@ -312,7 +312,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) @@ -326,7 +326,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) @@ -344,78 +344,65 @@ 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 - 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:: 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 None).""" + # 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: @@ -495,16 +482,11 @@ 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( - 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. @@ -526,7 +508,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. @@ -543,7 +525,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. @@ -553,7 +535,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 @@ -561,7 +543,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. @@ -582,7 +564,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) @@ -600,7 +582,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) @@ -609,28 +591,31 @@ 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: - 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`, + @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 ( + 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 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: @@ -655,7 +640,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 @@ -666,17 +651,25 @@ 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"]: + def shapes(self) -> KeysView["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""" - return set(self._shapes) + 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 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 @@ -689,7 +682,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 @@ -698,7 +691,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 @@ -710,7 +703,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/constraints.py b/pymunk/constraints.py index c4154e63..858874ee 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 @@ -68,15 +68,15 @@ "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 .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] @@ -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 @@ -120,32 +123,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 +147,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: @@ -213,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]]: @@ -275,12 +266,15 @@ 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) - b._constraints.add(self) + 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` @@ -293,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` @@ -320,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. @@ -339,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) @@ -349,20 +343,20 @@ 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) 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. @@ -382,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: @@ -402,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) @@ -412,28 +406,28 @@ 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) 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. @@ -448,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 @@ -486,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) @@ -496,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) @@ -519,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. @@ -540,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) @@ -550,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) @@ -560,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) @@ -587,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, @@ -623,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) @@ -633,46 +627,39 @@ 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) 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.""" @@ -734,40 +721,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.""" @@ -816,22 +796,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.""" @@ -847,30 +827,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.""" @@ -887,22 +867,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.""" @@ -919,12 +899,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/contact_point_set.py b/pymunk/contact_point_set.py index fdb4967d..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, List, Tuple +from typing import TYPE_CHECKING 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/examples/arrows.py b/pymunk/examples/arrows.py index beb3a188..bfe704d2 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 @@ -50,7 +49,7 @@ def post_solve_arrow_hit(arbiter, space, data): arrow_body, other_body, position, - data["flying_arrows"], + data, ) @@ -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,10 +97,8 @@ def main(): arrow_body, arrow_shape = create_arrow() 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 + flying_arrows: list[pymunk.Body] = [] + 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 25ada068..74d92239 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,7 @@ 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.on_collision(COLLTYPE_MOUSE, COLLTYPE_BALL, pre_solve=mouse_coll_func) ### Static line line_point1 = None 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/breakout.py b/pymunk/examples/breakout.py index 61497e63..5a335a4b 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.on_collision( + 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.on_collision( + 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.on_collision( + 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..9328c69b 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.on_collision( + 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/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/examples/contact_and_no_flipy.py b/pymunk/examples/contact_and_no_flipy.py index e80f9948..e7500bfd 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.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 4efbe61d..d79cad9c 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.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 ace96b02..6a667a2c 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.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/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/examples/platformer.py b/pymunk/examples/platformer.py index e7003a5e..50b25006 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.on_collision(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/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 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/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/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 diff --git a/pymunk/pygame_util.py b/pymunk/pygame_util.py index ee5d47b6..1dc400ef 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 -using pymunk together with pygame. +"""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 import pygame @@ -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 @@ -148,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, @@ -188,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, @@ -204,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. @@ -229,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..7752b95f 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 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 @@ -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/pymunk_extension_build.py b/pymunk/pymunk_extension_build.py index 7f84b9ca..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 @@ -20,7 +19,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): @@ -38,7 +37,7 @@ elif fn[-1] == "o": os.remove(fn_path) -libraries: List[str] = [] +libraries: list[str] = [] # if os == linux: # libraries.append('m') @@ -75,7 +74,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)], 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/shape_filter.py b/pymunk/shape_filter.py index a5200ce7..e9152f31 100644 --- a/pymunk/shape_filter.py +++ b/pymunk/shape_filter.py @@ -107,3 +107,47 @@ 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(group=2)) + False + >>> ShapeFilter(group=1).rejects_collision(ShapeFilter(group=1)) + True + + + Categories and Masks:: + + >>> 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 + ) diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 5ef4dd27..516f6146 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -1,6 +1,7 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple +import weakref +from typing import TYPE_CHECKING, Optional, Sequence if TYPE_CHECKING: from .body import Body @@ -10,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 @@ -41,81 +43,63 @@ 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 + _space: weakref.ref["Space"] = _dead_ref def __init__(self, shape: "Shape") -> None: self._shape = shape - self._body: Optional["Body"] = 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: - self._body = body if body is not None: - body._shapes.add(self) + self._body = weakref.ref(body) + body._shapes[self] = None + else: + self._body = _dead_ref def shapefree(cp_shape: ffi.CData) -> None: cp_space = cp.cpShapeGetSpace(cp_shape) 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) - self._set_id() + self._h = ffi.new_handle(self) # to prevent GC of the handle + cp.cpShapeSetUserData(self._shape, self._h) @property - def _id(self) -> int: - """Unique id of the Shape. + def mass(self) -> float: + """The mass of this shape. - .. note:: - Experimental API. Likely to change in future major, minor orpoint - releases. + 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 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 - - def _get_mass(self) -> float: 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 +116,61 @@ def center_of_gravity(self) -> Vec2d: v = cp.cpShapeGetCenterOfGravity(self._shape) return Vec2d(v.x, v.y) - def _get_sensor(self) -> bool: + @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. + """ return bool(cp.cpShapeGetSensor(self._shape)) - def _set_sensor(self, is_sensor: bool) -> None: + @sensor.setter + def 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 collision_type(self) -> int: + """User defined collision type for the shape. - Sensors only call collision callbacks, and never generate real - collisions. - """, - ) + Defaults to 0. - def _get_collision_type(self) -> int: + See the :py:meth:`Space.add_collision_handler` function for more + information on when to use this property. + """ return cp.cpShapeGetCollisionType(self._shape) - def _set_collision_type(self, t: int) -> None: + @collision_type.setter + def collision_type(self, t: int) -> None: cp.cpShapeSetCollisionType(self._shape, t) - collision_type = property( - _get_collision_type, - _set_collision_type, - doc="""User defined collision type for the shape. - - See :py:meth:`Space.add_collision_handler` function for more - information on when to use this property. - """, - ) - - 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 +200,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,24 +226,27 @@ def _set_surface_velocity(self, surface_v: Vec2d) -> None: """, ) - def _get_body(self) -> Optional["Body"]: - return self._body + @property + def body(self) -> Optional["Body"]: + """The body this shape is attached to. - def _set_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) + 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: + 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 = 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.""", - ) + body._shapes[self] = None + self._body = weakref.ref(body) + else: + self._body = _dead_ref def update(self, transform: Transform) -> BB: """Update, cache and return the bounding box of a shape with an @@ -303,20 +276,20 @@ 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 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), @@ -325,10 +298,12 @@ 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: + 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. + Returns None if it does not intersect + :rtype: :py:class:`SegmentQueryInfo` """ assert len(start) == 2 @@ -336,8 +311,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), @@ -345,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. @@ -365,13 +335,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: @@ -381,6 +345,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. @@ -409,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. @@ -439,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:: @@ -470,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. @@ -492,20 +463,20 @@ 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] + self, a: tuple[float, float], b: tuple[float, float] ) -> None: """Set the two endpoints for this segment. @@ -542,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 @@ -563,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: @@ -571,7 +542,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. @@ -586,23 +561,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) @@ -643,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. @@ -692,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 @@ -723,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 b065d968..185ec0ed 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -1,19 +1,10 @@ __docformat__ = "reStructuredText" +import math import platform import weakref -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Hashable, - List, - Optional, - Set, - Tuple, - 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 @@ -22,13 +13,11 @@ from . import _version from ._callbacks import * from ._chipmunk_cffi import ffi, lib - -cp = lib - +from ._collision_handler import CollisionHandler, _CollisionCallback from ._pickle import PickleMixin, _State -from .arbiter import _arbiter_from_dict, _arbiter_to_dict +from ._util import _dead_ref +from .arbiter import Arbiter, _arbiter_from_dict, _arbiter_to_dict from .body import Body -from .collision_handler import CollisionHandler from .query_info import PointQueryInfo, SegmentQueryInfo, ShapeQueryInfo from .shapes import Shape from .vec2d import Vec2d @@ -90,16 +79,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: 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) @@ -107,18 +98,18 @@ 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( + lib.cpSpaceEachConstraint( cp_space, lib.ext_cpSpaceConstraintIteratorFunc, cp_constraints_h ) for cp_constraint in cp_constraints: 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) + 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) @@ -127,47 +118,70 @@ 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[int, Shape] = {} + self._post_step_callbacks: dict[Any, Callable[["Space"], None]] = {} + self._removed_shapes: dict[Shape, None] = {} - self._shapes: Dict[int, Shape] = {} - 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() - - def _get_self(self) -> "Space": - return self + self._add_later: set[_AddableObjects] = set() + self._remove_later: set[_AddableObjects] = set() + 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. + + Since its a view that is returned it will update as shapes are + added. - (includes both static and non-static) + >>> 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.values()) + 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.proxy(self) - cp.cpSpaceAddBody(self._space, static_body._body) + static_body._space = weakref.ref(self) + lib.cpSpaceAddBody(self._space, static_body._body) @property def static_body(self) -> Body: @@ -181,22 +195,15 @@ 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 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,15 +219,19 @@ 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 lib.cpSpaceGetIterations(self._space) - def _set_gravity(self, gravity_vector: Tuple[float, float]) -> None: + @iterations.setter + def iterations(self, value: int) -> None: + 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( @@ -234,81 +245,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 lib.cpSpaceGetDamping(self._space) - def _get_idle_speed_threshold(self) -> float: - return cp.cpSpaceGetIdleSpeedThreshold(self._space) + @damping.setter + def damping(self, damping: float) -> None: + lib.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 lib.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: + lib.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 lib.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: + lib.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 lib.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: + lib.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 +314,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 lib.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: + lib.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 +331,20 @@ def _get_collision_persistence(self) -> float: ..Note:: Very very few games will need to change this value. - """, - ) + """ + return lib.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: + lib.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 lib.cpSpaceGetCurrentTimeStep(self._space) def add(self, *objs: _AddableObjects) -> None: """Add one or many shapes, bodies or constraints (joints) to the space @@ -412,8 +402,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." @@ -422,43 +411,51 @@ 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) - self._shapes[shape._id] = shape - cp.cpSpaceAddShape(self._space, shape._shape) + shape._space = weakref.ref(self) + self._shapes[shape] = None + lib.cpSpaceAddShape(self._space, shape._shape) def _add_body(self, body: "Body") -> None: """Adds a body to the space""" 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 - cp.cpSpaceAddBody(self._space, body._body) + self._bodies_to_check.add(body) + lib.cpSpaceAddBody(self._space, body._body) 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) + lib.cpSpaceAddConstraint(self._space, constraint._constraint) 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 - shape._space = None + assert shape in self._shapes, "shape not in space, already removed?" + 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) - del self._shapes[shape._id] + if lib.cpSpaceContainsShape(self._space, shape._shape): + lib.cpSpaceRemoveShape(self._space, shape._shape) + del self._shapes[shape] 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 = _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. - 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: @@ -468,47 +465,44 @@ 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: + """The number of threads to use for running the step function. - def _get_threads(self) -> int: + 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 int(lib.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. - """, - ) + 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 @@ -532,7 +526,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 @@ -541,7 +535,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. @@ -565,13 +559,20 @@ 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 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: self._locked = True if self.threaded: - cp.cpHastySpaceStep(self._space, dt) + lib.cpHastySpaceStep(self._space, dt) else: - cp.cpSpaceStep(self._space, dt) - self._removed_shapes = {} + lib.cpSpaceStep(self._space, dt) + self._removed_shapes.clear() finally: self._locked = False self.add(*self._add_later) @@ -583,92 +584,117 @@ def step(self, dt: float) -> None: for key in self._post_step_callbacks: self._post_step_callbacks[key](self) - self._post_step_callbacks = {} - - def add_collision_handler( - self, collision_type_a: int, collision_type_b: int - ) -> CollisionHandler: - """Return the :py:class:`CollisionHandler` for collisions between - objects of type collision_type_a and collision_type_b. + self._post_step_callbacks.clear() - Fill the desired collision callback functions, for details see the - :py:class:`CollisionHandler` object. + def on_collision( + self, + collision_type_a: Optional[int] = None, + collision_type_b: Optional[int] = None, + begin: Optional[_CollisionCallback] = None, + 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. 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). - - :param int collision_type_a: Collision type a - :param int collision_type_b: Collision type b - - :rtype: :py:class:`CollisionHandler` - """ - key = min(collision_type_a, collision_type_b), max( - collision_type_a, collision_type_b - ) - if key in self._handlers: - return self._handlers[key] + 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. + + Its possible to pass in None for one or both of the collision types. + 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 + overwrite the earlier. + + If multiple callbacks match the collision, the order will be that the + most specific handler is called first. + + 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. - h = cp.cpSpaceAddCollisionHandler( - self._space, collision_type_a, collision_type_b - ) - ch = CollisionHandler(h, self) - 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` """ + # key = min(collision_type_a, collision_type_b), max( + # 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." - 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_default_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. + key = collision_type_a, collision_type_b + 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 - 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] + if collision_type_b == None: + collision_type_b = wildcard - _h = cp.cpSpaceAddDefaultCollisionHandler(self._space) - h = CollisionHandler(_h, self) - self._handlers[None] = h - return h + h = lib.cpSpaceAddCollisionHandler( + self._space, collision_type_a, collision_type_b + ) + ch = CollisionHandler(h, self) + self._handlers[key] = ch + else: + ch = self._handlers[key] + + # 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 == empty_callback: + ch.pre_solve = None + elif pre_solve != None: + ch.pre_solve = pre_solve + ch.data["pre_solve"] = data + 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 == empty_callback: + ch.separate = None + elif separate != None: + ch.separate = separate + ch.data["separate"] = data + return 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, @@ -712,8 +738,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 @@ -722,13 +748,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 @@ -737,35 +761,21 @@ 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( + lib.cpSpacePointQuery( self._space, point, max_distance, shape_filter, - cp.ext_cpSpacePointQueryFunc, + lib.ext_cpSpacePointQueryFunc, data, ) return query_hits - 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 - 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. @@ -780,8 +790,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) @@ -792,11 +801,11 @@ 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 ) - shape = self._get_shape(_shape) + shape = Shape._from_cp_shape(_shape) if shape != None: return PointQueryInfo( @@ -809,11 +818,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. @@ -823,9 +832,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 @@ -836,26 +843,26 @@ 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) - cp.cpSpaceSegmentQuery( + lib.cpSpaceSegmentQuery( self._space, start, end, radius, shape_filter, - cp.ext_cpSpaceSegmentQueryFunc, + lib.ext_cpSpaceSegmentQueryFunc, data, ) return query_hits 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]: @@ -866,8 +873,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. @@ -877,11 +883,11 @@ 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 ) - shape = self._get_shape(_shape) + shape = Shape._from_cp_shape(_shape) if shape != None: return SegmentQueryInfo( shape, @@ -891,14 +897,13 @@ 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 collision detection. - .. Note:: - Sensor shapes are included in the result + Sensor shapes are included in the result :param bb: Bounding box :param shape_filter: Shape filter @@ -906,21 +911,20 @@ 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) - 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 - 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:: - 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` @@ -928,12 +932,12 @@ 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) - cp.cpSpaceShapeQuery( - self._space, shape._shape, cp.ext_cpSpaceShapeQueryFunc, data + lib.cpSpaceShapeQuery( + self._space, shape._shape, lib.ext_cpSpaceShapeQueryFunc, data ) return query_hits @@ -962,7 +966,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) @@ -976,10 +980,10 @@ 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) + lib.cpSpaceEachCachedArbiter(self._space, lib.ext_cpArbiterIteratorFunc, data) return _arbiters def __getstate__(self) -> _State: @@ -992,35 +996,38 @@ 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))) + + # to avoid circular dep + from . import empty_callback handlers = [] for k, v in self._handlers.items(): - h: Dict[str, Any] = {} - if v._begin is not None: + h: dict[str, Any] = {} + if v._begin != empty_callback: h["_begin"] = v._begin - if v._pre_solve is not None: + if v._pre_solve != empty_callback: h["_pre_solve"] = v._pre_solve - if v._post_solve is not None: + if v._post_solve != empty_callback: h["_post_solve"] = v._post_solve - if v._separate is not None: + if v._separate != empty_callback: h["_separate"] = v._separate handlers.append((k, h)) 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() @@ -1045,14 +1052,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": @@ -1062,28 +1069,51 @@ def __setstate__(self, state: _State) -> None: self.add(*v) elif k == "_handlers": for k2, hd in v: - if k2 == None: - h = self.add_default_collision_handler() - elif isinstance(k2, tuple): - h = self.add_collision_handler(k2[0], k2[1]) - else: - h = self.add_wildcard_collision_handler(k2) + 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.on_collision( + None, + None, + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + ) + elif isinstance(k2, tuple): + self.on_collision( + k2[0], + k2[1], + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=separate, + ) + else: + self.on_collision( + k2, + None, + begin=begin, + pre_solve=pre_solve, + post_solve=post_solve, + separate=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) diff --git a/pymunk/space_debug_draw_options.py b/pymunk/space_debug_draw_options.py index 92374c21..a689b4d3 100644 --- a/pymunk/space_debug_draw_options.py +++ b/pymunk/space_debug_draw_options.py @@ -1,13 +1,11 @@ __docformat__ = "reStructuredText" -from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence, Tuple, Type +from typing import TYPE_CHECKING, ClassVar, NamedTuple, Optional, Sequence if TYPE_CHECKING: from .shapes import Shape from types import TracebackType -import math - from ._chipmunk_cffi import ffi, lib from .body import Body from .transform import Transform @@ -24,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() @@ -32,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() @@ -60,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 @@ -98,17 +96,10 @@ def __init__(self) -> None: | SpaceDebugDrawOptions.DRAW_COLLISION_POINTS ) - def _get_shape_outline_color(self) -> SpaceDebugColor: - return self._c(self._options.shapeOutlineColor) + @property + def shape_outline_color(self) -> SpaceDebugColor: + """The outline color of shapes. - 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. - Should be a tuple of 4 ints between 0 and 255 (r,g,b,a). Example: @@ -124,22 +115,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,22 +144,19 @@ 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). - + Example: >>> import pymunk @@ -191,15 +176,19 @@ 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 def __exit__( self, - type: Optional[Type[BaseException]], + type: Optional[type[BaseException]], value: Optional[BaseException], traceback: Optional["TracebackType"], ) -> None: @@ -208,22 +197,15 @@ 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 + @property + def flags(self) -> _DrawFlags: + """Bit flags which of shapes, joints and collisions should be drawn. - flags = property( - _get_flags, - _set_flags, - doc="""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 @@ -234,11 +216,11 @@ def _set_flags(self, f: _DrawFlags) -> None: >>> 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)) @@ -249,31 +231,27 @@ 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: + 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) @@ -281,13 +259,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. + 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) + + @transform.setter + def transform(self, t: Transform) -> None: + self._options.transform = t def draw_circle( self, 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_arbiter.py b/pymunk/tests/test_arbiter.py index efccff94..2241f741 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 @@ -23,12 +24,11 @@ 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) -> None: self.assertEqual(arb.restitution, 0.18) arb.restitution = 1 - return True - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -52,12 +52,11 @@ 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) -> None: self.assertEqual(arb.friction, 0.18) arb.friction = 1 - return True - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(10): s.step(0.1) @@ -81,15 +80,14 @@ 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) -> 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 - return True - s.add_collision_handler(1, 2).pre_solve = pre_solve + s.on_collision(1, 2, pre_solve=pre_solve) for x in range(5): s.step(0.1) @@ -108,9 +106,9 @@ 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) -> 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) @@ -137,16 +135,14 @@ 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 self.assertRaises(Exception, f) - return True - - s.add_default_collision_handler().pre_solve = pre_solve + s.on_collision(2, 1, pre_solve=pre_solve) s.step(0.1) @@ -174,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.add_collision_handler(1, 2).post_solve = post_solve + s.on_collision(1, 2, post_solve=post_solve) s.step(0.1) @@ -196,15 +192,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) -> bool: - self.assertAlmostEqual(arb.total_ke, 43.438914027) - return True + def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: + r["ke"] = arb.total_ke - s.add_collision_handler(1, 2).post_solve = post_solve + s.on_collision(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 @@ -222,19 +220,17 @@ 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.on_collision(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 + s.on_collision(1, 2, pre_solve=pre_solve2) s.step(0.1) @@ -245,19 +241,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) -> bool: - self.assertAlmostEqual(arb.normal.x, 0.44721359) - self.assertAlmostEqual(arb.normal.y, 0.89442719) - return True + def pre_solve1(arb: p.Arbiter, space: p.Space, data: Any) -> None: + r["n"] = Vec2d(*arb.normal) - s.add_default_collision_handler().pre_solve = pre_solve1 + s.on_collision(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 @@ -281,7 +281,7 @@ def separate1(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.called1 = True self.assertFalse(arb.is_removal) - s.add_collision_handler(1, 2).separate = separate1 + s.on_collision(1, 2, separate=separate1) for x in range(10): s.step(0.1) @@ -292,17 +292,16 @@ 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.on_collision(1, 2, separate=separate2) s.remove(b1, c1) self.assertTrue(self.called2) - def testShapes(self) -> None: + def testShapesAndBodies(self) -> None: s = p.Space() s.gravity = 0, -100 @@ -321,18 +320,118 @@ def testShapes(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) - return True + 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.on_collision(1, 2, post_solve=pre_solve) s.step(0.1) self.assertTrue(self.called) + def testProcessCollision(self) -> None: + + def setup() -> p.Space: + 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 = data["expected"].pop(0) + process_collision = 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, + ) + + 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_str, expected_calls in test_matrix: + process_values = [ + 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) + expected_calls = list(zip(expected_calls[::2], expected_calls[1::2])) + # print("process_values, expected calls", process_values, expected_calls) + + s = setup() + hdata: dict[str, Any] = {} + hdata["process_values"] = process_values + hdata["expected"] = expected_calls + hdata["result"] = [] + s.on_collision( + 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) + next(iter(s.bodies)).position = 100, 100 + s.step(0.1) + + # print(h.data) + # print(all(h.data["result"])) + self.assertTrue(all(hdata["result"])) + # print("done") + if __name__ == "__main__": print("testing pymunk version " + p.version) 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_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_body.py b/pymunk/tests/test_body.py index 0ff1649f..c3ad57e5 100644 --- a/pymunk/tests/test_body.py +++ b/pymunk/tests/test_body.py @@ -1,6 +1,6 @@ +import math import pickle import unittest -from typing import List, Tuple import pymunk as p from pymunk.arbiter import Arbiter @@ -171,6 +171,71 @@ 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 + + with self.assertRaises(AssertionError): + b.mass = math.inf + + 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_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() @@ -237,9 +302,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) @@ -270,6 +335,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() @@ -369,5 +435,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_common.py b/pymunk/tests/test_common.py index f2e68203..f427374b 100644 --- a/pymunk/tests/test_common.py +++ b/pymunk/tests/test_common.py @@ -1,7 +1,10 @@ +import gc import unittest -from typing import Any, List +import weakref +from typing import Any import pymunk as p +from pymunk._weakkeysview import WeakKeysView from pymunk.vec2d import Vec2d @@ -42,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) @@ -123,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.on_collision(2, 0, separate=remove_first) # print(1) space.step(1.0 / 60) # print(2) @@ -134,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() + b1 = p.Body(1, 1) c1 = p.Circle(b1, 10) c1.collision_type = 2 - b2 = p.Body() + b2 = p.Body(1, 2) c2 = p.Circle(b2, 10) - b3 = p.Body() + b3 = p.Body(1, 3) c3 = p.Circle(b3, 10) # b1.position = 0, 0 @@ -163,10 +166,38 @@ 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.on_collision(2, 0, separate=separate) # print(1) space.step(1) # print(2) 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[p.Body, int] = 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 307e5185..a5186ea7 100644 --- a/pymunk/tests/test_constraint.py +++ b/pymunk/tests/test_constraint.py @@ -1,3 +1,4 @@ +import gc import pickle import unittest @@ -170,6 +171,46 @@ 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)) + + 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: diff --git a/pymunk/tests/test_shape.py b/pymunk/tests/test_shape.py index a328db1f..20bb4283 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) @@ -36,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) @@ -195,8 +190,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 @@ -302,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) -> None: self.num_of_begins += 1 - return True - s.add_default_collision_handler().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 7698fba0..9b146957 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -1,12 +1,13 @@ from __future__ import with_statement import copy +import functools import io import pickle 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 * @@ -73,7 +74,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) @@ -102,18 +103,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 +123,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() @@ -196,7 +197,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), ) @@ -268,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() @@ -339,6 +332,40 @@ 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) + + r1 = s.bb_query(p.BB(0, 0, 10, 10), p.ShapeFilter()) + assert len(r1), 2 + + r2 = s.point_query((0, 0), 10, p.ShapeFilter()) + assert len(r2), 2 + + r3 = s.point_query_nearest((0, 0), 10, p.ShapeFilter()) + assert r3 != None + self.assertEqual(r3.shape, s1) + + r4 = s.shape_query(p.Circle(p.Body(body_type=p.Body.KINEMATIC), 10)) + assert len(r4), 2 + + r5 = s.segment_query((0, 0), (10, 0), 1, p.ShapeFilter()) + assert len(r5), 2 + + 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() @@ -476,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) @@ -509,40 +528,16 @@ def testCollisionHandlerBegin(self) -> None: self.hits = 0 - def begin(arb: p.Arbiter, space: p.Space, data: Any) -> bool: - self.hits += h.data["test"] - return True + def begin(arb: p.Arbiter, space: p.Space, data: Any) -> None: + self.hits += 1 - h = s.add_collision_handler(0, 0) - h.data["test"] = 1 - h.begin = begin + s.on_collision(0, 0, begin=begin) for x in range(10): s.step(0.1) 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,42 +549,20 @@ 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) -> None: 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 - h.pre_solve = pre_solve + data = {"test": 1} + s.on_collision(0, 1, pre_solve=pre_solve, data=data) + s.step(0.1) self.assertEqual(c1, d["shapes"][1]) self.assertEqual(c2, d["shapes"][0]) 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 @@ -597,7 +570,7 @@ def testCollisionHandlerPostSolve(self) -> None: def post_solve(arb: p.Arbiter, space: p.Space, data: Any) -> None: self.hit += 1 - self.s.add_collision_handler(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() @@ -619,11 +592,9 @@ def testCollisionHandlerSeparate(self) -> None: self.separated = False def separate(arb: p.Arbiter, space: p.Space, data: Any) -> None: - self.separated = data["test"] + self.separated = True - h = s.add_collision_handler(0, 0) - h.data["test"] = True - h.separate = separate + s.on_collision(0, 0, separate=separate) for x in range(10): s.step(0.1) @@ -642,11 +613,37 @@ def separate(*_: Any) -> None: s.add(p.Circle(s.static_body, 2)) s.remove(c1) - s.add_default_collision_handler().separate = separate + s.on_collision(separate=separate) 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 + + s.on_collision( + None, + None, + begin=p.empty_callback, + pre_solve=p.empty_callback, + post_solve=p.empty_callback, + separate=p.empty_callback, + ) + + 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() @@ -675,15 +672,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.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.add_collision_handler(1, 0).separate = remove2 + s.on_collision(1, 0, separate=remove2) s.add(shape1) s.step(1) @@ -713,7 +710,7 @@ def separate(*_: Any) -> None: pass s.step(1) - s.add_wildcard_collision_handler(0).separate = separate + s.on_collision(0, separate=separate) s.remove(shape1) @@ -750,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.add_collision_handler(1, 2).post_solve = post_solve - space.add_collision_handler(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 @@ -782,28 +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, data: Any) -> bool: + 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) - 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) -> None: 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 + 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.add_collision_handler(0, 0).pre_solve = pre_solve_remove + s.on_collision(0, 0).pre_solve = pre_solve_remove s.step(0.1) @@ -814,11 +809,10 @@ 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) -> None: space.remove(*arb.shapes) - return True - s.add_collision_handler(0, 0).pre_solve = pre_solve + s.on_collision(0, 0, pre_solve=pre_solve) s.step(0.1) @@ -826,12 +820,84 @@ def pre_solve(arb: p.Arbiter, space: p.Space, data: Any) -> bool: 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[Optional[int], Optional[int]], + arb: p.Arbiter, + space: p.Space, + data: dict[Any, Any], + ) -> None: + callback_calls.append( + ( + name, + types, + (arb.shapes[0].collision_type, arb.shapes[1].collision_type), + ) + ) + + handler_order = [ + (1, 2), + (2, 1), + (1, None), + (2, None), + (None, None), + ] + + for t1, t2 in handler_order: + s.on_collision( + 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) + 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), (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) def testWildcardCollisionHandler(self) -> None: s = p.Space() @@ -843,12 +909,14 @@ 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) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - return True - s.add_wildcard_collision_handler(1).pre_solve = pre_solve + s.on_collision(1, None, pre_solve=pre_solve) + with self.assertRaises(AssertionError): + s.on_collision(None, 1, pre_solve=pre_solve) + s.step(0.1) self.assertEqual({}, d) @@ -872,12 +940,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) -> None: d["shapes"] = arb.shapes d["space"] = space # type: ignore - return True - s.add_default_collision_handler().pre_solve = pre_solve + s.on_collision(pre_solve=pre_solve) s.step(0.1) self.assertEqual(c1, d["shapes"][1]) @@ -905,16 +972,15 @@ 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) -> 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) - return True - ch = s.add_collision_handler(0, 0).pre_solve = pre_solve + s.on_collision(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) @@ -1021,17 +1087,13 @@ 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 = f1 + s.on_collision(begin=f1) - h = s.add_wildcard_collision_handler(1) - h.pre_solve = f1 + s.on_collision(1, pre_solve=f1) - h = s.add_collision_handler(1, 2) - h.post_solve = f1 + s.on_collision(1, 2, post_solve=f1) - h = s.add_collision_handler(3, 4) - h.separate = f1 + s.on_collision(3, 4, separate=f1) s2 = copy_func(s) @@ -1055,28 +1117,28 @@ 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._handlers[(None, None)] self.assertIsNotNone(h2.begin) - self.assertIsNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.pre_solve, None) + self.assertEqual(h2.post_solve, None) + self.assertEqual(h2.separate, None) - h2 = s2.add_wildcard_collision_handler(1) - self.assertIsNone(h2.begin) + h2 = s2._handlers[(1, None)] + self.assertEqual(h2.begin, None) self.assertIsNotNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.post_solve, None) + self.assertEqual(h2.separate, None) - h2 = s2.add_collision_handler(1, 2) - self.assertIsNone(h2.begin) - self.assertIsNone(h2.pre_solve) + h2 = s2._handlers[(1, 2)] + self.assertEqual(h2.begin, None) + self.assertEqual(h2.pre_solve, None) self.assertIsNotNone(h2.post_solve) - self.assertIsNone(h2.separate) + self.assertEqual(h2.separate, None) - h2 = s2.add_collision_handler(3, 4) - self.assertIsNone(h2.begin) - self.assertIsNone(h2.pre_solve) - self.assertIsNone(h2.post_solve) + h2 = s2._handlers[(3, 4)] + 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: @@ -1141,9 +1203,31 @@ def testPickleCachedArbiters(self) -> None: # TODO: to assert that everything is working as it should all # properties on the cached the arbiters should be asserted. + 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() + b = p.Body(1) + c = p.Circle(b, 10) + 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.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) + 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: diff --git a/pymunk/transform.py b/pymunk/transform.py index 7db941e8..6a6feb6c 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, 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 23dd45bf..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) @@ -108,7 +108,8 @@ import math import numbers import operator -from typing import NamedTuple, Tuple +import warnings +from typing import NamedTuple __all__ = ["Vec2d"] @@ -130,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) @@ -144,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) @@ -153,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) @@ -163,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) @@ -244,12 +245,41 @@ 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 + + @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 @@ -257,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 @@ -275,12 +310,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) @@ -315,13 +357,13 @@ 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 - 0 + 0.0 """ - if self.get_length_sqrd() == 0: - return 0 return math.atan2(self.y, self.x) @property @@ -335,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)) @@ -364,21 +406,21 @@ 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) >>> 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]: + def normalized_and_length(self) -> tuple["Vec2d", float]: """Normalize the vector and return its length before the normalization. >>> Vec2d(3, 0).normalized_and_length() @@ -386,12 +428,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. @@ -420,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 @@ -432,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)) @@ -444,11 +486,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_dist_sqrd(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. + + >>> 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)) @@ -456,10 +516,15 @@ 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 - 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)) @@ -467,18 +532,18 @@ 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 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 @@ -489,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) @@ -501,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. @@ -510,51 +575,69 @@ 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]: + 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) """ 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. >>> 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": """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) @@ -566,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( 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..b2a91029 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, @@ -107,11 +107,12 @@ 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; enum cpArbiterState state; + cpBool processCollision; }; struct cpArray @@ -175,9 +176,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 +239,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 +1001,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 +1115,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 1249d56c..a8b8d379 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,25 @@ 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); + 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); - 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); @@ -488,3 +495,5 @@ 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 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..3b4dd441 100644 --- a/pymunk_cffi/extensions_cdef.h +++ b/pymunk_cffi/extensions_cdef.h @@ -102,4 +102,6 @@ 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 void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data); \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1941dfda..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'", @@ -10,7 +9,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 = [ @@ -21,10 +20,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", @@ -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"]