From 1a9bac216425bb982a93f2f43f008b9a93ebdf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sat, 4 Jul 2026 17:57:16 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat:=E2=80=AFsimplify=20the=20python=20bin?= =?UTF-8?q?dings=20to=20only=20include=20the=203d-f32=20variant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python-bindings.yml | 26 +- .github/workflows/python-wheels.yml | 13 +- Cargo.toml | 3 - python/README.md | 70 +- python/dev.sh | 13 +- python/docs/api/errors.rst | 9 +- python/docs/api/geometry.rst | 16 - python/docs/api/loaders.rst | 6 +- python/docs/api/math.rst | 21 +- python/docs/api/serde.rst | 9 - python/docs/api/world.rst | 7 - python/docs/changelog.rst | 21 +- python/docs/conf.py | 25 +- python/docs/dim_scalar.rst | 84 -- python/docs/getting_started.rst | 9 +- python/docs/index.rst | 13 +- .../examples/render/matplotlib_animation.py | 21 +- python/examples/vehicle/drive.py | 2 - python/examples_tour.py | 34 +- python/rapier-py-2d-f64/Cargo.toml | 31 - python/rapier-py-2d-f64/README.md | 32 - python/rapier-py-2d-f64/pyproject.toml | 39 - .../python/rapier2d_f64/__init__.py | 338 ------ .../python/rapier2d_f64/_debug_render.py | 56 - .../python/rapier2d_f64/_event_handler.py | 110 -- .../python/rapier2d_f64/_math_helpers.py | 27 - .../python/rapier2d_f64/_pickle_setup.py | 46 - .../python/rapier2d_f64/_rapier2d_f64.pyi | 712 ----------- .../python/rapier2d_f64/py.typed | 0 python/rapier-py-2d-f64/src/lib.rs | 37 - python/rapier-py-2d/Cargo.toml | 29 - python/rapier-py-2d/README.md | 32 - python/rapier-py-2d/pyproject.toml | 39 - .../rapier-py-2d/python/rapier2d/__init__.py | 343 ------ .../rapier-py-2d/python/rapier2d/__init__.pyi | 20 - .../python/rapier2d/_debug_render.py | 56 - .../python/rapier2d/_event_handler.py | 110 -- .../python/rapier2d/_math_helpers.py | 27 - .../python/rapier2d/_pickle_setup.py | 46 - .../python/rapier2d/_rapier2d.pyi | 712 ----------- python/rapier-py-2d/python/rapier2d/math.py | 15 - python/rapier-py-2d/python/rapier2d/py.typed | 0 python/rapier-py-2d/src/lib.rs | 38 - python/rapier-py-3d-f64/Cargo.toml | 31 - python/rapier-py-3d-f64/README.md | 32 - python/rapier-py-3d-f64/pyproject.toml | 39 - .../python/rapier3d_f64/__init__.py | 352 ------ .../python/rapier3d_f64/_debug_render.py | 56 - .../python/rapier3d_f64/_event_handler.py | 110 -- .../python/rapier3d_f64/_math_helpers.py | 27 - .../python/rapier3d_f64/_pickle_setup.py | 46 - .../python/rapier3d_f64/_rapier3d_f64.pyi | 975 --------------- .../python/rapier3d_f64/py.typed | 0 python/rapier-py-3d-f64/src/lib.rs | 37 - python/rapier-py-3d/Cargo.toml | 11 +- python/rapier-py-3d/README.md | 11 - .../python/rapier3d/_pickle_setup.py | 23 +- python/rapier-py-3d/src/conv.rs | 216 ++++ python/rapier-py-3d/src/errors.rs | 94 ++ python/rapier-py-3d/src/lib.rs | 72 +- python/rapier-py-3d/src/math.rs | 1067 +++++++++++++++++ python/rapier-py-3d/src/serde_io.rs | 134 +++ python/rapier-testbed/README.md | 9 +- python/rapier-testbed/pyproject.toml | 3 +- .../rapier-testbed/rapier_testbed/__init__.py | 48 +- .../rapier-testbed/rapier_testbed/_meshes.py | 312 +---- .../rapier-testbed/rapier_testbed/_picker.py | 14 +- .../rapier_testbed/_registry.py | 2 +- .../rapier-testbed/rapier_testbed/_testbed.py | 280 +---- .../rapier_testbed/examples2/__init__.py | 1 - .../rapier_testbed/examples2/add_remove2.py | 83 -- .../rapier_testbed/examples2/ccd2.py | 89 -- .../examples2/character_controller2.py | 155 --- .../examples2/collision_groups2.py | 71 -- .../examples2/convex_polygons2.py | 73 -- .../rapier_testbed/examples2/damping2.py | 50 - .../rapier_testbed/examples2/drum2.py | 71 -- .../rapier_testbed/examples2/heightfield2.py | 63 - .../rapier_testbed/examples2/inv_pyramid2.py | 46 - .../examples2/inverse_kinematics2.py | 62 - .../examples2/joint_motor_position2.py | 69 -- .../rapier_testbed/examples2/joints2.py | 58 - .../examples2/locked_rotations2.py | 51 - .../examples2/one_way_platforms2.py | 103 -- .../examples2/pin_slot_joint2.py | 76 -- .../rapier_testbed/examples2/platform2.py | 86 -- .../rapier_testbed/examples2/polyline2.py | 63 - .../rapier_testbed/examples2/pyramid2.py | 47 - .../rapier_testbed/examples2/restitution2.py | 49 - .../rapier_testbed/examples2/rope_joints2.py | 68 -- .../rapier_testbed/examples2/s2d_arch.py | 97 -- .../examples2/s2d_ball_and_chain.py | 88 -- .../rapier_testbed/examples2/s2d_bridge.py | 72 -- .../examples2/s2d_card_house.py | 78 -- .../rapier_testbed/examples2/s2d_confined.py | 59 - .../examples2/s2d_far_pyramid.py | 51 - .../examples2/s2d_high_mass_ratio_1.py | 62 - .../examples2/s2d_high_mass_ratio_2.py | 66 - .../examples2/s2d_high_mass_ratio_3.py | 61 - .../examples2/s2d_joint_grid.py | 66 - .../rapier_testbed/examples2/s2d_pyramid.py | 44 - .../rapier_testbed/examples2/sensor2.py | 64 - .../examples2/stress_tests/__init__.py | 1 - .../examples2/stress_tests/balls2.py | 41 - .../examples2/stress_tests/boxes2.py | 64 - .../examples2/stress_tests/capsules2.py | 68 -- .../stress_tests/convex_polygons2.py | 76 -- .../examples2/stress_tests/heightfield2.py | 63 - .../examples2/stress_tests/joint_ball2.py | 53 - .../examples2/stress_tests/joint_fixed2.py | 73 -- .../stress_tests/joint_prismatic2.py | 63 - .../examples2/stress_tests/pyramid2.py | 47 - .../stress_tests/vertical_stacks2.py | 54 - .../rapier_testbed/examples2/trimesh2.py | 65 - .../examples2/utils/__init__.py | 1 - .../examples2/utils/character.py | 10 - .../rapier_testbed/examples2/utils/svg.py | 45 - .../rapier_testbed/examples2/voxels2.py | 56 - python/tests/test_controllers.py | 3 +- python/tests/test_controllers_2d.py | 105 -- python/tests/test_debug_render.py | 9 +- python/tests/test_debug_render_2d.py | 110 -- python/tests/test_deferred_bindings.py | 43 +- python/tests/test_dynamics.py | 9 +- python/tests/test_dynamics_2d.py | 129 -- python/tests/test_events_hooks.py | 8 +- python/tests/test_events_hooks_2d.py | 112 -- python/tests/test_examples.py | 2 +- python/tests/test_geometry.py | 8 +- python/tests/test_geometry_2d.py | 116 -- python/tests/test_joints.py | 8 +- python/tests/test_joints_2d.py | 120 -- python/tests/test_loaders.py | 2 +- python/tests/test_math.py | 25 +- python/tests/test_math_2d.py | 95 -- python/tests/test_multibody.py | 3 +- python/tests/test_new_bindings.py | 36 +- python/tests/test_pipeline.py | 9 +- python/tests/test_pipeline_2d.py | 112 -- python/tests/test_proxy_persistence.py | 17 +- python/tests/test_queries.py | 3 +- python/tests/test_serde.py | 9 +- python/tests/test_serde_2d.py | 93 -- python/tests/test_shapes.py | 3 +- python/tests/test_smoke.py | 14 +- 145 files changed, 1777 insertions(+), 9746 deletions(-) delete mode 100644 python/docs/dim_scalar.rst delete mode 100644 python/rapier-py-2d-f64/Cargo.toml delete mode 100644 python/rapier-py-2d-f64/README.md delete mode 100644 python/rapier-py-2d-f64/pyproject.toml delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/__init__.py delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/_debug_render.py delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/_event_handler.py delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/_math_helpers.py delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/_pickle_setup.py delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/_rapier2d_f64.pyi delete mode 100644 python/rapier-py-2d-f64/python/rapier2d_f64/py.typed delete mode 100644 python/rapier-py-2d-f64/src/lib.rs delete mode 100644 python/rapier-py-2d/Cargo.toml delete mode 100644 python/rapier-py-2d/README.md delete mode 100644 python/rapier-py-2d/pyproject.toml delete mode 100644 python/rapier-py-2d/python/rapier2d/__init__.py delete mode 100644 python/rapier-py-2d/python/rapier2d/__init__.pyi delete mode 100644 python/rapier-py-2d/python/rapier2d/_debug_render.py delete mode 100644 python/rapier-py-2d/python/rapier2d/_event_handler.py delete mode 100644 python/rapier-py-2d/python/rapier2d/_math_helpers.py delete mode 100644 python/rapier-py-2d/python/rapier2d/_pickle_setup.py delete mode 100644 python/rapier-py-2d/python/rapier2d/_rapier2d.pyi delete mode 100644 python/rapier-py-2d/python/rapier2d/math.py delete mode 100644 python/rapier-py-2d/python/rapier2d/py.typed delete mode 100644 python/rapier-py-2d/src/lib.rs delete mode 100644 python/rapier-py-3d-f64/Cargo.toml delete mode 100644 python/rapier-py-3d-f64/README.md delete mode 100644 python/rapier-py-3d-f64/pyproject.toml delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/__init__.py delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/_debug_render.py delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/_event_handler.py delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/_math_helpers.py delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/_pickle_setup.py delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/_rapier3d_f64.pyi delete mode 100644 python/rapier-py-3d-f64/python/rapier3d_f64/py.typed delete mode 100644 python/rapier-py-3d-f64/src/lib.rs create mode 100644 python/rapier-py-3d/src/conv.rs create mode 100644 python/rapier-py-3d/src/errors.rs create mode 100644 python/rapier-py-3d/src/math.rs create mode 100644 python/rapier-py-3d/src/serde_io.rs delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/__init__.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/add_remove2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/ccd2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/character_controller2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/collision_groups2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/convex_polygons2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/damping2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/drum2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/heightfield2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/inv_pyramid2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/inverse_kinematics2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/joint_motor_position2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/joints2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/locked_rotations2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/one_way_platforms2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/pin_slot_joint2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/platform2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/polyline2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/pyramid2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/restitution2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/rope_joints2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_arch.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_ball_and_chain.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_bridge.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_card_house.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_confined.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_far_pyramid.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_1.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_3.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_joint_grid.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/s2d_pyramid.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/sensor2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/__init__.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/balls2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/boxes2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/capsules2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/convex_polygons2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/heightfield2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_ball2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_fixed2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_prismatic2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/pyramid2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/stress_tests/vertical_stacks2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/trimesh2.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/utils/__init__.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/utils/character.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/utils/svg.py delete mode 100644 python/rapier-testbed/rapier_testbed/examples2/voxels2.py delete mode 100644 python/tests/test_controllers_2d.py delete mode 100644 python/tests/test_debug_render_2d.py delete mode 100644 python/tests/test_dynamics_2d.py delete mode 100644 python/tests/test_events_hooks_2d.py delete mode 100644 python/tests/test_geometry_2d.py delete mode 100644 python/tests/test_joints_2d.py delete mode 100644 python/tests/test_math_2d.py delete mode 100644 python/tests/test_pipeline_2d.py delete mode 100644 python/tests/test_serde_2d.py diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 1f60036b6..716a8044c 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -9,9 +9,9 @@ on: env: CARGO_TERM_COLOR: always -# The four engine packages live under python/rapier-py-{2,3}d{,-f64}; the -# pure-Python testbed under python/rapier-testbed; the shared parity test -# suite under python/tests (imports all four engine packages at once). +# The engine package lives under python/rapier-py-3d (rapier3d, f32); the +# pure-Python testbed under python/rapier-testbed; the test suite under +# python/tests. jobs: lint-rust: runs-on: ubuntu-latest @@ -33,8 +33,7 @@ jobs: run: | cargo clippy --no-deps \ -p rapier-py-core \ - -p rapier-py-2d -p rapier-py-2d-f64 \ - -p rapier-py-3d -p rapier-py-3d-f64 \ + -p rapier-py-3d \ -- -D warnings test: @@ -64,18 +63,15 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install maturin pytest pytest-timeout hypothesis numpy matplotlib - # Build + install all four engine packages into the runner's environment - # so the cross-flavor parity suite can import every variant. - - name: Build & install engine packages + # Build + install the rapier3d (f32) engine package into the runner's + # environment so the test suite can import it. + - name: Build & install engine package shell: bash - run: | - for crate in rapier-py-2d rapier-py-2d-f64 rapier-py-3d rapier-py-3d-f64; do - python -m pip install --no-build-isolation "./python/$crate" - done - - name: Install testbed (no deps; engine packages already present) + run: python -m pip install --no-build-isolation ./python/rapier-py-3d + - name: Install testbed (no deps; engine package already present) shell: bash run: python -m pip install --no-deps ./python/rapier-testbed - name: Smoke (import surface) - run: python -m pytest python/tests/test_smoke.py python/tests/test_math.py python/tests/test_math_2d.py -v - - name: Full parity test suite + run: python -m pytest python/tests/test_smoke.py python/tests/test_math.py -v + - name: Full test suite run: python -m pytest python/tests/ -q --timeout=120 diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index b2eb04a8c..64d01506e 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -1,13 +1,13 @@ name: Python wheels -# Build abi3 wheels for the four engine packages across every supported -# platform and (on a `python-v*` tag) publish them to PyPI via trusted -# publishing. +# Build abi3 wheels for the rapier3d (f32) engine package across every +# supported platform and (on a `python-v*` tag) publish them to PyPI via +# trusted publishing. # # abi3-py39 means ONE wheel per (package, platform, arch) covers Python 3.9+, # so there is deliberately no Python-version axis. # -# NOTE: no sdist is published. The rapier2d/3d engine crates share their +# NOTE: no sdist is published. The rapier3d engine crate shares its # source via the repo-root `src/` tree (`[lib] path = "../../src/lib.rs"`), # which lives outside any vendored crate directory, and maturin's sdist # packer cannot reach parent paths (`..` is rejected in `include`). A source @@ -38,9 +38,6 @@ jobs: matrix: package: - { dir: rapier-py-3d, name: rapier3d } - - { dir: rapier-py-3d-f64, name: rapier3d-f64 } - - { dir: rapier-py-2d, name: rapier2d } - - { dir: rapier-py-2d-f64, name: rapier2d-f64 } platform: - { runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, manylinux: auto } - { runner: ubuntu-latest, target: aarch64-unknown-linux-gnu, manylinux: auto } @@ -77,7 +74,7 @@ jobs: path: dist pattern: "wheels-*" merge-multiple: true - - name: Publish all four packages + - name: Publish the rapier3d package uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist diff --git a/Cargo.toml b/Cargo.toml index 7a98057dc..cbf0f5cce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,7 @@ members = [ "crates/mjcf-rs", "crates/rapier3d-mjcf", "python/rapier-py-core", - "python/rapier-py-2d", - "python/rapier-py-2d-f64", "python/rapier-py-3d", - "python/rapier-py-3d-f64", ] # Bare `cargo build` / `cargo test` / `cargo clippy` only touches the # pure-Rust crates. The Python-binding crates need a Python build env diff --git a/python/README.md b/python/README.md index 63d706d6c..cc76c1a3c 100644 --- a/python/README.md +++ b/python/README.md @@ -1,28 +1,23 @@ # Rapier Python bindings The Rapier physics engine exposed to Python via [PyO3](https://pyo3.rs/) and -[maturin](https://www.maturin.rs/). The bindings ship as **four independent -PyPI packages**, one per `(dimension, scalar)` flavor — they share an -identical API differing only in dimension and scalar type: +[maturin](https://www.maturin.rs/). The bindings ship as a single PyPI +package wrapping the 3D, `f32` engine: | PyPI dist | import name | crate (`python/…`) | dim | scalar | | ------------- | -------------- | ------------------ | --- | ------ | -| `rapier2d` | `rapier2d` | `rapier-py-2d` | 2D | f32 | | `rapier3d` | `rapier3d` | `rapier-py-3d` | 3D | f32 | -| `rapier2d-f64`| `rapier2d_f64` | `rapier-py-2d-f64` | 2D | f64 | -| `rapier3d-f64`| `rapier3d_f64` | `rapier-py-3d-f64` | 3D | f64 | -`rapier-py-core` holds the shared PyO3 binding macros (compiled into each +`rapier-py-core` holds the shared PyO3 binding macros (compiled into the package; it is not published on its own). The Panda3D visual testbed lives in the separate `rapier-testbed` package. -Full documentation lives in [`docs/`](docs/); the design of this split is -recorded in [`docs/repackaging/`](docs/repackaging/). +Full documentation lives in [`docs/`](docs/). -> **Note:** these are not yet published on PyPI. The instructions below build -> the packages locally from this checkout. There is currently no source +> **Note:** this is not yet published on PyPI. The instructions below build +> the package locally from this checkout. There is currently no source > distribution (sdist), so building requires the full git repository plus a -> Rust toolchain — see [`docs/repackaging/05-publishing-pypi.md`](docs/repackaging/05-publishing-pypi.md). +> Rust toolchain. ## Prerequisites @@ -39,13 +34,13 @@ pip install maturin ## Quick start -[`python/dev.sh`](dev.sh) does everything below in one command — builds all four -packages, runs the test suite, builds the docs, and smoke-tests the testbed. It +[`python/dev.sh`](dev.sh) does everything below in one command — builds the +package, runs the test suite, builds the docs, and smoke-tests the testbed. It creates and manages a `.venv` at the repo root on first run: ```bash python/dev.sh # build + test + docs + testbed (headless smoke) -python/dev.sh build # just build the four packages +python/dev.sh build # just build the package python/dev.sh test # build + run the test suite python/dev.sh docs # build + build the docs python/dev.sh testbed # build + install testbed, open the picker @@ -56,39 +51,29 @@ The sections below are the manual equivalents. ## 1. Build the bindings -Each flavor is its own package. Build the one(s) you want, editable, with -`maturin develop` (add `--release` for an optimized build — slower to compile, -much faster at runtime): +Build the package, editable, with `maturin develop` (add `--release` for an +optimized build — slower to compile, much faster at runtime): ```bash maturin develop -m python/rapier-py-3d/Cargo.toml # 3D f32 -> import rapier3d ``` -Swap `rapier-py-3d` for `rapier-py-2d`, `rapier-py-3d-f64`, or -`rapier-py-2d-f64` to build the other flavors (see the table above for the -matching import name). - ```bash python -c "import rapier3d; print(rapier3d.__version__)" # smoke check ``` ### Run the test suite -The test suite is cross-flavor (it parametrizes over all four packages), so -install every flavor first: - ```bash -for c in rapier-py-2d rapier-py-2d-f64 rapier-py-3d rapier-py-3d-f64; do - maturin develop --release -m "python/$c/Cargo.toml" -done +maturin develop --release -m python/rapier-py-3d/Cargo.toml pip install pytest pytest-timeout hypothesis numpy matplotlib -python -m pytest python/tests # ~708 passed +python -m pytest python/tests ``` ## 2. Build and open the docs -The docs use Sphinx autodoc, so the engine packages must be **built first** -(step 1 — build all four to avoid import warnings). Then: +The docs use Sphinx autodoc, so the engine package must be **built first** +(step 1). Then: ```bash pip install sphinx furo sphinx-autodoc-typehints @@ -100,14 +85,13 @@ open _build/html/index.html # macOS; Linux: xdg-open; Windows: start ## 3. Run the testbed examples `rapier-testbed` is a [Panda3D](https://www.panda3d.org/) visual gallery of -~127 examples ported from the Rust `examples2d/` and `examples3d/`. It drives -both the 2D and 3D engines. +examples ported from the Rust `examples3d/`. It drives the 3D engine. ### Install -The testbed depends on `rapier2d` and `rapier3d`. Since those aren't on PyPI -yet, build them first (step 1 above), then install the testbed with -`--no-deps` so pip uses your local builds instead of trying to fetch them. +The testbed depends on `rapier3d`. Since it isn't on PyPI yet, build it first +(step 1 above), then install the testbed with `--no-deps` so pip uses your +local build instead of trying to fetch it. Run this **from the repository root** (the `./python/...` path is relative to it, like the build steps above): @@ -130,10 +114,8 @@ Once installed, these run from **any** directory (they invoke the installed # Interactive picker — browse every example by category (opens a window): python -m rapier_testbed -# Jump straight into one example. 3D examples live under `examples3`, -# 2D under `examples2`: +# Jump straight into one example. 3D examples live under `examples3`: python -m rapier_testbed.examples3.domino3 -python -m rapier_testbed.examples2.pyramid2 # Headless — run a fixed number of steps with no window (needs no display; # this is what CI uses): @@ -144,7 +126,7 @@ To walk through **every** example in turn — each opens in a window, and closing it launches the next (Ctrl-C to stop): ```bash -python python/examples_tour.py # all (2D then 3D); accepts 2d|3d|--start NAME +python python/examples_tour.py # every 3D example; accepts --start NAME python/dev.sh tour # same, but builds + installs the testbed first ``` @@ -158,10 +140,10 @@ python/dev.sh tour # same, but builds + installs the testbe | | | | `W` | toggle wireframe | | | | | `Esc` | quit | -The examples span 9 categories: Collisions, Dynamics, Joints, Controls, -Robotics, Stress Tests, Debug, Misc, and "Inspired by Solver 2D". The picker -lists them all; each example's module name (for direct launch) matches its -file under `python/rapier-testbed/rapier_testbed/examples{2,3}/`. +The examples span categories such as Collisions, Dynamics, Joints, Controls, +Robotics, Stress Tests, Debug, and Misc. The picker lists them all; each +example's module name (for direct launch) matches its file under +`python/rapier-testbed/rapier_testbed/examples3/`. ## License diff --git a/python/dev.sh b/python/dev.sh index 8202d062c..c102073b7 100755 --- a/python/dev.sh +++ b/python/dev.sh @@ -5,13 +5,13 @@ # # Usage (run from anywhere; paths are resolved relative to the repo): # python/dev.sh # all: build + test + docs + testbed (headless smoke) -# python/dev.sh build # build all four engine packages (editable) +# python/dev.sh build # build the rapier3d engine package (editable) # python/dev.sh test # build, then run the full test suite # python/dev.sh docs # build, then build the HTML docs # python/dev.sh testbed # build + install testbed, open the picker # python/dev.sh testbed examples3.domino3 # build + install testbed, run one example # python/dev.sh tour # build + install testbed, run every example in turn -# # (close a window to advance; accepts 2d|3d|--start NAME) +# # (close a window to advance; accepts --start NAME) # python/dev.sh clean # remove build artifacts + the managed venv # # Environment: @@ -26,7 +26,7 @@ cd "$REPO_ROOT" PROFILE="${PROFILE:-release}" VENV="${RAPIER_PY_VENV:-$REPO_ROOT/.venv}" -CRATES=(rapier-py-2d rapier-py-2d-f64 rapier-py-3d rapier-py-3d-f64) +CRATES=(rapier-py-3d) log() { printf '\n\033[1;34m==>\033[0m \033[1m%s\033[0m\n' "$*"; } note() { printf ' %s\n' "$*"; } @@ -65,8 +65,7 @@ build_all() { log "Building $c ($PROFILE)" maturin_develop "python/$c/Cargo.toml" done - python -c "import rapier2d, rapier2d_f64, rapier3d, rapier3d_f64 as _; \ -print('built all four flavors, version', rapier3d.__version__)" + python -c "import rapier3d as _; print('built rapier3d, version', _.__version__)" } run_tests() { @@ -86,7 +85,7 @@ install_testbed() { pip_install panda3d numpy # Editable (-e) so edits to examples / camera / the testbed itself are # picked up by `python -m rapier_testbed` without reinstalling. --no-deps - # because its rapier2d/rapier3d deps are the local builds from build_all. + # because its rapier3d dep is the local build from build_all. # Note: --no-deps must precede -e, else pip reads it as the -e target. pip_install --no-deps -e "$REPO_ROOT/python/rapier-testbed" } @@ -111,7 +110,7 @@ cmd_testbed() { cmd_tour() { # Launch every example one after another (close a window to advance). - # Extra args (2d|3d|--start NAME) pass through to examples_tour.py. + # Extra args (--start NAME) pass through to examples_tour.py. ensure_venv; build_all; install_testbed log "Touring all examples (close each window to advance; Ctrl-C to stop)" python "$REPO_ROOT/python/examples_tour.py" "$@" diff --git a/python/docs/api/errors.rst b/python/docs/api/errors.rst index adeef7e5f..dca8b21d7 100644 --- a/python/docs/api/errors.rst +++ b/python/docs/api/errors.rst @@ -9,10 +9,6 @@ Every exception the bindings raise derives from a single base, rapier-specific failure; catch the leaf types when you need to tell them apart. -Each ``(dim, scalar)`` package has its **own** error tree — there is no -cross-flavor base shared between, say, ``rapier3d`` and ``rapier2d``, so -catch the error type from the package you imported. - Base ---- @@ -32,10 +28,7 @@ Core errors Loader errors ------------- -Raised by the loaders (see :doc:`loaders`). The error types are exported -by every package for a uniform error tree, but only the f32-3D -``rapier3d`` package ships loaders that actually raise them — the 2D and -f64 packages expose no loader machinery. +Raised by the loaders (see :doc:`loaders`). .. autoexception:: MeshConversionError :show-inheritance: diff --git a/python/docs/api/geometry.rst b/python/docs/api/geometry.rst index ee8e6f231..bf7375a1d 100644 --- a/python/docs/api/geometry.rst +++ b/python/docs/api/geometry.rst @@ -38,22 +38,6 @@ Shapes .. autoclass:: Cone .. autoclass:: ConvexPolyhedron -``Cylinder``, ``Cone``, and ``ConvexPolyhedron`` are 3D-only. The 2D -packages instead expose the flat shapes below. - -2D-only shapes -~~~~~~~~~~~~~~ - -The same surface in 2D lives under :mod:`rapier2d`: - -.. currentmodule:: rapier2d - -.. autoclass:: Segment -.. autoclass:: Polyline -.. autoclass:: ConvexPolygon - -.. currentmodule:: rapier3d - Bounding volumes ---------------- diff --git a/python/docs/api/loaders.rst b/python/docs/api/loaders.rst index d21fd5da7..53a4cfb49 100644 --- a/python/docs/api/loaders.rst +++ b/python/docs/api/loaders.rst @@ -1,10 +1,8 @@ Loaders ======= -Loaders for external scene formats. All three are **3D-only / f32**: -the upstream ``rapier3d-urdf``, ``rapier3d-meshloader``, and -``rapier3d-mjcf`` crates are 3D-only and have no ``-f64`` variant, so -the loaders are registered solely by the f32-3D ``rapier3d`` cdylib. +Loaders for external scene formats, backed by the upstream +``rapier3d-urdf``, ``rapier3d-meshloader``, and ``rapier3d-mjcf`` crates. URDF ---- diff --git a/python/docs/api/math.rst b/python/docs/api/math.rst index d65465934..2ce9f210b 100644 --- a/python/docs/api/math.rst +++ b/python/docs/api/math.rst @@ -2,8 +2,7 @@ Math ==== Vectors, points, rotations, isometries, and a handful of free -helpers. The 3D / f32 types live in ``rapier3d``; 2D and f64 -variants share the same API surface in their respective sub-modules. +helpers, in the 3D / f32 ``rapier3d`` package. .. currentmodule:: rapier3d @@ -16,8 +15,7 @@ Vectors and points .. data:: AngVector3 Alias of :class:`Vec3`, used for 3D angular quantities (angular - velocity, torque). In 2D the corresponding quantity is a plain - ``float`` and there is no ``AngVector2`` type. + velocity, torque). Rotations and orientations -------------------------- @@ -37,18 +35,3 @@ Pure-Python helpers re-exported under ``rapier3d.math``: .. automodule:: rapier3d.math :no-index: - -2D math types -------------- - -The same surface in 2D lives under :mod:`rapier2d`: - -.. currentmodule:: rapier2d - -.. autoclass:: Vec2 -.. autoclass:: Point2 -.. autoclass:: Rotation2 -.. autoclass:: Isometry2 - -In 2D the angular velocity / acceleration is a plain ``float``, so -there is no separate ``AngVector2`` type. diff --git a/python/docs/api/serde.rst b/python/docs/api/serde.rst index dc8da8221..901783387 100644 --- a/python/docs/api/serde.rst +++ b/python/docs/api/serde.rst @@ -31,12 +31,3 @@ Errors :class:`~rapier3d.SerializationError` (a subclass of :class:`~rapier3d.RapierError`) on malformed or truncated input. See :doc:`errors`. - -Flavor-tagging caveat ---------------------- - -Snapshots are **not** tagged with the ``(dim, scalar)`` flavor that -produced them. Restoring an f32 snapshot through -``rapier3d_f64.PhysicsWorld.restore(...)`` will silently misinterpret -the floats. Always restore a snapshot through the same flavor that -produced it. diff --git a/python/docs/api/world.rst b/python/docs/api/world.rst index d508416f6..b196d5215 100644 --- a/python/docs/api/world.rst +++ b/python/docs/api/world.rst @@ -43,10 +43,3 @@ helpers. .. autoclass:: PhysicsWorld :members: :undoc-members: - -.. note:: - - The same class exists for every ``(dim, scalar)`` flavor: - ``rapier3d.PhysicsWorld`` (3D / f32), ``rapier2d.PhysicsWorld``, - ``rapier3d_f64.PhysicsWorld``, and ``rapier2d_f64.PhysicsWorld``. - See :doc:`../dim_scalar`. diff --git a/python/docs/changelog.rst b/python/docs/changelog.rst index a089a280a..ae528f818 100644 --- a/python/docs/changelog.rst +++ b/python/docs/changelog.rst @@ -10,9 +10,9 @@ release cadence. The authoritative changelog is the Cargo Unreleased ---------- -**Breaking — repackaged into four PyPI packages.** The single ``rapier`` -umbrella package (with ``rapier.dim2`` / ``rapier.dim3.f64`` submodules) has -been replaced by four independent packages, one per ``(dim, scalar)`` flavor: +**Breaking — repackaged as ``rapier3d``.** The single ``rapier`` umbrella +package (with ``rapier.dim3`` submodules) has been replaced by the +``rapier3d`` package (3D / f32): .. list-table:: :header-rows: 1 @@ -21,15 +21,8 @@ been replaced by four independent packages, one per ``(dim, scalar)`` flavor: - After * - ``import rapier`` (3D f32 default) - ``import rapier3d`` - * - ``from rapier import dim2`` - - ``import rapier2d`` - * - ``from rapier.dim3 import f64`` - - ``import rapier3d_f64`` - * - ``from rapier.dim2 import f64`` - - ``import rapier2d_f64`` -There is no longer a cross-flavor ``rapier.RapierError`` base; each package -exposes its own error tree. Each package is a standard ``abi3`` maturin -wheel, so installs and platform support (manylinux/musllinux, macOS, Windows) -now go through the normal wheel pipeline. The Panda3D testbed moved to a -separate ``rapier-testbed`` package. +The package is a standard ``abi3`` maturin wheel, so installs and platform +support (manylinux/musllinux, macOS, Windows) now go through the normal +wheel pipeline. The Panda3D testbed moved to a separate ``rapier-testbed`` +package. diff --git a/python/docs/conf.py b/python/docs/conf.py index a4963c719..b84235852 100644 --- a/python/docs/conf.py +++ b/python/docs/conf.py @@ -1,13 +1,10 @@ """Sphinx configuration for the Rapier Python bindings. -These docs cover the four engine packages — ``rapier2d``, ``rapier3d``, -``rapier2d-f64``, ``rapier3d-f64`` — which share an identical API differing -only in dimension and scalar type. The pages are authored against the 3D/f32 -package (``rapier3d``) as the canonical reference. +These docs cover the ``rapier3d`` engine package (3D / f32). -Build from the ``python/docs/`` directory after the engine packages are -installed (``pip install ./python/rapier-py-3d`` etc., or ``maturin develop`` -in each crate):: +Build from the ``python/docs/`` directory after the engine package is +installed (``pip install ./python/rapier-py-3d``, or ``maturin develop`` +in the crate):: sphinx-build -b html . _build/html """ @@ -20,15 +17,15 @@ # -- Path bootstrap --------------------------------------------------------- # -# We need to ``import rapier3d`` (and siblings) so autodoc can read the real -# pyclass docstrings populated by PyO3 (``///`` Rust doc comments → Python -# ``__doc__``). The packages are normally installed editable (``maturin -# develop`` per crate). As a fallback — to support running ``sphinx-build`` -# from a stand-alone checkout where nothing has been installed yet — we also -# prepend each package's ``python/`` source dir to ``sys.path``. +# We need to ``import rapier3d`` so autodoc can read the real pyclass +# docstrings populated by PyO3 (``///`` Rust doc comments → Python +# ``__doc__``). The package is normally installed editable (``maturin +# develop``). As a fallback — to support running ``sphinx-build`` from a +# stand-alone checkout where nothing has been installed yet — we also +# prepend the package's ``python/`` source dir to ``sys.path``. _HERE = os.path.dirname(os.path.abspath(__file__)) -for _crate in ("rapier-py-3d", "rapier-py-3d-f64", "rapier-py-2d", "rapier-py-2d-f64"): +for _crate in ("rapier-py-3d",): _src = os.path.abspath(os.path.join(_HERE, "..", _crate, "python")) if _src not in sys.path: sys.path.insert(0, _src) diff --git a/python/docs/dim_scalar.rst b/python/docs/dim_scalar.rst deleted file mode 100644 index 16eede3f1..000000000 --- a/python/docs/dim_scalar.rst +++ /dev/null @@ -1,84 +0,0 @@ -Dimensions and scalar precision -=============================== - -Rapier's Python bindings ship as **four** independent PyPI packages, one -per ``(dim, scalar)`` flavor. They share an identical API differing only in -dimension and scalar type: - -.. list-table:: - :header-rows: 1 - :widths: 20 18 10 12 - - * - PyPI package - - import name - - dim - - scalar - * - ``rapier3d`` - - ``rapier3d`` - - 3 - - f32 - * - ``rapier3d-f64`` - - ``rapier3d_f64`` - - 3 - - f64 - * - ``rapier2d`` - - ``rapier2d`` - - 2 - - f32 - * - ``rapier2d-f64`` - - ``rapier2d_f64`` - - 2 - - f64 - -Install only the flavor(s) you need (``pip install rapier3d``). ``rapier3d`` -is the canonical reference these docs are written against, matching -``use rapier3d::prelude::*`` in Rust. - -Recommended import patterns ---------------------------- - -.. code-block:: python - - # The common case: 3D, single precision. - import rapier3d as rp - - # 2D, single precision. - import rapier2d as rp - - # 3D, double precision. - import rapier3d_f64 as rp - - # 2D, double precision. - import rapier2d_f64 as rp - -The Python API surface is identical across ``(dim, scalar)`` modulo -dim-specific types: - -* ``Vec2`` / ``Point2`` / ``Rotation2`` / ``Isometry2`` versus - ``Vec3`` / ``Point3`` / ``Rotation3`` / ``Quaternion`` / ``Isometry3``. -* ``SphericalJoint`` and the ray-cast vehicle controller are 3D-only. -* ``Cylinder``, ``Cone``, and ``ConvexPolyhedron`` are 3D-only shapes; - ``Segment``, ``Polyline``, and ``ConvexPolygon`` are 2D-only. -* Angular quantities (angular velocity, torque) are a plain ``float`` in - 2D — there is no ``AngVector2`` type. In 3D, ``AngVector3`` is an alias - for :class:`~rapier3d.Vec3`. - -Choosing a precision --------------------- - -Single precision (f32) is the default and is what the rest of the -Rapier ecosystem ships. It's the right answer for almost all use -cases — games, robotics simulation, and physics-based animation. - -Double precision (f64) is available for cases that require it: large -worlds where coordinates exceed ~1e5 metres, long-running deterministic -simulations where round-off drift matters, or comparisons against -reference solutions computed in f64. - -The two flavors have separate cdylibs and separate types — instances -do not interoperate. ``Vec3(1, 2, 3)`` from ``rapier3d`` is a -**different class** from ``Vec3(1, 2, 3)`` from ``rapier3d_f64``. - -Snapshot blobs are *not* tagged with their flavor: always restore a -snapshot through the same ``(dim, scalar)`` flavor that produced it, -otherwise the floats will be silently misinterpreted. diff --git a/python/docs/getting_started.rst b/python/docs/getting_started.rst index 80ad726c9..5c65242ff 100644 --- a/python/docs/getting_started.rst +++ b/python/docs/getting_started.rst @@ -4,13 +4,9 @@ Getting started Install ------- -Each ``(dim, scalar)`` flavor is its own PyPI package. Install the one you -need:: +Install the ``rapier3d`` package (3D, f32) from PyPI:: - pip install rapier3d # 3D, f32 (the common case) - pip install rapier2d # 2D, f32 - pip install rapier3d-f64 # 3D, f64 - pip install rapier2d-f64 # 2D, f64 + pip install rapier3d Wheels are ``abi3`` (Python ≥ 3.9) and are published for Linux (manylinux + musllinux, x86_64 + aarch64), macOS (arm64), and @@ -69,7 +65,6 @@ flow is also available and matches the Rust API. What to read next ----------------- -* :doc:`dim_scalar` — how to pick a ``(dim, scalar)`` flavor. * :doc:`api/dynamics` — rigid bodies, mass, integration. * :doc:`api/pipeline` — stepping a world manually & query pipelines. * :doc:`api/joints` — impulse + multibody joints, motors, limits. diff --git a/python/docs/index.rst b/python/docs/index.rst index 35e2d4c3e..c52a1ea09 100644 --- a/python/docs/index.rst +++ b/python/docs/index.rst @@ -8,13 +8,11 @@ rapier — Python bindings for the Rapier physics engine high-level plan and the final outputs, but did not hand-write the bulk of the code. Bug reports and PRs welcome. -`Rapier `_ is a fast, deterministic 2D/3D physics +`Rapier `_ is a fast, deterministic 3D physics engine written in Rust. It is exposed to Python via `PyO3 `_ and `maturin `_, -targeting Python ≥ 3.9 via the stable ``abi3`` ABI, as **four packages** — -``rapier2d``, ``rapier3d``, ``rapier2d-f64``, ``rapier3d-f64`` — one per -``(dim, scalar)`` flavor. These docs are written against ``rapier3d`` -(3D / f32); see :doc:`dim_scalar` for the others. +targeting Python ≥ 3.9 via the stable ``abi3`` ABI, as the ``rapier3d`` +package (3D / f32). The Python surface mirrors the Rust crates 1:1 (same type names, same method names, builder kwargs added on top), so anything you can read in @@ -50,10 +48,6 @@ A ball dropped onto a plane:: pos = world.rigid_bodies[ball].translation print(f"ball came to rest at y = {pos.y:.3f}") -``rapier3d`` provides the **3D / f32** flavor. The other ``(dim, scalar)`` -combinations live in the ``rapier2d``, ``rapier3d-f64``, and ``rapier2d-f64`` -packages — see :doc:`dim_scalar` for the full table. - Contents -------- @@ -62,7 +56,6 @@ Contents :caption: Guide getting_started - dim_scalar changelog .. toctree:: diff --git a/python/examples/render/matplotlib_animation.py b/python/examples/render/matplotlib_animation.py index c99db00d8..8981a36bb 100644 --- a/python/examples/render/matplotlib_animation.py +++ b/python/examples/render/matplotlib_animation.py @@ -1,5 +1,6 @@ -"""Animate a 2D world using the DebugLineCollector + matplotlib. +"""Animate a 3D world using the DebugLineCollector + matplotlib. +The debug line segments are projected onto the XY plane for a simple 2D plot. If matplotlib is not installed we still run the simulation; the script only exits with an error if neither code path works. A summary of the number of debug segments rendered is always printed. @@ -13,21 +14,21 @@ import sys -import rapier2d as rp +import rapier3d as rp def main() -> None: - world = rp.PhysicsWorld(gravity=(0, -9.81)) + world = rp.PhysicsWorld(gravity=(0, -9.81, 0)) # Floor + walls. - world.colliders.insert(rp.Collider.cuboid(10, 0.1).translation((0, -2)).build()) - world.colliders.insert(rp.Collider.cuboid(0.1, 5).translation((-5, 0)).build()) - world.colliders.insert(rp.Collider.cuboid(0.1, 5).translation((5, 0)).build()) + world.colliders.insert(rp.Collider.cuboid(10, 0.1, 10).translation((0, -2, 0)).build()) + world.colliders.insert(rp.Collider.cuboid(0.1, 5, 10).translation((-5, 0, 0)).build()) + world.colliders.insert(rp.Collider.cuboid(0.1, 5, 10).translation((5, 0, 0)).build()) # A few falling balls. for x in (-2.0, 0.0, 2.0): world.add_body( - rp.RigidBody.dynamic(translation=(x, 3.0)), + rp.RigidBody.dynamic(translation=(x, 3.0, 0.0)), colliders=[rp.Collider.ball(0.3)], ) @@ -44,7 +45,7 @@ def main() -> None: ) total += int(lines.shape[0]) - # Best-effort plot of the final frame. + # Best-effort plot of the final frame, projected onto the XY plane. try: import matplotlib # noqa: F401 @@ -59,8 +60,10 @@ def main() -> None: world.multibody_joints, world.narrow_phase, ) + # lines has shape (N, 2, 3); keep the X and Y columns for a 2D plot. + segments = lines[:, :, :2] fig, ax = plt.subplots() - ax.add_collection(LineCollection(lines)) + ax.add_collection(LineCollection(segments)) ax.set_aspect("equal") ax.autoscale_view() out = "matplotlib_animation.png" diff --git a/python/examples/vehicle/drive.py b/python/examples/vehicle/drive.py index cac235f3c..f2a44dca6 100644 --- a/python/examples/vehicle/drive.py +++ b/python/examples/vehicle/drive.py @@ -4,8 +4,6 @@ `examples3d/vehicle_controller3.rs`), lets it settle, then applies engine force on the rear wheels and reports the chassis velocity. -3D / f32 only — the ray-cast vehicle controller has no f64 mirror. - Run:: python python/examples/vehicle/drive.py diff --git a/python/examples_tour.py b/python/examples_tour.py index ca9f55842..f968d4a89 100755 --- a/python/examples_tour.py +++ b/python/examples_tour.py @@ -9,9 +9,7 @@ ``pip install --no-deps -e ./python/rapier-testbed``). Usage: - python python/examples_tour.py # all examples (2D, then 3D) - python python/examples_tour.py 2d # only 2D examples - python python/examples_tour.py 3d # only 3D examples + python python/examples_tour.py # all (3D) examples python python/examples_tour.py --list # print the example modules and exit python python/examples_tour.py --start NAME # begin at the first module # whose name contains NAME @@ -24,35 +22,25 @@ import sys -def example_modules(which: str) -> list[str]: +def example_modules() -> list[str]: """Fully-qualified module names for every runnable testbed example.""" - subs = { - "2d": ["examples2"], - "3d": ["examples3"], - "all": ["examples2", "examples3"], - }[which] mods: list[str] = [] - for sub in subs: - pkg = __import__(f"rapier_testbed.{sub}", fromlist=["_"]) - for info in pkgutil.walk_packages(pkg.__path__, prefix=f"rapier_testbed.{sub}."): - leaf = info.name.rsplit(".", 1)[-1] - if info.ispkg or leaf == "utils" or ".utils." in info.name: - continue - mods.append(info.name) - # "examples2" sorts before "examples3", so this keeps 2D ahead of 3D. + pkg = __import__("rapier_testbed.examples3", fromlist=["_"]) + for info in pkgutil.walk_packages(pkg.__path__, prefix="rapier_testbed.examples3."): + leaf = info.name.rsplit(".", 1)[-1] + if info.ispkg or leaf == "utils" or ".utils." in info.name: + continue + mods.append(info.name) return sorted(mods) def main(argv: list[str]) -> int: - which = "all" list_only = False start_at: str | None = None i = 0 while i < len(argv): a = argv[i] - if a in ("2d", "3d", "all"): - which = a - elif a in ("-l", "--list"): + if a in ("-l", "--list"): list_only = True elif a == "--start": i += 1 @@ -69,7 +57,7 @@ def main(argv: list[str]) -> int: i += 1 try: - mods = example_modules(which) + mods = example_modules() except ModuleNotFoundError: print( "rapier_testbed is not installed.\n" @@ -99,7 +87,7 @@ def main(argv: list[str]) -> int: total = len(mods) print( - f"Touring {total} example(s) [{which}]. " + f"Touring {total} example(s). " "Close each window (Esc) to advance; Ctrl-C here to stop.\n", flush=True, ) diff --git a/python/rapier-py-2d-f64/Cargo.toml b/python/rapier-py-2d-f64/Cargo.toml deleted file mode 100644 index 3b041fe47..000000000 --- a/python/rapier-py-2d-f64/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "rapier-py-2d-f64" -version.workspace = true -authors.workspace = true -description = "PyO3 extension module wrapping rapier2d-f64." -homepage.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -edition.workspace = true -publish = false - -[lib] -name = "_rapier2d_f64" -crate-type = ["cdylib"] -path = "src/lib.rs" - -[features] -# Phase 11 — opt-in determinism build (see `rapier-py-3d` for rationale). -determinism = ["rapier2d-f64/enhanced-determinism"] - -[dependencies] -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } -rapier-py-core = { path = "../rapier-py-core" } -# NB: the f64 engine crate has no `simd-stable` feature (SIMD via `wide` is -# f32-only), so this binding can't enable it. -rapier2d-f64 = { path = "../../crates/rapier2d-f64", features = ["serde-serialize", "debug-render"] } -# Phase 11 — Serialization, snapshots. -serde = { workspace = true, features = ["derive", "std"] } -bincode = { workspace = true } -serde_json = { workspace = true } diff --git a/python/rapier-py-2d-f64/README.md b/python/rapier-py-2d-f64/README.md deleted file mode 100644 index 59612f245..000000000 --- a/python/rapier-py-2d-f64/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# rapier2d-f64 - -Python bindings for [Rapier](https://rapier.rs) — a fast, deterministic -physics engine written in Rust. This package provides the **2D, f64** -build. - -```bash -pip install rapier2d-f64 -``` - -```python -import rapier2d_f64 as rp - -bodies = rp.RigidBodySet() -colliders = rp.ColliderSet() -body = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 5.0))) -``` - -## Related packages - -| Package | Dimension | Scalar | -| -------------- | --------- | ------ | -| `rapier2d` | 2D | f32 | -| `rapier3d` | 3D | f32 | -| `rapier2d-f64` | 2D | f64 | -| `rapier3d-f64` | 3D | f64 | - -All four share an identical API differing only in dimension and scalar type. - -## License - -Apache-2.0 diff --git a/python/rapier-py-2d-f64/pyproject.toml b/python/rapier-py-2d-f64/pyproject.toml deleted file mode 100644 index 3c59c3742..000000000 --- a/python/rapier-py-2d-f64/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -[build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" - -[project] -name = "rapier2d-f64" -dynamic = ["version"] -description = "Python bindings for Rapier (2D, f64) — a fast, deterministic physics engine." -readme = "README.md" -license = { text = "Apache-2.0" } -authors = [{ name = "Sébastien Crozet", email = "sebcrozet@dimforge.com" }] -keywords = ["physics", "dynamics", "rigid-body", "simulation", "rapier", "2d", "f64"] -classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Rust", - "Topic :: Games/Entertainment :: Simulation", - "Topic :: Scientific/Engineering :: Physics", -] -requires-python = ">=3.9" - -[project.urls] -Homepage = "https://rapier.rs" -Documentation = "https://rapier.rs/docs/" -Repository = "https://github.com/dimforge/rapier" -Issues = "https://github.com/dimforge/rapier/issues" - -[project.optional-dependencies] -numpy = ["numpy>=1.21"] -dev = ["pytest>=7", "hypothesis>=6", "numpy>=1.21", "ruff>=0.5", "mypy>=1.10"] - -[tool.maturin] -manifest-path = "Cargo.toml" -module-name = "rapier2d_f64._rapier2d_f64" -python-source = "python" -features = ["pyo3/extension-module"] diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/__init__.py b/python/rapier-py-2d-f64/python/rapier2d_f64/__init__.py deleted file mode 100644 index aa9c3bea4..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/__init__.py +++ /dev/null @@ -1,338 +0,0 @@ -"""2D, f64-precision bindings (`rapier.dim2.f64`).""" - -from __future__ import annotations - -from . import _rapier2d_f64 as _ext -from ._rapier2d_f64 import ( # noqa: F401 - # ---- math ---- - Vec2, - Point2, - Rotation2, - Isometry2, - rotation_from_angle, - # ---- errors ---- - RapierError, - InvalidHandle, - MeshConversionError, - UrdfError, - MjcfError, - SerializationError, - QueryFailure, - MeshLoaderError, - # ---- dynamics ---- - RigidBody, - RigidBodyBuilder, - RigidBodyHandle, - RigidBodySet, - RigidBodyType, - LockedAxes, - RigidBodyActivation, - RigidBodyDamping, - RigidBodyDominance, - RigidBodyCcd, - RigidBodyVelocity, - RigidBodyForces, - RigidBodyMassProps, - RigidBodyPosition, - RigidBodyAdditionalMassProps, - MassProperties, - IntegrationParameters, - SpringCoefficients, - CoefficientCombineRule, - IslandManager, - CCDSolver, - ImpulseJointSet, - MultibodyJointSet, - # ---- joints ---- - JointEnabled, - MotorModel, - JointLimits, - JointMotor, - JointAxesMask, - JointAxis, - ImpulseJointHandle, - MultibodyJointHandle, - MultibodyIndex, - MultibodyLinkId, - InverseKinematicsOption, - FixedJoint, - FixedJointBuilder, - RevoluteJoint, - RevoluteJointBuilder, - PrismaticJoint, - PrismaticJointBuilder, - PinSlotJoint, - PinSlotJointBuilder, - RopeJoint, - RopeJointBuilder, - SpringJoint, - SpringJointBuilder, - GenericJoint, - GenericJointBuilder, - ImpulseJoint, - MultibodyLink, - Multibody, - # ---- geometry ---- - Collider, - ColliderBuilder, - ColliderHandle, - ColliderSet, - ColliderType, - ColliderEnabled, - ColliderMaterial, - ColliderFlags, - ColliderParent, - ColliderPosition, - ColliderMassProps, - ShapeType, - SharedShape, - Ball, - Cuboid, - Capsule, - Triangle, - TriMesh, - HeightField, - Compound, - Segment, - Polyline, - ConvexPolygon, - Aabb, - BoundingSphere, - MeshConverter, - TriMeshFlags, - ActiveEvents, - ActiveHooks, - ActiveCollisionTypes, - Group, - InteractionTestMode, - InteractionGroups, - CollisionEventFlags, - CollisionEvent, - ContactForceEvent, - ContactData, - ContactManifold, - ContactManifoldData, - ContactPair, - IntersectionPair, - ColliderPair, - BroadPhasePairEvent, - BroadPhaseBvh, - BvhOptimizationStrategy, - NarrowPhase, - SolverContact, - # ---- pipeline ---- - PhysicsPipeline, - CollisionPipeline, - PhysicsWorld, - QueryPipeline, - QueryFilter, - QueryFilterFlags, - Ray, - RayIntersection, - PointProjection, - ShapeCastHit, - ShapeCastOptions, - ShapeCastStatus, - FeatureId, - NonlinearRigidMotion, - Counters, - StagesCounters, - CollisionDetectionCounters, - SolverCounters, - CCDCounters, - # ---- events / hooks ---- - SolverFlags, - PairFilterContext, - ContactModificationContext, - ChannelEventCollector, - # ---- controllers ---- - AxesMask, - CharacterLength, - CharacterAutostep, - CharacterCollision, - EffectiveCharacterMovement, - KinematicCharacterController, - PdErrors, - PidCorrection, - PdController, - PidController, - # ---- debug-render ---- - DebugRenderPipeline, - DebugRenderStyle, - DebugRenderMode, - DebugRenderObject, - DebugColor, - DebugLineCollector, -) - -from ._event_handler import EventHandler, PhysicsHooks # noqa: F401 -from ._debug_render import DebugRenderBackend # noqa: F401 - -# retag pyclasses for pickle. -from ._pickle_setup import retag_module as _retag - -_retag(_ext, "rapier2d_f64") - -__version__ = _ext.__version__ - -__all__ = [ - "Vec2", - "Point2", - "Rotation2", - "Isometry2", - "rotation_from_angle", - "RapierError", - "InvalidHandle", - "MeshConversionError", - "UrdfError", - "MjcfError", - "SerializationError", - "QueryFailure", - "MeshLoaderError", - "RigidBody", - "RigidBodyBuilder", - "RigidBodyHandle", - "RigidBodySet", - "RigidBodyType", - "LockedAxes", - "RigidBodyActivation", - "RigidBodyDamping", - "RigidBodyDominance", - "RigidBodyCcd", - "RigidBodyVelocity", - "RigidBodyForces", - "RigidBodyMassProps", - "RigidBodyPosition", - "RigidBodyAdditionalMassProps", - "MassProperties", - "IntegrationParameters", - "SpringCoefficients", - "CoefficientCombineRule", - "IslandManager", - "CCDSolver", - "ImpulseJointSet", - "MultibodyJointSet", - # ---- joints ---- - "JointEnabled", - "MotorModel", - "JointLimits", - "JointMotor", - "JointAxesMask", - "JointAxis", - "ImpulseJointHandle", - "MultibodyJointHandle", - "MultibodyIndex", - "MultibodyLinkId", - "InverseKinematicsOption", - "FixedJoint", - "FixedJointBuilder", - "RevoluteJoint", - "RevoluteJointBuilder", - "PrismaticJoint", - "PrismaticJointBuilder", - "PinSlotJoint", - "PinSlotJointBuilder", - "RopeJoint", - "RopeJointBuilder", - "SpringJoint", - "SpringJointBuilder", - "GenericJoint", - "GenericJointBuilder", - "ImpulseJoint", - "MultibodyLink", - "Multibody", - "Collider", - "ColliderBuilder", - "ColliderHandle", - "ColliderSet", - "ColliderType", - "ColliderEnabled", - "ColliderMaterial", - "ColliderFlags", - "ColliderParent", - "ColliderPosition", - "ColliderMassProps", - "ShapeType", - "SharedShape", - "Ball", - "Cuboid", - "Capsule", - "Triangle", - "TriMesh", - "HeightField", - "Compound", - "Segment", - "Polyline", - "ConvexPolygon", - "Aabb", - "BoundingSphere", - "MeshConverter", - "TriMeshFlags", - "ActiveEvents", - "ActiveHooks", - "ActiveCollisionTypes", - "Group", - "InteractionTestMode", - "InteractionGroups", - "CollisionEventFlags", - "CollisionEvent", - "ContactForceEvent", - "ContactData", - "ContactManifold", - "ContactManifoldData", - "ContactPair", - "IntersectionPair", - "ColliderPair", - "BroadPhasePairEvent", - "BroadPhaseBvh", - "BvhOptimizationStrategy", - "NarrowPhase", - "SolverContact", - # ---- pipeline ---- - "PhysicsPipeline", - "CollisionPipeline", - "PhysicsWorld", - "QueryPipeline", - "QueryFilter", - "QueryFilterFlags", - "Ray", - "RayIntersection", - "PointProjection", - "ShapeCastHit", - "ShapeCastOptions", - "ShapeCastStatus", - "FeatureId", - "NonlinearRigidMotion", - "Counters", - "StagesCounters", - "CollisionDetectionCounters", - "SolverCounters", - "CCDCounters", - # ---- events / hooks ---- - "SolverFlags", - "PairFilterContext", - "ContactModificationContext", - "ChannelEventCollector", - "EventHandler", - "PhysicsHooks", - # ---- controllers ---- - "AxesMask", - "CharacterLength", - "CharacterAutostep", - "CharacterCollision", - "EffectiveCharacterMovement", - "KinematicCharacterController", - "PdErrors", - "PidCorrection", - "PdController", - "PidController", - # ---- debug-render ---- - "DebugRenderPipeline", - "DebugRenderStyle", - "DebugRenderMode", - "DebugRenderObject", - "DebugColor", - "DebugLineCollector", - "DebugRenderBackend", - "__version__", -] diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/_debug_render.py b/python/rapier-py-2d-f64/python/rapier2d_f64/_debug_render.py deleted file mode 100644 index d833968d3..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/_debug_render.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Python `Protocol` shim for the debug-render backend API. - -This is a duck-typing protocol (`typing.Protocol`); user classes don't need to -inherit from it — they just need to implement the right method names. - -The Rust ``DebugRenderPipeline.render(..., backend=obj)`` accepts any of: - -- the built-in ``DebugLineCollector`` (fast path; pure-Rust buffer), -- a Python object exposing ``draw_line(object, a, b, color) -> None``. - -Calling into Python per line is significantly slower than the -``DebugLineCollector`` fast path; prefer the collector for anything beyond -prototyping or pure-Python visualization integrations. -""" - -from __future__ import annotations - -from typing import Any, Protocol, runtime_checkable - - -@runtime_checkable -class DebugRenderBackend(Protocol): - """Receiver of debug-render line draw calls. - - Implement this protocol on a class and pass an instance to - :meth:`DebugRenderPipeline.render`. Each call to ``draw_line`` receives: - - * ``object`` — a :class:`DebugRenderObject` describing what is being - rendered (collider shape, AABB, joint anchor, contact, ...). - * ``a``, ``b`` — line endpoints as ``Vec2``/``Vec3`` (or whatever the - cdylib's vector type is for the current ``(dim, scalar)``). - * ``color`` — a :class:`DebugColor` (HSLA tuple; use ``.rgba`` for RGBA - conversion if your downstream renderer expects RGB). - - Exceptions raised inside ``draw_line`` are stashed and re-raised after - ``render()`` returns — subsequent ``draw_line`` calls within the same - ``render`` invocation become no-ops once the slot is filled. - """ - - def draw_line( - self, - object: Any, - a: Any, - b: Any, - color: Any, - ) -> None: - """Draw a single line segment. - - ``object`` is a :class:`DebugRenderObject`. ``a`` / ``b`` are vectors - (``Vec2`` in 2D, ``Vec3`` in 3D). ``color`` is a :class:`DebugColor` - (HSLA storage; call ``.rgba`` to convert to RGB). - """ - ... - - -__all__ = ["DebugRenderBackend"] diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/_event_handler.py b/python/rapier-py-2d-f64/python/rapier2d_f64/_event_handler.py deleted file mode 100644 index 4aaedae32..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Python `Protocol` shims for the event-handler / physics-hooks APIs. - -These are duck-typing protocols (`typing.Protocol`); user classes don't need to -inherit from them, they just need to implement the right method names. - -Both protocols are dim/scalar-agnostic (they are exposed under both -`rapier.dim2` and `rapier.dim3`). -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable - -if TYPE_CHECKING: - # The exact types vary by (dim, scalar). We keep them as `Any` in the - # protocol so that user code doesn't lock itself to a single variant. - pass - - -@runtime_checkable -class EventHandler(Protocol): - """A receiver of collision / contact-force events. - - Assign an instance of any class implementing this protocol to - ``world.event_handler``. The methods are called from the physics solver - (with the GIL re-acquired) during ``world.step()``. - - Exceptions raised inside the callbacks are deferred by default and - re-raised after ``step()`` returns. Set ``world.event_error_policy = 'strict'`` - to short-circuit subsequent callbacks within the same step (the solver - still runs to completion since rapier does not support mid-step aborts, - but no further user callbacks are invoked). - - Note: ``bodies`` and ``colliders`` are passed as ``None`` to avoid borrow - conflicts with the in-flight ``step()`` — handles can still be obtained - from ``event`` / ``contact_pair`` and looked up against the world after - ``step()`` returns. - """ - - def handle_collision_event( - self, - bodies: Any, - colliders: Any, - event: Any, - contact_pair: Optional[Any], - ) -> None: - """Called when two colliders start or stop touching. - - ``event`` is a ``CollisionEvent`` (with ``.started`` / ``.stopped`` - flags). ``contact_pair`` is ``None`` for sensors. - """ - ... - - def handle_contact_force_event( - self, - dt: float, - bodies: Any, - colliders: Any, - contact_pair: Any, - total_force_magnitude: float, - ) -> None: - """Called when the contact-force magnitude exceeds the - ``contact_force_event_threshold`` of either collider in the pair. - - At least one of the involved colliders must have - ``ActiveEvents.CONTACT_FORCE_EVENTS`` set. - """ - ... - - -@runtime_checkable -class PhysicsHooks(Protocol): - """Custom collision / solver hooks invoked by the physics step. - - Assign an instance of any class implementing this protocol to - ``world.physics_hooks``. To receive callbacks, the relevant colliders must - have the appropriate ``ActiveHooks`` flags set (e.g. - ``ActiveHooks.FILTER_CONTACT_PAIRS``). - - All three methods are optional in the duck-typed sense — only define the - ones you care about. (But ``Protocol`` formally requires all three for type - checkers.) - """ - - def filter_contact_pair(self, ctx: Any) -> Any: - """Return ``SolverFlags`` to allow contact computation (``None`` to - completely discard the pair). The default behavior corresponds to - returning ``SolverFlags.COMPUTE_IMPULSES``. - - ``ctx`` is a ``PairFilterContext`` view (read-only). - """ - ... - - def filter_intersection_pair(self, ctx: Any) -> bool: - """Return ``True`` to allow intersection computation between the two - colliders in ``ctx`` (a ``PairFilterContext``).""" - ... - - def modify_solver_contacts(self, ctx: Any) -> None: - """Mutate ``ctx`` (a ``ContactModificationContext``) in place to - change the solver contacts before they're consumed by the solver. - - The context is **only valid for the duration of this call**: do not - keep references to it after the method returns. Doing so will raise - ``RuntimeError`` on access. - """ - ... - - -__all__ = ["EventHandler", "PhysicsHooks"] diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/_math_helpers.py b/python/rapier-py-2d-f64/python/rapier2d_f64/_math_helpers.py deleted file mode 100644 index d66ccb64b..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/_math_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Pure-Python conveniences that don't need to live in Rust. - -These work on any dim/scalar variant since they only assume vector/point -arithmetic (`__mul__`, `__add__`, `__sub__`, scalar mul) and a `float`-like -angle for `wrap_to_pi`. -""" - -from __future__ import annotations - -import math - - -def lerp(a, b, t): - """Linear interpolation: `a*(1-t) + b*t`. - - Works for any `Vec2`/`Vec3`/`Point2`/`Point3` (and plain floats) as long - as `a` and `b` support `+`, `-`, and scalar `*`. - """ - return a + (b - a) * t - - -def wrap_to_pi(angle: float) -> float: - """Wrap an angle in radians to the half-open interval `(-pi, pi]`.""" - a = (angle + math.pi) % (2.0 * math.pi) - if a <= 0.0: - a += 2.0 * math.pi - return a - math.pi diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/_pickle_setup.py b/python/rapier-py-2d-f64/python/rapier2d_f64/_pickle_setup.py deleted file mode 100644 index 695ea1f3f..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/_pickle_setup.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Make pyo3 pyclasses picklable from the correct dim/scalar namespace. - -The PyO3 macros that emit all `rapier` pyclasses set `module = "rapier"` on -every class declaration. Since the four cdylibs (`_rapier2d`, `_rapier2d_f64`, -`_rapier3d`, `_rapier3d_f64`) each define their own copy of (e.g.) -`MassProperties` but all four share `__module__ == "rapier"`, Python's -default pickling cannot disambiguate which flavor to restore: pickle would -fetch `rapier.MassProperties` (== the default 3D-f32 flavor) and reject -non-matching pyclass instances with `PicklingError: it's not the same object -as rapier.MassProperties`. - -To fix this without rewriting the macros, we patch `__module__` on every -exported class at import time so that each flavor's classes declare the -right module path: - - `rapier.dim2.MassProperties.__module__ = "rapier.dim2"` - - `rapier.dim2.f64.MassProperties.__module__ = "rapier.dim2.f64"` - - `rapier.dim3.MassProperties.__module__ = "rapier.dim3"` - - `rapier.dim3.f64.MassProperties.__module__ = "rapier.dim3.f64"` - -Pickle then resolves each instance to its actual flavor. -""" - -from __future__ import annotations - -import types -from typing import Any - - -def retag_module(mod: Any, target_module: str) -> None: - """Set `__module__` on every public class in `mod` to `target_module`.""" - for name in dir(mod): - if name.startswith("_"): - continue - cls = getattr(mod, name) - if not isinstance(cls, type): - continue - # Submodules can show up as types when they are nested package - # objects, skip those. - if isinstance(cls, types.ModuleType): - continue - try: - cls.__module__ = target_module - except (TypeError, AttributeError): - # Some pyclasses or built-in types may refuse mutation; skip - # them. We don't need pickle support for `bool`, `int`, etc. - pass diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/_rapier2d_f64.pyi b/python/rapier-py-2d-f64/python/rapier2d_f64/_rapier2d_f64.pyi deleted file mode 100644 index 619aaa1a4..000000000 --- a/python/rapier-py-2d-f64/python/rapier2d_f64/_rapier2d_f64.pyi +++ /dev/null @@ -1,712 +0,0 @@ -"""Type stubs for `rapier._rapier2d` (2D, f32). - -Phase 02: math layer only. -""" - -from __future__ import annotations - -from typing import Any, Iterator, Tuple, Union - -import numpy as np - -__version__: str - -# ---- Exception tree ------------------------------------------------------- -class RapierError(Exception): ... -class InvalidHandle(RapierError): ... -class MeshConversionError(RapierError): ... -class UrdfError(RapierError): ... -class MjcfError(RapierError): ... -class SerializationError(RapierError): ... -class QueryFailure(RapierError): ... -class MeshLoaderError(RapierError): ... - -VectorLike = Union[ - "Vec2", - "Point2", - Tuple[float, float], - list, - "np.ndarray", -] -RotationLike = Union["Rotation2", float, Tuple[float], list, "np.ndarray"] -IsometryLike = Union["Isometry2", Tuple[VectorLike, RotationLike]] - -# ---- Vec2 ---------------------------------------------------------------- -class Vec2: - def __init__(self, x: float, y: float) -> None: ... - @staticmethod - def zeros() -> Vec2: ... - @staticmethod - def unit_x() -> Vec2: ... - @staticmethod - def unit_y() -> Vec2: ... - @staticmethod - def from_tuple(t: VectorLike) -> Vec2: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Vec2: ... - - x: float - y: float - - def dot(self, other: VectorLike) -> float: ... - def norm(self) -> float: ... - def norm_squared(self) -> float: ... - def normalize(self) -> Vec2: ... - def lerp(self, other: VectorLike, t: float) -> Vec2: ... - def to_tuple(self) -> Tuple[float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def bitwise_equal(self, other: Vec2) -> bool: ... - - def __add__(self, other: VectorLike) -> Vec2: ... - def __sub__(self, other: VectorLike) -> Vec2: ... - def __mul__(self, scalar: float) -> Vec2: ... - def __rmul__(self, scalar: float) -> Vec2: ... - def __truediv__(self, scalar: float) -> Vec2: ... - def __neg__(self) -> Vec2: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Point2 -------------------------------------------------------------- -class Point2: - def __init__(self, x: float, y: float) -> None: ... - @staticmethod - def origin() -> Point2: ... - @staticmethod - def from_tuple(t: VectorLike) -> Point2: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Point2: ... - - x: float - y: float - coords: Vec2 - - def to_tuple(self) -> Tuple[float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def bitwise_equal(self, other: Point2) -> bool: ... - - def __add__(self, v: VectorLike) -> Point2: ... - def __sub__(self, other: Any) -> Union[Point2, Vec2]: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Rotation2 ----------------------------------------------------------- -class Rotation2: - def __init__(self, angle: float) -> None: ... - @staticmethod - def identity() -> Rotation2: ... - @staticmethod - def from_angle(angle: float) -> Rotation2: ... - - angle: float - cos_angle: float - sin_angle: float - - def inverse(self) -> Rotation2: ... - def to_matrix(self) -> np.ndarray: ... - def transform_point(self, p: VectorLike) -> Point2: ... - def transform_vector(self, v: VectorLike) -> Vec2: ... - def slerp(self, other: RotationLike, t: float) -> Rotation2: ... - def bitwise_equal(self, other: Rotation2) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Isometry2 ----------------------------------------------------------- -class Isometry2: - def __init__( - self, - translation: VectorLike | None = None, - rotation: RotationLike | None = None, - ) -> None: ... - @staticmethod - def identity() -> Isometry2: ... - @staticmethod - def from_translation(x: float, y: float) -> Isometry2: ... - @staticmethod - def from_rotation(angle: float) -> Isometry2: ... - - @property - def translation(self) -> Vec2: ... - @property - def rotation(self) -> Rotation2: ... - - def inverse(self) -> Isometry2: ... - def transform_point(self, p: VectorLike) -> Point2: ... - def transform_vector(self, v: VectorLike) -> Vec2: ... - def to_matrix(self) -> np.ndarray: ... - def lerp_slerp(self, other: Isometry2, t: float) -> Isometry2: ... - def bitwise_equal(self, other: Isometry2) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Free helpers -------------------------------------------------------- -def rotation_from_angle(angle: float) -> Rotation2: ... - -# ===================================================================== -# Phase 03 — Dynamics (2D) -# ===================================================================== - -import enum - -class RigidBodyType(enum.IntEnum): - DYNAMIC: int - FIXED: int - KINEMATIC_VELOCITY_BASED: int - KINEMATIC_POSITION_BASED: int - -class CoefficientCombineRule(enum.IntEnum): - AVERAGE: int - MIN: int - MULTIPLY: int - MAX: int - CLAMPED_SUM: int - -# Note: no FrictionModel in 2D. - -class ColliderHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> ColliderHandle: ... - @staticmethod - def invalid() -> ColliderHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class ColliderSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class ImpulseJointSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class MultibodyJointSet: - def __init__(self) -> None: ... - -class RigidBodyHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> RigidBodyHandle: ... - @staticmethod - def invalid() -> RigidBodyHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class LockedAxes: - TRANSLATION_LOCKED_X: LockedAxes - TRANSLATION_LOCKED_Y: LockedAxes - TRANSLATION_LOCKED: LockedAxes - ROTATION_LOCKED: LockedAxes - ALL: LockedAxes - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> LockedAxes: ... - @staticmethod - def all_axes() -> LockedAxes: ... - @property - def bits(self) -> int: ... - -class RigidBodyActivation: - sleep_threshold: float - linear_threshold: float - angular_threshold: float - time_until_sleep: float - sleeping: bool - def __init__(self) -> None: ... - @staticmethod - def active() -> RigidBodyActivation: ... - @staticmethod - def inactive() -> RigidBodyActivation: ... - @staticmethod - def cannot_sleep() -> RigidBodyActivation: ... - def is_active(self) -> bool: ... - -class SpringCoefficients: - stiffness: float - damping: float - def __init__(self, stiffness: float = 30.0, damping: float = 5.0) -> None: ... - @staticmethod - def contact_defaults() -> SpringCoefficients: ... - @staticmethod - def joint_defaults() -> SpringCoefficients: ... - -class MassProperties: - def __init__(self, local_com: VectorLike | None = None, mass: float = 0.0, principal_inertia: float = 0.0) -> None: ... - @staticmethod - def zero() -> MassProperties: ... - @staticmethod - def from_ball(density: float, radius: float) -> MassProperties: ... - @staticmethod - def from_cuboid(density: float, half_extents: VectorLike) -> MassProperties: ... - @staticmethod - def from_capsule(density: float, a: VectorLike, b: VectorLike, radius: float) -> MassProperties: ... - @property - def mass(self) -> float: ... - @property - def inv_mass(self) -> float: ... - @property - def local_com(self) -> Point2: ... - @property - def principal_inertia(self) -> float: ... - -class RigidBodyAdditionalMassProps: - @staticmethod - def from_mass(mass: float) -> RigidBodyAdditionalMassProps: ... - @staticmethod - def from_mass_properties(mp: MassProperties) -> RigidBodyAdditionalMassProps: ... - -class RigidBodyDamping: - linear_damping: float - angular_damping: float - -class RigidBodyDominance: - group: int - -class RigidBodyCcd: - ccd_enabled: bool - soft_ccd_prediction: float - -class RigidBodyVelocity: - linvel: Vec2 - angvel: float - -class RigidBodyForces: - force: Vec2 - torque: float - gravity_scale: float - -class RigidBodyMassProps: - local_mprops: MassProperties - -class RigidBodyPosition: - position: Isometry2 - next_position: Isometry2 - -class IntegrationParameters: - dt: float - min_ccd_dt: float - warmstart_coefficient: float - length_unit: float - normalized_allowed_linear_error: float - normalized_max_corrective_velocity: float - normalized_prediction_distance: float - num_solver_iterations: int - num_internal_pgs_iterations: int - num_internal_stabilization_iterations: int - min_island_size: int - max_ccd_substeps: int - contact_softness: SpringCoefficients - def __init__(self) -> None: ... - @staticmethod - def default_params() -> IntegrationParameters: ... - -class IslandManager: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - def active_dynamic_set(self) -> list[RigidBodyHandle]: ... - -class CCDSolver: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def solve_ccd(self, *args: Any, **kwargs: Any) -> None: ... - -class RigidBodyBuilder: - def __init__(self, body_type: RigidBodyType = ...) -> None: ... - def translation(self, v: VectorLike) -> RigidBodyBuilder: ... - def rotation(self, angle: float) -> RigidBodyBuilder: ... - def position(self, p: IsometryLike) -> RigidBodyBuilder: ... - def linvel(self, v: VectorLike) -> RigidBodyBuilder: ... - def angvel(self, a: float) -> RigidBodyBuilder: ... - def linear_damping(self, f: float) -> RigidBodyBuilder: ... - def angular_damping(self, f: float) -> RigidBodyBuilder: ... - def gravity_scale(self, f: float) -> RigidBodyBuilder: ... - def can_sleep(self, b: bool) -> RigidBodyBuilder: ... - def sleeping(self, b: bool) -> RigidBodyBuilder: ... - def ccd_enabled(self, b: bool) -> RigidBodyBuilder: ... - def soft_ccd_prediction(self, f: float) -> RigidBodyBuilder: ... - def dominance_group(self, g: int) -> RigidBodyBuilder: ... - def additional_mass(self, m: float) -> RigidBodyBuilder: ... - def locked_axes(self, la: LockedAxes) -> RigidBodyBuilder: ... - def build(self) -> RigidBody: ... - -class RigidBody: - position: Isometry2 - translation: Vec2 - rotation: Rotation2 - linvel: Vec2 - angvel: float - user_data: int - body_type: RigidBodyType - locked_axes: LockedAxes - @staticmethod - def dynamic(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def fixed(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_velocity_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_position_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def new_body(body_type: RigidBodyType, **kwargs: Any) -> RigidBodyBuilder: ... - def add_force(self, force: VectorLike, wake_up: bool = True) -> None: ... - def add_torque(self, torque: float, wake_up: bool = True) -> None: ... - def apply_impulse(self, impulse: VectorLike, wake_up: bool = True) -> None: ... - def apply_torque_impulse(self, torque_impulse: float, wake_up: bool = True) -> None: ... - def reset_forces(self, wake_up: bool = True) -> None: ... - def wake_up(self, strong: bool = True) -> None: ... - def sleep(self) -> None: ... - def kinetic_energy(self) -> float: ... - def recompute_mass_properties_from_colliders(self, colliders: ColliderSet) -> None: ... - def set_additional_mass(self, mass: float, wake_up: bool = True) -> None: ... - -class RigidBodySet: - def __init__(self) -> None: ... - def insert(self, body: RigidBody | RigidBodyBuilder) -> RigidBodyHandle: ... - def remove(self, handle: RigidBodyHandle, islands: IslandManager, colliders: ColliderSet, impulse_joints: ImpulseJointSet, multibody_joints: MultibodyJointSet, remove_attached_colliders: bool = True) -> RigidBody | None: ... - def get(self, handle: RigidBodyHandle) -> RigidBody | None: ... - def __getitem__(self, handle: RigidBodyHandle) -> RigidBody: ... - def __contains__(self, handle: RigidBodyHandle) -> bool: ... - def __len__(self) -> int: ... - -# ---- events / hooks (phase 07) ------------------------------------------- - -class SolverFlags: - COMPUTE_IMPULSES: SolverFlags - EMPTY: SolverFlags - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> SolverFlags: ... - @property - def bits(self) -> int: ... - def contains(self, other: SolverFlags) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: SolverFlags) -> SolverFlags: ... - def __and__(self, other: SolverFlags) -> SolverFlags: ... - def __xor__(self, other: SolverFlags) -> SolverFlags: ... - def __invert__(self) -> SolverFlags: ... - -class PairFilterContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - -class ContactModificationContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - @property - def local_n1(self) -> Vec2: ... - @property - def local_n2(self) -> Vec2: ... - normal: Vec2 - user_data: int - @property - def solver_contacts(self) -> list[SolverContact]: ... - def clear_solver_contacts(self) -> None: ... - def num_solver_contacts(self) -> int: ... - def remove_solver_contact(self, i: int) -> None: ... - def update_as_oneway_platform(self, allowed_local_n1: Vec2 | tuple[float, float], angle: float) -> None: ... - -class ChannelEventCollector: - def __init__(self) -> None: ... - def drain_collision_events(self) -> list[CollisionEvent]: ... - def drain_contact_force_events(self) -> list[ContactForceEvent]: ... - def clear(self) -> None: ... - def __len__(self) -> int: ... - -# ---- controllers (phase 08) ---------------------------------------------- -# 2D variant: no DynamicRayCastVehicleController / Wheel / RayCastInfo (3D-only upstream). - -from typing import Callable, Literal - -class CharacterLength: - kind: Literal["absolute", "relative"] - value: float - def __init__(self, value: float = 0.0, kind: str = "absolute") -> None: ... - @staticmethod - def absolute(value: float) -> CharacterLength: ... - @staticmethod - def relative(value: float) -> CharacterLength: ... - -class CharacterAutostep: - max_height: CharacterLength - min_width: CharacterLength - include_dynamic_bodies: bool - def __init__( - self, - max_height: CharacterLength | float | None = None, - min_width: CharacterLength | float | None = None, - include_dynamic_bodies: bool = True, - ) -> None: ... - @staticmethod - def disabled() -> None: ... - -class EffectiveCharacterMovement: - @property - def translation(self) -> Vec2: ... - @property - def grounded(self) -> bool: ... - @property - def is_sliding_down_slope(self) -> bool: ... - -class CharacterCollision: - @property - def handle(self) -> ColliderHandle: ... - @property - def character_pos(self) -> Isometry2: ... - @property - def translation_applied(self) -> Vec2: ... - @property - def translation_remaining(self) -> Vec2: ... - @property - def toi(self) -> ShapeCastHit: ... - -class KinematicCharacterController: - up: Vec2 - offset: CharacterLength - slide: bool - autostep: CharacterAutostep | None - max_slope_climb_angle: float - min_slope_slide_angle: float - snap_to_ground: CharacterLength | None - normal_nudge_factor: float - def __init__( - self, - *, - up: Vec2 | tuple[float, float] | None = None, - offset: CharacterLength | float | None = None, - slide: bool | None = None, - autostep: CharacterAutostep | None = None, - max_slope_climb_angle: float | None = None, - min_slope_slide_angle: float | None = None, - snap_to_ground: CharacterLength | float | None = None, - normal_nudge_factor: float | None = None, - ) -> None: ... - def move_shape( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - shape: SharedShape, - shape_pos: Isometry2, - desired_translation: Vec2 | tuple[float, float], - filter: QueryFilter | None = None, - events_callback: Callable[[CharacterCollision], None] | None = None, - ) -> EffectiveCharacterMovement: ... - def solve_character_collision_impulses( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - character_shape: SharedShape, - character_pos: Isometry2, - character_mass: float, - collisions: list[CharacterCollision], - filter: QueryFilter | None = None, - ) -> None: ... - -class AxesMask: - LIN_X: AxesMask - LIN_Y: AxesMask - ANG_Z: AxesMask - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> AxesMask: ... - @staticmethod - def all() -> AxesMask: ... - @staticmethod - def all_axes() -> AxesMask: ... - @property - def bits(self) -> int: ... - def contains(self, other: AxesMask) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: AxesMask) -> AxesMask: ... - def __and__(self, other: AxesMask) -> AxesMask: ... - def __xor__(self, other: AxesMask) -> AxesMask: ... - def __invert__(self) -> AxesMask: ... - def __sub__(self, other: AxesMask) -> AxesMask: ... - def __contains__(self, other: AxesMask) -> bool: ... - -class PdErrors: - linear: Vec2 - angular: float - def __init__( - self, - linear: Vec2 | tuple[float, float] | None = None, - angular: float | None = None, - ) -> None: ... - -class PidCorrection: - @property - def linear(self) -> Vec2: ... - @property - def angular(self) -> float: ... - -class PdController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec2 | tuple[float, float] | float | None = None, - Kd: Vec2 | tuple[float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_kd(self, v: Vec2 | tuple[float, float]) -> None: ... - def rigid_body_correction(self, body: RigidBody, target_pose: Isometry2) -> PidCorrection: ... - def correction(self, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -class PidController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec2 | tuple[float, float] | float | None = None, - Ki: Vec2 | tuple[float, float] | float | None = None, - Kd: Vec2 | tuple[float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_ki(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_kd(self, v: Vec2 | tuple[float, float]) -> None: ... - def reset(self) -> None: ... - def rigid_body_correction(self, dt: float, body: RigidBody, target_pose: Isometry2) -> PidCorrection: ... - def position_correction(self, dt: float, position: Isometry2, target_position: Isometry2) -> PidCorrection: ... - def update(self, dt: float, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -# ---- debug-render (phase 10) --------------------------------------------- - -class DebugRenderObject: - kind: int - RIGID_BODY: DebugRenderObject - COLLIDER: DebugRenderObject - COLLIDER_AABB: DebugRenderObject - IMPULSE_JOINT: DebugRenderObject - MULTIBODY_JOINT: DebugRenderObject - CONTACT_PAIR: DebugRenderObject - -class DebugColor: - def __init__(self, h: float = 0.0, s: float = 0.0, l: float = 0.0, a: float = 1.0) -> None: ... - @staticmethod - def from_hsla(h: float, s: float, l: float, a: float) -> DebugColor: ... - @staticmethod - def from_rgba(r: float, g: float, b: float, a: float) -> DebugColor: ... - @property - def hsla(self) -> list[float]: ... - @property - def rgba(self) -> list[float]: ... - -class DebugRenderMode: - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> DebugRenderMode: ... - @staticmethod - def default() -> DebugRenderMode: ... - @staticmethod - def all() -> DebugRenderMode: ... - COLLIDER_SHAPES: DebugRenderMode - RIGID_BODY_AXES: DebugRenderMode - MULTIBODY_JOINTS: DebugRenderMode - IMPULSE_JOINTS: DebugRenderMode - JOINTS: DebugRenderMode - SOLVER_CONTACTS: DebugRenderMode - CONTACTS: DebugRenderMode - COLLIDER_AABBS: DebugRenderMode - EMPTY: DebugRenderMode - @property - def bits(self) -> int: ... - def contains(self, other: DebugRenderMode) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __and__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __xor__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __invert__(self) -> DebugRenderMode: ... - def __bool__(self) -> bool: ... - -class DebugRenderStyle: - def __init__(self) -> None: ... - @staticmethod - def default() -> DebugRenderStyle: ... - subdivisions: int - border_subdivisions: int - collider_dynamic_color: DebugColor - collider_fixed_color: DebugColor - collider_kinematic_color: DebugColor - collider_parentless_color: DebugColor - impulse_joint_anchor_color: DebugColor - impulse_joint_separation_color: DebugColor - multibody_joint_anchor_color: DebugColor - multibody_joint_separation_color: DebugColor - sleep_color_multiplier: DebugColor - disabled_color_multiplier: DebugColor - rigid_body_axes_length: float - contact_depth_color: DebugColor - contact_normal_color: DebugColor - contact_normal_length: float - collider_aabb_color: DebugColor - -class DebugLineCollector: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def lines(self) -> np.ndarray: ... - def colors(self) -> np.ndarray: ... - def objects(self) -> np.ndarray: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[tuple[DebugRenderObject, Vec2, Vec2, DebugColor]]: ... - -class DebugRenderPipeline: - mode: DebugRenderMode - style: DebugRenderStyle - def __init__( - self, - mode: DebugRenderMode | None = None, - style: DebugRenderStyle | None = None, - ) -> None: ... - def render( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - backend: Any, - ) -> None: ... - def render_to_arrays( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... diff --git a/python/rapier-py-2d-f64/python/rapier2d_f64/py.typed b/python/rapier-py-2d-f64/python/rapier2d_f64/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/rapier-py-2d-f64/src/lib.rs b/python/rapier-py-2d-f64/src/lib.rs deleted file mode 100644 index 10aac5677..000000000 --- a/python/rapier-py-2d-f64/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! `_rapier2d_f64` — PyO3 extension module for rapier2d-f64 (2D, f64). - -use pyo3::prelude::*; - -use rapier2d_f64 as rapier; - -rapier_py_core::define_conv_types!(Real = f64, DIM = 2); -rapier_py_core::define_math_types!(DIM = 2); -rapier_py_core::define_geometry_types!(DIM = 2); -rapier_py_core::define_joints_types!(DIM = 2); -rapier_py_core::define_dynamics_types!(DIM = 2); -rapier_py_core::define_pipeline_types!(DIM = 2); -rapier_py_core::define_events_hooks_types!(DIM = 2); -rapier_py_core::define_controllers_types!(DIM = 2); - -// debug-render adapter. -rapier_py_core::define_debug_render_types!(DIM = 2); - -// must run AFTER every other `define_*_types!`. -rapier_py_core::define_serde_types!(DIM = 2); - -const RAPIER_PY_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[pymodule] -fn _rapier2d_f64(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - rapier_py_core::errors::register_errors(py, m)?; - register_math(py, m)?; - register_geometry(py, m)?; - register_joints(py, m)?; - register_dynamics(py, m)?; - register_pipeline(py, m)?; - register_events_hooks(py, m)?; - register_controllers(py, m)?; - register_debug_render(py, m)?; - m.add("__version__", RAPIER_PY_VERSION)?; - Ok(()) -} diff --git a/python/rapier-py-2d/Cargo.toml b/python/rapier-py-2d/Cargo.toml deleted file mode 100644 index f01924cdc..000000000 --- a/python/rapier-py-2d/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "rapier-py-2d" -version.workspace = true -authors.workspace = true -description = "PyO3 extension module wrapping rapier2d (f32)." -homepage.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -edition.workspace = true -publish = false - -[lib] -name = "_rapier2d" -crate-type = ["cdylib"] -path = "src/lib.rs" - -[features] -# Phase 11 — opt-in determinism build (see `rapier-py-3d` for rationale). -determinism = ["rapier2d/enhanced-determinism"] - -[dependencies] -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } -rapier-py-core = { path = "../rapier-py-core" } -rapier2d = { path = "../../crates/rapier2d", features = ["serde-serialize", "debug-render", "simd-stable"] } -# Phase 11 — Serialization, snapshots. -serde = { workspace = true, features = ["derive", "std"] } -bincode = { workspace = true } -serde_json = { workspace = true } diff --git a/python/rapier-py-2d/README.md b/python/rapier-py-2d/README.md deleted file mode 100644 index 65e68554c..000000000 --- a/python/rapier-py-2d/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# rapier2d - -Python bindings for [Rapier](https://rapier.rs) — a fast, deterministic -physics engine written in Rust. This package provides the **2D, f32** -build. - -```bash -pip install rapier2d -``` - -```python -import rapier2d as rp - -bodies = rp.RigidBodySet() -colliders = rp.ColliderSet() -body = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 5.0))) -``` - -## Related packages - -| Package | Dimension | Scalar | -| -------------- | --------- | ------ | -| `rapier2d` | 2D | f32 | -| `rapier3d` | 3D | f32 | -| `rapier2d-f64` | 2D | f64 | -| `rapier3d-f64` | 3D | f64 | - -All four share an identical API differing only in dimension and scalar type. - -## License - -Apache-2.0 diff --git a/python/rapier-py-2d/pyproject.toml b/python/rapier-py-2d/pyproject.toml deleted file mode 100644 index c3719cb3b..000000000 --- a/python/rapier-py-2d/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -[build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" - -[project] -name = "rapier2d" -dynamic = ["version"] -description = "Python bindings for Rapier (2D, f32) — a fast, deterministic physics engine." -readme = "README.md" -license = { text = "Apache-2.0" } -authors = [{ name = "Sébastien Crozet", email = "sebcrozet@dimforge.com" }] -keywords = ["physics", "dynamics", "rigid-body", "simulation", "rapier", "2d"] -classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Rust", - "Topic :: Games/Entertainment :: Simulation", - "Topic :: Scientific/Engineering :: Physics", -] -requires-python = ">=3.9" - -[project.urls] -Homepage = "https://rapier.rs" -Documentation = "https://rapier.rs/docs/" -Repository = "https://github.com/dimforge/rapier" -Issues = "https://github.com/dimforge/rapier/issues" - -[project.optional-dependencies] -numpy = ["numpy>=1.21"] -dev = ["pytest>=7", "hypothesis>=6", "numpy>=1.21", "ruff>=0.5", "mypy>=1.10"] - -[tool.maturin] -manifest-path = "Cargo.toml" -module-name = "rapier2d._rapier2d" -python-source = "python" -features = ["pyo3/extension-module"] diff --git a/python/rapier-py-2d/python/rapier2d/__init__.py b/python/rapier-py-2d/python/rapier2d/__init__.py deleted file mode 100644 index e3915b8fe..000000000 --- a/python/rapier-py-2d/python/rapier2d/__init__.py +++ /dev/null @@ -1,343 +0,0 @@ -"""2D bindings (f32 by default, with `rapier.dim2.f64` for double precision).""" - -from __future__ import annotations - -from . import _rapier2d as _ext -from . import math # noqa: F401 (re-export submodule) - -from ._rapier2d import ( # noqa: F401 - # ---- math ---- - Vec2, - Point2, - Rotation2, - Isometry2, - rotation_from_angle, - # ---- errors ---- - RapierError, - InvalidHandle, - MeshConversionError, - UrdfError, - MjcfError, - SerializationError, - QueryFailure, - MeshLoaderError, - # ---- dynamics ---- - RigidBody, - RigidBodyBuilder, - RigidBodyHandle, - RigidBodySet, - RigidBodyType, - LockedAxes, - RigidBodyActivation, - RigidBodyDamping, - RigidBodyDominance, - RigidBodyCcd, - RigidBodyVelocity, - RigidBodyForces, - RigidBodyMassProps, - RigidBodyPosition, - RigidBodyAdditionalMassProps, - MassProperties, - IntegrationParameters, - SpringCoefficients, - CoefficientCombineRule, - IslandManager, - CCDSolver, - ImpulseJointSet, - MultibodyJointSet, - # ---- joints ---- - JointEnabled, - MotorModel, - JointLimits, - JointMotor, - JointAxesMask, - JointAxis, - ImpulseJointHandle, - MultibodyJointHandle, - MultibodyIndex, - MultibodyLinkId, - InverseKinematicsOption, - FixedJoint, - FixedJointBuilder, - RevoluteJoint, - RevoluteJointBuilder, - PrismaticJoint, - PrismaticJointBuilder, - PinSlotJoint, - PinSlotJointBuilder, - RopeJoint, - RopeJointBuilder, - SpringJoint, - SpringJointBuilder, - GenericJoint, - GenericJointBuilder, - ImpulseJoint, - MultibodyLink, - Multibody, - # ---- geometry ---- - Collider, - ColliderBuilder, - ColliderHandle, - ColliderSet, - ColliderType, - ColliderEnabled, - ColliderMaterial, - ColliderFlags, - ColliderParent, - ColliderPosition, - ColliderMassProps, - ShapeType, - SharedShape, - Ball, - Cuboid, - Capsule, - Triangle, - TriMesh, - HeightField, - Compound, - Segment, - Polyline, - ConvexPolygon, - Aabb, - BoundingSphere, - MeshConverter, - TriMeshFlags, - ActiveEvents, - ActiveHooks, - ActiveCollisionTypes, - Group, - InteractionTestMode, - InteractionGroups, - CollisionEventFlags, - CollisionEvent, - ContactForceEvent, - ContactData, - ContactManifold, - ContactManifoldData, - ContactPair, - IntersectionPair, - ColliderPair, - BroadPhasePairEvent, - BroadPhaseBvh, - BvhOptimizationStrategy, - NarrowPhase, - SolverContact, - # ---- pipeline ---- - PhysicsPipeline, - CollisionPipeline, - PhysicsWorld, - QueryPipeline, - QueryFilter, - QueryFilterFlags, - Ray, - RayIntersection, - PointProjection, - ShapeCastHit, - ShapeCastOptions, - ShapeCastStatus, - FeatureId, - NonlinearRigidMotion, - Counters, - StagesCounters, - CollisionDetectionCounters, - SolverCounters, - CCDCounters, - # ---- events / hooks ---- - SolverFlags, - PairFilterContext, - ContactModificationContext, - ChannelEventCollector, - # ---- controllers ---- - AxesMask, - CharacterLength, - CharacterAutostep, - CharacterCollision, - EffectiveCharacterMovement, - KinematicCharacterController, - PdErrors, - PidCorrection, - PdController, - PidController, - # ---- debug-render ---- - DebugRenderPipeline, - DebugRenderStyle, - DebugRenderMode, - DebugRenderObject, - DebugColor, - DebugLineCollector, -) - -from ._event_handler import EventHandler, PhysicsHooks # noqa: F401 -from ._debug_render import DebugRenderBackend # noqa: F401 - -# retag pyclasses so pickle can resolve them by their flavor's -# fully-qualified module path. See `_pickle_setup.retag_module` for the -# rationale. -from ._pickle_setup import retag_module as _retag - -_retag(_ext, "rapier2d") - -__version__ = _ext.__version__ - -__all__ = [ - "Vec2", - "Point2", - "Rotation2", - "Isometry2", - "rotation_from_angle", - "RapierError", - "InvalidHandle", - "MeshConversionError", - "UrdfError", - "MjcfError", - "SerializationError", - "QueryFailure", - "MeshLoaderError", - "RigidBody", - "RigidBodyBuilder", - "RigidBodyHandle", - "RigidBodySet", - "RigidBodyType", - "LockedAxes", - "RigidBodyActivation", - "RigidBodyDamping", - "RigidBodyDominance", - "RigidBodyCcd", - "RigidBodyVelocity", - "RigidBodyForces", - "RigidBodyMassProps", - "RigidBodyPosition", - "RigidBodyAdditionalMassProps", - "MassProperties", - "IntegrationParameters", - "SpringCoefficients", - "CoefficientCombineRule", - "IslandManager", - "CCDSolver", - "ImpulseJointSet", - "MultibodyJointSet", - # ---- joints ---- - "JointEnabled", - "MotorModel", - "JointLimits", - "JointMotor", - "JointAxesMask", - "JointAxis", - "ImpulseJointHandle", - "MultibodyJointHandle", - "MultibodyIndex", - "MultibodyLinkId", - "InverseKinematicsOption", - "FixedJoint", - "FixedJointBuilder", - "RevoluteJoint", - "RevoluteJointBuilder", - "PrismaticJoint", - "PrismaticJointBuilder", - "PinSlotJoint", - "PinSlotJointBuilder", - "RopeJoint", - "RopeJointBuilder", - "SpringJoint", - "SpringJointBuilder", - "GenericJoint", - "GenericJointBuilder", - "ImpulseJoint", - "MultibodyLink", - "Multibody", - "Collider", - "ColliderBuilder", - "ColliderHandle", - "ColliderSet", - "ColliderType", - "ColliderEnabled", - "ColliderMaterial", - "ColliderFlags", - "ColliderParent", - "ColliderPosition", - "ColliderMassProps", - "ShapeType", - "SharedShape", - "Ball", - "Cuboid", - "Capsule", - "Triangle", - "TriMesh", - "HeightField", - "Compound", - "Segment", - "Polyline", - "ConvexPolygon", - "Aabb", - "BoundingSphere", - "MeshConverter", - "TriMeshFlags", - "ActiveEvents", - "ActiveHooks", - "ActiveCollisionTypes", - "Group", - "InteractionTestMode", - "InteractionGroups", - "CollisionEventFlags", - "CollisionEvent", - "ContactForceEvent", - "ContactData", - "ContactManifold", - "ContactManifoldData", - "ContactPair", - "IntersectionPair", - "ColliderPair", - "BroadPhasePairEvent", - "BroadPhaseBvh", - "BvhOptimizationStrategy", - "NarrowPhase", - "SolverContact", - # ---- pipeline ---- - "PhysicsPipeline", - "CollisionPipeline", - "PhysicsWorld", - "QueryPipeline", - "QueryFilter", - "QueryFilterFlags", - "Ray", - "RayIntersection", - "PointProjection", - "ShapeCastHit", - "ShapeCastOptions", - "ShapeCastStatus", - "FeatureId", - "NonlinearRigidMotion", - "Counters", - "StagesCounters", - "CollisionDetectionCounters", - "SolverCounters", - "CCDCounters", - # ---- events / hooks ---- - "SolverFlags", - "PairFilterContext", - "ContactModificationContext", - "ChannelEventCollector", - "EventHandler", - "PhysicsHooks", - # ---- controllers ---- - "AxesMask", - "CharacterLength", - "CharacterAutostep", - "CharacterCollision", - "EffectiveCharacterMovement", - "KinematicCharacterController", - "PdErrors", - "PidCorrection", - "PdController", - "PidController", - # ---- debug-render ---- - "DebugRenderPipeline", - "DebugRenderStyle", - "DebugRenderMode", - "DebugRenderObject", - "DebugColor", - "DebugLineCollector", - "DebugRenderBackend", - "math", - "__version__", -] diff --git a/python/rapier-py-2d/python/rapier2d/__init__.pyi b/python/rapier-py-2d/python/rapier2d/__init__.pyi deleted file mode 100644 index 0a917d1ea..000000000 --- a/python/rapier-py-2d/python/rapier2d/__init__.pyi +++ /dev/null @@ -1,20 +0,0 @@ -"""Stub for `rapier.dim2` (2D, f32).""" - -from ._rapier2d import ( - Isometry2 as Isometry2, - Point2 as Point2, - RapierError as RapierError, - InvalidHandle as InvalidHandle, - MeshConversionError as MeshConversionError, - UrdfError as UrdfError, - MjcfError as MjcfError, - SerializationError as SerializationError, - QueryFailure as QueryFailure, - MeshLoaderError as MeshLoaderError, - Rotation2 as Rotation2, - Vec2 as Vec2, - rotation_from_angle as rotation_from_angle, -) -from . import f64 as f64, math as math - -__version__: str diff --git a/python/rapier-py-2d/python/rapier2d/_debug_render.py b/python/rapier-py-2d/python/rapier2d/_debug_render.py deleted file mode 100644 index d833968d3..000000000 --- a/python/rapier-py-2d/python/rapier2d/_debug_render.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Python `Protocol` shim for the debug-render backend API. - -This is a duck-typing protocol (`typing.Protocol`); user classes don't need to -inherit from it — they just need to implement the right method names. - -The Rust ``DebugRenderPipeline.render(..., backend=obj)`` accepts any of: - -- the built-in ``DebugLineCollector`` (fast path; pure-Rust buffer), -- a Python object exposing ``draw_line(object, a, b, color) -> None``. - -Calling into Python per line is significantly slower than the -``DebugLineCollector`` fast path; prefer the collector for anything beyond -prototyping or pure-Python visualization integrations. -""" - -from __future__ import annotations - -from typing import Any, Protocol, runtime_checkable - - -@runtime_checkable -class DebugRenderBackend(Protocol): - """Receiver of debug-render line draw calls. - - Implement this protocol on a class and pass an instance to - :meth:`DebugRenderPipeline.render`. Each call to ``draw_line`` receives: - - * ``object`` — a :class:`DebugRenderObject` describing what is being - rendered (collider shape, AABB, joint anchor, contact, ...). - * ``a``, ``b`` — line endpoints as ``Vec2``/``Vec3`` (or whatever the - cdylib's vector type is for the current ``(dim, scalar)``). - * ``color`` — a :class:`DebugColor` (HSLA tuple; use ``.rgba`` for RGBA - conversion if your downstream renderer expects RGB). - - Exceptions raised inside ``draw_line`` are stashed and re-raised after - ``render()`` returns — subsequent ``draw_line`` calls within the same - ``render`` invocation become no-ops once the slot is filled. - """ - - def draw_line( - self, - object: Any, - a: Any, - b: Any, - color: Any, - ) -> None: - """Draw a single line segment. - - ``object`` is a :class:`DebugRenderObject`. ``a`` / ``b`` are vectors - (``Vec2`` in 2D, ``Vec3`` in 3D). ``color`` is a :class:`DebugColor` - (HSLA storage; call ``.rgba`` to convert to RGB). - """ - ... - - -__all__ = ["DebugRenderBackend"] diff --git a/python/rapier-py-2d/python/rapier2d/_event_handler.py b/python/rapier-py-2d/python/rapier2d/_event_handler.py deleted file mode 100644 index 4aaedae32..000000000 --- a/python/rapier-py-2d/python/rapier2d/_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Python `Protocol` shims for the event-handler / physics-hooks APIs. - -These are duck-typing protocols (`typing.Protocol`); user classes don't need to -inherit from them, they just need to implement the right method names. - -Both protocols are dim/scalar-agnostic (they are exposed under both -`rapier.dim2` and `rapier.dim3`). -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable - -if TYPE_CHECKING: - # The exact types vary by (dim, scalar). We keep them as `Any` in the - # protocol so that user code doesn't lock itself to a single variant. - pass - - -@runtime_checkable -class EventHandler(Protocol): - """A receiver of collision / contact-force events. - - Assign an instance of any class implementing this protocol to - ``world.event_handler``. The methods are called from the physics solver - (with the GIL re-acquired) during ``world.step()``. - - Exceptions raised inside the callbacks are deferred by default and - re-raised after ``step()`` returns. Set ``world.event_error_policy = 'strict'`` - to short-circuit subsequent callbacks within the same step (the solver - still runs to completion since rapier does not support mid-step aborts, - but no further user callbacks are invoked). - - Note: ``bodies`` and ``colliders`` are passed as ``None`` to avoid borrow - conflicts with the in-flight ``step()`` — handles can still be obtained - from ``event`` / ``contact_pair`` and looked up against the world after - ``step()`` returns. - """ - - def handle_collision_event( - self, - bodies: Any, - colliders: Any, - event: Any, - contact_pair: Optional[Any], - ) -> None: - """Called when two colliders start or stop touching. - - ``event`` is a ``CollisionEvent`` (with ``.started`` / ``.stopped`` - flags). ``contact_pair`` is ``None`` for sensors. - """ - ... - - def handle_contact_force_event( - self, - dt: float, - bodies: Any, - colliders: Any, - contact_pair: Any, - total_force_magnitude: float, - ) -> None: - """Called when the contact-force magnitude exceeds the - ``contact_force_event_threshold`` of either collider in the pair. - - At least one of the involved colliders must have - ``ActiveEvents.CONTACT_FORCE_EVENTS`` set. - """ - ... - - -@runtime_checkable -class PhysicsHooks(Protocol): - """Custom collision / solver hooks invoked by the physics step. - - Assign an instance of any class implementing this protocol to - ``world.physics_hooks``. To receive callbacks, the relevant colliders must - have the appropriate ``ActiveHooks`` flags set (e.g. - ``ActiveHooks.FILTER_CONTACT_PAIRS``). - - All three methods are optional in the duck-typed sense — only define the - ones you care about. (But ``Protocol`` formally requires all three for type - checkers.) - """ - - def filter_contact_pair(self, ctx: Any) -> Any: - """Return ``SolverFlags`` to allow contact computation (``None`` to - completely discard the pair). The default behavior corresponds to - returning ``SolverFlags.COMPUTE_IMPULSES``. - - ``ctx`` is a ``PairFilterContext`` view (read-only). - """ - ... - - def filter_intersection_pair(self, ctx: Any) -> bool: - """Return ``True`` to allow intersection computation between the two - colliders in ``ctx`` (a ``PairFilterContext``).""" - ... - - def modify_solver_contacts(self, ctx: Any) -> None: - """Mutate ``ctx`` (a ``ContactModificationContext``) in place to - change the solver contacts before they're consumed by the solver. - - The context is **only valid for the duration of this call**: do not - keep references to it after the method returns. Doing so will raise - ``RuntimeError`` on access. - """ - ... - - -__all__ = ["EventHandler", "PhysicsHooks"] diff --git a/python/rapier-py-2d/python/rapier2d/_math_helpers.py b/python/rapier-py-2d/python/rapier2d/_math_helpers.py deleted file mode 100644 index d66ccb64b..000000000 --- a/python/rapier-py-2d/python/rapier2d/_math_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Pure-Python conveniences that don't need to live in Rust. - -These work on any dim/scalar variant since they only assume vector/point -arithmetic (`__mul__`, `__add__`, `__sub__`, scalar mul) and a `float`-like -angle for `wrap_to_pi`. -""" - -from __future__ import annotations - -import math - - -def lerp(a, b, t): - """Linear interpolation: `a*(1-t) + b*t`. - - Works for any `Vec2`/`Vec3`/`Point2`/`Point3` (and plain floats) as long - as `a` and `b` support `+`, `-`, and scalar `*`. - """ - return a + (b - a) * t - - -def wrap_to_pi(angle: float) -> float: - """Wrap an angle in radians to the half-open interval `(-pi, pi]`.""" - a = (angle + math.pi) % (2.0 * math.pi) - if a <= 0.0: - a += 2.0 * math.pi - return a - math.pi diff --git a/python/rapier-py-2d/python/rapier2d/_pickle_setup.py b/python/rapier-py-2d/python/rapier2d/_pickle_setup.py deleted file mode 100644 index 695ea1f3f..000000000 --- a/python/rapier-py-2d/python/rapier2d/_pickle_setup.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Make pyo3 pyclasses picklable from the correct dim/scalar namespace. - -The PyO3 macros that emit all `rapier` pyclasses set `module = "rapier"` on -every class declaration. Since the four cdylibs (`_rapier2d`, `_rapier2d_f64`, -`_rapier3d`, `_rapier3d_f64`) each define their own copy of (e.g.) -`MassProperties` but all four share `__module__ == "rapier"`, Python's -default pickling cannot disambiguate which flavor to restore: pickle would -fetch `rapier.MassProperties` (== the default 3D-f32 flavor) and reject -non-matching pyclass instances with `PicklingError: it's not the same object -as rapier.MassProperties`. - -To fix this without rewriting the macros, we patch `__module__` on every -exported class at import time so that each flavor's classes declare the -right module path: - - `rapier.dim2.MassProperties.__module__ = "rapier.dim2"` - - `rapier.dim2.f64.MassProperties.__module__ = "rapier.dim2.f64"` - - `rapier.dim3.MassProperties.__module__ = "rapier.dim3"` - - `rapier.dim3.f64.MassProperties.__module__ = "rapier.dim3.f64"` - -Pickle then resolves each instance to its actual flavor. -""" - -from __future__ import annotations - -import types -from typing import Any - - -def retag_module(mod: Any, target_module: str) -> None: - """Set `__module__` on every public class in `mod` to `target_module`.""" - for name in dir(mod): - if name.startswith("_"): - continue - cls = getattr(mod, name) - if not isinstance(cls, type): - continue - # Submodules can show up as types when they are nested package - # objects, skip those. - if isinstance(cls, types.ModuleType): - continue - try: - cls.__module__ = target_module - except (TypeError, AttributeError): - # Some pyclasses or built-in types may refuse mutation; skip - # them. We don't need pickle support for `bool`, `int`, etc. - pass diff --git a/python/rapier-py-2d/python/rapier2d/_rapier2d.pyi b/python/rapier-py-2d/python/rapier2d/_rapier2d.pyi deleted file mode 100644 index 619aaa1a4..000000000 --- a/python/rapier-py-2d/python/rapier2d/_rapier2d.pyi +++ /dev/null @@ -1,712 +0,0 @@ -"""Type stubs for `rapier._rapier2d` (2D, f32). - -Phase 02: math layer only. -""" - -from __future__ import annotations - -from typing import Any, Iterator, Tuple, Union - -import numpy as np - -__version__: str - -# ---- Exception tree ------------------------------------------------------- -class RapierError(Exception): ... -class InvalidHandle(RapierError): ... -class MeshConversionError(RapierError): ... -class UrdfError(RapierError): ... -class MjcfError(RapierError): ... -class SerializationError(RapierError): ... -class QueryFailure(RapierError): ... -class MeshLoaderError(RapierError): ... - -VectorLike = Union[ - "Vec2", - "Point2", - Tuple[float, float], - list, - "np.ndarray", -] -RotationLike = Union["Rotation2", float, Tuple[float], list, "np.ndarray"] -IsometryLike = Union["Isometry2", Tuple[VectorLike, RotationLike]] - -# ---- Vec2 ---------------------------------------------------------------- -class Vec2: - def __init__(self, x: float, y: float) -> None: ... - @staticmethod - def zeros() -> Vec2: ... - @staticmethod - def unit_x() -> Vec2: ... - @staticmethod - def unit_y() -> Vec2: ... - @staticmethod - def from_tuple(t: VectorLike) -> Vec2: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Vec2: ... - - x: float - y: float - - def dot(self, other: VectorLike) -> float: ... - def norm(self) -> float: ... - def norm_squared(self) -> float: ... - def normalize(self) -> Vec2: ... - def lerp(self, other: VectorLike, t: float) -> Vec2: ... - def to_tuple(self) -> Tuple[float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def bitwise_equal(self, other: Vec2) -> bool: ... - - def __add__(self, other: VectorLike) -> Vec2: ... - def __sub__(self, other: VectorLike) -> Vec2: ... - def __mul__(self, scalar: float) -> Vec2: ... - def __rmul__(self, scalar: float) -> Vec2: ... - def __truediv__(self, scalar: float) -> Vec2: ... - def __neg__(self) -> Vec2: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Point2 -------------------------------------------------------------- -class Point2: - def __init__(self, x: float, y: float) -> None: ... - @staticmethod - def origin() -> Point2: ... - @staticmethod - def from_tuple(t: VectorLike) -> Point2: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Point2: ... - - x: float - y: float - coords: Vec2 - - def to_tuple(self) -> Tuple[float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def bitwise_equal(self, other: Point2) -> bool: ... - - def __add__(self, v: VectorLike) -> Point2: ... - def __sub__(self, other: Any) -> Union[Point2, Vec2]: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Rotation2 ----------------------------------------------------------- -class Rotation2: - def __init__(self, angle: float) -> None: ... - @staticmethod - def identity() -> Rotation2: ... - @staticmethod - def from_angle(angle: float) -> Rotation2: ... - - angle: float - cos_angle: float - sin_angle: float - - def inverse(self) -> Rotation2: ... - def to_matrix(self) -> np.ndarray: ... - def transform_point(self, p: VectorLike) -> Point2: ... - def transform_vector(self, v: VectorLike) -> Vec2: ... - def slerp(self, other: RotationLike, t: float) -> Rotation2: ... - def bitwise_equal(self, other: Rotation2) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Isometry2 ----------------------------------------------------------- -class Isometry2: - def __init__( - self, - translation: VectorLike | None = None, - rotation: RotationLike | None = None, - ) -> None: ... - @staticmethod - def identity() -> Isometry2: ... - @staticmethod - def from_translation(x: float, y: float) -> Isometry2: ... - @staticmethod - def from_rotation(angle: float) -> Isometry2: ... - - @property - def translation(self) -> Vec2: ... - @property - def rotation(self) -> Rotation2: ... - - def inverse(self) -> Isometry2: ... - def transform_point(self, p: VectorLike) -> Point2: ... - def transform_vector(self, v: VectorLike) -> Vec2: ... - def to_matrix(self) -> np.ndarray: ... - def lerp_slerp(self, other: Isometry2, t: float) -> Isometry2: ... - def bitwise_equal(self, other: Isometry2) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Free helpers -------------------------------------------------------- -def rotation_from_angle(angle: float) -> Rotation2: ... - -# ===================================================================== -# Phase 03 — Dynamics (2D) -# ===================================================================== - -import enum - -class RigidBodyType(enum.IntEnum): - DYNAMIC: int - FIXED: int - KINEMATIC_VELOCITY_BASED: int - KINEMATIC_POSITION_BASED: int - -class CoefficientCombineRule(enum.IntEnum): - AVERAGE: int - MIN: int - MULTIPLY: int - MAX: int - CLAMPED_SUM: int - -# Note: no FrictionModel in 2D. - -class ColliderHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> ColliderHandle: ... - @staticmethod - def invalid() -> ColliderHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class ColliderSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class ImpulseJointSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class MultibodyJointSet: - def __init__(self) -> None: ... - -class RigidBodyHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> RigidBodyHandle: ... - @staticmethod - def invalid() -> RigidBodyHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class LockedAxes: - TRANSLATION_LOCKED_X: LockedAxes - TRANSLATION_LOCKED_Y: LockedAxes - TRANSLATION_LOCKED: LockedAxes - ROTATION_LOCKED: LockedAxes - ALL: LockedAxes - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> LockedAxes: ... - @staticmethod - def all_axes() -> LockedAxes: ... - @property - def bits(self) -> int: ... - -class RigidBodyActivation: - sleep_threshold: float - linear_threshold: float - angular_threshold: float - time_until_sleep: float - sleeping: bool - def __init__(self) -> None: ... - @staticmethod - def active() -> RigidBodyActivation: ... - @staticmethod - def inactive() -> RigidBodyActivation: ... - @staticmethod - def cannot_sleep() -> RigidBodyActivation: ... - def is_active(self) -> bool: ... - -class SpringCoefficients: - stiffness: float - damping: float - def __init__(self, stiffness: float = 30.0, damping: float = 5.0) -> None: ... - @staticmethod - def contact_defaults() -> SpringCoefficients: ... - @staticmethod - def joint_defaults() -> SpringCoefficients: ... - -class MassProperties: - def __init__(self, local_com: VectorLike | None = None, mass: float = 0.0, principal_inertia: float = 0.0) -> None: ... - @staticmethod - def zero() -> MassProperties: ... - @staticmethod - def from_ball(density: float, radius: float) -> MassProperties: ... - @staticmethod - def from_cuboid(density: float, half_extents: VectorLike) -> MassProperties: ... - @staticmethod - def from_capsule(density: float, a: VectorLike, b: VectorLike, radius: float) -> MassProperties: ... - @property - def mass(self) -> float: ... - @property - def inv_mass(self) -> float: ... - @property - def local_com(self) -> Point2: ... - @property - def principal_inertia(self) -> float: ... - -class RigidBodyAdditionalMassProps: - @staticmethod - def from_mass(mass: float) -> RigidBodyAdditionalMassProps: ... - @staticmethod - def from_mass_properties(mp: MassProperties) -> RigidBodyAdditionalMassProps: ... - -class RigidBodyDamping: - linear_damping: float - angular_damping: float - -class RigidBodyDominance: - group: int - -class RigidBodyCcd: - ccd_enabled: bool - soft_ccd_prediction: float - -class RigidBodyVelocity: - linvel: Vec2 - angvel: float - -class RigidBodyForces: - force: Vec2 - torque: float - gravity_scale: float - -class RigidBodyMassProps: - local_mprops: MassProperties - -class RigidBodyPosition: - position: Isometry2 - next_position: Isometry2 - -class IntegrationParameters: - dt: float - min_ccd_dt: float - warmstart_coefficient: float - length_unit: float - normalized_allowed_linear_error: float - normalized_max_corrective_velocity: float - normalized_prediction_distance: float - num_solver_iterations: int - num_internal_pgs_iterations: int - num_internal_stabilization_iterations: int - min_island_size: int - max_ccd_substeps: int - contact_softness: SpringCoefficients - def __init__(self) -> None: ... - @staticmethod - def default_params() -> IntegrationParameters: ... - -class IslandManager: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - def active_dynamic_set(self) -> list[RigidBodyHandle]: ... - -class CCDSolver: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def solve_ccd(self, *args: Any, **kwargs: Any) -> None: ... - -class RigidBodyBuilder: - def __init__(self, body_type: RigidBodyType = ...) -> None: ... - def translation(self, v: VectorLike) -> RigidBodyBuilder: ... - def rotation(self, angle: float) -> RigidBodyBuilder: ... - def position(self, p: IsometryLike) -> RigidBodyBuilder: ... - def linvel(self, v: VectorLike) -> RigidBodyBuilder: ... - def angvel(self, a: float) -> RigidBodyBuilder: ... - def linear_damping(self, f: float) -> RigidBodyBuilder: ... - def angular_damping(self, f: float) -> RigidBodyBuilder: ... - def gravity_scale(self, f: float) -> RigidBodyBuilder: ... - def can_sleep(self, b: bool) -> RigidBodyBuilder: ... - def sleeping(self, b: bool) -> RigidBodyBuilder: ... - def ccd_enabled(self, b: bool) -> RigidBodyBuilder: ... - def soft_ccd_prediction(self, f: float) -> RigidBodyBuilder: ... - def dominance_group(self, g: int) -> RigidBodyBuilder: ... - def additional_mass(self, m: float) -> RigidBodyBuilder: ... - def locked_axes(self, la: LockedAxes) -> RigidBodyBuilder: ... - def build(self) -> RigidBody: ... - -class RigidBody: - position: Isometry2 - translation: Vec2 - rotation: Rotation2 - linvel: Vec2 - angvel: float - user_data: int - body_type: RigidBodyType - locked_axes: LockedAxes - @staticmethod - def dynamic(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def fixed(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_velocity_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_position_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def new_body(body_type: RigidBodyType, **kwargs: Any) -> RigidBodyBuilder: ... - def add_force(self, force: VectorLike, wake_up: bool = True) -> None: ... - def add_torque(self, torque: float, wake_up: bool = True) -> None: ... - def apply_impulse(self, impulse: VectorLike, wake_up: bool = True) -> None: ... - def apply_torque_impulse(self, torque_impulse: float, wake_up: bool = True) -> None: ... - def reset_forces(self, wake_up: bool = True) -> None: ... - def wake_up(self, strong: bool = True) -> None: ... - def sleep(self) -> None: ... - def kinetic_energy(self) -> float: ... - def recompute_mass_properties_from_colliders(self, colliders: ColliderSet) -> None: ... - def set_additional_mass(self, mass: float, wake_up: bool = True) -> None: ... - -class RigidBodySet: - def __init__(self) -> None: ... - def insert(self, body: RigidBody | RigidBodyBuilder) -> RigidBodyHandle: ... - def remove(self, handle: RigidBodyHandle, islands: IslandManager, colliders: ColliderSet, impulse_joints: ImpulseJointSet, multibody_joints: MultibodyJointSet, remove_attached_colliders: bool = True) -> RigidBody | None: ... - def get(self, handle: RigidBodyHandle) -> RigidBody | None: ... - def __getitem__(self, handle: RigidBodyHandle) -> RigidBody: ... - def __contains__(self, handle: RigidBodyHandle) -> bool: ... - def __len__(self) -> int: ... - -# ---- events / hooks (phase 07) ------------------------------------------- - -class SolverFlags: - COMPUTE_IMPULSES: SolverFlags - EMPTY: SolverFlags - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> SolverFlags: ... - @property - def bits(self) -> int: ... - def contains(self, other: SolverFlags) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: SolverFlags) -> SolverFlags: ... - def __and__(self, other: SolverFlags) -> SolverFlags: ... - def __xor__(self, other: SolverFlags) -> SolverFlags: ... - def __invert__(self) -> SolverFlags: ... - -class PairFilterContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - -class ContactModificationContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - @property - def local_n1(self) -> Vec2: ... - @property - def local_n2(self) -> Vec2: ... - normal: Vec2 - user_data: int - @property - def solver_contacts(self) -> list[SolverContact]: ... - def clear_solver_contacts(self) -> None: ... - def num_solver_contacts(self) -> int: ... - def remove_solver_contact(self, i: int) -> None: ... - def update_as_oneway_platform(self, allowed_local_n1: Vec2 | tuple[float, float], angle: float) -> None: ... - -class ChannelEventCollector: - def __init__(self) -> None: ... - def drain_collision_events(self) -> list[CollisionEvent]: ... - def drain_contact_force_events(self) -> list[ContactForceEvent]: ... - def clear(self) -> None: ... - def __len__(self) -> int: ... - -# ---- controllers (phase 08) ---------------------------------------------- -# 2D variant: no DynamicRayCastVehicleController / Wheel / RayCastInfo (3D-only upstream). - -from typing import Callable, Literal - -class CharacterLength: - kind: Literal["absolute", "relative"] - value: float - def __init__(self, value: float = 0.0, kind: str = "absolute") -> None: ... - @staticmethod - def absolute(value: float) -> CharacterLength: ... - @staticmethod - def relative(value: float) -> CharacterLength: ... - -class CharacterAutostep: - max_height: CharacterLength - min_width: CharacterLength - include_dynamic_bodies: bool - def __init__( - self, - max_height: CharacterLength | float | None = None, - min_width: CharacterLength | float | None = None, - include_dynamic_bodies: bool = True, - ) -> None: ... - @staticmethod - def disabled() -> None: ... - -class EffectiveCharacterMovement: - @property - def translation(self) -> Vec2: ... - @property - def grounded(self) -> bool: ... - @property - def is_sliding_down_slope(self) -> bool: ... - -class CharacterCollision: - @property - def handle(self) -> ColliderHandle: ... - @property - def character_pos(self) -> Isometry2: ... - @property - def translation_applied(self) -> Vec2: ... - @property - def translation_remaining(self) -> Vec2: ... - @property - def toi(self) -> ShapeCastHit: ... - -class KinematicCharacterController: - up: Vec2 - offset: CharacterLength - slide: bool - autostep: CharacterAutostep | None - max_slope_climb_angle: float - min_slope_slide_angle: float - snap_to_ground: CharacterLength | None - normal_nudge_factor: float - def __init__( - self, - *, - up: Vec2 | tuple[float, float] | None = None, - offset: CharacterLength | float | None = None, - slide: bool | None = None, - autostep: CharacterAutostep | None = None, - max_slope_climb_angle: float | None = None, - min_slope_slide_angle: float | None = None, - snap_to_ground: CharacterLength | float | None = None, - normal_nudge_factor: float | None = None, - ) -> None: ... - def move_shape( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - shape: SharedShape, - shape_pos: Isometry2, - desired_translation: Vec2 | tuple[float, float], - filter: QueryFilter | None = None, - events_callback: Callable[[CharacterCollision], None] | None = None, - ) -> EffectiveCharacterMovement: ... - def solve_character_collision_impulses( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - character_shape: SharedShape, - character_pos: Isometry2, - character_mass: float, - collisions: list[CharacterCollision], - filter: QueryFilter | None = None, - ) -> None: ... - -class AxesMask: - LIN_X: AxesMask - LIN_Y: AxesMask - ANG_Z: AxesMask - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> AxesMask: ... - @staticmethod - def all() -> AxesMask: ... - @staticmethod - def all_axes() -> AxesMask: ... - @property - def bits(self) -> int: ... - def contains(self, other: AxesMask) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: AxesMask) -> AxesMask: ... - def __and__(self, other: AxesMask) -> AxesMask: ... - def __xor__(self, other: AxesMask) -> AxesMask: ... - def __invert__(self) -> AxesMask: ... - def __sub__(self, other: AxesMask) -> AxesMask: ... - def __contains__(self, other: AxesMask) -> bool: ... - -class PdErrors: - linear: Vec2 - angular: float - def __init__( - self, - linear: Vec2 | tuple[float, float] | None = None, - angular: float | None = None, - ) -> None: ... - -class PidCorrection: - @property - def linear(self) -> Vec2: ... - @property - def angular(self) -> float: ... - -class PdController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec2 | tuple[float, float] | float | None = None, - Kd: Vec2 | tuple[float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_kd(self, v: Vec2 | tuple[float, float]) -> None: ... - def rigid_body_correction(self, body: RigidBody, target_pose: Isometry2) -> PidCorrection: ... - def correction(self, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -class PidController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec2 | tuple[float, float] | float | None = None, - Ki: Vec2 | tuple[float, float] | float | None = None, - Kd: Vec2 | tuple[float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_ki(self, v: Vec2 | tuple[float, float]) -> None: ... - def set_axes_kd(self, v: Vec2 | tuple[float, float]) -> None: ... - def reset(self) -> None: ... - def rigid_body_correction(self, dt: float, body: RigidBody, target_pose: Isometry2) -> PidCorrection: ... - def position_correction(self, dt: float, position: Isometry2, target_position: Isometry2) -> PidCorrection: ... - def update(self, dt: float, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -# ---- debug-render (phase 10) --------------------------------------------- - -class DebugRenderObject: - kind: int - RIGID_BODY: DebugRenderObject - COLLIDER: DebugRenderObject - COLLIDER_AABB: DebugRenderObject - IMPULSE_JOINT: DebugRenderObject - MULTIBODY_JOINT: DebugRenderObject - CONTACT_PAIR: DebugRenderObject - -class DebugColor: - def __init__(self, h: float = 0.0, s: float = 0.0, l: float = 0.0, a: float = 1.0) -> None: ... - @staticmethod - def from_hsla(h: float, s: float, l: float, a: float) -> DebugColor: ... - @staticmethod - def from_rgba(r: float, g: float, b: float, a: float) -> DebugColor: ... - @property - def hsla(self) -> list[float]: ... - @property - def rgba(self) -> list[float]: ... - -class DebugRenderMode: - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> DebugRenderMode: ... - @staticmethod - def default() -> DebugRenderMode: ... - @staticmethod - def all() -> DebugRenderMode: ... - COLLIDER_SHAPES: DebugRenderMode - RIGID_BODY_AXES: DebugRenderMode - MULTIBODY_JOINTS: DebugRenderMode - IMPULSE_JOINTS: DebugRenderMode - JOINTS: DebugRenderMode - SOLVER_CONTACTS: DebugRenderMode - CONTACTS: DebugRenderMode - COLLIDER_AABBS: DebugRenderMode - EMPTY: DebugRenderMode - @property - def bits(self) -> int: ... - def contains(self, other: DebugRenderMode) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __and__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __xor__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __invert__(self) -> DebugRenderMode: ... - def __bool__(self) -> bool: ... - -class DebugRenderStyle: - def __init__(self) -> None: ... - @staticmethod - def default() -> DebugRenderStyle: ... - subdivisions: int - border_subdivisions: int - collider_dynamic_color: DebugColor - collider_fixed_color: DebugColor - collider_kinematic_color: DebugColor - collider_parentless_color: DebugColor - impulse_joint_anchor_color: DebugColor - impulse_joint_separation_color: DebugColor - multibody_joint_anchor_color: DebugColor - multibody_joint_separation_color: DebugColor - sleep_color_multiplier: DebugColor - disabled_color_multiplier: DebugColor - rigid_body_axes_length: float - contact_depth_color: DebugColor - contact_normal_color: DebugColor - contact_normal_length: float - collider_aabb_color: DebugColor - -class DebugLineCollector: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def lines(self) -> np.ndarray: ... - def colors(self) -> np.ndarray: ... - def objects(self) -> np.ndarray: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[tuple[DebugRenderObject, Vec2, Vec2, DebugColor]]: ... - -class DebugRenderPipeline: - mode: DebugRenderMode - style: DebugRenderStyle - def __init__( - self, - mode: DebugRenderMode | None = None, - style: DebugRenderStyle | None = None, - ) -> None: ... - def render( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - backend: Any, - ) -> None: ... - def render_to_arrays( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... diff --git a/python/rapier-py-2d/python/rapier2d/math.py b/python/rapier-py-2d/python/rapier2d/math.py deleted file mode 100644 index 1fb1020e9..000000000 --- a/python/rapier-py-2d/python/rapier2d/math.py +++ /dev/null @@ -1,15 +0,0 @@ -"""`rapier.dim2.math` — math helpers (2D, f32).""" - -from __future__ import annotations - -from . import _rapier2d as _ext -from ._math_helpers import lerp, wrap_to_pi - -rotation_from_angle = _ext.rotation_from_angle - - -def linear_interp(a, b, t): - return lerp(a, b, t) - - -__all__ = ["lerp", "linear_interp", "rotation_from_angle", "wrap_to_pi"] diff --git a/python/rapier-py-2d/python/rapier2d/py.typed b/python/rapier-py-2d/python/rapier2d/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/rapier-py-2d/src/lib.rs b/python/rapier-py-2d/src/lib.rs deleted file mode 100644 index 1279dd09a..000000000 --- a/python/rapier-py-2d/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! `_rapier2d` — PyO3 extension module for rapier2d (2D, f32). - -use pyo3::prelude::*; - -use rapier2d as rapier; - -rapier_py_core::define_conv_types!(Real = f32, DIM = 2); -rapier_py_core::define_math_types!(DIM = 2); -rapier_py_core::define_geometry_types!(DIM = 2); -rapier_py_core::define_joints_types!(DIM = 2); -rapier_py_core::define_dynamics_types!(DIM = 2); -rapier_py_core::define_pipeline_types!(DIM = 2); -rapier_py_core::define_events_hooks_types!(DIM = 2); -rapier_py_core::define_controllers_types!(DIM = 2); - -// debug-render adapter. -rapier_py_core::define_debug_render_types!(DIM = 2); - -// must run AFTER every other `define_*_types!` because it adds -// additional `#[pymethods]` blocks on the existing pyclasses. -rapier_py_core::define_serde_types!(DIM = 2); - -const RAPIER_PY_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[pymodule] -fn _rapier2d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - rapier_py_core::errors::register_errors(py, m)?; - register_math(py, m)?; - register_geometry(py, m)?; - register_joints(py, m)?; - register_dynamics(py, m)?; - register_pipeline(py, m)?; - register_events_hooks(py, m)?; - register_controllers(py, m)?; - register_debug_render(py, m)?; - m.add("__version__", RAPIER_PY_VERSION)?; - Ok(()) -} diff --git a/python/rapier-py-3d-f64/Cargo.toml b/python/rapier-py-3d-f64/Cargo.toml deleted file mode 100644 index 05260995b..000000000 --- a/python/rapier-py-3d-f64/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "rapier-py-3d-f64" -version.workspace = true -authors.workspace = true -description = "PyO3 extension module wrapping rapier3d-f64." -homepage.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -edition.workspace = true -publish = false - -[lib] -name = "_rapier3d_f64" -crate-type = ["cdylib"] -path = "src/lib.rs" - -[features] -# Phase 11 — opt-in determinism build (see `rapier-py-3d` for rationale). -determinism = ["rapier3d-f64/enhanced-determinism"] - -[dependencies] -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } -rapier-py-core = { path = "../rapier-py-core" } -# NB: the f64 engine crate has no `simd-stable` feature (SIMD via `wide` is -# f32-only), so this binding can't enable it. -rapier3d-f64 = { path = "../../crates/rapier3d-f64", features = ["serde-serialize", "debug-render"] } -# Phase 11 — Serialization, snapshots. -serde = { workspace = true, features = ["derive", "std"] } -bincode = { workspace = true } -serde_json = { workspace = true } diff --git a/python/rapier-py-3d-f64/README.md b/python/rapier-py-3d-f64/README.md deleted file mode 100644 index b2c07a7e3..000000000 --- a/python/rapier-py-3d-f64/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# rapier3d-f64 - -Python bindings for [Rapier](https://rapier.rs) — a fast, deterministic -physics engine written in Rust. This package provides the **3D, f64** -build. - -```bash -pip install rapier3d-f64 -``` - -```python -import rapier3d_f64 as rp - -bodies = rp.RigidBodySet() -colliders = rp.ColliderSet() -body = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 5.0, 0.0))) -``` - -## Related packages - -| Package | Dimension | Scalar | -| -------------- | --------- | ------ | -| `rapier2d` | 2D | f32 | -| `rapier3d` | 3D | f32 | -| `rapier2d-f64` | 2D | f64 | -| `rapier3d-f64` | 3D | f64 | - -All four share an identical API differing only in dimension and scalar type. - -## License - -Apache-2.0 diff --git a/python/rapier-py-3d-f64/pyproject.toml b/python/rapier-py-3d-f64/pyproject.toml deleted file mode 100644 index 514f01e9c..000000000 --- a/python/rapier-py-3d-f64/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -[build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" - -[project] -name = "rapier3d-f64" -dynamic = ["version"] -description = "Python bindings for Rapier (3D, f64) — a fast, deterministic physics engine." -readme = "README.md" -license = { text = "Apache-2.0" } -authors = [{ name = "Sébastien Crozet", email = "sebcrozet@dimforge.com" }] -keywords = ["physics", "dynamics", "rigid-body", "simulation", "rapier", "3d", "f64"] -classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Rust", - "Topic :: Games/Entertainment :: Simulation", - "Topic :: Scientific/Engineering :: Physics", -] -requires-python = ">=3.9" - -[project.urls] -Homepage = "https://rapier.rs" -Documentation = "https://rapier.rs/docs/" -Repository = "https://github.com/dimforge/rapier" -Issues = "https://github.com/dimforge/rapier/issues" - -[project.optional-dependencies] -numpy = ["numpy>=1.21"] -dev = ["pytest>=7", "hypothesis>=6", "numpy>=1.21", "ruff>=0.5", "mypy>=1.10"] - -[tool.maturin] -manifest-path = "Cargo.toml" -module-name = "rapier3d_f64._rapier3d_f64" -python-source = "python" -features = ["pyo3/extension-module"] diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/__init__.py b/python/rapier-py-3d-f64/python/rapier3d_f64/__init__.py deleted file mode 100644 index 9d088a466..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/__init__.py +++ /dev/null @@ -1,352 +0,0 @@ -"""3D, f64-precision bindings (`rapier.dim3.f64`).""" - -from __future__ import annotations - -from . import _rapier3d_f64 as _ext -from ._rapier3d_f64 import ( # noqa: F401 - # ---- math ---- - Vec3, - Point3, - Rotation3, - Quaternion, - Isometry3, - AngVector3, - rotation_from_angle, - # ---- errors ---- - RapierError, - InvalidHandle, - MeshConversionError, - UrdfError, - MjcfError, - SerializationError, - QueryFailure, - MeshLoaderError, - # ---- dynamics ---- - RigidBody, - RigidBodyBuilder, - RigidBodyHandle, - RigidBodySet, - RigidBodyType, - LockedAxes, - RigidBodyActivation, - RigidBodyDamping, - RigidBodyDominance, - RigidBodyCcd, - RigidBodyVelocity, - RigidBodyForces, - RigidBodyMassProps, - RigidBodyPosition, - RigidBodyAdditionalMassProps, - MassProperties, - IntegrationParameters, - SpringCoefficients, - CoefficientCombineRule, - FrictionModel, - IslandManager, - CCDSolver, - ImpulseJointSet, - MultibodyJointSet, - # ---- joints ---- - JointEnabled, - MotorModel, - JointLimits, - JointMotor, - JointAxesMask, - JointAxis, - ImpulseJointHandle, - MultibodyJointHandle, - MultibodyIndex, - MultibodyLinkId, - InverseKinematicsOption, - FixedJoint, - FixedJointBuilder, - RevoluteJoint, - RevoluteJointBuilder, - PrismaticJoint, - PrismaticJointBuilder, - SphericalJoint, - SphericalJointBuilder, - RopeJoint, - RopeJointBuilder, - SpringJoint, - SpringJointBuilder, - GenericJoint, - GenericJointBuilder, - ImpulseJoint, - MultibodyLink, - Multibody, - # ---- geometry ---- - Collider, - ColliderBuilder, - ColliderHandle, - ColliderSet, - ColliderType, - ColliderEnabled, - ColliderMaterial, - ColliderFlags, - ColliderParent, - ColliderPosition, - ColliderMassProps, - ShapeType, - SharedShape, - Ball, - Cuboid, - Capsule, - Triangle, - TriMesh, - HeightField, - Compound, - Cylinder, - Cone, - ConvexPolyhedron, - Aabb, - BoundingSphere, - MeshConverter, - TriMeshFlags, - ActiveEvents, - ActiveHooks, - ActiveCollisionTypes, - Group, - InteractionTestMode, - InteractionGroups, - CollisionEventFlags, - CollisionEvent, - ContactForceEvent, - ContactData, - ContactManifold, - ContactManifoldData, - ContactPair, - IntersectionPair, - ColliderPair, - BroadPhasePairEvent, - BroadPhaseBvh, - BvhOptimizationStrategy, - NarrowPhase, - SolverContact, - # ---- pipeline ---- - PhysicsPipeline, - CollisionPipeline, - PhysicsWorld, - QueryPipeline, - QueryFilter, - QueryFilterFlags, - Ray, - RayIntersection, - PointProjection, - ShapeCastHit, - ShapeCastOptions, - ShapeCastStatus, - FeatureId, - NonlinearRigidMotion, - Counters, - StagesCounters, - CollisionDetectionCounters, - SolverCounters, - CCDCounters, - # ---- events / hooks ---- - SolverFlags, - PairFilterContext, - ContactModificationContext, - ChannelEventCollector, - # ---- controllers ---- - AxesMask, - CharacterLength, - CharacterAutostep, - CharacterCollision, - EffectiveCharacterMovement, - KinematicCharacterController, - PdErrors, - PidCorrection, - PdController, - PidController, - WheelTuning, - RayCastInfo, - Wheel, - DynamicRayCastVehicleController, - # ---- debug-render ---- - DebugRenderPipeline, - DebugRenderStyle, - DebugRenderMode, - DebugRenderObject, - DebugColor, - DebugLineCollector, -) - -from ._event_handler import EventHandler, PhysicsHooks # noqa: F401 -from ._debug_render import DebugRenderBackend # noqa: F401 - -# retag pyclasses for pickle. -from ._pickle_setup import retag_module as _retag - -_retag(_ext, "rapier3d_f64") - -__version__ = _ext.__version__ - -__all__ = [ - "Vec3", - "Point3", - "Rotation3", - "Quaternion", - "Isometry3", - "AngVector3", - "rotation_from_angle", - "RapierError", - "InvalidHandle", - "MeshConversionError", - "UrdfError", - "MjcfError", - "SerializationError", - "QueryFailure", - "MeshLoaderError", - "RigidBody", - "RigidBodyBuilder", - "RigidBodyHandle", - "RigidBodySet", - "RigidBodyType", - "LockedAxes", - "RigidBodyActivation", - "RigidBodyDamping", - "RigidBodyDominance", - "RigidBodyCcd", - "RigidBodyVelocity", - "RigidBodyForces", - "RigidBodyMassProps", - "RigidBodyPosition", - "RigidBodyAdditionalMassProps", - "MassProperties", - "IntegrationParameters", - "SpringCoefficients", - "CoefficientCombineRule", - "FrictionModel", - "IslandManager", - "CCDSolver", - "ImpulseJointSet", - "MultibodyJointSet", - # ---- joints ---- - "JointEnabled", - "MotorModel", - "JointLimits", - "JointMotor", - "JointAxesMask", - "JointAxis", - "ImpulseJointHandle", - "MultibodyJointHandle", - "MultibodyIndex", - "MultibodyLinkId", - "InverseKinematicsOption", - "FixedJoint", - "FixedJointBuilder", - "RevoluteJoint", - "RevoluteJointBuilder", - "PrismaticJoint", - "PrismaticJointBuilder", - "SphericalJoint", - "SphericalJointBuilder", - "RopeJoint", - "RopeJointBuilder", - "SpringJoint", - "SpringJointBuilder", - "GenericJoint", - "GenericJointBuilder", - "ImpulseJoint", - "MultibodyLink", - "Multibody", - "Collider", - "ColliderBuilder", - "ColliderHandle", - "ColliderSet", - "ColliderType", - "ColliderEnabled", - "ColliderMaterial", - "ColliderFlags", - "ColliderParent", - "ColliderPosition", - "ColliderMassProps", - "ShapeType", - "SharedShape", - "Ball", - "Cuboid", - "Capsule", - "Triangle", - "TriMesh", - "HeightField", - "Compound", - "Cylinder", - "Cone", - "ConvexPolyhedron", - "Aabb", - "BoundingSphere", - "MeshConverter", - "TriMeshFlags", - "ActiveEvents", - "ActiveHooks", - "ActiveCollisionTypes", - "Group", - "InteractionTestMode", - "InteractionGroups", - "CollisionEventFlags", - "CollisionEvent", - "ContactForceEvent", - "ContactData", - "ContactManifold", - "ContactManifoldData", - "ContactPair", - "IntersectionPair", - "ColliderPair", - "BroadPhasePairEvent", - "BroadPhaseBvh", - "BvhOptimizationStrategy", - "NarrowPhase", - "SolverContact", - # ---- pipeline ---- - "PhysicsPipeline", - "CollisionPipeline", - "PhysicsWorld", - "QueryPipeline", - "QueryFilter", - "QueryFilterFlags", - "Ray", - "RayIntersection", - "PointProjection", - "ShapeCastHit", - "ShapeCastOptions", - "ShapeCastStatus", - "FeatureId", - "NonlinearRigidMotion", - "Counters", - "StagesCounters", - "CollisionDetectionCounters", - "SolverCounters", - "CCDCounters", - # ---- events / hooks ---- - "SolverFlags", - "PairFilterContext", - "ContactModificationContext", - "ChannelEventCollector", - "EventHandler", - "PhysicsHooks", - # ---- controllers ---- - "AxesMask", - "CharacterLength", - "CharacterAutostep", - "CharacterCollision", - "EffectiveCharacterMovement", - "KinematicCharacterController", - "PdErrors", - "PidCorrection", - "PdController", - "PidController", - "WheelTuning", - "RayCastInfo", - "Wheel", - "DynamicRayCastVehicleController", - # ---- debug-render ---- - "DebugRenderPipeline", - "DebugRenderStyle", - "DebugRenderMode", - "DebugRenderObject", - "DebugColor", - "DebugLineCollector", - "DebugRenderBackend", - "__version__", -] diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/_debug_render.py b/python/rapier-py-3d-f64/python/rapier3d_f64/_debug_render.py deleted file mode 100644 index d833968d3..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/_debug_render.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Python `Protocol` shim for the debug-render backend API. - -This is a duck-typing protocol (`typing.Protocol`); user classes don't need to -inherit from it — they just need to implement the right method names. - -The Rust ``DebugRenderPipeline.render(..., backend=obj)`` accepts any of: - -- the built-in ``DebugLineCollector`` (fast path; pure-Rust buffer), -- a Python object exposing ``draw_line(object, a, b, color) -> None``. - -Calling into Python per line is significantly slower than the -``DebugLineCollector`` fast path; prefer the collector for anything beyond -prototyping or pure-Python visualization integrations. -""" - -from __future__ import annotations - -from typing import Any, Protocol, runtime_checkable - - -@runtime_checkable -class DebugRenderBackend(Protocol): - """Receiver of debug-render line draw calls. - - Implement this protocol on a class and pass an instance to - :meth:`DebugRenderPipeline.render`. Each call to ``draw_line`` receives: - - * ``object`` — a :class:`DebugRenderObject` describing what is being - rendered (collider shape, AABB, joint anchor, contact, ...). - * ``a``, ``b`` — line endpoints as ``Vec2``/``Vec3`` (or whatever the - cdylib's vector type is for the current ``(dim, scalar)``). - * ``color`` — a :class:`DebugColor` (HSLA tuple; use ``.rgba`` for RGBA - conversion if your downstream renderer expects RGB). - - Exceptions raised inside ``draw_line`` are stashed and re-raised after - ``render()`` returns — subsequent ``draw_line`` calls within the same - ``render`` invocation become no-ops once the slot is filled. - """ - - def draw_line( - self, - object: Any, - a: Any, - b: Any, - color: Any, - ) -> None: - """Draw a single line segment. - - ``object`` is a :class:`DebugRenderObject`. ``a`` / ``b`` are vectors - (``Vec2`` in 2D, ``Vec3`` in 3D). ``color`` is a :class:`DebugColor` - (HSLA storage; call ``.rgba`` to convert to RGB). - """ - ... - - -__all__ = ["DebugRenderBackend"] diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/_event_handler.py b/python/rapier-py-3d-f64/python/rapier3d_f64/_event_handler.py deleted file mode 100644 index 4aaedae32..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Python `Protocol` shims for the event-handler / physics-hooks APIs. - -These are duck-typing protocols (`typing.Protocol`); user classes don't need to -inherit from them, they just need to implement the right method names. - -Both protocols are dim/scalar-agnostic (they are exposed under both -`rapier.dim2` and `rapier.dim3`). -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable - -if TYPE_CHECKING: - # The exact types vary by (dim, scalar). We keep them as `Any` in the - # protocol so that user code doesn't lock itself to a single variant. - pass - - -@runtime_checkable -class EventHandler(Protocol): - """A receiver of collision / contact-force events. - - Assign an instance of any class implementing this protocol to - ``world.event_handler``. The methods are called from the physics solver - (with the GIL re-acquired) during ``world.step()``. - - Exceptions raised inside the callbacks are deferred by default and - re-raised after ``step()`` returns. Set ``world.event_error_policy = 'strict'`` - to short-circuit subsequent callbacks within the same step (the solver - still runs to completion since rapier does not support mid-step aborts, - but no further user callbacks are invoked). - - Note: ``bodies`` and ``colliders`` are passed as ``None`` to avoid borrow - conflicts with the in-flight ``step()`` — handles can still be obtained - from ``event`` / ``contact_pair`` and looked up against the world after - ``step()`` returns. - """ - - def handle_collision_event( - self, - bodies: Any, - colliders: Any, - event: Any, - contact_pair: Optional[Any], - ) -> None: - """Called when two colliders start or stop touching. - - ``event`` is a ``CollisionEvent`` (with ``.started`` / ``.stopped`` - flags). ``contact_pair`` is ``None`` for sensors. - """ - ... - - def handle_contact_force_event( - self, - dt: float, - bodies: Any, - colliders: Any, - contact_pair: Any, - total_force_magnitude: float, - ) -> None: - """Called when the contact-force magnitude exceeds the - ``contact_force_event_threshold`` of either collider in the pair. - - At least one of the involved colliders must have - ``ActiveEvents.CONTACT_FORCE_EVENTS`` set. - """ - ... - - -@runtime_checkable -class PhysicsHooks(Protocol): - """Custom collision / solver hooks invoked by the physics step. - - Assign an instance of any class implementing this protocol to - ``world.physics_hooks``. To receive callbacks, the relevant colliders must - have the appropriate ``ActiveHooks`` flags set (e.g. - ``ActiveHooks.FILTER_CONTACT_PAIRS``). - - All three methods are optional in the duck-typed sense — only define the - ones you care about. (But ``Protocol`` formally requires all three for type - checkers.) - """ - - def filter_contact_pair(self, ctx: Any) -> Any: - """Return ``SolverFlags`` to allow contact computation (``None`` to - completely discard the pair). The default behavior corresponds to - returning ``SolverFlags.COMPUTE_IMPULSES``. - - ``ctx`` is a ``PairFilterContext`` view (read-only). - """ - ... - - def filter_intersection_pair(self, ctx: Any) -> bool: - """Return ``True`` to allow intersection computation between the two - colliders in ``ctx`` (a ``PairFilterContext``).""" - ... - - def modify_solver_contacts(self, ctx: Any) -> None: - """Mutate ``ctx`` (a ``ContactModificationContext``) in place to - change the solver contacts before they're consumed by the solver. - - The context is **only valid for the duration of this call**: do not - keep references to it after the method returns. Doing so will raise - ``RuntimeError`` on access. - """ - ... - - -__all__ = ["EventHandler", "PhysicsHooks"] diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/_math_helpers.py b/python/rapier-py-3d-f64/python/rapier3d_f64/_math_helpers.py deleted file mode 100644 index d66ccb64b..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/_math_helpers.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Pure-Python conveniences that don't need to live in Rust. - -These work on any dim/scalar variant since they only assume vector/point -arithmetic (`__mul__`, `__add__`, `__sub__`, scalar mul) and a `float`-like -angle for `wrap_to_pi`. -""" - -from __future__ import annotations - -import math - - -def lerp(a, b, t): - """Linear interpolation: `a*(1-t) + b*t`. - - Works for any `Vec2`/`Vec3`/`Point2`/`Point3` (and plain floats) as long - as `a` and `b` support `+`, `-`, and scalar `*`. - """ - return a + (b - a) * t - - -def wrap_to_pi(angle: float) -> float: - """Wrap an angle in radians to the half-open interval `(-pi, pi]`.""" - a = (angle + math.pi) % (2.0 * math.pi) - if a <= 0.0: - a += 2.0 * math.pi - return a - math.pi diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/_pickle_setup.py b/python/rapier-py-3d-f64/python/rapier3d_f64/_pickle_setup.py deleted file mode 100644 index 695ea1f3f..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/_pickle_setup.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Make pyo3 pyclasses picklable from the correct dim/scalar namespace. - -The PyO3 macros that emit all `rapier` pyclasses set `module = "rapier"` on -every class declaration. Since the four cdylibs (`_rapier2d`, `_rapier2d_f64`, -`_rapier3d`, `_rapier3d_f64`) each define their own copy of (e.g.) -`MassProperties` but all four share `__module__ == "rapier"`, Python's -default pickling cannot disambiguate which flavor to restore: pickle would -fetch `rapier.MassProperties` (== the default 3D-f32 flavor) and reject -non-matching pyclass instances with `PicklingError: it's not the same object -as rapier.MassProperties`. - -To fix this without rewriting the macros, we patch `__module__` on every -exported class at import time so that each flavor's classes declare the -right module path: - - `rapier.dim2.MassProperties.__module__ = "rapier.dim2"` - - `rapier.dim2.f64.MassProperties.__module__ = "rapier.dim2.f64"` - - `rapier.dim3.MassProperties.__module__ = "rapier.dim3"` - - `rapier.dim3.f64.MassProperties.__module__ = "rapier.dim3.f64"` - -Pickle then resolves each instance to its actual flavor. -""" - -from __future__ import annotations - -import types -from typing import Any - - -def retag_module(mod: Any, target_module: str) -> None: - """Set `__module__` on every public class in `mod` to `target_module`.""" - for name in dir(mod): - if name.startswith("_"): - continue - cls = getattr(mod, name) - if not isinstance(cls, type): - continue - # Submodules can show up as types when they are nested package - # objects, skip those. - if isinstance(cls, types.ModuleType): - continue - try: - cls.__module__ = target_module - except (TypeError, AttributeError): - # Some pyclasses or built-in types may refuse mutation; skip - # them. We don't need pickle support for `bool`, `int`, etc. - pass diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/_rapier3d_f64.pyi b/python/rapier-py-3d-f64/python/rapier3d_f64/_rapier3d_f64.pyi deleted file mode 100644 index 72ce755bd..000000000 --- a/python/rapier-py-3d-f64/python/rapier3d_f64/_rapier3d_f64.pyi +++ /dev/null @@ -1,975 +0,0 @@ -"""Type stubs for `rapier._rapier3d` (3D, f32). - -Phase 02: math layer only. -""" - -from __future__ import annotations - -from typing import Any, Iterator, Tuple, Union - -import numpy as np - -__version__: str - -# ---- Exception tree ------------------------------------------------------- -class RapierError(Exception): ... -class InvalidHandle(RapierError): ... -class MeshConversionError(RapierError): ... -class UrdfError(RapierError): ... -class MjcfError(RapierError): ... -class SerializationError(RapierError): ... -class QueryFailure(RapierError): ... -class MeshLoaderError(RapierError): ... - -# Anything that can be coerced to a 3-vector. -VectorLike = Union[ - "Vec3", - "Point3", - Tuple[float, float, float], - list, - "np.ndarray", -] -# Anything that can be coerced to a 4-tuple (x, y, z, w) → unit quaternion. -RotationLike = Union[ - "Rotation3", - Tuple[float, float, float, float], - list, - "np.ndarray", -] -IsometryLike = Union["Isometry3", Tuple[VectorLike, RotationLike]] - -# ---- Vec3 ---------------------------------------------------------------- -class Vec3: - def __init__(self, x: float, y: float, z: float) -> None: ... - @staticmethod - def zeros() -> Vec3: ... - @staticmethod - def unit_x() -> Vec3: ... - @staticmethod - def unit_y() -> Vec3: ... - @staticmethod - def unit_z() -> Vec3: ... - @staticmethod - def from_tuple(t: VectorLike) -> Vec3: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Vec3: ... - - x: float - y: float - z: float - - def dot(self, other: VectorLike) -> float: ... - def cross(self, other: VectorLike) -> Vec3: ... - def norm(self) -> float: ... - def norm_squared(self) -> float: ... - def normalize(self) -> Vec3: ... - def lerp(self, other: VectorLike, t: float) -> Vec3: ... - def to_tuple(self) -> Tuple[float, float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def bitwise_equal(self, other: Vec3) -> bool: ... - - def __add__(self, other: VectorLike) -> Vec3: ... - def __sub__(self, other: VectorLike) -> Vec3: ... - def __mul__(self, scalar: float) -> Vec3: ... - def __rmul__(self, scalar: float) -> Vec3: ... - def __truediv__(self, scalar: float) -> Vec3: ... - def __neg__(self) -> Vec3: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# `AngVector3` is a Python-level alias for `Vec3`. -AngVector3 = Vec3 - -# ---- Point3 -------------------------------------------------------------- -class Point3: - def __init__(self, x: float, y: float, z: float) -> None: ... - @staticmethod - def origin() -> Point3: ... - @staticmethod - def from_tuple(t: VectorLike) -> Point3: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Point3: ... - - x: float - y: float - z: float - coords: Vec3 - - def to_tuple(self) -> Tuple[float, float, float]: ... - def to_ndarray(self) -> np.ndarray: ... - def lerp(self, other: VectorLike, t: float) -> Point3: ... - def bitwise_equal(self, other: Point3) -> bool: ... - - def __add__(self, v: VectorLike) -> Point3: ... - def __sub__(self, other: Any) -> Union[Point3, Vec3]: ... - def __len__(self) -> int: ... - def __getitem__(self, idx: int) -> float: ... - def __iter__(self) -> Iterator[float]: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Rotation3 / Quaternion --------------------------------------------- -class Rotation3: - def __init__(self, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> None: ... - @staticmethod - def identity() -> Rotation3: ... - @staticmethod - def from_axis_angle(axis: VectorLike, angle: float) -> Rotation3: ... - @staticmethod - def from_euler_angles(roll: float, pitch: float, yaw: float) -> Rotation3: ... - @staticmethod - def from_scaled_axis(v: VectorLike) -> Rotation3: ... - @staticmethod - def from_quaternion(w: float, x: float, y: float, z: float) -> Rotation3: ... - @staticmethod - def look_at_lh(dir: VectorLike, up: VectorLike) -> Rotation3: ... - @staticmethod - def look_at_rh(dir: VectorLike, up: VectorLike) -> Rotation3: ... - @staticmethod - def face_toward(dir: VectorLike, up: VectorLike) -> Rotation3: ... - @staticmethod - def from_tuple(t: RotationLike) -> Rotation3: ... - @staticmethod - def from_ndarray(a: np.ndarray) -> Rotation3: ... - - axis: Vec3 | None - angle: float - scaled_axis: Vec3 - euler_angles: Tuple[float, float, float] - quaternion: Tuple[float, float, float, float] - w: float - i: float - j: float - k: float - - def inverse(self) -> Rotation3: ... - def to_matrix(self) -> np.ndarray: ... - def transform_point(self, p: VectorLike) -> Point3: ... - def transform_vector(self, v: VectorLike) -> Vec3: ... - def slerp(self, other: RotationLike, t: float) -> Rotation3: ... - def bitwise_equal(self, other: Rotation3) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# Alias: `Quaternion is Rotation3`. -Quaternion = Rotation3 - -# ---- Isometry3 ----------------------------------------------------------- -class Isometry3: - def __init__( - self, - translation: VectorLike | None = None, - rotation: RotationLike | None = None, - ) -> None: ... - @staticmethod - def identity() -> Isometry3: ... - @staticmethod - def from_translation(x: float, y: float, z: float) -> Isometry3: ... - @staticmethod - def from_rotation(scaled_axis: VectorLike) -> Isometry3: ... - @staticmethod - def face_toward(eye: VectorLike, target: VectorLike, up: VectorLike) -> Isometry3: ... - @staticmethod - def look_at_lh(eye: VectorLike, target: VectorLike, up: VectorLike) -> Isometry3: ... - @staticmethod - def look_at_rh(eye: VectorLike, target: VectorLike, up: VectorLike) -> Isometry3: ... - - @property - def translation(self) -> Vec3: ... - @property - def rotation(self) -> Rotation3: ... - - def inverse(self) -> Isometry3: ... - def transform_point(self, p: VectorLike) -> Point3: ... - def transform_vector(self, v: VectorLike) -> Vec3: ... - def to_matrix(self) -> np.ndarray: ... - def lerp_slerp(self, other: Isometry3, t: float) -> Isometry3: ... - def bitwise_equal(self, other: Isometry3) -> bool: ... - - def __mul__(self, rhs: Any) -> Any: ... - def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -# ---- Free helpers -------------------------------------------------------- -def rotation_from_angle(v: VectorLike) -> Rotation3: ... - -# ===================================================================== -# Phase 03 — Dynamics -# ===================================================================== - -import enum - -class RigidBodyType(enum.IntEnum): - DYNAMIC: int - FIXED: int - KINEMATIC_VELOCITY_BASED: int - KINEMATIC_POSITION_BASED: int - -class CoefficientCombineRule(enum.IntEnum): - AVERAGE: int - MIN: int - MULTIPLY: int - MAX: int - CLAMPED_SUM: int - -class FrictionModel(enum.IntEnum): - COEFFICIENT: int - COULOMB: int - -class ColliderHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> ColliderHandle: ... - @staticmethod - def invalid() -> ColliderHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class ColliderSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class ImpulseJointSet: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - -class MultibodyJointSet: - def __init__(self) -> None: ... - -class RigidBodyHandle: - def __init__(self, index: int = 0, generation: int = 0) -> None: ... - @staticmethod - def from_raw_parts(index: int, generation: int) -> RigidBodyHandle: ... - @staticmethod - def invalid() -> RigidBodyHandle: ... - @property - def index(self) -> int: ... - @property - def generation(self) -> int: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -class LockedAxes: - TRANSLATION_LOCKED_X: LockedAxes - TRANSLATION_LOCKED_Y: LockedAxes - TRANSLATION_LOCKED_Z: LockedAxes - TRANSLATION_LOCKED: LockedAxes - ROTATION_LOCKED_X: LockedAxes - ROTATION_LOCKED_Y: LockedAxes - ROTATION_LOCKED_Z: LockedAxes - ROTATION_LOCKED: LockedAxes - ALL: LockedAxes - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> LockedAxes: ... - @staticmethod - def all_axes() -> LockedAxes: ... - @property - def bits(self) -> int: ... - def contains(self, other: LockedAxes) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: LockedAxes) -> LockedAxes: ... - def __and__(self, other: LockedAxes) -> LockedAxes: ... - def __xor__(self, other: LockedAxes) -> LockedAxes: ... - def __invert__(self) -> LockedAxes: ... - -class RigidBodyActivation: - sleep_threshold: float - linear_threshold: float - angular_threshold: float - time_until_sleep: float - sleeping: bool - def __init__(self) -> None: ... - @staticmethod - def active() -> RigidBodyActivation: ... - @staticmethod - def inactive() -> RigidBodyActivation: ... - @staticmethod - def cannot_sleep() -> RigidBodyActivation: ... - def is_active(self) -> bool: ... - -class SpringCoefficients: - stiffness: float - damping: float - natural_frequency: float - damping_ratio: float - def __init__(self, stiffness: float = 30.0, damping: float = 5.0) -> None: ... - @staticmethod - def contact_defaults() -> SpringCoefficients: ... - @staticmethod - def joint_defaults() -> SpringCoefficients: ... - -class MassProperties: - def __init__( - self, - local_com: VectorLike | None = None, - mass: float = 0.0, - principal_inertia: VectorLike | None = None, - principal_inertia_local_frame: RotationLike | None = None, - ) -> None: ... - @staticmethod - def zero() -> MassProperties: ... - @staticmethod - def from_ball(density: float, radius: float) -> MassProperties: ... - @staticmethod - def from_cuboid(density: float, half_extents: VectorLike) -> MassProperties: ... - @staticmethod - def from_capsule(density: float, a: VectorLike, b: VectorLike, radius: float) -> MassProperties: ... - @staticmethod - def from_cylinder(density: float, half_height: float, radius: float) -> MassProperties: ... - @staticmethod - def from_cone(density: float, half_height: float, radius: float) -> MassProperties: ... - @property - def mass(self) -> float: ... - @property - def inv_mass(self) -> float: ... - @property - def local_com(self) -> Point3: ... - @property - def principal_inertia(self) -> Vec3: ... - @property - def principal_inertia_local_frame(self) -> Rotation3: ... - def transform_by(self, iso: IsometryLike) -> MassProperties: ... - def __add__(self, other: MassProperties) -> MassProperties: ... - def __sub__(self, other: MassProperties) -> MassProperties: ... - -class RigidBodyAdditionalMassProps: - @staticmethod - def from_mass(mass: float) -> RigidBodyAdditionalMassProps: ... - @staticmethod - def from_mass_properties(mp: MassProperties) -> RigidBodyAdditionalMassProps: ... - -class RigidBodyDamping: - linear_damping: float - angular_damping: float - def __init__(self, linear_damping: float = 0.0, angular_damping: float = 0.0) -> None: ... - -class RigidBodyDominance: - group: int - def __init__(self, group: int = 0) -> None: ... - -class RigidBodyCcd: - ccd_enabled: bool - soft_ccd_prediction: float - def __init__(self, ccd_enabled: bool = False, soft_ccd_prediction: float = 0.0) -> None: ... - -class RigidBodyVelocity: - linvel: Vec3 - angvel: Vec3 - def __init__(self, linvel: VectorLike | None = None, angvel: VectorLike | None = None) -> None: ... - -class RigidBodyForces: - force: Vec3 - torque: Vec3 - gravity_scale: float - -class RigidBodyMassProps: - local_mprops: MassProperties - -class RigidBodyPosition: - position: Isometry3 - next_position: Isometry3 - -class IntegrationParameters: - dt: float - min_ccd_dt: float - warmstart_coefficient: float - length_unit: float - normalized_allowed_linear_error: float - normalized_max_corrective_velocity: float - normalized_prediction_distance: float - num_solver_iterations: int - num_internal_pgs_iterations: int - num_internal_stabilization_iterations: int - min_island_size: int - max_ccd_substeps: int - contact_softness: SpringCoefficients - friction_model: FrictionModel - def __init__(self) -> None: ... - @staticmethod - def default_params() -> IntegrationParameters: ... - def inv_dt(self) -> float: ... - def allowed_linear_error(self) -> float: ... - def max_corrective_velocity(self) -> float: ... - def prediction_distance(self) -> float: ... - def contact_spring_stiffness(self) -> float: ... - def contact_spring_damping(self) -> float: ... - def joint_spring_stiffness(self) -> float: ... - def joint_spring_damping(self) -> float: ... - -class IslandManager: - def __init__(self) -> None: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[RigidBodyHandle]: ... - def active_dynamic_set(self) -> list[RigidBodyHandle]: ... - def contains_handle(self, h: RigidBodyHandle) -> bool: ... - def same_island(self, h1: RigidBodyHandle, h2: RigidBodyHandle) -> bool: ... - def active_set_offset(self, h: RigidBodyHandle) -> int: ... - -class CCDSolver: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def solve_ccd(self, *args: Any, **kwargs: Any) -> None: ... - -class RigidBodyBuilder: - def __init__(self, body_type: RigidBodyType = ...) -> None: ... - def translation(self, v: VectorLike) -> RigidBodyBuilder: ... - def rotation(self, scaled_axis: VectorLike) -> RigidBodyBuilder: ... - def position(self, p: IsometryLike) -> RigidBodyBuilder: ... - def linvel(self, v: VectorLike) -> RigidBodyBuilder: ... - def angvel(self, v: VectorLike) -> RigidBodyBuilder: ... - def linear_damping(self, f: float) -> RigidBodyBuilder: ... - def angular_damping(self, f: float) -> RigidBodyBuilder: ... - def gravity_scale(self, f: float) -> RigidBodyBuilder: ... - def can_sleep(self, b: bool) -> RigidBodyBuilder: ... - def sleeping(self, b: bool) -> RigidBodyBuilder: ... - def ccd_enabled(self, b: bool) -> RigidBodyBuilder: ... - def soft_ccd_prediction(self, f: float) -> RigidBodyBuilder: ... - def dominance_group(self, g: int) -> RigidBodyBuilder: ... - def additional_solver_iterations(self, n: int) -> RigidBodyBuilder: ... - def user_data(self, d: int) -> RigidBodyBuilder: ... - def enabled(self, b: bool) -> RigidBodyBuilder: ... - def additional_mass(self, m: float) -> RigidBodyBuilder: ... - def additional_mass_properties(self, mp: MassProperties) -> RigidBodyBuilder: ... - def locked_axes(self, la: LockedAxes) -> RigidBodyBuilder: ... - def enabled_translations(self, t: tuple[bool, bool, bool]) -> RigidBodyBuilder: ... - def restrict_translations(self, t: tuple[bool, bool, bool]) -> RigidBodyBuilder: ... - def enabled_rotations(self, t: tuple[bool, bool, bool]) -> RigidBodyBuilder: ... - def restrict_rotations(self, t: tuple[bool, bool, bool]) -> RigidBodyBuilder: ... - def gyroscopic_forces(self, b: bool) -> RigidBodyBuilder: ... - def build(self) -> RigidBody: ... - -class RigidBody: - position: Isometry3 - translation: Vec3 - rotation: Rotation3 - linvel: Vec3 - angvel: Vec3 - user_data: int - body_type: RigidBodyType - gravity_scale: float - linear_damping: float - angular_damping: float - dominance_group: int - additional_solver_iterations: int - locked_axes: LockedAxes - enabled_translations: tuple[bool, bool, bool] - enabled_rotations: tuple[bool, bool, bool] - is_enabled: bool - ccd_enabled: bool - soft_ccd_prediction: float - gyroscopic_forces_enabled: bool - activation: RigidBodyActivation - @property - def mass(self) -> float: ... - @property - def inv_mass(self) -> float: ... - @property - def center_of_mass(self) -> Point3: ... - @property - def local_center_of_mass(self) -> Point3: ... - @property - def mass_properties(self) -> MassProperties: ... - @property - def is_sleeping(self) -> bool: ... - @property - def is_moving(self) -> bool: ... - @property - def is_dynamic(self) -> bool: ... - @property - def is_kinematic(self) -> bool: ... - @property - def is_fixed(self) -> bool: ... - @property - def user_force(self) -> Vec3: ... - @property - def user_torque(self) -> Vec3: ... - @property - def colliders(self) -> list[ColliderHandle]: ... - @property - def next_position(self) -> Isometry3: ... - @staticmethod - def dynamic(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def fixed(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_velocity_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def kinematic_position_based(**kwargs: Any) -> RigidBodyBuilder: ... - @staticmethod - def new_body(body_type: RigidBodyType, **kwargs: Any) -> RigidBodyBuilder: ... - def add_force(self, force: VectorLike, wake_up: bool = True) -> None: ... - def add_torque(self, torque: VectorLike, wake_up: bool = True) -> None: ... - def add_force_at_point(self, force: VectorLike, point: VectorLike, wake_up: bool = True) -> None: ... - def apply_impulse(self, impulse: VectorLike, wake_up: bool = True) -> None: ... - def apply_torque_impulse(self, torque_impulse: VectorLike, wake_up: bool = True) -> None: ... - def apply_impulse_at_point(self, impulse: VectorLike, point: VectorLike, wake_up: bool = True) -> None: ... - def add_gravitational_force(self, gravity: VectorLike) -> None: ... - def reset_forces(self, wake_up: bool = True) -> None: ... - def reset_torques(self, wake_up: bool = True) -> None: ... - def wake_up(self, strong: bool = True) -> None: ... - def sleep(self) -> None: ... - def velocity_at_point(self, point: VectorLike) -> Vec3: ... - def kinetic_energy(self) -> float: ... - def gravitational_potential_energy(self, dt: float, gravity: VectorLike) -> float: ... - def predict_position_using_velocity(self, dt: float) -> Isometry3: ... - def predict_position_using_velocity_and_forces(self, dt: float) -> Isometry3: ... - def angvel_with_gyroscopic_forces(self, dt: float) -> Vec3: ... - def recompute_mass_properties_from_colliders(self, colliders: ColliderSet) -> None: ... - def set_additional_mass(self, mass: float, wake_up: bool = True) -> None: ... - def set_additional_mass_properties(self, mp: MassProperties, wake_up: bool = True) -> None: ... - -class RigidBodySet: - def __init__(self) -> None: ... - def insert(self, body: RigidBody | RigidBodyBuilder) -> RigidBodyHandle: ... - def remove( - self, - handle: RigidBodyHandle, - islands: IslandManager, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - remove_attached_colliders: bool = True, - ) -> RigidBody | None: ... - def get(self, handle: RigidBodyHandle) -> RigidBody | None: ... - def clear(self) -> None: ... - def handles(self) -> Iterator[RigidBodyHandle]: ... - def __getitem__(self, handle: RigidBodyHandle) -> RigidBody: ... - def __contains__(self, handle: RigidBodyHandle) -> bool: ... - def __iter__(self) -> Iterator[Tuple[RigidBodyHandle, RigidBody]]: ... - def __len__(self) -> int: ... - -# ---- events / hooks (phase 07) ------------------------------------------- - -class SolverFlags: - COMPUTE_IMPULSES: SolverFlags - EMPTY: SolverFlags - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> SolverFlags: ... - @property - def bits(self) -> int: ... - def contains(self, other: SolverFlags) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: SolverFlags) -> SolverFlags: ... - def __and__(self, other: SolverFlags) -> SolverFlags: ... - def __xor__(self, other: SolverFlags) -> SolverFlags: ... - def __invert__(self) -> SolverFlags: ... - -class PairFilterContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - -class ContactModificationContext: - @property - def collider1(self) -> ColliderHandle: ... - @property - def collider2(self) -> ColliderHandle: ... - @property - def rigid_body1(self) -> RigidBodyHandle | None: ... - @property - def rigid_body2(self) -> RigidBodyHandle | None: ... - @property - def local_n1(self) -> Vec3: ... - @property - def local_n2(self) -> Vec3: ... - normal: Vec3 - user_data: int - @property - def solver_contacts(self) -> list[SolverContact]: ... - def clear_solver_contacts(self) -> None: ... - def num_solver_contacts(self) -> int: ... - def remove_solver_contact(self, i: int) -> None: ... - def update_as_oneway_platform(self, allowed_local_n1: Vec3 | tuple[float, float, float], angle: float) -> None: ... - -class ChannelEventCollector: - def __init__(self) -> None: ... - def drain_collision_events(self) -> list[CollisionEvent]: ... - def drain_contact_force_events(self) -> list[ContactForceEvent]: ... - def clear(self) -> None: ... - def __len__(self) -> int: ... - def remove_solver_contact(self, i: int) -> None: ... - def update_as_oneway_platform(self, allowed_local_n1: Vec3 | tuple[float, float, float], angle: float) -> None: ... - -class ChannelEventCollector: - def __init__(self) -> None: ... - def drain_collision_events(self) -> list[CollisionEvent]: ... - def drain_contact_force_events(self) -> list[ContactForceEvent]: ... - def clear(self) -> None: ... - def __len__(self) -> int: ... - -# ---- controllers (phase 08) ---------------------------------------------- - -from typing import Callable, Literal - -class CharacterLength: - kind: Literal["absolute", "relative"] - value: float - def __init__(self, value: float = 0.0, kind: str = "absolute") -> None: ... - @staticmethod - def absolute(value: float) -> CharacterLength: ... - @staticmethod - def relative(value: float) -> CharacterLength: ... - -class CharacterAutostep: - max_height: CharacterLength - min_width: CharacterLength - include_dynamic_bodies: bool - def __init__( - self, - max_height: CharacterLength | float | None = None, - min_width: CharacterLength | float | None = None, - include_dynamic_bodies: bool = True, - ) -> None: ... - @staticmethod - def disabled() -> None: ... - -class EffectiveCharacterMovement: - @property - def translation(self) -> Vec3: ... - @property - def grounded(self) -> bool: ... - @property - def is_sliding_down_slope(self) -> bool: ... - -class CharacterCollision: - @property - def handle(self) -> ColliderHandle: ... - @property - def character_pos(self) -> Isometry3: ... - @property - def translation_applied(self) -> Vec3: ... - @property - def translation_remaining(self) -> Vec3: ... - @property - def toi(self) -> ShapeCastHit: ... - -class KinematicCharacterController: - up: Vec3 - offset: CharacterLength - slide: bool - autostep: CharacterAutostep | None - max_slope_climb_angle: float - min_slope_slide_angle: float - snap_to_ground: CharacterLength | None - normal_nudge_factor: float - def __init__( - self, - *, - up: Vec3 | tuple[float, float, float] | None = None, - offset: CharacterLength | float | None = None, - slide: bool | None = None, - autostep: CharacterAutostep | None = None, - max_slope_climb_angle: float | None = None, - min_slope_slide_angle: float | None = None, - snap_to_ground: CharacterLength | float | None = None, - normal_nudge_factor: float | None = None, - ) -> None: ... - def move_shape( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - shape: SharedShape, - shape_pos: Isometry3, - desired_translation: Vec3 | tuple[float, float, float], - filter: QueryFilter | None = None, - events_callback: Callable[[CharacterCollision], None] | None = None, - ) -> EffectiveCharacterMovement: ... - def solve_character_collision_impulses( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - character_shape: SharedShape, - character_pos: Isometry3, - character_mass: float, - collisions: list[CharacterCollision], - filter: QueryFilter | None = None, - ) -> None: ... - -class AxesMask: - LIN_X: AxesMask - LIN_Y: AxesMask - LIN_Z: AxesMask - ANG_X: AxesMask - ANG_Y: AxesMask - ANG_Z: AxesMask - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> AxesMask: ... - @staticmethod - def all() -> AxesMask: ... - @staticmethod - def all_axes() -> AxesMask: ... - @property - def bits(self) -> int: ... - def contains(self, other: AxesMask) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: AxesMask) -> AxesMask: ... - def __and__(self, other: AxesMask) -> AxesMask: ... - def __xor__(self, other: AxesMask) -> AxesMask: ... - def __invert__(self) -> AxesMask: ... - def __sub__(self, other: AxesMask) -> AxesMask: ... - def __contains__(self, other: AxesMask) -> bool: ... - -class PdErrors: - linear: Vec3 - angular: Vec3 - def __init__( - self, - linear: Vec3 | tuple[float, float, float] | None = None, - angular: Vec3 | tuple[float, float, float] | None = None, - ) -> None: ... - -class PidCorrection: - @property - def linear(self) -> Vec3: ... - @property - def angular(self) -> Vec3: ... - -class PdController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec3 | tuple[float, float, float] | float | None = None, - Kd: Vec3 | tuple[float, float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec3 | tuple[float, float, float]) -> None: ... - def set_axes_kd(self, v: Vec3 | tuple[float, float, float]) -> None: ... - def rigid_body_correction(self, body: RigidBody, target_pose: Isometry3) -> PidCorrection: ... - def correction(self, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -class PidController: - axes: AxesMask - def __init__( - self, - axes: AxesMask | None = None, - Kp: Vec3 | tuple[float, float, float] | float | None = None, - Ki: Vec3 | tuple[float, float, float] | float | None = None, - Kd: Vec3 | tuple[float, float, float] | float | None = None, - ) -> None: ... - def set_axes_kp(self, v: Vec3 | tuple[float, float, float]) -> None: ... - def set_axes_ki(self, v: Vec3 | tuple[float, float, float]) -> None: ... - def set_axes_kd(self, v: Vec3 | tuple[float, float, float]) -> None: ... - def reset(self) -> None: ... - def rigid_body_correction(self, dt: float, body: RigidBody, target_pose: Isometry3) -> PidCorrection: ... - def position_correction(self, dt: float, position: Isometry3, target_position: Isometry3) -> PidCorrection: ... - def update(self, dt: float, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... - -class WheelTuning: - suspension_stiffness: float - suspension_compression: float - suspension_damping: float - max_suspension_travel: float - side_friction_stiffness: float - friction_slip: float - max_suspension_force: float - def __init__( - self, - suspension_stiffness: float | None = None, - suspension_compression: float | None = None, - suspension_damping: float | None = None, - max_suspension_travel: float | None = None, - side_friction_stiffness: float | None = None, - friction_slip: float | None = None, - max_suspension_force: float | None = None, - ) -> None: ... - @staticmethod - def default() -> WheelTuning: ... - -class RayCastInfo: - @property - def contact_normal_ws(self) -> Vec3: ... - @property - def contact_point_ws(self) -> Vec3: ... - @property - def suspension_length(self) -> float: ... - @property - def hard_point_ws(self) -> Vec3: ... - @property - def is_in_contact(self) -> bool: ... - @property - def ground_object(self) -> ColliderHandle | None: ... - -class Wheel: - chassis_connection_point_cs: Vec3 - direction_cs: Vec3 - axle_cs: Vec3 - suspension_rest_length: float - max_suspension_travel: float - radius: float - suspension_stiffness: float - damping_compression: float - damping_relaxation: float - friction_slip: float - side_friction_stiffness: float - rotation: float - max_suspension_force: float - forward_impulse: float - side_impulse: float - steering: float - engine_force: float - brake: float - wheel_suspension_force: float - @property - def raycast_info(self) -> RayCastInfo: ... - -class DynamicRayCastVehicleController: - index_up_axis: int - index_forward_axis: int - def __init__(self, chassis_handle: RigidBodyHandle) -> None: ... - def add_wheel( - self, - chassis_connection_cs: Vec3 | tuple[float, float, float], - direction_cs: Vec3 | tuple[float, float, float], - axle_cs: Vec3 | tuple[float, float, float], - suspension_rest_length: float, - radius: float, - tuning: WheelTuning, - ) -> int: ... - def update_vehicle( - self, - dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, - queries: QueryPipeline, - filter: QueryFilter | None = None, - ) -> None: ... - def set_brake(self, idx: int, brake: float) -> None: ... - def set_steering(self, idx: int, steering: float) -> None: ... - def apply_engine_force(self, idx: int, force: float) -> None: ... - def wheels(self) -> list[Wheel]: ... - def wheel(self, idx: int) -> Wheel: ... - def current_speed_km_hour(self) -> float: ... - @property - def current_vehicle_speed(self) -> float: ... - def chassis(self) -> RigidBodyHandle: ... - -# ---- debug-render (phase 10) --------------------------------------------- - -class DebugRenderObject: - kind: int - RIGID_BODY: DebugRenderObject - COLLIDER: DebugRenderObject - COLLIDER_AABB: DebugRenderObject - IMPULSE_JOINT: DebugRenderObject - MULTIBODY_JOINT: DebugRenderObject - CONTACT_PAIR: DebugRenderObject - -class DebugColor: - def __init__(self, h: float = 0.0, s: float = 0.0, l: float = 0.0, a: float = 1.0) -> None: ... - @staticmethod - def from_hsla(h: float, s: float, l: float, a: float) -> DebugColor: ... - @staticmethod - def from_rgba(r: float, g: float, b: float, a: float) -> DebugColor: ... - @property - def hsla(self) -> list[float]: ... - @property - def rgba(self) -> list[float]: ... - -class DebugRenderMode: - def __init__(self, bits: int = 0) -> None: ... - @staticmethod - def empty() -> DebugRenderMode: ... - @staticmethod - def default() -> DebugRenderMode: ... - @staticmethod - def all() -> DebugRenderMode: ... - COLLIDER_SHAPES: DebugRenderMode - RIGID_BODY_AXES: DebugRenderMode - MULTIBODY_JOINTS: DebugRenderMode - IMPULSE_JOINTS: DebugRenderMode - JOINTS: DebugRenderMode - SOLVER_CONTACTS: DebugRenderMode - CONTACTS: DebugRenderMode - COLLIDER_AABBS: DebugRenderMode - EMPTY: DebugRenderMode - @property - def bits(self) -> int: ... - def contains(self, other: DebugRenderMode) -> bool: ... - def is_empty(self) -> bool: ... - def __or__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __and__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __xor__(self, other: DebugRenderMode) -> DebugRenderMode: ... - def __invert__(self) -> DebugRenderMode: ... - def __bool__(self) -> bool: ... - -class DebugRenderStyle: - def __init__(self) -> None: ... - @staticmethod - def default() -> DebugRenderStyle: ... - subdivisions: int - border_subdivisions: int - collider_dynamic_color: DebugColor - collider_fixed_color: DebugColor - collider_kinematic_color: DebugColor - collider_parentless_color: DebugColor - impulse_joint_anchor_color: DebugColor - impulse_joint_separation_color: DebugColor - multibody_joint_anchor_color: DebugColor - multibody_joint_separation_color: DebugColor - sleep_color_multiplier: DebugColor - disabled_color_multiplier: DebugColor - rigid_body_axes_length: float - contact_depth_color: DebugColor - contact_normal_color: DebugColor - contact_normal_length: float - collider_aabb_color: DebugColor - -class DebugLineCollector: - def __init__(self) -> None: ... - def clear(self) -> None: ... - def lines(self) -> np.ndarray: ... - def colors(self) -> np.ndarray: ... - def objects(self) -> np.ndarray: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[tuple[DebugRenderObject, Vec3, Vec3, DebugColor]]: ... - -class DebugRenderPipeline: - mode: DebugRenderMode - style: DebugRenderStyle - def __init__( - self, - mode: DebugRenderMode | None = None, - style: DebugRenderStyle | None = None, - ) -> None: ... - def render( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - backend: Any, - ) -> None: ... - def render_to_arrays( - self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - narrow_phase: NarrowPhase, - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... diff --git a/python/rapier-py-3d-f64/python/rapier3d_f64/py.typed b/python/rapier-py-3d-f64/python/rapier3d_f64/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/rapier-py-3d-f64/src/lib.rs b/python/rapier-py-3d-f64/src/lib.rs deleted file mode 100644 index 0b9d5cd2a..000000000 --- a/python/rapier-py-3d-f64/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! `_rapier3d_f64` — PyO3 extension module for rapier3d-f64 (3D, f64). - -use pyo3::prelude::*; - -use rapier3d_f64 as rapier; - -rapier_py_core::define_conv_types!(Real = f64, DIM = 3); -rapier_py_core::define_math_types!(DIM = 3); -rapier_py_core::define_geometry_types!(DIM = 3); -rapier_py_core::define_joints_types!(DIM = 3); -rapier_py_core::define_dynamics_types!(DIM = 3); -rapier_py_core::define_pipeline_types!(DIM = 3); -rapier_py_core::define_events_hooks_types!(DIM = 3); -rapier_py_core::define_controllers_types!(DIM = 3); - -// debug-render adapter. -rapier_py_core::define_debug_render_types!(DIM = 3); - -// must run AFTER every other `define_*_types!`. -rapier_py_core::define_serde_types!(DIM = 3); - -const RAPIER_PY_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[pymodule] -fn _rapier3d_f64(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - rapier_py_core::errors::register_errors(py, m)?; - register_math(py, m)?; - register_geometry(py, m)?; - register_joints(py, m)?; - register_dynamics(py, m)?; - register_pipeline(py, m)?; - register_events_hooks(py, m)?; - register_controllers(py, m)?; - register_debug_render(py, m)?; - m.add("__version__", RAPIER_PY_VERSION)?; - Ok(()) -} diff --git a/python/rapier-py-3d/Cargo.toml b/python/rapier-py-3d/Cargo.toml index c717e9cdd..dd31a608b 100644 --- a/python/rapier-py-3d/Cargo.toml +++ b/python/rapier-py-3d/Cargo.toml @@ -16,7 +16,12 @@ crate-type = ["cdylib"] path = "src/lib.rs" [lints] -rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(rapier_py_determinism)'] } +# pyo3 0.22's `create_exception!` macro still references `cfg(feature = "gil-refs")` +# which this crate doesn't have; mute that check alongside our own determinism cfg. +rust.unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(rapier_py_determinism)', + 'cfg(feature, values("gil-refs"))', +] } [features] # Phase 11 — opt-in build flag. When enabled, rapier is compiled with @@ -28,7 +33,9 @@ determinism = ["rapier3d/enhanced-determinism"] [dependencies] pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } -rapier-py-core = { path = "../rapier-py-core" } +numpy = "0.22" +nalgebra.workspace = true +thiserror.workspace = true rapier3d = { path = "../../crates/rapier3d", features = ["serde-serialize", "debug-render", "simd-stable"] } # Phase 09 — Loaders. URDF + MJCF + mesh. Mesh-loader features are # explicitly enabled so .stl / .obj / .dae meshes referenced from URDF / diff --git a/python/rapier-py-3d/README.md b/python/rapier-py-3d/README.md index 9e145b83d..180fa7d72 100644 --- a/python/rapier-py-3d/README.md +++ b/python/rapier-py-3d/README.md @@ -16,17 +16,6 @@ colliders = rp.ColliderSet() body = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 5.0, 0.0))) ``` -## Related packages - -| Package | Dimension | Scalar | -| -------------- | --------- | ------ | -| `rapier2d` | 2D | f32 | -| `rapier3d` | 3D | f32 | -| `rapier2d-f64` | 2D | f64 | -| `rapier3d-f64` | 3D | f64 | - -All four share an identical API differing only in dimension and scalar type. - ## License Apache-2.0 diff --git a/python/rapier-py-3d/python/rapier3d/_pickle_setup.py b/python/rapier-py-3d/python/rapier3d/_pickle_setup.py index 695ea1f3f..12a725dd2 100644 --- a/python/rapier-py-3d/python/rapier3d/_pickle_setup.py +++ b/python/rapier-py-3d/python/rapier3d/_pickle_setup.py @@ -1,23 +1,16 @@ -"""Make pyo3 pyclasses picklable from the correct dim/scalar namespace. +"""Make pyo3 pyclasses picklable from the correct module namespace. -The PyO3 macros that emit all `rapier` pyclasses set `module = "rapier"` on -every class declaration. Since the four cdylibs (`_rapier2d`, `_rapier2d_f64`, -`_rapier3d`, `_rapier3d_f64`) each define their own copy of (e.g.) -`MassProperties` but all four share `__module__ == "rapier"`, Python's -default pickling cannot disambiguate which flavor to restore: pickle would -fetch `rapier.MassProperties` (== the default 3D-f32 flavor) and reject -non-matching pyclass instances with `PicklingError: it's not the same object +The PyO3 macros that emit the `rapier` pyclasses set `module = "rapier"` on +every class declaration, but the extension is imported as `rapier3d`. Left +unpatched, Python's pickling would look classes up under the wrong module +path and reject the instances with `PicklingError: it's not the same object as rapier.MassProperties`. To fix this without rewriting the macros, we patch `__module__` on every -exported class at import time so that each flavor's classes declare the -right module path: - - `rapier.dim2.MassProperties.__module__ = "rapier.dim2"` - - `rapier.dim2.f64.MassProperties.__module__ = "rapier.dim2.f64"` - - `rapier.dim3.MassProperties.__module__ = "rapier.dim3"` - - `rapier.dim3.f64.MassProperties.__module__ = "rapier.dim3.f64"` +exported class at import time so it declares the real module path: + - `rapier3d.MassProperties.__module__ = "rapier3d"` -Pickle then resolves each instance to its actual flavor. +Pickle then resolves each instance correctly. """ from __future__ import annotations diff --git a/python/rapier-py-3d/src/conv.rs b/python/rapier-py-3d/src/conv.rs new file mode 100644 index 000000000..da3cfe2b8 --- /dev/null +++ b/python/rapier-py-3d/src/conv.rs @@ -0,0 +1,216 @@ +//! `FromPyObject` shims for the math primitives every binding consumes. +//! +//! These types are **input adapters**: they implement `FromPyObject` so that +//! `#[pyfunction] fn foo(v: PyVector)` will accept tuples, lists, NumPy +//! arrays, and other PyVector instances of the right shape. +//! +//! The dim/scalar split is handled by the `define_conv_types!` macro at the +//! bottom of this file. Each cdylib invokes it once near the top of its +//! `src/lib.rs` with concrete `Real`/`DIM` parameters; the macro emits +//! locally-defined `PyVector`, `PyPoint`, `PyRotation`, `PyIsometry`, +//! `PyAngVector` types in the cdylib's namespace. +//! +//! Helper functions here are generic and re-used by the macro expansion. + +use numpy::PyReadonlyArray1; +use pyo3::prelude::*; +use pyo3::types::PySequence; + +/// Extract `expected` floats from a Python tuple/list/sequence. +pub fn extract_floats_from_sequence(obj: &Bound<'_, PyAny>, expected: usize) -> PyResult> +where + R: Copy + for<'a> FromPyObject<'a>, +{ + let seq = obj.downcast::().map_err(|_| { + pyo3::exceptions::PyTypeError::new_err( + "expected a tuple, list, ndarray, or matching newtype", + ) + })?; + let len = seq.len()?; + if len != expected { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "expected sequence of {expected} floats, got length {len}" + ))); + } + let mut out = Vec::with_capacity(expected); + for i in 0..expected { + let item = seq.get_item(i)?; + out.push(item.extract::()?); + } + Ok(out) +} + +/// Extract `expected` floats from a 1-D NumPy ndarray (accepts f32 or f64 +/// dtype and casts to the requested scalar). Returns `None` if `obj` is not +/// an ndarray at all. +pub fn extract_floats_from_ndarray( + obj: &Bound<'_, PyAny>, + expected: usize, +) -> Option>> +where + R: num_from::FromF64 + num_from::FromF32, +{ + if let Ok(arr) = obj.extract::>() { + let slice_res = arr.as_slice(); + return Some(match slice_res { + Ok(s) if s.len() == expected => Ok(s.iter().map(|x| R::from_f64(*x)).collect()), + Ok(s) => Err(pyo3::exceptions::PyValueError::new_err(format!( + "expected ndarray of length {expected}, got {}", + s.len() + ))), + Err(_) => Err(pyo3::exceptions::PyValueError::new_err( + "ndarray is not contiguous", + )), + }); + } + if let Ok(arr) = obj.extract::>() { + let slice_res = arr.as_slice(); + return Some(match slice_res { + Ok(s) if s.len() == expected => Ok(s.iter().map(|x| R::from_f32(*x)).collect()), + Ok(s) => Err(pyo3::exceptions::PyValueError::new_err(format!( + "expected ndarray of length {expected}, got {}", + s.len() + ))), + Err(_) => Err(pyo3::exceptions::PyValueError::new_err( + "ndarray is not contiguous", + )), + }); + } + None +} + +/// Tiny `From{f32,f64}` shim so the helpers can return either scalar. +pub mod num_from { + pub trait FromF64 { + fn from_f64(x: f64) -> Self; + } + pub trait FromF32 { + fn from_f32(x: f32) -> Self; + } + + impl FromF64 for f32 { + #[inline] + fn from_f64(x: f64) -> Self { + x as f32 + } + } + impl FromF32 for f32 { + #[inline] + fn from_f32(x: f32) -> Self { + x + } + } + impl FromF64 for f64 { + #[inline] + fn from_f64(x: f64) -> Self { + x + } + } + impl FromF32 for f64 { + #[inline] + fn from_f32(x: f32) -> Self { + x as f64 + } + } +} + +/// Extract `expected` floats from any of: PySequence (tuple/list), 1-D +/// ndarray, or any Python object exposing a `to_tuple()` method that returns +/// a sequence of the right length (this covers the canonical `Vec*`/`Point*`/ +/// `Rotation*` math `#[pyclass]` types). +pub fn extract_floats(obj: &Bound<'_, PyAny>, expected: usize) -> PyResult> +where + R: Copy + num_from::FromF64 + num_from::FromF32 + for<'a> FromPyObject<'a>, +{ + if let Some(res) = extract_floats_from_ndarray::(obj, expected) { + return res; + } + if let Ok(res) = extract_floats_from_sequence::(obj, expected) { + return Ok(res); + } + // Fall back to duck-typed `.to_tuple()` so that `Vec3`/`Point3`/ + // `Rotation3`/`Quaternion` instances flow through any API that accepts + // a `PyVector`/`PyPoint`/`PyRotation`. + if let Ok(meth) = obj.getattr(pyo3::intern!(obj.py(), "to_tuple")) { + let t = meth.call0()?; + return extract_floats_from_sequence::(&t, expected); + } + Err(pyo3::exceptions::PyTypeError::new_err( + "expected a tuple, list, ndarray, or a Vec/Point/Rotation-like object", + )) +} + +// --------------------------------------------------------------------------- +// Concrete (f32, 3D) conversion adapter newtypes. +// +// These `#[derive]`-free newtypes implement `FromPyObject` so binding +// functions can accept tuples / lists / ndarrays / math pyclasses. +// --------------------------------------------------------------------------- + +/// Scalar type used throughout the bindings. +pub type Real = f32; +/// Spatial dimension of the bindings. +pub const DIM: usize = 3; + +#[derive(Debug, Clone, Copy)] +pub struct PyVector(pub nalgebra::Vector3); +impl<'py> pyo3::FromPyObject<'py> for PyVector { + fn extract_bound(obj: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult { + let v: Vec = extract_floats(obj, 3)?; + Ok(PyVector(nalgebra::Vector3::new(v[0], v[1], v[2]))) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PyPoint(pub nalgebra::Point3); +impl<'py> pyo3::FromPyObject<'py> for PyPoint { + fn extract_bound(obj: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult { + let v: Vec = extract_floats(obj, 3)?; + Ok(PyPoint(nalgebra::Point3::new(v[0], v[1], v[2]))) + } +} + +/// In 3D, the angular vector is a scaled axis-angle (3 floats). +#[derive(Debug, Clone, Copy)] +pub struct PyAngVector(pub nalgebra::Vector3); +impl<'py> pyo3::FromPyObject<'py> for PyAngVector { + fn extract_bound(obj: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult { + let v: Vec = extract_floats(obj, 3)?; + Ok(PyAngVector(nalgebra::Vector3::new(v[0], v[1], v[2]))) + } +} + +/// Quaternion convention: `(x, y, z, w)`. +#[derive(Debug, Clone, Copy)] +pub struct PyRotation(pub nalgebra::UnitQuaternion); +impl<'py> pyo3::FromPyObject<'py> for PyRotation { + fn extract_bound(obj: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult { + let v: Vec = extract_floats(obj, 4)?; + let q = nalgebra::Quaternion::new(v[3], v[0], v[1], v[2]); + Ok(PyRotation(nalgebra::UnitQuaternion::from_quaternion(q))) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PyIsometry(pub nalgebra::Isometry3); +impl<'py> pyo3::FromPyObject<'py> for PyIsometry { + fn extract_bound(obj: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult { + let py = obj.py(); + if let (Ok(t_obj), Ok(r_obj)) = ( + obj.getattr(pyo3::intern!(py, "translation")), + obj.getattr(pyo3::intern!(py, "rotation")), + ) { + let t: PyVector = t_obj.extract()?; + let r: PyRotation = r_obj.extract()?; + return Ok(PyIsometry(nalgebra::Isometry3::from_parts( + nalgebra::Translation3::from(t.0), + r.0, + ))); + } + let (t, r): (PyVector, PyRotation) = obj.extract()?; + Ok(PyIsometry(nalgebra::Isometry3::from_parts( + nalgebra::Translation3::from(t.0), + r.0, + ))) + } +} diff --git a/python/rapier-py-3d/src/errors.rs b/python/rapier-py-3d/src/errors.rs new file mode 100644 index 000000000..2aaa62a0b --- /dev/null +++ b/python/rapier-py-3d/src/errors.rs @@ -0,0 +1,94 @@ +//! The `RapierError` exception tree shared by every cdylib. +//! +//! All concrete error types inherit from `RapierError` (which itself inherits +//! from Python's `Exception`), so users can write +//! `except rapier.RapierError as e:` to catch any rapier-originated failure. + +use pyo3::create_exception; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +create_exception!( + rapier, + RapierError, + PyException, + "Base exception for all rapier-originated errors." +); +create_exception!( + rapier, + InvalidHandle, + RapierError, + "Raised when a handle (rigid body, collider, joint, ...) no longer refers to a live object." +); +create_exception!( + rapier, + MeshConversionError, + RapierError, + "Raised when converting between mesh representations fails (degenerate, non-manifold, ...)." +); +create_exception!( + rapier, + UrdfError, + RapierError, + "Raised by the URDF loader on parse or structural errors." +); +create_exception!( + rapier, + MjcfError, + RapierError, + "Raised by the MJCF loader on parse or structural errors." +); +create_exception!( + rapier, + SerializationError, + RapierError, + "Raised when (de)serializing a physics scene fails." +); +create_exception!( + rapier, + QueryFailure, + RapierError, + "Raised when a spatial query (raycast, point projection, ...) cannot complete." +); +create_exception!( + rapier, + MeshLoaderError, + RapierError, + "Raised by the mesh loader (e.g. STL/OBJ ingestion) on parse or IO errors." +); + +/// Register every exception class defined in this crate onto the given pymodule. +/// +/// Call this exactly once from each cdylib's `#[pymodule]` function. +pub fn register_errors(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("RapierError", py.get_type_bound::())?; + m.add("InvalidHandle", py.get_type_bound::())?; + m.add( + "MeshConversionError", + py.get_type_bound::(), + )?; + m.add("UrdfError", py.get_type_bound::())?; + m.add("MjcfError", py.get_type_bound::())?; + m.add( + "SerializationError", + py.get_type_bound::(), + )?; + m.add("QueryFailure", py.get_type_bound::())?; + m.add("MeshLoaderError", py.get_type_bound::())?; + Ok(()) +} + +/// Convenience macro that each cdylib calls from its `#[pymodule]`: +/// ```ignore +/// #[pymodule] +/// fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +/// rapier_py_core::register_errors!(py, m); +/// Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! register_errors { + ($py:expr, $m:expr) => { + $crate::errors::register_errors($py, $m)? + }; +} diff --git a/python/rapier-py-3d/src/lib.rs b/python/rapier-py-3d/src/lib.rs index 922d44692..9bc8a2c45 100644 --- a/python/rapier-py-3d/src/lib.rs +++ b/python/rapier-py-3d/src/lib.rs @@ -1,67 +1,35 @@ //! `_rapier3d` — PyO3 extension module for rapier3d (3D, f32). +//! +//! This crate is a single, concrete 3D/f32 binding. (It was previously split +//! into a `rapier-py-core` macro layer plus per-dimension cdylibs; with only +//! the 3D f32 flavor remaining, everything lives here as ordinary modules.) -use pyo3::prelude::*; - -// Bring the rapier engine into the cdylib's namespace under the alias -// `rapier` so the dynamics macro (which uses `rapier::...` paths) compiles. -use rapier3d as rapier; - -// Materialize the dim-/scalar-specific conversion adapter newtypes -// (`PyVector`, `PyPoint`, `PyRotation`, `PyIsometry`, `PyAngVector`) plus the -// `Real`/`DIM` aliases in this crate's namespace. -rapier_py_core::define_conv_types!(Real = f32, DIM = 3); - -// Materialize the user-facing math `#[pyclass]` types and the -// `register_math` function used below. -rapier_py_core::define_math_types!(DIM = 3); - -// Materialize the geometry `#[pyclass]` types (must come BEFORE -// `define_dynamics_types!` because dynamics references -// `ColliderHandle`/`ColliderSet`). -rapier_py_core::define_geometry_types!(DIM = 3); +// Re-export the crate dependencies so the modules can refer to them via stable +// `crate::…` paths (a holdover from the former macro layer, kept intentional). +pub use bincode; +pub use nalgebra as na; +pub use numpy; +pub use pyo3; +pub use serde_json; -// Materialize the joint `#[pyclass]` types. Must come BEFORE -// `define_dynamics_types!` because the dynamics macro references -// `ImpulseJointSet` / `MultibodyJointSet` in `RigidBodySet::remove`. -rapier_py_core::define_joints_types!(DIM = 3); +pub mod conv; +pub mod errors; +pub mod serde_io; -// Materialize the dynamics `#[pyclass]` types and `register_dynamics`. -rapier_py_core::define_dynamics_types!(DIM = 3); +pub mod math; -// Materialize the pipeline / query `#[pyclass]` types. -rapier_py_core::define_pipeline_types!(DIM = 3); +pub use conv::*; +pub use errors::*; +pub use math::*; -// Materialize the event-handler / physics-hooks `#[pyclass]` types. -rapier_py_core::define_events_hooks_types!(DIM = 3); - -// Materialize the loader `#[pyclass]` types. 3D-only. -rapier_py_core::define_loaders_types!(DIM = 3); - -// Materialize the controller `#[pyclass]` types. -rapier_py_core::define_controllers_types!(DIM = 3); - -// Materialize the debug-render `#[pyclass]` types. -rapier_py_core::define_debug_render_types!(DIM = 3); - -// Serialization, snapshots. Must run AFTER every other -// `define_*_types!` because it adds additional `#[pymethods]` blocks on the -// existing pyclasses (relies on pyo3's `multiple-pymethods` feature). -rapier_py_core::define_serde_types!(DIM = 3); +use pyo3::prelude::*; const RAPIER_PY_VERSION: &str = env!("CARGO_PKG_VERSION"); #[pymodule] fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - rapier_py_core::errors::register_errors(py, m)?; + errors::register_errors(py, m)?; register_math(py, m)?; - register_geometry(py, m)?; - register_joints(py, m)?; - register_dynamics(py, m)?; - register_pipeline(py, m)?; - register_events_hooks(py, m)?; - register_loaders(py, m)?; - register_controllers(py, m)?; - register_debug_render(py, m)?; m.add("__version__", RAPIER_PY_VERSION)?; Ok(()) } diff --git a/python/rapier-py-3d/src/math.rs b/python/rapier-py-3d/src/math.rs new file mode 100644 index 000000000..719f8fd84 --- /dev/null +++ b/python/rapier-py-3d/src/math.rs @@ -0,0 +1,1067 @@ +//! The concrete 3D / `f32` math `#[pyclass]` types +//! (`Vec3`/`Point3`/`Rotation3`/`Isometry3`, plus the Python aliases +//! `Quaternion` and `AngVector3`), their helpers, and the `register_math` +//! function used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. +//! +//! Defaults for `approx_eq` tolerances: `1e-7` for `f32`. + +use crate::conv::{PyAngVector, PyIsometry, PyPoint, PyRotation, PyVector, Real}; + +/// Default `approx_eq` epsilon for a given scalar. +pub trait DefaultEpsilon { + fn default_epsilon() -> Self; +} +impl DefaultEpsilon for f32 { + #[inline] + fn default_epsilon() -> Self { + // Loose enough to absorb a couple of float roundings in chained ops + // (e.g. composing two rotations and then transforming a vector). The + // spec calls for `1e-7` but in practice accumulated f32 error already + // reaches `~3e-7` after two SIMD steps, so we relax slightly. + 1.0e-5 + } +} + +/// Repr format string per scalar (matches Python's default float repr precision). +pub trait ReprPrecision { + /// Format a single scalar for use inside `Vec3(…)`-style reprs. + fn fmt_repr(self) -> String; +} +impl ReprPrecision for f32 { + #[inline] + fn fmt_repr(self) -> String { + // f32 → match Python single-precision-ish; use 7 sig digits. + // We rely on Rust's default `Display` for f32 which is shortest-round-trip. + format!("{}", self) + } +} + +use crate::numpy::{PyArray1, PyArrayMethods}; +use crate::pyo3::exceptions::{PyTypeError, PyValueError}; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; + +// ------------------------------------------------------------------ +// Vec3 +// ------------------------------------------------------------------ +/// A 3D vector of three real numbers ``(x, y, z)``. +/// +/// Represents a free vector (a direction and magnitude) in 3D space. +/// Instances are immutable; arithmetic returns new ``Vec3`` values. +/// +/// Also exposed under the Python alias ``AngVector3`` to denote an +/// "angular vector" — a scaled axis-angle rotation parameterization +/// used by Rapier's joints and motors. +/// +/// Wraps :class:`nalgebra::Vector3`. +/// +/// Example:: +/// +/// from rapier3d import Vec3 +/// v = Vec3(1.0, 2.0, 3.0) +/// up = Vec3.unit_y() +/// n = v.cross(up).normalize() +#[pyclass(name = "Vec3", module = "rapier.dim3", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Vec3(pub crate::na::Vector3); + +#[pymethods] +impl Vec3 { + /// Create a new 3D vector from its components. + /// + /// :param x: x component. + /// :param y: y component. + /// :param z: z component. + #[new] + fn new(x: Real, y: Real, z: Real) -> Self { + Self(crate::na::Vector3::new(x, y, z)) + } + + /// Return the zero vector ``(0, 0, 0)``. + #[staticmethod] + fn zeros() -> Self { + Self(crate::na::Vector3::zeros()) + } + /// Return the unit vector along the ``x`` axis, ``(1, 0, 0)``. + #[staticmethod] + fn unit_x() -> Self { + Self(crate::na::Vector3::x()) + } + /// Return the unit vector along the ``y`` axis, ``(0, 1, 0)``. + #[staticmethod] + fn unit_y() -> Self { + Self(crate::na::Vector3::y()) + } + /// Return the unit vector along the ``z`` axis, ``(0, 0, 1)``. + #[staticmethod] + fn unit_z() -> Self { + Self(crate::na::Vector3::z()) + } + + /// Build a :class:`Vec3` from a 3-element tuple or list. + #[staticmethod] + fn from_tuple(t: PyVector) -> Self { + Self(t.0) + } + /// Build a :class:`Vec3` from a length-3 NumPy array. + #[staticmethod] + fn from_ndarray(a: PyVector) -> Self { + Self(a.0) + } + + /// The ``x`` component of the vector. + #[getter] + fn x(&self) -> Real { + self.0.x + } + /// The ``y`` component of the vector. + #[getter] + fn y(&self) -> Real { + self.0.y + } + /// The ``z`` component of the vector. + #[getter] + fn z(&self) -> Real { + self.0.z + } + + /// Dot product with another vector. + /// + /// :param other: the right-hand side vector. + /// :returns: ``x1*x2 + y1*y2 + z1*z2``. + fn dot(&self, other: PyVector) -> Real { + self.0.dot(&other.0) + } + /// Cross product with another vector. + /// + /// :param other: the right-hand side vector. + /// :returns: a new :class:`Vec3` orthogonal to both inputs, + /// following the right-hand rule. + fn cross(&self, other: PyVector) -> Self { + Self(self.0.cross(&other.0)) + } + /// Euclidean (L2) norm, ``sqrt(x*x + y*y + z*z)``. + fn norm(&self) -> Real { + self.0.norm() + } + /// Squared Euclidean norm, ``x*x + y*y + z*z``. + /// + /// Cheaper than :meth:`norm` when only comparisons are needed. + fn norm_squared(&self) -> Real { + self.0.norm_squared() + } + /// Return a unit-length copy of this vector. + /// + /// If the vector is exactly zero, returns the zero vector + /// unchanged (no division by zero). + fn normalize(&self) -> Self { + let n = self.0.norm(); + if n == 0 as Real { + Self(self.0) + } else { + Self(self.0 / n) + } + } + /// Linear interpolation between ``self`` and ``other``. + /// + /// :param other: the target vector. + /// :param t: interpolation parameter (``0`` returns ``self``, + /// ``1`` returns ``other``). + fn lerp(&self, other: PyVector, t: Real) -> Self { + Self(self.0 * (1 as Real - t) + other.0 * t) + } + + /// Return the components as a Python tuple ``(x, y, z)``. + fn to_tuple(&self) -> (Real, Real, Real) { + (self.0.x, self.0.y, self.0.z) + } + /// Return the components as a length-3 NumPy array. + fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + PyArray1::from_slice_bound(py, &[self.0.x, self.0.y, self.0.z]) + } + + /// Exact bitwise equality with another vector. + /// + /// Unlike ``==`` (tolerance-based), requires bit-identical + /// component values. + fn bitwise_equal(&self, other: &Self) -> bool { + self.0 == other.0 + } + + /// Component-wise addition: ``self + other``. + fn __add__(&self, other: PyVector) -> Self { + Self(self.0 + other.0) + } + /// Component-wise subtraction: ``self - other``. + fn __sub__(&self, other: PyVector) -> Self { + Self(self.0 - other.0) + } + /// Scalar multiplication: ``self * s``. + fn __mul__(&self, s: Real) -> Self { + Self(self.0 * s) + } + /// Reflected scalar multiplication: ``s * self``. + fn __rmul__(&self, s: Real) -> Self { + Self(self.0 * s) + } + /// Scalar division: ``self / s``. + /// + /// :raises ValueError: if ``s`` is zero. + fn __truediv__(&self, s: Real) -> PyResult { + if s == 0 as Real { + return Err(PyValueError::new_err("division by zero")); + } + Ok(Self(self.0 / s)) + } + /// Unary negation: ``-self``. + fn __neg__(&self) -> Self { + Self(-self.0) + } + + /// Number of components (always ``3``). + fn __len__(&self) -> usize { + 3 + } + /// Component access by index ``0``, ``1`` or ``2`` (negative + /// indices supported). + /// + /// :raises IndexError: if ``idx`` is out of range. + fn __getitem__(&self, idx: isize) -> PyResult { + let i = if idx < 0 { idx + 3 } else { idx }; + match i { + 0 => Ok(self.0.x), + 1 => Ok(self.0.y), + 2 => Ok(self.0.z), + _ => Err(pyo3::exceptions::PyIndexError::new_err( + "Vec3 index out of range", + )), + } + } + /// Iterate over the components in the order ``x``, ``y``, ``z``. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let py = slf.py(); + Py::new(py, Vec3Iter { v: slf.0, i: 0 }) + } + + /// Human-readable representation, e.g. ``Vec3(1, 2, 3)``. + fn __repr__(&self) -> String { + use crate::math::ReprPrecision; + format!( + "Vec3({}, {}, {})", + self.0.x.fmt_repr(), + self.0.y.fmt_repr(), + self.0.z.fmt_repr() + ) + } + + /// Tolerant equality (Euclidean distance ≤ default epsilon). + /// Only ``==`` and ``!=`` are supported. + /// + /// :raises TypeError: for ordering comparisons. + fn __richcmp__(&self, other: PyVector, op: CompareOp) -> PyResult { + use crate::math::DefaultEpsilon; + let eps: Real = Real::default_epsilon(); + let close = (self.0 - other.0).norm() <= eps; + match op { + CompareOp::Eq => Ok(close), + CompareOp::Ne => Ok(!close), + _ => Err(PyTypeError::new_err("Vec3 supports only == and !=")), + } + } +} + +/// Iterator yielding the components of a :class:`Vec3` in +/// ``x``, ``y``, ``z`` order. +#[pyclass] +pub struct Vec3Iter { + v: crate::na::Vector3, + i: usize, +} +#[pymethods] +impl Vec3Iter { + /// Return ``self`` (iterators are their own iterators). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Yield the next component, or stop after ``z``. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + let r = match slf.i { + 0 => Some(slf.v.x), + 1 => Some(slf.v.y), + 2 => Some(slf.v.z), + _ => None, + }; + if r.is_some() { + slf.i += 1; + } + r + } +} + +// ------------------------------------------------------------------ +// Point3 +// ------------------------------------------------------------------ +/// A point in 3D affine space ``(x, y, z)``. +/// +/// Conceptually distinct from :class:`Vec3`: a point represents a +/// location, a vector represents a displacement. Subtracting two +/// points yields a :class:`Vec3`; adding a :class:`Vec3` to a point +/// yields a new point. +/// +/// Wraps :class:`nalgebra::Point3`. +/// +/// Example:: +/// +/// from rapier3d import Point3, Vec3 +/// a = Point3(1.0, 2.0, 3.0) +/// b = a + Vec3(0.0, 0.0, 1.0) # Point3(1, 2, 4) +/// d = b - a # Vec3(0, 0, 1) +#[pyclass(name = "Point3", module = "rapier.dim3", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Point3(pub crate::na::Point3); + +#[pymethods] +impl Point3 { + /// Create a new 3D point from its coordinates. + /// + /// :param x: x coordinate. + /// :param y: y coordinate. + /// :param z: z coordinate. + #[new] + fn new(x: Real, y: Real, z: Real) -> Self { + Self(crate::na::Point3::new(x, y, z)) + } + /// Return the origin, ``Point3(0, 0, 0)``. + #[staticmethod] + fn origin() -> Self { + Self(crate::na::Point3::origin()) + } + /// Build a :class:`Point3` from a 3-element tuple or list. + #[staticmethod] + fn from_tuple(t: PyPoint) -> Self { + Self(t.0) + } + /// Build a :class:`Point3` from a length-3 NumPy array. + #[staticmethod] + fn from_ndarray(a: PyPoint) -> Self { + Self(a.0) + } + + /// The ``x`` coordinate of the point. + #[getter] + fn x(&self) -> Real { + self.0.x + } + /// The ``y`` coordinate of the point. + #[getter] + fn y(&self) -> Real { + self.0.y + } + /// The ``z`` coordinate of the point. + #[getter] + fn z(&self) -> Real { + self.0.z + } + + /// The coordinates of the point as a :class:`Vec3` from origin. + #[getter] + fn coords(&self) -> Vec3 { + Vec3(self.0.coords) + } + + /// Return the coordinates as a Python tuple ``(x, y, z)``. + fn to_tuple(&self) -> (Real, Real, Real) { + (self.0.x, self.0.y, self.0.z) + } + /// Return the coordinates as a length-3 NumPy array. + fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + PyArray1::from_slice_bound(py, &[self.0.x, self.0.y, self.0.z]) + } + + /// Linear interpolation between two points. + /// + /// :param other: the target point. + /// :param t: interpolation parameter (``0`` returns ``self``, + /// ``1`` returns ``other``). + fn lerp(&self, other: PyPoint, t: Real) -> Self { + Self(crate::na::Point3::from( + self.0.coords * (1 as Real - t) + other.0.coords * t, + )) + } + + /// Exact bitwise equality with another point. + fn bitwise_equal(&self, other: &Self) -> bool { + self.0 == other.0 + } + + /// Translate by a vector: ``point + vector`` returns a new + /// :class:`Point3`. + fn __add__(&self, v: PyVector) -> Self { + Self(self.0 + v.0) + } + + /// Subtraction. ``point - point`` returns a :class:`Vec3` + /// (displacement). ``point - vector`` returns a :class:`Point3` + /// (translation by ``-vector``). + fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let py = other.py(); + if let Ok(p) = other.extract::>() { + return Ok(Vec3(self.0 - p.0).into_py(py)); + } + if let Ok(p) = other.extract::() { + return Ok(Vec3(self.0 - p.0).into_py(py)); + } + let v: PyVector = other.extract()?; + Ok(Point3(self.0 - v.0).into_py(py)) + } + + /// Number of coordinates (always ``3``). + fn __len__(&self) -> usize { + 3 + } + /// Coordinate access by index ``0``, ``1`` or ``2`` (negative + /// indices supported). + /// + /// :raises IndexError: if ``idx`` is out of range. + fn __getitem__(&self, idx: isize) -> PyResult { + let i = if idx < 0 { idx + 3 } else { idx }; + match i { + 0 => Ok(self.0.x), + 1 => Ok(self.0.y), + 2 => Ok(self.0.z), + _ => Err(pyo3::exceptions::PyIndexError::new_err( + "Point3 index out of range", + )), + } + } + /// Iterate over the coordinates in the order ``x``, ``y``, ``z``. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let py = slf.py(); + Py::new(py, Point3Iter { p: slf.0, i: 0 }) + } + + /// Human-readable representation, e.g. ``Point3(1, 2, 3)``. + fn __repr__(&self) -> String { + use crate::math::ReprPrecision; + format!( + "Point3({}, {}, {})", + self.0.x.fmt_repr(), + self.0.y.fmt_repr(), + self.0.z.fmt_repr() + ) + } + + /// Tolerant equality (Euclidean distance ≤ default epsilon). + /// Only ``==`` and ``!=`` are supported. + /// + /// :raises TypeError: for ordering comparisons. + fn __richcmp__(&self, other: PyPoint, op: CompareOp) -> PyResult { + use crate::math::DefaultEpsilon; + let eps: Real = Real::default_epsilon(); + let close = (self.0 - other.0).norm() <= eps; + match op { + CompareOp::Eq => Ok(close), + CompareOp::Ne => Ok(!close), + _ => Err(PyTypeError::new_err("Point3 supports only == and !=")), + } + } +} + +/// Iterator yielding the coordinates of a :class:`Point3` in +/// ``x``, ``y``, ``z`` order. +#[pyclass] +pub struct Point3Iter { + p: crate::na::Point3, + i: usize, +} +#[pymethods] +impl Point3Iter { + /// Return ``self`` (iterators are their own iterators). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Yield the next coordinate, or stop after ``z``. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + let r = match slf.i { + 0 => Some(slf.p.x), + 1 => Some(slf.p.y), + 2 => Some(slf.p.z), + _ => None, + }; + if r.is_some() { + slf.i += 1; + } + r + } +} + +// ------------------------------------------------------------------ +// Rotation3 (alias Quaternion) +// ------------------------------------------------------------------ +/// A 3D rotation, represented internally as a unit quaternion. +/// +/// Encodes a rotation about an arbitrary axis in 3D space. Unit +/// quaternions avoid gimbal lock and are numerically friendly under +/// composition. The double-cover property (``q`` and ``-q`` encode +/// the same orientation) is accounted for in equality comparisons. +/// +/// Also exposed under the Python alias ``Quaternion``. +/// +/// Wraps :class:`nalgebra::UnitQuaternion`. +/// +/// Construct with :meth:`identity`, :meth:`from_axis_angle`, +/// :meth:`from_euler_angles`, :meth:`from_scaled_axis`, +/// :meth:`from_quaternion`, or the look-at builders. The raw +/// constructor takes quaternion coefficients ``(w, x, y, z)``. +/// +/// Example:: +/// +/// from math import pi +/// from rapier3d import Rotation3, Vec3 +/// r = Rotation3.from_axis_angle(Vec3.unit_z(), pi / 2) +/// v = r * Vec3(1.0, 0.0, 0.0) # ~Vec3(0, 1, 0) +#[pyclass(name = "Rotation3", module = "rapier.dim3", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Rotation3(pub crate::na::UnitQuaternion); + +#[pymethods] +impl Rotation3 { + /// Build a unit quaternion from raw coefficients ``(w, x, y, z)``. + /// + /// The input is normalized internally, so the supplied values do + /// not need to lie on the unit sphere. Default arguments form + /// the identity rotation. + /// + /// :param w: real part. + /// :param x: i (x-axis imaginary) part. + /// :param y: j (y-axis imaginary) part. + /// :param z: k (z-axis imaginary) part. + #[new] + #[pyo3(signature = (w=1.0 as Real, x=0.0 as Real, y=0.0 as Real, z=0.0 as Real))] + fn new(w: Real, x: Real, y: Real, z: Real) -> Self { + let q = crate::na::Quaternion::new(w, x, y, z); + Self(crate::na::UnitQuaternion::from_quaternion(q)) + } + + /// Return the identity rotation (zero rotation angle). + #[staticmethod] + fn identity() -> Self { + Self(crate::na::UnitQuaternion::identity()) + } + + /// Build a rotation from an axis-angle pair. + /// + /// :param axis: a non-zero rotation axis. It will be normalized. + /// :param angle: rotation angle in radians (right-hand rule). + /// :raises ValueError: if ``axis`` is the zero vector. + #[staticmethod] + fn from_axis_angle(axis: PyVector, angle: Real) -> PyResult { + let v = axis.0; + if v.norm() == 0 as Real { + return Err(PyValueError::new_err("axis must be non-zero")); + } + let unit = crate::na::Unit::new_normalize(v); + Ok(Self(crate::na::UnitQuaternion::from_axis_angle( + &unit, angle, + ))) + } + + /// Build a rotation from Tait-Bryan ``XYZ`` Euler angles in + /// radians. + /// + /// :param roll: rotation about the local x-axis. + /// :param pitch: rotation about the local y-axis. + /// :param yaw: rotation about the local z-axis. + #[staticmethod] + fn from_euler_angles(roll: Real, pitch: Real, yaw: Real) -> Self { + Self(crate::na::UnitQuaternion::from_euler_angles( + roll, pitch, yaw, + )) + } + + /// Build a rotation from a scaled axis-angle vector + /// ``axis * angle``. + /// + /// The vector's direction is the rotation axis; its magnitude is + /// the rotation angle in radians. + #[staticmethod] + fn from_scaled_axis(v: PyVector) -> Self { + Self(crate::na::UnitQuaternion::from_scaled_axis(v.0)) + } + + /// Build a rotation directly from quaternion coefficients + /// ``(w, x, y, z)``. Input is normalized. + #[staticmethod] + fn from_quaternion(w: Real, x: Real, y: Real, z: Real) -> Self { + let q = crate::na::Quaternion::new(w, x, y, z); + Self(crate::na::UnitQuaternion::from_quaternion(q)) + } + + /// Left-handed look-at rotation: aligns the local ``-z`` axis to + /// ``dir`` and the local ``y`` axis as close to ``up`` as + /// possible. + #[staticmethod] + fn look_at_lh(dir: PyVector, up: PyVector) -> Self { + Self(crate::na::UnitQuaternion::look_at_lh(&dir.0, &up.0)) + } + /// Right-handed look-at rotation: aligns the local ``z`` axis to + /// ``dir`` and the local ``y`` axis as close to ``up`` as + /// possible. + #[staticmethod] + fn look_at_rh(dir: PyVector, up: PyVector) -> Self { + Self(crate::na::UnitQuaternion::look_at_rh(&dir.0, &up.0)) + } + /// Rotation whose local ``z`` axis points along ``dir`` and + /// local ``y`` axis aligns with ``up`` as closely as possible. + #[staticmethod] + fn face_toward(dir: PyVector, up: PyVector) -> Self { + Self(crate::na::UnitQuaternion::face_towards(&dir.0, &up.0)) + } + + /// Build a :class:`Rotation3` from a 4-element tuple + /// ``(x, y, z, w)``. + #[staticmethod] + fn from_tuple(t: PyRotation) -> Self { + Self(t.0) + } + /// Build a :class:`Rotation3` from a length-4 NumPy array + /// ``[x, y, z, w]``. + #[staticmethod] + fn from_ndarray(a: PyRotation) -> Self { + Self(a.0) + } + + /// Unit rotation axis as a :class:`Vec3`, or ``None`` if the + /// rotation is the identity (no well-defined axis). + #[getter] + fn axis(&self) -> Option { + self.0.axis().map(|u| Vec3(u.into_inner())) + } + /// Rotation angle in radians, in ``[0, pi]``. + #[getter] + fn angle(&self) -> Real { + self.0.angle() + } + /// Scaled axis ``axis * angle`` as a :class:`Vec3`. + #[getter] + fn scaled_axis(&self) -> Vec3 { + Vec3(self.0.scaled_axis()) + } + /// Tait-Bryan ``XYZ`` Euler angles as ``(roll, pitch, yaw)`` in + /// radians. + #[getter] + fn euler_angles(&self) -> (Real, Real, Real) { + self.0.euler_angles() + } + /// Quaternion as `(x, y, z, w)`. + #[getter] + fn quaternion(&self) -> (Real, Real, Real, Real) { + let q = self.0.quaternion(); + (q.i, q.j, q.k, q.w) + } + /// Real (``w``) part of the underlying quaternion. + #[getter] + fn w(&self) -> Real { + self.0.quaternion().w + } + /// First imaginary (``i`` / ``x``) part of the quaternion. + #[getter] + fn i(&self) -> Real { + self.0.quaternion().i + } + /// Second imaginary (``j`` / ``y``) part of the quaternion. + #[getter] + fn j(&self) -> Real { + self.0.quaternion().j + } + /// Third imaginary (``k`` / ``z``) part of the quaternion. + #[getter] + fn k(&self) -> Real { + self.0.quaternion().k + } + + /// Return the inverse rotation (equivalent to the quaternion + /// conjugate for unit quaternions). + fn inverse(&self) -> Self { + Self(self.0.inverse()) + } + + /// Return the equivalent 3x3 rotation matrix, row-major + /// flattened into a length-9 NumPy array. + fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + let m = self.0.to_rotation_matrix(); + let mm = m.matrix(); + let mut out = [0 as Real; 9]; + for r in 0..3 { + for c in 0..3 { + out[r * 3 + c] = mm[(r, c)]; + } + } + PyArray1::from_slice_bound(py, &out) + } + + /// Returns the quaternion as `(x, y, z, w)`. + /// + /// This matches `PyRotation`'s `FromPyObject` impl, so any Rust + /// API that takes `PyRotation` accepts a `Rotation3` instance. + fn to_tuple(&self) -> (Real, Real, Real, Real) { + let q = self.0.quaternion(); + (q.i, q.j, q.k, q.w) + } + + /// Rotate a point about the origin and return a new + /// :class:`Point3`. + fn transform_point(&self, p: PyPoint) -> Point3 { + Point3(self.0 * p.0) + } + /// Rotate a vector and return a new :class:`Vec3`. + fn transform_vector(&self, v: PyVector) -> Vec3 { + Vec3(self.0 * v.0) + } + + /// Spherical-linear interpolation between two rotations. + /// + /// :param other: the target rotation. + /// :param t: interpolation parameter in ``[0, 1]``. + fn slerp(&self, other: PyRotation, t: Real) -> Self { + Self(self.0.slerp(&other.0, t)) + } + + /// Exact bitwise equality with another rotation. + fn bitwise_equal(&self, other: &Self) -> bool { + self.0 == other.0 + } + + /// Multiplication ``self * rhs``. Composes / transforms depending + /// on the right-hand side: + /// + /// - ``Rotation3 * Rotation3`` → composed :class:`Rotation3`. + /// - ``Rotation3 * Point3`` → rotated :class:`Point3`. + /// - ``Rotation3 * Vec3`` → rotated :class:`Vec3`. + fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { + let py = rhs.py(); + if let Ok(r) = rhs.extract::>() { + return Ok(Rotation3(self.0 * r.0).into_py(py)); + } + if let Ok(r) = rhs.extract::() { + return Ok(Rotation3(self.0 * r.0).into_py(py)); + } + if let Ok(p) = rhs.extract::>() { + return Ok(Point3(self.0 * p.0).into_py(py)); + } + if let Ok(p) = rhs.extract::() { + return Ok(Point3(self.0 * p.0).into_py(py)); + } + let v: PyVector = rhs.extract()?; + Ok(Vec3(self.0 * v.0).into_py(py)) + } + + /// Human-readable representation, e.g. + /// ``Rotation3(w=1, x=0, y=0, z=0)``. + fn __repr__(&self) -> String { + use crate::math::ReprPrecision; + let q = self.0.quaternion(); + format!( + "Rotation3(w={}, x={}, y={}, z={})", + q.w.fmt_repr(), + q.i.fmt_repr(), + q.j.fmt_repr(), + q.k.fmt_repr() + ) + } + + /// Tolerant equality on quaternion coefficients. Accounts for + /// the quaternion double cover, so ``q`` and ``-q`` compare + /// equal. Only ``==`` and ``!=`` are supported. + /// + /// :raises TypeError: for ordering comparisons. + fn __richcmp__(&self, other: PyRotation, op: CompareOp) -> PyResult { + use crate::math::DefaultEpsilon; + let eps: Real = Real::default_epsilon(); + let a = self.0.quaternion(); + let b = other.0.quaternion(); + // q == -q for orientation; check both. + let close_pos = (a.w - b.w).abs() <= eps + && (a.i - b.i).abs() <= eps + && (a.j - b.j).abs() <= eps + && (a.k - b.k).abs() <= eps; + let close_neg = (a.w + b.w).abs() <= eps + && (a.i + b.i).abs() <= eps + && (a.j + b.j).abs() <= eps + && (a.k + b.k).abs() <= eps; + let close = close_pos || close_neg; + match op { + CompareOp::Eq => Ok(close), + CompareOp::Ne => Ok(!close), + _ => Err(PyTypeError::new_err("Rotation3 supports only == and !=")), + } + } +} + +// ------------------------------------------------------------------ +// Isometry3 +// ------------------------------------------------------------------ +/// A 3D rigid-body transformation: a rotation followed by a +/// translation. +/// +/// Equivalently, an element of the special Euclidean group ``SE(3)``. +/// Used throughout Rapier to encode the pose of a rigid body or a +/// collider relative to its parent. Composition is right-to-left: +/// ``A * B`` applies ``B`` first then ``A``. +/// +/// Wraps :class:`nalgebra::Isometry3`. +/// +/// Example:: +/// +/// from math import pi +/// from rapier3d import Isometry3, Vec3, Rotation3 +/// pose = Isometry3( +/// Vec3(0.0, 1.0, 0.0), +/// Rotation3.from_axis_angle(Vec3.unit_y(), pi / 2), +/// ) +#[pyclass(name = "Isometry3", module = "rapier.dim3", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Isometry3(pub crate::na::Isometry3); + +#[pymethods] +impl Isometry3 { + /// Create an isometry from an optional translation and rotation. + /// + /// Both arguments default to identity (zero translation, + /// identity rotation). + /// + /// :param translation: a :class:`Vec3` (or tuple / ndarray). + /// :param rotation: a :class:`Rotation3` (or + /// ``(x, y, z, w)``-style input). + #[new] + #[pyo3(signature = (translation=None, rotation=None))] + fn new(translation: Option, rotation: Option) -> Self { + let t = translation + .map(|v| v.0) + .unwrap_or_else(crate::na::Vector3::zeros); + let r = rotation + .map(|r| r.0) + .unwrap_or_else(crate::na::UnitQuaternion::identity); + Self(crate::na::Isometry3::from_parts( + crate::na::Translation3::from(t), + r, + )) + } + + /// Return the identity isometry (no translation, no rotation). + #[staticmethod] + fn identity() -> Self { + Self(crate::na::Isometry3::identity()) + } + + /// Translation-only constructor. + /// + /// Named `from_translation` (not `translation`) so it doesn't shadow + /// the `translation` property used to read the translation vector. + #[staticmethod] + fn from_translation(x: Real, y: Real, z: Real) -> Self { + Self(crate::na::Isometry3::translation(x, y, z)) + } + + /// Rotation-only constructor from a scaled-axis-angle (axis * angle). + #[staticmethod] + fn from_rotation(scaled_axis: PyVector) -> Self { + Self(crate::na::Isometry3::rotation(scaled_axis.0)) + } + + /// Build an isometry whose local ``z`` axis points along + /// ``target - eye`` and whose local ``y`` aligns with ``up`` as + /// closely as possible. + /// + /// :param eye: the origin of the isometry. + /// :param target: the point being looked at. + /// :param up: the desired up direction. + #[staticmethod] + fn face_toward(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { + Self(crate::na::Isometry3::face_towards( + &eye.0, &target.0, &up.0, + )) + } + + /// Left-handed look-at view isometry: maps world space into a + /// camera frame whose ``-z`` looks at ``target`` from ``eye``. + #[staticmethod] + fn look_at_lh(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { + Self(crate::na::Isometry3::look_at_lh(&eye.0, &target.0, &up.0)) + } + /// Right-handed look-at view isometry: maps world space into a + /// camera frame whose ``z`` looks at ``target`` from ``eye``. + #[staticmethod] + fn look_at_rh(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { + Self(crate::na::Isometry3::look_at_rh(&eye.0, &target.0, &up.0)) + } + + /// Translation component of the isometry as a :class:`Vec3`. + #[getter] + fn translation(&self) -> Vec3 { + Vec3(self.0.translation.vector) + } + /// Rotation component of the isometry as a :class:`Rotation3`. + #[getter] + fn rotation(&self) -> Rotation3 { + Rotation3(self.0.rotation) + } + + /// Return the inverse isometry, such that + /// ``self * self.inverse()`` is the identity. + fn inverse(&self) -> Self { + Self(self.0.inverse()) + } + + /// Apply the full transformation (rotation then translation) + /// to a point. + fn transform_point(&self, p: PyPoint) -> Point3 { + Point3(self.0 * p.0) + } + /// Apply only the rotation part to a vector. Vectors are not + /// translated. + fn transform_vector(&self, v: PyVector) -> Vec3 { + Vec3(self.0 * v.0) + } + + /// Return the equivalent 4x4 homogeneous matrix, row-major + /// flattened into a length-16 NumPy array. + fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + let m = self.0.to_homogeneous(); + let mut out = [0 as Real; 16]; + for r in 0..4 { + for c in 0..4 { + out[r * 4 + c] = m[(r, c)]; + } + } + PyArray1::from_slice_bound(py, &out) + } + + /// Interpolate between two isometries: linear interpolation on + /// the translation, slerp on the rotation. + /// + /// :param other: the target isometry. + /// :param t: interpolation parameter in ``[0, 1]``. + fn lerp_slerp(&self, other: &Self, t: Real) -> Self { + Self(self.0.lerp_slerp(&other.0, t)) + } + + /// Exact bitwise equality with another isometry. + fn bitwise_equal(&self, other: &Self) -> bool { + self.0 == other.0 + } + + /// Multiplication ``self * rhs``. Composes / transforms depending + /// on the right-hand side: + /// + /// - ``Isometry3 * Isometry3`` → composed :class:`Isometry3`. + /// - ``Isometry3 * Point3`` → transformed :class:`Point3`. + /// - ``Isometry3 * Vec3`` → rotated :class:`Vec3` (no + /// translation). + fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { + let py = rhs.py(); + if let Ok(i) = rhs.extract::>() { + return Ok(Isometry3(self.0 * i.0).into_py(py)); + } + if let Ok(i) = rhs.extract::() { + return Ok(Isometry3(self.0 * i.0).into_py(py)); + } + if let Ok(p) = rhs.extract::>() { + return Ok(Point3(self.0 * p.0).into_py(py)); + } + if let Ok(p) = rhs.extract::() { + return Ok(Point3(self.0 * p.0).into_py(py)); + } + let v: PyVector = rhs.extract()?; + Ok(Vec3(self.0 * v.0).into_py(py)) + } + + /// Human-readable representation showing translation and the + /// rotation as quaternion components. + fn __repr__(&self) -> String { + use crate::math::ReprPrecision; + let t = self.0.translation.vector; + let q = self.0.rotation.quaternion(); + format!( + "Isometry3(translation=({}, {}, {}), rotation=(w={}, x={}, y={}, z={}))", + t.x.fmt_repr(), + t.y.fmt_repr(), + t.z.fmt_repr(), + q.w.fmt_repr(), + q.i.fmt_repr(), + q.j.fmt_repr(), + q.k.fmt_repr() + ) + } + + /// Tolerant equality. Both translation distance and the + /// quaternion components must be within a default epsilon + /// (accounting for the quaternion double cover). Only ``==`` + /// and ``!=`` are supported. + /// + /// :raises TypeError: for ordering comparisons. + fn __richcmp__(&self, other: PyIsometry, op: CompareOp) -> PyResult { + use crate::math::DefaultEpsilon; + let eps: Real = Real::default_epsilon(); + let t_close = + (self.0.translation.vector - other.0.translation.vector).norm() <= eps; + let a = self.0.rotation.quaternion(); + let b = other.0.rotation.quaternion(); + let r_close_pos = (a.w - b.w).abs() <= eps + && (a.i - b.i).abs() <= eps + && (a.j - b.j).abs() <= eps + && (a.k - b.k).abs() <= eps; + let r_close_neg = (a.w + b.w).abs() <= eps + && (a.i + b.i).abs() <= eps + && (a.j + b.j).abs() <= eps + && (a.k + b.k).abs() <= eps; + let close = t_close && (r_close_pos || r_close_neg); + match op { + CompareOp::Eq => Ok(close), + CompareOp::Ne => Ok(!close), + _ => Err(PyTypeError::new_err("Isometry3 supports only == and !=")), + } + } +} + +// ------------------------------------------------------------------ +// Free helper: rotation_from_angle +// +// In 3D the "angular vector" is a scaled axis-angle. This wraps +// `rapier::math::rotation_from_angle` semantics. +// ------------------------------------------------------------------ +/// Build a :class:`Rotation3` from a scaled-axis-angle vector. +/// +/// Mirrors the dimension-agnostic Rust helper +/// ``rapier::math::rotation_from_angle``; in 3D the "angular vector" +/// is ``axis * angle`` (its direction is the axis, its magnitude is +/// the rotation angle in radians). +/// +/// :param v: scaled-axis-angle vector. +#[pyfunction] +fn rotation_from_angle(v: PyVector) -> Rotation3 { + Rotation3(crate::na::UnitQuaternion::from_scaled_axis(v.0)) +} + +// ------------------------------------------------------------------ +// register_math +// ------------------------------------------------------------------ +pub fn register_math(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // Alias `Quaternion` -> Rotation3. + m.add("Quaternion", py.get_type_bound::())?; + // `AngVector3` is a Python-level alias for `Vec3`. + m.add("AngVector3", py.get_type_bound::())?; + m.add_function(crate::pyo3::wrap_pyfunction!(rotation_from_angle, m)?)?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/serde_io.rs b/python/rapier-py-3d/src/serde_io.rs new file mode 100644 index 000000000..252f07711 --- /dev/null +++ b/python/rapier-py-3d/src/serde_io.rs @@ -0,0 +1,134 @@ +//! Shared (dim/scalar-agnostic) helpers used by pickle/snapshot serialization. +//! +//! The on-disk format always begins with an 8-byte header: +//! - 4 bytes magic = `b"RPYS"` +//! - 4 bytes little-endian `u32` version (currently 1) +//! +//! Anything that follows is the bincode-serialized payload (or for the JSON +//! debug mode, a JSON object with a `"_rpys"` magic+version field, see below). +//! +//! Mismatched magic or unknown version => `SerializationError`. +//! +//! This module deliberately stays free of `rapier::...` paths so that it can +//! live in the dim/scalar-independent `rapier-py-core`. The dim/scalar-specific +//! glue (the `Py` round-trip on `RigidBody`, `Collider`, `RigidBodySet`, +//! etc., plus `PhysicsWorld.snapshot()`/`restore()`) is emitted by the +//! `define_serde_types!` macro, which is invoked once per cdylib. + +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +/// Magic header marking a rapier-py blob ("Rapier PYthon Snapshot"). +pub const MAGIC: &[u8; 4] = b"RPYS"; +/// Current on-disk format version. Bumped on incompatible payload changes. +pub const VERSION: u32 = 1; +/// Total size of the binary header (magic + u32 version). +pub const HEADER_SIZE: usize = 8; + +/// Prepend the 8-byte binary header to `payload`. +#[inline] +pub fn wrap_bincode(payload: Vec) -> Vec { + let mut out = Vec::with_capacity(HEADER_SIZE + payload.len()); + out.extend_from_slice(MAGIC); + out.extend_from_slice(&VERSION.to_le_bytes()); + out.extend_from_slice(&payload); + out +} + +/// Strip and validate the 8-byte binary header. Returns the body. +/// +/// Returns `SerializationError` on: +/// - Blob shorter than the header. +/// - Magic mismatch (`!= b"RPYS"`). +/// - Unsupported version (`!= VERSION`). +pub fn unwrap_bincode(blob: &[u8]) -> PyResult<&[u8]> { + if blob.len() < HEADER_SIZE { + return Err(crate::errors::SerializationError::new_err(format!( + "serialized blob too short ({} bytes); expected at least {}", + blob.len(), + HEADER_SIZE + ))); + } + if &blob[..4] != MAGIC { + return Err(crate::errors::SerializationError::new_err(format!( + "bad magic in serialized blob: expected {:?} got {:?}", + std::str::from_utf8(MAGIC).unwrap_or("?"), + std::str::from_utf8(&blob[..4]).unwrap_or("") + ))); + } + let mut ver_bytes = [0u8; 4]; + ver_bytes.copy_from_slice(&blob[4..8]); + let ver = u32::from_le_bytes(ver_bytes); + if ver != VERSION { + return Err(crate::errors::SerializationError::new_err(format!( + "unsupported serialization version {} (this build expects {})", + ver, VERSION + ))); + } + Ok(&blob[HEADER_SIZE..]) +} + +/// Convert any `bincode::Error` into a `SerializationError`. Used in the +/// `to_bytes`/`from_bytes` impls emitted by the macro. +#[inline] +pub fn bincode_err(e: bincode::Error) -> PyErr { + crate::errors::SerializationError::new_err(format!("bincode error: {}", e)) +} + +/// Same idea for `serde_json::Error`. +#[inline] +pub fn json_err(e: serde_json::Error) -> PyErr { + crate::errors::SerializationError::new_err(format!("json error: {}", e)) +} + +/// Bytes-from-borrow helper: wrap a `&[u8]` view into a Python `bytes`. +#[inline] +pub fn bytes_to_py<'py>(py: Python<'py>, blob: &[u8]) -> Bound<'py, PyBytes> { + PyBytes::new_bound(py, blob) +} + +/// Magic + version embedded in the JSON envelope as plain string/integer +/// fields so the file is self-describing for human readers. +pub const JSON_MAGIC: &str = "RPYS"; + +/// Build the JSON envelope `{"_magic": "RPYS", "_version": 1, "payload": }`. +pub fn wrap_json(payload: serde_json::Value) -> serde_json::Value { + serde_json::json!({ + "_magic": JSON_MAGIC, + "_version": VERSION, + "payload": payload, + }) +} + +/// Validate + extract the inner payload from the JSON envelope. +pub fn unwrap_json(envelope: serde_json::Value) -> PyResult { + let obj = envelope.as_object().ok_or_else(|| { + crate::errors::SerializationError::new_err( + "expected JSON object envelope, got something else", + ) + })?; + let magic = obj.get("_magic").and_then(|v| v.as_str()).ok_or_else(|| { + crate::errors::SerializationError::new_err("JSON envelope missing '_magic' field") + })?; + if magic != JSON_MAGIC { + return Err(crate::errors::SerializationError::new_err(format!( + "bad JSON magic: expected {:?} got {:?}", + JSON_MAGIC, magic + ))); + } + let ver = obj + .get("_version") + .and_then(|v| v.as_u64()) + .ok_or_else(|| { + crate::errors::SerializationError::new_err("JSON envelope missing '_version' field") + })?; + if ver as u32 != VERSION { + return Err(crate::errors::SerializationError::new_err(format!( + "unsupported JSON serialization version {} (expected {})", + ver, VERSION + ))); + } + obj.get("payload").cloned().ok_or_else(|| { + crate::errors::SerializationError::new_err("JSON envelope missing 'payload'") + }) +} diff --git a/python/rapier-testbed/README.md b/python/rapier-testbed/README.md index c20ad7e17..9be559c8e 100644 --- a/python/rapier-testbed/README.md +++ b/python/rapier-testbed/README.md @@ -1,9 +1,9 @@ # rapier-testbed A [Panda3D](https://www.panda3d.org/)-based visual testbed and example gallery -for the [Rapier](https://rapier.rs) Python bindings — ~127 examples ported from -the Rust `examples2d/` and `examples3d/`. It drives both the 2D and 3D (f32) -engines, so it depends on `rapier2d` and `rapier3d`. +for the [Rapier](https://rapier.rs) Python bindings — examples ported from +the Rust `examples3d/`. It drives the 3D (f32) engine, so it depends on +`rapier3d`. ## Install @@ -31,9 +31,8 @@ invoke the installed `rapier_testbed` module rather than a path: rapier-testbed python -m rapier_testbed # equivalent -# Jump straight into one example (3D under `examples3`, 2D under `examples2`): +# Jump straight into one example (under `examples3`): python -m rapier_testbed.examples3.domino3 -python -m rapier_testbed.examples2.pyramid2 # Headless — run a fixed number of steps with no window (no display needed; # used by the regression tests): diff --git a/python/rapier-testbed/pyproject.toml b/python/rapier-testbed/pyproject.toml index 442153870..5bd195cac 100644 --- a/python/rapier-testbed/pyproject.toml +++ b/python/rapier-testbed/pyproject.toml @@ -18,9 +18,8 @@ classifiers = [ "Topic :: Scientific/Engineering :: Physics", ] requires-python = ">=3.9" -# The testbed drives both the 2D and 3D (f32) engines, so it depends on both. +# The testbed drives the 3D (f32) engine. dependencies = [ - "rapier2d", "rapier3d", "numpy>=1.21", "panda3d>=1.10", diff --git a/python/rapier-testbed/rapier_testbed/__init__.py b/python/rapier-testbed/rapier_testbed/__init__.py index 20b072a30..f35062a92 100644 --- a/python/rapier-testbed/rapier_testbed/__init__.py +++ b/python/rapier-testbed/rapier_testbed/__init__.py @@ -56,8 +56,7 @@ def _autoload_bundled_examples() -> None: """ import importlib - # New examples ported from ``examples3d/`` / ``examples2d/`` should - # extend this list. + # New examples ported from ``examples3d/`` should extend this list. _bundled = ( "rapier_testbed.examples3.primitives3", # ── 3D examples (first half) ──────────────────────────────── @@ -114,51 +113,6 @@ def _autoload_bundled_examples() -> None: "rapier_testbed.examples3.stress_tests.ray_cast3", "rapier_testbed.examples3.stress_tests.stacks3", "rapier_testbed.examples3.stress_tests.trimesh3", - # ── 2D examples ───────────────────────────────────────────── - "rapier_testbed.examples2.add_remove2", - "rapier_testbed.examples2.drum2", - "rapier_testbed.examples2.inv_pyramid2", - "rapier_testbed.examples2.platform2", - "rapier_testbed.examples2.pyramid2", - "rapier_testbed.examples2.sensor2", - "rapier_testbed.examples2.convex_polygons2", - "rapier_testbed.examples2.heightfield2", - "rapier_testbed.examples2.polyline2", - "rapier_testbed.examples2.trimesh2", - "rapier_testbed.examples2.voxels2", - "rapier_testbed.examples2.collision_groups2", - "rapier_testbed.examples2.one_way_platforms2", - "rapier_testbed.examples2.locked_rotations2", - "rapier_testbed.examples2.restitution2", - "rapier_testbed.examples2.damping2", - "rapier_testbed.examples2.ccd2", - "rapier_testbed.examples2.joints2", - "rapier_testbed.examples2.rope_joints2", - "rapier_testbed.examples2.pin_slot_joint2", - "rapier_testbed.examples2.joint_motor_position2", - "rapier_testbed.examples2.inverse_kinematics2", - "rapier_testbed.examples2.character_controller2", - "rapier_testbed.examples2.s2d_high_mass_ratio_1", - "rapier_testbed.examples2.s2d_high_mass_ratio_2", - "rapier_testbed.examples2.s2d_high_mass_ratio_3", - "rapier_testbed.examples2.s2d_confined", - "rapier_testbed.examples2.s2d_pyramid", - "rapier_testbed.examples2.s2d_card_house", - "rapier_testbed.examples2.s2d_arch", - "rapier_testbed.examples2.s2d_bridge", - "rapier_testbed.examples2.s2d_ball_and_chain", - "rapier_testbed.examples2.s2d_joint_grid", - "rapier_testbed.examples2.s2d_far_pyramid", - "rapier_testbed.examples2.stress_tests.balls2", - "rapier_testbed.examples2.stress_tests.boxes2", - "rapier_testbed.examples2.stress_tests.capsules2", - "rapier_testbed.examples2.stress_tests.convex_polygons2", - "rapier_testbed.examples2.stress_tests.heightfield2", - "rapier_testbed.examples2.stress_tests.pyramid2", - "rapier_testbed.examples2.stress_tests.vertical_stacks2", - "rapier_testbed.examples2.stress_tests.joint_ball2", - "rapier_testbed.examples2.stress_tests.joint_fixed2", - "rapier_testbed.examples2.stress_tests.joint_prismatic2", ) for mod in _bundled: importlib.import_module(mod) diff --git a/python/rapier-testbed/rapier_testbed/_meshes.py b/python/rapier-testbed/rapier_testbed/_meshes.py index 45b24ef3b..cd4b96376 100644 --- a/python/rapier-testbed/rapier_testbed/_meshes.py +++ b/python/rapier-testbed/rapier_testbed/_meshes.py @@ -35,9 +35,8 @@ _CYLINDER_SEGMENTS: int = 22 _CONE_SEGMENTS: int = 22 -# Numeric ShapeType codes (must match parry's ``ShapeType`` enum). The 2D -# and 3D bindings expose separate ``ShapeType`` enum classes that share -# these values; we compare via ``int(...)`` to stay dim-agnostic. +# Numeric ShapeType codes (must match parry's ``ShapeType`` enum). We +# compare via ``int(...)`` against these values. _ST_BALL: int = 0 _ST_CUBOID: int = 1 _ST_CAPSULE: int = 2 @@ -451,35 +450,6 @@ def _heightfield_mesh_3d(heights: np.ndarray, scale: Tuple[float, float, float]) return _trimesh_mesh(pts, idx) -def _heightfield_mesh_2d(heights: np.ndarray, scale: Tuple[float, float]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """2D heightfield = polyline; render as a strip of vertical quads from - ``y=h`` down to ``y=-large`` (we use ``y=0`` instead — just a polyline). - """ - h = np.asarray(heights, dtype=np.float32).reshape(-1) - if h.size < 2: - return (np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.uint32)) - sx, sy = float(scale[0]), float(scale[1]) - n = h.size - xs = (np.arange(n, dtype=np.float32) / (n - 1) - 0.5) * sx - ys = h * sy - # Render as a thin filled strip from y=ys down to y=0. - top = np.stack([xs, ys, np.zeros_like(xs)], axis=-1) - bot = np.stack([xs, np.zeros_like(xs), np.zeros_like(xs)], axis=-1) - pts = np.concatenate([top, bot], axis=0).astype(np.float32) - quad_i = np.arange(n - 1, dtype=np.uint32) - v_top = quad_i - v_top_next = quad_i + 1 - v_bot = quad_i + n - v_bot_next = quad_i + n + 1 - idx = np.stack([ - np.stack([v_top, v_bot, v_bot_next], axis=-1), - np.stack([v_top, v_bot_next, v_top_next], axis=-1), - ], axis=1).reshape(-1, 3).astype(np.uint32) - return _trimesh_mesh(pts, idx) - - def _convex_polyhedron_mesh( points: np.ndarray, indices: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: @@ -529,152 +499,44 @@ def _voxels_mesh( return pos, nrm, idx -def _disk_mesh_2d(radius: float, segments: int = 36) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """2D ball → filled disk in the z=0 plane, normal +Z.""" - n = max(6, int(segments)) - r = float(radius) - phi = np.arange(n, dtype=np.float32) * (2 * math.pi / n) - cx = (np.cos(phi) * r).astype(np.float32) - cy = (np.sin(phi) * r).astype(np.float32) - center = np.array([[0.0, 0.0, 0.0]], dtype=np.float32) - ring = np.stack([cx, cy, np.zeros_like(cx)], axis=-1) - pos = np.concatenate([center, ring], axis=0).astype(np.float32) - nrm = np.tile(np.array([[0.0, 0.0, 1.0]], dtype=np.float32), (pos.shape[0], 1)) - i_arr = np.arange(n, dtype=np.uint32) - idx = np.stack([ - np.zeros(n, dtype=np.uint32), - 1 + i_arr, - 1 + (i_arr + 1) % n, - ], axis=-1).astype(np.uint32) - return pos, nrm, idx - - -def _convex_polygon_mesh_2d(points: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """2D convex polygon → fan triangulation in the z=0 plane. - - Normals point along +Z (out of the screen) so the shape lights up - correctly under the default front-facing directional light. - """ - pts = np.asarray(points, dtype=np.float32).reshape(-1, 2) - n = pts.shape[0] - if n < 3: - return (np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.uint32)) - pos3 = np.stack([pts[:, 0], pts[:, 1], np.zeros(n, dtype=np.float32)], axis=-1) - nrm = np.tile(np.array([[0.0, 0.0, 1.0]], dtype=np.float32), (n, 1)) - i = np.arange(1, n - 1, dtype=np.uint32) - idx = np.stack([np.zeros_like(i), i, i + 1], axis=-1) - return pos3, nrm, idx - - -def _polyline_mesh_2d(vertices: np.ndarray, indices: np.ndarray | None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """2D polyline → thin extruded strip in the z=0 plane. - - We render the polyline as a flat strip 0.02 units wide (in world - units) so it's at least visible. For something fancier (per-segment - normals etc.) we'd need offset polygons; this is the simplest - approach. - """ - v = np.asarray(vertices, dtype=np.float32).reshape(-1, 2) - if v.shape[0] < 2: - return (np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.uint32)) - if indices is None: - idx = np.stack([np.arange(v.shape[0] - 1, dtype=np.uint32), - np.arange(1, v.shape[0], dtype=np.uint32)], axis=-1) - else: - idx = np.asarray(indices, dtype=np.uint32).reshape(-1, 2) - - # Render each segment as a thin quad in the z=0 plane. - half_w = 0.02 - verts_out: List[np.ndarray] = [] - tris_out: List[np.ndarray] = [] - cur_base = 0 - for seg_a, seg_b in idx: - a = v[seg_a] - b = v[seg_b] - d = b - a - L = float(np.linalg.norm(d)) - if L < 1e-9: - continue - d /= L - perp = np.array([-d[1], d[0]], dtype=np.float32) * half_w - p0 = np.array([a[0] - perp[0], a[1] - perp[1], 0.0], dtype=np.float32) - p1 = np.array([a[0] + perp[0], a[1] + perp[1], 0.0], dtype=np.float32) - p2 = np.array([b[0] + perp[0], b[1] + perp[1], 0.0], dtype=np.float32) - p3 = np.array([b[0] - perp[0], b[1] - perp[1], 0.0], dtype=np.float32) - verts_out.append(np.stack([p0, p1, p2, p3], axis=0)) - tris_out.append(np.array([[cur_base, cur_base + 1, cur_base + 2], - [cur_base, cur_base + 2, cur_base + 3]], dtype=np.uint32)) - cur_base += 4 - if not verts_out: - return (np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.float32), - np.empty((0, 3), dtype=np.uint32)) - positions = np.concatenate(verts_out, axis=0).astype(np.float32, copy=False) - indices_out = np.concatenate(tris_out, axis=0).astype(np.uint32, copy=False) - normals = np.tile(np.array([[0.0, 0.0, 1.0]], dtype=np.float32), (positions.shape[0], 1)) - return positions, normals, indices_out - - # --------------------------------------------------------------------------- # Dispatch + Geom assembly # --------------------------------------------------------------------------- -def _identity_iso(dim: int) -> Any: - """Return a fresh identity isometry for the requested dimension.""" - if dim == 2: - import rapier2d as rp2 - return rp2.Isometry2.identity() +def _identity_iso() -> Any: + """Return a fresh identity isometry.""" import rapier3d as rp3 return rp3.Isometry3.identity() -def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]]: +def _shape_to_arrays(shape: Any) -> List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]]: """Lower a SharedShape into a flat list of ``(local_pose, pos, nrm, idx)``. Compound shapes recurse and prepend their sub-shape poses; everything else returns a single entry whose pose is identity. """ out: List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]] = [] - # Use the integer code: the 2D and 3D ShapeType enums are different - # Python objects but share the same numeric values. stype = int(shape.shape_type) - iso_identity = _identity_iso(dim) + iso_identity = _identity_iso() if stype == _ST_BALL: ball = shape.as_ball() - if dim == 2: - # 2D ball = filled disk in the (x, 0, y) plane. - p, n, i = _disk_mesh_2d(ball.radius) - else: - p, n, i = _sphere_mesh(ball.radius) + p, n, i = _sphere_mesh(ball.radius) out.append((iso_identity, p, n, i)) return out if stype == _ST_CUBOID: cub = shape.as_cuboid() he = cub.half_extents - if dim == 2: - # 2D world lives in the z=0 plane; cuboid is a thin slab. - p, n, i = _cuboid_mesh(he.x, he.y, 0.01) - else: - p, n, i = _cuboid_mesh(he.x, he.y, he.z) + p, n, i = _cuboid_mesh(he.x, he.y, he.z) out.append((iso_identity, p, n, i)) return out if stype == _ST_CAPSULE: cap = shape.as_capsule() - if dim == 2: - # 2D world (x, y) → 3D (x, y, 0). - ax = (float(cap.a.x), float(cap.a.y), 0.0) - bx = (float(cap.b.x), float(cap.b.y), 0.0) - else: - ax = (float(cap.a.x), float(cap.a.y), float(cap.a.z)) - bx = (float(cap.b.x), float(cap.b.y), float(cap.b.z)) + ax = (float(cap.a.x), float(cap.a.y), float(cap.a.z)) + bx = (float(cap.b.x), float(cap.b.y), float(cap.b.z)) p, n, i = _capsule_mesh(ax, bx, cap.radius) out.append((iso_identity, p, n, i)) return out @@ -693,40 +555,16 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda if stype == _ST_TRIANGLE: tri = shape.as_triangle() - if dim == 2: - # 2D world (x, y) → 3D (x, y, 0). - a = (float(tri.a.x), float(tri.a.y), 0.0) - b = (float(tri.b.x), float(tri.b.y), 0.0) - c = (float(tri.c.x), float(tri.c.y), 0.0) - else: - a = (float(tri.a.x), float(tri.a.y), float(tri.a.z)) - b = (float(tri.b.x), float(tri.b.y), float(tri.b.z)) - c = (float(tri.c.x), float(tri.c.y), float(tri.c.z)) + a = (float(tri.a.x), float(tri.a.y), float(tri.a.z)) + b = (float(tri.b.x), float(tri.b.y), float(tri.b.z)) + c = (float(tri.c.x), float(tri.c.y), float(tri.c.z)) p, n, i = _triangle_mesh(a, b, c) out.append((iso_identity, p, n, i)) return out - if stype == _ST_SEGMENT: - # 2D-only; render as a tiny polyline strip. - if dim == 2: - import rapier2d as rp2 - shape_seg = shape.as_segment() - v = np.array([[shape_seg.a.x, shape_seg.a.y], [shape_seg.b.x, shape_seg.b.y]], dtype=np.float32) - p, n, i = _polyline_mesh_2d(v, None) - out.append((iso_identity, p, n, i)) - return out - # 3D segment isn't exposed via SharedShape constructors; fall through to AABB. - if stype == _ST_TRIMESH: tm = shape.as_trimesh() v = np.asarray(tm.vertices, dtype=np.float32) - if v.ndim == 2 and v.shape[1] == 2: - # 2D trimesh: lift into z=0 plane (x, y, 0). - v3 = np.empty((v.shape[0], 3), dtype=np.float32) - v3[:, 0] = v[:, 0] - v3[:, 1] = v[:, 1] - v3[:, 2] = 0.0 - v = v3 p, n, i = _trimesh_mesh(v, tm.indices) out.append((iso_identity, p, n, i)) return out @@ -735,14 +573,7 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda vx = shape.as_voxels() centers = np.asarray(vx.centers, dtype=np.float32) vs = np.asarray(vx.voxel_size, dtype=np.float32) - if dim == 2: - # 2D world (x, y) → 3D (x, y, 0); give a thin slab in z. - c3 = np.zeros((centers.shape[0], 3), dtype=np.float32) - c3[:, :2] = centers.reshape(-1, 2) - centers = c3 - half = (float(vs[0]) * 0.5, float(vs[1]) * 0.5, 0.01) - else: - half = (float(vs[0]) * 0.5, float(vs[1]) * 0.5, float(vs[2]) * 0.5) + half = (float(vs[0]) * 0.5, float(vs[1]) * 0.5, float(vs[2]) * 0.5) p, n, i = _voxels_mesh(centers, half) out.append((iso_identity, p, n, i)) return out @@ -750,14 +581,10 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda if stype == _ST_HEIGHTFIELD: hf = shape.as_heightfield() sc = hf.scale - if dim == 2: - heights = np.asarray(hf.heights, dtype=np.float32) - p, n, i = _heightfield_mesh_2d(heights, (float(sc.x), float(sc.y))) - else: - heights = np.asarray(hf.heights, dtype=np.float32) - p, n, i = _heightfield_mesh_3d( - heights, (float(sc.x), float(sc.y), float(sc.z)) - ) + heights = np.asarray(hf.heights, dtype=np.float32) + p, n, i = _heightfield_mesh_3d( + heights, (float(sc.x), float(sc.y), float(sc.z)) + ) out.append((iso_identity, p, n, i)) return out @@ -767,28 +594,12 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda out.append((iso_identity, p, n, i)) return out - if stype == _ST_CONVEX_POLYGON: - # 2D convex polygon. - cp = shape.as_convex_polygon() - p, n, i = _convex_polygon_mesh_2d(cp.points) - out.append((iso_identity, p, n, i)) - return out - - if stype == _ST_POLYLINE: - po = shape.as_polyline() - p, n, i = _polyline_mesh_2d(po.vertices, po.indices) - out.append((iso_identity, p, n, i)) - return out - if stype == _ST_COMPOUND: comp = shape.as_compound() for sub_iso, sub_shape in comp.shapes(): - for inner_iso, p, n, i in _shape_to_arrays(sub_shape, dim): + for inner_iso, p, n, i in _shape_to_arrays(sub_shape): # Compose sub_iso ∘ inner_iso. - if dim == 2: - pose = _compose_iso_2d(sub_iso, inner_iso) - else: - pose = _compose_iso_3d(sub_iso, inner_iso) + pose = _compose_iso_3d(sub_iso, inner_iso) out.append((pose, p, n, i)) return out @@ -796,22 +607,14 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda if stype == _ST_ROUND_CUBOID: cub = shape.as_round_cuboid() he = cub.half_extents - if dim == 2: - p, n, i = _cuboid_mesh(he.x, he.y, 0.01) - else: - p, n, i = _cuboid_mesh(he.x, he.y, he.z) + p, n, i = _cuboid_mesh(he.x, he.y, he.z) out.append((iso_identity, p, n, i)) return out if stype == _ST_ROUND_TRIANGLE: tri = shape.as_round_triangle() - if dim == 2: - a = (float(tri.a.x), float(tri.a.y), 0.0) - b = (float(tri.b.x), float(tri.b.y), 0.0) - c = (float(tri.c.x), float(tri.c.y), 0.0) - else: - a = (float(tri.a.x), float(tri.a.y), float(tri.a.z)) - b = (float(tri.b.x), float(tri.b.y), float(tri.b.z)) - c = (float(tri.c.x), float(tri.c.y), float(tri.c.z)) + a = (float(tri.a.x), float(tri.a.y), float(tri.a.z)) + b = (float(tri.b.x), float(tri.b.y), float(tri.b.z)) + c = (float(tri.c.x), float(tri.c.y), float(tri.c.z)) p, n, i = _triangle_mesh(a, b, c) out.append((iso_identity, p, n, i)) return out @@ -830,38 +633,24 @@ def _shape_to_arrays(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.nda p, n, i = _convex_polyhedron_mesh(ch.points, ch.indices) out.append((iso_identity, p, n, i)) return out - if stype == _ST_ROUND_CONVEX_POLYGON: - cp = shape.as_round_convex_polygon() - p, n, i = _convex_polygon_mesh_2d(cp.points) - out.append((iso_identity, p, n, i)) - return out - # HALFSPACE, VOXELS, CUSTOM — no triangulation available, draw the AABB. - return _aabb_fallback(shape, dim) + # HALFSPACE, SEGMENT, POLYLINE, CUSTOM — no triangulation available, + # draw the AABB. + return _aabb_fallback(shape) -def _aabb_fallback(shape: Any, dim: int) -> List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]]: +def _aabb_fallback(shape: Any) -> List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]]: """Approximate a shape with its AABB, baking offset into the geometry.""" - iso = _identity_iso(dim) + iso = _identity_iso() a = shape.compute_aabb(iso) - if dim == 2: - # 2D AABB has 2-component points; render as a flat slab in the z=0 - # plane (thin Z extent). - hx = float(a.half_extents.x) - hy = float(a.half_extents.y) - cx = float(a.center.x) - cy = float(a.center.y) - p, n, i = _cuboid_mesh(hx, hy, 0.01) - p = p + np.array([cx, cy, 0.0], dtype=np.float32) - else: - hx = float(a.half_extents.x) - hy = float(a.half_extents.y) - hz = float(a.half_extents.z) - cx = float(a.center.x) - cy = float(a.center.y) - cz = float(a.center.z) - p, n, i = _cuboid_mesh(hx, hy, hz) - p = p + np.array([cx, cy, cz], dtype=np.float32) + hx = float(a.half_extents.x) + hy = float(a.half_extents.y) + hz = float(a.half_extents.z) + cx = float(a.center.x) + cy = float(a.center.y) + cz = float(a.center.z) + p, n, i = _cuboid_mesh(hx, hy, hz) + p = p + np.array([cx, cy, cz], dtype=np.float32) return [(iso, p, n, i)] @@ -884,22 +673,6 @@ def _compose_iso_3d(outer: Any, inner: Any) -> Any: return rp3.Isometry3((float(trans[0]), float(trans[1]), float(trans[2])), rot) -def _compose_iso_2d(outer: Any, inner: Any) -> Any: - """Return ``outer * inner`` as a fresh Isometry2.""" - import rapier2d as rp2 - - a_o = float(outer.rotation.angle) - a_i = float(inner.rotation.angle) - t_i_x = float(inner.translation.x) - t_i_y = float(inner.translation.y) - # Outer rotation applied to inner translation, then plus outer translation. - c, s = math.cos(a_o), math.sin(a_o) - tx = float(outer.translation.x) + c * t_i_x - s * t_i_y - ty = float(outer.translation.y) + s * t_i_x + c * t_i_y - rot = rp2.Rotation2.from_angle(a_o + a_i) - return rp2.Isometry2((tx, ty), rot) - - def _mat3_to_quat(m: np.ndarray) -> Tuple[float, float, float, float]: """Convert a 3×3 rotation matrix to (w, x, y, z).""" tr = m[0, 0] + m[1, 1] + m[2, 2] @@ -979,7 +752,7 @@ def _flatten_shading( def shape_to_meshes( - shape: Any, *, dim: int + shape: Any, ) -> List[Tuple[Any, np.ndarray, np.ndarray, np.ndarray]]: """Decompose a :class:`SharedShape` into raw flat-shaded NumPy mesh data. @@ -992,12 +765,12 @@ def shape_to_meshes( single face normal computed from its winding, with vertices duplicated so the normal isn't interpolated across the face. """ - raw = _shape_to_arrays(shape, dim) + raw = _shape_to_arrays(shape) return [(pose, *_flatten_shading(p, n, i)) for pose, p, n, i in raw] def shape_to_geoms( - shape: Any, *, dim: int, color_rgba: Tuple[float, float, float, float] + shape: Any, *, color_rgba: Tuple[float, float, float, float] ) -> List[Tuple[Any, Any]]: """Convert a Rapier ``SharedShape`` into a list of ``(local_pose, Geom)`` pairs. @@ -1012,7 +785,7 @@ def shape_to_geoms( GeomVertexFormat, ) - raw_smooth = _shape_to_arrays(shape, dim) + raw_smooth = _shape_to_arrays(shape) # Convert every shape to flat shading: compute per-face normals from # the actual triangle winding and explode vertices so the normal # isn't interpolated. This also fixes any winding/stored-normal @@ -1103,8 +876,7 @@ def color_for_body(body_handle_index: int, body_type: Any) -> Tuple[float, float dynamic bodies hash their handle index into a hue in the warm pastel palette. """ - # ``RigidBodyType`` is an ``IntEnum``; comparing as integer keeps - # this dim-agnostic (2D and 3D enums are distinct objects). + # ``RigidBodyType`` is an ``IntEnum``; compare as integer. bt = int(body_type) if body_type is not None else -1 # See ``rapier::dynamics::RigidBodyType``: # 0 = Dynamic, 1 = Fixed, 2 = KinematicPositionBased, 3 = KinematicVelocityBased. diff --git a/python/rapier-testbed/rapier_testbed/_picker.py b/python/rapier-testbed/rapier_testbed/_picker.py index cbfa0bf27..e302b4908 100644 --- a/python/rapier-testbed/rapier_testbed/_picker.py +++ b/python/rapier-testbed/rapier_testbed/_picker.py @@ -90,7 +90,7 @@ def run(initial: Optional[str] = None) -> None: def _launch_single(example: Example) -> None: if _is_headless(): - tb = Testbed(dim=example.dim, headless=True) + tb = Testbed(headless=True) tb.load_example(example.init_fn, f"{example.category} / {example.name}") tb.run() return @@ -102,7 +102,7 @@ def _launch_single(example: Example) -> None: base = ShowBase() base.userExit = lambda: base.taskMgr.stop() # type: ignore[union-attr] base.finalizeExit = lambda: None # type: ignore[union-attr] - tb = Testbed(dim=example.dim, base=base) + tb = Testbed(base=base) tb.load_example(example.init_fn, f"{example.category} / {example.name}") tb.run() @@ -210,7 +210,7 @@ def _load(example: Example) -> None: except Exception as e: print(f" (couldn't open window: {e!r})", file=sys.stderr) return - tb = Testbed(dim=example.dim, base=base, on_escape=_on_testbed_escape) + tb = Testbed(base=base, on_escape=_on_testbed_escape) tb.load_example(example.init_fn, f"{example.category} / {example.name}") tb.install_step_task() # Testbed.__init__ calls ``base.ignoreAll()`` so we re-wire the X @@ -309,7 +309,7 @@ def _cli_input_loop( ex = examples[idx - 1] last = idx - print(f" → {idx:>3}. {ex.category} / {ex.dim}D {ex.name}") + print(f" → {idx:>3}. {ex.category} / {ex.name}") print(" (Esc in the window = back to CLI; close window = quit)") cmd_queue.put(("launch", ex)) @@ -328,8 +328,8 @@ def _print_menu(examples: List[Example]) -> None: idx_width = max(2, len(str(n_total))) name_width = max(len(ex.name) for ex in examples) - # Cell layout: " NN. XD " + trailing pad. - cell_width = idx_width + 2 + 3 + name_width + 4 + # Cell layout: " NN. " + trailing pad. + cell_width = idx_width + 2 + name_width + 4 try: term_w = shutil.get_terminal_size((100, 24)).columns @@ -343,7 +343,7 @@ def _print_menu(examples: List[Example]) -> None: print(f" [ {cat} ]") row: List[str] = [] for ex in group: - cell = f" {cur_idx:>{idx_width}}. {ex.dim}D {ex.name:<{name_width}}" + cell = f" {cur_idx:>{idx_width}}. {ex.name:<{name_width}}" row.append(cell) cur_idx += 1 if len(row) == cols: diff --git a/python/rapier-testbed/rapier_testbed/_registry.py b/python/rapier-testbed/rapier_testbed/_registry.py index fde570f75..ee9fd1bfe 100644 --- a/python/rapier-testbed/rapier_testbed/_registry.py +++ b/python/rapier-testbed/rapier_testbed/_registry.py @@ -36,7 +36,7 @@ class Example: :param name: Human-readable example name (e.g. ``"Primitives"``). :param init_fn: Callable that receives a fresh :class:`Testbed` and populates it via :meth:`Testbed.set_world` and friends. - :param dim: ``2`` or ``3`` — selects 2D vs. 3D testbed mode. + :param dim: spatial dimension of the scene (always ``3``). """ category: str diff --git a/python/rapier-testbed/rapier_testbed/_testbed.py b/python/rapier-testbed/rapier_testbed/_testbed.py index b1cb93198..f052305a7 100644 --- a/python/rapier-testbed/rapier_testbed/_testbed.py +++ b/python/rapier-testbed/rapier_testbed/_testbed.py @@ -9,9 +9,9 @@ parameters, physics pipeline) and steps it each frame. Visualization uses :class:`DebugRenderPipeline` + :class:`DebugLineCollector` -to extract a ``(N, 2, D)`` array of line segments and an ``(N, 4)`` array of +to extract a ``(N, 2, 3)`` array of line segments and an ``(N, 4)`` array of RGBA colors each frame; these are uploaded into a Panda3D :class:`LineSegs` -geometry attached to the scene root (``render`` in 3D, ``aspect2d`` in 2D). +geometry attached to the scene root (``render``). Headless mode is selected by setting ``PANDA_NO_WINDOW=1`` in the environment — the testbed then runs ``N`` (default 60) physics steps @@ -21,7 +21,6 @@ from __future__ import annotations -import math import os import time from collections import deque @@ -30,7 +29,6 @@ import numpy as np import rapier3d as _rp3 -import rapier2d as _rp2 if TYPE_CHECKING: # pragma: no cover # Imported lazily inside __init__; this is for type-checkers only. @@ -38,14 +36,12 @@ _DEFAULT_GRAVITY_3D: Tuple[float, float, float] = (0.0, -9.81, 0.0) -_DEFAULT_GRAVITY_2D: Tuple[float, float] = (0.0, -9.81) _HEADLESS_ENV_VAR: str = "PANDA_NO_WINDOW" _HEADLESS_STEPS_DEFAULT: int = 60 CallbackFn = Callable[["Testbed"], None] Vec3Like = Union[Tuple[float, float, float], Any] -Vec2Like = Union[Tuple[float, float], Any] class _Arcball: @@ -219,9 +215,6 @@ class Testbed: Parameters ---------- - dim: - ``2`` for a 2D scene (orthographic camera, XY plane) or ``3`` for a - 3D scene (perspective camera + arcball mouse). Defaults to 3. headless: Force headless mode. When ``None`` (the default), the ``PANDA_NO_WINDOW`` environment variable is consulted. @@ -239,7 +232,6 @@ class Testbed: def __init__( self, - dim: int = 3, *, headless: Optional[bool] = None, headless_steps: int = _HEADLESS_STEPS_DEFAULT, @@ -248,7 +240,6 @@ def __init__( ) -> None: """Construct a testbed. - :param dim: 2 or 3. :param headless: skip window creation if True. :param headless_steps: number of frames to run in headless mode. :param base: existing Panda3D ``ShowBase`` to reuse. **Required @@ -259,10 +250,7 @@ def __init__( for ``python -m rapier_testbed.examples3.`` direct launches and headless runs). """ - if dim not in (2, 3): - raise ValueError(f"Testbed dim must be 2 or 3, got {dim!r}") - self.dim: int = dim - self._ns = _rp2 if dim == 2 else _rp3 + self._ns = _rp3 self._headless: bool = _is_headless() if headless is None else bool(headless) self._headless_steps: int = int(headless_steps) @@ -275,7 +263,7 @@ def __init__( self.colliders = ns.ColliderSet() self.impulse_joints = ns.ImpulseJointSet() self.multibody_joints = ns.MultibodyJointSet() - self.gravity = _DEFAULT_GRAVITY_3D if dim == 3 else _DEFAULT_GRAVITY_2D + self.gravity = _DEFAULT_GRAVITY_3D self._hooks: Optional[Any] = None self._event_handler: Optional[Any] = None @@ -308,8 +296,6 @@ def __init__( self._label_text: Any = None self._initial_camera_eye: Optional[Tuple[float, float, float]] = None self._initial_camera_at: Optional[Tuple[float, float, float]] = None - self._initial_2d_center: Tuple[float, float] = (0.0, 0.0) - self._initial_2d_zoom: float = 15.0 # ---- mesh-rendering state ---------------------------------------- # One NodePath per collider, parented to ``_mesh_root``. Each @@ -363,7 +349,7 @@ def set_world( Mirrors :meth:`rapier_testbed::Testbed::set_world`. Uses the testbed's current :attr:`gravity` (defaulting to ``(0, -9.81, 0)`` - / ``(0, -9.81)`` if the example doesn't override it). + if the example doesn't override it). """ self.set_world_with_params( bodies, colliders, impulse_joints, multibody_joints, self.gravity @@ -431,8 +417,6 @@ def add_collider(self, builder: Any, parent: Optional[Any] = None) -> Any: def look_at(self, eye: Vec3Like, at: Vec3Like) -> None: """Position the 3D camera at ``eye`` looking at ``at``.""" - if self.dim != 3: - raise RuntimeError("look_at requires a 3D testbed") e = _as_tuple3(eye) a = _as_tuple3(at) self._initial_camera_eye = e @@ -440,20 +424,6 @@ def look_at(self, eye: Vec3Like, at: Vec3Like) -> None: if self._base is not None: self._apply_camera_3d(e, a) - def set_camera_2d(self, center: Vec2Like = (0.0, 0.0), zoom: float = 15.0) -> None: - """Position the 2D orthographic camera. - - ``zoom`` is pixels per world unit (matching the Rust testbed's - ``look_at(at, zoom)``); the visible world height is ``600 / zoom``. - """ - if self.dim != 2: - raise RuntimeError("set_camera_2d requires a 2D testbed") - c = _as_tuple2(center) - self._initial_2d_center = c - self._initial_2d_zoom = float(zoom) - if self._base is not None: - self._apply_camera_2d(c, float(zoom)) - # ===================================================================== # Example lifecycle # ===================================================================== @@ -469,7 +439,7 @@ def load_example(self, init_fn: CallbackFn, label: str = "") -> None: self.colliders = ns.ColliderSet() self.impulse_joints = ns.ImpulseJointSet() self.multibody_joints = ns.MultibodyJointSet() - self.gravity = _DEFAULT_GRAVITY_3D if self.dim == 3 else _DEFAULT_GRAVITY_2D + self.gravity = _DEFAULT_GRAVITY_3D self._callbacks = [] self._step_count = 0 self._paused = False @@ -586,7 +556,7 @@ def _sync_mesh_nodes(self) -> None: continue color = self._color_for_collider(col) try: - geoms = _meshes.shape_to_geoms(col.shape, dim=self.dim, color_rgba=color) + geoms = _meshes.shape_to_geoms(col.shape, color_rgba=color) except Exception as exc: # Unsupported shape: skip but emit a one-off warning so # silent fallbacks are at least visible in the console. @@ -633,35 +603,19 @@ def _update_transforms(self) -> None: # directly so we can use it unconditionally). iso = col.position t = iso.translation - if self.dim == 3: - np_group.setPos(float(t.x), float(t.y), float(t.z)) - r = iso.rotation - # Panda3D LQuaternion takes (w, x, y, z) — matches Rapier. - np_group.setQuat(LQuaternion(float(r.w), float(r.i), float(r.j), float(r.k))) - else: - # Embed 2D world (x, y) into 3D as (x, y, 0). The 2D - # rotation is around the Z axis (out of the screen, with - # gravity pointing down on screen). - np_group.setPos(float(t.x), float(t.y), 0.0) - angle = float(iso.rotation.angle) - half = angle * 0.5 - np_group.setQuat(LQuaternion(math.cos(half), 0.0, 0.0, math.sin(half))) + np_group.setPos(float(t.x), float(t.y), float(t.z)) + r = iso.rotation + # Panda3D LQuaternion takes (w, x, y, z) — matches Rapier. + np_group.setQuat(LQuaternion(float(r.w), float(r.i), float(r.j), float(r.k))) def _apply_local_pose(self, np_node: Any, iso: Any) -> None: """Apply a sub-shape's local pose to a Compound-child NodePath.""" from panda3d.core import LQuaternion t = iso.translation - if self.dim == 3: - np_node.setPos(float(t.x), float(t.y), float(t.z)) - r = iso.rotation - np_node.setQuat(LQuaternion(float(r.w), float(r.i), float(r.j), float(r.k))) - else: - # 2D world (x, y) → 3D (x, y, 0); rotation around +Z. - np_node.setPos(float(t.x), float(t.y), 0.0) - angle = float(iso.rotation.angle) - half = angle * 0.5 - np_node.setQuat(LQuaternion(math.cos(half), 0.0, 0.0, math.sin(half))) + np_node.setPos(float(t.x), float(t.y), float(t.z)) + r = iso.rotation + np_node.setQuat(LQuaternion(float(r.w), float(r.i), float(r.j), float(r.k))) def _color_for_collider(self, col: Any) -> Tuple[float, float, float, float]: """Pick a per-collider color: parent-body palette when attached, @@ -732,11 +686,10 @@ def install_step_task(self) -> None: from direct.task import Task # Track tasks so ``_cleanup_scene`` removes them. Task names that - # weren't actually installed (no arcball / no 2D cam) just fail - # silently when removed. + # weren't actually installed (no arcball) just fail silently when + # removed. self._tracked_tasks.append("rapier-testbed-step") self._tracked_tasks.append("rapier-arcball-tick") - self._tracked_tasks.append("rapier-cam2d-tick") def _task(task: Any) -> Any: now = time.perf_counter() @@ -818,9 +771,8 @@ def _cleanup_scene(self) -> None: self._mesh_nodes.clear() self._known_colliders.clear() self._mesh_root = None - # Reset arcball / 2D-camera bindings. + # Reset arcball bindings. self._arcball = None - setattr(self, "_cam2d_bound", False) # Status / label text already destroyed via _tracked_widgets. self._label_text = None self._status_text = None @@ -837,7 +789,6 @@ def _init_panda3d(self) -> None: AmbientLight, DirectionalLight, LVector3, - OrthographicLens, TextNode, WindowProperties, ) @@ -862,8 +813,6 @@ def _init_panda3d(self) -> None: self._base.setBackgroundColor(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0) # ---- lighting: ambient + one directional key light ---- - # Both 2D and 3D scenes live in `render`. 2D is embedded in the - # z=0 plane and viewed with an orthographic camera (set up later). scene_root = self._base.render ambient = AmbientLight("rapier-ambient") ambient.setColor((0.50, 0.50, 0.50, 1.0)) @@ -873,13 +822,8 @@ def _init_panda3d(self) -> None: directional = DirectionalLight("rapier-key") directional.setColor((0.85, 0.85, 0.85, 1.0)) dir_np = scene_root.attachNewNode(directional) - if self.dim == 3: - # Aim the light down-and-forward (Y-up world). - dir_np.setHpr(-20, -45, 0) - else: - # 2D shapes all face +Z; aim the directional straight along -Z - # so face-normals get fully lit. - dir_np.setHpr(0, 0, 0) + # Aim the light down-and-forward (Y-up world). + dir_np.setHpr(-20, -45, 0) scene_root.setLight(dir_np) self._tracked_lights.append(dir_np) @@ -894,23 +838,18 @@ def _init_panda3d(self) -> None: except Exception: # pragma: no cover - cosmetic only pass - # 2D uses an orthographic main camera looking along -Z at the - # z=0 plane, with screen-up = +Y. Mouse-driven pan + wheel-zoom - # controls are wired in `_apply_camera_2d`. Lens + camera placement - # happen below, after the lens is fully configured. - # Empty placeholder; replaced on the first `_upload_lines` call. self._line_node = None self._line_segs = None - # On-screen text overlays. ``aspect2d``'s X range is + # On-screen text overlays. The HUD overlay layer's X range is # ``[-aspect_ratio, +aspect_ratio]`` and its Y range is # ``[-1, +1]`` (top = +1). We pin the text just inside the # top-left corner by re-computing X from the current aspect # ratio whenever the window resizes (see - # ``_layout_overlay_text``). ``base.a2dTopLeft`` doesn't work - # for us — its position is hard-coded for Z-up and ends up at - # the middle-left under our Y-up coord system. + # ``_layout_overlay_text``). Panda's built-in top-left anchor + # doesn't work for us — its position is hard-coded for Z-up and + # ends up at the middle-left under our Y-up coord system. self._label_text = OnscreenText( text=self._example_label, pos=(0, 0.92), @@ -940,18 +879,15 @@ def _init_panda3d(self) -> None: self._tracked_widgets.extend([self._label_text, self._status_text]) # Camera positioning. - if self.dim == 3: - if self._initial_camera_eye is None: - # Default 3D view (Y-up convention): elevated, slightly - # off-axis, looking down at the origin. - self._initial_camera_eye = (20.0, 25.0, 40.0) - self._initial_camera_at = (0.0, 0.0, 0.0) - self._apply_camera_3d( - self._initial_camera_eye, - self._initial_camera_at or (0.0, 0.0, 0.0), - ) - else: - self._apply_camera_2d(self._initial_2d_center, self._initial_2d_zoom) + if self._initial_camera_eye is None: + # Default 3D view (Y-up convention): elevated, slightly + # off-axis, looking down at the origin. + self._initial_camera_eye = (20.0, 25.0, 40.0) + self._initial_camera_at = (0.0, 0.0, 0.0) + self._apply_camera_3d( + self._initial_camera_eye, + self._initial_camera_at or (0.0, 0.0, 0.0), + ) # Keyboard handlers. Each digit binds three events: # - ``"1"`` (character event) — works on QWERTY / AZERTY-shifted etc. @@ -988,133 +924,6 @@ def _apply_camera_3d(self, eye: Tuple[float, float, float], at: Tuple[float, flo else: self._arcball.look_at(eye, at) - def _apply_camera_2d(self, center: Tuple[float, float], zoom: float) -> None: - """Place the main camera so 2D worlds render in the z=0 plane. - - Uses an orthographic projection: the world-X axis maps to screen-X, - world-Y maps to screen-Y, world-Z is the depth axis. The camera - sits at ``(cx, cy, +D)`` and looks toward ``(cx, cy, 0)`` with - screen-up = +Y. Wires up mouse pan / wheel zoom controls. - """ - if self._base is None: - return - from panda3d.core import LPoint3, LVector3, OrthographicLens - - cx, cy = float(center[0]), float(center[1]) - z = float(zoom) if zoom > 1e-6 else 1.0 - - try: - self._base.disableMouse() - except Exception: - pass - - # Orthographic lens. `zoom` matches the Rust testbed convention - # (`look_at(at, zoom)`): zoom is *pixels per world unit* against a - # 600px reference, so the visible world height is `600 / zoom`. (The - # examples pass the same zoom values as their Rust counterparts.) - lens = OrthographicLens() - film = 600.0 / z - try: - aspect = self._base.getAspectRatio() - except Exception: - aspect = 16.0 / 9.0 - # Film width tracks the window aspect so world units stay square (no - # stretching). Recomputed on resize in `_cam2d_tick`. - lens.setFilmSize(film * aspect, film) - lens.setNearFar(-1000, 1000) - try: - self._base.cam.node().setLens(lens) - except Exception: # pragma: no cover - fallback - return - - # Position the camera in front of the z=0 plane, looking along -Z. - # The explicit `up=+Y` form requires an LPoint3 target — passing - # three floats doesn't accept a 4th up-vector arg. - cam = self._base.camera - cam.setPos(cx, cy, 100.0) - cam.lookAt(LPoint3(cx, cy, 0.0), LVector3(0, 1, 0)) - - # Stash for the mouse pan/zoom handlers. - self._cam2d_center = [cx, cy] - self._cam2d_zoom = z - self._cam2d_lens = lens - self._cam2d_film_height = film - self._cam2d_aspect = aspect - - # Bind mouse pan + wheel zoom once. - if not getattr(self, "_cam2d_bound", False): - self._base.accept("mouse1", self._cam2d_press) - self._base.accept("mouse1-up", self._cam2d_release) - self._base.accept("mouse3", self._cam2d_press) - self._base.accept("mouse3-up", self._cam2d_release) - self._base.accept("wheel_up", self._cam2d_zoom_step, [-1.0]) - self._base.accept("wheel_down", self._cam2d_zoom_step, [1.0]) - self._base.taskMgr.add(self._cam2d_tick, "rapier-cam2d-tick") - self._cam2d_bound = True - self._cam2d_last_mouse: Optional[Tuple[float, float]] = None - self._cam2d_dragging: bool = False - - def _cam2d_press(self) -> None: - if self._base is None or not self._base.mouseWatcherNode.hasMouse(): - return - m = self._base.mouseWatcherNode.getMouse() - self._cam2d_last_mouse = (m.getX(), m.getY()) - self._cam2d_dragging = True - - def _cam2d_release(self) -> None: - self._cam2d_dragging = False - self._cam2d_last_mouse = None - - def _cam2d_zoom_step(self, sign: float) -> None: - self._cam2d_zoom = max(0.05, self._cam2d_zoom * (1.15 ** -sign)) - self._apply_camera_2d(tuple(self._cam2d_center), self._cam2d_zoom) - - def _cam2d_tick(self, task: Any) -> Any: - from direct.task import Task - from panda3d.core import LPoint3, LVector3 - - if self._base is None: - return Task.cont - - # Keep the orthographic projection square when the window is resized: - # the film width must follow the live window aspect, else the scene - # stretches. (Done here rather than via an "aspectRatioChanged" accept - # so we don't clobber the overlay-text resize handler on the same - # event.) - if hasattr(self, "_cam2d_lens"): - try: - aspect = self._base.getAspectRatio() - except Exception: - aspect = self._cam2d_aspect - if aspect > 1e-6 and abs(aspect - self._cam2d_aspect) > 1e-6: - self._cam2d_lens.setFilmSize( - self._cam2d_film_height * aspect, self._cam2d_film_height - ) - self._cam2d_aspect = aspect - - if not self._cam2d_dragging or self._cam2d_last_mouse is None: - return Task.cont - if self._base is None or not self._base.mouseWatcherNode.hasMouse(): - return Task.cont - m = self._base.mouseWatcherNode.getMouse() - cur = (m.getX(), m.getY()) - dx = cur[0] - self._cam2d_last_mouse[0] - dy = cur[1] - self._cam2d_last_mouse[1] - self._cam2d_last_mouse = cur - # Convert mouse-NDC delta into world units. Mouse coords run [-1, 1] - # so half the film size in world units per unit NDC. - film_h = self._cam2d_film_height - film_w = film_h * self._cam2d_aspect - self._cam2d_center[0] -= dx * 0.5 * film_w - self._cam2d_center[1] -= dy * 0.5 * film_h - cx, cy = self._cam2d_center - try: - self._base.camera.setPos(cx, cy, 100.0) - self._base.camera.lookAt(LPoint3(cx, cy, 0.0), LVector3(0, 1, 0)) - except Exception: - pass - return Task.cont - # ---- keyboard handlers ---------------------------------------------- def _toggle_pause(self) -> None: @@ -1148,8 +957,9 @@ def _goto_example_index(self, idx: int) -> None: def _layout_overlay_text(self) -> None: """Re-pin the on-screen label + status to the window's top-left. - ``aspect2d``'s X coords run from ``-aspect`` to ``+aspect``, so - the left edge of the window is at ``X = -aspect`` and depends + The HUD overlay layer's X coords run from ``-aspect`` to + ``+aspect``, so the left edge of the window is at ``X = -aspect`` + and depends on the current window dimensions. We re-compute that on each ``aspectRatioChanged`` event (fired by ShowBase's ``windowEvent`` when the user resizes the window) so the text @@ -1214,16 +1024,9 @@ def _upload_lines(self, lines: np.ndarray, colors: np.ndarray) -> None: n = int(lines.shape[0]) if lines.size else 0 - # Project (N, 2, D) → (2N, 3) float32 vertex positions. + # Project (N, 2, 3) → (2N, 3) float32 vertex positions. if n == 0: verts = np.empty((0, 3), dtype=np.float32) - elif lines.shape[2] == 2: - # 2D world embedded in (x, y, 0); flatten endpoints AB,AB,AB,… - flat = np.asarray(lines, dtype=np.float32).reshape(2 * n, 2) - verts = np.empty((2 * n, 3), dtype=np.float32) - verts[:, 0] = flat[:, 0] - verts[:, 1] = flat[:, 1] - verts[:, 2] = 0.0 else: verts = np.ascontiguousarray( np.asarray(lines, dtype=np.float32).reshape(2 * n, 3) @@ -1269,8 +1072,6 @@ def _upload_lines(self, lines: np.ndarray, colors: np.ndarray) -> None: if self._line_node is not None: self._line_node.removeNode() - # Both 2D and 3D scenes live under `render`; 2D just uses an - # orthographic camera looking along -Z at the z=0 plane. self._line_node = self._base.render.attachNewNode(node) # Line thickness via RenderModeAttrib (LineSegs equivalent of setThickness). self._line_node.setRenderModeThickness(1.5) @@ -1296,13 +1097,4 @@ def _as_tuple3(v: Any) -> Tuple[float, float, float]: return (float(seq[0]), float(seq[1]), float(seq[2])) -def _as_tuple2(v: Any) -> Tuple[float, float]: - if hasattr(v, "x") and hasattr(v, "y") and not hasattr(v, "z"): - return (float(v.x), float(v.y)) - seq = tuple(v) # type: ignore[arg-type] - if len(seq) != 2: - raise ValueError(f"expected a 2-vector, got {v!r}") - return (float(seq[0]), float(seq[1])) - - __all__ = ["Testbed", "CallbackFn"] diff --git a/python/rapier-testbed/rapier_testbed/examples2/__init__.py b/python/rapier-testbed/rapier_testbed/examples2/__init__.py deleted file mode 100644 index d596c8cb4..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Bundled 2D examples. diff --git a/python/rapier-testbed/rapier_testbed/examples2/add_remove2.py b/python/rapier-testbed/rapier_testbed/examples2/add_remove2.py deleted file mode 100644 index c0f2f5971..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/add_remove2.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Port of examples2d/add_remove2.rs.""" -from __future__ import annotations - -import random -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Add remove" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.5 - - positions = [(5.0, -1.0), (-5.0, -1.0)] - platform_handles = [] - for pos in positions: - handle = bodies.insert(rp.RigidBody.kinematic_position_based().translation(pos)) - colliders.insert_with_parent( - rp.Collider.cuboid(rad * 10.0, rad), handle, bodies - ) - platform_handles.append(handle) - - rng = random.Random(0) - state = {"step": 0} - - def callback(tb) -> None: - state["step"] += 1 - # Rotate the platforms. Rapier-py exposes no - # `set_next_kinematic_rotation` — emulate by overwriting position. - angle = -state["step"] * 0.016 - for rb_handle in platform_handles: - rb = tb.bodies.get(rb_handle) - if rb is None: - continue - tr = rb.translation - new_pose = rp.Isometry2((float(tr.x), float(tr.y)), angle) - rb.position = new_pose - - if state["step"] % 10 == 0: - x = rng.random() * 10.0 - 5.0 - y = rng.random() * 10.0 + 10.0 - h = tb.bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - tb.colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad), h, tb.bodies - ) - - # Remove bodies that fall below y = -10. - to_remove = [] - for h in list(tb.bodies.handles()): - rb = tb.bodies.get(h) - if rb is None: - continue - if rb.translation.y < -10.0: - to_remove.append(h) - for h in to_remove: - tb.bodies.remove( - h, - tb._islands, - tb.colliders, - tb.impulse_joints, - tb.multibody_joints, - True, - ) - - testbed.add_callback(callback) - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/ccd2.py b/python/rapier-testbed/rapier_testbed/examples2/ccd2.py deleted file mode 100644 index da02a8e98..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/ccd2.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Port of examples2d/ccd2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Dynamics" -NAME = "CCD" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - # Ground (CCD-enabled). - ground_size = 25.0 - ground_thickness = 0.1 - - ground_handle = bodies.insert(rp.RigidBody.fixed().ccd_enabled(True)) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_thickness), ground_handle, bodies - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_thickness, ground_size).translation((-3.0, 0.0)), - ground_handle, - bodies, - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_thickness, ground_size).translation((6.0, 0.0)), - ground_handle, - bodies, - ) - - # Sensor cuboid. - colliders.insert_with_parent( - rp.Collider.cuboid(ground_thickness, ground_size) - .translation((2.5, 0.0)) - .sensor(True) - .active_events(rp.ActiveEvents.COLLISION_EVENTS), - ground_handle, - bodies, - ) - - # Compound projectile (Rust uses a compound of three sub-cuboids). - radx = 0.4 - rady = 0.05 - - horizontal = rp.SharedShape.cuboid(radx, rady) - vertical = rp.SharedShape.cuboid(rady, radx) - - delta1 = rp.Isometry2.from_translation(0.0, radx - rady) - delta2 = rp.Isometry2.from_translation(-radx + rady, 0.0) - delta3 = rp.Isometry2.from_translation(radx - rady, 0.0) - - compound_shape = rp.SharedShape.compound( - [(delta1, horizontal), (delta2, vertical), (delta3, vertical)] - ) - - num = 6 - shift = (radx + 0.01) * 2.0 - centerx = shift * num / 2.0 - 0.5 - centery = shift / 2.0 + 4.0 - - for i in range(num): - for j in range(num): - x = i * shift - centerx - y = j * shift + centery - handle = bodies.insert( - rp.RigidBody.dynamic() - .translation((x, y)) - .linvel((100.0, -10.0)) - .ccd_enabled(True) - ) - colliders.insert_with_parent( - rp.Collider.new(compound_shape), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/character_controller2.py b/python/rapier-testbed/rapier_testbed/examples2/character_controller2.py deleted file mode 100644 index 421de68b2..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/character_controller2.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Port of examples2d/character_controller2.rs. - -The Rust example wires a keyboard-driven character via -``utils::character`` and ``KinematicCharacterController``. The Python -mini-testbed has no keyboard / egui layer, so the port keeps the static -scenery (floor + stairs + slopes + walls + tilting platform + heightfield -+ revolute-limited dynamic body) without the interactive controls. -""" -from __future__ import annotations - -import math - -import numpy as np - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Controls" -NAME = "Character controller" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 5.0 - ground_height = 0.1 - - floor_handle = bodies.insert( - rp.RigidBody.fixed().translation((0.0, -ground_height)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), floor_handle, bodies - ) - - character_handle = bodies.insert( - rp.RigidBody.kinematic_position_based() - .translation((-3.0, 5.0)) - .gravity_scale(10.0) - .soft_ccd_prediction(10.0) - ) - colliders.insert_with_parent( - rp.Collider.capsule_y(0.3, 0.15), character_handle, bodies - ) - - num = 8 - rad = 0.1 - shift = rad * 2.0 - centerx = shift * (num // 2) - centery = rad - for j in range(4): - for i in range(num): - x = i * shift - centerx - y = j * shift + centery - h = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), h, bodies) - - # Stairs. - stair_width = 1.0 - stair_height = 0.1 - for i in range(10): - x = i * stair_width / 2.0 - y = i * stair_height * 1.5 + 3.0 - colliders.insert( - rp.Collider.cuboid(stair_width / 2.0, stair_height / 2.0).translation((x, y)) - ) - - # Slope we can climb. - slope_angle = 0.2 - slope_size = 2.0 - colliders.insert( - rp.Collider.cuboid(slope_size, ground_height) - .translation((ground_size + slope_size, -ground_height + 0.4)) - .rotation(slope_angle) - ) - - # Impossible slope. - impossible_slope_angle = 0.9 - impossible_slope_size = 2.0 - colliders.insert( - rp.Collider.cuboid(slope_size, ground_height) - .translation( - ( - ground_size + slope_size * 2.0 + impossible_slope_size - 0.9, - -ground_height + 2.3, - ) - ) - .rotation(impossible_slope_angle) - ) - - # Wall. - wall_angle = math.pi / 2.0 - wall_size = 2.0 - wall_pos = ( - ground_size + slope_size * 2.0 + impossible_slope_size + 0.35, - -ground_height + 2.5 * 2.3, - ) - colliders.insert( - rp.Collider.cuboid(wall_size, ground_height) - .translation(wall_pos) - .rotation(wall_angle) - ) - colliders.insert(rp.Collider.cuboid(wall_size, ground_height).translation(wall_pos)) - - # Moving platform. - platform_handle = bodies.insert( - rp.RigidBody.kinematic_velocity_based().translation((-8.0, 0.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(2.0, ground_height), platform_handle, bodies - ) - - # Heightfield ground. - hf_size = (10.0, 1.0) - nsubdivs = 20 - heights = np.array( - [math.cos(i * hf_size[0] / nsubdivs / 2.0) * 1.5 for i in range(nsubdivs + 1)], - dtype=np.float32, - ) - colliders.insert( - rp.Collider.heightfield(heights, hf_size).translation((-8.0, 5.0)) - ) - - # Tilting limited-revolute body. - ground = bodies.insert(rp.RigidBody.fixed().translation((0.0, 5.0))) - body = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 5.0))) - colliders.insert_with_parent(rp.Collider.cuboid(1.0, 0.1), body, bodies) - joint = rp.RevoluteJoint.builder().limits(-0.3, 0.3) - impulse_joints.insert(ground, body, joint, wake_up=True) - - state = {"t": 0.0} - - def cb(tb) -> None: - state["t"] += tb._integration_parameters.dt - linvel = ( - math.sin(state["t"] * 2.0) * 2.0, - math.sin(state["t"] * 5.0) * 1.5, - ) - platform = tb.bodies.get(platform_handle) - if platform is not None: - platform.linvel = linvel - - testbed.add_callback(cb) - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=100.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/collision_groups2.py b/python/rapier-testbed/rapier_testbed/examples2/collision_groups2.py deleted file mode 100644 index 0e6017fd4..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/collision_groups2.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Port of examples2d/collision_groups2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Collision groups" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 5.0 - ground_height = 0.1 - - floor_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), floor_handle, bodies - ) - - green_group = rp.InteractionGroups( - memberships=rp.Group.GROUP_1, filter=rp.Group.GROUP_1 - ) - blue_group = rp.InteractionGroups( - memberships=rp.Group.GROUP_2, filter=rp.Group.GROUP_2 - ) - - green_floor = ( - rp.Collider.cuboid(1.0, 0.1) - .translation((0.0, 1.0)) - .collision_groups(green_group) - ) - colliders.insert_with_parent(green_floor, floor_handle, bodies) - - blue_floor = ( - rp.Collider.cuboid(1.0, 0.1) - .translation((0.0, 2.0)) - .collision_groups(blue_group) - ) - colliders.insert_with_parent(blue_floor, floor_handle, bodies) - - num = 8 - rad = 0.1 - shift = rad * 2.0 - centerx = shift * (num // 2) - centery = 2.5 - - for j in range(4): - for i in range(num): - x = i * shift - centerx - y = j * shift + centery - group = green_group if i % 2 == 0 else blue_group - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad).collision_groups(group), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=100.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/convex_polygons2.py b/python/rapier-testbed/rapier_testbed/examples2/convex_polygons2.py deleted file mode 100644 index 51e0723e2..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/convex_polygons2.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Port of examples2d/convex_polygons2.rs.""" -from __future__ import annotations - -import math -import random - -import numpy as np - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Convex polygons" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 30.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((-ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - num = 14 - scale = 4.0 - shift = scale - centerx = shift * num / 2.0 - centery = shift / 2.0 - - rng = random.Random(0) - - for i in range(num): - for j in range(num * 4): - x = i * shift - centerx - y = j * shift * 2.0 + centery + 2.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - pts = np.array( - [[rng.random() * scale, rng.random() * scale] for _ in range(10)], - dtype=np.float32, - ) - colliders.insert_with_parent(rp.Collider.convex_hull(pts), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 50.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/damping2.py b/python/rapier-testbed/rapier_testbed/examples2/damping2.py deleted file mode 100644 index 9823a30d2..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/damping2.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Port of examples2d/damping2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Dynamics" -NAME = "Damping" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - num = 10 - rad = 0.2 - subdiv = 1.0 / num - - for i in range(num): - angle = i * subdiv * math.pi * 2.0 - x = math.sin(angle) - y = math.cos(angle) - - rb = ( - rp.RigidBody.dynamic() - .translation((x, y)) - .linvel((x * 10.0, y * 10.0)) - .angvel(100.0) - .linear_damping((i + 1) * subdiv * 10.0) - .angular_damping((num - i) * subdiv * 10.0) - ) - rb_handle = bodies.insert(rb) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), rb_handle, bodies) - - testbed.set_world_with_params( - bodies, colliders, impulse_joints, multibody_joints, (0.0, 0.0) - ) - testbed.set_camera_2d(center=(3.0, 2.0), zoom=50.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/drum2.py b/python/rapier-testbed/rapier_testbed/examples2/drum2.py deleted file mode 100644 index 5a43a9e51..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/drum2.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Port of examples2d/drum2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Drum" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - num = 30 - rad = 0.2 - - shift = rad * 2.0 - centerx = shift * num / 2.0 - centery = shift * num / 2.0 - - for i in range(num): - for j in range(num): - x = i * shift - centerx - y = j * shift - centery - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - # Velocity-based kinematic drum. - platform_handle = bodies.insert(rp.RigidBody.kinematic_velocity_based()) - - sides = [ - (10.0, 0.25, (0.0, 10.0)), - (10.0, 0.25, (0.0, -10.0)), - (0.25, 10.0, (10.0, 0.0)), - (0.25, 10.0, (-10.0, 0.0)), - ] - balls = [ - (1.25, (6.0, 6.0)), - (1.25, (-6.0, 6.0)), - (1.25, (6.0, -6.0)), - (1.25, (-6.0, -6.0)), - ] - - for hx, hy, pos in sides: - colliders.insert_with_parent( - rp.Collider.cuboid(hx, hy).translation(pos), platform_handle, bodies - ) - for r, pos in balls: - colliders.insert_with_parent( - rp.Collider.ball(r).translation(pos), platform_handle, bodies - ) - - def callback(tb) -> None: - platform = tb.bodies.get(platform_handle) - if platform is not None: - platform.angvel = -0.15 - - testbed.add_callback(callback) - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=40.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/heightfield2.py b/python/rapier-testbed/rapier_testbed/examples2/heightfield2.py deleted file mode 100644 index 9e56e9b80..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/heightfield2.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Port of examples2d/heightfield2.rs.""" -from __future__ import annotations - -import math - -import numpy as np - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Heightfield" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = (50.0, 1.0) - nsubdivs = 2000 - - heights = np.zeros(nsubdivs + 1, dtype=np.float32) - for i in range(nsubdivs + 1): - if i == 0 or i == nsubdivs: - heights[i] = 8.0 - else: - heights[i] = math.cos(i * ground_size[0] / nsubdivs) * 2.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.heightfield(heights, ground_size), handle, bodies - ) - - num = 20 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * (num // 2) - centery = shift / 2.0 - - for i in range(num): - for j in range(num): - x = i * shift - centerx - y = j * shift + centery + 3.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - if j % 2 == 0: - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad), handle, bodies - ) - else: - colliders.insert_with_parent(rp.Collider.ball(rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/inv_pyramid2.py b/python/rapier-testbed/rapier_testbed/examples2/inv_pyramid2.py deleted file mode 100644 index c97451cdd..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/inv_pyramid2.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Port of examples2d/inv_pyramid2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Inv pyramid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 10.0 - ground_thickness = 1.0 - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_thickness), ground_handle, bodies - ) - - num = 6 - rad = 0.5 - y = rad - - for _ in range(num): - handle = bodies.insert( - rp.RigidBody.dynamic().translation((0.0, y + ground_thickness)) - ) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - y += rad + rad * 2.0 - rad *= 2.0 - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/inverse_kinematics2.py b/python/rapier-testbed/rapier_testbed/examples2/inverse_kinematics2.py deleted file mode 100644 index 01f226865..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/inverse_kinematics2.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Port of examples2d/inverse_kinematics2.rs. - -The Rust example uses the mouse position via the testbed graphics layer to -drive a multibody chain through inverse kinematics each frame. The Python -mini-testbed has no mouse picking, so this port just builds the static -chain — the chain is still a useful debug-render demo. -""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Joints" -NAME = "Inverse kinematics" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 1.0 - ground_height = 0.01 - - floor_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), floor_handle, bodies - ) - - num_segments = 10 - last_body = bodies.insert(rp.RigidBody.fixed()) - - for i in range(num_segments): - size = 1.0 / num_segments - new_body = bodies.insert(rp.RigidBody.dynamic().can_sleep(False)) - colliders.insert_with_parent( - rp.Collider.cuboid(size / 8.0, size / 2.0).density(0.0).sensor(True), - new_body, - bodies, - ) - - anchor1_y = (size / 2.0) if i != 0 else 0.0 - link_ab = ( - rp.RevoluteJoint.builder() - .local_anchor1((0.0, anchor1_y)) - .local_anchor2((0.0, -size / 2.0)) - ) - - multibody_joints.insert(last_body, new_body, link_ab, wake_up=True) - last_body = new_body - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=300.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/joint_motor_position2.py b/python/rapier-testbed/rapier_testbed/examples2/joint_motor_position2.py deleted file mode 100644 index 57ab9146b..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/joint_motor_position2.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Port of examples2d/joint_motor_position2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Joints" -NAME = "Joint motor position" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - - # Rectangles on motor (position-target). - for num in range(9): - x_pos = -6.0 + 1.5 * num - handle = bodies.insert( - rp.RigidBody.dynamic().translation((x_pos, 2.0)).can_sleep(False) - ) - colliders.insert_with_parent(rp.Collider.cuboid(0.1, 0.5), handle, bodies) - - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((x_pos, 1.5)) - .local_anchor2((0.0, -0.5)) - .motor_position(-math.pi + math.pi / 4.0 * num, 1000.0, 150.0) - ) - impulse_joints.insert(ground_handle, handle, joint, wake_up=True) - - # Rectangles on motor (velocity + limits). - for num in range(8): - x_pos = -6.0 + 1.5 * num - handle = bodies.insert( - rp.RigidBody.dynamic() - .translation((x_pos, 4.5)) - .rotation(math.pi) - .can_sleep(False) - ) - colliders.insert_with_parent(rp.Collider.cuboid(0.1, 0.5), handle, bodies) - - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((x_pos, 5.0)) - .local_anchor2((0.0, -0.5)) - .motor_velocity(1.5, 30.0) - .motor_max_force(100.0) - .limits(-math.pi, -math.pi + math.pi / 4.0 * num) - ) - impulse_joints.insert(ground_handle, handle, joint, wake_up=True) - - testbed.set_world_with_params( - bodies, colliders, impulse_joints, multibody_joints, (0.0, 0.0) - ) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=40.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/joints2.py b/python/rapier-testbed/rapier_testbed/examples2/joints2.py deleted file mode 100644 index 4062434ec..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/joints2.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Port of examples2d/joints2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Joints" -NAME = "Joints" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.4 - numi = 10 - numk = 10 - shift = 1.0 - - body_handles = [] - - for k in range(numk): - for i in range(numi): - if i == 0 and k == 0: - rb_builder = rp.RigidBody.fixed() - else: - rb_builder = rp.RigidBody.dynamic() - rb = rb_builder.translation((k * shift, -i * shift)) - child_handle = bodies.insert(rb) - colliders.insert_with_parent(rp.Collider.ball(rad), child_handle, bodies) - - # Vertical joint. - if i > 0: - parent_handle = body_handles[-1] - joint = rp.RevoluteJoint.builder().local_anchor2((0.0, shift)) - impulse_joints.insert(parent_handle, child_handle, joint, wake_up=True) - - # Horizontal joint. - if k > 0: - parent_index = len(body_handles) - numi - parent_handle = body_handles[parent_index] - joint = rp.RevoluteJoint.builder().local_anchor2((-shift, 0.0)) - impulse_joints.insert(parent_handle, child_handle, joint, wake_up=True) - - body_handles.append(child_handle) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(numk * rad, numi * -rad), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/locked_rotations2.py b/python/rapier-testbed/rapier_testbed/examples2/locked_rotations2.py deleted file mode 100644 index e0b452eed..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/locked_rotations2.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Port of examples2d/locked_rotations2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Dynamics" -NAME = "Locked rotations" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 5.0 - ground_height = 0.1 - - handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), handle, bodies - ) - - # A rectangle that only rotates (translations locked). - handle = bodies.insert( - rp.RigidBody.dynamic() - .translation((0.0, 3.0)) - .locked_axes(rp.LockedAxes.TRANSLATION_LOCKED) - ) - colliders.insert_with_parent(rp.Collider.cuboid(2.0, 0.6), handle, bodies) - - # A tilted capsule that cannot rotate. - handle = bodies.insert( - rp.RigidBody.dynamic() - .translation((0.0, 5.0)) - .rotation(1.0) - .locked_axes(rp.LockedAxes.ROTATION_LOCKED) - ) - colliders.insert_with_parent(rp.Collider.capsule_y(0.6, 0.4), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=40.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/one_way_platforms2.py b/python/rapier-testbed/rapier_testbed/examples2/one_way_platforms2.py deleted file mode 100644 index 2bfd7f077..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/one_way_platforms2.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Port of examples2d/one_way_platforms2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "One-way platforms" - - -class OneWayPlatformHook: - """PhysicsHook that makes two platforms only allow contacts from one side.""" - - def __init__(self, platform1, platform2) -> None: - self.platform1 = platform1 - self.platform2 = platform2 - - def modify_solver_contacts(self, ctx) -> None: - allowed_local_n1 = (0.0, 0.0) - - if ctx.collider1 == self.platform1: - allowed_local_n1 = (0.0, 1.0) - elif ctx.collider2 == self.platform1: - allowed_local_n1 = (0.0, -1.0) - - if ctx.collider1 == self.platform2: - allowed_local_n1 = (0.0, -1.0) - elif ctx.collider2 == self.platform2: - allowed_local_n1 = (0.0, 1.0) - - ctx.update_as_oneway_platform(allowed_local_n1, 0.1) - - tangent_velocity = -12.0 if ( - ctx.collider1 == self.platform1 or ctx.collider2 == self.platform2 - ) else 12.0 - for i in range(ctx.num_solver_contacts()): - sc = ctx.solver_contacts[i] - try: - sc.tangent_velocity = (tangent_velocity, sc.tangent_velocity.y) - except Exception: - # Some bindings expose tangent_velocity as a scalar; skip if - # the assignment shape doesn't match. - pass - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - handle = bodies.insert(rp.RigidBody.fixed()) - platform1 = colliders.insert_with_parent( - rp.Collider.cuboid(25.0, 0.5) - .translation((30.0, 2.0)) - .active_hooks(rp.ActiveHooks.MODIFY_SOLVER_CONTACTS), - handle, - bodies, - ) - platform2 = colliders.insert_with_parent( - rp.Collider.cuboid(25.0, 0.5) - .translation((-30.0, -2.0)) - .active_hooks(rp.ActiveHooks.MODIFY_SOLVER_CONTACTS), - handle, - bodies, - ) - - hooks = OneWayPlatformHook(platform1, platform2) - - state = {"step": 0} - - def spawn_cube_callback(tb) -> None: - state["step"] += 1 - if state["step"] % 200 == 0 and len(tb.bodies) <= 7: - h = tb.bodies.insert( - rp.RigidBody.dynamic().translation((20.0, 10.0)) - ) - tb.colliders.insert_with_parent( - rp.Collider.cuboid(1.5, 2.0), h, tb.bodies - ) - - for h in tb._islands.active_dynamic_set(): - rb = tb.bodies.get(h) - if rb is None: - continue - if rb.translation.y > 1.0: - rb.gravity_scale = 1.0 - elif rb.translation.y < -1.0: - rb.gravity_scale = -1.0 - - testbed.add_callback(spawn_cube_callback) - testbed.set_world_with_params( - bodies, colliders, impulse_joints, multibody_joints, (0.0, -9.81), hooks - ) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/pin_slot_joint2.py b/python/rapier-testbed/rapier_testbed/examples2/pin_slot_joint2.py deleted file mode 100644 index d30dab1d1..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/pin_slot_joint2.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Port of examples2d/pin_slot_joint2.rs. - -The Rust version drives the character with the keyboard via -``utils::character``. The Python port keeps the static scene (pin-slot -joint + tethered cube) but omits the keyboard-driven controller. -""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Joints" -NAME = "Pin Slot Joint" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 3.0 - ground_height = 0.1 - - floor_handle = bodies.insert( - rp.RigidBody.fixed().translation((0.0, -ground_height)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), floor_handle, bodies - ) - - character_handle = bodies.insert( - rp.RigidBody.kinematic_position_based().translation((0.0, 0.3)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.15, 0.3), character_handle, bodies - ) - - rad = 0.4 - cube_handle = bodies.insert(rp.RigidBody.dynamic().translation((1.0, 1.0))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), cube_handle, bodies) - - ball_handle = bodies.insert(rp.RigidBody.dynamic().translation((1.0, 1.0))) - colliders.insert_with_parent(rp.Collider.ball(0.1), ball_handle, bodies) - - fixed_joint = ( - rp.FixedJoint.builder() - .local_anchor1((0.0, 0.0)) - .local_anchor2((0.0, -0.4)) - ) - impulse_joints.insert(cube_handle, ball_handle, fixed_joint, wake_up=True) - - axis_len = math.sqrt(2.0) - axis = (1.0 / axis_len, 1.0 / axis_len) - pin_slot_joint = ( - rp.PinSlotJoint.builder(axis) - .local_anchor1((2.0, 2.0)) - .local_anchor2((0.0, 0.4)) - .limits(-1.0, float("inf")) - ) - impulse_joints.insert( - character_handle, cube_handle, pin_slot_joint, wake_up=True - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=100.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/platform2.py b/python/rapier-testbed/rapier_testbed/examples2/platform2.py deleted file mode 100644 index f89f6b600..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/platform2.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Port of examples2d/platform2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Platform" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 10.0 - ground_height = 0.1 - - handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), handle, bodies - ) - - num = 6 - rad = 0.2 - shift = rad * 2.0 - centerx = shift * num / 2.0 - centery = shift / 2.0 + 3.04 - - for i in range(num): - for j in range(num): - x = i * shift - centerx - y = j * shift + centery - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - velocity_based_platform_handle = bodies.insert( - rp.RigidBody.kinematic_velocity_based().translation((-10.0 * rad, 1.5 + 0.8)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(rad * 10.0, rad), velocity_based_platform_handle, bodies - ) - - position_based_platform_handle = bodies.insert( - rp.RigidBody.kinematic_position_based().translation( - (-10.0 * rad, 2.0 + 1.5 + 0.8) - ) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(rad * 10.0, rad), position_based_platform_handle, bodies - ) - - state = {"t": 0.0} - - def callback(tb) -> None: - dt = tb._integration_parameters.dt - state["t"] += dt - velocity = (math.sin(state["t"]) * 5.0, math.sin(state["t"] * 5.0)) - - platform = tb.bodies.get(velocity_based_platform_handle) - if platform is not None: - platform.linvel = velocity - - platform = tb.bodies.get(position_based_platform_handle) - if platform is not None: - tr = platform.translation - new_pos = rp.Isometry2( - (float(tr.x) + velocity[0] * dt, float(tr.y) + velocity[1] * dt), - 0.0, - ) - platform.position = new_pos - - testbed.add_callback(callback) - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=40.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/polyline2.py b/python/rapier-testbed/rapier_testbed/examples2/polyline2.py deleted file mode 100644 index 2f7a265b7..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/polyline2.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Port of examples2d/polyline2.rs.""" -from __future__ import annotations - -import math - -import numpy as np - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Polyline" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 50.0 - nsubdivs = 2000 - step_size = ground_size / nsubdivs - - pts = [[-ground_size / 2.0, 40.0]] - for i in range(1, nsubdivs - 1): - x = -ground_size / 2.0 + i * step_size - y = math.cos(i * step_size) * 2.0 - pts.append([x, y]) - pts.append([ground_size / 2.0, 40.0]) - - vertices = np.array(pts, dtype=np.float32) - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.polyline(vertices), handle, bodies) - - num = 20 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * (num // 2) - centery = shift / 2.0 - - for i in range(num): - for j in range(num): - x = i * shift - centerx - y = j * shift + centery + 3.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - if j % 2 == 0: - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad), handle, bodies - ) - else: - colliders.insert_with_parent(rp.Collider.ball(rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 0.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/pyramid2.py b/python/rapier-testbed/rapier_testbed/examples2/pyramid2.py deleted file mode 100644 index 66ea555cc..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/pyramid2.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Port of examples2d/pyramid2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Pyramid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 10.0 - ground_thickness = 1.0 - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_thickness), ground_handle, bodies - ) - - num = 10 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * num / 2.0 - centery = shift / 2.0 + ground_thickness + rad * 1.5 - - for i in range(num): - for j in range(i, num): - x = (i * shift / 2.0) + (j - i) * shift - centerx - y = i * shift + centery - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/restitution2.py b/python/rapier-testbed/rapier_testbed/examples2/restitution2.py deleted file mode 100644 index 5c282ee5a..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/restitution2.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Port of examples2d/restitution2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Dynamics" -NAME = "Restitution" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 20.0 - ground_height = 1.0 - - handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height).restitution(1.0), - handle, - bodies, - ) - - num = 10 - rad = 0.5 - - for j in range(2): - for i in range(num + 1): - x = i - num / 2.0 - handle = bodies.insert( - rp.RigidBody.dynamic().translation((x * 2.0, 10.0 * (j + 1))) - ) - colliders.insert_with_parent( - rp.Collider.ball(rad).restitution(i / num), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=25.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/rope_joints2.py b/python/rapier-testbed/rapier_testbed/examples2/rope_joints2.py deleted file mode 100644 index be4e1771c..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/rope_joints2.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Port of examples2d/rope_joints2.rs. - -The original example wires a manually-driven character via -``utils::character``. The Python port keeps the static scene (tether on a -kinematic character body) but omits the keyboard-driven controller; this -keeps the example useful for debug-rendering and headless smoke testing. -""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Joints" -NAME = "Rope Joints" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 0.75 - ground_height = 0.1 - - handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed().translation((-ground_size - ground_height, ground_size)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_height, ground_size), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed().translation((ground_size + ground_height, ground_size)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_height, ground_size), handle, bodies - ) - - character_handle = bodies.insert( - rp.RigidBody.kinematic_position_based().translation((0.0, 0.3)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.15, 0.3), character_handle, bodies - ) - - rad = 0.04 - child_handle = bodies.insert(rp.RigidBody.dynamic().translation((1.0, 1.0))) - colliders.insert_with_parent(rp.Collider.ball(rad), child_handle, bodies) - - joint = rp.RopeJoint.builder(2.0).local_anchor2((0.0, 0.0)) - impulse_joints.insert(character_handle, child_handle, joint, wake_up=True) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=100.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_arch.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_arch.py deleted file mode 100644 index 230a0be74..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_arch.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Port of examples2d/s2d_arch.rs.""" -from __future__ import annotations - -import numpy as np - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Arch" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ps1 = [ - (16.0, 0.0), - (14.93803712795643, 5.133601056842984), - (13.79871746027416, 10.24928069555078), - (12.56252963284711, 15.34107019122473), - (11.20040987372525, 20.39856541571217), - (9.66521217819836, 25.40369899225096), - (7.87179930638133, 30.3179337000085), - (5.635199558196225, 35.03820717801641), - (2.405937953536585, 39.09554102558315), - ] - ps2 = [ - (24.0, 0.0), - (22.33619528222415, 6.02299846205841), - (20.54936888969905, 12.00964361211476), - (18.60854610798073, 17.9470321677465), - (16.46769273811807, 23.81367936585418), - (14.05325025774858, 29.57079353071012), - (11.23551045834022, 35.13775818285372), - (7.752568160730571, 40.30450679009583), - (3.016931552701656, 44.28891593799322), - ] - scale = 0.25 - friction = 0.6 - - ps1 = [(x * scale, y * scale) for x, y in ps1] - ps2 = [(x * scale, y * scale) for x, y in ps2] - - colliders.insert( - rp.Collider.segment((-100.0, 0.0), (100.0, 0.0)).friction(0.6) - ) - - def insert_chunk(pts): - pts_arr = np.array(pts, dtype=np.float32) - handle = bodies.insert(rp.RigidBody.dynamic()) - colliders.insert_with_parent( - rp.Collider.convex_hull(pts_arr).friction(friction), handle, bodies - ) - - for i in range(8): - insert_chunk([ps1[i], ps2[i], ps2[i + 1], ps1[i + 1]]) - - for i in range(8): - insert_chunk( - [ - (-ps2[i][0], ps2[i][1]), - (-ps1[i][0], ps1[i][1]), - (-ps1[i + 1][0], ps1[i + 1][1]), - (-ps2[i + 1][0], ps2[i + 1][1]), - ] - ) - - insert_chunk( - [ - ps1[8], - ps2[8], - (-ps1[8][0], ps1[8][1]), - (-ps2[8][0], ps2[8][1]), - ] - ) - - for i in range(4): - handle = bodies.insert( - rp.RigidBody.dynamic().translation((0.0, 0.5 + ps2[8][1] + 1.0 * i)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(2.0, 0.5).friction(friction), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_ball_and_chain.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_ball_and_chain.py deleted file mode 100644 index e227a361d..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_ball_and_chain.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Port of examples2d/s2d_ball_and_chain.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Ball and chain" - - -def _local_anchor(body, pivot): - """Pure Python equivalent of ``body.position.inverse_transform_point(pivot)``.""" - pose = body.position - return pose.inverse().transform_point(pivot) - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground = bodies.insert(rp.RigidBody.fixed()) - - count = 40 - hx = 0.5 - density = 20.0 - friction = 0.6 - - prev = ground - for i in range(count): - rb = ( - rp.RigidBody.dynamic() - .linear_damping(0.1) - .angular_damping(0.1) - .translation(((1.0 + 2.0 * i) * hx, count * hx)) - ) - handle = bodies.insert(rb) - colliders.insert_with_parent( - rp.Collider.capsule_x(hx, 0.125).friction(friction).density(density), - handle, - bodies, - ) - - pivot = ((2.0 * i) * hx, count * hx) - a1 = _local_anchor(bodies.get(prev), pivot) - a2 = _local_anchor(bodies.get(handle), pivot) - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((float(a1.x), float(a1.y))) - .local_anchor2((float(a2.x), float(a2.y))) - .contacts_enabled(False) - ) - impulse_joints.insert(prev, handle, joint, wake_up=True) - prev = handle - - radius = 8.0 - handle = bodies.insert( - rp.RigidBody.dynamic() - .linear_damping(0.1) - .angular_damping(0.1) - .translation(((1.0 + 2.0 * count) * hx + radius - hx, count * hx)) - ) - colliders.insert_with_parent( - rp.Collider.ball(radius).friction(friction).density(density), handle, bodies - ) - - pivot = ((2.0 * count) * hx, count * hx) - a1 = _local_anchor(bodies.get(prev), pivot) - a2 = _local_anchor(bodies.get(handle), pivot) - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((float(a1.x), float(a1.y))) - .local_anchor2((float(a2.x), float(a2.y))) - .contacts_enabled(False) - ) - impulse_joints.insert(prev, handle, joint, wake_up=True) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_bridge.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_bridge.py deleted file mode 100644 index d25354527..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_bridge.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Port of examples2d/s2d_bridge.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Bridge" - - -def _local_anchor(body, pivot): - return body.position.inverse().transform_point(pivot) - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground = bodies.insert(rp.RigidBody.fixed()) - - density = 20.0 - x_base = -80.0 - count = 160 - prev = ground - - for i in range(count): - rb = ( - rp.RigidBody.dynamic() - .linear_damping(0.1) - .angular_damping(0.1) - .translation((x_base + 0.5 + 1.0 * i, 20.0)) - ) - handle = bodies.insert(rb) - colliders.insert_with_parent( - rp.Collider.cuboid(0.5, 0.125).density(density), handle, bodies - ) - - pivot = (x_base + 1.0 * i, 20.0) - a1 = _local_anchor(bodies.get(prev), pivot) - a2 = _local_anchor(bodies.get(handle), pivot) - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((float(a1.x), float(a1.y))) - .local_anchor2((float(a2.x), float(a2.y))) - .contacts_enabled(False) - ) - impulse_joints.insert(prev, handle, joint, wake_up=True) - prev = handle - - pivot = (x_base + 1.0 * count, 20.0) - a1 = _local_anchor(bodies.get(prev), pivot) - a2 = _local_anchor(bodies.get(ground), pivot) - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((float(a1.x), float(a1.y))) - .local_anchor2((float(a2.x), float(a2.y))) - .contacts_enabled(False) - ) - impulse_joints.insert(prev, ground, joint, wake_up=True) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_card_house.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_card_house.py deleted file mode 100644 index 1a6a4d64a..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_card_house.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Port of examples2d/s2d_card_house.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Card house" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - friction = 0.7 - - ground_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -2.0))) - colliders.insert_with_parent( - rp.Collider.cuboid(40.0, 2.0).friction(friction), ground_handle, bodies - ) - - scale = 10.0 - card_height = 0.2 * scale - card_thickness = 0.001 * scale - angle0 = 25.0 * math.pi / 180.0 - angle1 = -25.0 * math.pi / 180.0 - angle2 = 0.5 * math.pi - - def card_box(): - return rp.Collider.cuboid(card_thickness, card_height).friction(friction) - - nb = 5 - z0 = 0.0 - y = card_height - 0.02 * scale - - while nb != 0: - z = z0 - for i in range(nb): - if i != nb - 1: - h = bodies.insert( - rp.RigidBody.dynamic() - .translation((z + 0.25 * scale, y + card_height - 0.015 * scale)) - .rotation(angle2) - ) - colliders.insert_with_parent(card_box(), h, bodies) - - h = bodies.insert( - rp.RigidBody.dynamic().translation((z, y)).rotation(angle1) - ) - colliders.insert_with_parent(card_box(), h, bodies) - - z += 0.175 * scale - - h = bodies.insert( - rp.RigidBody.dynamic().translation((z, y)).rotation(angle0) - ) - colliders.insert_with_parent(card_box(), h, bodies) - - z += 0.175 * scale - - y += card_height * 2.0 - 0.03 * scale - z0 += 0.175 * scale - nb -= 1 - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_confined.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_confined.py deleted file mode 100644 index b6cd6fe34..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_confined.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Port of examples2d/s2d_confined.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Confined" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - radius = 0.5 - grid_count = 25 - friction = 0.6 - max_count = grid_count * grid_count - - walls = [ - ((-10.5, 0.0), (10.5, 0.0)), - ((-10.5, 0.0), (-10.5, 20.5)), - ((10.5, 0.0), (10.5, 20.5)), - ((-10.5, 20.5), (10.5, 20.5)), - ] - for a, b in walls: - colliders.insert( - rp.Collider.capsule_from_endpoints(a, b, radius).friction(friction) - ) - - count = 0 - column = 0 - while count < max_count: - row = 0 - for _ in range(grid_count): - x = -8.75 + column * 18.0 / grid_count - y = 1.5 + row * 18.0 / grid_count - body_handle = bodies.insert( - rp.RigidBody.dynamic().translation((x, y)).gravity_scale(0.0) - ) - colliders.insert_with_parent( - rp.Collider.ball(radius).friction(friction), body_handle, bodies - ) - count += 1 - row += 1 - column += 1 - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_far_pyramid.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_far_pyramid.py deleted file mode 100644 index 0fd2f4e47..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_far_pyramid.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Port of examples2d/s2d_far_pyramid.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Far pyramid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - origin = (100_000.0, -80_000.0) - friction = 0.6 - - ground_handle = bodies.insert( - rp.RigidBody.fixed().translation((origin[0] + 0.0, origin[1] + -1.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(100.0, 1.0).friction(friction), ground_handle, bodies - ) - - base_count = 10 - h = 0.5 - shift = 1.25 * h - - for i in range(base_count): - y = (2.0 * i + 1.0) * shift + 0.5 - for j in range(i, base_count): - x = (i + 1.0) * shift + 2.0 * (j - i) * shift - h * base_count - handle = bodies.insert( - rp.RigidBody.dynamic().translation((origin[0] + x, origin[1] + y)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(h, h).friction(friction), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(origin[0], origin[1] + 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_1.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_1.py deleted file mode 100644 index 1de37416f..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_1.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Port of examples2d/s2d_high_mass_ratio_1.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "High mass ratio 1" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - extent = 1.0 - friction = 0.5 - ground_width = 66.0 * extent - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.segment( - (-0.5 * 2.0 * ground_width, 0.0), (0.5 * 2.0 * ground_width, 0.0) - ).friction(friction), - ground_handle, - bodies, - ) - - for j in range(3): - count = 10 - offset = -20.0 * extent + 2.0 * (count + 1) * extent * j - y = extent - - while count > 0: - for i in range(count): - coeff = i - 0.5 * count - yy = y + 2.0 if count == 1 else y - position = (2.0 * coeff * extent + offset, yy) - parent = bodies.insert(rp.RigidBody.dynamic().translation(position)) - density = (j + 1.0) * 100.0 if count == 1 else 1.0 - colliders.insert_with_parent( - rp.Collider.cuboid(extent, extent) - .density(density) - .friction(friction), - parent, - bodies, - ) - - count -= 1 - y += 2.0 * extent - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_2.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_2.py deleted file mode 100644 index ce10ab796..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_2.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Port of examples2d/s2d_high_mass_ratio_2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "High mass ratio 2" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - extent = 1.0 - friction = 0.6 - ground_width = 66.0 * extent - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.segment( - (-0.5 * 2.0 * ground_width, 0.0), (0.5 * 2.0 * ground_width, 0.0) - ).friction(friction), - ground_handle, - bodies, - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((-9.0 * extent, 0.5 * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.5 * extent, 0.5 * extent).friction(friction), - handle, - bodies, - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((9.0 * extent, 0.5 * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.5 * extent, 0.5 * extent).friction(friction), - handle, - bodies, - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((0.0, (10.0 + 16.0) * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(10.0 * extent, 10.0 * extent).friction(friction), - handle, - bodies, - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_3.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_3.py deleted file mode 100644 index 91bf16b98..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_high_mass_ratio_3.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Port of examples2d/s2d_high_mass_ratio_3.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "High mass ratio 3" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - extent = 1.0 - friction = 0.6 - - ground_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -2.0))) - colliders.insert_with_parent( - rp.Collider.cuboid(40.0, 2.0).friction(friction), ground_handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((-9.0 * extent, 0.5 * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.5 * extent, 0.5 * extent).friction(friction), - handle, - bodies, - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((9.0 * extent, 0.5 * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(0.5 * extent, 0.5 * extent).friction(friction), - handle, - bodies, - ) - - handle = bodies.insert( - rp.RigidBody.dynamic().translation((0.0, (10.0 + 16.0) * extent)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(10.0 * extent, 10.0 * extent).friction(friction), - handle, - bodies, - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_joint_grid.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_joint_grid.py deleted file mode 100644 index a44e1fb4c..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_joint_grid.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Port of examples2d/s2d_joint_grid.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Joint grid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.4 - numi = 100 - numk = 100 - shift = 1.0 - - handles = [None] * (numi * numk) - index = 0 - - for k in range(numk): - for i in range(numi): - is_fixed = numk // 2 - 3 <= k <= numk // 2 + 3 and i == 0 - rb_builder = rp.RigidBody.fixed() if is_fixed else rp.RigidBody.dynamic() - handle = bodies.insert(rb_builder.translation((k * shift, -i * shift))) - colliders.insert_with_parent(rp.Collider.ball(rad), handle, bodies) - - if i > 0: - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((0.0, -0.5 * shift)) - .local_anchor2((0.0, 0.5 * shift)) - .contacts_enabled(False) - ) - impulse_joints.insert( - handles[index - 1], handle, joint, wake_up=True - ) - - if k > 0: - joint = ( - rp.RevoluteJoint.builder() - .local_anchor1((0.5 * shift, 0.0)) - .local_anchor2((-0.5 * shift, 0.0)) - .contacts_enabled(False) - ) - impulse_joints.insert( - handles[index - numi], handle, joint, wake_up=True - ) - - handles[index] = handle - index += 1 - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/s2d_pyramid.py b/python/rapier-testbed/rapier_testbed/examples2/s2d_pyramid.py deleted file mode 100644 index 1a2163139..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/s2d_pyramid.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Port of examples2d/s2d_pyramid.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Inspired by Solver 2D" -NAME = "Pyramid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -1.0))) - colliders.insert_with_parent( - rp.Collider.cuboid(100.0, 1.0).friction(0.6), ground_handle, bodies - ) - - base_count = 100 - h = 0.5 - shift = 1.0 * h - - for i in range(base_count): - y = (2.0 * i + 1.0) * shift - for j in range(i, base_count): - x = (i + 1.0) * shift + 2.0 * (j - i) * shift - h * base_count - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent( - rp.Collider.cuboid(h, h).friction(0.6), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=20.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/sensor2.py b/python/rapier-testbed/rapier_testbed/examples2/sensor2.py deleted file mode 100644 index 1967c0c26..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/sensor2.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Port of examples2d/sensor2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Sensor" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 200.1 - ground_height = 0.1 - - ground_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -ground_height))) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_height), ground_handle, bodies - ) - - num = 10 - rad = 0.2 - shift = rad * 2.0 - centerx = shift * num / 2.0 - - for i in range(num): - x = i * shift - centerx - y = 3.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - sensor_handle = bodies.insert(rp.RigidBody.dynamic().translation((0.0, 10.0))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), sensor_handle, bodies) - colliders.insert_with_parent( - rp.Collider.ball(rad * 5.0) - .density(0.0) - .sensor(True) - .active_events(rp.ActiveEvents.COLLISION_EVENTS), - sensor_handle, - bodies, - ) - - events = rp.ChannelEventCollector() - - def callback(tb) -> None: - # Drain so the queue does not grow without bound. - events.drain_collision_events() - - testbed.set_event_handler(events) - testbed.add_callback(callback) - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 1.0), zoom=100.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/__init__.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/__init__.py deleted file mode 100644 index 593761e2f..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Bundled 2D stress-test examples. diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/balls2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/balls2.py deleted file mode 100644 index ae9137b87..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/balls2.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Port of examples2d/stress_tests/balls2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Balls" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - num = 50 - rad = 1.0 - shiftx = rad * 2.5 - shifty = rad * 2.0 - centerx = shiftx * num / 2.0 - centery = shifty / 2.0 - - for i in range(num): - for j in range(num * 5): - x = i * shiftx - centerx - y = j * shifty + centery - rb = rp.RigidBody.fixed() if j == 0 else rp.RigidBody.dynamic() - handle = bodies.insert(rb.translation((x, y))) - colliders.insert_with_parent(rp.Collider.ball(rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=5.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/boxes2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/boxes2.py deleted file mode 100644 index aba831aea..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/boxes2.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Port of examples2d/stress_tests/boxes2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Boxes" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 25.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((-ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - num = 26 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * num / 2.0 - centery = shift / 2.0 - - for i in range(num): - for j in range(num * 5): - x = i * shift - centerx - y = j * shift + centery + 2.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 50.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/capsules2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/capsules2.py deleted file mode 100644 index a7f1c082d..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/capsules2.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Port of examples2d/stress_tests/capsules2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Capsules" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 25.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((ground_size, ground_size * 4.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 4.0, 1.2), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((-ground_size, ground_size * 4.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 4.0, 1.2), handle, bodies - ) - - num = 26 - numy = num * 5 - rad = 0.5 - shift = rad * 2.0 - shifty = rad * 5.0 - centerx = shift * num / 2.0 - centery = shift / 2.0 - - for i in range(num): - for j in range(numy): - x = i * shift - centerx - y = j * shifty + centery + 3.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent( - rp.Collider.capsule_y(rad * 1.5, rad), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 50.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/convex_polygons2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/convex_polygons2.py deleted file mode 100644 index 85f2f4e37..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/convex_polygons2.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Port of examples2d/stress_tests/convex_polygons2.rs.""" -from __future__ import annotations - -import math -import random - -import numpy as np - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Convex polygons" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 30.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((-ground_size, ground_size * 2.0)) - ) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size * 2.0, 1.2), handle, bodies - ) - - num = 26 - scale = 2.0 - shift = scale - centerx = shift * num / 2.0 - centery = shift / 2.0 - - rng = random.Random(0) - - for i in range(num): - for j in range(num * 5): - x = i * shift - centerx - y = j * shift * 2.0 + centery + 2.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - pts = np.array( - [ - [rng.random() * scale, rng.random() * scale] - for _ in range(10) - ], - dtype=np.float32, - ) - colliders.insert_with_parent(rp.Collider.convex_hull(pts), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 50.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/heightfield2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/heightfield2.py deleted file mode 100644 index 143e6dd29..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/heightfield2.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Port of examples2d/stress_tests/heightfield2.rs.""" -from __future__ import annotations - -import math - -import numpy as np - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Heightfield" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = (50.0, 1.0) - nsubdivs = 2000 - - heights = np.zeros(nsubdivs + 1, dtype=np.float32) - for i in range(nsubdivs + 1): - if i == 0 or i == nsubdivs: - heights[i] = 80.0 - else: - heights[i] = math.cos(i * ground_size[0] / nsubdivs) * 2.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.heightfield(heights, ground_size), handle, bodies - ) - - num = 26 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * (num // 2) - centery = shift / 2.0 - - for i in range(num): - for j in range(num * 5): - x = i * shift - centerx - y = j * shift + centery + 3.0 - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - if j % 2 == 0: - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad), handle, bodies - ) - else: - colliders.insert_with_parent(rp.Collider.ball(rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 50.0), zoom=10.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_ball2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_ball2.py deleted file mode 100644 index 284705c96..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_ball2.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Port of examples2d/stress_tests/joint_ball2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "(Stress test) joint ball" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.4 - numi = 100 - numk = 100 - shift = 1.0 - - body_handles = [] - - for k in range(numk): - for i in range(numi): - is_fixed = numk // 2 - 3 <= k <= numk // 2 + 3 and i == 0 - rb_builder = rp.RigidBody.fixed() if is_fixed else rp.RigidBody.dynamic() - child_handle = bodies.insert(rb_builder.translation((k * shift, -i * shift))) - colliders.insert_with_parent(rp.Collider.ball(rad), child_handle, bodies) - - if i > 0: - parent = body_handles[-1] - joint = rp.RevoluteJoint.builder().local_anchor2((0.0, shift)) - impulse_joints.insert(parent, child_handle, joint, wake_up=True) - - if k > 0: - parent_index = len(body_handles) - numi - parent = body_handles[parent_index] - joint = rp.RevoluteJoint.builder().local_anchor2((-shift, 0.0)) - impulse_joints.insert(parent, child_handle, joint, wake_up=True) - - body_handles.append(child_handle) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(numk * rad, numi * -rad), zoom=5.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_fixed2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_fixed2.py deleted file mode 100644 index 3a307dd22..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_fixed2.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Port of examples2d/stress_tests/joint_fixed2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "(Stress test) joint fixed" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.4 - num = 30 - shift = 1.0 - - body_handles = [] - - for xx in range(4): - x = xx * shift * (num + 2) - - for yy in range(4): - y = yy * shift * (num + 4) - - for k in range(num): - for i in range(num): - rb_builder = ( - rp.RigidBody.fixed() if k == 0 else rp.RigidBody.dynamic() - ) - child_handle = bodies.insert( - rb_builder.translation((x + k * shift, y - i * shift)) - ) - colliders.insert_with_parent( - rp.Collider.ball(rad), child_handle, bodies - ) - - if i > 0: - parent = body_handles[-1] - joint = ( - rp.FixedJoint.builder() - .local_frame2(rp.Isometry2.from_translation(0.0, shift)) - ) - impulse_joints.insert( - parent, child_handle, joint, wake_up=True - ) - - if k > 0: - parent_index = len(body_handles) - num - parent = body_handles[parent_index] - joint = ( - rp.FixedJoint.builder() - .local_frame2(rp.Isometry2.from_translation(-shift, 0.0)) - ) - impulse_joints.insert( - parent, child_handle, joint, wake_up=True - ) - - body_handles.append(child_handle) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(50.0, 50.0), zoom=5.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_prismatic2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_prismatic2.py deleted file mode 100644 index 61c86e5cc..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/joint_prismatic2.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Port of examples2d/stress_tests/joint_prismatic2.rs.""" -from __future__ import annotations - -import math - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "(Stress test) joint prismatic" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - rad = 0.4 - num = 10 - shift = 1.0 - - inv_sqrt2 = 1.0 / math.sqrt(2.0) - - for l in range(25): - y_l = l * shift * (num + 2) * 2.0 - - for j in range(50): - x = j * shift * 4.0 - - ground = bodies.insert(rp.RigidBody.fixed().translation((x, y_l))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), ground, bodies) - curr_parent = ground - - for i in range(num): - yy = y_l - (i + 1) * shift - density = 1.0 - child = bodies.insert(rp.RigidBody.dynamic().translation((x, yy))) - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad).density(density), child, bodies - ) - - axis = ( - (inv_sqrt2, inv_sqrt2) if i % 2 == 0 else (-inv_sqrt2, inv_sqrt2) - ) - prism = ( - rp.PrismaticJoint.builder(axis) - .local_anchor2((0.0, shift)) - .limits(-1.5, 1.5) - ) - impulse_joints.insert(curr_parent, child, prism, wake_up=True) - curr_parent = child - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(80.0, 80.0), zoom=15.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/pyramid2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/pyramid2.py deleted file mode 100644 index ca66913a2..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/pyramid2.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Port of examples2d/stress_tests/pyramid2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Pyramid" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 100.0 - ground_thickness = 1.0 - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_thickness), ground_handle, bodies - ) - - num = 100 - rad = 0.5 - shift = rad * 2.0 - centerx = shift * num / 2.0 - centery = shift / 2.0 + ground_thickness + rad * 1.5 - - for i in range(num): - for j in range(i, num): - x = (i * shift / 2.0) + (j - i) * shift - centerx - y = i * shift + centery - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent(rp.Collider.cuboid(rad, rad), handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=5.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/vertical_stacks2.py b/python/rapier-testbed/rapier_testbed/examples2/stress_tests/vertical_stacks2.py deleted file mode 100644 index 410fa8c5c..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/stress_tests/vertical_stacks2.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Port of examples2d/stress_tests/vertical_stacks2.rs.""" -from __future__ import annotations - -import rapier2d as rp -from ..._registry import register - -CATEGORY = "Stress Tests" -NAME = "Verticals stacks" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - num = 80 - rad = 0.5 - ground_size = num * rad * 10.0 - ground_thickness = 1.0 - - ground_handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent( - rp.Collider.cuboid(ground_size, ground_thickness), ground_handle, bodies - ) - - shiftx_centerx = [ - (rad * 2.0 + 0.0002, -num * rad * 2.0 * 1.5), - (rad * 2.0 + rad, num * rad * 2.0 * 1.5), - ] - - for shiftx, centerx in shiftx_centerx: - shifty = rad * 2.0 - centery = shifty / 2.0 + ground_thickness - - for i in range(num): - for j in range(1 + i * 2): - x = (j - i) * shiftx + centerx - y = (num - i - 1) * shifty + centery - handle = bodies.insert(rp.RigidBody.dynamic().translation((x, y))) - colliders.insert_with_parent( - rp.Collider.cuboid(rad, rad), handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 2.5), zoom=5.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from ..._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/trimesh2.py b/python/rapier-testbed/rapier_testbed/examples2/trimesh2.py deleted file mode 100644 index 003f6edf2..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/trimesh2.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Port of examples2d/trimesh2.rs. - -The Rust example tessellates the Rapier SVG logo and instantiates the -resulting trimeshes. The Python port uses the ``utils.svg`` placeholder -which returns a few regular n-gon meshes — visually different from the -logo, but exercises the trimesh code path. -""" -from __future__ import annotations - -import math - -import rapier2d as rp -from .._registry import register -from .utils import svg as svg_utils - -CATEGORY = "Collisions" -NAME = "Trimesh" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - ground_size = 25.0 - - handle = bodies.insert(rp.RigidBody.fixed()) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((ground_size, ground_size)) - ) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - handle = bodies.insert( - rp.RigidBody.fixed() - .rotation(math.pi / 2.0) - .translation((-ground_size, ground_size)) - ) - colliders.insert_with_parent(rp.Collider.cuboid(ground_size, 1.2), handle, bodies) - - rapier_logo_buffers = svg_utils.rapier_logo() - - for ith, (vtx, idx) in enumerate(rapier_logo_buffers): - for k in range(5): - collider = rp.Collider.trimesh(vtx, idx).contact_skin(0.2) - rb = rp.RigidBody.dynamic().translation( - (ith * 8.0 - 20.0, 20.0 + k * 11.0) - ) - handle = bodies.insert(rb) - colliders.insert_with_parent(collider, handle, bodies) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 20.0), zoom=17.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/rapier-testbed/rapier_testbed/examples2/utils/__init__.py b/python/rapier-testbed/rapier_testbed/examples2/utils/__init__.py deleted file mode 100644 index 9b5ffe60c..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Bundled 2D example utilities. diff --git a/python/rapier-testbed/rapier_testbed/examples2/utils/character.py b/python/rapier-testbed/rapier_testbed/examples2/utils/character.py deleted file mode 100644 index 483ec52a5..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/utils/character.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Minimal placeholder for the Rust ``utils::character`` helper. - -The Rust example wires a manually-driven character through the testbed's -keyboard and ``egui`` UI layers. The Python mini-testbed has neither, so -this helper is intentionally empty — examples that originally called -``character::update_character`` just build a static scene in Python. -""" -from __future__ import annotations - -__all__: list[str] = [] diff --git a/python/rapier-testbed/rapier_testbed/examples2/utils/svg.py b/python/rapier-testbed/rapier_testbed/examples2/utils/svg.py deleted file mode 100644 index cd3d5af28..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/utils/svg.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Minimal placeholder for the Rust ``utils::svg`` helper. - -The Rust example uses ``lyon`` + ``usvg`` to tessellate the Rapier SVG -logo into trimeshes. There is no equivalent in pure Python without -pulling in heavy dependencies, so this helper provides a triangle-mesh -substitute that the ``trimesh2`` port can use for visual diversity. -""" -from __future__ import annotations - -import math -from typing import List, Tuple - -import numpy as np - - -def rapier_logo() -> List[Tuple[np.ndarray, np.ndarray]]: - """Return a list of ``(vertices, indices)`` pairs. - - Each pair describes a small trimesh that stands in for one glyph of - the Rapier logo in the Rust testbed. The shapes are convex polygons - (regular n-gons of varying sides) so the scene still has variety. - """ - out: List[Tuple[np.ndarray, np.ndarray]] = [] - for sides in (5, 6, 7, 8, 9, 10): - radius = 2.0 - verts = np.array( - [ - [0.0, 0.0], - *[ - [math.cos(2.0 * math.pi * k / sides) * radius, - math.sin(2.0 * math.pi * k / sides) * radius] - for k in range(sides) - ], - ], - dtype=np.float32, - ) - idx = np.array( - [[0, k + 1, ((k + 1) % sides) + 1] for k in range(sides)], - dtype=np.uint32, - ) - out.append((verts, idx)) - return out - - -__all__ = ["rapier_logo"] diff --git a/python/rapier-testbed/rapier_testbed/examples2/voxels2.py b/python/rapier-testbed/rapier_testbed/examples2/voxels2.py deleted file mode 100644 index 3a90c95e5..000000000 --- a/python/rapier-testbed/rapier_testbed/examples2/voxels2.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Port of examples2d/voxels2.rs. - -binding gap: ``SharedShape::voxelized_mesh`` and -``ColliderBuilder::voxels_from_points`` are not yet exposed in the Python -bindings (no ``Collider.voxels*`` / ``SharedShape.voxelized_mesh``). The -port falls back to the dynamic-falling-objects scene without the voxel -ground / voxelized obstacle. -""" -from __future__ import annotations - -import rapier2d as rp -from .._registry import register - -CATEGORY = "Collisions" -NAME = "Voxels" - - -def init_world(testbed) -> None: - bodies = rp.RigidBodySet() - colliders = rp.ColliderSet() - impulse_joints = rp.ImpulseJointSet() - multibody_joints = rp.MultibodyJointSet() - - nx = 50 - for i in range(nx): - for j in range(10): - rb = rp.RigidBody.dynamic().translation( - (i * 2.0 - nx / 2.0, 20.0 + j * 2.0) - ) - rb_handle = bodies.insert(rb) - ball_radius = 0.5 - kind = j % 3 - if kind == 0: - co = rp.Collider.ball(ball_radius) - elif kind == 1: - co = rp.Collider.cuboid(ball_radius, ball_radius) - else: - co = rp.Collider.capsule_y(ball_radius, ball_radius) - colliders.insert_with_parent(co, rb_handle, bodies) - - # Stand-in for the voxelized wavy floor — a wide cuboid ground. - ground_handle = bodies.insert(rp.RigidBody.fixed().translation((0.0, -2.0))) - colliders.insert_with_parent( - rp.Collider.cuboid(200.0, 1.0), ground_handle, bodies - ) - - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints) - testbed.set_camera_2d(center=(0.0, 20.0), zoom=17.0) - - -register(CATEGORY, NAME, init_world, dim=2) - -if __name__ == "__main__": - from .._picker import run - - run(initial=f"{CATEGORY} / {NAME}") diff --git a/python/tests/test_controllers.py b/python/tests/test_controllers.py index a89d0b46a..bcbce1ca2 100644 --- a/python/tests/test_controllers.py +++ b/python/tests/test_controllers.py @@ -14,10 +14,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_controllers_2d.py b/python/tests/test_controllers_2d.py deleted file mode 100644 index ae9ab162e..000000000 --- a/python/tests/test_controllers_2d.py +++ /dev/null @@ -1,105 +0,0 @@ -"""2D controller tests. - -Only the character controller and the PID/PD family are bound in 2D (vehicle -is gated to 3D upstream — see `src/control/mod.rs`). -""" - -from __future__ import annotations - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_character_length_2d(ns): - a = ns.CharacterLength.absolute(0.5) - r = ns.CharacterLength.relative(0.1) - assert a.kind == "absolute" - assert r.kind == "relative" - - -def test_axes_mask_2d_constants(ns): - am = ns.AxesMask.LIN_X | ns.AxesMask.LIN_Y | ns.AxesMask.ANG_Z - assert ns.AxesMask.LIN_X in am - assert ns.AxesMask.LIN_Y in am - assert ns.AxesMask.ANG_Z in am - # In 2D there's no LIN_Z / ANG_X / ANG_Y. - assert not hasattr(ns.AxesMask, "LIN_Z") - assert not hasattr(ns.AxesMask, "ANG_X") - assert not hasattr(ns.AxesMask, "ANG_Y") - - -def test_character_controller_2d_moves(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81), auto_update_query=True) - # Flat ground. - w.add_body( - ns.RigidBody.fixed(translation=(0, -1.0)), - colliders=[ns.Collider.cuboid(50, 1)], - ) - w.update_query_pipeline() - - ctrl = ns.KinematicCharacterController( - up=(0, 1), - offset=ns.CharacterLength.absolute(0.01), - slide=True, - snap_to_ground=ns.CharacterLength.absolute(0.5), - ) - shape = ns.SharedShape.ball(0.5) - pose = ns.Isometry2.from_translation(0.0, 0.55) - mv = ctrl.move_shape( - 1.0 / 60.0, - w.rigid_bodies, - w.colliders, - w.query_pipeline, - shape, - pose, - (0.1, -0.05), - ns.QueryFilter(), - ) - assert isinstance(mv, ns.EffectiveCharacterMovement) - # Movement along +X should make it through. - assert abs(mv.translation.x - 0.1) < 1e-3 - assert mv.grounded is True - - -def test_pid_2d_construction(ns): - pid = ns.PidController( - axes=ns.AxesMask.all(), - Kp=(60.0, 60.0), - Ki=(0.0, 0.0), - Kd=(0.8, 0.8), - ) - pose = ns.Isometry2.from_translation(2.0, 0.0) - target = ns.Isometry2.from_translation(2.0, 0.0) - corr = pid.position_correction(1.0 / 60.0, pose, target) - # No error → no correction. - assert abs(corr.linear.x) < 1e-4 - assert abs(corr.linear.y) < 1e-4 - - -def test_pd_2d_correction(ns): - pd = ns.PdController( - axes=ns.AxesMask.all(), - Kp=(10.0, 10.0), - Kd=(1.0, 1.0), - ) - w = ns.PhysicsWorld() - h = w.add_body(ns.RigidBody.dynamic(translation=(0, 0)).build()) - body = w.rigid_bodies[h] - target = ns.Isometry2.from_translation(1.0, 0.0) - corr = pd.rigid_body_correction(body, target) - assert corr.linear.x > 0.0 - - -def test_no_vehicle_controller_in_2d(ns): - """The 3D-only vehicle controller is intentionally absent in 2D.""" - assert not hasattr(ns, "DynamicRayCastVehicleController") - assert not hasattr(ns, "Wheel") - assert not hasattr(ns, "WheelTuning") - assert not hasattr(ns, "RayCastInfo") diff --git a/python/tests/test_debug_render.py b/python/tests/test_debug_render.py index 9188270f2..467c9f98e 100644 --- a/python/tests/test_debug_render.py +++ b/python/tests/test_debug_render.py @@ -1,8 +1,4 @@ -"""3D debug-render tests. - -Parametrized across the f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`) -flavors. -""" +"""3D debug-render tests (f32).""" from __future__ import annotations @@ -10,10 +6,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_debug_render_2d.py b/python/tests/test_debug_render_2d.py deleted file mode 100644 index f02b07217..000000000 --- a/python/tests/test_debug_render_2d.py +++ /dev/null @@ -1,110 +0,0 @@ -"""2D debug-render tests (abbreviated). - -Parametrized across the f32 (`rapier.dim2`) and f64 (`rapier.dim2.f64`) -flavors. -""" - -from __future__ import annotations - -import numpy as np -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def _ball_on_ground_world(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - w.colliders.insert(ns.Collider.cuboid(50, 0.1).build()) - bh = w.rigid_bodies.insert( - ns.RigidBody.dynamic(translation=(0, 5)).build() - ) - w.colliders.insert_with_parent( - ns.Collider.ball(0.5), bh, w.rigid_bodies - ) - for _ in range(20): - w.step() - return w - - -def test_render_to_arrays_2d(ns): - w = _ball_on_ground_world(ns) - pipe = ns.DebugRenderPipeline() - lines, colors, objects = pipe.render_to_arrays( - w.rigid_bodies, - w.colliders, - w.impulse_joints, - w.multibody_joints, - w.narrow_phase, - ) - assert isinstance(lines, np.ndarray) - assert lines.ndim == 3 - assert lines.shape[1] == 2 - assert lines.shape[2] == 2 # 2D - assert lines.shape[0] > 0 - assert colors.shape == (lines.shape[0], 4) - assert objects.shape == (lines.shape[0],) - - -def test_collector_iter_2d(ns): - w = _ball_on_ground_world(ns) - pipe = ns.DebugRenderPipeline() - coll = ns.DebugLineCollector() - pipe.render( - w.rigid_bodies, - w.colliders, - w.impulse_joints, - w.multibody_joints, - w.narrow_phase, - coll, - ) - items = list(coll) - assert len(items) > 0 - obj, a, b, color = items[0] - assert isinstance(obj, ns.DebugRenderObject) - assert isinstance(a, ns.Vec2) - assert isinstance(b, ns.Vec2) - assert isinstance(color, ns.DebugColor) - - -def test_aabb_mode_2d(ns): - w = _ball_on_ground_world(ns) - pipe = ns.DebugRenderPipeline(mode=ns.DebugRenderMode.COLLIDER_AABBS) - lines, _, objects = pipe.render_to_arrays( - w.rigid_bodies, - w.colliders, - w.impulse_joints, - w.multibody_joints, - w.narrow_phase, - ) - # Two colliders × 4 AABB edges = at least 4 lines. - assert lines.shape[0] >= 4 - assert (objects == ns.DebugRenderObject.COLLIDER_AABB.kind).all() - - -def test_python_backend_2d(ns): - w = _ball_on_ground_world(ns) - pipe = ns.DebugRenderPipeline() - - class Recorder: - def __init__(self): - self.n = 0 - - def draw_line(self, obj, a, b, color): - self.n += 1 - - r = Recorder() - pipe.render( - w.rigid_bodies, - w.colliders, - w.impulse_joints, - w.multibody_joints, - w.narrow_phase, - r, - ) - assert r.n > 0 diff --git a/python/tests/test_deferred_bindings.py b/python/tests/test_deferred_bindings.py index 7c6beda34..771632a6c 100644 --- a/python/tests/test_deferred_bindings.py +++ b/python/tests/test_deferred_bindings.py @@ -2,10 +2,9 @@ Covers the items deferred from the first coverage pass: -* ``RevoluteJoint.angle`` (2D + 3D). +* ``RevoluteJoint.angle`` (3D). * extra shape constructors: ``round_triangle``, ``round_convex_hull``, ``voxels`` / ``voxels_from_points``, ``convex_mesh`` (3D), - ``convex_polyline`` / ``round_convex_polyline`` (2D), ``converted_trimesh``, and ``MeshConverter.CONVEX_DECOMPOSITION``. * URDF / MJCF ``collider_blueprint`` / ``rigid_body_blueprint`` and the MJCF ``contact_filter_mode`` (``ContactFilterMode``). @@ -21,24 +20,16 @@ import pytest -import rapier2d as dim2 import rapier3d as dim3 -import rapier2d_f64 as dim2_f64 -import rapier3d_f64 as dim3_f64 from rapier3d.loaders import mjcf as mjcf_loader from rapier3d.loaders import urdf as urdf_loader -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param -@pytest.fixture(params=[dim2, dim2_f64], ids=["2d-f32", "2d-f64"]) -def ns2(request): - return request.param - - # -------------------------------------------------------------------------- # RevoluteJoint.angle # -------------------------------------------------------------------------- @@ -51,20 +42,12 @@ def test_revolute_angle_3d(ns): assert j.angle(rot1, rot2) == pytest.approx(math.radians(30), abs=1e-4) -def test_revolute_angle_2d(ns2): - j = ns2.RevoluteJoint.builder().build() - rot1 = ns2.Rotation2.identity() - rot2 = ns2.Rotation2.from_angle(math.radians(20)) - assert j.angle(rot1, rot2) == pytest.approx(math.radians(20), abs=1e-4) - - # -------------------------------------------------------------------------- # shape constructors # -------------------------------------------------------------------------- _TET = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] _TET_IDX = [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]] -_QUAD = [(0, 0), (1, 0), (1, 1), (0, 1)] def test_round_triangle(ns): @@ -91,18 +74,6 @@ def test_voxels_3d(ns): assert s2.shape_type == ns.ShapeType.VOXELS -def test_convex_polyline_2d(ns2): - s = ns2.SharedShape.convex_polyline(_QUAD) - assert s.shape_type == ns2.ShapeType.CONVEX_POLYGON - s2 = ns2.SharedShape.round_convex_polyline(_QUAD, 0.05) - assert s2.shape_type == ns2.ShapeType.ROUND_CONVEX_POLYGON - - -def test_voxels_2d(ns2): - s = ns2.SharedShape.voxels((0.5, 0.5), [(0, 0), (1, 0), (0, 1)]) - assert s.shape_type == ns2.ShapeType.VOXELS - - def test_collider_shape_ctors_3d(ns): ns.Collider.round_triangle((0, 0, 0), (1, 0, 0), (0, 1, 0), 0.1).build() ns.Collider.round_convex_hull(_TET, 0.05).build() @@ -112,12 +83,6 @@ def test_collider_shape_ctors_3d(ns): ns.Collider.voxels_from_points((0.5, 0.5, 0.5), [(0, 0, 0), (0.9, 0, 0)]).build() -def test_collider_shape_ctors_2d(ns2): - ns2.Collider.convex_polyline(_QUAD).build() - ns2.Collider.round_convex_polyline(_QUAD, 0.05).build() - ns2.Collider.voxels((0.5, 0.5), [(0, 0), (1, 0)]).build() - - def test_converted_trimesh(ns): coll = ns.Collider.converted_trimesh( _TET, _TET_IDX, ns.MeshConverter.CONVEX_HULL @@ -130,8 +95,6 @@ def test_converted_trimesh(ns): def test_mesh_converter_convex_decomposition_is_3d_only(): assert hasattr(dim3.MeshConverter, "CONVEX_DECOMPOSITION") - assert hasattr(dim3_f64.MeshConverter, "CONVEX_DECOMPOSITION") - assert not hasattr(dim2.MeshConverter, "CONVEX_DECOMPOSITION") # -------------------------------------------------------------------------- @@ -208,7 +171,7 @@ def test_multibody_body_jacobian_shape(ns): w.step() # populate jacobians mb = w.multibody_joints.multibody(handle) jac = mb.body_jacobian(1) - rows = 6 if ns in (dim3, dim3_f64) else 3 + rows = 6 assert len(jac) == rows assert all(len(r) == mb.ndofs for r in jac) diff --git a/python/tests/test_dynamics.py b/python/tests/test_dynamics.py index 3dc671a78..e9af069df 100644 --- a/python/tests/test_dynamics.py +++ b/python/tests/test_dynamics.py @@ -1,8 +1,4 @@ -"""3D dynamics tests. - -Parametrized across the f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`) -flavors so we catch any codepath that quietly only got applied to one. -""" +"""3D dynamics tests (f32).""" from __future__ import annotations @@ -12,10 +8,9 @@ import rapier3d as rapier import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_dynamics_2d.py b/python/tests/test_dynamics_2d.py deleted file mode 100644 index 389348174..000000000 --- a/python/tests/test_dynamics_2d.py +++ /dev/null @@ -1,129 +0,0 @@ -"""2D dynamics tests. - -Parametrized across f32 and f64 flavors. -""" - -from __future__ import annotations - -import math as pymath - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_handle_hash_and_eq(ns): - h1 = ns.RigidBodyHandle.from_raw_parts(3, 7) - h2 = ns.RigidBodyHandle.from_raw_parts(3, 7) - h3 = ns.RigidBodyHandle.from_raw_parts(3, 8) - assert h1 == h2 - assert h1 != h3 - assert hash(h1) == hash(h2) - - -def test_build_insert_lookup(ns): - s = ns.RigidBodySet() - b = ns.RigidBody.dynamic(translation=(1.0, 2.0)).build() - h = s.insert(b) - assert h in s - body = s[h] - t = body.translation - assert abs(t.x - 1.0) < 1e-5 - assert abs(t.y - 2.0) < 1e-5 - - -def test_remove_invalidates_handle(ns): - s = ns.RigidBodySet() - islands = ns.IslandManager() - cset = ns.ColliderSet() - ijs = ns.ImpulseJointSet() - mjs = ns.MultibodyJointSet() - h = s.insert(ns.RigidBody.dynamic(translation=(0, 0)).build()) - removed = s.remove(h, islands, cset, ijs, mjs) - assert removed is not None - assert h not in s - with pytest.raises(ns.InvalidHandle): - _ = s[h] - - -def test_body_type_setter(ns): - s = ns.RigidBodySet() - h = s.insert(ns.RigidBody.dynamic().build()) - body = s[h] - body.body_type = ns.RigidBodyType.FIXED - assert body.body_type == ns.RigidBodyType.FIXED - - -def test_locked_axes_2d(ns): - la = ns.LockedAxes.TRANSLATION_LOCKED_X | ns.LockedAxes.ROTATION_LOCKED - assert ns.LockedAxes.TRANSLATION_LOCKED_X in la - assert ns.LockedAxes.ROTATION_LOCKED in la - # Round-trip - b = ns.RigidBody.dynamic().build() - b.locked_axes = la - assert b.locked_axes.bits == la.bits - - -def test_apply_impulse_2d(ns): - b = ns.RigidBody.dynamic().build() - b.set_additional_mass(1.0) - cset = ns.ColliderSet() - b.recompute_mass_properties_from_colliders(cset) - b.apply_impulse((10.0, 0.0)) - assert abs(b.linvel.x - 10.0) < 1e-4 - - -def test_kinetic_energy_2d(ns): - b = ns.RigidBody.dynamic().build() - b.set_additional_mass(2.0) - cset = ns.ColliderSet() - b.recompute_mass_properties_from_colliders(cset) - b.linvel = (3.0, 0.0) - ke = b.kinetic_energy() - assert abs(ke - 9.0) < 1e-4 - - -def test_angvel_scalar_2d(ns): - b = ns.RigidBody.dynamic().build() - b.angvel = 1.5 - assert abs(b.angvel - 1.5) < 1e-5 - - -def test_integration_parameters_2d(ns): - p = ns.IntegrationParameters() - p.dt = 1.0 / 30.0 - assert abs(p.dt - 1.0 / 30.0) < 1e-6 - - -def test_mass_properties_ball_2d(ns): - mp = ns.MassProperties.from_ball(1.0, 1.0) - expected = pymath.pi # 2D ball mass = density * pi * r^2 = 1 * pi * 1 = pi - assert abs(mp.mass - expected) < 1e-4 - - -def test_activation_struct_2d(ns): - a = ns.RigidBodyActivation.active() - assert a.is_active() - - -def test_island_manager_2d(ns): - im = ns.IslandManager() - assert len(im) == 0 - - -def test_spring_coefficients_2d(ns): - sc = ns.SpringCoefficients.contact_defaults() - assert sc.stiffness > 0.0 - - -def test_ccd_solver_2d(ns): - cs = ns.CCDSolver() - cs.clear() - with pytest.raises(NotImplementedError): - cs.solve_ccd() diff --git a/python/tests/test_events_hooks.py b/python/tests/test_events_hooks.py index 498279be5..ff972b53b 100644 --- a/python/tests/test_events_hooks.py +++ b/python/tests/test_events_hooks.py @@ -1,17 +1,13 @@ -"""3D events & hooks tests. - -Parametrized across f32 / f64 flavors. -""" +"""3D events & hooks tests (f32).""" from __future__ import annotations import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_events_hooks_2d.py b/python/tests/test_events_hooks_2d.py deleted file mode 100644 index 410bad508..000000000 --- a/python/tests/test_events_hooks_2d.py +++ /dev/null @@ -1,112 +0,0 @@ -"""2D events & hooks tests (abbreviated).""" - -from __future__ import annotations - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def _two_balls_world(ns): - w = ns.PhysicsWorld(gravity=(0, 0)) - h1 = w.rigid_bodies.insert( - ns.RigidBody.dynamic(translation=(-1.0, 0)) - .linvel(ns.Vec2(1.0, 0)) - .build() - ) - h2 = w.rigid_bodies.insert( - ns.RigidBody.dynamic(translation=(1.0, 0)) - .linvel(ns.Vec2(-1.0, 0)) - .build() - ) - c1 = ns.Collider.ball(0.5).active_events(ns.ActiveEvents.COLLISION_EVENTS) - c2 = ns.Collider.ball(0.5).active_events(ns.ActiveEvents.COLLISION_EVENTS) - ch1 = w.colliders.insert_with_parent(c1, h1, w.rigid_bodies) - ch2 = w.colliders.insert_with_parent(c2, h2, w.rigid_bodies) - return w, ch1, ch2 - - -def test_channel_collector_2d(ns): - w, ch1, ch2 = _two_balls_world(ns) - collector = ns.ChannelEventCollector() - w.event_handler = collector - for _ in range(60): - w.step() - events = collector.drain_collision_events() - assert any(e.started for e in events) - - -def test_custom_event_handler_2d(ns): - received = [] - - class MyHandler: - def handle_collision_event(self, bodies, colliders, event, contact_pair): - received.append(event) - - def handle_contact_force_event(self, *_a, **_k): - pass - - w, _, _ = _two_balls_world(ns) - w.event_handler = MyHandler() - for _ in range(60): - w.step() - assert len(received) > 0 - - -def test_solver_flags_2d(ns): - assert ns.SolverFlags.COMPUTE_IMPULSES.bits == 1 - assert ns.SolverFlags.empty().is_empty() - - -def test_physics_hooks_filter_2d(ns): - w, ch1, ch2 = _two_balls_world(ns) - - class Filter: - def filter_contact_pair(self, ctx): - return None - - def filter_intersection_pair(self, ctx): - return True - - def modify_solver_contacts(self, ctx): - pass - - w.physics_hooks = Filter() - c1 = w.colliders[ch1] - c1.active_hooks = ns.ActiveHooks.FILTER_CONTACT_PAIR - c2 = w.colliders[ch2] - c2.active_hooks = ns.ActiveHooks.FILTER_CONTACT_PAIR - - start = w.rigid_bodies[w.colliders[ch1].parent].translation.x - for _ in range(60): - w.step() - end = w.rigid_bodies[w.colliders[ch1].parent].translation.x - assert end > start + 0.5 - - -def test_event_callback_exception_deferred_2d(ns): - w, _, _ = _two_balls_world(ns) - - class Bad: - def handle_collision_event(self, *args, **kwargs): - raise RuntimeError("boom2d") - - def handle_contact_force_event(self, *args, **kwargs): - pass - - w.event_handler = Bad() - raised = False - for _ in range(120): - try: - w.step() - except RuntimeError as e: - assert "boom2d" in str(e) - raised = True - break - assert raised diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index 6a432494f..701ec55fd 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -33,7 +33,7 @@ # enhanced-determinism). Assert it drove backward at a real speed instead. "vehicle/drive.py": re.compile(r"^vehicle: speed=-\d+\.\d+ km/h vx=[-+]\d+\.\d+$"), "urdf/load_simple.py": "urdf: name=two_link links=2 joints=1", - "render/matplotlib_animation.py": "matplotlib: segments=9720 frames=120", + "render/matplotlib_animation.py": "matplotlib: segments=27000 frames=120", "serde/snapshot_restore.py": "snapshot: snap.y=0.58 later.y=0.60 bytes=1767", # perf/many_bodies prints a timing-dependent ms/frame value; we only # assert that it ran with the expected shape. diff --git a/python/tests/test_geometry.py b/python/tests/test_geometry.py index e97dd0bf5..acfcda8b6 100644 --- a/python/tests/test_geometry.py +++ b/python/tests/test_geometry.py @@ -1,7 +1,4 @@ -"""3D geometry tests. - -Parametrized across f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`). -""" +"""3D geometry tests (f32).""" from __future__ import annotations @@ -12,10 +9,9 @@ import rapier3d as rapier import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_geometry_2d.py b/python/tests/test_geometry_2d.py deleted file mode 100644 index d03eaaada..000000000 --- a/python/tests/test_geometry_2d.py +++ /dev/null @@ -1,116 +0,0 @@ -"""2D geometry tests.""" - -from __future__ import annotations - -import numpy as np -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_collider_ball_2d(ns): - cs = ns.ColliderSet() - coll = ns.Collider.ball(0.5).build() - h = cs.insert(coll) - fetched = cs[h] - assert fetched.shape.as_ball().radius == pytest.approx(0.5) - - -def test_collider_cuboid_2d(ns): - coll = ns.Collider.cuboid(1.0, 2.0).build() - cu = coll.shape.as_cuboid() - assert cu is not None - assert cu.half_extents.x == pytest.approx(1.0) - assert cu.half_extents.y == pytest.approx(2.0) - - -def test_collider_cuboid_2d_kwargs(ns): - coll = ns.Collider.cuboid(1.0, 2.0, density=3.0, sensor=True).build() - assert coll.density == 3.0 - assert coll.is_sensor is True - - -def test_collider_segment_2d(ns): - coll = ns.Collider.segment((0.0, 0.0), (1.0, 0.0)).build() - assert coll.shape.shape_type == ns.ShapeType.SEGMENT - seg = coll.shape.as_segment() - assert seg is not None - - -def test_collider_polyline_2d(ns): - verts = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]], dtype=np.float32) - coll = ns.Collider.polyline(verts).build() - assert coll.shape.shape_type == ns.ShapeType.POLYLINE - pl = coll.shape.as_polyline() - assert pl is not None - assert pl.num_segments() == 2 # connected pairs by default - - -def test_collider_convex_polygon_2d(ns): - pts = np.array( - [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], - dtype=np.float32, - ) - coll = ns.Collider.convex_polygon(pts).build() - cp = coll.shape.as_convex_polygon() - assert cp is not None - assert cp.num_points() >= 3 - - -def test_collider_capsule_2d(ns): - coll = ns.Collider.capsule(0.5, 0.2).build() - cap = coll.shape.as_capsule() - assert cap is not None - assert cap.radius == pytest.approx(0.2) - - -def test_collider_set_2d(ns): - cs = ns.ColliderSet() - h1 = cs.insert(ns.Collider.ball(0.5).build()) - h2 = cs.insert(ns.Collider.cuboid(1, 1).build()) - assert len(cs) == 2 - assert h1 in cs and h2 in cs - - -def test_interaction_groups_2d(ns): - g = ns.InteractionGroups(ns.Group.GROUP_0, ns.Group.ALL) - coll = ns.Collider.ball(0.5).collision_groups(g).build() - assert coll.collision_groups.memberships == ns.Group.GROUP_0 - - -def test_collider_translation_2d(ns): - coll = ns.Collider.ball(0.5).translation((3.0, 4.0)).build() - t = coll.translation - assert t.x == pytest.approx(3.0) and t.y == pytest.approx(4.0) - - -def test_shared_shape_cuboid_2d(ns): - s = ns.SharedShape.cuboid(0.5, 0.5) - assert s.shape_type == ns.ShapeType.CUBOID - - -def test_shared_shape_compute_aabb_2d(ns): - s = ns.SharedShape.ball(1.0) - aabb = s.compute_aabb(ns.Isometry2.identity()) - assert aabb.mins.x == pytest.approx(-1.0) - - -def test_active_events_2d(ns): - e = ns.ActiveEvents.COLLISION_EVENTS - coll = ns.Collider.ball(0.5).active_events(e).build() - assert coll.active_events.contains(ns.ActiveEvents.COLLISION_EVENTS) - - -def test_insert_with_parent_2d(ns): - bodies = ns.RigidBodySet() - cs = ns.ColliderSet() - rb = ns.RigidBody.dynamic(translation=(0.0, 0.0)).build() - rb_h = bodies.insert(rb) - ch = cs.insert_with_parent(ns.Collider.ball(0.5).build(), rb_h, bodies) - assert cs[ch].parent == rb_h diff --git a/python/tests/test_joints.py b/python/tests/test_joints.py index 6b6f331de..1a1920a4e 100644 --- a/python/tests/test_joints.py +++ b/python/tests/test_joints.py @@ -1,7 +1,4 @@ -"""3D joint tests. - -Parametrized across the f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`) flavors. -""" +"""3D joint tests (f32).""" from __future__ import annotations @@ -10,10 +7,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_joints_2d.py b/python/tests/test_joints_2d.py deleted file mode 100644 index bbec42d20..000000000 --- a/python/tests/test_joints_2d.py +++ /dev/null @@ -1,120 +0,0 @@ -"""2D joint tests. - -Parametrized across `rapier.dim2` (f32) and `rapier.dim2.f64`. -""" - -from __future__ import annotations - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_joint_axis_2d_has_three_variants(ns): - axes = [ns.JointAxis.LIN_X, ns.JointAxis.LIN_Y, ns.JointAxis.ANG_X] - # 2D has no LIN_Z/ANG_Y/ANG_Z. - assert not hasattr(ns.JointAxis, "LIN_Z") - assert not hasattr(ns.JointAxis, "ANG_Y") - assert not hasattr(ns.JointAxis, "ANG_Z") - for i, a in enumerate(axes): - for j, b in enumerate(axes): - if i == j: - assert a == b - else: - assert a != b - - -def test_joint_axes_mask_2d_has_only_lin_ang_x(ns): - # 2D mask: only LIN_X, LIN_Y, ANG_X. - assert not hasattr(ns.JointAxesMask, "LIN_Z") - assert not hasattr(ns.JointAxesMask, "ANG_Y") - assert not hasattr(ns.JointAxesMask, "ANG_Z") - mask = ns.JointAxesMask.LIN_X | ns.JointAxesMask.ANG_X - assert mask.contains(ns.JointAxesMask.LIN_X) - assert mask.contains(ns.JointAxesMask.ANG_X) - - -def test_no_spherical_joint_in_2d(ns): - assert not hasattr(ns, "SphericalJoint") - - -def test_pinslot_joint_constructable(ns): - j = ( - ns.PinSlotJoint.builder(axis=(1, 0)) - .local_anchor1((0, 0)) - .local_anchor2((0, 0)) - .build() - ) - assert j is not None - # Axis should have been recorded. - a1 = j.local_axis1 - assert abs(a1.x - 1.0) < 1e-6 - - -def test_fixed_joint_2d(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - a = w.rigid_bodies.insert(ns.RigidBody.dynamic(translation=(0, 5)).build()) - b = w.rigid_bodies.insert(ns.RigidBody.dynamic(translation=(1, 5)).build()) - j = ns.FixedJoint.builder().local_anchor1((1, 0)).build() - h = w.impulse_joints.insert(a, b, j) - assert h in w.impulse_joints - for _ in range(60): - w.step() - ba = w.rigid_bodies[a] - bb = w.rigid_bodies[b] - # Relative offset constant under fixed joint. - dx = bb.translation.x - ba.translation.x - dy = bb.translation.y - ba.translation.y - assert abs(dx) < 1.5 - assert abs(dy) < 1.5 - - -def test_revolute_joint_2d_no_axis_arg(ns): - # In 2D, RevoluteJoint takes no axis argument. - j = ns.RevoluteJoint.builder().limits(-1.0, 1.0).build() - lim = j.limits() - assert lim is not None - assert abs(lim.min + 1.0) < 1e-6 - - -def test_prismatic_joint_2d(ns): - j = ns.PrismaticJoint.builder(axis=(1, 0)).limits(-1.0, 1.0).build() - assert j.limits() is not None - - -def test_rope_joint_2d(ns): - j = ns.RopeJoint.builder(max_distance=3.0).build() - assert abs(j.max_distance - 3.0) < 1e-6 - - -def test_spring_joint_2d(ns): - j = ns.SpringJoint.builder(rest_length=1.0, stiffness=10.0, damping=0.5).build() - assert abs(j.rest_length - 1.0) < 1e-6 - - -def test_generic_joint_2d_with_locked_axes(ns): - mask = ns.JointAxesMask.LIN_X | ns.JointAxesMask.ANG_X - j = ns.GenericJoint.builder(locked_axes=mask).build() - assert j.locked_axes.contains(ns.JointAxesMask.LIN_X) - assert j.locked_axes.contains(ns.JointAxesMask.ANG_X) - - -def test_impulse_joint_set_attached_and_remove_2d(ns): - w = ns.PhysicsWorld() - a = w.rigid_bodies.insert(ns.RigidBody.dynamic().build()) - b = w.rigid_bodies.insert(ns.RigidBody.dynamic().build()) - - j = w.impulse_joints.insert(a, b, ns.FixedJoint()) - attached = list(w.impulse_joints.attached_joints(a)) - assert j in attached - - removed = w.impulse_joints.remove(j) - assert removed is not None - with pytest.raises(ns.InvalidHandle): - w.impulse_joints[j] diff --git a/python/tests/test_loaders.py b/python/tests/test_loaders.py index b49e433a1..e8c6b7bf6 100644 --- a/python/tests/test_loaders.py +++ b/python/tests/test_loaders.py @@ -1,7 +1,7 @@ """Loader tests. These exercise the URDF, MJCF, and mesh loader bindings. The loaders are -3D-only / f32-only — there are no f64 equivalents. +3D-only / f32-only. """ from __future__ import annotations diff --git a/python/tests/test_math.py b/python/tests/test_math.py index 8842221b8..822d3760a 100644 --- a/python/tests/test_math.py +++ b/python/tests/test_math.py @@ -1,8 +1,6 @@ """3D math layer tests. -These run against the default `rapier.dim3` (f32) namespace. The `f64` -variant runs the same assertions via parametrization (so we catch any -codepath that quietly only got applied to one scalar). +These run against the default `rapier.dim3` (f32) namespace. """ from __future__ import annotations @@ -13,10 +11,9 @@ import rapier3d as rapier import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param @@ -92,21 +89,13 @@ def test_vec3_bitwise_equal(ns): a = ns.Vec3(1, 2, 3) b = ns.Vec3(1, 2, 3) assert a.bitwise_equal(b) - if ns is dim3: - # f32: pick a perturbation that approx_eq (tol 1e-7) tolerates but - # bitwise check rejects. - c = ns.Vec3(1.0 + 1e-8, 2, 3) - else: - # f64: tighter tolerance (1e-14), need a smaller perturbation. - c = ns.Vec3(1.0 + 1e-15, 2, 3) + # f32: pick a perturbation that approx_eq (tol 1e-7) tolerates but + # bitwise check rejects. + c = ns.Vec3(1.0 + 1e-8, 2, 3) # approx __eq__ tolerates the perturbation assert a == c - # In both cases, exact bitwise equality should NOT hold for c vs a if the - # perturbation actually changed the underlying scalar. For f32, 1+1e-8 - # rounds to exactly 1.0 (below f32 epsilon ~1.19e-7), so c == a bitwise. - # We therefore only assert the negative bitwise case for f64 here. - if ns is dim3_f64: - assert not c.bitwise_equal(a) + # For f32, 1+1e-8 rounds to exactly 1.0 (below f32 epsilon ~1.19e-7), so + # c == a bitwise, hence we don't assert the negative bitwise case here. def test_vec3_lerp(ns): diff --git a/python/tests/test_math_2d.py b/python/tests/test_math_2d.py deleted file mode 100644 index e65b6154e..000000000 --- a/python/tests/test_math_2d.py +++ /dev/null @@ -1,95 +0,0 @@ -"""2D math layer tests.""" - -from __future__ import annotations - -import math as pymath - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_vec2_basic(ns): - v = ns.Vec2(3, 4) - assert (v.x, v.y) == (3.0, 4.0) - assert v.norm() == pytest.approx(5.0) - assert v.normalize() == ns.Vec2(0.6, 0.8) - - -def test_vec2_from_tuple(ns): - np = pytest.importorskip("numpy") - dtype = np.float32 if ns is dim2 else np.float64 - a = ns.Vec2(1, 2) - b = ns.Vec2.from_tuple((1.0, 2.0)) - c = ns.Vec2.from_ndarray(np.array([1.0, 2.0], dtype=dtype)) - assert a == b == c - - -def test_vec2_add_sub(ns): - assert ns.Vec2(1, 2) + ns.Vec2(3, 4) == ns.Vec2(4, 6) - assert ns.Vec2(3, 4) - (1, 2) == ns.Vec2(2, 2) - assert -ns.Vec2(1, 2) == ns.Vec2(-1, -2) - - -def test_vec2_dot(ns): - assert ns.Vec2(1, 2).dot((3, 4)) == pytest.approx(11.0) - - -def test_vec2_iter(ns): - assert list(ns.Vec2(1, 2)) == [1.0, 2.0] - assert len(ns.Vec2(1, 2)) == 2 - - -def test_point2_sub_returns_vec(ns): - d = ns.Point2(5, 7) - ns.Point2(1, 2) - assert isinstance(d, ns.Vec2) - assert d == ns.Vec2(4, 5) - - -def test_rotation2_angle(ns): - r = ns.Rotation2.from_angle(pymath.pi / 2) - assert r.angle == pytest.approx(pymath.pi / 2) - assert r.transform_vector((1, 0)) == ns.Vec2(0, 1) - assert (r.inverse() * r) == ns.Rotation2.identity() - - -def test_rotation2_compose(ns): - a = ns.Rotation2.from_angle(pymath.pi / 4) - b = ns.Rotation2.from_angle(pymath.pi / 4) - composed = a * b - assert composed.transform_vector((1, 0)) == ns.Vec2(0, 1) - - -def test_rotation2_slerp(ns): - # Use pi/2 instead of pi: slerp at exactly 180° is ambiguous in direction. - a = ns.Rotation2.identity() - b = ns.Rotation2.from_angle(pymath.pi / 2) - mid = a.slerp(b, 0.5) - assert mid == ns.Rotation2.from_angle(pymath.pi / 4) - - -def test_isometry2(ns): - iso = ns.Isometry2(translation=(1, 2), rotation=ns.Rotation2.identity()) - assert iso * ns.Point2(0, 0) == ns.Point2(1, 2) - - iso2 = ns.Isometry2.from_translation(3, 4) - assert iso2.transform_point((1, 1)) == ns.Point2(4, 5) - - -def test_isometry2_compose(ns): - a = ns.Isometry2.from_translation(1, 0) - b = ns.Isometry2.from_rotation(pymath.pi / 2) - composed = a * b - p = composed.transform_point((1, 0)) - assert p == ns.Point2(1, 1) - - -def test_rotation_from_angle_2d(ns): - r = ns.rotation_from_angle(pymath.pi / 2) - assert r.transform_vector((1, 0)) == ns.Vec2(0, 1) diff --git a/python/tests/test_multibody.py b/python/tests/test_multibody.py index 1e2d99007..6ac057db5 100644 --- a/python/tests/test_multibody.py +++ b/python/tests/test_multibody.py @@ -7,10 +7,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_new_bindings.py b/python/tests/test_new_bindings.py index 16d33b5d0..51b25a2ac 100644 --- a/python/tests/test_new_bindings.py +++ b/python/tests/test_new_bindings.py @@ -7,30 +7,21 @@ * joint ``softness`` / builder ``.softness(...)`` — spring coefficients on every impulse-joint type (and the generic joint). -Each test is parametrized over the 3D f32/f64 flavors; a couple of 2D -smoke checks confirm the dimension-uniform pieces compile for 2D too. +These run against the 3D f32 flavor. """ from __future__ import annotations import pytest -import rapier2d as dim2 import rapier3d as dim3 -import rapier2d_f64 as dim2_f64 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param -@pytest.fixture(params=[dim2, dim2_f64], ids=["2d-f32", "2d-f64"]) -def ns2(request): - return request.param - - # -------------------------------------------------------------------------- # halfspace # -------------------------------------------------------------------------- @@ -38,7 +29,7 @@ def ns2(request): def _up(ns): """Unit 'up' normal for the namespace's dimension.""" - return (0.0, 1.0, 0.0) if ns in (dim3, dim3_f64) else (0.0, 1.0) + return (0.0, 1.0, 0.0) def test_halfspace_shared_shape(ns): @@ -46,11 +37,6 @@ def test_halfspace_shared_shape(ns): assert s.shape_type == ns.ShapeType.HALFSPACE -def test_halfspace_shared_shape_2d(ns2): - s = ns2.SharedShape.halfspace(_up(ns2)) - assert s.shape_type == ns2.ShapeType.HALFSPACE - - def test_halfspace_collider_builder_and_kwargs(ns): coll = ns.Collider.halfspace(_up(ns), friction=0.7).build() assert coll.friction == pytest.approx(0.7) @@ -60,13 +46,13 @@ def test_halfspace_collider_builder_and_kwargs(ns): def test_halfspace_normalizes_non_unit_normal(ns): # A non-unit normal must still produce a valid half-space (the binding # normalizes it) rather than panicking or degenerating. - coll = ns.Collider.halfspace((0.0, 5.0, 0.0) if ns in (dim3, dim3_f64) else (0.0, 5.0)).build() + coll = ns.Collider.halfspace((0.0, 5.0, 0.0)).build() assert coll.shape.shape_type == ns.ShapeType.HALFSPACE def test_halfspace_as_immovable_ground(ns): # A dynamic ball should come to rest on a half-space ground. - gravity = (0.0, -9.81, 0.0) if ns in (dim3, dim3_f64) else (0.0, -9.81) + gravity = (0.0, -9.81, 0.0) world = ns.PhysicsWorld(gravity=gravity) world.colliders.insert(ns.Collider.halfspace(_up(ns)).build()) body = world.rigid_bodies.insert( @@ -102,11 +88,6 @@ def test_active_collision_types_flags(ns): assert A.KINEMATIC_KINEMATIC not in default -def test_active_collision_types_flags_2d(ns2): - A = ns2.ActiveCollisionTypes - assert A.KINEMATIC_FIXED in (A.DYNAMIC_FIXED | A.KINEMATIC_FIXED) - - def test_active_collision_types_collider_roundtrip(ns): A = ns.ActiveCollisionTypes # via kwarg @@ -127,7 +108,7 @@ def test_active_collision_types_enables_kinematic_fixed_contact(ns): A = ns.ActiveCollisionTypes world = ns.PhysicsWorld() - ground = ns.Collider.cuboid(*([5.0] * (3 if ns in (dim3, dim3_f64) else 2))) + ground = ns.Collider.cuboid(*([5.0] * 3)) ground = ground.active_collision_types(A.default_types() | A.KINEMATIC_FIXED) ground_handle = world.colliders.insert(ground.build()) @@ -171,11 +152,6 @@ def test_softness_present_on_all_joint_types(ns): assert not hasattr(ns.SpringJoint, "softness") -def test_softness_present_on_2d_joints(ns2): - assert hasattr(ns2.PinSlotJoint, "softness") - assert hasattr(ns2.PinSlotJointBuilder, "softness") - - def test_generic_joint_softness_roundtrip(ns): sc = ns.SpringCoefficients(stiffness=12.0, damping=0.8) j = ns.GenericJoint() diff --git a/python/tests/test_pipeline.py b/python/tests/test_pipeline.py index 0ed56ce97..d8c60c391 100644 --- a/python/tests/test_pipeline.py +++ b/python/tests/test_pipeline.py @@ -1,8 +1,4 @@ -"""3D pipeline tests. - -Parametrized across the f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`) -flavors. -""" +"""3D pipeline tests (f32).""" from __future__ import annotations @@ -11,10 +7,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_pipeline_2d.py b/python/tests/test_pipeline_2d.py deleted file mode 100644 index a5e67d551..000000000 --- a/python/tests/test_pipeline_2d.py +++ /dev/null @@ -1,112 +0,0 @@ -"""2D analogue of `test_pipeline.py` / `test_queries.py`.""" - -from __future__ import annotations - -import threading - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def test_world_default(ns): - w = ns.PhysicsWorld() - assert len(w.rigid_bodies) == 0 - assert w.gravity == ns.Vec2(0, 0) - - -def test_world_shared_subsets(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - assert w.rigid_bodies is w.rigid_bodies - assert w.colliders is w.colliders - - -def test_world_step_drops_ball_2d(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - w.colliders.insert(ns.Collider.cuboid(50, 0.1).build()) - h = w.add_body( - ns.RigidBody.dynamic(translation=(0, 5)), - colliders=[ns.Collider.ball(0.5)], - ) - for _ in range(120): - w.step() - body = w.rigid_bodies[h] - assert body.translation.y < 5.0 - assert body.translation.y > -1.0 - - -def test_world_gil_release_2d(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - w.colliders.insert(ns.Collider.cuboid(50, 0.1).build()) - h = w.add_body( - ns.RigidBody.dynamic(translation=(0, 5)), - colliders=[ns.Collider.ball(0.5)], - ) - - counter = [0] - - def bump(): - for _ in range(50): - counter[0] += 1 - - t = threading.Thread(target=bump) - t.start() - for _ in range(60): - w.step() - t.join() - assert counter[0] == 50 - - -def test_cast_ray_2d(ns): - w = ns.PhysicsWorld() - w.add_body( - ns.RigidBody.fixed(translation=(0, 0)), - colliders=[ns.Collider.cuboid(10, 0.5)], - ) - w.update_query_pipeline() - ray = ns.Ray(ns.Point2(0, 10), (0, -1)) - hit = w.query_pipeline.cast_ray(ray, 100.0, True) - assert hit is not None - _, toi = hit - # Ground top at y = 0.5 → toi = 9.5 - assert abs(toi - 9.5) < 0.05 - - -def test_intersect_shape_2d(ns): - w = ns.PhysicsWorld() - w.add_collider(ns.Collider.cuboid(1, 1).translation((0, 0))) - w.update_query_pipeline() - qp = w.query_pipeline - found = [] - qp.intersect_shape( - ns.Isometry2.from_translation(0, 0), - ns.SharedShape.cuboid(0.5, 0.5), - lambda h: found.append(h) or True, - ) - assert len(found) == 1 - - -def test_test_aabb_2d(ns): - w = ns.PhysicsWorld() - w.add_collider(ns.Collider.cuboid(1, 1).translation((0, 0))) - w.update_query_pipeline() - qp = w.query_pipeline - assert qp.test_aabb(ns.Aabb((-2, -2), (2, 2))) is True - assert qp.test_aabb(ns.Aabb((100, 100), (101, 101))) is False - - -def test_query_filter_2d(ns): - f = ns.QueryFilter.exclude_dynamic() - assert f is not None - - -def test_counters_2d(ns): - c = ns.Counters() - c.enable() - assert c.enabled diff --git a/python/tests/test_proxy_persistence.py b/python/tests/test_proxy_persistence.py index 44f9694bb..3d995240a 100644 --- a/python/tests/test_proxy_persistence.py +++ b/python/tests/test_proxy_persistence.py @@ -11,22 +11,14 @@ import pytest -import rapier2d -import rapier2d_f64 import rapier3d -import rapier3d_f64 -@pytest.fixture(params=[rapier3d, rapier3d_f64], ids=["3d-f32", "3d-f64"]) +@pytest.fixture(params=[rapier3d], ids=["3d-f32"]) def ns3(request): return request.param -@pytest.fixture(params=[rapier2d, rapier2d_f64], ids=["2d-f32", "2d-f64"]) -def ns2(request): - return request.param - - def test_rigid_body_mutation_persists(ns3): w = ns3.PhysicsWorld(gravity=(0, -9.81, 0)) h = w.add_body(ns3.RigidBody.dynamic(translation=(0, 5, 0))) @@ -37,13 +29,6 @@ def test_rigid_body_mutation_persists(ns3): assert abs(w.rigid_bodies[h].linvel.x - 5.0) < 1e-3 # persisted through a step -def test_rigid_body_mutation_persists_2d(ns2): - w = ns2.PhysicsWorld(gravity=(0, -9.81)) - h = w.add_body(ns2.RigidBody.dynamic(translation=(0, 5))) - w.rigid_bodies[h].linvel = ns2.Vec2(3.0, 0.0) - assert abs(w.rigid_bodies[h].linvel.x - 3.0) < 1e-5 - - def test_collider_mutation_persists(ns3): w = ns3.PhysicsWorld(gravity=(0, -9.81, 0)) h = w.add_body(ns3.RigidBody.dynamic()) diff --git a/python/tests/test_queries.py b/python/tests/test_queries.py index 30ad228ea..999a5c6e3 100644 --- a/python/tests/test_queries.py +++ b/python/tests/test_queries.py @@ -6,10 +6,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_serde.py b/python/tests/test_serde.py index 0952ecff8..34b0ddb1d 100644 --- a/python/tests/test_serde.py +++ b/python/tests/test_serde.py @@ -1,8 +1,4 @@ -"""3D serialization, snapshots, pickle. - -Parametrized across the f32 (`rapier.dim3`) and f64 (`rapier.dim3.f64`) -flavors. -""" +"""3D serialization, snapshots, pickle (f32).""" from __future__ import annotations @@ -11,10 +7,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_serde_2d.py b/python/tests/test_serde_2d.py deleted file mode 100644 index 1ebd2f870..000000000 --- a/python/tests/test_serde_2d.py +++ /dev/null @@ -1,93 +0,0 @@ -"""2D serialization, snapshots, pickle. - -Smaller analogue of `test_serde.py`. Parametrized across `dim2` (f32) and -`dim2.f64`. -""" - -from __future__ import annotations - -import pickle - -import pytest - -import rapier2d as dim2 -import rapier2d_f64 as dim2_f64 - - -@pytest.fixture(params=[dim2, dim2_f64], ids=["f32", "f64"]) -def ns(request): - return request.param - - -def _build_small_world(ns): - w = ns.PhysicsWorld(gravity=(0, -9.81)) - w.colliders.insert(ns.Collider.cuboid(50.0, 0.1).build()) - h1 = w.rigid_bodies.insert(ns.RigidBody.dynamic(translation=(0, 5)).build()) - w.colliders.insert_with_parent(ns.Collider.ball(0.5), h1, w.rigid_bodies) - h2 = w.rigid_bodies.insert(ns.RigidBody.dynamic(translation=(1.5, 5)).build()) - w.colliders.insert_with_parent(ns.Collider.cuboid(0.3, 0.3), h2, w.rigid_bodies) - h3 = w.rigid_bodies.insert(ns.RigidBody.dynamic(translation=(3.0, 5)).build()) - w.colliders.insert_with_parent(ns.Collider.ball(0.4), h3, w.rigid_bodies) - w.impulse_joints.insert(h1, h2, ns.FixedJointBuilder().build()) - w.impulse_joints.insert(h2, h3, ns.RevoluteJointBuilder().build()) - return w, [h1, h2, h3] - - -def _body_positions(w, handles): - return [tuple(w.rigid_bodies[h].translation) for h in handles] - - -def test_world_snapshot_round_trip(ns): - w, handles = _build_small_world(ns) - before = _body_positions(w, handles) - blob = w.snapshot() - assert blob[:4] == b"RPYS" - w2 = ns.PhysicsWorld.restore(blob) - after = _body_positions(w2, handles) - for a, b in zip(before, after): - for ai, bi in zip(a, b): - assert abs(ai - bi) <= 1e-5 - - -def test_pickle_world(ns): - w, handles = _build_small_world(ns) - w2 = pickle.loads(pickle.dumps(w)) - for _ in range(50): - w.step() - w2.step() - for a, b in zip(_body_positions(w, handles), _body_positions(w2, handles)): - for ai, bi in zip(a, b): - assert abs(ai - bi) <= 1e-5 - - -def test_pickle_handles(ns): - h = ns.RigidBodyHandle.from_raw_parts(11, 22) - assert pickle.loads(pickle.dumps(h)) == h - - -def test_pickle_shapes(ns): - s = ns.SharedShape.cuboid(1, 2) - s2 = pickle.loads(pickle.dumps(s)) - iso = ns.Isometry2.identity() - assert tuple(s.compute_aabb(iso).mins.to_tuple()) == tuple( - s2.compute_aabb(iso).mins.to_tuple() - ) - - -def test_pickle_mass_properties(ns): - mp = ns.MassProperties.from_ball(1.0, 1.0) - mp2 = pickle.loads(pickle.dumps(mp)) - assert mp2.mass == pytest.approx(mp.mass) - - -def test_snapshot_json_round_trip(ns): - w, _ = _build_small_world(ns) - s = w.snapshot_json() - w2 = ns.PhysicsWorld.restore_json(s) - assert len(w2.rigid_bodies) == len(w.rigid_bodies) - - -def test_channel_event_collector_refuses_pickle(ns): - coll = ns.ChannelEventCollector() - with pytest.raises(ns.SerializationError): - pickle.dumps(coll) diff --git a/python/tests/test_shapes.py b/python/tests/test_shapes.py index a0a43b47e..079320a33 100644 --- a/python/tests/test_shapes.py +++ b/python/tests/test_shapes.py @@ -10,10 +10,9 @@ import pytest import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -@pytest.fixture(params=[dim3, dim3_f64], ids=["f32", "f64"]) +@pytest.fixture(params=[dim3], ids=["f32"]) def ns(request): return request.param diff --git a/python/tests/test_smoke.py b/python/tests/test_smoke.py index de4f81577..89d9f028f 100644 --- a/python/tests/test_smoke.py +++ b/python/tests/test_smoke.py @@ -9,10 +9,7 @@ import rapier3d as rapier from rapier3d import RapierError -import rapier2d as dim2 import rapier3d as dim3 -import rapier3d_f64 as dim3_f64 -import rapier2d_f64 as dim2_f64 def test_version() -> None: @@ -37,18 +34,9 @@ def test_error_is_exception() -> None: def test_dim_namespaces() -> None: - assert dim2 is not dim3 - assert dim3_f64 is not dim3 - assert dim2_f64 is not dim2 - # The exception type is *defined per-extension*; reachable from each. - assert hasattr(dim2, "RapierError") + # The exception type is *defined per-extension*; reachable from the module. assert hasattr(dim3, "RapierError") - assert hasattr(dim3_f64, "RapierError") - assert hasattr(dim2_f64, "RapierError") def test_dim_versions() -> None: - assert isinstance(dim2.__version__, str) assert isinstance(dim3.__version__, str) - assert isinstance(dim3_f64.__version__, str) - assert isinstance(dim2_f64.__version__, str) From 4dd6ab735c3bfa6612d78419ad47b9e4f4864d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sat, 4 Jul 2026 18:00:00 +0200 Subject: [PATCH 2/3] chore: cargo fmt --- python/rapier-py-3d/src/lib.rs | 6 ++++++ python/rapier-py-3d/src/math.rs | 9 +++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/python/rapier-py-3d/src/lib.rs b/python/rapier-py-3d/src/lib.rs index 9bc8a2c45..eadee6f9c 100644 --- a/python/rapier-py-3d/src/lib.rs +++ b/python/rapier-py-3d/src/lib.rs @@ -4,6 +4,12 @@ //! into a `rapier-py-core` macro layer plus per-dimension cdylibs; with only //! the 3D f32 flavor remaining, everything lives here as ordinary modules.) +// pyo3 0.22's `#[pymethods]` macro emits `unsafe fn` wrappers whose bodies call +// pyo3-internal unsafe functions without an inner `unsafe {}` block. Under the +// 2024 edition that trips `unsafe_op_in_unsafe_fn`. The calls are pyo3-internal +// and correct; the previous macro-based layout hid them via macro hygiene. +#![allow(unsafe_op_in_unsafe_fn)] + // Re-export the crate dependencies so the modules can refer to them via stable // `crate::…` paths (a holdover from the former macro layer, kept intentional). pub use bincode; diff --git a/python/rapier-py-3d/src/math.rs b/python/rapier-py-3d/src/math.rs index 719f8fd84..cb6f8fd49 100644 --- a/python/rapier-py-3d/src/math.rs +++ b/python/rapier-py-3d/src/math.rs @@ -10,7 +10,7 @@ //! //! Defaults for `approx_eq` tolerances: `1e-7` for `f32`. -use crate::conv::{PyAngVector, PyIsometry, PyPoint, PyRotation, PyVector, Real}; +use crate::conv::*; /// Default `approx_eq` epsilon for a given scalar. pub trait DefaultEpsilon { @@ -883,9 +883,7 @@ impl Isometry3 { /// :param up: the desired up direction. #[staticmethod] fn face_toward(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { - Self(crate::na::Isometry3::face_towards( - &eye.0, &target.0, &up.0, - )) + Self(crate::na::Isometry3::face_towards(&eye.0, &target.0, &up.0)) } /// Left-handed look-at view isometry: maps world space into a @@ -1008,8 +1006,7 @@ impl Isometry3 { fn __richcmp__(&self, other: PyIsometry, op: CompareOp) -> PyResult { use crate::math::DefaultEpsilon; let eps: Real = Real::default_epsilon(); - let t_close = - (self.0.translation.vector - other.0.translation.vector).norm() <= eps; + let t_close = (self.0.translation.vector - other.0.translation.vector).norm() <= eps; let a = self.0.rotation.quaternion(); let b = other.0.rotation.quaternion(); let r_close_pos = (a.w - b.w).abs() <= eps From 7f92afffcc1eb95b4ccf431fb39e370774d8f09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sat, 4 Jul 2026 18:35:17 +0200 Subject: [PATCH 3/3] chore: de-macroize the python bindings --- .github/workflows/python-bindings.yml | 1 - Cargo.toml | 1 - python/rapier-py-3d/src/controllers.rs | 1917 ++++++++ python/rapier-py-3d/src/debug_render.rs | 1191 +++++ python/rapier-py-3d/src/dynamics.rs | 2767 +++++++++++ python/rapier-py-3d/src/errors.rs | 15 - python/rapier-py-3d/src/events_hooks.rs | 982 ++++ python/rapier-py-3d/src/geometry.rs | 4301 +++++++++++++++++ python/rapier-py-3d/src/joints.rs | 4176 +++++++++++++++++ python/rapier-py-3d/src/lib.rs | 43 + python/rapier-py-3d/src/loaders.rs | 1600 +++++++ python/rapier-py-3d/src/math.rs | 2 +- python/rapier-py-3d/src/pipeline.rs | 3255 +++++++++++++ python/rapier-py-3d/src/serde_glue.rs | 1719 +++++++ python/rapier-py-3d/src/serde_io.rs | 9 +- python/rapier-py-core/Cargo.toml | 37 - python/rapier-py-core/src/controllers.rs | 1828 -------- python/rapier-py-core/src/conv.rs | 302 -- python/rapier-py-core/src/debug_render.rs | 1244 ----- python/rapier-py-core/src/dynamics.rs | 3580 -------------- python/rapier-py-core/src/errors.rs | 94 - python/rapier-py-core/src/events_hooks.rs | 995 ---- python/rapier-py-core/src/geometry.rs | 5152 --------------------- python/rapier-py-core/src/joints.rs | 4249 ----------------- python/rapier-py-core/src/lib.rs | 38 - python/rapier-py-core/src/loaders.rs | 251 - python/rapier-py-core/src/math.rs | 1896 -------- python/rapier-py-core/src/mjcf.rs | 547 --- python/rapier-py-core/src/pipeline.rs | 2461 ---------- python/rapier-py-core/src/serde_io.rs | 134 - python/rapier-py-core/src/serde_macros.rs | 767 --- python/rapier-py-core/src/urdf.rs | 673 --- 32 files changed, 21956 insertions(+), 24271 deletions(-) create mode 100644 python/rapier-py-3d/src/controllers.rs create mode 100644 python/rapier-py-3d/src/debug_render.rs create mode 100644 python/rapier-py-3d/src/dynamics.rs create mode 100644 python/rapier-py-3d/src/events_hooks.rs create mode 100644 python/rapier-py-3d/src/geometry.rs create mode 100644 python/rapier-py-3d/src/joints.rs create mode 100644 python/rapier-py-3d/src/loaders.rs create mode 100644 python/rapier-py-3d/src/pipeline.rs create mode 100644 python/rapier-py-3d/src/serde_glue.rs delete mode 100644 python/rapier-py-core/Cargo.toml delete mode 100644 python/rapier-py-core/src/controllers.rs delete mode 100644 python/rapier-py-core/src/conv.rs delete mode 100644 python/rapier-py-core/src/debug_render.rs delete mode 100644 python/rapier-py-core/src/dynamics.rs delete mode 100644 python/rapier-py-core/src/errors.rs delete mode 100644 python/rapier-py-core/src/events_hooks.rs delete mode 100644 python/rapier-py-core/src/geometry.rs delete mode 100644 python/rapier-py-core/src/joints.rs delete mode 100644 python/rapier-py-core/src/lib.rs delete mode 100644 python/rapier-py-core/src/loaders.rs delete mode 100644 python/rapier-py-core/src/math.rs delete mode 100644 python/rapier-py-core/src/mjcf.rs delete mode 100644 python/rapier-py-core/src/pipeline.rs delete mode 100644 python/rapier-py-core/src/serde_io.rs delete mode 100644 python/rapier-py-core/src/serde_macros.rs delete mode 100644 python/rapier-py-core/src/urdf.rs diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 716a8044c..362922d02 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -32,7 +32,6 @@ jobs: - name: Cargo clippy (Python bindings) run: | cargo clippy --no-deps \ - -p rapier-py-core \ -p rapier-py-3d \ -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index cbf0f5cce..e7f37b7f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "crates/rapier3d-meshloader", "crates/mjcf-rs", "crates/rapier3d-mjcf", - "python/rapier-py-core", "python/rapier-py-3d", ] # Bare `cargo build` / `cargo test` / `cargo clippy` only touches the diff --git a/python/rapier-py-3d/src/controllers.rs b/python/rapier-py-3d/src/controllers.rs new file mode 100644 index 000000000..eea62f7c2 --- /dev/null +++ b/python/rapier-py-3d/src/controllers.rs @@ -0,0 +1,1917 @@ +//! The concrete 3D / `f32` controller `#[pyclass]`-es: the kinematic +//! character controller, the PID/PD family, and the 3D-only raycast vehicle +//! controller, plus the `register_controllers` function used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. + +use crate::*; +use rapier3d as rapier; + +use crate::pyo3::exceptions::PyTypeError; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; + +// ============================================================================ +// Shared: CharacterLength, CharacterAutostep, EffectiveCharacterMovement, +// CharacterCollision, KinematicCharacterController, AxesMask, PdErrors, +// PidCorrection. +// ============================================================================ + +// --------------------------------------------------------------- +// CharacterLength tagged union +// +// We don't expose this as a `#[pyclass] enum` because the upstream +// variants carry a `Real` payload. Instead we model it as a small +// frozen struct with a `kind` discriminant and a `value` payload, +// plus `.absolute(v)` / `.relative(v)` static constructors. Matches +// the API documented in `08-controllers.md`. +// --------------------------------------------------------------- +/// A length used by the kinematic character controller. +/// +/// Tagged union with two variants — ``absolute`` (raw world-space +/// units) and ``relative`` (a fraction of the controlled shape's +/// height). Use the :meth:`absolute` / :meth:`relative` static +/// constructors instead of the keyword constructor when possible. +/// +/// :ivar kind: ``"absolute"`` or ``"relative"``. +/// :ivar value: The numeric length, interpreted per ``kind``. +#[pyclass(name = "CharacterLength", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct CharacterLength { + #[pyo3(get)] + pub kind: &'static str, + #[pyo3(get)] + pub value: Real, +} + +#[pymethods] +impl CharacterLength { + /// Construct a ``CharacterLength`` from an explicit ``(value, kind)``. + /// + /// :param value: Numeric length. + /// :param kind: ``"absolute"`` or ``"relative"``. + /// :raises TypeError: if ``kind`` is not one of the two + /// accepted strings. + #[new] + #[pyo3(signature = (value=0.0 as Real, kind="absolute"))] + fn new(value: Real, kind: &str) -> PyResult { + match kind { + "absolute" => Ok(Self { + kind: "absolute", + value, + }), + "relative" => Ok(Self { + kind: "relative", + value, + }), + _ => Err(PyTypeError::new_err( + "CharacterLength kind must be \"absolute\" or \"relative\"", + )), + } + } + /// Build an absolute length expressed in world units. + /// + /// :param value: Length in the simulation's distance units. + #[staticmethod] + fn absolute(value: Real) -> Self { + Self { + kind: "absolute", + value, + } + } + /// Build a relative length expressed as a fraction of the shape height. + /// + /// :param value: Multiplier in ``[0, 1]`` of the controlled + /// shape's bounding height. + #[staticmethod] + fn relative(value: Real) -> Self { + Self { + kind: "relative", + value, + } + } + /// Return ``CharacterLength.absolute(v)`` / ``relative(v)`` repr. + fn __repr__(&self) -> String { + format!( + "CharacterLength.{}({})", + if self.kind == "absolute" { + "absolute" + } else { + "relative" + }, + self.value + ) + } + /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.kind == other.kind && self.value == other.value), + CompareOp::Ne => Ok(!(self.kind == other.kind && self.value == other.value)), + _ => Err(PyTypeError::new_err( + "CharacterLength supports only == and !=", + )), + } + } +} + +impl CharacterLength { + #[inline] + pub(crate) fn to_rapier(self) -> rapier::control::CharacterLength { + if self.kind == "relative" { + rapier::control::CharacterLength::Relative(self.value) + } else { + rapier::control::CharacterLength::Absolute(self.value) + } + } + #[inline] + pub(crate) fn from_rapier(v: rapier::control::CharacterLength) -> Self { + match v { + rapier::control::CharacterLength::Relative(x) => Self { + kind: "relative", + value: x, + }, + rapier::control::CharacterLength::Absolute(x) => Self { + kind: "absolute", + value: x, + }, + } + } +} + +// Accept a `(value, "kind")` tuple, a `CharacterLength` instance, or +// a bare float (interpreted as Absolute). +fn _extract_char_length(obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(c) = obj.extract::() { + return Ok(c); + } + if let Ok(f) = obj.extract::() { + return Ok(CharacterLength { + kind: "absolute", + value: f, + }); + } + Err(PyTypeError::new_err( + "expected a CharacterLength, a float (interpreted as absolute), or use CharacterLength.absolute/.relative", + )) +} + +// --------------------------------------------------------------- +// CharacterAutostep +// --------------------------------------------------------------- +/// Configuration for the kinematic character controller auto-step. +/// +/// Auto-step lets the character climb small ledges (stairs, kerbs) +/// automatically without falling off when the obstacle is shorter +/// than ``max_height`` and the landing platform is wider than +/// ``min_width``. Pass ``None`` (or :meth:`disabled`) to +/// :class:`KinematicCharacterController` to disable. +/// +/// :ivar max_height: Max obstacle height to step over. +/// :ivar min_width: Min platform width above the step. +/// :ivar include_dynamic_bodies: If ``True``, step up onto dynamic +/// bodies too (not just statics/kinematics). +#[pyclass(name = "CharacterAutostep", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct CharacterAutostep { + #[pyo3(get, set)] + pub max_height: CharacterLength, + #[pyo3(get, set)] + pub min_width: CharacterLength, + #[pyo3(get, set)] + pub include_dynamic_bodies: bool, +} + +#[pymethods] +impl CharacterAutostep { + /// Build a ``CharacterAutostep`` with optional overrides. + /// + /// :param max_height: :class:`CharacterLength` or bare float + /// (interpreted as absolute). Defaults to upstream value. + /// :param min_width: :class:`CharacterLength` or bare float. + /// :param include_dynamic_bodies: Whether dynamic surfaces + /// count as valid step targets. + #[new] + #[pyo3(signature = (max_height=None, min_width=None, include_dynamic_bodies=true))] + fn new( + max_height: Option<&Bound<'_, PyAny>>, + min_width: Option<&Bound<'_, PyAny>>, + include_dynamic_bodies: bool, + ) -> PyResult { + let default = rapier::control::CharacterAutostep::default(); + let mh = match max_height { + None => CharacterLength::from_rapier(default.max_height), + Some(o) => _extract_char_length(o)?, + }; + let mw = match min_width { + None => CharacterLength::from_rapier(default.min_width), + Some(o) => _extract_char_length(o)?, + }; + Ok(Self { + max_height: mh, + min_width: mw, + include_dynamic_bodies, + }) + } + /// Return ``None``, the sentinel to disable auto-step on the controller. + /// + /// Idiomatic: ``controller.autostep = CharacterAutostep.disabled()``. + #[staticmethod] + fn disabled() -> Option { + None + } + /// Return the ``CharacterAutostep(...)`` repr. + fn __repr__(&self) -> String { + format!( + "CharacterAutostep(max_height={:?}, min_width={:?}, include_dynamic_bodies={})", + self.max_height, self.min_width, self.include_dynamic_bodies + ) + } +} + +impl CharacterAutostep { + #[inline] + pub(crate) fn to_rapier(self) -> rapier::control::CharacterAutostep { + rapier::control::CharacterAutostep { + max_height: self.max_height.to_rapier(), + min_width: self.min_width.to_rapier(), + include_dynamic_bodies: self.include_dynamic_bodies, + } + } +} + +// --------------------------------------------------------------- +// EffectiveCharacterMovement +// --------------------------------------------------------------- +/// Result of :meth:`KinematicCharacterController.move_shape`. +/// +/// Reports the actual translation applied to the character after +/// collision-aware sliding, auto-step, and ground snapping, plus a +/// ``grounded`` flag derived from the controller's contact state. +/// +/// :ivar translation: World-space translation actually applied. +/// :ivar grounded: ``True`` if the character is in contact with a +/// surface this frame. +/// :ivar is_sliding_down_slope: ``True`` if the controller is +/// sliding the character down a too-steep slope. +#[pyclass(name = "EffectiveCharacterMovement", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct EffectiveCharacterMovement { + #[pyo3(get)] + pub translation: Vec3, + #[pyo3(get)] + pub grounded: bool, + #[pyo3(get)] + pub is_sliding_down_slope: bool, +} + +#[pymethods] +impl EffectiveCharacterMovement { + /// Return the ``EffectiveCharacterMovement(...)`` repr. + fn __repr__(&self) -> String { + format!( + "EffectiveCharacterMovement(translation={:?}, grounded={}, is_sliding_down_slope={})", + self.translation.0, self.grounded, self.is_sliding_down_slope + ) + } +} + +// --------------------------------------------------------------- +// CharacterCollision +// --------------------------------------------------------------- +/// A single collision event reported during character movement. +/// +/// Instances are delivered to the ``events_callback`` of +/// :meth:`KinematicCharacterController.move_shape` and can later +/// be fed back to +/// :meth:`KinematicCharacterController.solve_character_collision_impulses` +/// to apply reactive impulses to the bodies the character bumped. +/// +/// :ivar handle: Handle of the collider the character hit. +/// :ivar character_pos: World-space pose at the moment of contact. +/// :ivar translation_applied: Portion of the desired translation +/// successfully applied before the collision. +/// :ivar translation_remaining: Portion of translation still owed +/// (typically slid along the contact surface). +/// :ivar toi: ``ShapeCastHit`` with witness points and normals. +#[pyclass(name = "CharacterCollision", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct CharacterCollision { + #[pyo3(get)] + pub handle: ColliderHandle, + #[pyo3(get)] + pub character_pos: Isometry3, + #[pyo3(get)] + pub translation_applied: Vec3, + #[pyo3(get)] + pub translation_remaining: Vec3, + #[pyo3(get)] + pub toi: ShapeCastHit, +} + +#[pymethods] +impl CharacterCollision { + /// Return the ``CharacterCollision(handle=..., toi=...)`` repr. + fn __repr__(&self) -> String { + format!( + "CharacterCollision(handle={:?}, toi={})", + self.handle.0.into_raw_parts(), + self.toi.time_of_impact + ) + } +} + +impl CharacterCollision { + #[inline] + pub(crate) fn from_rapier(c: rapier::control::CharacterCollision) -> Self { + let trans_applied: crate::na::SVector = c.translation_applied.into(); + let trans_remaining: crate::na::SVector = c.translation_remaining.into(); + let iso: crate::na::Isometry = c.character_pos.into(); + Self { + handle: ColliderHandle(c.handle), + character_pos: Isometry3(iso), + translation_applied: Vec3(trans_applied), + translation_remaining: Vec3(trans_remaining), + toi: ShapeCastHit::from_parry(c.hit), + } + } + #[inline] + pub(crate) fn to_rapier(&self) -> rapier::control::CharacterCollision { + let iso: rapier::math::Pose = self.character_pos.0.into(); + let ta: rapier::math::Vector = self.translation_applied.0.into(); + let tr: rapier::math::Vector = self.translation_remaining.0.into(); + rapier::control::CharacterCollision { + handle: self.handle.0, + character_pos: iso, + translation_applied: ta, + translation_remaining: tr, + hit: rapier::parry::query::ShapeCastHit { + time_of_impact: self.toi.time_of_impact, + witness1: self.toi.witness1.0.coords.into(), + witness2: self.toi.witness2.0.coords.into(), + normal1: crate::na::Unit::new_unchecked(self.toi.normal1.0) + .into_inner() + .into(), + normal2: crate::na::Unit::new_unchecked(self.toi.normal2.0) + .into_inner() + .into(), + status: match self.toi.status { + ShapeCastStatus::OUT_OF_ITERATIONS => { + rapier::parry::query::ShapeCastStatus::OutOfIterations + } + ShapeCastStatus::CONVERGED => rapier::parry::query::ShapeCastStatus::Converged, + ShapeCastStatus::FAILED => rapier::parry::query::ShapeCastStatus::Failed, + ShapeCastStatus::PENETRATING_OR_WITHIN_TARGET_DIST => { + rapier::parry::query::ShapeCastStatus::PenetratingOrWithinTargetDist + } + }, + }, + } + } +} + +// --------------------------------------------------------------- +// KinematicCharacterController +// --------------------------------------------------------------- +/// Kinematic character controller (collision-aware shape mover). +/// +/// Drives a kinematic-position body / shape through a scene while +/// resolving collisions via sliding, auto-step (small obstacles), +/// and ground snapping (so the character sticks to descending +/// slopes). The controller itself owns no body — call +/// :meth:`move_shape` each frame with the desired translation and +/// then apply the returned :class:`EffectiveCharacterMovement` +/// to your body. +/// +/// Example:: +/// +/// controller = KinematicCharacterController(offset=0.01) +/// mv = controller.move_shape( +/// dt, bodies, colliders, queries, shape, pose, +/// desired_translation +/// ) +/// # then translate your kinematic body by mv.translation +#[pyclass(name = "KinematicCharacterController", module = "rapier")] +#[derive(Debug, Clone)] +pub struct KinematicCharacterController(pub rapier::control::KinematicCharacterController); + +#[pymethods] +impl KinematicCharacterController { + /// Build a controller with sensible defaults plus optional overrides. + /// + /// :param up: World-space up direction. + /// :param offset: Skin-width kept between the character shape + /// and obstacles (:class:`CharacterLength` or raw float). + /// :param slide: Whether to slide along obstacles on contact. + /// :param autostep: :class:`CharacterAutostep` config, or + /// ``None`` to disable. + /// :param max_slope_climb_angle: Slopes steeper than this + /// (radians) cannot be climbed. + /// :param min_slope_slide_angle: Slopes steeper than this + /// (radians) cause the character to slide down. + /// :param snap_to_ground: Distance below the character that + /// should be treated as still grounded after walking off + /// a small drop. ``None`` disables. + /// :param normal_nudge_factor: Small bias along contact + /// normals to avoid getting stuck on geometry. + #[new] + #[pyo3(signature = ( + up=None, + offset=None, + slide=None, + autostep=None, + max_slope_climb_angle=None, + min_slope_slide_angle=None, + snap_to_ground=None, + normal_nudge_factor=None, + ))] + #[allow(clippy::too_many_arguments)] + fn new( + py: Python<'_>, + up: Option, + offset: Option<&Bound<'_, PyAny>>, + slide: Option, + autostep: Option<&Bound<'_, PyAny>>, + max_slope_climb_angle: Option, + min_slope_slide_angle: Option, + snap_to_ground: Option<&Bound<'_, PyAny>>, + normal_nudge_factor: Option, + ) -> PyResult { + let _ = py; + let mut inner = rapier::control::KinematicCharacterController::default(); + if let Some(u) = up { + inner.up = u.0.into(); + } + if let Some(o) = offset { + inner.offset = _extract_char_length(o)?.to_rapier(); + } + if let Some(s) = slide { + inner.slide = s; + } + if let Some(a) = autostep { + if a.is_none() { + inner.autostep = None; + } else { + let auto: CharacterAutostep = a.extract()?; + inner.autostep = Some(auto.to_rapier()); + } + } + if let Some(v) = max_slope_climb_angle { + inner.max_slope_climb_angle = v; + } + if let Some(v) = min_slope_slide_angle { + inner.min_slope_slide_angle = v; + } + if let Some(s) = snap_to_ground { + if s.is_none() { + inner.snap_to_ground = None; + } else { + inner.snap_to_ground = Some(_extract_char_length(s)?.to_rapier()); + } + } + if let Some(v) = normal_nudge_factor { + inner.normal_nudge_factor = v; + } + Ok(Self(inner)) + } + + // ---- property getters / setters ---- + /// World-space "up" direction the controller uses to classify + /// slopes, ground contacts, and the auto-step axis. + #[getter] + fn up(&self) -> Vec3 { + let v: crate::na::SVector = self.0.up.into(); + Vec3(v) + } + #[setter] + /// Set the world-space up direction used by the controller. + fn set_up(&mut self, v: PyVector) { + self.0.up = v.0.into(); + } + + /// Skin width kept between the character and the environment. + #[getter] + fn offset(&self) -> CharacterLength { + CharacterLength::from_rapier(self.0.offset) + } + #[setter] + /// Set the controller's collision-skin offset. + /// + /// Accepts a :class:`CharacterLength` or a bare float + /// (interpreted as absolute world units). + fn set_offset(&mut self, py: Python<'_>, obj: Py) -> PyResult<()> { + let b = obj.bind(py); + self.0.offset = _extract_char_length(b)?.to_rapier(); + Ok(()) + } + + /// Whether to slide along contact surfaces. + #[getter] + fn slide(&self) -> bool { + self.0.slide + } + #[setter] + fn set_slide(&mut self, v: bool) { + self.0.slide = v; + } + + /// Optional :class:`CharacterAutostep` config (``None`` if disabled). + #[getter] + fn autostep(&self) -> Option { + self.0.autostep.map(|a| CharacterAutostep { + max_height: CharacterLength::from_rapier(a.max_height), + min_width: CharacterLength::from_rapier(a.min_width), + include_dynamic_bodies: a.include_dynamic_bodies, + }) + } + #[setter] + /// Set or clear the :class:`CharacterAutostep` configuration. + /// + /// Pass ``None`` to disable auto-step. + fn set_autostep(&mut self, py: Python<'_>, v: Option>) -> PyResult<()> { + self.0.autostep = match v { + None => None, + Some(obj) => { + let b = obj.bind(py); + if b.is_none() { + None + } else { + let a: CharacterAutostep = b.extract()?; + Some(a.to_rapier()) + } + } + }; + Ok(()) + } + + /// Max slope angle (radians) the character can still climb. + #[getter] + fn max_slope_climb_angle(&self) -> Real { + self.0.max_slope_climb_angle + } + #[setter] + fn set_max_slope_climb_angle(&mut self, v: Real) { + self.0.max_slope_climb_angle = v; + } + + /// Min slope angle (radians) at which the character will slide + /// downward (steeper than ``max_slope_climb_angle``). + #[getter] + fn min_slope_slide_angle(&self) -> Real { + self.0.min_slope_slide_angle + } + #[setter] + fn set_min_slope_slide_angle(&mut self, v: Real) { + self.0.min_slope_slide_angle = v; + } + + /// Distance below the character treated as still grounded for + /// snap-down on stairs / descending slopes. ``None`` disables. + #[getter] + fn snap_to_ground(&self) -> Option { + self.0.snap_to_ground.map(CharacterLength::from_rapier) + } + #[setter] + /// Set or clear the snap-to-ground distance. + /// + /// Accepts a :class:`CharacterLength`, a bare float (absolute), + /// or ``None`` to disable snapping. + fn set_snap_to_ground(&mut self, py: Python<'_>, v: Option>) -> PyResult<()> { + self.0.snap_to_ground = match v { + None => None, + Some(obj) => { + let b = obj.bind(py); + if b.is_none() { + None + } else { + Some(_extract_char_length(b)?.to_rapier()) + } + } + }; + Ok(()) + } + + /// Small nudge along contact normals to avoid sticking on geometry. + #[getter] + fn normal_nudge_factor(&self) -> Real { + self.0.normal_nudge_factor + } + #[setter] + fn set_normal_nudge_factor(&mut self, v: Real) { + self.0.normal_nudge_factor = v; + } + + // ---- methods ---- + + /// Compute the effective collision-aware translation for one step. + /// + /// Performs sweep tests through ``queries``, applies sliding, + /// optional auto-step and ground snapping, and returns the + /// translation actually applicable to the kinematic body. + /// + /// The ``bodies`` / ``colliders`` arguments are accepted for + /// API parity with the Rust API; the actual sweep uses the + /// sets already referenced by ``queries``. + /// + /// :param dt: Time step in seconds. + /// :param bodies: Rigid-body set (kept for API parity). + /// :param colliders: Collider set (kept for API parity). + /// :param queries: Up-to-date :class:`QueryPipeline`. + /// :param shape: Character collision shape. + /// :param shape_pos: World pose of the shape at frame start. + /// :param desired_translation: Desired translation this step. + /// :param filter: Optional query filter. + /// :param events_callback: Optional callable receiving each + /// :class:`CharacterCollision` as it is detected. + /// :returns: :class:`EffectiveCharacterMovement`. + #[pyo3(signature = ( + dt, bodies, colliders, queries, shape, shape_pos, desired_translation, + filter=None, events_callback=None + ))] + #[allow(clippy::too_many_arguments)] + fn move_shape( + &self, + py: Python<'_>, + dt: Real, + bodies: &Bound<'_, PyAny>, + colliders: &Bound<'_, PyAny>, + queries: &QueryPipeline, + shape: &SharedShape, + shape_pos: PyIsometry, + desired_translation: PyVector, + filter: Option<&QueryFilter>, + events_callback: Option>, + ) -> PyResult { + let _ = (bodies, colliders); // parity-only; queries owns them + let pose: rapier::math::Pose = shape_pos.0.into(); + let desired: rapier::math::Vector = desired_translation.0.into(); + let inner = &self.0; + let cb_obj = events_callback.map(|o| o.clone_ref(py)); + let cb_err: std::cell::RefCell> = std::cell::RefCell::new(None); + + let result = { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = queries.broad_phase.borrow(py); + let np = queries.narrow_phase.borrow(py); + let bodies = queries.bodies.borrow(py); + let colliders = queries.colliders.borrow(py); + + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: queries.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + let mv = inner.move_shape(dt, &qp, &*shape.0.0, &pose, desired, |collision| { + if let Some(ref obj) = cb_obj { + let py_col = CharacterCollision::from_rapier(collision); + if let Err(e) = obj.call1(py, (py_col,)) { + *cb_err.borrow_mut() = Some(e); + } + } + }); + let t: crate::na::SVector = mv.translation.into(); + Ok::(EffectiveCharacterMovement { + translation: Vec3(t), + grounded: mv.grounded, + is_sliding_down_slope: mv.is_sliding_down_slope, + }) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + }?; + if let Some(e) = cb_err.into_inner() { + return Err(e); + } + Ok(result) + } + + /// Push the dynamic bodies the character touched this frame. + /// + /// Iterates over the :class:`CharacterCollision` events + /// previously delivered by :meth:`move_shape` and applies + /// reactive impulses sized for ``character_mass`` onto any + /// dynamic colliders found. + /// + /// :param dt: Time step in seconds. + /// :param bodies: Rigid-body set (mutated). + /// :param colliders: Collider set (mutated). + /// :param queries: Up-to-date :class:`QueryPipeline`. + /// :param character_shape: Character collision shape. + /// :param character_pos: World pose (currently unused; kept for + /// forward-compat with upstream). + /// :param character_mass: Mass used to scale impulses. + /// :param collisions: List of :class:`CharacterCollision`. + /// :param filter: Optional query filter. + #[pyo3(signature = ( + dt, bodies, colliders, queries, character_shape, character_pos, + character_mass, collisions, filter=None + ))] + #[allow(clippy::too_many_arguments)] + fn solve_character_collision_impulses( + &self, + py: Python<'_>, + dt: Real, + bodies: Py, + colliders: Py, + queries: &QueryPipeline, + character_shape: &SharedShape, + character_pos: PyIsometry, + character_mass: Real, + collisions: Vec, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + let _ = character_pos; + // Build an owned QueryPipelineMut. Since QueryPipelineMut holds + // `&mut RigidBodySet` and `&mut ColliderSet`, we borrow_mut the + // Py<> handles. + let bp = queries.broad_phase.borrow(py); + let np = queries.narrow_phase.borrow(py); + let mut bodies_ref = bodies.borrow_mut(py); + let mut colliders_ref = colliders.borrow_mut(py); + let qf = filter.map(|f| f.as_rapier(None)).unwrap_or_default(); + let mut qpmut = bp.0.as_query_pipeline_mut( + np.0.query_dispatcher(), + &mut bodies_ref.0, + &mut colliders_ref.0, + qf, + ); + let collisions_rs: Vec<_> = collisions.iter().map(|c| c.to_rapier()).collect(); + self.0.solve_character_collision_impulses( + dt, + &mut qpmut, + &*character_shape.0.0, + character_mass, + collisions_rs.iter(), + ); + Ok(()) + } + + /// Return a debug string summarizing the controller config. + fn __repr__(&self) -> String { + format!( + "KinematicCharacterController(up={:?}, offset={:?}, slide={}, autostep={}, max_slope_climb_angle={}, min_slope_slide_angle={}, snap_to_ground={})", + self.0.up, + self.0.offset, + self.0.slide, + if self.0.autostep.is_some() { + "set" + } else { + "None" + }, + self.0.max_slope_climb_angle, + self.0.min_slope_slide_angle, + if self.0.snap_to_ground.is_some() { + "set" + } else { + "None" + }, + ) + } +} + +// --------------------------------------------------------------- +// AxesMask (controllers use this, distinct from LockedAxes). +// +// Upstream `AxesMask` is dim-aware: LIN_X, LIN_Y, LIN_Z, ANG_X, +// ANG_Y, ANG_Z (all present in 3D). +// --------------------------------------------------------------- +/// Bitflags selecting which DOFs a PID/PD controller drives. +/// +/// In 3D the bits are ``LIN_X``, ``LIN_Y``, ``LIN_Z``, ``ANG_X``, +/// ``ANG_Y``, ``ANG_Z``. In 2D only ``LIN_X``, ``LIN_Y``, and +/// ``ANG_Z`` exist (other axes are absent from upstream and not +/// exposed as class attributes). +/// +/// Combine bits with the usual ``|``, ``&``, ``^``, ``-``, ``~`` +/// operators. Distinct from :class:`LockedAxes`, which freezes +/// DOFs on a rigid body. +#[pyclass(name = "AxesMask", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AxesMask(pub rapier::dynamics::AxesMask); + +#[pymethods] +impl AxesMask { + /// Build an ``AxesMask`` from a raw bit pattern. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u8) -> Self { + Self(rapier::dynamics::AxesMask::from_bits_truncate(bits)) + } + /// Return the empty mask (no axes). + #[staticmethod] + fn empty() -> Self { + Self(rapier::dynamics::AxesMask::empty()) + } + /// Return the mask with every dim-valid axis set. + #[staticmethod] + fn all_axes() -> Self { + Self(rapier::dynamics::AxesMask::all()) + } + // For consistency with LockedAxes: + /// Alias of :meth:`all_axes` for parity with :class:`LockedAxes`. + #[staticmethod] + fn all() -> Self { + Self(rapier::dynamics::AxesMask::all()) + } + + #[classattr] + const LIN_X: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_X); + #[classattr] + const LIN_Y: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_Y); + #[classattr] + const ANG_Z: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_Z); + + /// Raw bits as an unsigned int. + #[getter] + fn bits(&self) -> u8 { + self.0.bits() + } + /// ``True`` if ``self`` is a superset of ``other``. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// ``True`` if no bits are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Python ``in`` operator: alias of :meth:`contains`. + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Bitwise OR: union of two masks. + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND: intersection of two masks. + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Bitwise XOR: symmetric difference of two masks. + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Bitwise NOT: complement within the valid axes for this dim. + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// Set subtraction. + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + /// ``False`` iff no bits are set. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("AxesMask supports only == and !=")), + } + } + /// Hash by raw bit pattern (consistent with ``==``). + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + /// Return ``AxesMask(bits=0b...)`` repr. + fn __repr__(&self) -> String { + format!("AxesMask(bits={:#010b})", self.0.bits()) + } +} + +// Dim-specific class attributes — multiple-pymethods is enabled, so +// we can have a second `impl AxesMask` block here. +#[pymethods] +impl AxesMask { + #[classattr] + const LIN_Z: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_Z); + #[classattr] + const ANG_X: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_X); + #[classattr] + const ANG_Y: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_Y); +} + +// ============================================================================ +// PID/PD controller (uses dim-specific angular type). +// ============================================================================ + +// PdErrors — position or velocity error pair. +/// Linear + angular error pair fed to :class:`PidController` / +/// :class:`PdController`. +/// +/// Used both as a "position error" (desired minus current pose) +/// and as a "velocity error" (desired minus current velocity). +/// In 3D ``angular`` is a 3-vector; in 2D it is a single ``float``. +/// +/// :ivar linear: Linear error vector. +/// :ivar angular: Angular error (vector in 3D, scalar in 2D). +#[pyclass(name = "PdErrors", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct PdErrors { + #[pyo3(get, set)] + pub linear: Vec3, + #[pyo3(get, set)] + pub angular: Vec3, +} + +#[pymethods] +impl PdErrors { + /// Build a ``PdErrors`` from optional ``linear`` / ``angular`` values. + /// + /// :param linear: Linear error (defaults to zero). + /// :param angular: Angular error (defaults to zero). + #[new] + #[pyo3(signature = (linear=None, angular=None))] + fn new(linear: Option, angular: Option<&Bound<'_, PyAny>>) -> PyResult { + let lin = linear + .map(|v| v.0) + .unwrap_or_else(crate::na::SVector::::zeros); + let ang = { + match angular { + None => Ok::(Vec3(crate::na::Vector3::zeros())), + Some(o) => { + let pv: PyVector = o.extract()?; + Ok(Vec3(pv.0)) + } + } + }?; + Ok(Self { + linear: Vec3(lin), + angular: ang, + }) + } + /// Return the ``PdErrors(linear=..., angular=...)`` repr. + fn __repr__(&self) -> String { + format!( + "PdErrors(linear={:?}, angular={:?})", + self.linear.0, self.angular + ) + } +} + +impl PdErrors { + #[inline] + pub(crate) fn to_rapier(&self) -> rapier::control::PdErrors { + rapier::control::PdErrors { + linear: self.linear.0.into(), + angular: { + let av: rapier::math::AngVector = self.angular.0.into(); + av + }, + } + } + #[inline] + #[allow(dead_code)] // symmetry with to_rapier; kept for completeness + pub(crate) fn from_rapier(p: rapier::control::PdErrors) -> Self { + let l: crate::na::SVector = p.linear.into(); + Self { + linear: Vec3(l), + angular: { + let r: crate::na::Vector3 = p.angular.into(); + Vec3(r) + }, + } + } +} + +// PidCorrection — linvel/angvel update produced by a controller. +/// Linear + angular velocity correction returned by PID/PD updates. +/// +/// The caller is expected to apply this as an impulse / velocity +/// delta to the controlled rigid body each step. +/// +/// :ivar linear: Linear velocity correction. +/// :ivar angular: Angular velocity correction (vector in 3D, +/// scalar in 2D). +#[pyclass(name = "PidCorrection", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct PidCorrection { + #[pyo3(get)] + pub linear: Vec3, + #[pyo3(get)] + pub angular: Vec3, +} + +#[pymethods] +impl PidCorrection { + /// Return the ``PidCorrection(linear=..., angular=...)`` repr. + fn __repr__(&self) -> String { + format!( + "PidCorrection(linear={:?}, angular={:?})", + self.linear.0, self.angular + ) + } +} + +impl PidCorrection { + #[inline] + fn from_velocity(v: rapier::dynamics::RigidBodyVelocity) -> Self { + let lin: crate::na::SVector = v.linvel.into(); + Self { + linear: Vec3(lin), + angular: { + let r: crate::na::Vector3 = v.angvel.into(); + Vec3(r) + }, + } + } +} + +// PdController — immutable PD. +/// Proportional-derivative controller (PD). +/// +/// Like :class:`PidController` but without the integral term — +/// cheaper per step, no integral wind-up, but cannot eliminate +/// steady-state error. Choose this when target tracking is +/// frequent and the loop is well-damped. +#[pyclass(name = "PdController", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct PdController(pub rapier::control::PdController); + +#[pymethods] +impl PdController { + /// Build a PD controller with optional per-axis ``Kp`` / ``Kd``. + /// + /// :param axes: :class:`AxesMask` selecting which DOFs to + /// control (defaults to all). + /// :param Kp: Proportional gain — scalar (broadcast) or a + /// vector matching linear-axis count. + /// :param Kd: Derivative gain — same shape as ``Kp``. + #[new] + #[pyo3(signature = (axes=None, Kp=None, Kd=None))] + #[allow(non_snake_case)] + fn new( + axes: Option<&AxesMask>, + Kp: Option<&Bound<'_, PyAny>>, + Kd: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let am = axes + .map(|a| a.0) + .unwrap_or_else(rapier::dynamics::AxesMask::all); + let mut inner = rapier::control::PdController::new(60.0 as Real, 0.8 as Real, am); + if let Some(k) = Kp { + let v: PyVector = k.extract()?; + inner.lin_kp = v.0.into(); + let ang_kp = { + // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). + let av: rapier::math::AngVector = if let Ok(f) = k.extract::() { + rapier::math::AngVector::splat(f) + } else { + let pv: PyVector = k.extract()?; + pv.0.into() + }; + Ok::(av) + }?; + inner.ang_kp = ang_kp; + } + if let Some(k) = Kd { + let v: PyVector = k.extract()?; + inner.lin_kd = v.0.into(); + let ang_kd = { + // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). + let av: rapier::math::AngVector = if let Ok(f) = k.extract::() { + rapier::math::AngVector::splat(f) + } else { + let pv: PyVector = k.extract()?; + pv.0.into() + }; + Ok::(av) + }?; + inner.ang_kd = ang_kd; + } + Ok(Self(inner)) + } + + /// :class:`AxesMask` selecting which DOFs are driven. + #[getter] + fn axes(&self) -> AxesMask { + AxesMask(self.0.axes) + } + #[setter] + /// Replace the :class:`AxesMask` selecting controlled DOFs. + fn set_axes(&mut self, v: &AxesMask) { + self.0.axes = v.0; + } + + /// Set the per-linear-axis proportional gain vector. + fn set_axes_kp(&mut self, v: PyVector) { + self.0.lin_kp = v.0.into(); + } + /// Set the per-linear-axis derivative gain vector. + fn set_axes_kd(&mut self, v: PyVector) { + self.0.lin_kd = v.0.into(); + } + + /// Velocity correction toward ``target_pose`` (zero target velocity). + /// + /// :param body: :class:`RigidBody` to drive. + /// :param target_pose: Desired world pose. + /// :returns: :class:`PidCorrection` to apply as a velocity delta. + #[pyo3(signature = (body, target_pose))] + fn rigid_body_correction( + &self, + py: Python<'_>, + body: &Bound<'_, PyAny>, + target_pose: PyIsometry, + ) -> PyResult { + let _ = py; + let rb = body.extract::>()?; + let target_pose: rapier::math::Pose = target_pose.0.into(); + let zero_vels = rapier::dynamics::RigidBodyVelocity::::zero(); + let owned = rb.to_owned_body(); + let corr = self.0.rigid_body_correction(&owned, target_pose, zero_vels); + Ok(PidCorrection::from_velocity(corr)) + } + + /// Velocity correction from explicit pose / velocity errors. + /// + /// :param pose_errors: Position error pair. + /// :param vel_errors: Velocity error pair. + /// :returns: :class:`PidCorrection`. + fn correction(&self, pose_errors: &PdErrors, vel_errors: &PdErrors) -> PidCorrection { + let pe = pose_errors.to_rapier(); + let ve = vel_errors.to_rapier(); + let c = self.0.correction(&pe, &ve); + PidCorrection::from_velocity(c) + } +} + +// PidController — full PID. +/// Proportional-integral-derivative controller (PID). +/// +/// Drives a rigid body toward a target pose along the DOFs picked +/// by an :class:`AxesMask`. Gains ``Kp`` / ``Ki`` / ``Kd`` may be +/// specified per axis (vector) or as a scalar broadcast to every +/// driven axis. Use :meth:`rigid_body_correction` per step to get +/// the velocity correction to apply. +#[pyclass(name = "PidController", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct PidController(pub rapier::control::PidController); + +#[pymethods] +impl PidController { + /// Build a PID with optional per-axis ``Kp`` / ``Ki`` / ``Kd``. + /// + /// :param axes: :class:`AxesMask` selecting controlled DOFs. + /// :param Kp: Proportional gain (scalar or vector). + /// :param Ki: Integral gain (scalar or vector). + /// :param Kd: Derivative gain (scalar or vector). + #[new] + #[pyo3(signature = (axes=None, Kp=None, Ki=None, Kd=None))] + #[allow(non_snake_case)] + fn new( + axes: Option<&AxesMask>, + Kp: Option<&Bound<'_, PyAny>>, + Ki: Option<&Bound<'_, PyAny>>, + Kd: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let am = axes + .map(|a| a.0) + .unwrap_or_else(rapier::dynamics::AxesMask::all); + let mut inner = + rapier::control::PidController::new(60.0 as Real, 1.0 as Real, 0.8 as Real, am); + if let Some(k) = Kp { + let v: PyVector = k.extract()?; + inner.pd.lin_kp = v.0.into(); + inner.pd.ang_kp = { + // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). + let av: rapier::math::AngVector = if let Ok(f) = k.extract::() { + rapier::math::AngVector::splat(f) + } else { + let pv: PyVector = k.extract()?; + pv.0.into() + }; + Ok::(av) + }?; + } + if let Some(k) = Kd { + let v: PyVector = k.extract()?; + inner.pd.lin_kd = v.0.into(); + inner.pd.ang_kd = { + // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). + let av: rapier::math::AngVector = if let Ok(f) = k.extract::() { + rapier::math::AngVector::splat(f) + } else { + let pv: PyVector = k.extract()?; + pv.0.into() + }; + Ok::(av) + }?; + } + if let Some(k) = Ki { + let v: PyVector = k.extract()?; + inner.lin_ki = v.0.into(); + inner.ang_ki = { + // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). + let av: rapier::math::AngVector = if let Ok(f) = k.extract::() { + rapier::math::AngVector::splat(f) + } else { + let pv: PyVector = k.extract()?; + pv.0.into() + }; + Ok::(av) + }?; + } + Ok(Self(inner)) + } + + /// :class:`AxesMask` selecting which DOFs are driven. + #[getter] + fn axes(&self) -> AxesMask { + AxesMask(self.0.axes()) + } + #[setter] + /// Replace the :class:`AxesMask` selecting controlled DOFs. + fn set_axes_attr(&mut self, v: &AxesMask) { + self.0.set_axes(v.0); + } + + /// Set the per-linear-axis proportional gain vector. + fn set_axes_kp(&mut self, v: PyVector) { + self.0.pd.lin_kp = v.0.into(); + } + /// Set the per-linear-axis integral gain vector. + fn set_axes_ki(&mut self, v: PyVector) { + self.0.lin_ki = v.0.into(); + } + /// Set the per-linear-axis derivative gain vector. + fn set_axes_kd(&mut self, v: PyVector) { + self.0.pd.lin_kd = v.0.into(); + } + + /// Reset the integral accumulators to zero. + fn reset(&mut self) { + self.0.reset_integrals(); + } + + /// One PID step: velocity correction toward ``target_pose``. + /// + /// :param dt: Time step in seconds. + /// :param body: :class:`RigidBody` to drive. + /// :param target_pose: Desired world pose. + /// :returns: :class:`PidCorrection`. + #[pyo3(signature = (dt, body, target_pose))] + fn rigid_body_correction( + &mut self, + dt: Real, + body: &Bound<'_, PyAny>, + target_pose: PyIsometry, + ) -> PyResult { + let rb = body.extract::>()?; + let pose: rapier::math::Pose = target_pose.0.into(); + let zero_vels = rapier::dynamics::RigidBodyVelocity::::zero(); + let owned = rb.to_owned_body(); + let corr = self.0.rigid_body_correction(dt, &owned, pose, zero_vels); + Ok(PidCorrection::from_velocity(corr)) + } + + /// PID correction from two poses (no rigid-body needed). + /// + /// Useful for offline / off-graph usage where you only have + /// the current and target world poses. Assumes zero current + /// and target velocities. + /// + /// :param dt: Time step in seconds. + /// :param position: Current world pose. + /// :param target_position: Desired world pose. + /// :returns: :class:`PidCorrection`. + #[pyo3(signature = (dt, position, target_position))] + fn position_correction( + &mut self, + dt: Real, + position: PyIsometry, + target_position: PyIsometry, + ) -> PyResult { + let pos: rapier::math::Pose = position.0.into(); + let tgt: rapier::math::Pose = target_position.0.into(); + // Compute pose errors manually using the same formula upstream + // uses (RigidBodyPosition::pose_errors with COM = origin). + let rb_pos = rapier::dynamics::RigidBodyPosition { + position: pos, + next_position: tgt, + }; + let pose_errors = rb_pos.pose_errors(rapier::math::Vector::ZERO); + let zero_vels: rapier::control::PdErrors = + rapier::dynamics::RigidBodyVelocity::::zero().into(); + let corr = self.0.correction(dt, &pose_errors, &zero_vels); + Ok(PidCorrection::from_velocity(corr)) + } + + /// Run one PID update step with explicit pose/velocity errors. + /// + /// :param dt: Time step in seconds. + /// :param pose_errors: Position error pair. + /// :param vel_errors: Velocity error pair. + /// :returns: :class:`PidCorrection`. + fn update(&mut self, dt: Real, pose_errors: &PdErrors, vel_errors: &PdErrors) -> PidCorrection { + let pe = pose_errors.to_rapier(); + let ve = vel_errors.to_rapier(); + let c = self.0.correction(dt, &pe, &ve); + PidCorrection::from_velocity(c) + } +} + +// ============================================================================ +// 3D-only: vehicle controller. +// ============================================================================ + +// --------------------------------------------------------------- +// WheelTuning +// --------------------------------------------------------------- +/// Spring / friction tuning parameters for a vehicle wheel. +/// +/// Used when adding a wheel via +/// :meth:`DynamicRayCastVehicleController.add_wheel`. +/// +/// .. note:: +/// :meth:`default` returns rapier's upstream defaults which are +/// typically *too soft* for most real-world setups — wheels tend +/// to bottom out and drift sideways. Expect to tighten +/// ``suspension_stiffness``, ``friction_slip``, and +/// ``side_friction_stiffness`` substantially. +#[pyclass(name = "WheelTuning", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct WheelTuning(pub rapier::control::WheelTuning); + +#[pymethods] +impl WheelTuning { + /// Build a ``WheelTuning`` with optional overrides. + /// + /// :param suspension_stiffness: Spring constant. + /// :param suspension_compression: Compression damping. + /// :param suspension_damping: Relaxation damping. + /// :param max_suspension_travel: Max compression travel. + /// :param side_friction_stiffness: Lateral tire grip. + /// :param friction_slip: Longitudinal friction coefficient. + /// :param max_suspension_force: Force cap on the spring. + #[new] + #[pyo3(signature = (suspension_stiffness=None, suspension_compression=None, suspension_damping=None, max_suspension_travel=None, side_friction_stiffness=None, friction_slip=None, max_suspension_force=None))] + #[allow(clippy::too_many_arguments)] + fn new( + suspension_stiffness: Option, + suspension_compression: Option, + suspension_damping: Option, + max_suspension_travel: Option, + side_friction_stiffness: Option, + friction_slip: Option, + max_suspension_force: Option, + ) -> Self { + let mut t = rapier::control::WheelTuning::default(); + if let Some(v) = suspension_stiffness { + t.suspension_stiffness = v; + } + if let Some(v) = suspension_compression { + t.suspension_compression = v; + } + if let Some(v) = suspension_damping { + t.suspension_damping = v; + } + if let Some(v) = max_suspension_travel { + t.max_suspension_travel = v; + } + if let Some(v) = side_friction_stiffness { + t.side_friction_stiffness = v; + } + if let Some(v) = friction_slip { + t.friction_slip = v; + } + if let Some(v) = max_suspension_force { + t.max_suspension_force = v; + } + Self(t) + } + + /// Return rapier's default ``WheelTuning``. + /// + /// Beware — the upstream defaults are intentionally soft and + /// often produce skating / bottoming-out behavior. Tune for + /// your vehicle. + #[staticmethod] + fn default() -> Self { + Self(rapier::control::WheelTuning::default()) + } + + /// Spring stiffness constant. + #[getter] + fn suspension_stiffness(&self) -> Real { + self.0.suspension_stiffness + } + #[setter] + /// Assign the suspension spring stiffness constant. + fn set_suspension_stiffness(&mut self, v: Real) { + self.0.suspension_stiffness = v; + } + /// Damping while the spring is compressing. + #[getter] + fn suspension_compression(&self) -> Real { + self.0.suspension_compression + } + #[setter] + /// Assign the compression-phase damping coefficient. + fn set_suspension_compression(&mut self, v: Real) { + self.0.suspension_compression = v; + } + /// Damping while the spring is relaxing. + #[getter] + fn suspension_damping(&self) -> Real { + self.0.suspension_damping + } + #[setter] + /// Assign the relaxation-phase damping coefficient. + fn set_suspension_damping(&mut self, v: Real) { + self.0.suspension_damping = v; + } + /// Max travel distance for the suspension. + #[getter] + fn max_suspension_travel(&self) -> Real { + self.0.max_suspension_travel + } + #[setter] + /// Assign the maximum suspension travel distance. + fn set_max_suspension_travel(&mut self, v: Real) { + self.0.max_suspension_travel = v; + } + /// Lateral (sideways) tire stiffness. + #[getter] + fn side_friction_stiffness(&self) -> Real { + self.0.side_friction_stiffness + } + #[setter] + /// Assign the lateral tire stiffness coefficient. + fn set_side_friction_stiffness(&mut self, v: Real) { + self.0.side_friction_stiffness = v; + } + /// Longitudinal friction coefficient. + #[getter] + fn friction_slip(&self) -> Real { + self.0.friction_slip + } + #[setter] + /// Assign the longitudinal friction coefficient. + fn set_friction_slip(&mut self, v: Real) { + self.0.friction_slip = v; + } + /// Maximum suspension spring force. + #[getter] + fn max_suspension_force(&self) -> Real { + self.0.max_suspension_force + } + #[setter] + /// Assign the cap on suspension spring force. + fn set_max_suspension_force(&mut self, v: Real) { + self.0.max_suspension_force = v; + } + + /// Return the ``WheelTuning(...)`` repr. + fn __repr__(&self) -> String { + format!( + "WheelTuning(suspension_stiffness={}, friction_slip={})", + self.0.suspension_stiffness, self.0.friction_slip + ) + } +} + +// --------------------------------------------------------------- +// RayCastInfo +// --------------------------------------------------------------- +/// Read-only per-wheel raycast hit info for the vehicle controller. +/// +/// Snapshot taken after the latest +/// :meth:`DynamicRayCastVehicleController.update_vehicle` call. +/// +/// :ivar contact_normal_ws: World-space normal at the contact. +/// :ivar contact_point_ws: World-space contact point. +/// :ivar suspension_length: Current spring length (compressed). +/// :ivar hard_point_ws: World-space wheel attachment point on chassis. +/// :ivar is_in_contact: ``True`` if the ray hit anything. +/// :ivar ground_object: Hit collider handle, if any. +#[pyclass(name = "RayCastInfo", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct RayCastInfo { + #[pyo3(get)] + pub contact_normal_ws: Vec3, + #[pyo3(get)] + pub contact_point_ws: Vec3, + #[pyo3(get)] + pub suspension_length: Real, + #[pyo3(get)] + pub hard_point_ws: Vec3, + #[pyo3(get)] + pub is_in_contact: bool, + #[pyo3(get)] + pub ground_object: Option, +} + +#[pymethods] +impl RayCastInfo { + /// Return the ``RayCastInfo(...)`` repr. + fn __repr__(&self) -> String { + format!( + "RayCastInfo(is_in_contact={}, suspension_length={})", + self.is_in_contact, self.suspension_length + ) + } +} + +impl RayCastInfo { + #[inline] + fn from_wheel(w: &rapier::control::Wheel) -> Self { + let r = w.raycast_info(); + let cn: crate::na::Vector3 = r.contact_normal_ws.into(); + let cp: crate::na::Vector3 = r.contact_point_ws.into(); + let hp: crate::na::Vector3 = r.hard_point_ws.into(); + Self { + contact_normal_ws: Vec3(cn), + contact_point_ws: Vec3(cp), + suspension_length: r.suspension_length, + hard_point_ws: Vec3(hp), + is_in_contact: r.is_in_contact, + ground_object: r.ground_object.map(ColliderHandle), + } + } +} + +// --------------------------------------------------------------- +// Wheel — a snapshot/proxy. The upstream controller owns the +// backing `Vec`; we project read+write fields into a Python +// class. Mutations on a `Wheel` instance returned by +// `.wheels()` / `.wheel(i)` do NOT propagate back to the +// controller — they are detached copies. Use the controller's +// `set_brake` / `set_steering` / `apply_engine_force` for +// mutations that need to take effect. +// --------------------------------------------------------------- +/// Detached snapshot of one vehicle wheel. +/// +/// .. warning:: +/// Instances returned by +/// :meth:`DynamicRayCastVehicleController.wheels` / +/// :meth:`DynamicRayCastVehicleController.wheel` are **copies**. +/// Mutating the fields here does **not** propagate back to the +/// controller. Use +/// :meth:`DynamicRayCastVehicleController.set_brake`, +/// :meth:`set_steering`, and :meth:`apply_engine_force` to +/// actually drive the wheel. +#[pyclass(name = "Wheel", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct Wheel { + #[pyo3(get, set)] + pub chassis_connection_point_cs: Vec3, + #[pyo3(get, set)] + pub direction_cs: Vec3, + #[pyo3(get, set)] + pub axle_cs: Vec3, + #[pyo3(get, set)] + pub suspension_rest_length: Real, + #[pyo3(get, set)] + pub max_suspension_travel: Real, + #[pyo3(get, set)] + pub radius: Real, + #[pyo3(get, set)] + pub suspension_stiffness: Real, + #[pyo3(get, set)] + pub damping_compression: Real, + #[pyo3(get, set)] + pub damping_relaxation: Real, + #[pyo3(get, set)] + pub friction_slip: Real, + #[pyo3(get, set)] + pub side_friction_stiffness: Real, + #[pyo3(get, set)] + pub rotation: Real, + #[pyo3(get, set)] + pub max_suspension_force: Real, + #[pyo3(get, set)] + pub forward_impulse: Real, + #[pyo3(get, set)] + pub side_impulse: Real, + #[pyo3(get, set)] + pub steering: Real, + #[pyo3(get, set)] + pub engine_force: Real, + #[pyo3(get, set)] + pub brake: Real, + #[pyo3(get, set)] + pub wheel_suspension_force: Real, + #[pyo3(get)] + pub raycast_info: RayCastInfo, +} + +#[pymethods] +impl Wheel { + /// Return the ``Wheel(...)`` repr. + fn __repr__(&self) -> String { + format!( + "Wheel(engine_force={}, brake={}, steering={})", + self.engine_force, self.brake, self.steering + ) + } +} + +impl Wheel { + #[inline] + pub(crate) fn from_rapier(w: &rapier::control::Wheel) -> Self { + let ccp: crate::na::Vector3 = w.chassis_connection_point_cs.into(); + let dir: crate::na::Vector3 = w.direction_cs.into(); + let axle: crate::na::Vector3 = w.axle_cs.into(); + Self { + chassis_connection_point_cs: Vec3(ccp), + direction_cs: Vec3(dir), + axle_cs: Vec3(axle), + suspension_rest_length: w.suspension_rest_length, + max_suspension_travel: w.max_suspension_travel, + radius: w.radius, + suspension_stiffness: w.suspension_stiffness, + damping_compression: w.damping_compression, + damping_relaxation: w.damping_relaxation, + friction_slip: w.friction_slip, + side_friction_stiffness: w.side_friction_stiffness, + rotation: w.rotation, + max_suspension_force: w.max_suspension_force, + forward_impulse: w.forward_impulse, + side_impulse: w.side_impulse, + steering: w.steering, + engine_force: w.engine_force, + brake: w.brake, + wheel_suspension_force: w.wheel_suspension_force, + raycast_info: RayCastInfo::from_wheel(w), + } + } +} + +// --------------------------------------------------------------- +// DynamicRayCastVehicleController +// --------------------------------------------------------------- +/// Raycast-based vehicle controller attached to a chassis body (3D only). +/// +/// Each "wheel" is a raycast from a chassis-space attachment point +/// along ``direction_cs`` for ``suspension_rest_length`` units. The +/// controller resolves spring/damper forces from each ray hit and +/// applies them to the chassis :class:`RigidBody` to simulate a +/// driving vehicle without needing actual wheel colliders. +#[pyclass(name = "DynamicRayCastVehicleController", module = "rapier")] +pub struct DynamicRayCastVehicleController(pub rapier::control::DynamicRayCastVehicleController); + +#[pymethods] +impl DynamicRayCastVehicleController { + /// Construct a vehicle controller for the given chassis body. + /// + /// :param chassis_handle: :class:`RigidBodyHandle` of the chassis + /// dynamic body. + #[new] + fn new(chassis_handle: &RigidBodyHandle) -> Self { + Self(rapier::control::DynamicRayCastVehicleController::new( + chassis_handle.0, + )) + } + + /// Append a new wheel to this controller. + /// + /// :param chassis_connection_cs: Wheel attachment point in + /// chassis-local coordinates. + /// :param direction_cs: Suspension axis (typically ``-up``). + /// :param axle_cs: Wheel spin axis in chassis-local coords. + /// :param suspension_rest_length: Spring rest length. + /// :param radius: Wheel radius (for raycast clipping). + /// :param tuning: :class:`WheelTuning` spring/friction params. + /// :returns: The index of the new wheel. + #[pyo3(signature = (chassis_connection_cs, direction_cs, axle_cs, suspension_rest_length, radius, tuning))] + #[allow(clippy::too_many_arguments)] + fn add_wheel( + &mut self, + chassis_connection_cs: PyVector, + direction_cs: PyVector, + axle_cs: PyVector, + suspension_rest_length: Real, + radius: Real, + tuning: &WheelTuning, + ) -> usize { + let ccs: rapier::math::Vector = chassis_connection_cs.0.into(); + let dir: rapier::math::Vector = direction_cs.0.into(); + let axl: rapier::math::Vector = axle_cs.0.into(); + self.0 + .add_wheel(ccs, dir, axl, suspension_rest_length, radius, &tuning.0); + self.0.wheels().len() - 1 + } + + /// Step the vehicle controller for one tick. + /// + /// Performs per-wheel raycasts through ``queries`` and updates + /// the chassis :class:`RigidBody` with the resulting forces and + /// torques. Call this before stepping the physics world. + /// + /// Requires a fresh :class:`QueryPipeline` — typically call + /// ``world.update_query_pipeline()`` first. + /// + /// :param dt: Time step in seconds. + /// :param bodies: Rigid-body set (mutated). + /// :param colliders: Collider set (mutated). + /// :param queries: Up-to-date :class:`QueryPipeline`. + /// :param filter: Optional query filter. + #[pyo3(signature = (dt, bodies, colliders, queries, filter=None))] + fn update_vehicle( + &mut self, + py: Python<'_>, + dt: Real, + bodies: Py, + colliders: Py, + queries: &QueryPipeline, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + let bp = queries.broad_phase.borrow(py); + let np = queries.narrow_phase.borrow(py); + let mut bodies_ref = bodies.borrow_mut(py); + let mut colliders_ref = colliders.borrow_mut(py); + let qf = filter.map(|f| f.as_rapier(None)).unwrap_or_default(); + let qpmut = bp.0.as_query_pipeline_mut( + np.0.query_dispatcher(), + &mut bodies_ref.0, + &mut colliders_ref.0, + qf, + ); + self.0.update_vehicle(dt, qpmut); + Ok(()) + } + + /// Set the brake force on wheel ``idx``. + /// + /// :raises TypeError: if ``idx`` is out of range. + fn set_brake(&mut self, idx: usize, brake: Real) -> PyResult<()> { + let wheels = self.0.wheels_mut(); + if idx >= wheels.len() { + return Err(PyTypeError::new_err(format!( + "wheel index {} out of range (len={})", + idx, + wheels.len() + ))); + } + wheels[idx].brake = brake; + Ok(()) + } + + /// Set the steering angle (radians) on wheel ``idx``. + /// + /// :raises TypeError: if ``idx`` is out of range. + fn set_steering(&mut self, idx: usize, steering: Real) -> PyResult<()> { + let wheels = self.0.wheels_mut(); + if idx >= wheels.len() { + return Err(PyTypeError::new_err(format!( + "wheel index {} out of range (len={})", + idx, + wheels.len() + ))); + } + wheels[idx].steering = steering; + Ok(()) + } + + /// Set the engine drive force on wheel ``idx``. + /// + /// :raises TypeError: if ``idx`` is out of range. + fn apply_engine_force(&mut self, idx: usize, force: Real) -> PyResult<()> { + let wheels = self.0.wheels_mut(); + if idx >= wheels.len() { + return Err(PyTypeError::new_err(format!( + "wheel index {} out of range (len={})", + idx, + wheels.len() + ))); + } + wheels[idx].engine_force = force; + Ok(()) + } + + /// Return a fresh list of :class:`Wheel` snapshots (detached copies). + fn wheels(&self) -> Vec { + self.0.wheels().iter().map(Wheel::from_rapier).collect() + } + + /// Return a snapshot of the single wheel at ``idx``. + /// + /// :raises TypeError: if ``idx`` is out of range. + fn wheel(&self, idx: usize) -> PyResult { + let wheels = self.0.wheels(); + wheels.get(idx).map(Wheel::from_rapier).ok_or_else(|| { + PyTypeError::new_err(format!( + "wheel index {} out of range (len={})", + idx, + wheels.len() + )) + }) + } + + /// Current forward speed in km/h. + fn current_speed_km_hour(&self) -> Real { + // upstream's `current_vehicle_speed` is in m/s; convert to km/h. + self.0.current_vehicle_speed * (3.6 as Real) + } + + /// Current forward speed in m/s along the chassis forward axis. + #[getter] + fn current_vehicle_speed(&self) -> Real { + self.0.current_vehicle_speed + } + + /// :class:`RigidBodyHandle` of the chassis body. + fn chassis(&self) -> RigidBodyHandle { + RigidBodyHandle(self.0.chassis) + } + + /// Index of the up-axis in chassis-local coords (``0=X``, ``1=Y``, ``2=Z``). + #[getter] + fn index_up_axis(&self) -> usize { + self.0.index_up_axis + } + #[setter] + /// Set the chassis-local up-axis index (``0=X``, ``1=Y``, ``2=Z``). + fn set_index_up_axis(&mut self, v: usize) { + self.0.index_up_axis = v; + } + /// Index of the forward-axis in chassis-local coords (``0=X``, ``1=Y``, ``2=Z``). + #[getter] + fn index_forward_axis(&self) -> usize { + self.0.index_forward_axis + } + #[setter] + /// Set the chassis-local forward-axis index (``0=X``, ``1=Y``, ``2=Z``). + fn set_index_forward_axis(&mut self, v: usize) { + self.0.index_forward_axis = v; + } + + /// Return a debug string with the wheel count and forward speed. + fn __repr__(&self) -> String { + format!( + "DynamicRayCastVehicleController(wheels={}, speed={} m/s)", + self.0.wheels().len(), + self.0.current_vehicle_speed + ) + } +} + +// ============================================================================ +// Registration. +// ============================================================================ + +pub fn register_controllers( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/debug_render.rs b/python/rapier-py-3d/src/debug_render.rs new file mode 100644 index 000000000..7a4441721 --- /dev/null +++ b/python/rapier-py-3d/src/debug_render.rs @@ -0,0 +1,1191 @@ +//! User-facing debug-render `#[pyclass]`-es for the 3D cdylib. +//! +//! Debug-Render Adapter. +//! +//! # API surface +//! +//! - `DebugRenderPipeline` — wraps `rapier::pipeline::DebugRenderPipeline`, +//! plus a convenience `render_to_arrays(...)` returning NumPy arrays. +//! - `DebugLineCollector` — built-in `DebugRenderBackend` that drains all +//! draw calls into internal `Vec`s exposed as NumPy arrays (or iterated as +//! `(object, a, b, color)` tuples). +//! - `DebugRenderBackend` (Python `Protocol` — see `python/rapier/_debug_render.py`). +//! - `DebugRenderStyle` — wraps the rapier struct with every public field as a +//! mutable Python property. +//! - `DebugRenderMode` — bitflags wrapper. +//! - `DebugRenderObject` — frozen tagged-union mirror of the Rust enum +//! (`COLLIDER_SHAPE`, `RIGID_BODY_FRAME`, etc.). +//! - `DebugColor` — small frozen wrapper around `[f32; 4]` with both an HSLA +//! and an RGBA constructor (rapier stores HSLA internally — see +//! `src/pipeline/debug_render_pipeline/debug_render_style.rs`). +//! +//! # NumPy copy semantics +//! +//! Both `render_to_arrays` and `DebugLineCollector.lines()` / `.colors()` / +//! `.objects()` allocate fresh `PyArray*` buffers and **copy** the underlying +//! data. There is no zero-copy aliasing — the collector remains owned by +//! Python and can be cleared / re-used safely. + +use crate::pyo3::exceptions::PyTypeError; +use crate::pyo3::pyclass::CompareOp; +use crate::*; +use rapier3d as rapier; + +use std::sync::{Arc as _DbgArc, Mutex as _DbgMutex}; + +// ===================================================================== +// Internal storage type. Each `DebugLine` records a single segment as +// emitted by rapier into the backend. +// ===================================================================== + +#[derive(Debug, Clone, Copy)] +struct DebugLine { + object_kind: u32, + a: crate::na::SVector, + b: crate::na::SVector, + color: [f32; 4], +} + +// ===================================================================== +// DebugRenderObject — tag mirror of `rapier::pipeline::DebugRenderObject`. +// +// The upstream enum carries handle + by-ref payloads; we expose only the +// discriminant + handles since the Python side has no way to safely +// borrow the referenced rapier objects after the call returns. +// ===================================================================== + +/// Tagged-union mirror of ``rapier::pipeline::DebugRenderObject``. +/// +/// Each segment emitted by :class:`DebugRenderPipeline` is tagged +/// with one of these. Use the class constants +/// :attr:`RIGID_BODY`, :attr:`COLLIDER`, :attr:`COLLIDER_AABB`, +/// :attr:`IMPULSE_JOINT`, :attr:`MULTIBODY_JOINT`, :attr:`CONTACT_PAIR` +/// for comparison. +/// +/// :ivar kind: Stable ``u32`` discriminant. +#[allow(non_camel_case_types)] +#[pyclass(name = "DebugRenderObject", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DebugRenderObject { + #[pyo3(get)] + pub kind: u32, +} + +// Stable u32 discriminants used both as `#[classattr]` values below +// and by the `_dro_kind` helper to tag each `DebugLine`. +const _DRO_RIGID_BODY: u32 = 0; +const _DRO_COLLIDER: u32 = 1; +const _DRO_COLLIDER_AABB: u32 = 2; +const _DRO_IMPULSE_JOINT: u32 = 3; +const _DRO_MULTIBODY_JOINT: u32 = 4; +const _DRO_CONTACT_PAIR: u32 = 5; + +#[pymethods] +impl DebugRenderObject { + #[classattr] + const RIGID_BODY: DebugRenderObject = DebugRenderObject { + kind: _DRO_RIGID_BODY, + }; + #[classattr] + const COLLIDER: DebugRenderObject = DebugRenderObject { + kind: _DRO_COLLIDER, + }; + #[classattr] + const COLLIDER_AABB: DebugRenderObject = DebugRenderObject { + kind: _DRO_COLLIDER_AABB, + }; + #[classattr] + const IMPULSE_JOINT: DebugRenderObject = DebugRenderObject { + kind: _DRO_IMPULSE_JOINT, + }; + #[classattr] + const MULTIBODY_JOINT: DebugRenderObject = DebugRenderObject { + kind: _DRO_MULTIBODY_JOINT, + }; + #[classattr] + const CONTACT_PAIR: DebugRenderObject = DebugRenderObject { + kind: _DRO_CONTACT_PAIR, + }; + + /// Return ``DebugRenderObject.`` repr. + fn __repr__(&self) -> String { + let s = match self.kind { + 0 => "RIGID_BODY", + 1 => "COLLIDER", + 2 => "COLLIDER_AABB", + 3 => "IMPULSE_JOINT", + 4 => "MULTIBODY_JOINT", + 5 => "CONTACT_PAIR", + _ => "UNKNOWN", + }; + format!("DebugRenderObject.{s}") + } + /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.kind == other.kind), + CompareOp::Ne => Ok(self.kind != other.kind), + _ => Err(PyTypeError::new_err( + "DebugRenderObject supports only == and !=", + )), + } + } + /// Hash by ``kind`` discriminant (consistent with ``==``). + fn __hash__(&self) -> u64 { + self.kind as u64 + } +} + +#[inline] +fn _dro_kind(obj: rapier::pipeline::DebugRenderObject<'_>) -> u32 { + match obj { + rapier::pipeline::DebugRenderObject::RigidBody(..) => _DRO_RIGID_BODY, + rapier::pipeline::DebugRenderObject::Collider(..) => _DRO_COLLIDER, + rapier::pipeline::DebugRenderObject::ColliderAabb(..) => _DRO_COLLIDER_AABB, + rapier::pipeline::DebugRenderObject::ImpulseJoint(..) => _DRO_IMPULSE_JOINT, + rapier::pipeline::DebugRenderObject::MultibodyJoint(..) => _DRO_MULTIBODY_JOINT, + rapier::pipeline::DebugRenderObject::ContactPair(..) => _DRO_CONTACT_PAIR, + } +} + +// ===================================================================== +// DebugColor — wrapper around `[f32; 4]` (rapier's `DebugColor` alias). +// +// Rapier stores colors in HSLA. The `from_rgba` constructor converts +// from RGBA via the standard HSL transform so users don't have to +// think about it; the `.rgba` getter goes the other way. The raw HSLA +// tuple is also accessible via `.hsla` for direct interop. +// ===================================================================== + +/// Color used by the debug-render pipeline. +/// +/// Internally stored as HSLA (hue in degrees ``[0, 360)``, +/// saturation / lightness / alpha each in ``[0, 1]``) because +/// that's what rapier's :class:`DebugRenderStyle` uses. Two +/// constructors are provided — :meth:`from_hsla` and +/// :meth:`from_rgba` — plus the :attr:`rgba` getter to convert +/// back to RGBA suitable for matplotlib / pygame / etc. +#[pyclass(name = "DebugColor", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct DebugColor(pub [f32; 4]); + +#[inline] +fn _hsla_to_rgba(hsla: [f32; 4]) -> [f32; 4] { + // h in degrees [0, 360), s/l/a in [0, 1]. + let mut h = hsla[0] % 360.0; + if h < 0.0 { + h += 360.0; + } + let s = hsla[1].clamp(0.0, 1.0); + let l = hsla[2].clamp(0.0, 1.0); + let a = hsla[3]; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let h_prime = h / 60.0; + let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs()); + let (r1, g1, b1) = match h_prime as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + let m = l - c * 0.5; + [r1 + m, g1 + m, b1 + m, a] +} + +#[inline] +fn _rgba_to_hsla(rgba: [f32; 4]) -> [f32; 4] { + let r = rgba[0]; + let g = rgba[1]; + let b = rgba[2]; + let a = rgba[3]; + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let l = (max + min) * 0.5; + let d = max - min; + let s = if d == 0.0 { + 0.0 + } else { + d / (1.0 - (2.0 * l - 1.0).abs()).max(1.0e-12) + }; + let h = if d == 0.0 { + 0.0 + } else if max == r { + 60.0 * (((g - b) / d) % 6.0) + } else if max == g { + 60.0 * (((b - r) / d) + 2.0) + } else { + 60.0 * (((r - g) / d) + 4.0) + }; + let h = if h < 0.0 { h + 360.0 } else { h }; + [h, s, l, a] +} + +#[pymethods] +impl DebugColor { + /// Build a ``DebugColor`` from a raw HSLA tuple. + /// + /// :param h: Hue in degrees ``[0, 360)``. + /// :param s: Saturation in ``[0, 1]``. + /// :param l: Lightness in ``[0, 1]``. + /// :param a: Alpha in ``[0, 1]``. + #[new] + #[pyo3(signature = (h = 0.0, s = 0.0, l = 0.0, a = 1.0))] + fn new(h: f32, s: f32, l: f32, a: f32) -> Self { + Self([h, s, l, a]) + } + + /// Construct a `DebugColor` from an HSLA tuple (hue in degrees, + /// saturation/lightness/alpha in `[0, 1]`). + #[staticmethod] + fn from_hsla(h: f32, s: f32, l: f32, a: f32) -> Self { + Self([h, s, l, a]) + } + + /// Construct a `DebugColor` from an RGBA tuple (`[0, 1]` each). + /// + /// Internally converted to HSLA since rapier's `DebugRenderStyle` + /// expects HSLA tuples. + #[staticmethod] + fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + Self(_rgba_to_hsla([r, g, b, a])) + } + + /// The raw HSLA tuple stored by this color. + #[getter] + fn hsla(&self) -> [f32; 4] { + self.0 + } + + /// The RGBA tuple after HSLA → RGBA conversion (each in `[0, 1]`). + #[getter] + fn rgba(&self) -> [f32; 4] { + _hsla_to_rgba(self.0) + } + + /// Hue component (degrees, ``[0, 360)``). + #[getter] + fn h(&self) -> f32 { + self.0[0] + } + /// Saturation component (``[0, 1]``). + #[getter] + fn s(&self) -> f32 { + self.0[1] + } + /// Lightness component (``[0, 1]``). + #[getter] + fn l(&self) -> f32 { + self.0[2] + } + /// Alpha component (``[0, 1]``). + #[getter] + fn a(&self) -> f32 { + self.0[3] + } + + /// Return the ``DebugColor(h=..., s=..., l=..., a=...)`` repr. + fn __repr__(&self) -> String { + format!( + "DebugColor(h={}, s={}, l={}, a={})", + self.0[0], self.0[1], self.0[2], self.0[3] + ) + } + /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("DebugColor supports only == and !=")), + } + } +} + +impl DebugColor { + #[inline] + fn extract_from(obj: &Bound<'_, PyAny>) -> PyResult<[f32; 4]> { + if let Ok(c) = obj.extract::() { + return Ok(c.0); + } + let v: Vec = crate::conv::extract_floats_from_sequence(obj, 4)?; + Ok([v[0], v[1], v[2], v[3]]) + } +} + +// ===================================================================== +// DebugRenderMode — bitflags wrapper. +// ===================================================================== + +/// Bitflags selecting what :class:`DebugRenderPipeline` renders. +/// +/// Combine flags with ``|``, ``&``, ``^``, ``~``. The +/// :meth:`default` set is +/// ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES`` and is what +/// ``DebugRenderPipeline()`` uses if no mode is passed. +#[pyclass(name = "DebugRenderMode", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DebugRenderMode(pub rapier::pipeline::DebugRenderMode); + +#[pymethods] +impl DebugRenderMode { + /// Build a ``DebugRenderMode`` from a raw bit pattern. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::pipeline::DebugRenderMode::from_bits_truncate(bits)) + } + /// Return the empty flag set (renders nothing). + #[staticmethod] + fn empty() -> Self { + Self(rapier::pipeline::DebugRenderMode::empty()) + } + /// Default flag set: ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES``. + #[staticmethod] + #[pyo3(name = "default")] + fn py_default() -> Self { + Self(rapier::pipeline::DebugRenderMode::default()) + } + /// Return the union of all known flags. + #[staticmethod] + fn all() -> Self { + Self(rapier::pipeline::DebugRenderMode::all()) + } + + #[classattr] + const COLLIDER_SHAPES: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::COLLIDER_SHAPES); + #[classattr] + const RIGID_BODY_AXES: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::RIGID_BODY_AXES); + #[classattr] + const MULTIBODY_JOINTS: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::MULTIBODY_JOINTS); + #[classattr] + const IMPULSE_JOINTS: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::IMPULSE_JOINTS); + #[classattr] + const JOINTS: DebugRenderMode = DebugRenderMode(rapier::pipeline::DebugRenderMode::JOINTS); + #[classattr] + const SOLVER_CONTACTS: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::SOLVER_CONTACTS); + #[classattr] + const CONTACTS: DebugRenderMode = DebugRenderMode(rapier::pipeline::DebugRenderMode::CONTACTS); + #[classattr] + const COLLIDER_AABBS: DebugRenderMode = + DebugRenderMode(rapier::pipeline::DebugRenderMode::COLLIDER_AABBS); + #[classattr] + const EMPTY: DebugRenderMode = DebugRenderMode(rapier::pipeline::DebugRenderMode::empty()); + + /// Raw bits as an unsigned int. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// ``True`` if ``self`` is a superset of ``other``. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// ``True`` if no bits are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Python ``in`` operator: alias of :meth:`contains`. + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Bitwise OR: union of two flag sets. + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND: intersection of two flag sets. + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Bitwise XOR: symmetric difference of two flag sets. + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Bitwise NOT: complement within all known flags. + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// ``False`` iff no bits are set. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "DebugRenderMode supports only == and !=", + )), + } + } + /// Hash by raw bit pattern (consistent with ``==``). + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + /// Return ``DebugRenderMode(bits=0b...)`` repr. + fn __repr__(&self) -> String { + format!("DebugRenderMode(bits={:#010b})", self.0.bits()) + } +} + +// ===================================================================== +// DebugRenderStyle — wraps `rapier::pipeline::DebugRenderStyle` with +// every public field as a mutable Python property. +// ===================================================================== + +/// Style configuration for the debug-render pipeline. +/// +/// Wraps ``rapier::pipeline::DebugRenderStyle`` with every public +/// field as a mutable Python property. The default constructor +/// uses rapier's upstream defaults. +/// +/// Color attributes accept either a :class:`DebugColor` or any +/// 4-float sequence (HSLA). +#[pyclass(name = "DebugRenderStyle", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct DebugRenderStyle(pub rapier::pipeline::DebugRenderStyle); + +#[pymethods] +impl DebugRenderStyle { + /// Build a ``DebugRenderStyle`` with rapier's default values. + #[new] + fn new() -> Self { + Self(rapier::pipeline::DebugRenderStyle::default()) + } + /// Return rapier's default ``DebugRenderStyle``. + #[staticmethod] + #[pyo3(name = "default")] + fn py_default() -> Self { + Self(rapier::pipeline::DebugRenderStyle::default()) + } + + /// Number of subdivisions used for curved shapes (cylinders, + /// spheres, capsules). + #[getter] + fn subdivisions(&self) -> u32 { + self.0.subdivisions + } + #[setter] + /// Assign the curved-shape tessellation subdivision count. + fn set_subdivisions(&mut self, v: u32) { + self.0.subdivisions = v; + } + /// Subdivisions used for borders of e.g. round-cuboids. + #[getter] + fn border_subdivisions(&self) -> u32 { + self.0.border_subdivisions + } + #[setter] + /// Assign the rounded-border tessellation subdivision count. + fn set_border_subdivisions(&mut self, v: u32) { + self.0.border_subdivisions = v; + } + + /// Color for colliders attached to a dynamic rigid body. + #[getter] + fn collider_dynamic_color(&self) -> DebugColor { + DebugColor(self.0.collider_dynamic_color) + } + #[setter] + /// Set the color used to render dynamic-body colliders. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_collider_dynamic_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.collider_dynamic_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color for colliders attached to a fixed rigid body. + #[getter] + fn collider_fixed_color(&self) -> DebugColor { + DebugColor(self.0.collider_fixed_color) + } + #[setter] + /// Set the color used to render fixed-body colliders. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_collider_fixed_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.collider_fixed_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color for colliders attached to a kinematic rigid body. + #[getter] + fn collider_kinematic_color(&self) -> DebugColor { + DebugColor(self.0.collider_kinematic_color) + } + #[setter] + /// Set the color used to render kinematic-body colliders. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_collider_kinematic_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.collider_kinematic_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color for colliders not attached to any rigid body. + #[getter] + fn collider_parentless_color(&self) -> DebugColor { + DebugColor(self.0.collider_parentless_color) + } + #[setter] + /// Set the color used to render parentless (orphan) colliders. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_collider_parentless_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.collider_parentless_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color of an impulse joint's anchor frame. + #[getter] + fn impulse_joint_anchor_color(&self) -> DebugColor { + DebugColor(self.0.impulse_joint_anchor_color) + } + #[setter] + /// Set the color used to render impulse-joint anchor frames. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_impulse_joint_anchor_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.impulse_joint_anchor_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color of the separation line shown when an impulse joint is + /// not satisfied. + #[getter] + fn impulse_joint_separation_color(&self) -> DebugColor { + DebugColor(self.0.impulse_joint_separation_color) + } + #[setter] + /// Set the color used to render unsatisfied-impulse-joint + /// separation segments. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_impulse_joint_separation_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.impulse_joint_separation_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color of a multibody joint's anchor frame. + #[getter] + fn multibody_joint_anchor_color(&self) -> DebugColor { + DebugColor(self.0.multibody_joint_anchor_color) + } + #[setter] + /// Set the color used to render multibody-joint anchor frames. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_multibody_joint_anchor_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.multibody_joint_anchor_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color of the separation line shown when a multibody joint is + /// not satisfied. + #[getter] + fn multibody_joint_separation_color(&self) -> DebugColor { + DebugColor(self.0.multibody_joint_separation_color) + } + #[setter] + /// Set the color used to render unsatisfied-multibody-joint + /// separation segments. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_multibody_joint_separation_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.multibody_joint_separation_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Multiplier applied to base colors of sleeping bodies. + #[getter] + fn sleep_color_multiplier(&self) -> DebugColor { + DebugColor(self.0.sleep_color_multiplier) + } + #[setter] + /// Set the HSLA multiplier applied to sleeping-body colors. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_sleep_color_multiplier(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.sleep_color_multiplier = DebugColor::extract_from(v)?; + Ok(()) + } + /// Multiplier applied to base colors of disabled bodies. + #[getter] + fn disabled_color_multiplier(&self) -> DebugColor { + DebugColor(self.0.disabled_color_multiplier) + } + #[setter] + /// Set the HSLA multiplier applied to disabled-body colors. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_disabled_color_multiplier(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.disabled_color_multiplier = DebugColor::extract_from(v)?; + Ok(()) + } + /// Length of the rigid-body axis triads when + /// :attr:`DebugRenderMode.RIGID_BODY_AXES` is set. + #[getter] + fn rigid_body_axes_length(&self) -> Real { + self.0.rigid_body_axes_length + } + #[setter] + /// Assign the rigid-body axis triad length in world units. + fn set_rigid_body_axes_length(&mut self, v: Real) { + self.0.rigid_body_axes_length = v; + } + /// Color of the segment drawn for each contact penetration depth. + #[getter] + fn contact_depth_color(&self) -> DebugColor { + DebugColor(self.0.contact_depth_color) + } + #[setter] + /// Set the color used to render contact penetration-depth segments. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_contact_depth_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.contact_depth_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Color of contact normal segments. + #[getter] + fn contact_normal_color(&self) -> DebugColor { + DebugColor(self.0.contact_normal_color) + } + #[setter] + /// Set the color used to render contact-normal segments. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_contact_normal_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.contact_normal_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Length of the drawn contact-normal segments. + #[getter] + fn contact_normal_length(&self) -> Real { + self.0.contact_normal_length + } + #[setter] + /// Assign the length (world units) of contact-normal segments. + fn set_contact_normal_length(&mut self, v: Real) { + self.0.contact_normal_length = v; + } + /// Color of collider AABB outlines (when + /// :attr:`DebugRenderMode.COLLIDER_AABBS` is set). + #[getter] + fn collider_aabb_color(&self) -> DebugColor { + DebugColor(self.0.collider_aabb_color) + } + #[setter] + /// Set the color used to render collider AABB outlines. + /// + /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. + fn set_collider_aabb_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.collider_aabb_color = DebugColor::extract_from(v)?; + Ok(()) + } + /// Return a brief ``DebugRenderStyle(...)`` repr. + fn __repr__(&self) -> String { + format!( + "DebugRenderStyle(subdivisions={}, ...)", + self.0.subdivisions + ) + } +} + +// ===================================================================== +// DebugLineCollector — `#[pyclass]` implementing the rapier +// `DebugRenderBackend` trait through an inner buffer. +// ===================================================================== + +/// Built-in `DebugRenderBackend` that drains each `draw_line` call into +/// an internal buffer exposed as NumPy arrays. +/// +/// Use `DebugRenderPipeline.render(backend=collector)` to populate, then +/// call `.lines()`, `.colors()`, `.objects()` for `(N, 2, D)`, `(N, 4)`, +/// `(N,)` NumPy arrays respectively (each call returns a fresh **copy** +/// of the underlying data). +#[pyclass(name = "DebugLineCollector", module = "rapier")] +pub struct DebugLineCollector { + // `Mutex>` so we can mutate from the &self + // `draw_line` implementation without exposing `unsafe`. The + // `Arc` lets us share the buffer with a transient `&mut` + // implementer used inside `render` (see `_run_with_backend`). + lines: _DbgArc<_DbgMutex>>, +} + +#[pymethods] +impl DebugLineCollector { + /// Build a new empty collector. + #[new] + fn new() -> Self { + Self { + lines: _DbgArc::new(_DbgMutex::new(Vec::new())), + } + } + + /// Drop every queued line. + fn clear(&self) { + self.lines.lock().unwrap().clear(); + } + + /// Number of lines currently queued. + fn __len__(&self) -> usize { + self.lines.lock().unwrap().len() + } + /// Return ``DebugLineCollector(len=N)`` repr. + fn __repr__(&self) -> String { + format!( + "DebugLineCollector(len={})", + self.lines.lock().unwrap().len() + ) + } + + /// Return a fresh `(N, 2, D)` NumPy array of segment endpoints. + fn lines<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray2> { + use crate::numpy::PyArray2; + let lines = self.lines.lock().unwrap(); + // Flatten as N*2 rows of D columns; reshape on the Python side + // if a (N, 2, D) view is desired (we expose `(N*2, D)` then + // reshape via numpy in the helper `render_to_arrays`). + let mut flat: Vec> = Vec::with_capacity(lines.len() * 2); + for ln in lines.iter() { + flat.push(_svec_to_row::<3>(ln.a)); + flat.push(_svec_to_row::<3>(ln.b)); + } + let arr = PyArray2::::from_vec2_bound(py, &flat) + .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 3], false)); + // Reshape (N*2, D) → (N, 2, D). We do this via numpy at the + // Python side; here we just return the (2N, D) view. + arr + } + + /// Return a fresh `(N, 4)` NumPy array of RGBA (post-HSLA→RGBA) + /// per-line colors. + fn colors<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray2> { + use crate::numpy::PyArray2; + let lines = self.lines.lock().unwrap(); + let mut flat: Vec> = Vec::with_capacity(lines.len()); + for ln in lines.iter() { + let rgba = _hsla_to_rgba(ln.color); + flat.push(rgba.to_vec()); + } + PyArray2::::from_vec2_bound(py, &flat) + .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 4], false)) + } + + /// Return a fresh `(N,)` NumPy array of `DebugRenderObject` kind + /// discriminants (one entry per line). + fn objects<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray1> { + use crate::numpy::PyArray1; + let lines = self.lines.lock().unwrap(); + let v: Vec = lines.iter().map(|ln| ln.object_kind).collect(); + PyArray1::::from_vec_bound(py, v) + } + + /// Iterate over the queued lines as + /// ``(object, a, b, color)`` tuples. + /// + /// The iterator takes a snapshot at iteration time — concurrent + /// :meth:`clear` / re-render calls do not invalidate it. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + // Snapshot of (object, a, b, color) tuples at iteration time. + let py = slf.py(); + let lines = slf.lines.lock().unwrap(); + let items: Vec<(DebugRenderObject, Vec3, Vec3, DebugColor)> = lines + .iter() + .map(|ln| { + ( + DebugRenderObject { + kind: ln.object_kind, + }, + Vec3(ln.a), + Vec3(ln.b), + DebugColor(ln.color), + ) + }) + .collect(); + Py::new(py, DebugLineCollectorIter { items, i: 0 }) + } +} + +/// Iterator over the ``(object, a, b, color)`` snapshot of a +/// :class:`DebugLineCollector`. +#[pyclass(module = "rapier")] +pub struct DebugLineCollectorIter { + items: Vec<(DebugRenderObject, Vec3, Vec3, DebugColor)>, + i: usize, +} + +#[pymethods] +impl DebugLineCollectorIter { + /// Return ``self`` (this object is already an iterator). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Return the next ``(object, a, b, color)`` tuple, or raise + /// ``StopIteration``. + fn __next__( + mut slf: PyRefMut<'_, Self>, + ) -> Option<(DebugRenderObject, Vec3, Vec3, DebugColor)> { + if slf.i >= slf.items.len() { + return None; + } + let idx = slf.i; + slf.i += 1; + Some(slf.items[idx]) + } +} + +// ===================================================================== +// PyDebugRenderBackend — adapter wrapping an arbitrary Python object +// implementing the `DebugRenderBackend` protocol. +// +// Mirrors the deferred-exception pattern from `events_hooks.rs`: errors +// raised inside `draw_line` are stashed in a shared slot and re-raised +// after `render()` returns. +// ===================================================================== + +struct _CollectorBackendAdapter { + buf: _DbgArc<_DbgMutex>>, +} + +impl rapier::pipeline::DebugRenderBackend for _CollectorBackendAdapter { + fn draw_line( + &mut self, + object: rapier::pipeline::DebugRenderObject<'_>, + a: rapier::math::Vector, + b: rapier::math::Vector, + color: rapier::pipeline::DebugColor, + ) { + let an: crate::na::SVector = a.into(); + let bn: crate::na::SVector = b.into(); + self.buf.lock().unwrap().push(DebugLine { + object_kind: _dro_kind(object), + a: an, + b: bn, + color, + }); + } +} + +struct _PyBackendAdapter { + obj: Py, + err_slot: _DbgArc<_DbgMutex>>, +} + +impl rapier::pipeline::DebugRenderBackend for _PyBackendAdapter { + fn draw_line( + &mut self, + object: rapier::pipeline::DebugRenderObject<'_>, + a: rapier::math::Vector, + b: rapier::math::Vector, + color: rapier::pipeline::DebugColor, + ) { + { + let slot = self.err_slot.lock().unwrap(); + if slot.is_some() { + return; + } + } + Python::with_gil(|py| { + let kind = _dro_kind(object); + let obj_py = DebugRenderObject { kind }; + let an: crate::na::SVector = a.into(); + let bn: crate::na::SVector = b.into(); + let a_py = Vec3(an); + let b_py = Vec3(bn); + let c_py = DebugColor(color); + let res = self + .obj + .bind(py) + .call_method1("draw_line", (obj_py, a_py, b_py, c_py)); + if let Err(e) = res { + let mut s = self.err_slot.lock().unwrap(); + if s.is_none() { + *s = Some(e); + } + } + }); + } +} + +// ===================================================================== +// DebugRenderPipeline — main user-facing class. +// ===================================================================== + +/// Main entry point for visualizing a rapier scene. +/// +/// Walks the rigid bodies, colliders, and joint sets and emits one +/// or more colored line segments per object. The segments are +/// delivered to a backend implementing the ``DebugRenderBackend`` +/// protocol — typically a :class:`DebugLineCollector` for offline +/// inspection (matplotlib, pygame, etc.), or a custom Python +/// object exposing ``draw_line(object, a, b, color)``. +/// +/// For purely-data use cases prefer :meth:`render_to_arrays`, +/// which returns NumPy arrays directly. +#[pyclass(name = "DebugRenderPipeline", module = "rapier", unsendable)] +pub struct DebugRenderPipeline { + inner: rapier::pipeline::DebugRenderPipeline, +} + +impl DebugRenderPipeline { + fn _run_with_backend( + &mut self, + bodies: &RigidBodySet, + colliders: &ColliderSet, + impulse_joints: &ImpulseJointSet, + multibody_joints: &MultibodyJointSet, + narrow_phase: &NarrowPhase, + backend: &mut dyn rapier::pipeline::DebugRenderBackend, + ) { + // The trait method `render` takes `&mut impl DebugRenderBackend` + // which is not directly object-safe via `&mut dyn`. Wrap it. + struct DynWrap<'a> { + inner: &'a mut dyn rapier::pipeline::DebugRenderBackend, + } + impl<'a> rapier::pipeline::DebugRenderBackend for DynWrap<'a> { + fn draw_line( + &mut self, + object: rapier::pipeline::DebugRenderObject<'_>, + a: rapier::math::Vector, + b: rapier::math::Vector, + color: rapier::pipeline::DebugColor, + ) { + self.inner.draw_line(object, a, b, color) + } + } + let mut wrap = DynWrap { inner: backend }; + self.inner.render( + &mut wrap, + &bodies.0, + &colliders.0, + &impulse_joints.0, + &multibody_joints.0, + &narrow_phase.0, + ); + } +} + +#[pymethods] +impl DebugRenderPipeline { + /// Build a pipeline with optional mode / style overrides. + /// + /// :param mode: :class:`DebugRenderMode` flags selecting what + /// to render. Defaults to + /// ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES``. + /// :param style: :class:`DebugRenderStyle` color and length + /// configuration. Defaults to rapier's upstream defaults. + #[new] + #[pyo3(signature = (mode = None, style = None))] + fn new(mode: Option<&DebugRenderMode>, style: Option<&DebugRenderStyle>) -> Self { + let m = mode.map(|m| m.0).unwrap_or_default(); + let s = style.map(|s| s.0).unwrap_or_default(); + Self { + inner: rapier::pipeline::DebugRenderPipeline::new(s, m), + } + } + + /// Current :class:`DebugRenderMode` flag set. + #[getter] + fn mode(&self) -> DebugRenderMode { + DebugRenderMode(self.inner.mode) + } + #[setter] + /// Replace the current :class:`DebugRenderMode` flag set. + fn set_mode(&mut self, v: &DebugRenderMode) { + self.inner.mode = v.0; + } + /// Current :class:`DebugRenderStyle`. + #[getter] + fn style(&self) -> DebugRenderStyle { + DebugRenderStyle(self.inner.style) + } + #[setter] + /// Replace the current :class:`DebugRenderStyle`. + fn set_style(&mut self, v: &DebugRenderStyle) { + self.inner.style = v.0; + } + + /// Render the scene into the given ``backend``. + /// + /// ``backend`` may be a :class:`DebugLineCollector` (fast path — + /// no Python callback per segment) or any Python object exposing + /// ``draw_line(object, a, b, color) -> None``. Exceptions raised + /// by a Python backend are deferred and re-raised after rendering + /// completes. + /// + /// For hot loops prefer the collector — each Python-side + /// ``draw_line`` call has to acquire the GIL. + /// + /// :param bodies: :class:`RigidBodySet`. + /// :param colliders: :class:`ColliderSet`. + /// :param impulse_joints: :class:`ImpulseJointSet`. + /// :param multibody_joints: :class:`MultibodyJointSet`. + /// :param narrow_phase: :class:`NarrowPhase`. + /// :param backend: Collector or Python ``DebugRenderBackend``. + #[pyo3(signature = (bodies, colliders, impulse_joints, multibody_joints, narrow_phase, backend))] + #[allow(clippy::too_many_arguments)] + fn render( + &mut self, + py: Python<'_>, + bodies: &RigidBodySet, + colliders: &ColliderSet, + impulse_joints: &ImpulseJointSet, + multibody_joints: &MultibodyJointSet, + narrow_phase: &NarrowPhase, + backend: &Bound<'_, PyAny>, + ) -> PyResult<()> { + // Fast path: DebugLineCollector. We bypass the Python callback + // dispatch entirely. + if let Ok(c) = backend.extract::>() { + let mut adapter = _CollectorBackendAdapter { + buf: _DbgArc::clone(&c.lines), + }; + self._run_with_backend( + bodies, + colliders, + impulse_joints, + multibody_joints, + narrow_phase, + &mut adapter, + ); + return Ok(()); + } + + // General path: Python-side backend implementing + // `draw_line(object, a, b, color)`. + let err_slot: _DbgArc<_DbgMutex>> = _DbgArc::new(_DbgMutex::new(None)); + let mut adapter = _PyBackendAdapter { + obj: backend.clone().unbind(), + err_slot: _DbgArc::clone(&err_slot), + }; + self._run_with_backend( + bodies, + colliders, + impulse_joints, + multibody_joints, + narrow_phase, + &mut adapter, + ); + drop(adapter); + let mut slot = err_slot.lock().unwrap(); + if let Some(e) = slot.take() { + return Err(e); + } + let _ = py; + Ok(()) + } + + /// Render the scene and return NumPy arrays directly. + /// + /// Convenience wrapper around :meth:`render` with a transient + /// :class:`DebugLineCollector`. + /// + /// :param bodies: :class:`RigidBodySet`. + /// :param colliders: :class:`ColliderSet`. + /// :param impulse_joints: :class:`ImpulseJointSet`. + /// :param multibody_joints: :class:`MultibodyJointSet`. + /// :param narrow_phase: :class:`NarrowPhase`. + /// :returns: A ``(lines, colors, objects)`` tuple where + /// ``lines`` is ``(N, 2, D)`` float, ``colors`` is + /// ``(N, 4)`` float32 RGBA in ``[0, 1]``, and ``objects`` + /// is ``(N,)`` uint32 of + /// :class:`DebugRenderObject` discriminants. + #[pyo3(signature = (bodies, colliders, impulse_joints, multibody_joints, narrow_phase))] + #[allow(clippy::too_many_arguments)] + fn render_to_arrays<'py>( + &mut self, + py: Python<'py>, + bodies: &RigidBodySet, + colliders: &ColliderSet, + impulse_joints: &ImpulseJointSet, + multibody_joints: &MultibodyJointSet, + narrow_phase: &NarrowPhase, + ) -> PyResult<( + Bound<'py, crate::numpy::PyArray3>, + Bound<'py, crate::numpy::PyArray2>, + Bound<'py, crate::numpy::PyArray1>, + )> { + use crate::numpy::{PyArray1, PyArray2, PyArray3, PyArrayMethods}; + + let buf: _DbgArc<_DbgMutex>> = _DbgArc::new(_DbgMutex::new(Vec::new())); + { + let mut adapter = _CollectorBackendAdapter { + buf: _DbgArc::clone(&buf), + }; + self._run_with_backend( + bodies, + colliders, + impulse_joints, + multibody_joints, + narrow_phase, + &mut adapter, + ); + } + + let lines = buf.lock().unwrap(); + let n = lines.len(); + + // (N, 2, D) lines array. Allocate flat and reshape. + let lines_arr = PyArray3::::zeros_bound(py, [n, 2, 3], false); + { + let mut view = unsafe { lines_arr.as_array_mut() }; + for (i, ln) in lines.iter().enumerate() { + let row_a = _svec_to_row::<3>(ln.a); + let row_b = _svec_to_row::<3>(ln.b); + for d in 0..3 { + view[[i, 0, d]] = row_a[d]; + view[[i, 1, d]] = row_b[d]; + } + } + } + + let mut col_flat: Vec> = Vec::with_capacity(n); + for ln in lines.iter() { + col_flat.push(_hsla_to_rgba(ln.color).to_vec()); + } + let colors_arr = if n == 0 { + PyArray2::::zeros_bound(py, [0, 4], false) + } else { + PyArray2::::from_vec2_bound(py, &col_flat) + .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 4], false)) + }; + + let objects_arr = + PyArray1::::from_vec_bound(py, lines.iter().map(|ln| ln.object_kind).collect()); + + Ok((lines_arr, colors_arr, objects_arr)) + } + + /// Return a brief ``DebugRenderPipeline(mode=0b...)`` repr. + fn __repr__(&self) -> String { + format!("DebugRenderPipeline(mode={:#010b})", self.inner.mode.bits()) + } +} + +// Helper: turn an `na::SVector` into a `Vec` of +// length `DIM`. Inlined so the row-builder loops above can reuse it. +#[inline] +fn _svec_to_row(v: crate::na::SVector) -> Vec { + let mut row = Vec::with_capacity(D); + for d in 0..D { + row.push(v[d]); + } + row +} + +pub fn register_debug_render( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/dynamics.rs b/python/rapier-py-3d/src/dynamics.rs new file mode 100644 index 000000000..892c0c0d1 --- /dev/null +++ b/python/rapier-py-3d/src/dynamics.rs @@ -0,0 +1,2767 @@ +//! The concrete 3D / `f32` rigid-body / dynamics `#[pyclass]` types +//! (rigid bodies and builders, mass properties, integration parameters, the +//! island manager, the CCD solver, ...) plus the `register_dynamics` function +//! used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. +//! +//! Forward-compat with later phases: the module relies on `ColliderHandle`, +//! `ColliderSet`, `ImpulseJointSet` and `MultibodyJointSet` being in scope +//! (they live in `geometry.rs` / `joints.rs`). + +use crate::*; +use rapier3d as rapier; + +use crate::pyo3::exceptions::{PyNotImplementedError, PyTypeError}; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; + +// -------- Glam <-> nalgebra conversion helpers -------- + +/// Convert a nalgebra `SVector` to the engine's `Vector`. +#[allow(dead_code)] +#[inline] +fn _na_vec_to_engine(v: crate::na::SVector) -> rapier::math::Vector { + v.into() +} + +/// Convert the engine's `Vector` back to nalgebra. +#[allow(dead_code)] +#[inline] +fn _engine_vec_to_na(v: rapier::math::Vector) -> crate::na::SVector { + v.into() +} + +// ============================================================ +// ColliderHandle / ColliderSet live in `geometry.rs`. +// The dynamics macro is invoked AFTER the geometry macro by each +// cdylib, so those types are already in scope here. +// ============================================================ + +// ============================================================ +// ImpulseJointSet / MultibodyJointSet live in `joints.rs`. The joints macro is invoked BEFORE this one in each cdylib, +// so those types are already in scope here. +// ============================================================ + +// ============================================================ +// RigidBodyHandle +// ============================================================ + +/// Opaque handle identifying a rigid body inside a `RigidBodySet`. +/// +/// Handles are stable across structural mutations of the set and +/// remain valid until the corresponding body is removed. They wrap an +/// ``(index, generation)`` pair: the generation guards against reusing +/// a slot freed by a previous body. +/// +/// Use ``RigidBodySet.insert(...)`` to obtain a handle. Handles are +/// hashable and may be used as dict keys. +#[pyclass(name = "RigidBodyHandle", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RigidBodyHandle(pub rapier::dynamics::RigidBodyHandle); + +#[pymethods] +impl RigidBodyHandle { + /// Build a handle from raw ``(index, generation)`` parts. + /// + /// Most code should obtain handles from ``RigidBodySet.insert`` + /// rather than constructing them directly. + /// + /// :param index: slot index inside the set (default ``0``). + /// :param generation: generation tag for the slot (default ``0``). + #[new] + #[pyo3(signature = (index=0, generation=0))] + fn new(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::RigidBodyHandle::from_raw_parts( + index, generation, + )) + } + /// Build a handle from raw ``(index, generation)`` parts. + /// + /// Equivalent to the constructor; provided for symmetry with the + /// Rust API and for use as a named static method. + #[staticmethod] + fn from_raw_parts(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::RigidBodyHandle::from_raw_parts( + index, generation, + )) + } + /// Return a sentinel handle that does not match any real body. + /// + /// Useful as a default value or as a placeholder before a body is + /// actually inserted. + #[staticmethod] + fn invalid() -> Self { + Self(rapier::dynamics::RigidBodyHandle::invalid()) + } + /// Slot index portion of the handle (read-only). + #[getter] + fn index(&self) -> u32 { + self.0.into_raw_parts().0 + } + /// Generation portion of the handle (read-only). + #[getter] + fn generation(&self) -> u32 { + self.0.into_raw_parts().1 + } + /// Hash combining index and generation, enabling use as dict key. + fn __hash__(&self) -> u64 { + let (i, g) = self.0.into_raw_parts(); + ((i as u64) << 32) | (g as u64) + } + /// Compare two handles for equality. Only ``==`` and ``!=`` are + /// supported. + /// + /// :raises TypeError: when an ordering comparison is requested. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "RigidBodyHandle supports only == and !=", + )), + } + } + fn __repr__(&self) -> String { + let (i, g) = self.0.into_raw_parts(); + format!("RigidBodyHandle(index={}, generation={})", i, g) + } +} + +// ============================================================ +// RigidBodyType +// ============================================================ + +/// Behavior class of a rigid body. +/// +/// Determines how the body responds to forces, contacts and joints: +/// +/// - ``DYNAMIC`` — fully simulated; responds to forces and contacts. +/// - ``FIXED`` — immovable; treated as having infinite mass. +/// - ``KINEMATIC_VELOCITY_BASED`` — animated by setting ``linvel`` / +/// ``angvel`` each frame; pushes dynamic bodies, ignores contacts. +/// - ``KINEMATIC_POSITION_BASED`` — animated by setting ``position`` +/// each frame; pushes dynamic bodies, ignores contacts. +#[pyclass(name = "RigidBodyType", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RigidBodyType { + /// Fully simulated: responds to forces, gravity and contacts. + DYNAMIC, + /// Immovable: behaves as if it had infinite mass. + FIXED, + /// Animated by writing to ``linvel`` / ``angvel`` each step. + KINEMATIC_VELOCITY_BASED, + /// Animated by writing to ``position`` each step. + KINEMATIC_POSITION_BASED, +} + +impl RigidBodyType { + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::RigidBodyType { + match self { + Self::DYNAMIC => rapier::dynamics::RigidBodyType::Dynamic, + Self::FIXED => rapier::dynamics::RigidBodyType::Fixed, + Self::KINEMATIC_VELOCITY_BASED => { + rapier::dynamics::RigidBodyType::KinematicVelocityBased + } + Self::KINEMATIC_POSITION_BASED => { + rapier::dynamics::RigidBodyType::KinematicPositionBased + } + } + } + #[inline] + pub(crate) fn from_rapier(t: rapier::dynamics::RigidBodyType) -> Self { + match t { + rapier::dynamics::RigidBodyType::Dynamic => Self::DYNAMIC, + rapier::dynamics::RigidBodyType::Fixed => Self::FIXED, + rapier::dynamics::RigidBodyType::KinematicVelocityBased => { + Self::KINEMATIC_VELOCITY_BASED + } + rapier::dynamics::RigidBodyType::KinematicPositionBased => { + Self::KINEMATIC_POSITION_BASED + } + } + } +} + +// ============================================================ +// CoefficientCombineRule +// ============================================================ + +/// Rule used to combine per-collider friction/restitution coefficients +/// at a contact. +/// +/// When two colliders touch, each has its own coefficient (friction or +/// restitution) and a rule. The effective coefficient is derived by +/// applying the combine rule of the *highest priority* among the two +/// (``MAX > MULTIPLY > MIN > AVERAGE``). +#[pyclass(name = "CoefficientCombineRule", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoefficientCombineRule { + /// Arithmetic mean of the two coefficients. + AVERAGE, + /// Minimum of the two coefficients. + MIN, + /// Product of the two coefficients. + MULTIPLY, + /// Maximum of the two coefficients. + MAX, + /// Sum of the two coefficients, clamped to ``[0, 1]``. + CLAMPED_SUM, +} + +impl CoefficientCombineRule { + #[allow(dead_code)] + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::CoefficientCombineRule { + match self { + Self::AVERAGE => rapier::dynamics::CoefficientCombineRule::Average, + Self::MIN => rapier::dynamics::CoefficientCombineRule::Min, + Self::MULTIPLY => rapier::dynamics::CoefficientCombineRule::Multiply, + Self::MAX => rapier::dynamics::CoefficientCombineRule::Max, + Self::CLAMPED_SUM => rapier::dynamics::CoefficientCombineRule::ClampedSum, + } + } + #[allow(dead_code)] + #[inline] + pub(crate) fn from_rapier(r: rapier::dynamics::CoefficientCombineRule) -> Self { + match r { + rapier::dynamics::CoefficientCombineRule::Average => Self::AVERAGE, + rapier::dynamics::CoefficientCombineRule::Min => Self::MIN, + rapier::dynamics::CoefficientCombineRule::Multiply => Self::MULTIPLY, + rapier::dynamics::CoefficientCombineRule::Max => Self::MAX, + rapier::dynamics::CoefficientCombineRule::ClampedSum => Self::CLAMPED_SUM, + } + } +} + +// ============================================================ +// RigidBodyActivation +// ============================================================ + +/// Sleep / activation state of a rigid body. +/// +/// A body whose linear and angular velocity stays below the sleep +/// thresholds for ``time_until_sleep`` seconds is put to sleep to save +/// compute. A sleeping body is skipped by the solver until something +/// wakes it (contact, force, joint, manual ``wake_up``). +/// +/// Construct via ``RigidBodyActivation()`` (active by default), +/// ``RigidBodyActivation.inactive()`` for an initially-sleeping body +/// or ``RigidBodyActivation.cannot_sleep()`` to disable sleep. +#[pyclass(name = "RigidBodyActivation", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyActivation(pub rapier::dynamics::RigidBodyActivation); + +#[pymethods] +impl RigidBodyActivation { + /// Build the default (active, can sleep) activation state. + #[new] + fn new() -> Self { + Self(rapier::dynamics::RigidBodyActivation::active()) + } + /// Default active activation: body is awake and may sleep later. + #[staticmethod] + fn active() -> Self { + Self(rapier::dynamics::RigidBodyActivation::active()) + } + /// Initially sleeping activation: body starts asleep. + #[staticmethod] + fn inactive() -> Self { + Self(rapier::dynamics::RigidBodyActivation::inactive()) + } + /// Activation that never sleeps — useful for player avatars and + /// other always-simulated bodies. + #[staticmethod] + fn cannot_sleep() -> Self { + Self(rapier::dynamics::RigidBodyActivation::cannot_sleep()) + } + + /// Normalized linear-velocity threshold below which the body may + /// fall asleep (read+write). + #[getter] + fn linear_threshold(&self) -> Real { + self.0.normalized_linear_threshold + } + /// Set the normalized linear sleep threshold. + #[setter] + fn set_linear_threshold(&mut self, v: Real) { + self.0.normalized_linear_threshold = v; + } + /// Alias for ``linear_threshold`` (read+write). + #[getter] + fn sleep_threshold(&self) -> Real { + self.0.normalized_linear_threshold + } + /// Alias for ``set_linear_threshold``. + #[setter] + fn set_sleep_threshold(&mut self, v: Real) { + self.0.normalized_linear_threshold = v; + } + /// Angular-velocity threshold below which the body may fall + /// asleep (read+write). + #[getter] + fn angular_threshold(&self) -> Real { + self.0.angular_threshold + } + /// Set the angular sleep threshold. + #[setter] + fn set_angular_threshold(&mut self, v: Real) { + self.0.angular_threshold = v; + } + /// Seconds the body must stay below thresholds before sleeping. + /// + /// Negative values disable sleep for this body. + #[getter] + fn time_until_sleep(&self) -> Real { + self.0.time_until_sleep + } + /// Set the sleep delay in seconds. + #[setter] + fn set_time_until_sleep(&mut self, v: Real) { + self.0.time_until_sleep = v; + } + /// Whether the body is currently sleeping (read+write). + #[getter] + fn sleeping(&self) -> bool { + self.0.sleeping + } + /// Force the sleeping flag directly. Prefer ``RigidBody.wake_up`` + /// / ``RigidBody.sleep`` for proper bookkeeping. + #[setter] + fn set_sleeping(&mut self, v: bool) { + self.0.sleeping = v; + } + /// Whether the body is currently active (i.e. not sleeping). + /// + /// :returns: ``True`` if the body participates in the simulation + /// this step. + fn is_active(&self) -> bool { + self.0.is_active() + } + fn __repr__(&self) -> String { + format!( + "RigidBodyActivation(linear={}, angular={}, time_until_sleep={}, sleeping={})", + self.0.normalized_linear_threshold, + self.0.angular_threshold, + self.0.time_until_sleep, + self.0.sleeping, + ) + } +} + +// ============================================================ +// SpringCoefficients +// ============================================================ + +/// Soft-constraint spring parameters used for contact and joint +/// regularization. +/// +/// Internally Rapier converts ``stiffness`` (natural frequency, Hz) +/// and ``damping`` (damping ratio, unitless, ``1`` = critical) to the +/// effective spring/damper coefficients used by the solver. Defaults +/// suitable for typical contacts and joints can be obtained via +/// ``SpringCoefficients.contact_defaults()`` and +/// ``SpringCoefficients.joint_defaults()``. +#[pyclass(name = "SpringCoefficients", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct SpringCoefficients(pub rapier::dynamics::SpringCoefficients); + +#[pymethods] +impl SpringCoefficients { + /// Build a spring with the given natural frequency and damping + /// ratio. + /// + /// :param stiffness: natural frequency in Hz (default ``30.0``). + /// :param damping: damping ratio (default ``5.0``). + #[new] + #[pyo3(signature = (stiffness=30.0 as Real, damping=5.0 as Real))] + fn new(stiffness: Real, damping: Real) -> Self { + Self(rapier::dynamics::SpringCoefficients { + natural_frequency: stiffness, + damping_ratio: damping, + }) + } + /// Return the default spring coefficients used for contact + /// regularization. + #[staticmethod] + fn contact_defaults() -> Self { + Self(rapier::dynamics::SpringCoefficients::contact_defaults()) + } + /// Return the default spring coefficients used for joint + /// regularization. + #[staticmethod] + fn joint_defaults() -> Self { + Self(rapier::dynamics::SpringCoefficients::joint_defaults()) + } + /// Spring natural frequency (alias for ``natural_frequency``). + #[getter] + fn stiffness(&self) -> Real { + self.0.natural_frequency + } + /// Set the natural frequency. + #[setter] + fn set_stiffness(&mut self, v: Real) { + self.0.natural_frequency = v; + } + /// Natural frequency of the spring, in Hz. + #[getter] + fn natural_frequency(&self) -> Real { + self.0.natural_frequency + } + /// Set the natural frequency, in Hz. + #[setter] + fn set_natural_frequency(&mut self, v: Real) { + self.0.natural_frequency = v; + } + /// Damping ratio (alias for ``damping_ratio``). + #[getter] + fn damping(&self) -> Real { + self.0.damping_ratio + } + /// Set the damping ratio. + #[setter] + fn set_damping(&mut self, v: Real) { + self.0.damping_ratio = v; + } + /// Damping ratio (unitless, ``1.0`` is critically damped). + #[getter] + fn damping_ratio(&self) -> Real { + self.0.damping_ratio + } + /// Set the damping ratio. + #[setter] + fn set_damping_ratio(&mut self, v: Real) { + self.0.damping_ratio = v; + } + fn __repr__(&self) -> String { + format!( + "SpringCoefficients(stiffness={}, damping={})", + self.0.natural_frequency, self.0.damping_ratio + ) + } +} + +// ============================================================ +// IslandManager +// ============================================================ + +/// Tracks groups of interacting (connected) rigid bodies — *islands* +/// — and which of them are currently active. +/// +/// The island manager is updated by ``PhysicsWorld.step`` each frame +/// and is mainly used internally by the solver. Iterating the +/// manager yields the handles of bodies that are awake this step. +#[pyclass(name = "IslandManager", module = "rapier", unsendable)] +pub struct IslandManager(pub rapier::dynamics::IslandManager); + +#[pymethods] +impl IslandManager { + /// Build an empty island manager. + #[new] + fn new() -> Self { + Self(rapier::dynamics::IslandManager::new()) + } + + /// Snapshot of every awake dynamic body currently tracked. + /// + /// :returns: a list of handles of bodies that are active this step. + fn active_dynamic_set(&self) -> Vec { + self.0.active_bodies().map(RigidBodyHandle).collect() + } + + /// Number of currently active bodies (``len(islands)``). + fn __len__(&self) -> usize { + self.0.active_bodies().count() + } + + /// Iterate over currently active body handles. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let handles: Vec = slf.0.active_bodies().map(RigidBodyHandle).collect(); + Py::new(slf.py(), RigidBodyHandleIter { handles, i: 0 }) + } + + /// Whether ``h`` is currently active. + /// + /// Sleeping bodies are not part of the active set and will return + /// ``False`` here. + fn contains_handle(&self, h: &RigidBodyHandle) -> bool { + // No public `contains` query; we iterate the active set. + // Sleeping bodies aren't in `active_bodies`, so this returns + // false for them — a later revision may revisit this. + self.0.active_bodies().any(|x| x == h.0) + } + + /// Whether ``h1`` and ``h2`` are part of the same active island. + /// + /// Currently approximated as both being in the active set; + /// returns ``False`` if either body is sleeping. + fn same_island(&self, h1: &RigidBodyHandle, h2: &RigidBodyHandle) -> bool { + // No public per-island id; approximate "same island" as both + // being in the active set. The pipeline integration wires this properly. + let mut a = false; + let mut b = false; + for h in self.0.active_bodies() { + if h == h1.0 { + a = true; + } + if h == h2.0 { + b = true; + } + if a && b { + break; + } + } + a && b + } + + /// Position of ``h`` inside the active set. + /// + /// Reserved for future use; currently always returns ``0``. + #[allow(unused_variables)] + fn active_set_offset(&self, h: &RigidBodyHandle) -> usize { + // Not exposed publicly in the engine; a later revision may surface it. + 0 + } +} + +/// Iterator yielding ``RigidBodyHandle`` values. +/// +/// Produced by ``IslandManager.__iter__`` and ``RigidBodySet.handles``; +/// not intended to be constructed directly. +#[pyclass] +pub struct RigidBodyHandleIter { + handles: Vec, + i: usize, +} + +#[pymethods] +impl RigidBodyHandleIter { + /// Return ``self`` so the object satisfies the iterator protocol. + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Return the next handle, or raise ``StopIteration``. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.handles.len() { + return None; + } + let v = slf.handles[slf.i]; + slf.i += 1; + Some(v) + } +} + +// ============================================================ +// CCDSolver — constructor + clear; solve_ccd handled by the pipeline. +// ============================================================ + +/// Continuous Collision Detection (CCD) solver. +/// +/// CCD prevents fast-moving bodies from tunneling through thin +/// obstacles by performing sub-step time-of-impact queries. Most users +/// should rely on ``PhysicsWorld.step``, which manages a ``CCDSolver`` +/// internally; this class exists mainly to mirror the engine's +/// structure. +#[pyclass(name = "CCDSolver", module = "rapier", unsendable)] +pub struct CCDSolver(pub rapier::dynamics::CCDSolver); + +#[pymethods] +impl CCDSolver { + /// Build a fresh CCD solver. + #[new] + fn new() -> Self { + Self(rapier::dynamics::CCDSolver::new()) + } + /// Clear any per-step CCD state. + fn clear(&mut self) { + // Stateless w.r.t. the public API; may be revisited. + } + /// Run a CCD pass. *Not exposed directly* — CCD is driven by + /// ``PhysicsWorld.step``. + /// + /// :raises NotImplementedError: always; use ``PhysicsWorld.step`` + /// which handles CCD internally. + #[allow(unused_variables)] + #[pyo3(signature = (*args, **kwargs))] + fn solve_ccd( + &self, + args: &Bound<'_, crate::pyo3::types::PyTuple>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "CCDSolver.solve_ccd is wired in the pipeline. \ + Call PhysicsWorld.step instead.", + )) + } +} + +// ============================================================ +// FrictionModel (3D only) +// ============================================================ + +/// Friction model used by the 3D contact solver. +/// +/// - ``COEFFICIENT`` — simplified pyramidal friction. Faster and +/// numerically friendly; the default. +/// - ``COULOMB`` — circular Coulomb friction cone. More physically +/// correct, slightly more expensive. +#[pyclass(name = "FrictionModel", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrictionModel { + /// Cheap pyramidal approximation of the friction cone. + COEFFICIENT, + /// True circular Coulomb friction cone. + COULOMB, +} + +impl FrictionModel { + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::FrictionModel { + match self { + Self::COEFFICIENT => rapier::dynamics::FrictionModel::Simplified, + Self::COULOMB => rapier::dynamics::FrictionModel::Coulomb, + } + } + #[inline] + pub(crate) fn from_rapier(r: rapier::dynamics::FrictionModel) -> Self { + match r { + rapier::dynamics::FrictionModel::Simplified => Self::COEFFICIENT, + rapier::dynamics::FrictionModel::Coulomb => Self::COULOMB, + } + } +} + +// ============================================================ +// MassProperties (3D) +// ============================================================ + +/// Mass, center of mass and inertia tensor of a rigid body (3D). +/// +/// In 3D the inertia tensor is stored in its diagonal (principal) +/// form together with the rotation ``principal_inertia_local_frame`` +/// aligning the principal axes with the body's local frame. +/// +/// Use one of the ``from_*`` factories to derive mass properties +/// from a shape and a density, or construct directly with +/// ``MassProperties(local_com, mass, principal_inertia, frame)``. +/// Mass properties of compound bodies can be combined with ``+``; +/// subtracting (``-``) inverts the operation. +#[pyclass(name = "MassProperties", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct MassProperties(pub rapier::dynamics::MassProperties); + +#[pymethods] +impl MassProperties { + /// Build mass properties from explicit values. + /// + /// :param local_com: center of mass in body-local space (default + /// origin). + /// :param mass: total mass (default ``0``). + /// :param principal_inertia: diagonal of the principal inertia + /// tensor (default zero). + /// :param principal_inertia_local_frame: rotation aligning the + /// principal axes with the body frame (default identity). + #[new] + #[pyo3(signature = (local_com=None, mass=0.0 as Real, principal_inertia=None, principal_inertia_local_frame=None))] + fn new( + local_com: Option, + mass: Real, + principal_inertia: Option, + principal_inertia_local_frame: Option, + ) -> Self { + let com = local_com + .map(|p| p.0.coords) + .unwrap_or_else(crate::na::Vector3::zeros); + let inertia = principal_inertia + .map(|v| v.0) + .unwrap_or_else(crate::na::Vector3::zeros); + let frame = principal_inertia_local_frame + .map(|r| r.0) + .unwrap_or_else(crate::na::UnitQuaternion::identity); + Self( + rapier::dynamics::MassProperties::with_principal_inertia_frame( + com.into(), + mass, + inertia.into(), + frame.into(), + ), + ) + } + + /// Return the zero mass properties (mass = inertia = ``0``). + #[staticmethod] + fn zero() -> Self { + Self(rapier::dynamics::MassProperties::default()) + } + + /// Mass properties of a uniform-density ball. + /// + /// :param density: mass per unit volume. + /// :param radius: ball radius. + #[staticmethod] + fn from_ball(density: Real, radius: Real) -> Self { + Self(rapier::dynamics::MassProperties::from_ball(density, radius)) + } + + /// Mass properties of a uniform-density axis-aligned cuboid. + /// + /// :param density: mass per unit volume. + /// :param half_extents: half-extents along ``x``, ``y``, ``z``. + #[staticmethod] + fn from_cuboid(density: Real, half_extents: PyVector) -> Self { + Self(rapier::dynamics::MassProperties::from_cuboid( + density, + half_extents.0.into(), + )) + } + + /// Mass properties of a uniform-density capsule between two + /// endpoints. + /// + /// :param density: mass per unit volume. + /// :param a: capsule segment start (body-local). + /// :param b: capsule segment end (body-local). + /// :param radius: capsule radius. + #[staticmethod] + fn from_capsule(density: Real, a: PyPoint, b: PyPoint, radius: Real) -> Self { + Self(rapier::dynamics::MassProperties::from_capsule( + density, + a.0.into(), + b.0.into(), + radius, + )) + } + + /// Mass properties of a uniform-density cylinder along the local + /// ``y`` axis. + /// + /// :param density: mass per unit volume. + /// :param half_height: half the cylinder height. + /// :param radius: cylinder radius. + #[staticmethod] + fn from_cylinder(density: Real, half_height: Real, radius: Real) -> Self { + Self(rapier::dynamics::MassProperties::from_cylinder( + density, + half_height, + radius, + )) + } + + /// Mass properties of a uniform-density cone along the local + /// ``y`` axis. + /// + /// :param density: mass per unit volume. + /// :param half_height: half the cone height. + /// :param radius: base radius. + #[staticmethod] + fn from_cone(density: Real, half_height: Real, radius: Real) -> Self { + Self(rapier::dynamics::MassProperties::from_cone( + density, + half_height, + radius, + )) + } + + /// Total mass (read-only). + #[getter] + fn mass(&self) -> Real { + self.0.mass() + } + /// Inverse mass — ``0`` for an infinite-mass body (read-only). + #[getter] + fn inv_mass(&self) -> Real { + self.0.inv_mass + } + + /// Center of mass expressed in body-local coordinates (read-only). + #[getter] + fn local_com(&self) -> Point3 { + let v: crate::na::Vector3 = self.0.local_com.into(); + Point3(crate::na::Point3::from(v)) + } + + /// Principal inertia (diagonal of the inertia tensor in the + /// principal frame) (read-only). + #[getter] + fn principal_inertia(&self) -> Vec3 { + let v: crate::na::Vector3 = self.0.principal_inertia().into(); + Vec3(v) + } + + /// Componentwise square root of the inverse principal inertia + /// (read-only). Used by the solver to scale angular impulses. + #[getter] + fn inv_principal_inertia_sqrt(&self) -> Vec3 { + let v = self.0.inv_principal_inertia; + Vec3(crate::na::Vector3::new(v.x.sqrt(), v.y.sqrt(), v.z.sqrt())) + } + + /// Rotation aligning the principal axes with the body frame + /// (read-only). + #[getter] + fn principal_inertia_local_frame(&self) -> Rotation3 { + let q: crate::na::UnitQuaternion = self.0.principal_inertia_local_frame.into(); + Rotation3(q) + } + + /// Return a copy transformed by ``iso``. + /// + /// Useful when expressing one body's mass properties in another + /// frame, e.g. when attaching as a child. + fn transform_by(&self, iso: PyIsometry) -> Self { + let pose: rapier::math::Pose = iso.0.into(); + Self(self.0.transform_by(&pose)) + } + + /// Sum two mass distributions (parallel-axis theorem). + fn __add__(&self, other: &MassProperties) -> Self { + Self(self.0 + other.0) + } + /// Subtract ``other`` from ``self``; inverse of ``__add__``. + fn __sub__(&self, other: &MassProperties) -> Self { + Self(self.0 - other.0) + } + + fn __repr__(&self) -> String { + format!( + "MassProperties(mass={}, local_com=({}, {}, {}))", + self.0.mass(), + self.0.local_com.x, + self.0.local_com.y, + self.0.local_com.z, + ) + } +} + +// ============================================================ +// RigidBodyAdditionalMassProps (3D) +// ============================================================ + +/// Extra mass / mass properties to add to a body on top of what its +/// colliders contribute. +/// +/// Useful when you want a body's collider geometry to provide most of +/// the inertia but still bias the total mass. +#[pyclass(name = "RigidBodyAdditionalMassProps", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyAdditionalMassProps(pub rapier::dynamics::RigidBodyAdditionalMassProps); + +#[pymethods] +impl RigidBodyAdditionalMassProps { + /// Add ``mass`` to the body without changing its center of mass + /// or inertia tensor. + #[staticmethod] + fn from_mass(mass: Real) -> Self { + Self(rapier::dynamics::RigidBodyAdditionalMassProps::Mass(mass)) + } + /// Add the given full mass properties to the body. + #[staticmethod] + fn from_mass_properties(mp: &MassProperties) -> Self { + Self(rapier::dynamics::RigidBodyAdditionalMassProps::MassProps( + mp.0, + )) + } +} + +// ============================================================ +// RigidBody component views (3D) +// ============================================================ + +/// Linear and angular damping coefficients (per-second decay rates) +/// applied to a body's velocity each step. +/// +/// Damping bleeds energy away independent of contacts; useful for +/// stabilizing simulations and modeling drag. +#[pyclass(name = "RigidBodyDamping", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyDamping { + /// Linear-velocity damping coefficient (read+write). + #[pyo3(get, set)] + pub linear_damping: Real, + /// Angular-velocity damping coefficient (read+write). + #[pyo3(get, set)] + pub angular_damping: Real, +} +#[pymethods] +impl RigidBodyDamping { + /// Build damping coefficients (default zero / no damping). + #[new] + #[pyo3(signature = (linear_damping=0.0 as Real, angular_damping=0.0 as Real))] + fn new(linear_damping: Real, angular_damping: Real) -> Self { + Self { + linear_damping, + angular_damping, + } + } +} + +/// Dominance group used to bias contact resolution between bodies. +/// +/// In a contact between bodies of different dominance, the +/// higher-dominance body behaves as if it had infinite mass with +/// respect to the lower-dominance one. Groups range in ``[-127, 127]``. +#[pyclass(name = "RigidBodyDominance", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyDominance { + /// Dominance group identifier (read+write). + #[pyo3(get, set)] + pub group: i8, +} +#[pymethods] +impl RigidBodyDominance { + /// Build a dominance descriptor (default group ``0``). + #[new] + #[pyo3(signature = (group=0))] + fn new(group: i8) -> Self { + Self { group } + } +} + +/// Per-body CCD configuration. +/// +/// ``ccd_enabled`` toggles substep CCD; ``soft_ccd_prediction`` is a +/// distance over which "soft" CCD pushes the body back from imminent +/// penetrations (``0`` disables soft CCD). +#[pyclass(name = "RigidBodyCcd", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyCcd { + /// Whether substep CCD is enabled for this body (read+write). + #[pyo3(get, set)] + pub ccd_enabled: bool, + /// Soft-CCD prediction distance (read+write); ``0`` disables it. + #[pyo3(get, set)] + pub soft_ccd_prediction: Real, +} +#[pymethods] +impl RigidBodyCcd { + /// Build a CCD descriptor (defaults: disabled, ``0`` soft CCD). + #[new] + #[pyo3(signature = (ccd_enabled=false, soft_ccd_prediction=0.0 as Real))] + fn new(ccd_enabled: bool, soft_ccd_prediction: Real) -> Self { + Self { + ccd_enabled, + soft_ccd_prediction, + } + } +} + +/// Linear and angular velocity of a rigid body (3D). +/// +/// In 3D both ``linvel`` and ``angvel`` are 3-component vectors. The +/// angular velocity is expressed in world space, with magnitude in +/// radians per second. +#[pyclass(name = "RigidBodyVelocity", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyVelocity { + /// Linear velocity in world space (read+write). + #[pyo3(get, set)] + pub linvel: Vec3, + /// Angular velocity in world space, rad/s (read+write). + #[pyo3(get, set)] + pub angvel: Vec3, +} +#[pymethods] +impl RigidBodyVelocity { + /// Build a velocity (default ``linvel = angvel = 0``). + #[new] + #[pyo3(signature = (linvel=None, angvel=None))] + fn new(linvel: Option, angvel: Option) -> Self { + let lin = linvel + .map(|v| v.0) + .unwrap_or_else(crate::na::Vector3::zeros); + let ang = angvel + .map(|v| v.0) + .unwrap_or_else(crate::na::Vector3::zeros); + Self { + linvel: Vec3(lin), + angvel: Vec3(ang), + } + } +} + +/// Accumulated external forces, torques and gravity scaling. +/// +/// ``force`` and ``torque`` accumulate over a single step and are +/// cleared automatically before the next ``step()`` (unlike impulses, +/// which act once instantaneously). +#[pyclass(name = "RigidBodyForces", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyForces { + /// Accumulated linear force, world space (read+write). + #[pyo3(get, set)] + pub force: Vec3, + /// Accumulated torque, world space (read+write). + #[pyo3(get, set)] + pub torque: Vec3, + /// Per-body multiplier on the world gravity (read+write). + #[pyo3(get, set)] + pub gravity_scale: Real, +} + +/// Body-local mass properties view. +/// +/// Mirror of the ``MassProperties`` stored on a ``RigidBody``. +#[pyclass(name = "RigidBodyMassProps", module = "rapier")] +#[derive(Debug, Clone)] +pub struct RigidBodyMassProps { + /// Mass properties expressed in body-local space (read-only). + #[pyo3(get)] + pub local_mprops: MassProperties, +} + +/// Current and predicted pose of a rigid body. +/// +/// ``position`` is the pose used for queries; ``next_position`` is the +/// pose that will be committed after integration. +#[pyclass(name = "RigidBodyPosition", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RigidBodyPosition { + /// Current world-space pose (read+write). + #[pyo3(get, set)] + pub position: Isometry3, + /// Predicted world-space pose after the next step (read+write). + #[pyo3(get, set)] + pub next_position: Isometry3, +} + +// ============================================================ +// LockedAxes (3D) +// ============================================================ + +/// Bitflags selecting which translation / rotation degrees of freedom +/// of a rigid body are locked (3D). +/// +/// Combine flags with ``|`` (union), ``&`` (intersection), ``^`` (xor) +/// and ``-`` (set difference). ``in`` / ``contains`` test subset +/// membership. +/// +/// Example:: +/// +/// # Lock translation along Y and all rotations: +/// axes = LockedAxes.TRANSLATION_LOCKED_Y | LockedAxes.ROTATION_LOCKED +#[pyclass(name = "LockedAxes", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LockedAxes(pub rapier::dynamics::LockedAxes); + +#[pymethods] +impl LockedAxes { + /// Build a ``LockedAxes`` value from a raw bitmask. + /// + /// :param bits: bitmask combining the ``LockedAxes`` class + /// attributes (default ``0`` = no axes locked). + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u8) -> Self { + Self(rapier::dynamics::LockedAxes::from_bits_truncate(bits)) + } + /// Return the empty flag set (no axes locked). + #[staticmethod] + fn empty() -> Self { + Self(rapier::dynamics::LockedAxes::empty()) + } + /// Return the flag set locking every translation and rotation + /// axis. + #[staticmethod] + fn all_axes() -> Self { + Self(rapier::dynamics::LockedAxes::all()) + } + + /// Lock translation along the local ``x`` axis. + #[classattr] + const TRANSLATION_LOCKED_X: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X); + /// Lock translation along the local ``y`` axis. + #[classattr] + const TRANSLATION_LOCKED_Y: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y); + /// Lock translation along the local ``z`` axis. + #[classattr] + const TRANSLATION_LOCKED_Z: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Z); + /// Lock all three translation axes. + #[classattr] + const TRANSLATION_LOCKED: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED); + /// Lock rotation around the local ``x`` axis. + #[classattr] + const ROTATION_LOCKED_X: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_X); + /// Lock rotation around the local ``y`` axis. + #[classattr] + const ROTATION_LOCKED_Y: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_Y); + /// Lock rotation around the local ``z`` axis. + #[classattr] + const ROTATION_LOCKED_Z: LockedAxes = + LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_Z); + /// Lock all three rotation axes. + #[classattr] + const ROTATION_LOCKED: LockedAxes = LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED); + /// Lock every translation and rotation axis. + #[classattr] + const ALL: LockedAxes = LockedAxes(rapier::dynamics::LockedAxes::all()); + + /// Raw bitmask backing this flag set (read-only). + #[getter] + fn bits(&self) -> u8 { + self.0.bits() + } + /// Whether ``self`` is a superset of ``other``. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Whether no axes are locked. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// ``other in self`` — subset test (alias for ``contains``). + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Union of two flag sets (``self | other``). + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Intersection of two flag sets (``self & other``). + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Symmetric difference (``self ^ other``). + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Complement of ``self`` within the full set of axes. + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// Set difference (``self - other``): axes in ``self`` not in + /// ``other``. + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + /// Truthiness: ``True`` if any axis is locked. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality comparison. Only ``==`` and ``!=`` are supported. + /// + /// :raises TypeError: when an ordering comparison is requested. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("LockedAxes supports only == and !=")), + } + } + /// Hash equal to the underlying bitmask. + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + fn __repr__(&self) -> String { + format!("LockedAxes(bits={:#010b})", self.0.bits()) + } +} + +// ============================================================ +// IntegrationParameters (3D — has friction_model) +// ============================================================ + +/// Tunable parameters for the time-step and contact/joint solver. +/// +/// The defaults (``IntegrationParameters()``) are appropriate for +/// most games and robotics simulations at ``dt ≈ 1/60`` seconds. +/// +/// Many fields are stored in *normalized* units of ``length_unit``: +/// e.g. ``normalized_allowed_linear_error`` is in fractions of +/// ``length_unit``. Use the helper methods ``allowed_linear_error()``, +/// ``max_corrective_velocity()`` and ``prediction_distance()`` to get +/// the de-normalized values. +#[pyclass(name = "IntegrationParameters", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct IntegrationParameters(pub rapier::dynamics::IntegrationParameters); + +#[pymethods] +impl IntegrationParameters { + /// Build the default integration parameters. + #[new] + fn new() -> Self { + Self(rapier::dynamics::IntegrationParameters::default()) + } + + /// Return the default integration parameters. + #[staticmethod] + fn default_params() -> Self { + Self(rapier::dynamics::IntegrationParameters::default()) + } + + /// Simulation timestep in seconds (default ``≈ 1/60``). + #[getter] + fn dt(&self) -> Real { + self.0.dt + } + /// Set the simulation timestep in seconds. + #[setter] + fn set_dt(&mut self, v: Real) { + self.0.dt = v; + } + /// Minimum substep length used by CCD, in seconds. + #[getter] + fn min_ccd_dt(&self) -> Real { + self.0.min_ccd_dt + } + /// Set the minimum CCD substep length. + #[setter] + fn set_min_ccd_dt(&mut self, v: Real) { + self.0.min_ccd_dt = v; + } + /// Warmstart coefficient in ``[0, 1]`` for the solver's impulse + /// reuse between steps; higher values converge faster but may + /// jitter. + #[getter] + fn warmstart_coefficient(&self) -> Real { + self.0.warmstart_coefficient + } + /// Set the warmstart coefficient. + #[setter] + fn set_warmstart_coefficient(&mut self, v: Real) { + self.0.warmstart_coefficient = v; + } + /// Reference length unit (1 meter by default). Other "normalized" + /// parameters are expressed in fractions of this unit. + #[getter] + fn length_unit(&self) -> Real { + self.0.length_unit + } + /// Set the reference length unit. + #[setter] + fn set_length_unit(&mut self, v: Real) { + self.0.length_unit = v; + } + /// Allowed penetration error, in fractions of ``length_unit``. + #[getter] + fn normalized_allowed_linear_error(&self) -> Real { + self.0.normalized_allowed_linear_error + } + /// Set the normalized allowed linear error. + #[setter] + fn set_normalized_allowed_linear_error(&mut self, v: Real) { + self.0.normalized_allowed_linear_error = v; + } + /// Maximum penetration-correction velocity, in fractions of + /// ``length_unit`` per second. + #[getter] + fn normalized_max_corrective_velocity(&self) -> Real { + self.0.normalized_max_corrective_velocity + } + /// Set the normalized maximum corrective velocity. + #[setter] + fn set_normalized_max_corrective_velocity(&mut self, v: Real) { + self.0.normalized_max_corrective_velocity = v; + } + /// Contact prediction distance, in fractions of ``length_unit``. + #[getter] + fn normalized_prediction_distance(&self) -> Real { + self.0.normalized_prediction_distance + } + /// Set the normalized prediction distance. + #[setter] + fn set_normalized_prediction_distance(&mut self, v: Real) { + self.0.normalized_prediction_distance = v; + } + /// Outer (TGS) solver iterations per step. + #[getter] + fn num_solver_iterations(&self) -> usize { + self.0.num_solver_iterations + } + /// Set the outer solver iteration count. + #[setter] + fn set_num_solver_iterations(&mut self, v: usize) { + self.0.num_solver_iterations = v; + } + /// Inner PGS iterations per outer solver iteration. + #[getter] + fn num_internal_pgs_iterations(&self) -> usize { + self.0.num_internal_pgs_iterations + } + /// Set the inner PGS iteration count. + #[setter] + fn set_num_internal_pgs_iterations(&mut self, v: usize) { + self.0.num_internal_pgs_iterations = v; + } + /// Inner stabilization iterations per outer solver iteration. + #[getter] + fn num_internal_stabilization_iterations(&self) -> usize { + self.0.num_internal_stabilization_iterations + } + /// Set the inner stabilization iteration count. + #[setter] + fn set_num_internal_stabilization_iterations(&mut self, v: usize) { + self.0.num_internal_stabilization_iterations = v; + } + /// Minimum number of bodies in an island before it is solved in + /// parallel. + #[getter] + fn min_island_size(&self) -> usize { + self.0.min_island_size + } + /// Set the minimum parallel-island size. + #[setter] + fn set_min_island_size(&mut self, v: usize) { + self.0.min_island_size = v; + } + /// Maximum number of CCD substeps per simulation step. + #[getter] + fn max_ccd_substeps(&self) -> usize { + self.0.max_ccd_substeps + } + /// Set the maximum CCD substep count. + #[setter] + fn set_max_ccd_substeps(&mut self, v: usize) { + self.0.max_ccd_substeps = v; + } + + /// Spring coefficients used to regularize contacts. + #[getter] + fn contact_softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.contact_softness) + } + /// Set the contact-spring coefficients. + #[setter] + fn set_contact_softness(&mut self, v: SpringCoefficients) { + self.0.contact_softness = v.0; + } + + /// 3D friction model used by the contact solver. + #[getter] + fn friction_model(&self) -> FrictionModel { + FrictionModel::from_rapier(self.0.friction_model) + } + /// Set the 3D friction model. + #[setter] + fn set_friction_model(&mut self, v: FrictionModel) { + self.0.friction_model = v.to_rapier(); + } + + /// Reciprocal of ``dt`` (``1 / dt``). + fn inv_dt(&self) -> Real { + self.0.inv_dt() + } + /// Allowed penetration error in world units (denormalized). + fn allowed_linear_error(&self) -> Real { + self.0.allowed_linear_error() + } + /// Maximum penetration-correction velocity in world units + /// (denormalized). + fn max_corrective_velocity(&self) -> Real { + self.0.max_corrective_velocity() + } + /// Contact prediction distance in world units (denormalized). + fn prediction_distance(&self) -> Real { + self.0.prediction_distance() + } + + /// Stiffness component of the contact regularization spring. + fn contact_spring_stiffness(&self) -> Real { + self.0.contact_softness.natural_frequency + } + /// Damping component of the contact regularization spring. + fn contact_spring_damping(&self) -> Real { + self.0.contact_softness.damping_ratio + } + /// Default joint-spring stiffness (engine constant). + fn joint_spring_stiffness(&self) -> Real { + rapier::dynamics::SpringCoefficients::::joint_defaults().natural_frequency + } + /// Default joint-spring damping (engine constant). + fn joint_spring_damping(&self) -> Real { + rapier::dynamics::SpringCoefficients::::joint_defaults().damping_ratio + } + + /// Equality comparison. Only ``==`` and ``!=`` are supported. + /// + /// :raises TypeError: when an ordering comparison is requested. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "IntegrationParameters supports only == and !=", + )), + } + } + fn __repr__(&self) -> String { + format!( + "IntegrationParameters(dt={}, num_solver_iterations={})", + self.0.dt, self.0.num_solver_iterations + ) + } +} + +// ============================================================ +// RigidBody — owned standalone (NOT a view, holds a full body). +// ============================================================ +// When inserted into a RigidBodySet, the set takes ownership and +// we return handles. When obtained via `set.get(handle).clone()` +// we hand back a fresh RigidBody that mirrors the stored one. + +/// A simulated rigid body. +/// +/// Holds pose, velocity, accumulated forces, mass properties and +/// solver flags. To create one, use a factory on this class to obtain +/// a ``RigidBodyBuilder``, configure it and ``build()`` (or insert +/// the builder directly into a ``RigidBodySet``):: +/// +/// rb = RigidBody.dynamic(translation=Vec3(0, 5, 0)).build() +/// handle = bodies.insert(rb) +/// +/// A body obtained from a ``RigidBodySet`` (e.g. ``world.rigid_bodies[h]``) +/// is a lightweight **view** into the stored body: reads and writes go +/// straight through to the set with no copy, so ``body.linvel = v`` +/// persists immediately. A body created via ``build()`` (not yet in a +/// set) owns its data until inserted. +#[pyclass(name = "RigidBody", module = "rapier")] +#[derive(Debug)] +pub struct RigidBody { + pub backing: RigidBodyBacking, +} + +/// Storage backing a `RigidBody`: either a standalone owned body +/// (pre-insertion) or a handle-backed view into a `RigidBodySet`. +#[derive(Debug)] +pub enum RigidBodyBacking { + Owned(Box), + InSet { + set: Py, + handle: rapier::dynamics::RigidBodyHandle, + }, +} + +// `Py` is not `derive`-Clone (cloning bumps the refcount under the +// GIL), so implement Clone by hand. +impl Clone for RigidBodyBacking { + fn clone(&self) -> Self { + match self { + RigidBodyBacking::Owned(b) => RigidBodyBacking::Owned(b.clone()), + RigidBodyBacking::InSet { set, handle } => { + Python::with_gil(|py| RigidBodyBacking::InSet { + set: set.clone_ref(py), + handle: *handle, + }) + } + } + } +} +impl Clone for RigidBody { + fn clone(&self) -> Self { + RigidBody { + backing: self.backing.clone(), + } + } +} + +impl RigidBody { + pub(crate) fn new_owned(body: rapier::dynamics::RigidBody) -> Self { + RigidBody { + backing: RigidBodyBacking::Owned(Box::new(body)), + } + } + + /// Run `f` with a shared reference to the underlying body. For an + /// `InSet` view this briefly borrows the set; a stale handle (body + /// already removed) panics, surfacing as a Python exception. + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::RigidBody) -> R) -> R { + match &self.backing { + RigidBodyBacking::Owned(b) => f(b), + RigidBodyBacking::InSet { set, handle } => Python::with_gil(|py| { + let set = set.bind(py).borrow(); + let body = set + .0 + .get(*handle) + .expect("RigidBody refers to a body that was removed from its set"); + f(body) + }), + } + } + + /// Run `f` with a mutable reference to the underlying body, writing + /// straight through to the set for an `InSet` view. + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::RigidBody) -> R) -> R { + match &mut self.backing { + RigidBodyBacking::Owned(b) => f(b), + RigidBodyBacking::InSet { set, handle } => Python::with_gil(|py| { + let mut set = set.bind(py).borrow_mut(); + let body = set + .0 + .get_mut(*handle) + .expect("RigidBody refers to a body that was removed from its set"); + f(body) + }), + } + } + + /// Clone the underlying body out (used by `insert` and by callers + /// that need an owned `&rapier::RigidBody`). + pub fn to_owned_body(&self) -> rapier::dynamics::RigidBody { + self.with_ref(|b| b.clone()) + } +} + +#[pymethods] +impl RigidBody { + // No `__new__`: construction goes through builders. + + /// World-space pose of the body (read+write). + #[getter] + fn position(&self) -> Isometry3 { + self.with_ref(|b| { + let r = b.position(); + let na_iso: crate::na::Isometry = (*r).into(); + Isometry3(na_iso) + }) + } + /// Teleport the body to ``p`` (wakes it). Use sparingly on + /// dynamic bodies as this bypasses the integrator. + #[setter] + fn set_position(&mut self, p: PyIsometry) { + let g: rapier::math::Pose = p.0.into(); + self.with_mut(|b| b.set_position(g, true)); + } + + /// Translation portion of the pose (read+write). + #[getter] + fn translation(&self) -> Vec3 { + self.with_ref(|b| { + let v: crate::na::SVector = b.translation().into(); + Vec3(v) + }) + } + /// Set the translation, leaving the rotation unchanged. + #[setter] + fn set_translation(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.with_mut(|b| b.set_translation(g, true)); + } + + /// Rotation portion of the pose (read+write). + #[getter] + fn rotation(&self) -> Rotation3 { + self.with_ref(|b| { + let r = *b.rotation(); + Rotation3(r.into()) + }) + } + /// Set the rotation, leaving the translation unchanged. + #[setter] + fn set_rotation(&mut self, r: PyRotation) { + let g: rapier::math::Rotation = r.0.into(); + self.with_mut(|b| b.set_rotation(g, true)); + } + + /// Linear velocity in world space (read+write). + #[getter] + fn linvel(&self) -> Vec3 { + self.with_ref(|b| { + let v: crate::na::SVector = b.linvel().into(); + Vec3(v) + }) + } + /// Set the linear velocity (wakes the body). + #[setter] + fn set_linvel(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.with_mut(|b| b.set_linvel(g, true)); + } + + /// Total mass of the body (colliders + additional) (read-only). + #[getter] + fn mass(&self) -> Real { + self.with_ref(|b| b.mass()) + } + /// Inverse mass — ``0`` for fixed or infinite-mass bodies + /// (read-only). + #[getter] + fn inv_mass(&self) -> Real { + self.with_ref(|b| b.mass_properties().local_mprops.inv_mass) + } + + /// Center of mass in world space (read-only). + #[getter] + fn center_of_mass(&self) -> Point3 { + let v: crate::na::SVector = self.with_ref(|b| b.center_of_mass()).into(); + Point3(crate::na::Point::from(v)) + } + + /// Center of mass in body-local space (read-only). + #[getter] + fn local_center_of_mass(&self) -> Point3 { + let v: crate::na::SVector = self.with_ref(|b| b.local_center_of_mass()).into(); + Point3(crate::na::Point::from(v)) + } + + /// Body-local mass properties (read-only). + /// + /// To change them, use ``set_additional_mass``, + /// ``set_additional_mass_properties`` or + /// ``recompute_mass_properties_from_colliders``. + #[getter] + fn mass_properties(&self) -> MassProperties { + self.with_ref(|b| MassProperties(b.mass_properties().local_mprops)) + } + + /// Behavior class of the body (dynamic/fixed/kinematic) + /// (read+write). + #[getter] + fn body_type(&self) -> RigidBodyType { + RigidBodyType::from_rapier(self.with_ref(|b| b.body_type())) + } + /// Change the body's behavior class. + #[setter] + fn set_body_type(&mut self, t: RigidBodyType) { + let rt = t.to_rapier(); + self.with_mut(|b| b.set_body_type(rt, true)); + } + + /// Multiplier applied to the world gravity for this body + /// (read+write). ``0`` disables gravity for the body. + #[getter] + fn gravity_scale(&self) -> Real { + self.with_ref(|b| b.gravity_scale()) + } + /// Set the gravity scale. + #[setter] + fn set_gravity_scale(&mut self, v: Real) { + self.with_mut(|b| b.set_gravity_scale(v, true)); + } + + /// Per-second linear-velocity damping coefficient (read+write). + #[getter] + fn linear_damping(&self) -> Real { + self.with_ref(|b| b.linear_damping()) + } + /// Set the linear damping coefficient. + #[setter] + fn set_linear_damping(&mut self, v: Real) { + self.with_mut(|b| b.set_linear_damping(v)); + } + + /// Per-second angular-velocity damping coefficient (read+write). + #[getter] + fn angular_damping(&self) -> Real { + self.with_ref(|b| b.angular_damping()) + } + /// Set the angular damping coefficient. + #[setter] + fn set_angular_damping(&mut self, v: Real) { + self.with_mut(|b| b.set_angular_damping(v)); + } + + /// Dominance group of the body (read+write); see + /// ``RigidBodyDominance``. + #[getter] + fn dominance_group(&self) -> i8 { + self.with_ref(|b| b.dominance_group()) + } + /// Set the dominance group (``[-127, 127]``). + #[setter] + fn set_dominance_group(&mut self, v: i8) { + self.with_mut(|b| b.set_dominance_group(v)); + } + + /// Extra solver iterations spent on this body's island + /// (read+write). ``0`` keeps the global default. + #[getter] + fn additional_solver_iterations(&self) -> usize { + self.with_ref(|b| b.additional_solver_iterations()) + } + /// Set the number of additional solver iterations. + #[setter] + fn set_additional_solver_iterations(&mut self, v: usize) { + self.with_mut(|b| b.set_additional_solver_iterations(v)); + } + + /// Currently locked degrees of freedom (read+write). + #[getter] + fn locked_axes(&self) -> LockedAxes { + LockedAxes(self.with_ref(|b| b.locked_axes())) + } + /// Replace the locked axes flag set (wakes the body). + #[setter] + fn set_locked_axes(&mut self, v: LockedAxes) { + self.with_mut(|b| b.set_locked_axes(v.0, true)); + } + + /// Whether the body is currently sleeping (read-only). + #[getter] + fn is_sleeping(&self) -> bool { + self.with_ref(|b| b.is_sleeping()) + } + /// Whether the body is currently moving (read-only). + #[getter] + fn is_moving(&self) -> bool { + self.with_ref(|b| b.is_moving()) + } + /// Whether the body is dynamic (read-only). + #[getter] + fn is_dynamic(&self) -> bool { + self.with_ref(|b| b.is_dynamic()) + } + /// Whether the body is kinematic (either variant) (read-only). + #[getter] + fn is_kinematic(&self) -> bool { + self.with_ref(|b| b.is_kinematic()) + } + /// Whether the body is fixed (read-only). + #[getter] + fn is_fixed(&self) -> bool { + self.with_ref(|b| b.is_fixed()) + } + + /// Whether the body participates in simulation (read+write). + /// + /// A disabled body is fully skipped by the solver and queries. + #[getter] + fn is_enabled(&self) -> bool { + self.with_ref(|b| b.is_enabled()) + } + /// Enable or disable the body. + #[setter] + fn set_is_enabled(&mut self, v: bool) { + self.with_mut(|b| b.set_enabled(v)); + } + + /// Whether substep CCD is enabled for this body (read+write). + #[getter] + fn ccd_enabled(&self) -> bool { + self.with_ref(|b| b.is_ccd_enabled()) + } + /// Enable or disable substep CCD for this body. + #[setter] + fn set_ccd_enabled(&mut self, v: bool) { + self.with_mut(|b| b.enable_ccd(v)); + } + + /// Soft-CCD prediction distance (read+write); ``0`` disables it. + #[getter] + fn soft_ccd_prediction(&self) -> Real { + self.with_ref(|b| b.soft_ccd_prediction()) + } + /// Set the soft-CCD prediction distance. + #[setter] + fn set_soft_ccd_prediction(&mut self, v: Real) { + self.with_mut(|b| b.set_soft_ccd_prediction(v)); + } + + /// Application-defined ``u128`` payload attached to this body + /// (read+write). + #[getter] + fn user_data(&self) -> u128 { + self.with_ref(|b| b.user_data) + } + /// Set the application-defined payload. + #[setter] + fn set_user_data(&mut self, v: u128) { + self.with_mut(|b| b.user_data = v); + } + + /// Force accumulated by user calls during the current step + /// (read-only). Cleared automatically before the next step. + #[getter] + fn user_force(&self) -> Vec3 { + let v: crate::na::SVector = self.with_ref(|b| b.user_force()).into(); + Vec3(v) + } + + /// Handles of all colliders attached to this body (read-only). + #[getter] + fn colliders(&self) -> Vec { + self.with_ref(|b| b.colliders().iter().copied().map(ColliderHandle).collect()) + } + + /// Activation / sleep state of this body (read+write). + #[getter] + fn activation(&self) -> RigidBodyActivation { + self.with_ref(|b| RigidBodyActivation(*b.activation())) + } + /// Replace the activation state. + #[setter] + fn set_activation(&mut self, v: RigidBodyActivation) { + self.with_mut(|b| *b.activation_mut() = v.0); + } + + /// Predicted world-space pose after the next step (read-only). + #[getter] + fn next_position(&self) -> Isometry3 { + self.with_ref(|b| { + let p: crate::na::Isometry = (*b.next_position()).into(); + Isometry3(p) + }) + } + + // ---- mutating methods ---- + + /// Accumulate a force to be applied during the next step. + /// + /// Forces accumulate over the step and are cleared automatically + /// before the next ``step()``. Use ``apply_impulse`` for an + /// instantaneous velocity change. + /// + /// :param force: force vector in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (force, wake_up=true))] + fn add_force(&mut self, force: PyVector, wake_up: bool) { + let g: rapier::math::Vector = force.0.into(); + self.with_mut(|b| b.add_force(g, wake_up)); + } + + /// Accumulate a force applied at a given world-space point. + /// + /// Equivalent to applying ``force`` at the body's center of mass + /// plus a torque ``(point - com) × force``. + /// + /// :param force: force vector in world space. + /// :param point: application point in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (force, point, wake_up=true))] + fn add_force_at_point(&mut self, force: PyVector, point: PyPoint, wake_up: bool) { + let f: rapier::math::Vector = force.0.into(); + let p: rapier::math::Vector = point.0.coords.into(); + self.with_mut(|b| b.add_force_at_point(f, p, wake_up)); + } + + /// Apply an instantaneous linear impulse (units: ``N·s``). + /// + /// :param impulse: impulse vector in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (impulse, wake_up=true))] + fn apply_impulse(&mut self, impulse: PyVector, wake_up: bool) { + let g: rapier::math::Vector = impulse.0.into(); + self.with_mut(|b| b.apply_impulse(g, wake_up)); + } + + /// Apply an instantaneous impulse at a given world-space point. + /// + /// :param impulse: impulse vector in world space. + /// :param point: application point in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (impulse, point, wake_up=true))] + fn apply_impulse_at_point(&mut self, impulse: PyVector, point: PyPoint, wake_up: bool) { + let imp: rapier::math::Vector = impulse.0.into(); + let p: rapier::math::Vector = point.0.coords.into(); + self.with_mut(|b| b.apply_impulse_at_point(imp, p, wake_up)); + } + + /// Clear any accumulated forces on this body. + /// + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (wake_up=true))] + fn reset_forces(&mut self, wake_up: bool) { + self.with_mut(|b| b.reset_forces(wake_up)); + } + /// Clear any accumulated torques on this body. + /// + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (wake_up=true))] + fn reset_torques(&mut self, wake_up: bool) { + self.with_mut(|b| b.reset_torques(wake_up)); + } + + /// Wake the body, allowing it to participate in the next step. + /// + /// :param strong: if ``True`` (default), reset the sleep timer + /// so the body stays awake longer before re-sleeping. + #[pyo3(signature = (strong=true))] + fn wake_up(&mut self, strong: bool) { + self.with_mut(|b| b.wake_up(strong)); + } + /// Put the body to sleep immediately. + fn sleep(&mut self) { + self.with_mut(|b| b.sleep()); + } + + /// World-space velocity at the given world-space point. + /// + /// Includes both translational and rotational contributions: + /// ``v_point = linvel + angvel × (point - com)``. + /// + /// :param point: world-space point. + fn velocity_at_point(&self, point: PyPoint) -> Vec3 { + let p: rapier::math::Vector = point.0.coords.into(); + let v: crate::na::SVector = self.with_ref(|b| b.velocity_at_point(p)).into(); + Vec3(v) + } + + /// Kinetic energy of the body. + /// + /// :returns: :math:`E_k = \\tfrac{1}{2} m \\|v\\|^2 + + /// \\tfrac{1}{2} \\omega^\\top I \\omega`. + fn kinetic_energy(&self) -> Real { + self.with_ref(|b| b.kinetic_energy()) + } + + /// Gravitational potential energy after integrating for ``dt`` + /// seconds under ``gravity``. + /// + /// :returns: :math:`-m\\, \\mathbf{g} \\cdot \\mathbf{p}` where + /// :math:`\\mathbf{p}` is the predicted position. + /// :param dt: integration step in seconds. + /// :param gravity: gravity vector in world space. + fn gravitational_potential_energy(&self, dt: Real, gravity: PyVector) -> Real { + let g: rapier::math::Vector = gravity.0.into(); + self.with_ref(|b| b.gravitational_potential_energy(dt, g)) + } + + /// Predict the body's pose after ``dt`` seconds using its + /// *current velocity only* (no forces, no integration). The body + /// is not modified. + /// + /// :param dt: prediction horizon in seconds. + fn predict_position_using_velocity(&self, dt: Real) -> Isometry3 { + let p: crate::na::Isometry = self + .with_ref(|b| b.predict_position_using_velocity(dt)) + .into(); + Isometry3(p) + } + + /// Predict the body's pose after ``dt`` seconds using its + /// current velocity *and* accumulated forces, without actually + /// integrating (the body is not modified). + /// + /// :param dt: prediction horizon in seconds. + fn predict_position_using_velocity_and_forces(&self, dt: Real) -> Isometry3 { + self.with_ref(|b| { + let p: crate::na::Isometry = + b.predict_position_using_velocity_and_forces(dt).into(); + Isometry3(p) + }) + } + + /// Overwrite this body's mass properties from the attached + /// colliders. + /// + /// Equivalent to summing the mass properties of every collider + /// in ``colliders`` that is attached to this body. Useful after + /// adding, removing or rescaling colliders. + /// + /// :param colliders: the ``ColliderSet`` holding this body's + /// colliders. + fn recompute_mass_properties_from_colliders(&mut self, colliders: &ColliderSet) { + self.with_mut(|b| b.recompute_mass_properties_from_colliders(&colliders.0)); + } + + /// Add ``mass`` on top of the colliders' contribution. + /// + /// :param mass: extra mass to add. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (mass, wake_up=true))] + fn set_additional_mass(&mut self, mass: Real, wake_up: bool) { + self.with_mut(|b| b.set_additional_mass(mass, wake_up)); + } + + /// Add full mass properties on top of the colliders' + /// contribution. + /// + /// :param mp: extra mass properties. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (mp, wake_up=true))] + fn set_additional_mass_properties(&mut self, mp: &MassProperties, wake_up: bool) { + self.with_mut(|b| b.set_additional_mass_properties(mp.0, wake_up)); + } + + /// Convenience for accumulating ``gravity * mass`` as a force + /// during the current step (useful when stepping with gravity + /// disabled globally). + /// + /// :param gravity: gravity acceleration vector. + fn add_gravitational_force(&mut self, gravity: PyVector) { + // Add gravity * mass as a force. + let g: rapier::math::Vector = gravity.0.into(); + let m = self.with_ref(|b| b.mass()); + self.with_mut(|b| b.add_force(g * m, true)); + } + + fn __repr__(&self) -> String { + let p = self.with_ref(|b| b.translation()); + format!( + "RigidBody(translation={:?}, type={:?})", + p, + self.with_ref(|b| b.body_type()) + ) + } +} + +// ============================================================ +// RigidBodyBuilder +// ============================================================ + +/// Fluent builder for ``RigidBody`` instances. +/// +/// Each setter returns ``self`` for chaining; call ``build()`` to +/// produce the actual body, or pass the builder directly to +/// ``RigidBodySet.insert``:: +/// +/// builder = (RigidBody.dynamic() +/// .translation(Vec3(0, 5, 0)) +/// .linvel(Vec3(1, 0, 0))) +/// handle = bodies.insert(builder) +/// +/// Most factories also accept keyword arguments matching builder +/// methods, e.g. ``RigidBody.dynamic(translation=Vec3(0, 5, 0))``. +#[pyclass(name = "RigidBodyBuilder", module = "rapier")] +#[derive(Debug, Clone)] +pub struct RigidBodyBuilder { + pub builder: rapier::dynamics::RigidBodyBuilder, +} + +impl RigidBodyBuilder { + pub fn from_kwargs( + base: rapier::dynamics::RigidBodyBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self { builder: base }; + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + me.apply_kwarg(&key, &v)?; + } + } + Ok(me) + } + + fn apply_kwarg(&mut self, key: &str, v: &Bound<'_, PyAny>) -> PyResult<()> { + match key { + "translation" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + self.builder = self.builder.clone().translation(g); + } + "linvel" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + self.builder = self.builder.clone().linvel(g); + } + "linear_damping" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().linear_damping(f); + } + "angular_damping" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().angular_damping(f); + } + "gravity_scale" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().gravity_scale(f); + } + "can_sleep" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().can_sleep(b); + } + "sleeping" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().sleeping(b); + } + "ccd_enabled" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().ccd_enabled(b); + } + "soft_ccd_prediction" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().soft_ccd_prediction(f); + } + "dominance_group" => { + let g: i8 = v.extract()?; + self.builder = self.builder.clone().dominance_group(g); + } + "additional_solver_iterations" => { + let n: usize = v.extract()?; + self.builder = self.builder.clone().additional_solver_iterations(n); + } + "user_data" => { + let d: u128 = v.extract()?; + self.builder = self.builder.clone().user_data(d); + } + "enabled" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().enabled(b); + } + "additional_mass" => { + let m: Real = v.extract()?; + self.builder = self.builder.clone().additional_mass(m); + } + "locked_axes" => { + let la: LockedAxes = v.extract()?; + self.builder = self.builder.clone().locked_axes(la.0); + } + "angvel" => { + Self::apply_angvel_kwarg(&mut self.builder, v)?; + } + "rotation" => { + Self::apply_rotation_kwarg(&mut self.builder, v)?; + } + "position" => { + let pi: PyIsometry = v.extract()?; + let g: rapier::math::Pose = pi.0.into(); + self.builder = self.builder.clone().pose(g); + } + "enabled_translations" => { + Self::apply_enabled_translations_kwarg(&mut self.builder, v)?; + } + "enabled_rotations" => { + Self::apply_enabled_rotations_kwarg(&mut self.builder, v)?; + } + "gyroscopic_forces" => { + Self::apply_gyroscopic_forces_kwarg(&mut self.builder, v)?; + } + "body_type" => { + let bt: RigidBodyType = v.extract()?; + // Re-create builder of that type, preserving fields. + // Simplest: clone and `.set_body_type` doesn't exist on + // the builder. We swap base type while keeping the rest. + // RigidBodyBuilder has no `.body_type` setter; build & + // re-create. For simplicity, just panic if + // people mix base ctor with body_type kwarg. + let _ = bt; + return Err(PyTypeError::new_err( + "Use RigidBody.new_body(body_type=...) to set body_type — \ + can't combine with the existing builder factory.", + )); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown RigidBodyBuilder kwarg: '{}'", + key + ))); + } + } + Ok(()) + } +} + +#[pymethods] +impl RigidBodyBuilder { + /// Build a fresh ``RigidBodyBuilder`` for the given body type. + /// + /// :param body_type: a ``RigidBodyType`` selecting the behavior + /// class (default ``DYNAMIC``). + #[new] + #[pyo3(signature = (body_type=RigidBodyType::DYNAMIC))] + fn new(body_type: RigidBodyType) -> Self { + Self { + builder: rapier::dynamics::RigidBodyBuilder::new(body_type.to_rapier()), + } + } + + /// Set the initial translation. + fn translation(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self { + builder: self.builder.clone().translation(g), + } + } + + /// Set the initial linear velocity. + fn linvel(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self { + builder: self.builder.clone().linvel(g), + } + } + + /// Set the linear-velocity damping coefficient (per second). + fn linear_damping(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().linear_damping(f), + } + } + /// Set the angular-velocity damping coefficient (per second). + fn angular_damping(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().angular_damping(f), + } + } + /// Set the gravity multiplier for this body (``0`` disables + /// gravity). + fn gravity_scale(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().gravity_scale(f), + } + } + /// Whether the body is allowed to fall asleep (default ``True``). + fn can_sleep(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().can_sleep(b), + } + } + /// Whether the body starts asleep. + fn sleeping(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().sleeping(b), + } + } + /// Enable substep CCD for this body. + fn ccd_enabled(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().ccd_enabled(b), + } + } + /// Set the soft-CCD prediction distance (``0`` disables it). + fn soft_ccd_prediction(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().soft_ccd_prediction(f), + } + } + /// Set the dominance group for contact biasing. + fn dominance_group(&self, g: i8) -> Self { + Self { + builder: self.builder.clone().dominance_group(g), + } + } + /// Request additional solver iterations on this body's island. + fn additional_solver_iterations(&self, n: usize) -> Self { + Self { + builder: self.builder.clone().additional_solver_iterations(n), + } + } + /// Attach an application-defined ``u128`` payload to the body. + fn user_data(&self, d: u128) -> Self { + Self { + builder: self.builder.clone().user_data(d), + } + } + /// Whether the body is enabled at creation (default ``True``). + fn enabled(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().enabled(b), + } + } + /// Add extra mass on top of the colliders' contribution. + fn additional_mass(&self, m: Real) -> Self { + Self { + builder: self.builder.clone().additional_mass(m), + } + } + /// Add extra mass properties on top of the colliders' + /// contribution. + fn additional_mass_properties(&self, mp: &MassProperties) -> Self { + Self { + builder: self.builder.clone().additional_mass_properties(mp.0), + } + } + /// Set which degrees of freedom are locked. + fn locked_axes(&self, la: &LockedAxes) -> Self { + Self { + builder: self.builder.clone().locked_axes(la.0), + } + } + /// Set the initial pose (translation and rotation). + fn position(&self, p: PyIsometry) -> Self { + let g: rapier::math::Pose = p.0.into(); + Self { + builder: self.builder.clone().pose(g), + } + } + + /// Set the initial rotation. + /// + /// In 3D, ``v`` is a scaled-axis-angle vector; in 2D, a scalar + /// angle in radians. + fn rotation(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + Self::apply_rotation_kwarg(&mut b, v)?; + Ok(Self { builder: b }) + } + + /// Set the initial angular velocity. + /// + /// In 3D, ``v`` is a vector; in 2D, a scalar. + fn angvel(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + Self::apply_angvel_kwarg(&mut b, v)?; + Ok(Self { builder: b }) + } + + /// Enable or disable individual translation axes. + /// + /// In 3D, ``v`` is a tuple ``(tx, ty, tz)``; in 2D, ``(tx, ty)``. + fn enabled_translations(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + Self::apply_enabled_translations_kwarg(&mut b, v)?; + Ok(Self { builder: b }) + } + + /// Alias for ``enabled_translations``. + fn restrict_translations(&self, v: &Bound<'_, PyAny>) -> PyResult { + self.enabled_translations(v) + } + + /// Enable or disable individual rotation axes. + /// + /// In 3D, ``v`` is a tuple ``(rx, ry, rz)``; in 2D, ``(rz,)``. + fn enabled_rotations(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + Self::apply_enabled_rotations_kwarg(&mut b, v)?; + Ok(Self { builder: b }) + } + + /// Alias for ``enabled_rotations``. + fn restrict_rotations(&self, v: &Bound<'_, PyAny>) -> PyResult { + self.enabled_rotations(v) + } + + /// Enable or disable gyroscopic-force integration (3D only). + /// + /// In 2D this builder ignores the kwarg. + fn gyroscopic_forces(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + Self::apply_gyroscopic_forces_kwarg(&mut b, v)?; + Ok(Self { builder: b }) + } + + /// Finalize the builder and return the configured ``RigidBody``. + fn build(&self) -> RigidBody { + RigidBody::new_owned(self.builder.build()) + } +} + +#[pymethods] +impl RigidBody { + // 3D-specific accessors (vector angvel) + /// Angular velocity as a world-space 3-vector (read+write). + #[getter] + fn angvel(&self) -> Vec3 { + let v: crate::na::Vector3 = self.with_ref(|b| b.angvel()).into(); + Vec3(v) + } + /// Set the angular velocity (wakes the body). + #[setter] + fn set_angvel(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.with_mut(|b| b.set_angvel(g, true)); + } + + /// Torque accumulated by user calls during the current step + /// (read-only). Cleared automatically at the next step. + #[getter] + fn user_torque(&self) -> Vec3 { + let v: crate::na::Vector3 = self.with_ref(|b| b.user_torque()).into(); + Vec3(v) + } + + /// Per-axis rotation enable mask as ``(rx, ry, rz)`` booleans + /// (``True`` = free to rotate). + #[getter] + fn enabled_rotations(&self) -> (bool, bool, bool) { + let arr = self.with_ref(|b| b.is_rotation_locked()); + (!arr[0], !arr[1], !arr[2]) + } + /// Enable / disable rotation around each axis. + /// + /// :param v: tuple ``(rx, ry, rz)`` of booleans where ``True`` + /// allows rotation and ``False`` locks it. + #[setter] + fn set_enabled_rotations(&mut self, v: (bool, bool, bool)) { + self.with_mut(|b| b.set_enabled_rotations(v.0, v.1, v.2, true)); + } + + /// Per-axis translation enable mask as ``(tx, ty, tz)`` booleans + /// (``True`` = free to translate). + #[getter] + fn enabled_translations(&self) -> (bool, bool, bool) { + let la = self.with_ref(|b| b.locked_axes()); + ( + !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X), + !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y), + !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Z), + ) + } + /// Enable / disable translation along each axis. + /// + /// :param v: tuple ``(tx, ty, tz)`` of booleans. + #[setter] + fn set_enabled_translations(&mut self, v: (bool, bool, bool)) { + self.with_mut(|b| b.set_enabled_translations(v.0, v.1, v.2, true)); + } + + /// Accumulate a torque to be applied during the next step. + /// + /// The torque is cleared after the step (like ``add_force``); + /// use ``apply_torque_impulse`` for an instantaneous change of + /// angular velocity. + /// + /// :param torque: torque vector in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (torque, wake_up=true))] + fn add_torque(&mut self, torque: PyVector, wake_up: bool) { + let g: rapier::math::Vector = torque.0.into(); + self.with_mut(|b| b.add_torque(g, wake_up)); + } + + /// Apply an instantaneous angular impulse (units: ``N·m·s``). + /// + /// :param torque_impulse: torque impulse vector in world space. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (torque_impulse, wake_up=true))] + fn apply_torque_impulse(&mut self, torque_impulse: PyVector, wake_up: bool) { + let g: rapier::math::Vector = torque_impulse.0.into(); + self.with_mut(|b| b.apply_torque_impulse(g, wake_up)); + } + + /// Whether gyroscopic forces are accounted for during integration + /// (read+write). Useful for asymmetric inertia tensors (e.g. + /// spinning tops). + #[getter] + fn gyroscopic_forces_enabled(&self) -> bool { + self.with_ref(|b| b.gyroscopic_forces_enabled()) + } + /// Enable or disable gyroscopic forces. + #[setter] + fn set_gyroscopic_forces_enabled(&mut self, v: bool) { + self.with_mut(|b| b.enable_gyroscopic_forces(v)); + } + + /// Predict the angular velocity after ``dt`` seconds *with* + /// gyroscopic forces taken into account. + /// + /// The body is not modified. + /// + /// :param dt: integration timestep in seconds. + fn angvel_with_gyroscopic_forces(&self, dt: Real) -> Vec3 { + let v: crate::na::Vector3 = self + .with_ref(|b| b.angvel_with_gyroscopic_forces(dt)) + .into(); + Vec3(v) + } + + // 3D-only builder static methods + /// Start building a dynamic rigid body. Returns a ``RigidBodyBuilder``. + /// + /// Accepts optional kwargs configuring builder fields + /// (``translation``, ``rotation``, ``linvel``, ``angvel``, + /// ``locked_axes``, ``ccd_enabled``, ``gravity_scale``, ...). + #[staticmethod] + #[pyo3(signature = (**kwargs))] + fn dynamic( + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::dynamic(), kwargs) + } + /// Start building a fixed (immovable) rigid body. + /// + /// Same kwargs as ``dynamic``. + #[staticmethod] + #[pyo3(signature = (**kwargs))] + fn fixed(kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>) -> PyResult { + RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::fixed(), kwargs) + } + /// Start building a velocity-based kinematic rigid body. + #[staticmethod] + #[pyo3(signature = (**kwargs))] + fn kinematic_velocity_based( + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + RigidBodyBuilder::from_kwargs( + rapier::dynamics::RigidBodyBuilder::kinematic_velocity_based(), + kwargs, + ) + } + /// Start building a position-based kinematic rigid body. + #[staticmethod] + #[pyo3(signature = (**kwargs))] + fn kinematic_position_based( + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + RigidBodyBuilder::from_kwargs( + rapier::dynamics::RigidBodyBuilder::kinematic_position_based(), + kwargs, + ) + } + + /// Start building a body of the given ``body_type``. + /// + /// :param body_type: a ``RigidBodyType`` selecting the behavior + /// class. + #[staticmethod] + #[pyo3(signature = (body_type, **kwargs))] + fn new_body( + body_type: RigidBodyType, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + RigidBodyBuilder::from_kwargs( + rapier::dynamics::RigidBodyBuilder::new(body_type.to_rapier()), + kwargs, + ) + } +} + +impl RigidBodyBuilder { + fn apply_angvel_kwarg( + builder: &mut rapier::dynamics::RigidBodyBuilder, + v: &Bound<'_, PyAny>, + ) -> PyResult<()> { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + *builder = builder.clone().angvel(g); + Ok(()) + } + + fn apply_rotation_kwarg( + builder: &mut rapier::dynamics::RigidBodyBuilder, + v: &Bound<'_, PyAny>, + ) -> PyResult<()> { + // 3D rotation kwarg is a scaled-axis-angle vector. + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + *builder = builder.clone().rotation(g); + Ok(()) + } + + fn apply_enabled_translations_kwarg( + builder: &mut rapier::dynamics::RigidBodyBuilder, + v: &Bound<'_, PyAny>, + ) -> PyResult<()> { + let t: (bool, bool, bool) = v.extract()?; + *builder = builder.clone().enabled_translations(t.0, t.1, t.2); + Ok(()) + } + + fn apply_enabled_rotations_kwarg( + builder: &mut rapier::dynamics::RigidBodyBuilder, + v: &Bound<'_, PyAny>, + ) -> PyResult<()> { + let t: (bool, bool, bool) = v.extract()?; + *builder = builder.clone().enabled_rotations(t.0, t.1, t.2); + Ok(()) + } + + fn apply_gyroscopic_forces_kwarg( + builder: &mut rapier::dynamics::RigidBodyBuilder, + v: &Bound<'_, PyAny>, + ) -> PyResult<()> { + let b: bool = v.extract()?; + *builder = builder.clone().gyroscopic_forces_enabled(b); + Ok(()) + } +} + +/// Container holding every ``RigidBody`` in a physics world. +/// +/// Bodies are addressed by ``RigidBodyHandle`` and live in the set +/// until ``remove``-d. The set supports ``len()``, ``in``, iteration +/// (yielding ``(handle, body)`` pairs) and ``[handle]`` lookup. +/// +/// Bodies returned by ``get`` / ``__getitem__`` / iteration are live +/// **views** into the set: ``set[h].linvel = v`` mutates the stored +/// body in place, with no copy. +#[pyclass(name = "RigidBodySet", module = "rapier", unsendable)] +pub struct RigidBodySet(pub rapier::dynamics::RigidBodySet); + +#[pymethods] +impl RigidBodySet { + /// Build an empty rigid-body set. + #[new] + fn new() -> Self { + Self(rapier::dynamics::RigidBodySet::new()) + } + + /// Insert a rigid body and return its handle. + /// + /// Accepts either a ``RigidBody`` or a ``RigidBodyBuilder``; + /// builders are built before insertion. + /// + /// :raises TypeError: if ``body`` is neither a ``RigidBody`` nor + /// a ``RigidBodyBuilder``. + fn insert(&mut self, body: &Bound<'_, PyAny>) -> PyResult { + // Accept either a RigidBodyBuilder or a RigidBody. + if let Ok(b) = body.extract::>() { + let rb = b.builder.build(); + return Ok(RigidBodyHandle(self.0.insert(rb))); + } + if let Ok(rb) = body.extract::>() { + let cloned = rb.to_owned_body(); + return Ok(RigidBodyHandle(self.0.insert(cloned))); + } + Err(PyTypeError::new_err( + "RigidBodySet.insert expects a RigidBody or RigidBodyBuilder", + )) + } + + /// Remove the rigid body identified by ``handle``. + /// + /// Joints anchored to the body are removed from + /// ``impulse_joints`` and ``multibody_joints``. If + /// ``remove_attached_colliders`` is ``True`` (default), every + /// collider attached to the body is also removed from + /// ``colliders``; otherwise those colliders are simply detached. + /// + /// :returns: the removed body, or ``None`` if no body matched + /// ``handle``. + #[pyo3(signature = (handle, islands, colliders, impulse_joints, multibody_joints, remove_attached_colliders=true))] + fn remove( + &mut self, + handle: &RigidBodyHandle, + islands: &mut IslandManager, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + remove_attached_colliders: bool, + ) -> Option { + self.0 + .remove( + handle.0, + &mut islands.0, + &mut colliders.0, + &mut impulse_joints.0, + &mut multibody_joints.0, + remove_attached_colliders, + ) + .map(RigidBody::new_owned) + } + + /// Return a live **view** of the body identified by ``handle``, or + /// ``None`` if there is none. + /// + /// The returned body reads and writes straight through to the set: + /// ``set.get(h).linvel = v`` persists immediately, with no copy. + fn get(slf: &Bound<'_, Self>, handle: &RigidBodyHandle) -> Option { + slf.borrow().0.get(handle.0)?; + Some(RigidBody { + backing: RigidBodyBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + /// Indexing form of ``get`` — returns a live view into the set. + /// + /// :raises InvalidHandle: if ``handle`` does not match any body. + fn __getitem__(slf: &Bound<'_, Self>, handle: &RigidBodyHandle) -> PyResult { + if slf.borrow().0.get(handle.0).is_none() { + return Err(crate::errors::InvalidHandle::new_err(format!( + "no rigid body for {:?}", + handle.0.into_raw_parts() + ))); + } + Ok(RigidBody { + backing: RigidBodyBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + /// ``handle in self`` — whether ``handle`` refers to a body + /// stored in this set. + fn __contains__(&self, handle: &RigidBodyHandle) -> bool { + self.0.contains(handle.0) + } + + /// Number of bodies in the set. + fn __len__(&self) -> usize { + self.0.len() + } + + /// Remove every body from the set. + fn clear(&mut self) { + // RigidBodySet doesn't expose `clear()`, so we re-init. + self.0 = rapier::dynamics::RigidBodySet::new(); + } + + /// Iterate over ``(handle, body)`` pairs. + /// + /// Each yielded body is a live view into the set (no copy). + fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { + let handles: Vec = + slf.borrow().0.iter().map(|(h, _)| h).collect(); + Py::new( + slf.py(), + RigidBodySetIter { + set: slf.clone().unbind(), + handles, + i: 0, + }, + ) + } + + /// Iterate over the handles of every body in the set. + fn handles(slf: PyRef<'_, Self>) -> PyResult> { + let handles: Vec = slf.0.iter().map(|(h, _)| RigidBodyHandle(h)).collect(); + Py::new(slf.py(), RigidBodyHandleIter { handles, i: 0 }) + } +} + +/// Iterator yielding ``(RigidBodyHandle, RigidBody)`` pairs from a +/// ``RigidBodySet``. +/// +/// Each ``RigidBody`` is a live view into the set. +#[pyclass] +pub struct RigidBodySetIter { + set: Py, + handles: Vec, + i: usize, +} + +#[pymethods] +impl RigidBodySetIter { + /// Return ``self`` so the object satisfies the iterator protocol. + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Return the next ``(handle, body)`` pair, or raise + /// ``StopIteration``. The body is built lazily as a live view. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(RigidBodyHandle, RigidBody)> { + if slf.i >= slf.handles.len() { + return None; + } + let py = slf.py(); + let handle = slf.handles[slf.i]; + slf.i += 1; + let set = slf.set.clone_ref(py); + Some(( + RigidBodyHandle(handle), + RigidBody { + backing: RigidBodyBacking::InSet { set, handle }, + }, + )) + } +} + +// ---------------------------------------------------------------------- +// register_dynamics — module entry point. +// ---------------------------------------------------------------------- + +pub fn register_dynamics( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + // ImpulseJointSet / MultibodyJointSet registered by joints.rs. + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/errors.rs b/python/rapier-py-3d/src/errors.rs index 2aaa62a0b..e37edfa72 100644 --- a/python/rapier-py-3d/src/errors.rs +++ b/python/rapier-py-3d/src/errors.rs @@ -77,18 +77,3 @@ pub fn register_errors(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> m.add("MeshLoaderError", py.get_type_bound::())?; Ok(()) } - -/// Convenience macro that each cdylib calls from its `#[pymodule]`: -/// ```ignore -/// #[pymodule] -/// fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { -/// rapier_py_core::register_errors!(py, m); -/// Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! register_errors { - ($py:expr, $m:expr) => { - $crate::errors::register_errors($py, $m)? - }; -} diff --git a/python/rapier-py-3d/src/events_hooks.rs b/python/rapier-py-3d/src/events_hooks.rs new file mode 100644 index 000000000..4b071b2a7 --- /dev/null +++ b/python/rapier-py-3d/src/events_hooks.rs @@ -0,0 +1,982 @@ +//! The concrete 3D / `f32` event-handler / physics-hooks `#[pyclass]` types and +//! the `register_events_hooks` function used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. +//! +//! It depends on: +//! - The `Real`/`DIM` aliases and `Py{Vector,Point,...}` adapters. +//! - The user-facing math types (`Vec3`, `Point3`, ...). +//! - The geometry types — in particular `CollisionEvent`, `ContactForceEvent`, +//! `ContactPair`, `ContactManifold`, `SolverContact`, `ColliderHandle`. +//! - The dynamics types — `RigidBodyHandle`. +//! +//! # Threading model +//! +//! `PhysicsPipeline.step` releases the GIL via `Python::allow_threads`. The +//! rapier solver may then invoke `EventHandler` / `PhysicsHooks` trait methods +//! from a worker thread. Our adapter types (`PyEventHandler`, `PyPhysicsHooks`, +//! `ChannelEventCollector`) hold `Py` callables; each callback wraps the +//! call in `Python::with_gil(...)` to re-acquire the GIL before touching any +//! Python object. +//! +//! Exceptions raised inside Python callbacks are **deferred**: they're stashed +//! into a shared `Mutex>` and re-raised after `step()` returns to +//! Python (the user can flip `world.event_error_policy = "strict"` to also set +//! an abort flag so the solver bails out early — but rapier doesn't currently +//! support mid-step aborts, so "strict" simply makes the error get re-raised +//! *and* future hook calls during the same step early-return without invoking +//! the user callback). +//! +//! # Safety +//! +//! `PyEventHandler` / `PyPhysicsHooks` declare `unsafe impl Send + Sync`. PyO3's +//! `Py` is `Send` already (it manages the refcount through atomic ops via +//! `clone_ref`), but we still re-acquire the GIL with `Python::with_gil` before +//! every interaction. The wrappers themselves never read or write the inner +//! Python object outside of a GIL section, so the `Send + Sync` upgrade is +//! sound. + +use crate::pyo3::exceptions::PyTypeError; +use crate::pyo3::pyclass::CompareOp; +use crate::*; +use rapier3d as rapier; + +use std::sync::{Arc, Mutex}; + +// ===================================================================== +// Deferred-error policy. +// ===================================================================== + +/// How to handle Python exceptions raised inside a callback. +/// +/// - `Defer` (default): catch, stash, re-raise after `step()` returns. +/// - `Strict`: catch, stash, set an abort flag so subsequent hook calls +/// within the same `step()` short-circuit. The solver still runs to +/// completion (rapier doesn't support mid-step aborts), but the +/// error is re-raised after `step()` returns. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EventErrorPolicy { + Defer, + Strict, +} + +/// Slot shared between every adapter wrapper and the world. Stores the +/// first exception raised by a Python callback during a step, plus the +/// error policy chosen by the user. +#[derive(Default)] +pub struct DeferredError { + pub err: Option, + pub aborted: bool, + pub policy_strict: bool, +} + +// ===================================================================== +// SolverFlags — bitflags wrapper. +// ===================================================================== + +/// Bitflags returned from ``PhysicsHooks.filter_contact_pair`` to +/// control how a contact pair is solved. +/// +/// Combine flags with ``|``, intersect with ``&``, test membership with +/// ``in`` or :meth:`contains`. ``COMPUTE_IMPULSES`` is the default; an +/// empty flag set disables impulse computation for that pair (it will +/// still generate collision events). +#[pyclass(name = "SolverFlags", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SolverFlags(pub rapier::geometry::SolverFlags); + +#[pymethods] +impl SolverFlags { + /// Construct a ``SolverFlags`` from a raw bit pattern. + /// + /// :param bits: Raw bit pattern. Bits that don't correspond to a + /// known flag are silently truncated. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::geometry::SolverFlags::from_bits_truncate(bits)) + } + /// Return an empty ``SolverFlags`` (no contact solving). + #[staticmethod] + fn empty() -> Self { + Self(rapier::geometry::SolverFlags::empty()) + } + /// Return the default ``SolverFlags`` (``COMPUTE_IMPULSES``). + #[staticmethod] + fn default_() -> Self { + Self(rapier::geometry::SolverFlags::default()) + } + + /// Compute impulses for this contact pair (i.e. solve the contact). + #[classattr] + const COMPUTE_IMPULSES: SolverFlags = + SolverFlags(rapier::geometry::SolverFlags::COMPUTE_IMPULSES); + /// Empty flag set — equivalent to :meth:`empty`. + #[classattr] + const EMPTY: SolverFlags = SolverFlags(rapier::geometry::SolverFlags::empty()); + + /// Raw underlying bit pattern. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// Return ``True`` if every bit set in ``other`` is also set here. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Return ``True`` if no flag is set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// ``other in self`` — alias for :meth:`contains`. + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Bitwise OR — union of two flag sets. + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND — intersection of two flag sets. + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Bitwise XOR — symmetric difference. + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Bitwise NOT — flip every known flag. + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// Truthy iff at least one flag is set. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality on the raw bit pattern; other comparisons raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("SolverFlags supports only == and !=")), + } + } + /// Hash on the raw bit pattern (makes ``SolverFlags`` set-safe). + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + /// Debug repr — shows the raw bit pattern in binary. + fn __repr__(&self) -> String { + format!("SolverFlags(bits={:#010b})", self.0.bits()) + } +} + +// ===================================================================== +// PairFilterContext — transient view, only valid during the callback. +// +// We keep raw pointers to the rapier-side context fields. The +// `valid` flag is flipped to `false` immediately after the Python +// callback returns; subsequent accesses raise. +// ===================================================================== + +/// Context passed to ``PhysicsHooks.filter_contact_pair`` / +/// ``filter_intersection_pair``. +/// +/// Exposes the two colliders and (when applicable) their parent rigid +/// bodies. The instance is a **transient view** valid only for the +/// duration of the callback that received it — keeping a reference +/// around and accessing it later raises ``RuntimeError``. +#[pyclass(name = "PairFilterContext", module = "rapier", unsendable)] +pub struct PairFilterContext { + collider1: rapier::geometry::ColliderHandle, + collider2: rapier::geometry::ColliderHandle, + rigid_body1: Option, + rigid_body2: Option, +} + +#[pymethods] +impl PairFilterContext { + /// Handle of the first collider in the pair. + #[getter] + fn collider1(&self) -> ColliderHandle { + ColliderHandle(self.collider1) + } + /// Handle of the second collider in the pair. + #[getter] + fn collider2(&self) -> ColliderHandle { + ColliderHandle(self.collider2) + } + /// Handle of ``collider1``'s parent rigid body, or ``None``. + #[getter] + fn rigid_body1(&self) -> Option { + self.rigid_body1.map(RigidBodyHandle) + } + /// Handle of ``collider2``'s parent rigid body, or ``None``. + #[getter] + fn rigid_body2(&self) -> Option { + self.rigid_body2.map(RigidBodyHandle) + } + /// Debug repr — shows the raw collider handles. + fn __repr__(&self) -> String { + format!( + "PairFilterContext(c1={:?}, c2={:?})", + self.collider1.into_raw_parts(), + self.collider2.into_raw_parts(), + ) + } +} + +// ===================================================================== +// ContactModificationContext — transient mutable view. +// +// The rapier callback gives us `&mut Vec`, `&mut Vector`, +// `&mut u32`. We stash raw pointers and a `valid` flag. Mutations from +// Python persist back to the rapier-side buffer. +// ===================================================================== + +/// Context passed to ``PhysicsHooks.modify_solver_contacts``. +/// +/// Lets the user inspect and mutate the solver contacts of a manifold +/// before they're handed to the constraint solver: clear them to skip +/// the contact, flip the normal, tweak per-contact user data, etc. +/// +/// Like :class:`PairFilterContext`, the instance is a **transient +/// view** valid only inside the callback — accessing mutating fields +/// (``normal``, ``user_data``, ``solver_contacts``) after the callback +/// returns raises ``RuntimeError``. +#[pyclass(name = "ContactModificationContext", module = "rapier", unsendable)] +pub struct ContactModificationContext { + collider1: rapier::geometry::ColliderHandle, + collider2: rapier::geometry::ColliderHandle, + rigid_body1: Option, + rigid_body2: Option, + // Read-only snapshot of `manifold.local_n1` (we don't expose the + // full manifold mutably — it would entangle lifetimes too deeply). + local_n1: rapier::math::Vector, + local_n2: rapier::math::Vector, + normal: *mut rapier::math::Vector, + solver_contacts: *mut Vec, + user_data: *mut u32, + valid: bool, +} + +impl ContactModificationContext { + #[inline] + fn check_valid(&self) -> PyResult<()> { + if !self.valid { + return Err(crate::pyo3::exceptions::PyRuntimeError::new_err( + "ContactModificationContext is no longer valid (used outside of callback)", + )); + } + Ok(()) + } +} + +#[pymethods] +impl ContactModificationContext { + /// Handle of the first collider in the pair. + #[getter] + fn collider1(&self) -> ColliderHandle { + ColliderHandle(self.collider1) + } + /// Handle of the second collider in the pair. + #[getter] + fn collider2(&self) -> ColliderHandle { + ColliderHandle(self.collider2) + } + /// Handle of ``collider1``'s parent rigid body, or ``None``. + #[getter] + fn rigid_body1(&self) -> Option { + self.rigid_body1.map(RigidBodyHandle) + } + /// Handle of ``collider2``'s parent rigid body, or ``None``. + #[getter] + fn rigid_body2(&self) -> Option { + self.rigid_body2.map(RigidBodyHandle) + } + /// Read-only snapshot of the manifold's local normal on ``collider1``. + #[getter] + fn local_n1(&self) -> Vec3 { + let v: crate::na::SVector = self.local_n1.into(); + Vec3(v) + } + /// Read-only snapshot of the manifold's local normal on ``collider2``. + #[getter] + fn local_n2(&self) -> Vec3 { + let v: crate::na::SVector = self.local_n2.into(); + Vec3(v) + } + + /// Mutable world-space contact normal (read). + /// + /// :raises RuntimeError: If accessed outside the callback. + #[getter] + fn normal(&self) -> PyResult { + self.check_valid()?; + // SAFETY: `valid` is true exactly during the callback, where + // the rapier-side normal is reachable through this raw pointer. + let v: rapier::math::Vector = unsafe { *self.normal }; + let nav: crate::na::SVector = v.into(); + Ok(Vec3(nav)) + } + /// Mutable world-space contact normal (write). + /// + /// :raises RuntimeError: If accessed outside the callback. + #[setter] + fn set_normal(&mut self, v: PyVector) -> PyResult<()> { + self.check_valid()?; + // SAFETY: see `normal` getter. + unsafe { + *self.normal = v.0.into(); + } + Ok(()) + } + + /// Per-manifold user data, mutable from inside the callback (read). + /// + /// :raises RuntimeError: If accessed outside the callback. + #[getter] + fn user_data(&self) -> PyResult { + self.check_valid()?; + // SAFETY: see `normal` getter. + Ok(unsafe { *self.user_data }) + } + /// Per-manifold user data (write). + /// + /// :raises RuntimeError: If accessed outside the callback. + #[setter] + fn set_user_data(&mut self, v: u32) -> PyResult<()> { + self.check_valid()?; + // SAFETY: see `normal` getter. + unsafe { + *self.user_data = v; + } + Ok(()) + } + + /// Return the current solver contacts as a list (read-only snapshot). + /// + /// :raises RuntimeError: If accessed outside the callback. + #[getter] + fn solver_contacts(&self) -> PyResult> { + self.check_valid()?; + // SAFETY: see `normal` getter. + let vec_ref: &Vec = unsafe { &*self.solver_contacts }; + let mut out = Vec::with_capacity(vec_ref.len()); + for sc in vec_ref { + out.push({ + let p: crate::na::Vector3 = sc.point.into(); + SolverContact { + point: Point3(crate::na::Point3::from(p)), + dist: sc.dist, + friction: sc.friction, + restitution: sc.restitution, + contact_id: sc.contact_id[0], + is_new: sc.is_new != 0.0, + } + }); + } + Ok(out) + } + + /// Clear all solver contacts (the manifold won't contribute to the solver). + /// + /// :raises RuntimeError: If accessed outside the callback. + fn clear_solver_contacts(&mut self) -> PyResult<()> { + self.check_valid()?; + // SAFETY: see `normal` getter. + unsafe { + (*self.solver_contacts).clear(); + } + Ok(()) + } + + /// Number of solver contacts currently in the manifold. + /// + /// :raises RuntimeError: If accessed outside the callback. + fn num_solver_contacts(&self) -> PyResult { + self.check_valid()?; + // SAFETY: see `normal` getter. + Ok(unsafe { (*self.solver_contacts).len() }) + } + + /// Drop the solver contact at index ``i``. Out-of-range indices are ignored. + /// + /// :param i: Zero-based index into the solver-contact list. + /// :raises RuntimeError: If accessed outside the callback. + fn remove_solver_contact(&mut self, i: usize) -> PyResult<()> { + self.check_valid()?; + // SAFETY: see `normal` getter. + unsafe { + let v = &mut *self.solver_contacts; + if i < v.len() { + v.remove(i); + } + } + Ok(()) + } + + /// Emulate a one-way platform contact (port of rapier's helper). + /// + /// The "oneway" behavior only allows contacts when the local + /// contact normal of ``collider1`` is roughly aligned with + /// ``allowed_local_n1`` (within ``angle`` radians). Contacts + /// pointing the wrong way are cleared so the constraint solver + /// ignores them — letting the body pass through from the other + /// side. + /// + /// :param allowed_local_n1: Local-space normal that should let the + /// contact through (e.g. ``Vec3(0, 1, 0)`` for a floor-only + /// platform). + /// :param angle: Half-cone tolerance in radians. + /// :raises RuntimeError: If accessed outside the callback. + fn update_as_oneway_platform( + &mut self, + allowed_local_n1: PyVector, + angle: Real, + ) -> PyResult<()> { + self.check_valid()?; + // Reconstruct a `ContactModificationContext` borrow shape on + // the fly and dispatch. We re-implement the logic inline to + // avoid the manifold borrow. + // + // SAFETY: see `normal` getter. + let allowed_n1: rapier::math::Vector = allowed_local_n1.0.into(); + let cang = Real::cos(angle); + let contact_is_ok = self.local_n1.dot(allowed_n1) >= cang; + + const UNKNOWN: u32 = 0; + const ALLOWED: u32 = 1; + const FORBIDDEN: u32 = 2; + + let (sc, ud) = unsafe { (&mut *self.solver_contacts, &mut *self.user_data) }; + match *ud { + UNKNOWN => { + if contact_is_ok { + *ud = ALLOWED; + } else { + sc.clear(); + if self.local_n1.length_squared() > 0.1 { + *ud = FORBIDDEN; + } + } + } + FORBIDDEN => { + if contact_is_ok && sc.iter().all(|c| c.dist > 0.0) { + *ud = ALLOWED; + } else { + sc.clear(); + } + } + ALLOWED if sc.is_empty() => { + *ud = UNKNOWN; + } + _ => {} + } + Ok(()) + } + + /// Debug repr — shows the raw collider handles. + fn __repr__(&self) -> String { + format!( + "ContactModificationContext(c1={:?}, c2={:?})", + self.collider1.into_raw_parts(), + self.collider2.into_raw_parts(), + ) + } +} + +// ===================================================================== +// PyEventHandler — adapts an arbitrary Python object implementing the +// `EventHandler` protocol to rapier's `EventHandler` trait. +// ===================================================================== + +/// Adapter wrapping an arbitrary Python `EventHandler`-protocol object. +pub struct PyEventHandler { + obj: Py, + err_slot: Arc>, +} + +impl PyEventHandler { + pub fn new(obj: Py, err_slot: Arc>) -> Self { + Self { obj, err_slot } + } +} + +// SAFETY: `Py` is `Send` (PyO3 manages the refcount via atomic +// ops on `clone_ref`). Every interaction with the inner Python object +// happens inside `Python::with_gil`, which re-acquires the GIL — so we +// never touch Python state without serialization. `Arc>` is +// both `Send` and `Sync`. +unsafe impl Send for PyEventHandler {} +unsafe impl Sync for PyEventHandler {} + +impl rapier::pipeline::EventHandler for PyEventHandler { + fn handle_collision_event( + &self, + _bodies: &rapier::dynamics::RigidBodySet, + _colliders: &rapier::geometry::ColliderSet, + event: rapier::geometry::CollisionEvent, + contact_pair: Option<&rapier::geometry::ContactPair>, + ) { + { + let slot = self.err_slot.lock().unwrap(); + if slot.aborted { + return; + } + } + Python::with_gil(|py| { + let py_event = CollisionEvent::from_rapier(event); + let py_pair: PyObject = match contact_pair { + Some(p) => Py::new(py, ContactPair(p.clone())) + .map(|h| h.into_any()) + .unwrap_or_else(|_| py.None()), + None => py.None(), + }; + let res = self.obj.bind(py).call_method1( + "handle_collision_event", + (py.None(), py.None(), py_event, py_pair), + ); + if let Err(e) = res { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + } + }); + } + + fn handle_contact_force_event( + &self, + dt: Real, + _bodies: &rapier::dynamics::RigidBodySet, + _colliders: &rapier::geometry::ColliderSet, + contact_pair: &rapier::geometry::ContactPair, + total_force_magnitude: Real, + ) { + { + let slot = self.err_slot.lock().unwrap(); + if slot.aborted { + return; + } + } + Python::with_gil(|py| { + let py_pair = match Py::new(py, ContactPair(contact_pair.clone())) { + Ok(p) => p, + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + return; + } + }; + let res = self.obj.bind(py).call_method1( + "handle_contact_force_event", + (dt, py.None(), py.None(), py_pair, total_force_magnitude), + ); + if let Err(e) = res { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + } + }); + } +} + +// ===================================================================== +// ChannelEventCollector — built-in EventHandler that funnels events +// into internal buffers, drained on demand from Python. +// ===================================================================== + +/// Recommended built-in event handler. +/// +/// A :class:`ChannelEventCollector` accumulates collision and +/// contact-force events into lock-free internal buffers. Attach it to +/// a world via ``world.event_handler``, then call +/// :meth:`drain_collision_events` / :meth:`drain_contact_force_events` +/// after each step to consume them:: +/// +/// c = ChannelEventCollector() +/// world.event_handler = c +/// world.step() +/// for event in c.drain_collision_events(): +/// ... +/// +/// Because the buffers are owned by Rust and the pipeline detects the +/// concrete type, this avoids the per-event Python callback overhead +/// of a generic ``EventHandler`` protocol object. +#[pyclass(name = "ChannelEventCollector", module = "rapier")] +pub struct ChannelEventCollector { + collisions: Arc>>, + forces: Arc>>, +} + +#[pymethods] +impl ChannelEventCollector { + /// Construct an empty collector with no events queued. + #[new] + fn new() -> Self { + Self { + collisions: Arc::new(Mutex::new(Vec::new())), + forces: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Consume and return every queued :class:`CollisionEvent`. + /// + /// After this call the internal collision buffer is empty. + /// + /// :returns: A list of collision events captured since the last + /// drain. + fn drain_collision_events(&self) -> Vec { + std::mem::take(&mut *self.collisions.lock().unwrap()) + } + + /// Consume and return every queued :class:`ContactForceEvent`. + /// + /// After this call the internal contact-force buffer is empty. + /// + /// :returns: A list of contact-force events captured since the + /// last drain. + fn drain_contact_force_events(&self) -> Vec { + std::mem::take(&mut *self.forces.lock().unwrap()) + } + + /// Drop every queued collision and contact-force event. + fn clear(&self) { + self.collisions.lock().unwrap().clear(); + self.forces.lock().unwrap().clear(); + } + + /// Total queued events (collisions + contact-force). + fn __len__(&self) -> usize { + self.collisions.lock().unwrap().len() + self.forces.lock().unwrap().len() + } + + /// Debug repr — shows the per-buffer queue lengths. + fn __repr__(&self) -> String { + let ce = self.collisions.lock().unwrap().len(); + let fe = self.forces.lock().unwrap().len(); + format!("ChannelEventCollector(collisions={ce}, forces={fe})") + } +} + +impl ChannelEventCollector { + /// Build an `EventHandler`-implementing adapter that pushes into + /// this collector's buffers. The adapter is `Send + Sync`. + pub fn as_event_handler(&self) -> ChannelEventCollectorAdapter { + ChannelEventCollectorAdapter { + collisions: Arc::clone(&self.collisions), + forces: Arc::clone(&self.forces), + } + } +} + +/// `EventHandler`-implementing adapter built on the fly by +/// `ChannelEventCollector::as_event_handler`. Holds `Arc>` +/// clones of the collector's buffers, so it can outlive a single call. +pub struct ChannelEventCollectorAdapter { + collisions: Arc>>, + forces: Arc>>, +} + +impl rapier::pipeline::EventHandler for ChannelEventCollectorAdapter { + fn handle_collision_event( + &self, + _bodies: &rapier::dynamics::RigidBodySet, + _colliders: &rapier::geometry::ColliderSet, + event: rapier::geometry::CollisionEvent, + _contact_pair: Option<&rapier::geometry::ContactPair>, + ) { + self.collisions + .lock() + .unwrap() + .push(CollisionEvent::from_rapier(event)); + } + + fn handle_contact_force_event( + &self, + dt: Real, + _bodies: &rapier::dynamics::RigidBodySet, + _colliders: &rapier::geometry::ColliderSet, + contact_pair: &rapier::geometry::ContactPair, + total_force_magnitude: Real, + ) { + let r = rapier::geometry::ContactForceEvent::from_contact_pair( + dt, + contact_pair, + total_force_magnitude, + ); + let v: crate::na::SVector = r.total_force.into(); + let d: crate::na::SVector = r.max_force_direction.into(); + self.forces.lock().unwrap().push(ContactForceEvent { + collider1: ColliderHandle(r.collider1), + collider2: ColliderHandle(r.collider2), + total_force: Vec3(v), + total_force_magnitude: r.total_force_magnitude, + max_force_direction: Vec3(d), + max_force_magnitude: r.max_force_magnitude, + }); + } +} + +// ===================================================================== +// PyPhysicsHooks — adapter for the `PhysicsHooks` Python protocol. +// ===================================================================== + +/// Adapter wrapping an arbitrary Python `PhysicsHooks`-protocol object. +pub struct PyPhysicsHooks { + obj: Py, + err_slot: Arc>, +} + +impl PyPhysicsHooks { + pub fn new(obj: Py, err_slot: Arc>) -> Self { + Self { obj, err_slot } + } +} + +// SAFETY: see `PyEventHandler`. +unsafe impl Send for PyPhysicsHooks {} +unsafe impl Sync for PyPhysicsHooks {} + +impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { + fn filter_contact_pair( + &self, + context: &rapier::pipeline::PairFilterContext, + ) -> Option { + { + let slot = self.err_slot.lock().unwrap(); + if slot.aborted { + return Some(rapier::geometry::SolverFlags::default()); + } + } + Python::with_gil(|py| { + let ctx = match Py::new( + py, + PairFilterContext { + collider1: context.collider1, + collider2: context.collider2, + rigid_body1: context.rigid_body1, + rigid_body2: context.rigid_body2, + }, + ) { + Ok(c) => c, + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + return Some(rapier::geometry::SolverFlags::default()); + } + }; + let res = self + .obj + .bind(py) + .call_method1("filter_contact_pair", (ctx,)); + match res { + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + Some(rapier::geometry::SolverFlags::default()) + } + Ok(v) => { + if v.is_none() { + None + } else if let Ok(f) = v.extract::() { + Some(f.0) + } else if let Ok(bits) = v.extract::() { + Some(rapier::geometry::SolverFlags::from_bits_truncate(bits)) + } else { + // Any other truthy return is treated as "default". + Some(rapier::geometry::SolverFlags::default()) + } + } + } + }) + } + + fn filter_intersection_pair(&self, context: &rapier::pipeline::PairFilterContext) -> bool { + { + let slot = self.err_slot.lock().unwrap(); + if slot.aborted { + return true; + } + } + Python::with_gil(|py| { + let ctx = match Py::new( + py, + PairFilterContext { + collider1: context.collider1, + collider2: context.collider2, + rigid_body1: context.rigid_body1, + rigid_body2: context.rigid_body2, + }, + ) { + Ok(c) => c, + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + return true; + } + }; + match self + .obj + .bind(py) + .call_method1("filter_intersection_pair", (ctx,)) + { + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + true + } + Ok(v) => v.extract::().unwrap_or(true), + } + }) + } + + fn modify_solver_contacts(&self, context: &mut rapier::pipeline::ContactModificationContext) { + { + let slot = self.err_slot.lock().unwrap(); + if slot.aborted { + return; + } + } + Python::with_gil(|py| { + let manifold_local_n1 = context.manifold.local_n1; + let manifold_local_n2 = context.manifold.local_n2; + let normal_ptr: *mut rapier::math::Vector = context.normal; + let sc_ptr: *mut Vec = context.solver_contacts; + let ud_ptr: *mut u32 = context.user_data; + let ctx_py = match Py::new( + py, + ContactModificationContext { + collider1: context.collider1, + collider2: context.collider2, + rigid_body1: context.rigid_body1, + rigid_body2: context.rigid_body2, + local_n1: manifold_local_n1, + local_n2: manifold_local_n2, + normal: normal_ptr, + solver_contacts: sc_ptr, + user_data: ud_ptr, + valid: true, + }, + ) { + Ok(c) => c, + Err(e) => { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + return; + } + }; + let res = self + .obj + .bind(py) + .call_method1("modify_solver_contacts", (ctx_py.clone_ref(py),)); + // Invalidate the transient view before letting Python see + // its `None` return. + { + let mut borrowed = ctx_py.borrow_mut(py); + borrowed.valid = false; + } + if let Err(e) = res { + let mut s = self.err_slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } + } + }); + } +} + +// ===================================================================== +// Public helpers used by `pipeline.rs` to construct the adapters. +// ===================================================================== + +/// Wrap an arbitrary Python object as a (boxed) rapier `EventHandler`, +/// or detect a `ChannelEventCollector` and short-circuit to its +/// lock-free buffer adapter. +/// +/// Returns `None` if `obj` is `Py::None` / Python `None`. +pub fn build_event_handler( + py: Python<'_>, + obj: Option<&Py>, + err_slot: Arc>, +) -> Option> { + let obj = obj?; + let bound = obj.bind(py); + if bound.is_none() { + return None; + } + // ChannelEventCollector: optimize away the Python callback entirely. + if let Ok(c) = bound.extract::>() { + return Some(Box::new(c.as_event_handler())); + } + Some(Box::new(PyEventHandler::new(obj.clone_ref(py), err_slot))) +} + +/// Wrap an arbitrary Python object as a (boxed) rapier `PhysicsHooks`. +pub fn build_physics_hooks( + py: Python<'_>, + obj: Option<&Py>, + err_slot: Arc>, +) -> Option> { + let obj = obj?; + let bound = obj.bind(py); + if bound.is_none() { + return None; + } + Some(Box::new(PyPhysicsHooks::new(obj.clone_ref(py), err_slot))) +} + +pub fn register_events_hooks( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/geometry.rs b/python/rapier-py-3d/src/geometry.rs new file mode 100644 index 000000000..af82807e7 --- /dev/null +++ b/python/rapier-py-3d/src/geometry.rs @@ -0,0 +1,4301 @@ +//! The concrete 3D / `f32` geometry, shape, and collider `#[pyclass]` types, +//! plus the `register_geometry` function used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. + +use crate::*; +use rapier3d as rapier; + +use crate::numpy::{PyArray2, PyReadonlyArray2, PyUntypedArrayMethods}; +use crate::pyo3::exceptions::{PyIndexError, PyTypeError, PyValueError}; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; + +/// Extract an `Mx3` `u32` matrix → `Vec<[u32; 3]>`. +pub fn extract_indices(obj: &Bound<'_, PyAny>) -> PyResult> { + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 3 { + return Err(PyValueError::new_err(format!( + "expected ndarray with shape (M, 3); got (M, {ncols})" + ))); + } + let slice = arr + .as_slice() + .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; + let mut out = Vec::with_capacity(nrows); + for chunk in slice.chunks_exact(3) { + out.push([chunk[0], chunk[1], chunk[2]]); + } + return Ok(out); + } + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 3 { + return Err(PyValueError::new_err(format!( + "expected ndarray with shape (M, 3); got (M, {ncols})" + ))); + } + let slice = arr + .as_slice() + .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; + let mut out = Vec::with_capacity(nrows); + for chunk in slice.chunks_exact(3) { + out.push([chunk[0] as u32, chunk[1] as u32, chunk[2] as u32]); + } + return Ok(out); + } + let seq: Vec> = obj.extract()?; + seq.iter() + .map(|c| { + if c.len() != 3 { + return Err(PyValueError::new_err(format!( + "expected inner sequence of length 3; got {}", + c.len(), + ))); + } + Ok([c[0], c[1], c[2]]) + }) + .collect() +} + +/// Extract an `Mx2` `u32` matrix → `Vec<[u32; 2]>`. +pub fn extract_indices_2(obj: &Bound<'_, PyAny>) -> PyResult> { + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 2 { + return Err(PyValueError::new_err(format!( + "expected ndarray with shape (M, 2); got (M, {ncols})" + ))); + } + let slice = arr + .as_slice() + .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; + let mut out = Vec::with_capacity(nrows); + for chunk in slice.chunks_exact(2) { + out.push([chunk[0], chunk[1]]); + } + return Ok(out); + } + let seq: Vec> = obj.extract()?; + seq.iter() + .map(|c| { + if c.len() != 2 { + return Err(PyValueError::new_err(format!( + "expected inner sequence of length 2; got {}", + c.len(), + ))); + } + Ok([c[0], c[1]]) + }) + .collect() +} + +// ============================================================ +// ColliderHandle (moved from dynamics) +// ============================================================ + +/// Opaque, generational handle identifying a collider in a `ColliderSet`. +/// +/// Handles remain valid until the collider is removed. After removal, +/// the slot may be reused with a bumped generation, so old handles +/// pointing to a freed slot compare unequal to the new one. Cheap to +/// copy and hash. +#[pyclass(name = "ColliderHandle", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ColliderHandle(pub rapier::geometry::ColliderHandle); + +#[pymethods] +impl ColliderHandle { + /// Construct a handle from raw `(index, generation)` parts. + /// + /// Mostly useful for tests and serialization round-trips; in + /// normal use, handles come from `ColliderSet.insert(...)`. + #[new] + #[pyo3(signature = (index=0, generation=0))] + fn new(index: u32, generation: u32) -> Self { + Self(rapier::geometry::ColliderHandle::from_raw_parts( + index, generation, + )) + } + /// Build a handle from raw `(index, generation)` parts. + #[staticmethod] + fn from_raw_parts(index: u32, generation: u32) -> Self { + Self(rapier::geometry::ColliderHandle::from_raw_parts( + index, generation, + )) + } + /// Return the sentinel "invalid" handle. + /// + /// Never refers to a real collider; useful as a default value. + #[staticmethod] + fn invalid() -> Self { + Self(rapier::geometry::ColliderSet::invalid_handle()) + } + /// Slot index inside the underlying `ColliderSet`. + #[getter] + fn index(&self) -> u32 { + self.0.into_raw_parts().0 + } + /// Generation counter for the slot. Bumped on each reuse. + #[getter] + fn generation(&self) -> u32 { + self.0.into_raw_parts().1 + } + fn __hash__(&self) -> u64 { + let (i, g) = self.0.into_raw_parts(); + ((i as u64) << 32) | (g as u64) + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "ColliderHandle supports only == and !=", + )), + } + } + fn __repr__(&self) -> String { + let (i, g) = self.0.into_raw_parts(); + format!("ColliderHandle(index={}, generation={})", i, g) + } +} + +// ============================================================ +// ShapeType (enum) +// ============================================================ + +/// Discriminant returned by `SharedShape.shape_type`. +/// +/// Use this to decide which `as_*()` downcast accessor to call on a +/// `SharedShape`. `CUSTOM` covers user-defined shapes that do not +/// match any built-in variant. +#[pyclass(name = "ShapeType", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShapeType { + BALL, + CUBOID, + CAPSULE, + SEGMENT, + TRIANGLE, + VOXELS, + TRIMESH, + POLYLINE, + HALFSPACE, + HEIGHTFIELD, + COMPOUND, + CONVEX_POLYGON, + CONVEX_POLYHEDRON, + CYLINDER, + CONE, + ROUND_CUBOID, + ROUND_TRIANGLE, + ROUND_CYLINDER, + ROUND_CONE, + ROUND_CONVEX_POLYHEDRON, + ROUND_CONVEX_POLYGON, + CUSTOM, +} + +impl ShapeType { + pub(crate) fn from_rapier(t: rapier::parry::shape::ShapeType) -> Self { + // The dim-specific variants are gated by parry's `dim2`/`dim3` + // features, but the cdylib already knows its own dim, so we + // express the mapping using the dim-specific helper macro. + { + use rapier::parry::shape::ShapeType as PT; + match t { + PT::Ball => ShapeType::BALL, + PT::Cuboid => ShapeType::CUBOID, + PT::Capsule => ShapeType::CAPSULE, + PT::Segment => ShapeType::SEGMENT, + PT::Triangle => ShapeType::TRIANGLE, + PT::Voxels => ShapeType::VOXELS, + PT::TriMesh => ShapeType::TRIMESH, + PT::Polyline => ShapeType::POLYLINE, + PT::HalfSpace => ShapeType::HALFSPACE, + PT::HeightField => ShapeType::HEIGHTFIELD, + PT::Compound => ShapeType::COMPOUND, + PT::ConvexPolyhedron => ShapeType::CONVEX_POLYHEDRON, + PT::Cylinder => ShapeType::CYLINDER, + PT::Cone => ShapeType::CONE, + PT::RoundCuboid => ShapeType::ROUND_CUBOID, + PT::RoundTriangle => ShapeType::ROUND_TRIANGLE, + PT::RoundCylinder => ShapeType::ROUND_CYLINDER, + PT::RoundCone => ShapeType::ROUND_CONE, + PT::RoundConvexPolyhedron => ShapeType::ROUND_CONVEX_POLYHEDRON, + _ => ShapeType::CUSTOM, + } + } + } +} + +// ============================================================ +// ColliderType +// ============================================================ + +/// Distinguishes a `SOLID` collider from a `SENSOR`. +/// +/// Solid colliders generate contact forces; sensor colliders only +/// fire intersection events and do not produce contact responses. +#[pyclass(name = "ColliderType", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColliderType { + SOLID, + SENSOR, +} + +impl ColliderType { + #[allow(dead_code)] + pub(crate) fn to_rapier(self) -> rapier::geometry::ColliderType { + match self { + Self::SOLID => rapier::geometry::ColliderType::Solid, + Self::SENSOR => rapier::geometry::ColliderType::Sensor, + } + } + #[allow(dead_code)] + pub(crate) fn from_rapier(t: rapier::geometry::ColliderType) -> Self { + match t { + rapier::geometry::ColliderType::Solid => Self::SOLID, + rapier::geometry::ColliderType::Sensor => Self::SENSOR, + } + } +} + +// ============================================================ +// ColliderEnabled +// ============================================================ + +/// Enabled-state of a collider. +/// +/// `DISABLED_BY_PARENT` means the parent rigid-body is disabled, so +/// the collider is effectively off without being explicitly disabled +/// by the user. +#[pyclass(name = "ColliderEnabled", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColliderEnabled { + ENABLED, + DISABLED_BY_PARENT, + DISABLED, +} + +impl ColliderEnabled { + #[allow(dead_code)] + pub(crate) fn from_rapier(t: rapier::geometry::ColliderEnabled) -> Self { + match t { + rapier::geometry::ColliderEnabled::Enabled => Self::ENABLED, + rapier::geometry::ColliderEnabled::DisabledByParent => Self::DISABLED_BY_PARENT, + rapier::geometry::ColliderEnabled::Disabled => Self::DISABLED, + } + } +} + +// ============================================================ +// ActiveEvents (bitflags) +// ============================================================ + +/// Per-collider opt-in flags selecting which events to emit. +/// +/// A collider only emits an event kind if it has the corresponding +/// flag set. Use `COLLISION_EVENTS` to receive `CollisionEvent` +/// (started/stopped) and `CONTACT_FORCE_EVENTS` to receive +/// `ContactForceEvent`. Combine flags via `|`. +#[pyclass(name = "ActiveEvents", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActiveEvents(pub rapier::pipeline::ActiveEvents); + +#[pymethods] +impl ActiveEvents { + /// Construct from a raw bit pattern. Unknown bits are dropped. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::pipeline::ActiveEvents::from_bits_truncate(bits)) + } + /// Return the empty flag set (no events). + #[staticmethod] + fn empty() -> Self { + Self(rapier::pipeline::ActiveEvents::empty()) + } + /// Return the flag set with every known event kind enabled. + #[staticmethod] + fn all_events() -> Self { + Self(rapier::pipeline::ActiveEvents::all()) + } + #[classattr] + const COLLISION_EVENTS: ActiveEvents = + ActiveEvents(rapier::pipeline::ActiveEvents::COLLISION_EVENTS); + #[classattr] + const CONTACT_FORCE_EVENTS: ActiveEvents = + ActiveEvents(rapier::pipeline::ActiveEvents::CONTACT_FORCE_EVENTS); + #[classattr] + const NONE: ActiveEvents = ActiveEvents(rapier::pipeline::ActiveEvents::empty()); + + /// Raw bit pattern of the flag set. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// True iff all flags in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no flags are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + fn __invert__(&self) -> Self { + Self(!self.0) + } + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("ActiveEvents supports only == and !=")), + } + } + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + fn __repr__(&self) -> String { + format!("ActiveEvents(bits={:#010b})", self.0.bits()) + } +} + +// ============================================================ +// ActiveHooks (bitflags) +// ============================================================ + +/// Per-collider opt-in flags selecting which `PhysicsHooks` callbacks +/// to invoke. +/// +/// A `PhysicsHooks` method only fires for a contact/intersection pair +/// if at least one of the involved colliders has the matching flag +/// set. Combine flags via `|`. +#[pyclass(name = "ActiveHooks", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActiveHooks(pub rapier::pipeline::ActiveHooks); + +#[pymethods] +impl ActiveHooks { + /// Construct from a raw bit pattern. Unknown bits are dropped. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::pipeline::ActiveHooks::from_bits_truncate(bits)) + } + /// Return the empty flag set (no hooks active). + #[staticmethod] + fn empty() -> Self { + Self(rapier::pipeline::ActiveHooks::empty()) + } + /// Return the flag set with every known hook enabled. + #[staticmethod] + fn all_hooks() -> Self { + Self(rapier::pipeline::ActiveHooks::all()) + } + #[classattr] + const FILTER_CONTACT_PAIR: ActiveHooks = + ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_CONTACT_PAIRS); + #[classattr] + const FILTER_CONTACT_PAIRS: ActiveHooks = + ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_CONTACT_PAIRS); + #[classattr] + const FILTER_INTERSECTION_PAIR: ActiveHooks = + ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_INTERSECTION_PAIR); + #[classattr] + const MODIFY_SOLVER_CONTACTS: ActiveHooks = + ActiveHooks(rapier::pipeline::ActiveHooks::MODIFY_SOLVER_CONTACTS); + #[classattr] + const NONE: ActiveHooks = ActiveHooks(rapier::pipeline::ActiveHooks::empty()); + + /// Raw bit pattern of the flag set. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// True iff all flags in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no flags are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + fn __invert__(&self) -> Self { + Self(!self.0) + } + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("ActiveHooks supports only == and !=")), + } + } + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + fn __repr__(&self) -> String { + format!("ActiveHooks(bits={:#010b})", self.0.bits()) + } +} + +// ============================================================ +// ActiveCollisionTypes (bitflags) +// ============================================================ + +/// Per-collider flags selecting which *rigid-body type* combinations +/// generate contacts. +/// +/// By default only pairs involving at least one dynamic body collide +/// (`DYNAMIC_DYNAMIC | DYNAMIC_KINEMATIC | DYNAMIC_FIXED`). Enable e.g. +/// `KINEMATIC_FIXED` or `KINEMATIC_KINEMATIC` to detect contacts that +/// would otherwise be skipped — useful for kinematic character or +/// platform queries. Combine flags via `|`. +#[pyclass(name = "ActiveCollisionTypes", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActiveCollisionTypes(pub rapier::geometry::ActiveCollisionTypes); + +#[pymethods] +impl ActiveCollisionTypes { + /// Construct from a raw bit pattern. Unknown bits are dropped. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u16) -> Self { + Self(rapier::geometry::ActiveCollisionTypes::from_bits_truncate( + bits, + )) + } + /// Return the empty flag set (no collision types active). + #[staticmethod] + fn empty() -> Self { + Self(rapier::geometry::ActiveCollisionTypes::empty()) + } + /// Return the flag set with every collision type enabled. + #[staticmethod] + fn all() -> Self { + Self(rapier::geometry::ActiveCollisionTypes::all()) + } + /// Return the default flag set: every pair involving at least one + /// dynamic body (`DYNAMIC_DYNAMIC | DYNAMIC_KINEMATIC | DYNAMIC_FIXED`). + #[staticmethod] + fn default_types() -> Self { + Self(rapier::geometry::ActiveCollisionTypes::default()) + } + #[classattr] + const DYNAMIC_DYNAMIC: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_DYNAMIC); + #[classattr] + const DYNAMIC_KINEMATIC: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_KINEMATIC); + #[classattr] + const DYNAMIC_FIXED: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_FIXED); + #[classattr] + const KINEMATIC_KINEMATIC: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::KINEMATIC_KINEMATIC); + #[classattr] + const KINEMATIC_FIXED: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::KINEMATIC_FIXED); + #[classattr] + const FIXED_FIXED: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::FIXED_FIXED); + #[classattr] + const NONE: ActiveCollisionTypes = + ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::empty()); + + /// Raw bit pattern of the flag set. + #[getter] + fn bits(&self) -> u16 { + self.0.bits() + } + /// True iff all flags in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no flags are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + fn __invert__(&self) -> Self { + Self(!self.0) + } + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "ActiveCollisionTypes supports only == and !=", + )), + } + } + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + fn __repr__(&self) -> String { + format!("ActiveCollisionTypes(bits={:#018b})", self.0.bits()) + } +} + +// ============================================================ +// Group (bitflags) +// ============================================================ + +/// 32-bit bitmask identifying one or more collision groups. +/// +/// Used as both the *membership* and *filter* side of an +/// `InteractionGroups`: two colliders interact iff each one's +/// memberships intersect the other's filter. Combine groups via +/// `|`, take complements via `~`, and use `ALL` / `NONE` for the +/// universe / empty set. +#[pyclass(name = "Group", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Group(pub rapier::geometry::Group); + +#[pymethods] +impl Group { + /// Construct from a raw bit pattern. All 32 bits are retained. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::geometry::Group::from_bits_retain(bits)) + } + + /// Construct from a raw bit pattern. All 32 bits are retained. + #[staticmethod] + fn from_bits(bits: u32) -> Self { + Self(rapier::geometry::Group::from_bits_retain(bits)) + } + + // Rapier's Group constants are 1-indexed (GROUP_1..GROUP_32). + // The spec asks for `GROUP_0..GROUP_31`. Expose both spellings to + // bridge both worlds. + #[classattr] + const GROUP_0: Group = Group(rapier::geometry::Group::GROUP_1); + #[classattr] + const GROUP_1: Group = Group(rapier::geometry::Group::GROUP_2); + #[classattr] + const GROUP_2: Group = Group(rapier::geometry::Group::GROUP_3); + #[classattr] + const GROUP_3: Group = Group(rapier::geometry::Group::GROUP_4); + #[classattr] + const GROUP_4: Group = Group(rapier::geometry::Group::GROUP_5); + #[classattr] + const GROUP_5: Group = Group(rapier::geometry::Group::GROUP_6); + #[classattr] + const GROUP_6: Group = Group(rapier::geometry::Group::GROUP_7); + #[classattr] + const GROUP_7: Group = Group(rapier::geometry::Group::GROUP_8); + #[classattr] + const GROUP_8: Group = Group(rapier::geometry::Group::GROUP_9); + #[classattr] + const GROUP_9: Group = Group(rapier::geometry::Group::GROUP_10); + #[classattr] + const GROUP_10: Group = Group(rapier::geometry::Group::GROUP_11); + #[classattr] + const GROUP_11: Group = Group(rapier::geometry::Group::GROUP_12); + #[classattr] + const GROUP_12: Group = Group(rapier::geometry::Group::GROUP_13); + #[classattr] + const GROUP_13: Group = Group(rapier::geometry::Group::GROUP_14); + #[classattr] + const GROUP_14: Group = Group(rapier::geometry::Group::GROUP_15); + #[classattr] + const GROUP_15: Group = Group(rapier::geometry::Group::GROUP_16); + #[classattr] + const GROUP_16: Group = Group(rapier::geometry::Group::GROUP_17); + #[classattr] + const GROUP_17: Group = Group(rapier::geometry::Group::GROUP_18); + #[classattr] + const GROUP_18: Group = Group(rapier::geometry::Group::GROUP_19); + #[classattr] + const GROUP_19: Group = Group(rapier::geometry::Group::GROUP_20); + #[classattr] + const GROUP_20: Group = Group(rapier::geometry::Group::GROUP_21); + #[classattr] + const GROUP_21: Group = Group(rapier::geometry::Group::GROUP_22); + #[classattr] + const GROUP_22: Group = Group(rapier::geometry::Group::GROUP_23); + #[classattr] + const GROUP_23: Group = Group(rapier::geometry::Group::GROUP_24); + #[classattr] + const GROUP_24: Group = Group(rapier::geometry::Group::GROUP_25); + #[classattr] + const GROUP_25: Group = Group(rapier::geometry::Group::GROUP_26); + #[classattr] + const GROUP_26: Group = Group(rapier::geometry::Group::GROUP_27); + #[classattr] + const GROUP_27: Group = Group(rapier::geometry::Group::GROUP_28); + #[classattr] + const GROUP_28: Group = Group(rapier::geometry::Group::GROUP_29); + #[classattr] + const GROUP_29: Group = Group(rapier::geometry::Group::GROUP_30); + #[classattr] + const GROUP_30: Group = Group(rapier::geometry::Group::GROUP_31); + #[classattr] + const GROUP_31: Group = Group(rapier::geometry::Group::GROUP_32); + + #[classattr] + const ALL: Group = Group(rapier::geometry::Group::ALL); + #[classattr] + const NONE: Group = Group(rapier::geometry::Group::NONE); + + /// Raw 32-bit pattern of the group set. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// True iff all groups in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no group bit is set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + fn __invert__(&self) -> Self { + Self(!self.0) + } + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("Group supports only == and !=")), + } + } + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + fn __repr__(&self) -> String { + format!("Group(bits={:#034b})", self.0.bits()) + } +} + +// ============================================================ +// InteractionTestMode +// ============================================================ + +/// How `InteractionGroups.test` combines the two pair-wise checks. +/// +/// `AND` (the default) requires each collider to be in the other's +/// filter set. `OR` only requires one side to pass. `DEFAULT` and +/// `ONLY_DYNAMIC` are kept for compatibility and behave like `AND`. +#[pyclass(name = "InteractionTestMode", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InteractionTestMode { + DEFAULT, + ONLY_DYNAMIC, + AND, + OR, +} + +impl InteractionTestMode { + pub(crate) fn to_rapier(self) -> rapier::geometry::InteractionTestMode { + match self { + Self::DEFAULT | Self::AND | Self::ONLY_DYNAMIC => { + rapier::geometry::InteractionTestMode::And + } + Self::OR => rapier::geometry::InteractionTestMode::Or, + } + } + #[allow(dead_code)] + pub(crate) fn from_rapier(t: rapier::geometry::InteractionTestMode) -> Self { + match t { + rapier::geometry::InteractionTestMode::And => Self::DEFAULT, + rapier::geometry::InteractionTestMode::Or => Self::OR, + } + } +} + +// ============================================================ +// InteractionGroups +// ============================================================ + +/// Pair (memberships, filter) used to selectively enable contacts. +/// +/// A collider with memberships M and filter F interacts with another +/// collider (memberships M', filter F') iff +/// `(M & F') != 0 and (M' & F) != 0` (the default `AND` mode). +/// In short: a pair interacts iff each one passes the other's filter. +/// Used for both `collision_groups` and `solver_groups` on a collider. +#[pyclass(name = "InteractionGroups", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct InteractionGroups(pub rapier::geometry::InteractionGroups); + +#[pymethods] +impl InteractionGroups { + /// Build a new interaction-groups value. + /// + /// :param memberships: Which groups this collider belongs to. + /// Defaults to `Group.ALL`. + /// :param filter: Which groups this collider is willing to + /// interact with. Defaults to `Group.ALL`. + /// :param test_mode: How both sides are combined. Defaults to + /// `InteractionTestMode.AND`. + #[new] + #[pyo3(signature = (memberships=None, filter=None, test_mode=None))] + fn new( + memberships: Option, + filter: Option, + test_mode: Option, + ) -> Self { + let m = memberships + .map(|g| g.0) + .unwrap_or(rapier::geometry::Group::ALL); + let f = filter.map(|g| g.0).unwrap_or(rapier::geometry::Group::ALL); + let mode = test_mode + .map(|m| m.to_rapier()) + .unwrap_or(rapier::geometry::InteractionTestMode::And); + Self(rapier::geometry::InteractionGroups::new(m, f, mode)) + } + + /// Interaction groups that match everything (memberships = filter = ALL). + #[staticmethod] + fn all() -> Self { + Self(rapier::geometry::InteractionGroups::all()) + } + /// Interaction groups that match nothing (memberships = filter = NONE). + #[staticmethod] + fn none() -> Self { + Self(rapier::geometry::InteractionGroups::none()) + } + + /// Return a copy with `memberships` replaced. Alias of `with_memberships`. + fn with_membership(&self, g: Group) -> Self { + Self(self.0.with_memberships(g.0)) + } + /// Return a copy with `memberships` replaced. + fn with_memberships(&self, g: Group) -> Self { + Self(self.0.with_memberships(g.0)) + } + /// Return a copy with `filter` replaced. + fn with_filter(&self, g: Group) -> Self { + Self(self.0.with_filter(g.0)) + } + + /// Groups this collider is a member of. + #[getter] + fn memberships(&self) -> Group { + Group(self.0.memberships) + } + #[setter] + fn set_memberships(&mut self, g: Group) { + self.0.memberships = g.0; + } + /// Groups this collider is willing to interact with. + #[getter] + fn filter(&self) -> Group { + Group(self.0.filter) + } + #[setter] + fn set_filter(&mut self, g: Group) { + self.0.filter = g.0; + } + /// How the two pair-wise group checks are combined. + #[getter] + fn test_mode(&self) -> InteractionTestMode { + InteractionTestMode::from_rapier(self.0.test_mode) + } + #[setter] + fn set_test_mode(&mut self, m: InteractionTestMode) { + self.0.test_mode = m.to_rapier(); + } + + /// Test whether `self` and `other` should interact. + /// + /// :returns: True iff both sides pass the other's filter under + /// the active `test_mode`. + fn test(&self, other: &Self) -> bool { + self.0.test(other.0) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "InteractionGroups supports only == and !=", + )), + } + } + fn __repr__(&self) -> String { + format!( + "InteractionGroups(memberships={:#010x}, filter={:#010x})", + self.0.memberships.bits(), + self.0.filter.bits(), + ) + } +} + +// ============================================================ +// CollisionEventFlags (bitflags) +// ============================================================ + +/// Side-flags attached to each `CollisionEvent`. +/// +/// `SENSOR` indicates the event involves a sensor collider; +/// `REMOVED` indicates the event was emitted because one of the +/// colliders was removed mid-frame. +#[pyclass(name = "CollisionEventFlags", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CollisionEventFlags(pub rapier::geometry::CollisionEventFlags); + +#[pymethods] +impl CollisionEventFlags { + /// Construct from a raw bit pattern. Unknown bits are dropped. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::geometry::CollisionEventFlags::from_bits_truncate( + bits, + )) + } + /// Return the empty flag set. + #[staticmethod] + fn empty() -> Self { + Self(rapier::geometry::CollisionEventFlags::empty()) + } + #[classattr] + const SENSOR: CollisionEventFlags = + CollisionEventFlags(rapier::geometry::CollisionEventFlags::SENSOR); + #[classattr] + const REMOVED: CollisionEventFlags = + CollisionEventFlags(rapier::geometry::CollisionEventFlags::REMOVED); + + /// Raw bit pattern of the flag set. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// True iff all flags in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no flags are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "CollisionEventFlags supports only == and !=", + )), + } + } + fn __repr__(&self) -> String { + format!("CollisionEventFlags(bits={:#010b})", self.0.bits()) + } +} + +// ============================================================ +// TriMeshFlags (bitflags) +// ============================================================ + +/// Build-time flags controlling triangle-mesh preprocessing. +/// +/// Passed to `SharedShape.trimesh(..., flags=...)` or +/// `MeshConverter.trimesh_with_flags(...)`. Toggle topology +/// validation, duplicate cleanup, edge-normal fixups, etc. +#[pyclass(name = "TriMeshFlags", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TriMeshFlags(pub rapier::parry::shape::TriMeshFlags); + +#[pymethods] +impl TriMeshFlags { + /// Construct from a raw bit pattern. Unknown bits are dropped. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u16) -> Self { + Self(rapier::parry::shape::TriMeshFlags::from_bits_truncate(bits)) + } + /// Return the empty flag set. + #[staticmethod] + fn empty() -> Self { + Self(rapier::parry::shape::TriMeshFlags::empty()) + } + + #[classattr] + const HALF_EDGE_TOPOLOGY: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::HALF_EDGE_TOPOLOGY); + #[classattr] + const CONNECTED_COMPONENTS: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::CONNECTED_COMPONENTS); + #[classattr] + const DELETE_BAD_TOPOLOGY_TRIANGLES: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES); + #[classattr] + const ORIENTED: TriMeshFlags = TriMeshFlags(rapier::parry::shape::TriMeshFlags::ORIENTED); + #[classattr] + const MERGE_DUPLICATE_VERTICES: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::MERGE_DUPLICATE_VERTICES); + #[classattr] + const DELETE_DEGENERATE_TRIANGLES: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_DEGENERATE_TRIANGLES); + #[classattr] + const DELETE_DUPLICATE_TRIANGLES: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_DUPLICATE_TRIANGLES); + #[classattr] + const FIX_INTERNAL_EDGES: TriMeshFlags = + TriMeshFlags(rapier::parry::shape::TriMeshFlags::FIX_INTERNAL_EDGES); + + /// Raw bit pattern of the flag set. + #[getter] + fn bits(&self) -> u16 { + self.0.bits() + } + /// True iff all flags in `other` are also set on `self`. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// True iff no flags are set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err("TriMeshFlags supports only == and !=")), + } + } + fn __repr__(&self) -> String { + format!("TriMeshFlags(bits={:#018b})", self.0.bits()) + } +} + +// ============================================================ +// ColliderMaterial +// ============================================================ + +/// Surface material: friction, restitution, and their combine rules. +/// +/// When two colliders touch, the engine combines their material +/// values using the configured `CoefficientCombineRule` (e.g. +/// `MAX`, `MIN`, `AVERAGE`, `MULTIPLY`). +#[pyclass(name = "ColliderMaterial", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct ColliderMaterial(pub rapier::geometry::ColliderMaterial); + +#[pymethods] +impl ColliderMaterial { + /// :param friction: Friction coefficient. Defaults to 1.0. + /// :param restitution: Restitution (bounciness). Defaults to 0.0. + #[new] + #[pyo3(signature = (friction=1.0 as Real, restitution=0.0 as Real))] + fn new(friction: Real, restitution: Real) -> Self { + Self(rapier::geometry::ColliderMaterial::new( + friction, + restitution, + )) + } + + /// Friction coefficient. + #[getter] + fn friction(&self) -> Real { + self.0.friction + } + #[setter] + fn set_friction(&mut self, v: Real) { + self.0.friction = v; + } + /// Restitution (bounciness) in `[0, 1]`. + #[getter] + fn restitution(&self) -> Real { + self.0.restitution + } + #[setter] + fn set_restitution(&mut self, v: Real) { + self.0.restitution = v; + } + + /// Rule used to combine friction coefficients with another collider. + #[getter] + fn friction_combine_rule(&self) -> CoefficientCombineRule { + CoefficientCombineRule::from_rapier(self.0.friction_combine_rule) + } + #[setter] + fn set_friction_combine_rule(&mut self, v: CoefficientCombineRule) { + self.0.friction_combine_rule = v.to_rapier(); + } + /// Rule used to combine restitution with another collider. + #[getter] + fn restitution_combine_rule(&self) -> CoefficientCombineRule { + CoefficientCombineRule::from_rapier(self.0.restitution_combine_rule) + } + #[setter] + fn set_restitution_combine_rule(&mut self, v: CoefficientCombineRule) { + self.0.restitution_combine_rule = v.to_rapier(); + } + + fn __repr__(&self) -> String { + format!( + "ColliderMaterial(friction={}, restitution={})", + self.0.friction, self.0.restitution, + ) + } +} + +// ============================================================ +// BvhOptimizationStrategy (enum) +// ============================================================ + +/// Strategy used by `BroadPhaseBvh` to keep its bounding hierarchy tidy. +/// +/// `AUTO` lets the engine pick a reasonable default; the other +/// variants force a specific behavior. +#[pyclass(name = "BvhOptimizationStrategy", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BvhOptimizationStrategy { + AUTO, + NO_OPTIMIZATION, + OPTIMIZE_FOR_INTERACTIONS, + OPTIMIZE_FOR_SPATIAL_QUERIES, + SUBTREE_OPTIMIZER, + NONE, +} + +impl BvhOptimizationStrategy { + pub(crate) fn to_rapier(self) -> rapier::geometry::BvhOptimizationStrategy { + match self { + Self::AUTO + | Self::OPTIMIZE_FOR_INTERACTIONS + | Self::OPTIMIZE_FOR_SPATIAL_QUERIES + | Self::SUBTREE_OPTIMIZER => { + rapier::geometry::BvhOptimizationStrategy::SubtreeOptimizer + } + Self::NO_OPTIMIZATION | Self::NONE => rapier::geometry::BvhOptimizationStrategy::None, + } + } +} + +// ============================================================ +// BroadPhaseBvh +// ============================================================ + +/// BVH-based broad-phase: emits candidate collider pairs each step. +/// +/// Usually accessed via `world.broad_phase`. Create one explicitly +/// only if you are driving the pipeline yourself. +#[pyclass(name = "BroadPhaseBvh", module = "rapier", unsendable)] +pub struct BroadPhaseBvh(pub rapier::geometry::BroadPhaseBvh); + +#[pymethods] +impl BroadPhaseBvh { + /// Construct a broad-phase with the default optimization strategy. + #[new] + fn new() -> Self { + Self(rapier::geometry::BroadPhaseBvh::new()) + } + + /// Construct a broad-phase tuned for `strategy`. + #[staticmethod] + fn optimized_for(strategy: BvhOptimizationStrategy) -> Self { + Self(rapier::geometry::BroadPhaseBvh::with_optimization_strategy( + strategy.to_rapier(), + )) + } + + /// Reset the broad-phase to an empty state. Equivalent to constructing a new one. + fn clear(&mut self) { + self.0 = rapier::geometry::BroadPhaseBvh::new(); + } +} + +// ============================================================ +// NarrowPhase +// ============================================================ + +/// Narrow-phase: computes per-pair contact manifolds and intersections. +/// +/// Usually accessed via `world.narrow_phase`. Use it to query +/// existing contact pairs and intersections between specific +/// colliders. +#[pyclass(name = "NarrowPhase", module = "rapier", unsendable)] +pub struct NarrowPhase(pub rapier::geometry::NarrowPhase); + +#[pymethods] +impl NarrowPhase { + /// Construct an empty narrow-phase. + #[new] + fn new() -> Self { + Self(rapier::geometry::NarrowPhase::new()) + } + + /// Return the contact pair between two colliders, if any. + /// + /// :returns: A snapshot `ContactPair` or `None` if the pair is + /// not currently tracked by the narrow-phase. + fn contact_pair(&self, h1: &ColliderHandle, h2: &ColliderHandle) -> Option { + self.0.contact_pair(h1.0, h2.0).cloned().map(ContactPair) + } + + /// Return whether two sensor/intersection colliders intersect. + /// + /// :returns: `True`/`False` if the pair is tracked, or `None` + /// when no intersection pair is being maintained. + fn intersection_pair(&self, h1: &ColliderHandle, h2: &ColliderHandle) -> Option { + self.0.intersection_pair(h1.0, h2.0) + } + + /// Snapshot every active contact pair as a list. + fn contact_pairs(&self) -> Vec { + self.0.contact_pairs().cloned().map(ContactPair).collect() + } + + /// Snapshot every tracked sensor/intersection pair. + /// + /// :returns: List of `(collider1, collider2, intersecting)` tuples. + fn intersection_pairs(&self) -> Vec<(ColliderHandle, ColliderHandle, bool)> { + self.0 + .intersection_pairs() + .map(|(h1, h2, i)| (ColliderHandle(h1), ColliderHandle(h2), i)) + .collect() + } + + /// Reset the narrow-phase to an empty state. Equivalent to + /// constructing a new one — the existing `NarrowPhase` doesn't + /// expose `clear()` directly. + fn clear(&mut self) { + self.0 = rapier::geometry::NarrowPhase::new(); + } +} + +// ============================================================ +// ColliderPair (named-tuple-like) +// ============================================================ + +/// Ordered pair of ``ColliderHandle`` values, indexable like a tuple. +/// +/// Used by ``BroadPhasePairEvent`` and various filter callbacks. +/// ``pair[0]`` returns ``collider1``, ``pair[1]`` returns ``collider2``. +#[pyclass(name = "ColliderPair", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct ColliderPair { + #[pyo3(get)] + pub collider1: ColliderHandle, + #[pyo3(get)] + pub collider2: ColliderHandle, +} + +#[pymethods] +impl ColliderPair { + /// Build a pair from two collider handles. + #[new] + fn new(collider1: ColliderHandle, collider2: ColliderHandle) -> Self { + Self { + collider1, + collider2, + } + } + fn __repr__(&self) -> String { + format!( + "ColliderPair(collider1={:?}, collider2={:?})", + self.collider1.0.into_raw_parts(), + self.collider2.0.into_raw_parts() + ) + } + fn __getitem__(&self, idx: isize) -> PyResult { + match idx { + 0 | -2 => Ok(self.collider1), + 1 | -1 => Ok(self.collider2), + _ => Err(PyIndexError::new_err("ColliderPair index out of range")), + } + } + fn __len__(&self) -> usize { + 2 + } +} + +// ============================================================ +// BroadPhasePairEvent (tagged enum) +// ============================================================ + +/// Pair-event emitted by the broad-phase when an AABB overlap +/// starts or stops. +/// +/// `added=True` corresponds to "Added"; `added=False` to "Removed". +/// These are coarse, AABB-level events — for actual contacts use +/// `CollisionEvent` and `ContactPair`. +#[pyclass(name = "BroadPhasePairEvent", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct BroadPhasePairEvent { + /// True iff this is an "Added" event; False for "Removed". + #[pyo3(get)] + pub added: bool, + #[pyo3(get)] + pub pair: ColliderPair, +} + +#[pymethods] +impl BroadPhasePairEvent { + /// Build an "Added" pair event. + #[staticmethod] + fn added(pair: ColliderPair) -> Self { + Self { added: true, pair } + } + /// Build a "Removed" pair event. + #[staticmethod] + fn removed(pair: ColliderPair) -> Self { + Self { added: false, pair } + } + /// True iff this is a "Removed" event (the inverse of `added`). + #[getter] + fn removed_(&self) -> bool { + !self.added + } + fn __repr__(&self) -> String { + if self.added { + format!( + "BroadPhasePairEvent.Added({:?})", + self.pair.collider1.0.into_raw_parts() + ) + } else { + format!( + "BroadPhasePairEvent.Removed({:?})", + self.pair.collider1.0.into_raw_parts() + ) + } + } +} + +// ============================================================ +// CollisionEvent (tagged union) +// ============================================================ + +/// Contact-level event flagging a new or ended collision pair. +/// +/// Only emitted for pairs where at least one collider has +/// `ActiveEvents.COLLISION_EVENTS` set. Inspect `started` to +/// distinguish "started" from "stopped", and `flags` for +/// extra context (`SENSOR`, `REMOVED`). +#[pyclass(name = "CollisionEvent", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct CollisionEvent { + #[pyo3(get)] + pub started: bool, + #[pyo3(get)] + pub collider1: ColliderHandle, + #[pyo3(get)] + pub collider2: ColliderHandle, + #[pyo3(get)] + pub flags: CollisionEventFlags, +} + +#[pymethods] +impl CollisionEvent { + /// Construct a "Started" event. + /// + /// Fired when two colliders begin overlapping at the contact + /// level. Requires `ActiveEvents.COLLISION_EVENTS` on at least + /// one of the colliders. + #[staticmethod] + #[pyo3(name = "STARTED")] + fn started_evt(c1: ColliderHandle, c2: ColliderHandle, flags: CollisionEventFlags) -> Self { + Self { + started: true, + collider1: c1, + collider2: c2, + flags, + } + } + /// Construct a "Stopped" event. + /// + /// Fired when two colliders stop overlapping (or one is + /// removed). Requires `ActiveEvents.COLLISION_EVENTS` on at + /// least one of the colliders. + #[staticmethod] + #[pyo3(name = "STOPPED")] + fn stopped_evt(c1: ColliderHandle, c2: ColliderHandle, flags: CollisionEventFlags) -> Self { + Self { + started: false, + collider1: c1, + collider2: c2, + flags, + } + } + + /// True iff this is a "Stopped" event (the inverse of `started`). + #[getter] + fn stopped(&self) -> bool { + !self.started + } + /// True iff the event involves a sensor collider. + #[getter] + fn sensor(&self) -> bool { + self.flags + .0 + .contains(rapier::geometry::CollisionEventFlags::SENSOR) + } + /// True iff the event was triggered by a collider removal. + #[getter] + fn removed(&self) -> bool { + self.flags + .0 + .contains(rapier::geometry::CollisionEventFlags::REMOVED) + } + + fn __repr__(&self) -> String { + let kind = if self.started { "Started" } else { "Stopped" }; + format!( + "CollisionEvent.{kind}(c1={:?}, c2={:?})", + self.collider1.0.into_raw_parts(), + self.collider2.0.into_raw_parts(), + ) + } +} + +impl CollisionEvent { + #[allow(dead_code)] + pub(crate) fn from_rapier(e: rapier::geometry::CollisionEvent) -> Self { + match e { + rapier::geometry::CollisionEvent::Started(c1, c2, f) => Self { + started: true, + collider1: ColliderHandle(c1), + collider2: ColliderHandle(c2), + flags: CollisionEventFlags(f), + }, + rapier::geometry::CollisionEvent::Stopped(c1, c2, f) => Self { + started: false, + collider1: ColliderHandle(c1), + collider2: ColliderHandle(c2), + flags: CollisionEventFlags(f), + }, + } + } +} + +// ============================================================ +// ColliderFlags (struct-of-fields, not a bitflags wrapper — +// mirrors rapier's `ColliderFlags` struct exactly). +// ============================================================ + +/// Bundle of the configurable flags on a `Collider`. +/// +/// Aggregates `collision_groups`, `solver_groups`, `active_hooks`, +/// `active_events`, and the read-only `enabled` state. This is a +/// value type — mutating it does not retroactively affect the +/// collider it was read from. +#[pyclass(name = "ColliderFlags", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct ColliderFlags(pub rapier::geometry::ColliderFlags); + +#[pymethods] +impl ColliderFlags { + /// Construct with default values (all groups allowed, no hooks/events). + #[new] + fn new() -> Self { + Self(rapier::geometry::ColliderFlags::default()) + } + + /// Groups controlling which colliders generate contact pairs. + #[getter] + fn collision_groups(&self) -> InteractionGroups { + InteractionGroups(self.0.collision_groups) + } + #[setter] + fn set_collision_groups(&mut self, v: InteractionGroups) { + self.0.collision_groups = v.0; + } + /// Groups controlling which contact pairs reach the solver. + #[getter] + fn solver_groups(&self) -> InteractionGroups { + InteractionGroups(self.0.solver_groups) + } + #[setter] + fn set_solver_groups(&mut self, v: InteractionGroups) { + self.0.solver_groups = v.0; + } + /// Hooks this collider opts into (see `ActiveHooks`). + #[getter] + fn active_hooks(&self) -> ActiveHooks { + ActiveHooks(self.0.active_hooks) + } + #[setter] + fn set_active_hooks(&mut self, v: ActiveHooks) { + self.0.active_hooks = v.0; + } + /// Rigid-body type combinations enabled (see `ActiveCollisionTypes`). + #[getter] + fn active_collision_types(&self) -> ActiveCollisionTypes { + ActiveCollisionTypes(self.0.active_collision_types) + } + #[setter] + fn set_active_collision_types(&mut self, v: ActiveCollisionTypes) { + self.0.active_collision_types = v.0; + } + /// Events this collider opts into (see `ActiveEvents`). + #[getter] + fn active_events(&self) -> ActiveEvents { + ActiveEvents(self.0.active_events) + } + #[setter] + fn set_active_events(&mut self, v: ActiveEvents) { + self.0.active_events = v.0; + } + /// Current enabled state. Read-only mirror of `Collider.is_enabled`. + #[getter] + fn enabled(&self) -> ColliderEnabled { + ColliderEnabled::from_rapier(self.0.enabled) + } +} + +// Dim+scalar-specific helpers that pick the right ndarray extractor +// and convert into the engine's `Vector` type. + +/// Extract vertex array (N, 3) and convert into `Vec`. +/// +/// Accepts both `float32` and `float64` ndarrays (cast to `Real`), as +/// well as list/tuple of triples. +#[allow(dead_code)] +pub(crate) fn extract_verts_for_dim( + obj: &crate::pyo3::Bound<'_, crate::pyo3::PyAny>, +) -> crate::pyo3::PyResult> { + use crate::numpy::{PyReadonlyArray2, PyUntypedArrayMethods}; + // Try the cdylib's matching-precision ndarray first (zero-copy). + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 3 { + return Err(crate::pyo3::exceptions::PyValueError::new_err(format!( + "expected ndarray with shape (N, 3); got (N, {ncols})" + ))); + } + let slice = arr.as_slice().map_err(|_| { + crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") + })?; + let mut out = Vec::with_capacity(nrows); + for c in slice.chunks_exact(3) { + out.push(rapier::math::Vector::new(c[0], c[1], c[2])); + } + return Ok(out); + } + // Try the other precision (lossy cast). + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 3 { + return Err(crate::pyo3::exceptions::PyValueError::new_err(format!( + "expected ndarray with shape (N, 3); got (N, {ncols})" + ))); + } + let slice = arr.as_slice().map_err(|_| { + crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") + })?; + let mut out = Vec::with_capacity(nrows); + for c in slice.chunks_exact(3) { + out.push(rapier::math::Vector::new( + c[0] as Real, + c[1] as Real, + c[2] as Real, + )); + } + return Ok(out); + } + if let Ok(arr) = obj.extract::>() { + let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); + if ncols != 3 { + return Err(crate::pyo3::exceptions::PyValueError::new_err(format!( + "expected ndarray with shape (N, 3); got (N, {ncols})" + ))); + } + let slice = arr.as_slice().map_err(|_| { + crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") + })?; + let mut out = Vec::with_capacity(nrows); + for c in slice.chunks_exact(3) { + out.push(rapier::math::Vector::new( + c[0] as Real, + c[1] as Real, + c[2] as Real, + )); + } + return Ok(out); + } + // Fall back to list/tuple of tuples / any sequence of length 3. + let seq: Vec<(Real, Real, Real)> = obj.extract()?; + Ok(seq + .into_iter() + .map(|(x, y, z)| rapier::math::Vector::new(x, y, z)) + .collect()) +} + +// ============================================================ +// Ball +// ============================================================ + +/// Sphere (3D) / disk (2D) shape view. +/// +/// Inspection-only — typically obtained via `shape.as_ball()`. +#[pyclass(name = "Ball", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Ball(pub rapier::parry::shape::Ball); + +#[pymethods] +impl Ball { + /// Build a ball of the given radius. + #[new] + fn new(radius: Real) -> Self { + Self(rapier::parry::shape::Ball::new(radius)) + } + /// Radius. + #[getter] + fn radius(&self) -> Real { + self.0.radius + } + fn __repr__(&self) -> String { + format!("Ball(radius={})", self.0.radius) + } +} + +// ============================================================ +// Cuboid +// ============================================================ + +/// Axis-aligned box (3D) / rectangle (2D) shape view. +/// +/// Inspection-only — typically obtained via `shape.as_cuboid()`. +/// The cuboid spans `[-half_extents, +half_extents]` in its local frame. +#[pyclass(name = "Cuboid", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Cuboid(pub rapier::parry::shape::Cuboid); + +#[pymethods] +impl Cuboid { + /// Build a cuboid from its half-extents vector. + #[new] + fn new(half_extents: PyVector) -> Self { + Self(rapier::parry::shape::Cuboid::new(half_extents.0.into())) + } + /// Half-extents along each local axis. + #[getter] + fn half_extents(&self) -> Vec3 { + let v: crate::na::SVector = self.0.half_extents.into(); + Vec3(v) + } + fn __repr__(&self) -> String { + format!("Cuboid(half_extents={:?})", self.0.half_extents) + } +} + +// ============================================================ +// Capsule +// ============================================================ + +/// Capsule shape view (a swept sphere between two endpoints). +/// +/// Inspection-only — typically obtained via `shape.as_capsule()`. +/// Construct via the explicit `(a, b, radius)` form or via the +/// `SharedShape.capsule_x` / `capsule_y` / `capsule_z` factories +/// to align it with a specific axis. +#[pyclass(name = "Capsule", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Capsule(pub rapier::parry::shape::Capsule); + +#[pymethods] +impl Capsule { + /// Build a capsule from its two endpoints and a radius. + #[new] + fn new(a: PyVector, b: PyVector, radius: Real) -> Self { + Self(rapier::parry::shape::Capsule::new( + a.0.into(), + b.0.into(), + radius, + )) + } + /// Radius of the spherical caps. + #[getter] + fn radius(&self) -> Real { + self.0.radius + } + /// Half the length of the inner segment. + #[getter] + fn half_height(&self) -> Real { + self.0.half_height() + } + /// Length of the inner segment. + #[getter] + fn height(&self) -> Real { + self.0.height() + } + /// First endpoint of the inner segment. + #[getter] + fn a(&self) -> Point3 { + let v: crate::na::SVector = self.0.segment.a.into(); + Point3(crate::na::Point::from(v)) + } + /// Second endpoint of the inner segment. + #[getter] + fn b(&self) -> Point3 { + let v: crate::na::SVector = self.0.segment.b.into(); + Point3(crate::na::Point::from(v)) + } + fn __repr__(&self) -> String { + format!( + "Capsule(half_height={}, radius={})", + self.0.half_height(), + self.0.radius + ) + } +} + +// ============================================================ +// Triangle +// ============================================================ + +/// Triangle shape view. +/// +/// Inspection-only — typically obtained via `shape.as_triangle()`. +#[pyclass(name = "Triangle", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Triangle(pub rapier::parry::shape::Triangle); + +#[pymethods] +impl Triangle { + /// Build a triangle from its three vertices. + #[new] + fn new(a: PyVector, b: PyVector, c: PyVector) -> Self { + Self(rapier::parry::shape::Triangle::new( + a.0.into(), + b.0.into(), + c.0.into(), + )) + } + /// First vertex. + #[getter] + fn a(&self) -> Point3 { + let v: crate::na::SVector = self.0.a.into(); + Point3(crate::na::Point::from(v)) + } + /// Second vertex. + #[getter] + fn b(&self) -> Point3 { + let v: crate::na::SVector = self.0.b.into(); + Point3(crate::na::Point::from(v)) + } + /// Third vertex. + #[getter] + fn c(&self) -> Point3 { + let v: crate::na::SVector = self.0.c.into(); + Point3(crate::na::Point::from(v)) + } +} + +// ============================================================ +// TriMesh view +// ============================================================ + +/// Triangle mesh shape view. +/// +/// Inspection-only — typically obtained via `shape.as_trimesh()`. +/// Construct new tri-meshes via `SharedShape.trimesh(...)`. +#[pyclass(name = "TriMesh", module = "rapier")] +#[derive(Clone)] +pub struct TriMesh(pub rapier::parry::shape::TriMesh); + +#[pymethods] +impl TriMesh { + /// Vertices as an `(N, D)` ndarray. + #[getter] + fn vertices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + let vs = self.0.vertices(); + let v: Vec> = vs + .iter() + .map(|p| { + let arr: [Real; 3] = (*p).into(); + arr.to_vec() + }) + .collect(); + PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") + } + /// Triangle indices as an `(M, 3)` ndarray of `u32`. + #[getter] + fn indices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + let idx = self.0.indices(); + let v: Vec> = idx.iter().map(|c| c.to_vec()).collect(); + PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") + } + /// Build-time preprocessing flags this mesh was created with. + #[getter] + fn flags(&self) -> TriMeshFlags { + TriMeshFlags(self.0.flags()) + } + /// Number of triangles. + fn num_triangles(&self) -> usize { + self.0.indices().len() + } + /// Number of vertices. + fn num_vertices(&self) -> usize { + self.0.vertices().len() + } +} + +// ============================================================ +// Voxels (voxel-grid shape) +// ============================================================ + +/// Voxel-grid shape view. +/// +/// Inspection-only — typically obtained via `shape.as_voxels()`. +/// Construct one with `SharedShape.voxels(...)` / +/// `SharedShape.voxels_from_points(...)` (or the matching +/// `Collider.*` builders). +#[pyclass(name = "Voxels", module = "rapier")] +#[derive(Clone)] +pub struct Voxels(pub rapier::parry::shape::Voxels); + +#[pymethods] +impl Voxels { + /// Per-axis size of one voxel cell, as a `(D,)` ndarray. + #[getter] + fn voxel_size<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray1> { + let s = self.0.voxel_size(); + let arr: [Real; 3] = s.into(); + crate::numpy::PyArray1::from_iter_bound(py, arr) + } + + /// Centers of all **filled** (non-empty) voxels, as an `(M, D)` + /// ndarray. Building one cuboid (of size `voxel_size`) per center + /// reconstructs the solid; empty cells are skipped. + #[getter] + fn centers<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray2> { + let v: Vec> = self + .0 + .voxels() + .filter(|vox| !vox.state.is_empty()) + .map(|vox| { + let arr: [Real; 3] = vox.center.into(); + arr.to_vec() + }) + .collect(); + crate::numpy::PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") + } + + /// Number of filled (non-empty) voxels. + fn num_voxels(&self) -> usize { + self.0.voxels().filter(|vox| !vox.state.is_empty()).count() + } +} + +// ============================================================ +// HeightField (2D: 1-D heights; 3D: 2-D heights) +// ============================================================ + +/// Heightfield shape view. +/// +/// Inspection-only — typically obtained via `shape.as_heightfield()`. +/// In 2D the heights are a 1-D array; in 3D they are 2-D. Use +/// `SharedShape.heightfield(...)` to construct one. +#[pyclass(name = "HeightField", module = "rapier")] +#[derive(Clone)] +pub struct HeightField(pub rapier::parry::shape::HeightField); + +#[pymethods] +impl HeightField { + /// Per-axis scale applied to the height samples. + #[getter] + fn scale(&self) -> Vec3 { + let v: crate::na::SVector = self.0.scale().into(); + Vec3(v) + } +} + +// ============================================================ +// Compound +// ============================================================ + +/// Compound shape view — a collection of sub-shapes with local poses. +/// +/// Inspection-only — typically obtained via `shape.as_compound()`. +/// Build one via `SharedShape.compound(...)`. +#[pyclass(name = "Compound", module = "rapier")] +#[derive(Clone)] +pub struct Compound(pub rapier::parry::shape::Compound); + +#[pymethods] +impl Compound { + /// Number of sub-shapes. + fn num_shapes(&self) -> usize { + self.0.shapes().len() + } + + /// Returns the list of `(pose, sub_shape)` parts. + fn shapes(&self) -> Vec<(Isometry3, SharedShape)> { + self.0 + .shapes() + .iter() + .map(|(pose, shape)| { + let iso: crate::na::Isometry = (*pose).into(); + (Isometry3(iso), SharedShape(shape.clone())) + }) + .collect() + } +} + +// ============================================================ +// SharedShape +// ============================================================ + +/// Reference-counted handle to any collision shape. +/// +/// `SharedShape` is the polymorphic type carried by every `Collider`. +/// Use the `ball`, `cuboid`, `capsule`, `trimesh`, ... static +/// constructors to build one, then downcast with `as_ball`, +/// `as_cuboid`, ... to inspect the concrete geometry. Cheap to +/// clone (it's an `Arc`). +#[pyclass(name = "SharedShape", module = "rapier")] +#[derive(Clone)] +pub struct SharedShape(pub rapier::parry::shape::SharedShape); + +#[pymethods] +impl SharedShape { + /// Discriminant identifying the concrete shape type. + #[getter] + fn shape_type(&self) -> ShapeType { + ShapeType::from_rapier(self.0.shape_type()) + } + + /// Build a ball / sphere shape. + #[staticmethod] + fn ball(radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::ball(radius)) + } + + /// Build a capsule from two endpoints and a radius. + /// + /// For axis-aligned capsules, prefer `capsule_x`, `capsule_y`, + /// or `capsule_z`. + #[staticmethod] + fn capsule(a: PyVector, b: PyVector, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::capsule( + a.0.into(), + b.0.into(), + radius, + )) + } + + /// Build a triangle from its three vertices. + #[staticmethod] + fn triangle(a: PyVector, b: PyVector, c: PyVector) -> Self { + Self(rapier::parry::shape::SharedShape::triangle( + a.0.into(), + b.0.into(), + c.0.into(), + )) + } + + /// Build a half-space (infinite plane) shape from its outward normal. + /// + /// The half-space is the unbounded region lying on the negative + /// side of the plane through the origin with the given outward + /// normal — i.e. the set of points ``p`` with + /// ``p · outward_normal <= 0``. Commonly used as an immovable + /// ground or wall. The normal is normalized automatically. + #[staticmethod] + fn halfspace(outward_normal: PyVector) -> Self { + let n = outward_normal.0.normalize(); + Self(rapier::parry::shape::SharedShape::halfspace(n.into())) + } + + /// Build a triangle with rounded edges of radius `border_radius`. + #[staticmethod] + fn round_triangle(a: PyVector, b: PyVector, c: PyVector, border_radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::round_triangle( + a.0.into(), + b.0.into(), + c.0.into(), + border_radius, + )) + } + + /// Build the convex hull of a point cloud, dilated by + /// `border_radius` (rounded corners/edges). + /// + /// :raises MeshConversionError: If the hull cannot be built. + #[staticmethod] + fn round_convex_hull(points: &Bound<'_, PyAny>, border_radius: Real) -> PyResult { + let pts = extract_verts_for_dim(points)?; + rapier::parry::shape::SharedShape::round_convex_hull(&pts, border_radius) + .map(Self) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err("round convex hull computation failed") + }) + } + + /// Build a voxel-grid shape by voxelizing a point cloud. + /// + /// :param voxel_size: Per-axis size of one voxel cell. + /// :param points: `(N, D)` ndarray of floats; each point marks the + /// voxel that contains it as filled. + #[staticmethod] + fn voxels_from_points(voxel_size: PyVector, points: &Bound<'_, PyAny>) -> PyResult { + let pts = extract_verts_for_dim(points)?; + Ok(Self(rapier::parry::shape::SharedShape::voxels_from_points( + voxel_size.0.into(), + &pts, + ))) + } + + /// Build a compound shape from a list of `(pose, sub_shape)` pairs. + #[staticmethod] + fn compound(parts: Vec<(PyIsometry, SharedShape)>) -> Self { + let parts: Vec<(rapier::math::Pose, rapier::parry::shape::SharedShape)> = + parts.into_iter().map(|(p, s)| (p.0.into(), s.0)).collect(); + Self(rapier::parry::shape::SharedShape::compound(parts)) + } + + /// Compute the world-space AABB enclosing this shape when + /// placed at `pose`. + fn compute_aabb(&self, pose: PyIsometry) -> Aabb { + let p: rapier::math::Pose = pose.0.into(); + Aabb(self.0.compute_aabb(&p)) + } + + /// Compute the world-space bounding sphere enclosing this shape + /// when placed at `pose`. + fn compute_bounding_sphere(&self, pose: PyIsometry) -> BoundingSphere { + let p: rapier::math::Pose = pose.0.into(); + BoundingSphere(self.0.compute_bounding_sphere(&p)) + } + + /// Compute mass properties (mass, center of mass, inertia + /// tensor) for this shape at the given uniform density. + fn compute_mass_properties(&self, density: Real) -> MassProperties { + MassProperties(self.0.mass_properties(density)) + } + /// Alias of `compute_mass_properties`. + fn mass_properties(&self, density: Real) -> MassProperties { + MassProperties(self.0.mass_properties(density)) + } + + /// Downcast to `Ball` if this is a ball shape. + fn as_ball(&self) -> Option { + self.0.as_ball().map(|s| Ball(*s)) + } + /// Downcast to `Cuboid` if this is a cuboid shape. + fn as_cuboid(&self) -> Option { + self.0.as_cuboid().map(|s| Cuboid(*s)) + } + /// Downcast to `Capsule` if this is a capsule shape. + fn as_capsule(&self) -> Option { + self.0.as_capsule().map(|s| Capsule(*s)) + } + /// Downcast to `Triangle` if this is a triangle shape. + fn as_triangle(&self) -> Option { + self.0.as_triangle().map(|s| Triangle(*s)) + } + /// Downcast to `TriMesh` if this is a triangle mesh shape. + fn as_trimesh(&self) -> Option { + self.0.as_trimesh().map(|s| TriMesh(s.clone())) + } + /// Downcast to `HeightField` if this is a heightfield shape. + fn as_heightfield(&self) -> Option { + self.0.as_heightfield().map(|s| HeightField(s.clone())) + } + /// Downcast to `Compound` if this is a compound shape. + fn as_compound(&self) -> Option { + self.0.as_compound().map(|s| Compound(s.clone())) + } + /// Downcast to `Voxels` if this is a voxel-grid shape. + fn as_voxels(&self) -> Option { + self.0.as_voxels().map(|s| Voxels(s.clone())) + } + + /// Downcast to `Cuboid` if this is a *rounded* cuboid shape. + /// + /// Returns the inner (unrounded) `Cuboid`; the border radius is + /// discarded. Use `shape_type` to detect the round variant. + fn as_round_cuboid(&self) -> Option { + self.0.as_round_cuboid().map(|s| Cuboid(s.inner_shape)) + } + /// Downcast to `Triangle` if this is a *rounded* triangle shape. + /// + /// Returns the inner (unrounded) `Triangle`; the border radius + /// is discarded. + fn as_round_triangle(&self) -> Option { + self.0.as_round_triangle().map(|s| Triangle(s.inner_shape)) + } + + fn __repr__(&self) -> String { + format!("SharedShape(type={:?})", self.0.shape_type()) + } +} + +// Dim-specific SharedShape statics as a separate pymethods block. + +#[pymethods] +impl SharedShape { + /// Build a 3D axis-aligned box of given half-extents. + #[staticmethod] + fn cuboid(hx: Real, hy: Real, hz: Real) -> Self { + Self(rapier::parry::shape::SharedShape::cuboid(hx, hy, hz)) + } + /// Build a 3D cuboid with rounded edges of radius `border_radius`. + #[staticmethod] + fn round_cuboid(hx: Real, hy: Real, hz: Real, border_radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::round_cuboid( + hx, + hy, + hz, + border_radius, + )) + } + /// Build a Y-axis cylinder. + #[staticmethod] + fn cylinder(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::cylinder( + half_height, + radius, + )) + } + /// Build a Y-axis cone (apex at +Y). + #[staticmethod] + fn cone(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::cone(half_height, radius)) + } + /// Build a Y-axis cylinder with rounded edges. + #[staticmethod] + fn round_cylinder(half_height: Real, radius: Real, border_radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::round_cylinder( + half_height, + radius, + border_radius, + )) + } + /// Build a Y-axis cone with a rounded base/apex. + #[staticmethod] + fn round_cone(half_height: Real, radius: Real, border_radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::round_cone( + half_height, + radius, + border_radius, + )) + } + /// Build a capsule aligned with the X axis. + #[staticmethod] + fn capsule_x(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::capsule_x( + half_height, + radius, + )) + } + /// Build a capsule aligned with the Y axis (the default). + #[staticmethod] + fn capsule_y(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::capsule_y( + half_height, + radius, + )) + } + /// Build a capsule aligned with the Z axis. + #[staticmethod] + fn capsule_z(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::SharedShape::capsule_z( + half_height, + radius, + )) + } + /// Build a triangle mesh from `(vertices, indices)` buffers. + /// + /// :param vertices: `(N, 3)` ndarray of floats. + /// :param indices: `(M, 3)` ndarray of `u32`. + /// :param flags: Optional preprocessing flags (`TriMeshFlags`). + /// :raises MeshConversionError: If the mesh cannot be built. + #[staticmethod] + #[pyo3(signature = (vertices, indices, flags=None))] + fn trimesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + flags: Option, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + let shape = match flags { + None => rapier::parry::shape::SharedShape::trimesh(verts, idx), + Some(f) => rapier::parry::shape::SharedShape::trimesh_with_flags(verts, idx, f.0), + } + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e:?}")))?; + Ok(Self(shape)) + } + + /// Build a convex hull around the given 3D point cloud. + /// + /// :raises MeshConversionError: If fewer than 4 non-coplanar + /// points were supplied. + #[staticmethod] + fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { + let pts = extract_verts_for_dim(points)?; + rapier::parry::shape::SharedShape::convex_hull(&pts) + .map(Self) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "convex hull computation failed (need ≥ 4 non-coplanar points)", + ) + }) + } + + /// Alias of `convex_hull` (3D). + #[staticmethod] + fn convex_polyhedron(points: &Bound<'_, PyAny>) -> PyResult { + // Alias for convex_hull in 3D since parry's convex_polyhedron + // takes already-prepared points. + Self::convex_hull(points) + } + + /// Build a convex mesh from vertices that are *already* convex. + /// + /// Unlike `convex_hull`, no hull is recomputed; the caller + /// guarantees convexity via the `(vertices, indices)` buffers. + /// + /// :param vertices: `(N, 3)` ndarray of floats. + /// :param indices: `(M, 3)` ndarray of `u32`. + /// :raises MeshConversionError: If the mesh is not a valid convex mesh. + #[staticmethod] + fn convex_mesh(vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + rapier::parry::shape::SharedShape::convex_mesh(verts, &idx) + .map(Self) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "convex mesh construction failed (invalid convex mesh)", + ) + }) + } + + /// Build a convex mesh dilated by `border_radius` (rounded edges). + /// + /// :raises MeshConversionError: If the mesh is not a valid convex mesh. + #[staticmethod] + fn round_convex_mesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + border_radius: Real, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + rapier::parry::shape::SharedShape::round_convex_mesh(verts, &idx, border_radius) + .map(Self) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "round convex mesh construction failed (invalid convex mesh)", + ) + }) + } + + /// Build a voxel-grid shape from explicit integer grid coordinates. + /// + /// :param voxel_size: Per-axis size of one voxel cell. + /// :param grid_coords: Sequence of `(i, j, k)` integer cell + /// coordinates marking the filled voxels. + #[staticmethod] + fn voxels(voxel_size: PyVector, grid_coords: &Bound<'_, PyAny>) -> PyResult { + let raw: Vec<(i64, i64, i64)> = grid_coords.extract()?; + let coords: Vec = raw + .iter() + .map(|&(x, y, z)| rapier::math::IVector::new(x as _, y as _, z as _)) + .collect(); + Ok(Self(rapier::parry::shape::SharedShape::voxels( + voxel_size.0.into(), + &coords, + ))) + } + + /// Decompose a triangle soup into a compound of convex pieces. + /// + /// :param vertices: `(N, 3)` ndarray of floats. + /// :param indices: `(M, 3)` ndarray of `u32`. + #[staticmethod] + #[pyo3(signature = (vertices, indices))] + fn convex_decomposition( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + Ok(Self( + rapier::parry::shape::SharedShape::convex_decomposition(&verts, &idx), + )) + } + + /// Build a 3D heightfield from a 2-D `heights` ndarray. + /// + /// :param heights: `(rows, cols)` ndarray of `Real`. + /// :param scale: Per-axis scaling vector. + #[staticmethod] + fn heightfield(heights: &Bound<'_, PyAny>, scale: PyVector) -> PyResult { + let arr: crate::numpy::PyReadonlyArray2 = heights.extract()?; + let slice = arr.as_slice().map_err(|_| { + crate::pyo3::exceptions::PyValueError::new_err("heights ndarray must be contiguous") + })?; + let nrows = arr.shape()[0]; + let ncols = arr.shape()[1]; + let arr2 = rapier::parry::utils::Array2::new(nrows, ncols, slice.to_vec()); + Ok(Self(rapier::parry::shape::SharedShape::heightfield( + arr2, + scale.0.into(), + ))) + } + + /// Downcast to `Cylinder` if this is a cylinder shape. + fn as_cylinder(&self) -> Option { + self.0.as_cylinder().map(|s| Cylinder(*s)) + } + /// Downcast to `Cone` if this is a cone shape. + fn as_cone(&self) -> Option { + self.0.as_cone().map(|s| Cone(*s)) + } + /// Downcast to `ConvexPolyhedron` if this is a convex polyhedron. + fn as_convex_polyhedron(&self) -> Option { + self.0 + .as_convex_polyhedron() + .map(|s| ConvexPolyhedron(s.clone())) + } + /// Downcast to `Cylinder` if this is a *rounded* cylinder. + /// + /// Returns the inner (unrounded) `Cylinder`; the border radius + /// is discarded. + fn as_round_cylinder(&self) -> Option { + self.0.as_round_cylinder().map(|s| Cylinder(s.inner_shape)) + } + /// Downcast to `Cone` if this is a *rounded* cone. + /// + /// Returns the inner (unrounded) `Cone`; the border radius is + /// discarded. + fn as_round_cone(&self) -> Option { + self.0.as_round_cone().map(|s| Cone(s.inner_shape)) + } + /// Downcast to `ConvexPolyhedron` if this is a *rounded* convex + /// polyhedron. Returns the inner (unrounded) polyhedron. + fn as_round_convex_polyhedron(&self) -> Option { + self.0 + .as_round_convex_polyhedron() + .map(|s| ConvexPolyhedron(s.inner_shape.clone())) + } +} // end impl SharedShape + +// ============================================================ +// Aabb (small wrapper over parry::bounding_volume::Aabb) +// ============================================================ + +/// Axis-aligned bounding box. +/// +/// Stored as a `(mins, maxs)` pair in world coordinates. Returned +/// by `Collider.compute_aabb()` and `SharedShape.compute_aabb()`. +#[pyclass(name = "Aabb", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Aabb(pub rapier::parry::bounding_volume::Aabb); + +#[pymethods] +impl Aabb { + /// Build an AABB from its `mins` and `maxs` corners. + #[new] + fn new(mins: PyVector, maxs: PyVector) -> Self { + let mins: rapier::math::Vector = mins.0.into(); + let maxs: rapier::math::Vector = maxs.0.into(); + Self(rapier::parry::bounding_volume::Aabb::new(mins, maxs)) + } + /// Lower corner. + #[getter] + fn mins(&self) -> Point3 { + let v: crate::na::SVector = self.0.mins.into(); + Point3(crate::na::Point::from(v)) + } + /// Upper corner. + #[getter] + fn maxs(&self) -> Point3 { + let v: crate::na::SVector = self.0.maxs.into(); + Point3(crate::na::Point::from(v)) + } + /// Geometric center of the AABB. + #[getter] + fn center(&self) -> Point3 { + let v: crate::na::SVector = self.0.center().into(); + Point3(crate::na::Point::from(v)) + } + /// Half-extents along each axis. + #[getter] + fn half_extents(&self) -> Vec3 { + let v: crate::na::SVector = self.0.half_extents().into(); + Vec3(v) + } + /// Volume (3D) or area (2D) of the AABB. + #[getter] + fn volume(&self) -> Real { + self.0.volume() + } +} + +// ============================================================ +// BoundingSphere +// ============================================================ + +/// Bounding sphere defined by a center and radius. +/// +/// Returned by `SharedShape.compute_bounding_sphere()`. +#[pyclass(name = "BoundingSphere", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct BoundingSphere(pub rapier::parry::bounding_volume::BoundingSphere); + +#[pymethods] +impl BoundingSphere { + /// Build a bounding sphere from its `center` and `radius`. + #[new] + fn new(center: PyVector, radius: Real) -> Self { + let c: rapier::math::Vector = center.0.into(); + Self(rapier::parry::bounding_volume::BoundingSphere { center: c, radius }) + } + /// Center of the sphere. + #[getter] + fn center(&self) -> Point3 { + let v: crate::na::SVector = self.0.center.into(); + Point3(crate::na::Point::from(v)) + } + /// Radius of the sphere. + #[getter] + fn radius(&self) -> Real { + self.0.radius + } +} + +// ============================================================ +// MeshConverter (tagged-union-ish; one variant constructor at a time) +// ============================================================ + +/// Strategy for turning a `(vertices, indices)` buffer pair into a +/// `SharedShape`. +/// +/// Picks the conversion variant via the class attributes +/// (`TRIMESH`, `OBB`, `BOUNDING_BOX`, `CONVEX_HULL`, `COMPOUND`) or +/// the `trimesh_with_flags(...)` factory, then call `build()`. +#[pyclass(name = "MeshConverter", module = "rapier")] +#[derive(Clone)] +pub struct MeshConverter(pub rapier::geometry::MeshConverter); + +#[pymethods] +impl MeshConverter { + /// Convert vertices/indices into a `TriMesh` shape. + #[classattr] + fn TRIMESH() -> Self { + Self(rapier::geometry::MeshConverter::TriMesh) + } + /// Convert into a tightly-fitting oriented bounding box (`Cuboid`). + #[classattr] + fn OBB() -> Self { + Self(rapier::geometry::MeshConverter::Obb) + } + /// Convert into an axis-aligned bounding box (`Cuboid`). + #[classattr] + fn BOUNDING_BOX() -> Self { + Self(rapier::geometry::MeshConverter::Aabb) + } + /// Convert into a convex hull of the input vertices. + #[classattr] + fn CONVEX_HULL() -> Self { + Self(rapier::geometry::MeshConverter::ConvexHull) + } + + /// Convert into a `TriMesh` using the provided preprocessing + /// `flags`. + #[staticmethod] + fn trimesh_with_flags(flags: TriMeshFlags) -> Self { + Self(rapier::geometry::MeshConverter::TriMeshWithFlags(flags.0)) + } + + /// Build a `SharedShape` from a (vertices, indices) buffer pair. + /// + /// `vertices` is a (N, D) ndarray of floats; `indices` is a (M, 3) + /// ndarray of u32. Raises `MeshConversionError` on failure. + fn build( + &self, + py: Python<'_>, + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + ) -> PyResult<(SharedShape, Isometry3)> { + let _ = py; + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + let result = self + .0 + .convert(verts, idx) + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e}")))?; + let iso: crate::na::Isometry = result.1.into(); + Ok((SharedShape(result.0), Isometry3(iso))) + } +} + +#[pymethods] +impl MeshConverter { + /// Convert into a compound shape via convex decomposition. + #[classattr] + fn COMPOUND() -> Self { + Self(rapier::geometry::MeshConverter::ConvexDecomposition) + } + /// Convert into a compound of convex pieces via convex + /// decomposition (V-HACD, default parameters). 3D only. + #[classattr] + fn CONVEX_DECOMPOSITION() -> Self { + Self(rapier::geometry::MeshConverter::ConvexDecomposition) + } +} + +// ============================================================ +// ContactManifoldData +// ============================================================ + +/// Per-manifold metadata accompanying a `ContactManifold`. +/// +/// Read-only view. Holds the parent rigid-body handles (if any), +/// the world-space contact normal, the number of solver-active +/// contacts, the relative dominance used by the solver, and a +/// user-data payload. +#[pyclass(name = "ContactManifoldData", module = "rapier", frozen)] +#[derive(Debug, Clone)] +pub struct ContactManifoldData { + #[pyo3(get)] + pub rigid_body1: Option, + #[pyo3(get)] + pub rigid_body2: Option, + #[pyo3(get)] + pub normal: Vec3, + #[pyo3(get)] + pub num_active_contacts: usize, + #[pyo3(get)] + pub relative_dominance: i16, + #[pyo3(get)] + pub user_data: u32, +} + +// ============================================================ +// ContactManifold +// ============================================================ + +/// Set of contact points sharing a common normal between two colliders. +/// +/// Read-only view. `points` are the individual `ContactData` entries; +/// `local_n1` / `local_n2` are the contact normal expressed in each +/// collider's local frame; `subshape1` / `subshape2` identify the +/// sub-shapes inside compound shapes (0 for non-compounds). +#[pyclass(name = "ContactManifold", module = "rapier", frozen)] +#[derive(Debug, Clone)] +pub struct ContactManifold { + #[pyo3(get)] + pub data: ContactManifoldData, + #[pyo3(get)] + pub points: Vec, + #[pyo3(get)] + pub local_n1: Vec3, + #[pyo3(get)] + pub local_n2: Vec3, + #[pyo3(get)] + pub subshape1: u32, + #[pyo3(get)] + pub subshape2: u32, +} + +// ============================================================ +// ContactPair (clone of rapier::geometry::ContactPair, but +// converted into Python-friendly shape views) +// ============================================================ + +/// Read-only view of the contacts between two colliders. +/// +/// Holds one or more `ContactManifold`s plus helpers to query the +/// deepest contact and the total impulse. Snapshots are returned +/// by `NarrowPhase.contact_pair(...)` and +/// `NarrowPhase.contact_pairs()`; mutating them does not affect +/// the simulation. +#[pyclass(name = "ContactPair", module = "rapier")] +#[derive(Clone)] +pub struct ContactPair(pub rapier::geometry::ContactPair); + +#[pymethods] +impl ContactPair { + /// Handle of the first collider. + #[getter] + fn collider1(&self) -> ColliderHandle { + ColliderHandle(self.0.collider1) + } + /// Handle of the second collider. + #[getter] + fn collider2(&self) -> ColliderHandle { + ColliderHandle(self.0.collider2) + } + + /// List of contact manifolds for this pair. + #[getter] + fn manifolds(&self) -> Vec { + self.0 + .manifolds + .iter() + .map(|m| { + let normal_v: crate::na::SVector = m.data.normal.into(); + let n1: crate::na::SVector = m.local_n1.into(); + let n2: crate::na::SVector = m.local_n2.into(); + + let points: Vec = m + .points + .iter() + .map(|c| { + let p1: crate::na::Vector3 = c.local_p1.into(); + let p2: crate::na::Vector3 = c.local_p2.into(); + ContactData { + local_p1: Point3(crate::na::Point3::from(p1)), + local_p2: Point3(crate::na::Point3::from(p2)), + dist: c.dist, + fid1: c.fid1.0, + fid2: c.fid2.0, + impulse: c.data.impulse, + tangent_impulse: (c.data.tangent_impulse[0], c.data.tangent_impulse[1]), + contact_id: 0, + } + }) + .collect(); + + ContactManifold { + data: ContactManifoldData { + rigid_body1: m.data.rigid_body1.map(RigidBodyHandle), + rigid_body2: m.data.rigid_body2.map(RigidBodyHandle), + normal: Vec3(normal_v), + num_active_contacts: m.data.solver_contacts.len(), + relative_dominance: m.data.relative_dominance, + user_data: m.data.user_data, + }, + points, + local_n1: Vec3(n1), + local_n2: Vec3(n2), + subshape1: m.subshape1, + subshape2: m.subshape2, + } + }) + .collect() + } + + /// True iff any manifold has at least one solver-active contact. + #[getter] + fn has_any_active_contact(&self) -> bool { + self.0.has_any_active_contact() + } + + /// Return the deepest (most penetrating) `ContactData`, if any. + fn find_deepest_contact(&self) -> Option { + self.0.find_deepest_contact().map(|(_, c)| { + let p1: crate::na::Vector3 = c.local_p1.into(); + let p2: crate::na::Vector3 = c.local_p2.into(); + ContactData { + local_p1: Point3(crate::na::Point3::from(p1)), + local_p2: Point3(crate::na::Point3::from(p2)), + dist: c.dist, + fid1: c.fid1.0, + fid2: c.fid2.0, + impulse: c.data.impulse, + tangent_impulse: (c.data.tangent_impulse[0], c.data.tangent_impulse[1]), + contact_id: 0, + } + }) + } + + /// Sum of contact impulses across all manifolds, as a world-space vector. + fn total_impulse(&self) -> Vec3 { + let v = self.0.total_impulse(); + let nav: crate::na::SVector = v.into(); + Vec3(nav) + } + + /// Magnitude of `total_impulse()`. + fn total_impulse_magnitude(&self) -> Real { + self.0.total_impulse_magnitude() + } +} + +// ============================================================ +// IntersectionPair (small view) +// ============================================================ + +/// Snapshot of one sensor/intersection pair tracked by the narrow-phase. +/// +/// Read-only view. `intersecting` is true while the pair currently +/// overlaps. +#[pyclass(name = "IntersectionPair", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct IntersectionPair { + #[pyo3(get)] + pub collider1: ColliderHandle, + #[pyo3(get)] + pub collider2: ColliderHandle, + #[pyo3(get)] + pub intersecting: bool, +} + +// ============================================================ +// ContactForceEvent (plain dataclass-style) +// ============================================================ + +/// Event fired when the contact force magnitude between two +/// colliders exceeds the configured threshold. +/// +/// Requires `ActiveEvents.CONTACT_FORCE_EVENTS` on at least one +/// collider; the threshold is set via +/// `Collider.contact_force_event_threshold` (or its builder). +/// `total_force` is the sum of contact impulses divided by the +/// time step; `max_force_direction` and `max_force_magnitude` +/// summarize the strongest single contact. +#[pyclass(name = "ContactForceEvent", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct ContactForceEvent { + #[pyo3(get)] + pub collider1: ColliderHandle, + #[pyo3(get)] + pub collider2: ColliderHandle, + #[pyo3(get)] + pub total_force: Vec3, + #[pyo3(get)] + pub total_force_magnitude: Real, + #[pyo3(get)] + pub max_force_direction: Vec3, + #[pyo3(get)] + pub max_force_magnitude: Real, +} + +// ---- 3D-only shape views ---- + +/// Cylinder shape view, axis-aligned to Y. +/// +/// Inspection-only — typically obtained via `shape.as_cylinder()`. +#[pyclass(name = "Cylinder", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Cylinder(pub rapier::parry::shape::Cylinder); + +#[pymethods] +impl Cylinder { + /// Construct a cylinder of given half-height (along Y) and radius. + #[new] + fn new(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::Cylinder::new(half_height, radius)) + } + /// Half-height of the cylinder along the Y axis. + #[getter] + fn half_height(&self) -> Real { + self.0.half_height + } + /// Cap radius. + #[getter] + fn radius(&self) -> Real { + self.0.radius + } + fn __repr__(&self) -> String { + format!( + "Cylinder(half_height={}, radius={})", + self.0.half_height, self.0.radius + ) + } +} + +/// Cone shape view, apex along +Y. +/// +/// Inspection-only — typically obtained via `shape.as_cone()`. +#[pyclass(name = "Cone", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Cone(pub rapier::parry::shape::Cone); + +#[pymethods] +impl Cone { + /// Construct a cone of given half-height (along Y) and base radius. + #[new] + fn new(half_height: Real, radius: Real) -> Self { + Self(rapier::parry::shape::Cone::new(half_height, radius)) + } + /// Half-height along the Y axis (apex at +half_height, base at -half_height). + #[getter] + fn half_height(&self) -> Real { + self.0.half_height + } + /// Base radius. + #[getter] + fn radius(&self) -> Real { + self.0.radius + } + fn __repr__(&self) -> String { + format!( + "Cone(half_height={}, radius={})", + self.0.half_height, self.0.radius + ) + } +} + +/// Convex polyhedron shape view. +/// +/// Inspection-only — typically obtained via +/// `shape.as_convex_polyhedron()`. +#[pyclass(name = "ConvexPolyhedron", module = "rapier")] +#[derive(Clone)] +pub struct ConvexPolyhedron(pub rapier::parry::shape::ConvexPolyhedron); + +#[pymethods] +impl ConvexPolyhedron { + /// Vertices of the polyhedron as an `(N, 3)` ndarray. + #[getter] + fn points<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + let pts = self.0.points(); + let n = pts.len(); + let mut flat = Vec::with_capacity(n * 3); + for p in pts { + flat.extend_from_slice(&[p.x, p.y, p.z]); + } + PyArray2::from_vec2_bound(py, &flat.chunks(3).map(|c| c.to_vec()).collect::>()) + .expect("contiguous 2D ndarray") + } + /// Number of vertices. + fn num_points(&self) -> usize { + self.0.points().len() + } + + /// Triangulated face indices as an `(M, 3)` ndarray of `u32`. + /// + /// Each (convex) face is fan-triangulated; the indices reference + /// rows of `points` and tile the polyhedron's surface. This lets a + /// renderer build the actual mesh straight from parry's topology + /// instead of recomputing a convex hull. + #[getter] + fn indices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + let adj = self.0.vertices_adj_to_face(); + let mut tris: Vec> = Vec::new(); + for face in self.0.faces() { + let first = face.first_vertex_or_edge as usize; + let n = face.num_vertices_or_edges as usize; + if n < 3 { + continue; + } + let v0 = adj[first]; + for i in 1..n - 1 { + tris.push(vec![v0, adj[first + i], adj[first + i + 1]]); + } + } + PyArray2::from_vec2_bound(py, &tris).expect("contiguous 2D ndarray") + } +} + +// ---- 3D HeightField additional accessor: heights as (nrows, ncols) ndarray ---- + +#[pymethods] +impl HeightField { + /// Height samples as an `(nrows, ncols)` float ndarray. + /// + /// World-space vertex positions are + /// `(x * scale.x, height[i,j] * scale.y, z * scale.z)` + /// where `x`/`z` are evenly-spaced in `[-0.5, 0.5]`. + #[getter] + fn heights<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray2> { + let h = self.0.heights(); + let nrows = h.nrows(); + let ncols = h.ncols(); + let rows: Vec> = (0..nrows) + .map(|i| (0..ncols).map(|j| h[(i, j)]).collect()) + .collect(); + crate::numpy::PyArray2::from_vec2_bound(py, &rows).expect("contiguous 2D ndarray") + } + /// Number of rows in the height grid (along the X axis). + #[getter] + fn nrows(&self) -> usize { + self.0.heights().nrows() + } + /// Number of columns in the height grid (along the Z axis). + #[getter] + fn ncols(&self) -> usize { + self.0.heights().ncols() + } +} + +// ---- ContactData (3D — tangent_impulse is 2 floats) ---- + +/// Single contact point inside a `ContactManifold`. +/// +/// Read-only snapshot. Points are expressed in each collider's +/// local frame; `dist` is the separation (negative if penetrating); +/// `impulse` is the normal impulse computed by the solver and +/// `tangent_impulse` is the friction impulse along the two +/// tangent directions of the manifold's frame. +#[pyclass(name = "ContactData", module = "rapier", frozen)] +#[derive(Debug, Clone)] +pub struct ContactData { + #[pyo3(get)] + pub local_p1: Point3, + #[pyo3(get)] + pub local_p2: Point3, + #[pyo3(get)] + pub dist: Real, + #[pyo3(get)] + pub fid1: u32, + #[pyo3(get)] + pub fid2: u32, + #[pyo3(get)] + pub impulse: Real, + #[pyo3(get)] + pub tangent_impulse: (Real, Real), + #[pyo3(get)] + pub contact_id: u32, +} + +#[pymethods] +impl ContactData { + fn __repr__(&self) -> String { + format!("ContactData(dist={}, impulse={})", self.dist, self.impulse) + } +} + +// ---- SolverContact view ---- + +/// One contact prepared for the constraints solver. +/// +/// Read-only snapshot. `point` is in world coordinates; +/// `friction`/`restitution` are the per-contact combined material +/// values; `is_new` is true when the contact is freshly generated +/// this step. +#[pyclass(name = "SolverContact", module = "rapier", frozen)] +#[derive(Debug, Clone)] +pub struct SolverContact { + #[pyo3(get)] + pub point: Point3, + #[pyo3(get)] + pub dist: Real, + #[pyo3(get)] + pub friction: Real, + #[pyo3(get)] + pub restitution: Real, + #[pyo3(get)] + pub contact_id: u32, + #[pyo3(get)] + pub is_new: bool, +} + +// ============================================================ +// Collider — owned standalone value (mirrors RigidBody pattern). +// ============================================================ + +/// Standalone collider: a shape + material + per-collider flags +/// that participates in collision detection. +/// +/// A collider may be attached to a `RigidBody` via +/// `ColliderSet.insert_with_parent(...)`. Use the static shape +/// factories (`Collider.ball`, `Collider.cuboid`, etc.) to obtain +/// a `ColliderBuilder` and chain configuration calls, then +/// `.build()` to get a `Collider`. A collider fetched from +/// `ColliderSet[handle]` is a live **view**: reads and writes go +/// straight through to the set with no copy, so mutations persist +/// immediately. +#[pyclass(name = "Collider", module = "rapier")] +pub struct Collider { + pub backing: ColliderBacking, +} + +/// Storage backing a `Collider`: a standalone owned collider +/// (pre-insertion) or a handle-backed view into a `ColliderSet`. +#[derive(Debug)] +pub enum ColliderBacking { + Owned(Box), + InSet { + set: Py, + handle: rapier::geometry::ColliderHandle, + }, +} + +impl Clone for ColliderBacking { + fn clone(&self) -> Self { + match self { + ColliderBacking::Owned(c) => ColliderBacking::Owned(c.clone()), + ColliderBacking::InSet { set, handle } => { + Python::with_gil(|py| ColliderBacking::InSet { + set: set.clone_ref(py), + handle: *handle, + }) + } + } + } +} +impl Clone for Collider { + fn clone(&self) -> Self { + Collider { + backing: self.backing.clone(), + } + } +} + +impl Collider { + pub(crate) fn new_owned(collider: rapier::geometry::Collider) -> Self { + Collider { + backing: ColliderBacking::Owned(Box::new(collider)), + } + } + + /// Run `f` with a shared reference to the underlying collider. + fn with_ref(&self, f: impl FnOnce(&rapier::geometry::Collider) -> R) -> R { + match &self.backing { + ColliderBacking::Owned(c) => f(c), + ColliderBacking::InSet { set, handle } => Python::with_gil(|py| { + let set = set.bind(py).borrow(); + let c = set + .0 + .get(*handle) + .expect("Collider refers to a collider that was removed from its set"); + f(c) + }), + } + } + + /// Run `f` with a mutable reference to the underlying collider, + /// writing straight through to the set for an `InSet` view. + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::geometry::Collider) -> R) -> R { + match &mut self.backing { + ColliderBacking::Owned(c) => f(c), + ColliderBacking::InSet { set, handle } => Python::with_gil(|py| { + let mut set = set.bind(py).borrow_mut(); + let c = set + .0 + .get_mut(*handle) + .expect("Collider refers to a collider that was removed from its set"); + f(c) + }), + } + } + + /// Clone the underlying collider out (used by `insert` and callers + /// needing an owned `&rapier::Collider`). + pub fn to_owned_collider(&self) -> rapier::geometry::Collider { + self.with_ref(|c| c.clone()) + } +} + +#[pymethods] +impl Collider { + // ---- read-only handle/parent ---- + + /// Handle of the parent rigid-body, if any. + #[getter] + fn parent(&self) -> Option { + self.with_ref(|c| c.parent()).map(RigidBodyHandle) + } + + // ---- position / translation / rotation ---- + + /// World-space pose of the collider. + #[getter] + fn position(&self) -> Isometry3 { + self.with_ref(|c| { + let iso: crate::na::Isometry = (*c.position()).into(); + Isometry3(iso) + }) + } + #[setter] + fn set_position(&mut self, p: PyIsometry) { + self.with_mut(|c| c.set_position(p.0.into())); + } + /// World-space translation component of the pose. + #[getter] + fn translation(&self) -> Vec3 { + self.with_ref(|c| { + let v: crate::na::SVector = c.translation().into(); + Vec3(v) + }) + } + #[setter] + fn set_translation(&mut self, v: PyVector) { + self.with_mut(|c| c.set_translation(v.0.into())); + } + /// World-space rotation component of the pose. + #[getter] + fn rotation(&self) -> Rotation3 { + self.with_ref(|c| Rotation3(c.rotation().into())) + } + #[setter] + fn set_rotation(&mut self, r: PyRotation) { + self.with_mut(|c| c.set_rotation(r.0.into())); + } + + // ---- shape ---- + + /// Underlying collision shape. + #[getter] + fn shape(&self) -> SharedShape { + self.with_ref(|c| SharedShape(c.shared_shape().clone())) + } + #[setter] + fn set_shape(&mut self, s: SharedShape) { + self.with_mut(|c| c.set_shape(s.0)); + } + + // ---- mass / density / mass_properties ---- + + /// Uniform density used to derive mass when no explicit mass is set. + #[getter] + fn density(&self) -> Real { + self.with_ref(|c| c.density()) + } + #[setter] + fn set_density(&mut self, v: Real) { + self.with_mut(|c| c.set_density(v)); + } + /// Explicit mass of the collider. + #[getter] + fn mass(&self) -> Real { + self.with_ref(|c| c.mass()) + } + #[setter] + fn set_mass(&mut self, v: Real) { + self.with_mut(|c| c.set_mass(v)); + } + /// Full mass properties (mass, center of mass, inertia tensor). + #[getter] + fn mass_properties(&self) -> MassProperties { + MassProperties(self.with_ref(|c| c.mass_properties())) + } + #[setter] + fn set_mass_properties(&mut self, mp: MassProperties) { + self.with_mut(|c| c.set_mass_properties(mp.0)); + } + /// Volume (3D) or area (2D) of the shape. + #[getter] + fn volume(&self) -> Real { + self.with_ref(|c| c.volume()) + } + + // ---- material ---- + + /// Friction coefficient. + #[getter] + fn friction(&self) -> Real { + self.with_ref(|c| c.friction()) + } + #[setter] + fn set_friction(&mut self, v: Real) { + self.with_mut(|c| c.set_friction(v)); + } + /// Restitution coefficient (bounciness). + #[getter] + fn restitution(&self) -> Real { + self.with_ref(|c| c.restitution()) + } + #[setter] + fn set_restitution(&mut self, v: Real) { + self.with_mut(|c| c.set_restitution(v)); + } + /// Rule used to combine friction with another collider's friction. + #[getter] + fn friction_combine_rule(&self) -> CoefficientCombineRule { + CoefficientCombineRule::from_rapier(self.with_ref(|c| c.friction_combine_rule())) + } + #[setter] + fn set_friction_combine_rule(&mut self, v: CoefficientCombineRule) { + self.with_mut(|c| c.set_friction_combine_rule(v.to_rapier())); + } + /// Rule used to combine restitution with another collider. + #[getter] + fn restitution_combine_rule(&self) -> CoefficientCombineRule { + CoefficientCombineRule::from_rapier(self.with_ref(|c| c.restitution_combine_rule())) + } + #[setter] + fn set_restitution_combine_rule(&mut self, v: CoefficientCombineRule) { + self.with_mut(|c| c.set_restitution_combine_rule(v.to_rapier())); + } + /// Material bundle (friction, restitution, and their combine rules). + #[getter] + fn material(&self) -> ColliderMaterial { + self.with_ref(|c| ColliderMaterial(*c.material())) + } + + // ---- sensor / enabled ---- + + /// True iff this collider is a sensor (no contact response). + #[getter] + fn is_sensor(&self) -> bool { + self.with_ref(|c| c.is_sensor()) + } + #[setter] + fn set_is_sensor(&mut self, v: bool) { + self.with_mut(|c| c.set_sensor(v)); + } + /// True iff the collider is currently enabled. + #[getter] + fn is_enabled(&self) -> bool { + self.with_ref(|c| c.is_enabled()) + } + #[setter] + fn set_is_enabled(&mut self, v: bool) { + self.with_mut(|c| c.set_enabled(v)); + } + + // ---- hooks / events ---- + + /// Event flags opted into by this collider (see `ActiveEvents`). + #[getter] + fn active_events(&self) -> ActiveEvents { + ActiveEvents(self.with_ref(|c| c.active_events())) + } + #[setter] + fn set_active_events(&mut self, v: ActiveEvents) { + self.with_mut(|c| c.set_active_events(v.0)); + } + /// Hook flags opted into by this collider (see `ActiveHooks`). + #[getter] + fn active_hooks(&self) -> ActiveHooks { + ActiveHooks(self.with_ref(|c| c.active_hooks())) + } + #[setter] + fn set_active_hooks(&mut self, v: ActiveHooks) { + self.with_mut(|c| c.set_active_hooks(v.0)); + } + /// Rigid-body type combinations this collider collides with + /// (see `ActiveCollisionTypes`). + #[getter] + fn active_collision_types(&self) -> ActiveCollisionTypes { + ActiveCollisionTypes(self.with_ref(|c| c.active_collision_types())) + } + #[setter] + fn set_active_collision_types(&mut self, v: ActiveCollisionTypes) { + self.with_mut(|c| c.set_active_collision_types(v.0)); + } + + // ---- groups ---- + + /// Groups controlling which colliders form contact pairs. + #[getter] + fn collision_groups(&self) -> InteractionGroups { + InteractionGroups(self.with_ref(|c| c.collision_groups())) + } + #[setter] + fn set_collision_groups(&mut self, v: InteractionGroups) { + self.with_mut(|c| c.set_collision_groups(v.0)); + } + /// Groups controlling which contact pairs reach the solver. + #[getter] + fn solver_groups(&self) -> InteractionGroups { + InteractionGroups(self.with_ref(|c| c.solver_groups())) + } + #[setter] + fn set_solver_groups(&mut self, v: InteractionGroups) { + self.with_mut(|c| c.set_solver_groups(v.0)); + } + + // ---- contact skin & event threshold ---- + + /// Thickness of the virtual "skin" around the collider, used to + /// reduce jitter on resting contacts. + #[getter] + fn contact_skin(&self) -> Real { + self.with_ref(|c| c.contact_skin()) + } + #[setter] + fn set_contact_skin(&mut self, v: Real) { + self.with_mut(|c| c.set_contact_skin(v)); + } + /// Force magnitude above which a `ContactForceEvent` is emitted. + /// + /// Requires `ActiveEvents.CONTACT_FORCE_EVENTS`. + #[getter] + fn contact_force_event_threshold(&self) -> Real { + self.with_ref(|c| c.contact_force_event_threshold()) + } + #[setter] + fn set_contact_force_event_threshold(&mut self, v: Real) { + self.with_mut(|c| c.set_contact_force_event_threshold(v)); + } + + // ---- user_data ---- + + /// Free 128-bit user payload (opaque to the engine). + #[getter] + fn user_data(&self) -> u128 { + self.with_ref(|c| c.user_data) + } + #[setter] + fn set_user_data(&mut self, v: u128) { + self.with_mut(|c| c.user_data = v); + } + + // ---- compute helpers ---- + + /// Compute the world-space AABB of this collider. + fn compute_aabb(&self) -> Aabb { + Aabb(self.with_ref(|c| c.compute_aabb())) + } + + // ---- Builder forwards (mirror RigidBody.dynamic etc) ---- + + /// Builder for a ball/sphere collider of the given radius. + /// + /// :param radius: Sphere radius. + /// :param kwargs: Optional `ColliderBuilder` kwargs (e.g. + /// `density`, `friction`, `sensor`, `translation`, + /// `rotation`). + #[staticmethod] + #[pyo3(signature = (radius, **kwargs))] + fn ball( + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::ball(radius), kwargs) + } + /// Builder for a triangle collider with the given vertices. + #[staticmethod] + fn triangle(a: PyVector, b: PyVector, c: PyVector) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::triangle( + a.0.into(), + b.0.into(), + c.0.into(), + ), + } + } + /// Builder for a half-space (infinite plane) collider. + /// + /// :param outward_normal: Plane normal; the solid half-space lies + /// on its negative side. Normalized automatically. + /// :param kwargs: Optional `ColliderBuilder` kwargs (e.g. + /// `friction`, `translation`, `rotation`). A half-space has no + /// finite volume, so leave `density`/`mass` unset on bodies + /// that use it. + #[staticmethod] + #[pyo3(signature = (outward_normal, **kwargs))] + fn halfspace( + outward_normal: PyVector, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let n = outward_normal.0.normalize(); + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::new(rapier::parry::shape::SharedShape::halfspace( + n.into(), + )), + kwargs, + ) + } + /// Builder wrapping an arbitrary `SharedShape`. + #[staticmethod] + #[pyo3(signature = (shape))] + fn new(shape: SharedShape) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::new(shape.0), + } + } + /// Builder for a compound collider built from `(pose, sub_shape)` parts. + #[staticmethod] + fn compound(parts: Vec<(PyIsometry, SharedShape)>) -> ColliderBuilder { + let parts: Vec<(rapier::math::Pose, rapier::parry::shape::SharedShape)> = + parts.into_iter().map(|(p, s)| (p.0.into(), s.0)).collect(); + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::compound(parts), + } + } + /// Builder for a triangle-mesh collider. + /// + /// :param vertices: `(N, D)` ndarray of floats. + /// :param indices: `(M, 3)` ndarray of `u32`. + /// :param flags: Optional `TriMeshFlags` preprocessing. + /// :raises MeshConversionError: If the mesh cannot be built. + #[staticmethod] + #[pyo3(signature = (vertices, indices, flags=None))] + fn trimesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + flags: Option, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + let b = match flags { + None => rapier::geometry::ColliderBuilder::trimesh(verts, idx), + Some(f) => rapier::geometry::ColliderBuilder::trimesh_with_flags(verts, idx, f.0), + } + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e:?}")))?; + Ok(ColliderBuilder { builder: b }) + } + + /// Builder for a collider wrapping the convex hull of a point cloud. + /// + /// :raises MeshConversionError: If the convex hull cannot be built. + #[staticmethod] + fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { + let pts = extract_verts_for_dim(points)?; + rapier::geometry::ColliderBuilder::convex_hull(&pts) + .map(|b| ColliderBuilder { builder: b }) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err("convex hull computation failed") + }) + } + /// Builder for a triangle collider with rounded edges. + #[staticmethod] + fn round_triangle( + a: PyVector, + b: PyVector, + c: PyVector, + border_radius: Real, + ) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::round_triangle( + a.0.into(), + b.0.into(), + c.0.into(), + border_radius, + ), + } + } + /// Builder for a rounded convex-hull collider. + /// + /// :raises MeshConversionError: If the hull cannot be built. + #[staticmethod] + fn round_convex_hull( + points: &Bound<'_, PyAny>, + border_radius: Real, + ) -> PyResult { + let pts = extract_verts_for_dim(points)?; + rapier::geometry::ColliderBuilder::round_convex_hull(&pts, border_radius) + .map(|b| ColliderBuilder { builder: b }) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err("round convex hull computation failed") + }) + } + /// Builder for a voxel-grid collider built by voxelizing points. + /// + /// :param voxel_size: Per-axis size of one voxel cell. + /// :param points: `(N, D)` ndarray of floats. + #[staticmethod] + fn voxels_from_points( + voxel_size: PyVector, + points: &Bound<'_, PyAny>, + ) -> PyResult { + let pts = extract_verts_for_dim(points)?; + Ok(ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::voxels_from_points( + voxel_size.0.into(), + &pts, + ), + }) + } + /// Builder for a collider whose shape is produced from a triangle + /// buffer by a `MeshConverter` (trimesh, hull, OBB, AABB, + /// convex decomposition, ...). + /// + /// :raises MeshConversionError: If the conversion fails. + #[staticmethod] + fn converted_trimesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + converter: &MeshConverter, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + rapier::geometry::ColliderBuilder::converted_trimesh(verts, idx, converter.0) + .map(|b| ColliderBuilder { builder: b }) + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e}"))) + } + + // Capsule constructors (parry uses `capsule_from_endpoints`). + /// Builder for a Y-axis capsule (the default axis). + #[staticmethod] + fn capsule(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), + } + } + /// Builder for an X-axis capsule. + #[staticmethod] + fn capsule_x(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::capsule_x(half_height, radius), + } + } + /// Builder for a Y-axis capsule. + #[staticmethod] + fn capsule_y(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), + } + } + /// Builder for a capsule defined by its two endpoints and a radius. + #[staticmethod] + fn capsule_from_endpoints(a: PyVector, b: PyVector, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::capsule_from_endpoints( + a.0.into(), + b.0.into(), + radius, + ), + } + } + + fn __repr__(&self) -> String { + format!( + "Collider(shape={:?}, sensor={}, parent={:?})", + self.with_ref(|c| c.shape().shape_type()), + self.with_ref(|c| c.is_sensor()), + self.with_ref(|c| c.parent()).map(|h| h.into_raw_parts()), + ) + } +} + +// Emit the dim-specific Collider statics as a separate pymethods +// block (multiple-pymethods feature). + +#[pymethods] +impl Collider { + /// Builder for a 3D axis-aligned box (cuboid) collider. + /// + /// :param hx: Half-extent along the local X axis. + /// :param hy: Half-extent along the local Y axis. + /// :param hz: Half-extent along the local Z axis. + /// :param kwargs: Optional `ColliderBuilder` kwargs. + #[staticmethod] + #[pyo3(signature = (hx, hy, hz, **kwargs))] + fn cuboid( + hx: Real, + hy: Real, + hz: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::cuboid(hx, hy, hz), + kwargs, + ) + } + /// Builder for a Y-axis cylinder collider. + #[staticmethod] + fn cylinder(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::cylinder(half_height, radius), + } + } + /// Builder for a Y-axis cone collider (apex at +Y). + #[staticmethod] + fn cone(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::cone(half_height, radius), + } + } + /// Builder for a Z-axis capsule collider. + #[staticmethod] + fn capsule_z(half_height: Real, radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::capsule_z(half_height, radius), + } + } + /// Builder for a 3D cuboid with rounded edges of `border_radius`. + #[staticmethod] + fn round_cuboid(hx: Real, hy: Real, hz: Real, border_radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::round_cuboid(hx, hy, hz, border_radius), + } + } + /// Builder for a Y-axis cylinder with rounded edges. + #[staticmethod] + fn round_cylinder(half_height: Real, radius: Real, border_radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::round_cylinder( + half_height, + radius, + border_radius, + ), + } + } + /// Builder for a Y-axis cone with rounded apex/base. + #[staticmethod] + fn round_cone(half_height: Real, radius: Real, border_radius: Real) -> ColliderBuilder { + ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::round_cone( + half_height, + radius, + border_radius, + ), + } + } + /// Builder for a convex-polyhedron collider (3D convex hull of `points`). + #[staticmethod] + fn convex_polyhedron(points: &Bound<'_, PyAny>) -> PyResult { + // 3D: convex_hull builds a ConvexPolyhedron internally; reuse it. + Self::convex_hull(points) + } + /// Builder for a compound collider obtained by convex + /// decomposition of a triangle mesh. + #[staticmethod] + #[pyo3(signature = (vertices, indices))] + fn convex_decomposition( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + Ok(ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::convex_decomposition(&verts, &idx), + }) + } + /// Builder for a convex-mesh collider (vertices assumed convex). + /// + /// :raises MeshConversionError: If the mesh is not a valid convex mesh. + #[staticmethod] + fn convex_mesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + rapier::geometry::ColliderBuilder::convex_mesh(verts, &idx) + .map(|b| ColliderBuilder { builder: b }) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "convex mesh construction failed (invalid convex mesh)", + ) + }) + } + /// Builder for a rounded convex-mesh collider. + /// + /// :raises MeshConversionError: If the mesh is not a valid convex mesh. + #[staticmethod] + fn round_convex_mesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + border_radius: Real, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + rapier::geometry::ColliderBuilder::round_convex_mesh(verts, &idx, border_radius) + .map(|b| ColliderBuilder { builder: b }) + .ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "round convex mesh construction failed (invalid convex mesh)", + ) + }) + } + /// Builder for a voxel-grid collider from integer grid coordinates. + /// + /// :param voxel_size: Per-axis size of one voxel cell. + /// :param grid_coords: Sequence of `(i, j, k)` integer cells. + #[staticmethod] + fn voxels(voxel_size: PyVector, grid_coords: &Bound<'_, PyAny>) -> PyResult { + let raw: Vec<(i64, i64, i64)> = grid_coords.extract()?; + let coords: Vec = raw + .iter() + .map(|&(x, y, z)| rapier::math::IVector::new(x as _, y as _, z as _)) + .collect(); + Ok(ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::voxels(voxel_size.0.into(), &coords), + }) + } + /// Builder for a 3D heightfield collider. + /// + /// :param heights: `(rows, cols)` ndarray of floats. + /// :param scale: Per-axis scaling. + #[staticmethod] + fn heightfield(heights: &Bound<'_, PyAny>, scale: PyVector) -> PyResult { + let arr: crate::numpy::PyReadonlyArray2 = heights.extract()?; + let slice = arr.as_slice().map_err(|_| { + crate::pyo3::exceptions::PyValueError::new_err("heights ndarray must be contiguous") + })?; + let nrows = arr.shape()[0]; + let ncols = arr.shape()[1]; + let arr2 = rapier::parry::utils::Array2::new(nrows, ncols, slice.to_vec()); + Ok(ColliderBuilder { + builder: rapier::geometry::ColliderBuilder::heightfield(arr2, scale.0.into()), + }) + } +} + +// ============================================================ +// ColliderBuilder +// ============================================================ + +/// Fluent builder for `Collider` values. +/// +/// Build one via a shape factory on `Collider` (e.g. +/// `Collider.ball(radius)`, `Collider.cuboid(hx, hy, hz)`), chain +/// configuration calls (`density`, `friction`, `sensor`, ...), +/// then call `build()` to obtain a `Collider`. +#[pyclass(name = "ColliderBuilder", module = "rapier")] +#[derive(Clone)] +pub struct ColliderBuilder { + pub builder: rapier::geometry::ColliderBuilder, +} + +impl ColliderBuilder { + pub fn from_kwargs( + base: rapier::geometry::ColliderBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self { builder: base }; + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + me.apply_kwarg(&key, &v)?; + } + } + Ok(me) + } + + fn apply_kwarg(&mut self, key: &str, v: &Bound<'_, PyAny>) -> PyResult<()> { + match key { + "density" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().density(f); + } + "mass" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().mass(f); + } + "friction" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().friction(f); + } + "restitution" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().restitution(f); + } + "sensor" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().sensor(b); + } + "translation" => { + let pv: PyVector = v.extract()?; + self.builder = self.builder.clone().translation(pv.0.into()); + } + "rotation" => { + { + let pv: PyVector = v.extract()?; + self.builder = self.builder.clone().rotation(pv.0.into()); + }; + } + "position" => { + let pi: PyIsometry = v.extract()?; + self.builder = self.builder.clone().position(pi.0.into()); + } + "user_data" => { + let d: u128 = v.extract()?; + self.builder = self.builder.clone().user_data(d); + } + "enabled" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().enabled(b); + } + "contact_skin" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().contact_skin(f); + } + "contact_force_event_threshold" => { + let f: Real = v.extract()?; + self.builder = self.builder.clone().contact_force_event_threshold(f); + } + "active_events" => { + let e: ActiveEvents = v.extract()?; + self.builder = self.builder.clone().active_events(e.0); + } + "active_hooks" => { + let h: ActiveHooks = v.extract()?; + self.builder = self.builder.clone().active_hooks(h.0); + } + "active_collision_types" => { + let t: ActiveCollisionTypes = v.extract()?; + self.builder = self.builder.clone().active_collision_types(t.0); + } + "collision_groups" => { + let g: InteractionGroups = v.extract()?; + self.builder = self.builder.clone().collision_groups(g.0); + } + "solver_groups" => { + let g: InteractionGroups = v.extract()?; + self.builder = self.builder.clone().solver_groups(g.0); + } + "friction_combine_rule" => { + let r: CoefficientCombineRule = v.extract()?; + self.builder = self.builder.clone().friction_combine_rule(r.to_rapier()); + } + "restitution_combine_rule" => { + let r: CoefficientCombineRule = v.extract()?; + self.builder = self.builder.clone().restitution_combine_rule(r.to_rapier()); + } + "mass_properties" => { + let mp: MassProperties = v.extract()?; + self.builder = self.builder.clone().mass_properties(mp.0); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown ColliderBuilder kwarg: '{}'", + key + ))); + } + } + Ok(()) + } +} + +#[pymethods] +impl ColliderBuilder { + /// Set the world-space translation. Returns a new builder. + fn translation(&self, v: PyVector) -> Self { + Self { + builder: self.builder.clone().translation(v.0.into()), + } + } + /// Set the world-space pose. Returns a new builder. + fn position(&self, p: PyIsometry) -> Self { + Self { + builder: self.builder.clone().position(p.0.into()), + } + } + /// Set the uniform density. Returns a new builder. + fn density(&self, d: Real) -> Self { + Self { + builder: self.builder.clone().density(d), + } + } + /// Set an explicit mass (overrides density). Returns a new builder. + fn mass(&self, m: Real) -> Self { + Self { + builder: self.builder.clone().mass(m), + } + } + /// Set the full mass properties. Returns a new builder. + fn mass_properties(&self, mp: &MassProperties) -> Self { + Self { + builder: self.builder.clone().mass_properties(mp.0), + } + } + /// Set the friction coefficient. Returns a new builder. + fn friction(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().friction(f), + } + } + /// Set the restitution coefficient. Returns a new builder. + fn restitution(&self, r: Real) -> Self { + Self { + builder: self.builder.clone().restitution(r), + } + } + /// Set the friction combine rule. Returns a new builder. + fn friction_combine_rule(&self, r: CoefficientCombineRule) -> Self { + Self { + builder: self.builder.clone().friction_combine_rule(r.to_rapier()), + } + } + /// Set the restitution combine rule. Returns a new builder. + fn restitution_combine_rule(&self, r: CoefficientCombineRule) -> Self { + Self { + builder: self.builder.clone().restitution_combine_rule(r.to_rapier()), + } + } + /// Toggle sensor mode (no contact response). Returns a new builder. + fn sensor(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().sensor(b), + } + } + /// Set the opt-in event flags. Returns a new builder. + fn active_events(&self, e: &ActiveEvents) -> Self { + Self { + builder: self.builder.clone().active_events(e.0), + } + } + /// Set the opt-in hook flags. Returns a new builder. + fn active_hooks(&self, h: &ActiveHooks) -> Self { + Self { + builder: self.builder.clone().active_hooks(h.0), + } + } + /// Set the enabled rigid-body-type collision combinations + /// (see `ActiveCollisionTypes`). Returns a new builder. + fn active_collision_types(&self, t: &ActiveCollisionTypes) -> Self { + Self { + builder: self.builder.clone().active_collision_types(t.0), + } + } + /// Set the collision groups. Returns a new builder. + fn collision_groups(&self, g: &InteractionGroups) -> Self { + Self { + builder: self.builder.clone().collision_groups(g.0), + } + } + /// Set the solver groups. Returns a new builder. + fn solver_groups(&self, g: &InteractionGroups) -> Self { + Self { + builder: self.builder.clone().solver_groups(g.0), + } + } + /// Set the contact skin thickness. Returns a new builder. + fn contact_skin(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().contact_skin(f), + } + } + /// Set the contact-force event threshold. Returns a new builder. + fn contact_force_event_threshold(&self, f: Real) -> Self { + Self { + builder: self.builder.clone().contact_force_event_threshold(f), + } + } + /// Set the user-data payload. Returns a new builder. + fn user_data(&self, d: u128) -> Self { + Self { + builder: self.builder.clone().user_data(d), + } + } + /// Toggle the initial enabled state. Returns a new builder. + fn enabled(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().enabled(b), + } + } + + /// Set the rotation. Returns a new builder. + /// + /// In 2D the argument is an angle in radians; in 3D it is a + /// rotation vector (axis * angle). + fn rotation(&self, v: &Bound<'_, PyAny>) -> PyResult { + let mut b = self.builder.clone(); + { + let pv: PyVector = v.extract()?; + b = b.clone().rotation(pv.0.into()); + }; + Ok(Self { builder: b }) + } + + /// Finalize the builder into a `Collider`. + fn build(&self) -> Collider { + Collider::new_owned(self.builder.clone().build()) + } +} + +// ============================================================ +// ColliderSet +// ============================================================ + +/// Owning container of `Collider`s identified by `ColliderHandle`. +/// +/// Acts like a dict keyed by handles: supports `len(set)`, +/// `handle in set`, `set[handle]`, iteration, plus `insert` and +/// `remove`. `set[handle]` returns a live view, so mutating it +/// (e.g. `set[h].set_sensor(True)`) persists in place. +#[pyclass(name = "ColliderSet", module = "rapier", unsendable)] +pub struct ColliderSet(pub rapier::geometry::ColliderSet); + +#[pymethods] +impl ColliderSet { + /// Construct an empty set. + #[new] + fn new() -> Self { + Self(rapier::geometry::ColliderSet::new()) + } + + /// Insert a `Collider` or `ColliderBuilder` standalone (no parent). + /// + /// :returns: The new `ColliderHandle`. + /// :raises TypeError: If `builder` is neither a `Collider` + /// nor a `ColliderBuilder`. + fn insert(&mut self, builder: &Bound<'_, PyAny>) -> PyResult { + if let Ok(b) = builder.extract::>() { + return Ok(ColliderHandle(self.0.insert(b.builder.clone().build()))); + } + if let Ok(c) = builder.extract::>() { + return Ok(ColliderHandle(self.0.insert(c.to_owned_collider()))); + } + Err(PyTypeError::new_err( + "ColliderSet.insert expects a Collider or ColliderBuilder", + )) + } + + /// Insert a collider attached to a parent rigid-body. + /// + /// The collider's position is interpreted relative to the + /// parent's pose. + /// + /// :param builder: A `Collider` or `ColliderBuilder`. + /// :param parent: Handle of the parent rigid-body. + /// :param bodies: The set containing `parent`. + /// :returns: The new `ColliderHandle`. + fn insert_with_parent( + &mut self, + builder: &Bound<'_, PyAny>, + parent: &RigidBodyHandle, + bodies: &mut RigidBodySet, + ) -> PyResult { + let coll = if let Ok(b) = builder.extract::>() { + b.builder.clone().build() + } else if let Ok(c) = builder.extract::>() { + c.to_owned_collider() + } else { + return Err(PyTypeError::new_err( + "ColliderSet.insert_with_parent expects a Collider or ColliderBuilder", + )); + }; + Ok(ColliderHandle(self.0.insert_with_parent( + coll, + parent.0, + &mut bodies.0, + ))) + } + + /// Remove a collider by handle and return it. + /// + /// :param wake_parent: If True, wakes the parent rigid-body so + /// islands re-evaluate. Defaults to True. + /// :returns: The removed `Collider`, or `None` if `handle` is unknown. + #[pyo3(signature = (handle, islands, bodies, wake_parent=true))] + fn remove( + &mut self, + handle: &ColliderHandle, + islands: &mut IslandManager, + bodies: &mut RigidBodySet, + wake_parent: bool, + ) -> Option { + self.0 + .remove(handle.0, &mut islands.0, &mut bodies.0, wake_parent) + .map(Collider::new_owned) + } + + /// Return a live **view** of the collider for `handle`, or `None` + /// if the handle is unknown. Reads and writes go straight through + /// to the set with no copy. + fn get(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> Option { + slf.borrow().0.get(handle.0)?; + Some(Collider { + backing: ColliderBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + fn __getitem__(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> PyResult { + if slf.borrow().0.get(handle.0).is_none() { + return Err(crate::errors::InvalidHandle::new_err(format!( + "no collider for {:?}", + handle.0.into_raw_parts(), + ))); + } + Ok(Collider { + backing: ColliderBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + fn __contains__(&self, handle: &ColliderHandle) -> bool { + self.0.contains(handle.0) + } + fn __len__(&self) -> usize { + self.0.len() + } + /// True iff the set contains no colliders. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Remove every collider from the set. + fn clear(&mut self) { + self.0 = rapier::geometry::ColliderSet::new(); + } + + fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { + let handles: Vec = + slf.borrow().0.iter().map(|(h, _)| h).collect(); + Py::new( + slf.py(), + ColliderSetIter { + set: slf.clone().unbind(), + handles, + i: 0, + }, + ) + } + + /// Return an iterator over the ``ColliderHandle`` values in the set. + fn handles(slf: PyRef<'_, Self>) -> PyResult> { + let h: Vec = slf.0.iter().map(|(h, _)| ColliderHandle(h)).collect(); + Py::new(slf.py(), ColliderHandleIter { handles: h, i: 0 }) + } +} + +/// Iterator yielding `(handle, collider)` pairs from a `ColliderSet`. +#[pyclass] +pub struct ColliderSetIter { + set: Py, + handles: Vec, + i: usize, +} +#[pymethods] +impl ColliderSetIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(ColliderHandle, Collider)> { + if slf.i >= slf.handles.len() { + return None; + } + let py = slf.py(); + let handle = slf.handles[slf.i]; + slf.i += 1; + let set = slf.set.clone_ref(py); + Some(( + ColliderHandle(handle), + Collider { + backing: ColliderBacking::InSet { set, handle }, + }, + )) + } +} + +/// Iterator yielding `ColliderHandle`s from a `ColliderSet`. +#[pyclass] +pub struct ColliderHandleIter { + handles: Vec, + i: usize, +} +#[pymethods] +impl ColliderHandleIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.handles.len() { + return None; + } + let h = slf.handles[slf.i]; + slf.i += 1; + Some(h) + } +} + +// ============================================================ +// ColliderParent / ColliderPosition value types +// ============================================================ + +/// Description of a collider's parent rigid-body and local offset. +/// +/// Holds the parent `RigidBodyHandle` and the pose of the collider +/// expressed in the parent's local frame. +#[pyclass(name = "ColliderParent", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct ColliderParent { + #[pyo3(get)] + pub handle: RigidBodyHandle, + #[pyo3(get)] + pub pos_wrt_parent: Isometry3, +} + +#[pymethods] +impl ColliderParent { + /// Build a `ColliderParent` from a parent handle and local pose. + #[new] + fn new(handle: RigidBodyHandle, pos_wrt_parent: PyIsometry) -> Self { + let iso: crate::na::Isometry = pos_wrt_parent.0; + Self { + handle, + pos_wrt_parent: Isometry3(iso), + } + } +} + +/// Standalone wrapper holding only a collider's world-space pose. +/// +/// Used in some lower-level APIs that decouple the position from +/// the full `Collider` struct. +#[pyclass(name = "ColliderPosition", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct ColliderPosition { + #[pyo3(get)] + pub position: Isometry3, +} + +#[pymethods] +impl ColliderPosition { + /// Build a `ColliderPosition` from a world-space pose. + #[new] + fn new(position: PyIsometry) -> Self { + let iso: crate::na::Isometry = position.0; + Self { + position: Isometry3(iso), + } + } +} + +// ============================================================ +// ColliderMassProps tagged union +// ============================================================ + +/// Tagged union describing how a collider's mass is specified. +/// +/// Build one of the three variants (`Density`, `Mass`, or +/// `MassProperties`) to override the default density-based mass +/// derivation. +#[pyclass(name = "ColliderMassProps", module = "rapier")] +#[derive(Clone)] +pub struct ColliderMassProps(pub rapier::geometry::ColliderMassProps); + +#[pymethods] +impl ColliderMassProps { + /// Build the `Density` variant from a uniform density value. + #[staticmethod] + #[pyo3(name = "Density")] + fn density(d: Real) -> Self { + Self(rapier::geometry::ColliderMassProps::Density(d)) + } + /// Build the `Mass` variant from an explicit mass value. + #[staticmethod] + #[pyo3(name = "Mass")] + fn mass(m: Real) -> Self { + Self(rapier::geometry::ColliderMassProps::Mass(m)) + } + /// Build the `MassProperties` variant from a full mass-properties value. + #[staticmethod] + #[pyo3(name = "MassProperties")] + fn mass_properties(mp: MassProperties) -> Self { + Self(rapier::geometry::ColliderMassProps::MassProperties( + Box::new(mp.0), + )) + } +} + +pub fn register_geometry( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // Shape views + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/joints.rs b/python/rapier-py-3d/src/joints.rs new file mode 100644 index 000000000..8a1db4cff --- /dev/null +++ b/python/rapier-py-3d/src/joints.rs @@ -0,0 +1,4176 @@ +//! The concrete 3D / `f32` joint `#[pyclass]` types and the `register_joints` +//! function used by `#[pymodule]`. +//! +//! Joints. This module used to be generated by a `(Real, DIM)`-parameterized +//! macro so that several cdylibs could each expand their own concrete joint +//! types. There is now a single concrete target — 3D, `f32` — so the types are +//! written out directly, with no macros. +//! +//! Note: `RigidBodyHandle`, `RigidBodySet`, and friends are defined by the +//! dynamics module (registered later). Rust resolves those names at +//! item-resolution time (after all modules are in the same crate), so +//! referring to them here is valid even though this module comes first. +//! +//! `register_joints(py, m)` is the `#[pymodule]` entry point. + +use crate::pyo3::exceptions::{PyTypeError, PyValueError}; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; +use crate::*; +use rapier3d as rapier; + +// ----------------------------------------------------------------- +// Note on cross-macro references +// ----------------------------------------------------------------- +// The `ImpulseJointSet` and `MultibodyJointSet` types emitted by +// this macro reference `RigidBodyHandle` and `RigidBodySet`, which +// are defined later by `define_dynamics_types!`. Rust resolves +// those names at item-resolution time (after *all* macros have +// expanded into the same module), so referring to them here is +// valid even though the dynamics macro is invoked after this one. + +// ----------------------------------------------------------------- +// JointEnabled enum +// ----------------------------------------------------------------- +/// Tri-state enable flag carried by every joint. +/// +/// ``ENABLED`` joints take part in constraint resolution. +/// ``DISABLED`` joints are kept in the set but ignored by the solver +/// (use this to temporarily turn a joint off without removing it). +/// ``DISABLED_BY_ATTACHED_BODY`` is set automatically by the engine +/// when one of the bodies the joint is attached to becomes disabled; +/// the joint re-enables itself once the body is re-enabled. +#[pyclass(name = "JointEnabled", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JointEnabled { + /// Joint participates normally in the constraint solver. + ENABLED, + /// Joint is implicitly disabled because one of its attached + /// rigid bodies is itself disabled. Set automatically by the + /// engine and cleared once the body is re-enabled. + DISABLED_BY_ATTACHED_BODY, + /// Joint is explicitly disabled by the user and ignored by the + /// solver until re-enabled. + DISABLED, +} + +impl JointEnabled { + #[inline] + pub(crate) fn from_rapier(e: rapier::dynamics::JointEnabled) -> Self { + match e { + rapier::dynamics::JointEnabled::Enabled => Self::ENABLED, + rapier::dynamics::JointEnabled::DisabledByAttachedBody => { + Self::DISABLED_BY_ATTACHED_BODY + } + rapier::dynamics::JointEnabled::Disabled => Self::DISABLED, + } + } + #[allow(dead_code)] + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::JointEnabled { + match self { + Self::ENABLED => rapier::dynamics::JointEnabled::Enabled, + Self::DISABLED_BY_ATTACHED_BODY => { + rapier::dynamics::JointEnabled::DisabledByAttachedBody + } + Self::DISABLED => rapier::dynamics::JointEnabled::Disabled, + } + } +} + +// ----------------------------------------------------------------- +// MotorModel enum +// ----------------------------------------------------------------- +/// Motor model used to convert PID error into a drive force. +/// +/// Both variants implement an internal PD controller around the +/// motor's ``(target_pos, target_vel, stiffness, damping)`` set +/// point. They differ in how that controller output is applied to +/// the bodies: +/// +/// * ``ACCELERATION_BASED`` interprets the controller output as a +/// desired acceleration and scales it by the effective mass of +/// the constrained axis. This gives motor behaviour that is +/// largely independent of the masses of the bodies and is the +/// recommended default. +/// * ``FORCE_BASED`` applies the controller output directly as a +/// generalized force; the resulting acceleration depends on the +/// effective mass. +/// +/// Note: a third ``VelocityBased`` variant existed in earlier +/// versions but was removed. To drive a pure velocity target, +/// configure a motor with ``stiffness=0`` and the desired +/// ``target_vel`` (see :py:meth:`set_motor_velocity`). +#[pyclass(name = "MotorModel", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MotorModel { + /// PID output is interpreted as a desired acceleration; the + /// engine scales by effective mass before applying. Behaviour + /// is largely mass-independent. + ACCELERATION_BASED, + /// PID output is applied directly as a generalized force; the + /// resulting motion depends on the bodies' masses and inertia. + FORCE_BASED, +} + +impl MotorModel { + #[inline] + pub(crate) fn from_rapier(m: rapier::dynamics::MotorModel) -> Self { + match m { + rapier::dynamics::MotorModel::AccelerationBased => Self::ACCELERATION_BASED, + rapier::dynamics::MotorModel::ForceBased => Self::FORCE_BASED, + } + } + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::MotorModel { + match self { + Self::ACCELERATION_BASED => rapier::dynamics::MotorModel::AccelerationBased, + Self::FORCE_BASED => rapier::dynamics::MotorModel::ForceBased, + } + } +} + +// ----------------------------------------------------------------- +// JointLimits — plain struct +// ----------------------------------------------------------------- +/// Inclusive ``[min, max]`` range that a joint axis is constrained to. +/// +/// Units are axis-dependent: world-length units for linear axes +/// (``LIN_X``/``LIN_Y``/``LIN_Z``) and radians for angular axes +/// (``ANG_X``/``ANG_Y``/``ANG_Z``). ``impulse`` is the last +/// constraint impulse applied by the solver to enforce the limit +/// and is set by the engine, not the user. +#[pyclass(name = "JointLimits", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct JointLimits { + #[pyo3(get, set)] + pub min: Real, + #[pyo3(get, set)] + pub max: Real, + #[pyo3(get, set)] + pub impulse: Real, +} + +#[pymethods] +impl JointLimits { + /// Construct a new limit range. + /// + /// :param min: Lower bound (radians for angular axes, world + /// units for linear axes). + /// :param max: Upper bound, same units as ``min``. + /// :param impulse: Initial impulse value, generally left at 0. + #[new] + #[pyo3(signature = (min=0.0 as Real, max=0.0 as Real, impulse=0.0 as Real))] + fn new(min: Real, max: Real, impulse: Real) -> Self { + Self { min, max, impulse } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + format!( + "JointLimits(min={}, max={}, impulse={})", + self.min, self.max, self.impulse + ) + } +} + +impl JointLimits { + #[inline] + pub(crate) fn from_rapier(l: &rapier::dynamics::JointLimits) -> Self { + Self { + min: l.min, + max: l.max, + impulse: l.impulse, + } + } +} + +// ----------------------------------------------------------------- +// JointMotor — plain struct +// ----------------------------------------------------------------- +/// Motor configuration driving a single joint axis. +/// +/// The motor implements a PD controller of the form +/// ``force = stiffness * (target_pos - pos) + damping * (target_vel - vel)``, +/// optionally clamped by ``max_force``. The ``model`` field selects +/// how that force is converted into joint motion (see +/// :class:`MotorModel`). ``impulse`` is the impulse applied on the +/// last solver step and is written by the engine. +/// +/// Convenience helpers on each concrete joint type (e.g. +/// ``set_motor_velocity``, ``set_motor_position``, ``set_motor``) +/// build the right combination of fields without constructing this +/// struct directly. +#[pyclass(name = "JointMotor", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct JointMotor { + #[pyo3(get, set)] + pub target_vel: Real, + #[pyo3(get, set)] + pub target_pos: Real, + #[pyo3(get, set)] + pub stiffness: Real, + #[pyo3(get, set)] + pub damping: Real, + #[pyo3(get, set)] + pub max_force: Real, + #[pyo3(get, set)] + pub impulse: Real, + #[pyo3(get, set)] + pub model: MotorModel, +} + +#[pymethods] +impl JointMotor { + /// Construct a new motor specification. + /// + /// :param target_vel: Velocity setpoint of the PD controller. + /// :param target_pos: Position setpoint of the PD controller. + /// :param stiffness: Position-error gain (spring term). + /// :param damping: Velocity-error gain (damper term). + /// :param max_force: Hard clamp on the generalized force the + /// motor may apply. + /// :param impulse: Last-step impulse, generally left at 0 on + /// construction. + /// :param model: Motor model (see :class:`MotorModel`). + #[new] + #[pyo3(signature = ( + target_vel=0.0 as Real, target_pos=0.0 as Real, + stiffness=0.0 as Real, damping=0.0 as Real, + max_force=Real::MAX, impulse=0.0 as Real, + model=MotorModel::ACCELERATION_BASED, + ))] + fn new( + target_vel: Real, + target_pos: Real, + stiffness: Real, + damping: Real, + max_force: Real, + impulse: Real, + model: MotorModel, + ) -> Self { + Self { + target_vel, + target_pos, + stiffness, + damping, + max_force, + impulse, + model, + } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + format!( + "JointMotor(target_pos={}, target_vel={}, stiffness={}, damping={}, max_force={}, model={:?})", + self.target_pos, + self.target_vel, + self.stiffness, + self.damping, + self.max_force, + self.model, + ) + } +} + +impl JointMotor { + #[inline] + pub(crate) fn from_rapier(m: &rapier::dynamics::JointMotor) -> Self { + Self { + target_vel: m.target_vel, + target_pos: m.target_pos, + stiffness: m.stiffness, + damping: m.damping, + max_force: m.max_force, + impulse: m.impulse, + model: MotorModel::from_rapier(m.model), + } + } +} + +// ----------------------------------------------------------------- +// JointAxesMask bitflags +// ----------------------------------------------------------------- +/// Bitflag set selecting a subset of joint axes. +/// +/// In 3D the available bits are ``LIN_X``, ``LIN_Y``, ``LIN_Z``, +/// ``ANG_X``, ``ANG_Y``, ``ANG_Z`` plus convenience aggregates +/// (``LIN_AXES``, ``ANG_AXES``, ``LOCKED_REVOLUTE_AXES``, +/// ``LOCKED_PRISMATIC_AXES``, ``LOCKED_FIXED_AXES``, +/// ``LOCKED_SPHERICAL_AXES``, and their ``FREE_*`` complements). +/// In 2D only ``LIN_X``, ``LIN_Y`` and ``ANG_X`` exist, along with +/// ``LOCKED_PIN_SLOT_AXES``. +/// +/// The class supports the usual bitwise operators (``|``, ``&``, +/// ``^``, ``~``, ``-``), ``in``, truthiness, and equality. Used +/// throughout :class:`GenericJoint` to describe locked / coupled +/// axes, and by :class:`InverseKinematicsOption` to restrict the +/// IK solver to a subset of axes. +#[pyclass(name = "JointAxesMask", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JointAxesMask(pub rapier::dynamics::JointAxesMask); + +#[pymethods] +impl JointAxesMask { + /// Construct a mask from a raw bit pattern. + /// + /// :param bits: Raw byte; unknown bits are truncated. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u8) -> Self { + Self(rapier::dynamics::JointAxesMask::from_bits_truncate(bits)) + } + /// Return the empty mask (no axes selected). + #[staticmethod] + fn empty() -> Self { + Self(rapier::dynamics::JointAxesMask::empty()) + } + /// Return the full mask (every axis selected). + #[staticmethod] + fn all_axes() -> Self { + Self(rapier::dynamics::JointAxesMask::all()) + } + + // Per-axis classattrs come from the dim-specific helper to keep + // dim2 / dim3 distinct. + #[classattr] + const LIN_X: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::LIN_X); + #[classattr] + const LIN_Y: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::LIN_Y); + #[classattr] + const ANG_X: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_X); + #[classattr] + const LIN_AXES: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::LIN_AXES); + #[classattr] + const ANG_AXES: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_AXES); + #[classattr] + const LOCKED_REVOLUTE_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_REVOLUTE_AXES); + #[classattr] + const LOCKED_PRISMATIC_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_PRISMATIC_AXES); + #[classattr] + const LOCKED_FIXED_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_FIXED_AXES); + #[classattr] + const FREE_REVOLUTE_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::FREE_REVOLUTE_AXES); + #[classattr] + const FREE_PRISMATIC_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::FREE_PRISMATIC_AXES); + #[classattr] + const FREE_FIXED_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::FREE_FIXED_AXES); + + /// Raw bit value backing this mask. + #[getter] + fn bits(&self) -> u8 { + self.0.bits() + } + /// Return ``True`` if every axis in ``other`` is also set here. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Return ``True`` if no axes are selected. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// ``other in self`` — same as :py:meth:`contains`. + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Bitwise OR (set union). + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND (set intersection). + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Bitwise XOR (set symmetric difference). + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Bitwise NOT (complement within the known axes). + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// Set difference (``self - other``). + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + /// Truthiness: ``True`` iff at least one axis is selected. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality and inequality comparisons; other operators raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "JointAxesMask supports only == and !=", + )), + } + } + /// Hash matching equality semantics. + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + format!("JointAxesMask(bits={:#010b})", self.0.bits()) + } +} + +// ----------------------------------------------------------------- +// JointAxis enum (dim-specific variants) +// ----------------------------------------------------------------- + +/// Enumeration of the degrees of freedom of a 3D joint. +/// +/// In 3D there are six axes: three linear +/// (``LIN_X``/``LIN_Y``/``LIN_Z``) and three angular +/// (``ANG_X``/``ANG_Y``/``ANG_Z``) expressed in the joint's local +/// frame. Pair with :class:`JointAxesMask` for set operations. +#[pyclass(name = "JointAxis", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JointAxis { + /// Translation along the body-local X axis. + LIN_X, + /// Translation along the body-local Y axis. + LIN_Y, + /// Translation along the body-local Z axis. + LIN_Z, + /// Rotation around the body-local X axis. + ANG_X, + /// Rotation around the body-local Y axis. + ANG_Y, + /// Rotation around the body-local Z axis. + ANG_Z, +} + +impl JointAxis { + #[inline] + pub(crate) fn to_rapier(self) -> rapier::dynamics::JointAxis { + match self { + JointAxis::LIN_X => rapier::dynamics::JointAxis::LinX, + JointAxis::LIN_Y => rapier::dynamics::JointAxis::LinY, + JointAxis::LIN_Z => rapier::dynamics::JointAxis::LinZ, + JointAxis::ANG_X => rapier::dynamics::JointAxis::AngX, + JointAxis::ANG_Y => rapier::dynamics::JointAxis::AngY, + JointAxis::ANG_Z => rapier::dynamics::JointAxis::AngZ, + } + } +} + +// 3D-only JointAxesMask classattrs (LIN_Z, ANG_Y, ANG_Z, plus +// LOCKED_SPHERICAL_AXES & FREE_SPHERICAL_AXES). 2D doesn't define +// those bits in the rapier::dynamics::JointAxesMask bitflags, so +// referencing them is gated by `3`. + +#[pymethods] +impl JointAxesMask { + #[classattr] + const LIN_Z: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::LIN_Z); + #[classattr] + const ANG_Y: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_Y); + #[classattr] + const ANG_Z: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_Z); + #[classattr] + const LOCKED_SPHERICAL_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_SPHERICAL_AXES); + #[classattr] + const FREE_SPHERICAL_AXES: JointAxesMask = + JointAxesMask(rapier::dynamics::JointAxesMask::FREE_SPHERICAL_AXES); +} + +// ================================================================= +// ImpulseJointHandle +// ================================================================= +/// Opaque handle identifying a joint inside an :class:`ImpulseJointSet`. +/// +/// Hashable and equality-comparable, so it can be used as a dict +/// key or stored in sets. Handles remain valid until the joint is +/// removed; a removed slot may be reused with a new ``generation``. +#[pyclass(name = "ImpulseJointHandle", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ImpulseJointHandle(pub rapier::dynamics::ImpulseJointHandle); + +#[pymethods] +impl ImpulseJointHandle { + /// Build a handle from raw ``(index, generation)`` parts. + /// + /// Mostly useful for round-tripping a handle through + /// serialization; in normal use, handles are returned by + /// :py:meth:`ImpulseJointSet.insert`. + #[new] + #[pyo3(signature = (index=0, generation=0))] + fn new(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::ImpulseJointHandle::from_raw_parts( + index, generation, + )) + } + /// Reconstruct a handle from raw ``(index, generation)`` parts. + #[staticmethod] + fn from_raw_parts(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::ImpulseJointHandle::from_raw_parts( + index, generation, + )) + } + /// Return the sentinel "invalid" handle that points to nothing. + #[staticmethod] + fn invalid() -> Self { + Self(rapier::dynamics::ImpulseJointHandle::invalid()) + } + /// Slot index of this handle. + #[getter] + fn index(&self) -> u32 { + self.0.into_raw_parts().0 + } + /// Generation counter, incremented when a slot is reused. + #[getter] + fn generation(&self) -> u32 { + self.0.into_raw_parts().1 + } + /// Hash matching equality semantics. + fn __hash__(&self) -> u64 { + let (i, g) = self.0.into_raw_parts(); + ((i as u64) << 32) | (g as u64) + } + /// Equality and inequality comparisons; other operators raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "ImpulseJointHandle supports only == and !=", + )), + } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + let (i, g) = self.0.into_raw_parts(); + format!("ImpulseJointHandle(index={}, generation={})", i, g) + } +} + +// ================================================================= +// MultibodyJointHandle +// ================================================================= +/// Opaque handle identifying a joint inside a :class:`MultibodyJointSet`. +/// +/// Hashable and equality-comparable. A multibody joint handle +/// references the joint between a multibody link and its parent +/// inside one reduced-coordinate articulation. +#[pyclass(name = "MultibodyJointHandle", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MultibodyJointHandle(pub rapier::dynamics::MultibodyJointHandle); + +#[pymethods] +impl MultibodyJointHandle { + /// Build a handle from raw ``(index, generation)`` parts. + #[new] + #[pyo3(signature = (index=0, generation=0))] + fn new(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::MultibodyJointHandle::from_raw_parts( + index, generation, + )) + } + /// Reconstruct a handle from raw ``(index, generation)`` parts. + #[staticmethod] + fn from_raw_parts(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::MultibodyJointHandle::from_raw_parts( + index, generation, + )) + } + /// Return the sentinel "invalid" handle that points to nothing. + #[staticmethod] + fn invalid() -> Self { + Self(rapier::dynamics::MultibodyJointHandle::invalid()) + } + /// Slot index of this handle. + #[getter] + fn index(&self) -> u32 { + self.0.into_raw_parts().0 + } + /// Generation counter, incremented when a slot is reused. + #[getter] + fn generation(&self) -> u32 { + self.0.into_raw_parts().1 + } + /// Hash matching equality semantics. + fn __hash__(&self) -> u64 { + let (i, g) = self.0.into_raw_parts(); + ((i as u64) << 32) | (g as u64) + } + /// Equality and inequality comparisons; other operators raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "MultibodyJointHandle supports only == and !=", + )), + } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + let (i, g) = self.0.into_raw_parts(); + format!("MultibodyJointHandle(index={}, generation={})", i, g) + } +} + +// ================================================================= +// MultibodyIndex (opaque) +// ================================================================= +/// Opaque handle identifying a single :class:`Multibody` articulation. +/// +/// Hashable and equality-comparable. Multiple +/// :class:`MultibodyJointHandle` instances can resolve to the same +/// ``MultibodyIndex`` when they belong to the same articulation. +#[pyclass(name = "MultibodyIndex", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MultibodyIndex(pub rapier::dynamics::MultibodyIndex); + +#[pymethods] +impl MultibodyIndex { + /// Slot index of this multibody. + #[getter] + fn index(&self) -> u32 { + self.0.0.into_raw_parts().0 + } + /// Generation counter, incremented when a slot is reused. + #[getter] + fn generation(&self) -> u32 { + self.0.0.into_raw_parts().1 + } + /// Hash matching equality semantics. + fn __hash__(&self) -> u64 { + let (i, g) = self.0.0.into_raw_parts(); + ((i as u64) << 32) | (g as u64) + } + /// Equality and inequality comparisons; other operators raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "MultibodyIndex supports only == and !=", + )), + } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + let (i, g) = self.0.0.into_raw_parts(); + format!("MultibodyIndex(index={}, generation={})", i, g) + } +} + +// ================================================================= +// MultibodyLinkId — opaque struct with `multibody`, `id` +// ================================================================= +/// Address of a single link within a multibody articulation. +/// +/// Combines a :class:`MultibodyIndex` (which articulation) with an +/// integer ``id`` (which link inside it). Hashable and +/// equality-comparable. +#[pyclass(name = "MultibodyLinkId", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct MultibodyLinkId(pub rapier::dynamics::MultibodyLinkId); + +#[pymethods] +impl MultibodyLinkId { + /// Articulation containing this link. + #[getter] + fn multibody(&self) -> MultibodyIndex { + MultibodyIndex(self.0.multibody) + } + /// Zero-based link index inside the articulation (0 == root). + #[getter] + fn id(&self) -> usize { + self.0.id + } + /// Equality and inequality comparisons; other operators raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "MultibodyLinkId supports only == and !=", + )), + } + } + /// Hash matching equality semantics. + fn __hash__(&self) -> u64 { + let (i, g) = self.0.multibody.0.into_raw_parts(); + (((i as u64) << 32) | (g as u64)) ^ (self.0.id as u64) + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + let (i, g) = self.0.multibody.0.into_raw_parts(); + format!( + "MultibodyLinkId(multibody=MultibodyIndex(index={}, generation={}), id={})", + i, g, self.0.id + ) + } +} + +// ================================================================= +// InverseKinematicsOption — dataclass-style +// ================================================================= +/// Tuning parameters for the multibody inverse-kinematics solver. +/// +/// IK is performed by damped least-squares: at each iteration the +/// solver builds the Jacobian of the link's pose w.r.t. the joint +/// coordinates and solves a damped linear system for the +/// per-iteration displacement, applying it in a loop until either +/// the residual is below the linear/angular epsilons or +/// ``max_iters`` is reached. +/// +/// :ivar damping: Levenberg-Marquardt damping term added to the +/// normal equations; larger values give a slower but more +/// stable solve near singular postures. +/// :ivar max_iters: Upper bound on the iteration count. +/// :ivar constrained_axes: Subset of axes the IK target should +/// match. By default all axes are constrained. +/// :ivar epsilon_linear: Linear residual tolerance (world units). +/// :ivar epsilon_angular: Angular residual tolerance (radians). +#[pyclass(name = "InverseKinematicsOption", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct InverseKinematicsOption { + #[pyo3(get, set)] + pub damping: Real, + #[pyo3(get, set)] + pub max_iters: usize, + #[pyo3(get, set)] + pub constrained_axes: JointAxesMask, + #[pyo3(get, set)] + pub epsilon_linear: Real, + #[pyo3(get, set)] + pub epsilon_angular: Real, +} + +#[pymethods] +impl InverseKinematicsOption { + /// Build a new IK option block. + /// + /// :param damping: Damping factor for the damped least-squares + /// solve. Defaults to 1.0. + /// :param max_iters: Maximum number of iterations. Defaults to 10. + /// :param constrained_axes: Axes to match against the target. + /// Defaults to all axes. + /// :param epsilon_linear: Convergence tolerance for the + /// translational residual (world units). Defaults to 1e-3. + /// :param epsilon_angular: Convergence tolerance for the + /// rotational residual (radians). Defaults to 1e-3. + #[new] + #[pyo3(signature = ( + damping=1.0 as Real, max_iters=10usize, + constrained_axes=None, + epsilon_linear=1.0e-3 as Real, epsilon_angular=1.0e-3 as Real, + ))] + fn new( + damping: Real, + max_iters: usize, + constrained_axes: Option, + epsilon_linear: Real, + epsilon_angular: Real, + ) -> Self { + let ca = constrained_axes + .unwrap_or_else(|| JointAxesMask(rapier::dynamics::JointAxesMask::all())); + Self { + damping, + max_iters, + constrained_axes: ca, + epsilon_linear, + epsilon_angular, + } + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + format!( + "InverseKinematicsOption(damping={}, max_iters={}, epsilon_linear={}, epsilon_angular={})", + self.damping, self.max_iters, self.epsilon_linear, self.epsilon_angular + ) + } +} + +impl InverseKinematicsOption { + #[inline] + pub(crate) fn to_rapier(&self) -> rapier::dynamics::InverseKinematicsOption { + rapier::dynamics::InverseKinematicsOption { + damping: self.damping, + max_iters: self.max_iters, + constrained_axes: self.constrained_axes.0, + epsilon_linear: self.epsilon_linear, + epsilon_angular: self.epsilon_angular, + } + } +} + +// ================================================================= +// FixedJoint + FixedJointBuilder +// ================================================================= +/// Rigidly weld two bodies together: no relative motion at all. +/// +/// All translational and rotational DOFs are locked, so the two +/// bodies behave as a single rigid object connected by their +/// local frames. Useful for composing bodies that should never +/// move relative to each other but still need to be separable +/// (unlike a true compound shape). +#[pyclass(name = "FixedJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct FixedJoint(pub rapier::dynamics::FixedJoint); + +#[pymethods] +impl FixedJoint { + /// Construct a fixed joint with identity local frames. + #[new] + fn new() -> Self { + Self(rapier::dynamics::FixedJoint::new()) + } + + /// Build a :class:`FixedJointBuilder`. + /// + /// :param body_a: Ignored (kept for API symmetry). + /// :param body_b: Ignored (kept for API symmetry). + /// :param kwargs: Optional keyword args forwarded to the + /// builder; accepted keys are ``local_anchor1``, + /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, + /// ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] + fn builder( + body_a: Option<&Bound<'_, PyAny>>, + body_b: Option<&Bound<'_, PyAny>>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let _ = (body_a, body_b); + FixedJointBuilder::from_kwargs(rapier::dynamics::FixedJointBuilder::new(), kwargs) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.contacts_enabled() + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.set_contacts_enabled(v); + } + + /// Body-local frame (position + orientation) on body 1. + #[getter] + fn local_frame1(&self) -> Isometry3 { + let pose: crate::na::Isometry = (*self.0.local_frame1()).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 1. + #[setter] + fn set_local_frame1(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.0.set_local_frame1(p); + } + /// Body-local frame on body 2. + #[getter] + fn local_frame2(&self) -> Isometry3 { + let pose: crate::na::Isometry = (*self.0.local_frame2()).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 2. + #[setter] + fn set_local_frame2(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.0.set_local_frame2(p); + } + + /// Body-local point on body 1 where the joint is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the joint is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } + /// Spring coefficients controlling this joint's softness + /// (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.softness()) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + let _ = self.0.set_softness(v.0); + } +} + +/// Fluent builder for :class:`FixedJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`FixedJoint`. +#[pyclass(name = "FixedJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct FixedJointBuilder(pub rapier::dynamics::FixedJointBuilder); + +impl FixedJointBuilder { + fn from_kwargs( + base: rapier::dynamics::FixedJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "local_frame1" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame1(p); + } + "local_frame2" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame2(p); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown FixedJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl FixedJointBuilder { + /// Construct an empty :class:`FixedJointBuilder`. + #[new] + fn new() -> Self { + Self(rapier::dynamics::FixedJointBuilder::new()) + } + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Set the body-local frame on body 1. + fn local_frame1(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame1(p)) + } + /// Set the body-local frame on body 2. + fn local_frame2(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame2(p)) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Set the spring softness coefficients (see + /// :class:`SpringCoefficients`). Returns a new builder. + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`FixedJoint`. + fn build(&self) -> FixedJoint { + FixedJoint(self.0.build()) + } +} + +// ================================================================= +// RevoluteJoint + RevoluteJointBuilder +// ================================================================= +/// Hinge / pin joint allowing a single rotational DOF. +/// +/// All three translational DOFs are locked and, in 3D, the two +/// rotational DOFs orthogonal to the rotation axis are locked +/// as well, leaving a single rotational degree of freedom about +/// ``local_axis1``/``local_axis2``. In 2D the axis is implicit +/// (the out-of-plane direction), so the constructor takes no +/// axis argument; in 3D it is required. +#[pyclass(name = "RevoluteJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RevoluteJoint(pub rapier::dynamics::RevoluteJoint); + +#[pymethods] +impl RevoluteJoint { + /// Construct a new revolute joint. + /// + /// :param axis: Rotation axis (required in 3D, ignored in 2D + /// where the axis is implicitly out-of-plane). + /// :raises TypeError: If ``axis`` is missing in 3D. + // Dim-agnostic constructor: 2D = no arg, 3D = required axis. + #[new] + #[pyo3(signature = (axis=None))] + fn new(axis: Option) -> PyResult { + { + let axis: PyVector = axis.ok_or_else(|| { + crate::pyo3::exceptions::PyTypeError::new_err( + "RevoluteJoint.new(axis=Vector) is required in 3D", + ) + })?; + let g: rapier::math::Vector = axis.0.into(); + Ok(Self(rapier::dynamics::RevoluteJoint::new(g))) + } + } + /// Build a :class:`RevoluteJointBuilder`. + /// + /// :param axis: Rotation axis (required in 3D, ignored in 2D). + /// :param kwargs: Optional keyword args forwarded to the + /// builder; accepted keys are ``local_anchor1``, + /// ``local_anchor2``, ``limits``, ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (axis=None, **kwargs))] + fn builder( + axis: Option, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let base = { + let axis: PyVector = axis.ok_or_else(|| { + crate::pyo3::exceptions::PyTypeError::new_err( + "RevoluteJoint.builder(axis=Vector) is required in 3D", + ) + })?; + let g: rapier::math::Vector = axis.0.into(); + Ok::( + rapier::dynamics::RevoluteJointBuilder::new(g), + ) + }?; + RevoluteJointBuilder::from_kwargs(base, kwargs) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.contacts_enabled() + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.set_contacts_enabled(v); + } + + /// Body-local point on body 1 where the joint is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the joint is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Return the angular limits, if configured. + fn limits(&self) -> Option { + self.0.limits().map(JointLimits::from_rapier) + } + /// Return the angular motor, if configured. + fn motor(&self) -> Option { + self.0.motor().map(JointMotor::from_rapier) + } + /// Apply angular ``[min, max]`` limits (radians). + fn set_limits(&mut self, min: Real, max: Real) { + self.0.set_limits([min, max]); + } + /// Configure the joint as a velocity-target motor with damping. + /// + /// :param target_vel: Desired angular velocity (rad/s). + /// :param factor: Damping coefficient. + fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) { + self.0.set_motor_velocity(target_vel, factor); + } + /// Configure the joint as a spring-damper motor toward + /// ``target_pos`` (radians). + fn set_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { + self.0.set_motor_position(target_pos, stiffness, damping); + } + /// Fully configure the motor with position and velocity setpoints. + fn set_motor(&mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) { + self.0.set_motor(target_pos, target_vel, stiffness, damping); + } + /// Clamp the maximum torque the motor can apply. + fn set_motor_max_force(&mut self, max_force: Real) { + self.0.set_motor_max_force(max_force); + } + /// Select the motor model (see :class:`MotorModel`). + fn set_motor_model(&mut self, model: MotorModel) { + self.0.set_motor_model(model.to_rapier()); + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } + /// Spring coefficients controlling this joint's softness + /// (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.softness()) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + let _ = self.0.set_softness(v.0); + } + /// Current hinge angle, in radians, given the world-space + /// rotations of the two attached bodies. + /// + /// :param body_rotation1: World rotation of body 1 + /// (e.g. ``RigidBody.rotation``). + /// :param body_rotation2: World rotation of body 2. + fn angle(&self, body_rotation1: PyRotation, body_rotation2: PyRotation) -> Real { + let i1 = + crate::na::Isometry::from_parts(crate::na::Translation::default(), body_rotation1.0); + let i2 = + crate::na::Isometry::from_parts(crate::na::Translation::default(), body_rotation2.0); + let p1: rapier::math::Pose = i1.into(); + let p2: rapier::math::Pose = i2.into(); + self.0.angle(&p1.rotation, &p2.rotation) + } +} + +/// Fluent builder for :class:`RevoluteJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`RevoluteJoint`. +#[pyclass(name = "RevoluteJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RevoluteJointBuilder(pub rapier::dynamics::RevoluteJointBuilder); + +impl RevoluteJointBuilder { + fn from_kwargs( + base: rapier::dynamics::RevoluteJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "limits" => { + let (min, max): (Real, Real) = v.extract()?; + me.0 = me.0.limits([min, max]); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown RevoluteJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl RevoluteJointBuilder { + /// Construct a new revolute joint builder. + /// + /// :param axis: Rotation axis (required in 3D, ignored in 2D). + #[new] + #[pyo3(signature = (axis=None))] + fn new(axis: Option) -> PyResult { + let base = { + let axis: PyVector = axis.ok_or_else(|| { + crate::pyo3::exceptions::PyTypeError::new_err( + "RevoluteJoint.builder(axis=Vector) is required in 3D", + ) + })?; + let g: rapier::math::Vector = axis.0.into(); + Ok::( + rapier::dynamics::RevoluteJointBuilder::new(g), + ) + }?; + Ok(Self(base)) + } + + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Apply angular ``[min, max]`` limits (radians). + fn limits(&self, min: Real, max: Real) -> Self { + Self(self.0.limits([min, max])) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Configure a velocity-target motor with damping. + fn motor_velocity(&self, target_vel: Real, factor: Real) -> Self { + Self(self.0.motor_velocity(target_vel, factor)) + } + /// Configure a spring-damper motor toward ``target_pos`` (radians). + fn motor_position(&self, target_pos: Real, stiffness: Real, damping: Real) -> Self { + Self(self.0.motor_position(target_pos, stiffness, damping)) + } + /// Fully configure the motor with both position and velocity + /// setpoints. + fn motor(&self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) -> Self { + Self(self.0.motor(target_pos, target_vel, stiffness, damping)) + } + /// Clamp the maximum torque the motor can apply. + fn motor_max_force(&self, max_force: Real) -> Self { + Self(self.0.motor_max_force(max_force)) + } + /// Select the motor model (see :class:`MotorModel`). + fn motor_model(&self, model: MotorModel) -> Self { + Self(self.0.motor_model(model.to_rapier())) + } + /// Set the spring softness coefficients (see + /// :class:`SpringCoefficients`). Returns a new builder. + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`RevoluteJoint`. + fn build(&self) -> RevoluteJoint { + RevoluteJoint(self.0.build()) + } +} + +// ================================================================= +// PrismaticJoint + PrismaticJointBuilder +// ================================================================= +/// Slider joint allowing a single translational DOF. +/// +/// All rotational DOFs and the two translations orthogonal to +/// ``local_axis1``/``local_axis2`` are locked, leaving a single +/// translational degree of freedom along the configured axis. The +/// axis must be supplied at construction time. +#[pyclass(name = "PrismaticJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct PrismaticJoint(pub rapier::dynamics::PrismaticJoint); + +#[pymethods] +impl PrismaticJoint { + /// Construct a new prismatic joint with the given sliding axis. + /// + /// :param axis: Translation axis in each body's local frame. + #[new] + fn new(axis: PyVector) -> Self { + let g: rapier::math::Vector = axis.0.into(); + Self(rapier::dynamics::PrismaticJoint::new(g)) + } + /// Build a :class:`PrismaticJointBuilder`. + /// + /// :param axis: Translation axis in each body's local frame. + /// :param kwargs: Optional keyword args forwarded to the + /// builder; accepted keys include ``local_anchor1``, + /// ``local_anchor2``, ``local_axis1``, ``local_axis2``, + /// ``limits``, ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (axis, **kwargs))] + fn builder( + axis: PyVector, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let g: rapier::math::Vector = axis.0.into(); + PrismaticJointBuilder::from_kwargs(rapier::dynamics::PrismaticJointBuilder::new(g), kwargs) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.contacts_enabled() + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.set_contacts_enabled(v); + } + + /// Body-local point on body 1 where the joint is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the joint is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Body-local translation axis on body 1. + #[getter] + fn local_axis1(&self) -> Vec3 { + let v: crate::na::SVector = self.0.local_axis1().into(); + Vec3(v) + } + /// Set the body-local translation axis on body 1. + #[setter] + fn set_local_axis1(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.0.set_local_axis1(g); + } + /// Body-local translation axis on body 2. + #[getter] + fn local_axis2(&self) -> Vec3 { + let v: crate::na::SVector = self.0.local_axis2().into(); + Vec3(v) + } + /// Set the body-local translation axis on body 2. + #[setter] + fn set_local_axis2(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.0.set_local_axis2(g); + } + + /// Return the linear motor, if configured. + fn motor(&self) -> Option { + self.0.motor().map(JointMotor::from_rapier) + } + /// Return the linear limits, if configured. + fn limits(&self) -> Option { + self.0.limits().map(JointLimits::from_rapier) + } + /// Apply linear ``[min, max]`` translation limits (world units). + fn set_limits(&mut self, min: Real, max: Real) { + self.0.set_limits([min, max]); + } + /// Configure a velocity-target motor with damping. + /// + /// :param target_vel: Desired velocity (world units per second). + /// :param factor: Damping coefficient. + fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) { + self.0.set_motor_velocity(target_vel, factor); + } + /// Configure a spring-damper motor toward ``target_pos`` + /// (world units). + fn set_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { + self.0.set_motor_position(target_pos, stiffness, damping); + } + /// Fully configure the motor with position and velocity setpoints. + fn set_motor(&mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) { + self.0.set_motor(target_pos, target_vel, stiffness, damping); + } + /// Clamp the maximum force the motor can apply. + fn set_motor_max_force(&mut self, max_force: Real) { + self.0.set_motor_max_force(max_force); + } + /// Select the motor model (see :class:`MotorModel`). + fn set_motor_model(&mut self, model: MotorModel) { + self.0.set_motor_model(model.to_rapier()); + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } + /// Spring coefficients controlling this joint's softness + /// (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.softness()) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + let _ = self.0.set_softness(v.0); + } +} + +/// Fluent builder for :class:`PrismaticJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`PrismaticJoint`. +#[pyclass(name = "PrismaticJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct PrismaticJointBuilder(pub rapier::dynamics::PrismaticJointBuilder); + +impl PrismaticJointBuilder { + fn from_kwargs( + base: rapier::dynamics::PrismaticJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "local_axis1" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + me.0 = me.0.local_axis1(g); + } + "local_axis2" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + me.0 = me.0.local_axis2(g); + } + "limits" => { + let (min, max): (Real, Real) = v.extract()?; + me.0 = me.0.limits([min, max]); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown PrismaticJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl PrismaticJointBuilder { + /// Construct a new prismatic joint builder with the given + /// sliding axis. + #[new] + fn new(axis: PyVector) -> Self { + let g: rapier::math::Vector = axis.0.into(); + Self(rapier::dynamics::PrismaticJointBuilder::new(g)) + } + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Set the body-local translation axis on body 1. + fn local_axis1(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self(self.0.local_axis1(g)) + } + /// Set the body-local translation axis on body 2. + fn local_axis2(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self(self.0.local_axis2(g)) + } + /// Apply linear ``[min, max]`` translation limits (world units). + fn limits(&self, min: Real, max: Real) -> Self { + Self(self.0.limits([min, max])) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Configure a velocity-target motor with damping. + fn motor_velocity(&self, target_vel: Real, factor: Real) -> Self { + Self(self.0.motor_velocity(target_vel, factor)) + } + /// Configure a spring-damper motor toward ``target_pos`` + /// (world units). + fn motor_position(&self, target_pos: Real, stiffness: Real, damping: Real) -> Self { + Self(self.0.motor_position(target_pos, stiffness, damping)) + } + /// Clamp the maximum force the motor can apply. + fn motor_max_force(&self, max_force: Real) -> Self { + Self(self.0.motor_max_force(max_force)) + } + /// Select the motor model (see :class:`MotorModel`). + fn motor_model(&self, model: MotorModel) -> Self { + Self(self.0.motor_model(model.to_rapier())) + } + /// Set the spring softness coefficients (see + /// :class:`SpringCoefficients`). Returns a new builder. + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`PrismaticJoint`. + fn build(&self) -> PrismaticJoint { + PrismaticJoint(self.0.build()) + } +} + +// ================================================================= +// RopeJoint + RopeJointBuilder +// ================================================================= +/// Distance constraint: anchor points stay within ``max_distance``. +/// +/// The two anchor points may move freely as long as the world +/// distance between them stays at or below ``max_distance``. +/// Conceptually models a slack rope of that length: there is no +/// force when the bodies are close, and a hard pull when the rope +/// becomes taut. +#[pyclass(name = "RopeJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RopeJoint(pub rapier::dynamics::RopeJoint); + +#[pymethods] +impl RopeJoint { + /// Construct a rope joint with the given maximum length. + /// + /// :param max_distance: Maximum allowed distance between + /// the anchor points, in world units. + #[new] + #[pyo3(signature = (max_distance=1.0 as Real))] + fn new(max_distance: Real) -> Self { + Self(rapier::dynamics::RopeJoint::new(max_distance)) + } + /// Build a :class:`RopeJointBuilder`. + /// + /// :param max_distance: Maximum rope length (world units). + /// :param kwargs: Optional keyword args; accepted keys are + /// ``local_anchor1``, ``local_anchor2``, ``max_distance``, + /// ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (max_distance, **kwargs))] + fn builder( + max_distance: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + RopeJointBuilder::from_kwargs( + rapier::dynamics::RopeJointBuilder::new(max_distance), + kwargs, + ) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.contacts_enabled() + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.set_contacts_enabled(v); + } + + /// Body-local point on body 1 where the rope is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the rope is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Maximum distance the rope can stretch (world units). + #[getter] + fn max_distance(&self) -> Real { + self.0.max_distance() + } + /// Set the maximum rope length. + #[setter] + fn set_max_distance(&mut self, v: Real) { + self.0.set_max_distance(v); + } + + /// Minimum distance enforced between the anchors. + /// + /// Exposed for API parity with :py:attr:`max_distance`; under + /// the hood it is stored as the lower limit of the joint's + /// ``LIN_X`` axis and is always ``0`` on a rope joint. + // Minimum length is not exposed as a field on RopeJoint; the + // engine encodes it as the lower limit of LinX which is always + // 0.0 here. Surface it as a read-only property for API parity. + #[getter] + fn min_distance(&self) -> Real { + self.0 + .data + .limits(rapier::dynamics::JointAxis::LinX) + .map(|l| l.min) + .unwrap_or(0.0) + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } + /// Spring coefficients controlling this joint's softness + /// (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.softness()) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + let _ = self.0.set_softness(v.0); + } +} + +/// Fluent builder for :class:`RopeJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`RopeJoint`. +#[pyclass(name = "RopeJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct RopeJointBuilder(pub rapier::dynamics::RopeJointBuilder); + +impl RopeJointBuilder { + fn from_kwargs( + base: rapier::dynamics::RopeJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "max_distance" => { + let d: Real = v.extract()?; + me.0 = me.0.max_distance(d); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown RopeJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl RopeJointBuilder { + /// Construct a new rope joint builder. + /// + /// :param max_distance: Maximum rope length (world units). + #[new] + #[pyo3(signature = (max_distance=1.0 as Real))] + fn new(max_distance: Real) -> Self { + Self(rapier::dynamics::RopeJointBuilder::new(max_distance)) + } + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Override the maximum rope length. + fn max_distance(&self, d: Real) -> Self { + Self(self.0.max_distance(d)) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Set the spring softness coefficients (see + /// :class:`SpringCoefficients`). Returns a new builder. + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`RopeJoint`. + fn build(&self) -> RopeJoint { + RopeJoint(self.0.build()) + } +} + +// ================================================================= +// SpringJoint + SpringJointBuilder +// ================================================================= +/// Distance spring-damper between two anchor points. +/// +/// Applies a force ``F = stiffness * (rest_length - dist) - +/// damping * d_dist/dt`` along the line connecting the two +/// anchor points; no DOF is locked, the constraint is purely a +/// soft restoring force. Useful for suspensions, soft links, and +/// other compliant connections. +#[pyclass(name = "SpringJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct SpringJoint(pub rapier::dynamics::SpringJoint); + +#[pymethods] +impl SpringJoint { + /// Construct a spring joint. + /// + /// :param rest_length: Distance at which the spring exerts no + /// force (world units). + /// :param stiffness: Spring constant. + /// :param damping: Velocity damping coefficient. + #[new] + #[pyo3(signature = (rest_length=0.0 as Real, stiffness=1.0 as Real, damping=0.0 as Real))] + fn new(rest_length: Real, stiffness: Real, damping: Real) -> Self { + Self(rapier::dynamics::SpringJoint::new( + rest_length, + stiffness, + damping, + )) + } + /// Build a :class:`SpringJointBuilder`. + /// + /// :param rest_length: Distance at which the spring exerts no + /// force (world units). + /// :param stiffness: Spring constant. + /// :param damping: Velocity damping coefficient. + /// :param kwargs: Optional keyword args; accepted keys include + /// ``local_anchor1``, ``local_anchor2``, + /// ``contacts_enabled``, ``spring_model``. + #[staticmethod] + #[pyo3(signature = (rest_length, stiffness, damping, **kwargs))] + fn builder( + rest_length: Real, + stiffness: Real, + damping: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + SpringJointBuilder::from_kwargs( + rapier::dynamics::SpringJointBuilder::new(rest_length, stiffness, damping), + kwargs, + ) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.contacts_enabled() + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.set_contacts_enabled(v); + } + + /// Body-local point on body 1 where the spring is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the spring is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Select how the spring force is applied (see :class:`MotorModel`). + fn set_spring_model(&mut self, model: MotorModel) { + self.0.set_spring_model(model.to_rapier()); + } + + /// Rest length of the spring (world units). + /// + /// Read back from the underlying ``LIN_X`` motor's + /// ``target_pos``; returns ``0`` if no motor is set. + // Read-only access to spring parameters via the underlying + // generic joint's motor on LinX. + #[getter] + fn rest_length(&self) -> Real { + self.0 + .data + .motor(rapier::dynamics::JointAxis::LinX) + .map(|m| m.target_pos) + .unwrap_or(0.0) + } + /// Spring stiffness (read-back from the underlying motor). + #[getter] + fn spring_stiffness(&self) -> Real { + self.0 + .data + .motor(rapier::dynamics::JointAxis::LinX) + .map(|m| m.stiffness) + .unwrap_or(0.0) + } + /// Spring damping coefficient (read-back from the underlying motor). + #[getter] + fn spring_damping(&self) -> Real { + self.0 + .data + .motor(rapier::dynamics::JointAxis::LinX) + .map(|m| m.damping) + .unwrap_or(0.0) + } + /// Motor model controlling how the spring force is applied + /// (see :class:`MotorModel`). + #[getter] + fn model(&self) -> MotorModel { + self.0 + .data + .motor(rapier::dynamics::JointAxis::LinX) + .map(|m| MotorModel::from_rapier(m.model)) + .unwrap_or(MotorModel::FORCE_BASED) + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } +} + +/// Fluent builder for :class:`SpringJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`SpringJoint`. +#[pyclass(name = "SpringJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct SpringJointBuilder(pub rapier::dynamics::SpringJointBuilder); + +impl SpringJointBuilder { + fn from_kwargs( + base: rapier::dynamics::SpringJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + "spring_model" => { + let m: MotorModel = v.extract()?; + me.0 = me.0.spring_model(m.to_rapier()); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown SpringJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl SpringJointBuilder { + /// Construct a new spring joint builder. + /// + /// :param rest_length: Distance at which the spring exerts no + /// force (world units). + /// :param stiffness: Spring constant. + /// :param damping: Velocity damping coefficient. + #[new] + #[pyo3(signature = (rest_length=0.0 as Real, stiffness=1.0 as Real, damping=0.0 as Real))] + fn new(rest_length: Real, stiffness: Real, damping: Real) -> Self { + Self(rapier::dynamics::SpringJointBuilder::new( + rest_length, + stiffness, + damping, + )) + } + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Select how the spring force is applied (see :class:`MotorModel`). + fn spring_model(&self, m: MotorModel) -> Self { + Self(self.0.spring_model(m.to_rapier())) + } + /// Materialize the builder into a :class:`SpringJoint`. + fn build(&self) -> SpringJoint { + SpringJoint(self.0.build()) + } +} + +// ================================================================= +// GenericJoint + GenericJointBuilder +// ================================================================= +/// Fully parametric joint, the building block of every concrete +/// joint type. +/// +/// A generic joint is described by: +/// +/// * Two body-local frames (``local_frame1``/``local_frame2``). +/// * A ``locked_axes`` mask listing the DOFs that are removed by +/// the constraint. By default no axis is locked (the joint +/// imposes nothing). +/// * A ``limit_axes`` / per-axis :class:`JointLimits` pair to bound +/// the motion on selected axes. +/// * A ``motor_axes`` / per-axis :class:`JointMotor` pair to drive +/// selected axes with a PD controller. +/// * A ``coupled_axes`` mask listing axes that share the same +/// limit / motor (used e.g. by :class:`RopeJoint` to couple all +/// linear axes into a single distance constraint). +/// +/// The concrete joint types (Fixed/Revolute/Prismatic/etc.) are +/// thin wrappers that pre-configure these masks for a given +/// mechanism. +#[pyclass(name = "GenericJoint", module = "rapier")] +#[derive(Debug)] +pub struct GenericJoint { + pub backing: GenericJointBacking, +} + +/// Storage backing a `GenericJoint`: a standalone owned value, or a +/// live view into the `data` of a joint stored in an `ImpulseJointSet` +/// (so `impulse_joint.data.set_limits(..)` persists in place). +#[derive(Debug)] +pub enum GenericJointBacking { + Owned(Box), + ImpulseJointData { + set: Py, + handle: rapier::dynamics::ImpulseJointHandle, + }, +} + +impl Clone for GenericJointBacking { + fn clone(&self) -> Self { + match self { + GenericJointBacking::Owned(g) => GenericJointBacking::Owned(g.clone()), + GenericJointBacking::ImpulseJointData { set, handle } => { + Python::with_gil(|py| GenericJointBacking::ImpulseJointData { + set: set.clone_ref(py), + handle: *handle, + }) + } + } + } +} +impl Clone for GenericJoint { + fn clone(&self) -> Self { + GenericJoint { + backing: self.backing.clone(), + } + } +} + +impl GenericJoint { + pub(crate) fn new_owned(joint: rapier::dynamics::GenericJoint) -> Self { + GenericJoint { + backing: GenericJointBacking::Owned(Box::new(joint)), + } + } + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::GenericJoint) -> R) -> R { + match &self.backing { + GenericJointBacking::Owned(g) => f(g), + GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { + let set = set.bind(py).borrow(); + let joint = set + .0 + .get(*handle) + .expect("GenericJoint refers to a joint that was removed from its set"); + f(&joint.data) + }), + } + } + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::GenericJoint) -> R) -> R { + match &mut self.backing { + GenericJointBacking::Owned(g) => f(g), + GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { + let mut set = set.bind(py).borrow_mut(); + let joint = set + .0 + .get_mut(*handle, true) + .expect("GenericJoint refers to a joint that was removed from its set"); + f(&mut joint.data) + }), + } + } + /// Copy the underlying joint out (it is `Copy` upstream). + pub fn to_owned_generic(&self) -> rapier::dynamics::GenericJoint { + self.with_ref(|g| *g) + } +} + +#[pymethods] +impl GenericJoint { + /// Construct a generic joint with the given locked axes. + /// + /// :param locked_axes: Axes to lock; defaults to no axes + /// (a fully free joint). + #[new] + #[pyo3(signature = (locked_axes=None))] + fn new(locked_axes: Option) -> Self { + let mask = locked_axes + .map(|m| m.0) + .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); + GenericJoint::new_owned(rapier::dynamics::GenericJoint::new(mask)) + } + /// Build a :class:`GenericJointBuilder`. + /// + /// :param locked_axes: Axes to lock. + /// :param kwargs: Optional keyword args; accepted keys include + /// ``local_anchor1``, ``local_anchor2``, ``local_axis1``, + /// ``local_axis2``, ``local_frame1``, ``local_frame2``, + /// ``locked_axes``, ``coupled_axes``, ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (locked_axes=None, **kwargs))] + fn builder( + locked_axes: Option, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mask = locked_axes + .map(|m| m.0) + .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); + GenericJointBuilder::from_kwargs(rapier::dynamics::GenericJointBuilder::new(mask), kwargs) + } + + /// Body-local frame (position + orientation) on body 1. + #[getter] + fn local_frame1(&self) -> Isometry3 { + let pose: crate::na::Isometry = self.with_ref(|g| g.local_frame1).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 1. + #[setter] + fn set_local_frame1(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.with_mut(|g| { + g.set_local_frame1(p); + }); + } + /// Body-local frame on body 2. + #[getter] + fn local_frame2(&self) -> Isometry3 { + let pose: crate::na::Isometry = self.with_ref(|g| g.local_frame2).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 2. + #[setter] + fn set_local_frame2(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.with_mut(|g| { + g.set_local_frame2(p); + }); + } + /// Body-local point on body 1 (origin of ``local_frame1``). + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::SVector = self.with_ref(|g| g.local_anchor1()).into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.with_mut(|gj| { + gj.set_local_anchor1(g); + }); + } + /// Body-local point on body 2 (origin of ``local_frame2``). + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::SVector = self.with_ref(|g| g.local_anchor2()).into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.with_mut(|gj| { + gj.set_local_anchor2(g); + }); + } + /// Reference axis on body 1 (X axis of ``local_frame1``). + #[getter] + fn local_axis1(&self) -> Vec3 { + let v: crate::na::SVector = self.with_ref(|g| g.local_axis1()).into(); + Vec3(v) + } + /// Set the reference axis on body 1. + #[setter] + fn set_local_axis1(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.with_mut(|gj| { + gj.set_local_axis1(g); + }); + } + /// Reference axis on body 2 (X axis of ``local_frame2``). + #[getter] + fn local_axis2(&self) -> Vec3 { + let v: crate::na::SVector = self.with_ref(|g| g.local_axis2()).into(); + Vec3(v) + } + /// Set the reference axis on body 2. + #[setter] + fn set_local_axis2(&mut self, v: PyVector) { + let g: rapier::math::Vector = v.0.into(); + self.with_mut(|gj| { + gj.set_local_axis2(g); + }); + } + + /// Mask of axes that are rigidly locked by this joint. + #[getter] + fn locked_axes(&self) -> JointAxesMask { + JointAxesMask(self.with_ref(|g| g.locked_axes)) + } + /// Replace the set of locked axes. + #[setter] + fn set_locked_axes(&mut self, v: JointAxesMask) { + self.with_mut(|g| g.locked_axes = v.0); + } + + /// Mask of axes that carry a :class:`JointLimits` configuration. + #[getter] + fn limit_axes(&self) -> JointAxesMask { + JointAxesMask(self.with_ref(|g| g.limit_axes)) + } + /// Mask of axes that carry a :class:`JointMotor` configuration. + #[getter] + fn motor_axes(&self) -> JointAxesMask { + JointAxesMask(self.with_ref(|g| g.motor_axes)) + } + /// Mask of axes that share their limits / motor (e.g. coupled + /// linear axes giving a spherical distance constraint). + #[getter] + fn coupled_axes(&self) -> JointAxesMask { + JointAxesMask(self.with_ref(|g| g.coupled_axes)) + } + /// Replace the set of coupled axes. + #[setter] + fn set_coupled_axes(&mut self, v: JointAxesMask) { + self.with_mut(|g| g.coupled_axes = v.0); + } + /// Spring coefficients controlling the softness of this joint's + /// constraints (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.with_ref(|g| g.softness)) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + self.with_mut(|g| g.softness = v.0); + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.with_ref(|g| g.contacts_enabled) + } + /// Enable or disable contacts between the attached bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.with_mut(|g| { + g.set_contacts_enabled(v); + }); + } + + /// Current :class:`JointEnabled` state. + #[getter] + fn enabled(&self) -> JointEnabled { + JointEnabled::from_rapier(self.with_ref(|g| g.enabled)) + } + /// Explicitly enable or disable the joint. + /// + /// Maps to :py:attr:`JointEnabled.ENABLED` / + /// :py:attr:`JointEnabled.DISABLED`. + fn set_enabled(&mut self, b: bool) { + self.with_mut(|g| { + g.set_enabled(b); + }); + } + /// Return ``True`` iff ``enabled == ENABLED``. + fn is_enabled(&self) -> bool { + self.with_ref(|g| g.is_enabled()) + } + + /// Opaque user-data integer carried by the joint. + #[getter] + fn user_data(&self) -> u128 { + self.with_ref(|g| g.user_data) + } + /// Set the user-data integer. + #[setter] + fn set_user_data(&mut self, v: u128) { + self.with_mut(|g| g.user_data = v); + } + + /// Return the limits configured on ``axis``, if any. + fn limits(&self, axis: JointAxis) -> Option { + self.with_ref(|g| g.limits(axis.to_rapier()).map(JointLimits::from_rapier)) + } + /// Configure ``[min, max]`` limits on ``axis``. + /// + /// Units are radians for angular axes and world units for + /// linear axes. + fn set_limits(&mut self, axis: JointAxis, min: Real, max: Real) { + self.with_mut(|g| { + g.set_limits(axis.to_rapier(), [min, max]); + }); + } + /// Return the motor configured on ``axis``, if any. + fn motor(&self, axis: JointAxis) -> Option { + self.with_ref(|g| g.motor(axis.to_rapier()).map(JointMotor::from_rapier)) + } + /// Fully configure the motor on ``axis``. + fn set_motor( + &mut self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) { + self.with_mut(|g| { + g.set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping); + }); + } + /// Configure ``axis`` as a velocity-target motor with damping. + fn set_motor_velocity(&mut self, axis: JointAxis, target_vel: Real, factor: Real) { + self.with_mut(|g| { + g.set_motor_velocity(axis.to_rapier(), target_vel, factor); + }); + } + /// Configure ``axis`` as a spring-damper motor toward + /// ``target_pos``. + fn set_motor_position( + &mut self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) { + self.with_mut(|g| { + g.set_motor_position(axis.to_rapier(), target_pos, stiffness, damping); + }); + } + /// Clamp the maximum force the motor on ``axis`` can apply. + fn set_motor_max_force(&mut self, axis: JointAxis, max_force: Real) { + self.with_mut(|g| { + g.set_motor_max_force(axis.to_rapier(), max_force); + }); + } + /// Select the motor model on ``axis`` (see :class:`MotorModel`). + fn set_motor_model(&mut self, axis: JointAxis, model: MotorModel) { + self.with_mut(|g| { + g.set_motor_model(axis.to_rapier(), model.to_rapier()); + }); + } + /// Return the motor model on ``axis``, if a motor is configured. + fn motor_model(&self, axis: JointAxis) -> Option { + self.with_ref(|g| g.motor_model(axis.to_rapier()).map(MotorModel::from_rapier)) + } + /// Add ``axes`` to the set of locked axes. + fn lock_axes(&mut self, axes: JointAxesMask) { + self.with_mut(|g| { + g.lock_axes(axes.0); + }); + } + + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + format!( + "GenericJoint(locked_axes={:#010b}, enabled={:?})", + self.with_ref(|g| g.locked_axes).bits(), + self.with_ref(|g| g.enabled) + ) + } +} + +/// Fluent builder for :class:`GenericJoint`. +/// +/// Modifier methods return a new builder; call :py:meth:`build` to +/// produce a :class:`GenericJoint`. The same builder can be +/// applied to many bodies to create equivalent joints. +#[pyclass(name = "GenericJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct GenericJointBuilder(pub rapier::dynamics::GenericJointBuilder); + +impl GenericJointBuilder { + fn from_kwargs( + base: rapier::dynamics::GenericJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "local_axis1" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + me.0 = me.0.local_axis1(g); + } + "local_axis2" => { + let pv: PyVector = v.extract()?; + let g: rapier::math::Vector = pv.0.into(); + me.0 = me.0.local_axis2(g); + } + "local_frame1" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame1(p); + } + "local_frame2" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame2(p); + } + "locked_axes" => { + let m: JointAxesMask = v.extract()?; + me.0 = me.0.locked_axes(m.0); + } + "coupled_axes" => { + let m: JointAxesMask = v.extract()?; + me.0 = me.0.coupled_axes(m.0); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown GenericJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl GenericJointBuilder { + /// Construct a new generic joint builder. + /// + /// :param locked_axes: Axes to lock; defaults to no axes. + #[new] + #[pyo3(signature = (locked_axes=None))] + fn new(locked_axes: Option) -> Self { + let mask = locked_axes + .map(|m| m.0) + .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); + Self(rapier::dynamics::GenericJointBuilder::new(mask)) + } + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Set the reference axis on body 1. + fn local_axis1(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self(self.0.local_axis1(g)) + } + /// Set the reference axis on body 2. + fn local_axis2(&self, v: PyVector) -> Self { + let g: rapier::math::Vector = v.0.into(); + Self(self.0.local_axis2(g)) + } + /// Set the body-local frame on body 1. + fn local_frame1(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame1(p)) + } + /// Set the body-local frame on body 2. + fn local_frame2(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame2(p)) + } + /// Override the set of locked axes. + fn locked_axes(&self, m: JointAxesMask) -> Self { + Self(self.0.locked_axes(m.0)) + } + /// Override the set of coupled axes. + fn coupled_axes(&self, m: JointAxesMask) -> Self { + Self(self.0.coupled_axes(m.0)) + } + /// Enable or disable contacts between the attached bodies. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Apply ``[min, max]`` limits on ``axis`` (radians for angular, + /// world units for linear axes). + fn limits(&self, axis: JointAxis, min: Real, max: Real) -> Self { + Self(self.0.limits(axis.to_rapier(), [min, max])) + } + /// Fully configure the motor on ``axis``. + fn motor( + &self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> Self { + Self( + self.0 + .set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping), + ) + } + /// Configure ``axis`` as a velocity-target motor with damping. + fn motor_velocity(&self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { + Self(self.0.motor_velocity(axis.to_rapier(), target_vel, factor)) + } + /// Configure ``axis`` as a spring-damper motor toward + /// ``target_pos``. + fn motor_position( + &self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) -> Self { + Self( + self.0 + .motor_position(axis.to_rapier(), target_pos, stiffness, damping), + ) + } + /// Clamp the maximum force the motor on ``axis`` can apply. + fn motor_max_force(&self, axis: JointAxis, max_force: Real) -> Self { + Self(self.0.motor_max_force(axis.to_rapier(), max_force)) + } + /// Select the motor model on ``axis`` (see :class:`MotorModel`). + fn motor_model(&self, axis: JointAxis, model: MotorModel) -> Self { + Self(self.0.motor_model(axis.to_rapier(), model.to_rapier())) + } + /// Set the user-data integer carried by the joint. + fn user_data(&self, d: u128) -> Self { + Self(self.0.user_data(d)) + } + /// Set the spring softness coefficients for this joint's + /// constraints (see :class:`SpringCoefficients`). + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`GenericJoint`. + fn build(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.build()) + } +} + +// ================================================================= +// ImpulseJoint (view) +// ================================================================= +/// Read-only view of a single joint stored in an +/// :class:`ImpulseJointSet`. +/// +/// Combines the two attached rigid-body handles with the +/// underlying :class:`GenericJoint` data and the last impulses +/// applied by the solver. +#[pyclass(name = "ImpulseJoint", module = "rapier")] +#[derive(Debug)] +pub struct ImpulseJoint { + pub backing: ImpulseJointBacking, +} + +/// Storage backing an `ImpulseJoint`: a standalone owned joint (e.g. +/// from `remove`) or a handle-backed view into an `ImpulseJointSet`. +#[derive(Debug)] +pub enum ImpulseJointBacking { + Owned(Box), + InSet { + set: Py, + handle: rapier::dynamics::ImpulseJointHandle, + }, +} + +impl Clone for ImpulseJointBacking { + fn clone(&self) -> Self { + match self { + ImpulseJointBacking::Owned(j) => ImpulseJointBacking::Owned(j.clone()), + ImpulseJointBacking::InSet { set, handle } => { + Python::with_gil(|py| ImpulseJointBacking::InSet { + set: set.clone_ref(py), + handle: *handle, + }) + } + } + } +} +impl Clone for ImpulseJoint { + fn clone(&self) -> Self { + ImpulseJoint { + backing: self.backing.clone(), + } + } +} + +impl ImpulseJoint { + pub(crate) fn new_owned(joint: rapier::dynamics::ImpulseJoint) -> Self { + ImpulseJoint { + backing: ImpulseJointBacking::Owned(Box::new(joint)), + } + } + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::ImpulseJoint) -> R) -> R { + match &self.backing { + ImpulseJointBacking::Owned(j) => f(j), + ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { + let set = set.bind(py).borrow(); + let j = set + .0 + .get(*handle) + .expect("ImpulseJoint refers to a joint that was removed from its set"); + f(j) + }), + } + } + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::ImpulseJoint) -> R) -> R { + match &mut self.backing { + ImpulseJointBacking::Owned(j) => f(j), + ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { + let mut set = set.bind(py).borrow_mut(); + let j = set + .0 + .get_mut(*handle, true) + .expect("ImpulseJoint refers to a joint that was removed from its set"); + f(j) + }), + } + } +} + +#[pymethods] +impl ImpulseJoint { + /// Handle of the first attached body. + #[getter] + fn body1(&self) -> RigidBodyHandle { + self.with_ref(|j| RigidBodyHandle(j.body1())) + } + /// Handle of the second attached body. + #[getter] + fn body2(&self) -> RigidBodyHandle { + self.with_ref(|j| RigidBodyHandle(j.body2())) + } + /// Underlying :class:`GenericJoint` description (read+write). + /// + /// For a joint stored in a set this is a live **view**: mutating it + /// in place persists, e.g. + /// ``joint.data.set_limits(JointAxis.LIN_X, -1.0, 1.0)``. Assigning + /// a whole :class:`GenericJoint` (``joint.data = gj``) also works. + #[getter] + fn data(&self) -> GenericJoint { + match &self.backing { + ImpulseJointBacking::InSet { set, handle } => GenericJoint { + backing: GenericJointBacking::ImpulseJointData { + set: Python::with_gil(|py| set.clone_ref(py)), + handle: *handle, + }, + }, + ImpulseJointBacking::Owned(j) => GenericJoint::new_owned(j.data), + } + } + /// Replace the joint description (persists for an in-set joint). + #[setter] + fn set_data(&mut self, data: GenericJoint) { + let g = data.to_owned_generic(); + self.with_mut(|j| j.data = g); + } + /// Per-axis impulses applied by the solver on the last step. + /// + /// :returns: Flat list of length 3 in 2D (lin_x, lin_y, ang_x) + /// and 6 in 3D (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z). + #[getter] + fn impulses(&self) -> Vec { + self.with_ref(|j| { + let v = j.impulses; + v.to_vec() + }) + } + /// Return a developer-readable representation. + fn __repr__(&self) -> String { + self.with_ref(|j| { + let (i1, g1) = j.body1().0.into_raw_parts(); + let (i2, g2) = j.body2().0.into_raw_parts(); + format!( + "ImpulseJoint(body1=RigidBodyHandle({}, {}), body2=RigidBodyHandle({}, {}))", + i1, g1, i2, g2 + ) + }) + } +} + +// ================================================================= +// ImpulseJointSet +// ================================================================= +/// Container of impulse-based joints. +/// +/// Joints stored here are enforced by the iterative +/// velocity-impulse constraint solver alongside contacts. This is +/// the right home for general joint constraints between arbitrary +/// pairs of rigid bodies that do not form a tree-structured +/// articulation; use :class:`MultibodyJointSet` for the latter. +/// +/// Supports ``len()``, ``in``, ``set[handle]``, and iteration +/// (yielding ``(ImpulseJointHandle, ImpulseJoint)`` pairs). +#[pyclass(name = "ImpulseJointSet", module = "rapier", unsendable)] +pub struct ImpulseJointSet(pub rapier::dynamics::ImpulseJointSet); + +#[pymethods] +impl ImpulseJointSet { + /// Construct an empty :class:`ImpulseJointSet`. + #[new] + fn new() -> Self { + Self(rapier::dynamics::ImpulseJointSet::new()) + } + + /// Insert a new joint between ``body1`` and ``body2``. + /// + /// :param body1: Handle of the first attached body. + /// :param body2: Handle of the second attached body. + /// :param joint: A concrete joint or builder; anything that + /// can be converted to a :class:`GenericJoint` is accepted. + /// :param wake_up: If ``True``, wake the attached bodies so + /// they participate in the next solver step. + /// :returns: A fresh :class:`ImpulseJointHandle`. + #[pyo3(signature = (body1, body2, joint, wake_up=true))] + fn insert( + &mut self, + body1: &RigidBodyHandle, + body2: &RigidBodyHandle, + joint: &Bound<'_, PyAny>, + wake_up: bool, + ) -> PyResult { + let data = { + // Accept any of the concrete joint wrappers OR a GenericJoint OR a + // builder. Return Result. + let obj = joint; + let result: crate::pyo3::PyResult = (|| { + if let Ok(j) = obj.extract::>() { + return Ok(j.to_owned_generic()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + { + let obj = obj; + { + let obj = obj; + // `try_extract_dim_specific_joint(obj) -> PyResult>` + // handles the dim-specific joint (SphericalJoint in 3D). + match try_extract_dim_specific_joint(obj)? { + Some(gj) => Ok(gj), + None => Err(crate::pyo3::exceptions::PyTypeError::new_err( + "expected a joint (concrete or generic) or a joint builder", + )), + } + } + } + })(); + result + }?; + Ok(ImpulseJointHandle( + self.0.insert(body1.0, body2.0, data, wake_up), + )) + } + + /// Rewire an existing joint to a new pair of bodies. + /// + /// The joint keeps its handle, configuration, and warm-started + /// impulses; only its attached bodies change. Use this to e.g. + /// transfer a grasp from one body to another without rebuilding + /// the constraint. + /// + /// :param handle: Handle returned by :py:meth:`insert`. + /// :param body1: Handle of the new first body. + /// :param body2: Handle of the new second body. + /// :param wake_up: If ``True``, wake the old and new bodies. + /// :returns: ``True`` if the joint existed and was rewired, + /// ``False`` if ``handle`` was stale. + #[pyo3(signature = (handle, body1, body2, wake_up=true))] + fn set_bodies( + &mut self, + handle: &ImpulseJointHandle, + body1: &RigidBodyHandle, + body2: &RigidBodyHandle, + wake_up: bool, + ) -> bool { + self.0 + .set_bodies(handle.0, body1.0, body2.0, wake_up) + .is_some() + } + + /// Remove the joint identified by ``handle``. + /// + /// :param handle: Handle returned by :py:meth:`insert`. + /// :param wake_up: If ``True``, wake the formerly attached + /// bodies. + /// :returns: The removed joint, or ``None`` if the handle was + /// stale. + #[pyo3(signature = (handle, wake_up=true))] + fn remove(&mut self, handle: &ImpulseJointHandle, wake_up: bool) -> Option { + self.0 + .remove(handle.0, wake_up) + .map(ImpulseJoint::new_owned) + } + + /// Return a live **view** of the joint pointed at by ``handle``, + /// if any. Assigning ``joint.data = gj`` persists in place. + fn get(slf: &Bound<'_, Self>, handle: &ImpulseJointHandle) -> Option { + slf.borrow().0.get(handle.0)?; + Some(ImpulseJoint { + backing: ImpulseJointBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + /// Indexed access (``self[handle]``) — returns a live view. + /// + /// :raises InvalidHandle: If ``handle`` does not point to a + /// joint in this set. + fn __getitem__(slf: &Bound<'_, Self>, handle: &ImpulseJointHandle) -> PyResult { + if slf.borrow().0.get(handle.0).is_none() { + return Err(crate::errors::InvalidHandle::new_err(format!( + "no impulse joint for {:?}", + handle.0.into_raw_parts() + ))); + } + Ok(ImpulseJoint { + backing: ImpulseJointBacking::InSet { + set: slf.clone().unbind(), + handle: handle.0, + }, + }) + } + + /// Return ``True`` iff ``handle`` resolves to a joint in this set. + fn __contains__(&self, handle: &ImpulseJointHandle) -> bool { + self.0.contains(handle.0) + } + + /// Number of joints currently in the set. + fn __len__(&self) -> usize { + self.0.len() + } + + /// Remove every joint from the set. + fn clear(&mut self) { + self.0 = rapier::dynamics::ImpulseJointSet::new(); + } + + /// Iterate over ``(handle, joint)`` pairs. + fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { + let handles: Vec = + slf.borrow().0.iter().map(|(h, _)| h).collect(); + Py::new( + slf.py(), + ImpulseJointSetIter { + set: slf.clone().unbind(), + handles, + i: 0, + }, + ) + } + + /// Iterate over the handles of every joint attached to ``body``. + fn attached_joints( + slf: PyRef<'_, Self>, + body: &RigidBodyHandle, + ) -> PyResult> { + let handles: Vec = slf + .0 + .attached_joints(body.0) + .map(|(_, _, h, _)| ImpulseJointHandle(h)) + .collect(); + Py::new(slf.py(), ImpulseJointHandleIter { handles, i: 0 }) + } + + /// Return the ``(body1, body2)`` pair the joint connects. + /// + /// :raises InvalidHandle: If ``handle`` is stale. + fn bodies_connected_by_joint( + &self, + handle: &ImpulseJointHandle, + ) -> PyResult<(RigidBodyHandle, RigidBodyHandle)> { + let j = self.0.get(handle.0).ok_or_else(|| { + crate::errors::InvalidHandle::new_err(format!( + "no impulse joint for {:?}", + handle.0.into_raw_parts() + )) + })?; + Ok((RigidBodyHandle(j.body1()), RigidBodyHandle(j.body2()))) + } +} + +/// Iterator yielding ``(ImpulseJointHandle, ImpulseJoint)`` pairs +/// for every joint in an :class:`ImpulseJointSet`. +#[pyclass] +pub struct ImpulseJointSetIter { + set: Py, + handles: Vec, + i: usize, +} + +#[pymethods] +impl ImpulseJointSetIter { + /// Return ``self`` (iterator protocol). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Advance and return the next ``(handle, joint)`` pair (lazy view). + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(ImpulseJointHandle, ImpulseJoint)> { + if slf.i >= slf.handles.len() { + return None; + } + let py = slf.py(); + let handle = slf.handles[slf.i]; + slf.i += 1; + let set = slf.set.clone_ref(py); + Some(( + ImpulseJointHandle(handle), + ImpulseJoint { + backing: ImpulseJointBacking::InSet { set, handle }, + }, + )) + } +} + +/// Iterator yielding handles of joints attached to a given body. +#[pyclass] +pub struct ImpulseJointHandleIter { + handles: Vec, + i: usize, +} + +#[pymethods] +impl ImpulseJointHandleIter { + /// Return ``self`` (iterator protocol). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Advance and return the next handle. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.handles.len() { + return None; + } + let h = slf.handles[slf.i]; + slf.i += 1; + Some(h) + } +} + +// ================================================================= +// MultibodyLink (view) +// ================================================================= +/// Read-only view of one link inside a multibody articulation. +/// +/// Each link wraps a rigid body and tracks its position in the +/// articulation tree (its ``link_id`` and its ``parent_id``, +/// which is ``None`` for the root). +#[pyclass(name = "MultibodyLink", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct MultibodyLink(pub rapier::dynamics::MultibodyLink); + +#[pymethods] +impl MultibodyLink { + /// Handle of the rigid body backing this link. + #[getter] + fn rigid_body(&self) -> RigidBodyHandle { + RigidBodyHandle(self.0.rigid_body_handle()) + } + /// Zero-based index of the link inside its articulation + /// (``0`` for the root). + #[getter] + fn link_id(&self) -> usize { + self.0.link_id() + } + /// Link index of the parent, or ``None`` if this is the root. + #[getter] + fn parent_id(&self) -> Option { + self.0.parent_id() + } + /// Return ``True`` iff this link is the root of its articulation. + fn is_root(&self) -> bool { + self.0.is_root() + } + /// Generalized coordinates of this link's joint. + /// + /// :returns: Flat list of length 3 in 2D (lin_x, lin_y, ang) and + /// 6 in 3D (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z). Only + /// the entries for the joint's unlocked degrees of freedom are + /// meaningful. + #[getter] + fn coords(&self) -> Vec { + { + let v = self.0.joint().coords(); + v.to_vec() + } + } + /// This link's joint angular coordinates as a rotation. + #[getter] + fn joint_rot(&self) -> Rotation3 { + Rotation3(self.0.joint().joint_rot().into()) + } +} + +// ================================================================= +// Multibody (view) +// ================================================================= +/// Reduced-coordinate articulation: a tree of links connected by +/// joints. +/// +/// A :class:`Multibody` represents an articulated system (robot, +/// character, ragdoll, ...) where each link is a rigid body and +/// the configuration is parameterized by the joint coordinates +/// (one per joint DOF, summing to :py:attr:`ndofs`). Unlike +/// impulse joints, the topology is restricted to a tree but the +/// constraints are exact and inertia is propagated analytically +/// along the kinematic chain. +#[pyclass(name = "Multibody", module = "rapier")] +#[derive(Debug)] +pub struct Multibody { + pub set: Py, + pub key: MultibodyKey, +} + +/// How a `Multibody` view locates its articulation within the +/// `MultibodyJointSet`: by stable index, or by one of its joint handles. +#[derive(Debug, Clone, Copy)] +pub enum MultibodyKey { + Index(rapier::dynamics::MultibodyIndex), + Joint(rapier::dynamics::MultibodyJointHandle), +} + +impl Clone for Multibody { + fn clone(&self) -> Self { + Python::with_gil(|py| Multibody { + set: self.set.clone_ref(py), + key: self.key, + }) + } +} + +impl Multibody { + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::Multibody) -> R) -> R { + Python::with_gil(|py| { + let set = self.set.bind(py).borrow(); + let mb = match self.key { + MultibodyKey::Index(i) => set.0.get_multibody(i), + MultibodyKey::Joint(h) => set.0.get(h).map(|(mb, _)| mb), + } + .expect("Multibody refers to an articulation no longer in its set"); + f(mb) + }) + } + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::Multibody) -> R) -> R { + Python::with_gil(|py| { + let mut set = self.set.bind(py).borrow_mut(); + let mb = match self.key { + MultibodyKey::Index(i) => set.0.get_multibody_mut(i), + MultibodyKey::Joint(h) => set.0.get_mut(h).map(|(mb, _)| mb), + } + .expect("Multibody refers to an articulation no longer in its set"); + f(mb) + }) + } +} + +#[pymethods] +impl Multibody { + /// Handle of the root link's rigid body. + #[getter] + fn root_handle(&self) -> RigidBodyHandle { + self.with_ref(|mb| RigidBodyHandle(mb.root().rigid_body_handle())) + } + /// Total number of links in the articulation. + #[getter] + fn num_links(&self) -> usize { + self.with_ref(|mb| mb.num_links()) + } + /// Total number of degrees of freedom across all joints. + #[getter] + fn ndofs(&self) -> usize { + self.with_ref(|mb| mb.ndofs()) + } + /// Return the link with index ``idx``, or ``None`` if out of + /// range. + fn get_link(&self, idx: usize) -> Option { + self.with_ref(|mb| mb.link(idx).copied().map(MultibodyLink)) + } + /// Iterate over the links in depth-first order from the root. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let links: Vec = + slf.with_ref(|mb| mb.links().copied().map(MultibodyLink).collect()); + Py::new(slf.py(), MultibodyLinkIter { links, i: 0 }) + } + /// Return ``True`` iff contacts between links of this + /// articulation are enabled. + fn self_contacts_enabled(&self) -> bool { + self.with_ref(|mb| mb.self_contacts_enabled()) + } + /// Enable or disable contacts between links of this + /// articulation. + fn set_self_contacts_enabled(&mut self, v: bool) { + self.with_mut(|mb| mb.set_self_contacts_enabled(v)); + } + + /// Per-degree-of-freedom joint damping coefficients + /// (length :attr:`ndofs`). + fn damping(&self) -> Vec { + self.with_ref(|mb| mb.damping().iter().copied().collect()) + } + /// Set the per-DOF joint damping coefficients. + /// + /// :raises ValueError: If ``values`` length differs from `ndofs`. + fn set_damping(&mut self, values: Vec) -> PyResult<()> { + self.with_mut(|mb| { + let d = mb.damping_mut(); + if values.len() != d.len() { + return Err(PyValueError::new_err(format!( + "expected {} damping values (ndofs), got {}", + d.len(), + values.len() + ))); + } + for (i, v) in values.iter().enumerate() { + d[i] = *v; + } + Ok(()) + }) + } + /// Generalized velocity vector (one entry per DOF). + fn generalized_velocity(&self) -> Vec { + self.with_ref(|mb| mb.generalized_velocity().iter().copied().collect()) + } + /// Set the generalized velocity vector. + /// + /// :raises ValueError: If ``values`` length differs from `ndofs`. + fn set_generalized_velocity(&mut self, values: Vec) -> PyResult<()> { + self.with_mut(|mb| { + let mut v = mb.generalized_velocity_mut(); + if values.len() != v.len() { + return Err(PyValueError::new_err(format!( + "expected {} velocity values (ndofs), got {}", + v.len(), + values.len() + ))); + } + for (i, x) in values.iter().enumerate() { + v[i] = *x; + } + Ok(()) + }) + } + /// Generalized acceleration vector from the last solver step. + fn generalized_acceleration(&self) -> Vec { + self.with_ref(|mb| mb.generalized_acceleration().iter().copied().collect()) + } + /// Velocity DOFs belonging to a single ``link``. + fn joint_velocity(&self, link: &MultibodyLink) -> Vec { + self.with_ref(|mb| mb.joint_velocity(&link.0).iter().copied().collect()) + } + /// The body Jacobian of link ``link_id`` as a row-major nested + /// list with ``2*dim`` rows (linear then angular) and `ndofs` + /// columns. + fn body_jacobian(&self, link_id: usize) -> Vec> { + self.with_ref(|mb| { + let j = mb.body_jacobian(link_id); + (0..j.nrows()) + .map(|r| (0..j.ncols()).map(|c| j[(r, c)]).collect()) + .collect() + }) + } + /// Link indices on the path from the root to ``link_id`` + /// (inclusive). + fn kinematic_branch(&self, link_id: usize) -> Vec { + self.with_ref(|mb| mb.kinematic_branch(link_id)) + } + /// Write each link's pose (and optionally mass properties) into + /// ``bodies`` from this multibody's current generalized state. + fn update_rigid_bodies(&self, bodies: &mut RigidBodySet, update_mass_properties: bool) { + self.with_ref(|mb| mb.update_rigid_bodies(&mut bodies.0, update_mass_properties)); + } + /// Recompute link poses from the generalized coordinates + /// (forward kinematics). + /// + /// :param bodies: Rigid-body set (read-only here). + /// :param read_root_pose_from_rigid_body: If ``True``, seed the + /// root link's pose from its rigid body first. + #[pyo3(signature = (bodies, read_root_pose_from_rigid_body=false))] + fn forward_kinematics(&mut self, bodies: &RigidBodySet, read_root_pose_from_rigid_body: bool) { + self.with_mut(|mb| mb.forward_kinematics(&bodies.0, read_root_pose_from_rigid_body)); + } +} + +/// Iterator yielding the :class:`MultibodyLink`-s of a +/// :class:`Multibody`. +#[pyclass] +pub struct MultibodyLinkIter { + links: Vec, + i: usize, +} + +#[pymethods] +impl MultibodyLinkIter { + /// Return ``self`` (iterator protocol). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Advance and return the next link. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.links.len() { + return None; + } + let l = slf.links[slf.i]; + slf.i += 1; + Some(l) + } +} + +// ================================================================= +// MultibodyJointSet +// ================================================================= +/// Container of reduced-coordinate multibody articulations. +/// +/// Joints inserted here build tree-structured articulations (one +/// :class:`Multibody` per connected component) whose constraints +/// are solved in reduced coordinates rather than via impulses. +/// Recommended for robots, characters, and any kinematic chain +/// where exact joint behaviour is more important than the +/// flexibility of arbitrary topologies (use +/// :class:`ImpulseJointSet` for those). +/// +/// Supports ``len()`` and iteration over joint handles. +#[pyclass(name = "MultibodyJointSet", module = "rapier", unsendable)] +pub struct MultibodyJointSet(pub rapier::dynamics::MultibodyJointSet); + +#[pymethods] +impl MultibodyJointSet { + /// Construct an empty :class:`MultibodyJointSet`. + #[new] + fn new() -> Self { + Self(rapier::dynamics::MultibodyJointSet::new()) + } + + /// Insert a dynamic joint between ``parent`` and ``link_body``. + /// + /// :param parent: Handle of the body that will become the + /// parent link. + /// :param link_body: Handle of the body that will become the + /// child link. + /// :param joint: A concrete joint or builder. + /// :param wake_up: If ``True``, wake the attached bodies. + /// :returns: A new handle, or ``None`` if the bodies cannot be + /// attached (e.g. doing so would create a cycle). + #[pyo3(signature = (parent, link_body, joint, wake_up=true))] + fn insert( + &mut self, + parent: &RigidBodyHandle, + link_body: &RigidBodyHandle, + joint: &Bound<'_, PyAny>, + wake_up: bool, + ) -> PyResult> { + let data = { + // Accept any of the concrete joint wrappers OR a GenericJoint OR a + // builder. Return Result. + let obj = joint; + let result: crate::pyo3::PyResult = (|| { + if let Ok(j) = obj.extract::>() { + return Ok(j.to_owned_generic()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + { + let obj = obj; + { + let obj = obj; + // `try_extract_dim_specific_joint(obj) -> PyResult>` + // handles the dim-specific joint (SphericalJoint in 3D). + match try_extract_dim_specific_joint(obj)? { + Some(gj) => Ok(gj), + None => Err(crate::pyo3::exceptions::PyTypeError::new_err( + "expected a joint (concrete or generic) or a joint builder", + )), + } + } + } + })(); + result + }?; + Ok(self + .0 + .insert(parent.0, link_body.0, data, wake_up) + .map(MultibodyJointHandle)) + } + + /// Insert a kinematic joint between ``parent`` and ``link_body``. + /// + /// Kinematic joints are user-driven: the solver enforces the + /// constraint but the joint coordinates themselves are + /// imposed externally rather than evolved by the dynamics. + /// + /// :param parent: Handle of the parent body. + /// :param link_body: Handle of the child body. + /// :param joint: A concrete joint or builder. + /// :param wake_up: If ``True``, wake the attached bodies. + #[pyo3(signature = (parent, link_body, joint, wake_up=true))] + fn insert_kinematic( + &mut self, + parent: &RigidBodyHandle, + link_body: &RigidBodyHandle, + joint: &Bound<'_, PyAny>, + wake_up: bool, + ) -> PyResult> { + let data = { + // Accept any of the concrete joint wrappers OR a GenericJoint OR a + // builder. Return Result. + let obj = joint; + let result: crate::pyo3::PyResult = (|| { + if let Ok(j) = obj.extract::>() { + return Ok(j.to_owned_generic()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + if let Ok(j) = obj.extract::>() { + return Ok(j.0.into()); + } + if let Ok(b) = obj.extract::>() { + return Ok(b.0.build().into()); + } + { + let obj = obj; + { + let obj = obj; + // `try_extract_dim_specific_joint(obj) -> PyResult>` + // handles the dim-specific joint (SphericalJoint in 3D). + match try_extract_dim_specific_joint(obj)? { + Some(gj) => Ok(gj), + None => Err(crate::pyo3::exceptions::PyTypeError::new_err( + "expected a joint (concrete or generic) or a joint builder", + )), + } + } + } + })(); + result + }?; + Ok(self + .0 + .insert_kinematic(parent.0, link_body.0, data, wake_up) + .map(MultibodyJointHandle)) + } + + /// Remove the joint identified by ``handle``. + /// + /// :param handle: Handle returned by :py:meth:`insert`. + /// :param wake_up: If ``True``, wake the formerly attached + /// bodies. + #[pyo3(signature = (handle, wake_up=true))] + fn remove(&mut self, handle: &MultibodyJointHandle, wake_up: bool) { + self.0.remove(handle.0, wake_up); + } + + /// Return the ``(multibody, link_id)`` pair containing the joint. + /// The multibody is a live **view** into the set. + fn get(slf: &Bound<'_, Self>, handle: &MultibodyJointHandle) -> Option<(Multibody, usize)> { + let id = slf.borrow().0.get(handle.0).map(|(_, id)| id)?; + Some(( + Multibody { + set: slf.clone().unbind(), + key: MultibodyKey::Joint(handle.0), + }, + id, + )) + } + + /// Return the :class:`Multibody` (live view) containing the joint. + fn multibody(slf: &Bound<'_, Self>, handle: &MultibodyJointHandle) -> Option { + slf.borrow().0.get(handle.0)?; + Some(Multibody { + set: slf.clone().unbind(), + key: MultibodyKey::Joint(handle.0), + }) + } + + /// Return the :class:`MultibodyLinkId` of ``body`` if it + /// belongs to one of the articulations stored here. + fn rigid_body_link(&self, body: &RigidBodyHandle) -> Option { + self.0.rigid_body_link(body.0).copied().map(MultibodyLinkId) + } + + /// Return the articulation (live view) referred to by ``index``. + fn get_multibody(slf: &Bound<'_, Self>, index: &MultibodyIndex) -> Option { + slf.borrow().0.get_multibody(index.0)?; + Some(Multibody { + set: slf.clone().unbind(), + key: MultibodyKey::Index(index.0), + }) + } + + /// Find the multibody joint connecting two bodies, if they are + /// directly linked within the same articulation. + /// + /// :returns: ``(handle, multibody, link)`` for the child link, or + /// ``None`` if the bodies are not joined in one articulation. + fn joint_between( + slf: &Bound<'_, Self>, + body1: &RigidBodyHandle, + body2: &RigidBodyHandle, + ) -> Option<(MultibodyJointHandle, Multibody, MultibodyLink)> { + let (h, link) = slf + .borrow() + .0 + .joint_between(body1.0, body2.0) + .map(|(h, _mb, link)| (h, *link))?; + Some(( + MultibodyJointHandle(h), + Multibody { + set: slf.clone().unbind(), + key: MultibodyKey::Joint(h), + }, + MultibodyLink(link), + )) + } + + /// Number of articulations currently stored. + fn __len__(&self) -> usize { + self.0.multibodies().count() + } + + /// Iterate over every joint handle in the set. + fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { + let mut handles: Vec = vec![]; + for (h, _, _, _) in slf.0.iter() { + handles.push(MultibodyJointHandle(h)); + } + Py::new(slf.py(), MultibodyJointSetIter { handles, i: 0 }) + } + + /// Iterate over the handles of every joint attached to ``body``. + fn attached_joints( + slf: PyRef<'_, Self>, + body: &RigidBodyHandle, + ) -> PyResult> { + let handles: Vec = slf + .0 + .attached_joints(body.0) + .map(|(_, _, h)| MultibodyJointHandle(h)) + .collect(); + Py::new(slf.py(), MultibodyJointHandleIter { handles, i: 0 }) + } + + /// Solve inverse kinematics for the link identified by + /// ``handle``. + /// + /// Iteratively drives the chain's joint coordinates so that + /// the link's pose matches ``target_pose``. The solver + /// terminates when the residual is below + /// :py:attr:`InverseKinematicsOption.epsilon_linear` and + /// :py:attr:`InverseKinematicsOption.epsilon_angular`, or + /// after :py:attr:`InverseKinematicsOption.max_iters` + /// iterations. + /// + /// :param bodies: Rigid-body set containing the articulation's + /// bodies. + /// :param handle: Handle of the joint whose link is being + /// positioned. + /// :param target_pose: Desired world-space pose for the link. + /// :param option: Solver tuning; defaults to a sensible + /// starting point if omitted. + /// :returns: Flat list of length :py:attr:`Multibody.ndofs` + /// giving the joint-coordinate displacement that achieves + /// the IK target. + /// :raises InvalidHandle: If ``handle`` is stale. + // Inverse kinematics passthrough for a single link of a multibody. + // + // This is a thin wrapper around `Multibody::inverse_kinematics` + // that returns the resulting displacement vector. + #[pyo3(signature = (bodies, handle, target_pose, option=None))] + fn inverse_kinematics_for_link( + &self, + bodies: &RigidBodySet, + handle: &MultibodyJointHandle, + target_pose: PyIsometry, + option: Option<&InverseKinematicsOption>, + ) -> PyResult> { + let (mb, link_id) = self.0.get(handle.0).ok_or_else(|| { + crate::errors::InvalidHandle::new_err(format!( + "no multibody joint for {:?}", + handle.0.into_raw_parts() + )) + })?; + let opts = option.copied().unwrap_or_else(|| InverseKinematicsOption { + damping: 1.0, + max_iters: 10, + constrained_axes: JointAxesMask(rapier::dynamics::JointAxesMask::all()), + epsilon_linear: 1.0e-3, + epsilon_angular: 1.0e-3, + }); + let rapier_opts = opts.to_rapier(); + let target: rapier::math::Pose = target_pose.0.into(); + let mut displacements = crate::na::DVector::::zeros(mb.ndofs()); + mb.inverse_kinematics( + &bodies.0, + link_id, + &rapier_opts, + &target, + |_| true, + &mut displacements, + ); + Ok(displacements.iter().copied().collect()) + } +} + +/// Iterator yielding every :class:`MultibodyJointHandle` in a +/// :class:`MultibodyJointSet`. +#[pyclass] +pub struct MultibodyJointSetIter { + handles: Vec, + i: usize, +} + +#[pymethods] +impl MultibodyJointSetIter { + /// Return ``self`` (iterator protocol). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Advance and return the next handle. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.handles.len() { + return None; + } + let h = slf.handles[slf.i]; + slf.i += 1; + Some(h) + } +} + +/// Iterator yielding the handles of every multibody joint +/// attached to a given rigid body. +#[pyclass] +pub struct MultibodyJointHandleIter { + handles: Vec, + i: usize, +} + +#[pymethods] +impl MultibodyJointHandleIter { + /// Return ``self`` (iterator protocol). + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + /// Advance and return the next handle. + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.i >= slf.handles.len() { + return None; + } + let h = slf.handles[slf.i]; + slf.i += 1; + Some(h) + } +} + +/// Dim-specific generic-joint extractor invoked by the shared +/// `__extract_generic_joint!` macro when none of the dim-agnostic +/// variants match. In 3D this handles `SphericalJoint`. +fn try_extract_dim_specific_joint( + obj: &Bound<'_, PyAny>, +) -> PyResult> { + if let Ok(j) = obj.extract::>() { + return Ok(Some(j.0.into())); + } + if let Ok(b) = obj.extract::>() { + return Ok(Some(b.0.build().into())); + } + Ok(None) +} + +// ----------------------------------------------------------------- +// SphericalJoint + SphericalJointBuilder (3D only) +// ----------------------------------------------------------------- +/// Ball-and-socket joint pinning two bodies at a shared point. +/// +/// All three translational DOFs are locked while all three +/// rotational DOFs remain free, so the two bodies coincide at the +/// anchor point but may rotate freely relative to each other. +/// Available only in 3D. +#[pyclass(name = "SphericalJoint", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct SphericalJoint(pub rapier::dynamics::SphericalJoint); + +#[pymethods] +impl SphericalJoint { + /// Build a new spherical joint with default anchors and frames. + #[new] + fn new() -> Self { + Self(rapier::dynamics::SphericalJoint::new()) + } + + /// Build a :class:`SphericalJointBuilder`. + /// + /// :param body_a: Ignored (kept for API symmetry with other + /// joint builders). + /// :param body_b: Ignored (kept for API symmetry). + /// :param kwargs: Optional keyword args forwarded to the + /// builder; accepted keys are ``local_anchor1``, + /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, + /// ``contacts_enabled``. + #[staticmethod] + #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] + fn builder( + py: Python<'_>, + body_a: Option<&Bound<'_, PyAny>>, + body_b: Option<&Bound<'_, PyAny>>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let _ = (py, body_a, body_b); + SphericalJointBuilder::from_kwargs(rapier::dynamics::SphericalJointBuilder::new(), kwargs) + } + + /// Whether collision detection is enabled between the + /// attached bodies. + #[getter] + fn contacts_enabled(&self) -> bool { + self.0.data.contacts_enabled + } + /// Enable or disable collision detection between the attached + /// bodies. + #[setter] + fn set_contacts_enabled(&mut self, v: bool) { + self.0.data.set_contacts_enabled(v); + } + + /// Body-local point on body 1 where the joint is attached. + #[getter] + fn local_anchor1(&self) -> Point3 { + let v: crate::na::Vector3 = self.0.local_anchor1().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 1. + #[setter] + fn set_local_anchor1(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor1(g); + } + /// Body-local point on body 2 where the joint is attached. + #[getter] + fn local_anchor2(&self) -> Point3 { + let v: crate::na::Vector3 = self.0.local_anchor2().into(); + Point3(crate::na::Point::from(v)) + } + /// Set the body-local anchor on body 2. + #[setter] + fn set_local_anchor2(&mut self, p: PyPoint) { + let g: rapier::math::Vector = p.0.coords.into(); + self.0.set_local_anchor2(g); + } + + /// Body-local frame on body 1 (position + orientation). + #[getter] + fn local_frame1(&self) -> Isometry3 { + let pose: crate::na::Isometry = (*self.0.local_frame1()).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 1. + #[setter] + fn set_local_frame1(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.0.set_local_frame1(p); + } + /// Body-local frame on body 2. + #[getter] + fn local_frame2(&self) -> Isometry3 { + let pose: crate::na::Isometry = (*self.0.local_frame2()).into(); + Isometry3(pose) + } + /// Set the body-local frame on body 2. + #[setter] + fn set_local_frame2(&mut self, iso: PyIsometry) { + let p: rapier::math::Pose = iso.0.into(); + self.0.set_local_frame2(p); + } + + /// Return the motor configured on ``axis``, if any. + fn motor(&self, axis: JointAxis) -> Option { + self.0.motor(axis.to_rapier()).map(JointMotor::from_rapier) + } + /// Return the limits configured on ``axis``, if any. + fn limits(&self, axis: JointAxis) -> Option { + self.0 + .limits(axis.to_rapier()) + .map(JointLimits::from_rapier) + } + /// Set angular limits on ``axis`` (radians; only angular + /// axes are meaningful on a spherical joint). + fn set_limits(&mut self, axis: JointAxis, min: Real, max: Real) { + self.0.set_limits(axis.to_rapier(), [min, max]); + } + /// Configure ``axis`` as a velocity-target motor with damping. + /// + /// :param target_vel: Desired velocity (rad/s for angular axes). + /// :param factor: Damping coefficient. + fn set_motor_velocity(&mut self, axis: JointAxis, target_vel: Real, factor: Real) { + self.0 + .set_motor_velocity(axis.to_rapier(), target_vel, factor); + } + /// Configure ``axis`` as a position-target spring-damper motor. + /// + /// :param target_pos: Desired position (radians). + /// :param stiffness: Spring gain. + /// :param damping: Damper gain. + fn set_motor_position( + &mut self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) { + self.0 + .set_motor_position(axis.to_rapier(), target_pos, stiffness, damping); + } + /// Fully configure the motor on ``axis``. + /// + /// :param target_pos: Position setpoint (radians). + /// :param target_vel: Velocity setpoint (rad/s). + /// :param stiffness: Spring gain. + /// :param damping: Damper gain. + fn set_motor( + &mut self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) { + self.0 + .set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping); + } + /// Clamp the maximum force the motor on ``axis`` can apply. + fn set_motor_max_force(&mut self, axis: JointAxis, max_force: Real) { + self.0.set_motor_max_force(axis.to_rapier(), max_force); + } + /// Select the motor model on ``axis`` (see :class:`MotorModel`). + fn set_motor_model(&mut self, axis: JointAxis, model: MotorModel) { + self.0.set_motor_model(axis.to_rapier(), model.to_rapier()); + } + + /// Access the underlying :class:`GenericJoint` description. + #[getter] + fn data(&self) -> GenericJoint { + GenericJoint::new_owned(self.0.data) + } + /// Spring coefficients controlling this joint's softness + /// (see :class:`SpringCoefficients`). + #[getter] + fn softness(&self) -> SpringCoefficients { + SpringCoefficients(self.0.softness()) + } + /// Set the spring coefficients controlling this joint's softness. + #[setter] + fn set_softness(&mut self, v: SpringCoefficients) { + let _ = self.0.set_softness(v.0); + } +} + +/// Fluent builder for :class:`SphericalJoint`. +/// +/// Modifier methods return a new builder, mirroring the Rust API. +/// Call :py:meth:`build` to produce a :class:`SphericalJoint`. +#[pyclass(name = "SphericalJointBuilder", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct SphericalJointBuilder(pub rapier::dynamics::SphericalJointBuilder); + +impl SphericalJointBuilder { + fn from_kwargs( + base: rapier::dynamics::SphericalJointBuilder, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let mut me = Self(base); + if let Some(kw) = kwargs { + for (k, v) in kw.iter() { + let key: String = k.extract()?; + match key.as_str() { + "local_anchor1" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor1(g); + } + "local_anchor2" => { + let p: PyPoint = v.extract()?; + let g: rapier::math::Vector = p.0.coords.into(); + me.0 = me.0.local_anchor2(g); + } + "local_frame1" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame1(p); + } + "local_frame2" => { + let iso: PyIsometry = v.extract()?; + let p: rapier::math::Pose = iso.0.into(); + me.0 = me.0.local_frame2(p); + } + "contacts_enabled" => { + let b: bool = v.extract()?; + me.0 = me.0.contacts_enabled(b); + } + _ => { + return Err(PyTypeError::new_err(format!( + "unknown SphericalJointBuilder kwarg: '{}'", + key + ))); + } + } + } + } + Ok(me) + } +} + +#[pymethods] +impl SphericalJointBuilder { + /// Construct an empty :class:`SphericalJointBuilder`. + #[new] + fn new() -> Self { + Self(rapier::dynamics::SphericalJointBuilder::new()) + } + + /// Set the body-local anchor on body 1. + fn local_anchor1(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor1(g)) + } + /// Set the body-local anchor on body 2. + fn local_anchor2(&self, p: PyPoint) -> Self { + let g: rapier::math::Vector = p.0.coords.into(); + Self(self.0.local_anchor2(g)) + } + /// Set the body-local frame (position + orientation) on body 1. + fn local_frame1(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame1(p)) + } + /// Set the body-local frame on body 2. + fn local_frame2(&self, iso: PyIsometry) -> Self { + let p: rapier::math::Pose = iso.0.into(); + Self(self.0.local_frame2(p)) + } + /// Enable or disable contacts between the bodies attached by + /// the joint. + fn contacts_enabled(&self, b: bool) -> Self { + Self(self.0.contacts_enabled(b)) + } + /// Configure a velocity-target motor with damping on ``axis``. + /// + /// :param target_vel: Velocity setpoint (rad/s for angular axes). + /// :param factor: Damping coefficient. + fn motor_velocity(&self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { + Self(self.0.motor_velocity(axis.to_rapier(), target_vel, factor)) + } + /// Configure a spring-damper motor toward ``target_pos`` on + /// ``axis``. + /// + /// :param target_pos: Position setpoint (radians). + /// :param stiffness: Spring gain. + /// :param damping: Damper gain. + fn motor_position( + &self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) -> Self { + Self( + self.0 + .motor_position(axis.to_rapier(), target_pos, stiffness, damping), + ) + } + /// Fully configure the motor on ``axis`` with both position and + /// velocity setpoints. + fn motor( + &self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> Self { + Self( + self.0 + .motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping), + ) + } + /// Clamp the maximum force the motor on ``axis`` can apply. + fn motor_max_force(&self, axis: JointAxis, max_force: Real) -> Self { + Self(self.0.motor_max_force(axis.to_rapier(), max_force)) + } + /// Select the motor model on ``axis`` (see :class:`MotorModel`). + fn motor_model(&self, axis: JointAxis, model: MotorModel) -> Self { + Self(self.0.motor_model(axis.to_rapier(), model.to_rapier())) + } + /// Apply ``[min, max]`` limits on ``axis`` (radians). + fn limits(&self, axis: JointAxis, min: Real, max: Real) -> Self { + Self(self.0.limits(axis.to_rapier(), [min, max])) + } + /// Materialize the builder into a :class:`SphericalJoint`. + /// Set the spring softness coefficients (see + /// :class:`SpringCoefficients`). Returns a new builder. + fn softness(&self, v: SpringCoefficients) -> Self { + Self(self.0.softness(v.0)) + } + /// Materialize the builder into a :class:`SphericalJoint`. + fn build(&self) -> SphericalJoint { + SphericalJoint(self.0.build()) + } +} + +// 3D RevoluteJoint constructor / builder need axis argument. +// The shared common macro emits the wrapper bodies; here we just +// expose the 3D constructors as additional impls. +#[pymethods] +impl RevoluteJoint { + /// Construct a 3D revolute joint with an explicit axis. + /// + /// :param axis: Rotation axis expressed in each body's local + /// frame. + #[staticmethod] + #[pyo3(name = "new_with_axis")] + fn new_with_axis(axis: PyVector) -> Self { + let g: rapier::math::Vector = axis.0.into(); + Self(rapier::dynamics::RevoluteJoint::new(g)) + } + + /// Body-local rotation axis on body 1. + #[getter] + fn local_axis1(&self) -> Vec3 { + let v: crate::na::Vector3 = self.0.data.local_axis1().into(); + Vec3(v) + } + /// Body-local rotation axis on body 2. + #[getter] + fn local_axis2(&self) -> Vec3 { + let v: crate::na::Vector3 = self.0.data.local_axis2().into(); + Vec3(v) + } +} + +#[pymethods] +impl RevoluteJointBuilder { + /// Set the body-local rotation axis on body 1. + fn local_axis1(&self, v: PyVector) -> Self { + // The Rust API has no `local_axis1` builder method on + // RevoluteJointBuilder; we round-trip via the underlying + // joint to set the axis. + let mut j = self.0.build(); + let g: rapier::math::Vector = v.0.into(); + j.data.set_local_axis1(g); + Self(rapier::dynamics::RevoluteJointBuilder(j)) + } + /// Set the body-local rotation axis on body 2. + fn local_axis2(&self, v: PyVector) -> Self { + let mut j = self.0.build(); + let g: rapier::math::Vector = v.0.into(); + j.data.set_local_axis2(g); + Self(rapier::dynamics::RevoluteJointBuilder(j)) + } +} + +pub fn register_joints( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + // Enums/handles/structs + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // Concrete joints + builders + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // Sets & views + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/lib.rs b/python/rapier-py-3d/src/lib.rs index eadee6f9c..bb1d741e5 100644 --- a/python/rapier-py-3d/src/lib.rs +++ b/python/rapier-py-3d/src/lib.rs @@ -9,6 +9,24 @@ // 2024 edition that trips `unsafe_op_in_unsafe_fn`. The calls are pyo3-internal // and correct; the previous macro-based layout hid them via macro hygiene. #![allow(unsafe_op_in_unsafe_fn)] +// These modules are a faithful, mechanical de-macroization of the former +// `rapier-py-core` macro layer. Clippy never lint-checked the macro-expanded +// code (it skips external-macro output), so the same code now surfaces +// warnings it did not before. The flagged patterns are all intentional: +// - SCREAMING_CASE enum variants and methods map 1:1 to Python enum members +// and factory names — renaming them would break the public Python API. +// - `new`-returning-a-builder, `to_/from_rapier` conventions, and boxed-fn +// signature types are pyo3 idioms. +// - the leftover identity `.into()`s come from the f64→f32 flavor collapse. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(clippy::useless_conversion)] +#![allow(clippy::wrong_self_convention)] +#![allow(clippy::new_ret_no_self)] +#![allow(clippy::type_complexity)] +#![allow(clippy::field_reassign_with_default)] +#![allow(clippy::redundant_closure_call)] +#![allow(clippy::redundant_locals)] // Re-export the crate dependencies so the modules can refer to them via stable // `crate::…` paths (a holdover from the former macro layer, kept intentional). @@ -22,11 +40,28 @@ pub mod conv; pub mod errors; pub mod serde_io; +pub mod controllers; +pub mod debug_render; +pub mod dynamics; +pub mod events_hooks; +pub mod geometry; +pub mod joints; +pub mod loaders; pub mod math; +pub mod pipeline; +pub mod serde_glue; +pub use controllers::*; pub use conv::*; +pub use debug_render::*; +pub use dynamics::*; pub use errors::*; +pub use events_hooks::*; +pub use geometry::*; +pub use joints::*; +pub use loaders::*; pub use math::*; +pub use pipeline::*; use pyo3::prelude::*; @@ -36,6 +71,14 @@ const RAPIER_PY_VERSION: &str = env!("CARGO_PKG_VERSION"); fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { errors::register_errors(py, m)?; register_math(py, m)?; + register_geometry(py, m)?; + register_joints(py, m)?; + register_dynamics(py, m)?; + register_pipeline(py, m)?; + register_events_hooks(py, m)?; + register_loaders(py, m)?; + register_controllers(py, m)?; + register_debug_render(py, m)?; m.add("__version__", RAPIER_PY_VERSION)?; Ok(()) } diff --git a/python/rapier-py-3d/src/loaders.rs b/python/rapier-py-3d/src/loaders.rs new file mode 100644 index 000000000..d045314f9 --- /dev/null +++ b/python/rapier-py-3d/src/loaders.rs @@ -0,0 +1,1600 @@ +//! Concrete 3D / f32 loader `#[pyclass]` types (URDF / MJCF / Mesh) and the +//! `register_loaders` entry point. +//! +//! The mesh, URDF and MJCF loaders were previously emitted by the +//! `__define_loaders_3d!` / `__define_loaders_urdf!` / `__define_loaders_mjcf!` +//! macros (spread across `loaders.rs` / `urdf.rs` / `mjcf.rs` in the old +//! `rapier-py-core` layer). With a single concrete target — 3D, `f32` — they +//! are written out directly here, with no macros. +//! +//! `register_loaders(py, m)` registers all three loaders for the `#[pymodule]` +//! entry point. +//! +//! `rapier3d-urdf` / `rapier3d-meshloader` / `rapier3d-mjcf` are 3D/f32-only +//! upstream, which is why the loaders only ever existed in this variant. + +use crate::*; +use rapier3d as rapier; + +// ============================================================== +// Mesh loader +// ============================================================== + +/// One shape extracted by :func:`load_from_path` or +/// :func:`load_from_raw_mesh`. +/// +/// Bundles the converted :class:`SharedShape`, its world-relative +/// :class:`Isometry3` pose (taken from any ``visual.origin`` / +/// ``collision.origin`` element in the source file), and the raw +/// triangle mesh ``(vertices, indices)`` used to build the shape. +/// +/// :ivar shape: Converted collision shape. +/// :ivar pose: World-relative pose of the shape. +/// :ivar vertices: ``(N, 3)`` float32 NumPy array of mesh vertices. +/// :ivar indices: ``(M, 3)`` uint32 NumPy array of triangle indices. +#[pyclass(name = "LoadedShape", module = "rapier")] +pub struct LoadedShape { + #[pyo3(get)] + pub shape: SharedShape, + #[pyo3(get)] + pub pose: Isometry3, + vertices: Vec<[f32; 3]>, + indices: Vec<[u32; 3]>, +} + +#[pymethods] +impl LoadedShape { + /// ``(N, 3)`` float32 NumPy array of raw mesh vertex positions. + #[getter] + fn vertices<'py>( + &self, + py: crate::pyo3::Python<'py>, + ) -> crate::pyo3::Bound<'py, crate::numpy::PyArray2> { + use crate::numpy::PyArray2; + let n = self.vertices.len(); + let mut flat: Vec = Vec::with_capacity(n * 3); + for v in &self.vertices { + flat.extend_from_slice(v); + } + PyArray2::from_vec2_bound( + py, + &self.vertices.iter().map(|v| v.to_vec()).collect::>(), + ) + .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 3], false)) + } + + /// ``(M, 3)`` uint32 NumPy array of triangle face indices. + #[getter] + fn indices<'py>( + &self, + py: crate::pyo3::Python<'py>, + ) -> crate::pyo3::Bound<'py, crate::numpy::PyArray2> { + use crate::numpy::PyArray2; + PyArray2::from_vec2_bound( + py, + &self.indices.iter().map(|v| v.to_vec()).collect::>(), + ) + .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 3], false)) + } + + /// Return the ``LoadedShape(...)`` repr. + fn __repr__(&self) -> String { + format!( + "LoadedShape(shape={:?}, n_vertices={}, n_faces={})", + self.shape.0.shape_type(), + self.vertices.len(), + self.indices.len(), + ) + } +} + +fn _loaded_shape_from_meshloader(loaded: rapier3d_meshloader::LoadedShape) -> LoadedShape { + let iso: crate::na::Isometry = loaded.pose.into(); + LoadedShape { + shape: SharedShape(loaded.shape), + pose: Isometry3(iso), + vertices: loaded.raw_mesh.vertices.clone(), + indices: loaded.raw_mesh.faces.clone(), + } +} + +/// Load shapes from a mesh file on disk. +/// +/// The file is parsed (formats supported by ``rapier3d-meshloader`` +/// — typically OBJ, GLTF, STL) into one or more groups; each group +/// is independently converted into a shape using ``converter``. +/// +/// :param path: Path to the source mesh file. +/// :param converter: :class:`MeshConverter` to use (defaults to +/// :attr:`MeshConverter.TriMesh`). +/// :param scale: Uniform scale applied during conversion. +/// :returns: A list with one entry per source group, each either a +/// :class:`LoadedShape` (success) or a ``MeshConversionError`` +/// instance (failure). +/// :raises MeshLoaderError: if the file itself failed to load. +#[pyfunction] +#[pyo3(name = "load_from_path")] +#[pyo3(signature = (path, converter=None, scale=1.0))] +fn loaders_mesh_load_from_path( + py: crate::pyo3::Python<'_>, + path: &str, + converter: Option, + scale: Real, +) -> crate::pyo3::PyResult> { + use crate::pyo3::IntoPy; + let converter = converter.unwrap_or(MeshConverter(rapier::geometry::MeshConverter::TriMesh)); + let scale_v = rapier::math::Vector::new(scale, scale, scale); + let results = rapier3d_meshloader::load_from_path(path, &converter.0, scale_v) + .map_err(|e| crate::errors::MeshLoaderError::new_err(format!("{e}")))?; + let mut out: Vec = Vec::with_capacity(results.len()); + for r in results { + match r { + Ok(loaded) => { + let py_loaded = _loaded_shape_from_meshloader(loaded); + out.push(crate::pyo3::Py::new(py, py_loaded)?.into_py(py)); + } + Err(e) => { + let exc = crate::errors::MeshConversionError::new_err(format!("{e}")); + out.push(exc.value_bound(py).clone().into()); + } + } + } + Ok(out) +} + +/// Load a single shape from an in-memory triangle mesh. +/// +/// Identical to :func:`load_from_path` except the mesh data comes +/// from NumPy arrays / sequences directly. No file IO. +/// +/// :param vertices: Sequence of 3D vertex positions. +/// :param indices: Sequence of triangle indices ``(i, j, k)``. +/// :param converter: :class:`MeshConverter` (defaults to TriMesh). +/// :param scale: Uniform scale applied during conversion. +/// :returns: A :class:`LoadedShape`. +/// :raises MeshConversionError: if the conversion failed. +#[pyfunction] +#[pyo3(name = "load_from_raw_mesh")] +#[pyo3(signature = (vertices, indices, converter=None, scale=1.0))] +fn loaders_mesh_load_from_raw_mesh( + _py: crate::pyo3::Python<'_>, + vertices: &crate::pyo3::Bound<'_, crate::pyo3::PyAny>, + indices: &crate::pyo3::Bound<'_, crate::pyo3::PyAny>, + converter: Option, + scale: Real, +) -> crate::pyo3::PyResult { + let verts = crate::geometry::extract_verts_for_dim(vertices)?; + let idx = crate::geometry::extract_indices(indices)?; + let converter = converter.unwrap_or(MeshConverter(rapier::geometry::MeshConverter::TriMesh)); + let mut mesh = mesh_loader::Mesh::default(); + mesh.vertices = verts.iter().map(|v| [v.x, v.y, v.z]).collect(); + mesh.faces = idx.clone(); + let scale_v = rapier::math::Vector::new(scale, scale, scale); + let (shape, pose) = rapier3d_meshloader::load_from_raw_mesh(&mesh, &converter.0, scale_v) + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e}")))?; + let iso: crate::na::Isometry = pose.into(); + Ok(LoadedShape { + shape: SharedShape(shape), + pose: Isometry3(iso), + vertices: mesh.vertices, + indices: mesh.faces, + }) +} + +// ============================================================== +// URDF loader +// ============================================================== + +// `rapier3d-urdf` resolves mesh references against a base directory. +use std::path::Path as _StdPath; + +// ----- URDF options ------------------------------------------------ + +/// Bitflags controlling the multibody-joint URDF insertion path. +/// +/// Combine flags with the usual ``|`` and ``&`` operators. Pass +/// the result to +/// :meth:`UrdfRobot.insert_using_multibody_joints`. +#[pyclass(name = "UrdfMultibodyOptions", module = "rapier", frozen)] +#[derive(Clone, Copy, Debug, Default)] +pub struct UrdfMultibodyOptions(pub rapier3d_urdf::UrdfMultibodyOptions); + +#[pymethods] +impl UrdfMultibodyOptions { + /// Build from a raw bit pattern. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u8) -> Self { + Self(rapier3d_urdf::UrdfMultibodyOptions::from_bits_truncate( + bits, + )) + } + /// Return the empty flag set (no flags). + #[staticmethod] + fn empty() -> Self { + Self(rapier3d_urdf::UrdfMultibodyOptions::empty()) + } + /// Treat URDF joints as kinematic rather than dynamic. + #[classattr] + const JOINTS_ARE_KINEMATIC: UrdfMultibodyOptions = + UrdfMultibodyOptions(rapier3d_urdf::UrdfMultibodyOptions::JOINTS_ARE_KINEMATIC); + /// Disable self-collisions between links of the same robot. + #[classattr] + const DISABLE_SELF_CONTACTS: UrdfMultibodyOptions = + UrdfMultibodyOptions(rapier3d_urdf::UrdfMultibodyOptions::DISABLE_SELF_CONTACTS); + /// Raw bits as an unsigned int. + #[getter] + fn bits(&self) -> u8 { + self.0.bits() + } + /// Bitwise OR: union of two flag sets. + fn __or__(&self, other: &UrdfMultibodyOptions) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND: intersection of two flag sets. + fn __and__(&self, other: &UrdfMultibodyOptions) -> Self { + Self(self.0 & other.0) + } + /// Return ``UrdfMultibodyOptions(bits=0b...)`` repr. + fn __repr__(&self) -> String { + format!("UrdfMultibodyOptions(bits={:#06b})", self.0.bits()) + } +} + +/// Configuration for the URDF loader. +/// +/// Mirrors ``rapier3d_urdf::UrdfLoaderOptions``. Pass an instance +/// to :meth:`UrdfRobot.from_file` / :meth:`from_str` / +/// :meth:`from_robot` to influence which colliders are produced +/// and how the robot is grounded. +/// +/// :ivar create_colliders_from_collision_shapes: If ``True``, +/// materialize ```` blocks as colliders. +/// :ivar create_colliders_from_visual_shapes: If ``True``, also +/// materialize ```` blocks as colliders (off by default). +/// :ivar apply_imported_mass_props: If ``True``, use the URDF +/// ```` block as the body's mass properties. +/// :ivar enable_joint_collisions: If ``True``, do not disable +/// collisions between links joined by a URDF joint. +/// :ivar make_roots_fixed: If ``True``, the robot's root bodies +/// are created as ``Fixed`` instead of ``Dynamic``. +/// :ivar trimesh_flags: :class:`TriMeshFlags` applied to mesh +/// colliders. +/// :ivar mesh_converter: Optional :class:`MeshConverter` controlling +/// how every referenced mesh is turned into a collider shape. +/// Defaults to ``None`` (trimesh, using ``trimesh_flags``). Set +/// e.g. ``MeshConverter.Obb()`` to get cheap proxy shapes while +/// keeping the original mesh available as a visual override (see +/// :attr:`UrdfColliderHandle.visual`). +/// :ivar shift: Rigid transform applied to every body of the +/// robot on import (applied *after* ``scale``). +/// :ivar scale: Uniform scale applied to every length read from the +/// URDF (link positions, joint anchors, mesh sizes, primitive +/// shape sizes, inertial offsets, prismatic joint limits). Mass +/// and inertia tensors are left unchanged. Defaults to ``1.0``. +/// :ivar squeeze_empty_fixed_links: If ``True`` (default), URDF links +/// with no ````/````/```` connected +/// by fixed joints are removed and the chain spliced together, so +/// bodyless "frame-only" links (e.g. ``world``, ``*_tcp``) no +/// longer create mass-less rigid-bodies. +#[pyclass(name = "UrdfLoaderOptions", module = "rapier")] +#[derive(Clone)] +pub struct UrdfLoaderOptions { + #[pyo3(get, set)] + pub create_colliders_from_collision_shapes: bool, + #[pyo3(get, set)] + pub create_colliders_from_visual_shapes: bool, + #[pyo3(get, set)] + pub apply_imported_mass_props: bool, + #[pyo3(get, set)] + pub enable_joint_collisions: bool, + #[pyo3(get, set)] + pub make_roots_fixed: bool, + #[pyo3(get, set)] + pub trimesh_flags: TriMeshFlags, + #[pyo3(get, set)] + pub mesh_converter: Option, + #[pyo3(get, set)] + pub shift: Isometry3, + #[pyo3(get, set)] + pub scale: Real, + #[pyo3(get, set)] + pub squeeze_empty_fixed_links: bool, + /// Template collider applied to every imported collider before + /// its shape/material are filled in. ``None`` keeps the rapier + /// default (density 0). + #[pyo3(get, set)] + pub collider_blueprint: Option, + /// Template rigid-body used for every imported link. ``None`` + /// keeps the rapier default (a dynamic body). + #[pyo3(get, set)] + pub rigid_body_blueprint: Option, +} + +#[pymethods] +impl UrdfLoaderOptions { + /// Build a ``UrdfLoaderOptions`` with optional overrides. + /// + /// See the class docstring for the meaning of each parameter. + #[new] + #[pyo3(signature = ( + create_colliders_from_collision_shapes = true, + create_colliders_from_visual_shapes = false, + apply_imported_mass_props = true, + enable_joint_collisions = false, + make_roots_fixed = false, + trimesh_flags = None, + mesh_converter = None, + shift = None, + scale = 1.0, + squeeze_empty_fixed_links = true, + collider_blueprint = None, + rigid_body_blueprint = None, + ))] + #[allow(clippy::too_many_arguments)] + fn new( + create_colliders_from_collision_shapes: bool, + create_colliders_from_visual_shapes: bool, + apply_imported_mass_props: bool, + enable_joint_collisions: bool, + make_roots_fixed: bool, + trimesh_flags: Option, + mesh_converter: Option, + shift: Option, + scale: Real, + squeeze_empty_fixed_links: bool, + collider_blueprint: Option, + rigid_body_blueprint: Option, + ) -> Self { + let default = rapier3d_urdf::UrdfLoaderOptions::default(); + let identity_iso: crate::na::Isometry = rapier::math::Pose::IDENTITY.into(); + Self { + create_colliders_from_collision_shapes, + create_colliders_from_visual_shapes, + apply_imported_mass_props, + enable_joint_collisions, + make_roots_fixed, + trimesh_flags: trimesh_flags.unwrap_or(TriMeshFlags(default.trimesh_flags)), + mesh_converter, + shift: shift.unwrap_or(Isometry3(identity_iso)), + scale, + squeeze_empty_fixed_links, + collider_blueprint, + rigid_body_blueprint, + } + } + + /// Return a debug string with the main loader-option booleans. + fn __repr__(&self) -> String { + format!( + "UrdfLoaderOptions(create_colliders_from_collision_shapes={}, create_colliders_from_visual_shapes={}, apply_imported_mass_props={}, enable_joint_collisions={}, make_roots_fixed={}, scale={})", + self.create_colliders_from_collision_shapes, + self.create_colliders_from_visual_shapes, + self.apply_imported_mass_props, + self.enable_joint_collisions, + self.make_roots_fixed, + self.scale, + ) + } +} + +impl UrdfLoaderOptions { + pub(crate) fn to_rapier(&self) -> rapier3d_urdf::UrdfLoaderOptions { + let mut o = rapier3d_urdf::UrdfLoaderOptions::default(); + o.create_colliders_from_collision_shapes = self.create_colliders_from_collision_shapes; + o.create_colliders_from_visual_shapes = self.create_colliders_from_visual_shapes; + o.apply_imported_mass_props = self.apply_imported_mass_props; + o.enable_joint_collisions = self.enable_joint_collisions; + o.make_roots_fixed = self.make_roots_fixed; + o.trimesh_flags = self.trimesh_flags.0; + o.mesh_converter = self.mesh_converter.as_ref().map(|m| m.0); + o.shift = self.shift.0.into(); + o.scale = self.scale; + o.squeeze_empty_fixed_links = self.squeeze_empty_fixed_links; + if let Some(cb) = &self.collider_blueprint { + o.collider_blueprint = cb.builder.clone(); + } + if let Some(rb) = &self.rigid_body_blueprint { + o.rigid_body_blueprint = rb.builder.clone(); + } + o + } +} + +// ----- URDF read-only views ---------------------------------------- + +/// Read-only view of a URDF ```` element. +/// +/// Wraps the raw ``urdf_rs::Link`` for inspection from Python. +/// Returned by :attr:`Robot.links`. +#[pyclass(name = "UrdfLink", module = "rapier")] +#[derive(Clone)] +pub struct UrdfLink { + pub raw: urdf_rs::Link, +} + +#[pymethods] +impl UrdfLink { + /// Name of the link as declared in the URDF. + #[getter] + fn name(&self) -> String { + self.raw.name.clone() + } + /// Number of ```` elements on the link. + #[getter] + fn n_visuals(&self) -> usize { + self.raw.visual.len() + } + /// Number of ```` elements on the link. + #[getter] + fn n_collisions(&self) -> usize { + self.raw.collision.len() + } + /// Mass from the link's ```` block. + #[getter] + fn mass(&self) -> f64 { + self.raw.inertial.mass.value + } + /// Return the ``UrdfLink(...)`` repr. + fn __repr__(&self) -> String { + format!( + "UrdfLink(name={:?}, n_visuals={}, n_collisions={}, mass={})", + self.raw.name, + self.raw.visual.len(), + self.raw.collision.len(), + self.raw.inertial.mass.value, + ) + } +} + +/// Read-only view of a URDF ```` element. +/// +/// Wraps the raw ``urdf_rs::Joint`` for inspection from Python. +/// Returned by :attr:`Robot.joints`. +#[pyclass(name = "UrdfJoint", module = "rapier")] +#[derive(Clone)] +pub struct UrdfJoint { + pub raw: urdf_rs::Joint, +} + +#[pymethods] +impl UrdfJoint { + /// Joint name as declared in the URDF. + #[getter] + fn name(&self) -> String { + self.raw.name.clone() + } + /// Joint kind as a debug string (``"Revolute"``, ``"Fixed"``, ...). + #[getter] + fn joint_type(&self) -> String { + format!("{:?}", self.raw.joint_type) + } + /// Name of the parent link. + #[getter] + fn parent(&self) -> String { + self.raw.parent.link.clone() + } + /// Name of the child link. + #[getter] + fn child(&self) -> String { + self.raw.child.link.clone() + } + /// Axis vector ``[x, y, z]`` in the parent link's frame. + #[getter] + fn axis(&self) -> Vec { + self.raw.axis.xyz.0.to_vec() + } + /// Return the ``UrdfJoint(...)`` repr. + fn __repr__(&self) -> String { + format!( + "UrdfJoint(name={:?}, type={:?}, parent={:?}, child={:?})", + self.raw.name, self.raw.joint_type, self.raw.parent.link, self.raw.child.link, + ) + } +} + +/// Raw URDF document view (links + joints, pre-simulation). +/// +/// Returned by :meth:`UrdfRobot.from_file` / :meth:`from_str` +/// alongside the simulation-ready :class:`UrdfRobot`. Use this to +/// inspect link / joint names before insertion, or to re-import the +/// same source via :meth:`UrdfRobot.from_robot`. +#[pyclass(name = "Robot", module = "rapier")] +#[derive(Clone)] +pub struct UrdfRobotSource { + pub raw: urdf_rs::Robot, +} + +#[pymethods] +impl UrdfRobotSource { + /// Robot name from the URDF ```` attribute. + #[getter] + fn name(&self) -> String { + self.raw.name.clone() + } + /// List of :class:`UrdfLink` views, in document order. + #[getter] + fn links(&self) -> Vec { + self.raw + .links + .iter() + .map(|l| UrdfLink { raw: l.clone() }) + .collect() + } + /// List of :class:`UrdfJoint` views, in document order. + #[getter] + fn joints(&self) -> Vec { + self.raw + .joints + .iter() + .map(|j| UrdfJoint { raw: j.clone() }) + .collect() + } + /// Return the ``Robot(...)`` repr. + fn __repr__(&self) -> String { + format!( + "Robot(name={:?}, n_links={}, n_joints={})", + self.raw.name, + self.raw.links.len(), + self.raw.joints.len(), + ) + } +} + +// ----- URDF handles ------------------------------------------------ + +/// Visual mesh override paired with a URDF collider. +/// +/// Populated by the loader only when a non-default +/// :attr:`UrdfLoaderOptions.mesh_converter` (e.g. +/// ``MeshConverter.Obb()``) replaced the source mesh with a cheap +/// proxy collider — this keeps the original high-resolution mesh +/// available for rendering. +/// +/// :ivar shape: The :class:`SharedShape` representing the visual mesh. +/// :ivar local_pose: Pose of the visual mesh in the collider's local +/// frame, as an :class:`Isometry3`. +#[pyclass(name = "UrdfVisual", module = "rapier")] +#[derive(Clone)] +pub struct UrdfVisual { + #[pyo3(get)] + pub shape: SharedShape, + #[pyo3(get)] + pub local_pose: Isometry3, +} + +/// Handle of one collider inserted from a URDF link. +/// +/// :ivar handle: Underlying :class:`ColliderHandle`. +/// :ivar visual: Optional :class:`UrdfVisual` mesh override (``None`` +/// unless a proxy :attr:`UrdfLoaderOptions.mesh_converter` was used). +#[pyclass(name = "UrdfColliderHandle", module = "rapier", frozen)] +#[derive(Clone, Debug)] +pub struct UrdfColliderHandle { + #[pyo3(get)] + pub handle: ColliderHandle, + pub visual: Option, +} + +#[pymethods] +impl UrdfColliderHandle { + /// Optional visual mesh override for this collider, or ``None``. + #[getter] + fn visual(&self) -> Option { + self.visual.as_ref().map(|v| UrdfVisual { + shape: SharedShape(v.shape.clone()), + local_pose: Isometry3(v.local_pose.into()), + }) + } +} + +/// Handle of one URDF link (its rigid body plus its colliders). +/// +/// :ivar body: :class:`RigidBodyHandle` for the link's body. +/// :ivar colliders: List of :class:`UrdfColliderHandle` attached to it. +#[pyclass(name = "UrdfLinkHandle", module = "rapier", frozen)] +#[derive(Clone, Debug)] +pub struct UrdfLinkHandle { + #[pyo3(get)] + pub body: RigidBodyHandle, + #[pyo3(get)] + pub colliders: Vec, +} + +/// Handle of one URDF joint after insertion. +/// +/// :ivar joint: Either an :class:`ImpulseJointHandle` (when the +/// impulse-joint insertion path was used) or +/// ``Optional[MultibodyJointHandle]`` (multibody path; ``None`` +/// for collapsed fixed joints). +/// :ivar link1: Parent link's :class:`RigidBodyHandle`. +/// :ivar link2: Child link's :class:`RigidBodyHandle`. +#[pyclass(name = "UrdfJointHandle", module = "rapier")] +pub struct UrdfJointHandle { + #[pyo3(get)] + pub joint: crate::pyo3::PyObject, + #[pyo3(get)] + pub link1: RigidBodyHandle, + #[pyo3(get)] + pub link2: RigidBodyHandle, +} + +/// Aggregate handle set returned by URDF insertion functions. +/// +/// :ivar links: One :class:`UrdfLinkHandle` per URDF link. +/// :ivar joints: One :class:`UrdfJointHandle` per URDF joint. +#[pyclass(name = "UrdfRobotHandles", module = "rapier")] +pub struct UrdfRobotHandles { + #[pyo3(get)] + pub links: Vec, + #[pyo3(get)] + pub joints: Vec>, +} + +// ----- UrdfRobot --------------------------------------------------- + +/// A URDF robot, ready to be inserted into the simulation. +/// +/// Build one via :meth:`from_file`, :meth:`from_str`, or +/// :meth:`from_robot`, optionally transform the whole robot with +/// :meth:`append_transform`, then commit it to the world via either +/// :meth:`insert_using_impulse_joints` (full 6-DOF joints with +/// constraints) or :meth:`insert_using_multibody_joints` +/// (reduced-coordinate articulation). +/// +/// The insertion methods *consume* the robot — subsequent calls +/// raise :class:`UrdfError`. +#[pyclass(name = "UrdfRobot", module = "rapier", unsendable)] +pub struct UrdfRobot { + pub inner: Option, +} + +#[pymethods] +impl UrdfRobot { + /// Parse a URDF file and return ``(UrdfRobot, Robot)``. + /// + /// Mesh references inside the URDF are resolved relative to + /// ``mesh_dir`` (default: the URDF file's parent directory). + /// + /// :param path: Path to the ``.urdf`` file on disk. + /// :param options: Optional :class:`UrdfLoaderOptions`. + /// :param mesh_dir: Directory used to resolve mesh refs. + /// :returns: ``(UrdfRobot, Robot)`` tuple — sim-ready robot + /// plus the raw document view. + /// :raises UrdfError: if the file can't be opened or parsed. + #[staticmethod] + #[pyo3(signature = (path, options=None, mesh_dir=None))] + fn from_file( + path: &str, + options: Option, + mesh_dir: Option<&str>, + ) -> crate::pyo3::PyResult<(UrdfRobot, UrdfRobotSource)> { + let opts = options.map(|o| o.to_rapier()).unwrap_or_default(); + let mesh_dir = mesh_dir.map(_StdPath::new); + let (robot, raw) = rapier3d_urdf::UrdfRobot::from_file(path, opts, mesh_dir) + .map_err(|e| crate::errors::UrdfError::new_err(format!("{e}")))?; + Ok((UrdfRobot { inner: Some(robot) }, UrdfRobotSource { raw })) + } + + /// Parse a URDF XML string and return ``(UrdfRobot, Robot)``. + /// + /// Equivalent to :meth:`from_file` but the source XML is + /// provided in memory. Mesh references are resolved relative + /// to ``mesh_dir`` (default: the current working directory). + /// + /// :param xml: Full URDF XML document. + /// :param options: Optional :class:`UrdfLoaderOptions`. + /// :param mesh_dir: Directory used to resolve mesh refs. + /// :returns: ``(UrdfRobot, Robot)`` tuple. + /// :raises UrdfError: if the XML can't be parsed. + #[staticmethod] + #[pyo3(signature = (xml, options=None, mesh_dir=None))] + fn from_str( + xml: &str, + options: Option, + mesh_dir: Option<&str>, + ) -> crate::pyo3::PyResult<(UrdfRobot, UrdfRobotSource)> { + let opts = options.map(|o| o.to_rapier()).unwrap_or_default(); + let mesh_dir = mesh_dir + .map(_StdPath::new) + .unwrap_or_else(|| _StdPath::new(".")); + let (robot, raw) = rapier3d_urdf::UrdfRobot::from_str(xml, opts, mesh_dir) + .map_err(|e| crate::errors::UrdfError::new_err(format!("{e}")))?; + Ok((UrdfRobot { inner: Some(robot) }, UrdfRobotSource { raw })) + } + + /// Build a sim-ready :class:`UrdfRobot` from a previously + /// parsed :class:`Robot` view. + /// + /// Lets you re-import the same document with different loader + /// options without re-parsing. + /// + /// :param robot: Source :class:`Robot` document view. + /// :param options: Optional :class:`UrdfLoaderOptions`. + /// :param mesh_dir: Directory used to resolve mesh refs. + #[staticmethod] + #[pyo3(signature = (robot, options=None, mesh_dir=None))] + fn from_robot( + robot: &UrdfRobotSource, + options: Option, + mesh_dir: Option<&str>, + ) -> crate::pyo3::PyResult { + let opts = options.map(|o| o.to_rapier()).unwrap_or_default(); + let mesh_dir = mesh_dir + .map(_StdPath::new) + .unwrap_or_else(|| _StdPath::new(".")); + let r = rapier3d_urdf::UrdfRobot::from_robot(&robot.raw, opts, mesh_dir); + Ok(UrdfRobot { inner: Some(r) }) + } + + /// Number of links waiting to be inserted. + /// + /// :raises UrdfError: if this robot has already been consumed. + #[getter] + fn n_links(&self) -> crate::pyo3::PyResult { + self.inner + .as_ref() + .map(|r| r.links.len()) + .ok_or_else(|| crate::errors::UrdfError::new_err("UrdfRobot was already consumed")) + } + + /// Number of joints waiting to be inserted. + /// + /// :raises UrdfError: if this robot has already been consumed. + #[getter] + fn n_joints(&self) -> crate::pyo3::PyResult { + self.inner + .as_ref() + .map(|r| r.joints.len()) + .ok_or_else(|| crate::errors::UrdfError::new_err("UrdfRobot was already consumed")) + } + + /// Apply a rigid transform to every body of the robot. + /// + /// Mutates the robot in place. World-anchored joints are kept + /// satisfied across the transform. + /// + /// :param iso: World-space :class:`Isometry3` to prepend to + /// every body pose. + /// :raises UrdfError: if this robot has already been consumed. + fn append_transform(&mut self, iso: Isometry3) -> crate::pyo3::PyResult<()> { + let robot = self + .inner + .as_mut() + .ok_or_else(|| crate::errors::UrdfError::new_err("UrdfRobot was already consumed"))?; + let pose: rapier::math::Pose = iso.0.into(); + robot.append_transform(&pose); + Ok(()) + } + + /// Insert the robot into the world using *impulse joints*. + /// + /// Each URDF joint becomes a full 6-DOF impulse joint with + /// position/velocity constraints. Compared to the multibody + /// path this is simpler conceptually but typically slower and + /// less stable for long kinematic chains. + /// + /// This call **consumes** the :class:`UrdfRobot`. + /// + /// :param bodies: :class:`RigidBodySet` to insert into. + /// :param colliders: :class:`ColliderSet` to insert into. + /// :param joints: :class:`ImpulseJointSet` to insert into. + /// :returns: :class:`UrdfRobotHandles` with all created handles. + /// :raises UrdfError: if this robot has already been consumed. + fn insert_using_impulse_joints( + &mut self, + py: crate::pyo3::Python<'_>, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + joints: &mut ImpulseJointSet, + ) -> crate::pyo3::PyResult> { + use crate::pyo3::IntoPy; + let robot = self + .inner + .take() + .ok_or_else(|| crate::errors::UrdfError::new_err("UrdfRobot was already consumed"))?; + let handles = + robot.insert_using_impulse_joints(&mut bodies.0, &mut colliders.0, &mut joints.0); + let links: Vec = handles + .links + .into_iter() + .map(|lh| { + let cols = lh + .colliders + .into_iter() + .map(|h| UrdfColliderHandle { + handle: ColliderHandle(h.handle), + visual: h.visual, + }) + .collect(); + UrdfLinkHandle { + body: RigidBodyHandle(lh.body), + colliders: cols, + } + }) + .collect(); + let joints_handles: Vec> = handles + .joints + .into_iter() + .map(|jh| { + let joint_py = ImpulseJointHandle(jh.joint).into_py(py); + crate::pyo3::Py::new( + py, + UrdfJointHandle { + joint: joint_py, + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc UrdfJointHandle") + }) + .collect(); + crate::pyo3::Py::new( + py, + UrdfRobotHandles { + links, + joints: joints_handles, + }, + ) + } + + /// Insert the robot into the world using *multibody joints*. + /// + /// Builds a reduced-coordinate articulation: each link's pose + /// is implicitly constrained by its joint, eliminating most + /// drift on long kinematic chains. Recommended for robotic + /// arms, humanoids, and other branched mechanisms. + /// + /// This call **consumes** the :class:`UrdfRobot`. + /// + /// :param bodies: :class:`RigidBodySet` to insert into. + /// :param colliders: :class:`ColliderSet` to insert into. + /// :param multibody_joints: :class:`MultibodyJointSet` to insert into. + /// :param options: Optional :class:`UrdfMultibodyOptions` flags. + /// :returns: :class:`UrdfRobotHandles` with all created handles. + /// :raises UrdfError: if this robot has already been consumed. + #[pyo3(signature = (bodies, colliders, multibody_joints, options=None))] + fn insert_using_multibody_joints( + &mut self, + py: crate::pyo3::Python<'_>, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + multibody_joints: &mut MultibodyJointSet, + options: Option, + ) -> crate::pyo3::PyResult> { + use crate::pyo3::IntoPy; + let robot = self + .inner + .take() + .ok_or_else(|| crate::errors::UrdfError::new_err("UrdfRobot was already consumed"))?; + let opts = options.map(|o| o.0).unwrap_or_default(); + let handles = robot.insert_using_multibody_joints( + &mut bodies.0, + &mut colliders.0, + &mut multibody_joints.0, + opts, + ); + let links: Vec = handles + .links + .into_iter() + .map(|lh| { + let cols = lh + .colliders + .into_iter() + .map(|h| UrdfColliderHandle { + handle: ColliderHandle(h.handle), + visual: h.visual, + }) + .collect(); + UrdfLinkHandle { + body: RigidBodyHandle(lh.body), + colliders: cols, + } + }) + .collect(); + let joints_handles: Vec> = handles + .joints + .into_iter() + .map(|jh| { + let joint_py = jh.joint.map(MultibodyJointHandle).into_py(py); + crate::pyo3::Py::new( + py, + UrdfJointHandle { + joint: joint_py, + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc UrdfJointHandle") + }) + .collect(); + crate::pyo3::Py::new( + py, + UrdfRobotHandles { + links, + joints: joints_handles, + }, + ) + } + + /// Return the ``UrdfRobot(...)`` repr (or ``"UrdfRobot(consumed)"`` + /// after insertion). + fn __repr__(&self) -> String { + match &self.inner { + Some(r) => format!( + "UrdfRobot(n_links={}, n_joints={})", + r.links.len(), + r.joints.len() + ), + None => "UrdfRobot(consumed)".to_string(), + } + } +} + +/// Register the URDF loader `#[pyclass]`-es into `m`. +pub fn register_loaders_urdf( + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +// ============================================================== +// MJCF loader +// ============================================================== + +/// Options applied to multibody joints created from MJCF joints. +/// +/// A flag set; combine flags with ``|`` and pass the result to +/// :meth:`MjcfRobot.insert_using_multibody_joints`. +#[pyclass(name = "MjcfMultibodyOptions", module = "rapier", frozen)] +#[derive(Clone, Copy, Debug, Default)] +pub struct MjcfMultibodyOptions(pub rapier3d_mjcf::MjcfMultibodyOptions); + +#[pymethods] +impl MjcfMultibodyOptions { + /// Build from a raw bit pattern. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u8) -> Self { + Self(rapier3d_mjcf::MjcfMultibodyOptions::from_bits_truncate( + bits, + )) + } + /// Return the empty flag set (no flags). + #[staticmethod] + fn empty() -> Self { + Self(rapier3d_mjcf::MjcfMultibodyOptions::empty()) + } + /// Treat the created multibody joints as kinematic. + #[classattr] + const JOINTS_ARE_KINEMATIC: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::JOINTS_ARE_KINEMATIC); + /// Disable self-collisions between links of the same multibody. + #[classattr] + const DISABLE_SELF_CONTACTS: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::DISABLE_SELF_CONTACTS); + /// Do not insert ```` loop-closure constraints as + /// impulse joints alongside the multibody. + #[classattr] + const SKIP_LOOP_CLOSURES: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_LOOP_CLOSURES); + /// Strip the per-joint motors baked in by the loader before + /// handing joints to the multibody solver. + #[classattr] + const SKIP_JOINT_MOTORS: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_MOTORS); + /// Strip per-joint limits before handing joints to the multibody. + #[classattr] + const SKIP_JOINT_LIMITS: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_LIMITS); + /// Raw bits as an unsigned int. + #[getter] + fn bits(&self) -> u8 { + self.0.bits() + } + /// Bitwise OR: union of two flag sets. + fn __or__(&self, other: &MjcfMultibodyOptions) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND: intersection of two flag sets. + fn __and__(&self, other: &MjcfMultibodyOptions) -> Self { + Self(self.0 & other.0) + } + /// Return ``MjcfMultibodyOptions(bits=0b...)`` repr. + fn __repr__(&self) -> String { + format!("MjcfMultibodyOptions(bits={:#08b})", self.0.bits()) + } +} + +/// How MJCF ``contype`` / ``conaffinity`` masks map onto rapier +/// :class:`InteractionGroups`. +#[pyclass(name = "ContactFilterMode", module = "rapier", eq, eq_int)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ContactFilterMode { + /// ``memberships = filter = contype | conaffinity`` (default). + Symmetric, + /// ``memberships = contype``, ``filter = conaffinity``. + Asymmetric, +} + +impl ContactFilterMode { + pub(crate) fn to_rapier(self) -> rapier3d_mjcf::ContactFilterMode { + match self { + ContactFilterMode::Symmetric => rapier3d_mjcf::ContactFilterMode::Symmetric, + ContactFilterMode::Asymmetric => rapier3d_mjcf::ContactFilterMode::Asymmetric, + } + } +} + +/// Configuration for the MJCF loader. +/// +/// Mirrors the common subset of ``rapier3d_mjcf::MjcfLoaderOptions``. +/// Pass an instance to :meth:`MjcfRobot.from_file` / :meth:`from_str`. +/// +/// :ivar create_colliders_from_collision_shapes: Build colliders for +/// ```` elements that participate in contact. Default ``True``. +/// :ivar create_colliders_from_visual_shapes: Build colliders for +/// visual-only ```` elements. Default ``False``. +/// :ivar apply_imported_mass_props: Use the model's inertial data for +/// body mass properties. Default ``True``. +/// :ivar enable_joint_collisions: Allow contacts between two bodies +/// sharing a joint. Default ``False``. +/// :ivar make_roots_fixed: Create root bodies as ``Fixed``. Default +/// ``False``. +/// :ivar trimesh_flags: :class:`TriMeshFlags` applied to mesh colliders. +/// :ivar mesh_converter: Optional :class:`MeshConverter` controlling +/// how meshes become collider shapes. Default ``None`` (trimesh). +/// :ivar shift: Rigid transform appended to every created body +/// (applied after ``scale``). +/// :ivar scale: Uniform scale applied to lengths read from the MJCF. +/// Default ``1.0``. +/// :ivar skip_plane_geoms: Skip ```` elements +/// entirely. Default ``True``. +/// :ivar disable_joint_motors: Skip the per-joint motor setup the +/// loader normally applies (springs / friction). Default ``False``. +#[pyclass(name = "MjcfLoaderOptions", module = "rapier")] +#[derive(Clone)] +pub struct MjcfLoaderOptions { + #[pyo3(get, set)] + pub create_colliders_from_collision_shapes: bool, + #[pyo3(get, set)] + pub create_colliders_from_visual_shapes: bool, + #[pyo3(get, set)] + pub apply_imported_mass_props: bool, + #[pyo3(get, set)] + pub enable_joint_collisions: bool, + #[pyo3(get, set)] + pub make_roots_fixed: bool, + #[pyo3(get, set)] + pub trimesh_flags: TriMeshFlags, + #[pyo3(get, set)] + pub mesh_converter: Option, + #[pyo3(get, set)] + pub shift: Isometry3, + #[pyo3(get, set)] + pub scale: Real, + #[pyo3(get, set)] + pub skip_plane_geoms: bool, + #[pyo3(get, set)] + pub disable_joint_motors: bool, + /// Template collider applied to every imported collider. ``None`` + /// keeps the rapier default (density 0). + #[pyo3(get, set)] + pub collider_blueprint: Option, + /// Template rigid-body used for every imported body. ``None`` + /// keeps the rapier default (a dynamic body). + #[pyo3(get, set)] + pub rigid_body_blueprint: Option, + /// How ``contype``/``conaffinity`` map onto collision groups. + #[pyo3(get, set)] + pub contact_filter_mode: ContactFilterMode, +} + +#[pymethods] +impl MjcfLoaderOptions { + /// Build an ``MjcfLoaderOptions`` with optional overrides. + #[new] + #[pyo3(signature = ( + create_colliders_from_collision_shapes = true, + create_colliders_from_visual_shapes = false, + apply_imported_mass_props = true, + enable_joint_collisions = false, + make_roots_fixed = false, + trimesh_flags = None, + mesh_converter = None, + shift = None, + scale = 1.0, + skip_plane_geoms = true, + disable_joint_motors = false, + collider_blueprint = None, + rigid_body_blueprint = None, + contact_filter_mode = ContactFilterMode::Symmetric, + ))] + #[allow(clippy::too_many_arguments)] + fn new( + create_colliders_from_collision_shapes: bool, + create_colliders_from_visual_shapes: bool, + apply_imported_mass_props: bool, + enable_joint_collisions: bool, + make_roots_fixed: bool, + trimesh_flags: Option, + mesh_converter: Option, + shift: Option, + scale: Real, + skip_plane_geoms: bool, + disable_joint_motors: bool, + collider_blueprint: Option, + rigid_body_blueprint: Option, + contact_filter_mode: ContactFilterMode, + ) -> Self { + let default = rapier3d_mjcf::MjcfLoaderOptions::default(); + let identity_iso: crate::na::Isometry = rapier::math::Pose::IDENTITY.into(); + Self { + create_colliders_from_collision_shapes, + create_colliders_from_visual_shapes, + apply_imported_mass_props, + enable_joint_collisions, + make_roots_fixed, + trimesh_flags: trimesh_flags.unwrap_or(TriMeshFlags(default.trimesh_flags)), + mesh_converter, + shift: shift.unwrap_or(Isometry3(identity_iso)), + scale, + skip_plane_geoms, + disable_joint_motors, + collider_blueprint, + rigid_body_blueprint, + contact_filter_mode, + } + } + + /// Return a debug string with the main loader-option booleans. + fn __repr__(&self) -> String { + format!( + "MjcfLoaderOptions(create_colliders_from_collision_shapes={}, create_colliders_from_visual_shapes={}, make_roots_fixed={}, scale={}, skip_plane_geoms={})", + self.create_colliders_from_collision_shapes, + self.create_colliders_from_visual_shapes, + self.make_roots_fixed, + self.scale, + self.skip_plane_geoms, + ) + } +} + +impl MjcfLoaderOptions { + pub(crate) fn to_rapier(&self) -> rapier3d_mjcf::MjcfLoaderOptions { + let mut o = rapier3d_mjcf::MjcfLoaderOptions::default(); + o.create_colliders_from_collision_shapes = self.create_colliders_from_collision_shapes; + o.create_colliders_from_visual_shapes = self.create_colliders_from_visual_shapes; + o.apply_imported_mass_props = self.apply_imported_mass_props; + o.enable_joint_collisions = self.enable_joint_collisions; + o.make_roots_fixed = self.make_roots_fixed; + o.trimesh_flags = self.trimesh_flags.0; + o.mesh_converter = self.mesh_converter.as_ref().map(|m| m.0); + o.shift = self.shift.0.into(); + o.scale = self.scale; + o.skip_plane_geoms = self.skip_plane_geoms; + o.disable_joint_motors = self.disable_joint_motors; + o.contact_filter_mode = self.contact_filter_mode.to_rapier(); + if let Some(cb) = &self.collider_blueprint { + o.collider_blueprint = cb.builder.clone(); + } + if let Some(rb) = &self.rigid_body_blueprint { + o.rigid_body_blueprint = rb.builder.clone(); + } + o + } +} + +/// Parsed MJCF model document (read-only). +/// +/// Returned by :meth:`MjcfRobot.from_file` / :meth:`from_str` +/// alongside the simulation-ready :class:`MjcfRobot`. +#[pyclass(name = "MjcfModel", module = "rapier", unsendable)] +pub struct MjcfModel { + pub raw: rapier3d_mjcf::mjcf_rs::model::Model, +} + +#[pymethods] +impl MjcfModel { + /// The ```` name, if any. + #[getter] + fn name(&self) -> Option { + self.raw.name.clone() + } + /// Return the ``MjcfModel(...)`` repr. + fn __repr__(&self) -> String { + format!("MjcfModel(name={:?})", self.raw.name) + } +} + +/// Handle of one collider inserted from an MJCF ````. +/// +/// :ivar handle: Underlying :class:`ColliderHandle`. +#[pyclass(name = "MjcfColliderHandle", module = "rapier", frozen)] +#[derive(Clone, Debug)] +pub struct MjcfColliderHandle { + #[pyo3(get)] + pub handle: ColliderHandle, +} + +/// Handle of one MJCF body (its rigid body plus its colliders). +/// +/// :ivar body: :class:`RigidBodyHandle` for the body. +/// :ivar colliders: List of :class:`MjcfColliderHandle` attached to it. +#[pyclass(name = "MjcfBodyHandle", module = "rapier", frozen)] +#[derive(Clone, Debug)] +pub struct MjcfBodyHandle { + #[pyo3(get)] + pub body: RigidBodyHandle, + #[pyo3(get)] + pub colliders: Vec, +} + +/// Handle of one MJCF joint after insertion. +/// +/// :ivar joint: Either an :class:`ImpulseJointHandle` (impulse-joint +/// path) or ``Optional[MultibodyJointHandle]`` (multibody path). +/// :ivar link1: Parent body's :class:`RigidBodyHandle`. +/// :ivar link2: Child body's :class:`RigidBodyHandle`. +#[pyclass(name = "MjcfJointHandle", module = "rapier")] +pub struct MjcfJointHandle { + #[pyo3(get)] + pub joint: crate::pyo3::PyObject, + #[pyo3(get)] + pub link1: RigidBodyHandle, + #[pyo3(get)] + pub link2: RigidBodyHandle, +} + +/// Aggregate handle set returned by MJCF insertion functions. +/// +/// :ivar bodies: One ``Optional[MjcfBodyHandle]`` per MJCF body, in +/// model order (entry 0 is the implicit world body and is usually +/// ``None``). +/// :ivar joints: One :class:`MjcfJointHandle` per inserted joint. +/// :ivar equality_joints: One :class:`MjcfJointHandle` per +/// ```` loop-closure constraint (always impulse joints). +#[pyclass(name = "MjcfRobotHandles", module = "rapier")] +pub struct MjcfRobotHandles { + #[pyo3(get)] + pub bodies: Vec>, + #[pyo3(get)] + pub joints: Vec>, + #[pyo3(get)] + pub equality_joints: Vec>, +} + +/// A MuJoCo MJCF model, ready to be inserted into the simulation. +/// +/// Build one via :meth:`from_file` / :meth:`from_str`, then commit it +/// to the world via :meth:`insert_using_impulse_joints` or +/// :meth:`insert_using_multibody_joints`. The insertion methods +/// *consume* the robot — subsequent calls raise :class:`MjcfError`. +#[pyclass(name = "MjcfRobot", module = "rapier", unsendable)] +pub struct MjcfRobot { + pub inner: Option, +} + +#[pymethods] +impl MjcfRobot { + /// Parse an MJCF file and return ``(MjcfRobot, MjcfModel)``. + /// + /// ```` directives and mesh references resolve relative + /// to the file's parent directory. + /// + /// :param path: Path to the ``.xml`` file on disk. + /// :param options: Optional :class:`MjcfLoaderOptions`. + /// :returns: ``(MjcfRobot, MjcfModel)`` tuple. + /// :raises MjcfError: if the file can't be opened or parsed. + #[staticmethod] + #[pyo3(signature = (path, options=None))] + fn from_file( + path: &str, + options: Option, + ) -> crate::pyo3::PyResult<(MjcfRobot, MjcfModel)> { + let opts = options.map(|o| o.to_rapier()).unwrap_or_default(); + let (robot, model) = rapier3d_mjcf::MjcfRobot::from_file(path, opts) + .map_err(|e| crate::errors::MjcfError::new_err(format!("{e}")))?; + Ok((MjcfRobot { inner: Some(robot) }, MjcfModel { raw: model })) + } + + /// Parse an MJCF XML string and return ``(MjcfRobot, MjcfModel)``. + /// + /// ```` directives resolve relative to ``base_dir`` + /// (default: the current working directory). + /// + /// :param xml: Full MJCF XML document. + /// :param options: Optional :class:`MjcfLoaderOptions`. + /// :param base_dir: Directory used to resolve includes / mesh refs. + /// :returns: ``(MjcfRobot, MjcfModel)`` tuple. + /// :raises MjcfError: if the XML can't be parsed. + #[staticmethod] + #[pyo3(signature = (xml, options=None, base_dir=None))] + fn from_str( + xml: &str, + options: Option, + base_dir: Option<&str>, + ) -> crate::pyo3::PyResult<(MjcfRobot, MjcfModel)> { + let opts = options.map(|o| o.to_rapier()).unwrap_or_default(); + let base_dir = base_dir.unwrap_or("."); + let (robot, model) = rapier3d_mjcf::MjcfRobot::from_str(xml, opts, base_dir) + .map_err(|e| crate::errors::MjcfError::new_err(format!("{e}")))?; + Ok((MjcfRobot { inner: Some(robot) }, MjcfModel { raw: model })) + } + + /// Prepend ``transform`` to the robot's root poses. + /// + /// Repositions the whole robot before insertion (e.g. to place a + /// second copy beside the first). Must be called before the robot + /// is consumed by an ``insert_*`` call. + /// + /// :param transform: world-space :class:`Isometry3` to apply. + /// :raises MjcfError: if this robot has already been consumed. + fn append_transform(&mut self, transform: Isometry3) -> crate::pyo3::PyResult<()> { + let robot = self + .inner + .as_mut() + .ok_or_else(|| crate::errors::MjcfError::new_err("MjcfRobot was already consumed"))?; + let pose: rapier::math::Pose = transform.0.into(); + robot.append_transform(&pose); + Ok(()) + } + + /// Insert the model into the world using *impulse joints*. + /// + /// This call **consumes** the :class:`MjcfRobot`. + /// + /// :param bodies: :class:`RigidBodySet` to insert into. + /// :param colliders: :class:`ColliderSet` to insert into. + /// :param impulse_joints: :class:`ImpulseJointSet` to insert into. + /// :returns: :class:`MjcfRobotHandles` with all created handles. + /// :raises MjcfError: if this robot has already been consumed. + fn insert_using_impulse_joints( + &mut self, + py: crate::pyo3::Python<'_>, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + ) -> crate::pyo3::PyResult> { + use crate::pyo3::IntoPy; + let robot = self + .inner + .take() + .ok_or_else(|| crate::errors::MjcfError::new_err("MjcfRobot was already consumed"))?; + let handles = robot.insert_using_impulse_joints( + &mut bodies.0, + &mut colliders.0, + &mut impulse_joints.0, + ); + { + let handles = handles; + let conv = |h| ImpulseJointHandle(h).into_py(py); + let bodies: Vec> = handles + .bodies + .into_iter() + .map(|b| { + b.map(|b| { + let cols = b + .colliders + .into_iter() + .map(|c| MjcfColliderHandle { + handle: ColliderHandle(c.handle), + }) + .collect(); + MjcfBodyHandle { + body: RigidBodyHandle(b.body), + colliders: cols, + } + }) + }) + .collect(); + let joints: Vec> = handles + .joints + .into_iter() + .map(|jh| { + crate::pyo3::Py::new( + py, + MjcfJointHandle { + joint: conv(jh.joint), + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc MjcfJointHandle") + }) + .collect(); + let equality_joints: Vec> = handles + .equality_joints + .into_iter() + .map(|jh| { + use crate::pyo3::IntoPy; + crate::pyo3::Py::new( + py, + MjcfJointHandle { + joint: ImpulseJointHandle(jh.joint).into_py(py), + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc MjcfJointHandle") + }) + .collect(); + crate::pyo3::Py::new( + py, + MjcfRobotHandles { + bodies, + joints, + equality_joints, + }, + ) + } + } + + /// Insert the model into the world using *multibody joints*. + /// + /// ```` loop-closure constraints are still inserted as + /// impulse joints (rapier multibodies are tree-structured), so an + /// :class:`ImpulseJointSet` is required as well. + /// + /// This call **consumes** the :class:`MjcfRobot`. + /// + /// :param bodies: :class:`RigidBodySet` to insert into. + /// :param colliders: :class:`ColliderSet` to insert into. + /// :param multibody_joints: :class:`MultibodyJointSet` to insert into. + /// :param impulse_joints: :class:`ImpulseJointSet` for equalities. + /// :param options: Optional :class:`MjcfMultibodyOptions` flags. + /// :returns: :class:`MjcfRobotHandles` with all created handles. + /// :raises MjcfError: if this robot has already been consumed. + #[pyo3(signature = (bodies, colliders, multibody_joints, impulse_joints, options=None))] + fn insert_using_multibody_joints( + &mut self, + py: crate::pyo3::Python<'_>, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + multibody_joints: &mut MultibodyJointSet, + impulse_joints: &mut ImpulseJointSet, + options: Option, + ) -> crate::pyo3::PyResult> { + use crate::pyo3::IntoPy; + let robot = self + .inner + .take() + .ok_or_else(|| crate::errors::MjcfError::new_err("MjcfRobot was already consumed"))?; + let opts = options.map(|o| o.0).unwrap_or_default(); + let handles = robot.insert_using_multibody_joints( + &mut bodies.0, + &mut colliders.0, + &mut multibody_joints.0, + &mut impulse_joints.0, + opts, + ); + { + let handles = handles; + let conv = |h: Option<_>| h.map(MultibodyJointHandle).into_py(py); + let bodies: Vec> = handles + .bodies + .into_iter() + .map(|b| { + b.map(|b| { + let cols = b + .colliders + .into_iter() + .map(|c| MjcfColliderHandle { + handle: ColliderHandle(c.handle), + }) + .collect(); + MjcfBodyHandle { + body: RigidBodyHandle(b.body), + colliders: cols, + } + }) + }) + .collect(); + let joints: Vec> = handles + .joints + .into_iter() + .map(|jh| { + crate::pyo3::Py::new( + py, + MjcfJointHandle { + joint: conv(jh.joint), + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc MjcfJointHandle") + }) + .collect(); + let equality_joints: Vec> = handles + .equality_joints + .into_iter() + .map(|jh| { + use crate::pyo3::IntoPy; + crate::pyo3::Py::new( + py, + MjcfJointHandle { + joint: ImpulseJointHandle(jh.joint).into_py(py), + link1: RigidBodyHandle(jh.link1), + link2: RigidBodyHandle(jh.link2), + }, + ) + .expect("alloc MjcfJointHandle") + }) + .collect(); + crate::pyo3::Py::new( + py, + MjcfRobotHandles { + bodies, + joints, + equality_joints, + }, + ) + } + } + + /// Return the ``MjcfRobot(...)`` repr (or ``"MjcfRobot(consumed)"``). + fn __repr__(&self) -> String { + match &self.inner { + Some(r) => format!( + "MjcfRobot(n_bodies={}, n_joints={})", + r.bodies.len(), + r.joints.len() + ), + None => "MjcfRobot(consumed)".to_string(), + } + } +} + +/// Register the MJCF loader `#[pyclass]`-es into `m`. +pub fn register_loaders_mjcf( + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +// ============================================================== +// Registration +// ============================================================== + +pub fn register_loaders( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + // Mesh loader + m.add_class::()?; + m.add_function(crate::pyo3::wrap_pyfunction!( + loaders_mesh_load_from_path, + m + )?)?; + m.add_function(crate::pyo3::wrap_pyfunction!( + loaders_mesh_load_from_raw_mesh, + m + )?)?; + // URDF + MJCF loaders. + register_loaders_urdf(m)?; + register_loaders_mjcf(m)?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/math.rs b/python/rapier-py-3d/src/math.rs index cb6f8fd49..fadc8a142 100644 --- a/python/rapier-py-3d/src/math.rs +++ b/python/rapier-py-3d/src/math.rs @@ -41,7 +41,7 @@ impl ReprPrecision for f32 { } } -use crate::numpy::{PyArray1, PyArrayMethods}; +use crate::numpy::PyArray1; use crate::pyo3::exceptions::{PyTypeError, PyValueError}; use crate::pyo3::prelude::*; use crate::pyo3::pyclass::CompareOp; diff --git a/python/rapier-py-3d/src/pipeline.rs b/python/rapier-py-3d/src/pipeline.rs new file mode 100644 index 000000000..877ba376c --- /dev/null +++ b/python/rapier-py-3d/src/pipeline.rs @@ -0,0 +1,3255 @@ +//! The concrete 3D / `f32` pipeline and scene-query `#[pyclass]` types +//! (`FeatureId`, `Ray`, `QueryPipeline`, `PhysicsPipeline`, `PhysicsWorld`, …), +//! their helpers, and the `register_pipeline` function used by `#[pymodule]`. +//! +//! This module used to be generated by a `(Real, DIM)`-parameterized macro so +//! that several cdylibs could each expand their own concrete types. There is +//! now a single concrete target — 3D, `f32` — so the types are written out +//! directly, with no macros. + +use crate::*; +use rapier3d as rapier; + +use crate::pyo3::exceptions::PyTypeError; +use crate::pyo3::prelude::*; +use crate::pyo3::pyclass::CompareOp; +use crate::pyo3::types::PyList as _PyList; + +// ===================================================================== +// FeatureId — tagged-union over parry's `FeatureId` (Vertex/Edge/Face/Unknown). +// ===================================================================== + +/// Tagged-union identifier for a shape feature returned by a query. +/// +/// Wraps parry's ``FeatureId``: one of ``Vertex(id)``, ``Edge(id)`` +/// (3D only), ``Face(id)``, or ``Unknown``. The :attr:`kind` field +/// holds the variant name (``"vertex"``, ``"edge"``, ``"face"``, +/// ``"unknown"``); :attr:`id` holds the numeric index (``0`` for +/// ``Unknown``). +/// +/// Construct via the static factories :meth:`Vertex`, :meth:`Edge`, +/// :meth:`Face`, :meth:`Unknown`. +#[pyclass(name = "FeatureId", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct FeatureId { + /// `"vertex"`, `"edge"` (3D only), `"face"`, or `"unknown"`. + #[pyo3(get)] + pub kind: &'static str, + /// 0 when `kind == "unknown"`, otherwise the parry id. + #[pyo3(get)] + pub id: u32, +} + +#[pymethods] +impl FeatureId { + /// Construct a ``Vertex(id)`` feature identifier. + #[staticmethod] + #[pyo3(name = "Vertex")] + fn vertex(id: u32) -> Self { + Self { kind: "vertex", id } + } + /// Construct a ``Face(id)`` feature identifier. + #[staticmethod] + #[pyo3(name = "Face")] + fn face(id: u32) -> Self { + Self { kind: "face", id } + } + /// Construct an ``Edge(id)`` feature identifier. + /// + /// Edges are only returned for 3D queries; the constructor is + /// exposed on both dimensions for portability. + // FeatureId::Edge: 3D only in parry, but we expose it on both + // dims as a generic constructor for portability. + #[staticmethod] + #[pyo3(name = "Edge")] + fn edge(id: u32) -> Self { + Self { kind: "edge", id } + } + /// Construct the ``Unknown`` feature identifier. + #[staticmethod] + #[pyo3(name = "Unknown")] + fn unknown() -> Self { + Self { + kind: "unknown", + id: 0, + } + } + /// ``True`` iff this is a ``Vertex(_)``. + #[getter] + fn is_vertex(&self) -> bool { + self.kind == "vertex" + } + /// ``True`` iff this is an ``Edge(_)``. + #[getter] + fn is_edge(&self) -> bool { + self.kind == "edge" + } + /// ``True`` iff this is a ``Face(_)``. + #[getter] + fn is_face(&self) -> bool { + self.kind == "face" + } + /// ``True`` iff this is ``Unknown``. + #[getter] + fn is_unknown(&self) -> bool { + self.kind == "unknown" + } + /// Human-readable ``FeatureId.Variant(id)`` repr. + fn __repr__(&self) -> String { + if self.kind == "unknown" { + "FeatureId.Unknown".to_string() + } else { + format!( + "FeatureId.{}({})", + match self.kind { + "vertex" => "Vertex", + "edge" => "Edge", + "face" => "Face", + _ => "Unknown", + }, + self.id + ) + } + } + /// Equality on (kind, id); other comparisons raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.kind == other.kind && self.id == other.id), + CompareOp::Ne => Ok(!(self.kind == other.kind && self.id == other.id)), + _ => Err(PyTypeError::new_err("FeatureId supports only == and !=")), + } + } +} + +impl FeatureId { + #[allow(dead_code)] + #[inline] + pub(crate) fn from_parry(f: rapier::parry::shape::FeatureId) -> Self { + { + use rapier::parry::shape::FeatureId as F; + match f { + F::Vertex(i) => Self { + kind: "vertex", + id: i, + }, + F::Edge(i) => Self { + kind: "edge", + id: i, + }, + F::Face(i) => Self { + kind: "face", + id: i, + }, + F::Unknown => Self { + kind: "unknown", + id: 0, + }, + } + } + } +} + +// ===================================================================== +// Ray +// ===================================================================== + +/// A half-line defined by an :attr:`origin` point and a :attr:`dir` +/// vector. +/// +/// Passed to the various ``cast_ray*`` / ``intersect_ray`` queries. +/// The direction need not be normalized — query time-of-impact (TOI) +/// values are expressed in units of ``dir``. +#[pyclass(name = "Ray", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct Ray(pub rapier::parry::query::Ray); + +#[pymethods] +impl Ray { + /// Construct a ray from an origin point and direction vector. + /// + /// :param origin: World-space origin point. + /// :param dir: World-space direction (does not need to be unit). + #[new] + fn new(origin: PyPoint, dir: PyVector) -> Self { + let o: rapier::math::Vector = origin.0.coords.into(); + let d: rapier::math::Vector = dir.0.into(); + Self(rapier::parry::query::Ray::new(o, d)) + } + /// World-space origin point. + #[getter] + fn origin(&self) -> Point3 { + let v: crate::na::SVector = self.0.origin.into(); + Point3(crate::na::Point::from(v)) + } + /// World-space direction vector. + #[getter] + fn dir(&self) -> Vec3 { + let v: crate::na::SVector = self.0.dir.into(); + Vec3(v) + } + /// Evaluate the ray's position at parameter ``t``. + /// + /// Computes ``origin + dir * t``. When ``dir`` is unit-length, ``t`` + /// is the Euclidean distance from the origin. + /// + /// :param t: Ray parameter (typically the TOI returned by a query). + /// :returns: World-space point on the ray. + fn point_at(&self, t: Real) -> Point3 { + let p: rapier::math::Vector = self.0.origin + self.0.dir * t; + let v: crate::na::SVector = p.into(); + Point3(crate::na::Point::from(v)) + } + /// Debug repr — shows the origin and direction. + fn __repr__(&self) -> String { + format!("Ray(origin={:?}, dir={:?})", self.0.origin, self.0.dir) + } +} + +// ===================================================================== +// RayIntersection +// ===================================================================== + +/// Result of a ray-vs-shape intersection query. +/// +/// :attr:`toi` is the ray parameter at the hit (see :meth:`Ray.point_at`). +/// :attr:`normal` is the world-space surface normal pointing away from +/// the hit collider. :attr:`feature` identifies the shape feature +/// (vertex / edge / face) that was hit, when known. +#[pyclass(name = "RayIntersection", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct RayIntersection { + /// Time of impact — ray parameter at the hit. + #[pyo3(get)] + pub toi: Real, + /// Surface normal at the hit, pointing away from the collider. + #[pyo3(get)] + pub normal: Vec3, + /// Shape feature that was hit. + #[pyo3(get)] + pub feature: FeatureId, +} + +#[pymethods] +impl RayIntersection { + /// Alias for :attr:`toi` matching parry's naming. + #[getter] + fn time_of_impact(&self) -> Real { + self.toi + } + /// Debug repr — shows TOI and normal. + fn __repr__(&self) -> String { + format!( + "RayIntersection(toi={}, normal={:?})", + self.toi, self.normal.0 + ) + } +} + +impl RayIntersection { + #[allow(dead_code)] + pub(crate) fn from_parry(r: rapier::parry::query::RayIntersection) -> Self { + let n: crate::na::SVector = r.normal.into(); + Self { + toi: r.time_of_impact, + normal: Vec3(n), + feature: FeatureId::from_parry(r.feature), + } + } +} + +// ===================================================================== +// PointProjection +// ===================================================================== + +/// Result of a project-point query. +/// +/// :attr:`point` is the world-space closest point on (or inside) the +/// collider's shape; :attr:`is_inside` is ``True`` iff the query +/// point lies inside the (solid) shape — in which case the projected +/// point may coincide with the query point. +#[pyclass(name = "PointProjection", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct PointProjection { + /// ``True`` iff the query point lies inside the shape. + #[pyo3(get)] + pub is_inside: bool, + /// World-space closest point on (or inside) the shape. + #[pyo3(get)] + pub point: Point3, +} + +#[pymethods] +impl PointProjection { + /// Debug repr — shows the projected point and ``is_inside`` flag. + fn __repr__(&self) -> String { + format!( + "PointProjection(is_inside={}, point={:?})", + self.is_inside, self.point.0 + ) + } +} + +impl PointProjection { + #[allow(dead_code)] + pub(crate) fn from_parry(p: rapier::parry::query::PointProjection) -> Self { + // parry's PointProjection.point is a glam Vector, not a Point. + let v: crate::na::SVector = p.point.into(); + Self { + is_inside: p.is_inside, + point: Point3(crate::na::Point::from(v)), + } + } +} + +// ===================================================================== +// ShapeCastStatus +// ===================================================================== + +/// Outcome status of a shape-cast query. +/// +/// - ``OUT_OF_ITERATIONS``: solver hit the iteration cap before +/// converging; the returned TOI is approximate. +/// - ``CONVERGED``: the solver converged on a TOI within tolerance. +/// - ``FAILED``: the solver failed (degenerate geometry or numerical +/// issue); the result should be ignored. +/// - ``PENETRATING_OR_WITHIN_TARGET_DIST``: the shapes are already in +/// contact (or within ``target_distance``) at ``t = 0``. +#[pyclass(name = "ShapeCastStatus", module = "rapier", eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShapeCastStatus { + OUT_OF_ITERATIONS, + CONVERGED, + FAILED, + PENETRATING_OR_WITHIN_TARGET_DIST, +} + +impl ShapeCastStatus { + #[allow(dead_code)] + fn from_parry(s: rapier::parry::query::ShapeCastStatus) -> Self { + use rapier::parry::query::ShapeCastStatus as S; + match s { + S::OutOfIterations => Self::OUT_OF_ITERATIONS, + S::Converged => Self::CONVERGED, + S::Failed => Self::FAILED, + S::PenetratingOrWithinTargetDist => Self::PENETRATING_OR_WITHIN_TARGET_DIST, + } + } +} + +// ===================================================================== +// ShapeCastOptions +// ===================================================================== + +/// Tuning parameters for shape-cast queries. +/// +/// Fields: +/// +/// - :attr:`max_time_of_impact`: upper bound on the returned TOI; +/// defaults to ``+inf``. +/// - :attr:`target_distance`: shapes are considered "in contact" +/// when their closest distance is within this margin (useful for +/// character-controller-style queries that want to stop just shy +/// of penetration). +/// - :attr:`stop_at_penetration`: when ``True`` (default), the query +/// reports a hit with ``t = 0`` if the shapes already overlap; when +/// ``False``, the cast continues past the initial penetration. +/// - :attr:`compute_impact_geometry_on_penetration`: when ``True`` +/// (default), compute witness points / normals even when the +/// ``PENETRATING_OR_WITHIN_TARGET_DIST`` status is returned. +#[pyclass(name = "ShapeCastOptions", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct ShapeCastOptions(pub rapier::parry::query::ShapeCastOptions); + +#[pymethods] +impl ShapeCastOptions { + /// Construct a ``ShapeCastOptions`` from explicit field values. + /// + /// :param max_time_of_impact: Upper bound on the TOI. + /// :param target_distance: Tolerance for "in contact". + /// :param stop_at_penetration: Whether to report ``t = 0`` hits. + /// :param compute_impact_geometry_on_penetration: Whether to + /// compute witness geometry on penetration. + #[new] + #[pyo3(signature = ( + max_time_of_impact=Real::MAX, + target_distance=0.0 as Real, + stop_at_penetration=true, + compute_impact_geometry_on_penetration=true, + ))] + fn new( + max_time_of_impact: Real, + target_distance: Real, + stop_at_penetration: bool, + compute_impact_geometry_on_penetration: bool, + ) -> Self { + Self(rapier::parry::query::ShapeCastOptions { + max_time_of_impact, + target_distance, + stop_at_penetration, + compute_impact_geometry_on_penetration, + }) + } + /// Shortcut constructor — defaults plus an explicit TOI bound. + /// + /// :param t: ``max_time_of_impact`` value. + #[staticmethod] + fn with_max_time_of_impact(t: Real) -> Self { + Self(rapier::parry::query::ShapeCastOptions::with_max_time_of_impact(t)) + } + /// Upper bound on the returned time of impact. + #[getter] + fn max_time_of_impact(&self) -> Real { + self.0.max_time_of_impact + } + /// Set :attr:`max_time_of_impact`. + #[setter] + fn set_max_time_of_impact(&mut self, v: Real) { + self.0.max_time_of_impact = v; + } + /// Distance below which the shapes are considered to be in contact. + #[getter] + fn target_distance(&self) -> Real { + self.0.target_distance + } + /// Set :attr:`target_distance`. + #[setter] + fn set_target_distance(&mut self, v: Real) { + self.0.target_distance = v; + } + /// Whether the cast reports ``t = 0`` hits when shapes overlap at the start. + #[getter] + fn stop_at_penetration(&self) -> bool { + self.0.stop_at_penetration + } + /// Set :attr:`stop_at_penetration`. + #[setter] + fn set_stop_at_penetration(&mut self, v: bool) { + self.0.stop_at_penetration = v; + } + /// Whether witness points / normals are computed on penetration. + #[getter] + fn compute_impact_geometry_on_penetration(&self) -> bool { + self.0.compute_impact_geometry_on_penetration + } + /// Set :attr:`compute_impact_geometry_on_penetration`. + #[setter] + fn set_compute_impact_geometry_on_penetration(&mut self, v: bool) { + self.0.compute_impact_geometry_on_penetration = v; + } + /// Debug repr — shows the main TOI and target-distance settings. + fn __repr__(&self) -> String { + format!( + "ShapeCastOptions(max_time_of_impact={}, target_distance={})", + self.0.max_time_of_impact, self.0.target_distance + ) + } +} + +// ===================================================================== +// ShapeCastHit +// ===================================================================== + +/// Result of a shape-cast query. +/// +/// :attr:`time_of_impact` is the parametric TOI along the cast +/// motion. :attr:`witness1` / :attr:`witness2` are the world-space +/// closest points on the cast shape and the hit collider at the time +/// of impact; :attr:`normal1` / :attr:`normal2` are the corresponding +/// outward surface normals. :attr:`status` is a :class:`ShapeCastStatus` +/// describing how the cast terminated. +#[pyclass(name = "ShapeCastHit", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy)] +pub struct ShapeCastHit { + /// Parametric TOI along the cast motion. + #[pyo3(get)] + pub time_of_impact: Real, + /// World-space witness point on the cast shape at impact. + #[pyo3(get)] + pub witness1: Point3, + /// World-space witness point on the hit collider at impact. + #[pyo3(get)] + pub witness2: Point3, + /// Outward surface normal on the cast shape at impact. + #[pyo3(get)] + pub normal1: Vec3, + /// Outward surface normal on the hit collider at impact. + #[pyo3(get)] + pub normal2: Vec3, + /// Termination status of the cast (see :class:`ShapeCastStatus`). + #[pyo3(get)] + pub status: ShapeCastStatus, +} + +#[pymethods] +impl ShapeCastHit { + /// Debug repr — shows the TOI and status. + fn __repr__(&self) -> String { + format!( + "ShapeCastHit(toi={}, status={:?})", + self.time_of_impact, self.status + ) + } +} + +impl ShapeCastHit { + #[allow(dead_code)] + pub(crate) fn from_parry(h: rapier::parry::query::ShapeCastHit) -> Self { + let w1: crate::na::SVector = h.witness1.into(); + let w2: crate::na::SVector = h.witness2.into(); + let n1: crate::na::SVector = h.normal1.into(); + let n2: crate::na::SVector = h.normal2.into(); + Self { + time_of_impact: h.time_of_impact, + witness1: Point3(crate::na::Point::from(w1)), + witness2: Point3(crate::na::Point::from(w2)), + normal1: Vec3(n1), + normal2: Vec3(n2), + status: ShapeCastStatus::from_parry(h.status), + } + } +} + +// ===================================================================== +// NonlinearRigidMotion +// ===================================================================== + +/// Curved rigid-body motion used by nonlinear shape casts. +/// +/// Describes a constant-velocity screw motion around a body's +/// :attr:`local_center`, starting from an initial pose :attr:`start` +/// with linear velocity :attr:`linvel` and angular velocity +/// :attr:`angvel`. Passed to +/// :meth:`QueryPipeline.cast_shape_nonlinear` to capture rotational +/// effects (e.g. swinging characters) that a straight-line +/// :meth:`QueryPipeline.cast_shape` would miss. +#[pyclass(name = "NonlinearRigidMotion", module = "rapier")] +#[derive(Debug, Clone, Copy)] +pub struct NonlinearRigidMotion(pub rapier::parry::query::NonlinearRigidMotion); + +#[pymethods] +impl NonlinearRigidMotion { + /// Construct a nonlinear motion from its constituent fields. + /// + /// :param start: Initial pose; defaults to identity. + /// :param local_center: Local-space pivot point; defaults to origin. + /// :param linvel: Linear velocity; defaults to zero. + /// :param angvel: Angular velocity (3D: ``Vec3``, 2D: scalar); + /// defaults to zero. + #[new] + #[pyo3(signature = (start=None, local_center=None, linvel=None, angvel=None))] + fn new( + start: Option, + local_center: Option, + linvel: Option, + angvel: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let s: rapier::math::Pose = start + .map(|i| i.0.into()) + .unwrap_or_else(rapier::math::Pose::identity); + let lc_vec: rapier::math::Vector = local_center + .map(|p| p.0.coords.into()) + .unwrap_or(rapier::math::Vector::ZERO); + let lv: rapier::math::Vector = linvel + .map(|v| v.0.into()) + .unwrap_or(rapier::math::Vector::ZERO); + let av = { + let av: rapier::math::Vector = match angvel { + None => rapier::math::Vector::ZERO, + Some(v) => { + let pv: PyVector = v.extract()?; + pv.0.into() + } + }; + Ok::(av) + }?; + Ok(Self(rapier::parry::query::NonlinearRigidMotion { + start: s, + local_center: lc_vec, + linvel: lv, + angvel: av, + })) + } + /// Identity motion — no rotation, no translation, identity start. + #[staticmethod] + fn identity() -> Self { + Self(rapier::parry::query::NonlinearRigidMotion::identity()) + } + /// Stationary motion held at ``pos`` for all times. + /// + /// :param pos: Pose to hold. + #[staticmethod] + fn constant_position(pos: PyIsometry) -> Self { + let p: rapier::math::Pose = pos.0.into(); + Self(rapier::parry::query::NonlinearRigidMotion::constant_position(p)) + } + /// Initial pose at ``t = 0``. + #[getter] + fn start(&self) -> Isometry3 { + let iso: crate::na::Isometry = self.0.start.into(); + Isometry3(iso) + } + /// Local-space pivot the angular velocity rotates around. + #[getter] + fn local_center(&self) -> Point3 { + let v: crate::na::SVector = self.0.local_center.into(); + Point3(crate::na::Point::from(v)) + } + /// Linear velocity (world-space, units per unit time). + #[getter] + fn linvel(&self) -> Vec3 { + let v: crate::na::SVector = self.0.linvel.into(); + Vec3(v) + } + /// Evaluate the motion at time ``t``. + /// + /// :param t: Time parameter (``0`` returns :attr:`start`). + /// :returns: Pose at time ``t``. + fn position_at_time(&self, t: Real) -> Isometry3 { + let p: crate::na::Isometry = self.0.position_at_time(t).into(); + Isometry3(p) + } +} + +// Dim-specific `angvel` getter — emitted in a separate pymethods block +// (multiple-pymethods feature) because the angvel type differs. +#[pymethods] +impl NonlinearRigidMotion { + /// Angular velocity (3D: ``Vec3``, world-space, radians per unit time). + #[getter] + fn angvel(&self) -> Vec3 { + let v: crate::na::Vector3 = self.0.angvel.into(); + Vec3(v) + } +} + +// ===================================================================== +// QueryFilterFlags +// ===================================================================== + +/// Bitflags controlling which colliders / bodies a scene query touches. +/// +/// Combine flags with ``|``. Common preset combinations are also +/// exposed as named static constructors on :class:`QueryFilter` +/// (e.g. ``QueryFilter.only_dynamic()``). +#[pyclass(name = "QueryFilterFlags", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct QueryFilterFlags(pub rapier::pipeline::QueryFilterFlags); + +#[pymethods] +impl QueryFilterFlags { + /// Construct from a raw bit pattern (unknown bits are truncated). + /// + /// :param bits: Raw bit pattern. + #[new] + #[pyo3(signature = (bits = 0))] + fn new(bits: u32) -> Self { + Self(rapier::pipeline::QueryFilterFlags::from_bits_truncate(bits)) + } + /// Return an empty flag set (no filtering). + #[staticmethod] + fn empty() -> Self { + Self(rapier::pipeline::QueryFilterFlags::empty()) + } + + /// Skip colliders attached to fixed (static) bodies. + #[classattr] + const EXCLUDE_FIXED: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_FIXED); + /// Skip colliders attached to kinematic bodies. + #[classattr] + const EXCLUDE_KINEMATIC: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_KINEMATIC); + /// Skip colliders attached to dynamic bodies. + #[classattr] + const EXCLUDE_DYNAMIC: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_DYNAMIC); + /// Skip sensor colliders. + #[classattr] + const EXCLUDE_SENSORS: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_SENSORS); + /// Skip non-sensor (solid) colliders. + #[classattr] + const EXCLUDE_SOLIDS: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_SOLIDS); + /// Keep only colliders attached to dynamic bodies. + #[classattr] + const ONLY_DYNAMIC: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_DYNAMIC); + /// Keep only colliders attached to kinematic bodies. + #[classattr] + const ONLY_KINEMATIC: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_KINEMATIC); + /// Keep only colliders attached to fixed (static) bodies. + #[classattr] + const ONLY_FIXED: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_FIXED); + /// Reserved — currently maps to an empty set (forward-compat). + // `EXCLUDE_COLLISION_INVALID` doesn't exist upstream; we map it to + // an empty flag set for forward-compat. + #[classattr] + const EXCLUDE_COLLISION_INVALID: QueryFilterFlags = + QueryFilterFlags(rapier::pipeline::QueryFilterFlags::empty()); + + /// Raw underlying bit pattern. + #[getter] + fn bits(&self) -> u32 { + self.0.bits() + } + /// ``True`` if every bit set in ``other`` is also set here. + fn contains(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// ``True`` if no flag is set. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// ``other in self`` — alias for :meth:`contains`. + fn __contains__(&self, other: &Self) -> bool { + self.0.contains(other.0) + } + /// Bitwise OR — union of flag sets. + fn __or__(&self, other: &Self) -> Self { + Self(self.0 | other.0) + } + /// Bitwise AND — intersection of flag sets. + fn __and__(&self, other: &Self) -> Self { + Self(self.0 & other.0) + } + /// Bitwise XOR — symmetric difference. + fn __xor__(&self, other: &Self) -> Self { + Self(self.0 ^ other.0) + } + /// Bitwise NOT — flip every known flag. + fn __invert__(&self) -> Self { + Self(!self.0) + } + /// Set difference — remove flags present in ``other``. + fn __sub__(&self, other: &Self) -> Self { + Self(self.0 - other.0) + } + /// Truthy iff at least one flag is set. + fn __bool__(&self) -> bool { + !self.0.is_empty() + } + /// Equality on the raw bit pattern; other comparisons raise. + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.0 == other.0), + CompareOp::Ne => Ok(self.0 != other.0), + _ => Err(PyTypeError::new_err( + "QueryFilterFlags supports only == and !=", + )), + } + } + /// Hash on the raw bit pattern (makes the value set-safe). + fn __hash__(&self) -> u64 { + self.0.bits() as u64 + } + /// Debug repr — shows the raw bit pattern in binary. + fn __repr__(&self) -> String { + format!("QueryFilterFlags(bits={:#010b})", self.0.bits()) + } +} + +// ===================================================================== +// QueryFilter — Python-side mutable builder. +// +// Storing a `rapier::pipeline::QueryFilter<'_>` directly is awkward +// because the upstream type carries a lifetime for the predicate +// borrow. We instead keep the constituent fields and reconstruct a +// borrowed `QueryFilter` inside each query call via `with_filter`. +// ===================================================================== + +/// Filter passed to scene queries to narrow the set of inspected colliders. +/// +/// A ``QueryFilter`` composes: +/// +/// - :class:`QueryFilterFlags` (membership: include/exclude +/// dynamic / kinematic / fixed / sensor / solid). +/// - :class:`InteractionGroups` (group-mask membership / filter bits). +/// - explicit exclusion of a single :class:`ColliderHandle` and/or a +/// single :class:`RigidBodyHandle`. +/// - an optional Python predicate ``f(handle, collider) -> bool`` +/// invoked per-collider to make the final keep/skip decision. +/// +/// All builder methods (``exclude_sensors``, ``groups``, +/// ``exclude_collider``, ``predicate``, ...) return a new +/// :class:`QueryFilter` rather than mutating in place, so they can be +/// chained. +#[pyclass(name = "QueryFilter", module = "rapier")] +pub struct QueryFilter { + pub flags: rapier::pipeline::QueryFilterFlags, + pub groups: Option, + pub exclude_collider: Option, + pub exclude_rigid_body: Option, + pub predicate: Option>, +} + +impl QueryFilter { + /// Deep-clone (the predicate `Py` requires a `Python<'_>` token). + fn clone_with(&self, py: Python<'_>) -> Self { + Self { + flags: self.flags, + groups: self.groups, + exclude_collider: self.exclude_collider, + exclude_rigid_body: self.exclude_rigid_body, + predicate: self.predicate.as_ref().map(|p| p.clone_ref(py)), + } + } +} + +impl QueryFilter { + /// Build a borrowed `QueryFilter<'a>` for upstream calls. + /// + /// `pred` is the predicate closure produced by the caller, kept + /// alive on the stack for the duration of one query call. + pub fn as_rapier<'a>( + &self, + pred: Option< + &'a dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + >, + ) -> rapier::pipeline::QueryFilter<'a> { + let mut qf = rapier::pipeline::QueryFilter { + flags: self.flags, + groups: self.groups, + exclude_collider: self.exclude_collider, + exclude_rigid_body: self.exclude_rigid_body, + predicate: None, + }; + if let Some(p) = pred { + qf.predicate = Some(p); + } + qf + } +} + +#[pymethods] +impl QueryFilter { + /// Construct a ``QueryFilter`` from explicit field values. + /// + /// :param flags: :class:`QueryFilterFlags` (defaults to empty). + /// :param groups: :class:`InteractionGroups` membership/filter. + /// :param exclude_collider: Single collider to skip. + /// :param exclude_rigid_body: Skip every collider attached to + /// this rigid body. + /// :param predicate: Optional ``f(ColliderHandle, Collider) -> bool`` + /// per-collider predicate; returning ``False`` skips the + /// collider. + #[new] + #[pyo3(signature = (flags=None, groups=None, exclude_collider=None, exclude_rigid_body=None, predicate=None))] + fn py_new( + flags: Option<&QueryFilterFlags>, + groups: Option<&InteractionGroups>, + exclude_collider: Option<&ColliderHandle>, + exclude_rigid_body: Option<&RigidBodyHandle>, + predicate: Option>, + ) -> Self { + Self { + flags: flags + .map(|f| f.0) + .unwrap_or_else(rapier::pipeline::QueryFilterFlags::empty), + groups: groups.map(|g| g.0), + exclude_collider: exclude_collider.map(|h| h.0), + exclude_rigid_body: exclude_rigid_body.map(|h| h.0), + predicate, + } + } + + /// Return an empty filter (no flags, no groups, no exclusions). + #[staticmethod] + fn new() -> Self { + Self::default_empty() + } + + /// Return a filter that excludes colliders attached to fixed bodies. + #[staticmethod] + fn exclude_fixed() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_FIXED; + me + } + /// Return a filter that excludes colliders attached to kinematic bodies. + #[staticmethod] + fn exclude_kinematic() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_KINEMATIC; + me + } + /// Return a filter that excludes colliders attached to dynamic bodies. + #[staticmethod] + fn exclude_dynamic() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_DYNAMIC; + me + } + /// Return a filter keeping only colliders attached to dynamic bodies. + #[staticmethod] + fn only_dynamic() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::ONLY_DYNAMIC; + me + } + /// Return a filter keeping only colliders attached to kinematic bodies. + #[staticmethod] + fn only_kinematic() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::ONLY_KINEMATIC; + me + } + /// Return a filter keeping only colliders attached to fixed bodies. + #[staticmethod] + fn only_fixed() -> Self { + let mut me = Self::default_empty(); + me.flags = rapier::pipeline::QueryFilterFlags::ONLY_FIXED; + me + } + + /// Builder: also exclude sensor colliders. + /// + /// :returns: A new :class:`QueryFilter` with ``EXCLUDE_SENSORS`` + /// added. + fn exclude_sensors(&self, py: Python<'_>) -> Self { + let mut me = self.clone_with(py); + me.flags |= rapier::pipeline::QueryFilterFlags::EXCLUDE_SENSORS; + me + } + /// Builder: also exclude non-sensor (solid) colliders. + /// + /// :returns: A new :class:`QueryFilter` with ``EXCLUDE_SOLIDS`` + /// added. + fn exclude_solids(&self, py: Python<'_>) -> Self { + let mut me = self.clone_with(py); + me.flags |= rapier::pipeline::QueryFilterFlags::EXCLUDE_SOLIDS; + me + } + /// Builder: restrict to colliders whose interaction groups overlap ``groups``. + /// + /// :param groups: Membership / filter masks. + /// :returns: A new :class:`QueryFilter`. + fn groups(&self, py: Python<'_>, groups: &InteractionGroups) -> Self { + let mut me = self.clone_with(py); + me.groups = Some(groups.0); + me + } + /// Builder: skip this specific collider. + /// + /// :param h: Collider handle to exclude. + /// :returns: A new :class:`QueryFilter`. + fn exclude_collider(&self, py: Python<'_>, h: &ColliderHandle) -> Self { + let mut me = self.clone_with(py); + me.exclude_collider = Some(h.0); + me + } + /// Builder: skip every collider attached to this body. + /// + /// :param h: Rigid-body handle whose colliders to exclude. + /// :returns: A new :class:`QueryFilter`. + fn exclude_rigid_body(&self, py: Python<'_>, h: &RigidBodyHandle) -> Self { + let mut me = self.clone_with(py); + me.exclude_rigid_body = Some(h.0); + me + } + /// Builder: install a per-collider predicate. + /// + /// The predicate is invoked with ``(ColliderHandle, Collider)`` + /// and must return a ``bool``; ``False`` skips the collider. + /// + /// :param fun: Python callable. + /// :returns: A new :class:`QueryFilter`. + fn predicate(&self, py: Python<'_>, fun: Py) -> Self { + let mut me = self.clone_with(py); + me.predicate = Some(fun.clone_ref(py)); + me + } + + /// Current :class:`QueryFilterFlags`. + #[getter] + fn get_flags(&self) -> QueryFilterFlags { + QueryFilterFlags(self.flags) + } + /// Current :class:`InteractionGroups`, or ``None``. + #[getter] + fn get_groups(&self) -> Option { + self.groups.map(InteractionGroups) + } + /// Handle of the explicitly excluded collider, or ``None``. + #[getter] + #[pyo3(name = "exclude_collider_handle")] + fn get_exclude_collider(&self) -> Option { + self.exclude_collider.map(ColliderHandle) + } + /// Handle of the rigid body whose colliders are excluded, or ``None``. + #[getter] + #[pyo3(name = "exclude_rigid_body_handle")] + fn get_exclude_rigid_body(&self) -> Option { + self.exclude_rigid_body.map(RigidBodyHandle) + } + /// Current per-collider predicate, or ``None``. + #[getter] + fn get_predicate(&self, py: Python<'_>) -> Option { + self.predicate.as_ref().map(|p| p.clone_ref(py)) + } + + /// Debug repr — shows all filter fields. + fn __repr__(&self) -> String { + format!( + "QueryFilter(flags={:#x}, groups={:?}, exclude_collider={:?}, exclude_rigid_body={:?}, predicate={})", + self.flags.bits(), + self.groups.map(|g| (g.memberships.bits(), g.filter.bits())), + self.exclude_collider.map(|h| h.into_raw_parts()), + self.exclude_rigid_body.map(|h| h.into_raw_parts()), + if self.predicate.is_some() { + "" + } else { + "None" + }, + ) + } +} + +impl QueryFilter { + fn default_empty() -> Self { + Self { + flags: rapier::pipeline::QueryFilterFlags::empty(), + groups: None, + exclude_collider: None, + exclude_rigid_body: None, + predicate: None, + } + } +} + +// ===================================================================== +// QueryPipeline — built on demand from the world's broad_phase. +// +// We expose two views: an immutable `QueryPipeline` and a mutable +// `QueryPipelineMut`. Both share the same query implementations. +// ===================================================================== + +/// Scene-query accelerator — a view over the broad-phase BVH. +/// +/// A :class:`QueryPipeline` holds shared references to the world's +/// :class:`BroadPhaseBvh`, :class:`NarrowPhase`, :class:`RigidBodySet`, +/// and :class:`ColliderSet`. Each query method (``cast_ray``, +/// ``cast_shape``, ``intersect_*``, ``project_point*``) re-borrows +/// those sets, builds the underlying parry ``QueryPipeline``, and +/// runs the requested query. +/// +/// **Validity:** the pipeline reflects the state of the broad-phase +/// BVH as of the last :meth:`update` (or +/// :meth:`PhysicsWorld.step` when ``auto_update_query=True``). +/// After mutating the collider set or moving bodies, call +/// :meth:`update` to refresh the BVH before issuing queries. +#[pyclass(name = "QueryPipeline", module = "rapier", unsendable)] +pub struct QueryPipeline { + /// The "owned" BVH that backs scene queries. + pub broad_phase: Py, + pub narrow_phase: Py, + pub bodies: Py, + pub colliders: Py, +} + +#[pymethods] +impl QueryPipeline { + /// Construct a query pipeline from explicit sub-set references. + /// + /// Usually you'll use :attr:`PhysicsWorld.query_pipeline` instead + /// of constructing this directly. + /// + /// :param broad_phase: Broad-phase BVH backing the queries. + /// :param narrow_phase: Narrow-phase store (for the query dispatcher). + /// :param bodies: Rigid-body set referenced by collider parents. + /// :param colliders: Collider set the queries operate on. + #[new] + fn new( + broad_phase: Py, + narrow_phase: Py, + bodies: Py, + colliders: Py, + ) -> Self { + Self { + broad_phase, + narrow_phase, + bodies, + colliders, + } + } + + /// Re-build the broad-phase BVH from the current body/collider state. + /// + /// Must be called after inserting/removing colliders or moving + /// bodies before issuing queries — otherwise the queries see a + /// stale BVH. :meth:`PhysicsWorld.step` calls this automatically + /// when ``world.auto_update_query`` is ``True``; the manual + /// variant lets you avoid a redundant rebuild when the BVH is + /// already up to date. + /// + /// :param bodies: Rigid-body set (accepted for API parity; the + /// pipeline uses its stored reference). + /// :param colliders: Collider set (same caveat as ``bodies``). + fn update( + &self, + py: Python<'_>, + bodies: Py, + colliders: Py, + ) -> PyResult<()> { + let _ = (bodies, colliders); // accepted for API parity + let mut bp = self.broad_phase.borrow_mut(py); + let bodies = self.bodies.borrow(py); + let mut colliders = self.colliders.borrow_mut(py); + let params = rapier::dynamics::IntegrationParameters::default(); + let modified = colliders.0.take_modified(); + let removed = colliders.0.take_removed(); + let mut events: Vec = Vec::new(); + bp.0.update( + ¶ms, + &colliders.0, + &bodies.0, + &modified, + &removed, + &mut events, + ); + Ok(()) + } + + /// Cast a ray and return the closest hit (TOI only). + /// + /// :param ray: Ray to cast. + /// :param max_toi: Upper bound on the returned TOI. + /// :param solid: If ``True``, a ray that starts inside a solid + /// shape hits at ``t = 0``; if ``False``, the ray exits + /// through the shape's back face. + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, toi)`` or ``None`` if no hit. + #[pyo3(signature = (ray, max_toi, solid, filter=None))] + fn cast_ray( + &self, + py: Python<'_>, + ray: &Ray, + max_toi: Real, + solid: bool, + filter: Option<&QueryFilter>, + ) -> PyResult> { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .cast_ray(&ray.0, max_toi, solid) + .map(|(h, t)| (ColliderHandle(h), t))) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Cast a ray and return the closest hit with surface geometry. + /// + /// :param ray: Ray to cast. + /// :param max_toi: Upper bound on the returned TOI. + /// :param solid: See :meth:`cast_ray`. + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, RayIntersection)`` or ``None``. + #[pyo3(signature = (ray, max_toi, solid, filter=None))] + fn cast_ray_and_get_normal( + &self, + py: Python<'_>, + ray: &Ray, + max_toi: Real, + solid: bool, + filter: Option<&QueryFilter>, + ) -> PyResult> { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .cast_ray_and_get_normal(&ray.0, max_toi, solid) + .map(|(h, ri)| (ColliderHandle(h), RayIntersection::from_parry(ri)))) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Iterate over every collider the ray passes through. + /// + /// The Python callback is invoked with ``(ColliderHandle, + /// RayIntersection)`` for each hit. Return ``True`` (or any + /// non-bool) to continue, ``False`` to stop iteration. + /// + /// :param ray: Ray to cast. + /// :param max_toi: Upper bound on the hit TOI. + /// :param solid: See :meth:`cast_ray`. + /// :param callback: ``(ColliderHandle, RayIntersection) -> bool``. + /// :param filter: Optional :class:`QueryFilter`. + #[pyo3(signature = (ray, max_toi, solid, callback, filter=None))] + fn intersect_ray( + &self, + py: Python<'_>, + ray: &Ray, + max_toi: Real, + solid: bool, + callback: Py, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + for (h, _co, ri) in qp.intersect_ray(ray.0, max_toi, solid) { + let py_ri = RayIntersection::from_parry(ri); + let res = callback.call1(py, (ColliderHandle(h), py_ri))?; + if let Ok(b) = res.extract::(py) { + if !b { + break; + } + } + } + Ok(()) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Find the collider whose shape is closest to ``point``. + /// + /// :param point: World-space query point. + /// :param solid: If ``True`` a point inside a solid shape + /// projects to itself (distance ``0``); if ``False`` it + /// projects to the shape's boundary. + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, PointProjection)`` or ``None`` if + /// no collider passed the filter. + #[pyo3(signature = (point, solid, filter=None))] + fn project_point( + &self, + py: Python<'_>, + point: PyPoint, + solid: bool, + filter: Option<&QueryFilter>, + ) -> PyResult> { + let p: rapier::math::Vector = point.0.coords.into(); + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .project_point(p, Real::MAX, solid) + .map(|(h, pp)| (ColliderHandle(h), PointProjection::from_parry(pp)))) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Like :meth:`project_point`, but also return the touched shape feature. + /// + /// :param point: World-space query point. + /// :param max_dist: Largest distance to search; colliders farther + /// than this from ``point`` are ignored. Defaults to unbounded. + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, PointProjection, FeatureId)`` or + /// ``None``. + #[pyo3(signature = (point, max_dist=None, filter=None))] + fn project_point_and_get_feature( + &self, + py: Python<'_>, + point: PyPoint, + max_dist: Option, + filter: Option<&QueryFilter>, + ) -> PyResult> { + let p: rapier::math::Vector = point.0.coords.into(); + let max_dist = max_dist.unwrap_or(Real::MAX); + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .project_point_and_get_feature(p, max_dist) + .map(|(h, pp, fi)| { + ( + ColliderHandle(h), + PointProjection::from_parry(pp), + FeatureId::from_parry(fi), + ) + })) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Invoke ``callback(ColliderHandle)`` for every collider containing ``point``. + /// + /// Return ``True`` (or any non-bool) from the callback to + /// continue, ``False`` to stop iteration. + /// + /// :param point: World-space query point. + /// :param callback: ``(ColliderHandle) -> bool``. + /// :param filter: Optional :class:`QueryFilter`. + #[pyo3(signature = (point, callback, filter=None))] + fn intersect_point( + &self, + py: Python<'_>, + point: PyPoint, + callback: Py, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + let p: rapier::math::Vector = point.0.coords.into(); + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + for (h, _co) in qp.intersect_point(p) { + let res = callback.call1(py, (ColliderHandle(h),))?; + if let Ok(b) = res.extract::(py) { + if !b { + break; + } + } + } + Ok(()) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Linear shape cast — sweep ``shape`` along ``shape_vel`` from ``shape_pose``. + /// + /// :param shape_pose: Initial pose of the cast shape. + /// :param shape_vel: Linear velocity vector (units per unit time). + /// :param shape: Shape to sweep. + /// :param options: :class:`ShapeCastOptions` (TOI bound, target + /// distance, penetration handling). + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, ShapeCastHit)`` for the earliest + /// hit, or ``None``. + #[pyo3(signature = (shape_pose, shape_vel, shape, options, filter=None))] + fn cast_shape( + &self, + py: Python<'_>, + shape_pose: PyIsometry, + shape_vel: PyVector, + shape: &SharedShape, + options: &ShapeCastOptions, + filter: Option<&QueryFilter>, + ) -> PyResult> { + let pose: rapier::math::Pose = shape_pose.0.into(); + let vel: rapier::math::Vector = shape_vel.0.into(); + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .cast_shape(&pose, vel, &*shape.0.0, options.0) + .map(|(h, hit)| (ColliderHandle(h), ShapeCastHit::from_parry(hit)))) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Nonlinear shape cast — sweep ``shape`` along a curved :class:`NonlinearRigidMotion`. + /// + /// Use this when the moving body rotates or has an offset center + /// (e.g. a hammer head swinging around a wrist), which a straight + /// :meth:`cast_shape` would miss. + /// + /// :param motion: Curved motion to sweep along. + /// :param shape: Shape to sweep. + /// :param options: :class:`ShapeCastOptions` — only + /// ``stop_at_penetration`` is honored by this query. + /// :param start_time: Lower bound on the cast parameter. + /// :param end_time: Upper bound on the cast parameter. + /// :param filter: Optional :class:`QueryFilter`. + /// :returns: ``(ColliderHandle, ShapeCastHit)`` or ``None``. + #[pyo3(signature = (motion, shape, options, start_time=0.0 as Real, end_time=Real::MAX, filter=None))] + #[allow(clippy::too_many_arguments)] + fn cast_shape_nonlinear( + &self, + py: Python<'_>, + motion: &NonlinearRigidMotion, + shape: &SharedShape, + options: &ShapeCastOptions, + start_time: Real, + end_time: Real, + filter: Option<&QueryFilter>, + ) -> PyResult> { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp + .cast_shape_nonlinear( + &motion.0, + &*shape.0.0, + start_time, + end_time, + options.0.stop_at_penetration, + ) + .map(|(h, hit)| (ColliderHandle(h), ShapeCastHit::from_parry(hit)))) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Invoke ``callback(ColliderHandle)`` for each collider intersecting ``shape`` at ``shape_pose``. + /// + /// Return ``True`` (or any non-bool) from the callback to + /// continue, ``False`` to stop iteration. + /// + /// :param shape_pose: World pose of the test shape. + /// :param shape: Test shape. + /// :param callback: ``(ColliderHandle) -> bool``. + /// :param filter: Optional :class:`QueryFilter`. + #[pyo3(signature = (shape_pose, shape, callback, filter=None))] + fn intersect_shape( + &self, + py: Python<'_>, + shape_pose: PyIsometry, + shape: &SharedShape, + callback: Py, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + let pose: rapier::math::Pose = shape_pose.0.into(); + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + for (h, _co) in qp.intersect_shape(pose, &*shape.0.0) { + let res = callback.call1(py, (ColliderHandle(h),))?; + if let Ok(b) = res.extract::(py) { + if !b { + break; + } + } + } + Ok(()) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Conservative AABB sweep — invoke ``callback(ColliderHandle)`` for every collider whose AABB intersects ``aabb``. + /// + /// The test is conservative: a candidate's AABB may overlap the + /// query AABB even when the underlying shapes don't (i.e. false + /// positives are possible — there are no false negatives). + /// Return ``False`` from the callback to stop iteration. + /// + /// :param aabb: Query AABB. + /// :param callback: ``(ColliderHandle) -> bool``. + /// :param filter: Optional :class:`QueryFilter`. + #[pyo3(signature = (aabb, callback, filter=None))] + fn intersect_aabb_conservative( + &self, + py: Python<'_>, + aabb: &Aabb, + callback: Py, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + for (h, _co) in qp.intersect_aabb_conservative(aabb.0) { + let res = callback.call1(py, (ColliderHandle(h),))?; + if let Ok(b) = res.extract::(py) { + if !b { + break; + } + } + } + Ok(()) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } + + /// Alias for :meth:`intersect_aabb_conservative`. + #[pyo3(signature = (aabb, callback, filter=None))] + fn colliders_with_aabb_intersecting_aabb( + &self, + py: Python<'_>, + aabb: &Aabb, + callback: Py, + filter: Option<&QueryFilter>, + ) -> PyResult<()> { + self.intersect_aabb_conservative(py, aabb, callback, filter) + } + + /// Return ``True`` iff any collider's AABB intersects ``aabb``. + /// + /// Conservative test (see :meth:`intersect_aabb_conservative`). + /// + /// :param aabb: Query AABB. + /// :param filter: Optional :class:`QueryFilter`. + #[pyo3(signature = (aabb, filter=None))] + fn test_aabb( + &self, + py: Python<'_>, + aabb: &Aabb, + filter: Option<&QueryFilter>, + ) -> PyResult { + { + // Borrow the world's sub-sets immutably (queries are read-only). + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + + // Build the raw QueryFilter, handling the (optional) Python predicate. + // The predicate is captured in a stack-pinned closure so the upstream + // borrow (`&'a dyn Fn(...)`) stays valid for the call. + let py_pred_obj: Option> = + filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); + let pred_err = std::cell::RefCell::new(None::); + let pred_closure = + |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { + let _ = co; + if let Some(ref obj) = py_pred_obj { + // Hand the predicate a live view into the set (no copy of the + // collider). The set is already borrowed immutably for the + // query, so the view's reads re-borrow shared — fine; the + // predicate must not mutate the collider mid-query. + let coll_py = Collider { + backing: ColliderBacking::InSet { + set: self.colliders.clone_ref(py), + handle: h, + }, + }; + let h_py = ColliderHandle(h); + match obj.call1(py, (h_py, coll_py)) { + Ok(r) => r.extract::(py).unwrap_or(true), + Err(e) => { + *pred_err.borrow_mut() = Some(e); + true + } + } + } else { + true + } + }; + let pred_dyn: Option< + &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, + > = if py_pred_obj.is_some() { + Some(&pred_closure) + } else { + None + }; + + let qf = filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { + let mut base = rapier::pipeline::QueryFilter::default(); + base.predicate = pred_dyn; + base + }); + + let qp = + bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); + + let result = (|qp: rapier::pipeline::QueryPipeline<'_>| { + Ok(qp.intersect_aabb_conservative(aabb.0).next().is_some()) + })(qp); + if let Some(e) = pred_err.into_inner() { + return Err(e); + } + result + } + } +} + +// ===================================================================== +// Counters and its sub-structs +// ===================================================================== + +/// Read-only per-stage timing breakdown for a single ``step()``. +/// +/// All values are in milliseconds and are only populated when the +/// owning :class:`Counters` is enabled via :meth:`Counters.enable`. +#[pyclass(name = "StagesCounters", module = "rapier", frozen)] +#[derive(Clone, Copy)] +pub struct StagesCounters(pub rapier::counters::StagesCounters); + +#[pymethods] +impl StagesCounters { + /// Time spent updating broad-phase / collider state, in ms. + #[getter] + fn update_time_ms(&self) -> f64 { + self.0.update_time.time_ms() + } + /// Total collision-detection time (broad + narrow phase), in ms. + #[getter] + fn collision_detection_time_ms(&self) -> f64 { + self.0.collision_detection_time.time_ms() + } + /// Time spent grouping bodies into solver islands, in ms. + #[getter] + fn island_construction_time_ms(&self) -> f64 { + self.0.island_construction_time.time_ms() + } + /// Time spent assembling per-island constraint data, in ms. + #[getter] + fn island_constraints_collection_time_ms(&self) -> f64 { + self.0.island_constraints_collection_time.time_ms() + } + /// Constraint-solver time, in ms. + #[getter] + fn solver_time_ms(&self) -> f64 { + self.0.solver_time.time_ms() + } + /// Continuous collision detection time, in ms. + #[getter] + fn ccd_time_ms(&self) -> f64 { + self.0.ccd_time.time_ms() + } + /// Time spent applying user-modified body / collider state, in ms. + #[getter] + fn user_changes_time_ms(&self) -> f64 { + self.0.user_changes.time_ms() + } + /// Debug repr — shows the dominant per-stage times. + fn __repr__(&self) -> String { + format!( + "StagesCounters(solver={}ms, collision_detection={}ms)", + self.0.solver_time.time_ms(), + self.0.collision_detection_time.time_ms() + ) + } +} + +/// Read-only collision-detection counters for a single ``step()``. +/// +/// Only populated when the owning :class:`Counters` is enabled. +#[pyclass(name = "CollisionDetectionCounters", module = "rapier", frozen)] +#[derive(Clone, Copy)] +pub struct CollisionDetectionCounters(pub rapier::counters::CollisionDetectionCounters); + +#[pymethods] +impl CollisionDetectionCounters { + /// Number of contact pairs detected. + #[getter] + fn ncontact_pairs(&self) -> usize { + self.0.ncontact_pairs + } + /// Broad-phase time, in ms. + #[getter] + fn broad_phase_time_ms(&self) -> f64 { + self.0.broad_phase_time.time_ms() + } + /// Narrow-phase time, in ms. + #[getter] + fn narrow_phase_time_ms(&self) -> f64 { + self.0.narrow_phase_time.time_ms() + } + /// Final broad-phase pass time (post-CCD), in ms. + #[getter] + fn final_broad_phase_time_ms(&self) -> f64 { + self.0.final_broad_phase_time.time_ms() + } +} + +/// Read-only solver counters for a single ``step()``. +/// +/// Only populated when the owning :class:`Counters` is enabled. +#[pyclass(name = "SolverCounters", module = "rapier", frozen)] +#[derive(Clone, Copy)] +pub struct SolverCounters(pub rapier::counters::SolverCounters); + +#[pymethods] +impl SolverCounters { + /// Number of solver constraints assembled. + #[getter] + fn nconstraints(&self) -> usize { + self.0.nconstraints + } + /// Number of contacts driven by the solver. + #[getter] + fn ncontacts(&self) -> usize { + self.0.ncontacts + } + /// Velocity-resolution (impulse) time, in ms. + #[getter] + fn velocity_resolution_time_ms(&self) -> f64 { + self.0.velocity_resolution_time.time_ms() + } + /// Velocity-constraint assembly time, in ms. + #[getter] + fn velocity_assembly_time_ms(&self) -> f64 { + self.0.velocity_assembly_time.time_ms() + } + /// Post-solve velocity-integration time, in ms. + #[getter] + fn velocity_update_time_ms(&self) -> f64 { + self.0.velocity_update_time.time_ms() + } +} + +/// Read-only continuous-collision-detection (CCD) counters. +/// +/// Only populated when the owning :class:`Counters` is enabled. +#[pyclass(name = "CCDCounters", module = "rapier", frozen)] +#[derive(Clone, Copy)] +pub struct CCDCounters(pub rapier::counters::CCDCounters); + +#[pymethods] +impl CCDCounters { + /// Number of CCD substeps taken this step. + #[getter] + fn num_substeps(&self) -> usize { + self.0.num_substeps + } + /// Time spent computing time-of-impact, in ms. + #[getter] + fn toi_computation_time_ms(&self) -> f64 { + self.0.toi_computation_time.time_ms() + } + /// Per-substep solver time, in ms. + #[getter] + fn solver_time_ms(&self) -> f64 { + self.0.solver_time.time_ms() + } + /// Per-substep broad-phase time, in ms. + #[getter] + fn broad_phase_time_ms(&self) -> f64 { + self.0.broad_phase_time.time_ms() + } + /// Per-substep narrow-phase time, in ms. + #[getter] + fn narrow_phase_time_ms(&self) -> f64 { + self.0.narrow_phase_time.time_ms() + } +} + +/// Aggregate read-only timing counters from a :class:`PhysicsPipeline`. +/// +/// Disabled by default — call :meth:`enable` before stepping the +/// pipeline to collect data, then read the per-stage sub-counters +/// (:attr:`stages`, :attr:`cd`, :attr:`solver`, :attr:`ccd`). +/// +/// Timing is best-effort and may be zero on platforms without a +/// high-resolution clock. +#[pyclass(name = "Counters", module = "rapier")] +#[derive(Clone, Copy)] +pub struct Counters(pub rapier::counters::Counters); + +#[pymethods] +impl Counters { + /// Construct a disabled ``Counters``. Call :meth:`enable` to start collecting. + #[new] + fn new() -> Self { + Self(rapier::counters::Counters::new(false)) + } + + /// Enable timing collection on the next ``step()``. + fn enable(&mut self) { + self.0.enable() + } + /// Disable timing collection. + fn disable(&mut self) { + self.0.disable() + } + /// Zero out every sub-counter. + fn reset(&mut self) { + self.0.reset() + } + /// ``True`` iff timing collection is currently active. + #[getter] + fn enabled(&self) -> bool { + self.0.enabled() + } + /// Total time for the last ``step()`` call, in ms. + #[getter] + fn step_time_ms(&self) -> f64 { + self.0.step_time_ms() + } + /// Time accumulated under the "custom" stage hook, in ms. + #[getter] + fn custom_time_ms(&self) -> f64 { + self.0.custom_time_ms() + } + /// Per-stage breakdown (see :class:`StagesCounters`). + #[getter] + fn stages(&self) -> StagesCounters { + StagesCounters(self.0.stages) + } + /// Collision-detection breakdown (see :class:`CollisionDetectionCounters`). + #[getter] + fn cd(&self) -> CollisionDetectionCounters { + CollisionDetectionCounters(self.0.cd) + } + /// Solver breakdown (see :class:`SolverCounters`). + #[getter] + fn solver(&self) -> SolverCounters { + SolverCounters(self.0.solver) + } + /// CCD breakdown (see :class:`CCDCounters`). + #[getter] + fn ccd(&self) -> CCDCounters { + CCDCounters(self.0.ccd) + } + /// Pretty-print the entire counter tree to stdout. + #[pyo3(name = "print")] + fn py_print(&self) { + use std::println; + println!("{}", self.0); + } + /// Debug repr — shows enabled state and total step time. + fn __repr__(&self) -> String { + format!( + "Counters(enabled={}, step_time_ms={})", + self.0.enabled(), + self.0.step_time_ms() + ) + } +} + +// ===================================================================== +// PhysicsPipeline +// ===================================================================== + +/// Low-level physics-step driver. +/// +/// The :class:`PhysicsPipeline` owns the persistent solver state but +/// requires you to hand it every sub-set (bodies, colliders, islands, +/// broad / narrow phase, joints, CCD solver) on each call to +/// :meth:`step`. For most users the higher-level :class:`PhysicsWorld` +/// aggregates these and is more convenient. +#[pyclass(name = "PhysicsPipeline", module = "rapier", unsendable)] +pub struct PhysicsPipeline(pub rapier::pipeline::PhysicsPipeline); + +#[pymethods] +impl PhysicsPipeline { + /// Construct a fresh :class:`PhysicsPipeline`. + #[new] + fn new() -> Self { + Self(rapier::pipeline::PhysicsPipeline::new()) + } + + /// Read-only :class:`Counters` populated by the last :meth:`step`. + /// + /// Enable timing on the pipeline before stepping to get non-zero + /// numbers. + #[getter] + fn counters(&self) -> Counters { + Counters(self.0.counters) + } + + /// Advance the simulation by one step. + /// + /// Releases the GIL via ``Python::allow_threads`` while the + /// solver runs, so other Python threads can make progress. + /// Hooks and event-handler callbacks re-acquire the GIL + /// transparently before touching Python objects; any exception + /// raised inside a callback is captured and re-raised after + /// :meth:`step` returns. + /// + /// :param gravity: World-space gravity vector. + /// :param integration_parameters: Solver tuning parameters. + /// :param islands: Body island manager (mutated). + /// :param broad_phase: Broad-phase BVH (mutated). + /// :param narrow_phase: Narrow-phase store (mutated). + /// :param bodies: Rigid-body set (mutated). + /// :param colliders: Collider set (mutated). + /// :param impulse_joints: Impulse-joint set (mutated). + /// :param multibody_joints: Multibody-joint set (mutated). + /// :param ccd_solver: CCD solver state (mutated). + /// :param hooks: Optional ``PhysicsHooks``-protocol object. + /// :param events: Optional ``EventHandler``-protocol object + /// (e.g. :class:`ChannelEventCollector`). + /// :raises: Any exception raised from a Python hook / event + /// callback is re-raised after the step completes. + #[pyo3(signature = ( + gravity, + integration_parameters, + islands, + broad_phase, + narrow_phase, + bodies, + colliders, + impulse_joints, + multibody_joints, + ccd_solver, + hooks=None, + events=None, + ))] + #[allow(clippy::too_many_arguments)] + fn step( + &mut self, + py: Python<'_>, + gravity: PyVector, + integration_parameters: &IntegrationParameters, + islands: &mut IslandManager, + broad_phase: &mut BroadPhaseBvh, + narrow_phase: &mut NarrowPhase, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + ccd_solver: &mut CCDSolver, + hooks: Option<&Bound<'_, PyAny>>, + events: Option<&Bound<'_, PyAny>>, + ) -> PyResult<()> { + let hooks_obj: Option> = hooks.and_then(|h| { + if h.is_none() { + None + } else { + Some(h.clone().unbind()) + } + }); + let events_obj: Option> = events.and_then(|e| { + if e.is_none() { + None + } else { + Some(e.clone().unbind()) + } + }); + let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError::default())); + let hooks_box = build_physics_hooks(py, hooks_obj.as_ref(), err_slot.clone()); + let events_box = build_event_handler(py, events_obj.as_ref(), err_slot.clone()); + + let g: rapier::math::Vector = gravity.0.into(); + let pp_inner = &mut self.0; + let ip_inner = &integration_parameters.0; + let islands_inner = &mut islands.0; + let bp_inner = &mut broad_phase.0; + let np_inner = &mut narrow_phase.0; + let bodies_inner = &mut bodies.0; + let colliders_inner = &mut colliders.0; + let ij_inner = &mut impulse_joints.0; + let mj_inner = &mut multibody_joints.0; + let ccd_inner = &mut ccd_solver.0; + let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = match hooks_box.as_deref() { + Some(h) => h, + None => &(), + }; + let events_ref: &dyn rapier::pipeline::EventHandler = match events_box.as_deref() { + Some(e) => e, + None => &(), + }; + py.allow_threads(|| { + pp_inner.step( + g, + ip_inner, + islands_inner, + bp_inner, + np_inner, + bodies_inner, + colliders_inner, + ij_inner, + mj_inner, + ccd_inner, + hooks_ref, + events_ref, + ); + }); + drop(hooks_box); + drop(events_box); + let mut slot = err_slot.lock().unwrap(); + if let Some(e) = slot.err.take() { + return Err(e); + } + Ok(()) + } +} + +// ===================================================================== +// CollisionPipeline +// ===================================================================== + +/// Collision-only step driver (no dynamics). +/// +/// A reduced :class:`PhysicsPipeline` that runs the broad / narrow +/// phases and emits collision / contact-force events but **skips** +/// joint and constraint solving. Useful for static scene queries, +/// trigger evaluation, or driving custom controllers. +#[pyclass(name = "CollisionPipeline", module = "rapier", unsendable)] +pub struct CollisionPipeline(pub rapier::pipeline::CollisionPipeline); + +#[pymethods] +impl CollisionPipeline { + /// Construct a fresh :class:`CollisionPipeline`. + #[new] + fn new() -> Self { + Self(rapier::pipeline::CollisionPipeline::new()) + } + + /// Run a single collision-detection pass. + /// + /// Releases the GIL while running; hook / event callbacks + /// re-acquire it on demand. See :meth:`PhysicsPipeline.step` for + /// the threading and error-propagation contract. + /// + /// :param prediction_distance: Distance used to inflate AABBs + /// when looking for new contact pairs. + /// :param islands: Body island manager. + /// :param broad_phase: Broad-phase BVH (mutated). + /// :param narrow_phase: Narrow-phase store (mutated). + /// :param bodies: Rigid-body set (mutated). + /// :param colliders: Collider set (mutated). + /// :param hooks: Optional ``PhysicsHooks``-protocol object. + /// :param events: Optional ``EventHandler``-protocol object. + #[pyo3(signature = ( + prediction_distance, + islands, + broad_phase, + narrow_phase, + bodies, + colliders, + hooks=None, + events=None, + ))] + #[allow(clippy::too_many_arguments)] + fn step( + &mut self, + py: Python<'_>, + prediction_distance: Real, + islands: &mut IslandManager, + broad_phase: &mut BroadPhaseBvh, + narrow_phase: &mut NarrowPhase, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + hooks: Option<&Bound<'_, PyAny>>, + events: Option<&Bound<'_, PyAny>>, + ) -> PyResult<()> { + let hooks_obj: Option> = hooks.and_then(|h| { + if h.is_none() { + None + } else { + Some(h.clone().unbind()) + } + }); + let events_obj: Option> = events.and_then(|e| { + if e.is_none() { + None + } else { + Some(e.clone().unbind()) + } + }); + let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError::default())); + let hooks_box = build_physics_hooks(py, hooks_obj.as_ref(), err_slot.clone()); + let events_box = build_event_handler(py, events_obj.as_ref(), err_slot.clone()); + + let cp_inner = &mut self.0; + let islands_inner = &mut islands.0; + let bp_inner = &mut broad_phase.0; + let np_inner = &mut narrow_phase.0; + let bodies_inner = &mut bodies.0; + let colliders_inner = &mut colliders.0; + let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = match hooks_box.as_deref() { + Some(h) => h, + None => &(), + }; + let events_ref: &dyn rapier::pipeline::EventHandler = match events_box.as_deref() { + Some(e) => e, + None => &(), + }; + py.allow_threads(|| { + cp_inner.step( + prediction_distance, + islands_inner, + bp_inner, + np_inner, + bodies_inner, + colliders_inner, + hooks_ref, + events_ref, + ); + }); + drop(hooks_box); + drop(events_box); + let mut slot = err_slot.lock().unwrap(); + if let Some(e) = slot.err.take() { + return Err(e); + } + Ok(()) + } +} + +// ===================================================================== +// PhysicsWorld — convenience aggregate. +// +// All sub-sets are stored as `Py<...>` so that property reads return +// the **same** Python object on every call (mutations persist). +// ===================================================================== + +/// Recommended entry point — aggregates every rapier sub-state in one object. +/// +/// A :class:`PhysicsWorld` owns the body / collider / joint sets, +/// broad / narrow phase, island manager, CCD solver, integration +/// parameters, and physics + query pipelines. It exposes them as +/// stable properties (the same Python object is returned on every +/// access, so mutations persist):: +/// +/// world = PhysicsWorld(gravity=Vec3(0.0, -9.81, 0.0), +/// auto_update_query=True) +/// ground = world.add_collider(ColliderBuilder.cuboid(50, 0.1, 50)) +/// ball = world.add_body( +/// RigidBodyBuilder.dynamic().translation(Vec3(0, 5, 0)), +/// colliders=[ColliderBuilder.ball(0.5)], +/// ) +/// for _ in range(60): +/// world.step() +/// +/// assert world.rigid_bodies is world.rigid_bodies # stable +/// +/// Configure scene queries via :attr:`auto_update_query` (refresh the +/// :class:`QueryPipeline` after every step) or call +/// :meth:`update_query_pipeline` manually for finer control. Attach a +/// :class:`ChannelEventCollector` to :attr:`event_handler` to consume +/// collision / contact-force events. +#[pyclass(name = "PhysicsWorld", module = "rapier", unsendable)] +pub struct PhysicsWorld { + pub bodies: Py, + pub colliders: Py, + pub impulse_joints: Py, + pub multibody_joints: Py, + pub broad_phase: Py, + pub narrow_phase: Py, + pub islands: Py, + pub ccd_solver: Py, + pub integration_parameters: Py, + pub physics_pipeline: Py, + pub query_pipeline: Py, + pub gravity: Vec3, + pub event_handler: Option>, + pub physics_hooks: Option>, + pub auto_update_query: bool, + /// Either `"defer"` (best-effort: stash exceptions, re-raise after + /// `step()`) or `"strict"` (also short-circuit further hook calls + /// within the same step). + pub event_error_policy: String, +} + +#[pymethods] +impl PhysicsWorld { + /// Construct a new world with default sub-states. + /// + /// :param gravity: World-space gravity vector. Defaults to zero + /// (no gravity). + /// :param auto_update_query: When ``True``, :meth:`step` calls + /// :meth:`update_query_pipeline` automatically; otherwise the + /// :attr:`query_pipeline` reflects the previous + /// :meth:`update` call. + #[new] + #[pyo3(signature = (gravity=None, auto_update_query=false))] + fn new(py: Python<'_>, gravity: Option, auto_update_query: bool) -> PyResult { + let g = gravity + .map(|v| v.0) + .unwrap_or_else(crate::na::SVector::::zeros); + + let bodies = Py::new(py, RigidBodySet(rapier::dynamics::RigidBodySet::new()))?; + let colliders = Py::new(py, ColliderSet(rapier::geometry::ColliderSet::new()))?; + let impulse_joints = Py::new( + py, + ImpulseJointSet(rapier::dynamics::ImpulseJointSet::new()), + )?; + let multibody_joints = Py::new( + py, + MultibodyJointSet(rapier::dynamics::MultibodyJointSet::new()), + )?; + let broad_phase = Py::new(py, BroadPhaseBvh(rapier::geometry::BroadPhaseBvh::new()))?; + let narrow_phase = Py::new(py, NarrowPhase(rapier::geometry::NarrowPhase::new()))?; + let islands = Py::new(py, IslandManager(rapier::dynamics::IslandManager::new()))?; + let ccd_solver = Py::new(py, CCDSolver(rapier::dynamics::CCDSolver::new()))?; + let integration_parameters = Py::new( + py, + IntegrationParameters(rapier::dynamics::IntegrationParameters::default()), + )?; + let physics_pipeline = Py::new( + py, + PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()), + )?; + + let query_pipeline = Py::new( + py, + QueryPipeline { + broad_phase: broad_phase.clone_ref(py), + narrow_phase: narrow_phase.clone_ref(py), + bodies: bodies.clone_ref(py), + colliders: colliders.clone_ref(py), + }, + )?; + + Ok(Self { + bodies, + colliders, + impulse_joints, + multibody_joints, + broad_phase, + narrow_phase, + islands, + ccd_solver, + integration_parameters, + physics_pipeline, + query_pipeline, + gravity: Vec3(g), + event_handler: None, + physics_hooks: None, + auto_update_query, + event_error_policy: "defer".to_string(), + }) + } + + // ---- shared sub-set accessors (return the same Python objects) ---- + + /// The world's :class:`RigidBodySet`. Returns the same Python + /// object on every call, so ``world.rigid_bodies is world.rigid_bodies``. + #[getter] + fn rigid_bodies(&self, py: Python<'_>) -> Py { + self.bodies.clone_ref(py) + } + /// The world's :class:`ColliderSet` (stable across calls). + #[getter] + fn colliders(&self, py: Python<'_>) -> Py { + self.colliders.clone_ref(py) + } + /// The world's :class:`ImpulseJointSet` (stable across calls). + #[getter] + fn impulse_joints(&self, py: Python<'_>) -> Py { + self.impulse_joints.clone_ref(py) + } + /// The world's :class:`MultibodyJointSet` (stable across calls). + #[getter] + fn multibody_joints(&self, py: Python<'_>) -> Py { + self.multibody_joints.clone_ref(py) + } + /// The world's :class:`BroadPhaseBvh` (stable across calls). + #[getter] + fn broad_phase(&self, py: Python<'_>) -> Py { + self.broad_phase.clone_ref(py) + } + /// The world's :class:`NarrowPhase` (stable across calls). + #[getter] + fn narrow_phase(&self, py: Python<'_>) -> Py { + self.narrow_phase.clone_ref(py) + } + /// The world's :class:`IslandManager` (stable across calls). + #[getter] + fn islands(&self, py: Python<'_>) -> Py { + self.islands.clone_ref(py) + } + /// The world's :class:`CCDSolver` (stable across calls). + #[getter] + fn ccd_solver(&self, py: Python<'_>) -> Py { + self.ccd_solver.clone_ref(py) + } + /// Current :class:`IntegrationParameters`. Stable across calls. + #[getter] + fn integration_parameters(&self, py: Python<'_>) -> Py { + self.integration_parameters.clone_ref(py) + } + /// Replace the stored integration parameters in-place. + #[setter] + fn set_integration_parameters( + &mut self, + py: Python<'_>, + ip: &IntegrationParameters, + ) -> PyResult<()> { + let mut cur = self.integration_parameters.borrow_mut(py); + cur.0 = ip.0; + Ok(()) + } + /// The world's underlying :class:`PhysicsPipeline` (stable across calls). + #[getter] + fn physics_pipeline(&self, py: Python<'_>) -> Py { + self.physics_pipeline.clone_ref(py) + } + /// The world's :class:`QueryPipeline` (stable across calls). + #[getter] + fn query_pipeline(&self, py: Python<'_>) -> Py { + self.query_pipeline.clone_ref(py) + } + + /// World-space gravity vector applied to dynamic bodies. + #[getter] + fn gravity(&self) -> Vec3 { + self.gravity + } + /// Set the gravity vector. + #[setter] + fn set_gravity(&mut self, v: PyVector) { + self.gravity = Vec3(v.0); + } + + /// Currently installed ``EventHandler``-protocol object, or ``None``. + /// + /// Usually a :class:`ChannelEventCollector`. + #[getter] + fn event_handler(&self, py: Python<'_>) -> Option { + self.event_handler.as_ref().map(|p| p.clone_ref(py)) + } + /// Install or remove the event handler. + /// + /// Pass ``None`` to detach. The handler is invoked from + /// :meth:`step` (with the GIL re-acquired). + #[setter] + fn set_event_handler(&mut self, py: Python<'_>, v: Option>) { + self.event_handler = v.map(|p| p.clone_ref(py)); + } + + /// Currently installed ``PhysicsHooks``-protocol object, or ``None``. + #[getter] + fn physics_hooks(&self, py: Python<'_>) -> Option { + self.physics_hooks.as_ref().map(|p| p.clone_ref(py)) + } + /// Install or remove the physics-hooks object. + /// + /// The hooks object's ``filter_contact_pair`` / + /// ``filter_intersection_pair`` / ``modify_solver_contacts`` + /// methods are invoked from :meth:`step` (with the GIL + /// re-acquired). + #[setter] + fn set_physics_hooks(&mut self, py: Python<'_>, v: Option>) { + self.physics_hooks = v.map(|p| p.clone_ref(py)); + } + + /// When ``True``, :meth:`step` refreshes the query pipeline after each call. + /// + /// Trade-off: setting this to ``True`` keeps :attr:`query_pipeline` + /// always fresh but adds a BVH rebuild after every step. Setting + /// it to ``False`` avoids the redundant rebuild — useful when you + /// only query infrequently or only after several steps; call + /// :meth:`update_query_pipeline` manually before issuing queries. + #[getter] + fn auto_update_query(&self) -> bool { + self.auto_update_query + } + /// Set :attr:`auto_update_query`. + #[setter] + fn set_auto_update_query(&mut self, v: bool) { + self.auto_update_query = v; + } + + /// Error-propagation policy for hook / event-handler exceptions. + /// + /// ``"defer"`` (default): catch every callback exception, stash the + /// first one, finish the step, and re-raise it at the end. + /// ``"strict"``: same, but also flip an abort flag so subsequent + /// callbacks within the same step short-circuit. + #[getter] + fn event_error_policy(&self) -> String { + self.event_error_policy.clone() + } + /// Set :attr:`event_error_policy` — must be ``"defer"`` or ``"strict"``. + /// + /// :raises ValueError: If ``v`` is neither ``"defer"`` nor ``"strict"``. + #[setter] + fn set_event_error_policy(&mut self, v: String) -> PyResult<()> { + match v.as_str() { + "defer" | "strict" => { + self.event_error_policy = v; + Ok(()) + } + _ => Err(crate::pyo3::exceptions::PyValueError::new_err( + "event_error_policy must be 'defer' or 'strict'", + )), + } + } + + // ---- step ---- + + /// Advance the simulation by one step using the world's configuration. + /// + /// Uses the stored :attr:`integration_parameters`, :attr:`gravity`, + /// :attr:`physics_hooks`, and :attr:`event_handler`. Releases the + /// GIL via ``Python::allow_threads`` while the solver runs; + /// callbacks re-acquire the GIL before invoking Python code. + /// When :attr:`auto_update_query` is ``True``, the query pipeline + /// is refreshed after the step. Exceptions raised inside Python + /// callbacks are deferred per :attr:`event_error_policy` and + /// re-raised after the step completes. + fn step(&self, py: Python<'_>) -> PyResult<()> { + let g = self.gravity.0; + // Build the (boxed) hook/event adapters out-of-band so that + // they can be passed as `&dyn Trait` into `allow_threads`. + let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError { + err: None, + aborted: false, + policy_strict: self.event_error_policy == "strict", + })); + let hooks_box = build_physics_hooks(py, self.physics_hooks.as_ref(), err_slot.clone()); + let events_box = build_event_handler(py, self.event_handler.as_ref(), err_slot.clone()); + { + let mut pp = self.physics_pipeline.borrow_mut(py); + let ip = self.integration_parameters.borrow(py); + let mut islands = self.islands.borrow_mut(py); + let mut bp = self.broad_phase.borrow_mut(py); + let mut np = self.narrow_phase.borrow_mut(py); + let mut bodies = self.bodies.borrow_mut(py); + let mut colliders = self.colliders.borrow_mut(py); + let mut ij = self.impulse_joints.borrow_mut(py); + let mut mj = self.multibody_joints.borrow_mut(py); + let mut ccd = self.ccd_solver.borrow_mut(py); + let g_engine: rapier::math::Vector = g.into(); + // Borrow the inner rapier values explicitly so the + // `Ungil` closure doesn't capture any `Python<'_>` token + // (which is `!Send`). + let pp_inner = &mut pp.0; + let ip_inner = &ip.0; + let islands_inner = &mut islands.0; + let bp_inner = &mut bp.0; + let np_inner = &mut np.0; + let bodies_inner = &mut bodies.0; + let colliders_inner = &mut colliders.0; + let ij_inner = &mut ij.0; + let mj_inner = &mut mj.0; + let ccd_inner = &mut ccd.0; + let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = match hooks_box.as_deref() { + Some(h) => h, + None => &(), + }; + let events_ref: &dyn rapier::pipeline::EventHandler = match events_box.as_deref() { + Some(e) => e, + None => &(), + }; + py.allow_threads(|| { + pp_inner.step( + g_engine, + ip_inner, + islands_inner, + bp_inner, + np_inner, + bodies_inner, + colliders_inner, + ij_inner, + mj_inner, + ccd_inner, + hooks_ref, + events_ref, + ); + }); + } + if self.auto_update_query { + self.update_query_pipeline(py)?; + } + drop(hooks_box); + drop(events_box); + let mut slot = err_slot.lock().unwrap(); + if let Some(e) = slot.err.take() { + return Err(e); + } + Ok(()) + } + + /// Refresh the :attr:`query_pipeline`'s broad-phase BVH. + /// + /// Drains the collider set's ``modified`` / ``removed`` change + /// sets and updates the BVH. Call this manually before issuing + /// scene queries when :attr:`auto_update_query` is ``False``, or + /// after directly mutating colliders / bodies and wanting an + /// immediate refresh. + fn update_query_pipeline(&self, py: Python<'_>) -> PyResult<()> { + let ip = self.integration_parameters.borrow(py); + let bodies = self.bodies.borrow(py); + let mut colliders = self.colliders.borrow_mut(py); + let mut bp = self.broad_phase.borrow_mut(py); + // Drain the colliders' "modified" / "removed" change sets so + // the broad-phase BVH catches up without double-handling them + // on the next `step()`. + let modified = colliders.0.take_modified(); + let removed = colliders.0.take_removed(); + let mut events: Vec = Vec::new(); + bp.0.update( + &ip.0, + &colliders.0, + &bodies.0, + &modified, + &removed, + &mut events, + ); + Ok(()) + } + + /// Wake a sleeping body, forcing it back into the active simulation. + /// + /// Useful after manually moving a body, applying forces, or + /// otherwise wanting to make sure it gets simulated on the next + /// step. No-op for already-awake bodies and for fixed bodies. + /// + /// :param handle: Handle of the body to wake. + /// :param strong: If ``True``, the body is guaranteed to stay + /// awake for several frames; if ``False`` it may sleep again + /// immediately once sleep conditions are met. + #[pyo3(signature = (handle, strong=true))] + fn wake_up(&self, py: Python<'_>, handle: &RigidBodyHandle, strong: bool) -> PyResult<()> { + let mut islands = self.islands.borrow_mut(py); + let mut bodies = self.bodies.borrow_mut(py); + islands.0.wake_up(&mut bodies.0, handle.0, strong); + Ok(()) + } + + /// Wake every sleeping body in the world. + /// + /// :param strong: See :meth:`wake_up`. + #[pyo3(signature = (strong=true))] + fn wake_up_all(&self, py: Python<'_>, strong: bool) -> PyResult<()> { + let mut islands = self.islands.borrow_mut(py); + let mut bodies = self.bodies.borrow_mut(py); + let handles: Vec<_> = bodies.0.iter().map(|(h, _)| h).collect(); + for handle in handles { + islands.0.wake_up(&mut bodies.0, handle, strong); + } + Ok(()) + } + + /// Return the handles of the currently active (awake) bodies. + /// + /// Sleeping bodies are skipped, as are bodies that never sleep but + /// aren't part of any active island (e.g. unattached fixed + /// bodies). This is the list to use when rendering or syncing + /// transforms, since the poses of sleeping bodies haven't moved + /// since the last step. Look each handle up via + /// :attr:`rigid_bodies`. + fn active_bodies(&self, py: Python<'_>) -> Vec { + let islands = self.islands.borrow(py); + islands.0.active_bodies().map(RigidBodyHandle).collect() + } + + /// Drop every body, collider, joint, and pipeline state. + /// + /// Leaves :attr:`gravity`, :attr:`integration_parameters`, and + /// :attr:`auto_update_query` untouched. Equivalent to constructing + /// a fresh world while reusing the same Python wrapper objects. + fn clear(&self, py: Python<'_>) -> PyResult<()> { + let mut bodies = self.bodies.borrow_mut(py); + let mut colliders = self.colliders.borrow_mut(py); + let mut ij = self.impulse_joints.borrow_mut(py); + let mut mj = self.multibody_joints.borrow_mut(py); + let mut bp = self.broad_phase.borrow_mut(py); + let mut np = self.narrow_phase.borrow_mut(py); + let mut islands = self.islands.borrow_mut(py); + let mut ccd = self.ccd_solver.borrow_mut(py); + bodies.0 = rapier::dynamics::RigidBodySet::new(); + colliders.0 = rapier::geometry::ColliderSet::new(); + ij.0 = rapier::dynamics::ImpulseJointSet::new(); + mj.0 = rapier::dynamics::MultibodyJointSet::new(); + bp.0 = rapier::geometry::BroadPhaseBvh::new(); + np.0 = rapier::geometry::NarrowPhase::new(); + islands.0 = rapier::dynamics::IslandManager::new(); + ccd.0 = rapier::dynamics::CCDSolver::new(); + Ok(()) + } + + // ---- pythonic add helpers ---- + + /// Insert a rigid body, optionally attaching child colliders in one call. + /// + /// :param builder: A :class:`RigidBody` or :class:`RigidBodyBuilder`. + /// :param colliders: Optional list of :class:`Collider` or + /// :class:`ColliderBuilder` instances attached to the new + /// body. + /// :returns: Handle of the newly inserted body. + /// :raises TypeError: If ``builder`` or any entry in ``colliders`` + /// is not a recognized rapier type. + #[pyo3(signature = (builder, colliders=None))] + fn add_body( + &self, + py: Python<'_>, + builder: &Bound<'_, PyAny>, + colliders: Option<&Bound<'_, _PyList>>, + ) -> PyResult { + let body = if let Ok(b) = builder.extract::>() { + b.builder.clone().build() + } else if let Ok(rb) = builder.extract::>() { + rb.to_owned_body() + } else { + return Err(PyTypeError::new_err( + "PhysicsWorld.add_body expects a RigidBody or RigidBodyBuilder", + )); + }; + let parent_handle = { + let mut bset = self.bodies.borrow_mut(py); + RigidBodyHandle(bset.0.insert(body)) + }; + if let Some(coll_list) = colliders { + for item in coll_list.iter() { + let coll = if let Ok(b) = item.extract::>() { + b.builder.clone().build() + } else if let Ok(c) = item.extract::>() { + c.to_owned_collider() + } else { + return Err(PyTypeError::new_err( + "PhysicsWorld.add_body colliders must be Collider or ColliderBuilder instances", + )); + }; + let mut cset = self.colliders.borrow_mut(py); + let mut bset = self.bodies.borrow_mut(py); + cset.0 + .insert_with_parent(coll, parent_handle.0, &mut bset.0); + } + } + Ok(parent_handle) + } + + /// Insert a collider, optionally attaching it to a parent rigid body. + /// + /// :param builder: A :class:`Collider` or :class:`ColliderBuilder`. + /// :param parent: Optional rigid-body handle; when ``None`` the + /// collider is added as standalone (no parent body). + /// :returns: Handle of the newly inserted collider. + /// :raises TypeError: If ``builder`` is not a recognized rapier + /// type. + #[pyo3(signature = (builder, parent=None))] + fn add_collider( + &self, + py: Python<'_>, + builder: &Bound<'_, PyAny>, + parent: Option<&RigidBodyHandle>, + ) -> PyResult { + let coll = if let Ok(b) = builder.extract::>() { + b.builder.clone().build() + } else if let Ok(c) = builder.extract::>() { + c.to_owned_collider() + } else { + return Err(PyTypeError::new_err( + "PhysicsWorld.add_collider expects a Collider or ColliderBuilder", + )); + }; + let mut cset = self.colliders.borrow_mut(py); + let handle = match parent { + None => cset.0.insert(coll), + Some(h) => { + let mut bset = self.bodies.borrow_mut(py); + cset.0.insert_with_parent(coll, h.0, &mut bset.0) + } + }; + Ok(ColliderHandle(handle)) + } + + /// Remove a rigid body, its colliders, and any attached joints. + /// + /// :param handle: Handle returned from :meth:`add_body`. + /// :returns: The removed :class:`RigidBody`, or ``None`` if the + /// handle was already invalid. + fn remove_body(&self, py: Python<'_>, handle: &RigidBodyHandle) -> PyResult> { + let mut bodies = self.bodies.borrow_mut(py); + let mut islands = self.islands.borrow_mut(py); + let mut colliders = self.colliders.borrow_mut(py); + let mut ij = self.impulse_joints.borrow_mut(py); + let mut mj = self.multibody_joints.borrow_mut(py); + Ok(bodies + .0 + .remove( + handle.0, + &mut islands.0, + &mut colliders.0, + &mut ij.0, + &mut mj.0, + true, + ) + .map(RigidBody::new_owned)) + } + + /// Remove a collider, detaching it from its parent body if any. + /// + /// :param handle: Handle returned from :meth:`add_collider`. + /// :returns: The removed :class:`Collider`, or ``None`` if the + /// handle was already invalid. + fn remove_collider( + &self, + py: Python<'_>, + handle: &ColliderHandle, + ) -> PyResult> { + let mut cset = self.colliders.borrow_mut(py); + let mut bset = self.bodies.borrow_mut(py); + let mut islands = self.islands.borrow_mut(py); + Ok(cset + .0 + .remove(handle.0, &mut islands.0, &mut bset.0, true) + .map(Collider::new_owned)) + } + + /// Debug repr — shows body and collider counts. + fn __repr__(&self, py: Python<'_>) -> String { + let nb = self.bodies.borrow(py).0.len(); + let nc = self.colliders.borrow(py).0.len(); + format!("PhysicsWorld(bodies={}, colliders={})", nb, nc) + } +} + +// touch the imported _PyList so unused warnings don't trip. +#[allow(dead_code)] +type _PipelinePyListAlias = _PyList; + +pub fn register_pipeline( + _py: crate::pyo3::Python<'_>, + m: &crate::pyo3::Bound<'_, crate::pyo3::types::PyModule>, +) -> crate::pyo3::PyResult<()> { + use crate::pyo3::prelude::*; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/serde_glue.rs b/python/rapier-py-3d/src/serde_glue.rs new file mode 100644 index 000000000..3df1ff45b --- /dev/null +++ b/python/rapier-py-3d/src/serde_glue.rs @@ -0,0 +1,1719 @@ +//! pickle / snapshot glue for the concrete 3D / `f32` binding. +//! +//! This module adds extra `#[pymethods]` blocks (enabled by pyo3's +//! `multiple-pymethods` feature) to attach `to_bytes`, `from_bytes`, and +//! `__reduce__` to the relevant pyclasses, plus `snapshot`, `restore`, +//! `snapshot_json`, and `restore_json` to `PhysicsWorld`. +//! +//! The implementations rely on rapier being compiled with `serde-serialize`, +//! which the `rapier-py-3d` cdylib enables in its Cargo.toml. +//! +//! This used to be generated per-cdylib by a `define_serde_types!(DIM)` macro; +//! with only the 3D/`f32` flavor remaining, the glue is written out directly, +//! with no macros. + +use crate::*; +use rapier3d as rapier; + +use crate::pyo3::prelude::*; +#[allow(unused_imports)] +use crate::pyo3::types::PyBytes; + +// ============================================================ +// Handle types — `__reduce__ = (from_raw_parts, (idx, gen))`. +// ============================================================ +#[pymethods] +impl RigidBodyHandle { + /// Pickle support: `(type(self), (index, generation))`. + fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { + let py = slf.py(); + let (i, g) = slf.0.into_raw_parts(); + let typ: Py = py + .get_type_bound::() + .getattr("from_raw_parts")? + .unbind(); + Ok((typ, (i, g))) + } +} + +#[pymethods] +impl ColliderHandle { + /// Pickle support: `(type(self), (index, generation))`. + fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { + let py = slf.py(); + let (i, g) = slf.0.into_raw_parts(); + let typ: Py = py + .get_type_bound::() + .getattr("from_raw_parts")? + .unbind(); + Ok((typ, (i, g))) + } +} + +#[pymethods] +impl ImpulseJointHandle { + /// Pickle support: `(type(self), (index, generation))`. + fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { + let py = slf.py(); + let (i, g) = slf.0.into_raw_parts(); + let typ: Py = py + .get_type_bound::() + .getattr("from_raw_parts")? + .unbind(); + Ok((typ, (i, g))) + } +} + +#[pymethods] +impl MultibodyJointHandle { + /// Pickle support: `(type(self), (index, generation))`. + fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { + let py = slf.py(); + let (i, g) = slf.0.into_raw_parts(); + let typ: Py = py + .get_type_bound::() + .getattr("from_raw_parts")? + .unbind(); + Ok((typ, (i, g))) + } +} + +// MultibodyIndex / MultibodyLinkId have a slightly different shape +// (no `from_raw_parts` constructor on the Python side). Provide them. +#[pymethods] +impl MultibodyIndex { + #[staticmethod] + fn from_raw_parts(index: u32, generation: u32) -> Self { + Self(rapier::dynamics::MultibodyIndex( + rapier::data::arena::Index::from_raw_parts(index, generation), + )) + } + fn into_raw_parts(&self) -> (u32, u32) { + self.0.0.into_raw_parts() + } + fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { + let py = slf.py(); + let (i, g) = slf.0.0.into_raw_parts(); + let ctor: Py = py + .get_type_bound::() + .getattr("from_raw_parts")? + .unbind(); + Ok((ctor, (i, g))) + } +} + +// `MultibodyLinkId` has a private `graph_id` field; we round-trip +// through bincode (it's serde-derived upstream). +#[pymethods] +impl MultibodyLinkId { + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let payload = crate::bincode::serialize(&self.0).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let body = crate::serde_io::unwrap_bincode(blob.as_bytes())?; + let inner: rapier::dynamics::MultibodyLinkId = + crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok(Self(inner)) + } + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// RigidBody / RigidBodyBuilder / Collider / ColliderBuilder +// ============================================================ + +// RigidBody is a thin view (owned standalone, or a handle into a set), +// so it has no single `body` field for the generic serde macro to read. +// Serialize by reading the underlying body out; deserialize as a fresh +// owned body. +#[pymethods] +impl RigidBody { + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.to_owned_body(); + let payload = crate::bincode::serialize(&inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner: rapier::dynamics::RigidBody = + crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok(RigidBody::new_owned(inner)) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// RigidBodyBuilder is NOT `Serialize`/`Deserialize` upstream. To make +// it picklable, we round-trip through its built form (a RigidBody), +// which is serde-derived. This preserves every field that influences +// the spawned body but discards the "builder vs body" distinction — +// round-tripped builders behave identically when `.build()` is called. +#[pymethods] +impl RigidBodyBuilder { + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let body = self.builder.clone().build(); + let payload = crate::bincode::serialize(&body).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body_buf = crate::serde_io::unwrap_bincode(buf)?; + let body: rapier::dynamics::RigidBody = + crate::bincode::deserialize(body_buf).map_err(crate::serde_io::bincode_err)?; + // RigidBodyBuilder has no `.from_body` constructor — rebuild + // a fresh builder of the correct type and copy in fields + // we can read back. (`can_sleep` is not retrievable, and the + // builder's sleeping/enabled bits aren't observable on a built + // body either, so we approximate.) + let mut b = rapier::dynamics::RigidBodyBuilder::new(body.body_type()); + b = b + .pose(*body.position()) + .linvel(body.linvel()) + .linear_damping(body.linear_damping()) + .angular_damping(body.angular_damping()) + .sleeping(body.is_sleeping()) + .gravity_scale(body.gravity_scale()) + .ccd_enabled(body.is_ccd_enabled()) + .soft_ccd_prediction(body.soft_ccd_prediction()) + .dominance_group(body.dominance_group()) + .additional_solver_iterations(body.additional_solver_iterations()) + .user_data(body.user_data) + .enabled(body.is_enabled()) + .locked_axes(body.locked_axes()); + b = b.angvel(body.angvel()); + Ok(Self { builder: b }) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// Collider is a thin view (owned, or a handle into a set); serialize by +// reading the underlying collider out, deserialize as a fresh owned one. +#[pymethods] +impl Collider { + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.to_owned_collider(); + let payload = crate::bincode::serialize(&inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner: rapier::geometry::Collider = + crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok(Collider::new_owned(inner)) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +impl ColliderBuilder { + #[inline] + pub(crate) fn _from_inner(builder: rapier::geometry::ColliderBuilder) -> Self { + Self { builder } + } +} + +#[pymethods] +impl ColliderBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.builder; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((ColliderBuilder::_from_inner)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Sets (newtype `Self(inner)`). +// ============================================================ +#[pymethods] +impl RigidBodySet { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((RigidBodySet)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl ColliderSet { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((ColliderSet)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl ImpulseJointSet { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((ImpulseJointSet)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl MultibodyJointSet { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((MultibodyJointSet)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Parameters / scratch / phases +// ============================================================ +#[pymethods] +impl IntegrationParameters { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((IntegrationParameters)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl IslandManager { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((IslandManager)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl CCDSolver { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((CCDSolver)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl BroadPhaseBvh { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((BroadPhaseBvh)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl NarrowPhase { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((NarrowPhase)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Mass / interaction / shape types +// ============================================================ +#[pymethods] +impl MassProperties { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((MassProperties)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl InteractionGroups { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((InteractionGroups)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl SharedShape { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((SharedShape)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Joint types (concrete + builders + generic). +// ============================================================ +#[pymethods] +impl FixedJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((FixedJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl FixedJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((FixedJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl RevoluteJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((RevoluteJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl RevoluteJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((RevoluteJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl PrismaticJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((PrismaticJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl PrismaticJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((PrismaticJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl RopeJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((RopeJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl RopeJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((RopeJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl SpringJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((SpringJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl SpringJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((SpringJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// GenericJoint is a thin view (owned, or a view into an in-set joint's +// data); serialize the underlying value, deserialize as owned. +#[pymethods] +impl GenericJoint { + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.to_owned_generic(); + let payload = crate::bincode::serialize(&inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let body = crate::serde_io::unwrap_bincode(blob.as_bytes())?; + let inner: rapier::dynamics::GenericJoint = + crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok(GenericJoint::new_owned(inner)) + } + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl GenericJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((GenericJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// Dim-specific joints (SphericalJoint in 3D). +#[pymethods] +impl SphericalJoint { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((SphericalJoint)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl SphericalJointBuilder { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((SphericalJointBuilder)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Math types — Vec, Point, Rotation, Isometry. +// ============================================================ +#[pymethods] +impl Vec3 { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((Vec3)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py.get_type_bound::().getattr("from_bytes")?.unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl Point3 { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((Point3)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl Rotation3 { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((Rotation3)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl Isometry3 { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((Isometry3)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Controllers — try to serialize. Wrap each in `__reduce__` only if +// serde-derivable on the inner. KinematicCharacterController, PdController, +// PidController, DynamicRayCastVehicleController are all +// `Serialize + Deserialize` upstream. +// ============================================================ +#[pymethods] +impl KinematicCharacterController { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((KinematicCharacterController)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl PdController { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((PdController)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl PidController { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((PidController)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// `DynamicRayCastVehicleController` + `WheelTuning` are 3D-only upstream. +#[pymethods] +impl DynamicRayCastVehicleController { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((DynamicRayCastVehicleController)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +#[pymethods] +impl WheelTuning { + /// Serialize this value to a bincode'd `bytes`. The blob is prefixed + /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = &self.0; + let payload = crate::bincode::serialize(inner).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). + #[staticmethod] + fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let inner = crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + Ok((WheelTuning)(inner)) + } + + /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.to_bytes(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("from_bytes")? + .unbind(); + Ok((ctor, (blob,))) + } +} + +// ============================================================ +// Pipelines + collectors — REFUSE pickle. +// ============================================================ +#[pymethods] +impl PhysicsPipeline { + fn __reduce__(&self) -> PyResult<()> { + Err(crate::errors::SerializationError::new_err( + "PhysicsPipeline is not serializable; it's stateless except for \ + per-step counters. Re-create a PhysicsPipeline after restoring \ + the world.", + )) + } +} + +#[pymethods] +impl CollisionPipeline { + fn __reduce__(&self) -> PyResult<()> { + Err(crate::errors::SerializationError::new_err( + "CollisionPipeline is not serializable; re-create one after restore.", + )) + } +} + +#[pymethods] +impl ChannelEventCollector { + fn __reduce__(&self) -> PyResult<()> { + Err(crate::errors::SerializationError::new_err( + "ChannelEventCollector is not serializable; drain events first.", + )) + } +} + +// ============================================================ +// PhysicsWorld — full snapshot / restore + pickle. +// +// Serialized payload is a tuple of the inner rapier values: +// (bodies, colliders, impulse_joints, multibody_joints, +// broad_phase, narrow_phase, islands, ccd_solver, +// integration_parameters, gravity) +// +// `event_handler`, `physics_hooks`, `event_error_policy` and +// `auto_update_query` are NOT serialized — restored worlds are +// returned with `event_handler = None`, `physics_hooks = None`, +// `event_error_policy = "defer"`, `auto_update_query = false`. +// ============================================================ + +// Borrow-aware shared serialization payload. +// Owned form used for deserialization; borrowed form used for +// serialization to avoid cloning huge component sets. +type _PhysicsWorldOwned = ( + rapier::dynamics::RigidBodySet, + rapier::geometry::ColliderSet, + rapier::dynamics::ImpulseJointSet, + rapier::dynamics::MultibodyJointSet, + rapier::geometry::BroadPhaseBvh, + rapier::geometry::NarrowPhase, + rapier::dynamics::IslandManager, + rapier::dynamics::CCDSolver, + rapier::dynamics::IntegrationParameters, + crate::na::SVector, +); + +#[pymethods] +impl PhysicsWorld { + /// Take a snapshot of the world's physical state. + /// + /// Returns a `bytes` blob containing every internal collection + /// (rigid bodies, colliders, joints, broad phase, narrow phase, + /// island manager, CCD solver, integration parameters, gravity). + /// + /// User-supplied `event_handler` / `physics_hooks` and the + /// `event_error_policy` are **NOT** serialized — re-attach them + /// after `restore`. + fn snapshot<'py>(&self, py: Python<'py>) -> PyResult> { + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + let ij = self.impulse_joints.borrow(py); + let mj = self.multibody_joints.borrow(py); + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let islands = self.islands.borrow(py); + let ccd = self.ccd_solver.borrow(py); + let ip = self.integration_parameters.borrow(py); + let tup = ( + &bodies.0, + &colliders.0, + &ij.0, + &mj.0, + &bp.0, + &np.0, + &islands.0, + &ccd.0, + &ip.0, + &self.gravity.0, + ); + let payload = crate::bincode::serialize(&tup).map_err(crate::serde_io::bincode_err)?; + Ok(crate::serde_io::bytes_to_py( + py, + &crate::serde_io::wrap_bincode(payload), + )) + } + + /// Re-create a `PhysicsWorld` from a snapshot. + #[staticmethod] + fn restore(py: Python<'_>, blob: &Bound<'_, PyBytes>) -> PyResult> { + let buf = blob.as_bytes(); + let body = crate::serde_io::unwrap_bincode(buf)?; + let owned: _PhysicsWorldOwned = + crate::bincode::deserialize(body).map_err(crate::serde_io::bincode_err)?; + let (bs, cs, ijs, mjs, bps, nps, isl, ccd, ip, g) = owned; + let bodies = Py::new(py, RigidBodySet(bs))?; + let colliders = Py::new(py, ColliderSet(cs))?; + let impulse_joints = Py::new(py, ImpulseJointSet(ijs))?; + let multibody_joints = Py::new(py, MultibodyJointSet(mjs))?; + let broad_phase = Py::new(py, BroadPhaseBvh(bps))?; + let narrow_phase = Py::new(py, NarrowPhase(nps))?; + let islands = Py::new(py, IslandManager(isl))?; + let ccd_solver = Py::new(py, CCDSolver(ccd))?; + let integration_parameters = Py::new(py, IntegrationParameters(ip))?; + let physics_pipeline = Py::new( + py, + PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()), + )?; + let query_pipeline = Py::new( + py, + QueryPipeline { + broad_phase: broad_phase.clone_ref(py), + narrow_phase: narrow_phase.clone_ref(py), + bodies: bodies.clone_ref(py), + colliders: colliders.clone_ref(py), + }, + )?; + Py::new( + py, + PhysicsWorld { + bodies, + colliders, + impulse_joints, + multibody_joints, + broad_phase, + narrow_phase, + islands, + ccd_solver, + integration_parameters, + physics_pipeline, + query_pipeline, + gravity: Vec3(g), + event_handler: None, + physics_hooks: None, + auto_update_query: false, + event_error_policy: "defer".to_string(), + }, + ) + } + + /// JSON debug snapshot — slower & much larger but human-readable. + /// + /// Returns a JSON string with a self-describing envelope: + /// `{"_magic": "RPYS", "_version": N, "payload": }`. + fn snapshot_json(&self, py: Python<'_>) -> PyResult { + let bodies = self.bodies.borrow(py); + let colliders = self.colliders.borrow(py); + let ij = self.impulse_joints.borrow(py); + let mj = self.multibody_joints.borrow(py); + let bp = self.broad_phase.borrow(py); + let np = self.narrow_phase.borrow(py); + let islands = self.islands.borrow(py); + let ccd = self.ccd_solver.borrow(py); + let ip = self.integration_parameters.borrow(py); + let tup = ( + &bodies.0, + &colliders.0, + &ij.0, + &mj.0, + &bp.0, + &np.0, + &islands.0, + &ccd.0, + &ip.0, + &self.gravity.0, + ); + let payload = crate::serde_json::to_value(tup).map_err(crate::serde_io::json_err)?; + let env = crate::serde_io::wrap_json(payload); + crate::serde_json::to_string(&env).map_err(crate::serde_io::json_err) + } + + /// Restore a world from a JSON snapshot. + #[staticmethod] + fn restore_json(py: Python<'_>, s: &str) -> PyResult> { + let env: crate::serde_json::Value = + crate::serde_json::from_str(s).map_err(crate::serde_io::json_err)?; + let payload = crate::serde_io::unwrap_json(env)?; + let owned: _PhysicsWorldOwned = + crate::serde_json::from_value(payload).map_err(crate::serde_io::json_err)?; + let (bs, cs, ijs, mjs, bps, nps, isl, ccd, ip, g) = owned; + let bodies = Py::new(py, RigidBodySet(bs))?; + let colliders = Py::new(py, ColliderSet(cs))?; + let impulse_joints = Py::new(py, ImpulseJointSet(ijs))?; + let multibody_joints = Py::new(py, MultibodyJointSet(mjs))?; + let broad_phase = Py::new(py, BroadPhaseBvh(bps))?; + let narrow_phase = Py::new(py, NarrowPhase(nps))?; + let islands = Py::new(py, IslandManager(isl))?; + let ccd_solver = Py::new(py, CCDSolver(ccd))?; + let integration_parameters = Py::new(py, IntegrationParameters(ip))?; + let physics_pipeline = Py::new( + py, + PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()), + )?; + let query_pipeline = Py::new( + py, + QueryPipeline { + broad_phase: broad_phase.clone_ref(py), + narrow_phase: narrow_phase.clone_ref(py), + bodies: bodies.clone_ref(py), + colliders: colliders.clone_ref(py), + }, + )?; + Py::new( + py, + PhysicsWorld { + bodies, + colliders, + impulse_joints, + multibody_joints, + broad_phase, + narrow_phase, + islands, + ccd_solver, + integration_parameters, + physics_pipeline, + query_pipeline, + gravity: Vec3(g), + event_handler: None, + physics_hooks: None, + auto_update_query: false, + event_error_policy: "defer".to_string(), + }, + ) + } + + /// Pickle support. Returns `(PhysicsWorld.restore, (snapshot_bytes,))`. + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { + let blob = self.snapshot(py)?; + let ctor: Py = py + .get_type_bound::() + .getattr("restore")? + .unbind(); + Ok((ctor, (blob,))) + } +} diff --git a/python/rapier-py-3d/src/serde_io.rs b/python/rapier-py-3d/src/serde_io.rs index 252f07711..1cd79cc1a 100644 --- a/python/rapier-py-3d/src/serde_io.rs +++ b/python/rapier-py-3d/src/serde_io.rs @@ -9,11 +9,10 @@ //! //! Mismatched magic or unknown version => `SerializationError`. //! -//! This module deliberately stays free of `rapier::...` paths so that it can -//! live in the dim/scalar-independent `rapier-py-core`. The dim/scalar-specific -//! glue (the `Py` round-trip on `RigidBody`, `Collider`, `RigidBodySet`, -//! etc., plus `PhysicsWorld.snapshot()`/`restore()`) is emitted by the -//! `define_serde_types!` macro, which is invoked once per cdylib. +//! This module deliberately stays free of `rapier::...` paths. The +//! type-specific glue (the `Py` round-trip on `RigidBody`, `Collider`, +//! `RigidBodySet`, etc., plus `PhysicsWorld.snapshot()`/`restore()`) lives in +//! the `serde_glue` module. use pyo3::prelude::*; use pyo3::types::PyBytes; diff --git a/python/rapier-py-core/Cargo.toml b/python/rapier-py-core/Cargo.toml deleted file mode 100644 index 212a7d679..000000000 --- a/python/rapier-py-core/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "rapier-py-core" -version.workspace = true -authors.workspace = true -description = "Shared PyO3 conversion shims and error tree for the Rapier Python bindings." -homepage.workspace = true -repository.workspace = true -license.workspace = true -rust-version.workspace = true -edition.workspace = true -publish = false - -[lib] -name = "rapier_py_core" -path = "src/lib.rs" - -[lints] -# pyo3 0.22's `create_exception!` macro still references `cfg(feature = "gil-refs")` -# which our crate doesn't have. Mute the check rather than poison every build. -rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("gil-refs"))'] } - -# Intentionally no Cargo features. The dim/scalar split lives in each cdylib -# crate, which expands the `define_conv_types!` macro with concrete types. -# That keeps Cargo feature unification (which would otherwise mash dim2 and -# dim3 into the same build) from corrupting the workspace. - -[dependencies] -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } -numpy = "0.22" -nalgebra.workspace = true -thiserror.workspace = true -# Phase 11 — Serialization. We use bincode 1.x (stable; rapier upstream uses -# it in its own tests) for the on-disk format and serde_json for the JSON -# debug mode. The `define_serde_types!` macro relies on these being in scope. -serde = { workspace = true, features = ["derive", "std"] } -bincode = { workspace = true } -serde_json = { workspace = true } diff --git a/python/rapier-py-core/src/controllers.rs b/python/rapier-py-core/src/controllers.rs deleted file mode 100644 index dd405eed5..000000000 --- a/python/rapier-py-core/src/controllers.rs +++ /dev/null @@ -1,1828 +0,0 @@ -//! Macro emitting the user-facing controller `#[pyclass]`-es per cdylib. -//! -//! Controllers. -//! -//! Like the other modules, this macro is invoked once per cdylib *after* -//! `define_conv_types!`, `define_math_types!`, `define_geometry_types!`, -//! `define_joints_types!`, `define_dynamics_types!`, and -//! `define_pipeline_types!`. It depends on items emitted by all of those. -//! -//! In 2D, the `DynamicRayCastVehicleController` (and its `Wheel`, -//! `WheelTuning`, `RayCastInfo` types) does **not** exist upstream -//! (`#[cfg(feature = "dim3")]` in `src/control/mod.rs`), so the 2D macro -//! arm skips registering them. `KinematicCharacterController` and the -//! PID family work in both 2D and 3D. - -/// Materialize the controller `#[pyclass]` types for a given `(Real, DIM)` pair. -#[macro_export] -macro_rules! define_controllers_types { - (DIM = 2) => { - $crate::__define_controllers_shared!(2, Vec2, Point2, Rotation2, Isometry2); - $crate::__define_controllers_pid!(2, Vec2); - $crate::__define_controllers_register!(2); - }; - (DIM = 3) => { - $crate::__define_controllers_shared!(3, Vec3, Point3, Rotation3, Isometry3); - $crate::__define_controllers_pid!(3, Vec3); - $crate::__define_controllers_vehicle!(Vec3); - $crate::__define_controllers_register!(3); - }; -} - -// ============================================================================ -// Shared (2D + 3D): CharacterLength, CharacterAutostep, -// EffectiveCharacterMovement, CharacterCollision, -// KinematicCharacterController, AxesMask, PdErrors, PidCorrection. -// ============================================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_controllers_shared { - ($dim:tt, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // --------------------------------------------------------------- - // CharacterLength tagged union - // - // We don't expose this as a `#[pyclass] enum` because the upstream - // variants carry a `Real` payload. Instead we model it as a small - // frozen struct with a `kind` discriminant and a `value` payload, - // plus `.absolute(v)` / `.relative(v)` static constructors. Matches - // the API documented in `08-controllers.md`. - // --------------------------------------------------------------- - /// A length used by the kinematic character controller. - /// - /// Tagged union with two variants — ``absolute`` (raw world-space - /// units) and ``relative`` (a fraction of the controlled shape's - /// height). Use the :meth:`absolute` / :meth:`relative` static - /// constructors instead of the keyword constructor when possible. - /// - /// :ivar kind: ``"absolute"`` or ``"relative"``. - /// :ivar value: The numeric length, interpreted per ``kind``. - #[pyclass(name = "CharacterLength", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct CharacterLength { - #[pyo3(get)] pub kind: &'static str, - #[pyo3(get)] pub value: Real, - } - - #[pymethods] - impl CharacterLength { - /// Construct a ``CharacterLength`` from an explicit ``(value, kind)``. - /// - /// :param value: Numeric length. - /// :param kind: ``"absolute"`` or ``"relative"``. - /// :raises TypeError: if ``kind`` is not one of the two - /// accepted strings. - #[new] - #[pyo3(signature = (value=0.0 as Real, kind="absolute"))] - fn new(value: Real, kind: &str) -> PyResult { - match kind { - "absolute" => Ok(Self { kind: "absolute", value }), - "relative" => Ok(Self { kind: "relative", value }), - _ => Err(PyTypeError::new_err( - "CharacterLength kind must be \"absolute\" or \"relative\"", - )), - } - } - /// Build an absolute length expressed in world units. - /// - /// :param value: Length in the simulation's distance units. - #[staticmethod] - fn absolute(value: Real) -> Self { Self { kind: "absolute", value } } - /// Build a relative length expressed as a fraction of the shape height. - /// - /// :param value: Multiplier in ``[0, 1]`` of the controlled - /// shape's bounding height. - #[staticmethod] - fn relative(value: Real) -> Self { Self { kind: "relative", value } } - /// Return ``CharacterLength.absolute(v)`` / ``relative(v)`` repr. - fn __repr__(&self) -> String { - format!("CharacterLength.{}({})", - if self.kind == "absolute" { "absolute" } else { "relative" }, - self.value) - } - /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.kind == other.kind && self.value == other.value), - CompareOp::Ne => Ok(!(self.kind == other.kind && self.value == other.value)), - _ => Err(PyTypeError::new_err("CharacterLength supports only == and !=")), - } - } - } - - impl CharacterLength { - #[inline] - fn to_rapier(self) -> rapier::control::CharacterLength { - if self.kind == "relative" { - rapier::control::CharacterLength::Relative(self.value) - } else { - rapier::control::CharacterLength::Absolute(self.value) - } - } - #[inline] - fn from_rapier(v: rapier::control::CharacterLength) -> Self { - match v { - rapier::control::CharacterLength::Relative(x) => Self { kind: "relative", value: x }, - rapier::control::CharacterLength::Absolute(x) => Self { kind: "absolute", value: x }, - } - } - } - - // Accept a `(value, "kind")` tuple, a `CharacterLength` instance, or - // a bare float (interpreted as Absolute). - fn _extract_char_length(obj: &Bound<'_, PyAny>) -> PyResult { - if let Ok(c) = obj.extract::() { - return Ok(c); - } - if let Ok(f) = obj.extract::() { - return Ok(CharacterLength { kind: "absolute", value: f }); - } - Err(PyTypeError::new_err( - "expected a CharacterLength, a float (interpreted as absolute), or use CharacterLength.absolute/.relative", - )) - } - - // --------------------------------------------------------------- - // CharacterAutostep - // --------------------------------------------------------------- - /// Configuration for the kinematic character controller auto-step. - /// - /// Auto-step lets the character climb small ledges (stairs, kerbs) - /// automatically without falling off when the obstacle is shorter - /// than ``max_height`` and the landing platform is wider than - /// ``min_width``. Pass ``None`` (or :meth:`disabled`) to - /// :class:`KinematicCharacterController` to disable. - /// - /// :ivar max_height: Max obstacle height to step over. - /// :ivar min_width: Min platform width above the step. - /// :ivar include_dynamic_bodies: If ``True``, step up onto dynamic - /// bodies too (not just statics/kinematics). - #[pyclass(name = "CharacterAutostep", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct CharacterAutostep { - #[pyo3(get, set)] pub max_height: CharacterLength, - #[pyo3(get, set)] pub min_width: CharacterLength, - #[pyo3(get, set)] pub include_dynamic_bodies: bool, - } - - #[pymethods] - impl CharacterAutostep { - /// Build a ``CharacterAutostep`` with optional overrides. - /// - /// :param max_height: :class:`CharacterLength` or bare float - /// (interpreted as absolute). Defaults to upstream value. - /// :param min_width: :class:`CharacterLength` or bare float. - /// :param include_dynamic_bodies: Whether dynamic surfaces - /// count as valid step targets. - #[new] - #[pyo3(signature = (max_height=None, min_width=None, include_dynamic_bodies=true))] - fn new( - max_height: Option<&Bound<'_, PyAny>>, - min_width: Option<&Bound<'_, PyAny>>, - include_dynamic_bodies: bool, - ) -> PyResult { - let default = rapier::control::CharacterAutostep::default(); - let mh = match max_height { - None => CharacterLength::from_rapier(default.max_height), - Some(o) => _extract_char_length(o)?, - }; - let mw = match min_width { - None => CharacterLength::from_rapier(default.min_width), - Some(o) => _extract_char_length(o)?, - }; - Ok(Self { - max_height: mh, - min_width: mw, - include_dynamic_bodies, - }) - } - /// Return ``None``, the sentinel to disable auto-step on the controller. - /// - /// Idiomatic: ``controller.autostep = CharacterAutostep.disabled()``. - #[staticmethod] - fn disabled() -> Option { None } - /// Return the ``CharacterAutostep(...)`` repr. - fn __repr__(&self) -> String { - format!("CharacterAutostep(max_height={:?}, min_width={:?}, include_dynamic_bodies={})", - self.max_height, self.min_width, self.include_dynamic_bodies) - } - } - - impl CharacterAutostep { - #[inline] - fn to_rapier(self) -> rapier::control::CharacterAutostep { - rapier::control::CharacterAutostep { - max_height: self.max_height.to_rapier(), - min_width: self.min_width.to_rapier(), - include_dynamic_bodies: self.include_dynamic_bodies, - } - } - } - - // --------------------------------------------------------------- - // EffectiveCharacterMovement - // --------------------------------------------------------------- - /// Result of :meth:`KinematicCharacterController.move_shape`. - /// - /// Reports the actual translation applied to the character after - /// collision-aware sliding, auto-step, and ground snapping, plus a - /// ``grounded`` flag derived from the controller's contact state. - /// - /// :ivar translation: World-space translation actually applied. - /// :ivar grounded: ``True`` if the character is in contact with a - /// surface this frame. - /// :ivar is_sliding_down_slope: ``True`` if the controller is - /// sliding the character down a too-steep slope. - #[pyclass(name = "EffectiveCharacterMovement", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct EffectiveCharacterMovement { - #[pyo3(get)] pub translation: $Vec, - #[pyo3(get)] pub grounded: bool, - #[pyo3(get)] pub is_sliding_down_slope: bool, - } - - #[pymethods] - impl EffectiveCharacterMovement { - /// Return the ``EffectiveCharacterMovement(...)`` repr. - fn __repr__(&self) -> String { - format!("EffectiveCharacterMovement(translation={:?}, grounded={}, is_sliding_down_slope={})", - self.translation.0, self.grounded, self.is_sliding_down_slope) - } - } - - // --------------------------------------------------------------- - // CharacterCollision - // --------------------------------------------------------------- - /// A single collision event reported during character movement. - /// - /// Instances are delivered to the ``events_callback`` of - /// :meth:`KinematicCharacterController.move_shape` and can later - /// be fed back to - /// :meth:`KinematicCharacterController.solve_character_collision_impulses` - /// to apply reactive impulses to the bodies the character bumped. - /// - /// :ivar handle: Handle of the collider the character hit. - /// :ivar character_pos: World-space pose at the moment of contact. - /// :ivar translation_applied: Portion of the desired translation - /// successfully applied before the collision. - /// :ivar translation_remaining: Portion of translation still owed - /// (typically slid along the contact surface). - /// :ivar toi: ``ShapeCastHit`` with witness points and normals. - #[pyclass(name = "CharacterCollision", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct CharacterCollision { - #[pyo3(get)] pub handle: ColliderHandle, - #[pyo3(get)] pub character_pos: $Iso, - #[pyo3(get)] pub translation_applied: $Vec, - #[pyo3(get)] pub translation_remaining: $Vec, - #[pyo3(get)] pub toi: ShapeCastHit, - } - - #[pymethods] - impl CharacterCollision { - /// Return the ``CharacterCollision(handle=..., toi=...)`` repr. - fn __repr__(&self) -> String { - format!("CharacterCollision(handle={:?}, toi={})", - self.handle.0.into_raw_parts(), self.toi.time_of_impact) - } - } - - impl CharacterCollision { - #[inline] - fn from_rapier(c: rapier::control::CharacterCollision) -> Self { - let trans_applied: $crate::na::SVector = c.translation_applied.into(); - let trans_remaining: $crate::na::SVector = c.translation_remaining.into(); - let iso: $crate::na::Isometry = c.character_pos.into(); - Self { - handle: ColliderHandle(c.handle), - character_pos: $Iso(iso), - translation_applied: $Vec(trans_applied), - translation_remaining: $Vec(trans_remaining), - toi: ShapeCastHit::from_parry(c.hit), - } - } - #[inline] - fn to_rapier(&self) -> rapier::control::CharacterCollision { - let iso: rapier::math::Pose = self.character_pos.0.into(); - let ta: rapier::math::Vector = self.translation_applied.0.into(); - let tr: rapier::math::Vector = self.translation_remaining.0.into(); - rapier::control::CharacterCollision { - handle: self.handle.0, - character_pos: iso, - translation_applied: ta, - translation_remaining: tr, - hit: rapier::parry::query::ShapeCastHit { - time_of_impact: self.toi.time_of_impact, - witness1: self.toi.witness1.0.coords.into(), - witness2: self.toi.witness2.0.coords.into(), - normal1: $crate::na::Unit::new_unchecked(self.toi.normal1.0).into_inner().into(), - normal2: $crate::na::Unit::new_unchecked(self.toi.normal2.0).into_inner().into(), - status: match self.toi.status { - ShapeCastStatus::OUT_OF_ITERATIONS => rapier::parry::query::ShapeCastStatus::OutOfIterations, - ShapeCastStatus::CONVERGED => rapier::parry::query::ShapeCastStatus::Converged, - ShapeCastStatus::FAILED => rapier::parry::query::ShapeCastStatus::Failed, - ShapeCastStatus::PENETRATING_OR_WITHIN_TARGET_DIST => rapier::parry::query::ShapeCastStatus::PenetratingOrWithinTargetDist, - }, - }, - } - } - } - - // --------------------------------------------------------------- - // KinematicCharacterController - // --------------------------------------------------------------- - /// Kinematic character controller (collision-aware shape mover). - /// - /// Drives a kinematic-position body / shape through a scene while - /// resolving collisions via sliding, auto-step (small obstacles), - /// and ground snapping (so the character sticks to descending - /// slopes). The controller itself owns no body — call - /// :meth:`move_shape` each frame with the desired translation and - /// then apply the returned :class:`EffectiveCharacterMovement` - /// to your body. - /// - /// Example:: - /// - /// controller = KinematicCharacterController(offset=0.01) - /// mv = controller.move_shape( - /// dt, bodies, colliders, queries, shape, pose, - /// desired_translation - /// ) - /// # then translate your kinematic body by mv.translation - #[pyclass(name = "KinematicCharacterController", module = "rapier")] - #[derive(Debug, Clone)] - pub struct KinematicCharacterController(pub rapier::control::KinematicCharacterController); - - #[pymethods] - impl KinematicCharacterController { - /// Build a controller with sensible defaults plus optional overrides. - /// - /// :param up: World-space up direction. - /// :param offset: Skin-width kept between the character shape - /// and obstacles (:class:`CharacterLength` or raw float). - /// :param slide: Whether to slide along obstacles on contact. - /// :param autostep: :class:`CharacterAutostep` config, or - /// ``None`` to disable. - /// :param max_slope_climb_angle: Slopes steeper than this - /// (radians) cannot be climbed. - /// :param min_slope_slide_angle: Slopes steeper than this - /// (radians) cause the character to slide down. - /// :param snap_to_ground: Distance below the character that - /// should be treated as still grounded after walking off - /// a small drop. ``None`` disables. - /// :param normal_nudge_factor: Small bias along contact - /// normals to avoid getting stuck on geometry. - #[new] - #[pyo3(signature = ( - up=None, - offset=None, - slide=None, - autostep=None, - max_slope_climb_angle=None, - min_slope_slide_angle=None, - snap_to_ground=None, - normal_nudge_factor=None, - ))] - #[allow(clippy::too_many_arguments)] - fn new( - py: Python<'_>, - up: Option, - offset: Option<&Bound<'_, PyAny>>, - slide: Option, - autostep: Option<&Bound<'_, PyAny>>, - max_slope_climb_angle: Option, - min_slope_slide_angle: Option, - snap_to_ground: Option<&Bound<'_, PyAny>>, - normal_nudge_factor: Option, - ) -> PyResult { - let _ = py; - let mut inner = rapier::control::KinematicCharacterController::default(); - if let Some(u) = up { inner.up = u.0.into(); } - if let Some(o) = offset { inner.offset = _extract_char_length(o)?.to_rapier(); } - if let Some(s) = slide { inner.slide = s; } - if let Some(a) = autostep { - if a.is_none() { - inner.autostep = None; - } else { - let auto: CharacterAutostep = a.extract()?; - inner.autostep = Some(auto.to_rapier()); - } - } - if let Some(v) = max_slope_climb_angle { inner.max_slope_climb_angle = v; } - if let Some(v) = min_slope_slide_angle { inner.min_slope_slide_angle = v; } - if let Some(s) = snap_to_ground { - if s.is_none() { - inner.snap_to_ground = None; - } else { - inner.snap_to_ground = Some(_extract_char_length(s)?.to_rapier()); - } - } - if let Some(v) = normal_nudge_factor { inner.normal_nudge_factor = v; } - Ok(Self(inner)) - } - - // ---- property getters / setters ---- - /// World-space "up" direction the controller uses to classify - /// slopes, ground contacts, and the auto-step axis. - #[getter] - fn up(&self) -> $Vec { - let v: $crate::na::SVector = self.0.up.into(); - $Vec(v) - } - #[setter] - /// Set the world-space up direction used by the controller. - fn set_up(&mut self, v: PyVector) { self.0.up = v.0.into(); } - - /// Skin width kept between the character and the environment. - #[getter] - fn offset(&self) -> CharacterLength { CharacterLength::from_rapier(self.0.offset) } - #[setter] - /// Set the controller's collision-skin offset. - /// - /// Accepts a :class:`CharacterLength` or a bare float - /// (interpreted as absolute world units). - fn set_offset(&mut self, py: Python<'_>, obj: Py) -> PyResult<()> { - let b = obj.bind(py); - self.0.offset = _extract_char_length(b)?.to_rapier(); - Ok(()) - } - - /// Whether to slide along contact surfaces. - #[getter] fn slide(&self) -> bool { self.0.slide } - #[setter] fn set_slide(&mut self, v: bool) { self.0.slide = v; } - - /// Optional :class:`CharacterAutostep` config (``None`` if disabled). - #[getter] - fn autostep(&self) -> Option { - self.0.autostep.map(|a| CharacterAutostep { - max_height: CharacterLength::from_rapier(a.max_height), - min_width: CharacterLength::from_rapier(a.min_width), - include_dynamic_bodies: a.include_dynamic_bodies, - }) - } - #[setter] - /// Set or clear the :class:`CharacterAutostep` configuration. - /// - /// Pass ``None`` to disable auto-step. - fn set_autostep(&mut self, py: Python<'_>, v: Option>) -> PyResult<()> { - self.0.autostep = match v { - None => None, - Some(obj) => { - let b = obj.bind(py); - if b.is_none() { None } else { - let a: CharacterAutostep = b.extract()?; - Some(a.to_rapier()) - } - } - }; - Ok(()) - } - - /// Max slope angle (radians) the character can still climb. - #[getter] fn max_slope_climb_angle(&self) -> Real { self.0.max_slope_climb_angle } - #[setter] fn set_max_slope_climb_angle(&mut self, v: Real) { self.0.max_slope_climb_angle = v; } - - /// Min slope angle (radians) at which the character will slide - /// downward (steeper than ``max_slope_climb_angle``). - #[getter] fn min_slope_slide_angle(&self) -> Real { self.0.min_slope_slide_angle } - #[setter] fn set_min_slope_slide_angle(&mut self, v: Real) { self.0.min_slope_slide_angle = v; } - - /// Distance below the character treated as still grounded for - /// snap-down on stairs / descending slopes. ``None`` disables. - #[getter] - fn snap_to_ground(&self) -> Option { - self.0.snap_to_ground.map(CharacterLength::from_rapier) - } - #[setter] - /// Set or clear the snap-to-ground distance. - /// - /// Accepts a :class:`CharacterLength`, a bare float (absolute), - /// or ``None`` to disable snapping. - fn set_snap_to_ground(&mut self, py: Python<'_>, v: Option>) -> PyResult<()> { - self.0.snap_to_ground = match v { - None => None, - Some(obj) => { - let b = obj.bind(py); - if b.is_none() { None } else { Some(_extract_char_length(b)?.to_rapier()) } - } - }; - Ok(()) - } - - /// Small nudge along contact normals to avoid sticking on geometry. - #[getter] fn normal_nudge_factor(&self) -> Real { self.0.normal_nudge_factor } - #[setter] fn set_normal_nudge_factor(&mut self, v: Real) { self.0.normal_nudge_factor = v; } - - // ---- methods ---- - - /// Compute the effective collision-aware translation for one step. - /// - /// Performs sweep tests through ``queries``, applies sliding, - /// optional auto-step and ground snapping, and returns the - /// translation actually applicable to the kinematic body. - /// - /// The ``bodies`` / ``colliders`` arguments are accepted for - /// API parity with the Rust API; the actual sweep uses the - /// sets already referenced by ``queries``. - /// - /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (kept for API parity). - /// :param colliders: Collider set (kept for API parity). - /// :param queries: Up-to-date :class:`QueryPipeline`. - /// :param shape: Character collision shape. - /// :param shape_pos: World pose of the shape at frame start. - /// :param desired_translation: Desired translation this step. - /// :param filter: Optional query filter. - /// :param events_callback: Optional callable receiving each - /// :class:`CharacterCollision` as it is detected. - /// :returns: :class:`EffectiveCharacterMovement`. - #[pyo3(signature = ( - dt, bodies, colliders, queries, shape, shape_pos, desired_translation, - filter=None, events_callback=None - ))] - #[allow(clippy::too_many_arguments)] - fn move_shape( - &self, - py: Python<'_>, - dt: Real, - bodies: &Bound<'_, PyAny>, - colliders: &Bound<'_, PyAny>, - queries: &QueryPipeline, - shape: &SharedShape, - shape_pos: PyIsometry, - desired_translation: PyVector, - filter: Option<&QueryFilter>, - events_callback: Option>, - ) -> PyResult { - let _ = (bodies, colliders); // parity-only; queries owns them - let pose: rapier::math::Pose = shape_pos.0.into(); - let desired: rapier::math::Vector = desired_translation.0.into(); - let inner = &self.0; - let cb_obj = events_callback.map(|o| o.clone_ref(py)); - let cb_err: std::cell::RefCell> = - std::cell::RefCell::new(None); - - let result = $crate::__qp_query!(queries, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - let mv = inner.move_shape( - dt, - &qp, - &*shape.0.0, - &pose, - desired, - |collision| { - if let Some(ref obj) = cb_obj { - let py_col = CharacterCollision::from_rapier(collision); - if let Err(e) = obj.call1(py, (py_col,)) { - *cb_err.borrow_mut() = Some(e); - } - } - }, - ); - let t: $crate::na::SVector = mv.translation.into(); - Ok::(EffectiveCharacterMovement { - translation: $Vec(t), - grounded: mv.grounded, - is_sliding_down_slope: mv.is_sliding_down_slope, - }) - })?; - if let Some(e) = cb_err.into_inner() { - return Err(e); - } - Ok(result) - } - - /// Push the dynamic bodies the character touched this frame. - /// - /// Iterates over the :class:`CharacterCollision` events - /// previously delivered by :meth:`move_shape` and applies - /// reactive impulses sized for ``character_mass`` onto any - /// dynamic colliders found. - /// - /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). - /// :param queries: Up-to-date :class:`QueryPipeline`. - /// :param character_shape: Character collision shape. - /// :param character_pos: World pose (currently unused; kept for - /// forward-compat with upstream). - /// :param character_mass: Mass used to scale impulses. - /// :param collisions: List of :class:`CharacterCollision`. - /// :param filter: Optional query filter. - #[pyo3(signature = ( - dt, bodies, colliders, queries, character_shape, character_pos, - character_mass, collisions, filter=None - ))] - #[allow(clippy::too_many_arguments)] - fn solve_character_collision_impulses( - &self, - py: Python<'_>, - dt: Real, - bodies: Py, - colliders: Py, - queries: &QueryPipeline, - character_shape: &SharedShape, - character_pos: PyIsometry, - character_mass: Real, - collisions: Vec, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - let _ = character_pos; - // Build an owned QueryPipelineMut. Since QueryPipelineMut holds - // `&mut RigidBodySet` and `&mut ColliderSet`, we borrow_mut the - // Py<> handles. - let bp = queries.broad_phase.borrow(py); - let np = queries.narrow_phase.borrow(py); - let mut bodies_ref = bodies.borrow_mut(py); - let mut colliders_ref = colliders.borrow_mut(py); - let qf = filter.map(|f| f.as_rapier(None)) - .unwrap_or_else(rapier::pipeline::QueryFilter::default); - let mut qpmut = bp.0.as_query_pipeline_mut( - np.0.query_dispatcher(), - &mut bodies_ref.0, - &mut colliders_ref.0, - qf, - ); - let collisions_rs: Vec<_> = collisions.iter().map(|c| c.to_rapier()).collect(); - self.0.solve_character_collision_impulses( - dt, - &mut qpmut, - &*character_shape.0.0, - character_mass, - collisions_rs.iter(), - ); - Ok(()) - } - - /// Return a debug string summarizing the controller config. - fn __repr__(&self) -> String { - format!( - "KinematicCharacterController(up={:?}, offset={:?}, slide={}, autostep={}, max_slope_climb_angle={}, min_slope_slide_angle={}, snap_to_ground={})", - self.0.up, - self.0.offset, - self.0.slide, - if self.0.autostep.is_some() { "set" } else { "None" }, - self.0.max_slope_climb_angle, - self.0.min_slope_slide_angle, - if self.0.snap_to_ground.is_some() { "set" } else { "None" }, - ) - } - } - - // --------------------------------------------------------------- - // AxesMask (controllers use this, distinct from LockedAxes). - // - // Upstream `AxesMask` is dim-aware: LIN_X, LIN_Y, (LIN_Z, ANG_X, - // ANG_Y only in 3D), ANG_Z (in both dims). - // --------------------------------------------------------------- - /// Bitflags selecting which DOFs a PID/PD controller drives. - /// - /// In 3D the bits are ``LIN_X``, ``LIN_Y``, ``LIN_Z``, ``ANG_X``, - /// ``ANG_Y``, ``ANG_Z``. In 2D only ``LIN_X``, ``LIN_Y``, and - /// ``ANG_Z`` exist (other axes are absent from upstream and not - /// exposed as class attributes). - /// - /// Combine bits with the usual ``|``, ``&``, ``^``, ``-``, ``~`` - /// operators. Distinct from :class:`LockedAxes`, which freezes - /// DOFs on a rigid body. - #[pyclass(name = "AxesMask", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct AxesMask(pub rapier::dynamics::AxesMask); - - #[pymethods] - impl AxesMask { - /// Build an ``AxesMask`` from a raw bit pattern. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier::dynamics::AxesMask::from_bits_truncate(bits)) - } - /// Return the empty mask (no axes). - #[staticmethod] fn empty() -> Self { Self(rapier::dynamics::AxesMask::empty()) } - /// Return the mask with every dim-valid axis set. - #[staticmethod] fn all_axes() -> Self { Self(rapier::dynamics::AxesMask::all()) } - // For consistency with LockedAxes: - /// Alias of :meth:`all_axes` for parity with :class:`LockedAxes`. - #[staticmethod] fn all() -> Self { Self(rapier::dynamics::AxesMask::all()) } - - #[classattr] - const LIN_X: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_X); - #[classattr] - const LIN_Y: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_Y); - #[classattr] - const ANG_Z: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_Z); - - /// Raw bits as an unsigned int. - #[getter] fn bits(&self) -> u8 { self.0.bits() } - /// ``True`` if ``self`` is a superset of ``other``. - fn contains(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// ``True`` if no bits are set. - fn is_empty(&self) -> bool { self.0.is_empty() } - /// Python ``in`` operator: alias of :meth:`contains`. - fn __contains__(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Bitwise OR: union of two masks. - fn __or__(&self, other: &Self) -> Self { Self(self.0 | other.0) } - /// Bitwise AND: intersection of two masks. - fn __and__(&self, other: &Self) -> Self { Self(self.0 & other.0) } - /// Bitwise XOR: symmetric difference of two masks. - fn __xor__(&self, other: &Self) -> Self { Self(self.0 ^ other.0) } - /// Bitwise NOT: complement within the valid axes for this dim. - fn __invert__(&self) -> Self { Self(!self.0) } - /// Set subtraction. - fn __sub__(&self, other: &Self) -> Self { Self(self.0 - other.0) } - /// ``False`` iff no bits are set. - fn __bool__(&self) -> bool { !self.0.is_empty() } - /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("AxesMask supports only == and !=")), - } - } - /// Hash by raw bit pattern (consistent with ``==``). - fn __hash__(&self) -> u64 { self.0.bits() as u64 } - /// Return ``AxesMask(bits=0b...)`` repr. - fn __repr__(&self) -> String { format!("AxesMask(bits={:#010b})", self.0.bits()) } - } - - // Dim-specific class attributes — multiple-pymethods is enabled, so - // we can have a second `impl AxesMask` block here. - $crate::__axes_mask_dim_consts!($dim); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __axes_mask_dim_consts { - (3) => { - #[pymethods] - impl AxesMask { - #[classattr] - const LIN_Z: AxesMask = AxesMask(rapier::dynamics::AxesMask::LIN_Z); - #[classattr] - const ANG_X: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_X); - #[classattr] - const ANG_Y: AxesMask = AxesMask(rapier::dynamics::AxesMask::ANG_Y); - } - }; - (2) => {}; -} - -// ============================================================================ -// PID/PD controller (shared, but uses dim-specific angular type). -// ============================================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_controllers_pid { - ($dim:tt, $Vec:ident) => { - // PdErrors — position or velocity error pair. - /// Linear + angular error pair fed to :class:`PidController` / - /// :class:`PdController`. - /// - /// Used both as a "position error" (desired minus current pose) - /// and as a "velocity error" (desired minus current velocity). - /// In 3D ``angular`` is a 3-vector; in 2D it is a single ``float``. - /// - /// :ivar linear: Linear error vector. - /// :ivar angular: Angular error (vector in 3D, scalar in 2D). - #[pyclass(name = "PdErrors", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PdErrors { - #[pyo3(get, set)] - pub linear: $Vec, - #[pyo3(get, set)] - pub angular: $crate::__pid_angvec_field!($dim), - } - - #[pymethods] - impl PdErrors { - /// Build a ``PdErrors`` from optional ``linear`` / ``angular`` values. - /// - /// :param linear: Linear error (defaults to zero). - /// :param angular: Angular error (defaults to zero). - #[new] - #[pyo3(signature = (linear=None, angular=None))] - fn new(linear: Option, angular: Option<&Bound<'_, PyAny>>) -> PyResult { - let lin = linear - .map(|v| v.0) - .unwrap_or_else($crate::na::SVector::::zeros); - let ang = $crate::__pid_angvec_extract!($dim, angular)?; - Ok(Self { - linear: $Vec(lin), - angular: ang, - }) - } - /// Return the ``PdErrors(linear=..., angular=...)`` repr. - fn __repr__(&self) -> String { - format!( - "PdErrors(linear={:?}, angular={:?})", - self.linear.0, self.angular - ) - } - } - - impl PdErrors { - #[inline] - fn to_rapier(&self) -> rapier::control::PdErrors { - rapier::control::PdErrors { - linear: self.linear.0.into(), - angular: $crate::__pid_angvec_to_engine!($dim, self.angular), - } - } - #[inline] - fn from_rapier(p: rapier::control::PdErrors) -> Self { - let l: $crate::na::SVector = p.linear.into(); - Self { - linear: $Vec(l), - angular: $crate::__pid_angvec_from_engine!($dim, p.angular), - } - } - } - - // PidCorrection — linvel/angvel update produced by a controller. - /// Linear + angular velocity correction returned by PID/PD updates. - /// - /// The caller is expected to apply this as an impulse / velocity - /// delta to the controlled rigid body each step. - /// - /// :ivar linear: Linear velocity correction. - /// :ivar angular: Angular velocity correction (vector in 3D, - /// scalar in 2D). - #[pyclass(name = "PidCorrection", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct PidCorrection { - #[pyo3(get)] - pub linear: $Vec, - #[pyo3(get)] - pub angular: $crate::__pid_angvec_field!($dim), - } - - #[pymethods] - impl PidCorrection { - /// Return the ``PidCorrection(linear=..., angular=...)`` repr. - fn __repr__(&self) -> String { - format!( - "PidCorrection(linear={:?}, angular={:?})", - self.linear.0, self.angular - ) - } - } - - impl PidCorrection { - #[inline] - fn from_velocity(v: rapier::dynamics::RigidBodyVelocity) -> Self { - let lin: $crate::na::SVector = v.linvel.into(); - Self { - linear: $Vec(lin), - angular: $crate::__pid_angvec_from_engine!($dim, v.angvel), - } - } - } - - // PdController — immutable PD. - /// Proportional-derivative controller (PD). - /// - /// Like :class:`PidController` but without the integral term — - /// cheaper per step, no integral wind-up, but cannot eliminate - /// steady-state error. Choose this when target tracking is - /// frequent and the loop is well-damped. - #[pyclass(name = "PdController", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PdController(pub rapier::control::PdController); - - #[pymethods] - impl PdController { - /// Build a PD controller with optional per-axis ``Kp`` / ``Kd``. - /// - /// :param axes: :class:`AxesMask` selecting which DOFs to - /// control (defaults to all). - /// :param Kp: Proportional gain — scalar (broadcast) or a - /// vector matching linear-axis count. - /// :param Kd: Derivative gain — same shape as ``Kp``. - #[new] - #[pyo3(signature = (axes=None, Kp=None, Kd=None))] - #[allow(non_snake_case)] - fn new( - axes: Option<&AxesMask>, - Kp: Option<&Bound<'_, PyAny>>, - Kd: Option<&Bound<'_, PyAny>>, - ) -> PyResult { - let am = axes - .map(|a| a.0) - .unwrap_or_else(rapier::dynamics::AxesMask::all); - let mut inner = rapier::control::PdController::new(60.0 as Real, 0.8 as Real, am); - if let Some(k) = Kp { - let v: PyVector = k.extract()?; - inner.lin_kp = v.0.into(); - let ang_kp = $crate::__pid_angvec_splat!($dim, k)?; - inner.ang_kp = ang_kp; - } - if let Some(k) = Kd { - let v: PyVector = k.extract()?; - inner.lin_kd = v.0.into(); - let ang_kd = $crate::__pid_angvec_splat!($dim, k)?; - inner.ang_kd = ang_kd; - } - Ok(Self(inner)) - } - - /// :class:`AxesMask` selecting which DOFs are driven. - #[getter] - fn axes(&self) -> AxesMask { - AxesMask(self.0.axes) - } - #[setter] - /// Replace the :class:`AxesMask` selecting controlled DOFs. - fn set_axes(&mut self, v: &AxesMask) { - self.0.axes = v.0; - } - - /// Set the per-linear-axis proportional gain vector. - fn set_axes_kp(&mut self, v: PyVector) { - self.0.lin_kp = v.0.into(); - } - /// Set the per-linear-axis derivative gain vector. - fn set_axes_kd(&mut self, v: PyVector) { - self.0.lin_kd = v.0.into(); - } - - /// Velocity correction toward ``target_pose`` (zero target velocity). - /// - /// :param body: :class:`RigidBody` to drive. - /// :param target_pose: Desired world pose. - /// :returns: :class:`PidCorrection` to apply as a velocity delta. - #[pyo3(signature = (body, target_pose))] - fn rigid_body_correction( - &self, - py: Python<'_>, - body: &Bound<'_, PyAny>, - target_pose: PyIsometry, - ) -> PyResult { - let _ = py; - let rb = body.extract::>()?; - let target_pose: rapier::math::Pose = target_pose.0.into(); - let zero_vels = rapier::dynamics::RigidBodyVelocity::::zero(); - let owned = rb.to_owned_body(); - let corr = self.0.rigid_body_correction(&owned, target_pose, zero_vels); - Ok(PidCorrection::from_velocity(corr)) - } - - /// Velocity correction from explicit pose / velocity errors. - /// - /// :param pose_errors: Position error pair. - /// :param vel_errors: Velocity error pair. - /// :returns: :class:`PidCorrection`. - fn correction(&self, pose_errors: &PdErrors, vel_errors: &PdErrors) -> PidCorrection { - let pe = pose_errors.to_rapier(); - let ve = vel_errors.to_rapier(); - let c = self.0.correction(&pe, &ve); - PidCorrection::from_velocity(c) - } - } - - // PidController — full PID. - /// Proportional-integral-derivative controller (PID). - /// - /// Drives a rigid body toward a target pose along the DOFs picked - /// by an :class:`AxesMask`. Gains ``Kp`` / ``Ki`` / ``Kd`` may be - /// specified per axis (vector) or as a scalar broadcast to every - /// driven axis. Use :meth:`rigid_body_correction` per step to get - /// the velocity correction to apply. - #[pyclass(name = "PidController", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PidController(pub rapier::control::PidController); - - #[pymethods] - impl PidController { - /// Build a PID with optional per-axis ``Kp`` / ``Ki`` / ``Kd``. - /// - /// :param axes: :class:`AxesMask` selecting controlled DOFs. - /// :param Kp: Proportional gain (scalar or vector). - /// :param Ki: Integral gain (scalar or vector). - /// :param Kd: Derivative gain (scalar or vector). - #[new] - #[pyo3(signature = (axes=None, Kp=None, Ki=None, Kd=None))] - #[allow(non_snake_case)] - fn new( - axes: Option<&AxesMask>, - Kp: Option<&Bound<'_, PyAny>>, - Ki: Option<&Bound<'_, PyAny>>, - Kd: Option<&Bound<'_, PyAny>>, - ) -> PyResult { - let am = axes - .map(|a| a.0) - .unwrap_or_else(rapier::dynamics::AxesMask::all); - let mut inner = - rapier::control::PidController::new(60.0 as Real, 1.0 as Real, 0.8 as Real, am); - if let Some(k) = Kp { - let v: PyVector = k.extract()?; - inner.pd.lin_kp = v.0.into(); - inner.pd.ang_kp = $crate::__pid_angvec_splat!($dim, k)?; - } - if let Some(k) = Kd { - let v: PyVector = k.extract()?; - inner.pd.lin_kd = v.0.into(); - inner.pd.ang_kd = $crate::__pid_angvec_splat!($dim, k)?; - } - if let Some(k) = Ki { - let v: PyVector = k.extract()?; - inner.lin_ki = v.0.into(); - inner.ang_ki = $crate::__pid_angvec_splat!($dim, k)?; - } - Ok(Self(inner)) - } - - /// :class:`AxesMask` selecting which DOFs are driven. - #[getter] - fn axes(&self) -> AxesMask { - AxesMask(self.0.axes()) - } - #[setter] - /// Replace the :class:`AxesMask` selecting controlled DOFs. - fn set_axes_attr(&mut self, v: &AxesMask) { - self.0.set_axes(v.0); - } - - /// Set the per-linear-axis proportional gain vector. - fn set_axes_kp(&mut self, v: PyVector) { - self.0.pd.lin_kp = v.0.into(); - } - /// Set the per-linear-axis integral gain vector. - fn set_axes_ki(&mut self, v: PyVector) { - self.0.lin_ki = v.0.into(); - } - /// Set the per-linear-axis derivative gain vector. - fn set_axes_kd(&mut self, v: PyVector) { - self.0.pd.lin_kd = v.0.into(); - } - - /// Reset the integral accumulators to zero. - fn reset(&mut self) { - self.0.reset_integrals(); - } - - /// One PID step: velocity correction toward ``target_pose``. - /// - /// :param dt: Time step in seconds. - /// :param body: :class:`RigidBody` to drive. - /// :param target_pose: Desired world pose. - /// :returns: :class:`PidCorrection`. - #[pyo3(signature = (dt, body, target_pose))] - fn rigid_body_correction( - &mut self, - dt: Real, - body: &Bound<'_, PyAny>, - target_pose: PyIsometry, - ) -> PyResult { - let rb = body.extract::>()?; - let pose: rapier::math::Pose = target_pose.0.into(); - let zero_vels = rapier::dynamics::RigidBodyVelocity::::zero(); - let owned = rb.to_owned_body(); - let corr = self.0.rigid_body_correction(dt, &owned, pose, zero_vels); - Ok(PidCorrection::from_velocity(corr)) - } - - /// PID correction from two poses (no rigid-body needed). - /// - /// Useful for offline / off-graph usage where you only have - /// the current and target world poses. Assumes zero current - /// and target velocities. - /// - /// :param dt: Time step in seconds. - /// :param position: Current world pose. - /// :param target_position: Desired world pose. - /// :returns: :class:`PidCorrection`. - #[pyo3(signature = (dt, position, target_position))] - fn position_correction( - &mut self, - dt: Real, - position: PyIsometry, - target_position: PyIsometry, - ) -> PyResult { - let pos: rapier::math::Pose = position.0.into(); - let tgt: rapier::math::Pose = target_position.0.into(); - // Compute pose errors manually using the same formula upstream - // uses (RigidBodyPosition::pose_errors with COM = origin). - let rb_pos = rapier::dynamics::RigidBodyPosition { - position: pos, - next_position: tgt, - }; - let pose_errors = rb_pos.pose_errors(rapier::math::Vector::ZERO); - let zero_vels: rapier::control::PdErrors = - rapier::dynamics::RigidBodyVelocity::::zero().into(); - let corr = self.0.correction(dt, &pose_errors, &zero_vels); - Ok(PidCorrection::from_velocity(corr)) - } - - /// Run one PID update step with explicit pose/velocity errors. - /// - /// :param dt: Time step in seconds. - /// :param pose_errors: Position error pair. - /// :param vel_errors: Velocity error pair. - /// :returns: :class:`PidCorrection`. - fn update( - &mut self, - dt: Real, - pose_errors: &PdErrors, - vel_errors: &PdErrors, - ) -> PidCorrection { - let pe = pose_errors.to_rapier(); - let ve = vel_errors.to_rapier(); - let c = self.0.correction(dt, &pe, &ve); - PidCorrection::from_velocity(c) - } - } - }; -} - -// 3D: angular = Vec3; 2D: angular = Real (bare scalar). -#[doc(hidden)] -#[macro_export] -macro_rules! __pid_angvec_field { - (3) => { - Vec3 - }; - (2) => { - Real - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __pid_angvec_extract { - (3, $obj:expr) => {{ - match $obj { - None => Ok::(Vec3($crate::na::Vector3::zeros())), - Some(o) => { - let pv: PyVector = o.extract()?; - Ok(Vec3(pv.0)) - } - } - }}; - (2, $obj:expr) => {{ - match $obj { - None => Ok::(0.0 as Real), - Some(o) => { - if let Ok(f) = o.extract::() { - Ok(f) - } else { - let v: Vec = $crate::conv::extract_floats(o, 1)?; - Ok(v[0]) - } - } - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __pid_angvec_to_engine { - (3, $v:expr) => {{ - let av: rapier::math::AngVector = $v.0.into(); - av - }}; - (2, $v:expr) => { - $v - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __pid_angvec_from_engine { - (3, $v:expr) => {{ - let r: $crate::na::Vector3 = $v.into(); - Vec3(r) - }}; - (2, $v:expr) => { - $v - }; -} - -// "Splat" a value extracted from a Python obj into the engine angular -// representation. Accepts either a single float (broadcast to all 3 axes in -// 3D) or a vector tuple. -#[doc(hidden)] -#[macro_export] -macro_rules! __pid_angvec_splat { - (3, $obj:expr) => {{ - // Accept a (x,y,z) tuple/list/Vec3 or a single float (broadcast). - let av: rapier::math::AngVector = if let Ok(f) = $obj.extract::() { - rapier::math::AngVector::splat(f) - } else { - let pv: PyVector = $obj.extract()?; - pv.0.into() - }; - Ok::(av) - }}; - (2, $obj:expr) => {{ - // Accept either a single float or a 1-tuple-like. - let av: Real = if let Ok(f) = $obj.extract::() { - f - } else if let Ok(pv) = $obj.extract::() { - // In 2D PyVector is 2 floats; use x component as the angular gain. - pv.0.x - } else { - let v: Vec = $crate::conv::extract_floats($obj, 1)?; - v[0] - }; - Ok::(av) - }}; -} - -// ============================================================================ -// 3D-only: vehicle controller. -// ============================================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_controllers_vehicle { - ($Vec:ident) => { - // --------------------------------------------------------------- - // WheelTuning - // --------------------------------------------------------------- - /// Spring / friction tuning parameters for a vehicle wheel. - /// - /// Used when adding a wheel via - /// :meth:`DynamicRayCastVehicleController.add_wheel`. - /// - /// .. note:: - /// :meth:`default` returns rapier's upstream defaults which are - /// typically *too soft* for most real-world setups — wheels tend - /// to bottom out and drift sideways. Expect to tighten - /// ``suspension_stiffness``, ``friction_slip``, and - /// ``side_friction_stiffness`` substantially. - #[pyclass(name = "WheelTuning", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct WheelTuning(pub rapier::control::WheelTuning); - - #[pymethods] - impl WheelTuning { - /// Build a ``WheelTuning`` with optional overrides. - /// - /// :param suspension_stiffness: Spring constant. - /// :param suspension_compression: Compression damping. - /// :param suspension_damping: Relaxation damping. - /// :param max_suspension_travel: Max compression travel. - /// :param side_friction_stiffness: Lateral tire grip. - /// :param friction_slip: Longitudinal friction coefficient. - /// :param max_suspension_force: Force cap on the spring. - #[new] - #[pyo3(signature = (suspension_stiffness=None, suspension_compression=None, suspension_damping=None, max_suspension_travel=None, side_friction_stiffness=None, friction_slip=None, max_suspension_force=None))] - #[allow(clippy::too_many_arguments)] - fn new( - suspension_stiffness: Option, - suspension_compression: Option, - suspension_damping: Option, - max_suspension_travel: Option, - side_friction_stiffness: Option, - friction_slip: Option, - max_suspension_force: Option, - ) -> Self { - let mut t = rapier::control::WheelTuning::default(); - if let Some(v) = suspension_stiffness { - t.suspension_stiffness = v; - } - if let Some(v) = suspension_compression { - t.suspension_compression = v; - } - if let Some(v) = suspension_damping { - t.suspension_damping = v; - } - if let Some(v) = max_suspension_travel { - t.max_suspension_travel = v; - } - if let Some(v) = side_friction_stiffness { - t.side_friction_stiffness = v; - } - if let Some(v) = friction_slip { - t.friction_slip = v; - } - if let Some(v) = max_suspension_force { - t.max_suspension_force = v; - } - Self(t) - } - - /// Return rapier's default ``WheelTuning``. - /// - /// Beware — the upstream defaults are intentionally soft and - /// often produce skating / bottoming-out behavior. Tune for - /// your vehicle. - #[staticmethod] - fn default() -> Self { - Self(rapier::control::WheelTuning::default()) - } - - /// Spring stiffness constant. - #[getter] - fn suspension_stiffness(&self) -> Real { - self.0.suspension_stiffness - } - #[setter] - /// Assign the suspension spring stiffness constant. - fn set_suspension_stiffness(&mut self, v: Real) { - self.0.suspension_stiffness = v; - } - /// Damping while the spring is compressing. - #[getter] - fn suspension_compression(&self) -> Real { - self.0.suspension_compression - } - #[setter] - /// Assign the compression-phase damping coefficient. - fn set_suspension_compression(&mut self, v: Real) { - self.0.suspension_compression = v; - } - /// Damping while the spring is relaxing. - #[getter] - fn suspension_damping(&self) -> Real { - self.0.suspension_damping - } - #[setter] - /// Assign the relaxation-phase damping coefficient. - fn set_suspension_damping(&mut self, v: Real) { - self.0.suspension_damping = v; - } - /// Max travel distance for the suspension. - #[getter] - fn max_suspension_travel(&self) -> Real { - self.0.max_suspension_travel - } - #[setter] - /// Assign the maximum suspension travel distance. - fn set_max_suspension_travel(&mut self, v: Real) { - self.0.max_suspension_travel = v; - } - /// Lateral (sideways) tire stiffness. - #[getter] - fn side_friction_stiffness(&self) -> Real { - self.0.side_friction_stiffness - } - #[setter] - /// Assign the lateral tire stiffness coefficient. - fn set_side_friction_stiffness(&mut self, v: Real) { - self.0.side_friction_stiffness = v; - } - /// Longitudinal friction coefficient. - #[getter] - fn friction_slip(&self) -> Real { - self.0.friction_slip - } - #[setter] - /// Assign the longitudinal friction coefficient. - fn set_friction_slip(&mut self, v: Real) { - self.0.friction_slip = v; - } - /// Maximum suspension spring force. - #[getter] - fn max_suspension_force(&self) -> Real { - self.0.max_suspension_force - } - #[setter] - /// Assign the cap on suspension spring force. - fn set_max_suspension_force(&mut self, v: Real) { - self.0.max_suspension_force = v; - } - - /// Return the ``WheelTuning(...)`` repr. - fn __repr__(&self) -> String { - format!( - "WheelTuning(suspension_stiffness={}, friction_slip={})", - self.0.suspension_stiffness, self.0.friction_slip - ) - } - } - - // --------------------------------------------------------------- - // RayCastInfo - // --------------------------------------------------------------- - /// Read-only per-wheel raycast hit info for the vehicle controller. - /// - /// Snapshot taken after the latest - /// :meth:`DynamicRayCastVehicleController.update_vehicle` call. - /// - /// :ivar contact_normal_ws: World-space normal at the contact. - /// :ivar contact_point_ws: World-space contact point. - /// :ivar suspension_length: Current spring length (compressed). - /// :ivar hard_point_ws: World-space wheel attachment point on chassis. - /// :ivar is_in_contact: ``True`` if the ray hit anything. - /// :ivar ground_object: Hit collider handle, if any. - #[pyclass(name = "RayCastInfo", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct RayCastInfo { - #[pyo3(get)] - pub contact_normal_ws: $Vec, - #[pyo3(get)] - pub contact_point_ws: $Vec, - #[pyo3(get)] - pub suspension_length: Real, - #[pyo3(get)] - pub hard_point_ws: $Vec, - #[pyo3(get)] - pub is_in_contact: bool, - #[pyo3(get)] - pub ground_object: Option, - } - - #[pymethods] - impl RayCastInfo { - /// Return the ``RayCastInfo(...)`` repr. - fn __repr__(&self) -> String { - format!( - "RayCastInfo(is_in_contact={}, suspension_length={})", - self.is_in_contact, self.suspension_length - ) - } - } - - impl RayCastInfo { - #[inline] - fn from_wheel(w: &rapier::control::Wheel) -> Self { - let r = w.raycast_info(); - let cn: $crate::na::Vector3 = r.contact_normal_ws.into(); - let cp: $crate::na::Vector3 = r.contact_point_ws.into(); - let hp: $crate::na::Vector3 = r.hard_point_ws.into(); - Self { - contact_normal_ws: $Vec(cn), - contact_point_ws: $Vec(cp), - suspension_length: r.suspension_length, - hard_point_ws: $Vec(hp), - is_in_contact: r.is_in_contact, - ground_object: r.ground_object.map(ColliderHandle), - } - } - } - - // --------------------------------------------------------------- - // Wheel — a snapshot/proxy. The upstream controller owns the - // backing `Vec`; we project read+write fields into a Python - // class. Mutations on a `Wheel` instance returned by - // `.wheels()` / `.wheel(i)` do NOT propagate back to the - // controller — they are detached copies. Use the controller's - // `set_brake` / `set_steering` / `apply_engine_force` for - // mutations that need to take effect. - // --------------------------------------------------------------- - /// Detached snapshot of one vehicle wheel. - /// - /// .. warning:: - /// Instances returned by - /// :meth:`DynamicRayCastVehicleController.wheels` / - /// :meth:`DynamicRayCastVehicleController.wheel` are **copies**. - /// Mutating the fields here does **not** propagate back to the - /// controller. Use - /// :meth:`DynamicRayCastVehicleController.set_brake`, - /// :meth:`set_steering`, and :meth:`apply_engine_force` to - /// actually drive the wheel. - #[pyclass(name = "Wheel", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct Wheel { - #[pyo3(get, set)] - pub chassis_connection_point_cs: $Vec, - #[pyo3(get, set)] - pub direction_cs: $Vec, - #[pyo3(get, set)] - pub axle_cs: $Vec, - #[pyo3(get, set)] - pub suspension_rest_length: Real, - #[pyo3(get, set)] - pub max_suspension_travel: Real, - #[pyo3(get, set)] - pub radius: Real, - #[pyo3(get, set)] - pub suspension_stiffness: Real, - #[pyo3(get, set)] - pub damping_compression: Real, - #[pyo3(get, set)] - pub damping_relaxation: Real, - #[pyo3(get, set)] - pub friction_slip: Real, - #[pyo3(get, set)] - pub side_friction_stiffness: Real, - #[pyo3(get, set)] - pub rotation: Real, - #[pyo3(get, set)] - pub max_suspension_force: Real, - #[pyo3(get, set)] - pub forward_impulse: Real, - #[pyo3(get, set)] - pub side_impulse: Real, - #[pyo3(get, set)] - pub steering: Real, - #[pyo3(get, set)] - pub engine_force: Real, - #[pyo3(get, set)] - pub brake: Real, - #[pyo3(get, set)] - pub wheel_suspension_force: Real, - #[pyo3(get)] - pub raycast_info: RayCastInfo, - } - - #[pymethods] - impl Wheel { - /// Return the ``Wheel(...)`` repr. - fn __repr__(&self) -> String { - format!( - "Wheel(engine_force={}, brake={}, steering={})", - self.engine_force, self.brake, self.steering - ) - } - } - - impl Wheel { - #[inline] - fn from_rapier(w: &rapier::control::Wheel) -> Self { - let ccp: $crate::na::Vector3 = w.chassis_connection_point_cs.into(); - let dir: $crate::na::Vector3 = w.direction_cs.into(); - let axle: $crate::na::Vector3 = w.axle_cs.into(); - Self { - chassis_connection_point_cs: $Vec(ccp), - direction_cs: $Vec(dir), - axle_cs: $Vec(axle), - suspension_rest_length: w.suspension_rest_length, - max_suspension_travel: w.max_suspension_travel, - radius: w.radius, - suspension_stiffness: w.suspension_stiffness, - damping_compression: w.damping_compression, - damping_relaxation: w.damping_relaxation, - friction_slip: w.friction_slip, - side_friction_stiffness: w.side_friction_stiffness, - rotation: w.rotation, - max_suspension_force: w.max_suspension_force, - forward_impulse: w.forward_impulse, - side_impulse: w.side_impulse, - steering: w.steering, - engine_force: w.engine_force, - brake: w.brake, - wheel_suspension_force: w.wheel_suspension_force, - raycast_info: RayCastInfo::from_wheel(w), - } - } - } - - // --------------------------------------------------------------- - // DynamicRayCastVehicleController - // --------------------------------------------------------------- - /// Raycast-based vehicle controller attached to a chassis body (3D only). - /// - /// Each "wheel" is a raycast from a chassis-space attachment point - /// along ``direction_cs`` for ``suspension_rest_length`` units. The - /// controller resolves spring/damper forces from each ray hit and - /// applies them to the chassis :class:`RigidBody` to simulate a - /// driving vehicle without needing actual wheel colliders. - #[pyclass(name = "DynamicRayCastVehicleController", module = "rapier")] - pub struct DynamicRayCastVehicleController( - pub rapier::control::DynamicRayCastVehicleController, - ); - - #[pymethods] - impl DynamicRayCastVehicleController { - /// Construct a vehicle controller for the given chassis body. - /// - /// :param chassis_handle: :class:`RigidBodyHandle` of the chassis - /// dynamic body. - #[new] - fn new(chassis_handle: &RigidBodyHandle) -> Self { - Self(rapier::control::DynamicRayCastVehicleController::new( - chassis_handle.0, - )) - } - - /// Append a new wheel to this controller. - /// - /// :param chassis_connection_cs: Wheel attachment point in - /// chassis-local coordinates. - /// :param direction_cs: Suspension axis (typically ``-up``). - /// :param axle_cs: Wheel spin axis in chassis-local coords. - /// :param suspension_rest_length: Spring rest length. - /// :param radius: Wheel radius (for raycast clipping). - /// :param tuning: :class:`WheelTuning` spring/friction params. - /// :returns: The index of the new wheel. - #[pyo3(signature = (chassis_connection_cs, direction_cs, axle_cs, suspension_rest_length, radius, tuning))] - #[allow(clippy::too_many_arguments)] - fn add_wheel( - &mut self, - chassis_connection_cs: PyVector, - direction_cs: PyVector, - axle_cs: PyVector, - suspension_rest_length: Real, - radius: Real, - tuning: &WheelTuning, - ) -> usize { - let ccs: rapier::math::Vector = chassis_connection_cs.0.into(); - let dir: rapier::math::Vector = direction_cs.0.into(); - let axl: rapier::math::Vector = axle_cs.0.into(); - self.0 - .add_wheel(ccs, dir, axl, suspension_rest_length, radius, &tuning.0); - self.0.wheels().len() - 1 - } - - /// Step the vehicle controller for one tick. - /// - /// Performs per-wheel raycasts through ``queries`` and updates - /// the chassis :class:`RigidBody` with the resulting forces and - /// torques. Call this before stepping the physics world. - /// - /// Requires a fresh :class:`QueryPipeline` — typically call - /// ``world.update_query_pipeline()`` first. - /// - /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). - /// :param queries: Up-to-date :class:`QueryPipeline`. - /// :param filter: Optional query filter. - #[pyo3(signature = (dt, bodies, colliders, queries, filter=None))] - fn update_vehicle( - &mut self, - py: Python<'_>, - dt: Real, - bodies: Py, - colliders: Py, - queries: &QueryPipeline, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - let bp = queries.broad_phase.borrow(py); - let np = queries.narrow_phase.borrow(py); - let mut bodies_ref = bodies.borrow_mut(py); - let mut colliders_ref = colliders.borrow_mut(py); - let qf = filter - .map(|f| f.as_rapier(None)) - .unwrap_or_else(rapier::pipeline::QueryFilter::default); - let qpmut = bp.0.as_query_pipeline_mut( - np.0.query_dispatcher(), - &mut bodies_ref.0, - &mut colliders_ref.0, - qf, - ); - self.0.update_vehicle(dt, qpmut); - Ok(()) - } - - /// Set the brake force on wheel ``idx``. - /// - /// :raises TypeError: if ``idx`` is out of range. - fn set_brake(&mut self, idx: usize, brake: Real) -> PyResult<()> { - let wheels = self.0.wheels_mut(); - if idx >= wheels.len() { - return Err(PyTypeError::new_err(format!( - "wheel index {} out of range (len={})", - idx, - wheels.len() - ))); - } - wheels[idx].brake = brake; - Ok(()) - } - - /// Set the steering angle (radians) on wheel ``idx``. - /// - /// :raises TypeError: if ``idx`` is out of range. - fn set_steering(&mut self, idx: usize, steering: Real) -> PyResult<()> { - let wheels = self.0.wheels_mut(); - if idx >= wheels.len() { - return Err(PyTypeError::new_err(format!( - "wheel index {} out of range (len={})", - idx, - wheels.len() - ))); - } - wheels[idx].steering = steering; - Ok(()) - } - - /// Set the engine drive force on wheel ``idx``. - /// - /// :raises TypeError: if ``idx`` is out of range. - fn apply_engine_force(&mut self, idx: usize, force: Real) -> PyResult<()> { - let wheels = self.0.wheels_mut(); - if idx >= wheels.len() { - return Err(PyTypeError::new_err(format!( - "wheel index {} out of range (len={})", - idx, - wheels.len() - ))); - } - wheels[idx].engine_force = force; - Ok(()) - } - - /// Return a fresh list of :class:`Wheel` snapshots (detached copies). - fn wheels(&self) -> Vec { - self.0.wheels().iter().map(Wheel::from_rapier).collect() - } - - /// Return a snapshot of the single wheel at ``idx``. - /// - /// :raises TypeError: if ``idx`` is out of range. - fn wheel(&self, idx: usize) -> PyResult { - let wheels = self.0.wheels(); - wheels.get(idx).map(Wheel::from_rapier).ok_or_else(|| { - PyTypeError::new_err(format!( - "wheel index {} out of range (len={})", - idx, - wheels.len() - )) - }) - } - - /// Current forward speed in km/h. - fn current_speed_km_hour(&self) -> Real { - // upstream's `current_vehicle_speed` is in m/s; convert to km/h. - self.0.current_vehicle_speed * (3.6 as Real) - } - - /// Current forward speed in m/s along the chassis forward axis. - #[getter] - fn current_vehicle_speed(&self) -> Real { - self.0.current_vehicle_speed - } - - /// :class:`RigidBodyHandle` of the chassis body. - fn chassis(&self) -> RigidBodyHandle { - RigidBodyHandle(self.0.chassis) - } - - /// Index of the up-axis in chassis-local coords (``0=X``, ``1=Y``, ``2=Z``). - #[getter] - fn index_up_axis(&self) -> usize { - self.0.index_up_axis - } - #[setter] - /// Set the chassis-local up-axis index (``0=X``, ``1=Y``, ``2=Z``). - fn set_index_up_axis(&mut self, v: usize) { - self.0.index_up_axis = v; - } - /// Index of the forward-axis in chassis-local coords (``0=X``, ``1=Y``, ``2=Z``). - #[getter] - fn index_forward_axis(&self) -> usize { - self.0.index_forward_axis - } - #[setter] - /// Set the chassis-local forward-axis index (``0=X``, ``1=Y``, ``2=Z``). - fn set_index_forward_axis(&mut self, v: usize) { - self.0.index_forward_axis = v; - } - - /// Return a debug string with the wheel count and forward speed. - fn __repr__(&self) -> String { - format!( - "DynamicRayCastVehicleController(wheels={}, speed={} m/s)", - self.0.wheels().len(), - self.0.current_vehicle_speed - ) - } - } - }; -} - -// ============================================================================ -// Per-cdylib registration. -// ============================================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_controllers_register { - (3) => { - pub fn register_controllers( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; - (2) => { - pub fn register_controllers( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/conv.rs b/python/rapier-py-core/src/conv.rs deleted file mode 100644 index 7d5e7164d..000000000 --- a/python/rapier-py-core/src/conv.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! `FromPyObject` shims for the math primitives every binding consumes. -//! -//! These types are **input adapters**: they implement `FromPyObject` so that -//! `#[pyfunction] fn foo(v: PyVector)` will accept tuples, lists, NumPy -//! arrays, and other PyVector instances of the right shape. -//! -//! The dim/scalar split is handled by the `define_conv_types!` macro at the -//! bottom of this file. Each cdylib invokes it once near the top of its -//! `src/lib.rs` with concrete `Real`/`DIM` parameters; the macro emits -//! locally-defined `PyVector`, `PyPoint`, `PyRotation`, `PyIsometry`, -//! `PyAngVector` types in the cdylib's namespace. -//! -//! Helper functions here are generic and re-used by the macro expansion. - -use numpy::PyReadonlyArray1; -use pyo3::prelude::*; -use pyo3::types::PySequence; - -/// Extract `expected` floats from a Python tuple/list/sequence. -pub fn extract_floats_from_sequence(obj: &Bound<'_, PyAny>, expected: usize) -> PyResult> -where - R: Copy + for<'a> FromPyObject<'a>, -{ - let seq = obj.downcast::().map_err(|_| { - pyo3::exceptions::PyTypeError::new_err( - "expected a tuple, list, ndarray, or matching newtype", - ) - })?; - let len = seq.len()?; - if len != expected { - return Err(pyo3::exceptions::PyValueError::new_err(format!( - "expected sequence of {expected} floats, got length {len}" - ))); - } - let mut out = Vec::with_capacity(expected); - for i in 0..expected { - let item = seq.get_item(i)?; - out.push(item.extract::()?); - } - Ok(out) -} - -/// Extract `expected` floats from a 1-D NumPy ndarray (accepts f32 or f64 -/// dtype and casts to the requested scalar). Returns `None` if `obj` is not -/// an ndarray at all. -pub fn extract_floats_from_ndarray( - obj: &Bound<'_, PyAny>, - expected: usize, -) -> Option>> -where - R: num_from::FromF64 + num_from::FromF32, -{ - if let Ok(arr) = obj.extract::>() { - let slice_res = arr.as_slice(); - return Some(match slice_res { - Ok(s) if s.len() == expected => Ok(s.iter().map(|x| R::from_f64(*x)).collect()), - Ok(s) => Err(pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray of length {expected}, got {}", - s.len() - ))), - Err(_) => Err(pyo3::exceptions::PyValueError::new_err( - "ndarray is not contiguous", - )), - }); - } - if let Ok(arr) = obj.extract::>() { - let slice_res = arr.as_slice(); - return Some(match slice_res { - Ok(s) if s.len() == expected => Ok(s.iter().map(|x| R::from_f32(*x)).collect()), - Ok(s) => Err(pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray of length {expected}, got {}", - s.len() - ))), - Err(_) => Err(pyo3::exceptions::PyValueError::new_err( - "ndarray is not contiguous", - )), - }); - } - None -} - -/// Tiny `From{f32,f64}` shim so the helpers can return either scalar. -pub mod num_from { - pub trait FromF64 { - fn from_f64(x: f64) -> Self; - } - pub trait FromF32 { - fn from_f32(x: f32) -> Self; - } - - impl FromF64 for f32 { - #[inline] - fn from_f64(x: f64) -> Self { - x as f32 - } - } - impl FromF32 for f32 { - #[inline] - fn from_f32(x: f32) -> Self { - x - } - } - impl FromF64 for f64 { - #[inline] - fn from_f64(x: f64) -> Self { - x - } - } - impl FromF32 for f64 { - #[inline] - fn from_f32(x: f32) -> Self { - x as f64 - } - } -} - -/// Extract `expected` floats from any of: PySequence (tuple/list), 1-D -/// ndarray, or any Python object exposing a `to_tuple()` method that returns -/// a sequence of the right length (this covers the canonical `Vec*`/`Point*`/ -/// `Rotation*` math `#[pyclass]` types). -pub fn extract_floats(obj: &Bound<'_, PyAny>, expected: usize) -> PyResult> -where - R: Copy + num_from::FromF64 + num_from::FromF32 + for<'a> FromPyObject<'a>, -{ - if let Some(res) = extract_floats_from_ndarray::(obj, expected) { - return res; - } - if let Ok(res) = extract_floats_from_sequence::(obj, expected) { - return Ok(res); - } - // Fall back to duck-typed `.to_tuple()` so that `Vec3`/`Point3`/ - // `Rotation3`/`Quaternion` instances flow through any API that accepts - // a `PyVector`/`PyPoint`/`PyRotation`. - if let Ok(meth) = obj.getattr(pyo3::intern!(obj.py(), "to_tuple")) { - let t = meth.call0()?; - return extract_floats_from_sequence::(&t, expected); - } - Err(pyo3::exceptions::PyTypeError::new_err( - "expected a tuple, list, ndarray, or a Vec/Point/Rotation-like object", - )) -} - -// --------------------------------------------------------------------------- -// Macro: define_conv_types! -// -// Each cdylib invokes this exactly once with its concrete (Real, DIM) parameters -// to materialize the `PyVector`, `PyPoint`, `PyRotation`, `PyIsometry`, -// `PyAngVector` adapter newtypes in its own namespace. -// --------------------------------------------------------------------------- - -/// Emit `Py{Vector,Point,Rotation,Isometry,AngVector}` adapter types specialized -/// for the given `(Real, DIM)` pair. The `dim` token must be the literal `2` or -/// `3` so the macro can pick the right nalgebra constructors. -#[macro_export] -macro_rules! define_conv_types { - (Real = $real:ty, DIM = 2) => { - $crate::define_conv_types!(@common $real, 2); - - #[derive(Debug, Clone, Copy)] - pub struct PyVector(pub $crate::na::Vector2<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyVector { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 2)?; - Ok(PyVector($crate::na::Vector2::new(v[0], v[1]))) - } - } - - #[derive(Debug, Clone, Copy)] - pub struct PyPoint(pub $crate::na::Point2<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyPoint { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 2)?; - Ok(PyPoint($crate::na::Point2::new(v[0], v[1]))) - } - } - - /// In 2D, the angular vector is a bare angle in radians. - #[derive(Debug, Clone, Copy)] - pub struct PyAngVector(pub $real); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyAngVector { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - if let Ok(f) = obj.extract::<$real>() { - return Ok(PyAngVector(f)); - } - let v: Vec<$real> = $crate::conv::extract_floats(obj, 1)?; - Ok(PyAngVector(v[0])) - } - } - - #[derive(Debug, Clone, Copy)] - pub struct PyRotation(pub $crate::na::UnitComplex<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyRotation { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - if let Ok(f) = obj.extract::<$real>() { - return Ok(PyRotation($crate::na::UnitComplex::new(f))); - } - let v: Vec<$real> = $crate::conv::extract_floats(obj, 1)?; - Ok(PyRotation($crate::na::UnitComplex::new(v[0]))) - } - } - - #[derive(Debug, Clone, Copy)] - pub struct PyIsometry(pub $crate::na::Isometry2<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyIsometry { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - // First try `Isometry2`-like objects exposing `translation` - // and `rotation` attributes (the canonical `#[pyclass]`). - let py = obj.py(); - if let (Ok(t_obj), Ok(r_obj)) = ( - obj.getattr($crate::pyo3::intern!(py, "translation")), - obj.getattr($crate::pyo3::intern!(py, "rotation")), - ) { - let t: PyVector = t_obj.extract()?; - let r: PyRotation = r_obj.extract()?; - return Ok(PyIsometry($crate::na::Isometry2::from_parts( - $crate::na::Translation2::from(t.0), - r.0, - ))); - } - let (t, r): (PyVector, PyRotation) = obj.extract()?; - Ok(PyIsometry($crate::na::Isometry2::from_parts( - $crate::na::Translation2::from(t.0), - r.0, - ))) - } - } - }; - - (Real = $real:ty, DIM = 3) => { - $crate::define_conv_types!(@common $real, 3); - - #[derive(Debug, Clone, Copy)] - pub struct PyVector(pub $crate::na::Vector3<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyVector { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 3)?; - Ok(PyVector($crate::na::Vector3::new(v[0], v[1], v[2]))) - } - } - - #[derive(Debug, Clone, Copy)] - pub struct PyPoint(pub $crate::na::Point3<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyPoint { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 3)?; - Ok(PyPoint($crate::na::Point3::new(v[0], v[1], v[2]))) - } - } - - /// In 3D, the angular vector is a scaled axis-angle (3 floats). - #[derive(Debug, Clone, Copy)] - pub struct PyAngVector(pub $crate::na::Vector3<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyAngVector { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 3)?; - Ok(PyAngVector($crate::na::Vector3::new(v[0], v[1], v[2]))) - } - } - - /// Quaternion convention: `(x, y, z, w)`. - #[derive(Debug, Clone, Copy)] - pub struct PyRotation(pub $crate::na::UnitQuaternion<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyRotation { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let v: Vec<$real> = $crate::conv::extract_floats(obj, 4)?; - let q = $crate::na::Quaternion::new(v[3], v[0], v[1], v[2]); - Ok(PyRotation($crate::na::UnitQuaternion::from_quaternion(q))) - } - } - - #[derive(Debug, Clone, Copy)] - pub struct PyIsometry(pub $crate::na::Isometry3<$real>); - impl<'py> $crate::pyo3::FromPyObject<'py> for PyIsometry { - fn extract_bound(obj: &$crate::pyo3::Bound<'py, $crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { - let py = obj.py(); - if let (Ok(t_obj), Ok(r_obj)) = ( - obj.getattr($crate::pyo3::intern!(py, "translation")), - obj.getattr($crate::pyo3::intern!(py, "rotation")), - ) { - let t: PyVector = t_obj.extract()?; - let r: PyRotation = r_obj.extract()?; - return Ok(PyIsometry($crate::na::Isometry3::from_parts( - $crate::na::Translation3::from(t.0), - r.0, - ))); - } - let (t, r): (PyVector, PyRotation) = obj.extract()?; - Ok(PyIsometry($crate::na::Isometry3::from_parts( - $crate::na::Translation3::from(t.0), - r.0, - ))) - } - } - }; - - (@common $real:ty, $dim:literal) => { - /// Scalar type selected for this cdylib. - pub type Real = $real; - /// Spatial dimension selected for this cdylib. - pub const DIM: usize = $dim; - }; -} diff --git a/python/rapier-py-core/src/debug_render.rs b/python/rapier-py-core/src/debug_render.rs deleted file mode 100644 index 62c508c9d..000000000 --- a/python/rapier-py-core/src/debug_render.rs +++ /dev/null @@ -1,1244 +0,0 @@ -//! Macro emitting the user-facing debug-render `#[pyclass]`-es per cdylib. -//! -//! Debug-Render Adapter. -//! -//! Like the other modules, this macro is invoked once per cdylib AFTER -//! `define_conv_types!`, `define_math_types!`, `define_geometry_types!`, -//! `define_joints_types!`, `define_dynamics_types!`, `define_pipeline_types!`, -//! `define_events_hooks_types!` (and, for 3D, `define_loaders_types!`) and -//! `define_controllers_types!`. It depends on items emitted by all of those: -//! - `Real`/`DIM`, the `PyVector` adapter. -//! - User-facing math types (`Vec*`, `Point*`). -//! - `RigidBodySet`, `ColliderSet`, `ImpulseJointSet`, `MultibodyJointSet`, -//! `NarrowPhase`. -//! -//! `define_debug_render_types!` produces `register_debug_render(py, m)` for the -//! `#[pymodule]` entry point. -//! -//! # Cargo feature -//! -//! All four `rapier-py-*` cdylibs enable the upstream `debug-render` Cargo -//! feature unconditionally so that `DebugRenderPipeline` (and the associated -//! trait, style, mode, object enums) are available. -//! -//! # API surface -//! -//! - `DebugRenderPipeline` — wraps `rapier::pipeline::DebugRenderPipeline`, -//! plus a convenience `render_to_arrays(...)` returning NumPy arrays. -//! - `DebugLineCollector` — built-in `DebugRenderBackend` that drains all -//! draw calls into internal `Vec`s exposed as NumPy arrays (or iterated as -//! `(object, a, b, color)` tuples). -//! - `DebugRenderBackend` (Python `Protocol` — see `python/rapier/_debug_render.py`). -//! - `DebugRenderStyle` — wraps the rapier struct with every public field as a -//! mutable Python property. -//! - `DebugRenderMode` — bitflags wrapper. -//! - `DebugRenderObject` — frozen tagged-union mirror of the Rust enum -//! (`COLLIDER_SHAPE`, `RIGID_BODY_FRAME`, etc.). -//! - `DebugColor` — small frozen wrapper around `[f32; 4]` with both an HSLA -//! and an RGBA constructor (rapier stores HSLA internally — see -//! `src/pipeline/debug_render_pipeline/debug_render_style.rs`). -//! -//! # NumPy copy semantics -//! -//! Both `render_to_arrays` and `DebugLineCollector.lines()` / `.colors()` / -//! `.objects()` allocate fresh `PyArray*` buffers and **copy** the underlying -//! data. There is no zero-copy aliasing — the collector remains owned by -//! Python and can be cleared / re-used safely. - -/// Materialize the debug-render `#[pyclass]` types for a given `(Real, DIM)` pair. -#[macro_export] -macro_rules! define_debug_render_types { - (DIM = 2) => { - $crate::__define_debug_render_shared!(2, Vec2); - $crate::__define_debug_render_register!(); - }; - (DIM = 3) => { - $crate::__define_debug_render_shared!(3, Vec3); - $crate::__define_debug_render_register!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_debug_render_shared { - ($dim:tt, $Vec:ident) => { - use std::sync::{Arc as _DbgArc, Mutex as _DbgMutex}; - - // ===================================================================== - // Internal storage type. Each `DebugLine` records a single segment as - // emitted by rapier into the backend. - // ===================================================================== - - #[derive(Debug, Clone, Copy)] - struct DebugLine { - object_kind: u32, - a: $crate::na::SVector, - b: $crate::na::SVector, - color: [f32; 4], - } - - // ===================================================================== - // DebugRenderObject — tag mirror of `rapier::pipeline::DebugRenderObject`. - // - // The upstream enum carries handle + by-ref payloads; we expose only the - // discriminant + handles since the Python side has no way to safely - // borrow the referenced rapier objects after the call returns. - // ===================================================================== - - /// Tagged-union mirror of ``rapier::pipeline::DebugRenderObject``. - /// - /// Each segment emitted by :class:`DebugRenderPipeline` is tagged - /// with one of these. Use the class constants - /// :attr:`RIGID_BODY`, :attr:`COLLIDER`, :attr:`COLLIDER_AABB`, - /// :attr:`IMPULSE_JOINT`, :attr:`MULTIBODY_JOINT`, :attr:`CONTACT_PAIR` - /// for comparison. - /// - /// :ivar kind: Stable ``u32`` discriminant. - #[allow(non_camel_case_types)] - #[pyclass(name = "DebugRenderObject", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct DebugRenderObject { - #[pyo3(get)] - pub kind: u32, - } - - // Stable u32 discriminants used both as `#[classattr]` values below - // and by the `_dro_kind` helper to tag each `DebugLine`. - const _DRO_RIGID_BODY: u32 = 0; - const _DRO_COLLIDER: u32 = 1; - const _DRO_COLLIDER_AABB: u32 = 2; - const _DRO_IMPULSE_JOINT: u32 = 3; - const _DRO_MULTIBODY_JOINT: u32 = 4; - const _DRO_CONTACT_PAIR: u32 = 5; - - #[pymethods] - impl DebugRenderObject { - #[classattr] - const RIGID_BODY: DebugRenderObject = DebugRenderObject { - kind: _DRO_RIGID_BODY, - }; - #[classattr] - const COLLIDER: DebugRenderObject = DebugRenderObject { - kind: _DRO_COLLIDER, - }; - #[classattr] - const COLLIDER_AABB: DebugRenderObject = DebugRenderObject { - kind: _DRO_COLLIDER_AABB, - }; - #[classattr] - const IMPULSE_JOINT: DebugRenderObject = DebugRenderObject { - kind: _DRO_IMPULSE_JOINT, - }; - #[classattr] - const MULTIBODY_JOINT: DebugRenderObject = DebugRenderObject { - kind: _DRO_MULTIBODY_JOINT, - }; - #[classattr] - const CONTACT_PAIR: DebugRenderObject = DebugRenderObject { - kind: _DRO_CONTACT_PAIR, - }; - - /// Return ``DebugRenderObject.`` repr. - fn __repr__(&self) -> String { - let s = match self.kind { - 0 => "RIGID_BODY", - 1 => "COLLIDER", - 2 => "COLLIDER_AABB", - 3 => "IMPULSE_JOINT", - 4 => "MULTIBODY_JOINT", - 5 => "CONTACT_PAIR", - _ => "UNKNOWN", - }; - format!("DebugRenderObject.{s}") - } - /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.kind == other.kind), - CompareOp::Ne => Ok(self.kind != other.kind), - _ => Err(PyTypeError::new_err( - "DebugRenderObject supports only == and !=", - )), - } - } - /// Hash by ``kind`` discriminant (consistent with ``==``). - fn __hash__(&self) -> u64 { - self.kind as u64 - } - } - - #[inline] - fn _dro_kind(obj: rapier::pipeline::DebugRenderObject<'_>) -> u32 { - match obj { - rapier::pipeline::DebugRenderObject::RigidBody(..) => _DRO_RIGID_BODY, - rapier::pipeline::DebugRenderObject::Collider(..) => _DRO_COLLIDER, - rapier::pipeline::DebugRenderObject::ColliderAabb(..) => _DRO_COLLIDER_AABB, - rapier::pipeline::DebugRenderObject::ImpulseJoint(..) => _DRO_IMPULSE_JOINT, - rapier::pipeline::DebugRenderObject::MultibodyJoint(..) => _DRO_MULTIBODY_JOINT, - rapier::pipeline::DebugRenderObject::ContactPair(..) => _DRO_CONTACT_PAIR, - } - } - - // ===================================================================== - // DebugColor — wrapper around `[f32; 4]` (rapier's `DebugColor` alias). - // - // Rapier stores colors in HSLA. The `from_rgba` constructor converts - // from RGBA via the standard HSL transform so users don't have to - // think about it; the `.rgba` getter goes the other way. The raw HSLA - // tuple is also accessible via `.hsla` for direct interop. - // ===================================================================== - - /// Color used by the debug-render pipeline. - /// - /// Internally stored as HSLA (hue in degrees ``[0, 360)``, - /// saturation / lightness / alpha each in ``[0, 1]``) because - /// that's what rapier's :class:`DebugRenderStyle` uses. Two - /// constructors are provided — :meth:`from_hsla` and - /// :meth:`from_rgba` — plus the :attr:`rgba` getter to convert - /// back to RGBA suitable for matplotlib / pygame / etc. - #[pyclass(name = "DebugColor", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct DebugColor(pub [f32; 4]); - - #[inline] - fn _hsla_to_rgba(hsla: [f32; 4]) -> [f32; 4] { - // h in degrees [0, 360), s/l/a in [0, 1]. - let mut h = hsla[0] % 360.0; - if h < 0.0 { - h += 360.0; - } - let s = hsla[1].clamp(0.0, 1.0); - let l = hsla[2].clamp(0.0, 1.0); - let a = hsla[3]; - - let c = (1.0 - (2.0 * l - 1.0).abs()) * s; - let h_prime = h / 60.0; - let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs()); - let (r1, g1, b1) = match h_prime as u32 { - 0 => (c, x, 0.0), - 1 => (x, c, 0.0), - 2 => (0.0, c, x), - 3 => (0.0, x, c), - 4 => (x, 0.0, c), - _ => (c, 0.0, x), - }; - let m = l - c * 0.5; - [r1 + m, g1 + m, b1 + m, a] - } - - #[inline] - fn _rgba_to_hsla(rgba: [f32; 4]) -> [f32; 4] { - let r = rgba[0]; - let g = rgba[1]; - let b = rgba[2]; - let a = rgba[3]; - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let l = (max + min) * 0.5; - let d = max - min; - let s = if d == 0.0 { - 0.0 - } else { - d / (1.0 - (2.0 * l - 1.0).abs()).max(1.0e-12) - }; - let h = if d == 0.0 { - 0.0 - } else if max == r { - 60.0 * (((g - b) / d) % 6.0) - } else if max == g { - 60.0 * (((b - r) / d) + 2.0) - } else { - 60.0 * (((r - g) / d) + 4.0) - }; - let h = if h < 0.0 { h + 360.0 } else { h }; - [h, s, l, a] - } - - #[pymethods] - impl DebugColor { - /// Build a ``DebugColor`` from a raw HSLA tuple. - /// - /// :param h: Hue in degrees ``[0, 360)``. - /// :param s: Saturation in ``[0, 1]``. - /// :param l: Lightness in ``[0, 1]``. - /// :param a: Alpha in ``[0, 1]``. - #[new] - #[pyo3(signature = (h = 0.0, s = 0.0, l = 0.0, a = 1.0))] - fn new(h: f32, s: f32, l: f32, a: f32) -> Self { - Self([h, s, l, a]) - } - - /// Construct a `DebugColor` from an HSLA tuple (hue in degrees, - /// saturation/lightness/alpha in `[0, 1]`). - #[staticmethod] - fn from_hsla(h: f32, s: f32, l: f32, a: f32) -> Self { - Self([h, s, l, a]) - } - - /// Construct a `DebugColor` from an RGBA tuple (`[0, 1]` each). - /// - /// Internally converted to HSLA since rapier's `DebugRenderStyle` - /// expects HSLA tuples. - #[staticmethod] - fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { - Self(_rgba_to_hsla([r, g, b, a])) - } - - /// The raw HSLA tuple stored by this color. - #[getter] - fn hsla(&self) -> [f32; 4] { - self.0 - } - - /// The RGBA tuple after HSLA → RGBA conversion (each in `[0, 1]`). - #[getter] - fn rgba(&self) -> [f32; 4] { - _hsla_to_rgba(self.0) - } - - /// Hue component (degrees, ``[0, 360)``). - #[getter] - fn h(&self) -> f32 { - self.0[0] - } - /// Saturation component (``[0, 1]``). - #[getter] - fn s(&self) -> f32 { - self.0[1] - } - /// Lightness component (``[0, 1]``). - #[getter] - fn l(&self) -> f32 { - self.0[2] - } - /// Alpha component (``[0, 1]``). - #[getter] - fn a(&self) -> f32 { - self.0[3] - } - - /// Return the ``DebugColor(h=..., s=..., l=..., a=...)`` repr. - fn __repr__(&self) -> String { - format!( - "DebugColor(h={}, s={}, l={}, a={})", - self.0[0], self.0[1], self.0[2], self.0[3] - ) - } - /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("DebugColor supports only == and !=")), - } - } - } - - impl DebugColor { - #[inline] - fn extract_from(obj: &Bound<'_, PyAny>) -> PyResult<[f32; 4]> { - if let Ok(c) = obj.extract::() { - return Ok(c.0); - } - let v: Vec = $crate::conv::extract_floats_from_sequence(obj, 4)?; - Ok([v[0], v[1], v[2], v[3]]) - } - } - - // ===================================================================== - // DebugRenderMode — bitflags wrapper. - // ===================================================================== - - /// Bitflags selecting what :class:`DebugRenderPipeline` renders. - /// - /// Combine flags with ``|``, ``&``, ``^``, ``~``. The - /// :meth:`default` set is - /// ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES`` and is what - /// ``DebugRenderPipeline()`` uses if no mode is passed. - #[pyclass(name = "DebugRenderMode", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct DebugRenderMode(pub rapier::pipeline::DebugRenderMode); - - #[pymethods] - impl DebugRenderMode { - /// Build a ``DebugRenderMode`` from a raw bit pattern. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::pipeline::DebugRenderMode::from_bits_truncate(bits)) - } - /// Return the empty flag set (renders nothing). - #[staticmethod] - fn empty() -> Self { - Self(rapier::pipeline::DebugRenderMode::empty()) - } - /// Default flag set: ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES``. - #[staticmethod] - #[pyo3(name = "default")] - fn py_default() -> Self { - Self(rapier::pipeline::DebugRenderMode::default()) - } - /// Return the union of all known flags. - #[staticmethod] - fn all() -> Self { - Self(rapier::pipeline::DebugRenderMode::all()) - } - - #[classattr] - const COLLIDER_SHAPES: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::COLLIDER_SHAPES); - #[classattr] - const RIGID_BODY_AXES: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::RIGID_BODY_AXES); - #[classattr] - const MULTIBODY_JOINTS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::MULTIBODY_JOINTS); - #[classattr] - const IMPULSE_JOINTS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::IMPULSE_JOINTS); - #[classattr] - const JOINTS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::JOINTS); - #[classattr] - const SOLVER_CONTACTS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::SOLVER_CONTACTS); - #[classattr] - const CONTACTS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::CONTACTS); - #[classattr] - const COLLIDER_AABBS: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::COLLIDER_AABBS); - #[classattr] - const EMPTY: DebugRenderMode = - DebugRenderMode(rapier::pipeline::DebugRenderMode::empty()); - - /// Raw bits as an unsigned int. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// ``True`` if ``self`` is a superset of ``other``. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// ``True`` if no bits are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - /// Python ``in`` operator: alias of :meth:`contains`. - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// Bitwise OR: union of two flag sets. - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - /// Bitwise AND: intersection of two flag sets. - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - /// Bitwise XOR: symmetric difference of two flag sets. - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - /// Bitwise NOT: complement within all known flags. - fn __invert__(&self) -> Self { - Self(!self.0) - } - /// ``False`` iff no bits are set. - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - /// Equality (``==`` / ``!=``). Other comparisons raise ``TypeError``. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "DebugRenderMode supports only == and !=", - )), - } - } - /// Hash by raw bit pattern (consistent with ``==``). - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - /// Return ``DebugRenderMode(bits=0b...)`` repr. - fn __repr__(&self) -> String { - format!("DebugRenderMode(bits={:#010b})", self.0.bits()) - } - } - - // ===================================================================== - // DebugRenderStyle — wraps `rapier::pipeline::DebugRenderStyle` with - // every public field as a mutable Python property. - // ===================================================================== - - /// Style configuration for the debug-render pipeline. - /// - /// Wraps ``rapier::pipeline::DebugRenderStyle`` with every public - /// field as a mutable Python property. The default constructor - /// uses rapier's upstream defaults. - /// - /// Color attributes accept either a :class:`DebugColor` or any - /// 4-float sequence (HSLA). - #[pyclass(name = "DebugRenderStyle", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct DebugRenderStyle(pub rapier::pipeline::DebugRenderStyle); - - #[pymethods] - impl DebugRenderStyle { - /// Build a ``DebugRenderStyle`` with rapier's default values. - #[new] - fn new() -> Self { - Self(rapier::pipeline::DebugRenderStyle::default()) - } - /// Return rapier's default ``DebugRenderStyle``. - #[staticmethod] - #[pyo3(name = "default")] - fn py_default() -> Self { - Self(rapier::pipeline::DebugRenderStyle::default()) - } - - /// Number of subdivisions used for curved shapes (cylinders, - /// spheres, capsules). - #[getter] - fn subdivisions(&self) -> u32 { - self.0.subdivisions - } - #[setter] - /// Assign the curved-shape tessellation subdivision count. - fn set_subdivisions(&mut self, v: u32) { - self.0.subdivisions = v; - } - /// Subdivisions used for borders of e.g. round-cuboids. - #[getter] - fn border_subdivisions(&self) -> u32 { - self.0.border_subdivisions - } - #[setter] - /// Assign the rounded-border tessellation subdivision count. - fn set_border_subdivisions(&mut self, v: u32) { - self.0.border_subdivisions = v; - } - - /// Color for colliders attached to a dynamic rigid body. - #[getter] - fn collider_dynamic_color(&self) -> DebugColor { - DebugColor(self.0.collider_dynamic_color) - } - #[setter] - /// Set the color used to render dynamic-body colliders. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_collider_dynamic_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.collider_dynamic_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color for colliders attached to a fixed rigid body. - #[getter] - fn collider_fixed_color(&self) -> DebugColor { - DebugColor(self.0.collider_fixed_color) - } - #[setter] - /// Set the color used to render fixed-body colliders. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_collider_fixed_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.collider_fixed_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color for colliders attached to a kinematic rigid body. - #[getter] - fn collider_kinematic_color(&self) -> DebugColor { - DebugColor(self.0.collider_kinematic_color) - } - #[setter] - /// Set the color used to render kinematic-body colliders. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_collider_kinematic_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.collider_kinematic_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color for colliders not attached to any rigid body. - #[getter] - fn collider_parentless_color(&self) -> DebugColor { - DebugColor(self.0.collider_parentless_color) - } - #[setter] - /// Set the color used to render parentless (orphan) colliders. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_collider_parentless_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.collider_parentless_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color of an impulse joint's anchor frame. - #[getter] - fn impulse_joint_anchor_color(&self) -> DebugColor { - DebugColor(self.0.impulse_joint_anchor_color) - } - #[setter] - /// Set the color used to render impulse-joint anchor frames. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_impulse_joint_anchor_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.impulse_joint_anchor_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color of the separation line shown when an impulse joint is - /// not satisfied. - #[getter] - fn impulse_joint_separation_color(&self) -> DebugColor { - DebugColor(self.0.impulse_joint_separation_color) - } - #[setter] - /// Set the color used to render unsatisfied-impulse-joint - /// separation segments. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_impulse_joint_separation_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.impulse_joint_separation_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color of a multibody joint's anchor frame. - #[getter] - fn multibody_joint_anchor_color(&self) -> DebugColor { - DebugColor(self.0.multibody_joint_anchor_color) - } - #[setter] - /// Set the color used to render multibody-joint anchor frames. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_multibody_joint_anchor_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.multibody_joint_anchor_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color of the separation line shown when a multibody joint is - /// not satisfied. - #[getter] - fn multibody_joint_separation_color(&self) -> DebugColor { - DebugColor(self.0.multibody_joint_separation_color) - } - #[setter] - /// Set the color used to render unsatisfied-multibody-joint - /// separation segments. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_multibody_joint_separation_color( - &mut self, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - self.0.multibody_joint_separation_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Multiplier applied to base colors of sleeping bodies. - #[getter] - fn sleep_color_multiplier(&self) -> DebugColor { - DebugColor(self.0.sleep_color_multiplier) - } - #[setter] - /// Set the HSLA multiplier applied to sleeping-body colors. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_sleep_color_multiplier(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.sleep_color_multiplier = DebugColor::extract_from(v)?; - Ok(()) - } - /// Multiplier applied to base colors of disabled bodies. - #[getter] - fn disabled_color_multiplier(&self) -> DebugColor { - DebugColor(self.0.disabled_color_multiplier) - } - #[setter] - /// Set the HSLA multiplier applied to disabled-body colors. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_disabled_color_multiplier(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.disabled_color_multiplier = DebugColor::extract_from(v)?; - Ok(()) - } - /// Length of the rigid-body axis triads when - /// :attr:`DebugRenderMode.RIGID_BODY_AXES` is set. - #[getter] - fn rigid_body_axes_length(&self) -> Real { - self.0.rigid_body_axes_length - } - #[setter] - /// Assign the rigid-body axis triad length in world units. - fn set_rigid_body_axes_length(&mut self, v: Real) { - self.0.rigid_body_axes_length = v; - } - /// Color of the segment drawn for each contact penetration depth. - #[getter] - fn contact_depth_color(&self) -> DebugColor { - DebugColor(self.0.contact_depth_color) - } - #[setter] - /// Set the color used to render contact penetration-depth segments. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_contact_depth_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.contact_depth_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Color of contact normal segments. - #[getter] - fn contact_normal_color(&self) -> DebugColor { - DebugColor(self.0.contact_normal_color) - } - #[setter] - /// Set the color used to render contact-normal segments. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_contact_normal_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.contact_normal_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Length of the drawn contact-normal segments. - #[getter] - fn contact_normal_length(&self) -> Real { - self.0.contact_normal_length - } - #[setter] - /// Assign the length (world units) of contact-normal segments. - fn set_contact_normal_length(&mut self, v: Real) { - self.0.contact_normal_length = v; - } - /// Color of collider AABB outlines (when - /// :attr:`DebugRenderMode.COLLIDER_AABBS` is set). - #[getter] - fn collider_aabb_color(&self) -> DebugColor { - DebugColor(self.0.collider_aabb_color) - } - #[setter] - /// Set the color used to render collider AABB outlines. - /// - /// Accepts a :class:`DebugColor` or a 4-tuple HSLA. - fn set_collider_aabb_color(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { - self.0.collider_aabb_color = DebugColor::extract_from(v)?; - Ok(()) - } - /// Return a brief ``DebugRenderStyle(...)`` repr. - fn __repr__(&self) -> String { - format!( - "DebugRenderStyle(subdivisions={}, ...)", - self.0.subdivisions - ) - } - } - - // ===================================================================== - // DebugLineCollector — `#[pyclass]` implementing the rapier - // `DebugRenderBackend` trait through an inner buffer. - // ===================================================================== - - /// Built-in `DebugRenderBackend` that drains each `draw_line` call into - /// an internal buffer exposed as NumPy arrays. - /// - /// Use `DebugRenderPipeline.render(backend=collector)` to populate, then - /// call `.lines()`, `.colors()`, `.objects()` for `(N, 2, D)`, `(N, 4)`, - /// `(N,)` NumPy arrays respectively (each call returns a fresh **copy** - /// of the underlying data). - #[pyclass(name = "DebugLineCollector", module = "rapier")] - pub struct DebugLineCollector { - // `Mutex>` so we can mutate from the &self - // `draw_line` implementation without exposing `unsafe`. The - // `Arc` lets us share the buffer with a transient `&mut` - // implementer used inside `render` (see `_run_with_backend`). - lines: _DbgArc<_DbgMutex>>, - } - - #[pymethods] - impl DebugLineCollector { - /// Build a new empty collector. - #[new] - fn new() -> Self { - Self { - lines: _DbgArc::new(_DbgMutex::new(Vec::new())), - } - } - - /// Drop every queued line. - fn clear(&self) { - self.lines.lock().unwrap().clear(); - } - - /// Number of lines currently queued. - fn __len__(&self) -> usize { - self.lines.lock().unwrap().len() - } - /// Return ``DebugLineCollector(len=N)`` repr. - fn __repr__(&self) -> String { - format!( - "DebugLineCollector(len={})", - self.lines.lock().unwrap().len() - ) - } - - /// Return a fresh `(N, 2, D)` NumPy array of segment endpoints. - fn lines<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray2> { - use $crate::numpy::PyArray2; - let lines = self.lines.lock().unwrap(); - // Flatten as N*2 rows of D columns; reshape on the Python side - // if a (N, 2, D) view is desired (we expose `(N*2, D)` then - // reshape via numpy in the helper `render_to_arrays`). - let mut flat: Vec> = Vec::with_capacity(lines.len() * 2); - for ln in lines.iter() { - flat.push(_svec_to_row::<$dim>(ln.a)); - flat.push(_svec_to_row::<$dim>(ln.b)); - } - let arr = PyArray2::::from_vec2_bound(py, &flat) - .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, $dim], false)); - // Reshape (N*2, D) → (N, 2, D). We do this via numpy at the - // Python side; here we just return the (2N, D) view. - arr - } - - /// Return a fresh `(N, 4)` NumPy array of RGBA (post-HSLA→RGBA) - /// per-line colors. - fn colors<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray2> { - use $crate::numpy::PyArray2; - let lines = self.lines.lock().unwrap(); - let mut flat: Vec> = Vec::with_capacity(lines.len()); - for ln in lines.iter() { - let rgba = _hsla_to_rgba(ln.color); - flat.push(rgba.to_vec()); - } - PyArray2::::from_vec2_bound(py, &flat) - .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 4], false)) - } - - /// Return a fresh `(N,)` NumPy array of `DebugRenderObject` kind - /// discriminants (one entry per line). - fn objects<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray1> { - use $crate::numpy::PyArray1; - let lines = self.lines.lock().unwrap(); - let v: Vec = lines.iter().map(|ln| ln.object_kind).collect(); - PyArray1::::from_vec_bound(py, v) - } - - /// Iterate over the queued lines as - /// ``(object, a, b, color)`` tuples. - /// - /// The iterator takes a snapshot at iteration time — concurrent - /// :meth:`clear` / re-render calls do not invalidate it. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - // Snapshot of (object, a, b, color) tuples at iteration time. - let py = slf.py(); - let lines = slf.lines.lock().unwrap(); - let items: Vec<(DebugRenderObject, $Vec, $Vec, DebugColor)> = lines - .iter() - .map(|ln| { - ( - DebugRenderObject { - kind: ln.object_kind, - }, - $Vec(ln.a), - $Vec(ln.b), - DebugColor(ln.color), - ) - }) - .collect(); - Py::new(py, DebugLineCollectorIter { items, i: 0 }) - } - } - - /// Iterator over the ``(object, a, b, color)`` snapshot of a - /// :class:`DebugLineCollector`. - #[pyclass(module = "rapier")] - pub struct DebugLineCollectorIter { - items: Vec<(DebugRenderObject, $Vec, $Vec, DebugColor)>, - i: usize, - } - - #[pymethods] - impl DebugLineCollectorIter { - /// Return ``self`` (this object is already an iterator). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Return the next ``(object, a, b, color)`` tuple, or raise - /// ``StopIteration``. - fn __next__( - mut slf: PyRefMut<'_, Self>, - ) -> Option<(DebugRenderObject, $Vec, $Vec, DebugColor)> { - if slf.i >= slf.items.len() { - return None; - } - let idx = slf.i; - slf.i += 1; - Some(slf.items[idx]) - } - } - - // ===================================================================== - // PyDebugRenderBackend — adapter wrapping an arbitrary Python object - // implementing the `DebugRenderBackend` protocol. - // - // Mirrors the deferred-exception pattern from `events_hooks.rs`: errors - // raised inside `draw_line` are stashed in a shared slot and re-raised - // after `render()` returns. - // ===================================================================== - - struct _CollectorBackendAdapter { - buf: _DbgArc<_DbgMutex>>, - } - - impl rapier::pipeline::DebugRenderBackend for _CollectorBackendAdapter { - fn draw_line( - &mut self, - object: rapier::pipeline::DebugRenderObject<'_>, - a: rapier::math::Vector, - b: rapier::math::Vector, - color: rapier::pipeline::DebugColor, - ) { - let an: $crate::na::SVector = a.into(); - let bn: $crate::na::SVector = b.into(); - self.buf.lock().unwrap().push(DebugLine { - object_kind: _dro_kind(object), - a: an, - b: bn, - color, - }); - } - } - - struct _PyBackendAdapter { - obj: Py, - err_slot: _DbgArc<_DbgMutex>>, - } - - impl rapier::pipeline::DebugRenderBackend for _PyBackendAdapter { - fn draw_line( - &mut self, - object: rapier::pipeline::DebugRenderObject<'_>, - a: rapier::math::Vector, - b: rapier::math::Vector, - color: rapier::pipeline::DebugColor, - ) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.is_some() { - return; - } - } - Python::with_gil(|py| { - let kind = _dro_kind(object); - let obj_py = DebugRenderObject { kind }; - let an: $crate::na::SVector = a.into(); - let bn: $crate::na::SVector = b.into(); - let a_py = $Vec(an); - let b_py = $Vec(bn); - let c_py = DebugColor(color); - let res = self - .obj - .bind(py) - .call_method1("draw_line", (obj_py, a_py, b_py, c_py)); - if let Err(e) = res { - let mut s = self.err_slot.lock().unwrap(); - if s.is_none() { - *s = Some(e); - } - } - }); - } - } - - // ===================================================================== - // DebugRenderPipeline — main user-facing class. - // ===================================================================== - - /// Main entry point for visualizing a rapier scene. - /// - /// Walks the rigid bodies, colliders, and joint sets and emits one - /// or more colored line segments per object. The segments are - /// delivered to a backend implementing the ``DebugRenderBackend`` - /// protocol — typically a :class:`DebugLineCollector` for offline - /// inspection (matplotlib, pygame, etc.), or a custom Python - /// object exposing ``draw_line(object, a, b, color)``. - /// - /// For purely-data use cases prefer :meth:`render_to_arrays`, - /// which returns NumPy arrays directly. - #[pyclass(name = "DebugRenderPipeline", module = "rapier", unsendable)] - pub struct DebugRenderPipeline { - inner: rapier::pipeline::DebugRenderPipeline, - } - - impl DebugRenderPipeline { - fn _run_with_backend( - &mut self, - bodies: &RigidBodySet, - colliders: &ColliderSet, - impulse_joints: &ImpulseJointSet, - multibody_joints: &MultibodyJointSet, - narrow_phase: &NarrowPhase, - backend: &mut dyn rapier::pipeline::DebugRenderBackend, - ) { - // The trait method `render` takes `&mut impl DebugRenderBackend` - // which is not directly object-safe via `&mut dyn`. Wrap it. - struct DynWrap<'a> { - inner: &'a mut dyn rapier::pipeline::DebugRenderBackend, - } - impl<'a> rapier::pipeline::DebugRenderBackend for DynWrap<'a> { - fn draw_line( - &mut self, - object: rapier::pipeline::DebugRenderObject<'_>, - a: rapier::math::Vector, - b: rapier::math::Vector, - color: rapier::pipeline::DebugColor, - ) { - self.inner.draw_line(object, a, b, color) - } - } - let mut wrap = DynWrap { inner: backend }; - self.inner.render( - &mut wrap, - &bodies.0, - &colliders.0, - &impulse_joints.0, - &multibody_joints.0, - &narrow_phase.0, - ); - } - } - - #[pymethods] - impl DebugRenderPipeline { - /// Build a pipeline with optional mode / style overrides. - /// - /// :param mode: :class:`DebugRenderMode` flags selecting what - /// to render. Defaults to - /// ``COLLIDER_SHAPES | JOINTS | RIGID_BODY_AXES``. - /// :param style: :class:`DebugRenderStyle` color and length - /// configuration. Defaults to rapier's upstream defaults. - #[new] - #[pyo3(signature = (mode = None, style = None))] - fn new(mode: Option<&DebugRenderMode>, style: Option<&DebugRenderStyle>) -> Self { - let m = mode - .map(|m| m.0) - .unwrap_or_else(rapier::pipeline::DebugRenderMode::default); - let s = style - .map(|s| s.0) - .unwrap_or_else(rapier::pipeline::DebugRenderStyle::default); - Self { - inner: rapier::pipeline::DebugRenderPipeline::new(s, m), - } - } - - /// Current :class:`DebugRenderMode` flag set. - #[getter] - fn mode(&self) -> DebugRenderMode { - DebugRenderMode(self.inner.mode) - } - #[setter] - /// Replace the current :class:`DebugRenderMode` flag set. - fn set_mode(&mut self, v: &DebugRenderMode) { - self.inner.mode = v.0; - } - /// Current :class:`DebugRenderStyle`. - #[getter] - fn style(&self) -> DebugRenderStyle { - DebugRenderStyle(self.inner.style) - } - #[setter] - /// Replace the current :class:`DebugRenderStyle`. - fn set_style(&mut self, v: &DebugRenderStyle) { - self.inner.style = v.0; - } - - /// Render the scene into the given ``backend``. - /// - /// ``backend`` may be a :class:`DebugLineCollector` (fast path — - /// no Python callback per segment) or any Python object exposing - /// ``draw_line(object, a, b, color) -> None``. Exceptions raised - /// by a Python backend are deferred and re-raised after rendering - /// completes. - /// - /// For hot loops prefer the collector — each Python-side - /// ``draw_line`` call has to acquire the GIL. - /// - /// :param bodies: :class:`RigidBodySet`. - /// :param colliders: :class:`ColliderSet`. - /// :param impulse_joints: :class:`ImpulseJointSet`. - /// :param multibody_joints: :class:`MultibodyJointSet`. - /// :param narrow_phase: :class:`NarrowPhase`. - /// :param backend: Collector or Python ``DebugRenderBackend``. - #[pyo3(signature = (bodies, colliders, impulse_joints, multibody_joints, narrow_phase, backend))] - #[allow(clippy::too_many_arguments)] - fn render( - &mut self, - py: Python<'_>, - bodies: &RigidBodySet, - colliders: &ColliderSet, - impulse_joints: &ImpulseJointSet, - multibody_joints: &MultibodyJointSet, - narrow_phase: &NarrowPhase, - backend: &Bound<'_, PyAny>, - ) -> PyResult<()> { - // Fast path: DebugLineCollector. We bypass the Python callback - // dispatch entirely. - if let Ok(c) = backend.extract::>() { - let mut adapter = _CollectorBackendAdapter { - buf: _DbgArc::clone(&c.lines), - }; - self._run_with_backend( - bodies, - colliders, - impulse_joints, - multibody_joints, - narrow_phase, - &mut adapter, - ); - return Ok(()); - } - - // General path: Python-side backend implementing - // `draw_line(object, a, b, color)`. - let err_slot: _DbgArc<_DbgMutex>> = - _DbgArc::new(_DbgMutex::new(None)); - let mut adapter = _PyBackendAdapter { - obj: backend.clone().unbind(), - err_slot: _DbgArc::clone(&err_slot), - }; - self._run_with_backend( - bodies, - colliders, - impulse_joints, - multibody_joints, - narrow_phase, - &mut adapter, - ); - drop(adapter); - let mut slot = err_slot.lock().unwrap(); - if let Some(e) = slot.take() { - return Err(e); - } - let _ = py; - Ok(()) - } - - /// Render the scene and return NumPy arrays directly. - /// - /// Convenience wrapper around :meth:`render` with a transient - /// :class:`DebugLineCollector`. - /// - /// :param bodies: :class:`RigidBodySet`. - /// :param colliders: :class:`ColliderSet`. - /// :param impulse_joints: :class:`ImpulseJointSet`. - /// :param multibody_joints: :class:`MultibodyJointSet`. - /// :param narrow_phase: :class:`NarrowPhase`. - /// :returns: A ``(lines, colors, objects)`` tuple where - /// ``lines`` is ``(N, 2, D)`` float, ``colors`` is - /// ``(N, 4)`` float32 RGBA in ``[0, 1]``, and ``objects`` - /// is ``(N,)`` uint32 of - /// :class:`DebugRenderObject` discriminants. - #[pyo3(signature = (bodies, colliders, impulse_joints, multibody_joints, narrow_phase))] - #[allow(clippy::too_many_arguments)] - fn render_to_arrays<'py>( - &mut self, - py: Python<'py>, - bodies: &RigidBodySet, - colliders: &ColliderSet, - impulse_joints: &ImpulseJointSet, - multibody_joints: &MultibodyJointSet, - narrow_phase: &NarrowPhase, - ) -> PyResult<( - Bound<'py, $crate::numpy::PyArray3>, - Bound<'py, $crate::numpy::PyArray2>, - Bound<'py, $crate::numpy::PyArray1>, - )> { - use $crate::numpy::{PyArray1, PyArray2, PyArray3, PyArrayMethods}; - - let buf: _DbgArc<_DbgMutex>> = - _DbgArc::new(_DbgMutex::new(Vec::new())); - { - let mut adapter = _CollectorBackendAdapter { - buf: _DbgArc::clone(&buf), - }; - self._run_with_backend( - bodies, - colliders, - impulse_joints, - multibody_joints, - narrow_phase, - &mut adapter, - ); - } - - let lines = buf.lock().unwrap(); - let n = lines.len(); - - // (N, 2, D) lines array. Allocate flat and reshape. - let lines_arr = PyArray3::::zeros_bound(py, [n, 2, $dim], false); - { - let mut view = unsafe { lines_arr.as_array_mut() }; - for (i, ln) in lines.iter().enumerate() { - let row_a = _svec_to_row::<$dim>(ln.a); - let row_b = _svec_to_row::<$dim>(ln.b); - for d in 0..$dim { - view[[i, 0, d]] = row_a[d]; - view[[i, 1, d]] = row_b[d]; - } - } - } - - let mut col_flat: Vec> = Vec::with_capacity(n); - for ln in lines.iter() { - col_flat.push(_hsla_to_rgba(ln.color).to_vec()); - } - let colors_arr = if n == 0 { - PyArray2::::zeros_bound(py, [0, 4], false) - } else { - PyArray2::::from_vec2_bound(py, &col_flat) - .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 4], false)) - }; - - let objects_arr = PyArray1::::from_vec_bound( - py, - lines.iter().map(|ln| ln.object_kind).collect(), - ); - - Ok((lines_arr, colors_arr, objects_arr)) - } - - /// Return a brief ``DebugRenderPipeline(mode=0b...)`` repr. - fn __repr__(&self) -> String { - format!("DebugRenderPipeline(mode={:#010b})", self.inner.mode.bits()) - } - } - - // Helper: turn an `na::SVector` into a `Vec` of - // length `DIM`. Inlined so the row-builder loops above can reuse it. - #[inline] - fn _svec_to_row(v: $crate::na::SVector) -> Vec { - let mut row = Vec::with_capacity(D); - for d in 0..D { - row.push(v[d]); - } - row - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_debug_render_register { - () => { - pub fn register_debug_render( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/dynamics.rs b/python/rapier-py-core/src/dynamics.rs deleted file mode 100644 index b9c2b9491..000000000 --- a/python/rapier-py-core/src/dynamics.rs +++ /dev/null @@ -1,3580 +0,0 @@ -//! Macro that emits the user-facing rigid-body / dynamics `#[pyclass]`-es -//! per cdylib. -//! -//! Like `define_math_types!`, this macro is invoked *once per cdylib*. It -//! expects the surrounding cdylib to already have: -//! - The `Real`/`DIM` aliases and `Py{Vector,Point,Rotation,Isometry,AngVector}` -//! adapter newtypes (from `define_conv_types!`). -//! - The user-facing math `#[pyclass]`-es (`Vec3`/`Point3`/`Rotation3`/ -//! `Isometry3` or the 2D analogues) (from `define_math_types!`). -//! - `use rapier3d as rapier;` (or the appropriate dim/scalar pair). -//! -//! Forward-compat with later phases: the macro emits **minimal stub** -//! `ColliderHandle`, `ColliderSet`, `ImpulseJointSet`, `MultibodyJointSet` -//! so the rigid-body API can compile. Those will be replaced by phases 04 -//! and 06. - -/// Materialize the dynamics `#[pyclass]` types for a given `(Real, DIM)` pair. -/// -/// Invoke once per cdylib after `define_conv_types!` and `define_math_types!`. -/// Produces `register_dynamics(py, m) -> PyResult<()>` for `#[pymodule]`. -#[macro_export] -macro_rules! define_dynamics_types { - (DIM = 2) => { - $crate::__define_dynamics_shared!(2); - $crate::__define_dynamics_2d!(); - $crate::__define_dynamics_register!(2); - }; - (DIM = 3) => { - $crate::__define_dynamics_shared!(3); - $crate::__define_dynamics_3d!(); - $crate::__define_dynamics_register!(3); - }; -} - -// ---------------------------------------------------------------------- -// Shared dim-agnostic dynamics types -// ---------------------------------------------------------------------- - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_shared { - ($dim:literal) => { - // Note: we don't `use` PyTypeError/CompareOp here because - // `define_math_types!` already brings them into scope. We do bring - // in PyNotImplementedError (math doesn't need it). - use $crate::pyo3::exceptions::PyNotImplementedError; - - // -------- Glam <-> nalgebra conversion helpers -------- - - /// Convert a nalgebra `SVector` to the engine's `Vector`. - #[allow(dead_code)] - #[inline] - fn _na_vec_to_engine(v: $crate::na::SVector) -> rapier::math::Vector { - v.into() - } - - /// Convert the engine's `Vector` back to nalgebra. - #[allow(dead_code)] - #[inline] - fn _engine_vec_to_na(v: rapier::math::Vector) -> $crate::na::SVector { - v.into() - } - - // ============================================================ - // ColliderHandle / ColliderSet live in `geometry.rs`. - // The dynamics macro is invoked AFTER the geometry macro by each - // cdylib, so those types are already in scope here. - // ============================================================ - - // ============================================================ - // ImpulseJointSet / MultibodyJointSet live in `joints.rs`. The joints macro is invoked BEFORE this one in each cdylib, - // so those types are already in scope here. - // ============================================================ - - // ============================================================ - // RigidBodyHandle - // ============================================================ - - /// Opaque handle identifying a rigid body inside a `RigidBodySet`. - /// - /// Handles are stable across structural mutations of the set and - /// remain valid until the corresponding body is removed. They wrap an - /// ``(index, generation)`` pair: the generation guards against reusing - /// a slot freed by a previous body. - /// - /// Use ``RigidBodySet.insert(...)`` to obtain a handle. Handles are - /// hashable and may be used as dict keys. - #[pyclass(name = "RigidBodyHandle", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct RigidBodyHandle(pub rapier::dynamics::RigidBodyHandle); - - #[pymethods] - impl RigidBodyHandle { - /// Build a handle from raw ``(index, generation)`` parts. - /// - /// Most code should obtain handles from ``RigidBodySet.insert`` - /// rather than constructing them directly. - /// - /// :param index: slot index inside the set (default ``0``). - /// :param generation: generation tag for the slot (default ``0``). - #[new] - #[pyo3(signature = (index=0, generation=0))] - fn new(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::RigidBodyHandle::from_raw_parts( - index, generation, - )) - } - /// Build a handle from raw ``(index, generation)`` parts. - /// - /// Equivalent to the constructor; provided for symmetry with the - /// Rust API and for use as a named static method. - #[staticmethod] - fn from_raw_parts(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::RigidBodyHandle::from_raw_parts( - index, generation, - )) - } - /// Return a sentinel handle that does not match any real body. - /// - /// Useful as a default value or as a placeholder before a body is - /// actually inserted. - #[staticmethod] - fn invalid() -> Self { - Self(rapier::dynamics::RigidBodyHandle::invalid()) - } - /// Slot index portion of the handle (read-only). - #[getter] - fn index(&self) -> u32 { - self.0.into_raw_parts().0 - } - /// Generation portion of the handle (read-only). - #[getter] - fn generation(&self) -> u32 { - self.0.into_raw_parts().1 - } - /// Hash combining index and generation, enabling use as dict key. - fn __hash__(&self) -> u64 { - let (i, g) = self.0.into_raw_parts(); - ((i as u64) << 32) | (g as u64) - } - /// Compare two handles for equality. Only ``==`` and ``!=`` are - /// supported. - /// - /// :raises TypeError: when an ordering comparison is requested. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "RigidBodyHandle supports only == and !=", - )), - } - } - fn __repr__(&self) -> String { - let (i, g) = self.0.into_raw_parts(); - format!("RigidBodyHandle(index={}, generation={})", i, g) - } - } - - // ============================================================ - // RigidBodyType - // ============================================================ - - /// Behavior class of a rigid body. - /// - /// Determines how the body responds to forces, contacts and joints: - /// - /// - ``DYNAMIC`` — fully simulated; responds to forces and contacts. - /// - ``FIXED`` — immovable; treated as having infinite mass. - /// - ``KINEMATIC_VELOCITY_BASED`` — animated by setting ``linvel`` / - /// ``angvel`` each frame; pushes dynamic bodies, ignores contacts. - /// - ``KINEMATIC_POSITION_BASED`` — animated by setting ``position`` - /// each frame; pushes dynamic bodies, ignores contacts. - #[pyclass(name = "RigidBodyType", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum RigidBodyType { - /// Fully simulated: responds to forces, gravity and contacts. - DYNAMIC, - /// Immovable: behaves as if it had infinite mass. - FIXED, - /// Animated by writing to ``linvel`` / ``angvel`` each step. - KINEMATIC_VELOCITY_BASED, - /// Animated by writing to ``position`` each step. - KINEMATIC_POSITION_BASED, - } - - impl RigidBodyType { - #[inline] - fn to_rapier(self) -> rapier::dynamics::RigidBodyType { - match self { - Self::DYNAMIC => rapier::dynamics::RigidBodyType::Dynamic, - Self::FIXED => rapier::dynamics::RigidBodyType::Fixed, - Self::KINEMATIC_VELOCITY_BASED => { - rapier::dynamics::RigidBodyType::KinematicVelocityBased - } - Self::KINEMATIC_POSITION_BASED => { - rapier::dynamics::RigidBodyType::KinematicPositionBased - } - } - } - #[inline] - fn from_rapier(t: rapier::dynamics::RigidBodyType) -> Self { - match t { - rapier::dynamics::RigidBodyType::Dynamic => Self::DYNAMIC, - rapier::dynamics::RigidBodyType::Fixed => Self::FIXED, - rapier::dynamics::RigidBodyType::KinematicVelocityBased => { - Self::KINEMATIC_VELOCITY_BASED - } - rapier::dynamics::RigidBodyType::KinematicPositionBased => { - Self::KINEMATIC_POSITION_BASED - } - } - } - } - - // ============================================================ - // CoefficientCombineRule - // ============================================================ - - /// Rule used to combine per-collider friction/restitution coefficients - /// at a contact. - /// - /// When two colliders touch, each has its own coefficient (friction or - /// restitution) and a rule. The effective coefficient is derived by - /// applying the combine rule of the *highest priority* among the two - /// (``MAX > MULTIPLY > MIN > AVERAGE``). - #[pyclass(name = "CoefficientCombineRule", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum CoefficientCombineRule { - /// Arithmetic mean of the two coefficients. - AVERAGE, - /// Minimum of the two coefficients. - MIN, - /// Product of the two coefficients. - MULTIPLY, - /// Maximum of the two coefficients. - MAX, - /// Sum of the two coefficients, clamped to ``[0, 1]``. - CLAMPED_SUM, - } - - impl CoefficientCombineRule { - #[allow(dead_code)] - #[inline] - fn to_rapier(self) -> rapier::dynamics::CoefficientCombineRule { - match self { - Self::AVERAGE => rapier::dynamics::CoefficientCombineRule::Average, - Self::MIN => rapier::dynamics::CoefficientCombineRule::Min, - Self::MULTIPLY => rapier::dynamics::CoefficientCombineRule::Multiply, - Self::MAX => rapier::dynamics::CoefficientCombineRule::Max, - Self::CLAMPED_SUM => rapier::dynamics::CoefficientCombineRule::ClampedSum, - } - } - #[allow(dead_code)] - #[inline] - fn from_rapier(r: rapier::dynamics::CoefficientCombineRule) -> Self { - match r { - rapier::dynamics::CoefficientCombineRule::Average => Self::AVERAGE, - rapier::dynamics::CoefficientCombineRule::Min => Self::MIN, - rapier::dynamics::CoefficientCombineRule::Multiply => Self::MULTIPLY, - rapier::dynamics::CoefficientCombineRule::Max => Self::MAX, - rapier::dynamics::CoefficientCombineRule::ClampedSum => Self::CLAMPED_SUM, - } - } - } - - // ============================================================ - // RigidBodyActivation - // ============================================================ - - /// Sleep / activation state of a rigid body. - /// - /// A body whose linear and angular velocity stays below the sleep - /// thresholds for ``time_until_sleep`` seconds is put to sleep to save - /// compute. A sleeping body is skipped by the solver until something - /// wakes it (contact, force, joint, manual ``wake_up``). - /// - /// Construct via ``RigidBodyActivation()`` (active by default), - /// ``RigidBodyActivation.inactive()`` for an initially-sleeping body - /// or ``RigidBodyActivation.cannot_sleep()`` to disable sleep. - #[pyclass(name = "RigidBodyActivation", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyActivation(pub rapier::dynamics::RigidBodyActivation); - - #[pymethods] - impl RigidBodyActivation { - /// Build the default (active, can sleep) activation state. - #[new] - fn new() -> Self { - Self(rapier::dynamics::RigidBodyActivation::active()) - } - /// Default active activation: body is awake and may sleep later. - #[staticmethod] - fn active() -> Self { - Self(rapier::dynamics::RigidBodyActivation::active()) - } - /// Initially sleeping activation: body starts asleep. - #[staticmethod] - fn inactive() -> Self { - Self(rapier::dynamics::RigidBodyActivation::inactive()) - } - /// Activation that never sleeps — useful for player avatars and - /// other always-simulated bodies. - #[staticmethod] - fn cannot_sleep() -> Self { - Self(rapier::dynamics::RigidBodyActivation::cannot_sleep()) - } - - /// Normalized linear-velocity threshold below which the body may - /// fall asleep (read+write). - #[getter] - fn linear_threshold(&self) -> Real { - self.0.normalized_linear_threshold - } - /// Set the normalized linear sleep threshold. - #[setter] - fn set_linear_threshold(&mut self, v: Real) { - self.0.normalized_linear_threshold = v; - } - /// Alias for ``linear_threshold`` (read+write). - #[getter] - fn sleep_threshold(&self) -> Real { - self.0.normalized_linear_threshold - } - /// Alias for ``set_linear_threshold``. - #[setter] - fn set_sleep_threshold(&mut self, v: Real) { - self.0.normalized_linear_threshold = v; - } - /// Angular-velocity threshold below which the body may fall - /// asleep (read+write). - #[getter] - fn angular_threshold(&self) -> Real { - self.0.angular_threshold - } - /// Set the angular sleep threshold. - #[setter] - fn set_angular_threshold(&mut self, v: Real) { - self.0.angular_threshold = v; - } - /// Seconds the body must stay below thresholds before sleeping. - /// - /// Negative values disable sleep for this body. - #[getter] - fn time_until_sleep(&self) -> Real { - self.0.time_until_sleep - } - /// Set the sleep delay in seconds. - #[setter] - fn set_time_until_sleep(&mut self, v: Real) { - self.0.time_until_sleep = v; - } - /// Whether the body is currently sleeping (read+write). - #[getter] - fn sleeping(&self) -> bool { - self.0.sleeping - } - /// Force the sleeping flag directly. Prefer ``RigidBody.wake_up`` - /// / ``RigidBody.sleep`` for proper bookkeeping. - #[setter] - fn set_sleeping(&mut self, v: bool) { - self.0.sleeping = v; - } - /// Whether the body is currently active (i.e. not sleeping). - /// - /// :returns: ``True`` if the body participates in the simulation - /// this step. - fn is_active(&self) -> bool { - self.0.is_active() - } - fn __repr__(&self) -> String { - format!( - "RigidBodyActivation(linear={}, angular={}, time_until_sleep={}, sleeping={})", - self.0.normalized_linear_threshold, - self.0.angular_threshold, - self.0.time_until_sleep, - self.0.sleeping, - ) - } - } - - // ============================================================ - // SpringCoefficients - // ============================================================ - - /// Soft-constraint spring parameters used for contact and joint - /// regularization. - /// - /// Internally Rapier converts ``stiffness`` (natural frequency, Hz) - /// and ``damping`` (damping ratio, unitless, ``1`` = critical) to the - /// effective spring/damper coefficients used by the solver. Defaults - /// suitable for typical contacts and joints can be obtained via - /// ``SpringCoefficients.contact_defaults()`` and - /// ``SpringCoefficients.joint_defaults()``. - #[pyclass(name = "SpringCoefficients", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct SpringCoefficients(pub rapier::dynamics::SpringCoefficients); - - #[pymethods] - impl SpringCoefficients { - /// Build a spring with the given natural frequency and damping - /// ratio. - /// - /// :param stiffness: natural frequency in Hz (default ``30.0``). - /// :param damping: damping ratio (default ``5.0``). - #[new] - #[pyo3(signature = (stiffness=30.0 as Real, damping=5.0 as Real))] - fn new(stiffness: Real, damping: Real) -> Self { - Self(rapier::dynamics::SpringCoefficients { - natural_frequency: stiffness, - damping_ratio: damping, - }) - } - /// Return the default spring coefficients used for contact - /// regularization. - #[staticmethod] - fn contact_defaults() -> Self { - Self(rapier::dynamics::SpringCoefficients::contact_defaults()) - } - /// Return the default spring coefficients used for joint - /// regularization. - #[staticmethod] - fn joint_defaults() -> Self { - Self(rapier::dynamics::SpringCoefficients::joint_defaults()) - } - /// Spring natural frequency (alias for ``natural_frequency``). - #[getter] - fn stiffness(&self) -> Real { - self.0.natural_frequency - } - /// Set the natural frequency. - #[setter] - fn set_stiffness(&mut self, v: Real) { - self.0.natural_frequency = v; - } - /// Natural frequency of the spring, in Hz. - #[getter] - fn natural_frequency(&self) -> Real { - self.0.natural_frequency - } - /// Set the natural frequency, in Hz. - #[setter] - fn set_natural_frequency(&mut self, v: Real) { - self.0.natural_frequency = v; - } - /// Damping ratio (alias for ``damping_ratio``). - #[getter] - fn damping(&self) -> Real { - self.0.damping_ratio - } - /// Set the damping ratio. - #[setter] - fn set_damping(&mut self, v: Real) { - self.0.damping_ratio = v; - } - /// Damping ratio (unitless, ``1.0`` is critically damped). - #[getter] - fn damping_ratio(&self) -> Real { - self.0.damping_ratio - } - /// Set the damping ratio. - #[setter] - fn set_damping_ratio(&mut self, v: Real) { - self.0.damping_ratio = v; - } - fn __repr__(&self) -> String { - format!( - "SpringCoefficients(stiffness={}, damping={})", - self.0.natural_frequency, self.0.damping_ratio - ) - } - } - - // ============================================================ - // IslandManager - // ============================================================ - - /// Tracks groups of interacting (connected) rigid bodies — *islands* - /// — and which of them are currently active. - /// - /// The island manager is updated by ``PhysicsWorld.step`` each frame - /// and is mainly used internally by the solver. Iterating the - /// manager yields the handles of bodies that are awake this step. - #[pyclass(name = "IslandManager", module = "rapier", unsendable)] - pub struct IslandManager(pub rapier::dynamics::IslandManager); - - #[pymethods] - impl IslandManager { - /// Build an empty island manager. - #[new] - fn new() -> Self { - Self(rapier::dynamics::IslandManager::new()) - } - - /// Snapshot of every awake dynamic body currently tracked. - /// - /// :returns: a list of handles of bodies that are active this step. - fn active_dynamic_set(&self) -> Vec { - self.0.active_bodies().map(RigidBodyHandle).collect() - } - - /// Number of currently active bodies (``len(islands)``). - fn __len__(&self) -> usize { - self.0.active_bodies().count() - } - - /// Iterate over currently active body handles. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let handles: Vec = - slf.0.active_bodies().map(RigidBodyHandle).collect(); - Py::new(slf.py(), RigidBodyHandleIter { handles, i: 0 }) - } - - /// Whether ``h`` is currently active. - /// - /// Sleeping bodies are not part of the active set and will return - /// ``False`` here. - fn contains_handle(&self, h: &RigidBodyHandle) -> bool { - // No public `contains` query; we iterate the active set. - // Sleeping bodies aren't in `active_bodies`, so this returns - // false for them — a later revision may revisit this. - self.0.active_bodies().any(|x| x == h.0) - } - - /// Whether ``h1`` and ``h2`` are part of the same active island. - /// - /// Currently approximated as both being in the active set; - /// returns ``False`` if either body is sleeping. - fn same_island(&self, h1: &RigidBodyHandle, h2: &RigidBodyHandle) -> bool { - // No public per-island id; approximate "same island" as both - // being in the active set. The pipeline integration wires this properly. - let mut a = false; - let mut b = false; - for h in self.0.active_bodies() { - if h == h1.0 { - a = true; - } - if h == h2.0 { - b = true; - } - if a && b { - break; - } - } - a && b - } - - /// Position of ``h`` inside the active set. - /// - /// Reserved for future use; currently always returns ``0``. - #[allow(unused_variables)] - fn active_set_offset(&self, h: &RigidBodyHandle) -> usize { - // Not exposed publicly in the engine; a later revision may surface it. - 0 - } - } - - /// Iterator yielding ``RigidBodyHandle`` values. - /// - /// Produced by ``IslandManager.__iter__`` and ``RigidBodySet.handles``; - /// not intended to be constructed directly. - #[pyclass] - pub struct RigidBodyHandleIter { - handles: Vec, - i: usize, - } - - #[pymethods] - impl RigidBodyHandleIter { - /// Return ``self`` so the object satisfies the iterator protocol. - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Return the next handle, or raise ``StopIteration``. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.handles.len() { - return None; - } - let v = slf.handles[slf.i]; - slf.i += 1; - Some(v) - } - } - - // ============================================================ - // CCDSolver — constructor + clear; solve_ccd handled by the pipeline. - // ============================================================ - - /// Continuous Collision Detection (CCD) solver. - /// - /// CCD prevents fast-moving bodies from tunneling through thin - /// obstacles by performing sub-step time-of-impact queries. Most users - /// should rely on ``PhysicsWorld.step``, which manages a ``CCDSolver`` - /// internally; this class exists mainly to mirror the engine's - /// structure. - #[pyclass(name = "CCDSolver", module = "rapier", unsendable)] - pub struct CCDSolver(pub rapier::dynamics::CCDSolver); - - #[pymethods] - impl CCDSolver { - /// Build a fresh CCD solver. - #[new] - fn new() -> Self { - Self(rapier::dynamics::CCDSolver::new()) - } - /// Clear any per-step CCD state. - fn clear(&mut self) { - // Stateless w.r.t. the public API; may be revisited. - } - /// Run a CCD pass. *Not exposed directly* — CCD is driven by - /// ``PhysicsWorld.step``. - /// - /// :raises NotImplementedError: always; use ``PhysicsWorld.step`` - /// which handles CCD internally. - #[allow(unused_variables)] - #[pyo3(signature = (*args, **kwargs))] - fn solve_ccd( - &self, - args: &Bound<'_, $crate::pyo3::types::PyTuple>, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult<()> { - Err(PyNotImplementedError::new_err( - "CCDSolver.solve_ccd is wired in the pipeline. \ - Call PhysicsWorld.step instead.", - )) - } - } - }; -} - -// ---------------------------------------------------------------------- -// 3D-specific dynamics types -// ---------------------------------------------------------------------- - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_3d { - () => { - // ============================================================ - // FrictionModel (3D only) - // ============================================================ - - /// Friction model used by the 3D contact solver. - /// - /// - ``COEFFICIENT`` — simplified pyramidal friction. Faster and - /// numerically friendly; the default. - /// - ``COULOMB`` — circular Coulomb friction cone. More physically - /// correct, slightly more expensive. - #[pyclass(name = "FrictionModel", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum FrictionModel { - /// Cheap pyramidal approximation of the friction cone. - COEFFICIENT, - /// True circular Coulomb friction cone. - COULOMB, - } - - impl FrictionModel { - #[inline] - fn to_rapier(self) -> rapier::dynamics::FrictionModel { - match self { - Self::COEFFICIENT => rapier::dynamics::FrictionModel::Simplified, - Self::COULOMB => rapier::dynamics::FrictionModel::Coulomb, - } - } - #[inline] - fn from_rapier(r: rapier::dynamics::FrictionModel) -> Self { - match r { - rapier::dynamics::FrictionModel::Simplified => Self::COEFFICIENT, - rapier::dynamics::FrictionModel::Coulomb => Self::COULOMB, - } - } - } - - // ============================================================ - // MassProperties (3D) - // ============================================================ - - /// Mass, center of mass and inertia tensor of a rigid body (3D). - /// - /// In 3D the inertia tensor is stored in its diagonal (principal) - /// form together with the rotation ``principal_inertia_local_frame`` - /// aligning the principal axes with the body's local frame. - /// - /// Use one of the ``from_*`` factories to derive mass properties - /// from a shape and a density, or construct directly with - /// ``MassProperties(local_com, mass, principal_inertia, frame)``. - /// Mass properties of compound bodies can be combined with ``+``; - /// subtracting (``-``) inverts the operation. - #[pyclass(name = "MassProperties", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct MassProperties(pub rapier::dynamics::MassProperties); - - #[pymethods] - impl MassProperties { - /// Build mass properties from explicit values. - /// - /// :param local_com: center of mass in body-local space (default - /// origin). - /// :param mass: total mass (default ``0``). - /// :param principal_inertia: diagonal of the principal inertia - /// tensor (default zero). - /// :param principal_inertia_local_frame: rotation aligning the - /// principal axes with the body frame (default identity). - #[new] - #[pyo3(signature = (local_com=None, mass=0.0 as Real, principal_inertia=None, principal_inertia_local_frame=None))] - fn new( - local_com: Option, - mass: Real, - principal_inertia: Option, - principal_inertia_local_frame: Option, - ) -> Self { - let com = local_com.map(|p| p.0.coords).unwrap_or_else($crate::na::Vector3::zeros); - let inertia = principal_inertia.map(|v| v.0).unwrap_or_else($crate::na::Vector3::zeros); - let frame = principal_inertia_local_frame - .map(|r| r.0) - .unwrap_or_else($crate::na::UnitQuaternion::identity); - Self(rapier::dynamics::MassProperties::with_principal_inertia_frame( - com.into(), - mass, - inertia.into(), - frame.into(), - )) - } - - /// Return the zero mass properties (mass = inertia = ``0``). - #[staticmethod] - fn zero() -> Self { Self(rapier::dynamics::MassProperties::default()) } - - /// Mass properties of a uniform-density ball. - /// - /// :param density: mass per unit volume. - /// :param radius: ball radius. - #[staticmethod] - fn from_ball(density: Real, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_ball(density, radius)) - } - - /// Mass properties of a uniform-density axis-aligned cuboid. - /// - /// :param density: mass per unit volume. - /// :param half_extents: half-extents along ``x``, ``y``, ``z``. - #[staticmethod] - fn from_cuboid(density: Real, half_extents: PyVector) -> Self { - Self(rapier::dynamics::MassProperties::from_cuboid( - density, half_extents.0.into(), - )) - } - - /// Mass properties of a uniform-density capsule between two - /// endpoints. - /// - /// :param density: mass per unit volume. - /// :param a: capsule segment start (body-local). - /// :param b: capsule segment end (body-local). - /// :param radius: capsule radius. - #[staticmethod] - fn from_capsule(density: Real, a: PyPoint, b: PyPoint, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_capsule( - density, a.0.into(), b.0.into(), radius, - )) - } - - /// Mass properties of a uniform-density cylinder along the local - /// ``y`` axis. - /// - /// :param density: mass per unit volume. - /// :param half_height: half the cylinder height. - /// :param radius: cylinder radius. - #[staticmethod] - fn from_cylinder(density: Real, half_height: Real, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_cylinder( - density, half_height, radius, - )) - } - - /// Mass properties of a uniform-density cone along the local - /// ``y`` axis. - /// - /// :param density: mass per unit volume. - /// :param half_height: half the cone height. - /// :param radius: base radius. - #[staticmethod] - fn from_cone(density: Real, half_height: Real, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_cone( - density, half_height, radius, - )) - } - - /// Total mass (read-only). - #[getter] fn mass(&self) -> Real { self.0.mass() } - /// Inverse mass — ``0`` for an infinite-mass body (read-only). - #[getter] fn inv_mass(&self) -> Real { self.0.inv_mass } - - /// Center of mass expressed in body-local coordinates (read-only). - #[getter] - fn local_com(&self) -> Point3 { - let v: $crate::na::Vector3 = self.0.local_com.into(); - Point3($crate::na::Point3::from(v)) - } - - /// Principal inertia (diagonal of the inertia tensor in the - /// principal frame) (read-only). - #[getter] - fn principal_inertia(&self) -> Vec3 { - let v: $crate::na::Vector3 = self.0.principal_inertia().into(); - Vec3(v) - } - - /// Componentwise square root of the inverse principal inertia - /// (read-only). Used by the solver to scale angular impulses. - #[getter] - fn inv_principal_inertia_sqrt(&self) -> Vec3 { - let v = self.0.inv_principal_inertia; - Vec3($crate::na::Vector3::new(v.x.sqrt(), v.y.sqrt(), v.z.sqrt())) - } - - /// Rotation aligning the principal axes with the body frame - /// (read-only). - #[getter] - fn principal_inertia_local_frame(&self) -> Rotation3 { - let q: $crate::na::UnitQuaternion = self.0.principal_inertia_local_frame.into(); - Rotation3(q) - } - - /// Return a copy transformed by ``iso``. - /// - /// Useful when expressing one body's mass properties in another - /// frame, e.g. when attaching as a child. - fn transform_by(&self, iso: PyIsometry) -> Self { - let pose: rapier::math::Pose = iso.0.into(); - Self(self.0.transform_by(&pose)) - } - - /// Sum two mass distributions (parallel-axis theorem). - fn __add__(&self, other: &MassProperties) -> Self { Self(self.0 + other.0) } - /// Subtract ``other`` from ``self``; inverse of ``__add__``. - fn __sub__(&self, other: &MassProperties) -> Self { Self(self.0 - other.0) } - - fn __repr__(&self) -> String { - format!( - "MassProperties(mass={}, local_com=({}, {}, {}))", - self.0.mass(), - self.0.local_com.x, self.0.local_com.y, self.0.local_com.z, - ) - } - } - - // ============================================================ - // RigidBodyAdditionalMassProps (3D) - // ============================================================ - - /// Extra mass / mass properties to add to a body on top of what its - /// colliders contribute. - /// - /// Useful when you want a body's collider geometry to provide most of - /// the inertia but still bias the total mass. - #[pyclass(name = "RigidBodyAdditionalMassProps", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyAdditionalMassProps(pub rapier::dynamics::RigidBodyAdditionalMassProps); - - #[pymethods] - impl RigidBodyAdditionalMassProps { - /// Add ``mass`` to the body without changing its center of mass - /// or inertia tensor. - #[staticmethod] - fn from_mass(mass: Real) -> Self { - Self(rapier::dynamics::RigidBodyAdditionalMassProps::Mass(mass)) - } - /// Add the given full mass properties to the body. - #[staticmethod] - fn from_mass_properties(mp: &MassProperties) -> Self { - Self(rapier::dynamics::RigidBodyAdditionalMassProps::MassProps(mp.0)) - } - } - - // ============================================================ - // RigidBody component views (3D) - // ============================================================ - - /// Linear and angular damping coefficients (per-second decay rates) - /// applied to a body's velocity each step. - /// - /// Damping bleeds energy away independent of contacts; useful for - /// stabilizing simulations and modeling drag. - #[pyclass(name = "RigidBodyDamping", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyDamping { - /// Linear-velocity damping coefficient (read+write). - #[pyo3(get, set)] pub linear_damping: Real, - /// Angular-velocity damping coefficient (read+write). - #[pyo3(get, set)] pub angular_damping: Real, - } - #[pymethods] - impl RigidBodyDamping { - /// Build damping coefficients (default zero / no damping). - #[new] - #[pyo3(signature = (linear_damping=0.0 as Real, angular_damping=0.0 as Real))] - fn new(linear_damping: Real, angular_damping: Real) -> Self { - Self { linear_damping, angular_damping } - } - } - - /// Dominance group used to bias contact resolution between bodies. - /// - /// In a contact between bodies of different dominance, the - /// higher-dominance body behaves as if it had infinite mass with - /// respect to the lower-dominance one. Groups range in ``[-127, 127]``. - #[pyclass(name = "RigidBodyDominance", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyDominance { - /// Dominance group identifier (read+write). - #[pyo3(get, set)] pub group: i8, - } - #[pymethods] - impl RigidBodyDominance { - /// Build a dominance descriptor (default group ``0``). - #[new] - #[pyo3(signature = (group=0))] - fn new(group: i8) -> Self { Self { group } } - } - - /// Per-body CCD configuration. - /// - /// ``ccd_enabled`` toggles substep CCD; ``soft_ccd_prediction`` is a - /// distance over which "soft" CCD pushes the body back from imminent - /// penetrations (``0`` disables soft CCD). - #[pyclass(name = "RigidBodyCcd", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyCcd { - /// Whether substep CCD is enabled for this body (read+write). - #[pyo3(get, set)] pub ccd_enabled: bool, - /// Soft-CCD prediction distance (read+write); ``0`` disables it. - #[pyo3(get, set)] pub soft_ccd_prediction: Real, - } - #[pymethods] - impl RigidBodyCcd { - /// Build a CCD descriptor (defaults: disabled, ``0`` soft CCD). - #[new] - #[pyo3(signature = (ccd_enabled=false, soft_ccd_prediction=0.0 as Real))] - fn new(ccd_enabled: bool, soft_ccd_prediction: Real) -> Self { - Self { ccd_enabled, soft_ccd_prediction } - } - } - - /// Linear and angular velocity of a rigid body (3D). - /// - /// In 3D both ``linvel`` and ``angvel`` are 3-component vectors. The - /// angular velocity is expressed in world space, with magnitude in - /// radians per second. - #[pyclass(name = "RigidBodyVelocity", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyVelocity { - /// Linear velocity in world space (read+write). - #[pyo3(get, set)] pub linvel: Vec3, - /// Angular velocity in world space, rad/s (read+write). - #[pyo3(get, set)] pub angvel: Vec3, - } - #[pymethods] - impl RigidBodyVelocity { - /// Build a velocity (default ``linvel = angvel = 0``). - #[new] - #[pyo3(signature = (linvel=None, angvel=None))] - fn new(linvel: Option, angvel: Option) -> Self { - let lin = linvel.map(|v| v.0).unwrap_or_else($crate::na::Vector3::zeros); - let ang = angvel.map(|v| v.0).unwrap_or_else($crate::na::Vector3::zeros); - Self { linvel: Vec3(lin), angvel: Vec3(ang) } - } - } - - /// Accumulated external forces, torques and gravity scaling. - /// - /// ``force`` and ``torque`` accumulate over a single step and are - /// cleared automatically before the next ``step()`` (unlike impulses, - /// which act once instantaneously). - #[pyclass(name = "RigidBodyForces", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyForces { - /// Accumulated linear force, world space (read+write). - #[pyo3(get, set)] pub force: Vec3, - /// Accumulated torque, world space (read+write). - #[pyo3(get, set)] pub torque: Vec3, - /// Per-body multiplier on the world gravity (read+write). - #[pyo3(get, set)] pub gravity_scale: Real, - } - - /// Body-local mass properties view. - /// - /// Mirror of the ``MassProperties`` stored on a ``RigidBody``. - #[pyclass(name = "RigidBodyMassProps", module = "rapier")] - #[derive(Debug, Clone)] - pub struct RigidBodyMassProps { - /// Mass properties expressed in body-local space (read-only). - #[pyo3(get)] pub local_mprops: MassProperties, - } - - /// Current and predicted pose of a rigid body. - /// - /// ``position`` is the pose used for queries; ``next_position`` is the - /// pose that will be committed after integration. - #[pyclass(name = "RigidBodyPosition", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyPosition { - /// Current world-space pose (read+write). - #[pyo3(get, set)] pub position: Isometry3, - /// Predicted world-space pose after the next step (read+write). - #[pyo3(get, set)] pub next_position: Isometry3, - } - - // ============================================================ - // LockedAxes (3D) - // ============================================================ - - /// Bitflags selecting which translation / rotation degrees of freedom - /// of a rigid body are locked (3D). - /// - /// Combine flags with ``|`` (union), ``&`` (intersection), ``^`` (xor) - /// and ``-`` (set difference). ``in`` / ``contains`` test subset - /// membership. - /// - /// Example:: - /// - /// # Lock translation along Y and all rotations: - /// axes = LockedAxes.TRANSLATION_LOCKED_Y | LockedAxes.ROTATION_LOCKED - #[pyclass(name = "LockedAxes", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct LockedAxes(pub rapier::dynamics::LockedAxes); - - #[pymethods] - impl LockedAxes { - /// Build a ``LockedAxes`` value from a raw bitmask. - /// - /// :param bits: bitmask combining the ``LockedAxes`` class - /// attributes (default ``0`` = no axes locked). - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier::dynamics::LockedAxes::from_bits_truncate(bits)) - } - /// Return the empty flag set (no axes locked). - #[staticmethod] - fn empty() -> Self { Self(rapier::dynamics::LockedAxes::empty()) } - /// Return the flag set locking every translation and rotation - /// axis. - #[staticmethod] - fn all_axes() -> Self { Self(rapier::dynamics::LockedAxes::all()) } - - /// Lock translation along the local ``x`` axis. - #[classattr] - const TRANSLATION_LOCKED_X: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X); - /// Lock translation along the local ``y`` axis. - #[classattr] - const TRANSLATION_LOCKED_Y: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y); - /// Lock translation along the local ``z`` axis. - #[classattr] - const TRANSLATION_LOCKED_Z: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Z); - /// Lock all three translation axes. - #[classattr] - const TRANSLATION_LOCKED: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED); - /// Lock rotation around the local ``x`` axis. - #[classattr] - const ROTATION_LOCKED_X: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_X); - /// Lock rotation around the local ``y`` axis. - #[classattr] - const ROTATION_LOCKED_Y: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_Y); - /// Lock rotation around the local ``z`` axis. - #[classattr] - const ROTATION_LOCKED_Z: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED_Z); - /// Lock all three rotation axes. - #[classattr] - const ROTATION_LOCKED: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED); - /// Lock every translation and rotation axis. - #[classattr] - const ALL: LockedAxes = LockedAxes(rapier::dynamics::LockedAxes::all()); - - /// Raw bitmask backing this flag set (read-only). - #[getter] fn bits(&self) -> u8 { self.0.bits() } - /// Whether ``self`` is a superset of ``other``. - fn contains(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Whether no axes are locked. - fn is_empty(&self) -> bool { self.0.is_empty() } - /// ``other in self`` — subset test (alias for ``contains``). - fn __contains__(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Union of two flag sets (``self | other``). - fn __or__(&self, other: &Self) -> Self { Self(self.0 | other.0) } - /// Intersection of two flag sets (``self & other``). - fn __and__(&self, other: &Self) -> Self { Self(self.0 & other.0) } - /// Symmetric difference (``self ^ other``). - fn __xor__(&self, other: &Self) -> Self { Self(self.0 ^ other.0) } - /// Complement of ``self`` within the full set of axes. - fn __invert__(&self) -> Self { Self(!self.0) } - /// Set difference (``self - other``): axes in ``self`` not in - /// ``other``. - fn __sub__(&self, other: &Self) -> Self { Self(self.0 - other.0) } - /// Truthiness: ``True`` if any axis is locked. - fn __bool__(&self) -> bool { !self.0.is_empty() } - /// Equality comparison. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: when an ordering comparison is requested. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("LockedAxes supports only == and !=")), - } - } - /// Hash equal to the underlying bitmask. - fn __hash__(&self) -> u64 { self.0.bits() as u64 } - fn __repr__(&self) -> String { - format!("LockedAxes(bits={:#010b})", self.0.bits()) - } - } - - // ============================================================ - // IntegrationParameters (3D — has friction_model) - // ============================================================ - - /// Tunable parameters for the time-step and contact/joint solver. - /// - /// The defaults (``IntegrationParameters()``) are appropriate for - /// most games and robotics simulations at ``dt ≈ 1/60`` seconds. - /// - /// Many fields are stored in *normalized* units of ``length_unit``: - /// e.g. ``normalized_allowed_linear_error`` is in fractions of - /// ``length_unit``. Use the helper methods ``allowed_linear_error()``, - /// ``max_corrective_velocity()`` and ``prediction_distance()`` to get - /// the de-normalized values. - #[pyclass(name = "IntegrationParameters", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct IntegrationParameters(pub rapier::dynamics::IntegrationParameters); - - #[pymethods] - impl IntegrationParameters { - /// Build the default integration parameters. - #[new] - fn new() -> Self { Self(rapier::dynamics::IntegrationParameters::default()) } - - /// Return the default integration parameters. - #[staticmethod] - fn default_params() -> Self { - Self(rapier::dynamics::IntegrationParameters::default()) - } - - /// Simulation timestep in seconds (default ``≈ 1/60``). - #[getter] fn dt(&self) -> Real { self.0.dt } - /// Set the simulation timestep in seconds. - #[setter] fn set_dt(&mut self, v: Real) { self.0.dt = v; } - /// Minimum substep length used by CCD, in seconds. - #[getter] fn min_ccd_dt(&self) -> Real { self.0.min_ccd_dt } - /// Set the minimum CCD substep length. - #[setter] fn set_min_ccd_dt(&mut self, v: Real) { self.0.min_ccd_dt = v; } - /// Warmstart coefficient in ``[0, 1]`` for the solver's impulse - /// reuse between steps; higher values converge faster but may - /// jitter. - #[getter] fn warmstart_coefficient(&self) -> Real { self.0.warmstart_coefficient } - /// Set the warmstart coefficient. - #[setter] fn set_warmstart_coefficient(&mut self, v: Real) { self.0.warmstart_coefficient = v; } - /// Reference length unit (1 meter by default). Other "normalized" - /// parameters are expressed in fractions of this unit. - #[getter] fn length_unit(&self) -> Real { self.0.length_unit } - /// Set the reference length unit. - #[setter] fn set_length_unit(&mut self, v: Real) { self.0.length_unit = v; } - /// Allowed penetration error, in fractions of ``length_unit``. - #[getter] fn normalized_allowed_linear_error(&self) -> Real { self.0.normalized_allowed_linear_error } - /// Set the normalized allowed linear error. - #[setter] fn set_normalized_allowed_linear_error(&mut self, v: Real) { self.0.normalized_allowed_linear_error = v; } - /// Maximum penetration-correction velocity, in fractions of - /// ``length_unit`` per second. - #[getter] fn normalized_max_corrective_velocity(&self) -> Real { self.0.normalized_max_corrective_velocity } - /// Set the normalized maximum corrective velocity. - #[setter] fn set_normalized_max_corrective_velocity(&mut self, v: Real) { self.0.normalized_max_corrective_velocity = v; } - /// Contact prediction distance, in fractions of ``length_unit``. - #[getter] fn normalized_prediction_distance(&self) -> Real { self.0.normalized_prediction_distance } - /// Set the normalized prediction distance. - #[setter] fn set_normalized_prediction_distance(&mut self, v: Real) { self.0.normalized_prediction_distance = v; } - /// Outer (TGS) solver iterations per step. - #[getter] fn num_solver_iterations(&self) -> usize { self.0.num_solver_iterations } - /// Set the outer solver iteration count. - #[setter] fn set_num_solver_iterations(&mut self, v: usize) { self.0.num_solver_iterations = v; } - /// Inner PGS iterations per outer solver iteration. - #[getter] fn num_internal_pgs_iterations(&self) -> usize { self.0.num_internal_pgs_iterations } - /// Set the inner PGS iteration count. - #[setter] fn set_num_internal_pgs_iterations(&mut self, v: usize) { self.0.num_internal_pgs_iterations = v; } - /// Inner stabilization iterations per outer solver iteration. - #[getter] fn num_internal_stabilization_iterations(&self) -> usize { self.0.num_internal_stabilization_iterations } - /// Set the inner stabilization iteration count. - #[setter] fn set_num_internal_stabilization_iterations(&mut self, v: usize) { self.0.num_internal_stabilization_iterations = v; } - /// Minimum number of bodies in an island before it is solved in - /// parallel. - #[getter] fn min_island_size(&self) -> usize { self.0.min_island_size } - /// Set the minimum parallel-island size. - #[setter] fn set_min_island_size(&mut self, v: usize) { self.0.min_island_size = v; } - /// Maximum number of CCD substeps per simulation step. - #[getter] fn max_ccd_substeps(&self) -> usize { self.0.max_ccd_substeps } - /// Set the maximum CCD substep count. - #[setter] fn set_max_ccd_substeps(&mut self, v: usize) { self.0.max_ccd_substeps = v; } - - /// Spring coefficients used to regularize contacts. - #[getter] - fn contact_softness(&self) -> SpringCoefficients { - SpringCoefficients(self.0.contact_softness) - } - /// Set the contact-spring coefficients. - #[setter] - fn set_contact_softness(&mut self, v: SpringCoefficients) { - self.0.contact_softness = v.0; - } - - /// 3D friction model used by the contact solver. - #[getter] - fn friction_model(&self) -> FrictionModel { - FrictionModel::from_rapier(self.0.friction_model) - } - /// Set the 3D friction model. - #[setter] - fn set_friction_model(&mut self, v: FrictionModel) { - self.0.friction_model = v.to_rapier(); - } - - /// Reciprocal of ``dt`` (``1 / dt``). - fn inv_dt(&self) -> Real { self.0.inv_dt() } - /// Allowed penetration error in world units (denormalized). - fn allowed_linear_error(&self) -> Real { self.0.allowed_linear_error() } - /// Maximum penetration-correction velocity in world units - /// (denormalized). - fn max_corrective_velocity(&self) -> Real { self.0.max_corrective_velocity() } - /// Contact prediction distance in world units (denormalized). - fn prediction_distance(&self) -> Real { self.0.prediction_distance() } - - /// Stiffness component of the contact regularization spring. - fn contact_spring_stiffness(&self) -> Real { self.0.contact_softness.natural_frequency } - /// Damping component of the contact regularization spring. - fn contact_spring_damping(&self) -> Real { self.0.contact_softness.damping_ratio } - /// Default joint-spring stiffness (engine constant). - fn joint_spring_stiffness(&self) -> Real { - rapier::dynamics::SpringCoefficients::::joint_defaults().natural_frequency - } - /// Default joint-spring damping (engine constant). - fn joint_spring_damping(&self) -> Real { - rapier::dynamics::SpringCoefficients::::joint_defaults().damping_ratio - } - - /// Equality comparison. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: when an ordering comparison is requested. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("IntegrationParameters supports only == and !=")), - } - } - fn __repr__(&self) -> String { - format!("IntegrationParameters(dt={}, num_solver_iterations={})", self.0.dt, self.0.num_solver_iterations) - } - } - - $crate::__define_dynamics_rigid_body_3d!(); - $crate::__define_dynamics_rigid_body_set!(); - }; -} - -// ---------------------------------------------------------------------- -// 2D-specific dynamics types -// ---------------------------------------------------------------------- - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_2d { - () => { - // ============================================================ - // MassProperties (2D) - // ============================================================ - - /// Mass, center of mass and scalar moment of inertia (2D). - /// - /// In 2D the inertia tensor reduces to a single scalar — the moment - /// of inertia about the out-of-plane (``z``) axis through the center - /// of mass. - /// - /// Use ``from_ball``, ``from_cuboid`` or ``from_capsule`` to derive - /// values from a shape and density, or construct directly with - /// ``MassProperties(local_com, mass, principal_inertia)``. Mass - /// properties combine with ``+`` (and invert with ``-``). - #[pyclass(name = "MassProperties", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct MassProperties(pub rapier::dynamics::MassProperties); - - #[pymethods] - impl MassProperties { - /// Build mass properties from explicit values. - /// - /// :param local_com: center of mass in body-local space (default - /// origin). - /// :param mass: total mass (default ``0``). - /// :param principal_inertia: scalar moment of inertia about the - /// center of mass (default ``0``). - #[new] - #[pyo3(signature = (local_com=None, mass=0.0 as Real, principal_inertia=0.0 as Real))] - fn new(local_com: Option, mass: Real, principal_inertia: Real) -> Self { - let com = local_com - .map(|p| p.0.coords) - .unwrap_or_else($crate::na::Vector2::zeros); - Self(rapier::dynamics::MassProperties::new( - com.into(), - mass, - principal_inertia, - )) - } - - /// Zero mass properties (mass = inertia = ``0``). - #[staticmethod] - fn zero() -> Self { - Self(rapier::dynamics::MassProperties::default()) - } - - /// Mass properties of a uniform-density disk. - /// - /// :param density: mass per unit area. - /// :param radius: disk radius. - #[staticmethod] - fn from_ball(density: Real, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_ball(density, radius)) - } - - /// Mass properties of a uniform-density axis-aligned rectangle. - /// - /// :param density: mass per unit area. - /// :param half_extents: half-extents along ``x`` and ``y``. - #[staticmethod] - fn from_cuboid(density: Real, half_extents: PyVector) -> Self { - Self(rapier::dynamics::MassProperties::from_cuboid( - density, - half_extents.0.into(), - )) - } - - /// Mass properties of a uniform-density capsule between two - /// endpoints. - /// - /// :param density: mass per unit area. - /// :param a: capsule segment start (body-local). - /// :param b: capsule segment end (body-local). - /// :param radius: capsule radius. - #[staticmethod] - fn from_capsule(density: Real, a: PyPoint, b: PyPoint, radius: Real) -> Self { - Self(rapier::dynamics::MassProperties::from_capsule( - density, - a.0.into(), - b.0.into(), - radius, - )) - } - - /// Total mass (read-only). - #[getter] - fn mass(&self) -> Real { - self.0.mass() - } - /// Inverse mass — ``0`` for an infinite-mass body (read-only). - #[getter] - fn inv_mass(&self) -> Real { - self.0.inv_mass - } - - /// Center of mass in body-local space (read-only). - #[getter] - fn local_com(&self) -> Point2 { - let v: $crate::na::Vector2 = self.0.local_com.into(); - Point2($crate::na::Point2::from(v)) - } - - /// Scalar moment of inertia about the center of mass (read-only). - #[getter] - fn principal_inertia(&self) -> Real { - self.0.principal_inertia() - } - - /// Square root of the inverse moment of inertia (read-only). - /// Used by the solver to scale angular impulses. - #[getter] - fn inv_principal_inertia_sqrt(&self) -> Real { - self.0.inv_principal_inertia.sqrt() - } - - /// Return a copy transformed by ``iso``. - fn transform_by(&self, iso: PyIsometry) -> Self { - let pose: rapier::math::Pose = iso.0.into(); - Self(self.0.transform_by(&pose)) - } - - /// Sum two mass distributions (parallel-axis theorem). - fn __add__(&self, other: &MassProperties) -> Self { - Self(self.0 + other.0) - } - /// Subtract ``other`` from ``self``; inverse of ``__add__``. - fn __sub__(&self, other: &MassProperties) -> Self { - Self(self.0 - other.0) - } - - fn __repr__(&self) -> String { - format!( - "MassProperties(mass={}, local_com=({}, {}))", - self.0.mass(), - self.0.local_com.x, - self.0.local_com.y, - ) - } - } - - // ============================================================ - // RigidBodyAdditionalMassProps (2D) - // ============================================================ - - /// Extra mass / mass properties to add to a body on top of what its - /// colliders contribute. - /// - /// Useful when you want a body's collider geometry to provide most of - /// the inertia but still bias the total mass. - #[pyclass(name = "RigidBodyAdditionalMassProps", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyAdditionalMassProps(pub rapier::dynamics::RigidBodyAdditionalMassProps); - - #[pymethods] - impl RigidBodyAdditionalMassProps { - /// Add ``mass`` to the body without changing its center of mass - /// or moment of inertia. - #[staticmethod] - fn from_mass(mass: Real) -> Self { - Self(rapier::dynamics::RigidBodyAdditionalMassProps::Mass(mass)) - } - /// Add the given full mass properties to the body. - #[staticmethod] - fn from_mass_properties(mp: &MassProperties) -> Self { - Self(rapier::dynamics::RigidBodyAdditionalMassProps::MassProps( - mp.0, - )) - } - } - - // ============================================================ - // RigidBody component views (2D) - // ============================================================ - - /// Linear and angular damping coefficients (per-second decay rates) - /// applied to a body's velocity each step. - #[pyclass(name = "RigidBodyDamping", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyDamping { - /// Linear-velocity damping coefficient (read+write). - #[pyo3(get, set)] - pub linear_damping: Real, - /// Angular-velocity damping coefficient (read+write). - #[pyo3(get, set)] - pub angular_damping: Real, - } - #[pymethods] - impl RigidBodyDamping { - /// Build damping coefficients (default zero / no damping). - #[new] - #[pyo3(signature = (linear_damping=0.0 as Real, angular_damping=0.0 as Real))] - fn new(linear_damping: Real, angular_damping: Real) -> Self { - Self { - linear_damping, - angular_damping, - } - } - } - - /// Dominance group used to bias contact resolution between bodies. - /// - /// In a contact between bodies of different dominance, the - /// higher-dominance body behaves as if it had infinite mass with - /// respect to the lower-dominance one. Groups range in ``[-127, 127]``. - #[pyclass(name = "RigidBodyDominance", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyDominance { - /// Dominance group identifier (read+write). - #[pyo3(get, set)] - pub group: i8, - } - #[pymethods] - impl RigidBodyDominance { - /// Build a dominance descriptor (default group ``0``). - #[new] - #[pyo3(signature = (group=0))] - fn new(group: i8) -> Self { - Self { group } - } - } - - /// Per-body CCD configuration. - /// - /// ``ccd_enabled`` toggles substep CCD; ``soft_ccd_prediction`` is a - /// distance over which "soft" CCD pushes the body back from imminent - /// penetrations (``0`` disables soft CCD). - #[pyclass(name = "RigidBodyCcd", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyCcd { - /// Whether substep CCD is enabled for this body (read+write). - #[pyo3(get, set)] - pub ccd_enabled: bool, - /// Soft-CCD prediction distance (read+write); ``0`` disables it. - #[pyo3(get, set)] - pub soft_ccd_prediction: Real, - } - #[pymethods] - impl RigidBodyCcd { - /// Build a CCD descriptor (defaults: disabled, ``0`` soft CCD). - #[new] - #[pyo3(signature = (ccd_enabled=false, soft_ccd_prediction=0.0 as Real))] - fn new(ccd_enabled: bool, soft_ccd_prediction: Real) -> Self { - Self { - ccd_enabled, - soft_ccd_prediction, - } - } - } - - /// Linear and angular velocity of a rigid body (2D). - /// - /// In 2D the linear velocity is a 2-vector and the angular velocity - /// is a scalar (rotation around the out-of-plane axis), in rad/s. - #[pyclass(name = "RigidBodyVelocity", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyVelocity { - /// Linear velocity in world space (read+write). - #[pyo3(get, set)] - pub linvel: Vec2, - /// Angular velocity, in rad/s (read+write). - #[pyo3(get, set)] - pub angvel: Real, - } - #[pymethods] - impl RigidBodyVelocity { - /// Build a velocity (default ``linvel = 0``, ``angvel = 0``). - #[new] - #[pyo3(signature = (linvel=None, angvel=0.0 as Real))] - fn new(linvel: Option, angvel: Real) -> Self { - let lin = linvel - .map(|v| v.0) - .unwrap_or_else($crate::na::Vector2::zeros); - Self { - linvel: Vec2(lin), - angvel, - } - } - } - - /// Accumulated external forces, torques and gravity scaling (2D). - /// - /// ``force`` and ``torque`` accumulate over a single step and are - /// cleared automatically before the next ``step()`` (unlike impulses, - /// which act once instantaneously). - #[pyclass(name = "RigidBodyForces", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyForces { - /// Accumulated linear force, world space (read+write). - #[pyo3(get, set)] - pub force: Vec2, - /// Accumulated torque (scalar in 2D) (read+write). - #[pyo3(get, set)] - pub torque: Real, - /// Per-body multiplier on the world gravity (read+write). - #[pyo3(get, set)] - pub gravity_scale: Real, - } - - /// Body-local mass properties view. - /// - /// Mirror of the ``MassProperties`` stored on a ``RigidBody``. - #[pyclass(name = "RigidBodyMassProps", module = "rapier")] - #[derive(Debug, Clone)] - pub struct RigidBodyMassProps { - /// Mass properties expressed in body-local space (read-only). - #[pyo3(get)] - pub local_mprops: MassProperties, - } - - /// Current and predicted pose of a rigid body (2D). - /// - /// ``position`` is the pose used for queries; ``next_position`` is - /// the pose that will be committed after integration. - #[pyclass(name = "RigidBodyPosition", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RigidBodyPosition { - /// Current world-space pose (read+write). - #[pyo3(get, set)] - pub position: Isometry2, - /// Predicted world-space pose after the next step (read+write). - #[pyo3(get, set)] - pub next_position: Isometry2, - } - - // ============================================================ - // LockedAxes (2D) - // ============================================================ - - /// Bitflags selecting which translation / rotation degrees of freedom - /// of a rigid body are locked (2D). - /// - /// Only ``x`` / ``y`` translations and the (single) out-of-plane - /// rotation are meaningful in 2D. ``ROTATION_LOCKED`` locks the only - /// rotation. Combine flags with ``|``, ``&``, ``^``, ``-``. - /// - /// Example:: - /// - /// # Pin the body in place along Y while still allowing it to - /// # rotate freely: - /// axes = LockedAxes.TRANSLATION_LOCKED_Y - #[pyclass(name = "LockedAxes", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct LockedAxes(pub rapier::dynamics::LockedAxes); - - #[pymethods] - impl LockedAxes { - /// Build a ``LockedAxes`` value from a raw bitmask. - /// - /// :param bits: bitmask combining the ``LockedAxes`` class - /// attributes (default ``0`` = no axes locked). - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier::dynamics::LockedAxes::from_bits_truncate(bits)) - } - /// Empty flag set (no axes locked). - #[staticmethod] - fn empty() -> Self { - Self(rapier::dynamics::LockedAxes::empty()) - } - /// Flag set locking every available 2D axis. - #[staticmethod] - fn all_axes() -> Self { - Self(rapier::dynamics::LockedAxes::all()) - } - - // 2D names: only X/Y translation and Z rotation are meaningful. - // `ROTATION_LOCKED` is an alias for ROTATION_LOCKED_Z in 2D. - /// Lock translation along the local ``x`` axis. - #[classattr] - const TRANSLATION_LOCKED_X: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X); - /// Lock translation along the local ``y`` axis. - #[classattr] - const TRANSLATION_LOCKED_Y: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y); - /// Lock translation along both ``x`` and ``y``. - #[classattr] - const TRANSLATION_LOCKED: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::from_bits_truncate( - rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X.bits() - | rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y.bits(), - )); - /// Lock the out-of-plane rotation. - #[classattr] - const ROTATION_LOCKED: LockedAxes = - LockedAxes(rapier::dynamics::LockedAxes::ROTATION_LOCKED); - /// Lock both translations and the rotation. - #[classattr] - const ALL: LockedAxes = LockedAxes(rapier::dynamics::LockedAxes::from_bits_truncate( - rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X.bits() - | rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y.bits() - | rapier::dynamics::LockedAxes::ROTATION_LOCKED.bits(), - )); - - /// Raw bitmask backing this flag set (read-only). - #[getter] - fn bits(&self) -> u8 { - self.0.bits() - } - /// Whether ``self`` is a superset of ``other``. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// Whether no axes are locked. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - /// ``other in self`` — subset test (alias for ``contains``). - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// Union of two flag sets (``self | other``). - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - /// Intersection of two flag sets (``self & other``). - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - /// Symmetric difference (``self ^ other``). - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - /// Complement of ``self`` within the full set of axes. - fn __invert__(&self) -> Self { - Self(!self.0) - } - /// Set difference (``self - other``). - fn __sub__(&self, other: &Self) -> Self { - Self(self.0 - other.0) - } - /// Truthiness: ``True`` if any axis is locked. - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - /// Equality comparison. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: when an ordering comparison is requested. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("LockedAxes supports only == and !=")), - } - } - /// Hash equal to the underlying bitmask. - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - fn __repr__(&self) -> String { - format!("LockedAxes(bits={:#010b})", self.0.bits()) - } - } - - // ============================================================ - // IntegrationParameters (2D — no friction_model) - // ============================================================ - - /// Tunable parameters for the time-step and contact/joint solver - /// (2D — friction is always pyramidal, so ``friction_model`` is not - /// exposed). - /// - /// The defaults (``IntegrationParameters()``) are appropriate for - /// most 2D games at ``dt ≈ 1/60`` seconds. Many fields are stored - /// in fractions of ``length_unit``; use ``allowed_linear_error()``, - /// ``max_corrective_velocity()`` and ``prediction_distance()`` to - /// get the values in world units. - #[pyclass(name = "IntegrationParameters", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct IntegrationParameters(pub rapier::dynamics::IntegrationParameters); - - #[pymethods] - impl IntegrationParameters { - /// Build the default integration parameters. - #[new] - fn new() -> Self { - Self(rapier::dynamics::IntegrationParameters::default()) - } - - /// Return the default integration parameters. - #[staticmethod] - fn default_params() -> Self { - Self(rapier::dynamics::IntegrationParameters::default()) - } - - /// Simulation timestep in seconds (default ``≈ 1/60``). - #[getter] - fn dt(&self) -> Real { - self.0.dt - } - /// Set the simulation timestep in seconds. - #[setter] - fn set_dt(&mut self, v: Real) { - self.0.dt = v; - } - /// Minimum substep length used by CCD, in seconds. - #[getter] - fn min_ccd_dt(&self) -> Real { - self.0.min_ccd_dt - } - /// Set the minimum CCD substep length. - #[setter] - fn set_min_ccd_dt(&mut self, v: Real) { - self.0.min_ccd_dt = v; - } - /// Warmstart coefficient in ``[0, 1]`` for impulse reuse between - /// steps. - #[getter] - fn warmstart_coefficient(&self) -> Real { - self.0.warmstart_coefficient - } - /// Set the warmstart coefficient. - #[setter] - fn set_warmstart_coefficient(&mut self, v: Real) { - self.0.warmstart_coefficient = v; - } - /// Reference length unit (typically ``1.0``). Other "normalized" - /// parameters are expressed in fractions of this. - #[getter] - fn length_unit(&self) -> Real { - self.0.length_unit - } - /// Set the reference length unit. - #[setter] - fn set_length_unit(&mut self, v: Real) { - self.0.length_unit = v; - } - /// Allowed penetration error, in fractions of ``length_unit``. - #[getter] - fn normalized_allowed_linear_error(&self) -> Real { - self.0.normalized_allowed_linear_error - } - /// Set the normalized allowed linear error. - #[setter] - fn set_normalized_allowed_linear_error(&mut self, v: Real) { - self.0.normalized_allowed_linear_error = v; - } - /// Maximum penetration-correction velocity, in fractions of - /// ``length_unit`` per second. - #[getter] - fn normalized_max_corrective_velocity(&self) -> Real { - self.0.normalized_max_corrective_velocity - } - /// Set the normalized maximum corrective velocity. - #[setter] - fn set_normalized_max_corrective_velocity(&mut self, v: Real) { - self.0.normalized_max_corrective_velocity = v; - } - /// Contact prediction distance, in fractions of ``length_unit``. - #[getter] - fn normalized_prediction_distance(&self) -> Real { - self.0.normalized_prediction_distance - } - /// Set the normalized prediction distance. - #[setter] - fn set_normalized_prediction_distance(&mut self, v: Real) { - self.0.normalized_prediction_distance = v; - } - /// Outer (TGS) solver iterations per step. - #[getter] - fn num_solver_iterations(&self) -> usize { - self.0.num_solver_iterations - } - /// Set the outer solver iteration count. - #[setter] - fn set_num_solver_iterations(&mut self, v: usize) { - self.0.num_solver_iterations = v; - } - /// Inner PGS iterations per outer solver iteration. - #[getter] - fn num_internal_pgs_iterations(&self) -> usize { - self.0.num_internal_pgs_iterations - } - /// Set the inner PGS iteration count. - #[setter] - fn set_num_internal_pgs_iterations(&mut self, v: usize) { - self.0.num_internal_pgs_iterations = v; - } - /// Inner stabilization iterations per outer solver iteration. - #[getter] - fn num_internal_stabilization_iterations(&self) -> usize { - self.0.num_internal_stabilization_iterations - } - /// Set the inner stabilization iteration count. - #[setter] - fn set_num_internal_stabilization_iterations(&mut self, v: usize) { - self.0.num_internal_stabilization_iterations = v; - } - /// Minimum number of bodies in an island before it is solved in - /// parallel. - #[getter] - fn min_island_size(&self) -> usize { - self.0.min_island_size - } - /// Set the minimum parallel-island size. - #[setter] - fn set_min_island_size(&mut self, v: usize) { - self.0.min_island_size = v; - } - /// Maximum number of CCD substeps per simulation step. - #[getter] - fn max_ccd_substeps(&self) -> usize { - self.0.max_ccd_substeps - } - /// Set the maximum CCD substep count. - #[setter] - fn set_max_ccd_substeps(&mut self, v: usize) { - self.0.max_ccd_substeps = v; - } - - /// Spring coefficients used to regularize contacts. - #[getter] - fn contact_softness(&self) -> SpringCoefficients { - SpringCoefficients(self.0.contact_softness) - } - /// Set the contact-spring coefficients. - #[setter] - fn set_contact_softness(&mut self, v: SpringCoefficients) { - self.0.contact_softness = v.0; - } - - /// Reciprocal of ``dt`` (``1 / dt``). - fn inv_dt(&self) -> Real { - self.0.inv_dt() - } - /// Allowed penetration error in world units (denormalized). - fn allowed_linear_error(&self) -> Real { - self.0.allowed_linear_error() - } - /// Maximum penetration-correction velocity in world units. - fn max_corrective_velocity(&self) -> Real { - self.0.max_corrective_velocity() - } - /// Contact prediction distance in world units (denormalized). - fn prediction_distance(&self) -> Real { - self.0.prediction_distance() - } - - /// Stiffness component of the contact regularization spring. - fn contact_spring_stiffness(&self) -> Real { - self.0.contact_softness.natural_frequency - } - /// Damping component of the contact regularization spring. - fn contact_spring_damping(&self) -> Real { - self.0.contact_softness.damping_ratio - } - /// Default joint-spring stiffness (engine constant). - fn joint_spring_stiffness(&self) -> Real { - rapier::dynamics::SpringCoefficients::::joint_defaults().natural_frequency - } - /// Default joint-spring damping (engine constant). - fn joint_spring_damping(&self) -> Real { - rapier::dynamics::SpringCoefficients::::joint_defaults().damping_ratio - } - - /// Equality comparison. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: when an ordering comparison is requested. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "IntegrationParameters supports only == and !=", - )), - } - } - fn __repr__(&self) -> String { - format!( - "IntegrationParameters(dt={}, num_solver_iterations={})", - self.0.dt, self.0.num_solver_iterations - ) - } - } - - $crate::__define_dynamics_rigid_body_2d!(); - $crate::__define_dynamics_rigid_body_set!(); - }; -} - -// ---------------------------------------------------------------------- -// RigidBody, RigidBodyBuilder — 3D -// ---------------------------------------------------------------------- -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_rigid_body_3d { - () => { - $crate::__define_dynamics_rigid_body_common!(3, Vec3, Point3, Rotation3, Isometry3); - - #[pymethods] - impl RigidBody { - // 3D-specific accessors (vector angvel) - /// Angular velocity as a world-space 3-vector (read+write). - #[getter] - fn angvel(&self) -> Vec3 { - let v: $crate::na::Vector3 = self.with_ref(|b| b.angvel()).into(); - Vec3(v) - } - /// Set the angular velocity (wakes the body). - #[setter] - fn set_angvel(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_angvel(g.into(), true)); - } - - /// Torque accumulated by user calls during the current step - /// (read-only). Cleared automatically at the next step. - #[getter] - fn user_torque(&self) -> Vec3 { - let v: $crate::na::Vector3 = self.with_ref(|b| b.user_torque()).into(); - Vec3(v) - } - - /// Per-axis rotation enable mask as ``(rx, ry, rz)`` booleans - /// (``True`` = free to rotate). - #[getter] - fn enabled_rotations(&self) -> (bool, bool, bool) { - let arr = self.with_ref(|b| b.is_rotation_locked()); - (!arr[0], !arr[1], !arr[2]) - } - /// Enable / disable rotation around each axis. - /// - /// :param v: tuple ``(rx, ry, rz)`` of booleans where ``True`` - /// allows rotation and ``False`` locks it. - #[setter] - fn set_enabled_rotations(&mut self, v: (bool, bool, bool)) { - self.with_mut(|b| b.set_enabled_rotations(v.0, v.1, v.2, true)); - } - - /// Per-axis translation enable mask as ``(tx, ty, tz)`` booleans - /// (``True`` = free to translate). - #[getter] - fn enabled_translations(&self) -> (bool, bool, bool) { - let la = self.with_ref(|b| b.locked_axes()); - ( - !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X), - !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y), - !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Z), - ) - } - /// Enable / disable translation along each axis. - /// - /// :param v: tuple ``(tx, ty, tz)`` of booleans. - #[setter] - fn set_enabled_translations(&mut self, v: (bool, bool, bool)) { - self.with_mut(|b| b.set_enabled_translations(v.0, v.1, v.2, true)); - } - - /// Accumulate a torque to be applied during the next step. - /// - /// The torque is cleared after the step (like ``add_force``); - /// use ``apply_torque_impulse`` for an instantaneous change of - /// angular velocity. - /// - /// :param torque: torque vector in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (torque, wake_up=true))] - fn add_torque(&mut self, torque: PyVector, wake_up: bool) { - let g: rapier::math::Vector = torque.0.into(); - self.with_mut(|b| b.add_torque(g.into(), wake_up)); - } - - /// Apply an instantaneous angular impulse (units: ``N·m·s``). - /// - /// :param torque_impulse: torque impulse vector in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (torque_impulse, wake_up=true))] - fn apply_torque_impulse(&mut self, torque_impulse: PyVector, wake_up: bool) { - let g: rapier::math::Vector = torque_impulse.0.into(); - self.with_mut(|b| b.apply_torque_impulse(g.into(), wake_up)); - } - - /// Whether gyroscopic forces are accounted for during integration - /// (read+write). Useful for asymmetric inertia tensors (e.g. - /// spinning tops). - #[getter] - fn gyroscopic_forces_enabled(&self) -> bool { - self.with_ref(|b| b.gyroscopic_forces_enabled()) - } - /// Enable or disable gyroscopic forces. - #[setter] - fn set_gyroscopic_forces_enabled(&mut self, v: bool) { - self.with_mut(|b| b.enable_gyroscopic_forces(v)); - } - - /// Predict the angular velocity after ``dt`` seconds *with* - /// gyroscopic forces taken into account. - /// - /// The body is not modified. - /// - /// :param dt: integration timestep in seconds. - fn angvel_with_gyroscopic_forces(&self, dt: Real) -> Vec3 { - let v: $crate::na::Vector3 = self - .with_ref(|b| b.angvel_with_gyroscopic_forces(dt)) - .into(); - Vec3(v) - } - - // 3D-only builder static methods - /// Start building a dynamic rigid body. Returns a ``RigidBodyBuilder``. - /// - /// Accepts optional kwargs configuring builder fields - /// (``translation``, ``rotation``, ``linvel``, ``angvel``, - /// ``locked_axes``, ``ccd_enabled``, ``gravity_scale``, ...). - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn dynamic( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::dynamic(), kwargs) - } - /// Start building a fixed (immovable) rigid body. - /// - /// Same kwargs as ``dynamic``. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn fixed( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::fixed(), kwargs) - } - /// Start building a velocity-based kinematic rigid body. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn kinematic_velocity_based( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::kinematic_velocity_based(), - kwargs, - ) - } - /// Start building a position-based kinematic rigid body. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn kinematic_position_based( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::kinematic_position_based(), - kwargs, - ) - } - - /// Start building a body of the given ``body_type``. - /// - /// :param body_type: a ``RigidBodyType`` selecting the behavior - /// class. - #[staticmethod] - #[pyo3(signature = (body_type, **kwargs))] - fn new_body( - body_type: RigidBodyType, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::new(body_type.to_rapier()), - kwargs, - ) - } - } - - impl RigidBodyBuilder { - fn apply_angvel_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - *builder = builder.clone().angvel(g.into()); - Ok(()) - } - - fn apply_rotation_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - // 3D rotation kwarg is a scaled-axis-angle vector. - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - *builder = builder.clone().rotation(g.into()); - Ok(()) - } - - fn apply_enabled_translations_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let t: (bool, bool, bool) = v.extract()?; - *builder = builder.clone().enabled_translations(t.0, t.1, t.2); - Ok(()) - } - - fn apply_enabled_rotations_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let t: (bool, bool, bool) = v.extract()?; - *builder = builder.clone().enabled_rotations(t.0, t.1, t.2); - Ok(()) - } - - fn apply_gyroscopic_forces_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let b: bool = v.extract()?; - *builder = builder.clone().gyroscopic_forces_enabled(b); - Ok(()) - } - } - }; -} - -// ---------------------------------------------------------------------- -// RigidBody, RigidBodyBuilder — 2D -// ---------------------------------------------------------------------- -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_rigid_body_2d { - () => { - $crate::__define_dynamics_rigid_body_common!(2, Vec2, Point2, Rotation2, Isometry2); - - #[pymethods] - impl RigidBody { - /// Angular velocity around the out-of-plane axis, in rad/s - /// (read+write). - #[getter] - fn angvel(&self) -> Real { - self.with_ref(|b| b.angvel()) - } - /// Set the angular velocity (wakes the body). - #[setter] - fn set_angvel(&mut self, v: Real) { - self.with_mut(|b| b.set_angvel(v, true)); - } - - /// Torque accumulated by user calls during the current step - /// (read-only). Cleared automatically at the next step. - #[getter] - fn user_torque(&self) -> Real { - self.with_ref(|b| b.user_torque()) - } - - /// Whether rotation is enabled, as a single-element tuple - /// ``(rz,)`` for API symmetry with 3D (``True`` = free to rotate). - #[getter] - fn enabled_rotations(&self) -> (bool,) { - (!self.with_ref(|b| b.is_rotation_locked()),) - } - /// Enable or disable rotation. Pass ``(True,)`` to allow rotation. - #[setter] - fn set_enabled_rotations(&mut self, v: (bool,)) { - self.with_mut(|b| b.lock_rotations(!v.0, true)); - } - - /// Per-axis translation enable mask as ``(tx, ty)`` booleans - /// (``True`` = free to translate). - #[getter] - fn enabled_translations(&self) -> (bool, bool) { - let la = self.with_ref(|b| b.locked_axes()); - ( - !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_X), - !la.contains(rapier::dynamics::LockedAxes::TRANSLATION_LOCKED_Y), - ) - } - /// Enable / disable translation along ``x`` and ``y``. - #[setter] - fn set_enabled_translations(&mut self, v: (bool, bool)) { - self.with_mut(|b| b.set_enabled_translations(v.0, v.1, true)); - } - - /// Accumulate a (scalar) torque to be applied during the next - /// step. Cleared after the step. - /// - /// :param torque: torque magnitude. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (torque, wake_up=true))] - fn add_torque(&mut self, torque: Real, wake_up: bool) { - self.with_mut(|b| b.add_torque(torque, wake_up)); - } - - /// Apply an instantaneous (scalar) angular impulse. - /// - /// :param torque_impulse: angular impulse magnitude. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (torque_impulse, wake_up=true))] - fn apply_torque_impulse(&mut self, torque_impulse: Real, wake_up: bool) { - self.with_mut(|b| b.apply_torque_impulse(torque_impulse, wake_up)); - } - - /// Start building a dynamic rigid body. - /// - /// Accepts optional kwargs configuring builder fields - /// (``translation``, ``rotation``, ``linvel``, ``angvel``, - /// ``locked_axes``, ``ccd_enabled``, ``gravity_scale``, ...). - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn dynamic( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::dynamic(), kwargs) - } - /// Start building a fixed (immovable) rigid body. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn fixed( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs(rapier::dynamics::RigidBodyBuilder::fixed(), kwargs) - } - /// Start building a velocity-based kinematic rigid body. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn kinematic_velocity_based( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::kinematic_velocity_based(), - kwargs, - ) - } - /// Start building a position-based kinematic rigid body. - #[staticmethod] - #[pyo3(signature = (**kwargs))] - fn kinematic_position_based( - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::kinematic_position_based(), - kwargs, - ) - } - - /// Start building a body of the given ``body_type``. - #[staticmethod] - #[pyo3(signature = (body_type, **kwargs))] - fn new_body( - body_type: RigidBodyType, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RigidBodyBuilder::from_kwargs( - rapier::dynamics::RigidBodyBuilder::new(body_type.to_rapier()), - kwargs, - ) - } - } - - impl RigidBodyBuilder { - fn apply_angvel_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let a: Real = v.extract()?; - *builder = builder.clone().angvel(a); - Ok(()) - } - - fn apply_rotation_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - // 2D rotation kwarg is a scalar angle. - let a: Real = v.extract()?; - *builder = builder.clone().rotation(a); - Ok(()) - } - - fn apply_enabled_translations_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let t: (bool, bool) = v.extract()?; - *builder = builder.clone().enabled_translations(t.0, t.1); - Ok(()) - } - - fn apply_enabled_rotations_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - let t: (bool,) = v.extract()?; - if t.0 { - *builder = builder.clone().locked_axes( - builder.clone().build().locked_axes() - & !rapier::dynamics::LockedAxes::ROTATION_LOCKED, - ); - } else { - *builder = builder.clone().locked_axes( - builder.clone().build().locked_axes() - | rapier::dynamics::LockedAxes::ROTATION_LOCKED, - ); - } - Ok(()) - } - - #[allow(unused_variables)] - fn apply_gyroscopic_forces_kwarg( - builder: &mut rapier::dynamics::RigidBodyBuilder, - v: &Bound<'_, PyAny>, - ) -> PyResult<()> { - // No gyroscopic forces in 2D — silently ignore. - Ok(()) - } - } - }; -} - -// ---------------------------------------------------------------------- -// RigidBody (dim-agnostic methods) + RigidBodyBuilder -// ---------------------------------------------------------------------- -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_rigid_body_common { - ($dim:literal, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // ============================================================ - // RigidBody — owned standalone (NOT a view, holds a full body). - // ============================================================ - // When inserted into a RigidBodySet, the set takes ownership and - // we return handles. When obtained via `set.get(handle).clone()` - // we hand back a fresh RigidBody that mirrors the stored one. - - /// A simulated rigid body. - /// - /// Holds pose, velocity, accumulated forces, mass properties and - /// solver flags. To create one, use a factory on this class to obtain - /// a ``RigidBodyBuilder``, configure it and ``build()`` (or insert - /// the builder directly into a ``RigidBodySet``):: - /// - /// rb = RigidBody.dynamic(translation=Vec3(0, 5, 0)).build() - /// handle = bodies.insert(rb) - /// - /// A body obtained from a ``RigidBodySet`` (e.g. ``world.rigid_bodies[h]``) - /// is a lightweight **view** into the stored body: reads and writes go - /// straight through to the set with no copy, so ``body.linvel = v`` - /// persists immediately. A body created via ``build()`` (not yet in a - /// set) owns its data until inserted. - #[pyclass(name = "RigidBody", module = "rapier")] - #[derive(Debug)] - pub struct RigidBody { - pub backing: RigidBodyBacking, - } - - /// Storage backing a `RigidBody`: either a standalone owned body - /// (pre-insertion) or a handle-backed view into a `RigidBodySet`. - #[derive(Debug)] - pub enum RigidBodyBacking { - Owned(Box), - InSet { - set: Py, - handle: rapier::dynamics::RigidBodyHandle, - }, - } - - // `Py` is not `derive`-Clone (cloning bumps the refcount under the - // GIL), so implement Clone by hand. - impl Clone for RigidBodyBacking { - fn clone(&self) -> Self { - match self { - RigidBodyBacking::Owned(b) => RigidBodyBacking::Owned(b.clone()), - RigidBodyBacking::InSet { set, handle } => { - Python::with_gil(|py| RigidBodyBacking::InSet { - set: set.clone_ref(py), - handle: *handle, - }) - } - } - } - } - impl Clone for RigidBody { - fn clone(&self) -> Self { - RigidBody { - backing: self.backing.clone(), - } - } - } - - impl RigidBody { - fn new_owned(body: rapier::dynamics::RigidBody) -> Self { - RigidBody { - backing: RigidBodyBacking::Owned(Box::new(body)), - } - } - - /// Run `f` with a shared reference to the underlying body. For an - /// `InSet` view this briefly borrows the set; a stale handle (body - /// already removed) panics, surfacing as a Python exception. - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::RigidBody) -> R) -> R { - match &self.backing { - RigidBodyBacking::Owned(b) => f(b), - RigidBodyBacking::InSet { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); - let body = set - .0 - .get(*handle) - .expect("RigidBody refers to a body that was removed from its set"); - f(body) - }), - } - } - - /// Run `f` with a mutable reference to the underlying body, writing - /// straight through to the set for an `InSet` view. - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::RigidBody) -> R) -> R { - match &mut self.backing { - RigidBodyBacking::Owned(b) => f(b), - RigidBodyBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); - let body = set - .0 - .get_mut(*handle) - .expect("RigidBody refers to a body that was removed from its set"); - f(body) - }), - } - } - - /// Clone the underlying body out (used by `insert` and by callers - /// that need an owned `&rapier::RigidBody`). - pub fn to_owned_body(&self) -> rapier::dynamics::RigidBody { - self.with_ref(|b| b.clone()) - } - } - - #[pymethods] - impl RigidBody { - // No `__new__`: construction goes through builders. - - /// World-space pose of the body (read+write). - #[getter] - fn position(&self) -> $Iso { - self.with_ref(|b| { - let r = b.position(); - let na_iso: $crate::na::Isometry = (*r).into(); - $Iso(na_iso) - }) - } - /// Teleport the body to ``p`` (wakes it). Use sparingly on - /// dynamic bodies as this bypasses the integrator. - #[setter] - fn set_position(&mut self, p: PyIsometry) { - let g: rapier::math::Pose = p.0.into(); - self.with_mut(|b| b.set_position(g, true)); - } - - /// Translation portion of the pose (read+write). - #[getter] - fn translation(&self) -> $Vec { - self.with_ref(|b| { - let v: $crate::na::SVector = b.translation().into(); - $Vec(v) - }) - } - /// Set the translation, leaving the rotation unchanged. - #[setter] - fn set_translation(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_translation(g, true)); - } - - /// Rotation portion of the pose (read+write). - #[getter] - fn rotation(&self) -> $Rot { - self.with_ref(|b| { - let r = *b.rotation(); - $Rot(r.into()) - }) - } - /// Set the rotation, leaving the translation unchanged. - #[setter] - fn set_rotation(&mut self, r: PyRotation) { - let g: rapier::math::Rotation = r.0.into(); - self.with_mut(|b| b.set_rotation(g, true)); - } - - /// Linear velocity in world space (read+write). - #[getter] - fn linvel(&self) -> $Vec { - self.with_ref(|b| { - let v: $crate::na::SVector = b.linvel().into(); - $Vec(v) - }) - } - /// Set the linear velocity (wakes the body). - #[setter] - fn set_linvel(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_linvel(g, true)); - } - - /// Total mass of the body (colliders + additional) (read-only). - #[getter] - fn mass(&self) -> Real { - self.with_ref(|b| b.mass()) - } - /// Inverse mass — ``0`` for fixed or infinite-mass bodies - /// (read-only). - #[getter] - fn inv_mass(&self) -> Real { - self.with_ref(|b| b.mass_properties().local_mprops.inv_mass) - } - - /// Center of mass in world space (read-only). - #[getter] - fn center_of_mass(&self) -> $Point { - let v: $crate::na::SVector = - self.with_ref(|b| b.center_of_mass()).into(); - $Point($crate::na::Point::from(v)) - } - - /// Center of mass in body-local space (read-only). - #[getter] - fn local_center_of_mass(&self) -> $Point { - let v: $crate::na::SVector = - self.with_ref(|b| b.local_center_of_mass()).into(); - $Point($crate::na::Point::from(v)) - } - - /// Body-local mass properties (read-only). - /// - /// To change them, use ``set_additional_mass``, - /// ``set_additional_mass_properties`` or - /// ``recompute_mass_properties_from_colliders``. - #[getter] - fn mass_properties(&self) -> MassProperties { - self.with_ref(|b| MassProperties(b.mass_properties().local_mprops)) - } - - /// Behavior class of the body (dynamic/fixed/kinematic) - /// (read+write). - #[getter] - fn body_type(&self) -> RigidBodyType { - RigidBodyType::from_rapier(self.with_ref(|b| b.body_type())) - } - /// Change the body's behavior class. - #[setter] - fn set_body_type(&mut self, t: RigidBodyType) { - let rt = t.to_rapier(); - self.with_mut(|b| b.set_body_type(rt, true)); - } - - /// Multiplier applied to the world gravity for this body - /// (read+write). ``0`` disables gravity for the body. - #[getter] - fn gravity_scale(&self) -> Real { - self.with_ref(|b| b.gravity_scale()) - } - /// Set the gravity scale. - #[setter] - fn set_gravity_scale(&mut self, v: Real) { - self.with_mut(|b| b.set_gravity_scale(v, true)); - } - - /// Per-second linear-velocity damping coefficient (read+write). - #[getter] - fn linear_damping(&self) -> Real { - self.with_ref(|b| b.linear_damping()) - } - /// Set the linear damping coefficient. - #[setter] - fn set_linear_damping(&mut self, v: Real) { - self.with_mut(|b| b.set_linear_damping(v)); - } - - /// Per-second angular-velocity damping coefficient (read+write). - #[getter] - fn angular_damping(&self) -> Real { - self.with_ref(|b| b.angular_damping()) - } - /// Set the angular damping coefficient. - #[setter] - fn set_angular_damping(&mut self, v: Real) { - self.with_mut(|b| b.set_angular_damping(v)); - } - - /// Dominance group of the body (read+write); see - /// ``RigidBodyDominance``. - #[getter] - fn dominance_group(&self) -> i8 { - self.with_ref(|b| b.dominance_group()) - } - /// Set the dominance group (``[-127, 127]``). - #[setter] - fn set_dominance_group(&mut self, v: i8) { - self.with_mut(|b| b.set_dominance_group(v)); - } - - /// Extra solver iterations spent on this body's island - /// (read+write). ``0`` keeps the global default. - #[getter] - fn additional_solver_iterations(&self) -> usize { - self.with_ref(|b| b.additional_solver_iterations()) - } - /// Set the number of additional solver iterations. - #[setter] - fn set_additional_solver_iterations(&mut self, v: usize) { - self.with_mut(|b| b.set_additional_solver_iterations(v)); - } - - /// Currently locked degrees of freedom (read+write). - #[getter] - fn locked_axes(&self) -> LockedAxes { - LockedAxes(self.with_ref(|b| b.locked_axes())) - } - /// Replace the locked axes flag set (wakes the body). - #[setter] - fn set_locked_axes(&mut self, v: LockedAxes) { - self.with_mut(|b| b.set_locked_axes(v.0, true)); - } - - /// Whether the body is currently sleeping (read-only). - #[getter] - fn is_sleeping(&self) -> bool { - self.with_ref(|b| b.is_sleeping()) - } - /// Whether the body is currently moving (read-only). - #[getter] - fn is_moving(&self) -> bool { - self.with_ref(|b| b.is_moving()) - } - /// Whether the body is dynamic (read-only). - #[getter] - fn is_dynamic(&self) -> bool { - self.with_ref(|b| b.is_dynamic()) - } - /// Whether the body is kinematic (either variant) (read-only). - #[getter] - fn is_kinematic(&self) -> bool { - self.with_ref(|b| b.is_kinematic()) - } - /// Whether the body is fixed (read-only). - #[getter] - fn is_fixed(&self) -> bool { - self.with_ref(|b| b.is_fixed()) - } - - /// Whether the body participates in simulation (read+write). - /// - /// A disabled body is fully skipped by the solver and queries. - #[getter] - fn is_enabled(&self) -> bool { - self.with_ref(|b| b.is_enabled()) - } - /// Enable or disable the body. - #[setter] - fn set_is_enabled(&mut self, v: bool) { - self.with_mut(|b| b.set_enabled(v)); - } - - /// Whether substep CCD is enabled for this body (read+write). - #[getter] - fn ccd_enabled(&self) -> bool { - self.with_ref(|b| b.is_ccd_enabled()) - } - /// Enable or disable substep CCD for this body. - #[setter] - fn set_ccd_enabled(&mut self, v: bool) { - self.with_mut(|b| b.enable_ccd(v)); - } - - /// Soft-CCD prediction distance (read+write); ``0`` disables it. - #[getter] - fn soft_ccd_prediction(&self) -> Real { - self.with_ref(|b| b.soft_ccd_prediction()) - } - /// Set the soft-CCD prediction distance. - #[setter] - fn set_soft_ccd_prediction(&mut self, v: Real) { - self.with_mut(|b| b.set_soft_ccd_prediction(v)); - } - - /// Application-defined ``u128`` payload attached to this body - /// (read+write). - #[getter] - fn user_data(&self) -> u128 { - self.with_ref(|b| b.user_data) - } - /// Set the application-defined payload. - #[setter] - fn set_user_data(&mut self, v: u128) { - self.with_mut(|b| b.user_data = v); - } - - /// Force accumulated by user calls during the current step - /// (read-only). Cleared automatically before the next step. - #[getter] - fn user_force(&self) -> $Vec { - let v: $crate::na::SVector = self.with_ref(|b| b.user_force()).into(); - $Vec(v) - } - - /// Handles of all colliders attached to this body (read-only). - #[getter] - fn colliders(&self) -> Vec { - self.with_ref(|b| b.colliders().iter().copied().map(ColliderHandle).collect()) - } - - /// Activation / sleep state of this body (read+write). - #[getter] - fn activation(&self) -> RigidBodyActivation { - self.with_ref(|b| RigidBodyActivation(*b.activation())) - } - /// Replace the activation state. - #[setter] - fn set_activation(&mut self, v: RigidBodyActivation) { - self.with_mut(|b| *b.activation_mut() = v.0); - } - - /// Predicted world-space pose after the next step (read-only). - #[getter] - fn next_position(&self) -> $Iso { - self.with_ref(|b| { - let p: $crate::na::Isometry = (*b.next_position()).into(); - $Iso(p) - }) - } - - // ---- mutating methods ---- - - /// Accumulate a force to be applied during the next step. - /// - /// Forces accumulate over the step and are cleared automatically - /// before the next ``step()``. Use ``apply_impulse`` for an - /// instantaneous velocity change. - /// - /// :param force: force vector in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (force, wake_up=true))] - fn add_force(&mut self, force: PyVector, wake_up: bool) { - let g: rapier::math::Vector = force.0.into(); - self.with_mut(|b| b.add_force(g, wake_up)); - } - - /// Accumulate a force applied at a given world-space point. - /// - /// Equivalent to applying ``force`` at the body's center of mass - /// plus a torque ``(point - com) × force``. - /// - /// :param force: force vector in world space. - /// :param point: application point in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (force, point, wake_up=true))] - fn add_force_at_point(&mut self, force: PyVector, point: PyPoint, wake_up: bool) { - let f: rapier::math::Vector = force.0.into(); - let p: rapier::math::Vector = point.0.coords.into(); - self.with_mut(|b| b.add_force_at_point(f, p, wake_up)); - } - - /// Apply an instantaneous linear impulse (units: ``N·s``). - /// - /// :param impulse: impulse vector in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (impulse, wake_up=true))] - fn apply_impulse(&mut self, impulse: PyVector, wake_up: bool) { - let g: rapier::math::Vector = impulse.0.into(); - self.with_mut(|b| b.apply_impulse(g, wake_up)); - } - - /// Apply an instantaneous impulse at a given world-space point. - /// - /// :param impulse: impulse vector in world space. - /// :param point: application point in world space. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (impulse, point, wake_up=true))] - fn apply_impulse_at_point(&mut self, impulse: PyVector, point: PyPoint, wake_up: bool) { - let imp: rapier::math::Vector = impulse.0.into(); - let p: rapier::math::Vector = point.0.coords.into(); - self.with_mut(|b| b.apply_impulse_at_point(imp, p, wake_up)); - } - - /// Clear any accumulated forces on this body. - /// - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (wake_up=true))] - fn reset_forces(&mut self, wake_up: bool) { - self.with_mut(|b| b.reset_forces(wake_up)); - } - /// Clear any accumulated torques on this body. - /// - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (wake_up=true))] - fn reset_torques(&mut self, wake_up: bool) { - self.with_mut(|b| b.reset_torques(wake_up)); - } - - /// Wake the body, allowing it to participate in the next step. - /// - /// :param strong: if ``True`` (default), reset the sleep timer - /// so the body stays awake longer before re-sleeping. - #[pyo3(signature = (strong=true))] - fn wake_up(&mut self, strong: bool) { - self.with_mut(|b| b.wake_up(strong)); - } - /// Put the body to sleep immediately. - fn sleep(&mut self) { - self.with_mut(|b| b.sleep()); - } - - /// World-space velocity at the given world-space point. - /// - /// Includes both translational and rotational contributions: - /// ``v_point = linvel + angvel × (point - com)``. - /// - /// :param point: world-space point. - fn velocity_at_point(&self, point: PyPoint) -> $Vec { - let p: rapier::math::Vector = point.0.coords.into(); - let v: $crate::na::SVector = - self.with_ref(|b| b.velocity_at_point(p)).into(); - $Vec(v) - } - - /// Kinetic energy of the body. - /// - /// :returns: :math:`E_k = \\tfrac{1}{2} m \\|v\\|^2 + - /// \\tfrac{1}{2} \\omega^\\top I \\omega`. - fn kinetic_energy(&self) -> Real { - self.with_ref(|b| b.kinetic_energy()) - } - - /// Gravitational potential energy after integrating for ``dt`` - /// seconds under ``gravity``. - /// - /// :returns: :math:`-m\\, \\mathbf{g} \\cdot \\mathbf{p}` where - /// :math:`\\mathbf{p}` is the predicted position. - /// :param dt: integration step in seconds. - /// :param gravity: gravity vector in world space. - fn gravitational_potential_energy(&self, dt: Real, gravity: PyVector) -> Real { - let g: rapier::math::Vector = gravity.0.into(); - self.with_ref(|b| b.gravitational_potential_energy(dt, g)) - } - - /// Predict the body's pose after ``dt`` seconds using its - /// *current velocity only* (no forces, no integration). The body - /// is not modified. - /// - /// :param dt: prediction horizon in seconds. - fn predict_position_using_velocity(&self, dt: Real) -> $Iso { - let p: $crate::na::Isometry = self - .with_ref(|b| b.predict_position_using_velocity(dt)) - .into(); - $Iso(p) - } - - /// Predict the body's pose after ``dt`` seconds using its - /// current velocity *and* accumulated forces, without actually - /// integrating (the body is not modified). - /// - /// :param dt: prediction horizon in seconds. - fn predict_position_using_velocity_and_forces(&self, dt: Real) -> $Iso { - self.with_ref(|b| { - let p: $crate::na::Isometry = - b.predict_position_using_velocity_and_forces(dt).into(); - $Iso(p) - }) - } - - /// Overwrite this body's mass properties from the attached - /// colliders. - /// - /// Equivalent to summing the mass properties of every collider - /// in ``colliders`` that is attached to this body. Useful after - /// adding, removing or rescaling colliders. - /// - /// :param colliders: the ``ColliderSet`` holding this body's - /// colliders. - fn recompute_mass_properties_from_colliders(&mut self, colliders: &ColliderSet) { - self.with_mut(|b| b.recompute_mass_properties_from_colliders(&colliders.0)); - } - - /// Add ``mass`` on top of the colliders' contribution. - /// - /// :param mass: extra mass to add. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (mass, wake_up=true))] - fn set_additional_mass(&mut self, mass: Real, wake_up: bool) { - self.with_mut(|b| b.set_additional_mass(mass, wake_up)); - } - - /// Add full mass properties on top of the colliders' - /// contribution. - /// - /// :param mp: extra mass properties. - /// :param wake_up: if ``True`` (default), wake the body up. - #[pyo3(signature = (mp, wake_up=true))] - fn set_additional_mass_properties(&mut self, mp: &MassProperties, wake_up: bool) { - self.with_mut(|b| b.set_additional_mass_properties(mp.0, wake_up)); - } - - /// Convenience for accumulating ``gravity * mass`` as a force - /// during the current step (useful when stepping with gravity - /// disabled globally). - /// - /// :param gravity: gravity acceleration vector. - fn add_gravitational_force(&mut self, gravity: PyVector) { - // Add gravity * mass as a force. - let g: rapier::math::Vector = gravity.0.into(); - let m = self.with_ref(|b| b.mass()); - self.with_mut(|b| b.add_force(g * m, true)); - } - - fn __repr__(&self) -> String { - let p = self.with_ref(|b| b.translation()); - format!( - "RigidBody(translation={:?}, type={:?})", - p, - self.with_ref(|b| b.body_type()) - ) - } - } - - // ============================================================ - // RigidBodyBuilder - // ============================================================ - - /// Fluent builder for ``RigidBody`` instances. - /// - /// Each setter returns ``self`` for chaining; call ``build()`` to - /// produce the actual body, or pass the builder directly to - /// ``RigidBodySet.insert``:: - /// - /// builder = (RigidBody.dynamic() - /// .translation(Vec3(0, 5, 0)) - /// .linvel(Vec3(1, 0, 0))) - /// handle = bodies.insert(builder) - /// - /// Most factories also accept keyword arguments matching builder - /// methods, e.g. ``RigidBody.dynamic(translation=Vec3(0, 5, 0))``. - #[pyclass(name = "RigidBodyBuilder", module = "rapier")] - #[derive(Debug, Clone)] - pub struct RigidBodyBuilder { - pub builder: rapier::dynamics::RigidBodyBuilder, - } - - impl RigidBodyBuilder { - pub fn from_kwargs( - base: rapier::dynamics::RigidBodyBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self { builder: base }; - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - me.apply_kwarg(&key, &v)?; - } - } - Ok(me) - } - - fn apply_kwarg(&mut self, key: &str, v: &Bound<'_, PyAny>) -> PyResult<()> { - match key { - "translation" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - self.builder = self.builder.clone().translation(g); - } - "linvel" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - self.builder = self.builder.clone().linvel(g); - } - "linear_damping" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().linear_damping(f); - } - "angular_damping" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().angular_damping(f); - } - "gravity_scale" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().gravity_scale(f); - } - "can_sleep" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().can_sleep(b); - } - "sleeping" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().sleeping(b); - } - "ccd_enabled" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().ccd_enabled(b); - } - "soft_ccd_prediction" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().soft_ccd_prediction(f); - } - "dominance_group" => { - let g: i8 = v.extract()?; - self.builder = self.builder.clone().dominance_group(g); - } - "additional_solver_iterations" => { - let n: usize = v.extract()?; - self.builder = self.builder.clone().additional_solver_iterations(n); - } - "user_data" => { - let d: u128 = v.extract()?; - self.builder = self.builder.clone().user_data(d); - } - "enabled" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().enabled(b); - } - "additional_mass" => { - let m: Real = v.extract()?; - self.builder = self.builder.clone().additional_mass(m); - } - "locked_axes" => { - let la: LockedAxes = v.extract()?; - self.builder = self.builder.clone().locked_axes(la.0); - } - "angvel" => { - Self::apply_angvel_kwarg(&mut self.builder, v)?; - } - "rotation" => { - Self::apply_rotation_kwarg(&mut self.builder, v)?; - } - "position" => { - let pi: PyIsometry = v.extract()?; - let g: rapier::math::Pose = pi.0.into(); - self.builder = self.builder.clone().pose(g); - } - "enabled_translations" => { - Self::apply_enabled_translations_kwarg(&mut self.builder, v)?; - } - "enabled_rotations" => { - Self::apply_enabled_rotations_kwarg(&mut self.builder, v)?; - } - "gyroscopic_forces" => { - Self::apply_gyroscopic_forces_kwarg(&mut self.builder, v)?; - } - "body_type" => { - let bt: RigidBodyType = v.extract()?; - // Re-create builder of that type, preserving fields. - // Simplest: clone and `.set_body_type` doesn't exist on - // the builder. We swap base type while keeping the rest. - // RigidBodyBuilder has no `.body_type` setter; build & - // re-create. For simplicity, just panic if - // people mix base ctor with body_type kwarg. - let _ = bt; - return Err(PyTypeError::new_err( - "Use RigidBody.new_body(body_type=...) to set body_type — \ - can't combine with the existing builder factory.", - )); - } - _ => { - return Err(PyTypeError::new_err(format!( - "unknown RigidBodyBuilder kwarg: '{}'", - key - ))); - } - } - Ok(()) - } - } - - #[pymethods] - impl RigidBodyBuilder { - /// Build a fresh ``RigidBodyBuilder`` for the given body type. - /// - /// :param body_type: a ``RigidBodyType`` selecting the behavior - /// class (default ``DYNAMIC``). - #[new] - #[pyo3(signature = (body_type=RigidBodyType::DYNAMIC))] - fn new(body_type: RigidBodyType) -> Self { - Self { - builder: rapier::dynamics::RigidBodyBuilder::new(body_type.to_rapier()), - } - } - - /// Set the initial translation. - fn translation(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self { - builder: self.builder.clone().translation(g), - } - } - - /// Set the initial linear velocity. - fn linvel(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self { - builder: self.builder.clone().linvel(g), - } - } - - /// Set the linear-velocity damping coefficient (per second). - fn linear_damping(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().linear_damping(f), - } - } - /// Set the angular-velocity damping coefficient (per second). - fn angular_damping(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().angular_damping(f), - } - } - /// Set the gravity multiplier for this body (``0`` disables - /// gravity). - fn gravity_scale(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().gravity_scale(f), - } - } - /// Whether the body is allowed to fall asleep (default ``True``). - fn can_sleep(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().can_sleep(b), - } - } - /// Whether the body starts asleep. - fn sleeping(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().sleeping(b), - } - } - /// Enable substep CCD for this body. - fn ccd_enabled(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().ccd_enabled(b), - } - } - /// Set the soft-CCD prediction distance (``0`` disables it). - fn soft_ccd_prediction(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().soft_ccd_prediction(f), - } - } - /// Set the dominance group for contact biasing. - fn dominance_group(&self, g: i8) -> Self { - Self { - builder: self.builder.clone().dominance_group(g), - } - } - /// Request additional solver iterations on this body's island. - fn additional_solver_iterations(&self, n: usize) -> Self { - Self { - builder: self.builder.clone().additional_solver_iterations(n), - } - } - /// Attach an application-defined ``u128`` payload to the body. - fn user_data(&self, d: u128) -> Self { - Self { - builder: self.builder.clone().user_data(d), - } - } - /// Whether the body is enabled at creation (default ``True``). - fn enabled(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().enabled(b), - } - } - /// Add extra mass on top of the colliders' contribution. - fn additional_mass(&self, m: Real) -> Self { - Self { - builder: self.builder.clone().additional_mass(m), - } - } - /// Add extra mass properties on top of the colliders' - /// contribution. - fn additional_mass_properties(&self, mp: &MassProperties) -> Self { - Self { - builder: self.builder.clone().additional_mass_properties(mp.0), - } - } - /// Set which degrees of freedom are locked. - fn locked_axes(&self, la: &LockedAxes) -> Self { - Self { - builder: self.builder.clone().locked_axes(la.0), - } - } - /// Set the initial pose (translation and rotation). - fn position(&self, p: PyIsometry) -> Self { - let g: rapier::math::Pose = p.0.into(); - Self { - builder: self.builder.clone().pose(g), - } - } - - /// Set the initial rotation. - /// - /// In 3D, ``v`` is a scaled-axis-angle vector; in 2D, a scalar - /// angle in radians. - fn rotation(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - Self::apply_rotation_kwarg(&mut b, v)?; - Ok(Self { builder: b }) - } - - /// Set the initial angular velocity. - /// - /// In 3D, ``v`` is a vector; in 2D, a scalar. - fn angvel(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - Self::apply_angvel_kwarg(&mut b, v)?; - Ok(Self { builder: b }) - } - - /// Enable or disable individual translation axes. - /// - /// In 3D, ``v`` is a tuple ``(tx, ty, tz)``; in 2D, ``(tx, ty)``. - fn enabled_translations(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - Self::apply_enabled_translations_kwarg(&mut b, v)?; - Ok(Self { builder: b }) - } - - /// Alias for ``enabled_translations``. - fn restrict_translations(&self, v: &Bound<'_, PyAny>) -> PyResult { - self.enabled_translations(v) - } - - /// Enable or disable individual rotation axes. - /// - /// In 3D, ``v`` is a tuple ``(rx, ry, rz)``; in 2D, ``(rz,)``. - fn enabled_rotations(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - Self::apply_enabled_rotations_kwarg(&mut b, v)?; - Ok(Self { builder: b }) - } - - /// Alias for ``enabled_rotations``. - fn restrict_rotations(&self, v: &Bound<'_, PyAny>) -> PyResult { - self.enabled_rotations(v) - } - - /// Enable or disable gyroscopic-force integration (3D only). - /// - /// In 2D this builder ignores the kwarg. - fn gyroscopic_forces(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - Self::apply_gyroscopic_forces_kwarg(&mut b, v)?; - Ok(Self { builder: b }) - } - - /// Finalize the builder and return the configured ``RigidBody``. - fn build(&self) -> RigidBody { - RigidBody::new_owned(self.builder.build()) - } - } - }; -} - -// ---------------------------------------------------------------------- -// RigidBodySet (shared between 2D and 3D) -// ---------------------------------------------------------------------- -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_rigid_body_set { - () => { - /// Container holding every ``RigidBody`` in a physics world. - /// - /// Bodies are addressed by ``RigidBodyHandle`` and live in the set - /// until ``remove``-d. The set supports ``len()``, ``in``, iteration - /// (yielding ``(handle, body)`` pairs) and ``[handle]`` lookup. - /// - /// Bodies returned by ``get`` / ``__getitem__`` / iteration are live - /// **views** into the set: ``set[h].linvel = v`` mutates the stored - /// body in place, with no copy. - #[pyclass(name = "RigidBodySet", module = "rapier", unsendable)] - pub struct RigidBodySet(pub rapier::dynamics::RigidBodySet); - - #[pymethods] - impl RigidBodySet { - /// Build an empty rigid-body set. - #[new] - fn new() -> Self { Self(rapier::dynamics::RigidBodySet::new()) } - - /// Insert a rigid body and return its handle. - /// - /// Accepts either a ``RigidBody`` or a ``RigidBodyBuilder``; - /// builders are built before insertion. - /// - /// :raises TypeError: if ``body`` is neither a ``RigidBody`` nor - /// a ``RigidBodyBuilder``. - fn insert(&mut self, body: &Bound<'_, PyAny>) -> PyResult { - // Accept either a RigidBodyBuilder or a RigidBody. - if let Ok(b) = body.extract::>() { - let rb = b.builder.build(); - return Ok(RigidBodyHandle(self.0.insert(rb))); - } - if let Ok(rb) = body.extract::>() { - let cloned = rb.to_owned_body(); - return Ok(RigidBodyHandle(self.0.insert(cloned))); - } - Err(PyTypeError::new_err( - "RigidBodySet.insert expects a RigidBody or RigidBodyBuilder", - )) - } - - /// Remove the rigid body identified by ``handle``. - /// - /// Joints anchored to the body are removed from - /// ``impulse_joints`` and ``multibody_joints``. If - /// ``remove_attached_colliders`` is ``True`` (default), every - /// collider attached to the body is also removed from - /// ``colliders``; otherwise those colliders are simply detached. - /// - /// :returns: the removed body, or ``None`` if no body matched - /// ``handle``. - #[pyo3(signature = (handle, islands, colliders, impulse_joints, multibody_joints, remove_attached_colliders=true))] - fn remove( - &mut self, - handle: &RigidBodyHandle, - islands: &mut IslandManager, - colliders: &mut ColliderSet, - impulse_joints: &mut ImpulseJointSet, - multibody_joints: &mut MultibodyJointSet, - remove_attached_colliders: bool, - ) -> Option { - self.0 - .remove( - handle.0, - &mut islands.0, - &mut colliders.0, - &mut impulse_joints.0, - &mut multibody_joints.0, - remove_attached_colliders, - ) - .map(RigidBody::new_owned) - } - - /// Return a live **view** of the body identified by ``handle``, or - /// ``None`` if there is none. - /// - /// The returned body reads and writes straight through to the set: - /// ``set.get(h).linvel = v`` persists immediately, with no copy. - fn get(slf: &Bound<'_, Self>, handle: &RigidBodyHandle) -> Option { - slf.borrow().0.get(handle.0)?; - Some(RigidBody { - backing: RigidBodyBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - /// Indexing form of ``get`` — returns a live view into the set. - /// - /// :raises InvalidHandle: if ``handle`` does not match any body. - fn __getitem__( - slf: &Bound<'_, Self>, - handle: &RigidBodyHandle, - ) -> PyResult { - if slf.borrow().0.get(handle.0).is_none() { - return Err($crate::errors::InvalidHandle::new_err(format!( - "no rigid body for {:?}", - handle.0.into_raw_parts() - ))); - } - Ok(RigidBody { - backing: RigidBodyBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - /// ``handle in self`` — whether ``handle`` refers to a body - /// stored in this set. - fn __contains__(&self, handle: &RigidBodyHandle) -> bool { - self.0.contains(handle.0) - } - - /// Number of bodies in the set. - fn __len__(&self) -> usize { self.0.len() } - - /// Remove every body from the set. - fn clear(&mut self) { - // RigidBodySet doesn't expose `clear()`, so we re-init. - self.0 = rapier::dynamics::RigidBodySet::new(); - } - - /// Iterate over ``(handle, body)`` pairs. - /// - /// Each yielded body is a live view into the set (no copy). - fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { - let handles: Vec = - slf.borrow().0.iter().map(|(h, _)| h).collect(); - Py::new( - slf.py(), - RigidBodySetIter { set: slf.clone().unbind(), handles, i: 0 }, - ) - } - - /// Iterate over the handles of every body in the set. - fn handles(slf: PyRef<'_, Self>) -> PyResult> { - let handles: Vec = - slf.0.iter().map(|(h, _)| RigidBodyHandle(h)).collect(); - Py::new(slf.py(), RigidBodyHandleIter { handles, i: 0 }) - } - - } - - /// Iterator yielding ``(RigidBodyHandle, RigidBody)`` pairs from a - /// ``RigidBodySet``. - /// - /// Each ``RigidBody`` is a live view into the set. - #[pyclass] - pub struct RigidBodySetIter { - set: Py, - handles: Vec, - i: usize, - } - - #[pymethods] - impl RigidBodySetIter { - /// Return ``self`` so the object satisfies the iterator protocol. - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Return the next ``(handle, body)`` pair, or raise - /// ``StopIteration``. The body is built lazily as a live view. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(RigidBodyHandle, RigidBody)> { - if slf.i >= slf.handles.len() { return None; } - let py = slf.py(); - let handle = slf.handles[slf.i]; - slf.i += 1; - let set = slf.set.clone_ref(py); - Some(( - RigidBodyHandle(handle), - RigidBody { backing: RigidBodyBacking::InSet { set, handle } }, - )) - } - } - }; -} - -// ---------------------------------------------------------------------- -// register_dynamics — per-dim entry point. -// ---------------------------------------------------------------------- -#[doc(hidden)] -#[macro_export] -macro_rules! __define_dynamics_register { - (3) => { - pub fn register_dynamics( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - // ImpulseJointSet / MultibodyJointSet registered by joints.rs. - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; - (2) => { - pub fn register_dynamics( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - // ImpulseJointSet / MultibodyJointSet registered by joints.rs. - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/errors.rs b/python/rapier-py-core/src/errors.rs deleted file mode 100644 index 2aaa62a0b..000000000 --- a/python/rapier-py-core/src/errors.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! The `RapierError` exception tree shared by every cdylib. -//! -//! All concrete error types inherit from `RapierError` (which itself inherits -//! from Python's `Exception`), so users can write -//! `except rapier.RapierError as e:` to catch any rapier-originated failure. - -use pyo3::create_exception; -use pyo3::exceptions::PyException; -use pyo3::prelude::*; - -create_exception!( - rapier, - RapierError, - PyException, - "Base exception for all rapier-originated errors." -); -create_exception!( - rapier, - InvalidHandle, - RapierError, - "Raised when a handle (rigid body, collider, joint, ...) no longer refers to a live object." -); -create_exception!( - rapier, - MeshConversionError, - RapierError, - "Raised when converting between mesh representations fails (degenerate, non-manifold, ...)." -); -create_exception!( - rapier, - UrdfError, - RapierError, - "Raised by the URDF loader on parse or structural errors." -); -create_exception!( - rapier, - MjcfError, - RapierError, - "Raised by the MJCF loader on parse or structural errors." -); -create_exception!( - rapier, - SerializationError, - RapierError, - "Raised when (de)serializing a physics scene fails." -); -create_exception!( - rapier, - QueryFailure, - RapierError, - "Raised when a spatial query (raycast, point projection, ...) cannot complete." -); -create_exception!( - rapier, - MeshLoaderError, - RapierError, - "Raised by the mesh loader (e.g. STL/OBJ ingestion) on parse or IO errors." -); - -/// Register every exception class defined in this crate onto the given pymodule. -/// -/// Call this exactly once from each cdylib's `#[pymodule]` function. -pub fn register_errors(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add("RapierError", py.get_type_bound::())?; - m.add("InvalidHandle", py.get_type_bound::())?; - m.add( - "MeshConversionError", - py.get_type_bound::(), - )?; - m.add("UrdfError", py.get_type_bound::())?; - m.add("MjcfError", py.get_type_bound::())?; - m.add( - "SerializationError", - py.get_type_bound::(), - )?; - m.add("QueryFailure", py.get_type_bound::())?; - m.add("MeshLoaderError", py.get_type_bound::())?; - Ok(()) -} - -/// Convenience macro that each cdylib calls from its `#[pymodule]`: -/// ```ignore -/// #[pymodule] -/// fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { -/// rapier_py_core::register_errors!(py, m); -/// Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! register_errors { - ($py:expr, $m:expr) => { - $crate::errors::register_errors($py, $m)? - }; -} diff --git a/python/rapier-py-core/src/events_hooks.rs b/python/rapier-py-core/src/events_hooks.rs deleted file mode 100644 index ae111a278..000000000 --- a/python/rapier-py-core/src/events_hooks.rs +++ /dev/null @@ -1,995 +0,0 @@ -//! Macro emitting the user-facing event-handler / physics-hooks `#[pyclass]`-es -//! per cdylib. -//! -//! Events & Hooks. -//! -//! Like the other modules, this macro is invoked once per cdylib AFTER -//! `define_conv_types!`, `define_math_types!`, `define_geometry_types!`, -//! `define_joints_types!`, `define_dynamics_types!`, and -//! `define_pipeline_types!`. It depends on: -//! - The `Real`/`DIM` aliases and `Py{Vector,Point,...}` adapters. -//! - The user-facing math types (`Vec*`, `Point*`, ...). -//! - The geometry types — in particular `CollisionEvent`, `ContactForceEvent`, -//! `ContactPair`, `ContactManifold`, `SolverContact`, `ColliderHandle`. -//! - The dynamics types — `RigidBodyHandle`. -//! -//! `define_events_hooks_types!` produces `register_events_hooks(py, m)` for the -//! `#[pymodule]` entry point. -//! -//! # Threading model -//! -//! `PhysicsPipeline.step` releases the GIL via `Python::allow_threads`. The -//! rapier solver may then invoke `EventHandler` / `PhysicsHooks` trait methods -//! from a worker thread. Our adapter types (`PyEventHandler`, `PyPhysicsHooks`, -//! `ChannelEventCollector`) hold `Py` callables; each callback wraps the -//! call in `Python::with_gil(...)` to re-acquire the GIL before touching any -//! Python object. -//! -//! Exceptions raised inside Python callbacks are **deferred**: they're stashed -//! into a shared `Mutex>` and re-raised after `step()` returns to -//! Python (the user can flip `world.event_error_policy = "strict"` to also set -//! an abort flag so the solver bails out early — but rapier doesn't currently -//! support mid-step aborts, so "strict" simply makes the error get re-raised -//! *and* future hook calls during the same step early-return without invoking -//! the user callback). -//! -//! # Safety -//! -//! `PyEventHandler` / `PyPhysicsHooks` declare `unsafe impl Send + Sync`. PyO3's -//! `Py` is `Send` already (it manages the refcount through atomic ops via -//! `clone_ref`), but we still re-acquire the GIL with `Python::with_gil` before -//! every interaction. The wrappers themselves never read or write the inner -//! Python object outside of a GIL section, so the `Send + Sync` upgrade is -//! sound. - -/// Materialize the events/hooks `#[pyclass]` types for a given `(Real, DIM)` pair. -#[macro_export] -macro_rules! define_events_hooks_types { - (DIM = 2) => { - $crate::__define_events_hooks_shared!(2, Vec2); - $crate::__define_events_hooks_register!(); - }; - (DIM = 3) => { - $crate::__define_events_hooks_shared!(3, Vec3); - $crate::__define_events_hooks_register!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_events_hooks_shared { - ($dim:tt, $Vec:ident) => { - use std::sync::{Arc, Mutex}; - - // ===================================================================== - // Deferred-error policy. - // ===================================================================== - - /// How to handle Python exceptions raised inside a callback. - /// - /// - `Defer` (default): catch, stash, re-raise after `step()` returns. - /// - `Strict`: catch, stash, set an abort flag so subsequent hook calls - /// within the same `step()` short-circuit. The solver still runs to - /// completion (rapier doesn't support mid-step aborts), but the - /// error is re-raised after `step()` returns. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum EventErrorPolicy { - Defer, - Strict, - } - - /// Slot shared between every adapter wrapper and the world. Stores the - /// first exception raised by a Python callback during a step, plus the - /// error policy chosen by the user. - #[derive(Default)] - pub struct DeferredError { - pub err: Option, - pub aborted: bool, - pub policy_strict: bool, - } - - // ===================================================================== - // SolverFlags — bitflags wrapper. - // ===================================================================== - - /// Bitflags returned from ``PhysicsHooks.filter_contact_pair`` to - /// control how a contact pair is solved. - /// - /// Combine flags with ``|``, intersect with ``&``, test membership with - /// ``in`` or :meth:`contains`. ``COMPUTE_IMPULSES`` is the default; an - /// empty flag set disables impulse computation for that pair (it will - /// still generate collision events). - #[pyclass(name = "SolverFlags", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct SolverFlags(pub rapier::geometry::SolverFlags); - - #[pymethods] - impl SolverFlags { - /// Construct a ``SolverFlags`` from a raw bit pattern. - /// - /// :param bits: Raw bit pattern. Bits that don't correspond to a - /// known flag are silently truncated. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::geometry::SolverFlags::from_bits_truncate(bits)) - } - /// Return an empty ``SolverFlags`` (no contact solving). - #[staticmethod] - fn empty() -> Self { - Self(rapier::geometry::SolverFlags::empty()) - } - /// Return the default ``SolverFlags`` (``COMPUTE_IMPULSES``). - #[staticmethod] - fn default_() -> Self { - Self(rapier::geometry::SolverFlags::default()) - } - - /// Compute impulses for this contact pair (i.e. solve the contact). - #[classattr] - const COMPUTE_IMPULSES: SolverFlags = - SolverFlags(rapier::geometry::SolverFlags::COMPUTE_IMPULSES); - /// Empty flag set — equivalent to :meth:`empty`. - #[classattr] - const EMPTY: SolverFlags = SolverFlags(rapier::geometry::SolverFlags::empty()); - - /// Raw underlying bit pattern. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// Return ``True`` if every bit set in ``other`` is also set here. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// Return ``True`` if no flag is set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - /// ``other in self`` — alias for :meth:`contains`. - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// Bitwise OR — union of two flag sets. - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - /// Bitwise AND — intersection of two flag sets. - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - /// Bitwise XOR — symmetric difference. - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - /// Bitwise NOT — flip every known flag. - fn __invert__(&self) -> Self { - Self(!self.0) - } - /// Truthy iff at least one flag is set. - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - /// Equality on the raw bit pattern; other comparisons raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("SolverFlags supports only == and !=")), - } - } - /// Hash on the raw bit pattern (makes ``SolverFlags`` set-safe). - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - /// Debug repr — shows the raw bit pattern in binary. - fn __repr__(&self) -> String { - format!("SolverFlags(bits={:#010b})", self.0.bits()) - } - } - - // ===================================================================== - // PairFilterContext — transient view, only valid during the callback. - // - // We keep raw pointers to the rapier-side context fields. The - // `valid` flag is flipped to `false` immediately after the Python - // callback returns; subsequent accesses raise. - // ===================================================================== - - /// Context passed to ``PhysicsHooks.filter_contact_pair`` / - /// ``filter_intersection_pair``. - /// - /// Exposes the two colliders and (when applicable) their parent rigid - /// bodies. The instance is a **transient view** valid only for the - /// duration of the callback that received it — keeping a reference - /// around and accessing it later raises ``RuntimeError``. - #[pyclass(name = "PairFilterContext", module = "rapier", unsendable)] - pub struct PairFilterContext { - collider1: rapier::geometry::ColliderHandle, - collider2: rapier::geometry::ColliderHandle, - rigid_body1: Option, - rigid_body2: Option, - } - - #[pymethods] - impl PairFilterContext { - /// Handle of the first collider in the pair. - #[getter] - fn collider1(&self) -> ColliderHandle { - ColliderHandle(self.collider1) - } - /// Handle of the second collider in the pair. - #[getter] - fn collider2(&self) -> ColliderHandle { - ColliderHandle(self.collider2) - } - /// Handle of ``collider1``'s parent rigid body, or ``None``. - #[getter] - fn rigid_body1(&self) -> Option { - self.rigid_body1.map(RigidBodyHandle) - } - /// Handle of ``collider2``'s parent rigid body, or ``None``. - #[getter] - fn rigid_body2(&self) -> Option { - self.rigid_body2.map(RigidBodyHandle) - } - /// Debug repr — shows the raw collider handles. - fn __repr__(&self) -> String { - format!( - "PairFilterContext(c1={:?}, c2={:?})", - self.collider1.into_raw_parts(), - self.collider2.into_raw_parts(), - ) - } - } - - // ===================================================================== - // ContactModificationContext — transient mutable view. - // - // The rapier callback gives us `&mut Vec`, `&mut Vector`, - // `&mut u32`. We stash raw pointers and a `valid` flag. Mutations from - // Python persist back to the rapier-side buffer. - // ===================================================================== - - /// Context passed to ``PhysicsHooks.modify_solver_contacts``. - /// - /// Lets the user inspect and mutate the solver contacts of a manifold - /// before they're handed to the constraint solver: clear them to skip - /// the contact, flip the normal, tweak per-contact user data, etc. - /// - /// Like :class:`PairFilterContext`, the instance is a **transient - /// view** valid only inside the callback — accessing mutating fields - /// (``normal``, ``user_data``, ``solver_contacts``) after the callback - /// returns raises ``RuntimeError``. - #[pyclass(name = "ContactModificationContext", module = "rapier", unsendable)] - pub struct ContactModificationContext { - collider1: rapier::geometry::ColliderHandle, - collider2: rapier::geometry::ColliderHandle, - rigid_body1: Option, - rigid_body2: Option, - // Read-only snapshot of `manifold.local_n1` (we don't expose the - // full manifold mutably — it would entangle lifetimes too deeply). - local_n1: rapier::math::Vector, - local_n2: rapier::math::Vector, - normal: *mut rapier::math::Vector, - solver_contacts: *mut Vec, - user_data: *mut u32, - valid: bool, - } - - impl ContactModificationContext { - #[inline] - fn check_valid(&self) -> PyResult<()> { - if !self.valid { - return Err($crate::pyo3::exceptions::PyRuntimeError::new_err( - "ContactModificationContext is no longer valid (used outside of callback)", - )); - } - Ok(()) - } - } - - #[pymethods] - impl ContactModificationContext { - /// Handle of the first collider in the pair. - #[getter] - fn collider1(&self) -> ColliderHandle { - ColliderHandle(self.collider1) - } - /// Handle of the second collider in the pair. - #[getter] - fn collider2(&self) -> ColliderHandle { - ColliderHandle(self.collider2) - } - /// Handle of ``collider1``'s parent rigid body, or ``None``. - #[getter] - fn rigid_body1(&self) -> Option { - self.rigid_body1.map(RigidBodyHandle) - } - /// Handle of ``collider2``'s parent rigid body, or ``None``. - #[getter] - fn rigid_body2(&self) -> Option { - self.rigid_body2.map(RigidBodyHandle) - } - /// Read-only snapshot of the manifold's local normal on ``collider1``. - #[getter] - fn local_n1(&self) -> $Vec { - let v: $crate::na::SVector = self.local_n1.into(); - $Vec(v) - } - /// Read-only snapshot of the manifold's local normal on ``collider2``. - #[getter] - fn local_n2(&self) -> $Vec { - let v: $crate::na::SVector = self.local_n2.into(); - $Vec(v) - } - - /// Mutable world-space contact normal (read). - /// - /// :raises RuntimeError: If accessed outside the callback. - #[getter] - fn normal(&self) -> PyResult<$Vec> { - self.check_valid()?; - // SAFETY: `valid` is true exactly during the callback, where - // the rapier-side normal is reachable through this raw pointer. - let v: rapier::math::Vector = unsafe { *self.normal }; - let nav: $crate::na::SVector = v.into(); - Ok($Vec(nav)) - } - /// Mutable world-space contact normal (write). - /// - /// :raises RuntimeError: If accessed outside the callback. - #[setter] - fn set_normal(&mut self, v: PyVector) -> PyResult<()> { - self.check_valid()?; - // SAFETY: see `normal` getter. - unsafe { - *self.normal = v.0.into(); - } - Ok(()) - } - - /// Per-manifold user data, mutable from inside the callback (read). - /// - /// :raises RuntimeError: If accessed outside the callback. - #[getter] - fn user_data(&self) -> PyResult { - self.check_valid()?; - // SAFETY: see `normal` getter. - Ok(unsafe { *self.user_data }) - } - /// Per-manifold user data (write). - /// - /// :raises RuntimeError: If accessed outside the callback. - #[setter] - fn set_user_data(&mut self, v: u32) -> PyResult<()> { - self.check_valid()?; - // SAFETY: see `normal` getter. - unsafe { - *self.user_data = v; - } - Ok(()) - } - - /// Return the current solver contacts as a list (read-only snapshot). - /// - /// :raises RuntimeError: If accessed outside the callback. - #[getter] - fn solver_contacts(&self) -> PyResult> { - self.check_valid()?; - // SAFETY: see `normal` getter. - let vec_ref: &Vec = - unsafe { &*self.solver_contacts }; - let mut out = Vec::with_capacity(vec_ref.len()); - for sc in vec_ref { - out.push($crate::__events_hooks_solver_contact_to_py!($dim, sc)); - } - Ok(out) - } - - /// Clear all solver contacts (the manifold won't contribute to the solver). - /// - /// :raises RuntimeError: If accessed outside the callback. - fn clear_solver_contacts(&mut self) -> PyResult<()> { - self.check_valid()?; - // SAFETY: see `normal` getter. - unsafe { - (*self.solver_contacts).clear(); - } - Ok(()) - } - - /// Number of solver contacts currently in the manifold. - /// - /// :raises RuntimeError: If accessed outside the callback. - fn num_solver_contacts(&self) -> PyResult { - self.check_valid()?; - // SAFETY: see `normal` getter. - Ok(unsafe { (*self.solver_contacts).len() }) - } - - /// Drop the solver contact at index ``i``. Out-of-range indices are ignored. - /// - /// :param i: Zero-based index into the solver-contact list. - /// :raises RuntimeError: If accessed outside the callback. - fn remove_solver_contact(&mut self, i: usize) -> PyResult<()> { - self.check_valid()?; - // SAFETY: see `normal` getter. - unsafe { - let v = &mut *self.solver_contacts; - if i < v.len() { - v.remove(i); - } - } - Ok(()) - } - - /// Emulate a one-way platform contact (port of rapier's helper). - /// - /// The "oneway" behavior only allows contacts when the local - /// contact normal of ``collider1`` is roughly aligned with - /// ``allowed_local_n1`` (within ``angle`` radians). Contacts - /// pointing the wrong way are cleared so the constraint solver - /// ignores them — letting the body pass through from the other - /// side. - /// - /// :param allowed_local_n1: Local-space normal that should let the - /// contact through (e.g. ``Vec3(0, 1, 0)`` for a floor-only - /// platform). - /// :param angle: Half-cone tolerance in radians. - /// :raises RuntimeError: If accessed outside the callback. - fn update_as_oneway_platform( - &mut self, - allowed_local_n1: PyVector, - angle: Real, - ) -> PyResult<()> { - self.check_valid()?; - // Reconstruct a `ContactModificationContext` borrow shape on - // the fly and dispatch. We re-implement the logic inline to - // avoid the manifold borrow. - // - // SAFETY: see `normal` getter. - let allowed_n1: rapier::math::Vector = allowed_local_n1.0.into(); - let cang = Real::cos(angle); - let contact_is_ok = self.local_n1.dot(allowed_n1) >= cang; - - const UNKNOWN: u32 = 0; - const ALLOWED: u32 = 1; - const FORBIDDEN: u32 = 2; - - let (sc, ud) = unsafe { (&mut *self.solver_contacts, &mut *self.user_data) }; - match *ud { - UNKNOWN => { - if contact_is_ok { - *ud = ALLOWED; - } else { - sc.clear(); - if self.local_n1.length_squared() > 0.1 { - *ud = FORBIDDEN; - } - } - } - FORBIDDEN => { - if contact_is_ok && sc.iter().all(|c| c.dist > 0.0) { - *ud = ALLOWED; - } else { - sc.clear(); - } - } - ALLOWED => { - if sc.is_empty() { - *ud = UNKNOWN; - } - } - _ => {} - } - Ok(()) - } - - /// Debug repr — shows the raw collider handles. - fn __repr__(&self) -> String { - format!( - "ContactModificationContext(c1={:?}, c2={:?})", - self.collider1.into_raw_parts(), - self.collider2.into_raw_parts(), - ) - } - } - - // ===================================================================== - // PyEventHandler — adapts an arbitrary Python object implementing the - // `EventHandler` protocol to rapier's `EventHandler` trait. - // ===================================================================== - - /// Adapter wrapping an arbitrary Python `EventHandler`-protocol object. - pub struct PyEventHandler { - obj: Py, - err_slot: Arc>, - } - - impl PyEventHandler { - pub fn new(obj: Py, err_slot: Arc>) -> Self { - Self { obj, err_slot } - } - } - - // SAFETY: `Py` is `Send` (PyO3 manages the refcount via atomic - // ops on `clone_ref`). Every interaction with the inner Python object - // happens inside `Python::with_gil`, which re-acquires the GIL — so we - // never touch Python state without serialization. `Arc>` is - // both `Send` and `Sync`. - unsafe impl Send for PyEventHandler {} - unsafe impl Sync for PyEventHandler {} - - impl rapier::pipeline::EventHandler for PyEventHandler { - fn handle_collision_event( - &self, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, - event: rapier::geometry::CollisionEvent, - contact_pair: Option<&rapier::geometry::ContactPair>, - ) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return; - } - } - Python::with_gil(|py| { - let py_event = CollisionEvent::from_rapier(event); - let py_pair: PyObject = match contact_pair { - Some(p) => Py::new(py, ContactPair(p.clone())) - .map(|h| h.into_any()) - .unwrap_or_else(|_| py.None()), - None => py.None(), - }; - let res = self.obj.bind(py).call_method1( - "handle_collision_event", - (py.None(), py.None(), py_event, py_pair), - ); - if let Err(e) = res { - $crate::__events_hooks_stash_err!(self.err_slot, e); - } - }); - } - - fn handle_contact_force_event( - &self, - dt: Real, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, - contact_pair: &rapier::geometry::ContactPair, - total_force_magnitude: Real, - ) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return; - } - } - Python::with_gil(|py| { - let py_pair = match Py::new(py, ContactPair(contact_pair.clone())) { - Ok(p) => p, - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - return; - } - }; - let res = self.obj.bind(py).call_method1( - "handle_contact_force_event", - (dt, py.None(), py.None(), py_pair, total_force_magnitude), - ); - if let Err(e) = res { - $crate::__events_hooks_stash_err!(self.err_slot, e); - } - }); - } - } - - // ===================================================================== - // ChannelEventCollector — built-in EventHandler that funnels events - // into internal buffers, drained on demand from Python. - // ===================================================================== - - /// Recommended built-in event handler. - /// - /// A :class:`ChannelEventCollector` accumulates collision and - /// contact-force events into lock-free internal buffers. Attach it to - /// a world via ``world.event_handler``, then call - /// :meth:`drain_collision_events` / :meth:`drain_contact_force_events` - /// after each step to consume them:: - /// - /// c = ChannelEventCollector() - /// world.event_handler = c - /// world.step() - /// for event in c.drain_collision_events(): - /// ... - /// - /// Because the buffers are owned by Rust and the pipeline detects the - /// concrete type, this avoids the per-event Python callback overhead - /// of a generic ``EventHandler`` protocol object. - #[pyclass(name = "ChannelEventCollector", module = "rapier")] - pub struct ChannelEventCollector { - collisions: Arc>>, - forces: Arc>>, - } - - #[pymethods] - impl ChannelEventCollector { - /// Construct an empty collector with no events queued. - #[new] - fn new() -> Self { - Self { - collisions: Arc::new(Mutex::new(Vec::new())), - forces: Arc::new(Mutex::new(Vec::new())), - } - } - - /// Consume and return every queued :class:`CollisionEvent`. - /// - /// After this call the internal collision buffer is empty. - /// - /// :returns: A list of collision events captured since the last - /// drain. - fn drain_collision_events(&self) -> Vec { - std::mem::take(&mut *self.collisions.lock().unwrap()) - } - - /// Consume and return every queued :class:`ContactForceEvent`. - /// - /// After this call the internal contact-force buffer is empty. - /// - /// :returns: A list of contact-force events captured since the - /// last drain. - fn drain_contact_force_events(&self) -> Vec { - std::mem::take(&mut *self.forces.lock().unwrap()) - } - - /// Drop every queued collision and contact-force event. - fn clear(&self) { - self.collisions.lock().unwrap().clear(); - self.forces.lock().unwrap().clear(); - } - - /// Total queued events (collisions + contact-force). - fn __len__(&self) -> usize { - self.collisions.lock().unwrap().len() + self.forces.lock().unwrap().len() - } - - /// Debug repr — shows the per-buffer queue lengths. - fn __repr__(&self) -> String { - let ce = self.collisions.lock().unwrap().len(); - let fe = self.forces.lock().unwrap().len(); - format!("ChannelEventCollector(collisions={ce}, forces={fe})") - } - } - - impl ChannelEventCollector { - /// Build an `EventHandler`-implementing adapter that pushes into - /// this collector's buffers. The adapter is `Send + Sync`. - pub fn as_event_handler(&self) -> ChannelEventCollectorAdapter { - ChannelEventCollectorAdapter { - collisions: Arc::clone(&self.collisions), - forces: Arc::clone(&self.forces), - } - } - } - - /// `EventHandler`-implementing adapter built on the fly by - /// `ChannelEventCollector::as_event_handler`. Holds `Arc>` - /// clones of the collector's buffers, so it can outlive a single call. - pub struct ChannelEventCollectorAdapter { - collisions: Arc>>, - forces: Arc>>, - } - - impl rapier::pipeline::EventHandler for ChannelEventCollectorAdapter { - fn handle_collision_event( - &self, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, - event: rapier::geometry::CollisionEvent, - _contact_pair: Option<&rapier::geometry::ContactPair>, - ) { - self.collisions - .lock() - .unwrap() - .push(CollisionEvent::from_rapier(event)); - } - - fn handle_contact_force_event( - &self, - dt: Real, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, - contact_pair: &rapier::geometry::ContactPair, - total_force_magnitude: Real, - ) { - let r = rapier::geometry::ContactForceEvent::from_contact_pair( - dt, - contact_pair, - total_force_magnitude, - ); - let v: $crate::na::SVector = r.total_force.into(); - let d: $crate::na::SVector = r.max_force_direction.into(); - self.forces.lock().unwrap().push(ContactForceEvent { - collider1: ColliderHandle(r.collider1), - collider2: ColliderHandle(r.collider2), - total_force: $Vec(v), - total_force_magnitude: r.total_force_magnitude, - max_force_direction: $Vec(d), - max_force_magnitude: r.max_force_magnitude, - }); - } - } - - // ===================================================================== - // PyPhysicsHooks — adapter for the `PhysicsHooks` Python protocol. - // ===================================================================== - - /// Adapter wrapping an arbitrary Python `PhysicsHooks`-protocol object. - pub struct PyPhysicsHooks { - obj: Py, - err_slot: Arc>, - } - - impl PyPhysicsHooks { - pub fn new(obj: Py, err_slot: Arc>) -> Self { - Self { obj, err_slot } - } - } - - // SAFETY: see `PyEventHandler`. - unsafe impl Send for PyPhysicsHooks {} - unsafe impl Sync for PyPhysicsHooks {} - - impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { - fn filter_contact_pair( - &self, - context: &rapier::pipeline::PairFilterContext, - ) -> Option { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return Some(rapier::geometry::SolverFlags::default()); - } - } - Python::with_gil(|py| { - let ctx = match Py::new( - py, - PairFilterContext { - collider1: context.collider1, - collider2: context.collider2, - rigid_body1: context.rigid_body1, - rigid_body2: context.rigid_body2, - }, - ) { - Ok(c) => c, - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - return Some(rapier::geometry::SolverFlags::default()); - } - }; - let res = self - .obj - .bind(py) - .call_method1("filter_contact_pair", (ctx,)); - match res { - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - Some(rapier::geometry::SolverFlags::default()) - } - Ok(v) => { - if v.is_none() { - None - } else if let Ok(f) = v.extract::() { - Some(f.0) - } else if let Ok(bits) = v.extract::() { - Some(rapier::geometry::SolverFlags::from_bits_truncate(bits)) - } else { - // Any other truthy return is treated as "default". - Some(rapier::geometry::SolverFlags::default()) - } - } - } - }) - } - - fn filter_intersection_pair( - &self, - context: &rapier::pipeline::PairFilterContext, - ) -> bool { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return true; - } - } - Python::with_gil(|py| { - let ctx = match Py::new( - py, - PairFilterContext { - collider1: context.collider1, - collider2: context.collider2, - rigid_body1: context.rigid_body1, - rigid_body2: context.rigid_body2, - }, - ) { - Ok(c) => c, - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - return true; - } - }; - match self - .obj - .bind(py) - .call_method1("filter_intersection_pair", (ctx,)) - { - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - true - } - Ok(v) => v.extract::().unwrap_or(true), - } - }) - } - - fn modify_solver_contacts( - &self, - context: &mut rapier::pipeline::ContactModificationContext, - ) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return; - } - } - Python::with_gil(|py| { - let manifold_local_n1 = context.manifold.local_n1; - let manifold_local_n2 = context.manifold.local_n2; - let normal_ptr: *mut rapier::math::Vector = context.normal; - let sc_ptr: *mut Vec = context.solver_contacts; - let ud_ptr: *mut u32 = context.user_data; - let ctx_py = match Py::new( - py, - ContactModificationContext { - collider1: context.collider1, - collider2: context.collider2, - rigid_body1: context.rigid_body1, - rigid_body2: context.rigid_body2, - local_n1: manifold_local_n1, - local_n2: manifold_local_n2, - normal: normal_ptr, - solver_contacts: sc_ptr, - user_data: ud_ptr, - valid: true, - }, - ) { - Ok(c) => c, - Err(e) => { - $crate::__events_hooks_stash_err!(self.err_slot, e); - return; - } - }; - let res = self - .obj - .bind(py) - .call_method1("modify_solver_contacts", (ctx_py.clone_ref(py),)); - // Invalidate the transient view before letting Python see - // its `None` return. - { - let mut borrowed = ctx_py.borrow_mut(py); - borrowed.valid = false; - } - if let Err(e) = res { - $crate::__events_hooks_stash_err!(self.err_slot, e); - } - }); - } - } - - // ===================================================================== - // Public helpers used by `pipeline.rs` to construct the adapters. - // ===================================================================== - - /// Wrap an arbitrary Python object as a (boxed) rapier `EventHandler`, - /// or detect a `ChannelEventCollector` and short-circuit to its - /// lock-free buffer adapter. - /// - /// Returns `None` if `obj` is `Py::None` / Python `None`. - pub fn build_event_handler( - py: Python<'_>, - obj: Option<&Py>, - err_slot: Arc>, - ) -> Option> { - let obj = obj?; - let bound = obj.bind(py); - if bound.is_none() { - return None; - } - // ChannelEventCollector: optimize away the Python callback entirely. - if let Ok(c) = bound.extract::>() { - return Some(Box::new(c.as_event_handler())); - } - Some(Box::new(PyEventHandler::new(obj.clone_ref(py), err_slot))) - } - - /// Wrap an arbitrary Python object as a (boxed) rapier `PhysicsHooks`. - pub fn build_physics_hooks( - py: Python<'_>, - obj: Option<&Py>, - err_slot: Arc>, - ) -> Option> { - let obj = obj?; - let bound = obj.bind(py); - if bound.is_none() { - return None; - } - Some(Box::new(PyPhysicsHooks::new(obj.clone_ref(py), err_slot))) - } - }; -} - -// ===================================================================== -// SolverContact dim-specific conversion. -// ===================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __events_hooks_solver_contact_to_py { - (3, $sc:expr) => {{ - let p: $crate::na::Vector3 = $sc.point.into(); - SolverContact { - point: Point3($crate::na::Point3::from(p)), - dist: $sc.dist, - friction: $sc.friction, - restitution: $sc.restitution, - contact_id: $sc.contact_id[0], - is_new: $sc.is_new != 0.0, - } - }}; - (2, $sc:expr) => {{ - let p: $crate::na::Vector2 = $sc.point.into(); - SolverContact { - point: Point2($crate::na::Point2::from(p)), - dist: $sc.dist, - friction: $sc.friction, - restitution: $sc.restitution, - contact_id: $sc.contact_id[0], - is_new: $sc.is_new != 0.0, - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __events_hooks_stash_err { - ($slot:expr, $err:expr) => {{ - let mut s = $slot.lock().unwrap(); - if s.err.is_none() { - s.err = Some($err); - } - if s.policy_strict { - s.aborted = true; - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_events_hooks_register { - () => { - pub fn register_events_hooks( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/geometry.rs b/python/rapier-py-core/src/geometry.rs deleted file mode 100644 index 15decfb62..000000000 --- a/python/rapier-py-core/src/geometry.rs +++ /dev/null @@ -1,5152 +0,0 @@ -//! Macro that emits the user-facing geometry / collider / shape `#[pyclass]`-es -//! per cdylib. -//! -//! Invoke `define_geometry_types!` once per cdylib **before** -//! `define_dynamics_types!` so the dynamics macro can reference -//! `ColliderHandle` / `ColliderSet` from the enclosing scope. -//! -//! Expects the surrounding cdylib to already have: -//! - The `Real`/`DIM` aliases and `Py{Vector,Point,Rotation,Isometry,AngVector}` -//! adapter newtypes (from `define_conv_types!`). -//! - The user-facing math `#[pyclass]`-es (`Vec*`/`Point*`/`Rotation*`/`Isometry*`) -//! from `define_math_types!`. -//! - `use rapier{2,3}d{,_f64} as rapier;` aliasing. - -use numpy::{PyReadonlyArray2, PyUntypedArrayMethods}; -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; - -/// Extract an `Mx3` `u32` matrix → `Vec<[u32; 3]>`. -pub fn extract_indices(obj: &Bound<'_, PyAny>) -> PyResult> { - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 3 { - return Err(PyValueError::new_err(format!( - "expected ndarray with shape (M, 3); got (M, {ncols})" - ))); - } - let slice = arr - .as_slice() - .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; - let mut out = Vec::with_capacity(nrows); - for chunk in slice.chunks_exact(3) { - out.push([chunk[0], chunk[1], chunk[2]]); - } - return Ok(out); - } - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 3 { - return Err(PyValueError::new_err(format!( - "expected ndarray with shape (M, 3); got (M, {ncols})" - ))); - } - let slice = arr - .as_slice() - .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; - let mut out = Vec::with_capacity(nrows); - for chunk in slice.chunks_exact(3) { - out.push([chunk[0] as u32, chunk[1] as u32, chunk[2] as u32]); - } - return Ok(out); - } - let seq: Vec> = obj.extract()?; - seq.iter() - .map(|c| { - if c.len() != 3 { - return Err(PyValueError::new_err(format!( - "expected inner sequence of length 3; got {}", - c.len(), - ))); - } - Ok([c[0], c[1], c[2]]) - }) - .collect() -} - -/// Extract an `Mx2` `u32` matrix → `Vec<[u32; 2]>`. -pub fn extract_indices_2(obj: &Bound<'_, PyAny>) -> PyResult> { - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 2 { - return Err(PyValueError::new_err(format!( - "expected ndarray with shape (M, 2); got (M, {ncols})" - ))); - } - let slice = arr - .as_slice() - .map_err(|_| PyValueError::new_err("ndarray must be contiguous"))?; - let mut out = Vec::with_capacity(nrows); - for chunk in slice.chunks_exact(2) { - out.push([chunk[0], chunk[1]]); - } - return Ok(out); - } - let seq: Vec> = obj.extract()?; - seq.iter() - .map(|c| { - if c.len() != 2 { - return Err(PyValueError::new_err(format!( - "expected inner sequence of length 2; got {}", - c.len(), - ))); - } - Ok([c[0], c[1]]) - }) - .collect() -} - -/// Materialize the geometry `#[pyclass]` types for a given `(Real, DIM)` pair. -/// -/// Produces `register_geometry(py, m) -> PyResult<()>` for `#[pymodule]`. -#[macro_export] -macro_rules! define_geometry_types { - (DIM = 2) => { - $crate::__define_geometry_shared!(2); - $crate::__define_geometry_2d!(); - $crate::__define_geometry_collider!(2, Vec2, Point2, Rotation2, Isometry2); - $crate::__define_geometry_register!(2); - }; - (DIM = 3) => { - $crate::__define_geometry_shared!(3); - $crate::__define_geometry_3d!(); - $crate::__define_geometry_collider!(3, Vec3, Point3, Rotation3, Isometry3); - $crate::__define_geometry_register!(3); - }; -} - -// ============================================================ -// Shared dim-agnostic geometry types -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_shared { - ($dim:tt) => { - use $crate::pyo3::exceptions::PyIndexError; - - // ============================================================ - // ColliderHandle (moved from dynamics) - // ============================================================ - - /// Opaque, generational handle identifying a collider in a `ColliderSet`. - /// - /// Handles remain valid until the collider is removed. After removal, - /// the slot may be reused with a bumped generation, so old handles - /// pointing to a freed slot compare unequal to the new one. Cheap to - /// copy and hash. - #[pyclass(name = "ColliderHandle", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct ColliderHandle(pub rapier::geometry::ColliderHandle); - - #[pymethods] - impl ColliderHandle { - /// Construct a handle from raw `(index, generation)` parts. - /// - /// Mostly useful for tests and serialization round-trips; in - /// normal use, handles come from `ColliderSet.insert(...)`. - #[new] - #[pyo3(signature = (index=0, generation=0))] - fn new(index: u32, generation: u32) -> Self { - Self(rapier::geometry::ColliderHandle::from_raw_parts( - index, generation, - )) - } - /// Build a handle from raw `(index, generation)` parts. - #[staticmethod] - fn from_raw_parts(index: u32, generation: u32) -> Self { - Self(rapier::geometry::ColliderHandle::from_raw_parts( - index, generation, - )) - } - /// Return the sentinel "invalid" handle. - /// - /// Never refers to a real collider; useful as a default value. - #[staticmethod] - fn invalid() -> Self { - Self(rapier::geometry::ColliderSet::invalid_handle()) - } - /// Slot index inside the underlying `ColliderSet`. - #[getter] - fn index(&self) -> u32 { - self.0.into_raw_parts().0 - } - /// Generation counter for the slot. Bumped on each reuse. - #[getter] - fn generation(&self) -> u32 { - self.0.into_raw_parts().1 - } - fn __hash__(&self) -> u64 { - let (i, g) = self.0.into_raw_parts(); - ((i as u64) << 32) | (g as u64) - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "ColliderHandle supports only == and !=", - )), - } - } - fn __repr__(&self) -> String { - let (i, g) = self.0.into_raw_parts(); - format!("ColliderHandle(index={}, generation={})", i, g) - } - } - - // ============================================================ - // ShapeType (enum) - // ============================================================ - - /// Discriminant returned by `SharedShape.shape_type`. - /// - /// Use this to decide which `as_*()` downcast accessor to call on a - /// `SharedShape`. `CUSTOM` covers user-defined shapes that do not - /// match any built-in variant. - #[pyclass(name = "ShapeType", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum ShapeType { - BALL, - CUBOID, - CAPSULE, - SEGMENT, - TRIANGLE, - VOXELS, - TRIMESH, - POLYLINE, - HALFSPACE, - HEIGHTFIELD, - COMPOUND, - CONVEX_POLYGON, - CONVEX_POLYHEDRON, - CYLINDER, - CONE, - ROUND_CUBOID, - ROUND_TRIANGLE, - ROUND_CYLINDER, - ROUND_CONE, - ROUND_CONVEX_POLYHEDRON, - ROUND_CONVEX_POLYGON, - CUSTOM, - } - - impl ShapeType { - fn from_rapier(t: rapier::parry::shape::ShapeType) -> Self { - use rapier::parry::shape::ShapeType as PT; - // The dim-specific variants are gated by parry's `dim2`/`dim3` - // features, but the cdylib already knows its own dim, so we - // express the mapping using the dim-specific helper macro. - $crate::__shape_type_from_rapier!($dim, t) - } - } - - // ============================================================ - // ColliderType - // ============================================================ - - /// Distinguishes a `SOLID` collider from a `SENSOR`. - /// - /// Solid colliders generate contact forces; sensor colliders only - /// fire intersection events and do not produce contact responses. - #[pyclass(name = "ColliderType", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum ColliderType { - SOLID, - SENSOR, - } - - impl ColliderType { - #[allow(dead_code)] - fn to_rapier(self) -> rapier::geometry::ColliderType { - match self { - Self::SOLID => rapier::geometry::ColliderType::Solid, - Self::SENSOR => rapier::geometry::ColliderType::Sensor, - } - } - #[allow(dead_code)] - fn from_rapier(t: rapier::geometry::ColliderType) -> Self { - match t { - rapier::geometry::ColliderType::Solid => Self::SOLID, - rapier::geometry::ColliderType::Sensor => Self::SENSOR, - } - } - } - - // ============================================================ - // ColliderEnabled - // ============================================================ - - /// Enabled-state of a collider. - /// - /// `DISABLED_BY_PARENT` means the parent rigid-body is disabled, so - /// the collider is effectively off without being explicitly disabled - /// by the user. - #[pyclass(name = "ColliderEnabled", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum ColliderEnabled { - ENABLED, - DISABLED_BY_PARENT, - DISABLED, - } - - impl ColliderEnabled { - #[allow(dead_code)] - fn from_rapier(t: rapier::geometry::ColliderEnabled) -> Self { - match t { - rapier::geometry::ColliderEnabled::Enabled => Self::ENABLED, - rapier::geometry::ColliderEnabled::DisabledByParent => Self::DISABLED_BY_PARENT, - rapier::geometry::ColliderEnabled::Disabled => Self::DISABLED, - } - } - } - - // ============================================================ - // ActiveEvents (bitflags) - // ============================================================ - - /// Per-collider opt-in flags selecting which events to emit. - /// - /// A collider only emits an event kind if it has the corresponding - /// flag set. Use `COLLISION_EVENTS` to receive `CollisionEvent` - /// (started/stopped) and `CONTACT_FORCE_EVENTS` to receive - /// `ContactForceEvent`. Combine flags via `|`. - #[pyclass(name = "ActiveEvents", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct ActiveEvents(pub rapier::pipeline::ActiveEvents); - - #[pymethods] - impl ActiveEvents { - /// Construct from a raw bit pattern. Unknown bits are dropped. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::pipeline::ActiveEvents::from_bits_truncate(bits)) - } - /// Return the empty flag set (no events). - #[staticmethod] - fn empty() -> Self { - Self(rapier::pipeline::ActiveEvents::empty()) - } - /// Return the flag set with every known event kind enabled. - #[staticmethod] - fn all_events() -> Self { - Self(rapier::pipeline::ActiveEvents::all()) - } - #[classattr] - const COLLISION_EVENTS: ActiveEvents = - ActiveEvents(rapier::pipeline::ActiveEvents::COLLISION_EVENTS); - #[classattr] - const CONTACT_FORCE_EVENTS: ActiveEvents = - ActiveEvents(rapier::pipeline::ActiveEvents::CONTACT_FORCE_EVENTS); - #[classattr] - const NONE: ActiveEvents = ActiveEvents(rapier::pipeline::ActiveEvents::empty()); - - /// Raw bit pattern of the flag set. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// True iff all flags in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no flags are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - fn __invert__(&self) -> Self { - Self(!self.0) - } - fn __sub__(&self, other: &Self) -> Self { - Self(self.0 - other.0) - } - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("ActiveEvents supports only == and !=")), - } - } - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - fn __repr__(&self) -> String { - format!("ActiveEvents(bits={:#010b})", self.0.bits()) - } - } - - // ============================================================ - // ActiveHooks (bitflags) - // ============================================================ - - /// Per-collider opt-in flags selecting which `PhysicsHooks` callbacks - /// to invoke. - /// - /// A `PhysicsHooks` method only fires for a contact/intersection pair - /// if at least one of the involved colliders has the matching flag - /// set. Combine flags via `|`. - #[pyclass(name = "ActiveHooks", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct ActiveHooks(pub rapier::pipeline::ActiveHooks); - - #[pymethods] - impl ActiveHooks { - /// Construct from a raw bit pattern. Unknown bits are dropped. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::pipeline::ActiveHooks::from_bits_truncate(bits)) - } - /// Return the empty flag set (no hooks active). - #[staticmethod] - fn empty() -> Self { - Self(rapier::pipeline::ActiveHooks::empty()) - } - /// Return the flag set with every known hook enabled. - #[staticmethod] - fn all_hooks() -> Self { - Self(rapier::pipeline::ActiveHooks::all()) - } - #[classattr] - const FILTER_CONTACT_PAIR: ActiveHooks = - ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_CONTACT_PAIRS); - #[classattr] - const FILTER_CONTACT_PAIRS: ActiveHooks = - ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_CONTACT_PAIRS); - #[classattr] - const FILTER_INTERSECTION_PAIR: ActiveHooks = - ActiveHooks(rapier::pipeline::ActiveHooks::FILTER_INTERSECTION_PAIR); - #[classattr] - const MODIFY_SOLVER_CONTACTS: ActiveHooks = - ActiveHooks(rapier::pipeline::ActiveHooks::MODIFY_SOLVER_CONTACTS); - #[classattr] - const NONE: ActiveHooks = ActiveHooks(rapier::pipeline::ActiveHooks::empty()); - - /// Raw bit pattern of the flag set. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// True iff all flags in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no flags are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - fn __invert__(&self) -> Self { - Self(!self.0) - } - fn __sub__(&self, other: &Self) -> Self { - Self(self.0 - other.0) - } - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("ActiveHooks supports only == and !=")), - } - } - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - fn __repr__(&self) -> String { - format!("ActiveHooks(bits={:#010b})", self.0.bits()) - } - } - - // ============================================================ - // ActiveCollisionTypes (bitflags) - // ============================================================ - - /// Per-collider flags selecting which *rigid-body type* combinations - /// generate contacts. - /// - /// By default only pairs involving at least one dynamic body collide - /// (`DYNAMIC_DYNAMIC | DYNAMIC_KINEMATIC | DYNAMIC_FIXED`). Enable e.g. - /// `KINEMATIC_FIXED` or `KINEMATIC_KINEMATIC` to detect contacts that - /// would otherwise be skipped — useful for kinematic character or - /// platform queries. Combine flags via `|`. - #[pyclass(name = "ActiveCollisionTypes", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct ActiveCollisionTypes(pub rapier::geometry::ActiveCollisionTypes); - - #[pymethods] - impl ActiveCollisionTypes { - /// Construct from a raw bit pattern. Unknown bits are dropped. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u16) -> Self { - Self(rapier::geometry::ActiveCollisionTypes::from_bits_truncate( - bits, - )) - } - /// Return the empty flag set (no collision types active). - #[staticmethod] - fn empty() -> Self { - Self(rapier::geometry::ActiveCollisionTypes::empty()) - } - /// Return the flag set with every collision type enabled. - #[staticmethod] - fn all() -> Self { - Self(rapier::geometry::ActiveCollisionTypes::all()) - } - /// Return the default flag set: every pair involving at least one - /// dynamic body (`DYNAMIC_DYNAMIC | DYNAMIC_KINEMATIC | DYNAMIC_FIXED`). - #[staticmethod] - fn default_types() -> Self { - Self(rapier::geometry::ActiveCollisionTypes::default()) - } - #[classattr] - const DYNAMIC_DYNAMIC: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_DYNAMIC); - #[classattr] - const DYNAMIC_KINEMATIC: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_KINEMATIC); - #[classattr] - const DYNAMIC_FIXED: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::DYNAMIC_FIXED); - #[classattr] - const KINEMATIC_KINEMATIC: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::KINEMATIC_KINEMATIC); - #[classattr] - const KINEMATIC_FIXED: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::KINEMATIC_FIXED); - #[classattr] - const FIXED_FIXED: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::FIXED_FIXED); - #[classattr] - const NONE: ActiveCollisionTypes = - ActiveCollisionTypes(rapier::geometry::ActiveCollisionTypes::empty()); - - /// Raw bit pattern of the flag set. - #[getter] - fn bits(&self) -> u16 { - self.0.bits() - } - /// True iff all flags in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no flags are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - fn __invert__(&self) -> Self { - Self(!self.0) - } - fn __sub__(&self, other: &Self) -> Self { - Self(self.0 - other.0) - } - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "ActiveCollisionTypes supports only == and !=", - )), - } - } - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - fn __repr__(&self) -> String { - format!("ActiveCollisionTypes(bits={:#018b})", self.0.bits()) - } - } - - // ============================================================ - // Group (bitflags) - // ============================================================ - - /// 32-bit bitmask identifying one or more collision groups. - /// - /// Used as both the *membership* and *filter* side of an - /// `InteractionGroups`: two colliders interact iff each one's - /// memberships intersect the other's filter. Combine groups via - /// `|`, take complements via `~`, and use `ALL` / `NONE` for the - /// universe / empty set. - #[pyclass(name = "Group", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct Group(pub rapier::geometry::Group); - - #[pymethods] - impl Group { - /// Construct from a raw bit pattern. All 32 bits are retained. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::geometry::Group::from_bits_retain(bits)) - } - - /// Construct from a raw bit pattern. All 32 bits are retained. - #[staticmethod] - fn from_bits(bits: u32) -> Self { - Self(rapier::geometry::Group::from_bits_retain(bits)) - } - - // Rapier's Group constants are 1-indexed (GROUP_1..GROUP_32). - // The spec asks for `GROUP_0..GROUP_31`. Expose both spellings to - // bridge both worlds. - #[classattr] - const GROUP_0: Group = Group(rapier::geometry::Group::GROUP_1); - #[classattr] - const GROUP_1: Group = Group(rapier::geometry::Group::GROUP_2); - #[classattr] - const GROUP_2: Group = Group(rapier::geometry::Group::GROUP_3); - #[classattr] - const GROUP_3: Group = Group(rapier::geometry::Group::GROUP_4); - #[classattr] - const GROUP_4: Group = Group(rapier::geometry::Group::GROUP_5); - #[classattr] - const GROUP_5: Group = Group(rapier::geometry::Group::GROUP_6); - #[classattr] - const GROUP_6: Group = Group(rapier::geometry::Group::GROUP_7); - #[classattr] - const GROUP_7: Group = Group(rapier::geometry::Group::GROUP_8); - #[classattr] - const GROUP_8: Group = Group(rapier::geometry::Group::GROUP_9); - #[classattr] - const GROUP_9: Group = Group(rapier::geometry::Group::GROUP_10); - #[classattr] - const GROUP_10: Group = Group(rapier::geometry::Group::GROUP_11); - #[classattr] - const GROUP_11: Group = Group(rapier::geometry::Group::GROUP_12); - #[classattr] - const GROUP_12: Group = Group(rapier::geometry::Group::GROUP_13); - #[classattr] - const GROUP_13: Group = Group(rapier::geometry::Group::GROUP_14); - #[classattr] - const GROUP_14: Group = Group(rapier::geometry::Group::GROUP_15); - #[classattr] - const GROUP_15: Group = Group(rapier::geometry::Group::GROUP_16); - #[classattr] - const GROUP_16: Group = Group(rapier::geometry::Group::GROUP_17); - #[classattr] - const GROUP_17: Group = Group(rapier::geometry::Group::GROUP_18); - #[classattr] - const GROUP_18: Group = Group(rapier::geometry::Group::GROUP_19); - #[classattr] - const GROUP_19: Group = Group(rapier::geometry::Group::GROUP_20); - #[classattr] - const GROUP_20: Group = Group(rapier::geometry::Group::GROUP_21); - #[classattr] - const GROUP_21: Group = Group(rapier::geometry::Group::GROUP_22); - #[classattr] - const GROUP_22: Group = Group(rapier::geometry::Group::GROUP_23); - #[classattr] - const GROUP_23: Group = Group(rapier::geometry::Group::GROUP_24); - #[classattr] - const GROUP_24: Group = Group(rapier::geometry::Group::GROUP_25); - #[classattr] - const GROUP_25: Group = Group(rapier::geometry::Group::GROUP_26); - #[classattr] - const GROUP_26: Group = Group(rapier::geometry::Group::GROUP_27); - #[classattr] - const GROUP_27: Group = Group(rapier::geometry::Group::GROUP_28); - #[classattr] - const GROUP_28: Group = Group(rapier::geometry::Group::GROUP_29); - #[classattr] - const GROUP_29: Group = Group(rapier::geometry::Group::GROUP_30); - #[classattr] - const GROUP_30: Group = Group(rapier::geometry::Group::GROUP_31); - #[classattr] - const GROUP_31: Group = Group(rapier::geometry::Group::GROUP_32); - - #[classattr] - const ALL: Group = Group(rapier::geometry::Group::ALL); - #[classattr] - const NONE: Group = Group(rapier::geometry::Group::NONE); - - /// Raw 32-bit pattern of the group set. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// True iff all groups in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no group bit is set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __xor__(&self, other: &Self) -> Self { - Self(self.0 ^ other.0) - } - fn __invert__(&self) -> Self { - Self(!self.0) - } - fn __sub__(&self, other: &Self) -> Self { - Self(self.0 - other.0) - } - fn __bool__(&self) -> bool { - !self.0.is_empty() - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("Group supports only == and !=")), - } - } - fn __hash__(&self) -> u64 { - self.0.bits() as u64 - } - fn __repr__(&self) -> String { - format!("Group(bits={:#034b})", self.0.bits()) - } - } - - // ============================================================ - // InteractionTestMode - // ============================================================ - - /// How `InteractionGroups.test` combines the two pair-wise checks. - /// - /// `AND` (the default) requires each collider to be in the other's - /// filter set. `OR` only requires one side to pass. `DEFAULT` and - /// `ONLY_DYNAMIC` are kept for compatibility and behave like `AND`. - #[pyclass(name = "InteractionTestMode", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum InteractionTestMode { - DEFAULT, - ONLY_DYNAMIC, - AND, - OR, - } - - impl InteractionTestMode { - fn to_rapier(self) -> rapier::geometry::InteractionTestMode { - match self { - Self::DEFAULT | Self::AND | Self::ONLY_DYNAMIC => { - rapier::geometry::InteractionTestMode::And - } - Self::OR => rapier::geometry::InteractionTestMode::Or, - } - } - #[allow(dead_code)] - fn from_rapier(t: rapier::geometry::InteractionTestMode) -> Self { - match t { - rapier::geometry::InteractionTestMode::And => Self::DEFAULT, - rapier::geometry::InteractionTestMode::Or => Self::OR, - } - } - } - - // ============================================================ - // InteractionGroups - // ============================================================ - - /// Pair (memberships, filter) used to selectively enable contacts. - /// - /// A collider with memberships M and filter F interacts with another - /// collider (memberships M', filter F') iff - /// `(M & F') != 0 and (M' & F) != 0` (the default `AND` mode). - /// In short: a pair interacts iff each one passes the other's filter. - /// Used for both `collision_groups` and `solver_groups` on a collider. - #[pyclass(name = "InteractionGroups", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct InteractionGroups(pub rapier::geometry::InteractionGroups); - - #[pymethods] - impl InteractionGroups { - /// Build a new interaction-groups value. - /// - /// :param memberships: Which groups this collider belongs to. - /// Defaults to `Group.ALL`. - /// :param filter: Which groups this collider is willing to - /// interact with. Defaults to `Group.ALL`. - /// :param test_mode: How both sides are combined. Defaults to - /// `InteractionTestMode.AND`. - #[new] - #[pyo3(signature = (memberships=None, filter=None, test_mode=None))] - fn new( - memberships: Option, - filter: Option, - test_mode: Option, - ) -> Self { - let m = memberships - .map(|g| g.0) - .unwrap_or(rapier::geometry::Group::ALL); - let f = filter.map(|g| g.0).unwrap_or(rapier::geometry::Group::ALL); - let mode = test_mode - .map(|m| m.to_rapier()) - .unwrap_or(rapier::geometry::InteractionTestMode::And); - Self(rapier::geometry::InteractionGroups::new(m, f, mode)) - } - - /// Interaction groups that match everything (memberships = filter = ALL). - #[staticmethod] - fn all() -> Self { - Self(rapier::geometry::InteractionGroups::all()) - } - /// Interaction groups that match nothing (memberships = filter = NONE). - #[staticmethod] - fn none() -> Self { - Self(rapier::geometry::InteractionGroups::none()) - } - - /// Return a copy with `memberships` replaced. Alias of `with_memberships`. - fn with_membership(&self, g: Group) -> Self { - Self(self.0.with_memberships(g.0)) - } - /// Return a copy with `memberships` replaced. - fn with_memberships(&self, g: Group) -> Self { - Self(self.0.with_memberships(g.0)) - } - /// Return a copy with `filter` replaced. - fn with_filter(&self, g: Group) -> Self { - Self(self.0.with_filter(g.0)) - } - - /// Groups this collider is a member of. - #[getter] - fn memberships(&self) -> Group { - Group(self.0.memberships) - } - #[setter] - fn set_memberships(&mut self, g: Group) { - self.0.memberships = g.0; - } - /// Groups this collider is willing to interact with. - #[getter] - fn filter(&self) -> Group { - Group(self.0.filter) - } - #[setter] - fn set_filter(&mut self, g: Group) { - self.0.filter = g.0; - } - /// How the two pair-wise group checks are combined. - #[getter] - fn test_mode(&self) -> InteractionTestMode { - InteractionTestMode::from_rapier(self.0.test_mode) - } - #[setter] - fn set_test_mode(&mut self, m: InteractionTestMode) { - self.0.test_mode = m.to_rapier(); - } - - /// Test whether `self` and `other` should interact. - /// - /// :returns: True iff both sides pass the other's filter under - /// the active `test_mode`. - fn test(&self, other: &Self) -> bool { - self.0.test(other.0) - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "InteractionGroups supports only == and !=", - )), - } - } - fn __repr__(&self) -> String { - format!( - "InteractionGroups(memberships={:#010x}, filter={:#010x})", - self.0.memberships.bits(), - self.0.filter.bits(), - ) - } - } - - // ============================================================ - // CollisionEventFlags (bitflags) - // ============================================================ - - /// Side-flags attached to each `CollisionEvent`. - /// - /// `SENSOR` indicates the event involves a sensor collider; - /// `REMOVED` indicates the event was emitted because one of the - /// colliders was removed mid-frame. - #[pyclass(name = "CollisionEventFlags", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct CollisionEventFlags(pub rapier::geometry::CollisionEventFlags); - - #[pymethods] - impl CollisionEventFlags { - /// Construct from a raw bit pattern. Unknown bits are dropped. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::geometry::CollisionEventFlags::from_bits_truncate( - bits, - )) - } - /// Return the empty flag set. - #[staticmethod] - fn empty() -> Self { - Self(rapier::geometry::CollisionEventFlags::empty()) - } - #[classattr] - const SENSOR: CollisionEventFlags = - CollisionEventFlags(rapier::geometry::CollisionEventFlags::SENSOR); - #[classattr] - const REMOVED: CollisionEventFlags = - CollisionEventFlags(rapier::geometry::CollisionEventFlags::REMOVED); - - /// Raw bit pattern of the flag set. - #[getter] - fn bits(&self) -> u32 { - self.0.bits() - } - /// True iff all flags in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no flags are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err( - "CollisionEventFlags supports only == and !=", - )), - } - } - fn __repr__(&self) -> String { - format!("CollisionEventFlags(bits={:#010b})", self.0.bits()) - } - } - - // ============================================================ - // TriMeshFlags (bitflags) - // ============================================================ - - /// Build-time flags controlling triangle-mesh preprocessing. - /// - /// Passed to `SharedShape.trimesh(..., flags=...)` or - /// `MeshConverter.trimesh_with_flags(...)`. Toggle topology - /// validation, duplicate cleanup, edge-normal fixups, etc. - #[pyclass(name = "TriMeshFlags", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct TriMeshFlags(pub rapier::parry::shape::TriMeshFlags); - - #[pymethods] - impl TriMeshFlags { - /// Construct from a raw bit pattern. Unknown bits are dropped. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u16) -> Self { - Self(rapier::parry::shape::TriMeshFlags::from_bits_truncate(bits)) - } - /// Return the empty flag set. - #[staticmethod] - fn empty() -> Self { - Self(rapier::parry::shape::TriMeshFlags::empty()) - } - - #[classattr] - const HALF_EDGE_TOPOLOGY: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::HALF_EDGE_TOPOLOGY); - #[classattr] - const CONNECTED_COMPONENTS: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::CONNECTED_COMPONENTS); - #[classattr] - const DELETE_BAD_TOPOLOGY_TRIANGLES: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_BAD_TOPOLOGY_TRIANGLES); - #[classattr] - const ORIENTED: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::ORIENTED); - #[classattr] - const MERGE_DUPLICATE_VERTICES: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::MERGE_DUPLICATE_VERTICES); - #[classattr] - const DELETE_DEGENERATE_TRIANGLES: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_DEGENERATE_TRIANGLES); - #[classattr] - const DELETE_DUPLICATE_TRIANGLES: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::DELETE_DUPLICATE_TRIANGLES); - #[classattr] - const FIX_INTERNAL_EDGES: TriMeshFlags = - TriMeshFlags(rapier::parry::shape::TriMeshFlags::FIX_INTERNAL_EDGES); - - /// Raw bit pattern of the flag set. - #[getter] - fn bits(&self) -> u16 { - self.0.bits() - } - /// True iff all flags in `other` are also set on `self`. - fn contains(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - /// True iff no flags are set. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn __contains__(&self, other: &Self) -> bool { - self.0.contains(other.0) - } - fn __or__(&self, other: &Self) -> Self { - Self(self.0 | other.0) - } - fn __and__(&self, other: &Self) -> Self { - Self(self.0 & other.0) - } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("TriMeshFlags supports only == and !=")), - } - } - fn __repr__(&self) -> String { - format!("TriMeshFlags(bits={:#018b})", self.0.bits()) - } - } - - // ============================================================ - // ColliderMaterial - // ============================================================ - - /// Surface material: friction, restitution, and their combine rules. - /// - /// When two colliders touch, the engine combines their material - /// values using the configured `CoefficientCombineRule` (e.g. - /// `MAX`, `MIN`, `AVERAGE`, `MULTIPLY`). - #[pyclass(name = "ColliderMaterial", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct ColliderMaterial(pub rapier::geometry::ColliderMaterial); - - #[pymethods] - impl ColliderMaterial { - /// :param friction: Friction coefficient. Defaults to 1.0. - /// :param restitution: Restitution (bounciness). Defaults to 0.0. - #[new] - #[pyo3(signature = (friction=1.0 as Real, restitution=0.0 as Real))] - fn new(friction: Real, restitution: Real) -> Self { - Self(rapier::geometry::ColliderMaterial::new( - friction, - restitution, - )) - } - - /// Friction coefficient. - #[getter] - fn friction(&self) -> Real { - self.0.friction - } - #[setter] - fn set_friction(&mut self, v: Real) { - self.0.friction = v; - } - /// Restitution (bounciness) in `[0, 1]`. - #[getter] - fn restitution(&self) -> Real { - self.0.restitution - } - #[setter] - fn set_restitution(&mut self, v: Real) { - self.0.restitution = v; - } - - /// Rule used to combine friction coefficients with another collider. - #[getter] - fn friction_combine_rule(&self) -> CoefficientCombineRule { - CoefficientCombineRule::from_rapier(self.0.friction_combine_rule) - } - #[setter] - fn set_friction_combine_rule(&mut self, v: CoefficientCombineRule) { - self.0.friction_combine_rule = v.to_rapier(); - } - /// Rule used to combine restitution with another collider. - #[getter] - fn restitution_combine_rule(&self) -> CoefficientCombineRule { - CoefficientCombineRule::from_rapier(self.0.restitution_combine_rule) - } - #[setter] - fn set_restitution_combine_rule(&mut self, v: CoefficientCombineRule) { - self.0.restitution_combine_rule = v.to_rapier(); - } - - fn __repr__(&self) -> String { - format!( - "ColliderMaterial(friction={}, restitution={})", - self.0.friction, self.0.restitution, - ) - } - } - - // ============================================================ - // BvhOptimizationStrategy (enum) - // ============================================================ - - /// Strategy used by `BroadPhaseBvh` to keep its bounding hierarchy tidy. - /// - /// `AUTO` lets the engine pick a reasonable default; the other - /// variants force a specific behavior. - #[pyclass(name = "BvhOptimizationStrategy", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum BvhOptimizationStrategy { - AUTO, - NO_OPTIMIZATION, - OPTIMIZE_FOR_INTERACTIONS, - OPTIMIZE_FOR_SPATIAL_QUERIES, - SUBTREE_OPTIMIZER, - NONE, - } - - impl BvhOptimizationStrategy { - fn to_rapier(self) -> rapier::geometry::BvhOptimizationStrategy { - match self { - Self::AUTO - | Self::OPTIMIZE_FOR_INTERACTIONS - | Self::OPTIMIZE_FOR_SPATIAL_QUERIES - | Self::SUBTREE_OPTIMIZER => { - rapier::geometry::BvhOptimizationStrategy::SubtreeOptimizer - } - Self::NO_OPTIMIZATION | Self::NONE => { - rapier::geometry::BvhOptimizationStrategy::None - } - } - } - } - - // ============================================================ - // BroadPhaseBvh - // ============================================================ - - /// BVH-based broad-phase: emits candidate collider pairs each step. - /// - /// Usually accessed via `world.broad_phase`. Create one explicitly - /// only if you are driving the pipeline yourself. - #[pyclass(name = "BroadPhaseBvh", module = "rapier", unsendable)] - pub struct BroadPhaseBvh(pub rapier::geometry::BroadPhaseBvh); - - #[pymethods] - impl BroadPhaseBvh { - /// Construct a broad-phase with the default optimization strategy. - #[new] - fn new() -> Self { - Self(rapier::geometry::BroadPhaseBvh::new()) - } - - /// Construct a broad-phase tuned for `strategy`. - #[staticmethod] - fn optimized_for(strategy: BvhOptimizationStrategy) -> Self { - Self(rapier::geometry::BroadPhaseBvh::with_optimization_strategy( - strategy.to_rapier(), - )) - } - - /// Reset the broad-phase to an empty state. Equivalent to constructing a new one. - fn clear(&mut self) { - self.0 = rapier::geometry::BroadPhaseBvh::new(); - } - } - - // ============================================================ - // NarrowPhase - // ============================================================ - - /// Narrow-phase: computes per-pair contact manifolds and intersections. - /// - /// Usually accessed via `world.narrow_phase`. Use it to query - /// existing contact pairs and intersections between specific - /// colliders. - #[pyclass(name = "NarrowPhase", module = "rapier", unsendable)] - pub struct NarrowPhase(pub rapier::geometry::NarrowPhase); - - #[pymethods] - impl NarrowPhase { - /// Construct an empty narrow-phase. - #[new] - fn new() -> Self { - Self(rapier::geometry::NarrowPhase::new()) - } - - /// Return the contact pair between two colliders, if any. - /// - /// :returns: A snapshot `ContactPair` or `None` if the pair is - /// not currently tracked by the narrow-phase. - fn contact_pair( - &self, - h1: &ColliderHandle, - h2: &ColliderHandle, - ) -> Option { - self.0.contact_pair(h1.0, h2.0).cloned().map(ContactPair) - } - - /// Return whether two sensor/intersection colliders intersect. - /// - /// :returns: `True`/`False` if the pair is tracked, or `None` - /// when no intersection pair is being maintained. - fn intersection_pair(&self, h1: &ColliderHandle, h2: &ColliderHandle) -> Option { - self.0.intersection_pair(h1.0, h2.0) - } - - /// Snapshot every active contact pair as a list. - fn contact_pairs(&self) -> Vec { - self.0.contact_pairs().cloned().map(ContactPair).collect() - } - - /// Snapshot every tracked sensor/intersection pair. - /// - /// :returns: List of `(collider1, collider2, intersecting)` tuples. - fn intersection_pairs(&self) -> Vec<(ColliderHandle, ColliderHandle, bool)> { - self.0 - .intersection_pairs() - .map(|(h1, h2, i)| (ColliderHandle(h1), ColliderHandle(h2), i)) - .collect() - } - - /// Reset the narrow-phase to an empty state. Equivalent to - /// constructing a new one — the existing `NarrowPhase` doesn't - /// expose `clear()` directly. - fn clear(&mut self) { - self.0 = rapier::geometry::NarrowPhase::new(); - } - } - - // ============================================================ - // ColliderPair (named-tuple-like) - // ============================================================ - - /// Ordered pair of ``ColliderHandle`` values, indexable like a tuple. - /// - /// Used by ``BroadPhasePairEvent`` and various filter callbacks. - /// ``pair[0]`` returns ``collider1``, ``pair[1]`` returns ``collider2``. - #[pyclass(name = "ColliderPair", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct ColliderPair { - #[pyo3(get)] - pub collider1: ColliderHandle, - #[pyo3(get)] - pub collider2: ColliderHandle, - } - - #[pymethods] - impl ColliderPair { - /// Build a pair from two collider handles. - #[new] - fn new(collider1: ColliderHandle, collider2: ColliderHandle) -> Self { - Self { - collider1, - collider2, - } - } - fn __repr__(&self) -> String { - format!( - "ColliderPair(collider1={:?}, collider2={:?})", - self.collider1.0.into_raw_parts(), - self.collider2.0.into_raw_parts() - ) - } - fn __getitem__(&self, idx: isize) -> PyResult { - match idx { - 0 | -2 => Ok(self.collider1), - 1 | -1 => Ok(self.collider2), - _ => Err(PyIndexError::new_err("ColliderPair index out of range")), - } - } - fn __len__(&self) -> usize { - 2 - } - } - - // ============================================================ - // BroadPhasePairEvent (tagged enum) - // ============================================================ - - /// Pair-event emitted by the broad-phase when an AABB overlap - /// starts or stops. - /// - /// `added=True` corresponds to "Added"; `added=False` to "Removed". - /// These are coarse, AABB-level events — for actual contacts use - /// `CollisionEvent` and `ContactPair`. - #[pyclass(name = "BroadPhasePairEvent", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct BroadPhasePairEvent { - /// True iff this is an "Added" event; False for "Removed". - #[pyo3(get)] - pub added: bool, - #[pyo3(get)] - pub pair: ColliderPair, - } - - #[pymethods] - impl BroadPhasePairEvent { - /// Build an "Added" pair event. - #[staticmethod] - fn added(pair: ColliderPair) -> Self { - Self { added: true, pair } - } - /// Build a "Removed" pair event. - #[staticmethod] - fn removed(pair: ColliderPair) -> Self { - Self { added: false, pair } - } - /// True iff this is a "Removed" event (the inverse of `added`). - #[getter] - fn removed_(&self) -> bool { - !self.added - } - fn __repr__(&self) -> String { - if self.added { - format!( - "BroadPhasePairEvent.Added({:?})", - self.pair.collider1.0.into_raw_parts() - ) - } else { - format!( - "BroadPhasePairEvent.Removed({:?})", - self.pair.collider1.0.into_raw_parts() - ) - } - } - } - - // ============================================================ - // CollisionEvent (tagged union) - // ============================================================ - - /// Contact-level event flagging a new or ended collision pair. - /// - /// Only emitted for pairs where at least one collider has - /// `ActiveEvents.COLLISION_EVENTS` set. Inspect `started` to - /// distinguish "started" from "stopped", and `flags` for - /// extra context (`SENSOR`, `REMOVED`). - #[pyclass(name = "CollisionEvent", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct CollisionEvent { - #[pyo3(get)] - pub started: bool, - #[pyo3(get)] - pub collider1: ColliderHandle, - #[pyo3(get)] - pub collider2: ColliderHandle, - #[pyo3(get)] - pub flags: CollisionEventFlags, - } - - #[pymethods] - impl CollisionEvent { - /// Construct a "Started" event. - /// - /// Fired when two colliders begin overlapping at the contact - /// level. Requires `ActiveEvents.COLLISION_EVENTS` on at least - /// one of the colliders. - #[staticmethod] - #[pyo3(name = "STARTED")] - fn started_evt( - c1: ColliderHandle, - c2: ColliderHandle, - flags: CollisionEventFlags, - ) -> Self { - Self { - started: true, - collider1: c1, - collider2: c2, - flags, - } - } - /// Construct a "Stopped" event. - /// - /// Fired when two colliders stop overlapping (or one is - /// removed). Requires `ActiveEvents.COLLISION_EVENTS` on at - /// least one of the colliders. - #[staticmethod] - #[pyo3(name = "STOPPED")] - fn stopped_evt( - c1: ColliderHandle, - c2: ColliderHandle, - flags: CollisionEventFlags, - ) -> Self { - Self { - started: false, - collider1: c1, - collider2: c2, - flags, - } - } - - /// True iff this is a "Stopped" event (the inverse of `started`). - #[getter] - fn stopped(&self) -> bool { - !self.started - } - /// True iff the event involves a sensor collider. - #[getter] - fn sensor(&self) -> bool { - self.flags - .0 - .contains(rapier::geometry::CollisionEventFlags::SENSOR) - } - /// True iff the event was triggered by a collider removal. - #[getter] - fn removed(&self) -> bool { - self.flags - .0 - .contains(rapier::geometry::CollisionEventFlags::REMOVED) - } - - fn __repr__(&self) -> String { - let kind = if self.started { "Started" } else { "Stopped" }; - format!( - "CollisionEvent.{kind}(c1={:?}, c2={:?})", - self.collider1.0.into_raw_parts(), - self.collider2.0.into_raw_parts(), - ) - } - } - - impl CollisionEvent { - #[allow(dead_code)] - pub(crate) fn from_rapier(e: rapier::geometry::CollisionEvent) -> Self { - match e { - rapier::geometry::CollisionEvent::Started(c1, c2, f) => Self { - started: true, - collider1: ColliderHandle(c1), - collider2: ColliderHandle(c2), - flags: CollisionEventFlags(f), - }, - rapier::geometry::CollisionEvent::Stopped(c1, c2, f) => Self { - started: false, - collider1: ColliderHandle(c1), - collider2: ColliderHandle(c2), - flags: CollisionEventFlags(f), - }, - } - } - } - - // ============================================================ - // ColliderFlags (struct-of-fields, not a bitflags wrapper — - // mirrors rapier's `ColliderFlags` struct exactly). - // ============================================================ - - /// Bundle of the configurable flags on a `Collider`. - /// - /// Aggregates `collision_groups`, `solver_groups`, `active_hooks`, - /// `active_events`, and the read-only `enabled` state. This is a - /// value type — mutating it does not retroactively affect the - /// collider it was read from. - #[pyclass(name = "ColliderFlags", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct ColliderFlags(pub rapier::geometry::ColliderFlags); - - #[pymethods] - impl ColliderFlags { - /// Construct with default values (all groups allowed, no hooks/events). - #[new] - fn new() -> Self { - Self(rapier::geometry::ColliderFlags::default()) - } - - /// Groups controlling which colliders generate contact pairs. - #[getter] - fn collision_groups(&self) -> InteractionGroups { - InteractionGroups(self.0.collision_groups) - } - #[setter] - fn set_collision_groups(&mut self, v: InteractionGroups) { - self.0.collision_groups = v.0; - } - /// Groups controlling which contact pairs reach the solver. - #[getter] - fn solver_groups(&self) -> InteractionGroups { - InteractionGroups(self.0.solver_groups) - } - #[setter] - fn set_solver_groups(&mut self, v: InteractionGroups) { - self.0.solver_groups = v.0; - } - /// Hooks this collider opts into (see `ActiveHooks`). - #[getter] - fn active_hooks(&self) -> ActiveHooks { - ActiveHooks(self.0.active_hooks) - } - #[setter] - fn set_active_hooks(&mut self, v: ActiveHooks) { - self.0.active_hooks = v.0; - } - /// Rigid-body type combinations enabled (see `ActiveCollisionTypes`). - #[getter] - fn active_collision_types(&self) -> ActiveCollisionTypes { - ActiveCollisionTypes(self.0.active_collision_types) - } - #[setter] - fn set_active_collision_types(&mut self, v: ActiveCollisionTypes) { - self.0.active_collision_types = v.0; - } - /// Events this collider opts into (see `ActiveEvents`). - #[getter] - fn active_events(&self) -> ActiveEvents { - ActiveEvents(self.0.active_events) - } - #[setter] - fn set_active_events(&mut self, v: ActiveEvents) { - self.0.active_events = v.0; - } - /// Current enabled state. Read-only mirror of `Collider.is_enabled`. - #[getter] - fn enabled(&self) -> ColliderEnabled { - ColliderEnabled::from_rapier(self.0.enabled) - } - } - }; -} - -// ============================================================ -// 3D-specific geometry types -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_3d { - () => { - $crate::__define_geometry_shapes_common!(3, Vec3, Point3, Isometry3); - - // ---- 3D-only shape views ---- - - /// Cylinder shape view, axis-aligned to Y. - /// - /// Inspection-only — typically obtained via `shape.as_cylinder()`. - #[pyclass(name = "Cylinder", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Cylinder(pub rapier::parry::shape::Cylinder); - - #[pymethods] - impl Cylinder { - /// Construct a cylinder of given half-height (along Y) and radius. - #[new] - fn new(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::Cylinder::new(half_height, radius)) - } - /// Half-height of the cylinder along the Y axis. - #[getter] - fn half_height(&self) -> Real { - self.0.half_height - } - /// Cap radius. - #[getter] - fn radius(&self) -> Real { - self.0.radius - } - fn __repr__(&self) -> String { - format!( - "Cylinder(half_height={}, radius={})", - self.0.half_height, self.0.radius - ) - } - } - - /// Cone shape view, apex along +Y. - /// - /// Inspection-only — typically obtained via `shape.as_cone()`. - #[pyclass(name = "Cone", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Cone(pub rapier::parry::shape::Cone); - - #[pymethods] - impl Cone { - /// Construct a cone of given half-height (along Y) and base radius. - #[new] - fn new(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::Cone::new(half_height, radius)) - } - /// Half-height along the Y axis (apex at +half_height, base at -half_height). - #[getter] - fn half_height(&self) -> Real { - self.0.half_height - } - /// Base radius. - #[getter] - fn radius(&self) -> Real { - self.0.radius - } - fn __repr__(&self) -> String { - format!( - "Cone(half_height={}, radius={})", - self.0.half_height, self.0.radius - ) - } - } - - /// Convex polyhedron shape view. - /// - /// Inspection-only — typically obtained via - /// `shape.as_convex_polyhedron()`. - #[pyclass(name = "ConvexPolyhedron", module = "rapier")] - #[derive(Clone)] - pub struct ConvexPolyhedron(pub rapier::parry::shape::ConvexPolyhedron); - - #[pymethods] - impl ConvexPolyhedron { - /// Vertices of the polyhedron as an `(N, 3)` ndarray. - #[getter] - fn points<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let pts = self.0.points(); - let n = pts.len(); - let mut flat = Vec::with_capacity(n * 3); - for p in pts { - flat.extend_from_slice(&[p.x, p.y, p.z]); - } - PyArray2::from_vec2_bound( - py, - &flat.chunks(3).map(|c| c.to_vec()).collect::>(), - ) - .expect("contiguous 2D ndarray") - } - /// Number of vertices. - fn num_points(&self) -> usize { - self.0.points().len() - } - - /// Triangulated face indices as an `(M, 3)` ndarray of `u32`. - /// - /// Each (convex) face is fan-triangulated; the indices reference - /// rows of `points` and tile the polyhedron's surface. This lets a - /// renderer build the actual mesh straight from parry's topology - /// instead of recomputing a convex hull. - #[getter] - fn indices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let adj = self.0.vertices_adj_to_face(); - let mut tris: Vec> = Vec::new(); - for face in self.0.faces() { - let first = face.first_vertex_or_edge as usize; - let n = face.num_vertices_or_edges as usize; - if n < 3 { - continue; - } - let v0 = adj[first]; - for i in 1..n - 1 { - tris.push(vec![v0, adj[first + i], adj[first + i + 1]]); - } - } - PyArray2::from_vec2_bound(py, &tris).expect("contiguous 2D ndarray") - } - } - - // ---- 3D HeightField additional accessor: heights as (nrows, ncols) ndarray ---- - - #[pymethods] - impl HeightField { - /// Height samples as an `(nrows, ncols)` float ndarray. - /// - /// World-space vertex positions are - /// `(x * scale.x, height[i,j] * scale.y, z * scale.z)` - /// where `x`/`z` are evenly-spaced in `[-0.5, 0.5]`. - #[getter] - fn heights<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray2> { - let h = self.0.heights(); - let nrows = h.nrows(); - let ncols = h.ncols(); - let rows: Vec> = (0..nrows) - .map(|i| (0..ncols).map(|j| h[(i, j)]).collect()) - .collect(); - $crate::numpy::PyArray2::from_vec2_bound(py, &rows).expect("contiguous 2D ndarray") - } - /// Number of rows in the height grid (along the X axis). - #[getter] - fn nrows(&self) -> usize { - self.0.heights().nrows() - } - /// Number of columns in the height grid (along the Z axis). - #[getter] - fn ncols(&self) -> usize { - self.0.heights().ncols() - } - } - - // ---- ContactData (3D — tangent_impulse is 2 floats) ---- - - /// Single contact point inside a `ContactManifold`. - /// - /// Read-only snapshot. Points are expressed in each collider's - /// local frame; `dist` is the separation (negative if penetrating); - /// `impulse` is the normal impulse computed by the solver and - /// `tangent_impulse` is the friction impulse along the two - /// tangent directions of the manifold's frame. - #[pyclass(name = "ContactData", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct ContactData { - #[pyo3(get)] - pub local_p1: Point3, - #[pyo3(get)] - pub local_p2: Point3, - #[pyo3(get)] - pub dist: Real, - #[pyo3(get)] - pub fid1: u32, - #[pyo3(get)] - pub fid2: u32, - #[pyo3(get)] - pub impulse: Real, - #[pyo3(get)] - pub tangent_impulse: (Real, Real), - #[pyo3(get)] - pub contact_id: u32, - } - - #[pymethods] - impl ContactData { - fn __repr__(&self) -> String { - format!("ContactData(dist={}, impulse={})", self.dist, self.impulse) - } - } - - // ---- SolverContact view ---- - - /// One contact prepared for the constraints solver. - /// - /// Read-only snapshot. `point` is in world coordinates; - /// `friction`/`restitution` are the per-contact combined material - /// values; `is_new` is true when the contact is freshly generated - /// this step. - #[pyclass(name = "SolverContact", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct SolverContact { - #[pyo3(get)] - pub point: Point3, - #[pyo3(get)] - pub dist: Real, - #[pyo3(get)] - pub friction: Real, - #[pyo3(get)] - pub restitution: Real, - #[pyo3(get)] - pub contact_id: u32, - #[pyo3(get)] - pub is_new: bool, - } - }; -} - -// ============================================================ -// 2D-specific geometry types -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_2d { - () => { - $crate::__define_geometry_shapes_common!(2, Vec2, Point2, Isometry2); - - // ---- 2D HeightField additional accessor: heights as 1-D ndarray ---- - - #[pymethods] - impl HeightField { - /// Height samples as a 1-D float ndarray of length `nheights`. - #[getter] - fn heights<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray1> { - let h = self.0.heights(); - $crate::numpy::PyArray1::from_iter_bound(py, h.iter().copied()) - } - /// Number of height samples. - #[getter] - fn nheights(&self) -> usize { - self.0.heights().len() - } - } - - // ---- 2D-only shape views ---- - - /// 2D segment between two endpoints. - /// - /// Inspection-only — typically obtained via `shape.as_segment()`. - #[pyclass(name = "Segment", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Segment(pub rapier::parry::shape::Segment); - - #[pymethods] - impl Segment { - /// Build a segment from two endpoints. - #[new] - fn new(a: PyVector, b: PyVector) -> Self { - Self(rapier::parry::shape::Segment::new(a.0.into(), b.0.into())) - } - /// First endpoint. - #[getter] - fn a(&self) -> Point2 { - let v: $crate::na::Vector2 = self.0.a.into(); - Point2($crate::na::Point2::from(v)) - } - /// Second endpoint. - #[getter] - fn b(&self) -> Point2 { - let v: $crate::na::Vector2 = self.0.b.into(); - Point2($crate::na::Point2::from(v)) - } - fn __repr__(&self) -> String { - format!("Segment(a={:?}, b={:?})", self.0.a, self.0.b) - } - } - - /// 2D polyline shape view. - /// - /// Inspection-only — typically obtained via `shape.as_polyline()`. - #[pyclass(name = "Polyline", module = "rapier")] - #[derive(Clone)] - pub struct Polyline(pub rapier::parry::shape::Polyline); - - #[pymethods] - impl Polyline { - /// Vertices as an `(N, 2)` ndarray. - #[getter] - fn vertices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let pts = self.0.vertices(); - let v: Vec> = pts.iter().map(|p| vec![p.x, p.y]).collect(); - PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - - /// Segment indices as an `(M, 2)` ndarray of `u32`. - #[getter] - fn indices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let idx = self.0.indices(); - let v: Vec> = idx.iter().map(|c| c.to_vec()).collect(); - PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - - /// Number of segments. - fn num_segments(&self) -> usize { - self.0.num_segments() - } - } - - /// Convex polygon shape view. - /// - /// Inspection-only — typically obtained via - /// `shape.as_convex_polygon()`. - #[pyclass(name = "ConvexPolygon", module = "rapier")] - #[derive(Clone)] - pub struct ConvexPolygon(pub rapier::parry::shape::ConvexPolygon); - - #[pymethods] - impl ConvexPolygon { - /// Polygon vertices as an `(N, 2)` ndarray. - #[getter] - fn points<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let pts = self.0.points(); - let v: Vec> = pts.iter().map(|p| vec![p.x, p.y]).collect(); - PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - /// Number of vertices. - fn num_points(&self) -> usize { - self.0.points().len() - } - } - - // ---- ContactData (2D — tangent_impulse is 1 float) ---- - - /// Single contact point inside a 2D `ContactManifold`. - /// - /// Read-only snapshot. Points are expressed in each collider's - /// local frame; `dist` is the separation (negative if penetrating); - /// `impulse` is the normal impulse computed by the solver and - /// `tangent_impulse` is the (scalar) friction impulse along the - /// tangent direction. - #[pyclass(name = "ContactData", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct ContactData { - #[pyo3(get)] - pub local_p1: Point2, - #[pyo3(get)] - pub local_p2: Point2, - #[pyo3(get)] - pub dist: Real, - #[pyo3(get)] - pub fid1: u32, - #[pyo3(get)] - pub fid2: u32, - #[pyo3(get)] - pub impulse: Real, - #[pyo3(get)] - pub tangent_impulse: Real, - #[pyo3(get)] - pub contact_id: u32, - } - - #[pymethods] - impl ContactData { - fn __repr__(&self) -> String { - format!("ContactData(dist={}, impulse={})", self.dist, self.impulse) - } - } - - /// One contact prepared for the 2D constraints solver. - /// - /// Read-only snapshot. `point` is in world coordinates; - /// `friction`/`restitution` are the per-contact combined material - /// values; `is_new` is true when the contact is freshly generated - /// this step. - #[pyclass(name = "SolverContact", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct SolverContact { - #[pyo3(get)] - pub point: Point2, - #[pyo3(get)] - pub dist: Real, - #[pyo3(get)] - pub friction: Real, - #[pyo3(get)] - pub restitution: Real, - #[pyo3(get)] - pub contact_id: u32, - #[pyo3(get)] - pub is_new: bool, - } - }; -} - -// ============================================================ -// Dim-agnostic shape views (Ball, Cuboid, Capsule, Triangle, -// TriMesh, HeightField, Compound) + MeshConverter + -// ContactManifold + ContactManifoldData + ContactPair + IntersectionPair -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_shapes_common { - ($dim:tt, $Vec:ident, $Point:ident, $Iso:ident) => { - // `PyArrayMethods` is already brought in by `define_math_types!` so we - // only need the new traits here. - use $crate::numpy::{PyArray2, PyUntypedArrayMethods}; - - // Dim+scalar-specific helpers that pick the right ndarray extractor - // and convert into the engine's `Vector` type. - $crate::__define_geometry_extract_verts_helper!($dim); - - // ============================================================ - // Ball - // ============================================================ - - /// Sphere (3D) / disk (2D) shape view. - /// - /// Inspection-only — typically obtained via `shape.as_ball()`. - #[pyclass(name = "Ball", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Ball(pub rapier::parry::shape::Ball); - - #[pymethods] - impl Ball { - /// Build a ball of the given radius. - #[new] - fn new(radius: Real) -> Self { - Self(rapier::parry::shape::Ball::new(radius)) - } - /// Radius. - #[getter] - fn radius(&self) -> Real { - self.0.radius - } - fn __repr__(&self) -> String { - format!("Ball(radius={})", self.0.radius) - } - } - - // ============================================================ - // Cuboid - // ============================================================ - - /// Axis-aligned box (3D) / rectangle (2D) shape view. - /// - /// Inspection-only — typically obtained via `shape.as_cuboid()`. - /// The cuboid spans `[-half_extents, +half_extents]` in its local frame. - #[pyclass(name = "Cuboid", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Cuboid(pub rapier::parry::shape::Cuboid); - - #[pymethods] - impl Cuboid { - /// Build a cuboid from its half-extents vector. - #[new] - fn new(half_extents: PyVector) -> Self { - Self(rapier::parry::shape::Cuboid::new(half_extents.0.into())) - } - /// Half-extents along each local axis. - #[getter] - fn half_extents(&self) -> $Vec { - let v: $crate::na::SVector = self.0.half_extents.into(); - $Vec(v) - } - fn __repr__(&self) -> String { - format!("Cuboid(half_extents={:?})", self.0.half_extents) - } - } - - // ============================================================ - // Capsule - // ============================================================ - - /// Capsule shape view (a swept sphere between two endpoints). - /// - /// Inspection-only — typically obtained via `shape.as_capsule()`. - /// Construct via the explicit `(a, b, radius)` form or via the - /// `SharedShape.capsule_x` / `capsule_y` / `capsule_z` factories - /// to align it with a specific axis. - #[pyclass(name = "Capsule", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Capsule(pub rapier::parry::shape::Capsule); - - #[pymethods] - impl Capsule { - /// Build a capsule from its two endpoints and a radius. - #[new] - fn new(a: PyVector, b: PyVector, radius: Real) -> Self { - Self(rapier::parry::shape::Capsule::new( - a.0.into(), - b.0.into(), - radius, - )) - } - /// Radius of the spherical caps. - #[getter] - fn radius(&self) -> Real { - self.0.radius - } - /// Half the length of the inner segment. - #[getter] - fn half_height(&self) -> Real { - self.0.half_height() - } - /// Length of the inner segment. - #[getter] - fn height(&self) -> Real { - self.0.height() - } - /// First endpoint of the inner segment. - #[getter] - fn a(&self) -> $Point { - let v: $crate::na::SVector = self.0.segment.a.into(); - $Point($crate::na::Point::from(v)) - } - /// Second endpoint of the inner segment. - #[getter] - fn b(&self) -> $Point { - let v: $crate::na::SVector = self.0.segment.b.into(); - $Point($crate::na::Point::from(v)) - } - fn __repr__(&self) -> String { - format!( - "Capsule(half_height={}, radius={})", - self.0.half_height(), - self.0.radius - ) - } - } - - // ============================================================ - // Triangle - // ============================================================ - - /// Triangle shape view. - /// - /// Inspection-only — typically obtained via `shape.as_triangle()`. - #[pyclass(name = "Triangle", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Triangle(pub rapier::parry::shape::Triangle); - - #[pymethods] - impl Triangle { - /// Build a triangle from its three vertices. - #[new] - fn new(a: PyVector, b: PyVector, c: PyVector) -> Self { - Self(rapier::parry::shape::Triangle::new( - a.0.into(), - b.0.into(), - c.0.into(), - )) - } - /// First vertex. - #[getter] - fn a(&self) -> $Point { - let v: $crate::na::SVector = self.0.a.into(); - $Point($crate::na::Point::from(v)) - } - /// Second vertex. - #[getter] - fn b(&self) -> $Point { - let v: $crate::na::SVector = self.0.b.into(); - $Point($crate::na::Point::from(v)) - } - /// Third vertex. - #[getter] - fn c(&self) -> $Point { - let v: $crate::na::SVector = self.0.c.into(); - $Point($crate::na::Point::from(v)) - } - } - - // ============================================================ - // TriMesh view - // ============================================================ - - /// Triangle mesh shape view. - /// - /// Inspection-only — typically obtained via `shape.as_trimesh()`. - /// Construct new tri-meshes via `SharedShape.trimesh(...)`. - #[pyclass(name = "TriMesh", module = "rapier")] - #[derive(Clone)] - pub struct TriMesh(pub rapier::parry::shape::TriMesh); - - #[pymethods] - impl TriMesh { - /// Vertices as an `(N, D)` ndarray. - #[getter] - fn vertices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let vs = self.0.vertices(); - let v: Vec> = vs - .iter() - .map(|p| { - let arr: [Real; $dim] = (*p).into(); - arr.to_vec() - }) - .collect(); - PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - /// Triangle indices as an `(M, 3)` ndarray of `u32`. - #[getter] - fn indices<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { - let idx = self.0.indices(); - let v: Vec> = idx.iter().map(|c| c.to_vec()).collect(); - PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - /// Build-time preprocessing flags this mesh was created with. - #[getter] - fn flags(&self) -> TriMeshFlags { - TriMeshFlags(self.0.flags()) - } - /// Number of triangles. - fn num_triangles(&self) -> usize { - self.0.indices().len() - } - /// Number of vertices. - fn num_vertices(&self) -> usize { - self.0.vertices().len() - } - } - - // ============================================================ - // Voxels (voxel-grid shape) - // ============================================================ - - /// Voxel-grid shape view. - /// - /// Inspection-only — typically obtained via `shape.as_voxels()`. - /// Construct one with `SharedShape.voxels(...)` / - /// `SharedShape.voxels_from_points(...)` (or the matching - /// `Collider.*` builders). - #[pyclass(name = "Voxels", module = "rapier")] - #[derive(Clone)] - pub struct Voxels(pub rapier::parry::shape::Voxels); - - #[pymethods] - impl Voxels { - /// Per-axis size of one voxel cell, as a `(D,)` ndarray. - #[getter] - fn voxel_size<'py>( - &self, - py: Python<'py>, - ) -> Bound<'py, $crate::numpy::PyArray1> { - let s = self.0.voxel_size(); - let arr: [Real; $dim] = s.into(); - $crate::numpy::PyArray1::from_iter_bound(py, arr.into_iter()) - } - - /// Centers of all **filled** (non-empty) voxels, as an `(M, D)` - /// ndarray. Building one cuboid (of size `voxel_size`) per center - /// reconstructs the solid; empty cells are skipped. - #[getter] - fn centers<'py>(&self, py: Python<'py>) -> Bound<'py, $crate::numpy::PyArray2> { - let v: Vec> = self - .0 - .voxels() - .filter(|vox| !vox.state.is_empty()) - .map(|vox| { - let arr: [Real; $dim] = vox.center.into(); - arr.to_vec() - }) - .collect(); - $crate::numpy::PyArray2::from_vec2_bound(py, &v).expect("contiguous ndarray") - } - - /// Number of filled (non-empty) voxels. - fn num_voxels(&self) -> usize { - self.0.voxels().filter(|vox| !vox.state.is_empty()).count() - } - } - - // ============================================================ - // HeightField (2D: 1-D heights; 3D: 2-D heights) - // ============================================================ - - /// Heightfield shape view. - /// - /// Inspection-only — typically obtained via `shape.as_heightfield()`. - /// In 2D the heights are a 1-D array; in 3D they are 2-D. Use - /// `SharedShape.heightfield(...)` to construct one. - #[pyclass(name = "HeightField", module = "rapier")] - #[derive(Clone)] - pub struct HeightField(pub rapier::parry::shape::HeightField); - - #[pymethods] - impl HeightField { - /// Per-axis scale applied to the height samples. - #[getter] - fn scale(&self) -> $Vec { - let v: $crate::na::SVector = self.0.scale().into(); - $Vec(v) - } - } - - // ============================================================ - // Compound - // ============================================================ - - /// Compound shape view — a collection of sub-shapes with local poses. - /// - /// Inspection-only — typically obtained via `shape.as_compound()`. - /// Build one via `SharedShape.compound(...)`. - #[pyclass(name = "Compound", module = "rapier")] - #[derive(Clone)] - pub struct Compound(pub rapier::parry::shape::Compound); - - #[pymethods] - impl Compound { - /// Number of sub-shapes. - fn num_shapes(&self) -> usize { - self.0.shapes().len() - } - - /// Returns the list of `(pose, sub_shape)` parts. - fn shapes(&self) -> Vec<($Iso, SharedShape)> { - self.0 - .shapes() - .iter() - .map(|(pose, shape)| { - let iso: $crate::na::Isometry = (*pose).into(); - ($Iso(iso), SharedShape(shape.clone())) - }) - .collect() - } - } - - // ============================================================ - // SharedShape - // ============================================================ - - /// Reference-counted handle to any collision shape. - /// - /// `SharedShape` is the polymorphic type carried by every `Collider`. - /// Use the `ball`, `cuboid`, `capsule`, `trimesh`, ... static - /// constructors to build one, then downcast with `as_ball`, - /// `as_cuboid`, ... to inspect the concrete geometry. Cheap to - /// clone (it's an `Arc`). - #[pyclass(name = "SharedShape", module = "rapier")] - #[derive(Clone)] - pub struct SharedShape(pub rapier::parry::shape::SharedShape); - - #[pymethods] - impl SharedShape { - /// Discriminant identifying the concrete shape type. - #[getter] - fn shape_type(&self) -> ShapeType { - ShapeType::from_rapier(self.0.shape_type()) - } - - /// Build a ball / sphere shape. - #[staticmethod] - fn ball(radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::ball(radius)) - } - - /// Build a capsule from two endpoints and a radius. - /// - /// For axis-aligned capsules, prefer `capsule_x`, `capsule_y`, - /// or `capsule_z`. - #[staticmethod] - fn capsule(a: PyVector, b: PyVector, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule( - a.0.into(), - b.0.into(), - radius, - )) - } - - /// Build a triangle from its three vertices. - #[staticmethod] - fn triangle(a: PyVector, b: PyVector, c: PyVector) -> Self { - Self(rapier::parry::shape::SharedShape::triangle( - a.0.into(), - b.0.into(), - c.0.into(), - )) - } - - /// Build a half-space (infinite plane) shape from its outward normal. - /// - /// The half-space is the unbounded region lying on the negative - /// side of the plane through the origin with the given outward - /// normal — i.e. the set of points ``p`` with - /// ``p · outward_normal <= 0``. Commonly used as an immovable - /// ground or wall. The normal is normalized automatically. - #[staticmethod] - fn halfspace(outward_normal: PyVector) -> Self { - let n = outward_normal.0.normalize(); - Self(rapier::parry::shape::SharedShape::halfspace(n.into())) - } - - /// Build a triangle with rounded edges of radius `border_radius`. - #[staticmethod] - fn round_triangle(a: PyVector, b: PyVector, c: PyVector, border_radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::round_triangle( - a.0.into(), - b.0.into(), - c.0.into(), - border_radius, - )) - } - - /// Build the convex hull of a point cloud, dilated by - /// `border_radius` (rounded corners/edges). - /// - /// :raises MeshConversionError: If the hull cannot be built. - #[staticmethod] - fn round_convex_hull(points: &Bound<'_, PyAny>, border_radius: Real) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::round_convex_hull(&pts, border_radius) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex hull computation failed", - ) - }) - } - - /// Build a voxel-grid shape by voxelizing a point cloud. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param points: `(N, D)` ndarray of floats; each point marks the - /// voxel that contains it as filled. - #[staticmethod] - fn voxels_from_points( - voxel_size: PyVector, - points: &Bound<'_, PyAny>, - ) -> PyResult { - let pts = extract_verts_for_dim(points)?; - Ok(Self(rapier::parry::shape::SharedShape::voxels_from_points( - voxel_size.0.into(), - &pts, - ))) - } - - /// Build a compound shape from a list of `(pose, sub_shape)` pairs. - #[staticmethod] - fn compound(parts: Vec<(PyIsometry, SharedShape)>) -> Self { - let parts: Vec<(rapier::math::Pose, rapier::parry::shape::SharedShape)> = - parts.into_iter().map(|(p, s)| (p.0.into(), s.0)).collect(); - Self(rapier::parry::shape::SharedShape::compound(parts)) - } - - /// Compute the world-space AABB enclosing this shape when - /// placed at `pose`. - fn compute_aabb(&self, pose: PyIsometry) -> Aabb { - let p: rapier::math::Pose = pose.0.into(); - Aabb(self.0.compute_aabb(&p)) - } - - /// Compute the world-space bounding sphere enclosing this shape - /// when placed at `pose`. - fn compute_bounding_sphere(&self, pose: PyIsometry) -> BoundingSphere { - let p: rapier::math::Pose = pose.0.into(); - BoundingSphere(self.0.compute_bounding_sphere(&p)) - } - - /// Compute mass properties (mass, center of mass, inertia - /// tensor) for this shape at the given uniform density. - fn compute_mass_properties(&self, density: Real) -> MassProperties { - MassProperties(self.0.mass_properties(density)) - } - /// Alias of `compute_mass_properties`. - fn mass_properties(&self, density: Real) -> MassProperties { - MassProperties(self.0.mass_properties(density)) - } - - /// Downcast to `Ball` if this is a ball shape. - fn as_ball(&self) -> Option { - self.0.as_ball().map(|s| Ball(*s)) - } - /// Downcast to `Cuboid` if this is a cuboid shape. - fn as_cuboid(&self) -> Option { - self.0.as_cuboid().map(|s| Cuboid(*s)) - } - /// Downcast to `Capsule` if this is a capsule shape. - fn as_capsule(&self) -> Option { - self.0.as_capsule().map(|s| Capsule(*s)) - } - /// Downcast to `Triangle` if this is a triangle shape. - fn as_triangle(&self) -> Option { - self.0.as_triangle().map(|s| Triangle(*s)) - } - /// Downcast to `TriMesh` if this is a triangle mesh shape. - fn as_trimesh(&self) -> Option { - self.0.as_trimesh().map(|s| TriMesh(s.clone())) - } - /// Downcast to `HeightField` if this is a heightfield shape. - fn as_heightfield(&self) -> Option { - self.0.as_heightfield().map(|s| HeightField(s.clone())) - } - /// Downcast to `Compound` if this is a compound shape. - fn as_compound(&self) -> Option { - self.0.as_compound().map(|s| Compound(s.clone())) - } - /// Downcast to `Voxels` if this is a voxel-grid shape. - fn as_voxels(&self) -> Option { - self.0.as_voxels().map(|s| Voxels(s.clone())) - } - - /// Downcast to `Cuboid` if this is a *rounded* cuboid shape. - /// - /// Returns the inner (unrounded) `Cuboid`; the border radius is - /// discarded. Use `shape_type` to detect the round variant. - fn as_round_cuboid(&self) -> Option { - self.0.as_round_cuboid().map(|s| Cuboid(s.inner_shape)) - } - /// Downcast to `Triangle` if this is a *rounded* triangle shape. - /// - /// Returns the inner (unrounded) `Triangle`; the border radius - /// is discarded. - fn as_round_triangle(&self) -> Option { - self.0.as_round_triangle().map(|s| Triangle(s.inner_shape)) - } - - fn __repr__(&self) -> String { - format!("SharedShape(type={:?})", self.0.shape_type()) - } - } - - // Dim-specific SharedShape statics as a separate pymethods block. - $crate::__define_geometry_sharedshape_dim_specific!($dim); - - // ============================================================ - // Aabb (small wrapper over parry::bounding_volume::Aabb) - // ============================================================ - - /// Axis-aligned bounding box. - /// - /// Stored as a `(mins, maxs)` pair in world coordinates. Returned - /// by `Collider.compute_aabb()` and `SharedShape.compute_aabb()`. - #[pyclass(name = "Aabb", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Aabb(pub rapier::parry::bounding_volume::Aabb); - - #[pymethods] - impl Aabb { - /// Build an AABB from its `mins` and `maxs` corners. - #[new] - fn new(mins: PyVector, maxs: PyVector) -> Self { - let mins: rapier::math::Vector = mins.0.into(); - let maxs: rapier::math::Vector = maxs.0.into(); - Self(rapier::parry::bounding_volume::Aabb::new( - mins.into(), - maxs.into(), - )) - } - /// Lower corner. - #[getter] - fn mins(&self) -> $Point { - let v: $crate::na::SVector = self.0.mins.into(); - $Point($crate::na::Point::from(v)) - } - /// Upper corner. - #[getter] - fn maxs(&self) -> $Point { - let v: $crate::na::SVector = self.0.maxs.into(); - $Point($crate::na::Point::from(v)) - } - /// Geometric center of the AABB. - #[getter] - fn center(&self) -> $Point { - let v: $crate::na::SVector = self.0.center().into(); - $Point($crate::na::Point::from(v)) - } - /// Half-extents along each axis. - #[getter] - fn half_extents(&self) -> $Vec { - let v: $crate::na::SVector = self.0.half_extents().into(); - $Vec(v) - } - /// Volume (3D) or area (2D) of the AABB. - #[getter] - fn volume(&self) -> Real { - self.0.volume() - } - } - - // ============================================================ - // BoundingSphere - // ============================================================ - - /// Bounding sphere defined by a center and radius. - /// - /// Returned by `SharedShape.compute_bounding_sphere()`. - #[pyclass(name = "BoundingSphere", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct BoundingSphere(pub rapier::parry::bounding_volume::BoundingSphere); - - #[pymethods] - impl BoundingSphere { - /// Build a bounding sphere from its `center` and `radius`. - #[new] - fn new(center: PyVector, radius: Real) -> Self { - let c: rapier::math::Vector = center.0.into(); - Self(rapier::parry::bounding_volume::BoundingSphere { center: c, radius }) - } - /// Center of the sphere. - #[getter] - fn center(&self) -> $Point { - let v: $crate::na::SVector = self.0.center.into(); - $Point($crate::na::Point::from(v)) - } - /// Radius of the sphere. - #[getter] - fn radius(&self) -> Real { - self.0.radius - } - } - - // ============================================================ - // MeshConverter (tagged-union-ish; one variant constructor at a time) - // ============================================================ - - /// Strategy for turning a `(vertices, indices)` buffer pair into a - /// `SharedShape`. - /// - /// Picks the conversion variant via the class attributes - /// (`TRIMESH`, `OBB`, `BOUNDING_BOX`, `CONVEX_HULL`, `COMPOUND`) or - /// the `trimesh_with_flags(...)` factory, then call `build()`. - #[pyclass(name = "MeshConverter", module = "rapier")] - #[derive(Clone)] - pub struct MeshConverter(pub rapier::geometry::MeshConverter); - - #[pymethods] - impl MeshConverter { - /// Convert vertices/indices into a `TriMesh` shape. - #[classattr] - fn TRIMESH() -> Self { - Self(rapier::geometry::MeshConverter::TriMesh) - } - /// Convert into a tightly-fitting oriented bounding box (`Cuboid`). - #[classattr] - fn OBB() -> Self { - Self(rapier::geometry::MeshConverter::Obb) - } - /// Convert into an axis-aligned bounding box (`Cuboid`). - #[classattr] - fn BOUNDING_BOX() -> Self { - Self(rapier::geometry::MeshConverter::Aabb) - } - /// Convert into a convex hull of the input vertices. - #[classattr] - fn CONVEX_HULL() -> Self { - Self(rapier::geometry::MeshConverter::ConvexHull) - } - - /// Convert into a `TriMesh` using the provided preprocessing - /// `flags`. - #[staticmethod] - fn trimesh_with_flags(flags: TriMeshFlags) -> Self { - Self(rapier::geometry::MeshConverter::TriMeshWithFlags(flags.0)) - } - - /// Build a `SharedShape` from a (vertices, indices) buffer pair. - /// - /// `vertices` is a (N, D) ndarray of floats; `indices` is a (M, 3) - /// ndarray of u32. Raises `MeshConversionError` on failure. - fn build( - &self, - py: Python<'_>, - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - ) -> PyResult<(SharedShape, $Iso)> { - let _ = py; - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - let result = self - .0 - .convert(verts, idx) - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e}")))?; - let iso: $crate::na::Isometry = result.1.into(); - Ok((SharedShape(result.0), $Iso(iso))) - } - } - - $crate::__define_meshconverter_dim_specific!($dim); - - // ============================================================ - // ContactManifoldData - // ============================================================ - - /// Per-manifold metadata accompanying a `ContactManifold`. - /// - /// Read-only view. Holds the parent rigid-body handles (if any), - /// the world-space contact normal, the number of solver-active - /// contacts, the relative dominance used by the solver, and a - /// user-data payload. - #[pyclass(name = "ContactManifoldData", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct ContactManifoldData { - #[pyo3(get)] - pub rigid_body1: Option, - #[pyo3(get)] - pub rigid_body2: Option, - #[pyo3(get)] - pub normal: $Vec, - #[pyo3(get)] - pub num_active_contacts: usize, - #[pyo3(get)] - pub relative_dominance: i16, - #[pyo3(get)] - pub user_data: u32, - } - - // ============================================================ - // ContactManifold - // ============================================================ - - /// Set of contact points sharing a common normal between two colliders. - /// - /// Read-only view. `points` are the individual `ContactData` entries; - /// `local_n1` / `local_n2` are the contact normal expressed in each - /// collider's local frame; `subshape1` / `subshape2` identify the - /// sub-shapes inside compound shapes (0 for non-compounds). - #[pyclass(name = "ContactManifold", module = "rapier", frozen)] - #[derive(Debug, Clone)] - pub struct ContactManifold { - #[pyo3(get)] - pub data: ContactManifoldData, - #[pyo3(get)] - pub points: Vec, - #[pyo3(get)] - pub local_n1: $Vec, - #[pyo3(get)] - pub local_n2: $Vec, - #[pyo3(get)] - pub subshape1: u32, - #[pyo3(get)] - pub subshape2: u32, - } - - // ============================================================ - // ContactPair (clone of rapier::geometry::ContactPair, but - // converted into Python-friendly shape views) - // ============================================================ - - /// Read-only view of the contacts between two colliders. - /// - /// Holds one or more `ContactManifold`s plus helpers to query the - /// deepest contact and the total impulse. Snapshots are returned - /// by `NarrowPhase.contact_pair(...)` and - /// `NarrowPhase.contact_pairs()`; mutating them does not affect - /// the simulation. - #[pyclass(name = "ContactPair", module = "rapier")] - #[derive(Clone)] - pub struct ContactPair(pub rapier::geometry::ContactPair); - - #[pymethods] - impl ContactPair { - /// Handle of the first collider. - #[getter] - fn collider1(&self) -> ColliderHandle { - ColliderHandle(self.0.collider1) - } - /// Handle of the second collider. - #[getter] - fn collider2(&self) -> ColliderHandle { - ColliderHandle(self.0.collider2) - } - - /// List of contact manifolds for this pair. - #[getter] - fn manifolds(&self) -> Vec { - self.0 - .manifolds - .iter() - .map(|m| { - let normal_v: $crate::na::SVector = m.data.normal.into(); - let n1: $crate::na::SVector = m.local_n1.into(); - let n2: $crate::na::SVector = m.local_n2.into(); - - let points: Vec = m - .points - .iter() - .map(|c| $crate::__define_geometry_contact_point_to_py!($dim, c)) - .collect(); - - ContactManifold { - data: ContactManifoldData { - rigid_body1: m.data.rigid_body1.map(RigidBodyHandle), - rigid_body2: m.data.rigid_body2.map(RigidBodyHandle), - normal: $Vec(normal_v), - num_active_contacts: m.data.solver_contacts.len(), - relative_dominance: m.data.relative_dominance, - user_data: m.data.user_data, - }, - points, - local_n1: $Vec(n1), - local_n2: $Vec(n2), - subshape1: m.subshape1, - subshape2: m.subshape2, - } - }) - .collect() - } - - /// True iff any manifold has at least one solver-active contact. - #[getter] - fn has_any_active_contact(&self) -> bool { - self.0.has_any_active_contact() - } - - /// Return the deepest (most penetrating) `ContactData`, if any. - fn find_deepest_contact(&self) -> Option { - self.0 - .find_deepest_contact() - .map(|(_, c)| $crate::__define_geometry_contact_point_to_py!($dim, c)) - } - - /// Sum of contact impulses across all manifolds, as a world-space vector. - fn total_impulse(&self) -> $Vec { - let v = self.0.total_impulse(); - let nav: $crate::na::SVector = v.into(); - $Vec(nav) - } - - /// Magnitude of `total_impulse()`. - fn total_impulse_magnitude(&self) -> Real { - self.0.total_impulse_magnitude() - } - } - - // ============================================================ - // IntersectionPair (small view) - // ============================================================ - - /// Snapshot of one sensor/intersection pair tracked by the narrow-phase. - /// - /// Read-only view. `intersecting` is true while the pair currently - /// overlaps. - #[pyclass(name = "IntersectionPair", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct IntersectionPair { - #[pyo3(get)] - pub collider1: ColliderHandle, - #[pyo3(get)] - pub collider2: ColliderHandle, - #[pyo3(get)] - pub intersecting: bool, - } - - // ============================================================ - // ContactForceEvent (plain dataclass-style) - // ============================================================ - - /// Event fired when the contact force magnitude between two - /// colliders exceeds the configured threshold. - /// - /// Requires `ActiveEvents.CONTACT_FORCE_EVENTS` on at least one - /// collider; the threshold is set via - /// `Collider.contact_force_event_threshold` (or its builder). - /// `total_force` is the sum of contact impulses divided by the - /// time step; `max_force_direction` and `max_force_magnitude` - /// summarize the strongest single contact. - #[pyclass(name = "ContactForceEvent", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct ContactForceEvent { - #[pyo3(get)] - pub collider1: ColliderHandle, - #[pyo3(get)] - pub collider2: ColliderHandle, - #[pyo3(get)] - pub total_force: $Vec, - #[pyo3(get)] - pub total_force_magnitude: Real, - #[pyo3(get)] - pub max_force_direction: $Vec, - #[pyo3(get)] - pub max_force_magnitude: Real, - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_sharedshape_dim_specific { - (3) => { - #[pymethods] - impl SharedShape { - /// Build a 3D axis-aligned box of given half-extents. - #[staticmethod] - fn cuboid(hx: Real, hy: Real, hz: Real) -> Self { - Self(rapier::parry::shape::SharedShape::cuboid(hx, hy, hz)) - } - /// Build a 3D cuboid with rounded edges of radius `border_radius`. - #[staticmethod] - fn round_cuboid(hx: Real, hy: Real, hz: Real, border_radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::round_cuboid( - hx, - hy, - hz, - border_radius, - )) - } - /// Build a Y-axis cylinder. - #[staticmethod] - fn cylinder(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::cylinder( - half_height, - radius, - )) - } - /// Build a Y-axis cone (apex at +Y). - #[staticmethod] - fn cone(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::cone(half_height, radius)) - } - /// Build a Y-axis cylinder with rounded edges. - #[staticmethod] - fn round_cylinder(half_height: Real, radius: Real, border_radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::round_cylinder( - half_height, - radius, - border_radius, - )) - } - /// Build a Y-axis cone with a rounded base/apex. - #[staticmethod] - fn round_cone(half_height: Real, radius: Real, border_radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::round_cone( - half_height, - radius, - border_radius, - )) - } - /// Build a capsule aligned with the X axis. - #[staticmethod] - fn capsule_x(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule_x( - half_height, - radius, - )) - } - /// Build a capsule aligned with the Y axis (the default). - #[staticmethod] - fn capsule_y(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule_y( - half_height, - radius, - )) - } - /// Build a capsule aligned with the Z axis. - #[staticmethod] - fn capsule_z(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule_z( - half_height, - radius, - )) - } - /// Build a triangle mesh from `(vertices, indices)` buffers. - /// - /// :param vertices: `(N, 3)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. - /// :param flags: Optional preprocessing flags (`TriMeshFlags`). - /// :raises MeshConversionError: If the mesh cannot be built. - #[staticmethod] - #[pyo3(signature = (vertices, indices, flags=None))] - fn trimesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - flags: Option, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - let shape = match flags { - None => rapier::parry::shape::SharedShape::trimesh(verts, idx), - Some(f) => { - rapier::parry::shape::SharedShape::trimesh_with_flags(verts, idx, f.0) - } - } - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e:?}")))?; - Ok(Self(shape)) - } - - /// Build a convex hull around the given 3D point cloud. - /// - /// :raises MeshConversionError: If fewer than 4 non-coplanar - /// points were supplied. - #[staticmethod] - fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::convex_hull(&pts) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex hull computation failed (need ≥ 4 non-coplanar points)", - ) - }) - } - - /// Alias of `convex_hull` (3D). - #[staticmethod] - fn convex_polyhedron(points: &Bound<'_, PyAny>) -> PyResult { - // Alias for convex_hull in 3D since parry's convex_polyhedron - // takes already-prepared points. - Self::convex_hull(points) - } - - /// Build a convex mesh from vertices that are *already* convex. - /// - /// Unlike `convex_hull`, no hull is recomputed; the caller - /// guarantees convexity via the `(vertices, indices)` buffers. - /// - /// :param vertices: `(N, 3)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. - /// :raises MeshConversionError: If the mesh is not a valid convex mesh. - #[staticmethod] - fn convex_mesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - rapier::parry::shape::SharedShape::convex_mesh(verts, &idx) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex mesh construction failed (invalid convex mesh)", - ) - }) - } - - /// Build a convex mesh dilated by `border_radius` (rounded edges). - /// - /// :raises MeshConversionError: If the mesh is not a valid convex mesh. - #[staticmethod] - fn round_convex_mesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - border_radius: Real, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - rapier::parry::shape::SharedShape::round_convex_mesh(verts, &idx, border_radius) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex mesh construction failed (invalid convex mesh)", - ) - }) - } - - /// Build a voxel-grid shape from explicit integer grid coordinates. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param grid_coords: Sequence of `(i, j, k)` integer cell - /// coordinates marking the filled voxels. - #[staticmethod] - fn voxels(voxel_size: PyVector, grid_coords: &Bound<'_, PyAny>) -> PyResult { - let raw: Vec<(i64, i64, i64)> = grid_coords.extract()?; - let coords: Vec = raw - .iter() - .map(|&(x, y, z)| rapier::math::IVector::new(x as _, y as _, z as _)) - .collect(); - Ok(Self(rapier::parry::shape::SharedShape::voxels( - voxel_size.0.into(), - &coords, - ))) - } - - /// Decompose a triangle soup into a compound of convex pieces. - /// - /// :param vertices: `(N, 3)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. - #[staticmethod] - #[pyo3(signature = (vertices, indices))] - fn convex_decomposition( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - Ok(Self( - rapier::parry::shape::SharedShape::convex_decomposition(&verts, &idx), - )) - } - - /// Build a 3D heightfield from a 2-D `heights` ndarray. - /// - /// :param heights: `(rows, cols)` ndarray of `Real`. - /// :param scale: Per-axis scaling vector. - #[staticmethod] - fn heightfield(heights: &Bound<'_, PyAny>, scale: PyVector) -> PyResult { - let arr: $crate::numpy::PyReadonlyArray2 = heights.extract()?; - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err( - "heights ndarray must be contiguous", - ) - })?; - let nrows = arr.shape()[0]; - let ncols = arr.shape()[1]; - let arr2 = rapier::parry::utils::Array2::new(nrows, ncols, slice.to_vec()); - Ok(Self(rapier::parry::shape::SharedShape::heightfield( - arr2, - scale.0.into(), - ))) - } - - /// Downcast to `Cylinder` if this is a cylinder shape. - fn as_cylinder(&self) -> Option { - self.0.as_cylinder().map(|s| Cylinder(*s)) - } - /// Downcast to `Cone` if this is a cone shape. - fn as_cone(&self) -> Option { - self.0.as_cone().map(|s| Cone(*s)) - } - /// Downcast to `ConvexPolyhedron` if this is a convex polyhedron. - fn as_convex_polyhedron(&self) -> Option { - self.0 - .as_convex_polyhedron() - .map(|s| ConvexPolyhedron(s.clone())) - } - /// Downcast to `Cylinder` if this is a *rounded* cylinder. - /// - /// Returns the inner (unrounded) `Cylinder`; the border radius - /// is discarded. - fn as_round_cylinder(&self) -> Option { - self.0.as_round_cylinder().map(|s| Cylinder(s.inner_shape)) - } - /// Downcast to `Cone` if this is a *rounded* cone. - /// - /// Returns the inner (unrounded) `Cone`; the border radius is - /// discarded. - fn as_round_cone(&self) -> Option { - self.0.as_round_cone().map(|s| Cone(s.inner_shape)) - } - /// Downcast to `ConvexPolyhedron` if this is a *rounded* convex - /// polyhedron. Returns the inner (unrounded) polyhedron. - fn as_round_convex_polyhedron(&self) -> Option { - self.0 - .as_round_convex_polyhedron() - .map(|s| ConvexPolyhedron(s.inner_shape.clone())) - } - } // end impl SharedShape - }; - - (2) => { - #[pymethods] - impl SharedShape { - /// Build a 2D axis-aligned rectangle of given half-extents. - #[staticmethod] - fn cuboid(hx: Real, hy: Real) -> Self { - Self(rapier::parry::shape::SharedShape::cuboid(hx, hy)) - } - /// Build a 2D rectangle with rounded corners of radius `border_radius`. - #[staticmethod] - fn round_cuboid(hx: Real, hy: Real, border_radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::round_cuboid( - hx, - hy, - border_radius, - )) - } - /// Build a 2D capsule aligned with the X axis. - #[staticmethod] - fn capsule_x(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule_x( - half_height, - radius, - )) - } - /// Build a 2D capsule aligned with the Y axis (the default). - #[staticmethod] - fn capsule_y(half_height: Real, radius: Real) -> Self { - Self(rapier::parry::shape::SharedShape::capsule_y( - half_height, - radius, - )) - } - /// Build a 2D segment shape from two endpoints. - #[staticmethod] - fn segment(a: PyVector, b: PyVector) -> Self { - Self(rapier::parry::shape::SharedShape::segment( - a.0.into(), - b.0.into(), - )) - } - /// Build a 2D polyline from `(vertices, indices)`. - /// - /// :param vertices: `(N, 2)` ndarray of floats. - /// :param indices: Optional `(M, 2)` ndarray of `u32`. When - /// omitted, consecutive vertices form the segments. - #[staticmethod] - #[pyo3(signature = (vertices, indices=None))] - fn polyline( - vertices: &Bound<'_, PyAny>, - indices: Option<&Bound<'_, PyAny>>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = match indices { - Some(i) => Some($crate::geometry::extract_indices_2(i)?), - None => None, - }; - Ok(Self(rapier::parry::shape::SharedShape::polyline( - verts, idx, - ))) - } - /// Build a 2D convex polygon from already-convex points. - /// - /// :raises MeshConversionError: If fewer than 3 non-collinear - /// points were supplied. - #[staticmethod] - fn convex_polygon(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::convex_polyline(pts) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex polygon construction failed (need ≥ 3 non-collinear points)", - ) - }) - } - /// Build a 2D convex hull around the given point cloud. - #[staticmethod] - fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::convex_hull(&pts) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex hull computation failed", - ) - }) - } - /// Build a triangle mesh from `(vertices, indices)` buffers. - /// - /// :param vertices: `(N, 2)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. - /// :param flags: Optional preprocessing flags (`TriMeshFlags`). - #[staticmethod] - #[pyo3(signature = (vertices, indices, flags=None))] - fn trimesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - flags: Option, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - let shape = match flags { - None => rapier::parry::shape::SharedShape::trimesh(verts, idx), - Some(f) => { - rapier::parry::shape::SharedShape::trimesh_with_flags(verts, idx, f.0) - } - } - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e:?}")))?; - Ok(Self(shape)) - } - /// Build a 2D heightfield from a 1-D `heights` vector and a scale. - #[staticmethod] - fn heightfield(heights: Vec, scale: PyVector) -> Self { - Self(rapier::parry::shape::SharedShape::heightfield( - heights, - scale.0.into(), - )) - } - - /// Build a 2D convex polyline from points assumed to form a convex - /// polygon (the points are *not* re-hulled, only sanitized). - /// - /// :raises MeshConversionError: If the polyline is degenerate. - #[staticmethod] - fn convex_polyline(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::convex_polyline(pts) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex polyline construction failed", - ) - }) - } - - /// Build a 2D convex polyline dilated by `border_radius` - /// (rounded corners). - /// - /// :raises MeshConversionError: If the polyline is degenerate. - #[staticmethod] - fn round_convex_polyline( - points: &Bound<'_, PyAny>, - border_radius: Real, - ) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::parry::shape::SharedShape::round_convex_polyline(pts, border_radius) - .map(Self) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex polyline construction failed", - ) - }) - } - - /// Build a voxel-grid shape from explicit integer grid coordinates. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param grid_coords: Sequence of `(i, j)` integer cell - /// coordinates marking the filled voxels. - #[staticmethod] - fn voxels(voxel_size: PyVector, grid_coords: &Bound<'_, PyAny>) -> PyResult { - let raw: Vec<(i64, i64)> = grid_coords.extract()?; - let coords: Vec = raw - .iter() - .map(|&(x, y)| rapier::math::IVector::new(x as _, y as _)) - .collect(); - Ok(Self(rapier::parry::shape::SharedShape::voxels( - voxel_size.0.into(), - &coords, - ))) - } - - /// Downcast to `Segment` if this is a segment shape. - fn as_segment(&self) -> Option { - self.0.as_segment().map(|s| Segment(*s)) - } - /// Downcast to `Polyline` if this is a polyline shape. - fn as_polyline(&self) -> Option { - self.0.as_polyline().map(|s| Polyline(s.clone())) - } - /// Downcast to `ConvexPolygon` if this is a convex polygon. - fn as_convex_polygon(&self) -> Option { - self.0.as_convex_polygon().map(|s| ConvexPolygon(s.clone())) - } - /// Downcast to `ConvexPolygon` if this is a *rounded* convex - /// polygon. Returns the inner (unrounded) polygon. - fn as_round_convex_polygon(&self) -> Option { - self.0 - .as_round_convex_polygon() - .map(|s| ConvexPolygon(s.inner_shape.clone())) - } - } // end impl SharedShape - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_meshconverter_dim_specific { - (3) => { - #[pymethods] - impl MeshConverter { - /// Convert into a compound shape via convex decomposition. - #[classattr] - fn COMPOUND() -> Self { - Self(rapier::geometry::MeshConverter::ConvexDecomposition) - } - /// Convert into a compound of convex pieces via convex - /// decomposition (V-HACD, default parameters). 3D only. - #[classattr] - fn CONVEX_DECOMPOSITION() -> Self { - Self(rapier::geometry::MeshConverter::ConvexDecomposition) - } - } - }; - (2) => { - #[pymethods] - impl MeshConverter { - /// Convert into a compound shape. Currently falls back to OBB - /// in 2D since convex decomposition is not yet available. - // No ConvexDecomposition in 2D in parry; reuse OBB as a sensible - // fallback so the COMPOUND constant exists. May be revisited. - #[classattr] - fn COMPOUND() -> Self { - Self(rapier::geometry::MeshConverter::Obb) - } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_extract_verts_helper { - (3) => { - /// Extract vertex array (N, 3) and convert into `Vec`. - /// - /// Accepts both `float32` and `float64` ndarrays (cast to `Real`), as - /// well as list/tuple of triples. - #[allow(dead_code)] - fn extract_verts_for_dim( - obj: &$crate::pyo3::Bound<'_, $crate::pyo3::PyAny>, - ) -> $crate::pyo3::PyResult> { - use $crate::numpy::{PyArrayMethods, PyReadonlyArray2, PyUntypedArrayMethods}; - // Try the cdylib's matching-precision ndarray first (zero-copy). - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 3 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 3); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(3) { - out.push(rapier::math::Vector::new(c[0], c[1], c[2])); - } - return Ok(out); - } - // Try the other precision (lossy cast). - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 3 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 3); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(3) { - out.push(rapier::math::Vector::new( - c[0] as Real, - c[1] as Real, - c[2] as Real, - )); - } - return Ok(out); - } - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 3 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 3); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(3) { - out.push(rapier::math::Vector::new( - c[0] as Real, - c[1] as Real, - c[2] as Real, - )); - } - return Ok(out); - } - // Fall back to list/tuple of tuples / any sequence of length 3. - let seq: Vec<(Real, Real, Real)> = obj.extract()?; - Ok(seq - .into_iter() - .map(|(x, y, z)| rapier::math::Vector::new(x, y, z)) - .collect()) - } - }; - (2) => { - #[allow(dead_code)] - fn extract_verts_for_dim( - obj: &$crate::pyo3::Bound<'_, $crate::pyo3::PyAny>, - ) -> $crate::pyo3::PyResult> { - use $crate::numpy::{PyArrayMethods, PyReadonlyArray2, PyUntypedArrayMethods}; - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 2 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 2); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(2) { - out.push(rapier::math::Vector::new(c[0], c[1])); - } - return Ok(out); - } - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 2 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 2); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(2) { - out.push(rapier::math::Vector::new(c[0] as Real, c[1] as Real)); - } - return Ok(out); - } - if let Ok(arr) = obj.extract::>() { - let (nrows, ncols) = (arr.shape()[0], arr.shape()[1]); - if ncols != 2 { - return Err($crate::pyo3::exceptions::PyValueError::new_err(format!( - "expected ndarray with shape (N, 2); got (N, {ncols})" - ))); - } - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err("ndarray must be contiguous") - })?; - let mut out = Vec::with_capacity(nrows); - for c in slice.chunks_exact(2) { - out.push(rapier::math::Vector::new(c[0] as Real, c[1] as Real)); - } - return Ok(out); - } - let seq: Vec<(Real, Real)> = obj.extract()?; - Ok(seq - .into_iter() - .map(|(x, y)| rapier::math::Vector::new(x, y)) - .collect()) - } - }; -} - -// ============================================================ -// Collider / ColliderBuilder / ColliderSet — dim-agnostic -// definitions (the Vec/Point/Iso identifiers are passed in). -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_collider { - ($dim:tt, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // ============================================================ - // Collider — owned standalone value (mirrors RigidBody pattern). - // ============================================================ - - /// Standalone collider: a shape + material + per-collider flags - /// that participates in collision detection. - /// - /// A collider may be attached to a `RigidBody` via - /// `ColliderSet.insert_with_parent(...)`. Use the static shape - /// factories (`Collider.ball`, `Collider.cuboid`, etc.) to obtain - /// a `ColliderBuilder` and chain configuration calls, then - /// `.build()` to get a `Collider`. A collider fetched from - /// `ColliderSet[handle]` is a live **view**: reads and writes go - /// straight through to the set with no copy, so mutations persist - /// immediately. - #[pyclass(name = "Collider", module = "rapier")] - pub struct Collider { - pub backing: ColliderBacking, - } - - /// Storage backing a `Collider`: a standalone owned collider - /// (pre-insertion) or a handle-backed view into a `ColliderSet`. - #[derive(Debug)] - pub enum ColliderBacking { - Owned(Box), - InSet { - set: Py, - handle: rapier::geometry::ColliderHandle, - }, - } - - impl Clone for ColliderBacking { - fn clone(&self) -> Self { - match self { - ColliderBacking::Owned(c) => ColliderBacking::Owned(c.clone()), - ColliderBacking::InSet { set, handle } => { - Python::with_gil(|py| ColliderBacking::InSet { - set: set.clone_ref(py), - handle: *handle, - }) - } - } - } - } - impl Clone for Collider { - fn clone(&self) -> Self { - Collider { - backing: self.backing.clone(), - } - } - } - - impl Collider { - fn new_owned(collider: rapier::geometry::Collider) -> Self { - Collider { - backing: ColliderBacking::Owned(Box::new(collider)), - } - } - - /// Run `f` with a shared reference to the underlying collider. - fn with_ref(&self, f: impl FnOnce(&rapier::geometry::Collider) -> R) -> R { - match &self.backing { - ColliderBacking::Owned(c) => f(c), - ColliderBacking::InSet { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); - let c = set - .0 - .get(*handle) - .expect("Collider refers to a collider that was removed from its set"); - f(c) - }), - } - } - - /// Run `f` with a mutable reference to the underlying collider, - /// writing straight through to the set for an `InSet` view. - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::geometry::Collider) -> R) -> R { - match &mut self.backing { - ColliderBacking::Owned(c) => f(c), - ColliderBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); - let c = set - .0 - .get_mut(*handle) - .expect("Collider refers to a collider that was removed from its set"); - f(c) - }), - } - } - - /// Clone the underlying collider out (used by `insert` and callers - /// needing an owned `&rapier::Collider`). - pub fn to_owned_collider(&self) -> rapier::geometry::Collider { - self.with_ref(|c| c.clone()) - } - } - - #[pymethods] - impl Collider { - // ---- read-only handle/parent ---- - - /// Handle of the parent rigid-body, if any. - #[getter] - fn parent(&self) -> Option { - self.with_ref(|c| c.parent()).map(RigidBodyHandle) - } - - // ---- position / translation / rotation ---- - - /// World-space pose of the collider. - #[getter] - fn position(&self) -> $Iso { - self.with_ref(|c| { - let iso: $crate::na::Isometry = (*c.position()).into(); - $Iso(iso) - }) - } - #[setter] - fn set_position(&mut self, p: PyIsometry) { - self.with_mut(|c| c.set_position(p.0.into())); - } - /// World-space translation component of the pose. - #[getter] - fn translation(&self) -> $Vec { - self.with_ref(|c| { - let v: $crate::na::SVector = c.translation().into(); - $Vec(v) - }) - } - #[setter] - fn set_translation(&mut self, v: PyVector) { - self.with_mut(|c| c.set_translation(v.0.into())); - } - /// World-space rotation component of the pose. - #[getter] - fn rotation(&self) -> $Rot { - self.with_ref(|c| $Rot(c.rotation().into())) - } - #[setter] - fn set_rotation(&mut self, r: PyRotation) { - self.with_mut(|c| c.set_rotation(r.0.into())); - } - - // ---- shape ---- - - /// Underlying collision shape. - #[getter] - fn shape(&self) -> SharedShape { - self.with_ref(|c| SharedShape(c.shared_shape().clone())) - } - #[setter] - fn set_shape(&mut self, s: SharedShape) { - self.with_mut(|c| c.set_shape(s.0)); - } - - // ---- mass / density / mass_properties ---- - - /// Uniform density used to derive mass when no explicit mass is set. - #[getter] - fn density(&self) -> Real { - self.with_ref(|c| c.density()) - } - #[setter] - fn set_density(&mut self, v: Real) { - self.with_mut(|c| c.set_density(v)); - } - /// Explicit mass of the collider. - #[getter] - fn mass(&self) -> Real { - self.with_ref(|c| c.mass()) - } - #[setter] - fn set_mass(&mut self, v: Real) { - self.with_mut(|c| c.set_mass(v)); - } - /// Full mass properties (mass, center of mass, inertia tensor). - #[getter] - fn mass_properties(&self) -> MassProperties { - MassProperties(self.with_ref(|c| c.mass_properties())) - } - #[setter] - fn set_mass_properties(&mut self, mp: MassProperties) { - self.with_mut(|c| c.set_mass_properties(mp.0)); - } - /// Volume (3D) or area (2D) of the shape. - #[getter] - fn volume(&self) -> Real { - self.with_ref(|c| c.volume()) - } - - // ---- material ---- - - /// Friction coefficient. - #[getter] - fn friction(&self) -> Real { - self.with_ref(|c| c.friction()) - } - #[setter] - fn set_friction(&mut self, v: Real) { - self.with_mut(|c| c.set_friction(v)); - } - /// Restitution coefficient (bounciness). - #[getter] - fn restitution(&self) -> Real { - self.with_ref(|c| c.restitution()) - } - #[setter] - fn set_restitution(&mut self, v: Real) { - self.with_mut(|c| c.set_restitution(v)); - } - /// Rule used to combine friction with another collider's friction. - #[getter] - fn friction_combine_rule(&self) -> CoefficientCombineRule { - CoefficientCombineRule::from_rapier(self.with_ref(|c| c.friction_combine_rule())) - } - #[setter] - fn set_friction_combine_rule(&mut self, v: CoefficientCombineRule) { - self.with_mut(|c| c.set_friction_combine_rule(v.to_rapier())); - } - /// Rule used to combine restitution with another collider. - #[getter] - fn restitution_combine_rule(&self) -> CoefficientCombineRule { - CoefficientCombineRule::from_rapier(self.with_ref(|c| c.restitution_combine_rule())) - } - #[setter] - fn set_restitution_combine_rule(&mut self, v: CoefficientCombineRule) { - self.with_mut(|c| c.set_restitution_combine_rule(v.to_rapier())); - } - /// Material bundle (friction, restitution, and their combine rules). - #[getter] - fn material(&self) -> ColliderMaterial { - self.with_ref(|c| ColliderMaterial(*c.material())) - } - - // ---- sensor / enabled ---- - - /// True iff this collider is a sensor (no contact response). - #[getter] - fn is_sensor(&self) -> bool { - self.with_ref(|c| c.is_sensor()) - } - #[setter] - fn set_is_sensor(&mut self, v: bool) { - self.with_mut(|c| c.set_sensor(v)); - } - /// True iff the collider is currently enabled. - #[getter] - fn is_enabled(&self) -> bool { - self.with_ref(|c| c.is_enabled()) - } - #[setter] - fn set_is_enabled(&mut self, v: bool) { - self.with_mut(|c| c.set_enabled(v)); - } - - // ---- hooks / events ---- - - /// Event flags opted into by this collider (see `ActiveEvents`). - #[getter] - fn active_events(&self) -> ActiveEvents { - ActiveEvents(self.with_ref(|c| c.active_events())) - } - #[setter] - fn set_active_events(&mut self, v: ActiveEvents) { - self.with_mut(|c| c.set_active_events(v.0)); - } - /// Hook flags opted into by this collider (see `ActiveHooks`). - #[getter] - fn active_hooks(&self) -> ActiveHooks { - ActiveHooks(self.with_ref(|c| c.active_hooks())) - } - #[setter] - fn set_active_hooks(&mut self, v: ActiveHooks) { - self.with_mut(|c| c.set_active_hooks(v.0)); - } - /// Rigid-body type combinations this collider collides with - /// (see `ActiveCollisionTypes`). - #[getter] - fn active_collision_types(&self) -> ActiveCollisionTypes { - ActiveCollisionTypes(self.with_ref(|c| c.active_collision_types())) - } - #[setter] - fn set_active_collision_types(&mut self, v: ActiveCollisionTypes) { - self.with_mut(|c| c.set_active_collision_types(v.0)); - } - - // ---- groups ---- - - /// Groups controlling which colliders form contact pairs. - #[getter] - fn collision_groups(&self) -> InteractionGroups { - InteractionGroups(self.with_ref(|c| c.collision_groups())) - } - #[setter] - fn set_collision_groups(&mut self, v: InteractionGroups) { - self.with_mut(|c| c.set_collision_groups(v.0)); - } - /// Groups controlling which contact pairs reach the solver. - #[getter] - fn solver_groups(&self) -> InteractionGroups { - InteractionGroups(self.with_ref(|c| c.solver_groups())) - } - #[setter] - fn set_solver_groups(&mut self, v: InteractionGroups) { - self.with_mut(|c| c.set_solver_groups(v.0)); - } - - // ---- contact skin & event threshold ---- - - /// Thickness of the virtual "skin" around the collider, used to - /// reduce jitter on resting contacts. - #[getter] - fn contact_skin(&self) -> Real { - self.with_ref(|c| c.contact_skin()) - } - #[setter] - fn set_contact_skin(&mut self, v: Real) { - self.with_mut(|c| c.set_contact_skin(v)); - } - /// Force magnitude above which a `ContactForceEvent` is emitted. - /// - /// Requires `ActiveEvents.CONTACT_FORCE_EVENTS`. - #[getter] - fn contact_force_event_threshold(&self) -> Real { - self.with_ref(|c| c.contact_force_event_threshold()) - } - #[setter] - fn set_contact_force_event_threshold(&mut self, v: Real) { - self.with_mut(|c| c.set_contact_force_event_threshold(v)); - } - - // ---- user_data ---- - - /// Free 128-bit user payload (opaque to the engine). - #[getter] - fn user_data(&self) -> u128 { - self.with_ref(|c| c.user_data) - } - #[setter] - fn set_user_data(&mut self, v: u128) { - self.with_mut(|c| c.user_data = v); - } - - // ---- compute helpers ---- - - /// Compute the world-space AABB of this collider. - fn compute_aabb(&self) -> Aabb { - Aabb(self.with_ref(|c| c.compute_aabb())) - } - - // ---- Builder forwards (mirror RigidBody.dynamic etc) ---- - - /// Builder for a ball/sphere collider of the given radius. - /// - /// :param radius: Sphere radius. - /// :param kwargs: Optional `ColliderBuilder` kwargs (e.g. - /// `density`, `friction`, `sensor`, `translation`, - /// `rotation`). - #[staticmethod] - #[pyo3(signature = (radius, **kwargs))] - fn ball( - radius: Real, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - ColliderBuilder::from_kwargs( - rapier::geometry::ColliderBuilder::ball(radius), - kwargs, - ) - } - /// Builder for a triangle collider with the given vertices. - #[staticmethod] - fn triangle(a: PyVector, b: PyVector, c: PyVector) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::triangle( - a.0.into(), - b.0.into(), - c.0.into(), - ), - } - } - /// Builder for a half-space (infinite plane) collider. - /// - /// :param outward_normal: Plane normal; the solid half-space lies - /// on its negative side. Normalized automatically. - /// :param kwargs: Optional `ColliderBuilder` kwargs (e.g. - /// `friction`, `translation`, `rotation`). A half-space has no - /// finite volume, so leave `density`/`mass` unset on bodies - /// that use it. - #[staticmethod] - #[pyo3(signature = (outward_normal, **kwargs))] - fn halfspace( - outward_normal: PyVector, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let n = outward_normal.0.normalize(); - ColliderBuilder::from_kwargs( - rapier::geometry::ColliderBuilder::new( - rapier::parry::shape::SharedShape::halfspace(n.into()), - ), - kwargs, - ) - } - /// Builder wrapping an arbitrary `SharedShape`. - #[staticmethod] - #[pyo3(signature = (shape))] - fn new(shape: SharedShape) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::new(shape.0), - } - } - /// Builder for a compound collider built from `(pose, sub_shape)` parts. - #[staticmethod] - fn compound(parts: Vec<(PyIsometry, SharedShape)>) -> ColliderBuilder { - let parts: Vec<(rapier::math::Pose, rapier::parry::shape::SharedShape)> = - parts.into_iter().map(|(p, s)| (p.0.into(), s.0)).collect(); - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::compound(parts), - } - } - /// Builder for a triangle-mesh collider. - /// - /// :param vertices: `(N, D)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. - /// :param flags: Optional `TriMeshFlags` preprocessing. - /// :raises MeshConversionError: If the mesh cannot be built. - #[staticmethod] - #[pyo3(signature = (vertices, indices, flags=None))] - fn trimesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - flags: Option, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - let b = match flags { - None => rapier::geometry::ColliderBuilder::trimesh(verts, idx), - Some(f) => { - rapier::geometry::ColliderBuilder::trimesh_with_flags(verts, idx, f.0) - } - } - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e:?}")))?; - Ok(ColliderBuilder { builder: b }) - } - - /// Builder for a collider wrapping the convex hull of a point cloud. - /// - /// :raises MeshConversionError: If the convex hull cannot be built. - #[staticmethod] - fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::geometry::ColliderBuilder::convex_hull(&pts) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex hull computation failed", - ) - }) - } - /// Builder for a triangle collider with rounded edges. - #[staticmethod] - fn round_triangle( - a: PyVector, - b: PyVector, - c: PyVector, - border_radius: Real, - ) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_triangle( - a.0.into(), - b.0.into(), - c.0.into(), - border_radius, - ), - } - } - /// Builder for a rounded convex-hull collider. - /// - /// :raises MeshConversionError: If the hull cannot be built. - #[staticmethod] - fn round_convex_hull( - points: &Bound<'_, PyAny>, - border_radius: Real, - ) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::geometry::ColliderBuilder::round_convex_hull(&pts, border_radius) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex hull computation failed", - ) - }) - } - /// Builder for a voxel-grid collider built by voxelizing points. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param points: `(N, D)` ndarray of floats. - #[staticmethod] - fn voxels_from_points( - voxel_size: PyVector, - points: &Bound<'_, PyAny>, - ) -> PyResult { - let pts = extract_verts_for_dim(points)?; - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::voxels_from_points( - voxel_size.0.into(), - &pts, - ), - }) - } - /// Builder for a collider whose shape is produced from a triangle - /// buffer by a `MeshConverter` (trimesh, hull, OBB, AABB, - /// convex decomposition, ...). - /// - /// :raises MeshConversionError: If the conversion fails. - #[staticmethod] - fn converted_trimesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - converter: &MeshConverter, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - rapier::geometry::ColliderBuilder::converted_trimesh( - verts, - idx, - converter.0.clone(), - ) - .map(|b| ColliderBuilder { builder: b }) - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e}"))) - } - - // Capsule constructors (parry uses `capsule_from_endpoints`). - /// Builder for a Y-axis capsule (the default axis). - #[staticmethod] - fn capsule(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), - } - } - /// Builder for an X-axis capsule. - #[staticmethod] - fn capsule_x(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::capsule_x(half_height, radius), - } - } - /// Builder for a Y-axis capsule. - #[staticmethod] - fn capsule_y(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), - } - } - /// Builder for a capsule defined by its two endpoints and a radius. - #[staticmethod] - fn capsule_from_endpoints(a: PyVector, b: PyVector, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::capsule_from_endpoints( - a.0.into(), - b.0.into(), - radius, - ), - } - } - - fn __repr__(&self) -> String { - format!( - "Collider(shape={:?}, sensor={}, parent={:?})", - self.with_ref(|c| c.shape().shape_type()), - self.with_ref(|c| c.is_sensor()), - self.with_ref(|c| c.parent()).map(|h| h.into_raw_parts()), - ) - } - } - - // Emit the dim-specific Collider statics as a separate pymethods - // block (multiple-pymethods feature). - $crate::__define_collider_dim_specific!($dim); - - // ============================================================ - // ColliderBuilder - // ============================================================ - - /// Fluent builder for `Collider` values. - /// - /// Build one via a shape factory on `Collider` (e.g. - /// `Collider.ball(radius)`, `Collider.cuboid(hx, hy, hz)`), chain - /// configuration calls (`density`, `friction`, `sensor`, ...), - /// then call `build()` to obtain a `Collider`. - #[pyclass(name = "ColliderBuilder", module = "rapier")] - #[derive(Clone)] - pub struct ColliderBuilder { - pub builder: rapier::geometry::ColliderBuilder, - } - - impl ColliderBuilder { - pub fn from_kwargs( - base: rapier::geometry::ColliderBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self { builder: base }; - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - me.apply_kwarg(&key, &v)?; - } - } - Ok(me) - } - - fn apply_kwarg(&mut self, key: &str, v: &Bound<'_, PyAny>) -> PyResult<()> { - match key { - "density" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().density(f); - } - "mass" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().mass(f); - } - "friction" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().friction(f); - } - "restitution" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().restitution(f); - } - "sensor" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().sensor(b); - } - "translation" => { - let pv: PyVector = v.extract()?; - self.builder = self.builder.clone().translation(pv.0.into()); - } - "rotation" => { - $crate::__collider_apply_rotation_kwarg!($dim, self.builder, v); - } - "position" => { - let pi: PyIsometry = v.extract()?; - self.builder = self.builder.clone().position(pi.0.into()); - } - "user_data" => { - let d: u128 = v.extract()?; - self.builder = self.builder.clone().user_data(d); - } - "enabled" => { - let b: bool = v.extract()?; - self.builder = self.builder.clone().enabled(b); - } - "contact_skin" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().contact_skin(f); - } - "contact_force_event_threshold" => { - let f: Real = v.extract()?; - self.builder = self.builder.clone().contact_force_event_threshold(f); - } - "active_events" => { - let e: ActiveEvents = v.extract()?; - self.builder = self.builder.clone().active_events(e.0); - } - "active_hooks" => { - let h: ActiveHooks = v.extract()?; - self.builder = self.builder.clone().active_hooks(h.0); - } - "active_collision_types" => { - let t: ActiveCollisionTypes = v.extract()?; - self.builder = self.builder.clone().active_collision_types(t.0); - } - "collision_groups" => { - let g: InteractionGroups = v.extract()?; - self.builder = self.builder.clone().collision_groups(g.0); - } - "solver_groups" => { - let g: InteractionGroups = v.extract()?; - self.builder = self.builder.clone().solver_groups(g.0); - } - "friction_combine_rule" => { - let r: CoefficientCombineRule = v.extract()?; - self.builder = self.builder.clone().friction_combine_rule(r.to_rapier()); - } - "restitution_combine_rule" => { - let r: CoefficientCombineRule = v.extract()?; - self.builder = self.builder.clone().restitution_combine_rule(r.to_rapier()); - } - "mass_properties" => { - let mp: MassProperties = v.extract()?; - self.builder = self.builder.clone().mass_properties(mp.0); - } - _ => { - return Err(PyTypeError::new_err(format!( - "unknown ColliderBuilder kwarg: '{}'", - key - ))); - } - } - Ok(()) - } - } - - #[pymethods] - impl ColliderBuilder { - /// Set the world-space translation. Returns a new builder. - fn translation(&self, v: PyVector) -> Self { - Self { - builder: self.builder.clone().translation(v.0.into()), - } - } - /// Set the world-space pose. Returns a new builder. - fn position(&self, p: PyIsometry) -> Self { - Self { - builder: self.builder.clone().position(p.0.into()), - } - } - /// Set the uniform density. Returns a new builder. - fn density(&self, d: Real) -> Self { - Self { - builder: self.builder.clone().density(d), - } - } - /// Set an explicit mass (overrides density). Returns a new builder. - fn mass(&self, m: Real) -> Self { - Self { - builder: self.builder.clone().mass(m), - } - } - /// Set the full mass properties. Returns a new builder. - fn mass_properties(&self, mp: &MassProperties) -> Self { - Self { - builder: self.builder.clone().mass_properties(mp.0), - } - } - /// Set the friction coefficient. Returns a new builder. - fn friction(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().friction(f), - } - } - /// Set the restitution coefficient. Returns a new builder. - fn restitution(&self, r: Real) -> Self { - Self { - builder: self.builder.clone().restitution(r), - } - } - /// Set the friction combine rule. Returns a new builder. - fn friction_combine_rule(&self, r: CoefficientCombineRule) -> Self { - Self { - builder: self.builder.clone().friction_combine_rule(r.to_rapier()), - } - } - /// Set the restitution combine rule. Returns a new builder. - fn restitution_combine_rule(&self, r: CoefficientCombineRule) -> Self { - Self { - builder: self.builder.clone().restitution_combine_rule(r.to_rapier()), - } - } - /// Toggle sensor mode (no contact response). Returns a new builder. - fn sensor(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().sensor(b), - } - } - /// Set the opt-in event flags. Returns a new builder. - fn active_events(&self, e: &ActiveEvents) -> Self { - Self { - builder: self.builder.clone().active_events(e.0), - } - } - /// Set the opt-in hook flags. Returns a new builder. - fn active_hooks(&self, h: &ActiveHooks) -> Self { - Self { - builder: self.builder.clone().active_hooks(h.0), - } - } - /// Set the enabled rigid-body-type collision combinations - /// (see `ActiveCollisionTypes`). Returns a new builder. - fn active_collision_types(&self, t: &ActiveCollisionTypes) -> Self { - Self { - builder: self.builder.clone().active_collision_types(t.0), - } - } - /// Set the collision groups. Returns a new builder. - fn collision_groups(&self, g: &InteractionGroups) -> Self { - Self { - builder: self.builder.clone().collision_groups(g.0), - } - } - /// Set the solver groups. Returns a new builder. - fn solver_groups(&self, g: &InteractionGroups) -> Self { - Self { - builder: self.builder.clone().solver_groups(g.0), - } - } - /// Set the contact skin thickness. Returns a new builder. - fn contact_skin(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().contact_skin(f), - } - } - /// Set the contact-force event threshold. Returns a new builder. - fn contact_force_event_threshold(&self, f: Real) -> Self { - Self { - builder: self.builder.clone().contact_force_event_threshold(f), - } - } - /// Set the user-data payload. Returns a new builder. - fn user_data(&self, d: u128) -> Self { - Self { - builder: self.builder.clone().user_data(d), - } - } - /// Toggle the initial enabled state. Returns a new builder. - fn enabled(&self, b: bool) -> Self { - Self { - builder: self.builder.clone().enabled(b), - } - } - - /// Set the rotation. Returns a new builder. - /// - /// In 2D the argument is an angle in radians; in 3D it is a - /// rotation vector (axis * angle). - fn rotation(&self, v: &Bound<'_, PyAny>) -> PyResult { - let mut b = self.builder.clone(); - $crate::__collider_apply_rotation_kwarg!($dim, b, v); - Ok(Self { builder: b }) - } - - /// Finalize the builder into a `Collider`. - fn build(&self) -> Collider { - Collider::new_owned(self.builder.clone().build()) - } - } - - // ============================================================ - // ColliderSet - // ============================================================ - - /// Owning container of `Collider`s identified by `ColliderHandle`. - /// - /// Acts like a dict keyed by handles: supports `len(set)`, - /// `handle in set`, `set[handle]`, iteration, plus `insert` and - /// `remove`. `set[handle]` returns a live view, so mutating it - /// (e.g. `set[h].set_sensor(True)`) persists in place. - #[pyclass(name = "ColliderSet", module = "rapier", unsendable)] - pub struct ColliderSet(pub rapier::geometry::ColliderSet); - - #[pymethods] - impl ColliderSet { - /// Construct an empty set. - #[new] - fn new() -> Self { - Self(rapier::geometry::ColliderSet::new()) - } - - /// Insert a `Collider` or `ColliderBuilder` standalone (no parent). - /// - /// :returns: The new `ColliderHandle`. - /// :raises TypeError: If `builder` is neither a `Collider` - /// nor a `ColliderBuilder`. - fn insert(&mut self, builder: &Bound<'_, PyAny>) -> PyResult { - if let Ok(b) = builder.extract::>() { - return Ok(ColliderHandle(self.0.insert(b.builder.clone().build()))); - } - if let Ok(c) = builder.extract::>() { - return Ok(ColliderHandle(self.0.insert(c.to_owned_collider()))); - } - Err(PyTypeError::new_err( - "ColliderSet.insert expects a Collider or ColliderBuilder", - )) - } - - /// Insert a collider attached to a parent rigid-body. - /// - /// The collider's position is interpreted relative to the - /// parent's pose. - /// - /// :param builder: A `Collider` or `ColliderBuilder`. - /// :param parent: Handle of the parent rigid-body. - /// :param bodies: The set containing `parent`. - /// :returns: The new `ColliderHandle`. - fn insert_with_parent( - &mut self, - builder: &Bound<'_, PyAny>, - parent: &RigidBodyHandle, - bodies: &mut RigidBodySet, - ) -> PyResult { - let coll = if let Ok(b) = builder.extract::>() { - b.builder.clone().build() - } else if let Ok(c) = builder.extract::>() { - c.to_owned_collider() - } else { - return Err(PyTypeError::new_err( - "ColliderSet.insert_with_parent expects a Collider or ColliderBuilder", - )); - }; - Ok(ColliderHandle(self.0.insert_with_parent( - coll, - parent.0, - &mut bodies.0, - ))) - } - - /// Remove a collider by handle and return it. - /// - /// :param wake_parent: If True, wakes the parent rigid-body so - /// islands re-evaluate. Defaults to True. - /// :returns: The removed `Collider`, or `None` if `handle` is unknown. - #[pyo3(signature = (handle, islands, bodies, wake_parent=true))] - fn remove( - &mut self, - handle: &ColliderHandle, - islands: &mut IslandManager, - bodies: &mut RigidBodySet, - wake_parent: bool, - ) -> Option { - self.0 - .remove(handle.0, &mut islands.0, &mut bodies.0, wake_parent) - .map(Collider::new_owned) - } - - /// Return a live **view** of the collider for `handle`, or `None` - /// if the handle is unknown. Reads and writes go straight through - /// to the set with no copy. - fn get(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> Option { - slf.borrow().0.get(handle.0)?; - Some(Collider { - backing: ColliderBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - fn __getitem__(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> PyResult { - if slf.borrow().0.get(handle.0).is_none() { - return Err($crate::errors::InvalidHandle::new_err(format!( - "no collider for {:?}", - handle.0.into_raw_parts(), - ))); - } - Ok(Collider { - backing: ColliderBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - fn __contains__(&self, handle: &ColliderHandle) -> bool { - self.0.contains(handle.0) - } - fn __len__(&self) -> usize { - self.0.len() - } - /// True iff the set contains no colliders. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Remove every collider from the set. - fn clear(&mut self) { - self.0 = rapier::geometry::ColliderSet::new(); - } - - fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { - let handles: Vec = - slf.borrow().0.iter().map(|(h, _)| h).collect(); - Py::new( - slf.py(), - ColliderSetIter { - set: slf.clone().unbind(), - handles, - i: 0, - }, - ) - } - - /// Return an iterator over the ``ColliderHandle`` values in the set. - fn handles(slf: PyRef<'_, Self>) -> PyResult> { - let h: Vec = slf.0.iter().map(|(h, _)| ColliderHandle(h)).collect(); - Py::new(slf.py(), ColliderHandleIter { handles: h, i: 0 }) - } - } - - /// Iterator yielding `(handle, collider)` pairs from a `ColliderSet`. - #[pyclass] - pub struct ColliderSetIter { - set: Py, - handles: Vec, - i: usize, - } - #[pymethods] - impl ColliderSetIter { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(ColliderHandle, Collider)> { - if slf.i >= slf.handles.len() { - return None; - } - let py = slf.py(); - let handle = slf.handles[slf.i]; - slf.i += 1; - let set = slf.set.clone_ref(py); - Some(( - ColliderHandle(handle), - Collider { - backing: ColliderBacking::InSet { set, handle }, - }, - )) - } - } - - /// Iterator yielding `ColliderHandle`s from a `ColliderSet`. - #[pyclass] - pub struct ColliderHandleIter { - handles: Vec, - i: usize, - } - #[pymethods] - impl ColliderHandleIter { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.handles.len() { - return None; - } - let h = slf.handles[slf.i]; - slf.i += 1; - Some(h) - } - } - - // ============================================================ - // ColliderParent / ColliderPosition value types - // ============================================================ - - /// Description of a collider's parent rigid-body and local offset. - /// - /// Holds the parent `RigidBodyHandle` and the pose of the collider - /// expressed in the parent's local frame. - #[pyclass(name = "ColliderParent", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct ColliderParent { - #[pyo3(get)] - pub handle: RigidBodyHandle, - #[pyo3(get)] - pub pos_wrt_parent: $Iso, - } - - #[pymethods] - impl ColliderParent { - /// Build a `ColliderParent` from a parent handle and local pose. - #[new] - fn new(handle: RigidBodyHandle, pos_wrt_parent: PyIsometry) -> Self { - let iso: $crate::na::Isometry = pos_wrt_parent.0; - Self { - handle, - pos_wrt_parent: $Iso(iso), - } - } - } - - /// Standalone wrapper holding only a collider's world-space pose. - /// - /// Used in some lower-level APIs that decouple the position from - /// the full `Collider` struct. - #[pyclass(name = "ColliderPosition", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct ColliderPosition { - #[pyo3(get)] - pub position: $Iso, - } - - #[pymethods] - impl ColliderPosition { - /// Build a `ColliderPosition` from a world-space pose. - #[new] - fn new(position: PyIsometry) -> Self { - let iso: $crate::na::Isometry = position.0; - Self { - position: $Iso(iso), - } - } - } - - // ============================================================ - // ColliderMassProps tagged union - // ============================================================ - - /// Tagged union describing how a collider's mass is specified. - /// - /// Build one of the three variants (`Density`, `Mass`, or - /// `MassProperties`) to override the default density-based mass - /// derivation. - #[pyclass(name = "ColliderMassProps", module = "rapier")] - #[derive(Clone)] - pub struct ColliderMassProps(pub rapier::geometry::ColliderMassProps); - - #[pymethods] - impl ColliderMassProps { - /// Build the `Density` variant from a uniform density value. - #[staticmethod] - #[pyo3(name = "Density")] - fn density(d: Real) -> Self { - Self(rapier::geometry::ColliderMassProps::Density(d)) - } - /// Build the `Mass` variant from an explicit mass value. - #[staticmethod] - #[pyo3(name = "Mass")] - fn mass(m: Real) -> Self { - Self(rapier::geometry::ColliderMassProps::Mass(m)) - } - /// Build the `MassProperties` variant from a full mass-properties value. - #[staticmethod] - #[pyo3(name = "MassProperties")] - fn mass_properties(mp: MassProperties) -> Self { - Self(rapier::geometry::ColliderMassProps::MassProperties( - Box::new(mp.0), - )) - } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __collider_apply_rotation_kwarg { - (3, $b:expr, $v:expr) => {{ - let pv: PyVector = $v.extract()?; - $b = $b.clone().rotation(pv.0.into()); - }}; - (2, $b:expr, $v:expr) => {{ - let f: Real = $v.extract()?; - $b = $b.clone().rotation(f); - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_collider_dim_specific { - (3) => { - #[pymethods] - impl Collider { - /// Builder for a 3D axis-aligned box (cuboid) collider. - /// - /// :param hx: Half-extent along the local X axis. - /// :param hy: Half-extent along the local Y axis. - /// :param hz: Half-extent along the local Z axis. - /// :param kwargs: Optional `ColliderBuilder` kwargs. - #[staticmethod] - #[pyo3(signature = (hx, hy, hz, **kwargs))] - fn cuboid( - hx: Real, - hy: Real, - hz: Real, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - ColliderBuilder::from_kwargs( - rapier::geometry::ColliderBuilder::cuboid(hx, hy, hz), - kwargs, - ) - } - /// Builder for a Y-axis cylinder collider. - #[staticmethod] - fn cylinder(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::cylinder(half_height, radius), - } - } - /// Builder for a Y-axis cone collider (apex at +Y). - #[staticmethod] - fn cone(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::cone(half_height, radius), - } - } - /// Builder for a Z-axis capsule collider. - #[staticmethod] - fn capsule_z(half_height: Real, radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::capsule_z(half_height, radius), - } - } - /// Builder for a 3D cuboid with rounded edges of `border_radius`. - #[staticmethod] - fn round_cuboid(hx: Real, hy: Real, hz: Real, border_radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_cuboid( - hx, - hy, - hz, - border_radius, - ), - } - } - /// Builder for a Y-axis cylinder with rounded edges. - #[staticmethod] - fn round_cylinder( - half_height: Real, - radius: Real, - border_radius: Real, - ) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_cylinder( - half_height, - radius, - border_radius, - ), - } - } - /// Builder for a Y-axis cone with rounded apex/base. - #[staticmethod] - fn round_cone(half_height: Real, radius: Real, border_radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_cone( - half_height, - radius, - border_radius, - ), - } - } - /// Builder for a convex-polyhedron collider (3D convex hull of `points`). - #[staticmethod] - fn convex_polyhedron(points: &Bound<'_, PyAny>) -> PyResult { - // 3D: convex_hull builds a ConvexPolyhedron internally; reuse it. - Self::convex_hull(points) - } - /// Builder for a compound collider obtained by convex - /// decomposition of a triangle mesh. - #[staticmethod] - #[pyo3(signature = (vertices, indices))] - fn convex_decomposition( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::convex_decomposition(&verts, &idx), - }) - } - /// Builder for a convex-mesh collider (vertices assumed convex). - /// - /// :raises MeshConversionError: If the mesh is not a valid convex mesh. - #[staticmethod] - fn convex_mesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - rapier::geometry::ColliderBuilder::convex_mesh(verts, &idx) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex mesh construction failed (invalid convex mesh)", - ) - }) - } - /// Builder for a rounded convex-mesh collider. - /// - /// :raises MeshConversionError: If the mesh is not a valid convex mesh. - #[staticmethod] - fn round_convex_mesh( - vertices: &Bound<'_, PyAny>, - indices: &Bound<'_, PyAny>, - border_radius: Real, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - rapier::geometry::ColliderBuilder::round_convex_mesh(verts, &idx, border_radius) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex mesh construction failed (invalid convex mesh)", - ) - }) - } - /// Builder for a voxel-grid collider from integer grid coordinates. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param grid_coords: Sequence of `(i, j, k)` integer cells. - #[staticmethod] - fn voxels( - voxel_size: PyVector, - grid_coords: &Bound<'_, PyAny>, - ) -> PyResult { - let raw: Vec<(i64, i64, i64)> = grid_coords.extract()?; - let coords: Vec = raw - .iter() - .map(|&(x, y, z)| rapier::math::IVector::new(x as _, y as _, z as _)) - .collect(); - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::voxels( - voxel_size.0.into(), - &coords, - ), - }) - } - /// Builder for a 3D heightfield collider. - /// - /// :param heights: `(rows, cols)` ndarray of floats. - /// :param scale: Per-axis scaling. - #[staticmethod] - fn heightfield( - heights: &Bound<'_, PyAny>, - scale: PyVector, - ) -> PyResult { - let arr: $crate::numpy::PyReadonlyArray2 = heights.extract()?; - let slice = arr.as_slice().map_err(|_| { - $crate::pyo3::exceptions::PyValueError::new_err( - "heights ndarray must be contiguous", - ) - })?; - let nrows = arr.shape()[0]; - let ncols = arr.shape()[1]; - let arr2 = rapier::parry::utils::Array2::new(nrows, ncols, slice.to_vec()); - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::heightfield(arr2, scale.0.into()), - }) - } - } - }; - (2) => { - #[pymethods] - impl Collider { - /// Builder for a 2D axis-aligned rectangle (cuboid) collider. - /// - /// :param hx: Half-extent along X. - /// :param hy: Half-extent along Y. - /// :param kwargs: Optional `ColliderBuilder` kwargs. - #[staticmethod] - #[pyo3(signature = (hx, hy, **kwargs))] - fn cuboid( - hx: Real, - hy: Real, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - ColliderBuilder::from_kwargs( - rapier::geometry::ColliderBuilder::cuboid(hx, hy), - kwargs, - ) - } - /// Builder for a 2D segment collider. - #[staticmethod] - fn segment(a: PyVector, b: PyVector) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::segment(a.0.into(), b.0.into()), - } - } - /// Builder for a 2D polyline collider. - /// - /// :param vertices: `(N, 2)` ndarray of floats. - /// :param indices: Optional `(M, 2)` segment indices. Defaults - /// to consecutive vertices. - #[staticmethod] - #[pyo3(signature = (vertices, indices=None))] - fn polyline( - vertices: &Bound<'_, PyAny>, - indices: Option<&Bound<'_, PyAny>>, - ) -> PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = match indices { - Some(i) => Some($crate::geometry::extract_indices_2(i)?), - None => None, - }; - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::polyline(verts, idx), - }) - } - /// Builder for a 2D convex polygon collider. - #[staticmethod] - fn convex_polygon(points: &Bound<'_, PyAny>) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::geometry::ColliderBuilder::convex_polyline(pts) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "convex polygon construction failed", - ) - }) - } - /// Builder for a 2D convex polyline collider (alias of - /// `convex_polygon`; points assumed convex). - #[staticmethod] - fn convex_polyline(points: &Bound<'_, PyAny>) -> PyResult { - Self::convex_polygon(points) - } - /// Builder for a 2D rounded convex polyline collider. - /// - /// :raises MeshConversionError: If the polyline is degenerate. - #[staticmethod] - fn round_convex_polyline( - points: &Bound<'_, PyAny>, - border_radius: Real, - ) -> PyResult { - let pts = extract_verts_for_dim(points)?; - rapier::geometry::ColliderBuilder::round_convex_polyline(pts, border_radius) - .map(|b| ColliderBuilder { builder: b }) - .ok_or_else(|| { - $crate::errors::MeshConversionError::new_err( - "round convex polyline construction failed", - ) - }) - } - /// Builder for a voxel-grid collider from integer grid coordinates. - /// - /// :param voxel_size: Per-axis size of one voxel cell. - /// :param grid_coords: Sequence of `(i, j)` integer cells. - #[staticmethod] - fn voxels( - voxel_size: PyVector, - grid_coords: &Bound<'_, PyAny>, - ) -> PyResult { - let raw: Vec<(i64, i64)> = grid_coords.extract()?; - let coords: Vec = raw - .iter() - .map(|&(x, y)| rapier::math::IVector::new(x as _, y as _)) - .collect(); - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::voxels( - voxel_size.0.into(), - &coords, - ), - }) - } - /// Builder for a 2D rectangle with rounded corners. - #[staticmethod] - fn round_cuboid(hx: Real, hy: Real, border_radius: Real) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_cuboid(hx, hy, border_radius), - } - } - /// Builder for a 2D heightfield collider. - #[staticmethod] - fn heightfield(heights: Vec, scale: PyVector) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::heightfield( - heights, - scale.0.into(), - ), - } - } - } - }; -} - -// ============================================================ -// register_geometry function -// ============================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_register { - (3) => { - pub fn register_geometry( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // Shape views - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; - (2) => { - pub fn register_geometry( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // Shape views (2D) - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __shape_type_from_rapier { - (3, $t:expr) => {{ - use rapier::parry::shape::ShapeType as PT; - match $t { - PT::Ball => ShapeType::BALL, - PT::Cuboid => ShapeType::CUBOID, - PT::Capsule => ShapeType::CAPSULE, - PT::Segment => ShapeType::SEGMENT, - PT::Triangle => ShapeType::TRIANGLE, - PT::Voxels => ShapeType::VOXELS, - PT::TriMesh => ShapeType::TRIMESH, - PT::Polyline => ShapeType::POLYLINE, - PT::HalfSpace => ShapeType::HALFSPACE, - PT::HeightField => ShapeType::HEIGHTFIELD, - PT::Compound => ShapeType::COMPOUND, - PT::ConvexPolyhedron => ShapeType::CONVEX_POLYHEDRON, - PT::Cylinder => ShapeType::CYLINDER, - PT::Cone => ShapeType::CONE, - PT::RoundCuboid => ShapeType::ROUND_CUBOID, - PT::RoundTriangle => ShapeType::ROUND_TRIANGLE, - PT::RoundCylinder => ShapeType::ROUND_CYLINDER, - PT::RoundCone => ShapeType::ROUND_CONE, - PT::RoundConvexPolyhedron => ShapeType::ROUND_CONVEX_POLYHEDRON, - _ => ShapeType::CUSTOM, - } - }}; - (2, $t:expr) => {{ - use rapier::parry::shape::ShapeType as PT; - match $t { - PT::Ball => ShapeType::BALL, - PT::Cuboid => ShapeType::CUBOID, - PT::Capsule => ShapeType::CAPSULE, - PT::Segment => ShapeType::SEGMENT, - PT::Triangle => ShapeType::TRIANGLE, - PT::Voxels => ShapeType::VOXELS, - PT::TriMesh => ShapeType::TRIMESH, - PT::Polyline => ShapeType::POLYLINE, - PT::HalfSpace => ShapeType::HALFSPACE, - PT::HeightField => ShapeType::HEIGHTFIELD, - PT::Compound => ShapeType::COMPOUND, - PT::ConvexPolygon => ShapeType::CONVEX_POLYGON, - PT::RoundCuboid => ShapeType::ROUND_CUBOID, - PT::RoundTriangle => ShapeType::ROUND_TRIANGLE, - PT::RoundConvexPolygon => ShapeType::ROUND_CONVEX_POLYGON, - _ => ShapeType::CUSTOM, - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_geometry_contact_point_to_py { - (3, $c:ident) => {{ - let p1: $crate::na::Vector3 = $c.local_p1.into(); - let p2: $crate::na::Vector3 = $c.local_p2.into(); - ContactData { - local_p1: Point3($crate::na::Point3::from(p1)), - local_p2: Point3($crate::na::Point3::from(p2)), - dist: $c.dist, - fid1: $c.fid1.0, - fid2: $c.fid2.0, - impulse: $c.data.impulse, - tangent_impulse: ($c.data.tangent_impulse[0], $c.data.tangent_impulse[1]), - contact_id: 0, - } - }}; - (2, $c:ident) => {{ - let p1: $crate::na::Vector2 = $c.local_p1.into(); - let p2: $crate::na::Vector2 = $c.local_p2.into(); - ContactData { - local_p1: Point2($crate::na::Point2::from(p1)), - local_p2: Point2($crate::na::Point2::from(p2)), - dist: $c.dist, - fid1: $c.fid1.0, - fid2: $c.fid2.0, - impulse: $c.data.impulse, - tangent_impulse: $c.data.tangent_impulse[0], - contact_id: 0, - } - }}; -} diff --git a/python/rapier-py-core/src/joints.rs b/python/rapier-py-core/src/joints.rs deleted file mode 100644 index d5f3b8199..000000000 --- a/python/rapier-py-core/src/joints.rs +++ /dev/null @@ -1,4249 +0,0 @@ -//! Macro emitting the user-facing joint `#[pyclass]`-es per cdylib. -//! -//! Joints. -//! -//! Like the other modules, this macro is invoked once per cdylib *after* -//! `define_conv_types!`, `define_math_types!` and `define_geometry_types!` and -//! *before* `define_dynamics_types!`. It depends on: -//! - The `Real`/`DIM` aliases and `Py{Vector,Point,Rotation,Isometry,AngVector}` -//! adapter types (from `define_conv_types!`). -//! - The user-facing math `#[pyclass]`-es (`Vec*`, `Point*`, `Rotation*`, -//! `Isometry*`) (from `define_math_types!`). -//! - `use rapier{2,3}d{,_f64} as rapier;`. -//! -//! Note: `RigidBodyHandle` is *not* in scope when this macro is invoked -//! (dynamics is defined later), so this module receives `RigidBodyHandle` -//! pieces via the engine type `rapier::dynamics::RigidBodyHandle` and only -//! references the wrapper at *register* time (Python-level resolution). -//! -//! `define_joints_types!` produces `register_joints(py, m)` for the -//! `#[pymodule]` entry point. - -/// Materialize the joint `#[pyclass]` types for a given `(Real, DIM)` pair. -#[macro_export] -macro_rules! define_joints_types { - (DIM = 2) => { - $crate::__define_joints_shared!(2, Vec2, Point2, Rotation2, Isometry2); - $crate::__define_joints_2d!(Vec2, Point2, Rotation2, Isometry2); - $crate::__define_joints_register!(2); - }; - (DIM = 3) => { - $crate::__define_joints_shared!(3, Vec3, Point3, Rotation3, Isometry3); - $crate::__define_joints_3d!(Vec3, Point3, Rotation3, Isometry3); - $crate::__define_joints_register!(3); - }; -} - -// ==================================================================== -// Shared dim-agnostic joint types -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joints_shared { - ($dim:tt, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // ----------------------------------------------------------------- - // Note on cross-macro references - // ----------------------------------------------------------------- - // The `ImpulseJointSet` and `MultibodyJointSet` types emitted by - // this macro reference `RigidBodyHandle` and `RigidBodySet`, which - // are defined later by `define_dynamics_types!`. Rust resolves - // those names at item-resolution time (after *all* macros have - // expanded into the same module), so referring to them here is - // valid even though the dynamics macro is invoked after this one. - - // ----------------------------------------------------------------- - // JointEnabled enum - // ----------------------------------------------------------------- - /// Tri-state enable flag carried by every joint. - /// - /// ``ENABLED`` joints take part in constraint resolution. - /// ``DISABLED`` joints are kept in the set but ignored by the solver - /// (use this to temporarily turn a joint off without removing it). - /// ``DISABLED_BY_ATTACHED_BODY`` is set automatically by the engine - /// when one of the bodies the joint is attached to becomes disabled; - /// the joint re-enables itself once the body is re-enabled. - #[pyclass(name = "JointEnabled", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum JointEnabled { - /// Joint participates normally in the constraint solver. - ENABLED, - /// Joint is implicitly disabled because one of its attached - /// rigid bodies is itself disabled. Set automatically by the - /// engine and cleared once the body is re-enabled. - DISABLED_BY_ATTACHED_BODY, - /// Joint is explicitly disabled by the user and ignored by the - /// solver until re-enabled. - DISABLED, - } - - impl JointEnabled { - #[inline] - fn from_rapier(e: rapier::dynamics::JointEnabled) -> Self { - match e { - rapier::dynamics::JointEnabled::Enabled => Self::ENABLED, - rapier::dynamics::JointEnabled::DisabledByAttachedBody => { - Self::DISABLED_BY_ATTACHED_BODY - } - rapier::dynamics::JointEnabled::Disabled => Self::DISABLED, - } - } - #[allow(dead_code)] - #[inline] - fn to_rapier(self) -> rapier::dynamics::JointEnabled { - match self { - Self::ENABLED => rapier::dynamics::JointEnabled::Enabled, - Self::DISABLED_BY_ATTACHED_BODY => { - rapier::dynamics::JointEnabled::DisabledByAttachedBody - } - Self::DISABLED => rapier::dynamics::JointEnabled::Disabled, - } - } - } - - // ----------------------------------------------------------------- - // MotorModel enum - // ----------------------------------------------------------------- - /// Motor model used to convert PID error into a drive force. - /// - /// Both variants implement an internal PD controller around the - /// motor's ``(target_pos, target_vel, stiffness, damping)`` set - /// point. They differ in how that controller output is applied to - /// the bodies: - /// - /// * ``ACCELERATION_BASED`` interprets the controller output as a - /// desired acceleration and scales it by the effective mass of - /// the constrained axis. This gives motor behaviour that is - /// largely independent of the masses of the bodies and is the - /// recommended default. - /// * ``FORCE_BASED`` applies the controller output directly as a - /// generalized force; the resulting acceleration depends on the - /// effective mass. - /// - /// Note: a third ``VelocityBased`` variant existed in earlier - /// versions but was removed. To drive a pure velocity target, - /// configure a motor with ``stiffness=0`` and the desired - /// ``target_vel`` (see :py:meth:`set_motor_velocity`). - #[pyclass(name = "MotorModel", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum MotorModel { - /// PID output is interpreted as a desired acceleration; the - /// engine scales by effective mass before applying. Behaviour - /// is largely mass-independent. - ACCELERATION_BASED, - /// PID output is applied directly as a generalized force; the - /// resulting motion depends on the bodies' masses and inertia. - FORCE_BASED, - } - - impl MotorModel { - #[inline] - fn from_rapier(m: rapier::dynamics::MotorModel) -> Self { - match m { - rapier::dynamics::MotorModel::AccelerationBased => Self::ACCELERATION_BASED, - rapier::dynamics::MotorModel::ForceBased => Self::FORCE_BASED, - } - } - #[inline] - fn to_rapier(self) -> rapier::dynamics::MotorModel { - match self { - Self::ACCELERATION_BASED => rapier::dynamics::MotorModel::AccelerationBased, - Self::FORCE_BASED => rapier::dynamics::MotorModel::ForceBased, - } - } - } - - // ----------------------------------------------------------------- - // JointLimits — plain struct - // ----------------------------------------------------------------- - /// Inclusive ``[min, max]`` range that a joint axis is constrained to. - /// - /// Units are axis-dependent: world-length units for linear axes - /// (``LIN_X``/``LIN_Y``/``LIN_Z``) and radians for angular axes - /// (``ANG_X``/``ANG_Y``/``ANG_Z``). ``impulse`` is the last - /// constraint impulse applied by the solver to enforce the limit - /// and is set by the engine, not the user. - #[pyclass(name = "JointLimits", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct JointLimits { - #[pyo3(get, set)] pub min: Real, - #[pyo3(get, set)] pub max: Real, - #[pyo3(get, set)] pub impulse: Real, - } - - #[pymethods] - impl JointLimits { - /// Construct a new limit range. - /// - /// :param min: Lower bound (radians for angular axes, world - /// units for linear axes). - /// :param max: Upper bound, same units as ``min``. - /// :param impulse: Initial impulse value, generally left at 0. - #[new] - #[pyo3(signature = (min=0.0 as Real, max=0.0 as Real, impulse=0.0 as Real))] - fn new(min: Real, max: Real, impulse: Real) -> Self { - Self { min, max, impulse } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!("JointLimits(min={}, max={}, impulse={})", - self.min, self.max, self.impulse) - } - } - - impl JointLimits { - #[inline] - fn from_rapier(l: &rapier::dynamics::JointLimits) -> Self { - Self { min: l.min, max: l.max, impulse: l.impulse } - } - } - - // ----------------------------------------------------------------- - // JointMotor — plain struct - // ----------------------------------------------------------------- - /// Motor configuration driving a single joint axis. - /// - /// The motor implements a PD controller of the form - /// ``force = stiffness * (target_pos - pos) + damping * (target_vel - vel)``, - /// optionally clamped by ``max_force``. The ``model`` field selects - /// how that force is converted into joint motion (see - /// :class:`MotorModel`). ``impulse`` is the impulse applied on the - /// last solver step and is written by the engine. - /// - /// Convenience helpers on each concrete joint type (e.g. - /// ``set_motor_velocity``, ``set_motor_position``, ``set_motor``) - /// build the right combination of fields without constructing this - /// struct directly. - #[pyclass(name = "JointMotor", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct JointMotor { - #[pyo3(get, set)] pub target_vel: Real, - #[pyo3(get, set)] pub target_pos: Real, - #[pyo3(get, set)] pub stiffness: Real, - #[pyo3(get, set)] pub damping: Real, - #[pyo3(get, set)] pub max_force: Real, - #[pyo3(get, set)] pub impulse: Real, - #[pyo3(get, set)] pub model: MotorModel, - } - - #[pymethods] - impl JointMotor { - /// Construct a new motor specification. - /// - /// :param target_vel: Velocity setpoint of the PD controller. - /// :param target_pos: Position setpoint of the PD controller. - /// :param stiffness: Position-error gain (spring term). - /// :param damping: Velocity-error gain (damper term). - /// :param max_force: Hard clamp on the generalized force the - /// motor may apply. - /// :param impulse: Last-step impulse, generally left at 0 on - /// construction. - /// :param model: Motor model (see :class:`MotorModel`). - #[new] - #[pyo3(signature = ( - target_vel=0.0 as Real, target_pos=0.0 as Real, - stiffness=0.0 as Real, damping=0.0 as Real, - max_force=Real::MAX, impulse=0.0 as Real, - model=MotorModel::ACCELERATION_BASED, - ))] - fn new( - target_vel: Real, target_pos: Real, - stiffness: Real, damping: Real, - max_force: Real, impulse: Real, - model: MotorModel, - ) -> Self { - Self { target_vel, target_pos, stiffness, damping, max_force, impulse, model } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!( - "JointMotor(target_pos={}, target_vel={}, stiffness={}, damping={}, max_force={}, model={:?})", - self.target_pos, self.target_vel, self.stiffness, self.damping, - self.max_force, self.model, - ) - } - } - - impl JointMotor { - #[inline] - fn from_rapier(m: &rapier::dynamics::JointMotor) -> Self { - Self { - target_vel: m.target_vel, - target_pos: m.target_pos, - stiffness: m.stiffness, - damping: m.damping, - max_force: m.max_force, - impulse: m.impulse, - model: MotorModel::from_rapier(m.model), - } - } - } - - // ----------------------------------------------------------------- - // JointAxesMask bitflags - // ----------------------------------------------------------------- - /// Bitflag set selecting a subset of joint axes. - /// - /// In 3D the available bits are ``LIN_X``, ``LIN_Y``, ``LIN_Z``, - /// ``ANG_X``, ``ANG_Y``, ``ANG_Z`` plus convenience aggregates - /// (``LIN_AXES``, ``ANG_AXES``, ``LOCKED_REVOLUTE_AXES``, - /// ``LOCKED_PRISMATIC_AXES``, ``LOCKED_FIXED_AXES``, - /// ``LOCKED_SPHERICAL_AXES``, and their ``FREE_*`` complements). - /// In 2D only ``LIN_X``, ``LIN_Y`` and ``ANG_X`` exist, along with - /// ``LOCKED_PIN_SLOT_AXES``. - /// - /// The class supports the usual bitwise operators (``|``, ``&``, - /// ``^``, ``~``, ``-``), ``in``, truthiness, and equality. Used - /// throughout :class:`GenericJoint` to describe locked / coupled - /// axes, and by :class:`InverseKinematicsOption` to restrict the - /// IK solver to a subset of axes. - #[pyclass(name = "JointAxesMask", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct JointAxesMask(pub rapier::dynamics::JointAxesMask); - - #[pymethods] - impl JointAxesMask { - /// Construct a mask from a raw bit pattern. - /// - /// :param bits: Raw byte; unknown bits are truncated. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier::dynamics::JointAxesMask::from_bits_truncate(bits)) - } - /// Return the empty mask (no axes selected). - #[staticmethod] - fn empty() -> Self { Self(rapier::dynamics::JointAxesMask::empty()) } - /// Return the full mask (every axis selected). - #[staticmethod] - fn all_axes() -> Self { Self(rapier::dynamics::JointAxesMask::all()) } - - // Per-axis classattrs come from the dim-specific helper to keep - // dim2 / dim3 distinct. - #[classattr] - const LIN_X: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LIN_X); - #[classattr] - const LIN_Y: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LIN_Y); - #[classattr] - const ANG_X: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::ANG_X); - #[classattr] - const LIN_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LIN_AXES); - #[classattr] - const ANG_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::ANG_AXES); - #[classattr] - const LOCKED_REVOLUTE_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_REVOLUTE_AXES); - #[classattr] - const LOCKED_PRISMATIC_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_PRISMATIC_AXES); - #[classattr] - const LOCKED_FIXED_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_FIXED_AXES); - #[classattr] - const FREE_REVOLUTE_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::FREE_REVOLUTE_AXES); - #[classattr] - const FREE_PRISMATIC_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::FREE_PRISMATIC_AXES); - #[classattr] - const FREE_FIXED_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::FREE_FIXED_AXES); - - /// Raw bit value backing this mask. - #[getter] fn bits(&self) -> u8 { self.0.bits() } - /// Return ``True`` if every axis in ``other`` is also set here. - fn contains(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Return ``True`` if no axes are selected. - fn is_empty(&self) -> bool { self.0.is_empty() } - /// ``other in self`` — same as :py:meth:`contains`. - fn __contains__(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Bitwise OR (set union). - fn __or__(&self, other: &Self) -> Self { Self(self.0 | other.0) } - /// Bitwise AND (set intersection). - fn __and__(&self, other: &Self) -> Self { Self(self.0 & other.0) } - /// Bitwise XOR (set symmetric difference). - fn __xor__(&self, other: &Self) -> Self { Self(self.0 ^ other.0) } - /// Bitwise NOT (complement within the known axes). - fn __invert__(&self) -> Self { Self(!self.0) } - /// Set difference (``self - other``). - fn __sub__(&self, other: &Self) -> Self { Self(self.0 - other.0) } - /// Truthiness: ``True`` iff at least one axis is selected. - fn __bool__(&self) -> bool { !self.0.is_empty() } - /// Equality and inequality comparisons; other operators raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("JointAxesMask supports only == and !=")), - } - } - /// Hash matching equality semantics. - fn __hash__(&self) -> u64 { self.0.bits() as u64 } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!("JointAxesMask(bits={:#010b})", self.0.bits()) - } - } - - // ----------------------------------------------------------------- - // JointAxis enum (dim-specific variants) - // ----------------------------------------------------------------- - $crate::__define_joint_axis!($dim); - - impl JointAxis { - #[inline] - fn to_rapier(self) -> rapier::dynamics::JointAxis { - $crate::__joint_axis_to_rapier!($dim, self) - } - } - - // 3D-only JointAxesMask classattrs (LIN_Z, ANG_Y, ANG_Z, plus - // LOCKED_SPHERICAL_AXES & FREE_SPHERICAL_AXES). 2D doesn't define - // those bits in the rapier::dynamics::JointAxesMask bitflags, so - // referencing them is gated by `$dim`. - $crate::__extra_joint_axes_mask_classattrs!($dim); - - // ================================================================= - // ImpulseJointHandle - // ================================================================= - /// Opaque handle identifying a joint inside an :class:`ImpulseJointSet`. - /// - /// Hashable and equality-comparable, so it can be used as a dict - /// key or stored in sets. Handles remain valid until the joint is - /// removed; a removed slot may be reused with a new ``generation``. - #[pyclass(name = "ImpulseJointHandle", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct ImpulseJointHandle(pub rapier::dynamics::ImpulseJointHandle); - - #[pymethods] - impl ImpulseJointHandle { - /// Build a handle from raw ``(index, generation)`` parts. - /// - /// Mostly useful for round-tripping a handle through - /// serialization; in normal use, handles are returned by - /// :py:meth:`ImpulseJointSet.insert`. - #[new] - #[pyo3(signature = (index=0, generation=0))] - fn new(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::ImpulseJointHandle::from_raw_parts(index, generation)) - } - /// Reconstruct a handle from raw ``(index, generation)`` parts. - #[staticmethod] - fn from_raw_parts(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::ImpulseJointHandle::from_raw_parts(index, generation)) - } - /// Return the sentinel "invalid" handle that points to nothing. - #[staticmethod] - fn invalid() -> Self { - Self(rapier::dynamics::ImpulseJointHandle::invalid()) - } - /// Slot index of this handle. - #[getter] - fn index(&self) -> u32 { self.0.into_raw_parts().0 } - /// Generation counter, incremented when a slot is reused. - #[getter] - fn generation(&self) -> u32 { self.0.into_raw_parts().1 } - /// Hash matching equality semantics. - fn __hash__(&self) -> u64 { - let (i, g) = self.0.into_raw_parts(); - ((i as u64) << 32) | (g as u64) - } - /// Equality and inequality comparisons; other operators raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("ImpulseJointHandle supports only == and !=")), - } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - let (i, g) = self.0.into_raw_parts(); - format!("ImpulseJointHandle(index={}, generation={})", i, g) - } - } - - // ================================================================= - // MultibodyJointHandle - // ================================================================= - /// Opaque handle identifying a joint inside a :class:`MultibodyJointSet`. - /// - /// Hashable and equality-comparable. A multibody joint handle - /// references the joint between a multibody link and its parent - /// inside one reduced-coordinate articulation. - #[pyclass(name = "MultibodyJointHandle", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct MultibodyJointHandle(pub rapier::dynamics::MultibodyJointHandle); - - #[pymethods] - impl MultibodyJointHandle { - /// Build a handle from raw ``(index, generation)`` parts. - #[new] - #[pyo3(signature = (index=0, generation=0))] - fn new(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::MultibodyJointHandle::from_raw_parts(index, generation)) - } - /// Reconstruct a handle from raw ``(index, generation)`` parts. - #[staticmethod] - fn from_raw_parts(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::MultibodyJointHandle::from_raw_parts(index, generation)) - } - /// Return the sentinel "invalid" handle that points to nothing. - #[staticmethod] - fn invalid() -> Self { - Self(rapier::dynamics::MultibodyJointHandle::invalid()) - } - /// Slot index of this handle. - #[getter] - fn index(&self) -> u32 { self.0.into_raw_parts().0 } - /// Generation counter, incremented when a slot is reused. - #[getter] - fn generation(&self) -> u32 { self.0.into_raw_parts().1 } - /// Hash matching equality semantics. - fn __hash__(&self) -> u64 { - let (i, g) = self.0.into_raw_parts(); - ((i as u64) << 32) | (g as u64) - } - /// Equality and inequality comparisons; other operators raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("MultibodyJointHandle supports only == and !=")), - } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - let (i, g) = self.0.into_raw_parts(); - format!("MultibodyJointHandle(index={}, generation={})", i, g) - } - } - - // ================================================================= - // MultibodyIndex (opaque) - // ================================================================= - /// Opaque handle identifying a single :class:`Multibody` articulation. - /// - /// Hashable and equality-comparable. Multiple - /// :class:`MultibodyJointHandle` instances can resolve to the same - /// ``MultibodyIndex`` when they belong to the same articulation. - #[pyclass(name = "MultibodyIndex", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct MultibodyIndex(pub rapier::dynamics::MultibodyIndex); - - #[pymethods] - impl MultibodyIndex { - /// Slot index of this multibody. - #[getter] - fn index(&self) -> u32 { self.0.0.into_raw_parts().0 } - /// Generation counter, incremented when a slot is reused. - #[getter] - fn generation(&self) -> u32 { self.0.0.into_raw_parts().1 } - /// Hash matching equality semantics. - fn __hash__(&self) -> u64 { - let (i, g) = self.0.0.into_raw_parts(); - ((i as u64) << 32) | (g as u64) - } - /// Equality and inequality comparisons; other operators raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("MultibodyIndex supports only == and !=")), - } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - let (i, g) = self.0.0.into_raw_parts(); - format!("MultibodyIndex(index={}, generation={})", i, g) - } - } - - // ================================================================= - // MultibodyLinkId — opaque struct with `multibody`, `id` - // ================================================================= - /// Address of a single link within a multibody articulation. - /// - /// Combines a :class:`MultibodyIndex` (which articulation) with an - /// integer ``id`` (which link inside it). Hashable and - /// equality-comparable. - #[pyclass(name = "MultibodyLinkId", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct MultibodyLinkId(pub rapier::dynamics::MultibodyLinkId); - - #[pymethods] - impl MultibodyLinkId { - /// Articulation containing this link. - #[getter] - fn multibody(&self) -> MultibodyIndex { MultibodyIndex(self.0.multibody) } - /// Zero-based link index inside the articulation (0 == root). - #[getter] - fn id(&self) -> usize { self.0.id } - /// Equality and inequality comparisons; other operators raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("MultibodyLinkId supports only == and !=")), - } - } - /// Hash matching equality semantics. - fn __hash__(&self) -> u64 { - let (i, g) = self.0.multibody.0.into_raw_parts(); - (((i as u64) << 32) | (g as u64)) ^ (self.0.id as u64) - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - let (i, g) = self.0.multibody.0.into_raw_parts(); - format!( - "MultibodyLinkId(multibody=MultibodyIndex(index={}, generation={}), id={})", - i, g, self.0.id - ) - } - } - - // ================================================================= - // InverseKinematicsOption — dataclass-style - // ================================================================= - /// Tuning parameters for the multibody inverse-kinematics solver. - /// - /// IK is performed by damped least-squares: at each iteration the - /// solver builds the Jacobian of the link's pose w.r.t. the joint - /// coordinates and solves a damped linear system for the - /// per-iteration displacement, applying it in a loop until either - /// the residual is below the linear/angular epsilons or - /// ``max_iters`` is reached. - /// - /// :ivar damping: Levenberg-Marquardt damping term added to the - /// normal equations; larger values give a slower but more - /// stable solve near singular postures. - /// :ivar max_iters: Upper bound on the iteration count. - /// :ivar constrained_axes: Subset of axes the IK target should - /// match. By default all axes are constrained. - /// :ivar epsilon_linear: Linear residual tolerance (world units). - /// :ivar epsilon_angular: Angular residual tolerance (radians). - #[pyclass(name = "InverseKinematicsOption", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct InverseKinematicsOption { - #[pyo3(get, set)] pub damping: Real, - #[pyo3(get, set)] pub max_iters: usize, - #[pyo3(get, set)] pub constrained_axes: JointAxesMask, - #[pyo3(get, set)] pub epsilon_linear: Real, - #[pyo3(get, set)] pub epsilon_angular: Real, - } - - #[pymethods] - impl InverseKinematicsOption { - /// Build a new IK option block. - /// - /// :param damping: Damping factor for the damped least-squares - /// solve. Defaults to 1.0. - /// :param max_iters: Maximum number of iterations. Defaults to 10. - /// :param constrained_axes: Axes to match against the target. - /// Defaults to all axes. - /// :param epsilon_linear: Convergence tolerance for the - /// translational residual (world units). Defaults to 1e-3. - /// :param epsilon_angular: Convergence tolerance for the - /// rotational residual (radians). Defaults to 1e-3. - #[new] - #[pyo3(signature = ( - damping=1.0 as Real, max_iters=10usize, - constrained_axes=None, - epsilon_linear=1.0e-3 as Real, epsilon_angular=1.0e-3 as Real, - ))] - fn new( - damping: Real, max_iters: usize, - constrained_axes: Option, - epsilon_linear: Real, epsilon_angular: Real, - ) -> Self { - let ca = constrained_axes - .unwrap_or_else(|| JointAxesMask(rapier::dynamics::JointAxesMask::all())); - Self { - damping, max_iters, constrained_axes: ca, - epsilon_linear, epsilon_angular, - } - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!( - "InverseKinematicsOption(damping={}, max_iters={}, epsilon_linear={}, epsilon_angular={})", - self.damping, self.max_iters, self.epsilon_linear, self.epsilon_angular - ) - } - } - - impl InverseKinematicsOption { - #[inline] - fn to_rapier(&self) -> rapier::dynamics::InverseKinematicsOption { - rapier::dynamics::InverseKinematicsOption { - damping: self.damping, - max_iters: self.max_iters, - constrained_axes: self.constrained_axes.0, - epsilon_linear: self.epsilon_linear, - epsilon_angular: self.epsilon_angular, - } - } - } - }; -} - -// ==================================================================== -// JointAxis enum (dim-specific because 2D has fewer variants) -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joint_axis { - (2) => { - /// Enumeration of the degrees of freedom of a 2D joint. - /// - /// In 2D there are three axes: two linear (``LIN_X``, ``LIN_Y``) - /// and one angular (``ANG_X``, the rotation around the implicit - /// out-of-plane axis). Pair with :class:`JointAxesMask` for set - /// operations. - #[pyclass(name = "JointAxis", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum JointAxis { - /// Translation along the body-local X axis. - LIN_X, - /// Translation along the body-local Y axis. - LIN_Y, - /// Rotation around the implicit out-of-plane axis. - ANG_X, - } - }; - (3) => { - /// Enumeration of the degrees of freedom of a 3D joint. - /// - /// In 3D there are six axes: three linear - /// (``LIN_X``/``LIN_Y``/``LIN_Z``) and three angular - /// (``ANG_X``/``ANG_Y``/``ANG_Z``) expressed in the joint's local - /// frame. Pair with :class:`JointAxesMask` for set operations. - #[pyclass(name = "JointAxis", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum JointAxis { - /// Translation along the body-local X axis. - LIN_X, - /// Translation along the body-local Y axis. - LIN_Y, - /// Translation along the body-local Z axis. - LIN_Z, - /// Rotation around the body-local X axis. - ANG_X, - /// Rotation around the body-local Y axis. - ANG_Y, - /// Rotation around the body-local Z axis. - ANG_Z, - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __extra_joint_axes_mask_classattrs { - (2) => { - #[pymethods] - impl JointAxesMask { - #[classattr] - const LOCKED_PIN_SLOT_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_PIN_SLOT_AXES); - } - }; - (3) => { - #[pymethods] - impl JointAxesMask { - #[classattr] - const LIN_Z: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::LIN_Z); - #[classattr] - const ANG_Y: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_Y); - #[classattr] - const ANG_Z: JointAxesMask = JointAxesMask(rapier::dynamics::JointAxesMask::ANG_Z); - #[classattr] - const LOCKED_SPHERICAL_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::LOCKED_SPHERICAL_AXES); - #[classattr] - const FREE_SPHERICAL_AXES: JointAxesMask = - JointAxesMask(rapier::dynamics::JointAxesMask::FREE_SPHERICAL_AXES); - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __joint_axis_to_rapier { - (2, $val:expr) => { - match $val { - JointAxis::LIN_X => rapier::dynamics::JointAxis::LinX, - JointAxis::LIN_Y => rapier::dynamics::JointAxis::LinY, - JointAxis::ANG_X => rapier::dynamics::JointAxis::AngX, - } - }; - (3, $val:expr) => { - match $val { - JointAxis::LIN_X => rapier::dynamics::JointAxis::LinX, - JointAxis::LIN_Y => rapier::dynamics::JointAxis::LinY, - JointAxis::LIN_Z => rapier::dynamics::JointAxis::LinZ, - JointAxis::ANG_X => rapier::dynamics::JointAxis::AngX, - JointAxis::ANG_Y => rapier::dynamics::JointAxis::AngY, - JointAxis::ANG_Z => rapier::dynamics::JointAxis::AngZ, - } - }; -} - -// ==================================================================== -// 3D-specific joint types: Revolute axis arg, SphericalJoint, etc. -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joints_3d { - ($Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - $crate::__define_joints_common!(3, $Vec, $Point, $Rot, $Iso); - - /// Dim-specific generic-joint extractor invoked by the shared - /// `__extract_generic_joint!` macro when none of the dim-agnostic - /// variants match. In 3D this handles `SphericalJoint`. - fn try_extract_dim_specific_joint( - obj: &Bound<'_, PyAny>, - ) -> PyResult> { - if let Ok(j) = obj.extract::>() { - return Ok(Some(j.0.into())); - } - if let Ok(b) = obj.extract::>() { - return Ok(Some(b.0.build().into())); - } - Ok(None) - } - - // ----------------------------------------------------------------- - // SphericalJoint + SphericalJointBuilder (3D only) - // ----------------------------------------------------------------- - /// Ball-and-socket joint pinning two bodies at a shared point. - /// - /// All three translational DOFs are locked while all three - /// rotational DOFs remain free, so the two bodies coincide at the - /// anchor point but may rotate freely relative to each other. - /// Available only in 3D. - #[pyclass(name = "SphericalJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct SphericalJoint(pub rapier::dynamics::SphericalJoint); - - #[pymethods] - impl SphericalJoint { - /// Build a new spherical joint with default anchors and frames. - #[new] - fn new() -> Self { - Self(rapier::dynamics::SphericalJoint::new()) - } - - /// Build a :class:`SphericalJointBuilder`. - /// - /// :param body_a: Ignored (kept for API symmetry with other - /// joint builders). - /// :param body_b: Ignored (kept for API symmetry). - /// :param kwargs: Optional keyword args forwarded to the - /// builder; accepted keys are ``local_anchor1``, - /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, - /// ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] - fn builder( - py: Python<'_>, - body_a: Option<&Bound<'_, PyAny>>, - body_b: Option<&Bound<'_, PyAny>>, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let _ = (py, body_a, body_b); - SphericalJointBuilder::from_kwargs( - rapier::dynamics::SphericalJointBuilder::new(), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { - self.0.data.contacts_enabled - } - /// Enable or disable collision detection between the attached - /// bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { - self.0.data.set_contacts_enabled(v); - } - - /// Body-local point on body 1 where the joint is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::Vector3 = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the joint is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::Vector3 = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Body-local frame on body 1 (position + orientation). - #[getter] - fn local_frame1(&self) -> $Iso { - let pose: $crate::na::Isometry = (*self.0.local_frame1()).into(); - $Iso(pose) - } - /// Set the body-local frame on body 1. - #[setter] - fn set_local_frame1(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.0.set_local_frame1(p); - } - /// Body-local frame on body 2. - #[getter] - fn local_frame2(&self) -> $Iso { - let pose: $crate::na::Isometry = (*self.0.local_frame2()).into(); - $Iso(pose) - } - /// Set the body-local frame on body 2. - #[setter] - fn set_local_frame2(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.0.set_local_frame2(p); - } - - /// Return the motor configured on ``axis``, if any. - fn motor(&self, axis: JointAxis) -> Option { - self.0.motor(axis.to_rapier()).map(JointMotor::from_rapier) - } - /// Return the limits configured on ``axis``, if any. - fn limits(&self, axis: JointAxis) -> Option { - self.0 - .limits(axis.to_rapier()) - .map(JointLimits::from_rapier) - } - /// Set angular limits on ``axis`` (radians; only angular - /// axes are meaningful on a spherical joint). - fn set_limits(&mut self, axis: JointAxis, min: Real, max: Real) { - self.0.set_limits(axis.to_rapier(), [min, max]); - } - /// Configure ``axis`` as a velocity-target motor with damping. - /// - /// :param target_vel: Desired velocity (rad/s for angular axes). - /// :param factor: Damping coefficient. - fn set_motor_velocity(&mut self, axis: JointAxis, target_vel: Real, factor: Real) { - self.0 - .set_motor_velocity(axis.to_rapier(), target_vel, factor); - } - /// Configure ``axis`` as a position-target spring-damper motor. - /// - /// :param target_pos: Desired position (radians). - /// :param stiffness: Spring gain. - /// :param damping: Damper gain. - fn set_motor_position( - &mut self, - axis: JointAxis, - target_pos: Real, - stiffness: Real, - damping: Real, - ) { - self.0 - .set_motor_position(axis.to_rapier(), target_pos, stiffness, damping); - } - /// Fully configure the motor on ``axis``. - /// - /// :param target_pos: Position setpoint (radians). - /// :param target_vel: Velocity setpoint (rad/s). - /// :param stiffness: Spring gain. - /// :param damping: Damper gain. - fn set_motor( - &mut self, - axis: JointAxis, - target_pos: Real, - target_vel: Real, - stiffness: Real, - damping: Real, - ) { - self.0 - .set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping); - } - /// Clamp the maximum force the motor on ``axis`` can apply. - fn set_motor_max_force(&mut self, axis: JointAxis, max_force: Real) { - self.0.set_motor_max_force(axis.to_rapier(), max_force); - } - /// Select the motor model on ``axis`` (see :class:`MotorModel`). - fn set_motor_model(&mut self, axis: JointAxis, model: MotorModel) { - self.0.set_motor_model(axis.to_rapier(), model.to_rapier()); - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { - GenericJoint::new_owned(self.0.data) - } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { - SpringCoefficients(self.0.softness()) - } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { - let _ = self.0.set_softness(v.0); - } - } - - /// Fluent builder for :class:`SphericalJoint`. - /// - /// Modifier methods return a new builder, mirroring the Rust API. - /// Call :py:meth:`build` to produce a :class:`SphericalJoint`. - #[pyclass(name = "SphericalJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct SphericalJointBuilder(pub rapier::dynamics::SphericalJointBuilder); - - impl SphericalJointBuilder { - fn from_kwargs( - base: rapier::dynamics::SphericalJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "local_frame1" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame1(p); - } - "local_frame2" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame2(p); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => { - return Err(PyTypeError::new_err(format!( - "unknown SphericalJointBuilder kwarg: '{}'", - key - ))); - } - } - } - } - Ok(me) - } - } - - #[pymethods] - impl SphericalJointBuilder { - /// Construct an empty :class:`SphericalJointBuilder`. - #[new] - fn new() -> Self { - Self(rapier::dynamics::SphericalJointBuilder::new()) - } - - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Set the body-local frame (position + orientation) on body 1. - fn local_frame1(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame1(p)) - } - /// Set the body-local frame on body 2. - fn local_frame2(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame2(p)) - } - /// Enable or disable contacts between the bodies attached by - /// the joint. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Configure a velocity-target motor with damping on ``axis``. - /// - /// :param target_vel: Velocity setpoint (rad/s for angular axes). - /// :param factor: Damping coefficient. - fn motor_velocity(&self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { - Self(self.0.motor_velocity(axis.to_rapier(), target_vel, factor)) - } - /// Configure a spring-damper motor toward ``target_pos`` on - /// ``axis``. - /// - /// :param target_pos: Position setpoint (radians). - /// :param stiffness: Spring gain. - /// :param damping: Damper gain. - fn motor_position( - &self, - axis: JointAxis, - target_pos: Real, - stiffness: Real, - damping: Real, - ) -> Self { - Self( - self.0 - .motor_position(axis.to_rapier(), target_pos, stiffness, damping), - ) - } - /// Fully configure the motor on ``axis`` with both position and - /// velocity setpoints. - fn motor( - &self, - axis: JointAxis, - target_pos: Real, - target_vel: Real, - stiffness: Real, - damping: Real, - ) -> Self { - Self( - self.0 - .motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping), - ) - } - /// Clamp the maximum force the motor on ``axis`` can apply. - fn motor_max_force(&self, axis: JointAxis, max_force: Real) -> Self { - Self(self.0.motor_max_force(axis.to_rapier(), max_force)) - } - /// Select the motor model on ``axis`` (see :class:`MotorModel`). - fn motor_model(&self, axis: JointAxis, model: MotorModel) -> Self { - Self(self.0.motor_model(axis.to_rapier(), model.to_rapier())) - } - /// Apply ``[min, max]`` limits on ``axis`` (radians). - fn limits(&self, axis: JointAxis, min: Real, max: Real) -> Self { - Self(self.0.limits(axis.to_rapier(), [min, max])) - } - /// Materialize the builder into a :class:`SphericalJoint`. - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { - Self(self.0.softness(v.0)) - } - /// Materialize the builder into a :class:`SphericalJoint`. - fn build(&self) -> SphericalJoint { - SphericalJoint(self.0.build()) - } - } - - // 3D RevoluteJoint constructor / builder need axis argument. - // The shared common macro emits the wrapper bodies; here we just - // expose the 3D constructors as additional impls. - #[pymethods] - impl RevoluteJoint { - /// Construct a 3D revolute joint with an explicit axis. - /// - /// :param axis: Rotation axis expressed in each body's local - /// frame. - #[staticmethod] - #[pyo3(name = "new_with_axis")] - fn new_with_axis(axis: PyVector) -> Self { - let g: rapier::math::Vector = axis.0.into(); - Self(rapier::dynamics::RevoluteJoint::new(g)) - } - - /// Body-local rotation axis on body 1. - #[getter] - fn local_axis1(&self) -> $Vec { - let v: $crate::na::Vector3 = self.0.data.local_axis1().into(); - $Vec(v) - } - /// Body-local rotation axis on body 2. - #[getter] - fn local_axis2(&self) -> $Vec { - let v: $crate::na::Vector3 = self.0.data.local_axis2().into(); - $Vec(v) - } - } - - #[pymethods] - impl RevoluteJointBuilder { - /// Set the body-local rotation axis on body 1. - fn local_axis1(&self, v: PyVector) -> Self { - // The Rust API has no `local_axis1` builder method on - // RevoluteJointBuilder; we round-trip via the underlying - // joint to set the axis. - let mut j = self.0.build(); - let g: rapier::math::Vector = v.0.into(); - j.data.set_local_axis1(g); - Self(rapier::dynamics::RevoluteJointBuilder(j)) - } - /// Set the body-local rotation axis on body 2. - fn local_axis2(&self, v: PyVector) -> Self { - let mut j = self.0.build(); - let g: rapier::math::Vector = v.0.into(); - j.data.set_local_axis2(g); - Self(rapier::dynamics::RevoluteJointBuilder(j)) - } - } - }; -} - -// ==================================================================== -// 2D-specific joint types: RevoluteJoint without axis, PinSlotJoint -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joints_2d { - ($Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - $crate::__define_joints_common!(2, $Vec, $Point, $Rot, $Iso); - - /// Dim-specific generic-joint extractor invoked by the shared - /// `__extract_generic_joint!` macro. In 2D this handles `PinSlotJoint`. - fn try_extract_dim_specific_joint( - obj: &Bound<'_, PyAny>, - ) -> PyResult> { - if let Ok(j) = obj.extract::>() { - return Ok(Some(j.0.into())); - } - if let Ok(b) = obj.extract::>() { - return Ok(Some(b.0.build().into())); - } - Ok(None) - } - - // ----------------------------------------------------------------- - // PinSlotJoint + PinSlotJointBuilder (2D only) - // ----------------------------------------------------------------- - /// 2D pin-in-slot joint: one body slides along an axis of the - /// other while remaining co-planar. - /// - /// Two rotational and one translational DOF are removed, leaving - /// the bodies free to translate along ``local_axis1``/``local_axis2`` - /// while their orientations stay locked together. Available only - /// in 2D. - #[pyclass(name = "PinSlotJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PinSlotJoint(pub rapier::dynamics::PinSlotJoint); - - #[pymethods] - impl PinSlotJoint { - /// Construct a pin-slot joint with the given sliding axis. - /// - /// :param axis: Slot direction in each body's local frame. - #[new] - fn new(axis: PyVector) -> Self { - let g: rapier::math::Vector = axis.0.into(); - Self(rapier::dynamics::PinSlotJoint::new(g)) - } - /// Build a :class:`PinSlotJointBuilder`. - /// - /// :param axis: Slot direction in each body's local frame. - /// :param kwargs: Optional keyword args forwarded to the - /// builder; accepted keys are ``local_anchor1``, - /// ``local_anchor2``, ``local_axis1``, ``local_axis2``, - /// ``contacts_enabled``, ``limits``. - #[staticmethod] - #[pyo3(signature = (axis, **kwargs))] - fn builder( - axis: PyVector, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let g: rapier::math::Vector = axis.0.into(); - PinSlotJointBuilder::from_kwargs( - rapier::dynamics::PinSlotJointBuilder::new(g), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { - self.0.contacts_enabled() - } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { - self.0.set_contacts_enabled(v); - } - - /// Body-local point on body 1 where the joint is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::Vector2 = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the joint is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::Vector2 = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Body-local slot direction on body 1. - #[getter] - fn local_axis1(&self) -> $Vec { - let v: $crate::na::Vector2 = self.0.local_axis1().into(); - $Vec(v) - } - /// Set the body-local slot direction on body 1. - #[setter] - fn set_local_axis1(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.0.set_local_axis1(g); - } - /// Body-local slot direction on body 2. - #[getter] - fn local_axis2(&self) -> $Vec { - let v: $crate::na::Vector2 = self.0.local_axis2().into(); - $Vec(v) - } - /// Set the body-local slot direction on body 2. - #[setter] - fn set_local_axis2(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.0.set_local_axis2(g); - } - - /// Return the linear-axis motor, if configured. - fn motor(&self) -> Option { - self.0.motor().map(JointMotor::from_rapier) - } - /// Return the linear-axis limits, if configured. - fn limits(&self) -> Option { - self.0.limits().map(JointLimits::from_rapier) - } - /// Apply linear ``[min, max]`` translation limits along the slot - /// (world units). - fn set_limits(&mut self, min: Real, max: Real) { - self.0.set_limits([min, max]); - } - /// Configure the slot as a velocity-target motor with damping. - fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.0.set_motor_velocity(target_vel, factor); - } - /// Configure the slot as a spring-damper motor toward - /// ``target_pos`` (world units). - fn set_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { - self.0.set_motor_position(target_pos, stiffness, damping); - } - /// Fully configure the slot motor with position and velocity - /// setpoints. - fn set_motor( - &mut self, - target_pos: Real, - target_vel: Real, - stiffness: Real, - damping: Real, - ) { - self.0.set_motor(target_pos, target_vel, stiffness, damping); - } - /// Clamp the maximum force the slot motor can apply. - fn set_motor_max_force(&mut self, max_force: Real) { - self.0.set_motor_max_force(max_force); - } - /// Select the motor model (see :class:`MotorModel`). - fn set_motor_model(&mut self, model: MotorModel) { - self.0.set_motor_model(model.to_rapier()); - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { - GenericJoint::new_owned(self.0.data) - } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { - SpringCoefficients(self.0.softness()) - } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { - let _ = self.0.set_softness(v.0); - } - } - - /// Fluent builder for :class:`PinSlotJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`PinSlotJoint`. - #[pyclass(name = "PinSlotJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PinSlotJointBuilder(pub rapier::dynamics::PinSlotJointBuilder); - - impl PinSlotJointBuilder { - fn from_kwargs( - base: rapier::dynamics::PinSlotJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "local_axis1" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis1(g); - } - "local_axis2" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis2(g); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - "limits" => { - let (min, max): (Real, Real) = v.extract()?; - me.0 = me.0.limits([min, max]); - } - _ => { - return Err(PyTypeError::new_err(format!( - "unknown PinSlotJointBuilder kwarg: '{}'", - key - ))); - } - } - } - } - Ok(me) - } - } - - #[pymethods] - impl PinSlotJointBuilder { - /// Construct a new pin-slot builder with the given sliding axis. - #[new] - fn new(axis: PyVector) -> Self { - let g: rapier::math::Vector = axis.0.into(); - Self(rapier::dynamics::PinSlotJointBuilder::new(g)) - } - - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Set the body-local slot direction on body 1. - fn local_axis1(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis1(g)) - } - /// Set the body-local slot direction on body 2. - fn local_axis2(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis2(g)) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Apply linear ``[min, max]`` translation limits along the slot - /// (world units). - fn limits(&self, min: Real, max: Real) -> Self { - Self(self.0.limits([min, max])) - } - /// Configure a velocity-target motor with damping along the slot. - fn motor_velocity(&self, target_vel: Real, factor: Real) -> Self { - Self(self.0.motor_velocity(target_vel, factor)) - } - /// Configure a spring-damper motor toward ``target_pos`` along - /// the slot. - fn motor_position(&self, target_pos: Real, stiffness: Real, damping: Real) -> Self { - Self(self.0.motor_position(target_pos, stiffness, damping)) - } - /// Clamp the maximum force the slot motor can apply. - fn motor_max_force(&self, max_force: Real) -> Self { - Self(self.0.motor_max_force(max_force)) - } - /// Select the motor model (see :class:`MotorModel`). - fn motor_model(&self, model: MotorModel) -> Self { - Self(self.0.motor_model(model.to_rapier())) - } - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { - Self(self.0.softness(v.0)) - } - /// Materialize the builder into a :class:`PinSlotJoint`. - fn build(&self) -> PinSlotJoint { - PinSlotJoint(self.0.build()) - } - } - }; -} - -// ==================================================================== -// Shared joint types: Fixed/Revolute/Prismatic/Rope/Spring/Generic -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joints_common { - ($dim:tt, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // ================================================================= - // FixedJoint + FixedJointBuilder - // ================================================================= - /// Rigidly weld two bodies together: no relative motion at all. - /// - /// All translational and rotational DOFs are locked, so the two - /// bodies behave as a single rigid object connected by their - /// local frames. Useful for composing bodies that should never - /// move relative to each other but still need to be separable - /// (unlike a true compound shape). - #[pyclass(name = "FixedJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct FixedJoint(pub rapier::dynamics::FixedJoint); - - #[pymethods] - impl FixedJoint { - /// Construct a fixed joint with identity local frames. - #[new] - fn new() -> Self { Self(rapier::dynamics::FixedJoint::new()) } - - /// Build a :class:`FixedJointBuilder`. - /// - /// :param body_a: Ignored (kept for API symmetry). - /// :param body_b: Ignored (kept for API symmetry). - /// :param kwargs: Optional keyword args forwarded to the - /// builder; accepted keys are ``local_anchor1``, - /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, - /// ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] - fn builder( - body_a: Option<&Bound<'_, PyAny>>, - body_b: Option<&Bound<'_, PyAny>>, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let _ = (body_a, body_b); - FixedJointBuilder::from_kwargs( - rapier::dynamics::FixedJointBuilder::new(), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.0.contacts_enabled() } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.0.set_contacts_enabled(v); } - - /// Body-local frame (position + orientation) on body 1. - #[getter] - fn local_frame1(&self) -> $Iso { - let pose: $crate::na::Isometry = (*self.0.local_frame1()).into(); - $Iso(pose) - } - /// Set the body-local frame on body 1. - #[setter] - fn set_local_frame1(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.0.set_local_frame1(p); - } - /// Body-local frame on body 2. - #[getter] - fn local_frame2(&self) -> $Iso { - let pose: $crate::na::Isometry = (*self.0.local_frame2()).into(); - $Iso(pose) - } - /// Set the body-local frame on body 2. - #[setter] - fn set_local_frame2(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.0.set_local_frame2(p); - } - - /// Body-local point on body 1 where the joint is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the joint is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { GenericJoint::new_owned(self.0.data) } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { SpringCoefficients(self.0.softness()) } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { let _ = self.0.set_softness(v.0); } - } - - /// Fluent builder for :class:`FixedJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`FixedJoint`. - #[pyclass(name = "FixedJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct FixedJointBuilder(pub rapier::dynamics::FixedJointBuilder); - - impl FixedJointBuilder { - fn from_kwargs( - base: rapier::dynamics::FixedJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "local_frame1" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame1(p); - } - "local_frame2" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame2(p); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => return Err(PyTypeError::new_err( - format!("unknown FixedJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl FixedJointBuilder { - /// Construct an empty :class:`FixedJointBuilder`. - #[new] - fn new() -> Self { Self(rapier::dynamics::FixedJointBuilder::new()) } - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Set the body-local frame on body 1. - fn local_frame1(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame1(p)) - } - /// Set the body-local frame on body 2. - fn local_frame2(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame2(p)) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { Self(self.0.softness(v.0)) } - /// Materialize the builder into a :class:`FixedJoint`. - fn build(&self) -> FixedJoint { FixedJoint(self.0.build()) } - } - - // ================================================================= - // RevoluteJoint + RevoluteJointBuilder - // ================================================================= - /// Hinge / pin joint allowing a single rotational DOF. - /// - /// All three translational DOFs are locked and, in 3D, the two - /// rotational DOFs orthogonal to the rotation axis are locked - /// as well, leaving a single rotational degree of freedom about - /// ``local_axis1``/``local_axis2``. In 2D the axis is implicit - /// (the out-of-plane direction), so the constructor takes no - /// axis argument; in 3D it is required. - #[pyclass(name = "RevoluteJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RevoluteJoint(pub rapier::dynamics::RevoluteJoint); - - #[pymethods] - impl RevoluteJoint { - /// Construct a new revolute joint. - /// - /// :param axis: Rotation axis (required in 3D, ignored in 2D - /// where the axis is implicitly out-of-plane). - /// :raises TypeError: If ``axis`` is missing in 3D. - // Dim-agnostic constructor: 2D = no arg, 3D = required axis. - #[new] - #[pyo3(signature = (axis=None))] - fn new(axis: Option) -> PyResult { - $crate::__revolute_new!($dim, axis) - } - /// Build a :class:`RevoluteJointBuilder`. - /// - /// :param axis: Rotation axis (required in 3D, ignored in 2D). - /// :param kwargs: Optional keyword args forwarded to the - /// builder; accepted keys are ``local_anchor1``, - /// ``local_anchor2``, ``limits``, ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (axis=None, **kwargs))] - fn builder( - axis: Option, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let base = $crate::__revolute_builder_new!($dim, axis)?; - RevoluteJointBuilder::from_kwargs(base, kwargs) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.0.contacts_enabled() } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.0.set_contacts_enabled(v); } - - /// Body-local point on body 1 where the joint is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the joint is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Return the angular limits, if configured. - fn limits(&self) -> Option { - self.0.limits().map(JointLimits::from_rapier) - } - /// Return the angular motor, if configured. - fn motor(&self) -> Option { - self.0.motor().map(JointMotor::from_rapier) - } - /// Apply angular ``[min, max]`` limits (radians). - fn set_limits(&mut self, min: Real, max: Real) { self.0.set_limits([min, max]); } - /// Configure the joint as a velocity-target motor with damping. - /// - /// :param target_vel: Desired angular velocity (rad/s). - /// :param factor: Damping coefficient. - fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.0.set_motor_velocity(target_vel, factor); - } - /// Configure the joint as a spring-damper motor toward - /// ``target_pos`` (radians). - fn set_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { - self.0.set_motor_position(target_pos, stiffness, damping); - } - /// Fully configure the motor with position and velocity setpoints. - fn set_motor(&mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) { - self.0.set_motor(target_pos, target_vel, stiffness, damping); - } - /// Clamp the maximum torque the motor can apply. - fn set_motor_max_force(&mut self, max_force: Real) { - self.0.set_motor_max_force(max_force); - } - /// Select the motor model (see :class:`MotorModel`). - fn set_motor_model(&mut self, model: MotorModel) { - self.0.set_motor_model(model.to_rapier()); - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { GenericJoint::new_owned(self.0.data) } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { SpringCoefficients(self.0.softness()) } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { let _ = self.0.set_softness(v.0); } - /// Current hinge angle, in radians, given the world-space - /// rotations of the two attached bodies. - /// - /// :param body_rotation1: World rotation of body 1 - /// (e.g. ``RigidBody.rotation``). - /// :param body_rotation2: World rotation of body 2. - fn angle(&self, body_rotation1: PyRotation, body_rotation2: PyRotation) -> Real { - let i1 = $crate::na::Isometry::from_parts( - $crate::na::Translation::default(), - body_rotation1.0, - ); - let i2 = $crate::na::Isometry::from_parts( - $crate::na::Translation::default(), - body_rotation2.0, - ); - let p1: rapier::math::Pose = i1.into(); - let p2: rapier::math::Pose = i2.into(); - self.0.angle(&p1.rotation, &p2.rotation) - } - } - - /// Fluent builder for :class:`RevoluteJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`RevoluteJoint`. - #[pyclass(name = "RevoluteJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RevoluteJointBuilder(pub rapier::dynamics::RevoluteJointBuilder); - - impl RevoluteJointBuilder { - fn from_kwargs( - base: rapier::dynamics::RevoluteJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "limits" => { - let (min, max): (Real, Real) = v.extract()?; - me.0 = me.0.limits([min, max]); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => return Err(PyTypeError::new_err( - format!("unknown RevoluteJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl RevoluteJointBuilder { - /// Construct a new revolute joint builder. - /// - /// :param axis: Rotation axis (required in 3D, ignored in 2D). - #[new] - #[pyo3(signature = (axis=None))] - fn new(axis: Option) -> PyResult { - let base = $crate::__revolute_builder_new!($dim, axis)?; - Ok(Self(base)) - } - - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Apply angular ``[min, max]`` limits (radians). - fn limits(&self, min: Real, max: Real) -> Self { - Self(self.0.limits([min, max])) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Configure a velocity-target motor with damping. - fn motor_velocity(&self, target_vel: Real, factor: Real) -> Self { - Self(self.0.motor_velocity(target_vel, factor)) - } - /// Configure a spring-damper motor toward ``target_pos`` (radians). - fn motor_position(&self, target_pos: Real, stiffness: Real, damping: Real) -> Self { - Self(self.0.motor_position(target_pos, stiffness, damping)) - } - /// Fully configure the motor with both position and velocity - /// setpoints. - fn motor(&self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) -> Self { - Self(self.0.motor(target_pos, target_vel, stiffness, damping)) - } - /// Clamp the maximum torque the motor can apply. - fn motor_max_force(&self, max_force: Real) -> Self { - Self(self.0.motor_max_force(max_force)) - } - /// Select the motor model (see :class:`MotorModel`). - fn motor_model(&self, model: MotorModel) -> Self { - Self(self.0.motor_model(model.to_rapier())) - } - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { Self(self.0.softness(v.0)) } - /// Materialize the builder into a :class:`RevoluteJoint`. - fn build(&self) -> RevoluteJoint { RevoluteJoint(self.0.build()) } - } - - // ================================================================= - // PrismaticJoint + PrismaticJointBuilder - // ================================================================= - /// Slider joint allowing a single translational DOF. - /// - /// All rotational DOFs and the two translations orthogonal to - /// ``local_axis1``/``local_axis2`` are locked, leaving a single - /// translational degree of freedom along the configured axis. The - /// axis must be supplied at construction time. - #[pyclass(name = "PrismaticJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PrismaticJoint(pub rapier::dynamics::PrismaticJoint); - - #[pymethods] - impl PrismaticJoint { - /// Construct a new prismatic joint with the given sliding axis. - /// - /// :param axis: Translation axis in each body's local frame. - #[new] - fn new(axis: PyVector) -> Self { - let g: rapier::math::Vector = axis.0.into(); - Self(rapier::dynamics::PrismaticJoint::new(g)) - } - /// Build a :class:`PrismaticJointBuilder`. - /// - /// :param axis: Translation axis in each body's local frame. - /// :param kwargs: Optional keyword args forwarded to the - /// builder; accepted keys include ``local_anchor1``, - /// ``local_anchor2``, ``local_axis1``, ``local_axis2``, - /// ``limits``, ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (axis, **kwargs))] - fn builder( - axis: PyVector, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let g: rapier::math::Vector = axis.0.into(); - PrismaticJointBuilder::from_kwargs( - rapier::dynamics::PrismaticJointBuilder::new(g), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.0.contacts_enabled() } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.0.set_contacts_enabled(v); } - - /// Body-local point on body 1 where the joint is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the joint is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Body-local translation axis on body 1. - #[getter] - fn local_axis1(&self) -> $Vec { - let v: $crate::na::SVector = self.0.local_axis1().into(); - $Vec(v) - } - /// Set the body-local translation axis on body 1. - #[setter] - fn set_local_axis1(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.0.set_local_axis1(g); - } - /// Body-local translation axis on body 2. - #[getter] - fn local_axis2(&self) -> $Vec { - let v: $crate::na::SVector = self.0.local_axis2().into(); - $Vec(v) - } - /// Set the body-local translation axis on body 2. - #[setter] - fn set_local_axis2(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.0.set_local_axis2(g); - } - - /// Return the linear motor, if configured. - fn motor(&self) -> Option { - self.0.motor().map(JointMotor::from_rapier) - } - /// Return the linear limits, if configured. - fn limits(&self) -> Option { - self.0.limits().map(JointLimits::from_rapier) - } - /// Apply linear ``[min, max]`` translation limits (world units). - fn set_limits(&mut self, min: Real, max: Real) { self.0.set_limits([min, max]); } - /// Configure a velocity-target motor with damping. - /// - /// :param target_vel: Desired velocity (world units per second). - /// :param factor: Damping coefficient. - fn set_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.0.set_motor_velocity(target_vel, factor); - } - /// Configure a spring-damper motor toward ``target_pos`` - /// (world units). - fn set_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { - self.0.set_motor_position(target_pos, stiffness, damping); - } - /// Fully configure the motor with position and velocity setpoints. - fn set_motor(&mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real) { - self.0.set_motor(target_pos, target_vel, stiffness, damping); - } - /// Clamp the maximum force the motor can apply. - fn set_motor_max_force(&mut self, max_force: Real) { - self.0.set_motor_max_force(max_force); - } - /// Select the motor model (see :class:`MotorModel`). - fn set_motor_model(&mut self, model: MotorModel) { - self.0.set_motor_model(model.to_rapier()); - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { GenericJoint::new_owned(self.0.data) } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { SpringCoefficients(self.0.softness()) } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { let _ = self.0.set_softness(v.0); } - } - - /// Fluent builder for :class:`PrismaticJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`PrismaticJoint`. - #[pyclass(name = "PrismaticJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct PrismaticJointBuilder(pub rapier::dynamics::PrismaticJointBuilder); - - impl PrismaticJointBuilder { - fn from_kwargs( - base: rapier::dynamics::PrismaticJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "local_axis1" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis1(g); - } - "local_axis2" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis2(g); - } - "limits" => { - let (min, max): (Real, Real) = v.extract()?; - me.0 = me.0.limits([min, max]); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => return Err(PyTypeError::new_err( - format!("unknown PrismaticJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl PrismaticJointBuilder { - /// Construct a new prismatic joint builder with the given - /// sliding axis. - #[new] - fn new(axis: PyVector) -> Self { - let g: rapier::math::Vector = axis.0.into(); - Self(rapier::dynamics::PrismaticJointBuilder::new(g)) - } - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Set the body-local translation axis on body 1. - fn local_axis1(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis1(g)) - } - /// Set the body-local translation axis on body 2. - fn local_axis2(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis2(g)) - } - /// Apply linear ``[min, max]`` translation limits (world units). - fn limits(&self, min: Real, max: Real) -> Self { - Self(self.0.limits([min, max])) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Configure a velocity-target motor with damping. - fn motor_velocity(&self, target_vel: Real, factor: Real) -> Self { - Self(self.0.motor_velocity(target_vel, factor)) - } - /// Configure a spring-damper motor toward ``target_pos`` - /// (world units). - fn motor_position(&self, target_pos: Real, stiffness: Real, damping: Real) -> Self { - Self(self.0.motor_position(target_pos, stiffness, damping)) - } - /// Clamp the maximum force the motor can apply. - fn motor_max_force(&self, max_force: Real) -> Self { - Self(self.0.motor_max_force(max_force)) - } - /// Select the motor model (see :class:`MotorModel`). - fn motor_model(&self, model: MotorModel) -> Self { - Self(self.0.motor_model(model.to_rapier())) - } - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { Self(self.0.softness(v.0)) } - /// Materialize the builder into a :class:`PrismaticJoint`. - fn build(&self) -> PrismaticJoint { PrismaticJoint(self.0.build()) } - } - - // ================================================================= - // RopeJoint + RopeJointBuilder - // ================================================================= - /// Distance constraint: anchor points stay within ``max_distance``. - /// - /// The two anchor points may move freely as long as the world - /// distance between them stays at or below ``max_distance``. - /// Conceptually models a slack rope of that length: there is no - /// force when the bodies are close, and a hard pull when the rope - /// becomes taut. - #[pyclass(name = "RopeJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RopeJoint(pub rapier::dynamics::RopeJoint); - - #[pymethods] - impl RopeJoint { - /// Construct a rope joint with the given maximum length. - /// - /// :param max_distance: Maximum allowed distance between - /// the anchor points, in world units. - #[new] - #[pyo3(signature = (max_distance=1.0 as Real))] - fn new(max_distance: Real) -> Self { - Self(rapier::dynamics::RopeJoint::new(max_distance)) - } - /// Build a :class:`RopeJointBuilder`. - /// - /// :param max_distance: Maximum rope length (world units). - /// :param kwargs: Optional keyword args; accepted keys are - /// ``local_anchor1``, ``local_anchor2``, ``max_distance``, - /// ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (max_distance, **kwargs))] - fn builder( - max_distance: Real, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - RopeJointBuilder::from_kwargs( - rapier::dynamics::RopeJointBuilder::new(max_distance), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.0.contacts_enabled() } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.0.set_contacts_enabled(v); } - - /// Body-local point on body 1 where the rope is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the rope is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Maximum distance the rope can stretch (world units). - #[getter] - fn max_distance(&self) -> Real { self.0.max_distance() } - /// Set the maximum rope length. - #[setter] - fn set_max_distance(&mut self, v: Real) { self.0.set_max_distance(v); } - - /// Minimum distance enforced between the anchors. - /// - /// Exposed for API parity with :py:attr:`max_distance`; under - /// the hood it is stored as the lower limit of the joint's - /// ``LIN_X`` axis and is always ``0`` on a rope joint. - // Minimum length is not exposed as a field on RopeJoint; the - // engine encodes it as the lower limit of LinX which is always - // 0.0 here. Surface it as a read-only property for API parity. - #[getter] - fn min_distance(&self) -> Real { - self.0 - .data - .limits(rapier::dynamics::JointAxis::LinX) - .map(|l| l.min) - .unwrap_or(0.0) - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { GenericJoint::new_owned(self.0.data) } - /// Spring coefficients controlling this joint's softness - /// (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { SpringCoefficients(self.0.softness()) } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { let _ = self.0.set_softness(v.0); } - } - - /// Fluent builder for :class:`RopeJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`RopeJoint`. - #[pyclass(name = "RopeJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct RopeJointBuilder(pub rapier::dynamics::RopeJointBuilder); - - impl RopeJointBuilder { - fn from_kwargs( - base: rapier::dynamics::RopeJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "max_distance" => { - let d: Real = v.extract()?; - me.0 = me.0.max_distance(d); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => return Err(PyTypeError::new_err( - format!("unknown RopeJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl RopeJointBuilder { - /// Construct a new rope joint builder. - /// - /// :param max_distance: Maximum rope length (world units). - #[new] - #[pyo3(signature = (max_distance=1.0 as Real))] - fn new(max_distance: Real) -> Self { - Self(rapier::dynamics::RopeJointBuilder::new(max_distance)) - } - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Override the maximum rope length. - fn max_distance(&self, d: Real) -> Self { - Self(self.0.max_distance(d)) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Set the spring softness coefficients (see - /// :class:`SpringCoefficients`). Returns a new builder. - fn softness(&self, v: SpringCoefficients) -> Self { Self(self.0.softness(v.0)) } - /// Materialize the builder into a :class:`RopeJoint`. - fn build(&self) -> RopeJoint { RopeJoint(self.0.build()) } - } - - // ================================================================= - // SpringJoint + SpringJointBuilder - // ================================================================= - /// Distance spring-damper between two anchor points. - /// - /// Applies a force ``F = stiffness * (rest_length - dist) - - /// damping * d_dist/dt`` along the line connecting the two - /// anchor points; no DOF is locked, the constraint is purely a - /// soft restoring force. Useful for suspensions, soft links, and - /// other compliant connections. - #[pyclass(name = "SpringJoint", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct SpringJoint(pub rapier::dynamics::SpringJoint); - - #[pymethods] - impl SpringJoint { - /// Construct a spring joint. - /// - /// :param rest_length: Distance at which the spring exerts no - /// force (world units). - /// :param stiffness: Spring constant. - /// :param damping: Velocity damping coefficient. - #[new] - #[pyo3(signature = (rest_length=0.0 as Real, stiffness=1.0 as Real, damping=0.0 as Real))] - fn new(rest_length: Real, stiffness: Real, damping: Real) -> Self { - Self(rapier::dynamics::SpringJoint::new(rest_length, stiffness, damping)) - } - /// Build a :class:`SpringJointBuilder`. - /// - /// :param rest_length: Distance at which the spring exerts no - /// force (world units). - /// :param stiffness: Spring constant. - /// :param damping: Velocity damping coefficient. - /// :param kwargs: Optional keyword args; accepted keys include - /// ``local_anchor1``, ``local_anchor2``, - /// ``contacts_enabled``, ``spring_model``. - #[staticmethod] - #[pyo3(signature = (rest_length, stiffness, damping, **kwargs))] - fn builder( - rest_length: Real, - stiffness: Real, - damping: Real, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - SpringJointBuilder::from_kwargs( - rapier::dynamics::SpringJointBuilder::new(rest_length, stiffness, damping), - kwargs, - ) - } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.0.contacts_enabled() } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.0.set_contacts_enabled(v); } - - /// Body-local point on body 1 where the spring is attached. - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor1().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor1(g); - } - /// Body-local point on body 2 where the spring is attached. - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_anchor2().into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.0.set_local_anchor2(g); - } - - /// Select how the spring force is applied (see :class:`MotorModel`). - fn set_spring_model(&mut self, model: MotorModel) { - self.0.set_spring_model(model.to_rapier()); - } - - /// Rest length of the spring (world units). - /// - /// Read back from the underlying ``LIN_X`` motor's - /// ``target_pos``; returns ``0`` if no motor is set. - // Read-only access to spring parameters via the underlying - // generic joint's motor on LinX. - #[getter] - fn rest_length(&self) -> Real { - self.0 - .data - .motor(rapier::dynamics::JointAxis::LinX) - .map(|m| m.target_pos) - .unwrap_or(0.0) - } - /// Spring stiffness (read-back from the underlying motor). - #[getter] - fn spring_stiffness(&self) -> Real { - self.0 - .data - .motor(rapier::dynamics::JointAxis::LinX) - .map(|m| m.stiffness) - .unwrap_or(0.0) - } - /// Spring damping coefficient (read-back from the underlying motor). - #[getter] - fn spring_damping(&self) -> Real { - self.0 - .data - .motor(rapier::dynamics::JointAxis::LinX) - .map(|m| m.damping) - .unwrap_or(0.0) - } - /// Motor model controlling how the spring force is applied - /// (see :class:`MotorModel`). - #[getter] - fn model(&self) -> MotorModel { - self.0 - .data - .motor(rapier::dynamics::JointAxis::LinX) - .map(|m| MotorModel::from_rapier(m.model)) - .unwrap_or(MotorModel::FORCE_BASED) - } - - /// Access the underlying :class:`GenericJoint` description. - #[getter] - fn data(&self) -> GenericJoint { GenericJoint::new_owned(self.0.data) } - } - - /// Fluent builder for :class:`SpringJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`SpringJoint`. - #[pyclass(name = "SpringJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct SpringJointBuilder(pub rapier::dynamics::SpringJointBuilder); - - impl SpringJointBuilder { - fn from_kwargs( - base: rapier::dynamics::SpringJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - "spring_model" => { - let m: MotorModel = v.extract()?; - me.0 = me.0.spring_model(m.to_rapier()); - } - _ => return Err(PyTypeError::new_err( - format!("unknown SpringJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl SpringJointBuilder { - /// Construct a new spring joint builder. - /// - /// :param rest_length: Distance at which the spring exerts no - /// force (world units). - /// :param stiffness: Spring constant. - /// :param damping: Velocity damping coefficient. - #[new] - #[pyo3(signature = (rest_length=0.0 as Real, stiffness=1.0 as Real, damping=0.0 as Real))] - fn new(rest_length: Real, stiffness: Real, damping: Real) -> Self { - Self(rapier::dynamics::SpringJointBuilder::new(rest_length, stiffness, damping)) - } - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Select how the spring force is applied (see :class:`MotorModel`). - fn spring_model(&self, m: MotorModel) -> Self { - Self(self.0.spring_model(m.to_rapier())) - } - /// Materialize the builder into a :class:`SpringJoint`. - fn build(&self) -> SpringJoint { SpringJoint(self.0.build()) } - } - - // ================================================================= - // GenericJoint + GenericJointBuilder - // ================================================================= - /// Fully parametric joint, the building block of every concrete - /// joint type. - /// - /// A generic joint is described by: - /// - /// * Two body-local frames (``local_frame1``/``local_frame2``). - /// * A ``locked_axes`` mask listing the DOFs that are removed by - /// the constraint. By default no axis is locked (the joint - /// imposes nothing). - /// * A ``limit_axes`` / per-axis :class:`JointLimits` pair to bound - /// the motion on selected axes. - /// * A ``motor_axes`` / per-axis :class:`JointMotor` pair to drive - /// selected axes with a PD controller. - /// * A ``coupled_axes`` mask listing axes that share the same - /// limit / motor (used e.g. by :class:`RopeJoint` to couple all - /// linear axes into a single distance constraint). - /// - /// The concrete joint types (Fixed/Revolute/Prismatic/etc.) are - /// thin wrappers that pre-configure these masks for a given - /// mechanism. - #[pyclass(name = "GenericJoint", module = "rapier")] - #[derive(Debug)] - pub struct GenericJoint { - pub backing: GenericJointBacking, - } - - /// Storage backing a `GenericJoint`: a standalone owned value, or a - /// live view into the `data` of a joint stored in an `ImpulseJointSet` - /// (so `impulse_joint.data.set_limits(..)` persists in place). - #[derive(Debug)] - pub enum GenericJointBacking { - Owned(Box), - ImpulseJointData { - set: Py, - handle: rapier::dynamics::ImpulseJointHandle, - }, - } - - impl Clone for GenericJointBacking { - fn clone(&self) -> Self { - match self { - GenericJointBacking::Owned(g) => GenericJointBacking::Owned(g.clone()), - GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { - GenericJointBacking::ImpulseJointData { - set: set.clone_ref(py), - handle: *handle, - } - }), - } - } - } - impl Clone for GenericJoint { - fn clone(&self) -> Self { - GenericJoint { backing: self.backing.clone() } - } - } - - impl GenericJoint { - fn new_owned(joint: rapier::dynamics::GenericJoint) -> Self { - GenericJoint { backing: GenericJointBacking::Owned(Box::new(joint)) } - } - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::GenericJoint) -> R) -> R { - match &self.backing { - GenericJointBacking::Owned(g) => f(g), - GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); - let joint = set.0.get(*handle).expect( - "GenericJoint refers to a joint that was removed from its set", - ); - f(&joint.data) - }), - } - } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::GenericJoint) -> R) -> R { - match &mut self.backing { - GenericJointBacking::Owned(g) => f(g), - GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); - let joint = set.0.get_mut(*handle, true).expect( - "GenericJoint refers to a joint that was removed from its set", - ); - f(&mut joint.data) - }), - } - } - /// Copy the underlying joint out (it is `Copy` upstream). - pub fn to_owned_generic(&self) -> rapier::dynamics::GenericJoint { - self.with_ref(|g| *g) - } - } - - #[pymethods] - impl GenericJoint { - /// Construct a generic joint with the given locked axes. - /// - /// :param locked_axes: Axes to lock; defaults to no axes - /// (a fully free joint). - #[new] - #[pyo3(signature = (locked_axes=None))] - fn new(locked_axes: Option) -> Self { - let mask = locked_axes - .map(|m| m.0) - .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); - GenericJoint::new_owned(rapier::dynamics::GenericJoint::new(mask)) - } - /// Build a :class:`GenericJointBuilder`. - /// - /// :param locked_axes: Axes to lock. - /// :param kwargs: Optional keyword args; accepted keys include - /// ``local_anchor1``, ``local_anchor2``, ``local_axis1``, - /// ``local_axis2``, ``local_frame1``, ``local_frame2``, - /// ``locked_axes``, ``coupled_axes``, ``contacts_enabled``. - #[staticmethod] - #[pyo3(signature = (locked_axes=None, **kwargs))] - fn builder( - locked_axes: Option, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mask = locked_axes - .map(|m| m.0) - .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); - GenericJointBuilder::from_kwargs( - rapier::dynamics::GenericJointBuilder::new(mask), - kwargs, - ) - } - - /// Body-local frame (position + orientation) on body 1. - #[getter] - fn local_frame1(&self) -> $Iso { - let pose: $crate::na::Isometry = self.with_ref(|g| g.local_frame1).into(); - $Iso(pose) - } - /// Set the body-local frame on body 1. - #[setter] - fn set_local_frame1(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.with_mut(|g| { g.set_local_frame1(p); }); - } - /// Body-local frame on body 2. - #[getter] - fn local_frame2(&self) -> $Iso { - let pose: $crate::na::Isometry = self.with_ref(|g| g.local_frame2).into(); - $Iso(pose) - } - /// Set the body-local frame on body 2. - #[setter] - fn set_local_frame2(&mut self, iso: PyIsometry) { - let p: rapier::math::Pose = iso.0.into(); - self.with_mut(|g| { g.set_local_frame2(p); }); - } - /// Body-local point on body 1 (origin of ``local_frame1``). - #[getter] - fn local_anchor1(&self) -> $Point { - let v: $crate::na::SVector = self.with_ref(|g| g.local_anchor1()).into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 1. - #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.with_mut(|gj| { gj.set_local_anchor1(g); }); - } - /// Body-local point on body 2 (origin of ``local_frame2``). - #[getter] - fn local_anchor2(&self) -> $Point { - let v: $crate::na::SVector = self.with_ref(|g| g.local_anchor2()).into(); - $Point($crate::na::Point::from(v)) - } - /// Set the body-local anchor on body 2. - #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { - let g: rapier::math::Vector = p.0.coords.into(); - self.with_mut(|gj| { gj.set_local_anchor2(g); }); - } - /// Reference axis on body 1 (X axis of ``local_frame1``). - #[getter] - fn local_axis1(&self) -> $Vec { - let v: $crate::na::SVector = self.with_ref(|g| g.local_axis1()).into(); - $Vec(v) - } - /// Set the reference axis on body 1. - #[setter] - fn set_local_axis1(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.with_mut(|gj| { gj.set_local_axis1(g); }); - } - /// Reference axis on body 2 (X axis of ``local_frame2``). - #[getter] - fn local_axis2(&self) -> $Vec { - let v: $crate::na::SVector = self.with_ref(|g| g.local_axis2()).into(); - $Vec(v) - } - /// Set the reference axis on body 2. - #[setter] - fn set_local_axis2(&mut self, v: PyVector) { - let g: rapier::math::Vector = v.0.into(); - self.with_mut(|gj| { gj.set_local_axis2(g); }); - } - - /// Mask of axes that are rigidly locked by this joint. - #[getter] - fn locked_axes(&self) -> JointAxesMask { JointAxesMask(self.with_ref(|g| g.locked_axes)) } - /// Replace the set of locked axes. - #[setter] - fn set_locked_axes(&mut self, v: JointAxesMask) { self.with_mut(|g| g.locked_axes = v.0); } - - /// Mask of axes that carry a :class:`JointLimits` configuration. - #[getter] - fn limit_axes(&self) -> JointAxesMask { JointAxesMask(self.with_ref(|g| g.limit_axes)) } - /// Mask of axes that carry a :class:`JointMotor` configuration. - #[getter] - fn motor_axes(&self) -> JointAxesMask { JointAxesMask(self.with_ref(|g| g.motor_axes)) } - /// Mask of axes that share their limits / motor (e.g. coupled - /// linear axes giving a spherical distance constraint). - #[getter] - fn coupled_axes(&self) -> JointAxesMask { JointAxesMask(self.with_ref(|g| g.coupled_axes)) } - /// Replace the set of coupled axes. - #[setter] - fn set_coupled_axes(&mut self, v: JointAxesMask) { self.with_mut(|g| g.coupled_axes = v.0); } - /// Spring coefficients controlling the softness of this joint's - /// constraints (see :class:`SpringCoefficients`). - #[getter] - fn softness(&self) -> SpringCoefficients { SpringCoefficients(self.with_ref(|g| g.softness)) } - /// Set the spring coefficients controlling this joint's softness. - #[setter] - fn set_softness(&mut self, v: SpringCoefficients) { self.with_mut(|g| g.softness = v.0); } - - /// Whether collision detection is enabled between the - /// attached bodies. - #[getter] - fn contacts_enabled(&self) -> bool { self.with_ref(|g| g.contacts_enabled) } - /// Enable or disable contacts between the attached bodies. - #[setter] - fn set_contacts_enabled(&mut self, v: bool) { self.with_mut(|g| { g.set_contacts_enabled(v); }); } - - /// Current :class:`JointEnabled` state. - #[getter] - fn enabled(&self) -> JointEnabled { JointEnabled::from_rapier(self.with_ref(|g| g.enabled)) } - /// Explicitly enable or disable the joint. - /// - /// Maps to :py:attr:`JointEnabled.ENABLED` / - /// :py:attr:`JointEnabled.DISABLED`. - fn set_enabled(&mut self, b: bool) { self.with_mut(|g| { g.set_enabled(b); }); } - /// Return ``True`` iff ``enabled == ENABLED``. - fn is_enabled(&self) -> bool { self.with_ref(|g| g.is_enabled()) } - - /// Opaque user-data integer carried by the joint. - #[getter] - fn user_data(&self) -> u128 { self.with_ref(|g| g.user_data) } - /// Set the user-data integer. - #[setter] - fn set_user_data(&mut self, v: u128) { self.with_mut(|g| g.user_data = v); } - - /// Return the limits configured on ``axis``, if any. - fn limits(&self, axis: JointAxis) -> Option { - self.with_ref(|g| g.limits(axis.to_rapier()).map(JointLimits::from_rapier)) - } - /// Configure ``[min, max]`` limits on ``axis``. - /// - /// Units are radians for angular axes and world units for - /// linear axes. - fn set_limits(&mut self, axis: JointAxis, min: Real, max: Real) { - self.with_mut(|g| { g.set_limits(axis.to_rapier(), [min, max]); }); - } - /// Return the motor configured on ``axis``, if any. - fn motor(&self, axis: JointAxis) -> Option { - self.with_ref(|g| g.motor(axis.to_rapier()).map(JointMotor::from_rapier)) - } - /// Fully configure the motor on ``axis``. - fn set_motor(&mut self, axis: JointAxis, target_pos: Real, target_vel: Real, - stiffness: Real, damping: Real) { - self.with_mut(|g| { - g.set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping); - }); - } - /// Configure ``axis`` as a velocity-target motor with damping. - fn set_motor_velocity(&mut self, axis: JointAxis, target_vel: Real, factor: Real) { - self.with_mut(|g| { g.set_motor_velocity(axis.to_rapier(), target_vel, factor); }); - } - /// Configure ``axis`` as a spring-damper motor toward - /// ``target_pos``. - fn set_motor_position(&mut self, axis: JointAxis, target_pos: Real, - stiffness: Real, damping: Real) { - self.with_mut(|g| { - g.set_motor_position(axis.to_rapier(), target_pos, stiffness, damping); - }); - } - /// Clamp the maximum force the motor on ``axis`` can apply. - fn set_motor_max_force(&mut self, axis: JointAxis, max_force: Real) { - self.with_mut(|g| { g.set_motor_max_force(axis.to_rapier(), max_force); }); - } - /// Select the motor model on ``axis`` (see :class:`MotorModel`). - fn set_motor_model(&mut self, axis: JointAxis, model: MotorModel) { - self.with_mut(|g| { g.set_motor_model(axis.to_rapier(), model.to_rapier()); }); - } - /// Return the motor model on ``axis``, if a motor is configured. - fn motor_model(&self, axis: JointAxis) -> Option { - self.with_ref(|g| g.motor_model(axis.to_rapier()).map(MotorModel::from_rapier)) - } - /// Add ``axes`` to the set of locked axes. - fn lock_axes(&mut self, axes: JointAxesMask) { - self.with_mut(|g| { g.lock_axes(axes.0); }); - } - - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!("GenericJoint(locked_axes={:#010b}, enabled={:?})", - self.with_ref(|g| g.locked_axes).bits(), self.with_ref(|g| g.enabled)) - } - } - - /// Fluent builder for :class:`GenericJoint`. - /// - /// Modifier methods return a new builder; call :py:meth:`build` to - /// produce a :class:`GenericJoint`. The same builder can be - /// applied to many bodies to create equivalent joints. - #[pyclass(name = "GenericJointBuilder", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct GenericJointBuilder(pub rapier::dynamics::GenericJointBuilder); - - impl GenericJointBuilder { - fn from_kwargs( - base: rapier::dynamics::GenericJointBuilder, - kwargs: Option<&Bound<'_, $crate::pyo3::types::PyDict>>, - ) -> PyResult { - let mut me = Self(base); - if let Some(kw) = kwargs { - for (k, v) in kw.iter() { - let key: String = k.extract()?; - match key.as_str() { - "local_anchor1" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor1(g); - } - "local_anchor2" => { - let p: PyPoint = v.extract()?; - let g: rapier::math::Vector = p.0.coords.into(); - me.0 = me.0.local_anchor2(g); - } - "local_axis1" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis1(g); - } - "local_axis2" => { - let pv: PyVector = v.extract()?; - let g: rapier::math::Vector = pv.0.into(); - me.0 = me.0.local_axis2(g); - } - "local_frame1" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame1(p); - } - "local_frame2" => { - let iso: PyIsometry = v.extract()?; - let p: rapier::math::Pose = iso.0.into(); - me.0 = me.0.local_frame2(p); - } - "locked_axes" => { - let m: JointAxesMask = v.extract()?; - me.0 = me.0.locked_axes(m.0); - } - "coupled_axes" => { - let m: JointAxesMask = v.extract()?; - me.0 = me.0.coupled_axes(m.0); - } - "contacts_enabled" => { - let b: bool = v.extract()?; - me.0 = me.0.contacts_enabled(b); - } - _ => return Err(PyTypeError::new_err( - format!("unknown GenericJointBuilder kwarg: '{}'", key) - )), - } - } - } - Ok(me) - } - } - - #[pymethods] - impl GenericJointBuilder { - /// Construct a new generic joint builder. - /// - /// :param locked_axes: Axes to lock; defaults to no axes. - #[new] - #[pyo3(signature = (locked_axes=None))] - fn new(locked_axes: Option) -> Self { - let mask = locked_axes - .map(|m| m.0) - .unwrap_or_else(rapier::dynamics::JointAxesMask::empty); - Self(rapier::dynamics::GenericJointBuilder::new(mask)) - } - /// Set the body-local anchor on body 1. - fn local_anchor1(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor1(g)) - } - /// Set the body-local anchor on body 2. - fn local_anchor2(&self, p: PyPoint) -> Self { - let g: rapier::math::Vector = p.0.coords.into(); - Self(self.0.local_anchor2(g)) - } - /// Set the reference axis on body 1. - fn local_axis1(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis1(g)) - } - /// Set the reference axis on body 2. - fn local_axis2(&self, v: PyVector) -> Self { - let g: rapier::math::Vector = v.0.into(); - Self(self.0.local_axis2(g)) - } - /// Set the body-local frame on body 1. - fn local_frame1(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame1(p)) - } - /// Set the body-local frame on body 2. - fn local_frame2(&self, iso: PyIsometry) -> Self { - let p: rapier::math::Pose = iso.0.into(); - Self(self.0.local_frame2(p)) - } - /// Override the set of locked axes. - fn locked_axes(&self, m: JointAxesMask) -> Self { - Self(self.0.locked_axes(m.0)) - } - /// Override the set of coupled axes. - fn coupled_axes(&self, m: JointAxesMask) -> Self { - Self(self.0.coupled_axes(m.0)) - } - /// Enable or disable contacts between the attached bodies. - fn contacts_enabled(&self, b: bool) -> Self { - Self(self.0.contacts_enabled(b)) - } - /// Apply ``[min, max]`` limits on ``axis`` (radians for angular, - /// world units for linear axes). - fn limits(&self, axis: JointAxis, min: Real, max: Real) -> Self { - Self(self.0.limits(axis.to_rapier(), [min, max])) - } - /// Fully configure the motor on ``axis``. - fn motor(&self, axis: JointAxis, target_pos: Real, target_vel: Real, - stiffness: Real, damping: Real) -> Self { - Self(self.0.set_motor(axis.to_rapier(), target_pos, target_vel, stiffness, damping)) - } - /// Configure ``axis`` as a velocity-target motor with damping. - fn motor_velocity(&self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { - Self(self.0.motor_velocity(axis.to_rapier(), target_vel, factor)) - } - /// Configure ``axis`` as a spring-damper motor toward - /// ``target_pos``. - fn motor_position(&self, axis: JointAxis, target_pos: Real, stiffness: Real, damping: Real) -> Self { - Self(self.0.motor_position(axis.to_rapier(), target_pos, stiffness, damping)) - } - /// Clamp the maximum force the motor on ``axis`` can apply. - fn motor_max_force(&self, axis: JointAxis, max_force: Real) -> Self { - Self(self.0.motor_max_force(axis.to_rapier(), max_force)) - } - /// Select the motor model on ``axis`` (see :class:`MotorModel`). - fn motor_model(&self, axis: JointAxis, model: MotorModel) -> Self { - Self(self.0.motor_model(axis.to_rapier(), model.to_rapier())) - } - /// Set the user-data integer carried by the joint. - fn user_data(&self, d: u128) -> Self { - Self(self.0.user_data(d)) - } - /// Set the spring softness coefficients for this joint's - /// constraints (see :class:`SpringCoefficients`). - fn softness(&self, v: SpringCoefficients) -> Self { Self(self.0.softness(v.0)) } - /// Materialize the builder into a :class:`GenericJoint`. - fn build(&self) -> GenericJoint { GenericJoint::new_owned(self.0.build()) } - } - - // ================================================================= - // ImpulseJoint (view) - // ================================================================= - /// Read-only view of a single joint stored in an - /// :class:`ImpulseJointSet`. - /// - /// Combines the two attached rigid-body handles with the - /// underlying :class:`GenericJoint` data and the last impulses - /// applied by the solver. - #[pyclass(name = "ImpulseJoint", module = "rapier")] - #[derive(Debug)] - pub struct ImpulseJoint { - pub backing: ImpulseJointBacking, - } - - /// Storage backing an `ImpulseJoint`: a standalone owned joint (e.g. - /// from `remove`) or a handle-backed view into an `ImpulseJointSet`. - #[derive(Debug)] - pub enum ImpulseJointBacking { - Owned(Box), - InSet { - set: Py, - handle: rapier::dynamics::ImpulseJointHandle, - }, - } - - impl Clone for ImpulseJointBacking { - fn clone(&self) -> Self { - match self { - ImpulseJointBacking::Owned(j) => ImpulseJointBacking::Owned(j.clone()), - ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { - ImpulseJointBacking::InSet { set: set.clone_ref(py), handle: *handle } - }), - } - } - } - impl Clone for ImpulseJoint { - fn clone(&self) -> Self { - ImpulseJoint { backing: self.backing.clone() } - } - } - - impl ImpulseJoint { - fn new_owned(joint: rapier::dynamics::ImpulseJoint) -> Self { - ImpulseJoint { backing: ImpulseJointBacking::Owned(Box::new(joint)) } - } - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::ImpulseJoint) -> R) -> R { - match &self.backing { - ImpulseJointBacking::Owned(j) => f(j), - ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); - let j = set.0.get(*handle).expect( - "ImpulseJoint refers to a joint that was removed from its set", - ); - f(j) - }), - } - } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::ImpulseJoint) -> R) -> R { - match &mut self.backing { - ImpulseJointBacking::Owned(j) => f(j), - ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); - let j = set.0.get_mut(*handle, true).expect( - "ImpulseJoint refers to a joint that was removed from its set", - ); - f(j) - }), - } - } - } - - #[pymethods] - impl ImpulseJoint { - /// Handle of the first attached body. - #[getter] - fn body1(&self) -> RigidBodyHandle { self.with_ref(|j| RigidBodyHandle(j.body1())) } - /// Handle of the second attached body. - #[getter] - fn body2(&self) -> RigidBodyHandle { self.with_ref(|j| RigidBodyHandle(j.body2())) } - /// Underlying :class:`GenericJoint` description (read+write). - /// - /// For a joint stored in a set this is a live **view**: mutating it - /// in place persists, e.g. - /// ``joint.data.set_limits(JointAxis.LIN_X, -1.0, 1.0)``. Assigning - /// a whole :class:`GenericJoint` (``joint.data = gj``) also works. - #[getter] - fn data(&self) -> GenericJoint { - match &self.backing { - ImpulseJointBacking::InSet { set, handle } => GenericJoint { - backing: GenericJointBacking::ImpulseJointData { - set: Python::with_gil(|py| set.clone_ref(py)), - handle: *handle, - }, - }, - ImpulseJointBacking::Owned(j) => GenericJoint::new_owned(j.data), - } - } - /// Replace the joint description (persists for an in-set joint). - #[setter] - fn set_data(&mut self, data: GenericJoint) { - let g = data.to_owned_generic(); - self.with_mut(|j| j.data = g); - } - /// Per-axis impulses applied by the solver on the last step. - /// - /// :returns: Flat list of length 3 in 2D (lin_x, lin_y, ang_x) - /// and 6 in 3D (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z). - #[getter] - fn impulses(&self) -> Vec { - self.with_ref(|j| $crate::__spatial_vector_to_vec!($dim, j.impulses)) - } - /// Return a developer-readable representation. - fn __repr__(&self) -> String { - self.with_ref(|j| { - let (i1, g1) = j.body1().0.into_raw_parts(); - let (i2, g2) = j.body2().0.into_raw_parts(); - format!( - "ImpulseJoint(body1=RigidBodyHandle({}, {}), body2=RigidBodyHandle({}, {}))", - i1, g1, i2, g2 - ) - }) - } - } - - // ================================================================= - // ImpulseJointSet - // ================================================================= - /// Container of impulse-based joints. - /// - /// Joints stored here are enforced by the iterative - /// velocity-impulse constraint solver alongside contacts. This is - /// the right home for general joint constraints between arbitrary - /// pairs of rigid bodies that do not form a tree-structured - /// articulation; use :class:`MultibodyJointSet` for the latter. - /// - /// Supports ``len()``, ``in``, ``set[handle]``, and iteration - /// (yielding ``(ImpulseJointHandle, ImpulseJoint)`` pairs). - #[pyclass(name = "ImpulseJointSet", module = "rapier", unsendable)] - pub struct ImpulseJointSet(pub rapier::dynamics::ImpulseJointSet); - - #[pymethods] - impl ImpulseJointSet { - /// Construct an empty :class:`ImpulseJointSet`. - #[new] - fn new() -> Self { Self(rapier::dynamics::ImpulseJointSet::new()) } - - /// Insert a new joint between ``body1`` and ``body2``. - /// - /// :param body1: Handle of the first attached body. - /// :param body2: Handle of the second attached body. - /// :param joint: A concrete joint or builder; anything that - /// can be converted to a :class:`GenericJoint` is accepted. - /// :param wake_up: If ``True``, wake the attached bodies so - /// they participate in the next solver step. - /// :returns: A fresh :class:`ImpulseJointHandle`. - #[pyo3(signature = (body1, body2, joint, wake_up=true))] - fn insert( - &mut self, - body1: &RigidBodyHandle, - body2: &RigidBodyHandle, - joint: &Bound<'_, PyAny>, - wake_up: bool, - ) -> PyResult { - let data = $crate::__extract_generic_joint!(joint)?; - Ok(ImpulseJointHandle(self.0.insert(body1.0, body2.0, data, wake_up))) - } - - /// Rewire an existing joint to a new pair of bodies. - /// - /// The joint keeps its handle, configuration, and warm-started - /// impulses; only its attached bodies change. Use this to e.g. - /// transfer a grasp from one body to another without rebuilding - /// the constraint. - /// - /// :param handle: Handle returned by :py:meth:`insert`. - /// :param body1: Handle of the new first body. - /// :param body2: Handle of the new second body. - /// :param wake_up: If ``True``, wake the old and new bodies. - /// :returns: ``True`` if the joint existed and was rewired, - /// ``False`` if ``handle`` was stale. - #[pyo3(signature = (handle, body1, body2, wake_up=true))] - fn set_bodies( - &mut self, - handle: &ImpulseJointHandle, - body1: &RigidBodyHandle, - body2: &RigidBodyHandle, - wake_up: bool, - ) -> bool { - self.0.set_bodies(handle.0, body1.0, body2.0, wake_up).is_some() - } - - /// Remove the joint identified by ``handle``. - /// - /// :param handle: Handle returned by :py:meth:`insert`. - /// :param wake_up: If ``True``, wake the formerly attached - /// bodies. - /// :returns: The removed joint, or ``None`` if the handle was - /// stale. - #[pyo3(signature = (handle, wake_up=true))] - fn remove(&mut self, handle: &ImpulseJointHandle, wake_up: bool) -> Option { - self.0.remove(handle.0, wake_up).map(ImpulseJoint::new_owned) - } - - /// Return a live **view** of the joint pointed at by ``handle``, - /// if any. Assigning ``joint.data = gj`` persists in place. - fn get(slf: &Bound<'_, Self>, handle: &ImpulseJointHandle) -> Option { - slf.borrow().0.get(handle.0)?; - Some(ImpulseJoint { - backing: ImpulseJointBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - /// Indexed access (``self[handle]``) — returns a live view. - /// - /// :raises InvalidHandle: If ``handle`` does not point to a - /// joint in this set. - fn __getitem__( - slf: &Bound<'_, Self>, - handle: &ImpulseJointHandle, - ) -> PyResult { - if slf.borrow().0.get(handle.0).is_none() { - return Err($crate::errors::InvalidHandle::new_err(format!( - "no impulse joint for {:?}", - handle.0.into_raw_parts() - ))); - } - Ok(ImpulseJoint { - backing: ImpulseJointBacking::InSet { - set: slf.clone().unbind(), - handle: handle.0, - }, - }) - } - - /// Return ``True`` iff ``handle`` resolves to a joint in this set. - fn __contains__(&self, handle: &ImpulseJointHandle) -> bool { - self.0.contains(handle.0) - } - - /// Number of joints currently in the set. - fn __len__(&self) -> usize { self.0.len() } - - /// Remove every joint from the set. - fn clear(&mut self) { - self.0 = rapier::dynamics::ImpulseJointSet::new(); - } - - /// Iterate over ``(handle, joint)`` pairs. - fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { - let handles: Vec = - slf.borrow().0.iter().map(|(h, _)| h).collect(); - Py::new( - slf.py(), - ImpulseJointSetIter { set: slf.clone().unbind(), handles, i: 0 }, - ) - } - - /// Iterate over the handles of every joint attached to ``body``. - fn attached_joints( - slf: PyRef<'_, Self>, - body: &RigidBodyHandle, - ) -> PyResult> { - let handles: Vec = slf - .0 - .attached_joints(body.0) - .map(|(_, _, h, _)| ImpulseJointHandle(h)) - .collect(); - Py::new(slf.py(), ImpulseJointHandleIter { handles, i: 0 }) - } - - /// Return the ``(body1, body2)`` pair the joint connects. - /// - /// :raises InvalidHandle: If ``handle`` is stale. - fn bodies_connected_by_joint( - &self, - handle: &ImpulseJointHandle, - ) -> PyResult<(RigidBodyHandle, RigidBodyHandle)> { - let j = self.0.get(handle.0).ok_or_else(|| { - $crate::errors::InvalidHandle::new_err(format!( - "no impulse joint for {:?}", - handle.0.into_raw_parts() - )) - })?; - Ok((RigidBodyHandle(j.body1()), RigidBodyHandle(j.body2()))) - } - } - - /// Iterator yielding ``(ImpulseJointHandle, ImpulseJoint)`` pairs - /// for every joint in an :class:`ImpulseJointSet`. - #[pyclass] - pub struct ImpulseJointSetIter { - set: Py, - handles: Vec, - i: usize, - } - - #[pymethods] - impl ImpulseJointSetIter { - /// Return ``self`` (iterator protocol). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Advance and return the next ``(handle, joint)`` pair (lazy view). - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(ImpulseJointHandle, ImpulseJoint)> { - if slf.i >= slf.handles.len() { return None; } - let py = slf.py(); - let handle = slf.handles[slf.i]; - slf.i += 1; - let set = slf.set.clone_ref(py); - Some(( - ImpulseJointHandle(handle), - ImpulseJoint { backing: ImpulseJointBacking::InSet { set, handle } }, - )) - } - } - - /// Iterator yielding handles of joints attached to a given body. - #[pyclass] - pub struct ImpulseJointHandleIter { - handles: Vec, - i: usize, - } - - #[pymethods] - impl ImpulseJointHandleIter { - /// Return ``self`` (iterator protocol). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Advance and return the next handle. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.handles.len() { return None; } - let h = slf.handles[slf.i]; - slf.i += 1; - Some(h) - } - } - - // ================================================================= - // MultibodyLink (view) - // ================================================================= - /// Read-only view of one link inside a multibody articulation. - /// - /// Each link wraps a rigid body and tracks its position in the - /// articulation tree (its ``link_id`` and its ``parent_id``, - /// which is ``None`` for the root). - #[pyclass(name = "MultibodyLink", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct MultibodyLink(pub rapier::dynamics::MultibodyLink); - - #[pymethods] - impl MultibodyLink { - /// Handle of the rigid body backing this link. - #[getter] - fn rigid_body(&self) -> RigidBodyHandle { - RigidBodyHandle(self.0.rigid_body_handle()) - } - /// Zero-based index of the link inside its articulation - /// (``0`` for the root). - #[getter] - fn link_id(&self) -> usize { self.0.link_id() } - /// Link index of the parent, or ``None`` if this is the root. - #[getter] - fn parent_id(&self) -> Option { self.0.parent_id() } - /// Return ``True`` iff this link is the root of its articulation. - fn is_root(&self) -> bool { self.0.is_root() } - /// Generalized coordinates of this link's joint. - /// - /// :returns: Flat list of length 3 in 2D (lin_x, lin_y, ang) and - /// 6 in 3D (lin_x, lin_y, lin_z, ang_x, ang_y, ang_z). Only - /// the entries for the joint's unlocked degrees of freedom are - /// meaningful. - #[getter] - fn coords(&self) -> Vec { - $crate::__spatial_vector_to_vec!($dim, self.0.joint().coords()) - } - /// This link's joint angular coordinates as a rotation. - #[getter] - fn joint_rot(&self) -> $Rot { - $Rot(self.0.joint().joint_rot().into()) - } - } - - // ================================================================= - // Multibody (view) - // ================================================================= - /// Reduced-coordinate articulation: a tree of links connected by - /// joints. - /// - /// A :class:`Multibody` represents an articulated system (robot, - /// character, ragdoll, ...) where each link is a rigid body and - /// the configuration is parameterized by the joint coordinates - /// (one per joint DOF, summing to :py:attr:`ndofs`). Unlike - /// impulse joints, the topology is restricted to a tree but the - /// constraints are exact and inertia is propagated analytically - /// along the kinematic chain. - #[pyclass(name = "Multibody", module = "rapier")] - #[derive(Debug)] - pub struct Multibody { - pub set: Py, - pub key: MultibodyKey, - } - - /// How a `Multibody` view locates its articulation within the - /// `MultibodyJointSet`: by stable index, or by one of its joint handles. - #[derive(Debug, Clone, Copy)] - pub enum MultibodyKey { - Index(rapier::dynamics::MultibodyIndex), - Joint(rapier::dynamics::MultibodyJointHandle), - } - - impl Clone for Multibody { - fn clone(&self) -> Self { - Python::with_gil(|py| Multibody { set: self.set.clone_ref(py), key: self.key }) - } - } - - impl Multibody { - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::Multibody) -> R) -> R { - Python::with_gil(|py| { - let set = self.set.bind(py).borrow(); - let mb = match self.key { - MultibodyKey::Index(i) => set.0.get_multibody(i), - MultibodyKey::Joint(h) => set.0.get(h).map(|(mb, _)| mb), - } - .expect("Multibody refers to an articulation no longer in its set"); - f(mb) - }) - } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::Multibody) -> R) -> R { - Python::with_gil(|py| { - let mut set = self.set.bind(py).borrow_mut(); - let mb = match self.key { - MultibodyKey::Index(i) => set.0.get_multibody_mut(i), - MultibodyKey::Joint(h) => set.0.get_mut(h).map(|(mb, _)| mb), - } - .expect("Multibody refers to an articulation no longer in its set"); - f(mb) - }) - } - } - - #[pymethods] - impl Multibody { - /// Handle of the root link's rigid body. - #[getter] - fn root_handle(&self) -> RigidBodyHandle { - self.with_ref(|mb| RigidBodyHandle(mb.root().rigid_body_handle())) - } - /// Total number of links in the articulation. - #[getter] - fn num_links(&self) -> usize { self.with_ref(|mb| mb.num_links()) } - /// Total number of degrees of freedom across all joints. - #[getter] - fn ndofs(&self) -> usize { self.with_ref(|mb| mb.ndofs()) } - /// Return the link with index ``idx``, or ``None`` if out of - /// range. - fn get_link(&self, idx: usize) -> Option { - self.with_ref(|mb| mb.link(idx).copied().map(MultibodyLink)) - } - /// Iterate over the links in depth-first order from the root. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let links: Vec = - slf.with_ref(|mb| mb.links().copied().map(MultibodyLink).collect()); - Py::new(slf.py(), MultibodyLinkIter { links, i: 0 }) - } - /// Return ``True`` iff contacts between links of this - /// articulation are enabled. - fn self_contacts_enabled(&self) -> bool { - self.with_ref(|mb| mb.self_contacts_enabled()) - } - /// Enable or disable contacts between links of this - /// articulation. - fn set_self_contacts_enabled(&mut self, v: bool) { - self.with_mut(|mb| mb.set_self_contacts_enabled(v)); - } - - /// Per-degree-of-freedom joint damping coefficients - /// (length :attr:`ndofs`). - fn damping(&self) -> Vec { - self.with_ref(|mb| mb.damping().iter().copied().collect()) - } - /// Set the per-DOF joint damping coefficients. - /// - /// :raises ValueError: If ``values`` length differs from `ndofs`. - fn set_damping(&mut self, values: Vec) -> PyResult<()> { - self.with_mut(|mb| { - let mut d = mb.damping_mut(); - if values.len() != d.len() { - return Err(PyValueError::new_err(format!( - "expected {} damping values (ndofs), got {}", - d.len(), - values.len() - ))); - } - for (i, v) in values.iter().enumerate() { - d[i] = *v; - } - Ok(()) - }) - } - /// Generalized velocity vector (one entry per DOF). - fn generalized_velocity(&self) -> Vec { - self.with_ref(|mb| mb.generalized_velocity().iter().copied().collect()) - } - /// Set the generalized velocity vector. - /// - /// :raises ValueError: If ``values`` length differs from `ndofs`. - fn set_generalized_velocity(&mut self, values: Vec) -> PyResult<()> { - self.with_mut(|mb| { - let mut v = mb.generalized_velocity_mut(); - if values.len() != v.len() { - return Err(PyValueError::new_err(format!( - "expected {} velocity values (ndofs), got {}", - v.len(), - values.len() - ))); - } - for (i, x) in values.iter().enumerate() { - v[i] = *x; - } - Ok(()) - }) - } - /// Generalized acceleration vector from the last solver step. - fn generalized_acceleration(&self) -> Vec { - self.with_ref(|mb| mb.generalized_acceleration().iter().copied().collect()) - } - /// Velocity DOFs belonging to a single ``link``. - fn joint_velocity(&self, link: &MultibodyLink) -> Vec { - self.with_ref(|mb| mb.joint_velocity(&link.0).iter().copied().collect()) - } - /// The body Jacobian of link ``link_id`` as a row-major nested - /// list with ``2*dim`` rows (linear then angular) and `ndofs` - /// columns. - fn body_jacobian(&self, link_id: usize) -> Vec> { - self.with_ref(|mb| { - let j = mb.body_jacobian(link_id); - (0..j.nrows()) - .map(|r| (0..j.ncols()).map(|c| j[(r, c)]).collect()) - .collect() - }) - } - /// Link indices on the path from the root to ``link_id`` - /// (inclusive). - fn kinematic_branch(&self, link_id: usize) -> Vec { - self.with_ref(|mb| mb.kinematic_branch(link_id)) - } - /// Write each link's pose (and optionally mass properties) into - /// ``bodies`` from this multibody's current generalized state. - fn update_rigid_bodies(&self, bodies: &mut RigidBodySet, update_mass_properties: bool) { - self.with_ref(|mb| mb.update_rigid_bodies(&mut bodies.0, update_mass_properties)); - } - /// Recompute link poses from the generalized coordinates - /// (forward kinematics). - /// - /// :param bodies: Rigid-body set (read-only here). - /// :param read_root_pose_from_rigid_body: If ``True``, seed the - /// root link's pose from its rigid body first. - #[pyo3(signature = (bodies, read_root_pose_from_rigid_body=false))] - fn forward_kinematics( - &mut self, - bodies: &RigidBodySet, - read_root_pose_from_rigid_body: bool, - ) { - self.with_mut(|mb| { - mb.forward_kinematics(&bodies.0, read_root_pose_from_rigid_body) - }); - } - } - - /// Iterator yielding the :class:`MultibodyLink`-s of a - /// :class:`Multibody`. - #[pyclass] - pub struct MultibodyLinkIter { - links: Vec, - i: usize, - } - - #[pymethods] - impl MultibodyLinkIter { - /// Return ``self`` (iterator protocol). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Advance and return the next link. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.links.len() { return None; } - let l = slf.links[slf.i]; - slf.i += 1; - Some(l) - } - } - - // ================================================================= - // MultibodyJointSet - // ================================================================= - /// Container of reduced-coordinate multibody articulations. - /// - /// Joints inserted here build tree-structured articulations (one - /// :class:`Multibody` per connected component) whose constraints - /// are solved in reduced coordinates rather than via impulses. - /// Recommended for robots, characters, and any kinematic chain - /// where exact joint behaviour is more important than the - /// flexibility of arbitrary topologies (use - /// :class:`ImpulseJointSet` for those). - /// - /// Supports ``len()`` and iteration over joint handles. - #[pyclass(name = "MultibodyJointSet", module = "rapier", unsendable)] - pub struct MultibodyJointSet(pub rapier::dynamics::MultibodyJointSet); - - #[pymethods] - impl MultibodyJointSet { - /// Construct an empty :class:`MultibodyJointSet`. - #[new] - fn new() -> Self { Self(rapier::dynamics::MultibodyJointSet::new()) } - - /// Insert a dynamic joint between ``parent`` and ``link_body``. - /// - /// :param parent: Handle of the body that will become the - /// parent link. - /// :param link_body: Handle of the body that will become the - /// child link. - /// :param joint: A concrete joint or builder. - /// :param wake_up: If ``True``, wake the attached bodies. - /// :returns: A new handle, or ``None`` if the bodies cannot be - /// attached (e.g. doing so would create a cycle). - #[pyo3(signature = (parent, link_body, joint, wake_up=true))] - fn insert( - &mut self, - parent: &RigidBodyHandle, - link_body: &RigidBodyHandle, - joint: &Bound<'_, PyAny>, - wake_up: bool, - ) -> PyResult> { - let data = $crate::__extract_generic_joint!(joint)?; - Ok(self.0.insert(parent.0, link_body.0, data, wake_up).map(MultibodyJointHandle)) - } - - /// Insert a kinematic joint between ``parent`` and ``link_body``. - /// - /// Kinematic joints are user-driven: the solver enforces the - /// constraint but the joint coordinates themselves are - /// imposed externally rather than evolved by the dynamics. - /// - /// :param parent: Handle of the parent body. - /// :param link_body: Handle of the child body. - /// :param joint: A concrete joint or builder. - /// :param wake_up: If ``True``, wake the attached bodies. - #[pyo3(signature = (parent, link_body, joint, wake_up=true))] - fn insert_kinematic( - &mut self, - parent: &RigidBodyHandle, - link_body: &RigidBodyHandle, - joint: &Bound<'_, PyAny>, - wake_up: bool, - ) -> PyResult> { - let data = $crate::__extract_generic_joint!(joint)?; - Ok(self - .0 - .insert_kinematic(parent.0, link_body.0, data, wake_up) - .map(MultibodyJointHandle)) - } - - /// Remove the joint identified by ``handle``. - /// - /// :param handle: Handle returned by :py:meth:`insert`. - /// :param wake_up: If ``True``, wake the formerly attached - /// bodies. - #[pyo3(signature = (handle, wake_up=true))] - fn remove(&mut self, handle: &MultibodyJointHandle, wake_up: bool) { - self.0.remove(handle.0, wake_up); - } - - /// Return the ``(multibody, link_id)`` pair containing the joint. - /// The multibody is a live **view** into the set. - fn get(slf: &Bound<'_, Self>, handle: &MultibodyJointHandle) -> Option<(Multibody, usize)> { - let id = slf.borrow().0.get(handle.0).map(|(_, id)| id)?; - Some(( - Multibody { - set: slf.clone().unbind(), - key: MultibodyKey::Joint(handle.0), - }, - id, - )) - } - - /// Return the :class:`Multibody` (live view) containing the joint. - fn multibody(slf: &Bound<'_, Self>, handle: &MultibodyJointHandle) -> Option { - slf.borrow().0.get(handle.0)?; - Some(Multibody { - set: slf.clone().unbind(), - key: MultibodyKey::Joint(handle.0), - }) - } - - /// Return the :class:`MultibodyLinkId` of ``body`` if it - /// belongs to one of the articulations stored here. - fn rigid_body_link(&self, body: &RigidBodyHandle) -> Option { - self.0.rigid_body_link(body.0).copied().map(MultibodyLinkId) - } - - /// Return the articulation (live view) referred to by ``index``. - fn get_multibody(slf: &Bound<'_, Self>, index: &MultibodyIndex) -> Option { - slf.borrow().0.get_multibody(index.0)?; - Some(Multibody { - set: slf.clone().unbind(), - key: MultibodyKey::Index(index.0), - }) - } - - /// Find the multibody joint connecting two bodies, if they are - /// directly linked within the same articulation. - /// - /// :returns: ``(handle, multibody, link)`` for the child link, or - /// ``None`` if the bodies are not joined in one articulation. - fn joint_between( - slf: &Bound<'_, Self>, - body1: &RigidBodyHandle, - body2: &RigidBodyHandle, - ) -> Option<(MultibodyJointHandle, Multibody, MultibodyLink)> { - let (h, link) = slf - .borrow() - .0 - .joint_between(body1.0, body2.0) - .map(|(h, _mb, link)| (h, *link))?; - Some(( - MultibodyJointHandle(h), - Multibody { - set: slf.clone().unbind(), - key: MultibodyKey::Joint(h), - }, - MultibodyLink(link), - )) - } - - /// Number of articulations currently stored. - fn __len__(&self) -> usize { self.0.multibodies().count() } - - /// Iterate over every joint handle in the set. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let mut handles: Vec = vec![]; - for (h, _, _, _) in slf.0.iter() { - handles.push(MultibodyJointHandle(h)); - } - Py::new(slf.py(), MultibodyJointSetIter { handles, i: 0 }) - } - - /// Iterate over the handles of every joint attached to ``body``. - fn attached_joints( - slf: PyRef<'_, Self>, - body: &RigidBodyHandle, - ) -> PyResult> { - let handles: Vec = slf - .0 - .attached_joints(body.0) - .map(|(_, _, h)| MultibodyJointHandle(h)) - .collect(); - Py::new(slf.py(), MultibodyJointHandleIter { handles, i: 0 }) - } - - /// Solve inverse kinematics for the link identified by - /// ``handle``. - /// - /// Iteratively drives the chain's joint coordinates so that - /// the link's pose matches ``target_pose``. The solver - /// terminates when the residual is below - /// :py:attr:`InverseKinematicsOption.epsilon_linear` and - /// :py:attr:`InverseKinematicsOption.epsilon_angular`, or - /// after :py:attr:`InverseKinematicsOption.max_iters` - /// iterations. - /// - /// :param bodies: Rigid-body set containing the articulation's - /// bodies. - /// :param handle: Handle of the joint whose link is being - /// positioned. - /// :param target_pose: Desired world-space pose for the link. - /// :param option: Solver tuning; defaults to a sensible - /// starting point if omitted. - /// :returns: Flat list of length :py:attr:`Multibody.ndofs` - /// giving the joint-coordinate displacement that achieves - /// the IK target. - /// :raises InvalidHandle: If ``handle`` is stale. - // Inverse kinematics passthrough for a single link of a multibody. - // - // This is a thin wrapper around `Multibody::inverse_kinematics` - // that returns the resulting displacement vector. - #[pyo3(signature = (bodies, handle, target_pose, option=None))] - fn inverse_kinematics_for_link( - &self, - bodies: &RigidBodySet, - handle: &MultibodyJointHandle, - target_pose: PyIsometry, - option: Option<&InverseKinematicsOption>, - ) -> PyResult> { - let (mb, link_id) = self.0.get(handle.0).ok_or_else(|| { - $crate::errors::InvalidHandle::new_err(format!( - "no multibody joint for {:?}", - handle.0.into_raw_parts() - )) - })?; - let opts = option - .copied() - .unwrap_or_else(|| InverseKinematicsOption { - damping: 1.0, - max_iters: 10, - constrained_axes: JointAxesMask(rapier::dynamics::JointAxesMask::all()), - epsilon_linear: 1.0e-3, - epsilon_angular: 1.0e-3, - }); - let rapier_opts = opts.to_rapier(); - let target: rapier::math::Pose = target_pose.0.into(); - let mut displacements = - $crate::na::DVector::::zeros(mb.ndofs()); - mb.inverse_kinematics( - &bodies.0, - link_id, - &rapier_opts, - &target, - |_| true, - &mut displacements, - ); - Ok(displacements.iter().copied().collect()) - } - } - - /// Iterator yielding every :class:`MultibodyJointHandle` in a - /// :class:`MultibodyJointSet`. - #[pyclass] - pub struct MultibodyJointSetIter { - handles: Vec, - i: usize, - } - - #[pymethods] - impl MultibodyJointSetIter { - /// Return ``self`` (iterator protocol). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Advance and return the next handle. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.handles.len() { return None; } - let h = slf.handles[slf.i]; - slf.i += 1; - Some(h) - } - } - - /// Iterator yielding the handles of every multibody joint - /// attached to a given rigid body. - #[pyclass] - pub struct MultibodyJointHandleIter { - handles: Vec, - i: usize, - } - - #[pymethods] - impl MultibodyJointHandleIter { - /// Return ``self`` (iterator protocol). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - /// Advance and return the next handle. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - if slf.i >= slf.handles.len() { return None; } - let h = slf.handles[slf.i]; - slf.i += 1; - Some(h) - } - } - }; -} - -// ==================================================================== -// Helper macros — dim-specific bits the common macro can't express. -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __revolute_new { - (2, $axis:expr) => {{ - let _ = $axis; - Ok(Self(rapier::dynamics::RevoluteJoint::new())) - }}; - (3, $axis:expr) => {{ - let axis: PyVector = $axis.ok_or_else(|| { - $crate::pyo3::exceptions::PyTypeError::new_err( - "RevoluteJoint.new(axis=Vector) is required in 3D", - ) - })?; - let g: rapier::math::Vector = axis.0.into(); - Ok(Self(rapier::dynamics::RevoluteJoint::new(g))) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __revolute_builder_new { - (2, $axis:expr) => {{ - let _ = $axis; - Ok::( - rapier::dynamics::RevoluteJointBuilder::new(), - ) - }}; - (3, $axis:expr) => {{ - let axis: PyVector = $axis.ok_or_else(|| { - $crate::pyo3::exceptions::PyTypeError::new_err( - "RevoluteJoint.builder(axis=Vector) is required in 3D", - ) - })?; - let g: rapier::math::Vector = axis.0.into(); - Ok::( - rapier::dynamics::RevoluteJointBuilder::new(g), - ) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __spatial_vector_to_vec { - // 2D: SpatialVector = parry::math::Vec3 (3 lanes: x, y, z). - (2, $sv:expr) => {{ - let v = $sv; - vec![v.x, v.y, v.z] - }}; - // 3D: SpatialVector = [Real; 6]. - (3, $sv:expr) => {{ - let v = $sv; - v.to_vec() - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __extract_generic_joint { - ($obj:expr) => {{ - // Accept any of the concrete joint wrappers OR a GenericJoint OR a - // builder. Return Result. - let obj = $obj; - let result: $crate::pyo3::PyResult = (|| { - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, GenericJoint>>() { - return Ok(j.to_owned_generic()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, GenericJointBuilder>>() { - return Ok(b.0.build()); - } - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, FixedJoint>>() { - return Ok(j.0.into()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, FixedJointBuilder>>() { - return Ok(b.0.build().into()); - } - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, RevoluteJoint>>() { - return Ok(j.0.into()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, RevoluteJointBuilder>>() { - return Ok(b.0.build().into()); - } - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, PrismaticJoint>>() { - return Ok(j.0.into()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, PrismaticJointBuilder>>() { - return Ok(b.0.build().into()); - } - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, RopeJoint>>() { - return Ok(j.0.into()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, RopeJointBuilder>>() { - return Ok(b.0.build().into()); - } - if let Ok(j) = obj.extract::<$crate::pyo3::PyRef<'_, SpringJoint>>() { - return Ok(j.0.into()); - } - if let Ok(b) = obj.extract::<$crate::pyo3::PyRef<'_, SpringJointBuilder>>() { - return Ok(b.0.build().into()); - } - $crate::__extract_generic_joint_dim_specific!(obj) - })(); - result - }}; -} - -// SphericalJoint is 3D only; PinSlotJoint is 2D only. Each dim macro -// (`__define_joints_2d!` / `__define_joints_3d!`) emits a free function -// `try_extract_dim_specific_joint` in the cdylib namespace that handles its -// dim's specific joint(s). This shared macro just calls into that. -#[doc(hidden)] -#[macro_export] -macro_rules! __extract_generic_joint_dim_specific { - ($obj:expr) => {{ - let obj = $obj; - $crate::__extract_generic_joint_dim_specific_inner!(obj) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __extract_generic_joint_dim_specific_inner { - ($obj:expr) => {{ - let obj = $obj; - // The dim-specific macros emit a free function - // `try_extract_dim_specific_joint(obj) -> PyResult>` - // that handles SphericalJoint (3D) or PinSlotJoint (2D). - match try_extract_dim_specific_joint(obj)? { - Some(gj) => Ok(gj), - None => Err($crate::pyo3::exceptions::PyTypeError::new_err( - "expected a joint (concrete or generic) or a joint builder", - )), - } - }}; -} - -// ==================================================================== -// register_joints — per-dim entry point. -// ==================================================================== - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_joints_register { - (3) => { - pub fn register_joints( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - // Enums/handles/structs - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // Concrete joints + builders - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // Sets & views - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; - (2) => { - pub fn register_joints( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/lib.rs b/python/rapier-py-core/src/lib.rs deleted file mode 100644 index 2b65cbfe7..000000000 --- a/python/rapier-py-core/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Shared PyO3 plumbing for the four `_rapier{2,3}d{,_f64}` extension modules. -//! -//! This crate is intentionally **not feature-flagged** on `(dim, scalar)` — -//! Cargo unifies features across workspace members, which would mash dim2 and -//! dim3 into the same build. Instead, the dim-/scalar-generic conversion -//! shims (`PyVector`, `PyPoint`, `PyRotation`, `PyIsometry`, `PyAngVector`) -//! are emitted by the `define_conv_types!` macro that each cdylib invokes -//! with its concrete `Real`/`DIM` parameters. -//! -//! The error tree (`RapierError` and its subclasses) IS dim/scalar-independent -//! and lives here as a concrete module that every cdylib registers. - -pub use nalgebra as na; -pub use numpy; -pub use pyo3; -// Re-export the serde/bincode/serde_json crates so the macros emitted by -// `define_serde_types!` can refer to them via stable paths -// (`$crate::bincode`, `$crate::serde_json`). -pub use bincode; -pub use serde_json; - -pub mod controllers; -pub mod conv; -pub mod debug_render; -pub mod dynamics; -pub mod errors; -pub mod events_hooks; -pub mod geometry; -pub mod joints; -pub mod loaders; -pub mod math; -pub mod mjcf; -pub mod pipeline; -pub mod serde_io; -pub mod serde_macros; -pub mod urdf; - -pub use errors::*; diff --git a/python/rapier-py-core/src/loaders.rs b/python/rapier-py-core/src/loaders.rs deleted file mode 100644 index 7f3475155..000000000 --- a/python/rapier-py-core/src/loaders.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Macro emitting the loader `#[pyclass]`-es per 3D cdylib. -//! -//! Loaders (URDF / MJCF / Mesh). -//! -//! Only 3D cdylibs invoke `define_loaders_types!(DIM = 3)` (the upstream -//! `rapier3d-urdf` / `rapier3d-meshloader` / `rapier3d-mjcf` crates are -//! 3D-only and only exist in the `rapier3d` (f32) variant). -//! -//! Like the other modules, this macro is invoked once per cdylib *after* -//! `define_conv_types!`, `define_math_types!`, `define_geometry_types!`, -//! `define_joints_types!` and `define_dynamics_types!`. -//! -//! `define_loaders_types!` produces `register_loaders(py, m)` for the -//! `#[pymodule]` entry point. -//! -//! ## Layout -//! -//! The mesh loader lives here. The URDF and MJCF loaders are large enough to -//! warrant their own files, mirroring the upstream `rapier3d-urdf` / -//! `rapier3d-mjcf` crate boundaries: -//! -//! - [`crate::urdf`] — emits `__define_loaders_urdf!` + `register_loaders_urdf`. -//! - [`crate::mjcf`] — emits `__define_loaders_mjcf!` + `register_loaders_mjcf`. -//! -//! `__define_loaders_3d!` calls those two macros so all three loaders expand -//! into the same cdylib namespace, and `register_loaders` chains their -//! per-loader registration helpers. -//! -//! ## Notes -//! -//! - `rapier3d-urdf` / `rapier3d-meshloader` / `rapier3d-mjcf` have no -//! `-f64` upstream variant. The 3D-f64 cdylib therefore does *not* expose -//! loader symbols. - -/// Materialize the loader `#[pyclass]` types for a given 3D cdylib. -#[macro_export] -macro_rules! define_loaders_types { - (DIM = 3) => { - $crate::__define_loaders_3d!(); - $crate::__define_loaders_register!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_loaders_3d { - () => { - // ============================================================== - // Mesh loader - // ============================================================== - - /// One shape extracted by :func:`load_from_path` or - /// :func:`load_from_raw_mesh`. - /// - /// Bundles the converted :class:`SharedShape`, its world-relative - /// :class:`Isometry3` pose (taken from any ``visual.origin`` / - /// ``collision.origin`` element in the source file), and the raw - /// triangle mesh ``(vertices, indices)`` used to build the shape. - /// - /// :ivar shape: Converted collision shape. - /// :ivar pose: World-relative pose of the shape. - /// :ivar vertices: ``(N, 3)`` float32 NumPy array of mesh vertices. - /// :ivar indices: ``(M, 3)`` uint32 NumPy array of triangle indices. - #[pyclass(name = "LoadedShape", module = "rapier")] - pub struct LoadedShape { - #[pyo3(get)] - pub shape: SharedShape, - #[pyo3(get)] - pub pose: Isometry3, - vertices: Vec<[f32; 3]>, - indices: Vec<[u32; 3]>, - } - - #[pymethods] - impl LoadedShape { - /// ``(N, 3)`` float32 NumPy array of raw mesh vertex positions. - #[getter] - fn vertices<'py>( - &self, - py: $crate::pyo3::Python<'py>, - ) -> $crate::pyo3::Bound<'py, $crate::numpy::PyArray2> { - use $crate::numpy::PyArray2; - let n = self.vertices.len(); - let mut flat: Vec = Vec::with_capacity(n * 3); - for v in &self.vertices { - flat.extend_from_slice(v); - } - PyArray2::from_vec2_bound( - py, - &self.vertices.iter().map(|v| v.to_vec()).collect::>(), - ) - .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 3], false)) - } - - /// ``(M, 3)`` uint32 NumPy array of triangle face indices. - #[getter] - fn indices<'py>( - &self, - py: $crate::pyo3::Python<'py>, - ) -> $crate::pyo3::Bound<'py, $crate::numpy::PyArray2> { - use $crate::numpy::PyArray2; - PyArray2::from_vec2_bound( - py, - &self.indices.iter().map(|v| v.to_vec()).collect::>(), - ) - .unwrap_or_else(|_| PyArray2::::zeros_bound(py, [0, 3], false)) - } - - /// Return the ``LoadedShape(...)`` repr. - fn __repr__(&self) -> String { - format!( - "LoadedShape(shape={:?}, n_vertices={}, n_faces={})", - self.shape.0.shape_type(), - self.vertices.len(), - self.indices.len(), - ) - } - } - - fn _loaded_shape_from_meshloader(loaded: rapier3d_meshloader::LoadedShape) -> LoadedShape { - let iso: $crate::na::Isometry = loaded.pose.into(); - LoadedShape { - shape: SharedShape(loaded.shape), - pose: Isometry3(iso), - vertices: loaded.raw_mesh.vertices.clone(), - indices: loaded.raw_mesh.faces.clone(), - } - } - - /// Load shapes from a mesh file on disk. - /// - /// The file is parsed (formats supported by ``rapier3d-meshloader`` - /// — typically OBJ, GLTF, STL) into one or more groups; each group - /// is independently converted into a shape using ``converter``. - /// - /// :param path: Path to the source mesh file. - /// :param converter: :class:`MeshConverter` to use (defaults to - /// :attr:`MeshConverter.TriMesh`). - /// :param scale: Uniform scale applied during conversion. - /// :returns: A list with one entry per source group, each either a - /// :class:`LoadedShape` (success) or a ``MeshConversionError`` - /// instance (failure). - /// :raises MeshLoaderError: if the file itself failed to load. - #[pyfunction] - #[pyo3(name = "load_from_path")] - #[pyo3(signature = (path, converter=None, scale=1.0))] - fn loaders_mesh_load_from_path( - py: $crate::pyo3::Python<'_>, - path: &str, - converter: Option, - scale: Real, - ) -> $crate::pyo3::PyResult> { - use $crate::pyo3::IntoPy; - let converter = - converter.unwrap_or(MeshConverter(rapier::geometry::MeshConverter::TriMesh)); - let scale_v = rapier::math::Vector::new(scale, scale, scale); - let results = rapier3d_meshloader::load_from_path(path, &converter.0, scale_v) - .map_err(|e| $crate::errors::MeshLoaderError::new_err(format!("{e}")))?; - let mut out: Vec<$crate::pyo3::PyObject> = Vec::with_capacity(results.len()); - for r in results { - match r { - Ok(loaded) => { - let py_loaded = _loaded_shape_from_meshloader(loaded); - out.push($crate::pyo3::Py::new(py, py_loaded)?.into_py(py)); - } - Err(e) => { - let exc = $crate::errors::MeshConversionError::new_err(format!("{e}")); - out.push(exc.value_bound(py).clone().into()); - } - } - } - Ok(out) - } - - /// Load a single shape from an in-memory triangle mesh. - /// - /// Identical to :func:`load_from_path` except the mesh data comes - /// from NumPy arrays / sequences directly. No file IO. - /// - /// :param vertices: Sequence of 3D vertex positions. - /// :param indices: Sequence of triangle indices ``(i, j, k)``. - /// :param converter: :class:`MeshConverter` (defaults to TriMesh). - /// :param scale: Uniform scale applied during conversion. - /// :returns: A :class:`LoadedShape`. - /// :raises MeshConversionError: if the conversion failed. - #[pyfunction] - #[pyo3(name = "load_from_raw_mesh")] - #[pyo3(signature = (vertices, indices, converter=None, scale=1.0))] - fn loaders_mesh_load_from_raw_mesh( - _py: $crate::pyo3::Python<'_>, - vertices: &$crate::pyo3::Bound<'_, $crate::pyo3::PyAny>, - indices: &$crate::pyo3::Bound<'_, $crate::pyo3::PyAny>, - converter: Option, - scale: Real, - ) -> $crate::pyo3::PyResult { - let verts = extract_verts_for_dim(vertices)?; - let idx = $crate::geometry::extract_indices(indices)?; - let converter = - converter.unwrap_or(MeshConverter(rapier::geometry::MeshConverter::TriMesh)); - let mut mesh = mesh_loader::Mesh::default(); - mesh.vertices = verts - .iter() - .map(|v| [v.x as f32, v.y as f32, v.z as f32]) - .collect(); - mesh.faces = idx.clone(); - let scale_v = rapier::math::Vector::new(scale, scale, scale); - let (shape, pose) = - rapier3d_meshloader::load_from_raw_mesh(&mesh, &converter.0, scale_v) - .map_err(|e| $crate::errors::MeshConversionError::new_err(format!("{e}")))?; - let iso: $crate::na::Isometry = pose.into(); - Ok(LoadedShape { - shape: SharedShape(shape), - pose: Isometry3(iso), - vertices: mesh.vertices, - indices: mesh.faces, - }) - } - - // The URDF and MJCF loaders live in their own files; expand them into - // the same cdylib namespace right after the mesh loader. - $crate::__define_loaders_urdf!(); - $crate::__define_loaders_mjcf!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_loaders_register { - () => { - pub fn register_loaders( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - // Mesh loader - m.add_class::()?; - m.add_function($crate::pyo3::wrap_pyfunction!( - loaders_mesh_load_from_path, - m - )?)?; - m.add_function($crate::pyo3::wrap_pyfunction!( - loaders_mesh_load_from_raw_mesh, - m - )?)?; - // URDF + MJCF loaders (defined in `crate::urdf` / `crate::mjcf`). - register_loaders_urdf(m)?; - register_loaders_mjcf(m)?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/math.rs b/python/rapier-py-core/src/math.rs deleted file mode 100644 index 1e0c2896f..000000000 --- a/python/rapier-py-core/src/math.rs +++ /dev/null @@ -1,1896 +0,0 @@ -//! Macros that emit the user-facing math `#[pyclass]` types -//! (`Vec2`/`Vec3`/`Point2`/`Point3`/`Rotation2`/`Rotation3`/`Isometry2`/`Isometry3`) -//! plus helpers, per cdylib. -//! -//! Why this lives in a macro rather than a generic module: -//! `#[pyclass]` types must be **concrete** Rust structs — PyO3 cannot register -//! generics. Each cdylib has a fixed `(Real, DIM)` pair, so we expand a macro -//! once per cdylib that synthesizes the concrete types and the `register_math` -//! function used by `#[pymodule]`. -//! -//! Defaults for `approx_eq` tolerances: `1e-7` for `f32`, `1e-14` for `f64`. - -/// Default `approx_eq` epsilon for a given scalar. -pub trait DefaultEpsilon { - fn default_epsilon() -> Self; -} -impl DefaultEpsilon for f32 { - #[inline] - fn default_epsilon() -> Self { - // Loose enough to absorb a couple of float roundings in chained ops - // (e.g. composing two rotations and then transforming a vector). The - // spec calls for `1e-7` but in practice accumulated f32 error already - // reaches `~3e-7` after two SIMD steps, so we relax slightly. - 1.0e-5 - } -} -impl DefaultEpsilon for f64 { - #[inline] - fn default_epsilon() -> Self { - // Same rationale: spec is `1e-14`, but f64 chained ops can reach - // `~5e-15` quickly. We keep one decade of slack. - 1.0e-12 - } -} - -/// Repr format string per scalar (matches Python's default float repr precision). -pub trait ReprPrecision { - /// Format a single scalar for use inside `Vec3(…)`-style reprs. - fn fmt_repr(self) -> String; -} -impl ReprPrecision for f32 { - #[inline] - fn fmt_repr(self) -> String { - // f32 → match Python single-precision-ish; use 7 sig digits. - // We rely on Rust's default `Display` for f32 which is shortest-round-trip. - format!("{}", self) - } -} -impl ReprPrecision for f64 { - #[inline] - fn fmt_repr(self) -> String { - format!("{}", self) - } -} - -/// Materialize the math `#[pyclass]` types for a given `(Real, DIM)` pair. -/// -/// Invoke **after** `define_conv_types!(Real = …, DIM = …)` in the same cdylib -/// so the `PyVector`/`PyPoint`/`PyRotation`/`PyIsometry`/`PyAngVector` adapters -/// are in scope. -/// -/// Produces a function `register_math(py, m) -> PyResult<()>` that the -/// `#[pymodule]` calls to add the classes to the module table. -#[macro_export] -macro_rules! define_math_types { - (DIM = 2) => { - $crate::__define_math_2d!(); - }; - (DIM = 3) => { - $crate::__define_math_3d!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_math_2d { - () => { - use $crate::numpy::{PyArray1, PyArrayMethods}; - use $crate::pyo3::exceptions::{PyTypeError, PyValueError}; - use $crate::pyo3::prelude::*; - use $crate::pyo3::pyclass::CompareOp; - - // ------------------------------------------------------------------ - // Vec2 - // ------------------------------------------------------------------ - /// A 2D vector of two real numbers ``(x, y)``. - /// - /// Represents a free vector (a direction and magnitude) in 2D space. - /// Instances are immutable; arithmetic returns new ``Vec2`` values. - /// - /// Wraps :class:`nalgebra::Vector2`. - /// - /// Construct with the component constructor, the named static methods - /// (:meth:`zeros`, :meth:`unit_x`, :meth:`unit_y`), or from a Python - /// tuple / NumPy array via :meth:`from_tuple` / :meth:`from_ndarray`. - /// - /// Example:: - /// - /// from rapier2d import Vec2 - /// v = Vec2(1.0, 2.0) - /// w = Vec2.unit_x() + v - #[pyclass(name = "Vec2", module = "rapier.dim2", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Vec2(pub $crate::na::Vector2); - - #[pymethods] - impl Vec2 { - /// Create a new 2D vector from its ``x`` and ``y`` components. - /// - /// :param x: x component. - /// :param y: y component. - #[new] - fn new(x: Real, y: Real) -> Self { - Self($crate::na::Vector2::new(x, y)) - } - - /// Return the zero vector ``(0, 0)``. - #[staticmethod] - fn zeros() -> Self { - Self($crate::na::Vector2::zeros()) - } - /// Return the unit vector along the ``x`` axis, ``(1, 0)``. - #[staticmethod] - fn unit_x() -> Self { - Self($crate::na::Vector2::x()) - } - /// Return the unit vector along the ``y`` axis, ``(0, 1)``. - #[staticmethod] - fn unit_y() -> Self { - Self($crate::na::Vector2::y()) - } - - /// Build a :class:`Vec2` from a 2-element tuple or list. - /// - /// :param t: any Python sequence convertible to a 2D vector. - #[staticmethod] - fn from_tuple(t: PyVector) -> Self { - Self(t.0) - } - - /// Build a :class:`Vec2` from a length-2 NumPy array. - /// - /// :param a: a 1D NumPy array of shape ``(2,)``. - #[staticmethod] - fn from_ndarray(a: PyVector) -> Self { - Self(a.0) - } - - /// The ``x`` component of the vector. - #[getter] - fn x(&self) -> Real { - self.0.x - } - /// The ``y`` component of the vector. - #[getter] - fn y(&self) -> Real { - self.0.y - } - - /// Dot product with another vector. - /// - /// :param other: the right-hand side vector. - /// :returns: ``self.x * other.x + self.y * other.y``. - fn dot(&self, other: PyVector) -> Real { - self.0.dot(&other.0) - } - /// Euclidean (L2) norm, i.e. ``sqrt(x*x + y*y)``. - fn norm(&self) -> Real { - self.0.norm() - } - /// Squared Euclidean norm, i.e. ``x*x + y*y``. - /// - /// Cheaper than :meth:`norm` when only comparisons are needed. - fn norm_squared(&self) -> Real { - self.0.norm_squared() - } - - /// Return a unit-length copy of this vector. - /// - /// If the vector is exactly zero, returns the zero vector - /// unchanged (no division by zero). - fn normalize(&self) -> Self { - let n = self.0.norm(); - if n == 0 as Real { - Self(self.0) - } else { - Self(self.0 / n) - } - } - - /// Linear interpolation between ``self`` and ``other``. - /// - /// :param other: the target vector. - /// :param t: interpolation parameter (``0`` returns ``self``, - /// ``1`` returns ``other``). Values outside ``[0, 1]`` - /// extrapolate. - fn lerp(&self, other: PyVector, t: Real) -> Self { - Self(self.0 * (1 as Real - t) + other.0 * t) - } - - /// Return the components as a Python tuple ``(x, y)``. - fn to_tuple(&self) -> (Real, Real) { - (self.0.x, self.0.y) - } - - /// Return the components as a length-2 NumPy array. - fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - PyArray1::from_slice_bound(py, &[self.0.x, self.0.y]) - } - - /// Exact bitwise equality with another vector. - /// - /// Unlike ``==`` (which uses a tolerance), this requires the - /// underlying floats to compare equal bit-for-bit. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Component-wise addition: ``self + other``. - fn __add__(&self, other: PyVector) -> Self { - Self(self.0 + other.0) - } - /// Component-wise subtraction: ``self - other``. - fn __sub__(&self, other: PyVector) -> Self { - Self(self.0 - other.0) - } - /// Scalar multiplication: ``self * s``. - fn __mul__(&self, s: Real) -> Self { - Self(self.0 * s) - } - /// Reflected scalar multiplication: ``s * self``. - fn __rmul__(&self, s: Real) -> Self { - Self(self.0 * s) - } - /// Scalar division: ``self / s``. - /// - /// :raises ValueError: if ``s`` is zero. - fn __truediv__(&self, s: Real) -> PyResult { - if s == 0 as Real { - return Err(PyValueError::new_err("division by zero")); - } - Ok(Self(self.0 / s)) - } - /// Unary negation: ``-self``. - fn __neg__(&self) -> Self { - Self(-self.0) - } - - /// Number of components (always ``2``). - fn __len__(&self) -> usize { - 2 - } - - /// Component access by index ``0`` or ``1`` (negative indices - /// supported). - /// - /// :raises IndexError: if ``idx`` is out of range. - fn __getitem__(&self, idx: isize) -> PyResult { - let i = if idx < 0 { idx + 2 } else { idx }; - match i { - 0 => Ok(self.0.x), - 1 => Ok(self.0.y), - _ => Err(pyo3::exceptions::PyIndexError::new_err( - "Vec2 index out of range", - )), - } - } - - /// Iterate over the components in the order ``x``, ``y``. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let py = slf.py(); - Py::new(py, Vec2Iter { v: slf.0, i: 0 }) - } - - /// Human-readable representation, e.g. ``Vec2(1, 2)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!("Vec2({}, {})", self.0.x.fmt_repr(), self.0.y.fmt_repr()) - } - - /// Tolerant equality comparison. - /// - /// Two vectors compare equal when their Euclidean distance is at - /// most a small default epsilon (``1e-5`` for ``f32``, - /// ``1e-12`` for ``f64``). Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyVector, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let close = (self.0 - other.0).norm() <= eps; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Vec2 supports only == and !=")), - } - } - } - - /// Iterator helper yielding the components of a :class:`Vec2` in - /// ``x``, ``y`` order. Produced by iterating over a :class:`Vec2`. - #[pyclass] - pub struct Vec2Iter { - v: $crate::na::Vector2, - i: usize, - } - #[pymethods] - impl Vec2Iter { - /// Return ``self`` (iterators are their own iterators). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Yield the next component, or stop after ``y``. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - let r = match slf.i { - 0 => Some(slf.v.x), - 1 => Some(slf.v.y), - _ => None, - }; - if r.is_some() { - slf.i += 1; - } - r - } - } - - // ------------------------------------------------------------------ - // Point2 - // ------------------------------------------------------------------ - /// A point in 2D affine space ``(x, y)``. - /// - /// Conceptually distinct from :class:`Vec2`: a point represents a - /// location, a vector represents a displacement. Subtracting two - /// points yields a :class:`Vec2`; adding a :class:`Vec2` to a point - /// yields a new point. - /// - /// Wraps :class:`nalgebra::Point2`. - /// - /// Example:: - /// - /// from rapier2d import Point2, Vec2 - /// p = Point2(1.0, 2.0) - /// q = p + Vec2(3.0, 0.0) # Point2(4, 2) - /// displacement = q - p # Vec2(3, 0) - #[pyclass(name = "Point2", module = "rapier.dim2", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Point2(pub $crate::na::Point2); - - #[pymethods] - impl Point2 { - /// Create a new 2D point from its ``x`` and ``y`` coordinates. - /// - /// :param x: x coordinate. - /// :param y: y coordinate. - #[new] - fn new(x: Real, y: Real) -> Self { - Self($crate::na::Point2::new(x, y)) - } - /// Return the origin, ``Point2(0, 0)``. - #[staticmethod] - fn origin() -> Self { - Self($crate::na::Point2::origin()) - } - /// Build a :class:`Point2` from a 2-element tuple or list. - #[staticmethod] - fn from_tuple(t: PyPoint) -> Self { - Self(t.0) - } - /// Build a :class:`Point2` from a length-2 NumPy array. - #[staticmethod] - fn from_ndarray(a: PyPoint) -> Self { - Self(a.0) - } - - /// The ``x`` coordinate of the point. - #[getter] - fn x(&self) -> Real { - self.0.x - } - /// The ``y`` coordinate of the point. - #[getter] - fn y(&self) -> Real { - self.0.y - } - - /// The coordinates of the point as a :class:`Vec2` from origin. - #[getter] - fn coords(&self) -> Vec2 { - Vec2(self.0.coords) - } - - /// Return the coordinates as a Python tuple ``(x, y)``. - fn to_tuple(&self) -> (Real, Real) { - (self.0.x, self.0.y) - } - /// Return the coordinates as a length-2 NumPy array. - fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - PyArray1::from_slice_bound(py, &[self.0.x, self.0.y]) - } - - /// Exact bitwise equality with another point. - /// - /// Unlike ``==`` (tolerance-based), requires bit-identical - /// component values. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Translate by a vector: ``point + vector`` returns a new - /// :class:`Point2`. - // p + v -> Point - fn __add__(&self, v: PyVector) -> Self { - Self(self.0 + v.0) - } - - /// Subtraction. ``point - point`` returns a :class:`Vec2` - /// (displacement). ``point - vector`` returns a :class:`Point2` - /// (translation by ``-vector``). - // p - p -> Vec ; p - v -> Point - fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult { - let py = other.py(); - if let Ok(p) = other.extract::>() { - return Ok(Vec2(self.0 - p.0).into_py(py)); - } - if let Ok(p) = other.extract::() { - return Ok(Vec2(self.0 - p.0).into_py(py)); - } - let v: PyVector = other.extract()?; - Ok(Point2(self.0 - v.0).into_py(py)) - } - - /// Number of coordinates (always ``2``). - fn __len__(&self) -> usize { - 2 - } - /// Coordinate access by index ``0`` or ``1`` (negative indices - /// supported). - /// - /// :raises IndexError: if ``idx`` is out of range. - fn __getitem__(&self, idx: isize) -> PyResult { - let i = if idx < 0 { idx + 2 } else { idx }; - match i { - 0 => Ok(self.0.x), - 1 => Ok(self.0.y), - _ => Err(pyo3::exceptions::PyIndexError::new_err( - "Point2 index out of range", - )), - } - } - /// Iterate over the coordinates in the order ``x``, ``y``. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let py = slf.py(); - Py::new(py, Point2Iter { p: slf.0, i: 0 }) - } - - /// Human-readable representation, e.g. ``Point2(1, 2)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!("Point2({}, {})", self.0.x.fmt_repr(), self.0.y.fmt_repr()) - } - - /// Tolerant equality (Euclidean distance ≤ default epsilon). - /// Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyPoint, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let close = (self.0 - other.0).norm() <= eps; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Point2 supports only == and !=")), - } - } - } - - /// Iterator yielding the coordinates of a :class:`Point2` in - /// ``x``, ``y`` order. - #[pyclass] - pub struct Point2Iter { - p: $crate::na::Point2, - i: usize, - } - #[pymethods] - impl Point2Iter { - /// Return ``self`` (iterators are their own iterators). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Yield the next coordinate, or stop after ``y``. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - let r = match slf.i { - 0 => Some(slf.p.x), - 1 => Some(slf.p.y), - _ => None, - }; - if r.is_some() { - slf.i += 1; - } - r - } - } - - // ------------------------------------------------------------------ - // Rotation2 - // ------------------------------------------------------------------ - /// A 2D rotation, represented internally as a unit complex number. - /// - /// Encodes a rotation by an angle (in radians) about the origin. - /// The unit-complex representation is numerically stable and avoids - /// the storage / normalization issues of a 2x2 matrix. - /// - /// Wraps :class:`nalgebra::UnitComplex`. - /// - /// Example:: - /// - /// from math import pi - /// from rapier2d import Rotation2, Vec2 - /// r = Rotation2(pi / 2) - /// v = r * Vec2(1.0, 0.0) # Vec2(0, 1) - #[pyclass(name = "Rotation2", module = "rapier.dim2", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Rotation2(pub $crate::na::UnitComplex); - - #[pymethods] - impl Rotation2 { - /// Create a rotation by ``angle`` radians (counter-clockwise). - /// - /// :param angle: rotation angle in radians. - #[new] - fn new(angle: Real) -> Self { - Self($crate::na::UnitComplex::new(angle)) - } - - /// Return the identity rotation (angle = 0). - #[staticmethod] - fn identity() -> Self { - Self($crate::na::UnitComplex::identity()) - } - /// Build a rotation from an angle in radians. - /// - /// Equivalent to the constructor; provided for symmetry with - /// :class:`Rotation3`. - #[staticmethod] - fn from_angle(angle: Real) -> Self { - Self($crate::na::UnitComplex::new(angle)) - } - - /// Rotation angle in radians, in ``[-pi, pi]``. - #[getter] - fn angle(&self) -> Real { - self.0.angle() - } - /// Cosine of the rotation angle. - #[getter] - fn cos_angle(&self) -> Real { - self.0.cos_angle() - } - /// Sine of the rotation angle. - #[getter] - fn sin_angle(&self) -> Real { - self.0.sin_angle() - } - - /// Return the inverse rotation (rotation by ``-angle``). - fn inverse(&self) -> Self { - Self(self.0.inverse()) - } - - /// Return the equivalent 2x2 rotation matrix, row-major flattened - /// into a length-4 NumPy array. - fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - let m = self.0.to_rotation_matrix(); - let mm = m.matrix(); - // Row-major flatten 2x2 - PyArray1::from_slice_bound(py, &[mm[(0, 0)], mm[(0, 1)], mm[(1, 0)], mm[(1, 1)]]) - } - - /// Returns the rotation angle as a 1-element tuple — matches - /// `PyRotation`'s `FromPyObject` impl in 2D. - fn to_tuple(&self) -> (Real,) { - (self.0.angle(),) - } - - /// Rotate a vector and return a new :class:`Vec2`. - /// - /// :param v: the input vector. - fn transform_vector(&self, v: PyVector) -> Vec2 { - Vec2(self.0 * v.0) - } - /// Rotate a point about the origin and return a new - /// :class:`Point2`. - /// - /// :param p: the input point. - fn transform_point(&self, p: PyPoint) -> Point2 { - Point2(self.0 * p.0) - } - - /// Spherical-linear interpolation between two rotations. - /// - /// :param other: the target rotation. - /// :param t: interpolation parameter in ``[0, 1]``. - fn slerp(&self, other: PyRotation, t: Real) -> Self { - Self(self.0.slerp(&other.0, t)) - } - - /// Exact bitwise equality with another rotation. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Multiplication ``self * rhs``. Composes / transforms depending - /// on the right-hand side: - /// - /// - ``Rotation2 * Rotation2`` → composed :class:`Rotation2`. - /// - ``Rotation2 * Point2`` → rotated :class:`Point2`. - /// - ``Rotation2 * Vec2`` → rotated :class:`Vec2`. - fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { - let py = rhs.py(); - if let Ok(r) = rhs.extract::() { - return Ok(Rotation2(self.0 * r.0).into_py(py)); - } - if let Ok(p) = rhs.extract::>() { - return Ok(Point2(self.0 * p.0).into_py(py)); - } - if let Ok(p) = rhs.extract::() { - return Ok(Point2(self.0 * p.0).into_py(py)); - } - let v: PyVector = rhs.extract()?; - Ok(Vec2(self.0 * v.0).into_py(py)) - } - - /// Human-readable representation, e.g. ``Rotation2(angle=1.5708)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!("Rotation2(angle={})", self.0.angle().fmt_repr()) - } - - /// Tolerant equality. Two rotations compare equal when the - /// shortest signed angular distance between them is at most a - /// default epsilon. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyRotation, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let close = (self.0.angle_to(&other.0)).abs() <= eps; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Rotation2 supports only == and !=")), - } - } - } - - // ------------------------------------------------------------------ - // Isometry2 - // ------------------------------------------------------------------ - /// A 2D rigid-body transformation: a rotation followed by a - /// translation. - /// - /// Equivalently, an element of the special Euclidean group ``SE(2)``. - /// Composition is right-to-left as usual: ``A * B`` applies ``B`` - /// first then ``A``. - /// - /// Wraps :class:`nalgebra::Isometry2`. - /// - /// Example:: - /// - /// from math import pi - /// from rapier2d import Isometry2, Vec2, Rotation2 - /// pose = Isometry2(Vec2(1.0, 0.0), Rotation2(pi / 2)) - /// identity = Isometry2.identity() - #[pyclass(name = "Isometry2", module = "rapier.dim2", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Isometry2(pub $crate::na::Isometry2); - - #[pymethods] - impl Isometry2 { - /// Create an isometry from an optional translation and rotation. - /// - /// Both arguments default to identity (zero translation, - /// no rotation). - /// - /// :param translation: a :class:`Vec2` (or tuple / ndarray). - /// :param rotation: a :class:`Rotation2` (or scalar angle). - #[new] - #[pyo3(signature = (translation=None, rotation=None))] - fn new(translation: Option, rotation: Option) -> Self { - let t = translation - .map(|v| v.0) - .unwrap_or_else($crate::na::Vector2::zeros); - let r = rotation - .map(|r| r.0) - .unwrap_or_else($crate::na::UnitComplex::identity); - Self($crate::na::Isometry2::from_parts( - $crate::na::Translation2::from(t), - r, - )) - } - - /// Return the identity isometry (no translation, no rotation). - #[staticmethod] - fn identity() -> Self { - Self($crate::na::Isometry2::identity()) - } - - /// Translation-only constructor. - /// - /// Named `from_translation` (not `translation`) so it doesn't shadow - /// the `translation` property used to read the translation vector. - #[staticmethod] - #[pyo3(signature = (x, y))] - fn from_translation(x: Real, y: Real) -> Self { - Self($crate::na::Isometry2::translation(x, y)) - } - - /// Rotation-only constructor. - #[staticmethod] - fn from_rotation(angle: Real) -> Self { - Self($crate::na::Isometry2::rotation(angle)) - } - - /// Translation component of the isometry as a :class:`Vec2`. - #[getter] - fn translation(&self) -> Vec2 { - Vec2(self.0.translation.vector) - } - /// Rotation component of the isometry as a :class:`Rotation2`. - #[getter] - fn rotation(&self) -> Rotation2 { - Rotation2(self.0.rotation) - } - - /// Return the inverse isometry, such that ``self * self.inverse()`` - /// is the identity. - fn inverse(&self) -> Self { - Self(self.0.inverse()) - } - - /// Apply the full transformation (rotation then translation) - /// to a point. - fn transform_point(&self, p: PyPoint) -> Point2 { - Point2(self.0 * p.0) - } - /// Apply only the rotation part to a vector. Vectors are not - /// translated. - fn transform_vector(&self, v: PyVector) -> Vec2 { - Vec2(self.0 * v.0) - } - - /// Return the equivalent 3x3 homogeneous matrix, row-major - /// flattened into a length-9 NumPy array. - fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - let m = self.0.to_homogeneous(); - let mut out = [0 as Real; 9]; - for r in 0..3 { - for c in 0..3 { - out[r * 3 + c] = m[(r, c)]; - } - } - PyArray1::from_slice_bound(py, &out) - } - - /// Interpolate between two isometries: linear interpolation on - /// the translation, slerp on the rotation. - /// - /// :param other: the target isometry. - /// :param t: interpolation parameter in ``[0, 1]``. - fn lerp_slerp(&self, other: &Self, t: Real) -> Self { - Self(self.0.lerp_slerp(&other.0, t)) - } - - /// Exact bitwise equality with another isometry. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Multiplication ``self * rhs``. Composes / transforms depending - /// on the right-hand side: - /// - /// - ``Isometry2 * Isometry2`` → composed :class:`Isometry2`. - /// - ``Isometry2 * Point2`` → transformed :class:`Point2`. - /// - ``Isometry2 * Vec2`` → rotated :class:`Vec2` (no - /// translation). - fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { - let py = rhs.py(); - if let Ok(i) = rhs.extract::>() { - return Ok(Isometry2(self.0 * i.0).into_py(py)); - } - if let Ok(i) = rhs.extract::() { - return Ok(Isometry2(self.0 * i.0).into_py(py)); - } - if let Ok(p) = rhs.extract::>() { - return Ok(Point2(self.0 * p.0).into_py(py)); - } - if let Ok(p) = rhs.extract::() { - return Ok(Point2(self.0 * p.0).into_py(py)); - } - let v: PyVector = rhs.extract()?; - Ok(Vec2(self.0 * v.0).into_py(py)) - } - - /// Human-readable representation showing translation and angle. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!( - "Isometry2(translation=({}, {}), angle={})", - self.0.translation.x.fmt_repr(), - self.0.translation.y.fmt_repr(), - self.0.rotation.angle().fmt_repr(), - ) - } - - /// Tolerant equality. Both translation distance and rotation - /// angle must be within a default epsilon. Only ``==`` and ``!=`` - /// are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyIsometry, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let t_close = - (self.0.translation.vector - other.0.translation.vector).norm() <= eps; - let r_close = (self.0.rotation.angle_to(&other.0.rotation)).abs() <= eps; - let close = t_close && r_close; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Isometry2 supports only == and !=")), - } - } - } - - // ------------------------------------------------------------------ - // Free helper: rotation_from_angle - // ------------------------------------------------------------------ - /// Build a :class:`Rotation2` from an angle in radians. - /// - /// Mirrors the dimension-agnostic Rust helper - /// ``rapier::math::rotation_from_angle``; in 2D the "angular - /// vector" is just a scalar angle. - /// - /// :param angle: rotation angle in radians. - #[pyfunction] - fn rotation_from_angle(angle: Real) -> Rotation2 { - Rotation2($crate::na::UnitComplex::new(angle)) - } - - // ------------------------------------------------------------------ - // register_math - // ------------------------------------------------------------------ - pub fn register_math(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_function($crate::pyo3::wrap_pyfunction!(rotation_from_angle, m)?)?; - Ok(()) - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_math_3d { - () => { - use $crate::numpy::{PyArray1, PyArrayMethods}; - use $crate::pyo3::exceptions::{PyTypeError, PyValueError}; - use $crate::pyo3::prelude::*; - use $crate::pyo3::pyclass::CompareOp; - - // ------------------------------------------------------------------ - // Vec3 - // ------------------------------------------------------------------ - /// A 3D vector of three real numbers ``(x, y, z)``. - /// - /// Represents a free vector (a direction and magnitude) in 3D space. - /// Instances are immutable; arithmetic returns new ``Vec3`` values. - /// - /// Also exposed under the Python alias ``AngVector3`` to denote an - /// "angular vector" — a scaled axis-angle rotation parameterization - /// used by Rapier's joints and motors. - /// - /// Wraps :class:`nalgebra::Vector3`. - /// - /// Example:: - /// - /// from rapier3d import Vec3 - /// v = Vec3(1.0, 2.0, 3.0) - /// up = Vec3.unit_y() - /// n = v.cross(up).normalize() - #[pyclass(name = "Vec3", module = "rapier.dim3", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Vec3(pub $crate::na::Vector3); - - #[pymethods] - impl Vec3 { - /// Create a new 3D vector from its components. - /// - /// :param x: x component. - /// :param y: y component. - /// :param z: z component. - #[new] - fn new(x: Real, y: Real, z: Real) -> Self { - Self($crate::na::Vector3::new(x, y, z)) - } - - /// Return the zero vector ``(0, 0, 0)``. - #[staticmethod] - fn zeros() -> Self { - Self($crate::na::Vector3::zeros()) - } - /// Return the unit vector along the ``x`` axis, ``(1, 0, 0)``. - #[staticmethod] - fn unit_x() -> Self { - Self($crate::na::Vector3::x()) - } - /// Return the unit vector along the ``y`` axis, ``(0, 1, 0)``. - #[staticmethod] - fn unit_y() -> Self { - Self($crate::na::Vector3::y()) - } - /// Return the unit vector along the ``z`` axis, ``(0, 0, 1)``. - #[staticmethod] - fn unit_z() -> Self { - Self($crate::na::Vector3::z()) - } - - /// Build a :class:`Vec3` from a 3-element tuple or list. - #[staticmethod] - fn from_tuple(t: PyVector) -> Self { - Self(t.0) - } - /// Build a :class:`Vec3` from a length-3 NumPy array. - #[staticmethod] - fn from_ndarray(a: PyVector) -> Self { - Self(a.0) - } - - /// The ``x`` component of the vector. - #[getter] - fn x(&self) -> Real { - self.0.x - } - /// The ``y`` component of the vector. - #[getter] - fn y(&self) -> Real { - self.0.y - } - /// The ``z`` component of the vector. - #[getter] - fn z(&self) -> Real { - self.0.z - } - - /// Dot product with another vector. - /// - /// :param other: the right-hand side vector. - /// :returns: ``x1*x2 + y1*y2 + z1*z2``. - fn dot(&self, other: PyVector) -> Real { - self.0.dot(&other.0) - } - /// Cross product with another vector. - /// - /// :param other: the right-hand side vector. - /// :returns: a new :class:`Vec3` orthogonal to both inputs, - /// following the right-hand rule. - fn cross(&self, other: PyVector) -> Self { - Self(self.0.cross(&other.0)) - } - /// Euclidean (L2) norm, ``sqrt(x*x + y*y + z*z)``. - fn norm(&self) -> Real { - self.0.norm() - } - /// Squared Euclidean norm, ``x*x + y*y + z*z``. - /// - /// Cheaper than :meth:`norm` when only comparisons are needed. - fn norm_squared(&self) -> Real { - self.0.norm_squared() - } - /// Return a unit-length copy of this vector. - /// - /// If the vector is exactly zero, returns the zero vector - /// unchanged (no division by zero). - fn normalize(&self) -> Self { - let n = self.0.norm(); - if n == 0 as Real { - Self(self.0) - } else { - Self(self.0 / n) - } - } - /// Linear interpolation between ``self`` and ``other``. - /// - /// :param other: the target vector. - /// :param t: interpolation parameter (``0`` returns ``self``, - /// ``1`` returns ``other``). - fn lerp(&self, other: PyVector, t: Real) -> Self { - Self(self.0 * (1 as Real - t) + other.0 * t) - } - - /// Return the components as a Python tuple ``(x, y, z)``. - fn to_tuple(&self) -> (Real, Real, Real) { - (self.0.x, self.0.y, self.0.z) - } - /// Return the components as a length-3 NumPy array. - fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - PyArray1::from_slice_bound(py, &[self.0.x, self.0.y, self.0.z]) - } - - /// Exact bitwise equality with another vector. - /// - /// Unlike ``==`` (tolerance-based), requires bit-identical - /// component values. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Component-wise addition: ``self + other``. - fn __add__(&self, other: PyVector) -> Self { - Self(self.0 + other.0) - } - /// Component-wise subtraction: ``self - other``. - fn __sub__(&self, other: PyVector) -> Self { - Self(self.0 - other.0) - } - /// Scalar multiplication: ``self * s``. - fn __mul__(&self, s: Real) -> Self { - Self(self.0 * s) - } - /// Reflected scalar multiplication: ``s * self``. - fn __rmul__(&self, s: Real) -> Self { - Self(self.0 * s) - } - /// Scalar division: ``self / s``. - /// - /// :raises ValueError: if ``s`` is zero. - fn __truediv__(&self, s: Real) -> PyResult { - if s == 0 as Real { - return Err(PyValueError::new_err("division by zero")); - } - Ok(Self(self.0 / s)) - } - /// Unary negation: ``-self``. - fn __neg__(&self) -> Self { - Self(-self.0) - } - - /// Number of components (always ``3``). - fn __len__(&self) -> usize { - 3 - } - /// Component access by index ``0``, ``1`` or ``2`` (negative - /// indices supported). - /// - /// :raises IndexError: if ``idx`` is out of range. - fn __getitem__(&self, idx: isize) -> PyResult { - let i = if idx < 0 { idx + 3 } else { idx }; - match i { - 0 => Ok(self.0.x), - 1 => Ok(self.0.y), - 2 => Ok(self.0.z), - _ => Err(pyo3::exceptions::PyIndexError::new_err( - "Vec3 index out of range", - )), - } - } - /// Iterate over the components in the order ``x``, ``y``, ``z``. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let py = slf.py(); - Py::new(py, Vec3Iter { v: slf.0, i: 0 }) - } - - /// Human-readable representation, e.g. ``Vec3(1, 2, 3)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!( - "Vec3({}, {}, {})", - self.0.x.fmt_repr(), - self.0.y.fmt_repr(), - self.0.z.fmt_repr() - ) - } - - /// Tolerant equality (Euclidean distance ≤ default epsilon). - /// Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyVector, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let close = (self.0 - other.0).norm() <= eps; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Vec3 supports only == and !=")), - } - } - } - - /// Iterator yielding the components of a :class:`Vec3` in - /// ``x``, ``y``, ``z`` order. - #[pyclass] - pub struct Vec3Iter { - v: $crate::na::Vector3, - i: usize, - } - #[pymethods] - impl Vec3Iter { - /// Return ``self`` (iterators are their own iterators). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Yield the next component, or stop after ``z``. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - let r = match slf.i { - 0 => Some(slf.v.x), - 1 => Some(slf.v.y), - 2 => Some(slf.v.z), - _ => None, - }; - if r.is_some() { - slf.i += 1; - } - r - } - } - - // ------------------------------------------------------------------ - // Point3 - // ------------------------------------------------------------------ - /// A point in 3D affine space ``(x, y, z)``. - /// - /// Conceptually distinct from :class:`Vec3`: a point represents a - /// location, a vector represents a displacement. Subtracting two - /// points yields a :class:`Vec3`; adding a :class:`Vec3` to a point - /// yields a new point. - /// - /// Wraps :class:`nalgebra::Point3`. - /// - /// Example:: - /// - /// from rapier3d import Point3, Vec3 - /// a = Point3(1.0, 2.0, 3.0) - /// b = a + Vec3(0.0, 0.0, 1.0) # Point3(1, 2, 4) - /// d = b - a # Vec3(0, 0, 1) - #[pyclass(name = "Point3", module = "rapier.dim3", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Point3(pub $crate::na::Point3); - - #[pymethods] - impl Point3 { - /// Create a new 3D point from its coordinates. - /// - /// :param x: x coordinate. - /// :param y: y coordinate. - /// :param z: z coordinate. - #[new] - fn new(x: Real, y: Real, z: Real) -> Self { - Self($crate::na::Point3::new(x, y, z)) - } - /// Return the origin, ``Point3(0, 0, 0)``. - #[staticmethod] - fn origin() -> Self { - Self($crate::na::Point3::origin()) - } - /// Build a :class:`Point3` from a 3-element tuple or list. - #[staticmethod] - fn from_tuple(t: PyPoint) -> Self { - Self(t.0) - } - /// Build a :class:`Point3` from a length-3 NumPy array. - #[staticmethod] - fn from_ndarray(a: PyPoint) -> Self { - Self(a.0) - } - - /// The ``x`` coordinate of the point. - #[getter] - fn x(&self) -> Real { - self.0.x - } - /// The ``y`` coordinate of the point. - #[getter] - fn y(&self) -> Real { - self.0.y - } - /// The ``z`` coordinate of the point. - #[getter] - fn z(&self) -> Real { - self.0.z - } - - /// The coordinates of the point as a :class:`Vec3` from origin. - #[getter] - fn coords(&self) -> Vec3 { - Vec3(self.0.coords) - } - - /// Return the coordinates as a Python tuple ``(x, y, z)``. - fn to_tuple(&self) -> (Real, Real, Real) { - (self.0.x, self.0.y, self.0.z) - } - /// Return the coordinates as a length-3 NumPy array. - fn to_ndarray<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - PyArray1::from_slice_bound(py, &[self.0.x, self.0.y, self.0.z]) - } - - /// Linear interpolation between two points. - /// - /// :param other: the target point. - /// :param t: interpolation parameter (``0`` returns ``self``, - /// ``1`` returns ``other``). - fn lerp(&self, other: PyPoint, t: Real) -> Self { - Self($crate::na::Point3::from( - self.0.coords * (1 as Real - t) + other.0.coords * t, - )) - } - - /// Exact bitwise equality with another point. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Translate by a vector: ``point + vector`` returns a new - /// :class:`Point3`. - fn __add__(&self, v: PyVector) -> Self { - Self(self.0 + v.0) - } - - /// Subtraction. ``point - point`` returns a :class:`Vec3` - /// (displacement). ``point - vector`` returns a :class:`Point3` - /// (translation by ``-vector``). - fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult { - let py = other.py(); - if let Ok(p) = other.extract::>() { - return Ok(Vec3(self.0 - p.0).into_py(py)); - } - if let Ok(p) = other.extract::() { - return Ok(Vec3(self.0 - p.0).into_py(py)); - } - let v: PyVector = other.extract()?; - Ok(Point3(self.0 - v.0).into_py(py)) - } - - /// Number of coordinates (always ``3``). - fn __len__(&self) -> usize { - 3 - } - /// Coordinate access by index ``0``, ``1`` or ``2`` (negative - /// indices supported). - /// - /// :raises IndexError: if ``idx`` is out of range. - fn __getitem__(&self, idx: isize) -> PyResult { - let i = if idx < 0 { idx + 3 } else { idx }; - match i { - 0 => Ok(self.0.x), - 1 => Ok(self.0.y), - 2 => Ok(self.0.z), - _ => Err(pyo3::exceptions::PyIndexError::new_err( - "Point3 index out of range", - )), - } - } - /// Iterate over the coordinates in the order ``x``, ``y``, ``z``. - fn __iter__(slf: PyRef<'_, Self>) -> PyResult> { - let py = slf.py(); - Py::new(py, Point3Iter { p: slf.0, i: 0 }) - } - - /// Human-readable representation, e.g. ``Point3(1, 2, 3)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - format!( - "Point3({}, {}, {})", - self.0.x.fmt_repr(), - self.0.y.fmt_repr(), - self.0.z.fmt_repr() - ) - } - - /// Tolerant equality (Euclidean distance ≤ default epsilon). - /// Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyPoint, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let close = (self.0 - other.0).norm() <= eps; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Point3 supports only == and !=")), - } - } - } - - /// Iterator yielding the coordinates of a :class:`Point3` in - /// ``x``, ``y``, ``z`` order. - #[pyclass] - pub struct Point3Iter { - p: $crate::na::Point3, - i: usize, - } - #[pymethods] - impl Point3Iter { - /// Return ``self`` (iterators are their own iterators). - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - /// Yield the next coordinate, or stop after ``z``. - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - let r = match slf.i { - 0 => Some(slf.p.x), - 1 => Some(slf.p.y), - 2 => Some(slf.p.z), - _ => None, - }; - if r.is_some() { - slf.i += 1; - } - r - } - } - - // ------------------------------------------------------------------ - // Rotation3 (alias Quaternion) - // ------------------------------------------------------------------ - /// A 3D rotation, represented internally as a unit quaternion. - /// - /// Encodes a rotation about an arbitrary axis in 3D space. Unit - /// quaternions avoid gimbal lock and are numerically friendly under - /// composition. The double-cover property (``q`` and ``-q`` encode - /// the same orientation) is accounted for in equality comparisons. - /// - /// Also exposed under the Python alias ``Quaternion``. - /// - /// Wraps :class:`nalgebra::UnitQuaternion`. - /// - /// Construct with :meth:`identity`, :meth:`from_axis_angle`, - /// :meth:`from_euler_angles`, :meth:`from_scaled_axis`, - /// :meth:`from_quaternion`, or the look-at builders. The raw - /// constructor takes quaternion coefficients ``(w, x, y, z)``. - /// - /// Example:: - /// - /// from math import pi - /// from rapier3d import Rotation3, Vec3 - /// r = Rotation3.from_axis_angle(Vec3.unit_z(), pi / 2) - /// v = r * Vec3(1.0, 0.0, 0.0) # ~Vec3(0, 1, 0) - #[pyclass(name = "Rotation3", module = "rapier.dim3", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Rotation3(pub $crate::na::UnitQuaternion); - - #[pymethods] - impl Rotation3 { - /// Build a unit quaternion from raw coefficients ``(w, x, y, z)``. - /// - /// The input is normalized internally, so the supplied values do - /// not need to lie on the unit sphere. Default arguments form - /// the identity rotation. - /// - /// :param w: real part. - /// :param x: i (x-axis imaginary) part. - /// :param y: j (y-axis imaginary) part. - /// :param z: k (z-axis imaginary) part. - #[new] - #[pyo3(signature = (w=1.0 as Real, x=0.0 as Real, y=0.0 as Real, z=0.0 as Real))] - fn new(w: Real, x: Real, y: Real, z: Real) -> Self { - let q = $crate::na::Quaternion::new(w, x, y, z); - Self($crate::na::UnitQuaternion::from_quaternion(q)) - } - - /// Return the identity rotation (zero rotation angle). - #[staticmethod] - fn identity() -> Self { - Self($crate::na::UnitQuaternion::identity()) - } - - /// Build a rotation from an axis-angle pair. - /// - /// :param axis: a non-zero rotation axis. It will be normalized. - /// :param angle: rotation angle in radians (right-hand rule). - /// :raises ValueError: if ``axis`` is the zero vector. - #[staticmethod] - fn from_axis_angle(axis: PyVector, angle: Real) -> PyResult { - let v = axis.0; - if v.norm() == 0 as Real { - return Err(PyValueError::new_err("axis must be non-zero")); - } - let unit = $crate::na::Unit::new_normalize(v); - Ok(Self($crate::na::UnitQuaternion::from_axis_angle( - &unit, angle, - ))) - } - - /// Build a rotation from Tait-Bryan ``XYZ`` Euler angles in - /// radians. - /// - /// :param roll: rotation about the local x-axis. - /// :param pitch: rotation about the local y-axis. - /// :param yaw: rotation about the local z-axis. - #[staticmethod] - fn from_euler_angles(roll: Real, pitch: Real, yaw: Real) -> Self { - Self($crate::na::UnitQuaternion::from_euler_angles( - roll, pitch, yaw, - )) - } - - /// Build a rotation from a scaled axis-angle vector - /// ``axis * angle``. - /// - /// The vector's direction is the rotation axis; its magnitude is - /// the rotation angle in radians. - #[staticmethod] - fn from_scaled_axis(v: PyVector) -> Self { - Self($crate::na::UnitQuaternion::from_scaled_axis(v.0)) - } - - /// Build a rotation directly from quaternion coefficients - /// ``(w, x, y, z)``. Input is normalized. - #[staticmethod] - fn from_quaternion(w: Real, x: Real, y: Real, z: Real) -> Self { - let q = $crate::na::Quaternion::new(w, x, y, z); - Self($crate::na::UnitQuaternion::from_quaternion(q)) - } - - /// Left-handed look-at rotation: aligns the local ``-z`` axis to - /// ``dir`` and the local ``y`` axis as close to ``up`` as - /// possible. - #[staticmethod] - fn look_at_lh(dir: PyVector, up: PyVector) -> Self { - Self($crate::na::UnitQuaternion::look_at_lh(&dir.0, &up.0)) - } - /// Right-handed look-at rotation: aligns the local ``z`` axis to - /// ``dir`` and the local ``y`` axis as close to ``up`` as - /// possible. - #[staticmethod] - fn look_at_rh(dir: PyVector, up: PyVector) -> Self { - Self($crate::na::UnitQuaternion::look_at_rh(&dir.0, &up.0)) - } - /// Rotation whose local ``z`` axis points along ``dir`` and - /// local ``y`` axis aligns with ``up`` as closely as possible. - #[staticmethod] - fn face_toward(dir: PyVector, up: PyVector) -> Self { - Self($crate::na::UnitQuaternion::face_towards(&dir.0, &up.0)) - } - - /// Build a :class:`Rotation3` from a 4-element tuple - /// ``(x, y, z, w)``. - #[staticmethod] - fn from_tuple(t: PyRotation) -> Self { - Self(t.0) - } - /// Build a :class:`Rotation3` from a length-4 NumPy array - /// ``[x, y, z, w]``. - #[staticmethod] - fn from_ndarray(a: PyRotation) -> Self { - Self(a.0) - } - - /// Unit rotation axis as a :class:`Vec3`, or ``None`` if the - /// rotation is the identity (no well-defined axis). - #[getter] - fn axis(&self) -> Option { - self.0.axis().map(|u| Vec3(u.into_inner())) - } - /// Rotation angle in radians, in ``[0, pi]``. - #[getter] - fn angle(&self) -> Real { - self.0.angle() - } - /// Scaled axis ``axis * angle`` as a :class:`Vec3`. - #[getter] - fn scaled_axis(&self) -> Vec3 { - Vec3(self.0.scaled_axis()) - } - /// Tait-Bryan ``XYZ`` Euler angles as ``(roll, pitch, yaw)`` in - /// radians. - #[getter] - fn euler_angles(&self) -> (Real, Real, Real) { - self.0.euler_angles() - } - /// Quaternion as `(x, y, z, w)`. - #[getter] - fn quaternion(&self) -> (Real, Real, Real, Real) { - let q = self.0.quaternion(); - (q.i, q.j, q.k, q.w) - } - /// Real (``w``) part of the underlying quaternion. - #[getter] - fn w(&self) -> Real { - self.0.quaternion().w - } - /// First imaginary (``i`` / ``x``) part of the quaternion. - #[getter] - fn i(&self) -> Real { - self.0.quaternion().i - } - /// Second imaginary (``j`` / ``y``) part of the quaternion. - #[getter] - fn j(&self) -> Real { - self.0.quaternion().j - } - /// Third imaginary (``k`` / ``z``) part of the quaternion. - #[getter] - fn k(&self) -> Real { - self.0.quaternion().k - } - - /// Return the inverse rotation (equivalent to the quaternion - /// conjugate for unit quaternions). - fn inverse(&self) -> Self { - Self(self.0.inverse()) - } - - /// Return the equivalent 3x3 rotation matrix, row-major - /// flattened into a length-9 NumPy array. - fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - let m = self.0.to_rotation_matrix(); - let mm = m.matrix(); - let mut out = [0 as Real; 9]; - for r in 0..3 { - for c in 0..3 { - out[r * 3 + c] = mm[(r, c)]; - } - } - PyArray1::from_slice_bound(py, &out) - } - - /// Returns the quaternion as `(x, y, z, w)`. - /// - /// This matches `PyRotation`'s `FromPyObject` impl, so any Rust - /// API that takes `PyRotation` accepts a `Rotation3` instance. - fn to_tuple(&self) -> (Real, Real, Real, Real) { - let q = self.0.quaternion(); - (q.i, q.j, q.k, q.w) - } - - /// Rotate a point about the origin and return a new - /// :class:`Point3`. - fn transform_point(&self, p: PyPoint) -> Point3 { - Point3(self.0 * p.0) - } - /// Rotate a vector and return a new :class:`Vec3`. - fn transform_vector(&self, v: PyVector) -> Vec3 { - Vec3(self.0 * v.0) - } - - /// Spherical-linear interpolation between two rotations. - /// - /// :param other: the target rotation. - /// :param t: interpolation parameter in ``[0, 1]``. - fn slerp(&self, other: PyRotation, t: Real) -> Self { - Self(self.0.slerp(&other.0, t)) - } - - /// Exact bitwise equality with another rotation. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Multiplication ``self * rhs``. Composes / transforms depending - /// on the right-hand side: - /// - /// - ``Rotation3 * Rotation3`` → composed :class:`Rotation3`. - /// - ``Rotation3 * Point3`` → rotated :class:`Point3`. - /// - ``Rotation3 * Vec3`` → rotated :class:`Vec3`. - fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { - let py = rhs.py(); - if let Ok(r) = rhs.extract::>() { - return Ok(Rotation3(self.0 * r.0).into_py(py)); - } - if let Ok(r) = rhs.extract::() { - return Ok(Rotation3(self.0 * r.0).into_py(py)); - } - if let Ok(p) = rhs.extract::>() { - return Ok(Point3(self.0 * p.0).into_py(py)); - } - if let Ok(p) = rhs.extract::() { - return Ok(Point3(self.0 * p.0).into_py(py)); - } - let v: PyVector = rhs.extract()?; - Ok(Vec3(self.0 * v.0).into_py(py)) - } - - /// Human-readable representation, e.g. - /// ``Rotation3(w=1, x=0, y=0, z=0)``. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - let q = self.0.quaternion(); - format!( - "Rotation3(w={}, x={}, y={}, z={})", - q.w.fmt_repr(), - q.i.fmt_repr(), - q.j.fmt_repr(), - q.k.fmt_repr() - ) - } - - /// Tolerant equality on quaternion coefficients. Accounts for - /// the quaternion double cover, so ``q`` and ``-q`` compare - /// equal. Only ``==`` and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyRotation, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let a = self.0.quaternion(); - let b = other.0.quaternion(); - // q == -q for orientation; check both. - let close_pos = (a.w - b.w).abs() <= eps - && (a.i - b.i).abs() <= eps - && (a.j - b.j).abs() <= eps - && (a.k - b.k).abs() <= eps; - let close_neg = (a.w + b.w).abs() <= eps - && (a.i + b.i).abs() <= eps - && (a.j + b.j).abs() <= eps - && (a.k + b.k).abs() <= eps; - let close = close_pos || close_neg; - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Rotation3 supports only == and !=")), - } - } - } - - // ------------------------------------------------------------------ - // Isometry3 - // ------------------------------------------------------------------ - /// A 3D rigid-body transformation: a rotation followed by a - /// translation. - /// - /// Equivalently, an element of the special Euclidean group ``SE(3)``. - /// Used throughout Rapier to encode the pose of a rigid body or a - /// collider relative to its parent. Composition is right-to-left: - /// ``A * B`` applies ``B`` first then ``A``. - /// - /// Wraps :class:`nalgebra::Isometry3`. - /// - /// Example:: - /// - /// from math import pi - /// from rapier3d import Isometry3, Vec3, Rotation3 - /// pose = Isometry3( - /// Vec3(0.0, 1.0, 0.0), - /// Rotation3.from_axis_angle(Vec3.unit_y(), pi / 2), - /// ) - #[pyclass(name = "Isometry3", module = "rapier.dim3", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Isometry3(pub $crate::na::Isometry3); - - #[pymethods] - impl Isometry3 { - /// Create an isometry from an optional translation and rotation. - /// - /// Both arguments default to identity (zero translation, - /// identity rotation). - /// - /// :param translation: a :class:`Vec3` (or tuple / ndarray). - /// :param rotation: a :class:`Rotation3` (or - /// ``(x, y, z, w)``-style input). - #[new] - #[pyo3(signature = (translation=None, rotation=None))] - fn new(translation: Option, rotation: Option) -> Self { - let t = translation - .map(|v| v.0) - .unwrap_or_else($crate::na::Vector3::zeros); - let r = rotation - .map(|r| r.0) - .unwrap_or_else($crate::na::UnitQuaternion::identity); - Self($crate::na::Isometry3::from_parts( - $crate::na::Translation3::from(t), - r, - )) - } - - /// Return the identity isometry (no translation, no rotation). - #[staticmethod] - fn identity() -> Self { - Self($crate::na::Isometry3::identity()) - } - - /// Translation-only constructor. - /// - /// Named `from_translation` (not `translation`) so it doesn't shadow - /// the `translation` property used to read the translation vector. - #[staticmethod] - fn from_translation(x: Real, y: Real, z: Real) -> Self { - Self($crate::na::Isometry3::translation(x, y, z)) - } - - /// Rotation-only constructor from a scaled-axis-angle (axis * angle). - #[staticmethod] - fn from_rotation(scaled_axis: PyVector) -> Self { - Self($crate::na::Isometry3::rotation(scaled_axis.0)) - } - - /// Build an isometry whose local ``z`` axis points along - /// ``target - eye`` and whose local ``y`` aligns with ``up`` as - /// closely as possible. - /// - /// :param eye: the origin of the isometry. - /// :param target: the point being looked at. - /// :param up: the desired up direction. - #[staticmethod] - fn face_toward(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { - Self($crate::na::Isometry3::face_towards( - &eye.0, &target.0, &up.0, - )) - } - - /// Left-handed look-at view isometry: maps world space into a - /// camera frame whose ``-z`` looks at ``target`` from ``eye``. - #[staticmethod] - fn look_at_lh(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { - Self($crate::na::Isometry3::look_at_lh(&eye.0, &target.0, &up.0)) - } - /// Right-handed look-at view isometry: maps world space into a - /// camera frame whose ``z`` looks at ``target`` from ``eye``. - #[staticmethod] - fn look_at_rh(eye: PyPoint, target: PyPoint, up: PyVector) -> Self { - Self($crate::na::Isometry3::look_at_rh(&eye.0, &target.0, &up.0)) - } - - /// Translation component of the isometry as a :class:`Vec3`. - #[getter] - fn translation(&self) -> Vec3 { - Vec3(self.0.translation.vector) - } - /// Rotation component of the isometry as a :class:`Rotation3`. - #[getter] - fn rotation(&self) -> Rotation3 { - Rotation3(self.0.rotation) - } - - /// Return the inverse isometry, such that - /// ``self * self.inverse()`` is the identity. - fn inverse(&self) -> Self { - Self(self.0.inverse()) - } - - /// Apply the full transformation (rotation then translation) - /// to a point. - fn transform_point(&self, p: PyPoint) -> Point3 { - Point3(self.0 * p.0) - } - /// Apply only the rotation part to a vector. Vectors are not - /// translated. - fn transform_vector(&self, v: PyVector) -> Vec3 { - Vec3(self.0 * v.0) - } - - /// Return the equivalent 4x4 homogeneous matrix, row-major - /// flattened into a length-16 NumPy array. - fn to_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - let m = self.0.to_homogeneous(); - let mut out = [0 as Real; 16]; - for r in 0..4 { - for c in 0..4 { - out[r * 4 + c] = m[(r, c)]; - } - } - PyArray1::from_slice_bound(py, &out) - } - - /// Interpolate between two isometries: linear interpolation on - /// the translation, slerp on the rotation. - /// - /// :param other: the target isometry. - /// :param t: interpolation parameter in ``[0, 1]``. - fn lerp_slerp(&self, other: &Self, t: Real) -> Self { - Self(self.0.lerp_slerp(&other.0, t)) - } - - /// Exact bitwise equality with another isometry. - fn bitwise_equal(&self, other: &Self) -> bool { - self.0 == other.0 - } - - /// Multiplication ``self * rhs``. Composes / transforms depending - /// on the right-hand side: - /// - /// - ``Isometry3 * Isometry3`` → composed :class:`Isometry3`. - /// - ``Isometry3 * Point3`` → transformed :class:`Point3`. - /// - ``Isometry3 * Vec3`` → rotated :class:`Vec3` (no - /// translation). - fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> PyResult { - let py = rhs.py(); - if let Ok(i) = rhs.extract::>() { - return Ok(Isometry3(self.0 * i.0).into_py(py)); - } - if let Ok(i) = rhs.extract::() { - return Ok(Isometry3(self.0 * i.0).into_py(py)); - } - if let Ok(p) = rhs.extract::>() { - return Ok(Point3(self.0 * p.0).into_py(py)); - } - if let Ok(p) = rhs.extract::() { - return Ok(Point3(self.0 * p.0).into_py(py)); - } - let v: PyVector = rhs.extract()?; - Ok(Vec3(self.0 * v.0).into_py(py)) - } - - /// Human-readable representation showing translation and the - /// rotation as quaternion components. - fn __repr__(&self) -> String { - use $crate::math::ReprPrecision; - let t = self.0.translation.vector; - let q = self.0.rotation.quaternion(); - format!( - "Isometry3(translation=({}, {}, {}), rotation=(w={}, x={}, y={}, z={}))", - t.x.fmt_repr(), - t.y.fmt_repr(), - t.z.fmt_repr(), - q.w.fmt_repr(), - q.i.fmt_repr(), - q.j.fmt_repr(), - q.k.fmt_repr() - ) - } - - /// Tolerant equality. Both translation distance and the - /// quaternion components must be within a default epsilon - /// (accounting for the quaternion double cover). Only ``==`` - /// and ``!=`` are supported. - /// - /// :raises TypeError: for ordering comparisons. - fn __richcmp__(&self, other: PyIsometry, op: CompareOp) -> PyResult { - use $crate::math::DefaultEpsilon; - let eps: Real = Real::default_epsilon(); - let t_close = - (self.0.translation.vector - other.0.translation.vector).norm() <= eps; - let a = self.0.rotation.quaternion(); - let b = other.0.rotation.quaternion(); - let r_close_pos = (a.w - b.w).abs() <= eps - && (a.i - b.i).abs() <= eps - && (a.j - b.j).abs() <= eps - && (a.k - b.k).abs() <= eps; - let r_close_neg = (a.w + b.w).abs() <= eps - && (a.i + b.i).abs() <= eps - && (a.j + b.j).abs() <= eps - && (a.k + b.k).abs() <= eps; - let close = t_close && (r_close_pos || r_close_neg); - match op { - CompareOp::Eq => Ok(close), - CompareOp::Ne => Ok(!close), - _ => Err(PyTypeError::new_err("Isometry3 supports only == and !=")), - } - } - } - - // ------------------------------------------------------------------ - // Free helper: rotation_from_angle - // - // In 3D the "angular vector" is a scaled axis-angle. This wraps - // `rapier::math::rotation_from_angle` semantics. - // ------------------------------------------------------------------ - /// Build a :class:`Rotation3` from a scaled-axis-angle vector. - /// - /// Mirrors the dimension-agnostic Rust helper - /// ``rapier::math::rotation_from_angle``; in 3D the "angular vector" - /// is ``axis * angle`` (its direction is the axis, its magnitude is - /// the rotation angle in radians). - /// - /// :param v: scaled-axis-angle vector. - #[pyfunction] - fn rotation_from_angle(v: PyVector) -> Rotation3 { - Rotation3($crate::na::UnitQuaternion::from_scaled_axis(v.0)) - } - - // ------------------------------------------------------------------ - // register_math - // ------------------------------------------------------------------ - pub fn register_math(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // Alias `Quaternion` -> Rotation3. - m.add("Quaternion", py.get_type_bound::())?; - // `AngVector3` is a Python-level alias for `Vec3`. - m.add("AngVector3", py.get_type_bound::())?; - m.add_function($crate::pyo3::wrap_pyfunction!(rotation_from_angle, m)?)?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/mjcf.rs b/python/rapier-py-core/src/mjcf.rs deleted file mode 100644 index 54fe33be1..000000000 --- a/python/rapier-py-core/src/mjcf.rs +++ /dev/null @@ -1,547 +0,0 @@ -//! MJCF (MuJoCo XML) loader `#[pyclass]`-es for the Python bindings (3D, f32 only). -//! -//! The class definitions are emitted by the `__define_loaders_mjcf!` macro, -//! invoked from `__define_loaders_3d!` in [`crate::loaders`]. Keeping them in -//! their own file mirrors the upstream `rapier3d-mjcf` crate boundary. -//! -//! Only a common subset of `MjcfLoaderOptions` is exposed (the -//! `collider_blueprint` / `rigid_body_blueprint` and `contact_filter_mode` -//! fields are left at their defaults), and actuator / sensor / keyframe -//! bindings are not yet surfaced. - -/// Materialize the MJCF loader `#[pyclass]` types and `register_loaders_mjcf`. -#[doc(hidden)] -#[macro_export] -macro_rules! __define_loaders_mjcf { - () => { - /// Options applied to multibody joints created from MJCF joints. - /// - /// A flag set; combine flags with ``|`` and pass the result to - /// :meth:`MjcfRobot.insert_using_multibody_joints`. - #[pyclass(name = "MjcfMultibodyOptions", module = "rapier", frozen)] - #[derive(Clone, Copy, Debug, Default)] - pub struct MjcfMultibodyOptions(pub rapier3d_mjcf::MjcfMultibodyOptions); - - #[pymethods] - impl MjcfMultibodyOptions { - /// Build from a raw bit pattern. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier3d_mjcf::MjcfMultibodyOptions::from_bits_truncate(bits)) - } - /// Return the empty flag set (no flags). - #[staticmethod] - fn empty() -> Self { Self(rapier3d_mjcf::MjcfMultibodyOptions::empty()) } - /// Treat the created multibody joints as kinematic. - #[classattr] - const JOINTS_ARE_KINEMATIC: MjcfMultibodyOptions = - MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::JOINTS_ARE_KINEMATIC); - /// Disable self-collisions between links of the same multibody. - #[classattr] - const DISABLE_SELF_CONTACTS: MjcfMultibodyOptions = - MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::DISABLE_SELF_CONTACTS); - /// Do not insert ```` loop-closure constraints as - /// impulse joints alongside the multibody. - #[classattr] - const SKIP_LOOP_CLOSURES: MjcfMultibodyOptions = - MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_LOOP_CLOSURES); - /// Strip the per-joint motors baked in by the loader before - /// handing joints to the multibody solver. - #[classattr] - const SKIP_JOINT_MOTORS: MjcfMultibodyOptions = - MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_MOTORS); - /// Strip per-joint limits before handing joints to the multibody. - #[classattr] - const SKIP_JOINT_LIMITS: MjcfMultibodyOptions = - MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_LIMITS); - /// Raw bits as an unsigned int. - #[getter] - fn bits(&self) -> u8 { self.0.bits() } - /// Bitwise OR: union of two flag sets. - fn __or__(&self, other: &MjcfMultibodyOptions) -> Self { Self(self.0 | other.0) } - /// Bitwise AND: intersection of two flag sets. - fn __and__(&self, other: &MjcfMultibodyOptions) -> Self { Self(self.0 & other.0) } - /// Return ``MjcfMultibodyOptions(bits=0b...)`` repr. - fn __repr__(&self) -> String { - format!("MjcfMultibodyOptions(bits={:#08b})", self.0.bits()) - } - } - - /// How MJCF ``contype`` / ``conaffinity`` masks map onto rapier - /// :class:`InteractionGroups`. - #[pyclass(name = "ContactFilterMode", module = "rapier", eq, eq_int)] - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum ContactFilterMode { - /// ``memberships = filter = contype | conaffinity`` (default). - Symmetric, - /// ``memberships = contype``, ``filter = conaffinity``. - Asymmetric, - } - - impl ContactFilterMode { - fn to_rapier(self) -> rapier3d_mjcf::ContactFilterMode { - match self { - ContactFilterMode::Symmetric => rapier3d_mjcf::ContactFilterMode::Symmetric, - ContactFilterMode::Asymmetric => rapier3d_mjcf::ContactFilterMode::Asymmetric, - } - } - } - - /// Configuration for the MJCF loader. - /// - /// Mirrors the common subset of ``rapier3d_mjcf::MjcfLoaderOptions``. - /// Pass an instance to :meth:`MjcfRobot.from_file` / :meth:`from_str`. - /// - /// :ivar create_colliders_from_collision_shapes: Build colliders for - /// ```` elements that participate in contact. Default ``True``. - /// :ivar create_colliders_from_visual_shapes: Build colliders for - /// visual-only ```` elements. Default ``False``. - /// :ivar apply_imported_mass_props: Use the model's inertial data for - /// body mass properties. Default ``True``. - /// :ivar enable_joint_collisions: Allow contacts between two bodies - /// sharing a joint. Default ``False``. - /// :ivar make_roots_fixed: Create root bodies as ``Fixed``. Default - /// ``False``. - /// :ivar trimesh_flags: :class:`TriMeshFlags` applied to mesh colliders. - /// :ivar mesh_converter: Optional :class:`MeshConverter` controlling - /// how meshes become collider shapes. Default ``None`` (trimesh). - /// :ivar shift: Rigid transform appended to every created body - /// (applied after ``scale``). - /// :ivar scale: Uniform scale applied to lengths read from the MJCF. - /// Default ``1.0``. - /// :ivar skip_plane_geoms: Skip ```` elements - /// entirely. Default ``True``. - /// :ivar disable_joint_motors: Skip the per-joint motor setup the - /// loader normally applies (springs / friction). Default ``False``. - #[pyclass(name = "MjcfLoaderOptions", module = "rapier")] - #[derive(Clone)] - pub struct MjcfLoaderOptions { - #[pyo3(get, set)] pub create_colliders_from_collision_shapes: bool, - #[pyo3(get, set)] pub create_colliders_from_visual_shapes: bool, - #[pyo3(get, set)] pub apply_imported_mass_props: bool, - #[pyo3(get, set)] pub enable_joint_collisions: bool, - #[pyo3(get, set)] pub make_roots_fixed: bool, - #[pyo3(get, set)] pub trimesh_flags: TriMeshFlags, - #[pyo3(get, set)] pub mesh_converter: Option, - #[pyo3(get, set)] pub shift: Isometry3, - #[pyo3(get, set)] pub scale: Real, - #[pyo3(get, set)] pub skip_plane_geoms: bool, - #[pyo3(get, set)] pub disable_joint_motors: bool, - /// Template collider applied to every imported collider. ``None`` - /// keeps the rapier default (density 0). - #[pyo3(get, set)] pub collider_blueprint: Option, - /// Template rigid-body used for every imported body. ``None`` - /// keeps the rapier default (a dynamic body). - #[pyo3(get, set)] pub rigid_body_blueprint: Option, - /// How ``contype``/``conaffinity`` map onto collision groups. - #[pyo3(get, set)] pub contact_filter_mode: ContactFilterMode, - } - - #[pymethods] - impl MjcfLoaderOptions { - /// Build an ``MjcfLoaderOptions`` with optional overrides. - #[new] - #[pyo3(signature = ( - create_colliders_from_collision_shapes = true, - create_colliders_from_visual_shapes = false, - apply_imported_mass_props = true, - enable_joint_collisions = false, - make_roots_fixed = false, - trimesh_flags = None, - mesh_converter = None, - shift = None, - scale = 1.0, - skip_plane_geoms = true, - disable_joint_motors = false, - collider_blueprint = None, - rigid_body_blueprint = None, - contact_filter_mode = ContactFilterMode::Symmetric, - ))] - #[allow(clippy::too_many_arguments)] - fn new( - create_colliders_from_collision_shapes: bool, - create_colliders_from_visual_shapes: bool, - apply_imported_mass_props: bool, - enable_joint_collisions: bool, - make_roots_fixed: bool, - trimesh_flags: Option, - mesh_converter: Option, - shift: Option, - scale: Real, - skip_plane_geoms: bool, - disable_joint_motors: bool, - collider_blueprint: Option, - rigid_body_blueprint: Option, - contact_filter_mode: ContactFilterMode, - ) -> Self { - let default = rapier3d_mjcf::MjcfLoaderOptions::default(); - let identity_iso: $crate::na::Isometry = rapier::math::Pose::IDENTITY.into(); - Self { - create_colliders_from_collision_shapes, - create_colliders_from_visual_shapes, - apply_imported_mass_props, - enable_joint_collisions, - make_roots_fixed, - trimesh_flags: trimesh_flags.unwrap_or(TriMeshFlags(default.trimesh_flags)), - mesh_converter, - shift: shift.unwrap_or(Isometry3(identity_iso)), - scale, - skip_plane_geoms, - disable_joint_motors, - collider_blueprint, - rigid_body_blueprint, - contact_filter_mode, - } - } - - /// Return a debug string with the main loader-option booleans. - fn __repr__(&self) -> String { - format!( - "MjcfLoaderOptions(create_colliders_from_collision_shapes={}, create_colliders_from_visual_shapes={}, make_roots_fixed={}, scale={}, skip_plane_geoms={})", - self.create_colliders_from_collision_shapes, - self.create_colliders_from_visual_shapes, - self.make_roots_fixed, - self.scale, - self.skip_plane_geoms, - ) - } - } - - impl MjcfLoaderOptions { - fn to_rapier(&self) -> rapier3d_mjcf::MjcfLoaderOptions { - let mut o = rapier3d_mjcf::MjcfLoaderOptions::default(); - o.create_colliders_from_collision_shapes = self.create_colliders_from_collision_shapes; - o.create_colliders_from_visual_shapes = self.create_colliders_from_visual_shapes; - o.apply_imported_mass_props = self.apply_imported_mass_props; - o.enable_joint_collisions = self.enable_joint_collisions; - o.make_roots_fixed = self.make_roots_fixed; - o.trimesh_flags = self.trimesh_flags.0; - o.mesh_converter = self.mesh_converter.as_ref().map(|m| m.0.clone()); - o.shift = self.shift.0.into(); - o.scale = self.scale; - o.skip_plane_geoms = self.skip_plane_geoms; - o.disable_joint_motors = self.disable_joint_motors; - o.contact_filter_mode = self.contact_filter_mode.to_rapier(); - if let Some(cb) = &self.collider_blueprint { - o.collider_blueprint = cb.builder.clone(); - } - if let Some(rb) = &self.rigid_body_blueprint { - o.rigid_body_blueprint = rb.builder.clone(); - } - o - } - } - - /// Parsed MJCF model document (read-only). - /// - /// Returned by :meth:`MjcfRobot.from_file` / :meth:`from_str` - /// alongside the simulation-ready :class:`MjcfRobot`. - #[pyclass(name = "MjcfModel", module = "rapier", unsendable)] - pub struct MjcfModel { - pub raw: rapier3d_mjcf::mjcf_rs::model::Model, - } - - #[pymethods] - impl MjcfModel { - /// The ```` name, if any. - #[getter] - fn name(&self) -> Option { self.raw.name.clone() } - /// Return the ``MjcfModel(...)`` repr. - fn __repr__(&self) -> String { - format!("MjcfModel(name={:?})", self.raw.name) - } - } - - /// Handle of one collider inserted from an MJCF ````. - /// - /// :ivar handle: Underlying :class:`ColliderHandle`. - #[pyclass(name = "MjcfColliderHandle", module = "rapier", frozen)] - #[derive(Clone, Debug)] - pub struct MjcfColliderHandle { - #[pyo3(get)] pub handle: ColliderHandle, - } - - /// Handle of one MJCF body (its rigid body plus its colliders). - /// - /// :ivar body: :class:`RigidBodyHandle` for the body. - /// :ivar colliders: List of :class:`MjcfColliderHandle` attached to it. - #[pyclass(name = "MjcfBodyHandle", module = "rapier", frozen)] - #[derive(Clone, Debug)] - pub struct MjcfBodyHandle { - #[pyo3(get)] pub body: RigidBodyHandle, - #[pyo3(get)] pub colliders: Vec, - } - - /// Handle of one MJCF joint after insertion. - /// - /// :ivar joint: Either an :class:`ImpulseJointHandle` (impulse-joint - /// path) or ``Optional[MultibodyJointHandle]`` (multibody path). - /// :ivar link1: Parent body's :class:`RigidBodyHandle`. - /// :ivar link2: Child body's :class:`RigidBodyHandle`. - #[pyclass(name = "MjcfJointHandle", module = "rapier")] - pub struct MjcfJointHandle { - #[pyo3(get)] pub joint: $crate::pyo3::PyObject, - #[pyo3(get)] pub link1: RigidBodyHandle, - #[pyo3(get)] pub link2: RigidBodyHandle, - } - - /// Aggregate handle set returned by MJCF insertion functions. - /// - /// :ivar bodies: One ``Optional[MjcfBodyHandle]`` per MJCF body, in - /// model order (entry 0 is the implicit world body and is usually - /// ``None``). - /// :ivar joints: One :class:`MjcfJointHandle` per inserted joint. - /// :ivar equality_joints: One :class:`MjcfJointHandle` per - /// ```` loop-closure constraint (always impulse joints). - #[pyclass(name = "MjcfRobotHandles", module = "rapier")] - pub struct MjcfRobotHandles { - #[pyo3(get)] pub bodies: Vec>, - #[pyo3(get)] pub joints: Vec<$crate::pyo3::Py>, - #[pyo3(get)] pub equality_joints: Vec<$crate::pyo3::Py>, - } - - /// A MuJoCo MJCF model, ready to be inserted into the simulation. - /// - /// Build one via :meth:`from_file` / :meth:`from_str`, then commit it - /// to the world via :meth:`insert_using_impulse_joints` or - /// :meth:`insert_using_multibody_joints`. The insertion methods - /// *consume* the robot — subsequent calls raise :class:`MjcfError`. - #[pyclass(name = "MjcfRobot", module = "rapier", unsendable)] - pub struct MjcfRobot { - pub inner: Option, - } - - #[pymethods] - impl MjcfRobot { - /// Parse an MJCF file and return ``(MjcfRobot, MjcfModel)``. - /// - /// ```` directives and mesh references resolve relative - /// to the file's parent directory. - /// - /// :param path: Path to the ``.xml`` file on disk. - /// :param options: Optional :class:`MjcfLoaderOptions`. - /// :returns: ``(MjcfRobot, MjcfModel)`` tuple. - /// :raises MjcfError: if the file can't be opened or parsed. - #[staticmethod] - #[pyo3(signature = (path, options=None))] - fn from_file( - path: &str, - options: Option, - ) -> $crate::pyo3::PyResult<(MjcfRobot, MjcfModel)> { - let opts = options - .map(|o| o.to_rapier()) - .unwrap_or_else(rapier3d_mjcf::MjcfLoaderOptions::default); - let (robot, model) = rapier3d_mjcf::MjcfRobot::from_file(path, opts) - .map_err(|e| $crate::errors::MjcfError::new_err(format!("{e}")))?; - Ok((MjcfRobot { inner: Some(robot) }, MjcfModel { raw: model })) - } - - /// Parse an MJCF XML string and return ``(MjcfRobot, MjcfModel)``. - /// - /// ```` directives resolve relative to ``base_dir`` - /// (default: the current working directory). - /// - /// :param xml: Full MJCF XML document. - /// :param options: Optional :class:`MjcfLoaderOptions`. - /// :param base_dir: Directory used to resolve includes / mesh refs. - /// :returns: ``(MjcfRobot, MjcfModel)`` tuple. - /// :raises MjcfError: if the XML can't be parsed. - #[staticmethod] - #[pyo3(signature = (xml, options=None, base_dir=None))] - fn from_str( - xml: &str, - options: Option, - base_dir: Option<&str>, - ) -> $crate::pyo3::PyResult<(MjcfRobot, MjcfModel)> { - let opts = options - .map(|o| o.to_rapier()) - .unwrap_or_else(rapier3d_mjcf::MjcfLoaderOptions::default); - let base_dir = base_dir.unwrap_or("."); - let (robot, model) = rapier3d_mjcf::MjcfRobot::from_str(xml, opts, base_dir) - .map_err(|e| $crate::errors::MjcfError::new_err(format!("{e}")))?; - Ok((MjcfRobot { inner: Some(robot) }, MjcfModel { raw: model })) - } - - /// Prepend ``transform`` to the robot's root poses. - /// - /// Repositions the whole robot before insertion (e.g. to place a - /// second copy beside the first). Must be called before the robot - /// is consumed by an ``insert_*`` call. - /// - /// :param transform: world-space :class:`Isometry3` to apply. - /// :raises MjcfError: if this robot has already been consumed. - fn append_transform(&mut self, transform: Isometry3) -> $crate::pyo3::PyResult<()> { - let robot = self.inner.as_mut().ok_or_else(|| { - $crate::errors::MjcfError::new_err("MjcfRobot was already consumed") - })?; - let pose: rapier::math::Pose = transform.0.into(); - robot.append_transform(&pose); - Ok(()) - } - - /// Insert the model into the world using *impulse joints*. - /// - /// This call **consumes** the :class:`MjcfRobot`. - /// - /// :param bodies: :class:`RigidBodySet` to insert into. - /// :param colliders: :class:`ColliderSet` to insert into. - /// :param impulse_joints: :class:`ImpulseJointSet` to insert into. - /// :returns: :class:`MjcfRobotHandles` with all created handles. - /// :raises MjcfError: if this robot has already been consumed. - fn insert_using_impulse_joints( - &mut self, - py: $crate::pyo3::Python<'_>, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - impulse_joints: &mut ImpulseJointSet, - ) -> $crate::pyo3::PyResult<$crate::pyo3::Py> { - use $crate::pyo3::IntoPy; - let robot = self.inner.take().ok_or_else(|| { - $crate::errors::MjcfError::new_err("MjcfRobot was already consumed") - })?; - let handles = robot.insert_using_impulse_joints( - &mut bodies.0, - &mut colliders.0, - &mut impulse_joints.0, - ); - $crate::__mjcf_build_handles!(py, handles, |h| ImpulseJointHandle(h).into_py(py)) - } - - /// Insert the model into the world using *multibody joints*. - /// - /// ```` loop-closure constraints are still inserted as - /// impulse joints (rapier multibodies are tree-structured), so an - /// :class:`ImpulseJointSet` is required as well. - /// - /// This call **consumes** the :class:`MjcfRobot`. - /// - /// :param bodies: :class:`RigidBodySet` to insert into. - /// :param colliders: :class:`ColliderSet` to insert into. - /// :param multibody_joints: :class:`MultibodyJointSet` to insert into. - /// :param impulse_joints: :class:`ImpulseJointSet` for equalities. - /// :param options: Optional :class:`MjcfMultibodyOptions` flags. - /// :returns: :class:`MjcfRobotHandles` with all created handles. - /// :raises MjcfError: if this robot has already been consumed. - #[pyo3(signature = (bodies, colliders, multibody_joints, impulse_joints, options=None))] - fn insert_using_multibody_joints( - &mut self, - py: $crate::pyo3::Python<'_>, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - multibody_joints: &mut MultibodyJointSet, - impulse_joints: &mut ImpulseJointSet, - options: Option, - ) -> $crate::pyo3::PyResult<$crate::pyo3::Py> { - use $crate::pyo3::IntoPy; - let robot = self.inner.take().ok_or_else(|| { - $crate::errors::MjcfError::new_err("MjcfRobot was already consumed") - })?; - let opts = options.map(|o| o.0).unwrap_or_default(); - let handles = robot.insert_using_multibody_joints( - &mut bodies.0, - &mut colliders.0, - &mut multibody_joints.0, - &mut impulse_joints.0, - opts, - ); - $crate::__mjcf_build_handles!(py, handles, |h: Option<_>| h.map(MultibodyJointHandle).into_py(py)) - } - - /// Return the ``MjcfRobot(...)`` repr (or ``"MjcfRobot(consumed)"``). - fn __repr__(&self) -> String { - match &self.inner { - Some(r) => format!("MjcfRobot(n_bodies={}, n_joints={})", r.bodies.len(), r.joints.len()), - None => "MjcfRobot(consumed)".to_string(), - } - } - } - - /// Register the MJCF loader `#[pyclass]`-es into `m`. - pub fn register_loaders_mjcf( - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} - -/// Shared body/joint-handle marshalling for both MJCF insertion paths. -/// -/// `$conv` maps the path-specific joint-handle type to a `PyObject`. -#[doc(hidden)] -#[macro_export] -macro_rules! __mjcf_build_handles { - ($py:expr, $handles:expr, $conv:expr) => {{ - let handles = $handles; - let conv = $conv; - let bodies: Vec> = handles - .bodies - .into_iter() - .map(|b| { - b.map(|b| { - let cols = b - .colliders - .into_iter() - .map(|c| MjcfColliderHandle { - handle: ColliderHandle(c.handle), - }) - .collect(); - MjcfBodyHandle { - body: RigidBodyHandle(b.body), - colliders: cols, - } - }) - }) - .collect(); - let joints: Vec<$crate::pyo3::Py> = handles - .joints - .into_iter() - .map(|jh| { - $crate::pyo3::Py::new( - $py, - MjcfJointHandle { - joint: conv(jh.joint), - link1: RigidBodyHandle(jh.link1), - link2: RigidBodyHandle(jh.link2), - }, - ) - .expect("alloc MjcfJointHandle") - }) - .collect(); - let equality_joints: Vec<$crate::pyo3::Py> = handles - .equality_joints - .into_iter() - .map(|jh| { - use $crate::pyo3::IntoPy; - $crate::pyo3::Py::new( - $py, - MjcfJointHandle { - joint: ImpulseJointHandle(jh.joint).into_py($py), - link1: RigidBodyHandle(jh.link1), - link2: RigidBodyHandle(jh.link2), - }, - ) - .expect("alloc MjcfJointHandle") - }) - .collect(); - $crate::pyo3::Py::new( - $py, - MjcfRobotHandles { - bodies, - joints, - equality_joints, - }, - ) - }}; -} diff --git a/python/rapier-py-core/src/pipeline.rs b/python/rapier-py-core/src/pipeline.rs deleted file mode 100644 index be03535f9..000000000 --- a/python/rapier-py-core/src/pipeline.rs +++ /dev/null @@ -1,2461 +0,0 @@ -//! Macro emitting the user-facing pipeline / query `#[pyclass]`-es per cdylib. -//! -//! Like `define_dynamics_types!` and `define_geometry_types!`, this macro is -//! invoked once per cdylib AFTER the math/geometry/dynamics macros. It expects -//! the surrounding cdylib to already have: -//! - The `Real`/`DIM` aliases and `Py{Vector,Point,Rotation,Isometry,AngVector}` -//! adapters in scope (from `define_conv_types!`). -//! - The user-facing math types (`define_math_types!`). -//! - The geometry types (`define_geometry_types!`). -//! - The dynamics types (`define_dynamics_types!`). -//! - `use rapier{2,3}d{,_f64} as rapier;` aliasing. -//! -//! Produces `register_pipeline(py, m) -> PyResult<()>` for the `#[pymodule]`. - -/// Materialize the pipeline / query `#[pyclass]` types for a given -/// `(Real, DIM)` pair. -#[macro_export] -macro_rules! define_pipeline_types { - (DIM = 2) => { - $crate::__define_pipeline_shared!(2, Vec2, Point2, Rotation2, Isometry2); - $crate::__define_pipeline_register!(); - }; - (DIM = 3) => { - $crate::__define_pipeline_shared!(3, Vec3, Point3, Rotation3, Isometry3); - $crate::__define_pipeline_register!(); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_pipeline_shared { - ($dim:tt, $Vec:ident, $Point:ident, $Rot:ident, $Iso:ident) => { - // PyO3 / pyo3 utility imports — math/geometry macros already brought - // `PyTypeError`, `PyAny`, `Bound`, `Py`, etc. into scope, so we only - // pull in what's new for pipeline. - use $crate::pyo3::types::PyList as _PyList; - - // ===================================================================== - // FeatureId — tagged-union over parry's `FeatureId` (Vertex/Edge/Face/Unknown). - // ===================================================================== - - /// Tagged-union identifier for a shape feature returned by a query. - /// - /// Wraps parry's ``FeatureId``: one of ``Vertex(id)``, ``Edge(id)`` - /// (3D only), ``Face(id)``, or ``Unknown``. The :attr:`kind` field - /// holds the variant name (``"vertex"``, ``"edge"``, ``"face"``, - /// ``"unknown"``); :attr:`id` holds the numeric index (``0`` for - /// ``Unknown``). - /// - /// Construct via the static factories :meth:`Vertex`, :meth:`Edge`, - /// :meth:`Face`, :meth:`Unknown`. - #[pyclass(name = "FeatureId", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct FeatureId { - /// `"vertex"`, `"edge"` (3D only), `"face"`, or `"unknown"`. - #[pyo3(get)] pub kind: &'static str, - /// 0 when `kind == "unknown"`, otherwise the parry id. - #[pyo3(get)] pub id: u32, - } - - #[pymethods] - impl FeatureId { - /// Construct a ``Vertex(id)`` feature identifier. - #[staticmethod] - #[pyo3(name = "Vertex")] - fn vertex(id: u32) -> Self { Self { kind: "vertex", id } } - /// Construct a ``Face(id)`` feature identifier. - #[staticmethod] - #[pyo3(name = "Face")] - fn face(id: u32) -> Self { Self { kind: "face", id } } - /// Construct an ``Edge(id)`` feature identifier. - /// - /// Edges are only returned for 3D queries; the constructor is - /// exposed on both dimensions for portability. - // FeatureId::Edge: 3D only in parry, but we expose it on both - // dims as a generic constructor for portability. - #[staticmethod] - #[pyo3(name = "Edge")] - fn edge(id: u32) -> Self { Self { kind: "edge", id } } - /// Construct the ``Unknown`` feature identifier. - #[staticmethod] - #[pyo3(name = "Unknown")] - fn unknown() -> Self { Self { kind: "unknown", id: 0 } } - /// ``True`` iff this is a ``Vertex(_)``. - #[getter] fn is_vertex(&self) -> bool { self.kind == "vertex" } - /// ``True`` iff this is an ``Edge(_)``. - #[getter] fn is_edge(&self) -> bool { self.kind == "edge" } - /// ``True`` iff this is a ``Face(_)``. - #[getter] fn is_face(&self) -> bool { self.kind == "face" } - /// ``True`` iff this is ``Unknown``. - #[getter] fn is_unknown(&self) -> bool { self.kind == "unknown" } - /// Human-readable ``FeatureId.Variant(id)`` repr. - fn __repr__(&self) -> String { - if self.kind == "unknown" { - "FeatureId.Unknown".to_string() - } else { - format!("FeatureId.{}({})", - match self.kind { - "vertex" => "Vertex", - "edge" => "Edge", - "face" => "Face", - _ => "Unknown", - }, self.id) - } - } - /// Equality on (kind, id); other comparisons raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.kind == other.kind && self.id == other.id), - CompareOp::Ne => Ok(!(self.kind == other.kind && self.id == other.id)), - _ => Err(PyTypeError::new_err("FeatureId supports only == and !=")), - } - } - } - - impl FeatureId { - #[allow(dead_code)] - #[inline] - pub(crate) fn from_parry(f: rapier::parry::shape::FeatureId) -> Self { - $crate::__feature_id_from_parry!($dim, f) - } - } - - // ===================================================================== - // Ray - // ===================================================================== - - /// A half-line defined by an :attr:`origin` point and a :attr:`dir` - /// vector. - /// - /// Passed to the various ``cast_ray*`` / ``intersect_ray`` queries. - /// The direction need not be normalized — query time-of-impact (TOI) - /// values are expressed in units of ``dir``. - #[pyclass(name = "Ray", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct Ray(pub rapier::parry::query::Ray); - - #[pymethods] - impl Ray { - /// Construct a ray from an origin point and direction vector. - /// - /// :param origin: World-space origin point. - /// :param dir: World-space direction (does not need to be unit). - #[new] - fn new(origin: PyPoint, dir: PyVector) -> Self { - let o: rapier::math::Vector = origin.0.coords.into(); - let d: rapier::math::Vector = dir.0.into(); - Self(rapier::parry::query::Ray::new(o, d)) - } - /// World-space origin point. - #[getter] - fn origin(&self) -> $Point { - let v: $crate::na::SVector = self.0.origin.into(); - $Point($crate::na::Point::from(v)) - } - /// World-space direction vector. - #[getter] - fn dir(&self) -> $Vec { - let v: $crate::na::SVector = self.0.dir.into(); - $Vec(v) - } - /// Evaluate the ray's position at parameter ``t``. - /// - /// Computes ``origin + dir * t``. When ``dir`` is unit-length, ``t`` - /// is the Euclidean distance from the origin. - /// - /// :param t: Ray parameter (typically the TOI returned by a query). - /// :returns: World-space point on the ray. - fn point_at(&self, t: Real) -> $Point { - let p: rapier::math::Vector = self.0.origin + self.0.dir * t; - let v: $crate::na::SVector = p.into(); - $Point($crate::na::Point::from(v)) - } - /// Debug repr — shows the origin and direction. - fn __repr__(&self) -> String { - format!("Ray(origin={:?}, dir={:?})", self.0.origin, self.0.dir) - } - } - - // ===================================================================== - // RayIntersection - // ===================================================================== - - /// Result of a ray-vs-shape intersection query. - /// - /// :attr:`toi` is the ray parameter at the hit (see :meth:`Ray.point_at`). - /// :attr:`normal` is the world-space surface normal pointing away from - /// the hit collider. :attr:`feature` identifies the shape feature - /// (vertex / edge / face) that was hit, when known. - #[pyclass(name = "RayIntersection", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct RayIntersection { - /// Time of impact — ray parameter at the hit. - #[pyo3(get)] pub toi: Real, - /// Surface normal at the hit, pointing away from the collider. - #[pyo3(get)] pub normal: $Vec, - /// Shape feature that was hit. - #[pyo3(get)] pub feature: FeatureId, - } - - #[pymethods] - impl RayIntersection { - /// Alias for :attr:`toi` matching parry's naming. - #[getter] fn time_of_impact(&self) -> Real { self.toi } - /// Debug repr — shows TOI and normal. - fn __repr__(&self) -> String { - format!("RayIntersection(toi={}, normal={:?})", self.toi, self.normal.0) - } - } - - impl RayIntersection { - #[allow(dead_code)] - pub(crate) fn from_parry(r: rapier::parry::query::RayIntersection) -> Self { - let n: $crate::na::SVector = r.normal.into(); - Self { - toi: r.time_of_impact, - normal: $Vec(n), - feature: FeatureId::from_parry(r.feature), - } - } - } - - // ===================================================================== - // PointProjection - // ===================================================================== - - /// Result of a project-point query. - /// - /// :attr:`point` is the world-space closest point on (or inside) the - /// collider's shape; :attr:`is_inside` is ``True`` iff the query - /// point lies inside the (solid) shape — in which case the projected - /// point may coincide with the query point. - #[pyclass(name = "PointProjection", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct PointProjection { - /// ``True`` iff the query point lies inside the shape. - #[pyo3(get)] pub is_inside: bool, - /// World-space closest point on (or inside) the shape. - #[pyo3(get)] pub point: $Point, - } - - #[pymethods] - impl PointProjection { - /// Debug repr — shows the projected point and ``is_inside`` flag. - fn __repr__(&self) -> String { - format!("PointProjection(is_inside={}, point={:?})", - self.is_inside, self.point.0) - } - } - - impl PointProjection { - #[allow(dead_code)] - pub(crate) fn from_parry(p: rapier::parry::query::PointProjection) -> Self { - // parry's PointProjection.point is a glam Vector, not a Point. - let v: $crate::na::SVector = p.point.into(); - Self { - is_inside: p.is_inside, - point: $Point($crate::na::Point::from(v)), - } - } - } - - // ===================================================================== - // ShapeCastStatus - // ===================================================================== - - /// Outcome status of a shape-cast query. - /// - /// - ``OUT_OF_ITERATIONS``: solver hit the iteration cap before - /// converging; the returned TOI is approximate. - /// - ``CONVERGED``: the solver converged on a TOI within tolerance. - /// - ``FAILED``: the solver failed (degenerate geometry or numerical - /// issue); the result should be ignored. - /// - ``PENETRATING_OR_WITHIN_TARGET_DIST``: the shapes are already in - /// contact (or within ``target_distance``) at ``t = 0``. - #[pyclass(name = "ShapeCastStatus", module = "rapier", eq, eq_int)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum ShapeCastStatus { - OUT_OF_ITERATIONS, - CONVERGED, - FAILED, - PENETRATING_OR_WITHIN_TARGET_DIST, - } - - impl ShapeCastStatus { - #[allow(dead_code)] - fn from_parry(s: rapier::parry::query::ShapeCastStatus) -> Self { - use rapier::parry::query::ShapeCastStatus as S; - match s { - S::OutOfIterations => Self::OUT_OF_ITERATIONS, - S::Converged => Self::CONVERGED, - S::Failed => Self::FAILED, - S::PenetratingOrWithinTargetDist => Self::PENETRATING_OR_WITHIN_TARGET_DIST, - } - } - } - - // ===================================================================== - // ShapeCastOptions - // ===================================================================== - - /// Tuning parameters for shape-cast queries. - /// - /// Fields: - /// - /// - :attr:`max_time_of_impact`: upper bound on the returned TOI; - /// defaults to ``+inf``. - /// - :attr:`target_distance`: shapes are considered "in contact" - /// when their closest distance is within this margin (useful for - /// character-controller-style queries that want to stop just shy - /// of penetration). - /// - :attr:`stop_at_penetration`: when ``True`` (default), the query - /// reports a hit with ``t = 0`` if the shapes already overlap; when - /// ``False``, the cast continues past the initial penetration. - /// - :attr:`compute_impact_geometry_on_penetration`: when ``True`` - /// (default), compute witness points / normals even when the - /// ``PENETRATING_OR_WITHIN_TARGET_DIST`` status is returned. - #[pyclass(name = "ShapeCastOptions", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct ShapeCastOptions(pub rapier::parry::query::ShapeCastOptions); - - #[pymethods] - impl ShapeCastOptions { - /// Construct a ``ShapeCastOptions`` from explicit field values. - /// - /// :param max_time_of_impact: Upper bound on the TOI. - /// :param target_distance: Tolerance for "in contact". - /// :param stop_at_penetration: Whether to report ``t = 0`` hits. - /// :param compute_impact_geometry_on_penetration: Whether to - /// compute witness geometry on penetration. - #[new] - #[pyo3(signature = ( - max_time_of_impact=Real::MAX, - target_distance=0.0 as Real, - stop_at_penetration=true, - compute_impact_geometry_on_penetration=true, - ))] - fn new( - max_time_of_impact: Real, - target_distance: Real, - stop_at_penetration: bool, - compute_impact_geometry_on_penetration: bool, - ) -> Self { - Self(rapier::parry::query::ShapeCastOptions { - max_time_of_impact, - target_distance, - stop_at_penetration, - compute_impact_geometry_on_penetration, - }) - } - /// Shortcut constructor — defaults plus an explicit TOI bound. - /// - /// :param t: ``max_time_of_impact`` value. - #[staticmethod] - fn with_max_time_of_impact(t: Real) -> Self { - Self(rapier::parry::query::ShapeCastOptions::with_max_time_of_impact(t)) - } - /// Upper bound on the returned time of impact. - #[getter] fn max_time_of_impact(&self) -> Real { self.0.max_time_of_impact } - /// Set :attr:`max_time_of_impact`. - #[setter] fn set_max_time_of_impact(&mut self, v: Real) { self.0.max_time_of_impact = v; } - /// Distance below which the shapes are considered to be in contact. - #[getter] fn target_distance(&self) -> Real { self.0.target_distance } - /// Set :attr:`target_distance`. - #[setter] fn set_target_distance(&mut self, v: Real) { self.0.target_distance = v; } - /// Whether the cast reports ``t = 0`` hits when shapes overlap at the start. - #[getter] fn stop_at_penetration(&self) -> bool { self.0.stop_at_penetration } - /// Set :attr:`stop_at_penetration`. - #[setter] fn set_stop_at_penetration(&mut self, v: bool) { self.0.stop_at_penetration = v; } - /// Whether witness points / normals are computed on penetration. - #[getter] fn compute_impact_geometry_on_penetration(&self) -> bool { - self.0.compute_impact_geometry_on_penetration - } - /// Set :attr:`compute_impact_geometry_on_penetration`. - #[setter] fn set_compute_impact_geometry_on_penetration(&mut self, v: bool) { - self.0.compute_impact_geometry_on_penetration = v; - } - /// Debug repr — shows the main TOI and target-distance settings. - fn __repr__(&self) -> String { - format!("ShapeCastOptions(max_time_of_impact={}, target_distance={})", - self.0.max_time_of_impact, self.0.target_distance) - } - } - - // ===================================================================== - // ShapeCastHit - // ===================================================================== - - /// Result of a shape-cast query. - /// - /// :attr:`time_of_impact` is the parametric TOI along the cast - /// motion. :attr:`witness1` / :attr:`witness2` are the world-space - /// closest points on the cast shape and the hit collider at the time - /// of impact; :attr:`normal1` / :attr:`normal2` are the corresponding - /// outward surface normals. :attr:`status` is a :class:`ShapeCastStatus` - /// describing how the cast terminated. - #[pyclass(name = "ShapeCastHit", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy)] - pub struct ShapeCastHit { - /// Parametric TOI along the cast motion. - #[pyo3(get)] pub time_of_impact: Real, - /// World-space witness point on the cast shape at impact. - #[pyo3(get)] pub witness1: $Point, - /// World-space witness point on the hit collider at impact. - #[pyo3(get)] pub witness2: $Point, - /// Outward surface normal on the cast shape at impact. - #[pyo3(get)] pub normal1: $Vec, - /// Outward surface normal on the hit collider at impact. - #[pyo3(get)] pub normal2: $Vec, - /// Termination status of the cast (see :class:`ShapeCastStatus`). - #[pyo3(get)] pub status: ShapeCastStatus, - } - - #[pymethods] - impl ShapeCastHit { - /// Debug repr — shows the TOI and status. - fn __repr__(&self) -> String { - format!("ShapeCastHit(toi={}, status={:?})", - self.time_of_impact, self.status) - } - } - - impl ShapeCastHit { - #[allow(dead_code)] - pub(crate) fn from_parry(h: rapier::parry::query::ShapeCastHit) -> Self { - let w1: $crate::na::SVector = h.witness1.into(); - let w2: $crate::na::SVector = h.witness2.into(); - let n1: $crate::na::SVector = h.normal1.into(); - let n2: $crate::na::SVector = h.normal2.into(); - Self { - time_of_impact: h.time_of_impact, - witness1: $Point($crate::na::Point::from(w1)), - witness2: $Point($crate::na::Point::from(w2)), - normal1: $Vec(n1), - normal2: $Vec(n2), - status: ShapeCastStatus::from_parry(h.status), - } - } - } - - // ===================================================================== - // NonlinearRigidMotion - // ===================================================================== - - /// Curved rigid-body motion used by nonlinear shape casts. - /// - /// Describes a constant-velocity screw motion around a body's - /// :attr:`local_center`, starting from an initial pose :attr:`start` - /// with linear velocity :attr:`linvel` and angular velocity - /// :attr:`angvel`. Passed to - /// :meth:`QueryPipeline.cast_shape_nonlinear` to capture rotational - /// effects (e.g. swinging characters) that a straight-line - /// :meth:`QueryPipeline.cast_shape` would miss. - #[pyclass(name = "NonlinearRigidMotion", module = "rapier")] - #[derive(Debug, Clone, Copy)] - pub struct NonlinearRigidMotion(pub rapier::parry::query::NonlinearRigidMotion); - - #[pymethods] - impl NonlinearRigidMotion { - /// Construct a nonlinear motion from its constituent fields. - /// - /// :param start: Initial pose; defaults to identity. - /// :param local_center: Local-space pivot point; defaults to origin. - /// :param linvel: Linear velocity; defaults to zero. - /// :param angvel: Angular velocity (3D: ``Vec3``, 2D: scalar); - /// defaults to zero. - #[new] - #[pyo3(signature = (start=None, local_center=None, linvel=None, angvel=None))] - fn new( - start: Option, - local_center: Option, - linvel: Option, - angvel: Option<&Bound<'_, PyAny>>, - ) -> PyResult { - let s: rapier::math::Pose = start - .map(|i| i.0.into()) - .unwrap_or_else(rapier::math::Pose::identity); - let lc_vec: rapier::math::Vector = local_center - .map(|p| p.0.coords.into()) - .unwrap_or(rapier::math::Vector::ZERO); - let lv: rapier::math::Vector = linvel - .map(|v| v.0.into()) - .unwrap_or(rapier::math::Vector::ZERO); - let av = $crate::__nonlinear_angvel_extract!($dim, angvel)?; - Ok(Self(rapier::parry::query::NonlinearRigidMotion { - start: s, - local_center: lc_vec, - linvel: lv, - angvel: av, - })) - } - /// Identity motion — no rotation, no translation, identity start. - #[staticmethod] - fn identity() -> Self { - Self(rapier::parry::query::NonlinearRigidMotion::identity()) - } - /// Stationary motion held at ``pos`` for all times. - /// - /// :param pos: Pose to hold. - #[staticmethod] - fn constant_position(pos: PyIsometry) -> Self { - let p: rapier::math::Pose = pos.0.into(); - Self(rapier::parry::query::NonlinearRigidMotion::constant_position(p)) - } - /// Initial pose at ``t = 0``. - #[getter] - fn start(&self) -> $Iso { - let iso: $crate::na::Isometry = self.0.start.into(); - $Iso(iso) - } - /// Local-space pivot the angular velocity rotates around. - #[getter] - fn local_center(&self) -> $Point { - let v: $crate::na::SVector = self.0.local_center.into(); - $Point($crate::na::Point::from(v)) - } - /// Linear velocity (world-space, units per unit time). - #[getter] - fn linvel(&self) -> $Vec { - let v: $crate::na::SVector = self.0.linvel.into(); - $Vec(v) - } - /// Evaluate the motion at time ``t``. - /// - /// :param t: Time parameter (``0`` returns :attr:`start`). - /// :returns: Pose at time ``t``. - fn position_at_time(&self, t: Real) -> $Iso { - let p: $crate::na::Isometry = self.0.position_at_time(t).into(); - $Iso(p) - } - } - - // Dim-specific `angvel` getter — emitted in a separate pymethods block - // (multiple-pymethods feature) because the angvel type differs. - $crate::__nonlinear_angvel_getter!($dim, $Vec); - - // ===================================================================== - // QueryFilterFlags - // ===================================================================== - - /// Bitflags controlling which colliders / bodies a scene query touches. - /// - /// Combine flags with ``|``. Common preset combinations are also - /// exposed as named static constructors on :class:`QueryFilter` - /// (e.g. ``QueryFilter.only_dynamic()``). - #[pyclass(name = "QueryFilterFlags", module = "rapier", frozen)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct QueryFilterFlags(pub rapier::pipeline::QueryFilterFlags); - - #[pymethods] - impl QueryFilterFlags { - /// Construct from a raw bit pattern (unknown bits are truncated). - /// - /// :param bits: Raw bit pattern. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u32) -> Self { - Self(rapier::pipeline::QueryFilterFlags::from_bits_truncate(bits)) - } - /// Return an empty flag set (no filtering). - #[staticmethod] - fn empty() -> Self { Self(rapier::pipeline::QueryFilterFlags::empty()) } - - /// Skip colliders attached to fixed (static) bodies. - #[classattr] - const EXCLUDE_FIXED: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_FIXED); - /// Skip colliders attached to kinematic bodies. - #[classattr] - const EXCLUDE_KINEMATIC: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_KINEMATIC); - /// Skip colliders attached to dynamic bodies. - #[classattr] - const EXCLUDE_DYNAMIC: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_DYNAMIC); - /// Skip sensor colliders. - #[classattr] - const EXCLUDE_SENSORS: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_SENSORS); - /// Skip non-sensor (solid) colliders. - #[classattr] - const EXCLUDE_SOLIDS: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::EXCLUDE_SOLIDS); - /// Keep only colliders attached to dynamic bodies. - #[classattr] - const ONLY_DYNAMIC: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_DYNAMIC); - /// Keep only colliders attached to kinematic bodies. - #[classattr] - const ONLY_KINEMATIC: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_KINEMATIC); - /// Keep only colliders attached to fixed (static) bodies. - #[classattr] - const ONLY_FIXED: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::ONLY_FIXED); - /// Reserved — currently maps to an empty set (forward-compat). - // `EXCLUDE_COLLISION_INVALID` doesn't exist upstream; we map it to - // an empty flag set for forward-compat. - #[classattr] - const EXCLUDE_COLLISION_INVALID: QueryFilterFlags = - QueryFilterFlags(rapier::pipeline::QueryFilterFlags::empty()); - - /// Raw underlying bit pattern. - #[getter] fn bits(&self) -> u32 { self.0.bits() } - /// ``True`` if every bit set in ``other`` is also set here. - fn contains(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// ``True`` if no flag is set. - fn is_empty(&self) -> bool { self.0.is_empty() } - /// ``other in self`` — alias for :meth:`contains`. - fn __contains__(&self, other: &Self) -> bool { self.0.contains(other.0) } - /// Bitwise OR — union of flag sets. - fn __or__(&self, other: &Self) -> Self { Self(self.0 | other.0) } - /// Bitwise AND — intersection of flag sets. - fn __and__(&self, other: &Self) -> Self { Self(self.0 & other.0) } - /// Bitwise XOR — symmetric difference. - fn __xor__(&self, other: &Self) -> Self { Self(self.0 ^ other.0) } - /// Bitwise NOT — flip every known flag. - fn __invert__(&self) -> Self { Self(!self.0) } - /// Set difference — remove flags present in ``other``. - fn __sub__(&self, other: &Self) -> Self { Self(self.0 - other.0) } - /// Truthy iff at least one flag is set. - fn __bool__(&self) -> bool { !self.0.is_empty() } - /// Equality on the raw bit pattern; other comparisons raise. - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyTypeError::new_err("QueryFilterFlags supports only == and !=")), - } - } - /// Hash on the raw bit pattern (makes the value set-safe). - fn __hash__(&self) -> u64 { self.0.bits() as u64 } - /// Debug repr — shows the raw bit pattern in binary. - fn __repr__(&self) -> String { - format!("QueryFilterFlags(bits={:#010b})", self.0.bits()) - } - } - - // ===================================================================== - // QueryFilter — Python-side mutable builder. - // - // Storing a `rapier::pipeline::QueryFilter<'_>` directly is awkward - // because the upstream type carries a lifetime for the predicate - // borrow. We instead keep the constituent fields and reconstruct a - // borrowed `QueryFilter` inside each query call via `with_filter`. - // ===================================================================== - - /// Filter passed to scene queries to narrow the set of inspected colliders. - /// - /// A ``QueryFilter`` composes: - /// - /// - :class:`QueryFilterFlags` (membership: include/exclude - /// dynamic / kinematic / fixed / sensor / solid). - /// - :class:`InteractionGroups` (group-mask membership / filter bits). - /// - explicit exclusion of a single :class:`ColliderHandle` and/or a - /// single :class:`RigidBodyHandle`. - /// - an optional Python predicate ``f(handle, collider) -> bool`` - /// invoked per-collider to make the final keep/skip decision. - /// - /// All builder methods (``exclude_sensors``, ``groups``, - /// ``exclude_collider``, ``predicate``, ...) return a new - /// :class:`QueryFilter` rather than mutating in place, so they can be - /// chained. - #[pyclass(name = "QueryFilter", module = "rapier")] - pub struct QueryFilter { - pub flags: rapier::pipeline::QueryFilterFlags, - pub groups: Option, - pub exclude_collider: Option, - pub exclude_rigid_body: Option, - pub predicate: Option>, - } - - impl QueryFilter { - /// Deep-clone (the predicate `Py` requires a `Python<'_>` token). - fn clone_with(&self, py: Python<'_>) -> Self { - Self { - flags: self.flags, - groups: self.groups, - exclude_collider: self.exclude_collider, - exclude_rigid_body: self.exclude_rigid_body, - predicate: self.predicate.as_ref().map(|p| p.clone_ref(py)), - } - } - } - - impl QueryFilter { - /// Build a borrowed `QueryFilter<'a>` for upstream calls. - /// - /// `pred` is the predicate closure produced by the caller, kept - /// alive on the stack for the duration of one query call. - pub fn as_rapier<'a>( - &self, - pred: Option<&'a dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool>, - ) -> rapier::pipeline::QueryFilter<'a> { - let mut qf = rapier::pipeline::QueryFilter { - flags: self.flags, - groups: self.groups, - exclude_collider: self.exclude_collider, - exclude_rigid_body: self.exclude_rigid_body, - predicate: None, - }; - if let Some(p) = pred { - qf.predicate = Some(p); - } - qf - } - } - - #[pymethods] - impl QueryFilter { - /// Construct a ``QueryFilter`` from explicit field values. - /// - /// :param flags: :class:`QueryFilterFlags` (defaults to empty). - /// :param groups: :class:`InteractionGroups` membership/filter. - /// :param exclude_collider: Single collider to skip. - /// :param exclude_rigid_body: Skip every collider attached to - /// this rigid body. - /// :param predicate: Optional ``f(ColliderHandle, Collider) -> bool`` - /// per-collider predicate; returning ``False`` skips the - /// collider. - #[new] - #[pyo3(signature = (flags=None, groups=None, exclude_collider=None, exclude_rigid_body=None, predicate=None))] - fn py_new( - flags: Option<&QueryFilterFlags>, - groups: Option<&InteractionGroups>, - exclude_collider: Option<&ColliderHandle>, - exclude_rigid_body: Option<&RigidBodyHandle>, - predicate: Option>, - ) -> Self { - Self { - flags: flags.map(|f| f.0).unwrap_or_else(rapier::pipeline::QueryFilterFlags::empty), - groups: groups.map(|g| g.0), - exclude_collider: exclude_collider.map(|h| h.0), - exclude_rigid_body: exclude_rigid_body.map(|h| h.0), - predicate, - } - } - - /// Return an empty filter (no flags, no groups, no exclusions). - #[staticmethod] - fn new() -> Self { Self::default_empty() } - - /// Return a filter that excludes colliders attached to fixed bodies. - #[staticmethod] - fn exclude_fixed() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_FIXED; - me - } - /// Return a filter that excludes colliders attached to kinematic bodies. - #[staticmethod] - fn exclude_kinematic() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_KINEMATIC; - me - } - /// Return a filter that excludes colliders attached to dynamic bodies. - #[staticmethod] - fn exclude_dynamic() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::EXCLUDE_DYNAMIC; - me - } - /// Return a filter keeping only colliders attached to dynamic bodies. - #[staticmethod] - fn only_dynamic() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::ONLY_DYNAMIC; - me - } - /// Return a filter keeping only colliders attached to kinematic bodies. - #[staticmethod] - fn only_kinematic() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::ONLY_KINEMATIC; - me - } - /// Return a filter keeping only colliders attached to fixed bodies. - #[staticmethod] - fn only_fixed() -> Self { - let mut me = Self::default_empty(); - me.flags = rapier::pipeline::QueryFilterFlags::ONLY_FIXED; - me - } - - /// Builder: also exclude sensor colliders. - /// - /// :returns: A new :class:`QueryFilter` with ``EXCLUDE_SENSORS`` - /// added. - fn exclude_sensors(&self, py: Python<'_>) -> Self { - let mut me = self.clone_with(py); - me.flags |= rapier::pipeline::QueryFilterFlags::EXCLUDE_SENSORS; - me - } - /// Builder: also exclude non-sensor (solid) colliders. - /// - /// :returns: A new :class:`QueryFilter` with ``EXCLUDE_SOLIDS`` - /// added. - fn exclude_solids(&self, py: Python<'_>) -> Self { - let mut me = self.clone_with(py); - me.flags |= rapier::pipeline::QueryFilterFlags::EXCLUDE_SOLIDS; - me - } - /// Builder: restrict to colliders whose interaction groups overlap ``groups``. - /// - /// :param groups: Membership / filter masks. - /// :returns: A new :class:`QueryFilter`. - fn groups(&self, py: Python<'_>, groups: &InteractionGroups) -> Self { - let mut me = self.clone_with(py); - me.groups = Some(groups.0); - me - } - /// Builder: skip this specific collider. - /// - /// :param h: Collider handle to exclude. - /// :returns: A new :class:`QueryFilter`. - fn exclude_collider(&self, py: Python<'_>, h: &ColliderHandle) -> Self { - let mut me = self.clone_with(py); - me.exclude_collider = Some(h.0); - me - } - /// Builder: skip every collider attached to this body. - /// - /// :param h: Rigid-body handle whose colliders to exclude. - /// :returns: A new :class:`QueryFilter`. - fn exclude_rigid_body(&self, py: Python<'_>, h: &RigidBodyHandle) -> Self { - let mut me = self.clone_with(py); - me.exclude_rigid_body = Some(h.0); - me - } - /// Builder: install a per-collider predicate. - /// - /// The predicate is invoked with ``(ColliderHandle, Collider)`` - /// and must return a ``bool``; ``False`` skips the collider. - /// - /// :param fun: Python callable. - /// :returns: A new :class:`QueryFilter`. - fn predicate(&self, py: Python<'_>, fun: Py) -> Self { - let mut me = self.clone_with(py); - me.predicate = Some(fun.clone_ref(py)); - me - } - - /// Current :class:`QueryFilterFlags`. - #[getter] fn get_flags(&self) -> QueryFilterFlags { QueryFilterFlags(self.flags) } - /// Current :class:`InteractionGroups`, or ``None``. - #[getter] fn get_groups(&self) -> Option { - self.groups.map(InteractionGroups) - } - /// Handle of the explicitly excluded collider, or ``None``. - #[getter] - #[pyo3(name = "exclude_collider_handle")] - fn get_exclude_collider(&self) -> Option { - self.exclude_collider.map(ColliderHandle) - } - /// Handle of the rigid body whose colliders are excluded, or ``None``. - #[getter] - #[pyo3(name = "exclude_rigid_body_handle")] - fn get_exclude_rigid_body(&self) -> Option { - self.exclude_rigid_body.map(RigidBodyHandle) - } - /// Current per-collider predicate, or ``None``. - #[getter] fn get_predicate(&self, py: Python<'_>) -> Option { - self.predicate.as_ref().map(|p| p.clone_ref(py)) - } - - /// Debug repr — shows all filter fields. - fn __repr__(&self) -> String { - format!( - "QueryFilter(flags={:#x}, groups={:?}, exclude_collider={:?}, exclude_rigid_body={:?}, predicate={})", - self.flags.bits(), - self.groups.map(|g| (g.memberships.bits(), g.filter.bits())), - self.exclude_collider.map(|h| h.into_raw_parts()), - self.exclude_rigid_body.map(|h| h.into_raw_parts()), - if self.predicate.is_some() { "" } else { "None" }, - ) - } - } - - impl QueryFilter { - fn default_empty() -> Self { - Self { - flags: rapier::pipeline::QueryFilterFlags::empty(), - groups: None, - exclude_collider: None, - exclude_rigid_body: None, - predicate: None, - } - } - } - - // ===================================================================== - // QueryPipeline — built on demand from the world's broad_phase. - // - // We expose two views: an immutable `QueryPipeline` and a mutable - // `QueryPipelineMut`. Both share the same query implementations. - // ===================================================================== - - /// Scene-query accelerator — a view over the broad-phase BVH. - /// - /// A :class:`QueryPipeline` holds shared references to the world's - /// :class:`BroadPhaseBvh`, :class:`NarrowPhase`, :class:`RigidBodySet`, - /// and :class:`ColliderSet`. Each query method (``cast_ray``, - /// ``cast_shape``, ``intersect_*``, ``project_point*``) re-borrows - /// those sets, builds the underlying parry ``QueryPipeline``, and - /// runs the requested query. - /// - /// **Validity:** the pipeline reflects the state of the broad-phase - /// BVH as of the last :meth:`update` (or - /// :meth:`PhysicsWorld.step` when ``auto_update_query=True``). - /// After mutating the collider set or moving bodies, call - /// :meth:`update` to refresh the BVH before issuing queries. - #[pyclass(name = "QueryPipeline", module = "rapier", unsendable)] - pub struct QueryPipeline { - /// The "owned" BVH that backs scene queries. - pub broad_phase: Py, - pub narrow_phase: Py, - pub bodies: Py, - pub colliders: Py, - } - - #[pymethods] - impl QueryPipeline { - /// Construct a query pipeline from explicit sub-set references. - /// - /// Usually you'll use :attr:`PhysicsWorld.query_pipeline` instead - /// of constructing this directly. - /// - /// :param broad_phase: Broad-phase BVH backing the queries. - /// :param narrow_phase: Narrow-phase store (for the query dispatcher). - /// :param bodies: Rigid-body set referenced by collider parents. - /// :param colliders: Collider set the queries operate on. - #[new] - fn new( - broad_phase: Py, - narrow_phase: Py, - bodies: Py, - colliders: Py, - ) -> Self { - Self { broad_phase, narrow_phase, bodies, colliders } - } - - /// Re-build the broad-phase BVH from the current body/collider state. - /// - /// Must be called after inserting/removing colliders or moving - /// bodies before issuing queries — otherwise the queries see a - /// stale BVH. :meth:`PhysicsWorld.step` calls this automatically - /// when ``world.auto_update_query`` is ``True``; the manual - /// variant lets you avoid a redundant rebuild when the BVH is - /// already up to date. - /// - /// :param bodies: Rigid-body set (accepted for API parity; the - /// pipeline uses its stored reference). - /// :param colliders: Collider set (same caveat as ``bodies``). - fn update( - &self, - py: Python<'_>, - bodies: Py, - colliders: Py, - ) -> PyResult<()> { - let _ = (bodies, colliders); // accepted for API parity - let mut bp = self.broad_phase.borrow_mut(py); - let bodies = self.bodies.borrow(py); - let mut colliders = self.colliders.borrow_mut(py); - let params = rapier::dynamics::IntegrationParameters::default(); - let modified = colliders.0.take_modified(); - let removed = colliders.0.take_removed(); - let mut events: Vec = Vec::new(); - bp.0.update(¶ms, &colliders.0, &bodies.0, &modified, &removed, &mut events); - Ok(()) - } - - /// Cast a ray and return the closest hit (TOI only). - /// - /// :param ray: Ray to cast. - /// :param max_toi: Upper bound on the returned TOI. - /// :param solid: If ``True``, a ray that starts inside a solid - /// shape hits at ``t = 0``; if ``False``, the ray exits - /// through the shape's back face. - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, toi)`` or ``None`` if no hit. - #[pyo3(signature = (ray, max_toi, solid, filter=None))] - fn cast_ray( - &self, - py: Python<'_>, - ray: &Ray, - max_toi: Real, - solid: bool, - filter: Option<&QueryFilter>, - ) -> PyResult> { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.cast_ray(&ray.0, max_toi, solid) - .map(|(h, t)| (ColliderHandle(h), t))) - }) - } - - /// Cast a ray and return the closest hit with surface geometry. - /// - /// :param ray: Ray to cast. - /// :param max_toi: Upper bound on the returned TOI. - /// :param solid: See :meth:`cast_ray`. - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, RayIntersection)`` or ``None``. - #[pyo3(signature = (ray, max_toi, solid, filter=None))] - fn cast_ray_and_get_normal( - &self, - py: Python<'_>, - ray: &Ray, - max_toi: Real, - solid: bool, - filter: Option<&QueryFilter>, - ) -> PyResult> { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.cast_ray_and_get_normal(&ray.0, max_toi, solid) - .map(|(h, ri)| (ColliderHandle(h), RayIntersection::from_parry(ri)))) - }) - } - - /// Iterate over every collider the ray passes through. - /// - /// The Python callback is invoked with ``(ColliderHandle, - /// RayIntersection)`` for each hit. Return ``True`` (or any - /// non-bool) to continue, ``False`` to stop iteration. - /// - /// :param ray: Ray to cast. - /// :param max_toi: Upper bound on the hit TOI. - /// :param solid: See :meth:`cast_ray`. - /// :param callback: ``(ColliderHandle, RayIntersection) -> bool``. - /// :param filter: Optional :class:`QueryFilter`. - #[pyo3(signature = (ray, max_toi, solid, callback, filter=None))] - fn intersect_ray( - &self, - py: Python<'_>, - ray: &Ray, - max_toi: Real, - solid: bool, - callback: Py, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - for (h, _co, ri) in qp.intersect_ray(ray.0, max_toi, solid) { - let py_ri = RayIntersection::from_parry(ri); - let res = callback.call1(py, (ColliderHandle(h), py_ri))?; - if let Ok(b) = res.extract::(py) { - if !b { break; } - } - } - Ok(()) - }) - } - - /// Find the collider whose shape is closest to ``point``. - /// - /// :param point: World-space query point. - /// :param solid: If ``True`` a point inside a solid shape - /// projects to itself (distance ``0``); if ``False`` it - /// projects to the shape's boundary. - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, PointProjection)`` or ``None`` if - /// no collider passed the filter. - #[pyo3(signature = (point, solid, filter=None))] - fn project_point( - &self, - py: Python<'_>, - point: PyPoint, - solid: bool, - filter: Option<&QueryFilter>, - ) -> PyResult> { - let p: rapier::math::Vector = point.0.coords.into(); - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.project_point(p, Real::MAX, solid) - .map(|(h, pp)| (ColliderHandle(h), PointProjection::from_parry(pp)))) - }) - } - - /// Like :meth:`project_point`, but also return the touched shape feature. - /// - /// :param point: World-space query point. - /// :param max_dist: Largest distance to search; colliders farther - /// than this from ``point`` are ignored. Defaults to unbounded. - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, PointProjection, FeatureId)`` or - /// ``None``. - #[pyo3(signature = (point, max_dist=None, filter=None))] - fn project_point_and_get_feature( - &self, - py: Python<'_>, - point: PyPoint, - max_dist: Option, - filter: Option<&QueryFilter>, - ) -> PyResult> { - let p: rapier::math::Vector = point.0.coords.into(); - let max_dist = max_dist.unwrap_or(Real::MAX); - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.project_point_and_get_feature(p, max_dist) - .map(|(h, pp, fi)| ( - ColliderHandle(h), - PointProjection::from_parry(pp), - FeatureId::from_parry(fi), - ))) - }) - } - - /// Invoke ``callback(ColliderHandle)`` for every collider containing ``point``. - /// - /// Return ``True`` (or any non-bool) from the callback to - /// continue, ``False`` to stop iteration. - /// - /// :param point: World-space query point. - /// :param callback: ``(ColliderHandle) -> bool``. - /// :param filter: Optional :class:`QueryFilter`. - #[pyo3(signature = (point, callback, filter=None))] - fn intersect_point( - &self, - py: Python<'_>, - point: PyPoint, - callback: Py, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - let p: rapier::math::Vector = point.0.coords.into(); - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - for (h, _co) in qp.intersect_point(p) { - let res = callback.call1(py, (ColliderHandle(h),))?; - if let Ok(b) = res.extract::(py) { - if !b { break; } - } - } - Ok(()) - }) - } - - /// Linear shape cast — sweep ``shape`` along ``shape_vel`` from ``shape_pose``. - /// - /// :param shape_pose: Initial pose of the cast shape. - /// :param shape_vel: Linear velocity vector (units per unit time). - /// :param shape: Shape to sweep. - /// :param options: :class:`ShapeCastOptions` (TOI bound, target - /// distance, penetration handling). - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, ShapeCastHit)`` for the earliest - /// hit, or ``None``. - #[pyo3(signature = (shape_pose, shape_vel, shape, options, filter=None))] - fn cast_shape( - &self, - py: Python<'_>, - shape_pose: PyIsometry, - shape_vel: PyVector, - shape: &SharedShape, - options: &ShapeCastOptions, - filter: Option<&QueryFilter>, - ) -> PyResult> { - let pose: rapier::math::Pose = shape_pose.0.into(); - let vel: rapier::math::Vector = shape_vel.0.into(); - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.cast_shape(&pose, vel, &*shape.0.0, options.0) - .map(|(h, hit)| (ColliderHandle(h), ShapeCastHit::from_parry(hit)))) - }) - } - - /// Nonlinear shape cast — sweep ``shape`` along a curved :class:`NonlinearRigidMotion`. - /// - /// Use this when the moving body rotates or has an offset center - /// (e.g. a hammer head swinging around a wrist), which a straight - /// :meth:`cast_shape` would miss. - /// - /// :param motion: Curved motion to sweep along. - /// :param shape: Shape to sweep. - /// :param options: :class:`ShapeCastOptions` — only - /// ``stop_at_penetration`` is honored by this query. - /// :param start_time: Lower bound on the cast parameter. - /// :param end_time: Upper bound on the cast parameter. - /// :param filter: Optional :class:`QueryFilter`. - /// :returns: ``(ColliderHandle, ShapeCastHit)`` or ``None``. - #[pyo3(signature = (motion, shape, options, start_time=0.0 as Real, end_time=Real::MAX, filter=None))] - #[allow(clippy::too_many_arguments)] - fn cast_shape_nonlinear( - &self, - py: Python<'_>, - motion: &NonlinearRigidMotion, - shape: &SharedShape, - options: &ShapeCastOptions, - start_time: Real, - end_time: Real, - filter: Option<&QueryFilter>, - ) -> PyResult> { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.cast_shape_nonlinear( - &motion.0, - &*shape.0.0, - start_time, - end_time, - options.0.stop_at_penetration, - ).map(|(h, hit)| (ColliderHandle(h), ShapeCastHit::from_parry(hit)))) - }) - } - - /// Invoke ``callback(ColliderHandle)`` for each collider intersecting ``shape`` at ``shape_pose``. - /// - /// Return ``True`` (or any non-bool) from the callback to - /// continue, ``False`` to stop iteration. - /// - /// :param shape_pose: World pose of the test shape. - /// :param shape: Test shape. - /// :param callback: ``(ColliderHandle) -> bool``. - /// :param filter: Optional :class:`QueryFilter`. - #[pyo3(signature = (shape_pose, shape, callback, filter=None))] - fn intersect_shape( - &self, - py: Python<'_>, - shape_pose: PyIsometry, - shape: &SharedShape, - callback: Py, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - let pose: rapier::math::Pose = shape_pose.0.into(); - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - for (h, _co) in qp.intersect_shape(pose, &*shape.0.0) { - let res = callback.call1(py, (ColliderHandle(h),))?; - if let Ok(b) = res.extract::(py) { - if !b { break; } - } - } - Ok(()) - }) - } - - /// Conservative AABB sweep — invoke ``callback(ColliderHandle)`` for every collider whose AABB intersects ``aabb``. - /// - /// The test is conservative: a candidate's AABB may overlap the - /// query AABB even when the underlying shapes don't (i.e. false - /// positives are possible — there are no false negatives). - /// Return ``False`` from the callback to stop iteration. - /// - /// :param aabb: Query AABB. - /// :param callback: ``(ColliderHandle) -> bool``. - /// :param filter: Optional :class:`QueryFilter`. - #[pyo3(signature = (aabb, callback, filter=None))] - fn intersect_aabb_conservative( - &self, - py: Python<'_>, - aabb: &Aabb, - callback: Py, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - for (h, _co) in qp.intersect_aabb_conservative(aabb.0) { - let res = callback.call1(py, (ColliderHandle(h),))?; - if let Ok(b) = res.extract::(py) { - if !b { break; } - } - } - Ok(()) - }) - } - - /// Alias for :meth:`intersect_aabb_conservative`. - #[pyo3(signature = (aabb, callback, filter=None))] - fn colliders_with_aabb_intersecting_aabb( - &self, - py: Python<'_>, - aabb: &Aabb, - callback: Py, - filter: Option<&QueryFilter>, - ) -> PyResult<()> { - self.intersect_aabb_conservative(py, aabb, callback, filter) - } - - /// Return ``True`` iff any collider's AABB intersects ``aabb``. - /// - /// Conservative test (see :meth:`intersect_aabb_conservative`). - /// - /// :param aabb: Query AABB. - /// :param filter: Optional :class:`QueryFilter`. - #[pyo3(signature = (aabb, filter=None))] - fn test_aabb( - &self, - py: Python<'_>, - aabb: &Aabb, - filter: Option<&QueryFilter>, - ) -> PyResult { - $crate::__qp_query!(self, py, filter, |qp: rapier::pipeline::QueryPipeline<'_>| { - Ok(qp.intersect_aabb_conservative(aabb.0).next().is_some()) - }) - } - } - - // ===================================================================== - // Counters and its sub-structs - // ===================================================================== - - /// Read-only per-stage timing breakdown for a single ``step()``. - /// - /// All values are in milliseconds and are only populated when the - /// owning :class:`Counters` is enabled via :meth:`Counters.enable`. - #[pyclass(name = "StagesCounters", module = "rapier", frozen)] - #[derive(Clone, Copy)] - pub struct StagesCounters(pub rapier::counters::StagesCounters); - - #[pymethods] - impl StagesCounters { - /// Time spent updating broad-phase / collider state, in ms. - #[getter] fn update_time_ms(&self) -> f64 { self.0.update_time.time_ms() } - /// Total collision-detection time (broad + narrow phase), in ms. - #[getter] fn collision_detection_time_ms(&self) -> f64 { self.0.collision_detection_time.time_ms() } - /// Time spent grouping bodies into solver islands, in ms. - #[getter] fn island_construction_time_ms(&self) -> f64 { self.0.island_construction_time.time_ms() } - /// Time spent assembling per-island constraint data, in ms. - #[getter] fn island_constraints_collection_time_ms(&self) -> f64 { self.0.island_constraints_collection_time.time_ms() } - /// Constraint-solver time, in ms. - #[getter] fn solver_time_ms(&self) -> f64 { self.0.solver_time.time_ms() } - /// Continuous collision detection time, in ms. - #[getter] fn ccd_time_ms(&self) -> f64 { self.0.ccd_time.time_ms() } - /// Time spent applying user-modified body / collider state, in ms. - #[getter] fn user_changes_time_ms(&self) -> f64 { self.0.user_changes.time_ms() } - /// Debug repr — shows the dominant per-stage times. - fn __repr__(&self) -> String { - format!("StagesCounters(solver={}ms, collision_detection={}ms)", - self.0.solver_time.time_ms(), - self.0.collision_detection_time.time_ms()) - } - } - - /// Read-only collision-detection counters for a single ``step()``. - /// - /// Only populated when the owning :class:`Counters` is enabled. - #[pyclass(name = "CollisionDetectionCounters", module = "rapier", frozen)] - #[derive(Clone, Copy)] - pub struct CollisionDetectionCounters(pub rapier::counters::CollisionDetectionCounters); - - #[pymethods] - impl CollisionDetectionCounters { - /// Number of contact pairs detected. - #[getter] fn ncontact_pairs(&self) -> usize { self.0.ncontact_pairs } - /// Broad-phase time, in ms. - #[getter] fn broad_phase_time_ms(&self) -> f64 { self.0.broad_phase_time.time_ms() } - /// Narrow-phase time, in ms. - #[getter] fn narrow_phase_time_ms(&self) -> f64 { self.0.narrow_phase_time.time_ms() } - /// Final broad-phase pass time (post-CCD), in ms. - #[getter] fn final_broad_phase_time_ms(&self) -> f64 { self.0.final_broad_phase_time.time_ms() } - } - - /// Read-only solver counters for a single ``step()``. - /// - /// Only populated when the owning :class:`Counters` is enabled. - #[pyclass(name = "SolverCounters", module = "rapier", frozen)] - #[derive(Clone, Copy)] - pub struct SolverCounters(pub rapier::counters::SolverCounters); - - #[pymethods] - impl SolverCounters { - /// Number of solver constraints assembled. - #[getter] fn nconstraints(&self) -> usize { self.0.nconstraints } - /// Number of contacts driven by the solver. - #[getter] fn ncontacts(&self) -> usize { self.0.ncontacts } - /// Velocity-resolution (impulse) time, in ms. - #[getter] fn velocity_resolution_time_ms(&self) -> f64 { self.0.velocity_resolution_time.time_ms() } - /// Velocity-constraint assembly time, in ms. - #[getter] fn velocity_assembly_time_ms(&self) -> f64 { self.0.velocity_assembly_time.time_ms() } - /// Post-solve velocity-integration time, in ms. - #[getter] fn velocity_update_time_ms(&self) -> f64 { self.0.velocity_update_time.time_ms() } - } - - /// Read-only continuous-collision-detection (CCD) counters. - /// - /// Only populated when the owning :class:`Counters` is enabled. - #[pyclass(name = "CCDCounters", module = "rapier", frozen)] - #[derive(Clone, Copy)] - pub struct CCDCounters(pub rapier::counters::CCDCounters); - - #[pymethods] - impl CCDCounters { - /// Number of CCD substeps taken this step. - #[getter] fn num_substeps(&self) -> usize { self.0.num_substeps } - /// Time spent computing time-of-impact, in ms. - #[getter] fn toi_computation_time_ms(&self) -> f64 { self.0.toi_computation_time.time_ms() } - /// Per-substep solver time, in ms. - #[getter] fn solver_time_ms(&self) -> f64 { self.0.solver_time.time_ms() } - /// Per-substep broad-phase time, in ms. - #[getter] fn broad_phase_time_ms(&self) -> f64 { self.0.broad_phase_time.time_ms() } - /// Per-substep narrow-phase time, in ms. - #[getter] fn narrow_phase_time_ms(&self) -> f64 { self.0.narrow_phase_time.time_ms() } - } - - /// Aggregate read-only timing counters from a :class:`PhysicsPipeline`. - /// - /// Disabled by default — call :meth:`enable` before stepping the - /// pipeline to collect data, then read the per-stage sub-counters - /// (:attr:`stages`, :attr:`cd`, :attr:`solver`, :attr:`ccd`). - /// - /// Timing is best-effort and may be zero on platforms without a - /// high-resolution clock. - #[pyclass(name = "Counters", module = "rapier")] - #[derive(Clone, Copy)] - pub struct Counters(pub rapier::counters::Counters); - - #[pymethods] - impl Counters { - /// Construct a disabled ``Counters``. Call :meth:`enable` to start collecting. - #[new] - fn new() -> Self { Self(rapier::counters::Counters::new(false)) } - - /// Enable timing collection on the next ``step()``. - fn enable(&mut self) { self.0.enable() } - /// Disable timing collection. - fn disable(&mut self) { self.0.disable() } - /// Zero out every sub-counter. - fn reset(&mut self) { self.0.reset() } - /// ``True`` iff timing collection is currently active. - #[getter] fn enabled(&self) -> bool { self.0.enabled() } - /// Total time for the last ``step()`` call, in ms. - #[getter] fn step_time_ms(&self) -> f64 { self.0.step_time_ms() } - /// Time accumulated under the "custom" stage hook, in ms. - #[getter] fn custom_time_ms(&self) -> f64 { self.0.custom_time_ms() } - /// Per-stage breakdown (see :class:`StagesCounters`). - #[getter] fn stages(&self) -> StagesCounters { StagesCounters(self.0.stages) } - /// Collision-detection breakdown (see :class:`CollisionDetectionCounters`). - #[getter] fn cd(&self) -> CollisionDetectionCounters { CollisionDetectionCounters(self.0.cd) } - /// Solver breakdown (see :class:`SolverCounters`). - #[getter] fn solver(&self) -> SolverCounters { SolverCounters(self.0.solver) } - /// CCD breakdown (see :class:`CCDCounters`). - #[getter] fn ccd(&self) -> CCDCounters { CCDCounters(self.0.ccd) } - /// Pretty-print the entire counter tree to stdout. - #[pyo3(name = "print")] - fn py_print(&self) { - use std::println; - println!("{}", self.0); - } - /// Debug repr — shows enabled state and total step time. - fn __repr__(&self) -> String { - format!("Counters(enabled={}, step_time_ms={})", - self.0.enabled(), self.0.step_time_ms()) - } - } - - // ===================================================================== - // PhysicsPipeline - // ===================================================================== - - /// Low-level physics-step driver. - /// - /// The :class:`PhysicsPipeline` owns the persistent solver state but - /// requires you to hand it every sub-set (bodies, colliders, islands, - /// broad / narrow phase, joints, CCD solver) on each call to - /// :meth:`step`. For most users the higher-level :class:`PhysicsWorld` - /// aggregates these and is more convenient. - #[pyclass(name = "PhysicsPipeline", module = "rapier", unsendable)] - pub struct PhysicsPipeline(pub rapier::pipeline::PhysicsPipeline); - - #[pymethods] - impl PhysicsPipeline { - /// Construct a fresh :class:`PhysicsPipeline`. - #[new] - fn new() -> Self { Self(rapier::pipeline::PhysicsPipeline::new()) } - - /// Read-only :class:`Counters` populated by the last :meth:`step`. - /// - /// Enable timing on the pipeline before stepping to get non-zero - /// numbers. - #[getter] - fn counters(&self) -> Counters { Counters(self.0.counters) } - - /// Advance the simulation by one step. - /// - /// Releases the GIL via ``Python::allow_threads`` while the - /// solver runs, so other Python threads can make progress. - /// Hooks and event-handler callbacks re-acquire the GIL - /// transparently before touching Python objects; any exception - /// raised inside a callback is captured and re-raised after - /// :meth:`step` returns. - /// - /// :param gravity: World-space gravity vector. - /// :param integration_parameters: Solver tuning parameters. - /// :param islands: Body island manager (mutated). - /// :param broad_phase: Broad-phase BVH (mutated). - /// :param narrow_phase: Narrow-phase store (mutated). - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). - /// :param impulse_joints: Impulse-joint set (mutated). - /// :param multibody_joints: Multibody-joint set (mutated). - /// :param ccd_solver: CCD solver state (mutated). - /// :param hooks: Optional ``PhysicsHooks``-protocol object. - /// :param events: Optional ``EventHandler``-protocol object - /// (e.g. :class:`ChannelEventCollector`). - /// :raises: Any exception raised from a Python hook / event - /// callback is re-raised after the step completes. - #[pyo3(signature = ( - gravity, - integration_parameters, - islands, - broad_phase, - narrow_phase, - bodies, - colliders, - impulse_joints, - multibody_joints, - ccd_solver, - hooks=None, - events=None, - ))] - #[allow(clippy::too_many_arguments)] - fn step( - &mut self, - py: Python<'_>, - gravity: PyVector, - integration_parameters: &IntegrationParameters, - islands: &mut IslandManager, - broad_phase: &mut BroadPhaseBvh, - narrow_phase: &mut NarrowPhase, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - impulse_joints: &mut ImpulseJointSet, - multibody_joints: &mut MultibodyJointSet, - ccd_solver: &mut CCDSolver, - hooks: Option<&Bound<'_, PyAny>>, - events: Option<&Bound<'_, PyAny>>, - ) -> PyResult<()> { - let hooks_obj: Option> = - hooks.and_then(|h| if h.is_none() { None } else { Some(h.clone().unbind()) }); - let events_obj: Option> = - events.and_then(|e| if e.is_none() { None } else { Some(e.clone().unbind()) }); - let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError::default())); - let hooks_box = build_physics_hooks(py, hooks_obj.as_ref(), err_slot.clone()); - let events_box = build_event_handler(py, events_obj.as_ref(), err_slot.clone()); - - let g: rapier::math::Vector = gravity.0.into(); - let pp_inner = &mut self.0; - let ip_inner = &integration_parameters.0; - let islands_inner = &mut islands.0; - let bp_inner = &mut broad_phase.0; - let np_inner = &mut narrow_phase.0; - let bodies_inner = &mut bodies.0; - let colliders_inner = &mut colliders.0; - let ij_inner = &mut impulse_joints.0; - let mj_inner = &mut multibody_joints.0; - let ccd_inner = &mut ccd_solver.0; - let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = match hooks_box.as_deref() { - Some(h) => h, - None => &(), - }; - let events_ref: &dyn rapier::pipeline::EventHandler = match events_box.as_deref() { - Some(e) => e, - None => &(), - }; - py.allow_threads(|| { - pp_inner.step( - g, - ip_inner, - islands_inner, - bp_inner, - np_inner, - bodies_inner, - colliders_inner, - ij_inner, - mj_inner, - ccd_inner, - hooks_ref, - events_ref, - ); - }); - drop(hooks_box); - drop(events_box); - let mut slot = err_slot.lock().unwrap(); - if let Some(e) = slot.err.take() { - return Err(e); - } - Ok(()) - } - } - - // ===================================================================== - // CollisionPipeline - // ===================================================================== - - /// Collision-only step driver (no dynamics). - /// - /// A reduced :class:`PhysicsPipeline` that runs the broad / narrow - /// phases and emits collision / contact-force events but **skips** - /// joint and constraint solving. Useful for static scene queries, - /// trigger evaluation, or driving custom controllers. - #[pyclass(name = "CollisionPipeline", module = "rapier", unsendable)] - pub struct CollisionPipeline(pub rapier::pipeline::CollisionPipeline); - - #[pymethods] - impl CollisionPipeline { - /// Construct a fresh :class:`CollisionPipeline`. - #[new] - fn new() -> Self { Self(rapier::pipeline::CollisionPipeline::new()) } - - /// Run a single collision-detection pass. - /// - /// Releases the GIL while running; hook / event callbacks - /// re-acquire it on demand. See :meth:`PhysicsPipeline.step` for - /// the threading and error-propagation contract. - /// - /// :param prediction_distance: Distance used to inflate AABBs - /// when looking for new contact pairs. - /// :param islands: Body island manager. - /// :param broad_phase: Broad-phase BVH (mutated). - /// :param narrow_phase: Narrow-phase store (mutated). - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). - /// :param hooks: Optional ``PhysicsHooks``-protocol object. - /// :param events: Optional ``EventHandler``-protocol object. - #[pyo3(signature = ( - prediction_distance, - islands, - broad_phase, - narrow_phase, - bodies, - colliders, - hooks=None, - events=None, - ))] - #[allow(clippy::too_many_arguments)] - fn step( - &mut self, - py: Python<'_>, - prediction_distance: Real, - islands: &mut IslandManager, - broad_phase: &mut BroadPhaseBvh, - narrow_phase: &mut NarrowPhase, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - hooks: Option<&Bound<'_, PyAny>>, - events: Option<&Bound<'_, PyAny>>, - ) -> PyResult<()> { - let hooks_obj: Option> = - hooks.and_then(|h| if h.is_none() { None } else { Some(h.clone().unbind()) }); - let events_obj: Option> = - events.and_then(|e| if e.is_none() { None } else { Some(e.clone().unbind()) }); - let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError::default())); - let hooks_box = build_physics_hooks(py, hooks_obj.as_ref(), err_slot.clone()); - let events_box = build_event_handler(py, events_obj.as_ref(), err_slot.clone()); - - let cp_inner = &mut self.0; - let islands_inner = &mut islands.0; - let bp_inner = &mut broad_phase.0; - let np_inner = &mut narrow_phase.0; - let bodies_inner = &mut bodies.0; - let colliders_inner = &mut colliders.0; - let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = match hooks_box.as_deref() { - Some(h) => h, - None => &(), - }; - let events_ref: &dyn rapier::pipeline::EventHandler = match events_box.as_deref() { - Some(e) => e, - None => &(), - }; - py.allow_threads(|| { - cp_inner.step( - prediction_distance, - islands_inner, - bp_inner, - np_inner, - bodies_inner, - colliders_inner, - hooks_ref, - events_ref, - ); - }); - drop(hooks_box); - drop(events_box); - let mut slot = err_slot.lock().unwrap(); - if let Some(e) = slot.err.take() { - return Err(e); - } - Ok(()) - } - } - - // ===================================================================== - // PhysicsWorld — convenience aggregate. - // - // All sub-sets are stored as `Py<...>` so that property reads return - // the **same** Python object on every call (mutations persist). - // ===================================================================== - - /// Recommended entry point — aggregates every rapier sub-state in one object. - /// - /// A :class:`PhysicsWorld` owns the body / collider / joint sets, - /// broad / narrow phase, island manager, CCD solver, integration - /// parameters, and physics + query pipelines. It exposes them as - /// stable properties (the same Python object is returned on every - /// access, so mutations persist):: - /// - /// world = PhysicsWorld(gravity=Vec3(0.0, -9.81, 0.0), - /// auto_update_query=True) - /// ground = world.add_collider(ColliderBuilder.cuboid(50, 0.1, 50)) - /// ball = world.add_body( - /// RigidBodyBuilder.dynamic().translation(Vec3(0, 5, 0)), - /// colliders=[ColliderBuilder.ball(0.5)], - /// ) - /// for _ in range(60): - /// world.step() - /// - /// assert world.rigid_bodies is world.rigid_bodies # stable - /// - /// Configure scene queries via :attr:`auto_update_query` (refresh the - /// :class:`QueryPipeline` after every step) or call - /// :meth:`update_query_pipeline` manually for finer control. Attach a - /// :class:`ChannelEventCollector` to :attr:`event_handler` to consume - /// collision / contact-force events. - #[pyclass(name = "PhysicsWorld", module = "rapier", unsendable)] - pub struct PhysicsWorld { - pub bodies: Py, - pub colliders: Py, - pub impulse_joints: Py, - pub multibody_joints: Py, - pub broad_phase: Py, - pub narrow_phase: Py, - pub islands: Py, - pub ccd_solver: Py, - pub integration_parameters: Py, - pub physics_pipeline: Py, - pub query_pipeline: Py, - pub gravity: $Vec, - pub event_handler: Option>, - pub physics_hooks: Option>, - pub auto_update_query: bool, - /// Either `"defer"` (best-effort: stash exceptions, re-raise after - /// `step()`) or `"strict"` (also short-circuit further hook calls - /// within the same step). - pub event_error_policy: String, - } - - #[pymethods] - impl PhysicsWorld { - /// Construct a new world with default sub-states. - /// - /// :param gravity: World-space gravity vector. Defaults to zero - /// (no gravity). - /// :param auto_update_query: When ``True``, :meth:`step` calls - /// :meth:`update_query_pipeline` automatically; otherwise the - /// :attr:`query_pipeline` reflects the previous - /// :meth:`update` call. - #[new] - #[pyo3(signature = (gravity=None, auto_update_query=false))] - fn new( - py: Python<'_>, - gravity: Option, - auto_update_query: bool, - ) -> PyResult { - let g = gravity.map(|v| v.0).unwrap_or_else($crate::na::SVector::::zeros); - - let bodies = Py::new(py, RigidBodySet(rapier::dynamics::RigidBodySet::new()))?; - let colliders = Py::new(py, ColliderSet(rapier::geometry::ColliderSet::new()))?; - let impulse_joints = Py::new(py, ImpulseJointSet(rapier::dynamics::ImpulseJointSet::new()))?; - let multibody_joints = Py::new(py, MultibodyJointSet(rapier::dynamics::MultibodyJointSet::new()))?; - let broad_phase = Py::new(py, BroadPhaseBvh(rapier::geometry::BroadPhaseBvh::new()))?; - let narrow_phase = Py::new(py, NarrowPhase(rapier::geometry::NarrowPhase::new()))?; - let islands = Py::new(py, IslandManager(rapier::dynamics::IslandManager::new()))?; - let ccd_solver = Py::new(py, CCDSolver(rapier::dynamics::CCDSolver::new()))?; - let integration_parameters = Py::new( - py, - IntegrationParameters(rapier::dynamics::IntegrationParameters::default()), - )?; - let physics_pipeline = Py::new(py, PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()))?; - - let query_pipeline = Py::new(py, QueryPipeline { - broad_phase: broad_phase.clone_ref(py), - narrow_phase: narrow_phase.clone_ref(py), - bodies: bodies.clone_ref(py), - colliders: colliders.clone_ref(py), - })?; - - Ok(Self { - bodies, colliders, impulse_joints, multibody_joints, - broad_phase, narrow_phase, islands, ccd_solver, - integration_parameters, physics_pipeline, query_pipeline, - gravity: $Vec(g), - event_handler: None, - physics_hooks: None, - auto_update_query, - event_error_policy: "defer".to_string(), - }) - } - - // ---- shared sub-set accessors (return the same Python objects) ---- - - /// The world's :class:`RigidBodySet`. Returns the same Python - /// object on every call, so ``world.rigid_bodies is world.rigid_bodies``. - #[getter] - fn rigid_bodies(&self, py: Python<'_>) -> Py { - self.bodies.clone_ref(py) - } - /// The world's :class:`ColliderSet` (stable across calls). - #[getter] - fn colliders(&self, py: Python<'_>) -> Py { - self.colliders.clone_ref(py) - } - /// The world's :class:`ImpulseJointSet` (stable across calls). - #[getter] - fn impulse_joints(&self, py: Python<'_>) -> Py { - self.impulse_joints.clone_ref(py) - } - /// The world's :class:`MultibodyJointSet` (stable across calls). - #[getter] - fn multibody_joints(&self, py: Python<'_>) -> Py { - self.multibody_joints.clone_ref(py) - } - /// The world's :class:`BroadPhaseBvh` (stable across calls). - #[getter] - fn broad_phase(&self, py: Python<'_>) -> Py { - self.broad_phase.clone_ref(py) - } - /// The world's :class:`NarrowPhase` (stable across calls). - #[getter] - fn narrow_phase(&self, py: Python<'_>) -> Py { - self.narrow_phase.clone_ref(py) - } - /// The world's :class:`IslandManager` (stable across calls). - #[getter] - fn islands(&self, py: Python<'_>) -> Py { - self.islands.clone_ref(py) - } - /// The world's :class:`CCDSolver` (stable across calls). - #[getter] - fn ccd_solver(&self, py: Python<'_>) -> Py { - self.ccd_solver.clone_ref(py) - } - /// Current :class:`IntegrationParameters`. Stable across calls. - #[getter] - fn integration_parameters(&self, py: Python<'_>) -> Py { - self.integration_parameters.clone_ref(py) - } - /// Replace the stored integration parameters in-place. - #[setter] - fn set_integration_parameters( - &mut self, - py: Python<'_>, - ip: &IntegrationParameters, - ) -> PyResult<()> { - let mut cur = self.integration_parameters.borrow_mut(py); - cur.0 = ip.0; - Ok(()) - } - /// The world's underlying :class:`PhysicsPipeline` (stable across calls). - #[getter] - fn physics_pipeline(&self, py: Python<'_>) -> Py { - self.physics_pipeline.clone_ref(py) - } - /// The world's :class:`QueryPipeline` (stable across calls). - #[getter] - fn query_pipeline(&self, py: Python<'_>) -> Py { - self.query_pipeline.clone_ref(py) - } - - /// World-space gravity vector applied to dynamic bodies. - #[getter] - fn gravity(&self) -> $Vec { self.gravity } - /// Set the gravity vector. - #[setter] - fn set_gravity(&mut self, v: PyVector) { - self.gravity = $Vec(v.0); - } - - /// Currently installed ``EventHandler``-protocol object, or ``None``. - /// - /// Usually a :class:`ChannelEventCollector`. - #[getter] - fn event_handler(&self, py: Python<'_>) -> Option { - self.event_handler.as_ref().map(|p| p.clone_ref(py)) - } - /// Install or remove the event handler. - /// - /// Pass ``None`` to detach. The handler is invoked from - /// :meth:`step` (with the GIL re-acquired). - #[setter] - fn set_event_handler(&mut self, py: Python<'_>, v: Option>) { - self.event_handler = v.map(|p| p.clone_ref(py)); - } - - /// Currently installed ``PhysicsHooks``-protocol object, or ``None``. - #[getter] - fn physics_hooks(&self, py: Python<'_>) -> Option { - self.physics_hooks.as_ref().map(|p| p.clone_ref(py)) - } - /// Install or remove the physics-hooks object. - /// - /// The hooks object's ``filter_contact_pair`` / - /// ``filter_intersection_pair`` / ``modify_solver_contacts`` - /// methods are invoked from :meth:`step` (with the GIL - /// re-acquired). - #[setter] - fn set_physics_hooks(&mut self, py: Python<'_>, v: Option>) { - self.physics_hooks = v.map(|p| p.clone_ref(py)); - } - - /// When ``True``, :meth:`step` refreshes the query pipeline after each call. - /// - /// Trade-off: setting this to ``True`` keeps :attr:`query_pipeline` - /// always fresh but adds a BVH rebuild after every step. Setting - /// it to ``False`` avoids the redundant rebuild — useful when you - /// only query infrequently or only after several steps; call - /// :meth:`update_query_pipeline` manually before issuing queries. - #[getter] fn auto_update_query(&self) -> bool { self.auto_update_query } - /// Set :attr:`auto_update_query`. - #[setter] fn set_auto_update_query(&mut self, v: bool) { self.auto_update_query = v; } - - /// Error-propagation policy for hook / event-handler exceptions. - /// - /// ``"defer"`` (default): catch every callback exception, stash the - /// first one, finish the step, and re-raise it at the end. - /// ``"strict"``: same, but also flip an abort flag so subsequent - /// callbacks within the same step short-circuit. - #[getter] - fn event_error_policy(&self) -> String { self.event_error_policy.clone() } - /// Set :attr:`event_error_policy` — must be ``"defer"`` or ``"strict"``. - /// - /// :raises ValueError: If ``v`` is neither ``"defer"`` nor ``"strict"``. - #[setter] - fn set_event_error_policy(&mut self, v: String) -> PyResult<()> { - match v.as_str() { - "defer" | "strict" => { - self.event_error_policy = v; - Ok(()) - } - _ => Err($crate::pyo3::exceptions::PyValueError::new_err( - "event_error_policy must be 'defer' or 'strict'", - )), - } - } - - // ---- step ---- - - /// Advance the simulation by one step using the world's configuration. - /// - /// Uses the stored :attr:`integration_parameters`, :attr:`gravity`, - /// :attr:`physics_hooks`, and :attr:`event_handler`. Releases the - /// GIL via ``Python::allow_threads`` while the solver runs; - /// callbacks re-acquire the GIL before invoking Python code. - /// When :attr:`auto_update_query` is ``True``, the query pipeline - /// is refreshed after the step. Exceptions raised inside Python - /// callbacks are deferred per :attr:`event_error_policy` and - /// re-raised after the step completes. - fn step(&self, py: Python<'_>) -> PyResult<()> { - let g = self.gravity.0; - // Build the (boxed) hook/event adapters out-of-band so that - // they can be passed as `&dyn Trait` into `allow_threads`. - let err_slot = std::sync::Arc::new(std::sync::Mutex::new(DeferredError { - err: None, - aborted: false, - policy_strict: self.event_error_policy == "strict", - })); - let hooks_box = build_physics_hooks(py, self.physics_hooks.as_ref(), err_slot.clone()); - let events_box = build_event_handler(py, self.event_handler.as_ref(), err_slot.clone()); - { - let mut pp = self.physics_pipeline.borrow_mut(py); - let ip = self.integration_parameters.borrow(py); - let mut islands = self.islands.borrow_mut(py); - let mut bp = self.broad_phase.borrow_mut(py); - let mut np = self.narrow_phase.borrow_mut(py); - let mut bodies = self.bodies.borrow_mut(py); - let mut colliders = self.colliders.borrow_mut(py); - let mut ij = self.impulse_joints.borrow_mut(py); - let mut mj = self.multibody_joints.borrow_mut(py); - let mut ccd = self.ccd_solver.borrow_mut(py); - let g_engine: rapier::math::Vector = g.into(); - // Borrow the inner rapier values explicitly so the - // `Ungil` closure doesn't capture any `Python<'_>` token - // (which is `!Send`). - let pp_inner = &mut pp.0; - let ip_inner = &ip.0; - let islands_inner = &mut islands.0; - let bp_inner = &mut bp.0; - let np_inner = &mut np.0; - let bodies_inner = &mut bodies.0; - let colliders_inner = &mut colliders.0; - let ij_inner = &mut ij.0; - let mj_inner = &mut mj.0; - let ccd_inner = &mut ccd.0; - let hooks_ref: &dyn rapier::pipeline::PhysicsHooks = - match hooks_box.as_deref() { - Some(h) => h, - None => &(), - }; - let events_ref: &dyn rapier::pipeline::EventHandler = - match events_box.as_deref() { - Some(e) => e, - None => &(), - }; - py.allow_threads(|| { - pp_inner.step( - g_engine, - ip_inner, - islands_inner, - bp_inner, - np_inner, - bodies_inner, - colliders_inner, - ij_inner, - mj_inner, - ccd_inner, - hooks_ref, - events_ref, - ); - }); - } - if self.auto_update_query { - self.update_query_pipeline(py)?; - } - drop(hooks_box); - drop(events_box); - let mut slot = err_slot.lock().unwrap(); - if let Some(e) = slot.err.take() { - return Err(e); - } - Ok(()) - } - - /// Refresh the :attr:`query_pipeline`'s broad-phase BVH. - /// - /// Drains the collider set's ``modified`` / ``removed`` change - /// sets and updates the BVH. Call this manually before issuing - /// scene queries when :attr:`auto_update_query` is ``False``, or - /// after directly mutating colliders / bodies and wanting an - /// immediate refresh. - fn update_query_pipeline(&self, py: Python<'_>) -> PyResult<()> { - let ip = self.integration_parameters.borrow(py); - let bodies = self.bodies.borrow(py); - let mut colliders = self.colliders.borrow_mut(py); - let mut bp = self.broad_phase.borrow_mut(py); - // Drain the colliders' "modified" / "removed" change sets so - // the broad-phase BVH catches up without double-handling them - // on the next `step()`. - let modified = colliders.0.take_modified(); - let removed = colliders.0.take_removed(); - let mut events: Vec = Vec::new(); - bp.0.update(&ip.0, &colliders.0, &bodies.0, &modified, &removed, &mut events); - Ok(()) - } - - /// Wake a sleeping body, forcing it back into the active simulation. - /// - /// Useful after manually moving a body, applying forces, or - /// otherwise wanting to make sure it gets simulated on the next - /// step. No-op for already-awake bodies and for fixed bodies. - /// - /// :param handle: Handle of the body to wake. - /// :param strong: If ``True``, the body is guaranteed to stay - /// awake for several frames; if ``False`` it may sleep again - /// immediately once sleep conditions are met. - #[pyo3(signature = (handle, strong=true))] - fn wake_up(&self, py: Python<'_>, handle: &RigidBodyHandle, strong: bool) -> PyResult<()> { - let mut islands = self.islands.borrow_mut(py); - let mut bodies = self.bodies.borrow_mut(py); - islands.0.wake_up(&mut bodies.0, handle.0, strong); - Ok(()) - } - - /// Wake every sleeping body in the world. - /// - /// :param strong: See :meth:`wake_up`. - #[pyo3(signature = (strong=true))] - fn wake_up_all(&self, py: Python<'_>, strong: bool) -> PyResult<()> { - let mut islands = self.islands.borrow_mut(py); - let mut bodies = self.bodies.borrow_mut(py); - let handles: Vec<_> = bodies.0.iter().map(|(h, _)| h).collect(); - for handle in handles { - islands.0.wake_up(&mut bodies.0, handle, strong); - } - Ok(()) - } - - /// Return the handles of the currently active (awake) bodies. - /// - /// Sleeping bodies are skipped, as are bodies that never sleep but - /// aren't part of any active island (e.g. unattached fixed - /// bodies). This is the list to use when rendering or syncing - /// transforms, since the poses of sleeping bodies haven't moved - /// since the last step. Look each handle up via - /// :attr:`rigid_bodies`. - fn active_bodies(&self, py: Python<'_>) -> Vec { - let islands = self.islands.borrow(py); - islands.0.active_bodies().map(RigidBodyHandle).collect() - } - - /// Drop every body, collider, joint, and pipeline state. - /// - /// Leaves :attr:`gravity`, :attr:`integration_parameters`, and - /// :attr:`auto_update_query` untouched. Equivalent to constructing - /// a fresh world while reusing the same Python wrapper objects. - fn clear(&self, py: Python<'_>) -> PyResult<()> { - let mut bodies = self.bodies.borrow_mut(py); - let mut colliders = self.colliders.borrow_mut(py); - let mut ij = self.impulse_joints.borrow_mut(py); - let mut mj = self.multibody_joints.borrow_mut(py); - let mut bp = self.broad_phase.borrow_mut(py); - let mut np = self.narrow_phase.borrow_mut(py); - let mut islands = self.islands.borrow_mut(py); - let mut ccd = self.ccd_solver.borrow_mut(py); - bodies.0 = rapier::dynamics::RigidBodySet::new(); - colliders.0 = rapier::geometry::ColliderSet::new(); - ij.0 = rapier::dynamics::ImpulseJointSet::new(); - mj.0 = rapier::dynamics::MultibodyJointSet::new(); - bp.0 = rapier::geometry::BroadPhaseBvh::new(); - np.0 = rapier::geometry::NarrowPhase::new(); - islands.0 = rapier::dynamics::IslandManager::new(); - ccd.0 = rapier::dynamics::CCDSolver::new(); - Ok(()) - } - - // ---- pythonic add helpers ---- - - /// Insert a rigid body, optionally attaching child colliders in one call. - /// - /// :param builder: A :class:`RigidBody` or :class:`RigidBodyBuilder`. - /// :param colliders: Optional list of :class:`Collider` or - /// :class:`ColliderBuilder` instances attached to the new - /// body. - /// :returns: Handle of the newly inserted body. - /// :raises TypeError: If ``builder`` or any entry in ``colliders`` - /// is not a recognized rapier type. - #[pyo3(signature = (builder, colliders=None))] - fn add_body( - &self, - py: Python<'_>, - builder: &Bound<'_, PyAny>, - colliders: Option<&Bound<'_, _PyList>>, - ) -> PyResult { - let body = if let Ok(b) = builder.extract::>() { - b.builder.clone().build() - } else if let Ok(rb) = builder.extract::>() { - rb.to_owned_body() - } else { - return Err(PyTypeError::new_err( - "PhysicsWorld.add_body expects a RigidBody or RigidBodyBuilder", - )); - }; - let parent_handle = { - let mut bset = self.bodies.borrow_mut(py); - RigidBodyHandle(bset.0.insert(body)) - }; - if let Some(coll_list) = colliders { - for item in coll_list.iter() { - let coll = if let Ok(b) = item.extract::>() { - b.builder.clone().build() - } else if let Ok(c) = item.extract::>() { - c.to_owned_collider() - } else { - return Err(PyTypeError::new_err( - "PhysicsWorld.add_body colliders must be Collider or ColliderBuilder instances", - )); - }; - let mut cset = self.colliders.borrow_mut(py); - let mut bset = self.bodies.borrow_mut(py); - cset.0.insert_with_parent(coll, parent_handle.0, &mut bset.0); - } - } - Ok(parent_handle) - } - - /// Insert a collider, optionally attaching it to a parent rigid body. - /// - /// :param builder: A :class:`Collider` or :class:`ColliderBuilder`. - /// :param parent: Optional rigid-body handle; when ``None`` the - /// collider is added as standalone (no parent body). - /// :returns: Handle of the newly inserted collider. - /// :raises TypeError: If ``builder`` is not a recognized rapier - /// type. - #[pyo3(signature = (builder, parent=None))] - fn add_collider( - &self, - py: Python<'_>, - builder: &Bound<'_, PyAny>, - parent: Option<&RigidBodyHandle>, - ) -> PyResult { - let coll = if let Ok(b) = builder.extract::>() { - b.builder.clone().build() - } else if let Ok(c) = builder.extract::>() { - c.to_owned_collider() - } else { - return Err(PyTypeError::new_err( - "PhysicsWorld.add_collider expects a Collider or ColliderBuilder", - )); - }; - let mut cset = self.colliders.borrow_mut(py); - let handle = match parent { - None => cset.0.insert(coll), - Some(h) => { - let mut bset = self.bodies.borrow_mut(py); - cset.0.insert_with_parent(coll, h.0, &mut bset.0) - } - }; - Ok(ColliderHandle(handle)) - } - - /// Remove a rigid body, its colliders, and any attached joints. - /// - /// :param handle: Handle returned from :meth:`add_body`. - /// :returns: The removed :class:`RigidBody`, or ``None`` if the - /// handle was already invalid. - fn remove_body( - &self, - py: Python<'_>, - handle: &RigidBodyHandle, - ) -> PyResult> { - let mut bodies = self.bodies.borrow_mut(py); - let mut islands = self.islands.borrow_mut(py); - let mut colliders = self.colliders.borrow_mut(py); - let mut ij = self.impulse_joints.borrow_mut(py); - let mut mj = self.multibody_joints.borrow_mut(py); - Ok(bodies.0.remove( - handle.0, - &mut islands.0, - &mut colliders.0, - &mut ij.0, - &mut mj.0, - true, - ).map(RigidBody::new_owned)) - } - - /// Remove a collider, detaching it from its parent body if any. - /// - /// :param handle: Handle returned from :meth:`add_collider`. - /// :returns: The removed :class:`Collider`, or ``None`` if the - /// handle was already invalid. - fn remove_collider( - &self, - py: Python<'_>, - handle: &ColliderHandle, - ) -> PyResult> { - let mut cset = self.colliders.borrow_mut(py); - let mut bset = self.bodies.borrow_mut(py); - let mut islands = self.islands.borrow_mut(py); - Ok(cset.0.remove(handle.0, &mut islands.0, &mut bset.0, true) - .map(Collider::new_owned)) - } - - /// Debug repr — shows body and collider counts. - fn __repr__(&self, py: Python<'_>) -> String { - let nb = self.bodies.borrow(py).0.len(); - let nc = self.colliders.borrow(py).0.len(); - format!("PhysicsWorld(bodies={}, colliders={})", nb, nc) - } - } - - // touch the imported _PyList so unused warnings don't trip. - #[allow(dead_code)] - type _PipelinePyListAlias = _PyList; - }; -} - -// ============================================================================ -// Helpers: dim-specific bits used by the macro. -// ============================================================================ - -#[doc(hidden)] -#[macro_export] -macro_rules! __feature_id_edge_dim_specific { - (3) => { - #[staticmethod] - #[pyo3(name = "Edge")] - fn edge(id: u32) -> Self { - Self { kind: "edge", id } - } - }; - (2) => { - // FeatureId::Edge is 3D-only in parry; we still let users construct - // it on 2D for portability, but it's never returned in 2D queries. - #[staticmethod] - #[pyo3(name = "Edge")] - fn edge(id: u32) -> Self { - Self { kind: "edge", id } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __feature_id_from_parry { - (3, $f:expr) => {{ - use rapier::parry::shape::FeatureId as F; - match $f { - F::Vertex(i) => Self { - kind: "vertex", - id: i, - }, - F::Edge(i) => Self { - kind: "edge", - id: i, - }, - F::Face(i) => Self { - kind: "face", - id: i, - }, - F::Unknown => Self { - kind: "unknown", - id: 0, - }, - } - }}; - (2, $f:expr) => {{ - use rapier::parry::shape::FeatureId as F; - match $f { - F::Vertex(i) => Self { - kind: "vertex", - id: i, - }, - F::Face(i) => Self { - kind: "face", - id: i, - }, - F::Unknown => Self { - kind: "unknown", - id: 0, - }, - } - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __nonlinear_angvel_extract { - (3, $angvel:expr) => {{ - let av: rapier::math::Vector = match $angvel { - None => rapier::math::Vector::ZERO, - Some(v) => { - let pv: PyVector = v.extract()?; - pv.0.into() - } - }; - Ok::(av) - }}; - (2, $angvel:expr) => {{ - let av: Real = match $angvel { - None => 0.0 as Real, - Some(v) => { - let f: Real = v.extract()?; - f - } - }; - Ok::(av) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __nonlinear_angvel_getter { - (3, $Vec:ident) => { - #[pymethods] - impl NonlinearRigidMotion { - /// Angular velocity (3D: ``Vec3``, world-space, radians per unit time). - #[getter] - fn angvel(&self) -> $Vec { - let v: $crate::na::Vector3 = self.0.angvel.into(); - $Vec(v) - } - } - }; - (2, $Vec:ident) => { - #[pymethods] - impl NonlinearRigidMotion { - /// Angular velocity (2D: scalar, radians per unit time). - #[getter] - fn angvel(&self) -> Real { - self.0.angvel - } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __qp_query { - ($self:expr, $py:expr, $filter:expr, $body:expr) => {{ - // Borrow the world's sub-sets immutably (queries are read-only). - let bp = $self.broad_phase.borrow($py); - let np = $self.narrow_phase.borrow($py); - let bodies = $self.bodies.borrow($py); - let colliders = $self.colliders.borrow($py); - - // Build the raw QueryFilter, handling the (optional) Python predicate. - // The predicate is captured in a stack-pinned closure so the upstream - // borrow (`&'a dyn Fn(...)`) stays valid for the call. - let py_pred_obj: Option> = - $filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref($py))); - let pred_err = std::cell::RefCell::new(None::<$crate::pyo3::PyErr>); - let pred_closure = - |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| -> bool { - let _ = co; - if let Some(ref obj) = py_pred_obj { - // Hand the predicate a live view into the set (no copy of the - // collider). The set is already borrowed immutably for the - // query, so the view's reads re-borrow shared — fine; the - // predicate must not mutate the collider mid-query. - let coll_py = Collider { - backing: ColliderBacking::InSet { - set: $self.colliders.clone_ref($py), - handle: h, - }, - }; - let h_py = ColliderHandle(h); - match obj.call1($py, (h_py, coll_py)) { - Ok(r) => r.extract::($py).unwrap_or(true), - Err(e) => { - *pred_err.borrow_mut() = Some(e); - true - } - } - } else { - true - } - }; - let pred_dyn: Option< - &dyn Fn(rapier::geometry::ColliderHandle, &rapier::geometry::Collider) -> bool, - > = if py_pred_obj.is_some() { - Some(&pred_closure) - } else { - None - }; - - let qf = $filter.map(|f| f.as_rapier(pred_dyn)).unwrap_or_else(|| { - let mut base = rapier::pipeline::QueryFilter::default(); - base.predicate = pred_dyn; - base - }); - - let qp = - bp.0.as_query_pipeline(np.0.query_dispatcher(), &bodies.0, &colliders.0, qf); - - let result = ($body)(qp); - if let Some(e) = pred_err.into_inner() { - return Err(e); - } - result - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_pipeline_register { - () => { - pub fn register_pipeline( - _py: $crate::pyo3::Python<'_>, - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -} diff --git a/python/rapier-py-core/src/serde_io.rs b/python/rapier-py-core/src/serde_io.rs deleted file mode 100644 index 252f07711..000000000 --- a/python/rapier-py-core/src/serde_io.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Shared (dim/scalar-agnostic) helpers used by pickle/snapshot serialization. -//! -//! The on-disk format always begins with an 8-byte header: -//! - 4 bytes magic = `b"RPYS"` -//! - 4 bytes little-endian `u32` version (currently 1) -//! -//! Anything that follows is the bincode-serialized payload (or for the JSON -//! debug mode, a JSON object with a `"_rpys"` magic+version field, see below). -//! -//! Mismatched magic or unknown version => `SerializationError`. -//! -//! This module deliberately stays free of `rapier::...` paths so that it can -//! live in the dim/scalar-independent `rapier-py-core`. The dim/scalar-specific -//! glue (the `Py` round-trip on `RigidBody`, `Collider`, `RigidBodySet`, -//! etc., plus `PhysicsWorld.snapshot()`/`restore()`) is emitted by the -//! `define_serde_types!` macro, which is invoked once per cdylib. - -use pyo3::prelude::*; -use pyo3::types::PyBytes; - -/// Magic header marking a rapier-py blob ("Rapier PYthon Snapshot"). -pub const MAGIC: &[u8; 4] = b"RPYS"; -/// Current on-disk format version. Bumped on incompatible payload changes. -pub const VERSION: u32 = 1; -/// Total size of the binary header (magic + u32 version). -pub const HEADER_SIZE: usize = 8; - -/// Prepend the 8-byte binary header to `payload`. -#[inline] -pub fn wrap_bincode(payload: Vec) -> Vec { - let mut out = Vec::with_capacity(HEADER_SIZE + payload.len()); - out.extend_from_slice(MAGIC); - out.extend_from_slice(&VERSION.to_le_bytes()); - out.extend_from_slice(&payload); - out -} - -/// Strip and validate the 8-byte binary header. Returns the body. -/// -/// Returns `SerializationError` on: -/// - Blob shorter than the header. -/// - Magic mismatch (`!= b"RPYS"`). -/// - Unsupported version (`!= VERSION`). -pub fn unwrap_bincode(blob: &[u8]) -> PyResult<&[u8]> { - if blob.len() < HEADER_SIZE { - return Err(crate::errors::SerializationError::new_err(format!( - "serialized blob too short ({} bytes); expected at least {}", - blob.len(), - HEADER_SIZE - ))); - } - if &blob[..4] != MAGIC { - return Err(crate::errors::SerializationError::new_err(format!( - "bad magic in serialized blob: expected {:?} got {:?}", - std::str::from_utf8(MAGIC).unwrap_or("?"), - std::str::from_utf8(&blob[..4]).unwrap_or("") - ))); - } - let mut ver_bytes = [0u8; 4]; - ver_bytes.copy_from_slice(&blob[4..8]); - let ver = u32::from_le_bytes(ver_bytes); - if ver != VERSION { - return Err(crate::errors::SerializationError::new_err(format!( - "unsupported serialization version {} (this build expects {})", - ver, VERSION - ))); - } - Ok(&blob[HEADER_SIZE..]) -} - -/// Convert any `bincode::Error` into a `SerializationError`. Used in the -/// `to_bytes`/`from_bytes` impls emitted by the macro. -#[inline] -pub fn bincode_err(e: bincode::Error) -> PyErr { - crate::errors::SerializationError::new_err(format!("bincode error: {}", e)) -} - -/// Same idea for `serde_json::Error`. -#[inline] -pub fn json_err(e: serde_json::Error) -> PyErr { - crate::errors::SerializationError::new_err(format!("json error: {}", e)) -} - -/// Bytes-from-borrow helper: wrap a `&[u8]` view into a Python `bytes`. -#[inline] -pub fn bytes_to_py<'py>(py: Python<'py>, blob: &[u8]) -> Bound<'py, PyBytes> { - PyBytes::new_bound(py, blob) -} - -/// Magic + version embedded in the JSON envelope as plain string/integer -/// fields so the file is self-describing for human readers. -pub const JSON_MAGIC: &str = "RPYS"; - -/// Build the JSON envelope `{"_magic": "RPYS", "_version": 1, "payload": }`. -pub fn wrap_json(payload: serde_json::Value) -> serde_json::Value { - serde_json::json!({ - "_magic": JSON_MAGIC, - "_version": VERSION, - "payload": payload, - }) -} - -/// Validate + extract the inner payload from the JSON envelope. -pub fn unwrap_json(envelope: serde_json::Value) -> PyResult { - let obj = envelope.as_object().ok_or_else(|| { - crate::errors::SerializationError::new_err( - "expected JSON object envelope, got something else", - ) - })?; - let magic = obj.get("_magic").and_then(|v| v.as_str()).ok_or_else(|| { - crate::errors::SerializationError::new_err("JSON envelope missing '_magic' field") - })?; - if magic != JSON_MAGIC { - return Err(crate::errors::SerializationError::new_err(format!( - "bad JSON magic: expected {:?} got {:?}", - JSON_MAGIC, magic - ))); - } - let ver = obj - .get("_version") - .and_then(|v| v.as_u64()) - .ok_or_else(|| { - crate::errors::SerializationError::new_err("JSON envelope missing '_version' field") - })?; - if ver as u32 != VERSION { - return Err(crate::errors::SerializationError::new_err(format!( - "unsupported JSON serialization version {} (expected {})", - ver, VERSION - ))); - } - obj.get("payload").cloned().ok_or_else(|| { - crate::errors::SerializationError::new_err("JSON envelope missing 'payload'") - }) -} diff --git a/python/rapier-py-core/src/serde_macros.rs b/python/rapier-py-core/src/serde_macros.rs deleted file mode 100644 index 45082b8c8..000000000 --- a/python/rapier-py-core/src/serde_macros.rs +++ /dev/null @@ -1,767 +0,0 @@ -//! pickle / snapshot macro emitted per-cdylib. -//! -//! Each cdylib invokes `define_serde_types!(DIM = 2|3)` AFTER all the other -//! macros (`define_math_types!`, `define_dynamics_types!`, etc.). This macro -//! adds extra `#[pymethods]` blocks (enabled by pyo3's `multiple-pymethods` -//! feature) to attach `to_bytes`, `from_bytes`, and `__reduce__` to the -//! relevant pyclasses, plus `snapshot`, `restore`, `snapshot_json`, and -//! `restore_json` to `PhysicsWorld`. -//! -//! The implementations rely on rapier being compiled with `serde-serialize`, -//! which each `rapier-py-{2d,2d-f64,3d,3d-f64}` cdylib enables in its -//! Cargo.toml. - -/// Materialize the per-cdylib serde glue. -/// -/// Must be invoked **after** every other `define_*_types!` macro because it -/// adds extra `#[pymethods]` blocks on those pyclasses. -#[macro_export] -macro_rules! define_serde_types { - (DIM = 2) => { - $crate::__define_serde_common!(2); - }; - (DIM = 3) => { - $crate::__define_serde_common!(3); - }; -} - -/// Internal: emit a `__reduce__` for a frozen, copy-able handle-style class. -/// -/// `$Cls` must already expose a `from_raw_parts(index, generation) -> Self` -/// staticmethod and `.index()` / `.generation()` getters. -#[doc(hidden)] -#[macro_export] -macro_rules! __reduce_handle { - ($Cls:ident) => { - #[pymethods] - impl $Cls { - /// Pickle support: `(type(self), (index, generation))`. - fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { - let py = slf.py(); - let (i, g) = slf.0.into_raw_parts(); - let typ: Py = py - .get_type_bound::<$Cls>() - .getattr("from_raw_parts")? - .unbind(); - Ok((typ, (i, g))) - } - } - }; -} - -/// Internal: emit `to_bytes` / `from_bytes` / `__reduce__` for an owned-value -/// pyclass that wraps a serde-able `rapier` value. -/// -/// `$Cls` is the pyclass name (the wrapper, e.g. `RigidBodyBuilder`). -/// `$inner` is the field path on `Self` ending in the rapier inner value -/// (e.g. `body`, `builder`, `0`). -#[doc(hidden)] -#[macro_export] -macro_rules! __serde_owned_value { - ($Cls:ident, $field:tt, $constructor:expr, $ctor_name:literal) => { - #[pymethods] - impl $Cls { - /// Serialize this value to a bincode'd `bytes`. The blob is prefixed - /// with the 4-byte magic `RPYS` and a 4-byte little-endian version. - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = &self.$field; - let payload = - $crate::bincode::serialize(inner).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - - /// Deserialize from a `bytes` blob produced by `to_bytes` (or pickle). - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let buf = blob.as_bytes(); - let body = $crate::serde_io::unwrap_bincode(buf)?; - let inner = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - Ok(($constructor)(inner)) - } - - /// Pickle support: `(cls.from_bytes, (snapshot_bytes,))`. - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py.get_type_bound::<$Cls>().getattr($ctor_name)?.unbind(); - Ok((ctor, (blob,))) - } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __define_serde_common { - ($dim:tt) => { - // pyo3::types::PyBytes already in scope from earlier macros, but - // earlier macros didn't import it on every dim-specific block. Bring - // it in unconditionally so the templates below compile. - use $crate::pyo3::exceptions::PyTypeError as _PickleTypeError; - #[allow(unused_imports)] - use $crate::pyo3::types::PyBytes; - - // ============================================================ - // Handle types — `__reduce__ = (from_raw_parts, (idx, gen))`. - // ============================================================ - $crate::__reduce_handle!(RigidBodyHandle); - $crate::__reduce_handle!(ColliderHandle); - $crate::__reduce_handle!(ImpulseJointHandle); - $crate::__reduce_handle!(MultibodyJointHandle); - - // MultibodyIndex / MultibodyLinkId have a slightly different shape - // (no `from_raw_parts` constructor on the Python side). Provide them. - #[pymethods] - impl MultibodyIndex { - #[staticmethod] - fn from_raw_parts(index: u32, generation: u32) -> Self { - Self(rapier::dynamics::MultibodyIndex( - rapier::data::arena::Index::from_raw_parts(index, generation), - )) - } - fn into_raw_parts(&self) -> (u32, u32) { - self.0.0.into_raw_parts() - } - fn __reduce__(slf: PyRef<'_, Self>) -> PyResult<(Py, (u32, u32))> { - let py = slf.py(); - let (i, g) = slf.0.0.into_raw_parts(); - let ctor: Py = py - .get_type_bound::() - .getattr("from_raw_parts")? - .unbind(); - Ok((ctor, (i, g))) - } - } - - // `MultibodyLinkId` has a private `graph_id` field; we round-trip - // through bincode (it's serde-derived upstream). - #[pymethods] - impl MultibodyLinkId { - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let payload = - $crate::bincode::serialize(&self.0).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let body = $crate::serde_io::unwrap_bincode(blob.as_bytes())?; - let inner: rapier::dynamics::MultibodyLinkId = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - Ok(Self(inner)) - } - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("from_bytes")? - .unbind(); - Ok((ctor, (blob,))) - } - } - - // ============================================================ - // RigidBody / RigidBodyBuilder / Collider / ColliderBuilder - // ============================================================ - - // RigidBody is a thin view (owned standalone, or a handle into a set), - // so it has no single `body` field for the generic serde macro to read. - // Serialize by reading the underlying body out; deserialize as a fresh - // owned body. - #[pymethods] - impl RigidBody { - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.to_owned_body(); - let payload = - $crate::bincode::serialize(&inner).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let buf = blob.as_bytes(); - let body = $crate::serde_io::unwrap_bincode(buf)?; - let inner: rapier::dynamics::RigidBody = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - Ok(RigidBody::new_owned(inner)) - } - - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("from_bytes")? - .unbind(); - Ok((ctor, (blob,))) - } - } - - // RigidBodyBuilder is NOT `Serialize`/`Deserialize` upstream. To make - // it picklable, we round-trip through its built form (a RigidBody), - // which is serde-derived. This preserves every field that influences - // the spawned body but discards the "builder vs body" distinction — - // round-tripped builders behave identically when `.build()` is called. - #[pymethods] - impl RigidBodyBuilder { - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let body = self.builder.clone().build(); - let payload = - $crate::bincode::serialize(&body).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let buf = blob.as_bytes(); - let body_buf = $crate::serde_io::unwrap_bincode(buf)?; - let body: rapier::dynamics::RigidBody = $crate::bincode::deserialize(body_buf) - .map_err($crate::serde_io::bincode_err)?; - // RigidBodyBuilder has no `.from_body` constructor — rebuild - // a fresh builder of the correct type and copy in fields - // we can read back. (`can_sleep` is not retrievable, and the - // builder's sleeping/enabled bits aren't observable on a built - // body either, so we approximate.) - let mut b = rapier::dynamics::RigidBodyBuilder::new(body.body_type()); - b = b - .pose(*body.position()) - .linvel(body.linvel()) - .linear_damping(body.linear_damping()) - .angular_damping(body.angular_damping()) - .sleeping(body.is_sleeping()) - .gravity_scale(body.gravity_scale()) - .ccd_enabled(body.is_ccd_enabled()) - .soft_ccd_prediction(body.soft_ccd_prediction()) - .dominance_group(body.dominance_group()) - .additional_solver_iterations(body.additional_solver_iterations()) - .user_data(body.user_data) - .enabled(body.is_enabled()) - .locked_axes(body.locked_axes()); - $crate::__rb_builder_angvel_from_body!($dim, b, body); - Ok(Self { builder: b }) - } - - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("from_bytes")? - .unbind(); - Ok((ctor, (blob,))) - } - } - - // Collider is a thin view (owned, or a handle into a set); serialize by - // reading the underlying collider out, deserialize as a fresh owned one. - #[pymethods] - impl Collider { - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.to_owned_collider(); - let payload = - $crate::bincode::serialize(&inner).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let buf = blob.as_bytes(); - let body = $crate::serde_io::unwrap_bincode(buf)?; - let inner: rapier::geometry::Collider = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - Ok(Collider::new_owned(inner)) - } - - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("from_bytes")? - .unbind(); - Ok((ctor, (blob,))) - } - } - - impl ColliderBuilder { - #[inline] - pub(crate) fn _from_inner(builder: rapier::geometry::ColliderBuilder) -> Self { - Self { builder } - } - } - $crate::__serde_owned_value!( - ColliderBuilder, - builder, - ColliderBuilder::_from_inner, - "from_bytes" - ); - - // ============================================================ - // Sets (newtype `Self(inner)`). - // ============================================================ - $crate::__serde_owned_value!(RigidBodySet, 0, RigidBodySet, "from_bytes"); - $crate::__serde_owned_value!(ColliderSet, 0, ColliderSet, "from_bytes"); - $crate::__serde_owned_value!(ImpulseJointSet, 0, ImpulseJointSet, "from_bytes"); - $crate::__serde_owned_value!(MultibodyJointSet, 0, MultibodyJointSet, "from_bytes"); - - // ============================================================ - // Parameters / scratch / phases - // ============================================================ - $crate::__serde_owned_value!( - IntegrationParameters, - 0, - IntegrationParameters, - "from_bytes" - ); - $crate::__serde_owned_value!(IslandManager, 0, IslandManager, "from_bytes"); - $crate::__serde_owned_value!(CCDSolver, 0, CCDSolver, "from_bytes"); - $crate::__serde_owned_value!(BroadPhaseBvh, 0, BroadPhaseBvh, "from_bytes"); - $crate::__serde_owned_value!(NarrowPhase, 0, NarrowPhase, "from_bytes"); - - // ============================================================ - // Mass / interaction / shape types - // ============================================================ - $crate::__serde_owned_value!(MassProperties, 0, MassProperties, "from_bytes"); - $crate::__serde_owned_value!(InteractionGroups, 0, InteractionGroups, "from_bytes"); - $crate::__serde_owned_value!(SharedShape, 0, SharedShape, "from_bytes"); - - // ============================================================ - // Joint types (concrete + builders + generic). - // ============================================================ - $crate::__serde_owned_value!(FixedJoint, 0, FixedJoint, "from_bytes"); - $crate::__serde_owned_value!(FixedJointBuilder, 0, FixedJointBuilder, "from_bytes"); - $crate::__serde_owned_value!(RevoluteJoint, 0, RevoluteJoint, "from_bytes"); - $crate::__serde_owned_value!(RevoluteJointBuilder, 0, RevoluteJointBuilder, "from_bytes"); - $crate::__serde_owned_value!(PrismaticJoint, 0, PrismaticJoint, "from_bytes"); - $crate::__serde_owned_value!( - PrismaticJointBuilder, - 0, - PrismaticJointBuilder, - "from_bytes" - ); - $crate::__serde_owned_value!(RopeJoint, 0, RopeJoint, "from_bytes"); - $crate::__serde_owned_value!(RopeJointBuilder, 0, RopeJointBuilder, "from_bytes"); - $crate::__serde_owned_value!(SpringJoint, 0, SpringJoint, "from_bytes"); - $crate::__serde_owned_value!(SpringJointBuilder, 0, SpringJointBuilder, "from_bytes"); - // GenericJoint is a thin view (owned, or a view into an in-set joint's - // data); serialize the underlying value, deserialize as owned. - #[pymethods] - impl GenericJoint { - fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.to_owned_generic(); - let payload = - $crate::bincode::serialize(&inner).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - #[staticmethod] - fn from_bytes(blob: &Bound<'_, PyBytes>) -> PyResult { - let body = $crate::serde_io::unwrap_bincode(blob.as_bytes())?; - let inner: rapier::dynamics::GenericJoint = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - Ok(GenericJoint::new_owned(inner)) - } - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.to_bytes(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("from_bytes")? - .unbind(); - Ok((ctor, (blob,))) - } - } - $crate::__serde_owned_value!(GenericJointBuilder, 0, GenericJointBuilder, "from_bytes"); - - $crate::__define_serde_dim_specific_joints!($dim); - - // ============================================================ - // Math types — Vec, Point, Rotation, Isometry. - // ============================================================ - $crate::__define_serde_math!($dim); - - // ============================================================ - // Controllers — try to serialize. Wrap each in `__reduce__` only if - // serde-derivable on the inner. KinematicCharacterController, PdController, - // PidController, DynamicRayCastVehicleController are all - // `Serialize + Deserialize` upstream. - // ============================================================ - $crate::__serde_owned_value!( - KinematicCharacterController, - 0, - KinematicCharacterController, - "from_bytes" - ); - $crate::__serde_owned_value!(PdController, 0, PdController, "from_bytes"); - $crate::__serde_owned_value!(PidController, 0, PidController, "from_bytes"); - - // `DynamicRayCastVehicleController` + `WheelTuning` are 3D-only - // upstream. Skip in 2D builds. - $crate::__define_serde_vehicle!($dim); - - // ============================================================ - // Pipelines + collectors — REFUSE pickle. - // ============================================================ - #[pymethods] - impl PhysicsPipeline { - fn __reduce__(&self) -> PyResult<()> { - Err($crate::errors::SerializationError::new_err( - "PhysicsPipeline is not serializable; it's stateless except for \ - per-step counters. Re-create a PhysicsPipeline after restoring \ - the world.", - )) - } - } - - #[pymethods] - impl CollisionPipeline { - fn __reduce__(&self) -> PyResult<()> { - Err($crate::errors::SerializationError::new_err( - "CollisionPipeline is not serializable; re-create one after restore.", - )) - } - } - - #[pymethods] - impl ChannelEventCollector { - fn __reduce__(&self) -> PyResult<()> { - Err($crate::errors::SerializationError::new_err( - "ChannelEventCollector is not serializable; drain events first.", - )) - } - } - - // ============================================================ - // PhysicsWorld — full snapshot / restore + pickle. - // - // Serialized payload is a tuple of the inner rapier values: - // (bodies, colliders, impulse_joints, multibody_joints, - // broad_phase, narrow_phase, islands, ccd_solver, - // integration_parameters, gravity) - // - // `event_handler`, `physics_hooks`, `event_error_policy` and - // `auto_update_query` are NOT serialized — restored worlds are - // returned with `event_handler = None`, `physics_hooks = None`, - // `event_error_policy = "defer"`, `auto_update_query = false`. - // ============================================================ - - // Borrow-aware shared serialization payload. - // Owned form used for deserialization; borrowed form used for - // serialization to avoid cloning huge component sets. - type _PhysicsWorldOwned = ( - rapier::dynamics::RigidBodySet, - rapier::geometry::ColliderSet, - rapier::dynamics::ImpulseJointSet, - rapier::dynamics::MultibodyJointSet, - rapier::geometry::BroadPhaseBvh, - rapier::geometry::NarrowPhase, - rapier::dynamics::IslandManager, - rapier::dynamics::CCDSolver, - rapier::dynamics::IntegrationParameters, - $crate::na::SVector, - ); - - #[pymethods] - impl PhysicsWorld { - /// Take a snapshot of the world's physical state. - /// - /// Returns a `bytes` blob containing every internal collection - /// (rigid bodies, colliders, joints, broad phase, narrow phase, - /// island manager, CCD solver, integration parameters, gravity). - /// - /// User-supplied `event_handler` / `physics_hooks` and the - /// `event_error_policy` are **NOT** serialized — re-attach them - /// after `restore`. - fn snapshot<'py>(&self, py: Python<'py>) -> PyResult> { - let bodies = self.bodies.borrow(py); - let colliders = self.colliders.borrow(py); - let ij = self.impulse_joints.borrow(py); - let mj = self.multibody_joints.borrow(py); - let bp = self.broad_phase.borrow(py); - let np = self.narrow_phase.borrow(py); - let islands = self.islands.borrow(py); - let ccd = self.ccd_solver.borrow(py); - let ip = self.integration_parameters.borrow(py); - let tup = ( - &bodies.0, - &colliders.0, - &ij.0, - &mj.0, - &bp.0, - &np.0, - &islands.0, - &ccd.0, - &ip.0, - &self.gravity.0, - ); - let payload = - $crate::bincode::serialize(&tup).map_err($crate::serde_io::bincode_err)?; - Ok($crate::serde_io::bytes_to_py( - py, - &$crate::serde_io::wrap_bincode(payload), - )) - } - - /// Re-create a `PhysicsWorld` from a snapshot. - #[staticmethod] - fn restore(py: Python<'_>, blob: &Bound<'_, PyBytes>) -> PyResult> { - let buf = blob.as_bytes(); - let body = $crate::serde_io::unwrap_bincode(buf)?; - let owned: _PhysicsWorldOwned = - $crate::bincode::deserialize(body).map_err($crate::serde_io::bincode_err)?; - let (bs, cs, ijs, mjs, bps, nps, isl, ccd, ip, g) = owned; - let bodies = Py::new(py, RigidBodySet(bs))?; - let colliders = Py::new(py, ColliderSet(cs))?; - let impulse_joints = Py::new(py, ImpulseJointSet(ijs))?; - let multibody_joints = Py::new(py, MultibodyJointSet(mjs))?; - let broad_phase = Py::new(py, BroadPhaseBvh(bps))?; - let narrow_phase = Py::new(py, NarrowPhase(nps))?; - let islands = Py::new(py, IslandManager(isl))?; - let ccd_solver = Py::new(py, CCDSolver(ccd))?; - let integration_parameters = Py::new(py, IntegrationParameters(ip))?; - let physics_pipeline = Py::new( - py, - PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()), - )?; - let query_pipeline = Py::new( - py, - QueryPipeline { - broad_phase: broad_phase.clone_ref(py), - narrow_phase: narrow_phase.clone_ref(py), - bodies: bodies.clone_ref(py), - colliders: colliders.clone_ref(py), - }, - )?; - Py::new( - py, - PhysicsWorld { - bodies, - colliders, - impulse_joints, - multibody_joints, - broad_phase, - narrow_phase, - islands, - ccd_solver, - integration_parameters, - physics_pipeline, - query_pipeline, - gravity: $crate::serde_io_make_vec!($dim, g), - event_handler: None, - physics_hooks: None, - auto_update_query: false, - event_error_policy: "defer".to_string(), - }, - ) - } - - /// JSON debug snapshot — slower & much larger but human-readable. - /// - /// Returns a JSON string with a self-describing envelope: - /// `{"_magic": "RPYS", "_version": N, "payload": }`. - fn snapshot_json(&self, py: Python<'_>) -> PyResult { - let bodies = self.bodies.borrow(py); - let colliders = self.colliders.borrow(py); - let ij = self.impulse_joints.borrow(py); - let mj = self.multibody_joints.borrow(py); - let bp = self.broad_phase.borrow(py); - let np = self.narrow_phase.borrow(py); - let islands = self.islands.borrow(py); - let ccd = self.ccd_solver.borrow(py); - let ip = self.integration_parameters.borrow(py); - let tup = ( - &bodies.0, - &colliders.0, - &ij.0, - &mj.0, - &bp.0, - &np.0, - &islands.0, - &ccd.0, - &ip.0, - &self.gravity.0, - ); - let payload = - $crate::serde_json::to_value(&tup).map_err($crate::serde_io::json_err)?; - let env = $crate::serde_io::wrap_json(payload); - $crate::serde_json::to_string(&env).map_err($crate::serde_io::json_err) - } - - /// Restore a world from a JSON snapshot. - #[staticmethod] - fn restore_json(py: Python<'_>, s: &str) -> PyResult> { - let env: $crate::serde_json::Value = - $crate::serde_json::from_str(s).map_err($crate::serde_io::json_err)?; - let payload = $crate::serde_io::unwrap_json(env)?; - let owned: _PhysicsWorldOwned = - $crate::serde_json::from_value(payload).map_err($crate::serde_io::json_err)?; - let (bs, cs, ijs, mjs, bps, nps, isl, ccd, ip, g) = owned; - let bodies = Py::new(py, RigidBodySet(bs))?; - let colliders = Py::new(py, ColliderSet(cs))?; - let impulse_joints = Py::new(py, ImpulseJointSet(ijs))?; - let multibody_joints = Py::new(py, MultibodyJointSet(mjs))?; - let broad_phase = Py::new(py, BroadPhaseBvh(bps))?; - let narrow_phase = Py::new(py, NarrowPhase(nps))?; - let islands = Py::new(py, IslandManager(isl))?; - let ccd_solver = Py::new(py, CCDSolver(ccd))?; - let integration_parameters = Py::new(py, IntegrationParameters(ip))?; - let physics_pipeline = Py::new( - py, - PhysicsPipeline(rapier::pipeline::PhysicsPipeline::new()), - )?; - let query_pipeline = Py::new( - py, - QueryPipeline { - broad_phase: broad_phase.clone_ref(py), - narrow_phase: narrow_phase.clone_ref(py), - bodies: bodies.clone_ref(py), - colliders: colliders.clone_ref(py), - }, - )?; - Py::new( - py, - PhysicsWorld { - bodies, - colliders, - impulse_joints, - multibody_joints, - broad_phase, - narrow_phase, - islands, - ccd_solver, - integration_parameters, - physics_pipeline, - query_pipeline, - gravity: $crate::serde_io_make_vec!($dim, g), - event_handler: None, - physics_hooks: None, - auto_update_query: false, - event_error_policy: "defer".to_string(), - }, - ) - } - - /// Pickle support. Returns `(PhysicsWorld.restore, (snapshot_bytes,))`. - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Py, (Bound<'py, PyBytes>,))> { - let blob = self.snapshot(py)?; - let ctor: Py = py - .get_type_bound::() - .getattr("restore")? - .unbind(); - Ok((ctor, (blob,))) - } - } - }; -} - -// 3D-only vehicle controller types. -#[doc(hidden)] -#[macro_export] -macro_rules! __define_serde_vehicle { - (3) => { - $crate::__serde_owned_value!( - DynamicRayCastVehicleController, - 0, - DynamicRayCastVehicleController, - "from_bytes" - ); - $crate::__serde_owned_value!(WheelTuning, 0, WheelTuning, "from_bytes"); - }; - (2) => {}; -} - -// Dim-specific joints (SphericalJoint in 3D, PinSlotJoint in 2D). -#[doc(hidden)] -#[macro_export] -macro_rules! __define_serde_dim_specific_joints { - (3) => { - $crate::__serde_owned_value!(SphericalJoint, 0, SphericalJoint, "from_bytes"); - $crate::__serde_owned_value!( - SphericalJointBuilder, - 0, - SphericalJointBuilder, - "from_bytes" - ); - }; - (2) => { - $crate::__serde_owned_value!(PinSlotJoint, 0, PinSlotJoint, "from_bytes"); - $crate::__serde_owned_value!(PinSlotJointBuilder, 0, PinSlotJointBuilder, "from_bytes"); - }; -} - -// Wrap a deserialized `na::SVector` into the dim-specific Vec -// type (`Vec2` or `Vec3`). -#[doc(hidden)] -#[macro_export] -macro_rules! serde_io_make_vec { - (2, $g:expr) => { - Vec2($g) - }; - (3, $g:expr) => { - Vec3($g) - }; -} - -// `body.angvel()` has different shapes in 2D (scalar) vs 3D (Vector). Used -// inside `RigidBodyBuilder::from_bytes`. Returned by value in both cases. -#[doc(hidden)] -#[macro_export] -macro_rules! __rb_builder_angvel_from_body { - (2, $b:ident, $body:ident) => { - $b = $b.angvel($body.angvel()); - }; - (3, $b:ident, $body:ident) => { - $b = $b.angvel($body.angvel()); - }; -} - -// Math types per dimension — Vec, Point, Rotation, Isometry. -#[doc(hidden)] -#[macro_export] -macro_rules! __define_serde_math { - (3) => { - $crate::__serde_owned_value!(Vec3, 0, Vec3, "from_bytes"); - $crate::__serde_owned_value!(Point3, 0, Point3, "from_bytes"); - $crate::__serde_owned_value!(Rotation3, 0, Rotation3, "from_bytes"); - $crate::__serde_owned_value!(Isometry3, 0, Isometry3, "from_bytes"); - }; - (2) => { - $crate::__serde_owned_value!(Vec2, 0, Vec2, "from_bytes"); - $crate::__serde_owned_value!(Point2, 0, Point2, "from_bytes"); - $crate::__serde_owned_value!(Rotation2, 0, Rotation2, "from_bytes"); - $crate::__serde_owned_value!(Isometry2, 0, Isometry2, "from_bytes"); - }; -} diff --git a/python/rapier-py-core/src/urdf.rs b/python/rapier-py-core/src/urdf.rs deleted file mode 100644 index d86491fa7..000000000 --- a/python/rapier-py-core/src/urdf.rs +++ /dev/null @@ -1,673 +0,0 @@ -//! URDF loader `#[pyclass]`-es for the Python bindings (3D, f32 only). -//! -//! The class definitions are emitted by the `__define_loaders_urdf!` macro, -//! invoked from `__define_loaders_3d!` in [`crate::loaders`]. Keeping them in -//! their own file mirrors the upstream `rapier3d-urdf` crate boundary; see -//! [`crate::loaders`] for the shared loader plumbing (mesh loader, dimension -//! gating, and the top-level `register_loaders`). - -/// Materialize the URDF loader `#[pyclass]` types and `register_loaders_urdf`. -#[doc(hidden)] -#[macro_export] -macro_rules! __define_loaders_urdf { - () => { - // `rapier3d-urdf` resolves mesh references against a base directory. - use std::path::Path as _StdPath; - - // ----- URDF options ------------------------------------------------ - - /// Bitflags controlling the multibody-joint URDF insertion path. - /// - /// Combine flags with the usual ``|`` and ``&`` operators. Pass - /// the result to - /// :meth:`UrdfRobot.insert_using_multibody_joints`. - #[pyclass(name = "UrdfMultibodyOptions", module = "rapier", frozen)] - #[derive(Clone, Copy, Debug, Default)] - pub struct UrdfMultibodyOptions(pub rapier3d_urdf::UrdfMultibodyOptions); - - #[pymethods] - impl UrdfMultibodyOptions { - /// Build from a raw bit pattern. - #[new] - #[pyo3(signature = (bits = 0))] - fn new(bits: u8) -> Self { - Self(rapier3d_urdf::UrdfMultibodyOptions::from_bits_truncate(bits)) - } - /// Return the empty flag set (no flags). - #[staticmethod] - fn empty() -> Self { Self(rapier3d_urdf::UrdfMultibodyOptions::empty()) } - /// Treat URDF joints as kinematic rather than dynamic. - #[classattr] - const JOINTS_ARE_KINEMATIC: UrdfMultibodyOptions = - UrdfMultibodyOptions(rapier3d_urdf::UrdfMultibodyOptions::JOINTS_ARE_KINEMATIC); - /// Disable self-collisions between links of the same robot. - #[classattr] - const DISABLE_SELF_CONTACTS: UrdfMultibodyOptions = - UrdfMultibodyOptions(rapier3d_urdf::UrdfMultibodyOptions::DISABLE_SELF_CONTACTS); - /// Raw bits as an unsigned int. - #[getter] - fn bits(&self) -> u8 { self.0.bits() } - /// Bitwise OR: union of two flag sets. - fn __or__(&self, other: &UrdfMultibodyOptions) -> Self { Self(self.0 | other.0) } - /// Bitwise AND: intersection of two flag sets. - fn __and__(&self, other: &UrdfMultibodyOptions) -> Self { Self(self.0 & other.0) } - /// Return ``UrdfMultibodyOptions(bits=0b...)`` repr. - fn __repr__(&self) -> String { - format!("UrdfMultibodyOptions(bits={:#06b})", self.0.bits()) - } - } - - /// Configuration for the URDF loader. - /// - /// Mirrors ``rapier3d_urdf::UrdfLoaderOptions``. Pass an instance - /// to :meth:`UrdfRobot.from_file` / :meth:`from_str` / - /// :meth:`from_robot` to influence which colliders are produced - /// and how the robot is grounded. - /// - /// :ivar create_colliders_from_collision_shapes: If ``True``, - /// materialize ```` blocks as colliders. - /// :ivar create_colliders_from_visual_shapes: If ``True``, also - /// materialize ```` blocks as colliders (off by default). - /// :ivar apply_imported_mass_props: If ``True``, use the URDF - /// ```` block as the body's mass properties. - /// :ivar enable_joint_collisions: If ``True``, do not disable - /// collisions between links joined by a URDF joint. - /// :ivar make_roots_fixed: If ``True``, the robot's root bodies - /// are created as ``Fixed`` instead of ``Dynamic``. - /// :ivar trimesh_flags: :class:`TriMeshFlags` applied to mesh - /// colliders. - /// :ivar mesh_converter: Optional :class:`MeshConverter` controlling - /// how every referenced mesh is turned into a collider shape. - /// Defaults to ``None`` (trimesh, using ``trimesh_flags``). Set - /// e.g. ``MeshConverter.Obb()`` to get cheap proxy shapes while - /// keeping the original mesh available as a visual override (see - /// :attr:`UrdfColliderHandle.visual`). - /// :ivar shift: Rigid transform applied to every body of the - /// robot on import (applied *after* ``scale``). - /// :ivar scale: Uniform scale applied to every length read from the - /// URDF (link positions, joint anchors, mesh sizes, primitive - /// shape sizes, inertial offsets, prismatic joint limits). Mass - /// and inertia tensors are left unchanged. Defaults to ``1.0``. - /// :ivar squeeze_empty_fixed_links: If ``True`` (default), URDF links - /// with no ````/````/```` connected - /// by fixed joints are removed and the chain spliced together, so - /// bodyless "frame-only" links (e.g. ``world``, ``*_tcp``) no - /// longer create mass-less rigid-bodies. - #[pyclass(name = "UrdfLoaderOptions", module = "rapier")] - #[derive(Clone)] - pub struct UrdfLoaderOptions { - #[pyo3(get, set)] pub create_colliders_from_collision_shapes: bool, - #[pyo3(get, set)] pub create_colliders_from_visual_shapes: bool, - #[pyo3(get, set)] pub apply_imported_mass_props: bool, - #[pyo3(get, set)] pub enable_joint_collisions: bool, - #[pyo3(get, set)] pub make_roots_fixed: bool, - #[pyo3(get, set)] pub trimesh_flags: TriMeshFlags, - #[pyo3(get, set)] pub mesh_converter: Option, - #[pyo3(get, set)] pub shift: Isometry3, - #[pyo3(get, set)] pub scale: Real, - #[pyo3(get, set)] pub squeeze_empty_fixed_links: bool, - /// Template collider applied to every imported collider before - /// its shape/material are filled in. ``None`` keeps the rapier - /// default (density 0). - #[pyo3(get, set)] pub collider_blueprint: Option, - /// Template rigid-body used for every imported link. ``None`` - /// keeps the rapier default (a dynamic body). - #[pyo3(get, set)] pub rigid_body_blueprint: Option, - } - - #[pymethods] - impl UrdfLoaderOptions { - /// Build a ``UrdfLoaderOptions`` with optional overrides. - /// - /// See the class docstring for the meaning of each parameter. - #[new] - #[pyo3(signature = ( - create_colliders_from_collision_shapes = true, - create_colliders_from_visual_shapes = false, - apply_imported_mass_props = true, - enable_joint_collisions = false, - make_roots_fixed = false, - trimesh_flags = None, - mesh_converter = None, - shift = None, - scale = 1.0, - squeeze_empty_fixed_links = true, - collider_blueprint = None, - rigid_body_blueprint = None, - ))] - #[allow(clippy::too_many_arguments)] - fn new( - create_colliders_from_collision_shapes: bool, - create_colliders_from_visual_shapes: bool, - apply_imported_mass_props: bool, - enable_joint_collisions: bool, - make_roots_fixed: bool, - trimesh_flags: Option, - mesh_converter: Option, - shift: Option, - scale: Real, - squeeze_empty_fixed_links: bool, - collider_blueprint: Option, - rigid_body_blueprint: Option, - ) -> Self { - let default = rapier3d_urdf::UrdfLoaderOptions::default(); - let identity_iso: $crate::na::Isometry = rapier::math::Pose::IDENTITY.into(); - Self { - create_colliders_from_collision_shapes, - create_colliders_from_visual_shapes, - apply_imported_mass_props, - enable_joint_collisions, - make_roots_fixed, - trimesh_flags: trimesh_flags.unwrap_or(TriMeshFlags(default.trimesh_flags)), - mesh_converter, - shift: shift.unwrap_or(Isometry3(identity_iso)), - scale, - squeeze_empty_fixed_links, - collider_blueprint, - rigid_body_blueprint, - } - } - - /// Return a debug string with the main loader-option booleans. - fn __repr__(&self) -> String { - format!( - "UrdfLoaderOptions(create_colliders_from_collision_shapes={}, create_colliders_from_visual_shapes={}, apply_imported_mass_props={}, enable_joint_collisions={}, make_roots_fixed={}, scale={})", - self.create_colliders_from_collision_shapes, - self.create_colliders_from_visual_shapes, - self.apply_imported_mass_props, - self.enable_joint_collisions, - self.make_roots_fixed, - self.scale, - ) - } - } - - impl UrdfLoaderOptions { - fn to_rapier(&self) -> rapier3d_urdf::UrdfLoaderOptions { - let mut o = rapier3d_urdf::UrdfLoaderOptions::default(); - o.create_colliders_from_collision_shapes = self.create_colliders_from_collision_shapes; - o.create_colliders_from_visual_shapes = self.create_colliders_from_visual_shapes; - o.apply_imported_mass_props = self.apply_imported_mass_props; - o.enable_joint_collisions = self.enable_joint_collisions; - o.make_roots_fixed = self.make_roots_fixed; - o.trimesh_flags = self.trimesh_flags.0; - o.mesh_converter = self.mesh_converter.as_ref().map(|m| m.0.clone()); - o.shift = self.shift.0.into(); - o.scale = self.scale; - o.squeeze_empty_fixed_links = self.squeeze_empty_fixed_links; - if let Some(cb) = &self.collider_blueprint { - o.collider_blueprint = cb.builder.clone(); - } - if let Some(rb) = &self.rigid_body_blueprint { - o.rigid_body_blueprint = rb.builder.clone(); - } - o - } - } - - // ----- URDF read-only views ---------------------------------------- - - /// Read-only view of a URDF ```` element. - /// - /// Wraps the raw ``urdf_rs::Link`` for inspection from Python. - /// Returned by :attr:`Robot.links`. - #[pyclass(name = "UrdfLink", module = "rapier")] - #[derive(Clone)] - pub struct UrdfLink { - pub raw: urdf_rs::Link, - } - - #[pymethods] - impl UrdfLink { - /// Name of the link as declared in the URDF. - #[getter] - fn name(&self) -> String { self.raw.name.clone() } - /// Number of ```` elements on the link. - #[getter] - fn n_visuals(&self) -> usize { self.raw.visual.len() } - /// Number of ```` elements on the link. - #[getter] - fn n_collisions(&self) -> usize { self.raw.collision.len() } - /// Mass from the link's ```` block. - #[getter] - fn mass(&self) -> f64 { self.raw.inertial.mass.value } - /// Return the ``UrdfLink(...)`` repr. - fn __repr__(&self) -> String { - format!( - "UrdfLink(name={:?}, n_visuals={}, n_collisions={}, mass={})", - self.raw.name, - self.raw.visual.len(), - self.raw.collision.len(), - self.raw.inertial.mass.value, - ) - } - } - - /// Read-only view of a URDF ```` element. - /// - /// Wraps the raw ``urdf_rs::Joint`` for inspection from Python. - /// Returned by :attr:`Robot.joints`. - #[pyclass(name = "UrdfJoint", module = "rapier")] - #[derive(Clone)] - pub struct UrdfJoint { - pub raw: urdf_rs::Joint, - } - - #[pymethods] - impl UrdfJoint { - /// Joint name as declared in the URDF. - #[getter] - fn name(&self) -> String { self.raw.name.clone() } - /// Joint kind as a debug string (``"Revolute"``, ``"Fixed"``, ...). - #[getter] - fn joint_type(&self) -> String { format!("{:?}", self.raw.joint_type) } - /// Name of the parent link. - #[getter] - fn parent(&self) -> String { self.raw.parent.link.clone() } - /// Name of the child link. - #[getter] - fn child(&self) -> String { self.raw.child.link.clone() } - /// Axis vector ``[x, y, z]`` in the parent link's frame. - #[getter] - fn axis(&self) -> Vec { self.raw.axis.xyz.0.to_vec() } - /// Return the ``UrdfJoint(...)`` repr. - fn __repr__(&self) -> String { - format!( - "UrdfJoint(name={:?}, type={:?}, parent={:?}, child={:?})", - self.raw.name, self.raw.joint_type, self.raw.parent.link, self.raw.child.link, - ) - } - } - - /// Raw URDF document view (links + joints, pre-simulation). - /// - /// Returned by :meth:`UrdfRobot.from_file` / :meth:`from_str` - /// alongside the simulation-ready :class:`UrdfRobot`. Use this to - /// inspect link / joint names before insertion, or to re-import the - /// same source via :meth:`UrdfRobot.from_robot`. - #[pyclass(name = "Robot", module = "rapier")] - #[derive(Clone)] - pub struct UrdfRobotSource { - pub raw: urdf_rs::Robot, - } - - #[pymethods] - impl UrdfRobotSource { - /// Robot name from the URDF ```` attribute. - #[getter] - fn name(&self) -> String { self.raw.name.clone() } - /// List of :class:`UrdfLink` views, in document order. - #[getter] - fn links(&self) -> Vec { - self.raw.links.iter().map(|l| UrdfLink { raw: l.clone() }).collect() - } - /// List of :class:`UrdfJoint` views, in document order. - #[getter] - fn joints(&self) -> Vec { - self.raw.joints.iter().map(|j| UrdfJoint { raw: j.clone() }).collect() - } - /// Return the ``Robot(...)`` repr. - fn __repr__(&self) -> String { - format!( - "Robot(name={:?}, n_links={}, n_joints={})", - self.raw.name, self.raw.links.len(), self.raw.joints.len(), - ) - } - } - - // ----- URDF handles ------------------------------------------------ - - /// Visual mesh override paired with a URDF collider. - /// - /// Populated by the loader only when a non-default - /// :attr:`UrdfLoaderOptions.mesh_converter` (e.g. - /// ``MeshConverter.Obb()``) replaced the source mesh with a cheap - /// proxy collider — this keeps the original high-resolution mesh - /// available for rendering. - /// - /// :ivar shape: The :class:`SharedShape` representing the visual mesh. - /// :ivar local_pose: Pose of the visual mesh in the collider's local - /// frame, as an :class:`Isometry3`. - #[pyclass(name = "UrdfVisual", module = "rapier")] - #[derive(Clone)] - pub struct UrdfVisual { - #[pyo3(get)] pub shape: SharedShape, - #[pyo3(get)] pub local_pose: Isometry3, - } - - /// Handle of one collider inserted from a URDF link. - /// - /// :ivar handle: Underlying :class:`ColliderHandle`. - /// :ivar visual: Optional :class:`UrdfVisual` mesh override (``None`` - /// unless a proxy :attr:`UrdfLoaderOptions.mesh_converter` was used). - #[pyclass(name = "UrdfColliderHandle", module = "rapier", frozen)] - #[derive(Clone, Debug)] - pub struct UrdfColliderHandle { - #[pyo3(get)] pub handle: ColliderHandle, - pub visual: Option, - } - - #[pymethods] - impl UrdfColliderHandle { - /// Optional visual mesh override for this collider, or ``None``. - #[getter] - fn visual(&self) -> Option { - self.visual.as_ref().map(|v| UrdfVisual { - shape: SharedShape(v.shape.clone()), - local_pose: Isometry3(v.local_pose.into()), - }) - } - } - - /// Handle of one URDF link (its rigid body plus its colliders). - /// - /// :ivar body: :class:`RigidBodyHandle` for the link's body. - /// :ivar colliders: List of :class:`UrdfColliderHandle` attached to it. - #[pyclass(name = "UrdfLinkHandle", module = "rapier", frozen)] - #[derive(Clone, Debug)] - pub struct UrdfLinkHandle { - #[pyo3(get)] pub body: RigidBodyHandle, - #[pyo3(get)] pub colliders: Vec, - } - - /// Handle of one URDF joint after insertion. - /// - /// :ivar joint: Either an :class:`ImpulseJointHandle` (when the - /// impulse-joint insertion path was used) or - /// ``Optional[MultibodyJointHandle]`` (multibody path; ``None`` - /// for collapsed fixed joints). - /// :ivar link1: Parent link's :class:`RigidBodyHandle`. - /// :ivar link2: Child link's :class:`RigidBodyHandle`. - #[pyclass(name = "UrdfJointHandle", module = "rapier")] - pub struct UrdfJointHandle { - #[pyo3(get)] pub joint: $crate::pyo3::PyObject, - #[pyo3(get)] pub link1: RigidBodyHandle, - #[pyo3(get)] pub link2: RigidBodyHandle, - } - - /// Aggregate handle set returned by URDF insertion functions. - /// - /// :ivar links: One :class:`UrdfLinkHandle` per URDF link. - /// :ivar joints: One :class:`UrdfJointHandle` per URDF joint. - #[pyclass(name = "UrdfRobotHandles", module = "rapier")] - pub struct UrdfRobotHandles { - #[pyo3(get)] pub links: Vec, - #[pyo3(get)] pub joints: Vec<$crate::pyo3::Py>, - } - - // ----- UrdfRobot --------------------------------------------------- - - /// A URDF robot, ready to be inserted into the simulation. - /// - /// Build one via :meth:`from_file`, :meth:`from_str`, or - /// :meth:`from_robot`, optionally transform the whole robot with - /// :meth:`append_transform`, then commit it to the world via either - /// :meth:`insert_using_impulse_joints` (full 6-DOF joints with - /// constraints) or :meth:`insert_using_multibody_joints` - /// (reduced-coordinate articulation). - /// - /// The insertion methods *consume* the robot — subsequent calls - /// raise :class:`UrdfError`. - #[pyclass(name = "UrdfRobot", module = "rapier", unsendable)] - pub struct UrdfRobot { - pub inner: Option, - } - - #[pymethods] - impl UrdfRobot { - /// Parse a URDF file and return ``(UrdfRobot, Robot)``. - /// - /// Mesh references inside the URDF are resolved relative to - /// ``mesh_dir`` (default: the URDF file's parent directory). - /// - /// :param path: Path to the ``.urdf`` file on disk. - /// :param options: Optional :class:`UrdfLoaderOptions`. - /// :param mesh_dir: Directory used to resolve mesh refs. - /// :returns: ``(UrdfRobot, Robot)`` tuple — sim-ready robot - /// plus the raw document view. - /// :raises UrdfError: if the file can't be opened or parsed. - #[staticmethod] - #[pyo3(signature = (path, options=None, mesh_dir=None))] - fn from_file( - path: &str, - options: Option, - mesh_dir: Option<&str>, - ) -> $crate::pyo3::PyResult<(UrdfRobot, UrdfRobotSource)> { - let opts = options - .map(|o| o.to_rapier()) - .unwrap_or_else(rapier3d_urdf::UrdfLoaderOptions::default); - let mesh_dir = mesh_dir.map(_StdPath::new); - let (robot, raw) = rapier3d_urdf::UrdfRobot::from_file(path, opts, mesh_dir) - .map_err(|e| $crate::errors::UrdfError::new_err(format!("{e}")))?; - Ok((UrdfRobot { inner: Some(robot) }, UrdfRobotSource { raw })) - } - - /// Parse a URDF XML string and return ``(UrdfRobot, Robot)``. - /// - /// Equivalent to :meth:`from_file` but the source XML is - /// provided in memory. Mesh references are resolved relative - /// to ``mesh_dir`` (default: the current working directory). - /// - /// :param xml: Full URDF XML document. - /// :param options: Optional :class:`UrdfLoaderOptions`. - /// :param mesh_dir: Directory used to resolve mesh refs. - /// :returns: ``(UrdfRobot, Robot)`` tuple. - /// :raises UrdfError: if the XML can't be parsed. - #[staticmethod] - #[pyo3(signature = (xml, options=None, mesh_dir=None))] - fn from_str( - xml: &str, - options: Option, - mesh_dir: Option<&str>, - ) -> $crate::pyo3::PyResult<(UrdfRobot, UrdfRobotSource)> { - let opts = options - .map(|o| o.to_rapier()) - .unwrap_or_else(rapier3d_urdf::UrdfLoaderOptions::default); - let mesh_dir = mesh_dir - .map(_StdPath::new) - .unwrap_or_else(|| _StdPath::new(".")); - let (robot, raw) = rapier3d_urdf::UrdfRobot::from_str(xml, opts, mesh_dir) - .map_err(|e| $crate::errors::UrdfError::new_err(format!("{e}")))?; - Ok((UrdfRobot { inner: Some(robot) }, UrdfRobotSource { raw })) - } - - /// Build a sim-ready :class:`UrdfRobot` from a previously - /// parsed :class:`Robot` view. - /// - /// Lets you re-import the same document with different loader - /// options without re-parsing. - /// - /// :param robot: Source :class:`Robot` document view. - /// :param options: Optional :class:`UrdfLoaderOptions`. - /// :param mesh_dir: Directory used to resolve mesh refs. - #[staticmethod] - #[pyo3(signature = (robot, options=None, mesh_dir=None))] - fn from_robot( - robot: &UrdfRobotSource, - options: Option, - mesh_dir: Option<&str>, - ) -> $crate::pyo3::PyResult { - let opts = options - .map(|o| o.to_rapier()) - .unwrap_or_else(rapier3d_urdf::UrdfLoaderOptions::default); - let mesh_dir = mesh_dir - .map(_StdPath::new) - .unwrap_or_else(|| _StdPath::new(".")); - let r = rapier3d_urdf::UrdfRobot::from_robot(&robot.raw, opts, mesh_dir); - Ok(UrdfRobot { inner: Some(r) }) - } - - /// Number of links waiting to be inserted. - /// - /// :raises UrdfError: if this robot has already been consumed. - #[getter] - fn n_links(&self) -> $crate::pyo3::PyResult { - self.inner.as_ref() - .map(|r| r.links.len()) - .ok_or_else(|| $crate::errors::UrdfError::new_err("UrdfRobot was already consumed")) - } - - /// Number of joints waiting to be inserted. - /// - /// :raises UrdfError: if this robot has already been consumed. - #[getter] - fn n_joints(&self) -> $crate::pyo3::PyResult { - self.inner.as_ref() - .map(|r| r.joints.len()) - .ok_or_else(|| $crate::errors::UrdfError::new_err("UrdfRobot was already consumed")) - } - - /// Apply a rigid transform to every body of the robot. - /// - /// Mutates the robot in place. World-anchored joints are kept - /// satisfied across the transform. - /// - /// :param iso: World-space :class:`Isometry3` to prepend to - /// every body pose. - /// :raises UrdfError: if this robot has already been consumed. - fn append_transform(&mut self, iso: Isometry3) -> $crate::pyo3::PyResult<()> { - let robot = self.inner.as_mut().ok_or_else(|| { - $crate::errors::UrdfError::new_err("UrdfRobot was already consumed") - })?; - let pose: rapier::math::Pose = iso.0.into(); - robot.append_transform(&pose); - Ok(()) - } - - /// Insert the robot into the world using *impulse joints*. - /// - /// Each URDF joint becomes a full 6-DOF impulse joint with - /// position/velocity constraints. Compared to the multibody - /// path this is simpler conceptually but typically slower and - /// less stable for long kinematic chains. - /// - /// This call **consumes** the :class:`UrdfRobot`. - /// - /// :param bodies: :class:`RigidBodySet` to insert into. - /// :param colliders: :class:`ColliderSet` to insert into. - /// :param joints: :class:`ImpulseJointSet` to insert into. - /// :returns: :class:`UrdfRobotHandles` with all created handles. - /// :raises UrdfError: if this robot has already been consumed. - fn insert_using_impulse_joints( - &mut self, - py: $crate::pyo3::Python<'_>, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - joints: &mut ImpulseJointSet, - ) -> $crate::pyo3::PyResult<$crate::pyo3::Py> { - use $crate::pyo3::IntoPy; - let robot = self.inner.take().ok_or_else(|| { - $crate::errors::UrdfError::new_err("UrdfRobot was already consumed") - })?; - let handles = robot.insert_using_impulse_joints( - &mut bodies.0, - &mut colliders.0, - &mut joints.0, - ); - let links: Vec = handles.links.into_iter().map(|lh| { - let cols = lh.colliders.into_iter().map(|h| UrdfColliderHandle { - handle: ColliderHandle(h.handle), - visual: h.visual, - }).collect(); - UrdfLinkHandle { - body: RigidBodyHandle(lh.body), - colliders: cols, - } - }).collect(); - let joints_handles: Vec<$crate::pyo3::Py> = handles.joints.into_iter().map(|jh| { - let joint_py = ImpulseJointHandle(jh.joint).into_py(py); - $crate::pyo3::Py::new(py, UrdfJointHandle { - joint: joint_py, - link1: RigidBodyHandle(jh.link1), - link2: RigidBodyHandle(jh.link2), - }).expect("alloc UrdfJointHandle") - }).collect(); - $crate::pyo3::Py::new(py, UrdfRobotHandles { links, joints: joints_handles }) - } - - /// Insert the robot into the world using *multibody joints*. - /// - /// Builds a reduced-coordinate articulation: each link's pose - /// is implicitly constrained by its joint, eliminating most - /// drift on long kinematic chains. Recommended for robotic - /// arms, humanoids, and other branched mechanisms. - /// - /// This call **consumes** the :class:`UrdfRobot`. - /// - /// :param bodies: :class:`RigidBodySet` to insert into. - /// :param colliders: :class:`ColliderSet` to insert into. - /// :param multibody_joints: :class:`MultibodyJointSet` to insert into. - /// :param options: Optional :class:`UrdfMultibodyOptions` flags. - /// :returns: :class:`UrdfRobotHandles` with all created handles. - /// :raises UrdfError: if this robot has already been consumed. - #[pyo3(signature = (bodies, colliders, multibody_joints, options=None))] - fn insert_using_multibody_joints( - &mut self, - py: $crate::pyo3::Python<'_>, - bodies: &mut RigidBodySet, - colliders: &mut ColliderSet, - multibody_joints: &mut MultibodyJointSet, - options: Option, - ) -> $crate::pyo3::PyResult<$crate::pyo3::Py> { - use $crate::pyo3::IntoPy; - let robot = self.inner.take().ok_or_else(|| { - $crate::errors::UrdfError::new_err("UrdfRobot was already consumed") - })?; - let opts = options.map(|o| o.0).unwrap_or_default(); - let handles = robot.insert_using_multibody_joints( - &mut bodies.0, - &mut colliders.0, - &mut multibody_joints.0, - opts, - ); - let links: Vec = handles.links.into_iter().map(|lh| { - let cols = lh.colliders.into_iter().map(|h| UrdfColliderHandle { - handle: ColliderHandle(h.handle), - visual: h.visual, - }).collect(); - UrdfLinkHandle { - body: RigidBodyHandle(lh.body), - colliders: cols, - } - }).collect(); - let joints_handles: Vec<$crate::pyo3::Py> = handles.joints.into_iter().map(|jh| { - let joint_py = jh.joint.map(MultibodyJointHandle).into_py(py); - $crate::pyo3::Py::new(py, UrdfJointHandle { - joint: joint_py, - link1: RigidBodyHandle(jh.link1), - link2: RigidBodyHandle(jh.link2), - }).expect("alloc UrdfJointHandle") - }).collect(); - $crate::pyo3::Py::new(py, UrdfRobotHandles { links, joints: joints_handles }) - } - - /// Return the ``UrdfRobot(...)`` repr (or ``"UrdfRobot(consumed)"`` - /// after insertion). - fn __repr__(&self) -> String { - match &self.inner { - Some(r) => format!("UrdfRobot(n_links={}, n_joints={})", r.links.len(), r.joints.len()), - None => "UrdfRobot(consumed)".to_string(), - } - } - } - - /// Register the URDF loader `#[pyclass]`-es into `m`. - pub fn register_loaders_urdf( - m: &$crate::pyo3::Bound<'_, $crate::pyo3::types::PyModule>, - ) -> $crate::pyo3::PyResult<()> { - use $crate::pyo3::prelude::*; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) - } - }; -}