diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0aa52d58..183825b9 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -9,10 +9,16 @@ jobs: strategy: matrix: os: - [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + [ + ubuntu-latest, + ubuntu-24.04-arm, + windows-latest, + macos-latest, + macos-15-intel, + ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -22,16 +28,14 @@ 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.23.3 + uses: pypa/cibuildwheel@v3.3.0 env: - CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* pp310-* pp311-*" - CIBW_TEST_COMMAND: "python -m pymunk.tests" - # CIBW_BUILD_VERBOSITY: 3 + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-* pp311-*" with: package-dir: "$SDIST_PATH" - name: Upload wheel artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: ${{ github.ref == 'refs/heads/master'}} with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} @@ -39,9 +43,8 @@ jobs: retention-days: 7 - name: Upload sdist artifact - uses: actions/upload-artifact@v4 - if: - ${{ github.ref == 'refs/heads/master' && matrix.os == + uses: actions/upload-artifact@v5 + if: ${{ github.ref == 'refs/heads/master' && matrix.os == 'windows-latest'}} with: name: sdist-${{ matrix.os }}-${{ strategy.job-index }} @@ -52,17 +55,16 @@ jobs: name: Build wheels for wasm / emscripten runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - - uses: pypa/cibuildwheel@v2.23.3 + - uses: pypa/cibuildwheel@v3.3.0 env: CIBW_PLATFORM: pyodide - PYMUNK_BUILD_SLIM: 1 - CIBW_TEST_COMMAND: "python -m pymunk.tests" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: ${{ github.ref == 'refs/heads/master' }} with: + name: cibw-wheels-pyodide-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl retention-days: 7 @@ -71,38 +73,14 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: true - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install cibuildwheel - run: | - python -m pip install -U pip - # Use main branch until iOS support is released. - # python -m pip install cibuildwheel==3.0.0 - python -m pip install git+https://github.com/pypa/cibuildwheel.git - - - name: Build wheels - run: python -m cibuildwheel + - run: brew upgrade cmake --formula + - uses: pypa/cibuildwheel@v3.3.0 env: - IPHONEOS_DEPLOYMENT_TARGET: "12.0" CIBW_PLATFORM: ios - CIBW_ARCHS: auto - CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD_IOS: | - # download wheel and pretend that it's compatible with the current platform settings - curl https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl -L -o cffi-1.17.1-py3-none-any.whl && \ - pip install cffi-1.17.1-py3-none-any.whl && \ - # install the rest of the dependencies - python -m pip install setuptools wheel - CIBW_XBUILD_TOOLS: "curl pkg-config tar" - CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation" - PYMUNK_BUILD_SLIM: 1 - - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: ${{ github.ref == 'refs/heads/master' }} with: name: cibw-wheels-ios-${{ matrix.os }}-${{ strategy.job-index }} diff --git a/.harper-dictionary.txt b/.harper-dictionary.txt new file mode 100644 index 00000000..99427371 --- /dev/null +++ b/.harper-dictionary.txt @@ -0,0 +1,12 @@ +Emscripten +aafigure +cffi +matplotlib +numpy +png +pygame +pyglet +pymunk +pythonic +setuptools +wav diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34874b03..ac1086d4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,13 +2,51 @@ Changelog ========= +Pymunk 7.2.0 (2025-11-02) +------------------------- + +This release provides wheels for Python 3.14. It also contains a new method +that make it possible to get the inverse transform of a Transform. + +**Wheels for Python 3.14** + +Changes: + + - Build wheels for 3.14 + - Added Tranform.inverted() to get the inverse transform. + + +Pymunk 7.1.0 (2025-06-29) +------------------------- + +**Minor fix for wildcard collision handler!** + +This is a minor release. It contains a fix for 'wildcard' collision +handlers (`space.on_collision(X, None, ...)`). Now the handler will be +invoked twice if two shapes of the same type collide. + +This release also updates cibuildwheel used to build wheels, and as part of +the upgrade Pymunk will no longer publish wheels for 32bit Linux. + +Changes: + +- Fixed wildcard collision handler to be called twice for collisions between + two shapes of same type. +- Allow get of `DampedSpring.force_func` +- Allow get of `DampedRotarySpring.torque_func` +- Rewrote some properties to use @property. Note that this means type checking + with mypy needs at least mypy 1.16.0. +- Updated to cibuildwheel 3.0 to build Pymunk. This also means Pymunk will not + publish wheels for 32bit Linux anymore. + + Pymunk 7.0.1 (2025-06-07) ------------------------- **Minor fixes and experimental iOS wheel!** This is a minor patch release, which adds experimental iOS wheels, minor bug -fix and fixes the verison number that was wrong in the previous release. +fix and fixes the version number that was wrong in the previous release. Changes: diff --git a/CITATION.cff b/CITATION.cff index 67e9dcad..cd2b0556 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: 7.0.1 -date-released: 2025-06-07 +version: 7.2.0 +date-released: 2025-11-02 url: "https://pymunk.org" diff --git a/Munk2D b/Munk2D index 5ef74989..ade7ed72 160000 --- a/Munk2D +++ b/Munk2D @@ -1 +1 @@ -Subproject commit 5ef7498946f0e956f294cb3fea283626921e4128 +Subproject commit ade7ed72849e60289eefb7a41e79ae6322fefaf3 diff --git a/README.rst b/README.rst index d9bd6f95..c721c598 100644 --- a/README.rst +++ b/README.rst @@ -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 (7.0.1), -using Munk2D 2.0 rev 5ef7498946f0e956f294cb3fea283626921e4128. +This release is based on the latest Pymunk release (7.2.0), +using Munk2D 2.0.1 rev ade7ed72849e60289eefb7a41e79ae6322fefaf3. Installation @@ -131,7 +131,7 @@ Dependencies / Requirements Basically Pymunk have been made to be as easy to install and distribute as possible, usually `pip install` will take care of everything for you. -- Python (Runs on CPython 3.8 and later and Pypy3) +- Python (Runs on CPython 3.9 and later and Pypy3) - Chipmunk (Prebuilt and included when using binary wheels) - CFFI (will be installed automatically by Pip) - Setuptools (should be included with Pip) @@ -156,5 +156,6 @@ Older Pythons - 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, -and no wheels are built.) +Pymunk version. It might work on newer Pymunks as well, but it's not tested, +and no wheels are built. + diff --git a/TODO.txt b/TODO.txt index f0b261c9..2484d4ed 100644 --- a/TODO.txt +++ b/TODO.txt @@ -40,7 +40,6 @@ v6.x - Update benchmark - Make benchmark that compares different pymunk and python versions easily - Update get_vertices doc to use body.local_to_world shorthand to conv. -- Package pymunk for Pyodide - 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! @@ -52,6 +51,7 @@ v6.x - Make space step size default 1/60, and/or make it possible to set on the space instead of pasing in to step function to make api easier to use "right" - 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()? +- Record video of the constraints and put on youtube (and link to that instead of the Chipmunk2d video of constraints) v7 --- diff --git a/docs/src/_static/custom.css b/docs/src/_static/custom.css index b992ea0b..a7201932 100644 --- a/docs/src/_static/custom.css +++ b/docs/src/_static/custom.css @@ -60,4 +60,8 @@ div.sphinxsidebar { div.body h2 { margin-top: 40px; +} + +video { + width: 100% } \ No newline at end of file diff --git a/docs/src/_static/examples/pillow_util_demo.png b/docs/src/_static/examples/pillow_util_demo.png new file mode 100644 index 00000000..ba4a20fc Binary files /dev/null and b/docs/src/_static/examples/pillow_util_demo.png differ diff --git a/docs/src/_static/My_Sincerest_Apologies.png b/docs/src/_static/showcase/My_Sincerest_Apologies.png similarity index 100% rename from docs/src/_static/My_Sincerest_Apologies.png rename to docs/src/_static/showcase/My_Sincerest_Apologies.png diff --git a/docs/src/_static/PySimpleGUI.png b/docs/src/_static/showcase/PySimpleGUI.png similarity index 100% rename from docs/src/_static/PySimpleGUI.png rename to docs/src/_static/showcase/PySimpleGUI.png diff --git a/docs/src/_static/SubTerrex.png b/docs/src/_static/showcase/SubTerrex.png similarity index 100% rename from docs/src/_static/SubTerrex.png rename to docs/src/_static/showcase/SubTerrex.png diff --git a/docs/src/_static/aimoveneat.png b/docs/src/_static/showcase/aimoveneat.png similarity index 100% rename from docs/src/_static/aimoveneat.png rename to docs/src/_static/showcase/aimoveneat.png diff --git a/docs/src/_static/ambient-chimes.png b/docs/src/_static/showcase/ambient-chimes.png similarity index 100% rename from docs/src/_static/ambient-chimes.png rename to docs/src/_static/showcase/ambient-chimes.png diff --git a/docs/src/_static/angry-birds-python.png b/docs/src/_static/showcase/angry-birds-python.png similarity index 100% rename from docs/src/_static/angry-birds-python.png rename to docs/src/_static/showcase/angry-birds-python.png diff --git a/docs/src/_static/ar-physics.jpg b/docs/src/_static/showcase/ar-physics.jpg similarity index 100% rename from docs/src/_static/ar-physics.jpg rename to docs/src/_static/showcase/ar-physics.jpg diff --git a/docs/src/_static/arcade-library.png b/docs/src/_static/showcase/arcade-library.png similarity index 100% rename from docs/src/_static/arcade-library.png rename to docs/src/_static/showcase/arcade-library.png diff --git a/docs/src/_static/auto-icenav.jpg b/docs/src/_static/showcase/auto-icenav.jpg similarity index 100% rename from docs/src/_static/auto-icenav.jpg rename to docs/src/_static/showcase/auto-icenav.jpg diff --git a/docs/src/_static/beneath-the-ice.png b/docs/src/_static/showcase/beneath-the-ice.png similarity index 100% rename from docs/src/_static/beneath-the-ice.png rename to docs/src/_static/showcase/beneath-the-ice.png diff --git a/docs/src/_static/billiARds.png b/docs/src/_static/showcase/billiARds.png similarity index 100% rename from docs/src/_static/billiARds.png rename to docs/src/_static/showcase/billiARds.png diff --git a/docs/src/_static/bouncingballs-beatuifulpatterns.png b/docs/src/_static/showcase/bouncingballs-beatuifulpatterns.png similarity index 100% rename from docs/src/_static/bouncingballs-beatuifulpatterns.png rename to docs/src/_static/showcase/bouncingballs-beatuifulpatterns.png diff --git a/docs/src/_static/carconf.png b/docs/src/_static/showcase/carconf.png similarity index 100% rename from docs/src/_static/carconf.png rename to docs/src/_static/showcase/carconf.png diff --git a/docs/src/_static/carrom-rl.png b/docs/src/_static/showcase/carrom-rl.png similarity index 100% rename from docs/src/_static/carrom-rl.png rename to docs/src/_static/showcase/carrom-rl.png diff --git a/docs/src/_static/showcase/cloth.png b/docs/src/_static/showcase/cloth.png new file mode 100644 index 00000000..3aa159db Binary files /dev/null and b/docs/src/_static/showcase/cloth.png differ diff --git a/docs/src/_static/showcase/fissionmunk.png b/docs/src/_static/showcase/fissionmunk.png new file mode 100644 index 00000000..ee21f0b5 Binary files /dev/null and b/docs/src/_static/showcase/fissionmunk.png differ diff --git a/docs/src/_static/galtonBoard.png b/docs/src/_static/showcase/galtonBoard.png similarity index 100% rename from docs/src/_static/galtonBoard.png rename to docs/src/_static/showcase/galtonBoard.png diff --git a/docs/src/_static/guide-the-ball.png b/docs/src/_static/showcase/guide-the-ball.png similarity index 100% rename from docs/src/_static/guide-the-ball.png rename to docs/src/_static/showcase/guide-the-ball.png diff --git a/docs/src/_static/invisipin.png b/docs/src/_static/showcase/invisipin.png similarity index 100% rename from docs/src/_static/invisipin.png rename to docs/src/_static/showcase/invisipin.png diff --git a/docs/src/_static/legged_robot.png b/docs/src/_static/showcase/legged_robot.png similarity index 100% rename from docs/src/_static/legged_robot.png rename to docs/src/_static/showcase/legged_robot.png diff --git a/docs/src/_static/manim-physics.png b/docs/src/_static/showcase/manim-physics.png similarity index 100% rename from docs/src/_static/manim-physics.png rename to docs/src/_static/showcase/manim-physics.png diff --git a/docs/src/_static/pykart-1.1.png b/docs/src/_static/showcase/pykart-1.1.png similarity index 100% rename from docs/src/_static/pykart-1.1.png rename to docs/src/_static/showcase/pykart-1.1.png diff --git a/docs/src/_static/pyphysicssandbox.png b/docs/src/_static/showcase/pyphysicssandbox.png similarity index 100% rename from docs/src/_static/pyphysicssandbox.png rename to docs/src/_static/showcase/pyphysicssandbox.png diff --git a/docs/src/_static/reinforcement-learning-car.png b/docs/src/_static/showcase/reinforcement-learning-car.png similarity index 100% rename from docs/src/_static/reinforcement-learning-car.png rename to docs/src/_static/showcase/reinforcement-learning-car.png diff --git a/docs/src/_static/suika-in-python.jpg b/docs/src/_static/showcase/suika-in-python.jpg similarity index 100% rename from docs/src/_static/suika-in-python.jpg rename to docs/src/_static/showcase/suika-in-python.jpg diff --git a/docs/src/_static/virtuaplant.png b/docs/src/_static/showcase/virtuaplant.png similarity index 100% rename from docs/src/_static/virtuaplant.png rename to docs/src/_static/showcase/virtuaplant.png diff --git a/docs/src/advanced.rst b/docs/src/advanced.rst index 176cdad7..20ad5110 100644 --- a/docs/src/advanced.rst +++ b/docs/src/advanced.rst @@ -1,289 +1,270 @@ -Advanced +Advanced ======== -In this section different "Advanced" topics are covered, things you normally -dont need to worry about when you use Pymunk but might be of interest if you -want a better understanding of Pymunk for example to extend it. +In this section different "Advanced" topics are covered, things you normally dont need +to worry about when you use Pymunk but might be of interest if you want a better +understanding of Pymunk for example to extend it. -First off, Pymunk is a pythonic library built around the C-library -Chipmunk2D, which provides almost all of the base functionality around the -physics simulation such as collision detection, impulse solving etc. -Bascially it runs the simulation, and Pymunk calls it with input, and -receives the result. - -To wrap Chipmunk Pymunk uses CFFI in API mode. On top of the CFFI wrapping is -a handmade pythonic layer to make it nice to use from Python code. +First off, Pymunk is a pythonic library built around the C-library Chipmunk2D, which +provides almost all of the base functionality around the physics simulation such as +collision detection, impulse solving etc. Bascially it runs the simulation, and Pymunk +calls it with input, and receives the result. +To wrap Chipmunk Pymunk uses CFFI in API mode. On top of the CFFI wrapping is a handmade +pythonic layer to make it nice to use from Python code. Impulse Solver Algorithm ------------------------ -Pymunk in itself only performs a minimum amount of physics calculation, -instead those are handled by the underlying C-library Chipmunk 2D. Chipmunk2D -(and therefor Pymunk) uses standard Euler to perform the integration - -Scott/slembcke (the creator of Chipmunk2D), wrote this to describe the method -used on the -`Chipmunk2D Forum `_: - - Chipmunk works like this: - - 1. Integrate the positions of everything and finds colliding pairs. - 2. Pre-calculate a number of properties of the contacts and joints. - (mass properties, bounce velocities, etc) - 3. Integrate the velocities of everything. - 4. Run a number of solver iterations, to fix velocity constraints. - - Case 1, a box sitting on the ground: - - 1. The box is at rest, it's velocity is (very near) zero, it's position - doesn't change. Generate a contact with the ground. - 2. Precalculate the contact properties, if elasticity is set, the - "bounce" velocity is calculated now (as 0). - 3. Integrate the velocity, gravity makes the object accelerate downwards. - 4. Solve the contact. Velocity should converge back to 0. If elasticity - was set, it will be handled correctly without resorting to threshold - velocities. - - #1 is really the most important part. If you were to update the - velocity before the postition, it would cause the box to move itself - into a position where it would intersect the ground. While Chipmunk has - to solve these overlaps anyway, avoiding them seems desirable. Another - very useful property is that when cpSpaceStep() returns, all of the - collision detection data structures are up to date. No need to reindex - them twice in a single frame if you want to make queries. +Pymunk in itself only performs a minimum amount of physics calculation, instead those +are handled by the underlying C-library Chipmunk 2D. Chipmunk2D (and therefor Pymunk) +uses standard Euler to perform the integration + +Scott/slembcke (the creator of Chipmunk2D), wrote this to describe the method used on +the `Chipmunk2D Forum +`_: + + Chipmunk works like this: + + 1. Integrate the positions of everything and finds colliding pairs. + 2. Pre-calculate a number of properties of the contacts and joints. (mass + properties, bounce velocities, etc) + 3. Integrate the velocities of everything. + 4. Run a number of solver iterations, to fix velocity constraints. + + Case 1, a box sitting on the ground: + 1. The box is at rest, it's velocity is (very near) zero, it's position doesn't + change. Generate a contact with the ground. + 2. Precalculate the contact properties, if elasticity is set, the "bounce" velocity + is calculated now (as 0). + 3. Integrate the velocity, gravity makes the object accelerate downwards. + 4. Solve the contact. Velocity should converge back to 0. If elasticity was set, it + will be handled correctly without resorting to threshold velocities. + + #1 is really the most important part. If you were to update the velocity before the + postition, it would cause the box to move itself into a position where it would + intersect the ground. While Chipmunk has to solve these overlaps anyway, avoiding + them seems desirable. Another very useful property is that when cpSpaceStep() + returns, all of the collision detection data structures are up to date. No need to + reindex them twice in a single frame if you want to make queries. Collision Detection Algorithm ----------------------------- -Just as the impulse solver, the collision detection is also handled by the -underlying C-library Chipmunk2D. +Just as the impulse solver, the collision detection is also handled by the underlying +C-library Chipmunk2D. -Chipmunk uses GJK/EPA to find collisions between the tricky cases (e.g. -polygons, segment shapes). There is a blog post -`here `_ +Chipmunk uses GJK/EPA to find collisions between the tricky cases (e.g. polygons, +segment shapes). There is a blog post `here +`_ with more details. - Why CFFI? --------- -This is a straight copy from the github issue tracking the CFFI upgrade. +This is a straight copy from the github issue tracking the CFFI upgrade. https://github.com/viblo/pymunk/issues/99 CFFI have a number of advantages but also a downsides. Advantages (compared to ctypes): -* Its an active project. The developers and users are active, there are new - releases being made and its possible to ask and get answers within a day on - the CFFI mailing list. -* Its said to be the way forward for Pypy, with promise of better performance - compares to ctypes. -* A little easier than ctypes to wrap things since you can just copy-paste the - c headers. +- Its an active project. The developers and users are active, there are new releases + being made and its possible to ask and get answers within a day on the CFFI mailing + list. +- Its said to be the way forward for Pypy, with promise of better performance compares + to ctypes. +- A little easier than ctypes to wrap things since you can just copy-paste the c + headers. Disadvatages (compared to ctypes): -* ctypes is part of the CPython standard library, CFFI is not. That means that - it will be more difficult to install Pymunk if it uses CFFI, since a - copy-paste install is no longer possible in an easy way. +- ctypes is part of the CPython standard library, CFFI is not. That means that it will + be more difficult to install Pymunk if it uses CFFI, since a copy-paste install is no + longer possible in an easy way. -For me I see the 1st advantage as the main point. I have had great difficulties -with strange segfaults with 64bit pythons on windows, and also sometimes on -32bit python, and support for 64bit python on both windows and linux is -something I really want. Hopefully those problems will be easier to handle with -CFFI since it has an active community. +For me I see the 1st advantage as the main point. I have had great difficulties with +strange segfaults with 64bit pythons on windows, and also sometimes on 32bit python, and +support for 64bit python on both windows and linux is something I really want. Hopefully +those problems will be easier to handle with CFFI since it has an active community. -Then comes the 3rd advantage, that its a bit easier to wrap the c code. For -ctypes I have a automatic wrapping script that does most of the low level -wrapping, but its not supported, very difficult to set up (I only managed -inside a VM with linux) and quite annoying. CFFI would be a clear improvement. +Then comes the 3rd advantage, that its a bit easier to wrap the c code. For ctypes I +have a automatic wrapping script that does most of the low level wrapping, but its not +supported, very difficult to set up (I only managed inside a VM with linux) and quite +annoying. CFFI would be a clear improvement. -For the disadvantage of ctypes I think it will be acceptable, even if not -ideal. Many python packages have to be installed in some way (like pygame), -and nowadays with pip its very easy to do. So I hope that it will be ok. +For the disadvantage of ctypes I think it will be acceptable, even if not ideal. Many +python packages have to be installed in some way (like pygame), and nowadays with pip +its very easy to do. So I hope that it will be ok. See the next section on why ctypes was used initially. Why ctypes? (OBSOLETE) ---------------------- -The reasons for ctypes instead of [your favorite wrapping solution] can be -summarized as - -* You only need to write pure python code when wrapping. This is good for - several reasons. I can not really code in c. Sure, I can read it and write - easy things, but Im not a good c coder. What I do know quite well is - python. I imagine that the same is true for most people using pymunk, - after all its a python library. :) Hopefully this means that users of - pymunk can look at how stuff is actually done very easily, and for example - add a missing chipmunk method/property on their own in their own code - without much problem, and without being required to compile/build anything. - -* ctypes is included in the standard library. Anyone with python has it - already, no dependencies on 3rd party libraries, and some guarantee that it - will stick around for a long time. - -* The only thing required to run pymunk is python and a c compiler (in those - cases a prebuilt version of chipmunk is not included). This should maximize - the multiplatformness of pymunk, only thing that would even better would - be a pure python library (which might be a bad idea for other reasons, - mainly speed). - -* Not much magic going on. Working with ctypes is quite straight forward. - Sure, pymunk uses a generator which is a bit of a pain, but at least its - possible to sidestep it if required, which Ive done in some cases. Ive also - got a share amount of problems when stuff didnt work as expected, but I - imagine it would have been even worse with other solutions. At least its - only the c library and python, and not some 3rd party in between. - -* Non api-breaking fixes in chipmunk doesnt affect pymunk. If a bugfix, some - optimization or whatever is done in chipmunk that doesnt affect the API, - then its enough with a recompile of chipmunk with the new code to benefit - from the fix. Easy for everyone. - -* Ctypes can run on other python implementations than cpython. Right now pypy - feels the most promising and it is be able to run ctypes just fine. - -As I see it, the main benefit another solution could give would be speed. -However, there are a couple of arguments why I dont find this as important as -the benefits of ctypes - -* You are writing your game in python in the first place, if you really - required top performance than maybe rewrite the whole thing in c would be - better anyway? Or make a optimized binding to chipmunk. - - For example, if you really need excellent performance then one possible - optimization would be to write the drawing code in c as well, and have that - interact with chipmunk directly. That way it can be made more performant - than any generic wrapping solution as it would skip the whole layer. - -* The bottleneck in a full game/application is somewhere else than in the - physics wrapping in many cases. If your game has AI, logic and so on in - python, then the wrapper overhead added by ctypes is not so bad in - comparison. - -* Pypy. ctypes on pypy has the potential to be very quick. However, right now - with pypy-1.9 the speed of pymunk is actually a bit slower on pypy than on - cpython. Hopefully this will improve in the future. - -Note that pymunk has been around since late 2007 which means not all -wrapping options that exist today did exist or was not stable/complete -enough for use by pymunk in the beginning. There are more options available -today, and using ctypes is not set in stone. If a better alternative comes -around then pymunk might switch given the improvements are big enough. - +The reasons for ctypes instead of [your favorite wrapping solution] can be summarized as + +- You only need to write pure python code when wrapping. This is good for several + reasons. I can not really code in c. Sure, I can read it and write easy things, but Im + not a good c coder. What I do know quite well is python. I imagine that the same is + true for most people using pymunk, after all its a python library. :) Hopefully this + means that users of pymunk can look at how stuff is actually done very easily, and for + example add a missing chipmunk method/property on their own in their own code without + much problem, and without being required to compile/build anything. +- ctypes is included in the standard library. Anyone with python has it already, no + dependencies on 3rd party libraries, and some guarantee that it will stick around for + a long time. +- The only thing required to run pymunk is python and a c compiler (in those cases a + prebuilt version of chipmunk is not included). This should maximize the + multiplatformness of pymunk, only thing that would even better would be a pure python + library (which might be a bad idea for other reasons, mainly speed). +- Not much magic going on. Working with ctypes is quite straight forward. Sure, pymunk + uses a generator which is a bit of a pain, but at least its possible to sidestep it if + required, which Ive done in some cases. Ive also got a share amount of problems when + stuff didnt work as expected, but I imagine it would have been even worse with other + solutions. At least its only the c library and python, and not some 3rd party in + between. +- Non api-breaking fixes in chipmunk doesnt affect pymunk. If a bugfix, some + optimization or whatever is done in chipmunk that doesnt affect the API, then its + enough with a recompile of chipmunk with the new code to benefit from the fix. Easy + for everyone. +- Ctypes can run on other python implementations than cpython. Right now pypy feels the + most promising and it is be able to run ctypes just fine. + +As I see it, the main benefit another solution could give would be speed. However, there +are a couple of arguments why I dont find this as important as the benefits of ctypes + +- You are writing your game in python in the first place, if you really required top + performance than maybe rewrite the whole thing in c would be better anyway? Or make a + optimized binding to chipmunk. + + For example, if you really need excellent performance then one possible optimization + would be to write the drawing code in c as well, and have that interact with chipmunk + directly. That way it can be made more performant than any generic wrapping solution + as it would skip the whole layer. + +- The bottleneck in a full game/application is somewhere else than in the physics + wrapping in many cases. If your game has AI, logic and so on in python, then the + wrapper overhead added by ctypes is not so bad in comparison. +- Pypy. ctypes on pypy has the potential to be very quick. However, right now with + pypy-1.9 the speed of pymunk is actually a bit slower on pypy than on cpython. + Hopefully this will improve in the future. + +Note that pymunk has been around since late 2007 which means not all wrapping options +that exist today did exist or was not stable/complete enough for use by pymunk in the +beginning. There are more options available today, and using ctypes is not set in stone. +If a better alternative comes around then pymunk might switch given the improvements are +big enough. Code Layout ----------- Most of Pymunk should be quite straight forward. -Except for the documented API Pymunk has a couple of interesting parts. Low -level bindings to Chipmunk, a custom documentation generation extension and a -customized setup.py file to allow compilation of Chipmunk. - - +Except for the documented API Pymunk has a couple of interesting parts. Low level +bindings to Chipmunk, a custom documentation generation extension and a customized +setup.py file to allow compilation of Chipmunk. docs/src/ext/autoexample.py - A Sphinx extension that scans a directory and extracts the toplevel - docstring. Used to autogenerate the examples documentation. + A Sphinx extension that scans a directory and extracts the toplevel docstring. Used + to autogenerate the examples documentation. pymunk/_callbacks.py Callbacks cannot be specified on a class, so they are all gathered here. pymunk/_chipmunk_cffi.py - This file only contains a call to _cffi_backend .py, and exists mostly - as a wrapper to be able to switch between abi and api mode of Cffi. This - is currently not in use in the released code, but is used during - experimentation. - + This file only contains a call to _cffi_backend .py, and exists mostly as a wrapper + to be able to switch between abi and api mode of Cffi. This is currently not in use + in the released code, but is used during experimentation. + pymunk/pymunk_extension_build.py - The low level Chipmunk bindings are located in this file. Contains - configuration for how to build Chipmunk2D by Cffi into a pyd file. + The low level Chipmunk bindings are located in this file. Contains configuration for + how to build Chipmunk2D by Cffi into a pyd file. setup.py - Except for the standard setup stuff this file also contain the custom - build commands to build Chipmunk from source, using a build_ext extension. + Except for the standard setup stuff this file also contain the custom build commands + to build Chipmunk from source, using a build_ext extension. pymunk/examples/* - Collection of examples of usages of Pymunk. Tries to showcase most common - features of Pymunk. + Collection of examples of usages of Pymunk. Tries to showcase most common features + of Pymunk. pymunk/tests/* - Collection of (unit) tests. Does not cover all cases, but most core - things are there. The tests require a working chipmunk library file. + Collection of (unit) tests. Does not cover all cases, but most core things are + there. The tests require a working chipmunk library file. pymunk_cffi/* - Custom (c source and header) extensions to Chipmunk2D used by Pymunk. - Will be used when Cffi builds the pyd extension. + Custom (c source and header) extensions to Chipmunk2D used by Pymunk. Will be used + when Cffi builds the pyd extension. tools/* - Collection of helper scripts that can be used to various development tasks - such as generating documentation. - + Collection of helper scripts that can be used to various development tasks such as + generating documentation. Tests ----- -There are a number of unit tests included in the pymunk.tests package -(pymunk/tests). Not exactly all the code is tested, but most of it (at the time -of writing its about 85% of the core parts). +There are a number of unit tests included in the pymunk.tests package (pymunk/tests). +Not exactly all the code is tested, but most of it (at the time of writing its about 85% +of the core parts). + +The tests can be run by calling the module -The tests can be run by calling the module :: +:: > python -m pymunk.tests -Its possible to control which tests to run, by specifying a filtering -argument. The matching is as broad as possible, so `UnitTest` matches all the -unit tests, `test_arbiter` all tests in `test_arbiter.py` and -`testResetitution` matches the exact `testRestitution` test case :: +Its possible to control which tests to run, by specifying a filtering argument. The +matching is as broad as possible, so `UnitTest` matches all the unit tests, +`test_arbiter` all tests in `test_arbiter.py` and `testResetitution` matches the exact +`testRestitution` test case + +:: > python -m pymunk.tests -f testRestitution -To see all options to the tests command use -h :: +To see all options to the tests command use -h + +:: > python -m pymunk.tests -h -Since the tests cover even the optional parts, you either have to make sure -all the optional dependencies are installed, or filter out those tests. - - +Since the tests cover even the optional parts, you either have to make sure all the +optional dependencies are installed, or filter out those tests. + Working with non-wrapped parts of Chipmunk ------------------------------------------ -In case you need to use something that exist in Chipmunk but currently is not -included in pymunk the easiest method is to add it manually. +In case you need to use something that exist in Chipmunk but currently is not included +in pymunk the easiest method is to add it manually. -For example, lets assume that the is_sleeping property of a body was not -wrapped by pymunk. The Chipmunk method to get this property is named -cpBodyIsSleeping. +For example, lets assume that the is_sleeping property of a body was not wrapped by +pymunk. The Chipmunk method to get this property is named cpBodyIsSleeping. -First we need to check if its included in the cdef definition in +First we need to check if its included in the cdef definition in pymunk_extension_build.py. If its not just add it. - + `cpBool cpBodyIsSleeping(const cpBody *body);` - -Then to make it easy to use we want to create a python method that looks nice:: + +Then to make it easy to use we want to create a python method that looks nice: + +:: def is_sleeping(body): return cp.cpBodyIsSleeping(body._body) Now we are ready with the mapping and ready to use our new method. - -Note that any cdef methods are not part of the public API, and as such might -change between minor or patch verisons of Pymunk. +Note that any cdef methods are not part of the public API, and as such might change +between minor or patch verisons of Pymunk. Weak References and free Methods ------------------------------------ - -Internally Pymunk allocates structs from Chipmunk (the c library). For example a -Body struct is created from inside the constructor method when a pymunk.Body is -created. Because of this its important that the corresponding c side memory is -deallocated properly when not needed anymore, usually when the Python side -object is garbage collected. Most Pymunk objects use `ffi.gc` with a custom -free function to do this. Note that the order of freeing is very important to -avoid errors. +-------------------------------- + +Internally Pymunk allocates structs from Chipmunk (the c library). For example a Body +struct is created from inside the constructor method when a pymunk.Body is created. +Because of this its important that the corresponding c side memory is deallocated +properly when not needed anymore, usually when the Python side object is garbage +collected. Most Pymunk objects use `ffi.gc` with a custom free function to do this. Note +that the order of freeing is very important to avoid errors. diff --git a/docs/src/conf.py b/docs/src/conf.py index 1bb31b5b..eb1b35c6 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -93,4 +93,6 @@ "pygame.locals", "pygame.color", "pyglet", + "pillow", + "PIL" ] diff --git a/docs/src/showcase.rst b/docs/src/showcase.rst index fb9db392..baa22014 100644 --- a/docs/src/showcase.rst +++ b/docs/src/showcase.rst @@ -16,7 +16,7 @@ Suika in Python --------------- :sup:`made by Ole-Batting. Retrieved 2024-08-18` -.. figure:: _static/suika-in-python.jpg +.. figure:: _static/showcase/suika-in-python.jpg :target: https://youtu.be/In0EvhenzeQ A reimplementation of the `Suika Game @@ -31,7 +31,7 @@ PyKart - build and drive your vehicles -------------------------------------- :sup:`made by Valdotorium. Retrieved 2024-08-18` -.. figure:: _static/pykart-1.1.png +.. figure:: _static/showcase/pykart-1.1.png :target: https://valdotorium.itch.io/pykart A driving game in which players can drive the vehicles they built in generated @@ -43,7 +43,7 @@ Guide The Ball -------------- :sup:`made by Francis Ali. Retrieved 2020-03-30` -.. figure:: _static/guide-the-ball.png +.. figure:: _static/showcase/guide-the-ball.png :target: https://github.com/francisA88/Guide-The-Ball-1 A game combining Pymunk with Kivy that runs on Android mobiles! In Guide The @@ -54,7 +54,7 @@ My Sincerest Apologies ---------------------- :sup:`made by The Larry and Dan show (mauve, larry). Retrieved 2018-10-25` -.. figure:: _static/My_Sincerest_Apologies.png +.. figure:: _static/showcase/My_Sincerest_Apologies.png :target: https://pyweek.org/e/wasabi24/ **Winner of PyWeek 24** (Overall Team Entry) @@ -77,7 +77,7 @@ Beneath the Ice :sup:`made by Team Chimera (mit-mit, Lucid Design Ar). Retrieved 2016-09-25` -.. figure:: _static/beneath-the-ice.png +.. figure:: _static/showcase/beneath-the-ice.png :target: https://pyweek.org/e/chimera22/ **Winner of PyWeek 22** (Overall Team Entry) @@ -91,7 +91,7 @@ Invisipin --------- :sup:`made by Tee. Retrieved 2016-01-25` -.. figure:: _static/invisipin.png +.. figure:: _static/showcase/invisipin.png :target: https://pyweek.org/e/Tee-py20/ **Winner of PyWeek 20** (Overall Individual Entry) @@ -104,7 +104,7 @@ Angry Birds in Python --------------------- :sup:`made by Estevao Fonseca. Retrieved 2016-10-30` -.. figure:: _static/angry-birds-python.png +.. figure:: _static/showcase/angry-birds-python.png :target: https://github.com/estevaofon/angry-birds-python Angry Birds game written in python using Pygame and Pymunk! @@ -114,7 +114,7 @@ SubTerrex --------- :sup:`made by Paul Paterson. Retrieved 2016-01-25` -.. figure:: _static/SubTerrex.png +.. figure:: _static/showcase/SubTerrex.png :target: http://www.pygame.org/project-SubTerrex-2389-.html A cave exploration game where you explore caves by descending into them on @@ -124,12 +124,38 @@ ropes. Non-Games ========= + +2d Net/Cloth Simulation +----------------------- +:sup:`made by Retrieved 2025-09-19` + +.. figure:: _static/showcase/cloth.png + :target: https://youtu.be/PkF7_AFVdE0?si=bCQMAA6yGB0f8RIP + +Ever wanted to create a somewhat realistic 2d simulation? Here's a beautiful +demonstration of it's power when combined with a GUI library like Kivy. + + +FissionMunk +----------- +:sup:`made by Aniket Mishra. Retrieved 2025-09-17` + +.. figure:: _static/showcase/fissionmunk.png + :target: https://github.com/iAniket23/fissionmunk + +FissionMunk is a lightweight 2D physics open source python library designed to +simulate nuclear fission reactor mechanics. It enables users to visualize +interactions between neutrons and uranium atoms, providing valuable insights +into fission dynamics with customizable parameters, such as neutron occurrence +probabilities, moderators, and control rods. + + AUTO-IceNav ----------- :sup:`made by de Schaetzen, Rodrigue and Botros, Alexander and Zhong, Ninghan and Murrant, Kevin and Gash. Retrieved 2024-11-27` -.. figure:: _static/auto-icenav.jpg +.. figure:: _static/showcase/auto-icenav.jpg :target: https://github.com/rdesc/AUTO-IceNav Ice conditions often require ships to reduce speed and deviate from their main @@ -144,7 +170,7 @@ Bouncing Balls, Beautiful Patterns ---------------------------------- :sup:`made by Alessandro Giusti. Retrieved 2022-01-25` -.. figure:: _static/bouncingballs-beatuifulpatterns.png +.. figure:: _static/showcase/bouncingballs-beatuifulpatterns.png :target: https://youtu.be/nDhOGsBj1fA Satisfying simulations of bouncing balls obeying physical laws; for a fleeting @@ -160,7 +186,7 @@ Manim Physics ------------- :sup:`made by pdcxs, Matheart & Iced-Tea3. Retrieved 2021-07-05` -.. figure:: _static/manim-physics.png +.. figure:: _static/showcase/manim-physics.png :target: https://github.com/Matheart/manim-physics This is a 2D physics simulation plugin for `Manim @@ -173,7 +199,7 @@ Computer Vision and Phyiscs --------------------------- :sup:`made by Amirabbas Asadi. Retrieved 2021-06-25` -.. figure:: _static/ar-physics.jpg +.. figure:: _static/showcase/ar-physics.jpg :target: https://amirabbasasadi.github.io/vision/python/2021/06/03/having-fun-with-computer-vision-and-physics.html Just playing with OpenCV + a hand tracking model + a physics engine :) @@ -189,7 +215,7 @@ Galton Board ------------ :sup:`made by Nav. Retrieved 2021-05-17` -.. figure:: _static/galtonBoard.png +.. figure:: _static/showcase/galtonBoard.png :target: https://github.com/nav9/GaltonBoard The quincunx (also known as a Galton board) created in Pymunk. @@ -204,7 +230,7 @@ PySimpleGUI Desktop Demo ------------------------ :sup:`made by PySimpleGUI/Mike. Retrieved 2020-10-13` -.. figure:: _static/PySimpleGUI.png +.. figure:: _static/showcase/PySimpleGUI.png :target: https://github.com/PySimpleGUI/PySimpleGUI Demo of using PySimpleGUI together with Pymunk to create bouncing balls @@ -215,7 +241,7 @@ Legged robot using differential evolution and perception -------------------------------------------------------- :sup:`made by Nav. Retrieved 2020-08-20` -.. figure:: _static/legged_robot.png +.. figure:: _static/showcase/legged_robot.png :target: https://www.youtube.com/watch?v=n-OCy6ToLsU&feature=youtu.be Legged robot first using Differential Evolution to navigate terrain and then @@ -226,7 +252,7 @@ Simulation of ambient chimes | Circle in a hexagon -------------------------------------------------- :sup:`made by Jan Abraham.Retrieved 2019-11-17` -.. figure:: _static/ambient-chimes.png +.. figure:: _static/showcase/ambient-chimes.png :target: https://youtu.be/7MRJS8ZV9VA An ambient piano chord produced by the simulation of a bouncing ball. The @@ -237,7 +263,7 @@ I teach AI to move with using NEAT ---------------------------------- :sup:`made by Cheesy AI. Retrieved 2019-11-17` -.. figure:: _static/aimoveneat.png +.. figure:: _static/showcase/aimoveneat.png :target: https://youtu.be/ipWIH1g9DSw Recently I learned Pymunk 2d physics library. It is very cool so with that I @@ -250,7 +276,7 @@ Car Configuration with Differential Evolotion --------------------------------------------- :sup:`made by Nav. Retrieved 2019-05-05` -.. figure:: _static/carconf.png +.. figure:: _static/showcase/carconf.png :target: https://youtu.be/7ok4ESgrKg0 Among the simplest AI algorithms: Differential Evolution. Brought to life with @@ -267,7 +293,7 @@ VirtuaPlant ----------- :sup:`made by Jan Seidl. Retrieved 2018-06-13` -.. figure:: _static/virtuaplant.png +.. figure:: _static/showcase/virtuaplant.png :target: https://wroot.org/projects/virtuaplant/ VirtuaPlant is an Industrial Control Systems simulator which adds a "similar @@ -282,7 +308,7 @@ The Python Arcade Library ------------------------- :sup:`made by Paul. Retrieved 2018-03-05` -.. figure:: _static/arcade-library.png +.. figure:: _static/showcase/arcade-library.png :target: http://arcade.academy/examples/index.html Arcade is an easy-to-learn Python library for creating 2D video games. It is @@ -294,7 +320,7 @@ billiARds - A Game of Augmented Reality Pool -------------------------------------------- :sup:`made by Alex Baikovitz. Retrieved 2017-05-21` -.. figure:: _static/billiARds.png +.. figure:: _static/showcase/billiARds.png :target: https://youtu.be/5ft3SDvuhgw Alex built billiARds for his 15-112 (Fundamentals of Programming and Computer @@ -307,7 +333,7 @@ pyPhysicsSandbox ---------------- :sup:`made by Jay Shaffstall. Retrieved 2017-01-01` -.. figure:: _static/pyphysicssandbox.png +.. figure:: _static/showcase/pyphysicssandbox.png :target: https://github.com/jshaffstall/PyPhysicsSandbox pyPhysicsSandbox is a simple wrapper around Pymunk that makes it easy to write @@ -319,7 +345,7 @@ Carrom Simulation ----------------- :sup:`made by Samiran Roy. Retrieved 2016-10-27` -.. figure:: _static/carrom-rl.png +.. figure:: _static/showcase/carrom-rl.png :target: https://github.com/samiranrl/Carrom_rl An open source Carrom Simulator interface for testing intelligent/learning @@ -333,7 +359,7 @@ Self Driving Car ---------------- :sup:`made by Matt Harvey. Retrieved 2016-08-07` -.. figure:: _static/reinforcement-learning-car.png +.. figure:: _static/showcase/reinforcement-learning-car.png :target: https://github.com/harvitronix/reinforcement-learning-car A project that trains a virtual car to how to move an object around a screen @@ -353,9 +379,19 @@ List of papers which has used or mentioned Pymunk: An Interconnected Soft Modular Robot with Locomotive Modules and Flexible Structures Actuated Through a Single Method An Adaptive Flooding Detection Framework With Blockchain Mitigation for Satellite Communications TR‐Bench: Benchmarking Deep Reinforcement Learning for Flipper‐Track Robot Control + Towards Generalist Robots: Action Reasoning and Scaling Data + Composing Intuitive Theories Across Domains and in Real Time .. (list made using "Chicago" style citation) +#. Zandonati, Ben, Tomás Lozano-Pérez, and Leslie Pack Kaelbling. + "Rational Inverse Reasoning." + arXiv preprint arXiv:2508.08983 (2025). + +#. Balaban, Halely, and Tomer D. Ullman. + "The capacity limits of moving objects in the imagination." + Nature Communications 16, no. 1 (2025): 5899. + #. Schiøtt, Jonas Myhre, Viktor Sebastian Petersen, and Dimitrios P. Papadopoulos. "pix2pockets: Shot Suggestions in 8-Ball Pool from a Single Image in the Wild." arXiv preprint arXiv:2504.12045 (2025). @@ -436,10 +472,6 @@ List of papers which has used or mentioned Pymunk: "RoboPack: Learning Tactile-Informed Dynamics Models for Dense Packing." In ICRA 2024 Workshop on 3D Visual Representations for Robot Manipulation. -#. Balaban, Halely, and Tomer Ullman. - "The capacity limits of tracking in the imagination." - (2024). - #. Memery, Sean, Mirella Lapata, and Kartic Subr. "SimLM: Can Language Models Infer Parameters of Physical Systems?." arXiv preprint arXiv:2312.14215 (2023). diff --git a/dump/pillow_util.py b/dump/pillow_util.py new file mode 100644 index 00000000..ae3df1a2 --- /dev/null +++ b/dump/pillow_util.py @@ -0,0 +1,151 @@ +# ---------------------------------------------------------------------------- +# pymunk +# Copyright (c) 2007-2025 Victor Blomqvist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ---------------------------------------------------------------------------- + +"""This submodule contains helper functions to help with quick prototyping +using pymunk together with Pillow/PIL. + +For now this is experimental. + +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. +""" + +__docformat__ = "reStructuredText" + +__all__ = [ + "DrawOptions", +] + +from typing import Sequence + +from PIL import ImageDraw, Image + +import pymunk +from pymunk.space_debug_draw_options import SpaceDebugColor +from pymunk.vec2d import Vec2d + + +class DrawOptions(pymunk.SpaceDebugDrawOptions): + def __init__(self, im: Image.Image) -> None: + """Draw a pymunk.Space on a pillow Image object. + + Typical usage:: + + >>> import pymunk + >>> from PIL import Image + >>> image = Image.new("RGB", (1000, 700), "white") + >>> space = pymunk.Space() + >>> options = pymunk.pillow_util.DrawOptions(image) + >>> space.debug_draw(options) + + You can control the color of a shape by setting shape.color to the color + you want it drawn in:: + + >>> c = pymunk.Circle(None, 10) + >>> c.color = (255,0,0,255) + + See pillow_util.demo.py for a full example + + :Parameters: + im : Image.Image + Image that the objects will be drawn on + """ + self.image = im + self.draw = ImageDraw.Draw(im) + super(DrawOptions, self).__init__() + + def draw_circle( + self, + pos: Vec2d, + angle: float, + radius: float, + outline_color: SpaceDebugColor, + fill_color: SpaceDebugColor, + ) -> None: + self.draw.circle(pos, radius, fill_color.as_int(), outline_color.as_int(), 1) + + circle_edge = pos + Vec2d.from_polar(radius, angle) + p2 = circle_edge + line_r = 2 if radius > 20 else 1 + self.draw.line([pos, p2], outline_color.as_int(), line_r) + + + def draw_segment(self, a: Vec2d, b: Vec2d, color: SpaceDebugColor) -> None: + self.draw.line([a,b], color.as_int()) + + def draw_fat_segment( + self, + a: tuple[float, float], + b: tuple[float, float], + radius: float, + outline_color: SpaceDebugColor, + fill_color: SpaceDebugColor, + ) -> None: + p1 = a + p2 = b + + r = round(max(1, radius * 2)) + self.draw.line([a,b], fill_color.as_int(), r) + + if r > 2: + orthog = [abs(p2[1] - p1[1]), abs(p2[0] - p1[0])] + if orthog[0] == 0 and orthog[1] == 0: + return + scale = radius / (orthog[0] * orthog[0] + orthog[1] * orthog[1]) ** 0.5 + orthog[0] = round(orthog[0] * scale) + orthog[1] = round(orthog[1] * scale) + points = [ + (p1[0] - orthog[0], p1[1] - orthog[1]), + (p1[0] + orthog[0], p1[1] + orthog[1]), + (p2[0] + orthog[0], p2[1] + orthog[1]), + (p2[0] - orthog[0], p2[1] - orthog[1]), + ] + self.draw.polygon(points, fill_color.as_int()) + self.draw.circle((round(p1[0]), round(p1[1])), radius-1, fill_color.as_int()) + self.draw.circle((round(p2[0]), round(p2[1])), radius-1, fill_color.as_int()) + + def draw_polygon( + self, + verts: Sequence[tuple[float, float]], + radius: float, + outline_color: SpaceDebugColor, + fill_color: SpaceDebugColor, + ) -> None: + ps = [v for v in verts] + ps += [ps[0]] + + self.draw.polygon(ps, fill_color.as_int()) + + if radius > 0: + for i in range(len(verts)): + a = verts[i] + b = verts[(i + 1) % len(verts)] + self.draw_fat_segment(a, b, radius, outline_color, outline_color) + + def draw_dot( + self, size: float, pos: tuple[float, float], color: SpaceDebugColor + ) -> None: + self.draw.circle(pos, size, color.as_int(), width=0) + + diff --git a/dump/pillow_util_demo.py b/dump/pillow_util_demo.py new file mode 100644 index 00000000..2baffc96 --- /dev/null +++ b/dump/pillow_util_demo.py @@ -0,0 +1,36 @@ +"""Showcase what the output of pymunk.pyglet_util draw methods will look like. + +See pygame_util_demo.py for a comparison to pygame. +""" + +__docformat__ = "reStructuredText" + +from PIL import Image, ImageDraw, ImageColor, ImageShow + +import pymunk +import pymunk.pillow_util + +from .shapes_for_draw_demos import fill_space + +img = Image.new("RGB", (1000, 700), "white") +draw = ImageDraw.Draw(img) + +space = pymunk.Space() +draw_options = pymunk.pillow_util.DrawOptions(img) +captions = fill_space(space) + + + +labels = [] + +draw.text((5,5),"Demo example of shapes drawn by pillow_util.draw()", fill=(100,100,100)) + +for caption in captions: + x, y = caption[0] + y = y - 10 + draw.text((x,y),caption[1], fill=(50,50,50)) + +space.debug_draw(draw_options) + +img.save("pillow_util_demo.png") +ImageShow.show(img) \ No newline at end of file diff --git a/pymunk/_version.py b/pymunk/_version.py index a2586964..3e8c70f2 100644 --- a/pymunk/_version.py +++ b/pymunk/_version.py @@ -32,9 +32,9 @@ cp = _chipmunk_cffi.lib ffi = _chipmunk_cffi.ffi -version = "7.0.1" +version = "7.2.0" chipmunk_version = "%s-%s" % ( ffi.string(cp.cpVersionString).decode("utf-8"), - "5ef7498946f0e956f294cb3fea283626921e4128", + "ade7ed72849e60289eefb7a41e79ae6322fefaf3", ) diff --git a/pymunk/arbiter.py b/pymunk/arbiter.py index a78708ef..e07f0e88 100644 --- a/pymunk/arbiter.py +++ b/pymunk/arbiter.py @@ -155,29 +155,26 @@ def friction(self) -> float: def friction(self, friction: float) -> None: lib.cpArbiterSetFriction(self._arbiter, friction) - def _get_surface_velocity(self) -> Vec2d: + @property + def surface_velocity(self) -> Vec2d: + """The calculated surface velocity for this collision pair. + + Setting the value in a pre_solve() callback will override the value + calculated by the space. the default calculation subtracts the + surface velocity of the second shape from the first and then projects + that onto the tangent of the collision. This is so that only + friction is affected by default calculation. Using a custom + calculation, you can make something that responds like a pinball + bumper, or where the surface velocity is dependent on the location + of the contact point. + """ v = lib.cpArbiterGetSurfaceVelocity(self._arbiter) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, velocity: tuple[float, float]) -> None: + @surface_velocity.setter + def surface_velocity(self, velocity: tuple[float, float]) -> None: lib.cpArbiterSetSurfaceVelocity(self._arbiter, velocity) - surface_velocity = property( - _get_surface_velocity, - _set_surface_velocity, - doc="""The calculated surface velocity for this collision pair. - - Setting the value in a pre_solve() callback will override the value - calculated by the space. the default calculation subtracts the - surface velocity of the second shape from the first and then projects - that onto the tangent of the collision. This is so that only - friction is affected by default calculation. Using a custom - calculation, you can make something that responds like a pinball - bumper, or where the surface velocity is dependent on the location - of the contact point. - """, - ) - @property def total_impulse(self) -> Vec2d: """Returns the impulse that was applied this step to resolve the diff --git a/pymunk/body.py b/pymunk/body.py index b63635c0..bd26b212 100644 --- a/pymunk/body.py +++ b/pymunk/body.py @@ -275,74 +275,62 @@ def moment(self, moment: float) -> None: ), "Dynamic bodies must have moment > 0 if they are attached to a Space" lib.cpBodySetMoment(self._body, moment) - def _set_position(self, pos: tuple[float, float]) -> None: - assert len(pos) == 2 - lib.cpBodySetPosition(self._body, pos) + @property + def position(self) -> Vec2d: + """Position of the body. - def _get_position(self) -> Vec2d: + When changing the position 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.""" v = lib.cpBodyGetPosition(self._body) return Vec2d(v.x, v.y) - position = property( - _get_position, - _set_position, - doc="""Position of the body. - - When changing the position 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.""", - ) - - def _set_center_of_gravity(self, cog: tuple[float, float]) -> None: - assert len(cog) == 2 - lib.cpBodySetCenterOfGravity(self._body, cog) - - def _get_center_of_gravity(self) -> Vec2d: - v = lib.cpBodyGetCenterOfGravity(self._body) - return Vec2d(v.x, v.y) + @position.setter + def position(self, pos: tuple[float, float]) -> None: + assert len(pos) == 2 + lib.cpBodySetPosition(self._body, pos) - center_of_gravity = property( - _get_center_of_gravity, - _set_center_of_gravity, - doc="""Location of the center of gravity in body local coordinates. + @property + def center_of_gravity(self) -> Vec2d: + """Location of the center of gravity in body local coordinates. The default value is (0, 0), meaning the center of gravity is the same as the position of the body. - """, - ) + """ + v = lib.cpBodyGetCenterOfGravity(self._body) + return Vec2d(v.x, v.y) - def _set_velocity(self, vel: tuple[float, float]) -> None: - assert len(vel) == 2 - lib.cpBodySetVelocity(self._body, vel) + @center_of_gravity.setter + def center_of_gravity(self, cog: tuple[float, float]) -> None: + assert len(cog) == 2 + lib.cpBodySetCenterOfGravity(self._body, cog) - def _get_velocity(self) -> Vec2d: + @property + def velocity(self) -> Vec2d: + """Linear velocity of the center of gravity of the body.""" v = lib.cpBodyGetVelocity(self._body) return Vec2d(v.x, v.y) - velocity = property( - _get_velocity, - _set_velocity, - doc="""Linear velocity of the center of gravity of the body.""", - ) + @velocity.setter + def velocity(self, vel: tuple[float, float]) -> None: + assert len(vel) == 2 + lib.cpBodySetVelocity(self._body, vel) - def _set_force(self, f: tuple[float, float]) -> None: - assert len(f) == 2 - lib.cpBodySetForce(self._body, f) + @property + def force(self) -> Vec2d: + """Force applied to the center of gravity of the body. - def _get_force(self) -> Vec2d: + This value is reset for every time step. Note that this is not the + total of forces acting on the body (such as from collisions), but the + force applied manually from the apply force functions.""" v = lib.cpBodyGetForce(self._body) return Vec2d(v.x, v.y) - force = property( - _get_force, - _set_force, - doc="""Force applied to the center of gravity of the body. - - This value is reset for every time step. Note that this is not the - total of forces acting on the body (such as from collisions), but the - force applied manually from the apply force functions.""", - ) + @force.setter + def force(self, f: tuple[float, float]) -> None: + assert len(f) == 2 + lib.cpBodySetForce(self._body, f) @property def angle(self) -> float: @@ -404,28 +392,19 @@ def space(self) -> Optional["Space"]: ) return self._space() - def _set_velocity_func(self, func: _VelocityFunc) -> None: - if func == Body.update_velocity: - lib.cpBodySetVelocityUpdateFunc( - self._body, ffi.addressof(lib, "cpBodyUpdateVelocity") - ) - else: - self._velocity_func = func - lib.cpBodySetVelocityUpdateFunc(self._body, lib.ext_cpBodyVelocityFunc) + @property + def velocity_func(self) -> _VelocityFunc: + """The velocity callback function. - velocity_func = property( - fset=_set_velocity_func, - doc="""The velocity callback function. - - The velocity callback function is called each time step, and can be + The velocity callback function is called each time step, and can be used to set a body's velocity. ``func(body : Body, gravity, damping, dt)`` - There are many cases when this can be useful. One example is individual - gravity for some bodies, and another is to limit the velocity which is - useful to prevent tunneling. - + There are many cases when this can be useful. One example is individual + gravity for some bodies, and another is to limit the velocity which is + useful to prevent tunneling. + Example of a callback that sets gravity to zero for a object. >>> import pymunk @@ -435,7 +414,7 @@ def _set_velocity_func(self, func: _VelocityFunc) -> None: >>> space.add(body) >>> def zero_gravity(body, gravity, damping, dt): ... pymunk.Body.update_velocity(body, (0,0), damping, dt) - ... + ... >>> body.velocity_func = zero_gravity >>> space.step(1) >>> space.step(1) @@ -456,10 +435,38 @@ def _set_velocity_func(self, func: _VelocityFunc) -> None: ... >>> body.velocity_func = limit_velocity - """, - ) + """ + if self._velocity_func is None: + return Body.update_velocity + else: + return self._velocity_func + + @velocity_func.setter + def velocity_func(self, func: _VelocityFunc) -> None: + if func == Body.update_velocity: + lib.cpBodySetVelocityUpdateFunc( + self._body, ffi.addressof(lib, "cpBodyUpdateVelocity") + ) + else: + self._velocity_func = func + lib.cpBodySetVelocityUpdateFunc(self._body, lib.ext_cpBodyVelocityFunc) + + @property + def position_func(self) -> _PositionFunc: + """The position callback function. + + The position callback function is called each time step and can be + used to update the body's position. + + ``func(body, dt) -> None`` + """ + if self._position_func == None: + return Body.update_position + else: + return self._position_func - def _set_position_func(self, func: _PositionFunc) -> None: + @position_func.setter + def position_func(self, func: _PositionFunc) -> None: if func == Body.update_position: lib.cpBodySetPositionUpdateFunc( self._body, ffi.addressof(lib, "cpBodyUpdatePosition") @@ -468,17 +475,6 @@ def _set_position_func(self, func: _PositionFunc) -> None: self._position_func = func lib.cpBodySetPositionUpdateFunc(self._body, lib.ext_cpBodyPositionFunc) - position_func = property( - fset=_set_position_func, - doc="""The position callback function. - - The position callback function is called each time step and can be - used to update the body's position. - - ``func(body, dt) -> None`` - """, - ) - @property def kinetic_energy(self) -> float: """Get the kinetic energy of a body.""" diff --git a/pymunk/constraints.py b/pymunk/constraints.py index 858874ee..145ab80a 100644 --- a/pymunk/constraints.py +++ b/pymunk/constraints.py @@ -329,26 +329,26 @@ def __init__( _constraint = lib.cpPinJointNew(a._body, b._body, anchor_a, anchor_b) self._init(a, b, _constraint) - def _get_anchor_a(self) -> Vec2d: + @property + def 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: + @anchor_a.setter + def anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPinJointSetAnchorA(self._constraint, anchor) - anchor_a = property(_get_anchor_a, _set_anchor_a) - - def _get_anchor_b(self) -> Vec2d: + @property + def 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: + @anchor_b.setter + def 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) - @property def distance(self) -> float: return lib.cpPinJointGetDist(self._constraint) @@ -392,26 +392,26 @@ def __init__( ) self._init(a, b, _constraint) - def _get_anchor_a(self) -> Vec2d: + @property + def 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: + @anchor_a.setter + def anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpSlideJointSetAnchorA(self._constraint, anchor) - anchor_a = property(_get_anchor_a, _set_anchor_a) - - def _get_anchor_b(self) -> Vec2d: + @property + def 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: + @anchor_b.setter + def 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) - @property def min(self) -> float: return lib.cpSlideJointGetMin(self._constraint) @@ -476,26 +476,26 @@ def __init__( self._init(a, b, _constraint) - def _get_anchor_a(self) -> Vec2d: + @property + def 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: + @anchor_a.setter + def anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPivotJointSetAnchorA(self._constraint, anchor) - anchor_a = property(_get_anchor_a, _set_anchor_a) - - def _get_anchor_b(self) -> Vec2d: + @property + def 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: + @anchor_b.setter + def anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpPivotJointSetAnchorB(self._constraint, anchor) - anchor_b = property(_get_anchor_b, _set_anchor_b) - class GrooveJoint(Constraint): """GrooveJoint is similar to a PivotJoint, but with a linear slide. @@ -530,36 +530,36 @@ def __init__( ) self._init(a, b, _constraint) - def _get_anchor_b(self) -> Vec2d: + @property + def 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: + @anchor_b.setter + def anchor_b(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpGrooveJointSetAnchorB(self._constraint, anchor) - anchor_b = property(_get_anchor_b, _set_anchor_b) - - def _get_groove_a(self) -> Vec2d: + @property + def 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: + @groove_a.setter + def groove_a(self, groove: tuple[float, float]) -> None: assert len(groove) == 2 lib.cpGrooveJointSetGrooveA(self._constraint, groove) - groove_a = property(_get_groove_a, _set_groove_a) - - def _get_groove_b(self) -> Vec2d: + @property + def 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: + @groove_b.setter + def groove_b(self, groove: tuple[float, float]) -> None: assert len(groove) == 2 lib.cpGrooveJointSetGrooveB(self._constraint, groove) - groove_b = property(_get_groove_b, _set_groove_b) - class DampedSpring(Constraint): """DampedSpring is a damped spring. @@ -567,6 +567,8 @@ class DampedSpring(Constraint): The spring allows you to define the rest length, stiffness and damping. """ + _force_func: Optional[_ForceFunc] = None + _pickle_attrs_init = Constraint._pickle_attrs_init + [ "anchor_a", "anchor_b", @@ -613,26 +615,26 @@ def __init__( self._init(a, b, _constraint) - def _get_anchor_a(self) -> Vec2d: + @property + def 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: + @anchor_a.setter + def anchor_a(self, anchor: tuple[float, float]) -> None: assert len(anchor) == 2 lib.cpDampedSpringSetAnchorA(self._constraint, anchor) - anchor_a = property(_get_anchor_a, _set_anchor_a) - - def _get_anchor_b(self) -> Vec2d: + @property + def 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: + @anchor_b.setter + def 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) - @property def rest_length(self) -> float: """The distance the spring wants to be.""" @@ -665,7 +667,22 @@ def spring_force(spring: "DampedSpring", dist: float) -> float: """Default damped spring force function.""" return lib.defaultSpringForce(spring._constraint, dist) - def _set_force_func(self, func: _ForceFunc) -> None: + @property + def force_func(self) -> _ForceFunc: + """The force callback function. + + The force callback function is called each time step and is used to + calculate the force of the spring (exclusing any damping). + + Defaults to :py:func:`DampedSpring.spring_force` + """ + if self._force_func == None: + return DampedSpring.spring_force + else: + return self._force_func + + @force_func.setter + def force_func(self, func: _ForceFunc) -> None: self._force_func = func if func == DampedSpring.spring_force: lib.cpDampedSpringSetSpringForceFunc( @@ -679,17 +696,6 @@ def _set_force_func(self, func: _ForceFunc) -> None: self._constraint, lib.ext_cpDampedSpringForceFunc ) - force_func = property( - fset=_set_force_func, - doc="""The force callback function. - - The force callback function is called each time step and is used to - calculate the force of the spring (exclusing any damping). - - Defaults to :py:func:`DampedSpring.spring_force` - """, - ) - class DampedRotarySpring(Constraint): """DampedRotarySpring works like the DammpedSpring but in a angular fashion.""" @@ -762,7 +768,10 @@ def torque_func(self) -> _TorqueFunc: Defaults to :py:func:`DampedRotarySpring.spring_torque` """ - raise AttributeError("unreadable attribute 'torque_func'") + if self._torque_func == None: + return DampedRotarySpring.spring_torque + else: + return self._torque_func @torque_func.setter def torque_func(self, func: _TorqueFunc) -> None: diff --git a/pymunk/shapes.py b/pymunk/shapes.py index 516f6146..0f04aac4 100644 --- a/pymunk/shapes.py +++ b/pymunk/shapes.py @@ -135,7 +135,7 @@ def collision_type(self) -> int: Defaults to 0. - See the :py:meth:`Space.add_collision_handler` function for more + See the :py:meth:`Space.on_collision` function for more information on when to use this property. """ return cp.cpShapeGetCollisionType(self._shape) @@ -207,25 +207,22 @@ def friction(self) -> float: def friction(self, u: float) -> None: cp.cpShapeSetFriction(self._shape, u) - def _get_surface_velocity(self) -> Vec2d: + @property + def surface_velocity(self) -> Vec2d: + """The surface velocity of the object. + + Useful for creating conveyor belts or players that move around. This + value is only used when calculating friction, not resolving the + collision. + """ v = cp.cpShapeGetSurfaceVelocity(self._shape) return Vec2d(v.x, v.y) - def _set_surface_velocity(self, surface_v: tuple[float, float]) -> None: + @surface_velocity.setter + def surface_velocity(self, surface_v: tuple[float, float]) -> None: assert len(surface_v) == 2 cp.cpShapeSetSurfaceVelocity(self._shape, surface_v) - surface_velocity = property( - _get_surface_velocity, - _set_surface_velocity, - doc="""The surface velocity of the object. - - Useful for creating conveyor belts or players that move around. This - value is only used when calculating friction, not resolving the - collision. - """, - ) - @property def body(self) -> Optional["Body"]: """The body this shape is attached to. diff --git a/pymunk/space.py b/pymunk/space.py index 8d5d95c4..968a03ed 100644 --- a/pymunk/space.py +++ b/pymunk/space.py @@ -226,24 +226,21 @@ def iterations(self) -> int: 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 - lib.cpSpaceSetGravity(self._space, gravity_vector) - - def _get_gravity(self) -> Vec2d: - v = lib.cpSpaceGetGravity(self._space) - return Vec2d(v.x, v.y) - - gravity = property( - _get_gravity, - _set_gravity, - doc="""Global gravity applied to the space. + @property + def gravity(self) -> Vec2d: + """Global gravity applied to the space. Defaults to (0,0). Can be overridden on a per body basis by writing custom integration functions and set it on the body: :py:meth:`pymunk.Body.velocity_func`. - """, - ) + """ + v = lib.cpSpaceGetGravity(self._space) + return Vec2d(v.x, v.y) + + @gravity.setter + def gravity(self, gravity_vector: tuple[float, float]) -> None: + assert len(gravity_vector) == 2 + lib.cpSpaceSetGravity(self._space, gravity_vector) @property def damping(self) -> float: diff --git a/pymunk/tests/doctests.py b/pymunk/tests/doctests.py index a36e3283..fc2d46bb 100644 --- a/pymunk/tests/doctests.py +++ b/pymunk/tests/doctests.py @@ -6,7 +6,7 @@ import pymunk ignores = ["pymunk_extension_build"] -all_dependencies = ["pygame", "pyglet", "matplotlib", "_pyglet"] +all_dependencies = ["pygame", "pyglet", "matplotlib", "_pyglet", "pillow", "PIL"] def load_tests( diff --git a/pymunk/tests/test_constraint.py b/pymunk/tests/test_constraint.py index a5186ea7..5a3c4233 100644 --- a/pymunk/tests/test_constraint.py +++ b/pymunk/tests/test_constraint.py @@ -392,7 +392,10 @@ def f(spring: p.DampedSpring, dist: float) -> float: return 1 + self.assertEqual(j.force_func, DampedSpring.spring_force) + j.force_func = f + self.assertEqual(f, j.force_func) s.step(1) self.assertEqual(j.impulse, 1) @@ -451,7 +454,10 @@ def f(spring: p.DampedRotarySpring, relative_angle: float) -> float: return 1 + self.assertEqual(j.torque_func, DampedRotarySpring.spring_torque) + j.torque_func = f + self.assertEqual(f, j.torque_func) s.step(1) self.assertEqual(j.impulse, 1) diff --git a/pymunk/tests/test_space.py b/pymunk/tests/test_space.py index 37d5f5ef..12de0694 100644 --- a/pymunk/tests/test_space.py +++ b/pymunk/tests/test_space.py @@ -858,6 +858,79 @@ def callback( ] self.assertListEqual(callback_calls, expected_calls) + def testSameTypeCollisionHandler(self) -> None: + s = p.Space() + + 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].friction, arb.shapes[1].friction), + ) + ) + + handler_order = [ + (1, 1), + (1, 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 = 1 + c1.friction = 1 + + b2 = p.Body(body_type=p.Body.STATIC) + c2 = p.Circle(b2, 10) + c2.collision_type = 1 + c2.friction = 2 + + s.add(b1, c1, b2, c2) + + s.step(0.1) + b1.position = 100, 100 + s.step(0.1) + + expected_calls = [ + ("begin", (1, 1), (2, 1)), + ("begin", (1, None), (2, 1)), + ("begin", (1, None), (1, 2)), + ("begin", (None, None), (2, 1)), + ("pre_solve", (1, 1), (2, 1)), + ("pre_solve", (1, None), (2, 1)), + ("pre_solve", (1, None), (1, 2)), + ("pre_solve", (None, None), (2, 1)), + ("post_solve", (1, 1), (2, 1)), + ("post_solve", (1, None), (2, 1)), + ("post_solve", (1, None), (1, 2)), + ("post_solve", (None, None), (2, 1)), + ("separate", (1, 1), (2, 1)), + ("separate", (1, None), (2, 1)), + ("separate", (1, None), (1, 2)), + ("separate", (None, None), (2, 1)), + ] + self.assertListEqual(callback_calls, expected_calls) + def testWildcardCollisionHandler(self) -> None: s = p.Space() b1 = p.Body(1, 1) diff --git a/pymunk/tests/test_transform.py b/pymunk/tests/test_transform.py index db888559..fe85d51b 100644 --- a/pymunk/tests/test_transform.py +++ b/pymunk/tests/test_transform.py @@ -5,34 +5,6 @@ class UnitTestTransform(unittest.TestCase): - def testInit(self) -> None: - - t = p.Transform(1, 2, 3, 4, 5, 6) - self.assertEqual(t.a, 1) - self.assertEqual(t.b, 2) - self.assertEqual(t.c, 3) - self.assertEqual(t.d, 4) - self.assertEqual(t.tx, 5) - self.assertEqual(t.ty, 6) - self.assertEqual(str(t), "Transform(a=1, b=2, c=3, d=4, tx=5, ty=6)") - - t = p.Transform(b=4, ty=2) - self.assertEqual(t.a, 1) - self.assertEqual(t.b, 4) - self.assertEqual(t.c, 0) - self.assertEqual(t.d, 1) - self.assertEqual(t.tx, 0) - self.assertEqual(t.ty, 2) - - def testIdentity(self) -> None: - t = p.Transform.identity() - self.assertEqual(t.a, 1) - self.assertEqual(t.b, 0) - self.assertEqual(t.c, 0) - self.assertEqual(t.d, 1) - self.assertEqual(t.tx, 0) - self.assertEqual(t.ty, 0) - def testPickle(self) -> None: x = p.Transform.identity() s = pickle.dumps(x, 2) diff --git a/pymunk/transform.py b/pymunk/transform.py index 6a6feb6c..4744b4dc 100644 --- a/pymunk/transform.py +++ b/pymunk/transform.py @@ -164,6 +164,36 @@ def rotated(self, t: float) -> "Transform": """ return self @ Transform.rotation(t) + def inverted(self): + """Invert this Transform and return the result + + >>> t = Transform.translation(3,4).scaled(2) + >>> v1 = Vec2d(5, 6) + >>> v2 = (t @ v1) + >>> t.inverted() @ v2 == v1 + True + + >>> t = Transform(1,2,3,4,5,6) + >>> t @ t.inverted() + Transform(a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0) + + >>> i = Transform.identity() + >>> i == i.inverted() + True + """ + det = self.a * self.d - self.b * self.c + if det == 0: + raise ValueError("Singular transform cannot be inverted") + + inv_a = self.d / det + inv_b = -self.b / det + inv_c = -self.c / det + inv_d = self.a / det + inv_tx = (self.c * self.ty - self.d * self.tx) / det + inv_ty = (self.b * self.tx - self.a * self.ty) / det + + return Transform(inv_a, inv_b, inv_c, inv_d, inv_tx, inv_ty) + @staticmethod def translation(x: float, y: float) -> "Transform": """A translation transform diff --git a/pyproject.toml b/pyproject.toml index 5ddf9090..7d5b4a94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ build-backend = "setuptools.build_meta" [project] name = "pymunk" -version = "7.0.1" # remember to change me for new versions! -# Require cffi >1.14.0 since that (and older) has problem with returing structs from functions. +version = "7.2.0" # remember to change me for new versions! +# Require cffi >1.14.0 since that (and older) has problem with returning structs from functions. # Require cffi >= 1.17.1 since older cant work with latest setuptools version dependencies = [ "cffi >= 1.17.1; platform_system != 'Emscripten'", @@ -36,7 +36,16 @@ classifiers = [ requires-python = ">=3.9" [project.optional-dependencies] -dev = ["pyglet", "pygame", "sphinx", "aafigure", "wheel", "matplotlib", "numpy"] +dev = [ + "pyglet", + "pygame", + "pillow", + "sphinx", + "aafigure", + "wheel", + "matplotlib", + "numpy", +] [project.urls] Homepage = "https://www.pymunk.org" Documentation = "https://www.pymunk.org" @@ -88,3 +97,55 @@ module = "pymunk.util" # This is not typed disallow_untyped_defs = false check_untyped_defs = false + + +[tool.cibuildwheel] +test-command = "python -m pymunk.tests" +enable = ["pypy"] +build = [ + "cp39-*", + "cp310-*", + "cp311-*", + "cp312-*", + "cp313-*", + "cp314-*", + "pp311-*", +] + +[tool.cibuildwheel.linux] + +[tool.cibuildwheel.macos] + +[tool.cibuildwheel.windows] + +[tool.cibuildwheel.ios] +environment = { PYMUNK_BUILD_SLIM = "1" } +test-skip = "*" +archs = "all" +skip = "cp314-*" # Until 3.14 build is figured out.. +build-verbosity = 1 +build-frontend = { name = "pip", args = "--no-build-isolation" } +xbuild-tools = ["curl", "pkg-config", "tar"] +before-build = [ + # download wheel and pretend that it's compatible with the current platform settings + "curl https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl -L -o cffi-1.17.1-py3-none-any.whl", + "pip install cffi-1.17.1-py3-none-any.whl", + # install the rest of the dependencies + "python -m pip install setuptools wheel cffi", +] + +[tool.cibuildwheel.pyodide] +environment = { PYMUNK_BUILD_SLIM = "1" } + +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "setup.py" }, + # { file = "**/*.h" }, + # { file = "**/*.c" }, + { file = "Munk2D/**/*.c" }, + { file = "Munk2D/**/*.h" }, + { file = "pymunk/**/*.c" }, + { file = "pymunk/**/*.cdef" }, + { file = "pymunk/**/*.h" }, +] diff --git a/tools/generate_docs.py b/tools/generate_docs.py index 1c018fea..81a789a8 100644 --- a/tools/generate_docs.py +++ b/tools/generate_docs.py @@ -1,6 +1,7 @@ import os import sys +# uv run sphinx-build -E -b html docs/src docs/html def main(): os.system("""python -m sphinx -E -b html ../docs/src ../docs/html""")