From 291cb3404faa16ccae13642a89eb9a4a69becbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Thu, 24 Sep 2026 17:39:17 +0200 Subject: [PATCH 1/3] feat(c): add missing bindings --- c/README.md | 6 + c/doxygen/reference.dox | 3 +- c/examples/RapierNative.cs | 4 +- c/examples/falling_ball.c | 2 +- c/include/rapier.h | 4604 ++++++++++++++++++-- c/include/rapier.hpp | 2 +- c/include/rapier_math.h | 23 +- c/rapier3d-ffi/Cargo.toml | 3 +- c/src/array_views.rs | 53 + c/src/config_data.rs | 54 +- c/src/control.rs | 369 +- c/src/descriptors.rs | 15 +- c/src/dynamics.rs | 52 + c/src/extra.rs | 82 +- c/src/geometry.rs | 75 +- c/src/geometry_views.rs | 29 + c/src/handle_access.rs | 371 +- c/src/handle_world.rs | 4 + c/src/joint_access.rs | 4 +- c/src/joint_extras.rs | 338 ++ c/src/joints.rs | 14 +- c/src/lib.rs | 14 + c/src/objects.rs | 51 +- c/src/owner_handle_tests.rs | 4 +- c/src/pipeline.rs | 201 +- c/src/queries_extras.rs | 1370 ++++++ c/src/read_access.rs | 203 + c/src/render.rs | 40 +- c/src/robotics.rs | 464 +- c/src/scoped_access.rs | 47 +- c/src/shape_desc.rs | 21 + c/src/soft_body.rs | 40 +- c/src/soft_desc.rs | 196 +- c/src/soft_extras.rs | 1035 +++++ c/src/soft_extras_tests.rs | 430 ++ c/src/soft_recipes.rs | 45 + c/src/types.rs | 83 +- c/src/world_extras.rs | 648 +++ c/src/world_queries.rs | 13 +- c/src/world_tests.rs | 8 +- c/testbed/examples2d/utils/character.h | 2 +- c/testbed/examples3d/utils/character.h | 2 +- c/testbed/examples3d/vehicle_controller3.c | 8 +- c/testbed/grab.c | 14 +- c/testbed/gui.c | 2 +- c/testbed/tests/grab.c | 4 +- c/tests/handles.c | 4 +- c/tests/integration.c | 16 +- c/tools/generate-header.py | 2 +- c/tools/test-native.py | 2 +- 50 files changed, 10404 insertions(+), 672 deletions(-) create mode 100644 c/src/joint_extras.rs create mode 100644 c/src/queries_extras.rs create mode 100644 c/src/soft_extras.rs create mode 100644 c/src/soft_extras_tests.rs create mode 100644 c/src/world_extras.rs diff --git a/c/README.md b/c/README.md index 8efdd7140..51368c363 100644 --- a/c/README.md +++ b/c/README.md @@ -50,6 +50,12 @@ Install each dimension/precision configuration to a separate prefix. When shippi a shared-library build, include the library and configure its runtime search path (on Windows, place the DLL beside your executable). +Without CMake, define `RAPIER_DIM2`/`RAPIER_DIM3`, `RAPIER_F32`/`RAPIER_F64`, and +`RAPIER_FEM`, `RAPIER_ROBOTICS`, `RAPIER_PARALLEL` for the enabled library features +yourself. Call `r3CheckAbi` (or `r2CheckAbi`) at startup, as in the +[C example](examples/falling_ball.c): it fails when these defines change a structure +layout of the linked library. + ## Run the testbed ```sh diff --git a/c/doxygen/reference.dox b/c/doxygen/reference.dox index adcbae312..2a0d109cd 100644 --- a/c/doxygen/reference.dox +++ b/c/doxygen/reference.dox @@ -13,7 +13,8 @@ RAII ownership; they do not change the C ABI. @section selection Build selection Define exactly one of `RAPIER_DIM2` / `RAPIER_DIM3` and one of `RAPIER_F32` / `RAPIER_F64` before including the headers. The defaults are 3D/f32. -Flags must match the linked library; CheckAbi checks version and core layouts. +Flags must match the linked library, including `RAPIER_FEM` and `RAPIER_ROBOTICS`. +CheckAbi, given ABI_FEATURES, checks the version, core layouts, and these feature defines. PodLayout exposes additional structure sizes for foreign-language bindings. Use uint32_t for ABI booleans, not C/C++ bool; inputs must be 0 or 1. diff --git a/c/examples/RapierNative.cs b/c/examples/RapierNative.cs index 0bcc4daa9..44b92e461 100644 --- a/c/examples/RapierNative.cs +++ b/c/examples/RapierNative.cs @@ -46,7 +46,7 @@ public sealed class World : SafeHandleZeroOrMinusOneIsInvalid public World() : base(true) { } protected override bool ReleaseHandle() { return r3FreeWorld(handle) == 0; } } - [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3CheckAbi(uint version,uint dimension,UIntPtr realSize,UIntPtr vectorSize,UIntPtr poseSize); + [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3CheckAbi(uint version,uint dimension,UIntPtr realSize,UIntPtr vectorSize,UIntPtr poseSize,uint features); [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern IntPtr r3LastError(); [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern uint r3LastStatus(); [DllImport(Library, CallingConvention=CallingConvention.Cdecl, ExactSpelling=true)] static extern World r3NewWorld(); @@ -64,7 +64,7 @@ static void Check(uint status) // The native library must be installed for the process architecture before invoking this method. public static Vector SimulateOneSecond() { - Check(r3CheckAbi(1,3,(UIntPtr)4,(UIntPtr)Marshal.SizeOf(),(UIntPtr)Marshal.SizeOf())); + Check(r3CheckAbi(1,3,(UIntPtr)4,(UIntPtr)Marshal.SizeOf(),(UIntPtr)Marshal.SizeOf(),0)); PodLayout layout = r3PodLayout(); if (layout.rigidBodyDesc != (UIntPtr)Marshal.SizeOf()) throw new InvalidOperationException("RigidBodyDesc layout does not match the native library."); diff --git a/c/examples/falling_ball.c b/c/examples/falling_ball.c index 741e6f95f..961a18b0e 100644 --- a/c/examples/falling_ball.c +++ b/c/examples/falling_ball.c @@ -14,7 +14,7 @@ int main(void) { CHECK(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), - sizeof(RAPIER_TYPE(Pose)))); + sizeof(RAPIER_TYPE(Pose)), RAPIER_CONST(ABI_FEATURES))); RAPIER_TYPE(World) *world = RAPIER_FN(NewWorld)(); CHECK(RAPIER_FN(LastStatus)()); diff --git a/c/include/rapier.h b/c/include/rapier.h index 8a8c476bf..22ed6d595 100644 --- a/c/include/rapier.h +++ b/c/include/rapier.h @@ -64,6 +64,23 @@ #if defined(RAPIER_DIM2) +#if defined(RAPIER_DIM3) +/** + * @ingroup worlds + * Friction model solving one Coulomb friction constraint per group of up to 4 contacts plus a + * twist constraint; faster but less accurate (default). + */ +#define R2_FRICTION_MODEL_SIMPLIFIED 0 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup worlds + * Friction model solving one Coulomb friction constraint per contact point. + */ +#define R2_FRICTION_MODEL_COULOMB 1 +#endif + #if defined(RAPIER_DIM2) /** * @ingroup joints @@ -140,6 +157,30 @@ */ #define R2_SOFT_DESC_VOLUMETRIC 9 +#if defined(RAPIER_DIM2) +/** + * @ingroup soft_bodies + * Soft-body selector: closed counter-clockwise polygon of particles preserving its area (2D). + */ +#define R2_SOFT_DESC_POLYGON 10 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup soft_bodies + * Soft-body selector: triangle mesh without cells, held by shape matching (2D). + */ +#define R2_SOFT_DESC_TRIMESH 11 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup soft_bodies + * Soft-body selector: cloth with separate warp, weft and shear softness (3D). + */ +#define R2_SOFT_DESC_CLOTH_ANISOTROPIC 12 +#endif + /** * @ingroup soft_bodies * Soft-body selector: binding skinned. @@ -268,6 +309,12 @@ */ #define R2_SHAPE_DESC_ROUND_CYLINDER 15 +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cone. + */ +#define R2_SHAPE_DESC_ROUND_CONE 16 + /** * @ingroup colliders * Mass density. @@ -352,6 +399,60 @@ */ #define R2_COMBINE_MAX 3 +/** + * @ingroup colliders + * Use the sum of the two material coefficients, clamped to [0, 1]. + */ +#define R2_COMBINE_CLAMPED_SUM 4 + +/** + * @ingroup colliders + * Use the geometric mean (square root of the product) of the two material coefficients. + */ +#define R2_COMBINE_GEOMETRIC_MEAN 5 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two dynamic bodies. + */ +#define R2_COLLISION_TYPES_DYNAMIC_DYNAMIC 1 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a dynamic and a kinematic body. + */ +#define R2_COLLISION_TYPES_DYNAMIC_KINEMATIC 12 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a dynamic and a fixed body (or a collider without parent). + */ +#define R2_COLLISION_TYPES_DYNAMIC_FIXED 2 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two kinematic bodies. + */ +#define R2_COLLISION_TYPES_KINEMATIC_KINEMATIC 52224 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a kinematic and a fixed body (or a collider without parent). + */ +#define R2_COLLISION_TYPES_KINEMATIC_FIXED 8704 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two fixed bodies (or colliders without parent). + */ +#define R2_COLLISION_TYPES_FIXED_FIXED 32 + +/** + * @ingroup colliders + * Default active collision types: dynamic-dynamic, dynamic-kinematic, and dynamic-fixed. + */ +#define R2_COLLISION_TYPES_DEFAULT 15 + /** * @ingroup colliders * Invoke the contact-pair filtering hook for this collider. @@ -538,6 +639,42 @@ */ #define R2_SOFT_SOLVER_FEM 1 +/** + * @ingroup soft_bodies + * Edge plastic flow (R2SoftBodyMaterial::edgePlasticFlow): both a squeeze and a stretch set. + */ +#define R2_SOFT_EDGE_PLASTIC_FLOW_BOTH 0 + +/** + * @ingroup soft_bodies + * Edge plastic flow: only a squeeze sets; a stretched edge springs back. + */ +#define R2_SOFT_EDGE_PLASTIC_FLOW_COMPRESSION 1 + +/** + * @ingroup soft_bodies + * Edge plastic flow: only a stretch sets; a squeezed edge springs back. + */ +#define R2_SOFT_EDGE_PLASTIC_FLOW_TENSION 2 + +/** + * @ingroup soft_bodies + * Overlap patch constraints (R2SoftRecoverySettings::overlapPatchConstraints): keep them. + */ +#define R2_SOFT_PATCH_CONSTRAINTS_KEEP 0 + +/** + * @ingroup soft_bodies + * Overlap patch constraints: stand them down inside the patch. + */ +#define R2_SOFT_PATCH_CONSTRAINTS_STAND_DOWN 1 + +/** + * @ingroup soft_bodies + * Overlap patch constraints: align them with the overlap normal. + */ +#define R2_SOFT_PATCH_CONSTRAINTS_ALONG_NORMAL 2 + /** * @ingroup joints * Joint axis index for translation along local X. @@ -750,34 +887,71 @@ /** * @ingroup joints - * Skip joints that would close a loop in the articulation. + * Do not insert MJCF equality constraints (loop closures) as impulse joints. MJCF only: URDF + * insertion rejects it. */ #define R2_MULTIBODY_SKIP_LOOP_CLOSURES 4 /** * @ingroup joints - * Do not import joint motors into the articulation. + * Do not import joint motors into the articulation. MJCF only: URDF insertion rejects it. */ #define R2_MULTIBODY_SKIP_JOINT_MOTORS 8 /** * @ingroup joints - * Do not import joint limits into the articulation. + * Do not import joint limits into the articulation. MJCF only: URDF insertion rejects it. */ #define R2_MULTIBODY_SKIP_JOINT_LIMITS 16 /** * @ingroup joints - * Do not import joint springs into the articulation. + * Do not import joint springs into the articulation. MJCF only: URDF insertion rejects it. */ #define R2_MULTIBODY_SKIP_JOINT_SPRINGS 32 +/** + * @ingroup shapes + * Compute the half-edge topology of the triangle mesh. + */ +#define R2_TRIMESH_HALF_EDGE_TOPOLOGY 1 + +/** + * @ingroup shapes + * Compute the connected components of the triangle mesh. + */ +#define R2_TRIMESH_CONNECTED_COMPONENTS 2 + +/** + * @ingroup shapes + * Delete the triangles breaking the half-edge topology. + */ +#define R2_TRIMESH_DELETE_BAD_TOPOLOGY_TRIANGLES 4 + +/** + * @ingroup shapes + * Treat the triangle mesh as oriented (outward normals) and compute its pseudo-normals. + */ +#define R2_TRIMESH_ORIENTED 8 + /** * @ingroup shapes * Merge triangle-mesh vertices with identical positions. */ #define R2_TRIMESH_MERGE_DUPLICATE_VERTICES 16 +/** + * @ingroup shapes + * Delete the triangles with a zero area. + */ +#define R2_TRIMESH_DELETE_DEGENERATE_TRIANGLES 32 + +/** + * @ingroup shapes + * Delete the triangles sharing their three vertices with another triangle. + */ +#define R2_TRIMESH_DELETE_DUPLICATE_TRIANGLES 64 + /** * @ingroup shapes * Correct contact normals at internal mesh edges; includes duplicate-vertex merging. @@ -802,6 +976,144 @@ */ #define R2_HEIGHTFIELD_FIX_INTERNAL_EDGES 1 +/** + * @ingroup controllers + * Controller axis bit: translation along X. + */ +#define R2_AXES_MASK_LIN_X 1 + +/** + * @ingroup controllers + * Controller axis bit: translation along Y. + */ +#define R2_AXES_MASK_LIN_Y 2 + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: translation along Z. + */ +#define R2_AXES_MASK_LIN_Z 4 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: rotation about X. + */ +#define R2_AXES_MASK_ANG_X 8 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: rotation about Y. + */ +#define R2_AXES_MASK_ANG_Y 16 +#endif + +/** + * @ingroup controllers + * Controller axis bit: rotation about Z (the only rotation axis in 2D). + */ +#define R2_AXES_MASK_ANG_Z 32 + +/** + * @ingroup errors + * ABI feature bit: RAPIER_FEM, which changes the layout of R2IntegrationParameters. + */ +#define R2_ABI_FEATURE_FEM 1 + +/** + * @ingroup errors + * ABI feature bit: RAPIER_ROBOTICS (3D, f32 only), which declares the URDF/MJCF API. + */ +#define R2_ABI_FEATURE_ROBOTICS 2 + +#if (defined(RAPIER_FEM) && defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup errors + * R2_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R2_ABI_FEATURES 3 +#endif + +#if (defined(RAPIER_FEM) && !(defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32))) +/** + * @ingroup errors + * R2_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R2_ABI_FEATURES 1 +#endif + +#if (!defined(RAPIER_FEM) && defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup errors + * R2_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R2_ABI_FEATURES 2 +#endif + +#if (!defined(RAPIER_FEM) && !(defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32))) +/** + * @ingroup errors + * R2_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R2_ABI_FEATURES 0 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Load each mesh as a triangle mesh, with the given trimesh flags. + */ +#define R2_MESH_CONVERTER_TRIMESH 0 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its oriented bounding box. + */ +#define R2_MESH_CONVERTER_OBB 1 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its axis-aligned bounding box. + */ +#define R2_MESH_CONVERTER_AABB 2 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its convex hull. + */ +#define R2_MESH_CONVERTER_CONVEX_HULL 3 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its convex decomposition. + */ +#define R2_MESH_CONVERTER_CONVEX_DECOMPOSITION 4 +#endif + +/** + * @ingroup events + * Collision event flag: at least one of the colliders was a sensor when the event fired. + */ +#define R2_COLLISION_EVENT_SENSOR 1 + +/** + * @ingroup events + * Collision event flag: the collision stopped because at least one collider was removed. + */ +#define R2_COLLISION_EVENT_REMOVED 2 + /** * Immutable owned byte buffer. Release with the matching FreeBytes function. * @ingroup worlds @@ -810,7 +1122,7 @@ typedef struct R2Bytes R2Bytes; /** * Borrowed native contact context. Valid only during its callback; never retain or free it. - * @ingroup events + * @ingroup callbacks */ typedef struct R2ContactModificationContext R2ContactModificationContext; @@ -823,8 +1135,9 @@ typedef struct R2DynamicRayCastVehicleController R2DynamicRayCastVehicleControll #endif /** - * Events accumulate until clear. Copying events never drains them, allowing two-call buffer - * sizing. + * Events accumulate across steps until r2EventCollector_Clear: reading them never drains the + * collector, allowing two-call buffer sizing. Optional callbacks also see each event during the + * step. * @ingroup events */ typedef struct R2EventCollector R2EventCollector; @@ -837,6 +1150,24 @@ typedef struct R2EventCollector R2EventCollector; */ typedef struct R2KinematicCharacterController R2KinematicCharacterController; +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Shapes loaded from a mesh file (STL, COLLADA or Wavefront OBJ), one per mesh of the file. + * Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R2LoadedMeshes R2LoadedMeshes; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned physics hooks applying the `` rules (excluded pairs, pair friction) of an + * inserted MJCF robot. Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R2MjcfContactHooks R2MjcfContactHooks; +#endif + #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) /** * Loaded MJCF robot and its visual/keyframe data. Release with the matching Free function. @@ -1045,7 +1376,7 @@ typedef struct R2SoftBodyMaterial { */ R2Real edgePlasticMax; /** - * Plastic flow direction: 0 both, 1 compression only, 2 tension only. + * Plastic flow direction: R2_SOFT_EDGE_PLASTIC_FLOW_BOTH, _COMPRESSION or _TENSION. */ uint32_t edgePlasticFlow; /** @@ -1152,8 +1483,8 @@ typedef struct R2SoftRecoverySettings { */ R2Real overlapConstraintPace; /** - * Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap - * normal. + * Per-point constraints inside overlap patches: R2_SOFT_PATCH_CONSTRAINTS_KEEP, _STAND_DOWN or + * _ALONG_NORMAL. */ uint32_t overlapPatchConstraints; /** @@ -1294,7 +1625,7 @@ typedef struct R2IntegrationParameters { */ size_t numSolverIterations; /** - * PGS iterations per solver substep. + * PGS iterations per solver substep; must be positive. */ size_t numInternalPgsIterations; /** @@ -1327,7 +1658,7 @@ typedef struct R2IntegrationParameters { R2Bool warmstartJoints; #if defined(RAPIER_DIM3) /** - * Friction model: 0 simplified, 1 Coulomb (3D only). + * Friction model of rigid-body contacts, R2_FRICTION_MODEL_* (3D only). */ uint32_t frictionModel; #endif @@ -1932,6 +2263,23 @@ typedef struct R2DihedralView { size_t count; } R2DihedralView; +/** + * Borrowed array of soft-body descriptions. count counts descriptions. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup soft_bodies + */ +typedef struct R2SoftBodyDescView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R2SoftBodyDesc *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R2SoftBodyDescView; + /** * Optional boolean override. When disabled, retain the recipe's native default. * @ingroup math @@ -2003,7 +2351,7 @@ typedef struct R2ShapeDesc { */ R2Real halfHeight; /** - * Rounding radius for a rounded shape. + * Rounding radius of a round cylinder or round cone (the round cuboid reads radius instead). */ R2Real borderRadius; /** @@ -2163,11 +2511,11 @@ typedef struct R2ColliderDesc { */ R2Real restitution; /** - * R2_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + * R2_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. */ uint32_t frictionCombineRule; /** - * R2_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + * R2_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. */ uint32_t restitutionCombineRule; /** @@ -2218,6 +2566,8 @@ typedef struct R2ColliderDesc { * for topology arrays are element counts (edges, triangles, or tetrahedra). * Nonempty topology overrides the generator's topology. Zero counts retain it. * Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. + * R2_SOFT_DESC_POLYGON reads positions; R2_SOFT_DESC_TRIMESH reads positions and cells (its + * triangles become edges and a boundary, not cells). * @ingroup soft_bodies */ typedef struct R2SoftBodyDesc { @@ -2241,6 +2591,24 @@ typedef struct R2SoftBodyDesc { * Cloth basis step along its second parameter axis. */ struct R2Vector dv; +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth edges along du (warp). + */ + struct R2SpringCoefficients warpSoftness; +#endif +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth edges along dv (weft). + */ + struct R2SpringCoefficients weftSoftness; +#endif +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth diagonal edges (shear). + */ + struct R2SpringCoefficients shearSoftness; +#endif /** * First recipe resolution; interpretation depends on kind. */ @@ -2334,11 +2702,23 @@ typedef struct R2SoftBodyDesc { */ R2SurfaceElementView skinIndices; /** - * Soft-body material coefficients. + * Borrowed descriptions merged into this body, their particles numbered after this one's in + * order. Each contributes its particles, masses, pinned particles and elements (after its own + * translation and total mass); every other setting comes from this description. Appended + * descriptions cannot append others nor have a skin. */ - struct R2SoftBodyMaterial material; + struct R2SoftBodyDescView appended; /** - * R2_SOFT_CELL_VOLUME, R2_SOFT_CELL_COROTATIONAL, or R2_SOFT_CELL_NEO_HOOKEAN. + * Borrowed structural edges added after appending (seams); indices count this body's particles + * then the appended ones. Their rest length is the current distance of their particles. + */ + struct R2EdgeView addedEdges; + /** + * Soft-body material coefficients. + */ + struct R2SoftBodyMaterial material; + /** + * R2_SOFT_CELL_VOLUME, R2_SOFT_CELL_COROTATIONAL, or R2_SOFT_CELL_NEO_HOOKEAN. */ uint32_t cellModel; /** @@ -2365,6 +2745,11 @@ typedef struct R2SoftBodyDesc { * Optional shape-matching override; disabled retains recipe defaults. */ struct R2OptionalBool shapeMatching; + /** + * Optional override of the ORIENTED flag of the generated collision surface (when disabled, a + * closed surface is oriented). Set it to false for a shell whose inner side holds bodies. + */ + struct R2OptionalBool oriented; /** * Whether self-collision is enabled. */ @@ -2841,7 +3226,8 @@ typedef struct R2PodLayout { */ typedef struct R2CharacterLength { /** - * Value used when enabled is 1. + * Nonnegative length: a fraction of the character shape height when relative is 1, a + * world-space length otherwise. */ R2Real value; /** @@ -2850,6 +3236,29 @@ typedef struct R2CharacterLength { R2Bool relative; } R2CharacterLength; +/** + * Copy of the automatic stepping settings. + * @ingroup controllers + */ +typedef struct R2CharacterAutostep { + /** + * Whether automatic stepping is enabled. + */ + R2Bool enabled; + /** + * Maximum height of the steps climbed automatically. + */ + struct R2CharacterLength max_height; + /** + * Minimum free width required on top of a step. + */ + struct R2CharacterLength min_width; + /** + * Whether the character can also step over dynamic bodies. + */ + R2Bool include_dynamic_bodies; +} R2CharacterAutostep; + /** * Allowed character motion and ground-contact state. * @ingroup controllers @@ -2927,6 +3336,34 @@ typedef struct R2PidGains { R2AngVector ang_kd; } R2PidGains; +/** + * Stateless proportional-derivative controller: a PID controller without integral term, stored as + * a plain value. Initialize with r2DefaultPdController. + * @ingroup controllers + */ +typedef struct R2PdController { + /** + * Linear proportional gain per axis. + */ + struct R2Vector lin_kp; + /** + * Linear derivative gain per axis. + */ + struct R2Vector lin_kd; + /** + * Angular proportional gain per axis. + */ + R2AngVector ang_kp; + /** + * Angular derivative gain per axis. + */ + R2AngVector ang_kd; + /** + * Controlled axes, a combination of R2_AXES_MASK_* bits. + */ + uint32_t axes; +} R2PdController; + /** * Linear and angular velocity correction computed by a controller. * @ingroup controllers @@ -3146,7 +3583,7 @@ typedef struct R2CollisionEvent { */ R2Bool started; /** - * Event flags: bit 0 sensor pair, bit 1 removed collider. + * Bitmask of R2_COLLISION_EVENT_SENSOR and R2_COLLISION_EVENT_REMOVED. */ uint32_t flags; } R2CollisionEvent; @@ -3181,7 +3618,8 @@ typedef struct R2ContactForceEvent { */ R2Real max_force_magnitude; /** - * 1 for a starting event, 0 for a stopping event. + * 1 for the first step the total force magnitude exceeds the threshold, 0 on the following + * steps while it stays above it. No event is emitted when the force drops below it. */ R2Bool started; } R2ContactForceEvent; @@ -3190,7 +3628,7 @@ typedef struct R2ContactForceEvent { * Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes * impulses. * For sensor intersections only, zero rejects and any positive value accepts. - * @ingroup math + * @ingroup callbacks */ typedef int32_t (RAPIER_CALL *R2PairFilter)(void *user_data, const struct R2ReadContext *read, @@ -3306,7 +3744,7 @@ typedef struct R2DebugLine { */ struct R2Vector b; /** - * RGBA color, four floats. + * HSLA color: hue in degrees, then saturation, lightness and alpha in [0, 1]. */ float color[4]; } R2DebugLine; @@ -3431,6 +3869,11 @@ typedef struct R2BuildFeatures { * Whether this library exposes Rapier's parallel execution and thread-pool APIs. */ R2Bool parallel; + /** + * Whether the library is built with enhanced-determinism: the simulation, and the math + * functions such as r2Sin, give bit-identical results on every platform. + */ + R2Bool enhanced_determinism; } R2BuildFeatures; /** @@ -3470,7 +3913,7 @@ typedef struct R2ContactPair { /** * Sensor intersection state for a collider pair. - * @ingroup math + * @ingroup events */ typedef struct R2IntersectionPair { /** @@ -3516,6 +3959,18 @@ typedef struct R2ContactPoint { * Normal impulse applied at this contact. */ R2Real impulse; +#if defined(RAPIER_DIM2) + /** + * Friction impulse along the tangent basis of the contact. + */ + R2Real tangent_impulse[1]; +#endif +#if defined(RAPIER_DIM3) + /** + * Friction impulses along the two tangent basis vectors of the contact. + */ + R2Real tangent_impulse[2]; +#endif } R2ContactPoint; #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) @@ -3821,6 +4276,335 @@ typedef struct R2VoxelQuery { R2Bool found; } R2VoxelQuery; +/** + * Impulses applied by an impulse joint during the last step, along the axes of its joint frame. + * @ingroup joints + */ +typedef struct R2JointImpulses { + /** + * Impulse applied along the locked translational axes. + */ + struct R2Vector linear; + /** + * Angular impulse applied along the locked rotational axes (a scalar in 2D). + */ + R2AngVector angular; + /** + * Impulse applied by the limit of each axis, in translation-then-rotation order. + */ + R2Real limits[R2_JOINT_DOF_COUNT]; + /** + * Impulse applied by the motor of each axis, in translation-then-rotation order. + */ + R2Real motors[R2_JOINT_DOF_COUNT]; +} R2JointImpulses; + +/** + * Optional shape-cast result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R2OptionalShapeCastHit { + /** + * Shape-cast impact details. + */ + struct R2ShapeCastHit hit; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalShapeCastHit; + +/** + * Optional point projection. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R2OptionalPointProjection { + /** + * Closest projection details. + */ + struct R2PointProjection projection; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalPointProjection; + +/** + * Rigid motion with constant linear and angular velocities. At time t, the shape at start is + * rotated by angvel * t around its local_center point, then translated by linvel * t. + * @ingroup queries + */ +typedef struct R2NonlinearRigidMotion { + /** + * World-space pose at time zero. + */ + struct R2Pose start; + /** + * Rotation center, in the local coordinates of the moving shape. + */ + struct R2Vector local_center; + /** + * World-space linear velocity. + */ + struct R2Vector linvel; + /** + * World-space angular velocity, in radians per second. + */ + R2AngVector angvel; +} R2NonlinearRigidMotion; + +/** + * Optional contact pair; check found before reading the pair. + * @ingroup events + */ +typedef struct R2OptionalContactPair { + /** + * Contact pair summary. + */ + struct R2ContactPair pair; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalContactPair; + +/** + * Optional intersection pair; check found before reading the pair. + * @ingroup events + */ +typedef struct R2OptionalIntersectionPair { + /** + * Intersection pair state. + */ + struct R2IntersectionPair pair; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R2Bool found; +} R2OptionalIntersectionPair; + +/** + * Geometric contact manifold of a contact pair: contacts sharing one normal. Local data follow the + * pair's own collider1/collider2 order (see r2ContactPair). + * @ingroup events + */ +typedef struct R2ContactManifold { + /** + * Contact normal in collider 1 local coordinates, pointing outward from it. + */ + struct R2Vector local_n1; + /** + * Contact normal in collider 2 local coordinates, pointing outward from it. + */ + struct R2Vector local_n2; + /** + * World-space contact normal, pointing from collider 1 toward collider 2. + */ + struct R2Vector normal; + /** + * Index of the subshape of collider 1 (for composite shapes), zero otherwise. + */ + uint32_t subshape1; + /** + * Index of the subshape of collider 2 (for composite shapes), zero otherwise. + */ + uint32_t subshape2; + /** + * Number of geometric contact points; see r2ContactPoints. + */ + size_t num_points; + /** + * Number of solver contacts; see r2SolverContacts. + */ + size_t num_solver_contacts; + /** + * Application data, persistent across steps and editable by contact-modification hooks. + */ + uint32_t user_data; +} R2ContactManifold; + +/** + * Contact seen by the constraint solver. Points are world-space, on each body's surface. + * @ingroup events + */ +typedef struct R2SolverContact { + /** + * World-space contact point on collider 1's body. + */ + struct R2Vector point1; + /** + * World-space contact point on collider 2's body. + */ + struct R2Vector point2; + /** + * Signed separation along the normal, contact skins deducted; negative means penetration. + */ + R2Real distance; + /** + * Desired world-space tangent relative velocity, e.g. for conveyor belts; zero by default. + */ + struct R2Vector tangent_velocity; +} R2SolverContact; + +/** + * Called during the step for each collision event, after it was added to the collector. contacts + * holds the geometric contacts of the pair at that time (none for sensors), in the event's + * collider order; it is borrowed for this call only. + * @ingroup events + */ +typedef void (RAPIER_CALL *R2CollisionEventCallback)(void *user_data, + const struct R2ReadContext *read, + const struct R2CollisionEvent *event, + const struct R2ContactPoint *contacts, + size_t contact_count); + +/** + * Called during the step for each contact-force event, after it was added to the collector. + * @ingroup events + */ +typedef void (RAPIER_CALL *R2ContactForceEventCallback)(void *user_data, + const struct R2ReadContext *read, + const struct R2ContactForceEvent *event); + +/** + * Callbacks invoked while stepping, in addition to collecting the events. They follow the + * R2PhysicsHooks rules: never unwind or retain arguments, read through the ReadContext, never + * mutate the world, and be safe for concurrent invocation in parallel builds. NULL callbacks are + * skipped. + * @ingroup events + */ +typedef struct R2EventCallbacks { + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; + /** + * Optional collision start/stop callback. + */ + R2CollisionEventCallback collision_event; + /** + * Optional contact-force callback. + */ + R2ContactForceEventCallback contact_force_event; +} R2EventCallbacks; + +/** + * Debug-render colors and sizes. Colors are HSLA: hue in degrees, then saturation, lightness and + * alpha in [0, 1]; multipliers scale each component. Initialize with + * r2DefaultDebugRenderStyle. + * @ingroup events + */ +typedef struct R2DebugRenderStyle { + /** + * Positive number of subdivisions approximating curved shapes. + */ + uint32_t subdivisions; + /** + * Positive number of subdivisions approximating the borders of round shapes. + */ + uint32_t border_subdivisions; + /** + * Color of colliders attached to dynamic bodies. + */ + float collider_dynamic_color[4]; + /** + * Color of colliders attached to fixed bodies. + */ + float collider_fixed_color[4]; + /** + * Color of colliders attached to kinematic bodies. + */ + float collider_kinematic_color[4]; + /** + * Color of colliders without a parent body. + */ + float collider_parentless_color[4]; + /** + * Color of the lines from a body's center of mass to its impulse-joint anchors. + */ + float impulse_joint_anchor_color[4]; + /** + * Color of the line between the two anchors of an impulse joint. + */ + float impulse_joint_separation_color[4]; + /** + * Color of the lines from a body's center of mass to its multibody-joint anchors. + */ + float multibody_joint_anchor_color[4]; + /** + * Color of the line between the two anchors of a multibody joint. + */ + float multibody_joint_separation_color[4]; + /** + * Color multiplier for entities of sleeping bodies. + */ + float sleep_color_multiplier[4]; + /** + * Color multiplier for entities of awake bodies eligible for sleep. + */ + float sleep_eligible_color_multiplier[4]; + /** + * Color multiplier for entities of disabled bodies. + */ + float disabled_color_multiplier[4]; + /** + * Nonnegative length of the rendered body axes. + */ + R2Real rigid_body_axes_length; + /** + * Color of the segments joining the two points of a contact. + */ + float contact_depth_color[4]; + /** + * Color of the contact normals. + */ + float contact_normal_color[4]; + /** + * Nonnegative length of the contact normals. + */ + R2Real contact_normal_length; + /** + * Color of soft-body elements. + */ + float soft_body_element_color[4]; + /** + * Color of unloaded soft-body elements when coloring them by load. + */ + float soft_body_slack_color[4]; + /** + * Color of soft-body elements at their tear threshold when coloring them by load. + */ + float soft_body_loaded_color[4]; + /** + * Color of the soft-body cluster frames. + */ + float soft_body_frame_color[4]; + /** + * Color of the collider bounding boxes. + */ + float collider_aabb_color[4]; + /** + * Color of the vertex pseudo-normals of triangle meshes and polylines. + */ + float vertex_pseudo_normal_color[4]; + /** + * Color of the edge pseudo-normals of triangle meshes (3D only). + */ + float edge_pseudo_normal_color[4]; + /** + * Nonnegative length of the pseudo-normals. + */ + R2Real pseudo_normal_length; + /** + * Color of the normals of soft-body volume contacts. + */ + float volume_contact_normal_color[4]; + /** + * Color of the volume gradients drawn at the particles of a volume constraint. + */ + float volume_gradient_color[4]; +} R2DebugRenderStyle; + /** * @ingroup errors * Operation succeeded. @@ -4325,34 +5109,74 @@ R2Status RAPIER_CALL r2KinematicCharacterController_SetSnapToGround(struct R2Kin struct R2CharacterLength distance); /** - * Computes movement without moving any collider. Use the returned translation to set the character - * target. - * NULL query options use the default filter. Query state reflects the latest Step or - * DetectCollisions call. + * Return the normalized up direction. * @ingroup controllers */ RAPIER_API -struct R2CharacterMovement RAPIER_CALL r2KinematicCharacterController_MoveShape(const struct R2World *world, - const struct R2QueryOptions *options, - struct R2KinematicCharacterController *controller, - R2Real dt, - const R2SharedShape *shape, - struct R2Pose pose, - struct R2Vector desired_translation); +struct R2Vector RAPIER_CALL r2KinematicCharacterController_Up(const struct R2KinematicCharacterController *controller); /** - * Copy collisions recorded by the most recent MoveShape call. - * @see @ref output_buffers + * Return the collision separation margin. * @ingroup controllers */ RAPIER_API -size_t RAPIER_CALL r2KinematicCharacterController_Collisions(const struct R2KinematicCharacterController *controller, +struct R2CharacterLength RAPIER_CALL r2KinematicCharacterController_Offset(const struct R2KinematicCharacterController *controller); + +/** + * Return the automatic stepping settings. When disabled, enabled is 0 and the other fields hold + * Rapier's defaults. + * @ingroup controllers + */ +RAPIER_API +struct R2CharacterAutostep RAPIER_CALL r2KinematicCharacterController_Autostep(const struct R2KinematicCharacterController *controller); + +/** + * Set the small distance by which sliding motion is pushed along hit normals to avoid getting stuck; + * it must be finite and nonnegative. Large values cause bumps when sliding on flat ground. + * @ingroup controllers + */ +RAPIER_API +R2Status RAPIER_CALL r2KinematicCharacterController_SetNormalNudgeFactor(struct R2KinematicCharacterController *controller, + R2Real value); + +/** + * Return the normal nudge factor set by SetNormalNudgeFactor. + * @ingroup controllers + */ +RAPIER_API +R2Real RAPIER_CALL r2KinematicCharacterController_NormalNudgeFactor(const struct R2KinematicCharacterController *controller); + +/** + * Computes movement without moving any collider. Use the returned translation to set the character + * target. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup controllers + */ +RAPIER_API +struct R2CharacterMovement RAPIER_CALL r2KinematicCharacterController_MoveShape(const struct R2World *world, + const struct R2QueryOptions *options, + struct R2KinematicCharacterController *controller, + R2Real dt, + const R2SharedShape *shape, + struct R2Pose pose, + struct R2Vector desired_translation); + +/** + * Copy collisions recorded by the most recent MoveShape call. + * @see @ref output_buffers + * @ingroup controllers + */ +RAPIER_API +size_t RAPIER_CALL r2KinematicCharacterController_Collisions(const struct R2KinematicCharacterController *controller, struct R2CharacterCollision *buffer, size_t capacity); /** - * Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and - * filter. + * Applies impulses to the dynamic bodies hit by the most recent MoveShape call. Use the same + * shape, dt and query options as that call; NULL options use the default filter. + * Unlike MoveShape, the options' predicate is called once per collider of the world before the + * impulses are applied, while the world is locked for writing: it may only use Read* functions. * @ingroup controllers */ RAPIER_API @@ -4360,11 +5184,11 @@ R2Status RAPIER_CALL r2KinematicCharacterController_SolveCharacterCollisionImpul const R2SharedShape *shape, R2Real dt, R2Real mass, - const struct R2QueryFilter *filter); + const struct R2QueryOptions *options); /** - * Allocate a PID controller with supplied gains and controlled axes. Release with - * r2FreePidController. + * Allocate a PID controller with Rapier's defaults: kp = 60, ki = 1 and kd = 0.8 on every axis, all + * axes controlled, and zero integrals. Release with r2FreePidController. * @ingroup controllers */ RAPIER_API struct R2PidController *RAPIER_CALL r2NewPidController(void); @@ -4391,13 +5215,45 @@ R2Status RAPIER_CALL r2PidController_SetGains(struct R2PidController *controller struct R2PidGains gains); /** - * AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. + * Set the controlled axes, a combination of R2_AXES_MASK_* bits. Gains are unchanged; unknown + * bits are rejected. * @ingroup controllers */ RAPIER_API R2Status RAPIER_CALL r2PidController_SetAxes(struct R2PidController *controller, uint32_t axes); +/** + * Return the controlled axes as R2_AXES_MASK_* bits. + * @ingroup controllers + */ +RAPIER_API uint32_t RAPIER_CALL r2PidController_Axes(const struct R2PidController *controller); + +/** + * Reset to zero the linear and angular errors accumulated by the integral term. + * @ingroup controllers + */ +RAPIER_API R2Status RAPIER_CALL r2PidController_ResetIntegrals(struct R2PidController *controller); + +/** + * Return Rapier's default PD controller: kp = 60 and kd = 0.8 on every axis, all axes controlled. + * This POD value owns no resources. + * @ingroup controllers + */ +RAPIER_API struct R2PdController RAPIER_CALL r2DefaultPdController(void); + +/** + * Compute the velocity change bringing the body toward the target pose and velocities. Neither the + * body nor the controller is modified. + * @ingroup controllers + */ +RAPIER_API +struct R2VelocityCorrection RAPIER_CALL r2PdController_RigidBodyCorrection(const struct R2PdController *controller, + struct R2RigidBodyHandle body, + struct R2Pose target_pose, + struct R2Vector target_linvel, + R2AngVector target_angvel); + /** * Compute a velocity correction, preserving the body's state and updating PID integrals. * @ingroup controllers @@ -4488,12 +5344,16 @@ R2Status RAPIER_CALL r2DynamicRayCastVehicleController_SetWheelControls(struct R #if defined(RAPIER_DIM3) /** * Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + * NULL options use the default filter. The chassis colliders are always excluded, in addition + * to the filter's own exclusions. The options' predicate is called once per collider of the + * world before the update, while the world is locked for writing: it may only use Read* + * functions. * @ingroup controllers */ RAPIER_API R2Status RAPIER_CALL r2DynamicRayCastVehicleController_UpdateVehicle(struct R2DynamicRayCastVehicleController *controller, R2Real dt, - const struct R2QueryFilter *filter); + const struct R2QueryOptions *options); #endif #if defined(RAPIER_DIM3) @@ -4691,7 +5551,9 @@ RAPIER_API struct R2JointBodies RAPIER_CALL r2ImpulseJoint_Bodies(struct R2Impul RAPIER_API struct R2InverseKinematicsOptions RAPIER_CALL r2DefaultInverseKinematicsOptions(void); /** - * Return the articulation degrees of freedom associated with the joint. + * Return the degrees of freedom of the whole multibody containing the joint (not of the joint + * alone), including the free root of a dynamic multibody. After inserting a joint, the root's + * contribution is only updated by the next step. * @ingroup joints */ RAPIER_API size_t RAPIER_CALL r2MultibodyJoint_Ndofs(struct R2MultibodyJointHandle handle); @@ -4801,12 +5663,14 @@ RAPIER_API R2Bool RAPIER_CALL r2SoftBody_Contains(struct R2SoftBodyHandle handle /** * Remove a body and its joints, optionally keeping colliders as standalone objects. - * Returns whether a body was removed; a stale handle returns false without error. + * A removed or stale handle fails with R2_INVALID_HANDLE, like the other Remove functions. + * Removing a soft-body cluster proxy removes its cluster (see r2SoftBody_RemoveCluster). The + * root body of a soft body is rejected: remove the soft body with r2RemoveSoftBody. * @ingroup rigid_bodies */ RAPIER_API -R2Bool RAPIER_CALL r2RemoveRigidBody(struct R2RigidBodyHandle handle, - R2Bool remove_attached_colliders); +R2Status RAPIER_CALL r2RemoveRigidBody(struct R2RigidBodyHandle handle, + R2Bool remove_attached_colliders); /** * Return the world setting documented by R2IntegrationParameters::dt. @@ -4949,14 +5813,14 @@ RAPIER_API R2Status RAPIER_CALL r2SetNumInternalPgsIterations(struct R2World *wo /** * Return the world setting documented by * R2IntegrationParameters::numInternalStabilizationIterations. - * @ingroup errors + * @ingroup worlds */ RAPIER_API size_t RAPIER_CALL r2NumInternalStabilizationIterations(const struct R2World *world); /** * Set the world setting documented by * R2IntegrationParameters::numInternalStabilizationIterations. - * @ingroup errors + * @ingroup worlds */ RAPIER_API R2Status RAPIER_CALL r2SetNumInternalStabilizationIterations(struct R2World *world, @@ -5012,25 +5876,25 @@ RAPIER_API R2Status RAPIER_CALL r2SetFrictionInBiasPass(struct R2World *world, R /** * Return the world setting documented by R2IntegrationParameters::warmstartJoints. - * @ingroup joints + * @ingroup worlds */ RAPIER_API R2Bool RAPIER_CALL r2WarmstartJoints(const struct R2World *world); /** * Set the world setting documented by R2IntegrationParameters::warmstartJoints. - * @ingroup joints + * @ingroup worlds */ RAPIER_API R2Status RAPIER_CALL r2SetWarmstartJoints(struct R2World *world, R2Bool value); /** * Return the world setting documented by R2IntegrationParameters::contactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API struct R2SpringCoefficients RAPIER_CALL r2ContactSoftness(const struct R2World *world); /** * Set the world setting documented by R2IntegrationParameters::contactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API R2Status RAPIER_CALL r2SetContactSoftness(struct R2World *world, @@ -5038,13 +5902,13 @@ R2Status RAPIER_CALL r2SetContactSoftness(struct R2World *world, /** * Return the world setting documented by R2IntegrationParameters::staticContactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API struct R2SpringCoefficients RAPIER_CALL r2StaticContactSoftness(const struct R2World *world); /** * Set the world setting documented by R2IntegrationParameters::staticContactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API R2Status RAPIER_CALL r2SetStaticContactSoftness(struct R2World *world, @@ -5052,7 +5916,7 @@ R2Status RAPIER_CALL r2SetStaticContactSoftness(struct R2World *world, /** * Applies Rapier's persistent one-way platform logic to the borrowed manifold. - * @ingroup worlds + * @ingroup callbacks */ RAPIER_API R2Status RAPIER_CALL r2ContactModificationContext_UpdateAsOnewayPlatform(struct R2ContactModificationContext *context, @@ -5061,7 +5925,7 @@ R2Status RAPIER_CALL r2ContactModificationContext_UpdateAsOnewayPlatform(struct /** * Sets the tangent velocity of every rigid solver contact in this manifold. - * @ingroup worlds + * @ingroup callbacks */ RAPIER_API R2Status RAPIER_CALL r2ContactModificationContext_SetTangentVelocity(struct R2ContactModificationContext *context, @@ -5081,13 +5945,13 @@ RAPIER_API struct R2EventCollector *RAPIER_CALL r2NewEventCollector(void); RAPIER_API R2Status RAPIER_CALL r2FreeEventCollector(struct R2EventCollector *events); /** - * Discard all collected events. Does not change the world. + * Discard all collected events. Does not change the world or the callbacks. * @ingroup events */ RAPIER_API R2Status RAPIER_CALL r2EventCollector_Clear(struct R2EventCollector *events); /** - * Copy the collected collision start/stop events without removing them. + * Copy the collision start/stop events collected since the last clear, without removing them. * @see @ref output_buffers * @ingroup events */ @@ -5097,7 +5961,7 @@ size_t RAPIER_CALL r2EventCollector_CollisionEvents(const struct R2EventCollecto size_t capacity); /** - * Copy the collected contact-force events without removing them. + * Copy the contact-force events collected since the last clear, without removing them. * @see @ref output_buffers * @ingroup events */ @@ -5107,7 +5971,7 @@ size_t RAPIER_CALL r2EventCollector_ContactForceEvents(const struct R2EventColle size_t capacity); /** - * Return the number of queued soft-body tear events. + * Return the number of soft-body tear events collected since the last clear. * @ingroup events */ RAPIER_API size_t RAPIER_CALL r2EventCollector_TearEventCount(const struct R2EventCollector *events); @@ -5134,8 +5998,9 @@ RAPIER_API struct R2Vector RAPIER_CALL r2Gravity(const struct R2World *world); RAPIER_API R2Status RAPIER_CALL r2SetGravity(struct R2World *world, struct R2Vector value); /** - * Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. - * Advance simulation by one timestep. Hooks and events may be NULL. + * Advance simulation by one timestep. Hooks and events may be NULL. Events are appended to the + * collector, which is never cleared automatically. This call invalidates all borrowed set-element + * pointers. * @ingroup worlds */ RAPIER_API @@ -5144,7 +6009,8 @@ R2Status RAPIER_CALL r2Step(struct R2World *world, const struct R2EventCollector *events); /** - * Refresh collision detection without advancing simulation. Hooks and events may be NULL. + * Refresh collision detection without advancing simulation. Hooks and events may be NULL; events + * are appended to the collector. * @ingroup worlds */ RAPIER_API @@ -5181,9 +6047,10 @@ RAPIER_API struct R2Bytes *RAPIER_CALL r2SerializeWorld(const struct R2World *wo RAPIER_API struct R2World *RAPIER_CALL r2DeserializeWorld(const uint8_t *data, size_t count); /** - * Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. + * Copy the debug-render lines of the world with the default style. mode combines R2_DEBUG_* bits; + * colors are HSLA (hue in degrees), matching Rapier DebugColor. * @see @ref output_buffers - * @ingroup worlds + * @ingroup events */ RAPIER_API size_t RAPIER_CALL r2DebugRender(const struct R2World *world, @@ -5503,7 +6370,9 @@ RAPIER_API struct R2SoftBodyHandle RAPIER_CALL r2SoftBodyTearEvent_SoftBody(const struct R2SoftBodyTearEvent *event); /** - * Copy the soft-body handles produced by the tear. + * Copy the soft bodies the torn body is in after the tear, the one keeping the handle first: the + * torn body alone when nothing was split off. Entry i holds the particles given by + * r2SoftBodyTearEvent_PieceParticles(event, i, ...). * @see @ref output_buffers * @ingroup soft_bodies */ @@ -5571,7 +6440,9 @@ size_t RAPIER_CALL r2SoftBodyTearEvent_InsertedParticles(const struct R2SoftBody size_t capacity); /** - * Copy original particle indices belonging to a resulting piece. + * Copy the particles of the piece_index-th body of r2SoftBodyTearEvent_Bodies, as indices in + * the torn body after the tear (the indices the other event fields use); entry i is the piece's + * particle i. piece_index must be less than r2SoftBodyTearEvent_PieceCount. * @see @ref output_buffers * @ingroup soft_bodies */ @@ -5680,7 +6551,7 @@ RAPIER_API const char *RAPIER_CALL r2Version(void); RAPIER_API const char *RAPIER_CALL r2BuildProfile(void); /** - * Return profiling, SIMD width, and parallelism of the linked library. + * Return profiling, SIMD width, parallelism, and determinism of the linked library. * @ingroup errors */ RAPIER_API struct R2BuildFeatures RAPIER_CALL r2BuildFeatures(void); @@ -5732,7 +6603,8 @@ size_t RAPIER_CALL r2ContactPairs(const struct R2World *world, size_t capacity); /** - * Return the narrow-phase contact pair for two colliders, or report R2_NOT_FOUND. + * Return the narrow-phase contact pair for two colliders, or report R2_NOT_FOUND. Its collider1 and + * collider2 follow the narrow-phase order, which may differ from the argument order. * @ingroup events */ RAPIER_API @@ -5752,9 +6624,11 @@ size_t RAPIER_CALL r2IntersectionPairs(const struct R2World *world, /** * Contact points in collider-local space; normal in world space. Geometric manifolds may be * recycled. + * local_p1/local_p2 follow the pair's own collider1/collider2 order (see r2ContactPair), which + * may differ from the argument order. * For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. * @see @ref output_buffers - * @ingroup worlds + * @ingroup events */ RAPIER_API size_t RAPIER_CALL r2ContactPoints(struct R2ColliderHandle collider1, @@ -5782,7 +6656,11 @@ R2Status RAPIER_CALL r2MultibodyJoint_SetGeneralizedVelocity(struct R2MultibodyJ size_t count); /** - * Check this before passing any dimension/precision-dependent structs across the ABI. + * Check that the header matches the linked library before passing any structure across the ABI. + * Pass R2_ABI_VERSION, R2_DIMENSION, the sizes of R2Real, R2Vector and R2Pose, and + * R2_ABI_FEATURES. Fails with R2_INVALID_ARGUMENT when the version, dimension, precision, or + * the RAPIER_FEM/RAPIER_ROBOTICS defines differ from the library, since they change structure + * layouts. * @ingroup errors */ RAPIER_API @@ -5790,7 +6668,119 @@ R2Status RAPIER_CALL r2CheckAbi(uint32_t version, uint32_t dimension, size_t real_size, size_t vector_size, - size_t pose_size); + size_t pose_size, + uint32_t features); + +/** + * Copy the rigid bodies quarantined by the most recent Step because their pose or velocity became + * non-finite (NaN or infinite). Rapier disabled them, restored their last valid pose when known, + * and zeroed their velocities and forces; re-enable them with RigidBody_SetEnabled once the cause + * is fixed. The list is cleared at the start of every Step and may hold handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2QuarantinedRigidBodies(const struct R2World *world, + struct R2RigidBodyHandle *buffer, + size_t capacity); + +/** + * Copy the colliders quarantined by the most recent Step because their own pose or shape became + * non-finite, independently of their parent. Rapier disabled them; re-enable them with + * Collider_SetEnabled once fixed. The list is cleared at the start of every Step and may hold + * handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2QuarantinedColliders(const struct R2World *world, + struct R2ColliderHandle *buffer, + size_t capacity); + +/** + * Copy the soft bodies quarantined by the most recent Step because a particle position or velocity + * became non-finite. Rapier disabled them and zeroed their velocities but left the non-finite + * positions: fix them with SoftBody_SetParticlePosition before SoftBody_SetEnabled. The list is + * cleared at the start of every Step and may hold handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r2QuarantinedSoftBodies(const struct R2World *world, + struct R2SoftBodyHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return the world setting documented by R2IntegrationParameters::frictionModel. + * @ingroup worlds + */ +RAPIER_API uint32_t RAPIER_CALL r2FrictionModel(const struct R2World *world); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set the world setting documented by R2IntegrationParameters::frictionModel. + * @ingroup worlds + */ +RAPIER_API R2Status RAPIER_CALL r2SetFrictionModel(struct R2World *world, uint32_t value); +#endif + +/** + * Sine of an angle in radians, computed by Rapier's math backend. With enhanced-determinism + * (see BuildFeatures), the result is identical on every platform. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Sin(R2Real x); + +/** + * Cosine of an angle in radians, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Cos(R2Real x); + +/** + * Tangent of an angle in radians, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Tan(R2Real x); + +/** + * Arcsine in radians, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Asin(R2Real x); + +/** + * Arccosine in radians, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Acos(R2Real x); + +/** + * Angle in radians of the point (x, y), in [-pi, pi], computed by Rapier's math backend. See + * r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Atan2(R2Real y, R2Real x); + +/** + * Exponential e^x, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Exp(R2Real x); + +/** + * Natural logarithm, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Ln(R2Real x); + +/** + * base raised to the power exponent, computed by Rapier's math backend. See r2Sin. + * @ingroup math + */ +RAPIER_API R2Real RAPIER_CALL r2Powf(R2Real base, R2Real exponent); /** * Return owned local-space rendering geometry; release it with r2FreeShapeMesh. subdivisions @@ -5839,10 +6829,23 @@ R2SharedShape *RAPIER_CALL r2RoundCylinderSharedShape(R2Real half_height, R2Real border_radius); #endif +#if defined(RAPIER_DIM3) +/** + * Create an owned round cone shape. Release it with r2FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2RoundConeSharedShape(R2Real half_height, + R2Real radius, + R2Real border_radius); +#endif + #if defined(RAPIER_DIM3) /** * Tessellate a ball or capsule with independent longitude/latitude subdivision counts. * Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. + * ntheta (3 to 4096) is read by balls, capsules, cones and cylinders; nphi (2 to 4096) by balls + * and capsules. Other shapes ignore them. * @ingroup shapes */ RAPIER_API @@ -5954,7 +6957,8 @@ struct R2UrdfRobotHandles *RAPIER_CALL r2UrdfRobot_InsertUsingMultibodyJoints(st #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) /** - * Body handles in source order; absent MJCF bodies have invalid handles. + * One body handle per imported URDF link, in source order. Links merged away by + * squeezeEmptyFixedLinks have no entry. * @see @ref output_buffers * @ingroup robotics */ @@ -6229,6 +7233,105 @@ size_t RAPIER_CALL r2MjcfVisualMesh_Texture(const R2MjcfVisualMesh *visual, size_t capacity); #endif +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load a URDF robot from a NUL-terminated UTF-8 string. Relative mesh paths are resolved from + * mesh_dir (NULL resolves them from the current directory). Options and their blueprint resources + * are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R2UrdfRobot *RAPIER_CALL r2UrdfRobotFromString(const char *urdf, + const char *mesh_dir, + const struct R2UrdfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release owned MJCF contact hooks. NULL is allowed. Do not free them while a step still uses + * them, and do not free them twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeMjcfContactHooks(struct R2MjcfContactHooks *hooks); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Build the contact rules of an inserted MJCF robot. robot must be the robot these handles were + * inserted from. The rules refer to the inserted colliders; the returned hooks are owned. + * @ingroup robotics + */ +RAPIER_API +struct R2MjcfContactHooks *RAPIER_CALL r2MjcfRobotHandles_ContactHooks(const struct R2MjcfRobotHandles *handles, + const struct R2MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return physics hooks forwarding to these contact rules, with hooks as their user_data. Pass + * them to r2Step; hooks must outlive every step using them. The inserted colliders already + * enable the contact-filtering and contact-modification hooks. + * @ingroup robotics + */ +RAPIER_API +struct R2PhysicsHooks RAPIER_CALL r2MjcfContactHooks_PhysicsHooks(const struct R2MjcfContactHooks *hooks); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release owned loaded meshes. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R2Status RAPIER_CALL r2FreeLoadedMeshes(struct R2LoadedMeshes *meshes); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load every mesh of a file from a UTF-8 path and convert it into a shape with converter (an + * R2_MESH_CONVERTER_* value). trimesh_flags (R2_TRIMESH_* bits) apply to + * R2_MESH_CONVERTER_TRIMESH and must be 0 otherwise. scale multiplies the vertices before + * conversion. A mesh failing to convert does not fail the load; see r2LoadedMeshes_CloneShape. + * @ingroup robotics + */ +RAPIER_API +struct R2LoadedMeshes *RAPIER_CALL r2LoadedMeshesFromFile(const char *path, + uint32_t converter, + uint32_t trimesh_flags, + struct R2Vector scale); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of meshes read from the file, including those that failed to convert. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r2LoadedMeshes_Count(const struct R2LoadedMeshes *meshes); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return an owned shape wrapper sharing the geometry of a loaded mesh. Release it with + * FreeSharedShape. Returns NULL with INVALID_ARGUMENT if the index is out of range or if that + * mesh failed to convert. + * @ingroup robotics + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2LoadedMeshes_CloneShape(const struct R2LoadedMeshes *meshes, + size_t index); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the pose to give the shape of a loaded mesh (for example the center of its bounding + * box). Reports INVALID_ARGUMENT if the index is out of range or if that mesh failed to convert. + * @ingroup robotics + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2LoadedMeshes_Pose(const struct R2LoadedMeshes *meshes, + size_t index); +#endif + /** * Return the rigid body world-space pose. * @ingroup rigid_bodies @@ -6448,7 +7551,8 @@ RAPIER_API R2Bool RAPIER_CALL r2Collider_IsSensor(struct R2ColliderHandle handle RAPIER_API struct R2RigidBodyHandle RAPIER_CALL r2Collider_Parent(struct R2ColliderHandle handle); /** - * Set the collider world-space pose. + * Set the collider world-space pose. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. * @ingroup colliders */ RAPIER_API @@ -6456,7 +7560,8 @@ R2Status RAPIER_CALL r2Collider_SetPosition(struct R2ColliderHandle handle, struct R2Pose value); /** - * Set the collider world-space translation. + * Set the collider world-space translation. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. * @ingroup colliders */ RAPIER_API @@ -6579,54 +7684,160 @@ R2Status RAPIER_CALL r2ImpulseJoint_SetDesc(struct R2ImpulseJointHandle handle, R2Bool wake_up); /** - * Replace the shape geometry with a borrowed tri mesh. Counts are elements. - * Copies no arrays. Invalid view metadata leaves the description unchanged. - * Geometry and flags are validated when the description is built or inserted. - * @ingroup shapes + * Return the collider body-type collision activation bitmask (R2_COLLISION_TYPES_* bits). + * @ingroup colliders */ -RAPIER_API -R2Status RAPIER_CALL r2ShapeDesc_SetTrimesh(struct R2ShapeDesc *desc, - struct R2VectorView vertices, - struct R2TriangleView indices, - uint32_t flags); +RAPIER_API uint16_t RAPIER_CALL r2Collider_ActiveCollisionTypes(struct R2ColliderHandle handle); /** - * Replace the shape geometry with a borrowed polyline. Counts are elements. - * Copies no arrays. Invalid view metadata leaves the description unchanged. - * Geometry and flags are validated when the description is built or inserted. - * @ingroup shapes + * Return the collider physics-hook activation bitmask. + * @ingroup colliders */ -RAPIER_API -R2Status RAPIER_CALL r2ShapeDesc_SetPolyline(struct R2ShapeDesc *desc, - struct R2VectorView vertices, - struct R2EdgeView indices, - uint32_t flags); +RAPIER_API uint32_t RAPIER_CALL r2Collider_ActiveHooks(struct R2ColliderHandle handle); /** - * Replace the shape geometry with a borrowed convex hull point cloud. - * @ingroup shapes + * Return the collider friction combination rule (R2_COMBINE_*). + * @ingroup colliders */ -RAPIER_API -R2Status RAPIER_CALL r2ShapeDesc_SetConvexHull(struct R2ShapeDesc *desc, - struct R2VectorView vertices); +RAPIER_API uint32_t RAPIER_CALL r2Collider_FrictionCombineRule(struct R2ColliderHandle handle); /** - * Select an explicit particle recipe and borrow its positions. Other fields are preserved. - * @ingroup soft_bodies + * Return the collider restitution combination rule (R2_COMBINE_*). + * @ingroup colliders */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBodyDesc_SetParticles(struct R2SoftBodyDesc *desc, - struct R2VectorView positions); +RAPIER_API uint32_t RAPIER_CALL r2Collider_RestitutionCombineRule(struct R2ColliderHandle handle); /** - * Select a surface recipe and borrow its vertices and elements. Other fields are preserved. - * @ingroup soft_bodies + * Return the collider pose relative to its parent rigid body, or its world-space pose if it has + * no parent. + * @ingroup colliders */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBodyDesc_SetSurfaceMesh(struct R2SoftBodyDesc *desc, +RAPIER_API struct R2Pose RAPIER_CALL r2Collider_PositionWrtParent(struct R2ColliderHandle handle); + +/** + * Return the rigid body signed dominance group. + * @ingroup rigid_bodies + */ +RAPIER_API int8_t RAPIER_CALL r2RigidBody_DominanceGroup(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body additional solver iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r2RigidBody_AdditionalSolverIterations(struct R2RigidBodyHandle handle); + +/** + * Return the rigid body additional PGS iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r2RigidBody_AdditionalPgsIterations(struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body may exceed the angular-velocity limit of its CCD. + * @ingroup rigid_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RigidBody_IsFastRotationAllowed(struct R2RigidBodyHandle handle); + +/** + * Set the collider world-space rotation. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. + * @ingroup colliders + */ +RAPIER_API +R2Status RAPIER_CALL r2Collider_SetRotation(struct R2ColliderHandle handle, + struct R2Rotation value); + +/** + * Allow or disallow the rigid body to exceed the angular-velocity limit of its CCD (e.g. for + * wheels). + * @ingroup rigid_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2RigidBody_SetAllowFastRotation(struct R2RigidBodyHandle handle, + R2Bool value); + +/** + * Replace the shape geometry with a borrowed tri mesh. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetTrimesh(struct R2ShapeDesc *desc, + struct R2VectorView vertices, + struct R2TriangleView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed polyline. Counts are elements. + * Copies no arrays. Invalid view metadata leaves the description unchanged. + * Geometry and flags are validated when the description is built or inserted. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetPolyline(struct R2ShapeDesc *desc, + struct R2VectorView vertices, + struct R2EdgeView indices, + uint32_t flags); + +/** + * Replace the shape geometry with a borrowed convex hull point cloud. + * @ingroup shapes + */ +RAPIER_API +R2Status RAPIER_CALL r2ShapeDesc_SetConvexHull(struct R2ShapeDesc *desc, + struct R2VectorView vertices); + +/** + * Select an explicit particle recipe and borrow its positions. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetParticles(struct R2SoftBodyDesc *desc, + struct R2VectorView positions); + +/** + * Select a surface recipe and borrow its vertices and elements. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetSurfaceMesh(struct R2SoftBodyDesc *desc, struct R2VectorView vertices, R2SurfaceElementView elements); +#if defined(RAPIER_DIM2) +/** + * Select a 2D triangle-mesh recipe and borrow its vertices and triangles (stored in positions and + * cells). The triangles become structural edges and a boundary, not cells, and shape matching + * holds the shape. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetTrimesh(struct R2SoftBodyDesc *desc, + struct R2VectorView vertices, + struct R2TriangleView triangles); +#endif + +/** + * Borrow descriptions to merge into this body (see R2SoftBodyDesc::appended); preserve all other + * fields. No allocation or element reads. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetAppended(struct R2SoftBodyDesc *desc, + struct R2SoftBodyDescView view); + +/** + * Borrow structural edges added after appending (see R2SoftBodyDesc::addedEdges); preserve all + * other fields. No allocation or element reads. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBodyDesc_SetAddedEdges(struct R2SoftBodyDesc *desc, + struct R2EdgeView view); + /** * Borrow skin geometry. Other fields, including skinCollision, are preserved. * @ingroup soft_bodies @@ -6810,6 +8021,18 @@ struct R2ColliderDesc RAPIER_CALL r2RoundCylinderColliderDesc(R2Real half_height R2Real border_radius); #endif +#if defined(RAPIER_DIM3) +/** + * Return a rounded Y-aligned cone description; dimensions exclude border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R2ColliderDesc RAPIER_CALL r2RoundConeColliderDesc(R2Real half_height, + R2Real radius, + R2Real border_radius); +#endif + /** * Return a X-aligned capsule description; half_height is half the segment length, excluding caps. * Returns a description without allocating or validating. Build/insert validates its fields. @@ -6884,6 +8107,36 @@ struct R2SoftBodyDesc RAPIER_CALL r2ClothSoftBodyDesc(struct R2Vector origin, size_t nv); #endif +#if defined(RAPIER_DIM3) +/** + * Return a cloth recipe like r2ClothSoftBodyDesc whose edges along du (warp), along dv (weft) + * and diagonal (shear) get their own softness; the material's bendSoftness still applies to the + * bending edges. The softness is stored in warpSoftness, weftSoftness and shearSoftness. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2SoftBodyDesc RAPIER_CALL r2ClothAnisotropicSoftBodyDesc(struct R2Vector origin, + struct R2Vector du, + struct R2Vector dv, + size_t nu, + size_t nv, + struct R2SpringCoefficients warp, + struct R2SpringCoefficients weft, + struct R2SpringCoefficients shear); +#endif + +#if defined(RAPIER_DIM2) +/** + * Return a closed polygon recipe from at least 3 counter-clockwise points: structural edges along + * the boundary, bending edges between second neighbors, and area preservation. The points are + * borrowed until preview/insertion. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API struct R2SoftBodyDesc RAPIER_CALL r2PolygonSoftBodyDesc(struct R2VectorView points); +#endif + #if defined(RAPIER_DIM2) /** * Return a closed regular polygon recipe with the specified boundary particle count and area @@ -6962,257 +8215,571 @@ size_t RAPIER_CALL r2SoftBodyDesc_CellIndices(const struct R2SoftBodyDesc *desc, size_t capacity); /** - * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape - * clone alive while using it as a cache key. - * @ingroup shapes + * Return the world-space velocity of the indexed particle. + * @ingroup soft_bodies */ -RAPIER_API size_t RAPIER_CALL r2Collider_ShapeIdentity(struct R2ColliderHandle handle); +RAPIER_API +struct R2Vector RAPIER_CALL r2SoftBody_ParticleVelocity(struct R2SoftBodyHandle handle, + size_t index); /** - * Return the soft body particle count. + * Return the solver simulating the soft body's elasticity (R2_SOFT_SOLVER_*). Always + * R2_SOFT_SOLVER_CONSTRAINTS in a library built without FEM. * @ingroup soft_bodies */ -RAPIER_API size_t RAPIER_CALL r2SoftBody_NumParticles(struct R2SoftBodyHandle handle); +RAPIER_API uint32_t RAPIER_CALL r2SoftBody_Solver(struct R2SoftBodyHandle handle); /** - * Return a counter that changes when particle connectivity changes; use it to invalidate mesh - * caches. + * Override the softness of every structural or bending edge fully contained in a live cluster: + * regional stiffness for cloth and ropes. A NULL softness restores the body material's. * @ingroup soft_bodies */ -RAPIER_API uint32_t RAPIER_CALL r2SoftBody_TopologyVersion(struct R2SoftBodyHandle handle); +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterEdgeSoftness(struct R2SoftBodyHandle handle, + uint32_t cluster, + const struct R2SpringCoefficients *softness); /** - * Return the soft body mass. + * Apply a world-space impulse to every free particle within falloff_radius of the world-space + * point, scaled linearly from 1 at the point to 0 at that radius and divided by the particle's + * mass. A falloff_radius of zero or less gives every free particle the whole impulse. Pinned + * particles ignore it. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. * @ingroup soft_bodies */ -RAPIER_API R2Real RAPIER_CALL r2SoftBody_Mass(struct R2SoftBodyHandle handle); +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyImpulseAtPoint(struct R2SoftBodyHandle handle, + struct R2Vector impulse, + struct R2Vector point, + R2Real falloff_radius, + R2Bool wake_up); /** - * Return the soft body current volume. + * Apply an impulse of the given magnitude pointing away from the world-space center to every + * free particle within falloff_radius, scaled linearly from 1 at the center to 0 at that + * radius and divided by the particle's mass. A particle on the center gets nothing; a + * falloff_radius of zero or less pushes every free particle fully. A negative magnitude pulls + * toward the center. Pinned particles ignore it. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. * @ingroup soft_bodies */ -RAPIER_API R2Real RAPIER_CALL r2SoftBody_Volume(struct R2SoftBodyHandle handle); +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyRadialImpulse(struct R2SoftBodyHandle handle, + struct R2Vector center, + R2Real magnitude, + R2Real falloff_radius, + R2Bool wake_up); /** - * Return the soft body undeformed volume. + * Undo every permanent (plastic) deformation: edge rest lengths, dihedral rest angles, cell + * rest shapes and particle rest positions return to their creation state. The particles stay + * put and spring back elastically. * @ingroup soft_bodies */ -RAPIER_API R2Real RAPIER_CALL r2SoftBody_RestVolume(struct R2SoftBodyHandle handle); +RAPIER_API R2Status RAPIER_CALL r2SoftBody_ResetPlasticity(struct R2SoftBodyHandle handle); /** - * Return the soft body target volume multiplier. + * Mark the indexed edge as torn. The tear is applied at the end of the next step and reported + * by a tear event; use r2SoftBody_Tear to tear immediately. * @ingroup soft_bodies */ -RAPIER_API R2Real RAPIER_CALL r2SoftBody_VolumeFactor(struct R2SoftBodyHandle handle); +RAPIER_API R2Status RAPIER_CALL r2SoftBody_TearEdge(struct R2SoftBodyHandle handle, size_t index); /** - * Return the soft body world-space center of mass. + * Mark the indexed cell as torn. The tear is applied at the end of the next step and reported + * by a tear event: no cell is removed, one of its particles splits along the plane + * perpendicular to the cell's principal rest stretch. * @ingroup soft_bodies */ -RAPIER_API struct R2Vector RAPIER_CALL r2SoftBody_CenterOfMass(struct R2SoftBodyHandle handle); +RAPIER_API R2Status RAPIER_CALL r2SoftBody_TearCell(struct R2SoftBodyHandle handle, size_t index); /** - * Return the soft body root rigid-proxy handle. - * @ingroup soft_bodies + * Return the soft body owning this deformable collider (a soft-body collision mesh), or an invalid + * handle for any other collider. + * @ingroup colliders */ -RAPIER_API struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_RootBody(struct R2SoftBodyHandle handle); +RAPIER_API struct R2SoftBodyHandle RAPIER_CALL r2Collider_SoftBody(struct R2ColliderHandle handle); /** - * Return whether the soft body is enabled. + * Return the soft body owning this deformable collider, or an invalid handle for any other + * collider. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2SoftBodyHandle RAPIER_CALL r2ReadCollider_SoftBody(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the number of soft bodies the torn body is in after the tear: the length of + * r2SoftBodyTearEvent_Bodies, and the exclusive bound of the piece_index of + * r2SoftBodyTearEvent_PieceParticles. It is 1 when nothing was split off. * @ingroup soft_bodies */ -RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsEnabled(struct R2SoftBodyHandle handle); +RAPIER_API size_t RAPIER_CALL r2SoftBodyTearEvent_PieceCount(const struct R2SoftBodyTearEvent *event); /** - * Return whether the soft body is sleeping. + * Return the world setting documented by R2SoftRecoverySettings::authoredVelocityMargin. * @ingroup soft_bodies */ -RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsSleeping(struct R2SoftBodyHandle handle); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryAuthoredVelocityMargin(const struct R2World *world); /** - * Copy world-space particle velocities. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::edgeSpeculation. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_ParticleVelocities(struct R2SoftBodyHandle handle, - struct R2Vector *buffer, - size_t capacity); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryEdgeSpeculation(const struct R2World *world); /** - * Copy flattened edge vertex indices. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::invertedCellDetection. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_Edges(struct R2SoftBodyHandle handle, - uint32_t *buffer, - size_t capacity); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryInvertedCellDetection(const struct R2World *world); /** - * Copy flattened cell vertex indices. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::selfCrossingDetection. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_Cells(struct R2SoftBodyHandle handle, - uint32_t *buffer, - size_t capacity); +RAPIER_API R2Bool RAPIER_CALL r2RecoverySelfCrossingDetection(const struct R2World *world); /** - * Copy flattened boundary element indices. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::detectionMotionGating. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_Boundary(struct R2SoftBodyHandle handle, - uint32_t *buffer, - size_t capacity); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryDetectionMotionGating(const struct R2World *world); /** - * Copy piece identifiers. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::crossBodyDetection. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_Pieces(struct R2SoftBodyHandle handle, - struct R2SoftBodyHandle *buffer, - size_t capacity); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryCrossBodyDetection(const struct R2World *world); /** - * Set the soft body particle world-space velocity. + * Return the world setting documented by R2SoftRecoverySettings::selfStandDown. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_SetParticleVelocity(struct R2SoftBodyHandle handle, - size_t index, - struct R2Vector value); +RAPIER_API R2Bool RAPIER_CALL r2RecoverySelfStandDown(const struct R2World *world); /** - * Set the next world-space target position of a pinned particle. + * Return the world setting documented by R2SoftRecoverySettings::crossBodyExpelGate. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_SetParticleKinematicTarget(struct R2SoftBodyHandle handle, - size_t index, - struct R2Vector value); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryCrossBodyExpelGate(const struct R2World *world); /** - * Enable or disable pinning the particle for the soft body. + * Return the world setting documented by R2SoftRecoverySettings::edgeStandDown. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_SetParticlePinned(struct R2SoftBodyHandle handle, - size_t index, - R2Bool value); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryEdgeStandDown(const struct R2World *world); /** - * Apply a world-space impulse to one particle. - * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * Return the world setting documented by R2SoftRecoverySettings::crossingRepulsion. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_ApplyParticleImpulse(struct R2SoftBodyHandle handle, - size_t index, - struct R2Vector value, - R2Bool wake_up); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryCrossingRepulsion(const struct R2World *world); /** - * Accumulate a world-space force; it persists until reset. - * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * Return the world setting documented by R2SoftRecoverySettings::crossingRepulsionGuide. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_AddForce(struct R2SoftBodyHandle handle, - struct R2Vector value, - R2Bool wake_up); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryCrossingRepulsionGuide(const struct R2World *world); /** - * Apply a world-space linear impulse. - * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * Return the world setting documented by R2SoftRecoverySettings::crossingRepulsionSelfGuide. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_ApplyImpulse(struct R2SoftBodyHandle handle, - struct R2Vector value, - R2Bool wake_up); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryCrossingRepulsionSelfGuide(const struct R2World *world); /** - * Clear accumulated user forces. - * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * Return the world setting documented by R2SoftRecoverySettings::recoveryPace. * @ingroup soft_bodies */ -RAPIER_API R2Status RAPIER_CALL r2SoftBody_ResetForces(struct R2SoftBodyHandle handle, R2Bool wake_up); +RAPIER_API R2Real RAPIER_CALL r2RecoveryRecoveryPace(const struct R2World *world); /** - * Enable or disable the soft body. + * Return the world setting documented by R2SoftRecoverySettings::overlapConstraints. * @ingroup soft_bodies */ -RAPIER_API R2Status RAPIER_CALL r2SoftBody_SetEnabled(struct R2SoftBodyHandle handle, R2Bool value); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapConstraints(const struct R2World *world); /** - * Set the soft body target volume multiplier. + * Return the world setting documented by R2SoftRecoverySettings::overlapRigid. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_SetVolumeFactor(struct R2SoftBodyHandle handle, - R2Real value); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapRigid(const struct R2World *world); /** - * Attach a particle to a rigid body at the supplied body-local anchor. + * Return the world setting documented by R2SoftRecoverySettings::overlapSkipSelfTangled. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_AttachParticle(struct R2SoftBodyHandle handle, - size_t index, - struct R2RigidBodyHandle rigid_body); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapSkipSelfTangled(const struct R2World *world); /** - * Remove a particle attachment to a rigid body. + * Return the world setting documented by R2SoftRecoverySettings::overlapEdgeStandDown. * @ingroup soft_bodies */ -RAPIER_API R2Status RAPIER_CALL r2SoftBody_DetachParticle(struct R2SoftBodyHandle handle, size_t index); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapEdgeStandDown(const struct R2World *world); /** - * Copy cluster indices. - * @see @ref output_buffers + * Return the world setting documented by R2SoftRecoverySettings::overlapConstraintPace. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r2SoftBody_Clusters(struct R2SoftBodyHandle handle, - uint32_t *buffer, - size_t capacity); +RAPIER_API R2Real RAPIER_CALL r2RecoveryOverlapConstraintPace(const struct R2World *world); /** - * Return the rigid proxy for the selected cluster. + * Return the world setting documented by R2SoftRecoverySettings::overlapPatchConstraints. * @ingroup soft_bodies */ -RAPIER_API -struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_ClusterProxy(struct R2SoftBodyHandle handle, - uint32_t cluster); +RAPIER_API uint32_t RAPIER_CALL r2RecoveryOverlapPatchConstraints(const struct R2World *world); /** - * Copy particle indices for a cluster. - * @see @ref output_buffers + * Set the world setting documented by R2SoftRecoverySettings::overlapPatchConstraints. * @ingroup soft_bodies */ RAPIER_API -size_t RAPIER_CALL r2SoftBody_ClusterParticles(struct R2SoftBodyHandle handle, - uint32_t cluster, - uint32_t *buffer, - size_t capacity); +R2Status RAPIER_CALL r2RecoverySetOverlapPatchConstraints(struct R2World *world, + uint32_t value); /** - * Enable or disable pinning the cluster for the soft body. + * Return the world setting documented by R2SoftRecoverySettings::overlapSkinVolume. * @ingroup soft_bodies */ -RAPIER_API -R2Status RAPIER_CALL r2SoftBody_SetClusterPinned(struct R2SoftBodyHandle handle, - uint32_t cluster, - R2Bool value); +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapSkinVolume(const struct R2World *world); /** - * Set the next world-space target pose of a pinned cluster. + * Return the world setting documented by R2SoftRecoverySettings::overlapKeptDepth. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RecoveryOverlapKeptDepth(const struct R2World *world); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapSelfRegions. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapSelfRegions(const struct R2World *world); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapNormalPush. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapNormalPush(const struct R2World *world); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapMultiVolume. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2RecoveryOverlapMultiVolume(const struct R2World *world); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapSplit. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r2RecoveryOverlapSplit(const struct R2World *world); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapSplit. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapSplit(struct R2World *world, uint32_t value); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapPatience. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r2RecoveryOverlapPatience(const struct R2World *world); + +/** + * Set the world setting documented by R2SoftRecoverySettings::overlapPatience. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2RecoverySetOverlapPatience(struct R2World *world, uint32_t value); + +/** + * Return the world setting documented by R2SoftRecoverySettings::overlapProgressMargin. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2RecoveryOverlapProgressMargin(const struct R2World *world); + +#if defined(RAPIER_FEM) +/** + * Return the world setting documented by R2SoftFemParameters::linearTolerance. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2FemLinearTolerance(const struct R2World *world); +#endif + +#if defined(RAPIER_FEM) +/** + * Return the world setting documented by R2SoftFemParameters::maxLinearIterations. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2FemMaxLinearIterations(const struct R2World *world); +#endif + +#if defined(RAPIER_FEM) +/** + * Return the world setting documented by R2SoftFemParameters::maxDenseDofs. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2FemMaxDenseDofs(const struct R2World *world); +#endif + +/** + * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape + * clone alive while using it as a cache key. + * @ingroup shapes + */ +RAPIER_API size_t RAPIER_CALL r2Collider_ShapeIdentity(struct R2ColliderHandle handle); + +/** + * Return the soft body particle count. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r2SoftBody_NumParticles(struct R2SoftBodyHandle handle); + +/** + * Return a counter that changes when particle connectivity changes; use it to invalidate mesh + * caches. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r2SoftBody_TopologyVersion(struct R2SoftBodyHandle handle); + +/** + * Return the soft body mass. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_Mass(struct R2SoftBodyHandle handle); + +/** + * Return the soft body current volume. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_Volume(struct R2SoftBodyHandle handle); + +/** + * Return the soft body undeformed volume. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_RestVolume(struct R2SoftBodyHandle handle); + +/** + * Return the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API R2Real RAPIER_CALL r2SoftBody_VolumeFactor(struct R2SoftBodyHandle handle); + +/** + * Return the soft body world-space center of mass. + * @ingroup soft_bodies + */ +RAPIER_API struct R2Vector RAPIER_CALL r2SoftBody_CenterOfMass(struct R2SoftBodyHandle handle); + +/** + * Return the soft body root rigid-proxy handle. + * @ingroup soft_bodies + */ +RAPIER_API struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_RootBody(struct R2SoftBodyHandle handle); + +/** + * Return whether the soft body is enabled. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsEnabled(struct R2SoftBodyHandle handle); + +/** + * Return whether the soft body is sleeping. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_IsSleeping(struct R2SoftBodyHandle handle); + +/** + * Copy world-space particle velocities. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_ParticleVelocities(struct R2SoftBodyHandle handle, + struct R2Vector *buffer, + size_t capacity); + +/** + * Copy flattened edge vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Edges(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened cell vertex indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Cells(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy flattened boundary element indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Boundary(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Copy piece identifiers. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Pieces(struct R2SoftBodyHandle handle, + struct R2SoftBodyHandle *buffer, + size_t capacity); + +/** + * Set the soft body particle world-space velocity. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticleVelocity(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value); + +/** + * Set the next world-space target position of a pinned particle. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticleKinematicTarget(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value); + +/** + * Enable or disable pinning the particle for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetParticlePinned(struct R2SoftBodyHandle handle, + size_t index, + R2Bool value); + +/** + * Apply a world-space impulse to one particle. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyParticleImpulse(struct R2SoftBodyHandle handle, + size_t index, + struct R2Vector value, + R2Bool wake_up); + +/** + * Accumulate a world-space force; it persists until reset. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_AddForce(struct R2SoftBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Add the same world-space velocity change to every free particle: the whole body is kicked at + * the same velocity, whatever the particle masses (the value is not divided by the mass). Pinned + * particles ignore it. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_ApplyImpulse(struct R2SoftBodyHandle handle, + struct R2Vector value, + R2Bool wake_up); + +/** + * Clear accumulated user forces. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_ResetForces(struct R2SoftBodyHandle handle, R2Bool wake_up); + +/** + * Enable or disable the soft body. + * @ingroup soft_bodies + */ +RAPIER_API R2Status RAPIER_CALL r2SoftBody_SetEnabled(struct R2SoftBodyHandle handle, R2Bool value); + +/** + * Set the soft body target volume multiplier. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetVolumeFactor(struct R2SoftBodyHandle handle, + R2Real value); + +/** + * Attach a particle to a rigid body by a two-way point-to-point constraint (unlike pinning). The + * anchor is the particle's current position, expressed in the rigid body's local frame; a particle + * attached twice keeps both attachments. Undo it with r2SoftBody_DetachParticle. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_AttachParticle(struct R2SoftBodyHandle handle, + size_t index, + struct R2RigidBodyHandle rigid_body); + +/** + * Detach a particle from every rigid body it was attached to with r2SoftBody_AttachParticle. + * Returns whether it was attached at all. + * @ingroup soft_bodies + */ +RAPIER_API R2Bool RAPIER_CALL r2SoftBody_DetachParticle(struct R2SoftBodyHandle handle, size_t index); + +/** + * Copy cluster indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_Clusters(struct R2SoftBodyHandle handle, + uint32_t *buffer, + size_t capacity); + +/** + * Return the rigid proxy for the selected cluster. + * @ingroup soft_bodies + */ +RAPIER_API +struct R2RigidBodyHandle RAPIER_CALL r2SoftBody_ClusterProxy(struct R2SoftBodyHandle handle, + uint32_t cluster); + +/** + * Copy particle indices for a cluster. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r2SoftBody_ClusterParticles(struct R2SoftBodyHandle handle, + uint32_t cluster, + uint32_t *buffer, + size_t capacity); + +/** + * Enable or disable pinning the cluster for the soft body. + * @ingroup soft_bodies + */ +RAPIER_API +R2Status RAPIER_CALL r2SoftBody_SetClusterPinned(struct R2SoftBodyHandle handle, + uint32_t cluster, + R2Bool value); + +/** + * Set the next world-space target pose of a pinned cluster. * @ingroup soft_bodies */ RAPIER_API @@ -7230,7 +8797,9 @@ R2Status RAPIER_CALL r2SoftBody_SetClusterShapeMatchingEnabled(struct R2SoftBody R2Bool value); /** - * Set the soft body cluster shape-matching stiffness multiplier. + * Scale the material stiffness (Young modulus) of every cell fully contained in a live cluster: + * regional materials without a separate body. Cells straddling the cluster's boundary keep their + * stiffness; use r2SoftBody_SetClusterEdgeSoftness for edges. * @ingroup soft_bodies */ RAPIER_API @@ -7505,7 +9074,7 @@ RAPIER_API R2Real RAPIER_CALL r2RigidBody_KineticEnergy(struct R2RigidBodyHandle /** * Return the rigid body soft-CCD prediction distance. - * @ingroup soft_bodies + * @ingroup rigid_bodies */ RAPIER_API R2Real RAPIER_CALL r2RigidBody_SoftCcdPrediction(struct R2RigidBodyHandle handle); @@ -7597,7 +9166,7 @@ R2Status RAPIER_CALL r2RigidBody_SetAdditionalMass(struct R2RigidBodyHandle hand /** * Set the rigid body soft-CCD prediction distance. - * @ingroup soft_bodies + * @ingroup rigid_bodies */ RAPIER_API R2Status RAPIER_CALL r2RigidBody_SetSoftCcdPrediction(struct R2RigidBodyHandle handle, @@ -7884,8 +9453,7 @@ RAPIER_API R2Bool RAPIER_CALL r2Collider_IsEnabled(struct R2ColliderHandle handl RAPIER_API struct R2Aabb RAPIER_CALL r2Collider_ComputeAabb(struct R2ColliderHandle handle); /** - * Return an owned wrapper sharing the collider geometry. Release with r2FreeSharedShape. - * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * Return an owned wrapper sharing the collider geometry. Release it with r2FreeSharedShape. * @ingroup shapes */ RAPIER_API R2SharedShape *RAPIER_CALL r2Collider_CloneShape(struct R2ColliderHandle handle); @@ -8033,7 +9601,7 @@ R2Status RAPIER_CALL r2ImpulseJoint_SetEnabled(struct R2ImpulseJointHandle handl /** * Set the joint desc joint spring coefficients. - * @ingroup soft_bodies + * @ingroup joints */ RAPIER_API R2Status RAPIER_CALL r2JointDesc_SetSoftness(struct R2JointDesc *desc, @@ -8042,7 +9610,7 @@ R2Status RAPIER_CALL r2JointDesc_SetSoftness(struct R2JointDesc *desc, /** * Set the impulse joint joint spring coefficients. * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. - * @ingroup soft_bodies + * @ingroup joints */ RAPIER_API R2Status RAPIER_CALL r2ImpulseJoint_SetSoftness(struct R2ImpulseJointHandle handle, @@ -8302,6 +9870,55 @@ R2Status RAPIER_CALL r2ImpulseJoint_SetMotorVelocity(struct R2ImpulseJointHandle R2Real factor, R2Bool wake_up); +/** + * Return the impulses applied by the impulse joint during the last step. They are zero before + * its first step, and their components are expressed along the axes of the joint frame. + * @ingroup joints + */ +RAPIER_API struct R2JointImpulses RAPIER_CALL r2ImpulseJoint_Impulses(struct R2ImpulseJointHandle handle); + +/** + * Return the impulse joint application-owned 128-bit user value. + * @ingroup joints + */ +RAPIER_API struct R2UserData RAPIER_CALL r2ImpulseJoint_UserData(struct R2ImpulseJointHandle handle); + +/** + * Return the number of impulse joints in the world. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r2ImpulseJointCount(const struct R2World *world); + +/** + * Return the number of multibody joints in the world, which is the number of handles copied by + * r2MultibodyJointHandles. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r2MultibodyJointCount(const struct R2World *world); + +/** + * Copies the multibody joint configuration without returning a borrowed joint pointer. + * @ingroup joints + */ +RAPIER_API struct R2JointDesc RAPIER_CALL r2MultibodyJoint_Desc(struct R2MultibodyJointHandle handle); + +/** + * Replaces the multibody joint configuration after validation. lockedAxes defines the degrees of + * freedom of the multibody and cannot change: a different value reports INVALID_ARGUMENT. + * wake_up = 1 wakes the two connected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R2Status RAPIER_CALL r2MultibodyJoint_SetDesc(struct R2MultibodyJointHandle handle, + const struct R2JointDesc *desc, + R2Bool wake_up); + +/** + * Return the two bodies connected by a multibody joint: its parent link, then its own link. + * @ingroup joints + */ +RAPIER_API struct R2JointBodies RAPIER_CALL r2MultibodyJoint_Bodies(struct R2MultibodyJointHandle handle); + /** * Create an owned compound shape by convex decomposition of the input surface. Release it with * r2FreeSharedShape. @@ -8340,6 +9957,18 @@ R2SharedShape *RAPIER_CALL r2VoxelizedMeshSharedShape(struct R2VectorView vertic */ RAPIER_API R2SharedShape *RAPIER_CALL r2ConvexHullSharedShape(struct R2VectorView vertices); +#if defined(RAPIER_DIM3) +/** + * Create an owned convex polyhedron from vertices and triangle indices assumed to form a convex + * mesh (no convex hull is computed); fails on degenerate input. Release it with r2FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R2SharedShape *RAPIER_CALL r2ConvexMeshSharedShape(struct R2VectorView vertices, + struct R2TriangleView indices); +#endif + /** * Create an owned triangle mesh from vertices and triangle indices. Release it with * r2FreeSharedShape. @@ -8352,6 +9981,7 @@ R2SharedShape *RAPIER_CALL r2TrimeshSharedShape(struct R2VectorView vertices, /** * Create an owned polyline from vertices and edge indices. Release it with r2FreeSharedShape. + * Empty indices connect the vertices in order (a line strip). * Copies typed input geometry into an owned shared shape; arrays may be released on return. * @ingroup shapes */ @@ -8428,6 +10058,19 @@ struct R2VelocityCorrection RAPIER_CALL r2ReadPidController_RigidBodyCorrection( struct R2Vector target_linvel, R2AngVector target_angvel); +/** + * Compute a PD velocity correction from callback-visible body state. Neither the body nor the + * controller is modified. The context is valid only during its callback. + * @ingroup callbacks + */ +RAPIER_API +struct R2VelocityCorrection RAPIER_CALL r2ReadPdController_RigidBodyCorrection(const struct R2ReadContext *context, + const struct R2PdController *controller, + struct R2RigidBodyHandle body, + struct R2Pose target_pose, + struct R2Vector target_linvel, + R2AngVector target_angvel); + /** * Return the number of rigid body objects in the world. Uses only the callback-scoped read * context; never retain the context. @@ -9002,17 +10645,320 @@ struct R2RigidBodyHandle RAPIER_CALL r2ReadCollider_Parent(const struct R2ReadCo struct R2ColliderHandle handle); /** - * Copy callback-visible body states in the supplied handle order. All handles must belong to the - * context world. + * Copy callback-visible body states in the supplied handle order. All handles must belong to the + * context world. + * @see @ref output_buffers + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context, + const struct R2RigidBodyHandle *handles, + size_t handle_count, + struct R2RigidBodyState *states, + size_t capacity); + +/** + * Return the collider body-type collision activation bitmask (R2_COLLISION_TYPES_* bits). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint16_t RAPIER_CALL r2ReadCollider_ActiveCollisionTypes(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider physics-hook activation bitmask. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r2ReadCollider_ActiveHooks(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider friction combination rule (R2_COMBINE_*). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r2ReadCollider_FrictionCombineRule(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider restitution combination rule (R2_COMBINE_*). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r2ReadCollider_RestitutionCombineRule(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the collider pose relative to its parent rigid body, or its world-space pose if it has + * no parent. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2ReadCollider_PositionWrtParent(const struct R2ReadContext *context, + struct R2ColliderHandle handle); + +/** + * Return the rigid body signed dominance group. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +int8_t RAPIER_CALL r2ReadRigidBody_DominanceGroup(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body additional solver iterations for connected bodies. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBody_AdditionalSolverIterations(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return the rigid body additional PGS iterations for connected bodies. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ReadRigidBody_AdditionalPgsIterations(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Return whether the rigid body may exceed the angular-velocity limit of its CCD. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ReadRigidBody_IsFastRotationAllowed(const struct R2ReadContext *context, + struct R2RigidBodyHandle handle); + +/** + * Copy the hits of every collider intersected by the ray, in no particular order. The ray is + * origin + direction * t for 0 <= t <= max_toi; direction need not be normalized. solid treats an + * interior origin as a hit at t = 0. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r2IntersectRay(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector origin, + struct R2Vector direction, + R2Real max_toi, + R2Bool solid, + struct R2RayHit *buffer, + size_t capacity); + +/** + * Sweep shape from pose along velocity and return the first hit, with found = 0 on a miss + * (R2_OK). Time is bounded by options.max_time_of_impact. The shape is borrowed for this call. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2OptionalShapeCastHit RAPIER_CALL r2TryCastShape(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Pose pose, + struct R2Vector velocity, + const R2SharedShape *shape, + struct R2ShapeCastOptions options); + +/** + * Return the closest surface projection within max_distance, with found = 0 if there is none + * (R2_OK). With solid = 1, an interior point projects to itself. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2OptionalPointProjection RAPIER_CALL r2TryProjectPoint(const struct R2World *world, + const struct R2QueryOptions *query_options, + struct R2Vector point, + R2Real max_distance, + R2Bool solid); + +/** + * Return the world-space pose of the motion at the given time. + * @ingroup queries + */ +RAPIER_API +struct R2Pose RAPIER_CALL r2NonlinearRigidMotion_PositionAtTime(const struct R2NonlinearRigidMotion *motion, + R2Real time); + +/** + * Sweep shape along a rotating motion and return the first hit between start_time and end_time, + * with found = 0 on a miss (R2_OK). With stop_at_penetration = 1, a shape already intersecting a + * collider at start_time hits it at start_time; with 0, that penetration is ignored while the + * motion separates the shapes. witness1/normal1 are world-space; witness2/normal2 are local to the + * shape, posed by r2NonlinearRigidMotion_PositionAtTime at the time of impact. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R2OptionalShapeCastHit RAPIER_CALL r2TryCastShapeNonlinear(const struct R2World *world, + const struct R2QueryOptions *query_options, + const struct R2NonlinearRigidMotion *motion, + const R2SharedShape *shape, + R2Real start_time, + R2Real end_time, + R2Bool stop_at_penetration); + +/** + * Return the narrow-phase contact pair for two colliders, with found = 0 if the broad phase + * found no potential contact between them (R2_OK). The pair's collider1 and collider2 follow the + * narrow-phase order, which may differ from the argument order. + * @ingroup events + */ +RAPIER_API +struct R2OptionalContactPair RAPIER_CALL r2TryContactPair(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2); + +/** + * Return the intersection state of two colliders involving a sensor, or report R2_NOT_FOUND if + * the broad phase found no potential intersection. The result keeps the argument order. + * @ingroup events + */ +RAPIER_API +struct R2IntersectionPair RAPIER_CALL r2IntersectionPair(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2); + +/** + * Return the intersection state of two colliders involving a sensor, with found = 0 if the + * broad phase found no potential intersection (R2_OK). The pair keeps the argument order. + * @ingroup events + */ +RAPIER_API +struct R2OptionalIntersectionPair RAPIER_CALL r2TryIntersectionPair(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2); + +/** + * Copy the narrow-phase contact pairs involving the collider, including pairs without active + * solver contacts. The collider may be either collider1 or collider2 of each pair. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2Collider_ContactPairs(struct R2ColliderHandle handle, + struct R2ContactPair *buffer, + size_t capacity); + +/** + * Copy the intersection pairs involving the collider, in the narrow-phase order. The collider may + * be either collider1 or collider2 of each pair. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2Collider_IntersectionPairs(struct R2ColliderHandle handle, + struct R2IntersectionPair *buffer, + size_t capacity); + +/** + * Copy the geometric contact manifolds of a contact pair, or report R2_NOT_FOUND without a pair. + * Their order matches the manifold_index of r2ContactPoints. Soft pairs have no rigid + * manifolds. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2ContactManifolds(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + struct R2ContactManifold *buffer, + size_t capacity); + +/** + * Copy the solver contacts of one manifold of a contact pair, or report R2_NOT_FOUND without a + * pair. Points are resolved through the bodies' current poses. With contact clustering (3D + * composite shapes), the solver may use merged manifolds instead; use contact pair totals then. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r2SolverContacts(struct R2ColliderHandle collider1, + struct R2ColliderHandle collider2, + size_t manifold_index, + struct R2SolverContact *buffer, + size_t capacity); + +/** + * Return whether the context holds the contact candidates of two soft surfaces rather than a + * manifold. Solver-contact accessors see no contacts in a soft context. + * @ingroup callbacks + */ +RAPIER_API +R2Bool RAPIER_CALL r2ContactModificationContext_IsSoft(const struct R2ContactModificationContext *context); + +/** + * Return the number of solver contacts of the manifold; zero for a soft context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r2ContactModificationContext_SolverContactCount(const struct R2ContactModificationContext *context); + +/** + * Return a solver contact of the manifold. Inside the hook, points are world-space. + * @ingroup callbacks + */ +RAPIER_API +struct R2SolverContact RAPIER_CALL r2ContactModificationContext_SolverContact(const struct R2ContactModificationContext *context, + size_t index); + +/** + * Replace the points, distance and tangent velocity of a solver contact of the manifold. Points + * are world-space; a distance differing from their gap along the normal shifts the contact. + * @ingroup callbacks + */ +RAPIER_API +R2Status RAPIER_CALL r2ContactModificationContext_SetSolverContact(struct R2ContactModificationContext *context, + size_t index, + const struct R2SolverContact *contact); + +/** + * Remove a solver contact of the manifold. The last solver contact takes its index. + * @ingroup callbacks + */ +RAPIER_API +R2Status RAPIER_CALL r2ContactModificationContext_RemoveSolverContact(struct R2ContactModificationContext *context, + size_t index); + +/** + * Replace the callbacks invoked while stepping with this collector; NULL removes them. They take + * effect from the next Step or DetectCollisions call. + * @ingroup events + */ +RAPIER_API +R2Status RAPIER_CALL r2EventCollector_SetCallbacks(struct R2EventCollector *events, + const struct R2EventCallbacks *callbacks); + +/** + * Return native default debug-render style. This POD value owns no resources. + * @ingroup events + */ +RAPIER_API struct R2DebugRenderStyle RAPIER_CALL r2DefaultDebugRenderStyle(void); + +/** + * Copy the debug-render lines of the world drawn with the given style. mode combines R2_DEBUG_* + * bits. NULL style uses the default style. * @see @ref output_buffers - * @ingroup callbacks + * @ingroup events */ RAPIER_API -size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context, - const struct R2RigidBodyHandle *handles, - size_t handle_count, - struct R2RigidBodyState *states, - size_t capacity); +size_t RAPIER_CALL r2DebugRenderWithStyle(const struct R2World *world, + uint32_t mode, + const struct R2DebugRenderStyle *style, + struct R2DebugLine *buffer, + size_t capacity); #ifdef __cplusplus } // extern "C" @@ -9020,6 +10966,23 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context #else /* RAPIER_DIM3 */ +#if defined(RAPIER_DIM3) +/** + * @ingroup worlds + * Friction model solving one Coulomb friction constraint per group of up to 4 contacts plus a + * twist constraint; faster but less accurate (default). + */ +#define R3_FRICTION_MODEL_SIMPLIFIED 0 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup worlds + * Friction model solving one Coulomb friction constraint per contact point. + */ +#define R3_FRICTION_MODEL_COULOMB 1 +#endif + #if defined(RAPIER_DIM2) /** * @ingroup joints @@ -9096,6 +11059,30 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context */ #define R3_SOFT_DESC_VOLUMETRIC 9 +#if defined(RAPIER_DIM2) +/** + * @ingroup soft_bodies + * Soft-body selector: closed counter-clockwise polygon of particles preserving its area (2D). + */ +#define R3_SOFT_DESC_POLYGON 10 +#endif + +#if defined(RAPIER_DIM2) +/** + * @ingroup soft_bodies + * Soft-body selector: triangle mesh without cells, held by shape matching (2D). + */ +#define R3_SOFT_DESC_TRIMESH 11 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup soft_bodies + * Soft-body selector: cloth with separate warp, weft and shear softness (3D). + */ +#define R3_SOFT_DESC_CLOTH_ANISOTROPIC 12 +#endif + /** * @ingroup soft_bodies * Soft-body selector: binding skinned. @@ -9224,6 +11211,12 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context */ #define R3_SHAPE_DESC_ROUND_CYLINDER 15 +/** + * @ingroup shapes + * ShapeDesc kind selecting a round cone. + */ +#define R3_SHAPE_DESC_ROUND_CONE 16 + /** * @ingroup colliders * Mass density. @@ -9308,6 +11301,60 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context */ #define R3_COMBINE_MAX 3 +/** + * @ingroup colliders + * Use the sum of the two material coefficients, clamped to [0, 1]. + */ +#define R3_COMBINE_CLAMPED_SUM 4 + +/** + * @ingroup colliders + * Use the geometric mean (square root of the product) of the two material coefficients. + */ +#define R3_COMBINE_GEOMETRIC_MEAN 5 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two dynamic bodies. + */ +#define R3_COLLISION_TYPES_DYNAMIC_DYNAMIC 1 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a dynamic and a kinematic body. + */ +#define R3_COLLISION_TYPES_DYNAMIC_KINEMATIC 12 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a dynamic and a fixed body (or a collider without parent). + */ +#define R3_COLLISION_TYPES_DYNAMIC_FIXED 2 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two kinematic bodies. + */ +#define R3_COLLISION_TYPES_KINEMATIC_KINEMATIC 52224 + +/** + * @ingroup colliders + * Active collision type bit: contacts between a kinematic and a fixed body (or a collider without parent). + */ +#define R3_COLLISION_TYPES_KINEMATIC_FIXED 8704 + +/** + * @ingroup colliders + * Active collision type bit: contacts between two fixed bodies (or colliders without parent). + */ +#define R3_COLLISION_TYPES_FIXED_FIXED 32 + +/** + * @ingroup colliders + * Default active collision types: dynamic-dynamic, dynamic-kinematic, and dynamic-fixed. + */ +#define R3_COLLISION_TYPES_DEFAULT 15 + /** * @ingroup colliders * Invoke the contact-pair filtering hook for this collider. @@ -9494,6 +11541,42 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context */ #define R3_SOFT_SOLVER_FEM 1 +/** + * @ingroup soft_bodies + * Edge plastic flow (R3SoftBodyMaterial::edgePlasticFlow): both a squeeze and a stretch set. + */ +#define R3_SOFT_EDGE_PLASTIC_FLOW_BOTH 0 + +/** + * @ingroup soft_bodies + * Edge plastic flow: only a squeeze sets; a stretched edge springs back. + */ +#define R3_SOFT_EDGE_PLASTIC_FLOW_COMPRESSION 1 + +/** + * @ingroup soft_bodies + * Edge plastic flow: only a stretch sets; a squeezed edge springs back. + */ +#define R3_SOFT_EDGE_PLASTIC_FLOW_TENSION 2 + +/** + * @ingroup soft_bodies + * Overlap patch constraints (R3SoftRecoverySettings::overlapPatchConstraints): keep them. + */ +#define R3_SOFT_PATCH_CONSTRAINTS_KEEP 0 + +/** + * @ingroup soft_bodies + * Overlap patch constraints: stand them down inside the patch. + */ +#define R3_SOFT_PATCH_CONSTRAINTS_STAND_DOWN 1 + +/** + * @ingroup soft_bodies + * Overlap patch constraints: align them with the overlap normal. + */ +#define R3_SOFT_PATCH_CONSTRAINTS_ALONG_NORMAL 2 + /** * @ingroup joints * Joint axis index for translation along local X. @@ -9706,34 +11789,71 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context /** * @ingroup joints - * Skip joints that would close a loop in the articulation. + * Do not insert MJCF equality constraints (loop closures) as impulse joints. MJCF only: URDF + * insertion rejects it. */ #define R3_MULTIBODY_SKIP_LOOP_CLOSURES 4 /** * @ingroup joints - * Do not import joint motors into the articulation. + * Do not import joint motors into the articulation. MJCF only: URDF insertion rejects it. */ #define R3_MULTIBODY_SKIP_JOINT_MOTORS 8 /** * @ingroup joints - * Do not import joint limits into the articulation. + * Do not import joint limits into the articulation. MJCF only: URDF insertion rejects it. */ #define R3_MULTIBODY_SKIP_JOINT_LIMITS 16 /** * @ingroup joints - * Do not import joint springs into the articulation. + * Do not import joint springs into the articulation. MJCF only: URDF insertion rejects it. */ #define R3_MULTIBODY_SKIP_JOINT_SPRINGS 32 +/** + * @ingroup shapes + * Compute the half-edge topology of the triangle mesh. + */ +#define R3_TRIMESH_HALF_EDGE_TOPOLOGY 1 + +/** + * @ingroup shapes + * Compute the connected components of the triangle mesh. + */ +#define R3_TRIMESH_CONNECTED_COMPONENTS 2 + +/** + * @ingroup shapes + * Delete the triangles breaking the half-edge topology. + */ +#define R3_TRIMESH_DELETE_BAD_TOPOLOGY_TRIANGLES 4 + +/** + * @ingroup shapes + * Treat the triangle mesh as oriented (outward normals) and compute its pseudo-normals. + */ +#define R3_TRIMESH_ORIENTED 8 + /** * @ingroup shapes * Merge triangle-mesh vertices with identical positions. */ #define R3_TRIMESH_MERGE_DUPLICATE_VERTICES 16 +/** + * @ingroup shapes + * Delete the triangles with a zero area. + */ +#define R3_TRIMESH_DELETE_DEGENERATE_TRIANGLES 32 + +/** + * @ingroup shapes + * Delete the triangles sharing their three vertices with another triangle. + */ +#define R3_TRIMESH_DELETE_DUPLICATE_TRIANGLES 64 + /** * @ingroup shapes * Correct contact normals at internal mesh edges; includes duplicate-vertex merging. @@ -9758,6 +11878,144 @@ size_t RAPIER_CALL r2ReadRigidBodyReadStates(const struct R2ReadContext *context */ #define R3_HEIGHTFIELD_FIX_INTERNAL_EDGES 1 +/** + * @ingroup controllers + * Controller axis bit: translation along X. + */ +#define R3_AXES_MASK_LIN_X 1 + +/** + * @ingroup controllers + * Controller axis bit: translation along Y. + */ +#define R3_AXES_MASK_LIN_Y 2 + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: translation along Z. + */ +#define R3_AXES_MASK_LIN_Z 4 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: rotation about X. + */ +#define R3_AXES_MASK_ANG_X 8 +#endif + +#if defined(RAPIER_DIM3) +/** + * @ingroup controllers + * Controller axis bit: rotation about Y. + */ +#define R3_AXES_MASK_ANG_Y 16 +#endif + +/** + * @ingroup controllers + * Controller axis bit: rotation about Z (the only rotation axis in 2D). + */ +#define R3_AXES_MASK_ANG_Z 32 + +/** + * @ingroup errors + * ABI feature bit: RAPIER_FEM, which changes the layout of R3IntegrationParameters. + */ +#define R3_ABI_FEATURE_FEM 1 + +/** + * @ingroup errors + * ABI feature bit: RAPIER_ROBOTICS (3D, f32 only), which declares the URDF/MJCF API. + */ +#define R3_ABI_FEATURE_ROBOTICS 2 + +#if (defined(RAPIER_FEM) && defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup errors + * R3_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R3_ABI_FEATURES 3 +#endif + +#if (defined(RAPIER_FEM) && !(defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32))) +/** + * @ingroup errors + * R3_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R3_ABI_FEATURES 1 +#endif + +#if (!defined(RAPIER_FEM) && defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup errors + * R3_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R3_ABI_FEATURES 2 +#endif + +#if (!defined(RAPIER_FEM) && !(defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32))) +/** + * @ingroup errors + * R3_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. + */ +#define R3_ABI_FEATURES 0 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Load each mesh as a triangle mesh, with the given trimesh flags. + */ +#define R3_MESH_CONVERTER_TRIMESH 0 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its oriented bounding box. + */ +#define R3_MESH_CONVERTER_OBB 1 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its axis-aligned bounding box. + */ +#define R3_MESH_CONVERTER_AABB 2 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its convex hull. + */ +#define R3_MESH_CONVERTER_CONVEX_HULL 3 +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * @ingroup robotics + * Replace each mesh by its convex decomposition. + */ +#define R3_MESH_CONVERTER_CONVEX_DECOMPOSITION 4 +#endif + +/** + * @ingroup events + * Collision event flag: at least one of the colliders was a sensor when the event fired. + */ +#define R3_COLLISION_EVENT_SENSOR 1 + +/** + * @ingroup events + * Collision event flag: the collision stopped because at least one collider was removed. + */ +#define R3_COLLISION_EVENT_REMOVED 2 + /** * Immutable owned byte buffer. Release with the matching FreeBytes function. * @ingroup worlds @@ -9766,7 +12024,7 @@ typedef struct R3Bytes R3Bytes; /** * Borrowed native contact context. Valid only during its callback; never retain or free it. - * @ingroup events + * @ingroup callbacks */ typedef struct R3ContactModificationContext R3ContactModificationContext; @@ -9779,8 +12037,9 @@ typedef struct R3DynamicRayCastVehicleController R3DynamicRayCastVehicleControll #endif /** - * Events accumulate until clear. Copying events never drains them, allowing two-call buffer - * sizing. + * Events accumulate across steps until r3EventCollector_Clear: reading them never drains the + * collector, allowing two-call buffer sizing. Optional callbacks also see each event during the + * step. * @ingroup events */ typedef struct R3EventCollector R3EventCollector; @@ -9793,6 +12052,24 @@ typedef struct R3EventCollector R3EventCollector; */ typedef struct R3KinematicCharacterController R3KinematicCharacterController; +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Shapes loaded from a mesh file (STL, COLLADA or Wavefront OBJ), one per mesh of the file. + * Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R3LoadedMeshes R3LoadedMeshes; +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Owned physics hooks applying the `` rules (excluded pairs, pair friction) of an + * inserted MJCF robot. Release with the matching Free function. + * @ingroup robotics + */ +typedef struct R3MjcfContactHooks R3MjcfContactHooks; +#endif + #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) /** * Loaded MJCF robot and its visual/keyframe data. Release with the matching Free function. @@ -10001,7 +12278,7 @@ typedef struct R3SoftBodyMaterial { */ R3Real edgePlasticMax; /** - * Plastic flow direction: 0 both, 1 compression only, 2 tension only. + * Plastic flow direction: R3_SOFT_EDGE_PLASTIC_FLOW_BOTH, _COMPRESSION or _TENSION. */ uint32_t edgePlasticFlow; /** @@ -10108,8 +12385,8 @@ typedef struct R3SoftRecoverySettings { */ R3Real overlapConstraintPace; /** - * Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap - * normal. + * Per-point constraints inside overlap patches: R3_SOFT_PATCH_CONSTRAINTS_KEEP, _STAND_DOWN or + * _ALONG_NORMAL. */ uint32_t overlapPatchConstraints; /** @@ -10250,7 +12527,7 @@ typedef struct R3IntegrationParameters { */ size_t numSolverIterations; /** - * PGS iterations per solver substep. + * PGS iterations per solver substep; must be positive. */ size_t numInternalPgsIterations; /** @@ -10283,7 +12560,7 @@ typedef struct R3IntegrationParameters { R3Bool warmstartJoints; #if defined(RAPIER_DIM3) /** - * Friction model: 0 simplified, 1 Coulomb (3D only). + * Friction model of rigid-body contacts, R3_FRICTION_MODEL_* (3D only). */ uint32_t frictionModel; #endif @@ -10875,18 +13152,35 @@ typedef struct R3Dihedral { * Copying this view does not copy its data or extend its lifetime. No Free is needed. * Data must remain live through the build/insert call that reads the description. * NULL is permitted only when count is zero. - * @ingroup math + * @ingroup math + */ +typedef struct R3DihedralView { + /** + * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + */ + const struct R3Dihedral *data; + /** + * Number of elements, not bytes unless the element type is a byte. + */ + size_t count; +} R3DihedralView; + +/** + * Borrowed array of soft-body descriptions. count counts descriptions. + * Data must remain live through the build/insert call that reads the description. + * NULL is permitted only when count is zero. + * @ingroup soft_bodies */ -typedef struct R3DihedralView { +typedef struct R3SoftBodyDescView { /** * Borrowed pointer to contiguous elements; NULL is allowed when count is zero. */ - const struct R3Dihedral *data; + const struct R3SoftBodyDesc *data; /** * Number of elements, not bytes unless the element type is a byte. */ size_t count; -} R3DihedralView; +} R3SoftBodyDescView; /** * Optional boolean override. When disabled, retain the recipe's native default. @@ -10959,7 +13253,7 @@ typedef struct R3ShapeDesc { */ R3Real halfHeight; /** - * Rounding radius for a rounded shape. + * Rounding radius of a round cylinder or round cone (the round cuboid reads radius instead). */ R3Real borderRadius; /** @@ -11119,11 +13413,11 @@ typedef struct R3ColliderDesc { */ R3Real restitution; /** - * R3_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + * R3_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. */ uint32_t frictionCombineRule; /** - * R3_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + * R3_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. */ uint32_t restitutionCombineRule; /** @@ -11174,6 +13468,8 @@ typedef struct R3ColliderDesc { * for topology arrays are element counts (edges, triangles, or tetrahedra). * Nonempty topology overrides the generator's topology. Zero counts retain it. * Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. + * R3_SOFT_DESC_POLYGON reads positions; R3_SOFT_DESC_TRIMESH reads positions and cells (its + * triangles become edges and a boundary, not cells). * @ingroup soft_bodies */ typedef struct R3SoftBodyDesc { @@ -11197,6 +13493,24 @@ typedef struct R3SoftBodyDesc { * Cloth basis step along its second parameter axis. */ struct R3Vector dv; +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth edges along du (warp). + */ + struct R3SpringCoefficients warpSoftness; +#endif +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth edges along dv (weft). + */ + struct R3SpringCoefficients weftSoftness; +#endif +#if defined(RAPIER_DIM3) + /** + * Softness of the anisotropic cloth diagonal edges (shear). + */ + struct R3SpringCoefficients shearSoftness; +#endif /** * First recipe resolution; interpretation depends on kind. */ @@ -11289,6 +13603,18 @@ typedef struct R3SoftBodyDesc { * Borrowed skin topology. */ R3SurfaceElementView skinIndices; + /** + * Borrowed descriptions merged into this body, their particles numbered after this one's in + * order. Each contributes its particles, masses, pinned particles and elements (after its own + * translation and total mass); every other setting comes from this description. Appended + * descriptions cannot append others nor have a skin. + */ + struct R3SoftBodyDescView appended; + /** + * Borrowed structural edges added after appending (seams); indices count this body's particles + * then the appended ones. Their rest length is the current distance of their particles. + */ + struct R3EdgeView addedEdges; /** * Soft-body material coefficients. */ @@ -11321,6 +13647,11 @@ typedef struct R3SoftBodyDesc { * Optional shape-matching override; disabled retains recipe defaults. */ struct R3OptionalBool shapeMatching; + /** + * Optional override of the ORIENTED flag of the generated collision surface (when disabled, a + * closed surface is oriented). Set it to false for a shell whose inner side holds bodies. + */ + struct R3OptionalBool oriented; /** * Whether self-collision is enabled. */ @@ -11797,7 +14128,8 @@ typedef struct R3PodLayout { */ typedef struct R3CharacterLength { /** - * Value used when enabled is 1. + * Nonnegative length: a fraction of the character shape height when relative is 1, a + * world-space length otherwise. */ R3Real value; /** @@ -11806,6 +14138,29 @@ typedef struct R3CharacterLength { R3Bool relative; } R3CharacterLength; +/** + * Copy of the automatic stepping settings. + * @ingroup controllers + */ +typedef struct R3CharacterAutostep { + /** + * Whether automatic stepping is enabled. + */ + R3Bool enabled; + /** + * Maximum height of the steps climbed automatically. + */ + struct R3CharacterLength max_height; + /** + * Minimum free width required on top of a step. + */ + struct R3CharacterLength min_width; + /** + * Whether the character can also step over dynamic bodies. + */ + R3Bool include_dynamic_bodies; +} R3CharacterAutostep; + /** * Allowed character motion and ground-contact state. * @ingroup controllers @@ -11883,6 +14238,34 @@ typedef struct R3PidGains { R3AngVector ang_kd; } R3PidGains; +/** + * Stateless proportional-derivative controller: a PID controller without integral term, stored as + * a plain value. Initialize with r3DefaultPdController. + * @ingroup controllers + */ +typedef struct R3PdController { + /** + * Linear proportional gain per axis. + */ + struct R3Vector lin_kp; + /** + * Linear derivative gain per axis. + */ + struct R3Vector lin_kd; + /** + * Angular proportional gain per axis. + */ + R3AngVector ang_kp; + /** + * Angular derivative gain per axis. + */ + R3AngVector ang_kd; + /** + * Controlled axes, a combination of R3_AXES_MASK_* bits. + */ + uint32_t axes; +} R3PdController; + /** * Linear and angular velocity correction computed by a controller. * @ingroup controllers @@ -12102,7 +14485,7 @@ typedef struct R3CollisionEvent { */ R3Bool started; /** - * Event flags: bit 0 sensor pair, bit 1 removed collider. + * Bitmask of R3_COLLISION_EVENT_SENSOR and R3_COLLISION_EVENT_REMOVED. */ uint32_t flags; } R3CollisionEvent; @@ -12137,7 +14520,8 @@ typedef struct R3ContactForceEvent { */ R3Real max_force_magnitude; /** - * 1 for a starting event, 0 for a stopping event. + * 1 for the first step the total force magnitude exceeds the threshold, 0 on the following + * steps while it stays above it. No event is emitted when the force drops below it. */ R3Bool started; } R3ContactForceEvent; @@ -12146,7 +14530,7 @@ typedef struct R3ContactForceEvent { * Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes * impulses. * For sensor intersections only, zero rejects and any positive value accepts. - * @ingroup math + * @ingroup callbacks */ typedef int32_t (RAPIER_CALL *R3PairFilter)(void *user_data, const struct R3ReadContext *read, @@ -12262,7 +14646,7 @@ typedef struct R3DebugLine { */ struct R3Vector b; /** - * RGBA color, four floats. + * HSLA color: hue in degrees, then saturation, lightness and alpha in [0, 1]. */ float color[4]; } R3DebugLine; @@ -12387,6 +14771,11 @@ typedef struct R3BuildFeatures { * Whether this library exposes Rapier's parallel execution and thread-pool APIs. */ R3Bool parallel; + /** + * Whether the library is built with enhanced-determinism: the simulation, and the math + * functions such as r3Sin, give bit-identical results on every platform. + */ + R3Bool enhanced_determinism; } R3BuildFeatures; /** @@ -12426,7 +14815,7 @@ typedef struct R3ContactPair { /** * Sensor intersection state for a collider pair. - * @ingroup math + * @ingroup events */ typedef struct R3IntersectionPair { /** @@ -12472,6 +14861,18 @@ typedef struct R3ContactPoint { * Normal impulse applied at this contact. */ R3Real impulse; +#if defined(RAPIER_DIM2) + /** + * Friction impulse along the tangent basis of the contact. + */ + R3Real tangent_impulse[1]; +#endif +#if defined(RAPIER_DIM3) + /** + * Friction impulses along the two tangent basis vectors of the contact. + */ + R3Real tangent_impulse[2]; +#endif } R3ContactPoint; #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) @@ -12777,6 +15178,335 @@ typedef struct R3VoxelQuery { R3Bool found; } R3VoxelQuery; +/** + * Impulses applied by an impulse joint during the last step, along the axes of its joint frame. + * @ingroup joints + */ +typedef struct R3JointImpulses { + /** + * Impulse applied along the locked translational axes. + */ + struct R3Vector linear; + /** + * Angular impulse applied along the locked rotational axes (a scalar in 2D). + */ + R3AngVector angular; + /** + * Impulse applied by the limit of each axis, in translation-then-rotation order. + */ + R3Real limits[R3_JOINT_DOF_COUNT]; + /** + * Impulse applied by the motor of each axis, in translation-then-rotation order. + */ + R3Real motors[R3_JOINT_DOF_COUNT]; +} R3JointImpulses; + +/** + * Optional shape-cast result. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R3OptionalShapeCastHit { + /** + * Shape-cast impact details. + */ + struct R3ShapeCastHit hit; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalShapeCastHit; + +/** + * Optional point projection. A miss is found = 0 with status OK. + * @ingroup queries + */ +typedef struct R3OptionalPointProjection { + /** + * Closest projection details. + */ + struct R3PointProjection projection; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalPointProjection; + +/** + * Rigid motion with constant linear and angular velocities. At time t, the shape at start is + * rotated by angvel * t around its local_center point, then translated by linvel * t. + * @ingroup queries + */ +typedef struct R3NonlinearRigidMotion { + /** + * World-space pose at time zero. + */ + struct R3Pose start; + /** + * Rotation center, in the local coordinates of the moving shape. + */ + struct R3Vector local_center; + /** + * World-space linear velocity. + */ + struct R3Vector linvel; + /** + * World-space angular velocity, in radians per second. + */ + R3AngVector angvel; +} R3NonlinearRigidMotion; + +/** + * Optional contact pair; check found before reading the pair. + * @ingroup events + */ +typedef struct R3OptionalContactPair { + /** + * Contact pair summary. + */ + struct R3ContactPair pair; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalContactPair; + +/** + * Optional intersection pair; check found before reading the pair. + * @ingroup events + */ +typedef struct R3OptionalIntersectionPair { + /** + * Intersection pair state. + */ + struct R3IntersectionPair pair; + /** + * Whether a result exists; other result fields are meaningful only when this is 1. + */ + R3Bool found; +} R3OptionalIntersectionPair; + +/** + * Geometric contact manifold of a contact pair: contacts sharing one normal. Local data follow the + * pair's own collider1/collider2 order (see r3ContactPair). + * @ingroup events + */ +typedef struct R3ContactManifold { + /** + * Contact normal in collider 1 local coordinates, pointing outward from it. + */ + struct R3Vector local_n1; + /** + * Contact normal in collider 2 local coordinates, pointing outward from it. + */ + struct R3Vector local_n2; + /** + * World-space contact normal, pointing from collider 1 toward collider 2. + */ + struct R3Vector normal; + /** + * Index of the subshape of collider 1 (for composite shapes), zero otherwise. + */ + uint32_t subshape1; + /** + * Index of the subshape of collider 2 (for composite shapes), zero otherwise. + */ + uint32_t subshape2; + /** + * Number of geometric contact points; see r3ContactPoints. + */ + size_t num_points; + /** + * Number of solver contacts; see r3SolverContacts. + */ + size_t num_solver_contacts; + /** + * Application data, persistent across steps and editable by contact-modification hooks. + */ + uint32_t user_data; +} R3ContactManifold; + +/** + * Contact seen by the constraint solver. Points are world-space, on each body's surface. + * @ingroup events + */ +typedef struct R3SolverContact { + /** + * World-space contact point on collider 1's body. + */ + struct R3Vector point1; + /** + * World-space contact point on collider 2's body. + */ + struct R3Vector point2; + /** + * Signed separation along the normal, contact skins deducted; negative means penetration. + */ + R3Real distance; + /** + * Desired world-space tangent relative velocity, e.g. for conveyor belts; zero by default. + */ + struct R3Vector tangent_velocity; +} R3SolverContact; + +/** + * Called during the step for each collision event, after it was added to the collector. contacts + * holds the geometric contacts of the pair at that time (none for sensors), in the event's + * collider order; it is borrowed for this call only. + * @ingroup events + */ +typedef void (RAPIER_CALL *R3CollisionEventCallback)(void *user_data, + const struct R3ReadContext *read, + const struct R3CollisionEvent *event, + const struct R3ContactPoint *contacts, + size_t contact_count); + +/** + * Called during the step for each contact-force event, after it was added to the collector. + * @ingroup events + */ +typedef void (RAPIER_CALL *R3ContactForceEventCallback)(void *user_data, + const struct R3ReadContext *read, + const struct R3ContactForceEvent *event); + +/** + * Callbacks invoked while stepping, in addition to collecting the events. They follow the + * R3PhysicsHooks rules: never unwind or retain arguments, read through the ReadContext, never + * mutate the world, and be safe for concurrent invocation in parallel builds. NULL callbacks are + * skipped. + * @ingroup events + */ +typedef struct R3EventCallbacks { + /** + * Application data; Rapier does not own pointers encoded in it. + */ + void *user_data; + /** + * Optional collision start/stop callback. + */ + R3CollisionEventCallback collision_event; + /** + * Optional contact-force callback. + */ + R3ContactForceEventCallback contact_force_event; +} R3EventCallbacks; + +/** + * Debug-render colors and sizes. Colors are HSLA: hue in degrees, then saturation, lightness and + * alpha in [0, 1]; multipliers scale each component. Initialize with + * r3DefaultDebugRenderStyle. + * @ingroup events + */ +typedef struct R3DebugRenderStyle { + /** + * Positive number of subdivisions approximating curved shapes. + */ + uint32_t subdivisions; + /** + * Positive number of subdivisions approximating the borders of round shapes. + */ + uint32_t border_subdivisions; + /** + * Color of colliders attached to dynamic bodies. + */ + float collider_dynamic_color[4]; + /** + * Color of colliders attached to fixed bodies. + */ + float collider_fixed_color[4]; + /** + * Color of colliders attached to kinematic bodies. + */ + float collider_kinematic_color[4]; + /** + * Color of colliders without a parent body. + */ + float collider_parentless_color[4]; + /** + * Color of the lines from a body's center of mass to its impulse-joint anchors. + */ + float impulse_joint_anchor_color[4]; + /** + * Color of the line between the two anchors of an impulse joint. + */ + float impulse_joint_separation_color[4]; + /** + * Color of the lines from a body's center of mass to its multibody-joint anchors. + */ + float multibody_joint_anchor_color[4]; + /** + * Color of the line between the two anchors of a multibody joint. + */ + float multibody_joint_separation_color[4]; + /** + * Color multiplier for entities of sleeping bodies. + */ + float sleep_color_multiplier[4]; + /** + * Color multiplier for entities of awake bodies eligible for sleep. + */ + float sleep_eligible_color_multiplier[4]; + /** + * Color multiplier for entities of disabled bodies. + */ + float disabled_color_multiplier[4]; + /** + * Nonnegative length of the rendered body axes. + */ + R3Real rigid_body_axes_length; + /** + * Color of the segments joining the two points of a contact. + */ + float contact_depth_color[4]; + /** + * Color of the contact normals. + */ + float contact_normal_color[4]; + /** + * Nonnegative length of the contact normals. + */ + R3Real contact_normal_length; + /** + * Color of soft-body elements. + */ + float soft_body_element_color[4]; + /** + * Color of unloaded soft-body elements when coloring them by load. + */ + float soft_body_slack_color[4]; + /** + * Color of soft-body elements at their tear threshold when coloring them by load. + */ + float soft_body_loaded_color[4]; + /** + * Color of the soft-body cluster frames. + */ + float soft_body_frame_color[4]; + /** + * Color of the collider bounding boxes. + */ + float collider_aabb_color[4]; + /** + * Color of the vertex pseudo-normals of triangle meshes and polylines. + */ + float vertex_pseudo_normal_color[4]; + /** + * Color of the edge pseudo-normals of triangle meshes (3D only). + */ + float edge_pseudo_normal_color[4]; + /** + * Nonnegative length of the pseudo-normals. + */ + R3Real pseudo_normal_length; + /** + * Color of the normals of soft-body volume contacts. + */ + float volume_contact_normal_color[4]; + /** + * Color of the volume gradients drawn at the particles of a volume constraint. + */ + float volume_gradient_color[4]; +} R3DebugRenderStyle; + /** * @ingroup errors * Operation succeeded. @@ -13280,6 +16010,44 @@ R3Status RAPIER_CALL r3KinematicCharacterController_SetSnapToGround(struct R3Kin R3Bool enabled, struct R3CharacterLength distance); +/** + * Return the normalized up direction. + * @ingroup controllers + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3KinematicCharacterController_Up(const struct R3KinematicCharacterController *controller); + +/** + * Return the collision separation margin. + * @ingroup controllers + */ +RAPIER_API +struct R3CharacterLength RAPIER_CALL r3KinematicCharacterController_Offset(const struct R3KinematicCharacterController *controller); + +/** + * Return the automatic stepping settings. When disabled, enabled is 0 and the other fields hold + * Rapier's defaults. + * @ingroup controllers + */ +RAPIER_API +struct R3CharacterAutostep RAPIER_CALL r3KinematicCharacterController_Autostep(const struct R3KinematicCharacterController *controller); + +/** + * Set the small distance by which sliding motion is pushed along hit normals to avoid getting stuck; + * it must be finite and nonnegative. Large values cause bumps when sliding on flat ground. + * @ingroup controllers + */ +RAPIER_API +R3Status RAPIER_CALL r3KinematicCharacterController_SetNormalNudgeFactor(struct R3KinematicCharacterController *controller, + R3Real value); + +/** + * Return the normal nudge factor set by SetNormalNudgeFactor. + * @ingroup controllers + */ +RAPIER_API +R3Real RAPIER_CALL r3KinematicCharacterController_NormalNudgeFactor(const struct R3KinematicCharacterController *controller); + /** * Computes movement without moving any collider. Use the returned translation to set the character * target. @@ -13307,8 +16075,10 @@ size_t RAPIER_CALL r3KinematicCharacterController_Collisions(const struct R3Kine size_t capacity); /** - * Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and - * filter. + * Applies impulses to the dynamic bodies hit by the most recent MoveShape call. Use the same + * shape, dt and query options as that call; NULL options use the default filter. + * Unlike MoveShape, the options' predicate is called once per collider of the world before the + * impulses are applied, while the world is locked for writing: it may only use Read* functions. * @ingroup controllers */ RAPIER_API @@ -13316,11 +16086,11 @@ R3Status RAPIER_CALL r3KinematicCharacterController_SolveCharacterCollisionImpul const R3SharedShape *shape, R3Real dt, R3Real mass, - const struct R3QueryFilter *filter); + const struct R3QueryOptions *options); /** - * Allocate a PID controller with supplied gains and controlled axes. Release with - * r3FreePidController. + * Allocate a PID controller with Rapier's defaults: kp = 60, ki = 1 and kd = 0.8 on every axis, all + * axes controlled, and zero integrals. Release with r3FreePidController. * @ingroup controllers */ RAPIER_API struct R3PidController *RAPIER_CALL r3NewPidController(void); @@ -13347,13 +16117,45 @@ R3Status RAPIER_CALL r3PidController_SetGains(struct R3PidController *controller struct R3PidGains gains); /** - * AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. + * Set the controlled axes, a combination of R3_AXES_MASK_* bits. Gains are unchanged; unknown + * bits are rejected. * @ingroup controllers */ RAPIER_API R3Status RAPIER_CALL r3PidController_SetAxes(struct R3PidController *controller, uint32_t axes); +/** + * Return the controlled axes as R3_AXES_MASK_* bits. + * @ingroup controllers + */ +RAPIER_API uint32_t RAPIER_CALL r3PidController_Axes(const struct R3PidController *controller); + +/** + * Reset to zero the linear and angular errors accumulated by the integral term. + * @ingroup controllers + */ +RAPIER_API R3Status RAPIER_CALL r3PidController_ResetIntegrals(struct R3PidController *controller); + +/** + * Return Rapier's default PD controller: kp = 60 and kd = 0.8 on every axis, all axes controlled. + * This POD value owns no resources. + * @ingroup controllers + */ +RAPIER_API struct R3PdController RAPIER_CALL r3DefaultPdController(void); + +/** + * Compute the velocity change bringing the body toward the target pose and velocities. Neither the + * body nor the controller is modified. + * @ingroup controllers + */ +RAPIER_API +struct R3VelocityCorrection RAPIER_CALL r3PdController_RigidBodyCorrection(const struct R3PdController *controller, + struct R3RigidBodyHandle body, + struct R3Pose target_pose, + struct R3Vector target_linvel, + R3AngVector target_angvel); + /** * Compute a velocity correction, preserving the body's state and updating PID integrals. * @ingroup controllers @@ -13444,12 +16246,16 @@ R3Status RAPIER_CALL r3DynamicRayCastVehicleController_SetWheelControls(struct R #if defined(RAPIER_DIM3) /** * Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + * NULL options use the default filter. The chassis colliders are always excluded, in addition + * to the filter's own exclusions. The options' predicate is called once per collider of the + * world before the update, while the world is locked for writing: it may only use Read* + * functions. * @ingroup controllers */ RAPIER_API R3Status RAPIER_CALL r3DynamicRayCastVehicleController_UpdateVehicle(struct R3DynamicRayCastVehicleController *controller, R3Real dt, - const struct R3QueryFilter *filter); + const struct R3QueryOptions *options); #endif #if defined(RAPIER_DIM3) @@ -13647,7 +16453,9 @@ RAPIER_API struct R3JointBodies RAPIER_CALL r3ImpulseJoint_Bodies(struct R3Impul RAPIER_API struct R3InverseKinematicsOptions RAPIER_CALL r3DefaultInverseKinematicsOptions(void); /** - * Return the articulation degrees of freedom associated with the joint. + * Return the degrees of freedom of the whole multibody containing the joint (not of the joint + * alone), including the free root of a dynamic multibody. After inserting a joint, the root's + * contribution is only updated by the next step. * @ingroup joints */ RAPIER_API size_t RAPIER_CALL r3MultibodyJoint_Ndofs(struct R3MultibodyJointHandle handle); @@ -13757,12 +16565,14 @@ RAPIER_API R3Bool RAPIER_CALL r3SoftBody_Contains(struct R3SoftBodyHandle handle /** * Remove a body and its joints, optionally keeping colliders as standalone objects. - * Returns whether a body was removed; a stale handle returns false without error. + * A removed or stale handle fails with R3_INVALID_HANDLE, like the other Remove functions. + * Removing a soft-body cluster proxy removes its cluster (see r3SoftBody_RemoveCluster). The + * root body of a soft body is rejected: remove the soft body with r3RemoveSoftBody. * @ingroup rigid_bodies */ RAPIER_API -R3Bool RAPIER_CALL r3RemoveRigidBody(struct R3RigidBodyHandle handle, - R3Bool remove_attached_colliders); +R3Status RAPIER_CALL r3RemoveRigidBody(struct R3RigidBodyHandle handle, + R3Bool remove_attached_colliders); /** * Return the world setting documented by R3IntegrationParameters::dt. @@ -13905,14 +16715,14 @@ RAPIER_API R3Status RAPIER_CALL r3SetNumInternalPgsIterations(struct R3World *wo /** * Return the world setting documented by * R3IntegrationParameters::numInternalStabilizationIterations. - * @ingroup errors + * @ingroup worlds */ RAPIER_API size_t RAPIER_CALL r3NumInternalStabilizationIterations(const struct R3World *world); /** * Set the world setting documented by * R3IntegrationParameters::numInternalStabilizationIterations. - * @ingroup errors + * @ingroup worlds */ RAPIER_API R3Status RAPIER_CALL r3SetNumInternalStabilizationIterations(struct R3World *world, @@ -13968,25 +16778,25 @@ RAPIER_API R3Status RAPIER_CALL r3SetFrictionInBiasPass(struct R3World *world, R /** * Return the world setting documented by R3IntegrationParameters::warmstartJoints. - * @ingroup joints + * @ingroup worlds */ RAPIER_API R3Bool RAPIER_CALL r3WarmstartJoints(const struct R3World *world); /** * Set the world setting documented by R3IntegrationParameters::warmstartJoints. - * @ingroup joints + * @ingroup worlds */ RAPIER_API R3Status RAPIER_CALL r3SetWarmstartJoints(struct R3World *world, R3Bool value); /** * Return the world setting documented by R3IntegrationParameters::contactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API struct R3SpringCoefficients RAPIER_CALL r3ContactSoftness(const struct R3World *world); /** * Set the world setting documented by R3IntegrationParameters::contactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API R3Status RAPIER_CALL r3SetContactSoftness(struct R3World *world, @@ -13994,13 +16804,13 @@ R3Status RAPIER_CALL r3SetContactSoftness(struct R3World *world, /** * Return the world setting documented by R3IntegrationParameters::staticContactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API struct R3SpringCoefficients RAPIER_CALL r3StaticContactSoftness(const struct R3World *world); /** * Set the world setting documented by R3IntegrationParameters::staticContactSoftness. - * @ingroup soft_bodies + * @ingroup worlds */ RAPIER_API R3Status RAPIER_CALL r3SetStaticContactSoftness(struct R3World *world, @@ -14008,7 +16818,7 @@ R3Status RAPIER_CALL r3SetStaticContactSoftness(struct R3World *world, /** * Applies Rapier's persistent one-way platform logic to the borrowed manifold. - * @ingroup worlds + * @ingroup callbacks */ RAPIER_API R3Status RAPIER_CALL r3ContactModificationContext_UpdateAsOnewayPlatform(struct R3ContactModificationContext *context, @@ -14017,7 +16827,7 @@ R3Status RAPIER_CALL r3ContactModificationContext_UpdateAsOnewayPlatform(struct /** * Sets the tangent velocity of every rigid solver contact in this manifold. - * @ingroup worlds + * @ingroup callbacks */ RAPIER_API R3Status RAPIER_CALL r3ContactModificationContext_SetTangentVelocity(struct R3ContactModificationContext *context, @@ -14037,13 +16847,13 @@ RAPIER_API struct R3EventCollector *RAPIER_CALL r3NewEventCollector(void); RAPIER_API R3Status RAPIER_CALL r3FreeEventCollector(struct R3EventCollector *events); /** - * Discard all collected events. Does not change the world. + * Discard all collected events. Does not change the world or the callbacks. * @ingroup events */ RAPIER_API R3Status RAPIER_CALL r3EventCollector_Clear(struct R3EventCollector *events); /** - * Copy the collected collision start/stop events without removing them. + * Copy the collision start/stop events collected since the last clear, without removing them. * @see @ref output_buffers * @ingroup events */ @@ -14053,7 +16863,7 @@ size_t RAPIER_CALL r3EventCollector_CollisionEvents(const struct R3EventCollecto size_t capacity); /** - * Copy the collected contact-force events without removing them. + * Copy the contact-force events collected since the last clear, without removing them. * @see @ref output_buffers * @ingroup events */ @@ -14063,7 +16873,7 @@ size_t RAPIER_CALL r3EventCollector_ContactForceEvents(const struct R3EventColle size_t capacity); /** - * Return the number of queued soft-body tear events. + * Return the number of soft-body tear events collected since the last clear. * @ingroup events */ RAPIER_API size_t RAPIER_CALL r3EventCollector_TearEventCount(const struct R3EventCollector *events); @@ -14090,8 +16900,9 @@ RAPIER_API struct R3Vector RAPIER_CALL r3Gravity(const struct R3World *world); RAPIER_API R3Status RAPIER_CALL r3SetGravity(struct R3World *world, struct R3Vector value); /** - * Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. - * Advance simulation by one timestep. Hooks and events may be NULL. + * Advance simulation by one timestep. Hooks and events may be NULL. Events are appended to the + * collector, which is never cleared automatically. This call invalidates all borrowed set-element + * pointers. * @ingroup worlds */ RAPIER_API @@ -14100,7 +16911,8 @@ R3Status RAPIER_CALL r3Step(struct R3World *world, const struct R3EventCollector *events); /** - * Refresh collision detection without advancing simulation. Hooks and events may be NULL. + * Refresh collision detection without advancing simulation. Hooks and events may be NULL; events + * are appended to the collector. * @ingroup worlds */ RAPIER_API @@ -14137,9 +16949,10 @@ RAPIER_API struct R3Bytes *RAPIER_CALL r3SerializeWorld(const struct R3World *wo RAPIER_API struct R3World *RAPIER_CALL r3DeserializeWorld(const uint8_t *data, size_t count); /** - * Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. + * Copy the debug-render lines of the world with the default style. mode combines R3_DEBUG_* bits; + * colors are HSLA (hue in degrees), matching Rapier DebugColor. * @see @ref output_buffers - * @ingroup worlds + * @ingroup events */ RAPIER_API size_t RAPIER_CALL r3DebugRender(const struct R3World *world, @@ -14459,7 +17272,9 @@ RAPIER_API struct R3SoftBodyHandle RAPIER_CALL r3SoftBodyTearEvent_SoftBody(const struct R3SoftBodyTearEvent *event); /** - * Copy the soft-body handles produced by the tear. + * Copy the soft bodies the torn body is in after the tear, the one keeping the handle first: the + * torn body alone when nothing was split off. Entry i holds the particles given by + * r3SoftBodyTearEvent_PieceParticles(event, i, ...). * @see @ref output_buffers * @ingroup soft_bodies */ @@ -14527,7 +17342,9 @@ size_t RAPIER_CALL r3SoftBodyTearEvent_InsertedParticles(const struct R3SoftBody size_t capacity); /** - * Copy original particle indices belonging to a resulting piece. + * Copy the particles of the piece_index-th body of r3SoftBodyTearEvent_Bodies, as indices in + * the torn body after the tear (the indices the other event fields use); entry i is the piece's + * particle i. piece_index must be less than r3SoftBodyTearEvent_PieceCount. * @see @ref output_buffers * @ingroup soft_bodies */ @@ -14636,7 +17453,7 @@ RAPIER_API const char *RAPIER_CALL r3Version(void); RAPIER_API const char *RAPIER_CALL r3BuildProfile(void); /** - * Return profiling, SIMD width, and parallelism of the linked library. + * Return profiling, SIMD width, parallelism, and determinism of the linked library. * @ingroup errors */ RAPIER_API struct R3BuildFeatures RAPIER_CALL r3BuildFeatures(void); @@ -14688,7 +17505,8 @@ size_t RAPIER_CALL r3ContactPairs(const struct R3World *world, size_t capacity); /** - * Return the narrow-phase contact pair for two colliders, or report R3_NOT_FOUND. + * Return the narrow-phase contact pair for two colliders, or report R3_NOT_FOUND. Its collider1 and + * collider2 follow the narrow-phase order, which may differ from the argument order. * @ingroup events */ RAPIER_API @@ -14708,9 +17526,11 @@ size_t RAPIER_CALL r3IntersectionPairs(const struct R3World *world, /** * Contact points in collider-local space; normal in world space. Geometric manifolds may be * recycled. + * local_p1/local_p2 follow the pair's own collider1/collider2 order (see r3ContactPair), which + * may differ from the argument order. * For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. * @see @ref output_buffers - * @ingroup worlds + * @ingroup events */ RAPIER_API size_t RAPIER_CALL r3ContactPoints(struct R3ColliderHandle collider1, @@ -14738,7 +17558,11 @@ R3Status RAPIER_CALL r3MultibodyJoint_SetGeneralizedVelocity(struct R3MultibodyJ size_t count); /** - * Check this before passing any dimension/precision-dependent structs across the ABI. + * Check that the header matches the linked library before passing any structure across the ABI. + * Pass R3_ABI_VERSION, R3_DIMENSION, the sizes of R3Real, R3Vector and R3Pose, and + * R3_ABI_FEATURES. Fails with R3_INVALID_ARGUMENT when the version, dimension, precision, or + * the RAPIER_FEM/RAPIER_ROBOTICS defines differ from the library, since they change structure + * layouts. * @ingroup errors */ RAPIER_API @@ -14746,7 +17570,119 @@ R3Status RAPIER_CALL r3CheckAbi(uint32_t version, uint32_t dimension, size_t real_size, size_t vector_size, - size_t pose_size); + size_t pose_size, + uint32_t features); + +/** + * Copy the rigid bodies quarantined by the most recent Step because their pose or velocity became + * non-finite (NaN or infinite). Rapier disabled them, restored their last valid pose when known, + * and zeroed their velocities and forces; re-enable them with RigidBody_SetEnabled once the cause + * is fixed. The list is cleared at the start of every Step and may hold handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3QuarantinedRigidBodies(const struct R3World *world, + struct R3RigidBodyHandle *buffer, + size_t capacity); + +/** + * Copy the colliders quarantined by the most recent Step because their own pose or shape became + * non-finite, independently of their parent. Rapier disabled them; re-enable them with + * Collider_SetEnabled once fixed. The list is cleared at the start of every Step and may hold + * handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3QuarantinedColliders(const struct R3World *world, + struct R3ColliderHandle *buffer, + size_t capacity); + +/** + * Copy the soft bodies quarantined by the most recent Step because a particle position or velocity + * became non-finite. Rapier disabled them and zeroed their velocities but left the non-finite + * positions: fix them with SoftBody_SetParticlePosition before SoftBody_SetEnabled. The list is + * cleared at the start of every Step and may hold handles removed since. + * @see @ref output_buffers + * @ingroup worlds + */ +RAPIER_API +size_t RAPIER_CALL r3QuarantinedSoftBodies(const struct R3World *world, + struct R3SoftBodyHandle *buffer, + size_t capacity); + +#if defined(RAPIER_DIM3) +/** + * Return the world setting documented by R3IntegrationParameters::frictionModel. + * @ingroup worlds + */ +RAPIER_API uint32_t RAPIER_CALL r3FrictionModel(const struct R3World *world); +#endif + +#if defined(RAPIER_DIM3) +/** + * Set the world setting documented by R3IntegrationParameters::frictionModel. + * @ingroup worlds + */ +RAPIER_API R3Status RAPIER_CALL r3SetFrictionModel(struct R3World *world, uint32_t value); +#endif + +/** + * Sine of an angle in radians, computed by Rapier's math backend. With enhanced-determinism + * (see BuildFeatures), the result is identical on every platform. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Sin(R3Real x); + +/** + * Cosine of an angle in radians, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Cos(R3Real x); + +/** + * Tangent of an angle in radians, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Tan(R3Real x); + +/** + * Arcsine in radians, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Asin(R3Real x); + +/** + * Arccosine in radians, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Acos(R3Real x); + +/** + * Angle in radians of the point (x, y), in [-pi, pi], computed by Rapier's math backend. See + * r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Atan2(R3Real y, R3Real x); + +/** + * Exponential e^x, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Exp(R3Real x); + +/** + * Natural logarithm, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Ln(R3Real x); + +/** + * base raised to the power exponent, computed by Rapier's math backend. See r3Sin. + * @ingroup math + */ +RAPIER_API R3Real RAPIER_CALL r3Powf(R3Real base, R3Real exponent); /** * Return owned local-space rendering geometry; release it with r3FreeShapeMesh. subdivisions @@ -14795,10 +17731,23 @@ R3SharedShape *RAPIER_CALL r3RoundCylinderSharedShape(R3Real half_height, R3Real border_radius); #endif +#if defined(RAPIER_DIM3) +/** + * Create an owned round cone shape. Release it with r3FreeSharedShape. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3RoundConeSharedShape(R3Real half_height, + R3Real radius, + R3Real border_radius); +#endif + #if defined(RAPIER_DIM3) /** * Tessellate a ball or capsule with independent longitude/latitude subdivision counts. * Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. + * ntheta (3 to 4096) is read by balls, capsules, cones and cylinders; nphi (2 to 4096) by balls + * and capsules. Other shapes ignore them. * @ingroup shapes */ RAPIER_API @@ -14910,7 +17859,8 @@ struct R3UrdfRobotHandles *RAPIER_CALL r3UrdfRobot_InsertUsingMultibodyJoints(st #if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) /** - * Body handles in source order; absent MJCF bodies have invalid handles. + * One body handle per imported URDF link, in source order. Links merged away by + * squeezeEmptyFixedLinks have no entry. * @see @ref output_buffers * @ingroup robotics */ @@ -15185,6 +18135,105 @@ size_t RAPIER_CALL r3MjcfVisualMesh_Texture(const R3MjcfVisualMesh *visual, size_t capacity); #endif +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load a URDF robot from a NUL-terminated UTF-8 string. Relative mesh paths are resolved from + * mesh_dir (NULL resolves them from the current directory). Options and their blueprint resources + * are borrowed through this call; the robot is owned. + * @ingroup robotics + */ +RAPIER_API +struct R3UrdfRobot *RAPIER_CALL r3UrdfRobotFromString(const char *urdf, + const char *mesh_dir, + const struct R3UrdfLoaderOptions *options); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release owned MJCF contact hooks. NULL is allowed. Do not free them while a step still uses + * them, and do not free them twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeMjcfContactHooks(struct R3MjcfContactHooks *hooks); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Build the contact rules of an inserted MJCF robot. robot must be the robot these handles were + * inserted from. The rules refer to the inserted colliders; the returned hooks are owned. + * @ingroup robotics + */ +RAPIER_API +struct R3MjcfContactHooks *RAPIER_CALL r3MjcfRobotHandles_ContactHooks(const struct R3MjcfRobotHandles *handles, + const struct R3MjcfRobot *robot); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return physics hooks forwarding to these contact rules, with hooks as their user_data. Pass + * them to r3Step; hooks must outlive every step using them. The inserted colliders already + * enable the contact-filtering and contact-modification hooks. + * @ingroup robotics + */ +RAPIER_API +struct R3PhysicsHooks RAPIER_CALL r3MjcfContactHooks_PhysicsHooks(const struct R3MjcfContactHooks *hooks); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Release owned loaded meshes. NULL is allowed. Do not pass borrowed pointers or free the object + * twice. + * @ingroup robotics + */ +RAPIER_API R3Status RAPIER_CALL r3FreeLoadedMeshes(struct R3LoadedMeshes *meshes); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Load every mesh of a file from a UTF-8 path and convert it into a shape with converter (an + * R3_MESH_CONVERTER_* value). trimesh_flags (R3_TRIMESH_* bits) apply to + * R3_MESH_CONVERTER_TRIMESH and must be 0 otherwise. scale multiplies the vertices before + * conversion. A mesh failing to convert does not fail the load; see r3LoadedMeshes_CloneShape. + * @ingroup robotics + */ +RAPIER_API +struct R3LoadedMeshes *RAPIER_CALL r3LoadedMeshesFromFile(const char *path, + uint32_t converter, + uint32_t trimesh_flags, + struct R3Vector scale); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the number of meshes read from the file, including those that failed to convert. + * @ingroup robotics + */ +RAPIER_API size_t RAPIER_CALL r3LoadedMeshes_Count(const struct R3LoadedMeshes *meshes); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return an owned shape wrapper sharing the geometry of a loaded mesh. Release it with + * FreeSharedShape. Returns NULL with INVALID_ARGUMENT if the index is out of range or if that + * mesh failed to convert. + * @ingroup robotics + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3LoadedMeshes_CloneShape(const struct R3LoadedMeshes *meshes, + size_t index); +#endif + +#if (defined(RAPIER_ROBOTICS) && defined(RAPIER_DIM3) && defined(RAPIER_F32)) +/** + * Return the pose to give the shape of a loaded mesh (for example the center of its bounding + * box). Reports INVALID_ARGUMENT if the index is out of range or if that mesh failed to convert. + * @ingroup robotics + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3LoadedMeshes_Pose(const struct R3LoadedMeshes *meshes, + size_t index); +#endif + /** * Return the rigid body world-space pose. * @ingroup rigid_bodies @@ -15404,7 +18453,8 @@ RAPIER_API R3Bool RAPIER_CALL r3Collider_IsSensor(struct R3ColliderHandle handle RAPIER_API struct R3RigidBodyHandle RAPIER_CALL r3Collider_Parent(struct R3ColliderHandle handle); /** - * Set the collider world-space pose. + * Set the collider world-space pose. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. * @ingroup colliders */ RAPIER_API @@ -15412,7 +18462,8 @@ R3Status RAPIER_CALL r3Collider_SetPosition(struct R3ColliderHandle handle, struct R3Pose value); /** - * Set the collider world-space translation. + * Set the collider world-space translation. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. * @ingroup colliders */ RAPIER_API @@ -15522,17 +18573,90 @@ size_t RAPIER_CALL r3RigidBodyReadStates(const struct R3World *world, * Copies joint configuration without returning a borrowed joint pointer. * @ingroup joints */ -RAPIER_API struct R3JointDesc RAPIER_CALL r3ImpulseJoint_Desc(struct R3ImpulseJointHandle handle); +RAPIER_API struct R3JointDesc RAPIER_CALL r3ImpulseJoint_Desc(struct R3ImpulseJointHandle handle); + +/** + * Replaces configuration after validation, resetting cached limit/motor impulses. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3ImpulseJoint_SetDesc(struct R3ImpulseJointHandle handle, + const struct R3JointDesc *desc, + R3Bool wake_up); + +/** + * Return the collider body-type collision activation bitmask (R3_COLLISION_TYPES_* bits). + * @ingroup colliders + */ +RAPIER_API uint16_t RAPIER_CALL r3Collider_ActiveCollisionTypes(struct R3ColliderHandle handle); + +/** + * Return the collider physics-hook activation bitmask. + * @ingroup colliders + */ +RAPIER_API uint32_t RAPIER_CALL r3Collider_ActiveHooks(struct R3ColliderHandle handle); + +/** + * Return the collider friction combination rule (R3_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API uint32_t RAPIER_CALL r3Collider_FrictionCombineRule(struct R3ColliderHandle handle); + +/** + * Return the collider restitution combination rule (R3_COMBINE_*). + * @ingroup colliders + */ +RAPIER_API uint32_t RAPIER_CALL r3Collider_RestitutionCombineRule(struct R3ColliderHandle handle); + +/** + * Return the collider pose relative to its parent rigid body, or its world-space pose if it has + * no parent. + * @ingroup colliders + */ +RAPIER_API struct R3Pose RAPIER_CALL r3Collider_PositionWrtParent(struct R3ColliderHandle handle); + +/** + * Return the rigid body signed dominance group. + * @ingroup rigid_bodies + */ +RAPIER_API int8_t RAPIER_CALL r3RigidBody_DominanceGroup(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body additional solver iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r3RigidBody_AdditionalSolverIterations(struct R3RigidBodyHandle handle); + +/** + * Return the rigid body additional PGS iterations for connected bodies. + * @ingroup rigid_bodies + */ +RAPIER_API size_t RAPIER_CALL r3RigidBody_AdditionalPgsIterations(struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body may exceed the angular-velocity limit of its CCD. + * @ingroup rigid_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RigidBody_IsFastRotationAllowed(struct R3RigidBodyHandle handle); + +/** + * Set the collider world-space rotation. For a collider attached to a rigid body, prefer + * SetPositionWrtParent: the body pose overwrites it at the next step. + * @ingroup colliders + */ +RAPIER_API +R3Status RAPIER_CALL r3Collider_SetRotation(struct R3ColliderHandle handle, + struct R3Rotation value); /** - * Replaces configuration after validation, resetting cached limit/motor impulses. - * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. - * @ingroup joints + * Allow or disallow the rigid body to exceed the angular-velocity limit of its CCD (e.g. for + * wheels). + * @ingroup rigid_bodies */ RAPIER_API -R3Status RAPIER_CALL r3ImpulseJoint_SetDesc(struct R3ImpulseJointHandle handle, - const struct R3JointDesc *desc, - R3Bool wake_up); +R3Status RAPIER_CALL r3RigidBody_SetAllowFastRotation(struct R3RigidBodyHandle handle, + R3Bool value); /** * Replace the shape geometry with a borrowed tri mesh. Counts are elements. @@ -15583,6 +18707,39 @@ R3Status RAPIER_CALL r3SoftBodyDesc_SetSurfaceMesh(struct R3SoftBodyDesc *desc, struct R3VectorView vertices, R3SurfaceElementView elements); +#if defined(RAPIER_DIM2) +/** + * Select a 2D triangle-mesh recipe and borrow its vertices and triangles (stored in positions and + * cells). The triangles become structural edges and a boundary, not cells, and shape matching + * holds the shape. Other fields are preserved. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetTrimesh(struct R3SoftBodyDesc *desc, + struct R3VectorView vertices, + struct R3TriangleView triangles); +#endif + +/** + * Borrow descriptions to merge into this body (see R3SoftBodyDesc::appended); preserve all other + * fields. No allocation or element reads. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetAppended(struct R3SoftBodyDesc *desc, + struct R3SoftBodyDescView view); + +/** + * Borrow structural edges added after appending (see R3SoftBodyDesc::addedEdges); preserve all + * other fields. No allocation or element reads. + * Invalid view metadata leaves the description unchanged. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBodyDesc_SetAddedEdges(struct R3SoftBodyDesc *desc, + struct R3EdgeView view); + /** * Borrow skin geometry. Other fields, including skinCollision, are preserved. * @ingroup soft_bodies @@ -15766,6 +18923,18 @@ struct R3ColliderDesc RAPIER_CALL r3RoundCylinderColliderDesc(R3Real half_height R3Real border_radius); #endif +#if defined(RAPIER_DIM3) +/** + * Return a rounded Y-aligned cone description; dimensions exclude border_radius. + * Returns a description without allocating or validating. Build/insert validates its fields. + * @ingroup colliders + */ +RAPIER_API +struct R3ColliderDesc RAPIER_CALL r3RoundConeColliderDesc(R3Real half_height, + R3Real radius, + R3Real border_radius); +#endif + /** * Return a X-aligned capsule description; half_height is half the segment length, excluding caps. * Returns a description without allocating or validating. Build/insert validates its fields. @@ -15840,6 +19009,36 @@ struct R3SoftBodyDesc RAPIER_CALL r3ClothSoftBodyDesc(struct R3Vector origin, size_t nv); #endif +#if defined(RAPIER_DIM3) +/** + * Return a cloth recipe like r3ClothSoftBodyDesc whose edges along du (warp), along dv (weft) + * and diagonal (shear) get their own softness; the material's bendSoftness still applies to the + * bending edges. The softness is stored in warpSoftness, weftSoftness and shearSoftness. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3ClothAnisotropicSoftBodyDesc(struct R3Vector origin, + struct R3Vector du, + struct R3Vector dv, + size_t nu, + size_t nv, + struct R3SpringCoefficients warp, + struct R3SpringCoefficients weft, + struct R3SpringCoefficients shear); +#endif + +#if defined(RAPIER_DIM2) +/** + * Return a closed polygon recipe from at least 3 counter-clockwise points: structural edges along + * the boundary, bending edges between second neighbors, and area preservation. The points are + * borrowed until preview/insertion. + * Initializes a recipe without allocating. Geometry is validated during preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API struct R3SoftBodyDesc RAPIER_CALL r3PolygonSoftBodyDesc(struct R3VectorView points); +#endif + #if defined(RAPIER_DIM2) /** * Return a closed regular polygon recipe with the specified boundary particle count and area @@ -15882,40 +19081,349 @@ struct R3SoftBodyDesc RAPIER_CALL r3ClothTubeSoftBodyDesc(struct R3Vector origin #endif /** - * Initializes a borrowed meshing recipe. Mesh generation happens on preview/insertion. + * Initializes a borrowed meshing recipe. Mesh generation happens on preview/insertion. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyDesc RAPIER_CALL r3VolumetricSoftBodyDesc(struct R3VectorView vertices, + R3SurfaceElementView surface, + struct R3VolumeMeshParameters parameters); + +/** + * Returns a material with the same softness for each constraint family. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3SoftBodyMaterial RAPIER_CALL r3UniformSoftBodyMaterial(struct R3SpringCoefficients value); + +/** + * Copies generated particle positions into caller-owned storage; no persistent builder. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyDesc_ParticlePositions(const struct R3SoftBodyDesc *desc, + struct R3Vector *buffer, + size_t capacity); + +/** + * Copies generated cell indices into caller-owned storage. Counts scalar indices. + * @see @ref output_buffers + * @ingroup soft_bodies + */ +RAPIER_API +size_t RAPIER_CALL r3SoftBodyDesc_CellIndices(const struct R3SoftBodyDesc *desc, + uint32_t *buffer, + size_t capacity); + +/** + * Return the world-space velocity of the indexed particle. + * @ingroup soft_bodies + */ +RAPIER_API +struct R3Vector RAPIER_CALL r3SoftBody_ParticleVelocity(struct R3SoftBodyHandle handle, + size_t index); + +/** + * Return the solver simulating the soft body's elasticity (R3_SOFT_SOLVER_*). Always + * R3_SOFT_SOLVER_CONSTRAINTS in a library built without FEM. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3SoftBody_Solver(struct R3SoftBodyHandle handle); + +/** + * Override the softness of every structural or bending edge fully contained in a live cluster: + * regional stiffness for cloth and ropes. A NULL softness restores the body material's. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_SetClusterEdgeSoftness(struct R3SoftBodyHandle handle, + uint32_t cluster, + const struct R3SpringCoefficients *softness); + +/** + * Apply a world-space impulse to every free particle within falloff_radius of the world-space + * point, scaled linearly from 1 at the point to 0 at that radius and divided by the particle's + * mass. A falloff_radius of zero or less gives every free particle the whole impulse. Pinned + * particles ignore it. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_ApplyImpulseAtPoint(struct R3SoftBodyHandle handle, + struct R3Vector impulse, + struct R3Vector point, + R3Real falloff_radius, + R3Bool wake_up); + +/** + * Apply an impulse of the given magnitude pointing away from the world-space center to every + * free particle within falloff_radius, scaled linearly from 1 at the center to 0 at that + * radius and divided by the particle's mass. A particle on the center gets nothing; a + * falloff_radius of zero or less pushes every free particle fully. A negative magnitude pulls + * toward the center. Pinned particles ignore it. + * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3SoftBody_ApplyRadialImpulse(struct R3SoftBodyHandle handle, + struct R3Vector center, + R3Real magnitude, + R3Real falloff_radius, + R3Bool wake_up); + +/** + * Undo every permanent (plastic) deformation: edge rest lengths, dihedral rest angles, cell + * rest shapes and particle rest positions return to their creation state. The particles stay + * put and spring back elastically. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_ResetPlasticity(struct R3SoftBodyHandle handle); + +/** + * Mark the indexed edge as torn. The tear is applied at the end of the next step and reported + * by a tear event; use r3SoftBody_Tear to tear immediately. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_TearEdge(struct R3SoftBodyHandle handle, size_t index); + +/** + * Mark the indexed cell as torn. The tear is applied at the end of the next step and reported + * by a tear event: no cell is removed, one of its particles splits along the plane + * perpendicular to the cell's principal rest stretch. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3SoftBody_TearCell(struct R3SoftBodyHandle handle, size_t index); + +/** + * Return the soft body owning this deformable collider (a soft-body collision mesh), or an invalid + * handle for any other collider. + * @ingroup colliders + */ +RAPIER_API struct R3SoftBodyHandle RAPIER_CALL r3Collider_SoftBody(struct R3ColliderHandle handle); + +/** + * Return the soft body owning this deformable collider, or an invalid handle for any other + * collider. Uses only the callback-scoped read context; never retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3SoftBodyHandle RAPIER_CALL r3ReadCollider_SoftBody(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the number of soft bodies the torn body is in after the tear: the length of + * r3SoftBodyTearEvent_Bodies, and the exclusive bound of the piece_index of + * r3SoftBodyTearEvent_PieceParticles. It is 1 when nothing was split off. + * @ingroup soft_bodies + */ +RAPIER_API size_t RAPIER_CALL r3SoftBodyTearEvent_PieceCount(const struct R3SoftBodyTearEvent *event); + +/** + * Return the world setting documented by R3SoftRecoverySettings::authoredVelocityMargin. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryAuthoredVelocityMargin(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::edgeSpeculation. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryEdgeSpeculation(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::invertedCellDetection. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryInvertedCellDetection(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::selfCrossingDetection. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoverySelfCrossingDetection(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::detectionMotionGating. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryDetectionMotionGating(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::crossBodyDetection. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryCrossBodyDetection(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::selfStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoverySelfStandDown(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::crossBodyExpelGate. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryCrossBodyExpelGate(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::edgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryEdgeStandDown(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::crossingRepulsion. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryCrossingRepulsion(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::crossingRepulsionGuide. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryCrossingRepulsionGuide(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::crossingRepulsionSelfGuide. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryCrossingRepulsionSelfGuide(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::recoveryPace. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RecoveryRecoveryPace(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapConstraints. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapConstraints(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapRigid. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapRigid(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapSkipSelfTangled. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapSkipSelfTangled(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapEdgeStandDown. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapEdgeStandDown(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapConstraintPace. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RecoveryOverlapConstraintPace(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapPatchConstraints. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3RecoveryOverlapPatchConstraints(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapPatchConstraints. + * @ingroup soft_bodies + */ +RAPIER_API +R3Status RAPIER_CALL r3RecoverySetOverlapPatchConstraints(struct R3World *world, + uint32_t value); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapSkinVolume. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapSkinVolume(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapKeptDepth. + * @ingroup soft_bodies + */ +RAPIER_API R3Real RAPIER_CALL r3RecoveryOverlapKeptDepth(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapSelfRegions. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapSelfRegions(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapNormalPush. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapNormalPush(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapMultiVolume. + * @ingroup soft_bodies + */ +RAPIER_API R3Bool RAPIER_CALL r3RecoveryOverlapMultiVolume(const struct R3World *world); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapSplit. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3RecoveryOverlapSplit(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapSplit. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapSplit(struct R3World *world, uint32_t value); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapPatience. + * @ingroup soft_bodies + */ +RAPIER_API uint32_t RAPIER_CALL r3RecoveryOverlapPatience(const struct R3World *world); + +/** + * Set the world setting documented by R3SoftRecoverySettings::overlapPatience. + * @ingroup soft_bodies + */ +RAPIER_API R3Status RAPIER_CALL r3RecoverySetOverlapPatience(struct R3World *world, uint32_t value); + +/** + * Return the world setting documented by R3SoftRecoverySettings::overlapProgressMargin. * @ingroup soft_bodies */ -RAPIER_API -struct R3SoftBodyDesc RAPIER_CALL r3VolumetricSoftBodyDesc(struct R3VectorView vertices, - R3SurfaceElementView surface, - struct R3VolumeMeshParameters parameters); +RAPIER_API R3Real RAPIER_CALL r3RecoveryOverlapProgressMargin(const struct R3World *world); +#if defined(RAPIER_FEM) /** - * Returns a material with the same softness for each constraint family. + * Return the world setting documented by R3SoftFemParameters::linearTolerance. * @ingroup soft_bodies */ -RAPIER_API -struct R3SoftBodyMaterial RAPIER_CALL r3UniformSoftBodyMaterial(struct R3SpringCoefficients value); +RAPIER_API R3Real RAPIER_CALL r3FemLinearTolerance(const struct R3World *world); +#endif +#if defined(RAPIER_FEM) /** - * Copies generated particle positions into caller-owned storage; no persistent builder. - * @see @ref output_buffers + * Return the world setting documented by R3SoftFemParameters::maxLinearIterations. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r3SoftBodyDesc_ParticlePositions(const struct R3SoftBodyDesc *desc, - struct R3Vector *buffer, - size_t capacity); +RAPIER_API size_t RAPIER_CALL r3FemMaxLinearIterations(const struct R3World *world); +#endif +#if defined(RAPIER_FEM) /** - * Copies generated cell indices into caller-owned storage. Counts scalar indices. - * @see @ref output_buffers + * Return the world setting documented by R3SoftFemParameters::maxDenseDofs. * @ingroup soft_bodies */ -RAPIER_API -size_t RAPIER_CALL r3SoftBodyDesc_CellIndices(const struct R3SoftBodyDesc *desc, - uint32_t *buffer, - size_t capacity); +RAPIER_API size_t RAPIER_CALL r3FemMaxDenseDofs(const struct R3World *world); +#endif /** * Return a process-local geometry identity for caching, not a serializable ID. Keep a shared-shape @@ -16084,7 +19592,9 @@ R3Status RAPIER_CALL r3SoftBody_AddForce(struct R3SoftBodyHandle handle, R3Bool wake_up); /** - * Apply a world-space linear impulse. + * Add the same world-space velocity change to every free particle: the whole body is kicked at + * the same velocity, whatever the particle masses (the value is not divided by the mass). Pinned + * particles ignore it. * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. * @ingroup soft_bodies */ @@ -16115,7 +19625,9 @@ R3Status RAPIER_CALL r3SoftBody_SetVolumeFactor(struct R3SoftBodyHandle handle, R3Real value); /** - * Attach a particle to a rigid body at the supplied body-local anchor. + * Attach a particle to a rigid body by a two-way point-to-point constraint (unlike pinning). The + * anchor is the particle's current position, expressed in the rigid body's local frame; a particle + * attached twice keeps both attachments. Undo it with r3SoftBody_DetachParticle. * @ingroup soft_bodies */ RAPIER_API @@ -16124,10 +19636,11 @@ R3Status RAPIER_CALL r3SoftBody_AttachParticle(struct R3SoftBodyHandle handle, struct R3RigidBodyHandle rigid_body); /** - * Remove a particle attachment to a rigid body. + * Detach a particle from every rigid body it was attached to with r3SoftBody_AttachParticle. + * Returns whether it was attached at all. * @ingroup soft_bodies */ -RAPIER_API R3Status RAPIER_CALL r3SoftBody_DetachParticle(struct R3SoftBodyHandle handle, size_t index); +RAPIER_API R3Bool RAPIER_CALL r3SoftBody_DetachParticle(struct R3SoftBodyHandle handle, size_t index); /** * Copy cluster indices. @@ -16186,7 +19699,9 @@ R3Status RAPIER_CALL r3SoftBody_SetClusterShapeMatchingEnabled(struct R3SoftBody R3Bool value); /** - * Set the soft body cluster shape-matching stiffness multiplier. + * Scale the material stiffness (Young modulus) of every cell fully contained in a live cluster: + * regional materials without a separate body. Cells straddling the cluster's boundary keep their + * stiffness; use r3SoftBody_SetClusterEdgeSoftness for edges. * @ingroup soft_bodies */ RAPIER_API @@ -16461,7 +19976,7 @@ RAPIER_API R3Real RAPIER_CALL r3RigidBody_KineticEnergy(struct R3RigidBodyHandle /** * Return the rigid body soft-CCD prediction distance. - * @ingroup soft_bodies + * @ingroup rigid_bodies */ RAPIER_API R3Real RAPIER_CALL r3RigidBody_SoftCcdPrediction(struct R3RigidBodyHandle handle); @@ -16553,7 +20068,7 @@ R3Status RAPIER_CALL r3RigidBody_SetAdditionalMass(struct R3RigidBodyHandle hand /** * Set the rigid body soft-CCD prediction distance. - * @ingroup soft_bodies + * @ingroup rigid_bodies */ RAPIER_API R3Status RAPIER_CALL r3RigidBody_SetSoftCcdPrediction(struct R3RigidBodyHandle handle, @@ -16840,8 +20355,7 @@ RAPIER_API R3Bool RAPIER_CALL r3Collider_IsEnabled(struct R3ColliderHandle handl RAPIER_API struct R3Aabb RAPIER_CALL r3Collider_ComputeAabb(struct R3ColliderHandle handle); /** - * Return an owned wrapper sharing the collider geometry. Release with r3FreeSharedShape. - * Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. + * Return an owned wrapper sharing the collider geometry. Release it with r3FreeSharedShape. * @ingroup shapes */ RAPIER_API R3SharedShape *RAPIER_CALL r3Collider_CloneShape(struct R3ColliderHandle handle); @@ -16989,7 +20503,7 @@ R3Status RAPIER_CALL r3ImpulseJoint_SetEnabled(struct R3ImpulseJointHandle handl /** * Set the joint desc joint spring coefficients. - * @ingroup soft_bodies + * @ingroup joints */ RAPIER_API R3Status RAPIER_CALL r3JointDesc_SetSoftness(struct R3JointDesc *desc, @@ -16998,7 +20512,7 @@ R3Status RAPIER_CALL r3JointDesc_SetSoftness(struct R3JointDesc *desc, /** * Set the impulse joint joint spring coefficients. * wake_up = 1 wakes affected bodies; 0 preserves their sleep state. - * @ingroup soft_bodies + * @ingroup joints */ RAPIER_API R3Status RAPIER_CALL r3ImpulseJoint_SetSoftness(struct R3ImpulseJointHandle handle, @@ -17258,6 +20772,55 @@ R3Status RAPIER_CALL r3ImpulseJoint_SetMotorVelocity(struct R3ImpulseJointHandle R3Real factor, R3Bool wake_up); +/** + * Return the impulses applied by the impulse joint during the last step. They are zero before + * its first step, and their components are expressed along the axes of the joint frame. + * @ingroup joints + */ +RAPIER_API struct R3JointImpulses RAPIER_CALL r3ImpulseJoint_Impulses(struct R3ImpulseJointHandle handle); + +/** + * Return the impulse joint application-owned 128-bit user value. + * @ingroup joints + */ +RAPIER_API struct R3UserData RAPIER_CALL r3ImpulseJoint_UserData(struct R3ImpulseJointHandle handle); + +/** + * Return the number of impulse joints in the world. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r3ImpulseJointCount(const struct R3World *world); + +/** + * Return the number of multibody joints in the world, which is the number of handles copied by + * r3MultibodyJointHandles. + * @ingroup joints + */ +RAPIER_API size_t RAPIER_CALL r3MultibodyJointCount(const struct R3World *world); + +/** + * Copies the multibody joint configuration without returning a borrowed joint pointer. + * @ingroup joints + */ +RAPIER_API struct R3JointDesc RAPIER_CALL r3MultibodyJoint_Desc(struct R3MultibodyJointHandle handle); + +/** + * Replaces the multibody joint configuration after validation. lockedAxes defines the degrees of + * freedom of the multibody and cannot change: a different value reports INVALID_ARGUMENT. + * wake_up = 1 wakes the two connected bodies; 0 preserves their sleep state. + * @ingroup joints + */ +RAPIER_API +R3Status RAPIER_CALL r3MultibodyJoint_SetDesc(struct R3MultibodyJointHandle handle, + const struct R3JointDesc *desc, + R3Bool wake_up); + +/** + * Return the two bodies connected by a multibody joint: its parent link, then its own link. + * @ingroup joints + */ +RAPIER_API struct R3JointBodies RAPIER_CALL r3MultibodyJoint_Bodies(struct R3MultibodyJointHandle handle); + /** * Create an owned compound shape by convex decomposition of the input surface. Release it with * r3FreeSharedShape. @@ -17296,6 +20859,18 @@ R3SharedShape *RAPIER_CALL r3VoxelizedMeshSharedShape(struct R3VectorView vertic */ RAPIER_API R3SharedShape *RAPIER_CALL r3ConvexHullSharedShape(struct R3VectorView vertices); +#if defined(RAPIER_DIM3) +/** + * Create an owned convex polyhedron from vertices and triangle indices assumed to form a convex + * mesh (no convex hull is computed); fails on degenerate input. Release it with r3FreeSharedShape. + * Copies typed input geometry into an owned shared shape; arrays may be released on return. + * @ingroup shapes + */ +RAPIER_API +R3SharedShape *RAPIER_CALL r3ConvexMeshSharedShape(struct R3VectorView vertices, + struct R3TriangleView indices); +#endif + /** * Create an owned triangle mesh from vertices and triangle indices. Release it with * r3FreeSharedShape. @@ -17308,6 +20883,7 @@ R3SharedShape *RAPIER_CALL r3TrimeshSharedShape(struct R3VectorView vertices, /** * Create an owned polyline from vertices and edge indices. Release it with r3FreeSharedShape. + * Empty indices connect the vertices in order (a line strip). * Copies typed input geometry into an owned shared shape; arrays may be released on return. * @ingroup shapes */ @@ -17384,6 +20960,19 @@ struct R3VelocityCorrection RAPIER_CALL r3ReadPidController_RigidBodyCorrection( struct R3Vector target_linvel, R3AngVector target_angvel); +/** + * Compute a PD velocity correction from callback-visible body state. Neither the body nor the + * controller is modified. The context is valid only during its callback. + * @ingroup callbacks + */ +RAPIER_API +struct R3VelocityCorrection RAPIER_CALL r3ReadPdController_RigidBodyCorrection(const struct R3ReadContext *context, + const struct R3PdController *controller, + struct R3RigidBodyHandle body, + struct R3Pose target_pose, + struct R3Vector target_linvel, + R3AngVector target_angvel); + /** * Return the number of rigid body objects in the world. Uses only the callback-scoped read * context; never retain the context. @@ -17970,6 +21559,309 @@ size_t RAPIER_CALL r3ReadRigidBodyReadStates(const struct R3ReadContext *context struct R3RigidBodyState *states, size_t capacity); +/** + * Return the collider body-type collision activation bitmask (R3_COLLISION_TYPES_* bits). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint16_t RAPIER_CALL r3ReadCollider_ActiveCollisionTypes(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider physics-hook activation bitmask. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r3ReadCollider_ActiveHooks(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider friction combination rule (R3_COMBINE_*). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r3ReadCollider_FrictionCombineRule(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider restitution combination rule (R3_COMBINE_*). Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +uint32_t RAPIER_CALL r3ReadCollider_RestitutionCombineRule(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the collider pose relative to its parent rigid body, or its world-space pose if it has + * no parent. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3ReadCollider_PositionWrtParent(const struct R3ReadContext *context, + struct R3ColliderHandle handle); + +/** + * Return the rigid body signed dominance group. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +int8_t RAPIER_CALL r3ReadRigidBody_DominanceGroup(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body additional solver iterations for connected bodies. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadRigidBody_AdditionalSolverIterations(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return the rigid body additional PGS iterations for connected bodies. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ReadRigidBody_AdditionalPgsIterations(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Return whether the rigid body may exceed the angular-velocity limit of its CCD. Uses only the callback-scoped read context; never + * retain the context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ReadRigidBody_IsFastRotationAllowed(const struct R3ReadContext *context, + struct R3RigidBodyHandle handle); + +/** + * Copy the hits of every collider intersected by the ray, in no particular order. The ray is + * origin + direction * t for 0 <= t <= max_toi; direction need not be normalized. solid treats an + * interior origin as a hit at t = 0. + * @see @ref output_buffers + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +size_t RAPIER_CALL r3IntersectRay(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector origin, + struct R3Vector direction, + R3Real max_toi, + R3Bool solid, + struct R3RayHit *buffer, + size_t capacity); + +/** + * Sweep shape from pose along velocity and return the first hit, with found = 0 on a miss + * (R3_OK). Time is bounded by options.max_time_of_impact. The shape is borrowed for this call. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3OptionalShapeCastHit RAPIER_CALL r3TryCastShape(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Pose pose, + struct R3Vector velocity, + const R3SharedShape *shape, + struct R3ShapeCastOptions options); + +/** + * Return the closest surface projection within max_distance, with found = 0 if there is none + * (R3_OK). With solid = 1, an interior point projects to itself. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3OptionalPointProjection RAPIER_CALL r3TryProjectPoint(const struct R3World *world, + const struct R3QueryOptions *query_options, + struct R3Vector point, + R3Real max_distance, + R3Bool solid); + +/** + * Return the world-space pose of the motion at the given time. + * @ingroup queries + */ +RAPIER_API +struct R3Pose RAPIER_CALL r3NonlinearRigidMotion_PositionAtTime(const struct R3NonlinearRigidMotion *motion, + R3Real time); + +/** + * Sweep shape along a rotating motion and return the first hit between start_time and end_time, + * with found = 0 on a miss (R3_OK). With stop_at_penetration = 1, a shape already intersecting a + * collider at start_time hits it at start_time; with 0, that penetration is ignored while the + * motion separates the shapes. witness1/normal1 are world-space; witness2/normal2 are local to the + * shape, posed by r3NonlinearRigidMotion_PositionAtTime at the time of impact. + * NULL query options use the default filter. Query state reflects the latest Step or + * DetectCollisions call. + * @ingroup queries + */ +RAPIER_API +struct R3OptionalShapeCastHit RAPIER_CALL r3TryCastShapeNonlinear(const struct R3World *world, + const struct R3QueryOptions *query_options, + const struct R3NonlinearRigidMotion *motion, + const R3SharedShape *shape, + R3Real start_time, + R3Real end_time, + R3Bool stop_at_penetration); + +/** + * Return the narrow-phase contact pair for two colliders, with found = 0 if the broad phase + * found no potential contact between them (R3_OK). The pair's collider1 and collider2 follow the + * narrow-phase order, which may differ from the argument order. + * @ingroup events + */ +RAPIER_API +struct R3OptionalContactPair RAPIER_CALL r3TryContactPair(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2); + +/** + * Return the intersection state of two colliders involving a sensor, or report R3_NOT_FOUND if + * the broad phase found no potential intersection. The result keeps the argument order. + * @ingroup events + */ +RAPIER_API +struct R3IntersectionPair RAPIER_CALL r3IntersectionPair(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2); + +/** + * Return the intersection state of two colliders involving a sensor, with found = 0 if the + * broad phase found no potential intersection (R3_OK). The pair keeps the argument order. + * @ingroup events + */ +RAPIER_API +struct R3OptionalIntersectionPair RAPIER_CALL r3TryIntersectionPair(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2); + +/** + * Copy the narrow-phase contact pairs involving the collider, including pairs without active + * solver contacts. The collider may be either collider1 or collider2 of each pair. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3Collider_ContactPairs(struct R3ColliderHandle handle, + struct R3ContactPair *buffer, + size_t capacity); + +/** + * Copy the intersection pairs involving the collider, in the narrow-phase order. The collider may + * be either collider1 or collider2 of each pair. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3Collider_IntersectionPairs(struct R3ColliderHandle handle, + struct R3IntersectionPair *buffer, + size_t capacity); + +/** + * Copy the geometric contact manifolds of a contact pair, or report R3_NOT_FOUND without a pair. + * Their order matches the manifold_index of r3ContactPoints. Soft pairs have no rigid + * manifolds. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3ContactManifolds(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + struct R3ContactManifold *buffer, + size_t capacity); + +/** + * Copy the solver contacts of one manifold of a contact pair, or report R3_NOT_FOUND without a + * pair. Points are resolved through the bodies' current poses. With contact clustering (3D + * composite shapes), the solver may use merged manifolds instead; use contact pair totals then. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3SolverContacts(struct R3ColliderHandle collider1, + struct R3ColliderHandle collider2, + size_t manifold_index, + struct R3SolverContact *buffer, + size_t capacity); + +/** + * Return whether the context holds the contact candidates of two soft surfaces rather than a + * manifold. Solver-contact accessors see no contacts in a soft context. + * @ingroup callbacks + */ +RAPIER_API +R3Bool RAPIER_CALL r3ContactModificationContext_IsSoft(const struct R3ContactModificationContext *context); + +/** + * Return the number of solver contacts of the manifold; zero for a soft context. + * @ingroup callbacks + */ +RAPIER_API +size_t RAPIER_CALL r3ContactModificationContext_SolverContactCount(const struct R3ContactModificationContext *context); + +/** + * Return a solver contact of the manifold. Inside the hook, points are world-space. + * @ingroup callbacks + */ +RAPIER_API +struct R3SolverContact RAPIER_CALL r3ContactModificationContext_SolverContact(const struct R3ContactModificationContext *context, + size_t index); + +/** + * Replace the points, distance and tangent velocity of a solver contact of the manifold. Points + * are world-space; a distance differing from their gap along the normal shifts the contact. + * @ingroup callbacks + */ +RAPIER_API +R3Status RAPIER_CALL r3ContactModificationContext_SetSolverContact(struct R3ContactModificationContext *context, + size_t index, + const struct R3SolverContact *contact); + +/** + * Remove a solver contact of the manifold. The last solver contact takes its index. + * @ingroup callbacks + */ +RAPIER_API +R3Status RAPIER_CALL r3ContactModificationContext_RemoveSolverContact(struct R3ContactModificationContext *context, + size_t index); + +/** + * Replace the callbacks invoked while stepping with this collector; NULL removes them. They take + * effect from the next Step or DetectCollisions call. + * @ingroup events + */ +RAPIER_API +R3Status RAPIER_CALL r3EventCollector_SetCallbacks(struct R3EventCollector *events, + const struct R3EventCallbacks *callbacks); + +/** + * Return native default debug-render style. This POD value owns no resources. + * @ingroup events + */ +RAPIER_API struct R3DebugRenderStyle RAPIER_CALL r3DefaultDebugRenderStyle(void); + +/** + * Copy the debug-render lines of the world drawn with the given style. mode combines R3_DEBUG_* + * bits. NULL style uses the default style. + * @see @ref output_buffers + * @ingroup events + */ +RAPIER_API +size_t RAPIER_CALL r3DebugRenderWithStyle(const struct R3World *world, + uint32_t mode, + const struct R3DebugRenderStyle *style, + struct R3DebugLine *buffer, + size_t capacity); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/c/include/rapier.hpp b/c/include/rapier.hpp index 002ddc350..c95fa785c 100644 --- a/c/include/rapier.hpp +++ b/c/include/rapier.hpp @@ -105,7 +105,7 @@ inline RAPIER_TYPE(QueryOptions) queryOptions() { inline World make_world() { check(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), - sizeof(RAPIER_TYPE(Pose)))); + sizeof(RAPIER_TYPE(Pose)), RAPIER_CONST(ABI_FEATURES))); RAPIER_TYPE(World) *value = RAPIER_FN(NewWorld)(); check(RAPIER_FN(LastStatus)()); return World(value); diff --git a/c/include/rapier_math.h b/c/include/rapier_math.h index fb2eebfea..65efac5e4 100644 --- a/c/include/rapier_math.h +++ b/c/include/rapier_math.h @@ -1,5 +1,7 @@ /** @file * Inline value constructors and arithmetic; no allocation or error-state changes. + * Trigonometry uses the library's Sin and Cos, so results match across platforms with + * enhanced-determinism; square roots are exactly rounded everywhere. * @defgroup inline_math Inline math * @ingroup math * @{ @@ -46,9 +48,9 @@ static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationFromAxisAngle)(RAPIER_TYPE RAPIER_TYPE(Rotation) result = {0, 0, 0, 1}; return result; } - RAPIER_TYPE(Real) scale = (RAPIER_TYPE(Real))sin(angle / 2) / length; + RAPIER_TYPE(Real) scale = RAPIER_FN(Sin)(angle / 2) / length; RAPIER_TYPE(Rotation) result = {axis.x * scale, axis.y * scale, axis.z * scale, - (RAPIER_TYPE(Real))cos(angle / 2)}; + RAPIER_FN(Cos)(angle / 2)}; return result; } #endif @@ -125,8 +127,8 @@ static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationMul)(RAPIER_TYPE(Rotation) static inline RAPIER_TYPE(Vector) RAPIER_FN(RotationTransformVector)(RAPIER_TYPE(Rotation) rotation, RAPIER_TYPE(Vector) vector) { #if defined(RAPIER_DIM2) - const RAPIER_TYPE(Real) c = (RAPIER_TYPE(Real))cos(rotation.angle), - s = (RAPIER_TYPE(Real))sin(rotation.angle); + const RAPIER_TYPE(Real) c = RAPIER_FN(Cos)(rotation.angle), + s = RAPIER_FN(Sin)(rotation.angle); return RAPIER_FN(Vector)(c * vector.x - s * vector.y, s * vector.x + c * vector.y); #else @@ -156,6 +158,19 @@ static inline RAPIER_TYPE(Pose) RAPIER_FN(TranslationPose)(RAPIER_TYPE(Vector) t #endif return RAPIER_FN(Pose)(translation, rotation); } +/** Construct mass properties from a local center of mass, a mass, and principal angular inertia + * (a scalar in 2D); in 3D the principal inertia frame is the identity rotation. */ +static inline RAPIER_TYPE(MassProperties) RAPIER_FN(MassProperties)(RAPIER_TYPE(Vector) local_com, + RAPIER_TYPE(Real) mass, + RAPIER_TYPE(AngVector) principal_inertia) { +#if defined(RAPIER_DIM2) + RAPIER_TYPE(MassProperties) result = {local_com, mass, principal_inertia}; +#else + RAPIER_TYPE(Rotation) frame = {0, 0, 0, 1}; + RAPIER_TYPE(MassProperties) result = {local_com, mass, principal_inertia, frame}; +#endif + return result; +} /** Return the inverse of a normalized rotation. */ static inline RAPIER_TYPE(Rotation) RAPIER_FN(RotationInverse)(RAPIER_TYPE(Rotation) rotation) { #if defined(RAPIER_DIM2) diff --git a/c/rapier3d-ffi/Cargo.toml b/c/rapier3d-ffi/Cargo.toml index 94807de27..2f949acea 100644 --- a/c/rapier3d-ffi/Cargo.toml +++ b/c/rapier3d-ffi/Cargo.toml @@ -22,13 +22,14 @@ profiler = ["rapier/profiler"] simd8 = ["rapier/simd8"] enhanced-determinism = ["rapier/enhanced-determinism"] fem = ["rapier/fem"] -robotics = ["dep:rapier3d-urdf", "dep:rapier3d-mjcf"] +robotics = ["dep:rapier3d-urdf", "dep:rapier3d-mjcf", "dep:rapier3d-meshloader"] [dependencies] rapier-c-macros = { path = "../rapier-c-macros" } rapier = { package = "rapier3d", path = "../../crates/rapier3d", features = ["serde-serialize", "debug-render"] } rapier3d-urdf = { workspace = true, optional = true, features = ["stl", "collada", "wavefront"] } rapier3d-mjcf = { workspace = true, optional = true, features = ["stl", "wavefront", "msh"] } +rapier3d-meshloader = { workspace = true, optional = true, features = ["stl", "collada", "wavefront"] } bincode.workspace = true serde.workspace = true diff --git a/c/src/array_views.rs b/c/src/array_views.rs index 80a7cb1f2..70fe7d128 100644 --- a/c/src/array_views.rs +++ b/c/src/array_views.rs @@ -280,6 +280,59 @@ pub unsafe extern "C" fn rpr_soft_body_desc_set_surface_mesh( Ok(()) }) } +/// Select a 2D triangle-mesh recipe and borrow its vertices and triangles (stored in positions and +/// cells). The triangles become structural edges and a boundary, not cells, and shape matching +/// holds the shape. Other fields are preserved. +/// @ingroup soft_bodies +#[cfg(feature = "dim2")] +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_trimesh( + desc: *mut RprSoftBodyDesc, + vertices: RprVectorView, + triangles: RprTriangleView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(vertices.data, vertices.count)?; + validate_view(triangles.data, triangles.count)?; + let desc = get_mut(desc)?; + desc.kind = RPR_SOFT_DESC_TRIMESH; + desc.positions = vertices; + desc.cells = triangles; + Ok(()) + }) +} +/// Borrow descriptions to merge into this body (see RprSoftBodyDesc::appended); preserve all other +/// fields. No allocation or element reads. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_appended( + desc: *mut RprSoftBodyDesc, + view: RprSoftBodyDescView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.appended = view; + Ok(()) + }) +} +/// Borrow structural edges added after appending (see RprSoftBodyDesc::addedEdges); preserve all +/// other fields. No allocation or element reads. +/// Invalid view metadata leaves the description unchanged. +/// @ingroup soft_bodies +#[rapier_export(soft_body_desc)] +pub unsafe extern "C" fn rpr_soft_body_desc_set_added_edges( + desc: *mut RprSoftBodyDesc, + view: RprEdgeView, +) -> RprStatus { + ffi(|| unsafe { + validate_view(view.data, view.count)?; + let desc = get_mut(desc)?; + desc.addedEdges = view; + Ok(()) + }) +} /// Borrow skin geometry. Other fields, including skinCollision, are preserved. /// @ingroup soft_bodies #[rapier_export(soft_body_desc)] diff --git a/c/src/config_data.rs b/c/src/config_data.rs index dfdae9f3b..b3921c2a7 100644 --- a/c/src/config_data.rs +++ b/c/src/config_data.rs @@ -72,7 +72,7 @@ pub struct RprSoftBodyMaterial { pub edgePlasticCreep: RprReal, /// Maximum permanent edge-length change as a fraction of its initial length. pub edgePlasticMax: RprReal, - /// Plastic flow direction: 0 both, 1 compression only, 2 tension only. + /// Plastic flow direction: RPR_SOFT_EDGE_PLASTIC_FLOW_BOTH, _COMPRESSION or _TENSION. pub edgePlasticFlow: u32, /// Optional strain threshold for tearing; disabled means no strain-based tearing. pub tearStrain: RprOptionalReal, @@ -227,8 +227,8 @@ pub struct RprSoftRecoverySettings { pub overlapEdgeStandDown: RprBool, /// Velocity-change limit per step, as a multiple of recoveryPace. pub overlapConstraintPace: RprReal, - /// Per-point constraints inside overlap patches: 0 keep, 1 stand down, 2 align with overlap - /// normal. + /// Per-point constraints inside overlap patches: RPR_SOFT_PATCH_CONSTRAINTS_KEEP, _STAND_DOWN or + /// _ALONG_NORMAL. pub overlapPatchConstraints: u32, /// Measure overlap on contact-skin surfaces rather than bare geometry. pub overlapSkinVolume: RprBool, @@ -444,7 +444,7 @@ pub struct RprIntegrationParameters { pub normalizedMaxLinearVelocity: RprReal, /// Number of solver substeps/iterations; must be positive. pub numSolverIterations: usize, - /// PGS iterations per solver substep. + /// PGS iterations per solver substep; must be positive. pub numInternalPgsIterations: usize, /// Stabilization iterations after velocity solving. pub numInternalStabilizationIterations: usize, @@ -461,7 +461,7 @@ pub struct RprIntegrationParameters { /// Whether to warmstart joint constraints. pub warmstartJoints: RprBool, #[cfg(feature = "dim3")] - /// Friction model: 0 simplified, 1 Coulomb (3D only). + /// Friction model of rigid-body contacts, RPR_FRICTION_MODEL_* (3D only). pub frictionModel: u32, } impl From for RprIntegrationParameters { @@ -488,10 +488,7 @@ impl From for RprIntegrationParameters { frictionInBiasPass: value.friction_in_bias_pass as RprBool, warmstartJoints: value.warmstart_joints as RprBool, #[cfg(feature = "dim3")] - frictionModel: match value.friction_model { - FrictionModel::Simplified => 0, - FrictionModel::Coulomb => 1, - }, + frictionModel: friction_model_value(value.friction_model), } } } @@ -509,8 +506,8 @@ impl RprIntegrationParameters { normalized_max_corrective_velocity: nonnegative(self.normalizedMaxCorrectiveVelocity)?, normalized_prediction_distance: nonnegative(self.normalizedPredictionDistance)?, normalized_max_linear_velocity: nonnegative(self.normalizedMaxLinearVelocity)?, - num_solver_iterations: self.numSolverIterations, - num_internal_pgs_iterations: self.numInternalPgsIterations, + num_solver_iterations: iterations(self.numSolverIterations)?, + num_internal_pgs_iterations: iterations(self.numInternalPgsIterations)?, num_internal_stabilization_iterations: self.numInternalStabilizationIterations, max_ccd_substeps: self.maxCcdSubsteps, contact_clustering: boolean(self.contactClustering)?, @@ -521,14 +518,39 @@ impl RprIntegrationParameters { friction_in_bias_pass: boolean(self.frictionInBiasPass)?, warmstart_joints: boolean(self.warmstartJoints)?, #[cfg(feature = "dim3")] - friction_model: match self.frictionModel { - 0 => FrictionModel::Simplified, - 1 => FrictionModel::Coulomb, - _ => return Err(invalid("invalid friction_model")), - }, + friction_model: friction_model(self.frictionModel)?, }) } } +/// @ingroup worlds +/// Friction model solving one Coulomb friction constraint per group of up to 4 contacts plus a +/// twist constraint; faster but less accurate (default). +#[cfg(feature = "dim3")] +pub const RPR_FRICTION_MODEL_SIMPLIFIED: u32 = 0; +/// @ingroup worlds +/// Friction model solving one Coulomb friction constraint per contact point. +#[cfg(feature = "dim3")] +pub const RPR_FRICTION_MODEL_COULOMB: u32 = 1; +/// Validates a solver iteration count, which must be positive. +pub(crate) fn iterations(value: usize) -> Result { + ensure(value > 0, "iteration count must be positive")?; + Ok(value) +} +#[cfg(feature = "dim3")] +pub(crate) fn friction_model(value: u32) -> Result { + match value { + RPR_FRICTION_MODEL_SIMPLIFIED => Ok(FrictionModel::Simplified), + RPR_FRICTION_MODEL_COULOMB => Ok(FrictionModel::Coulomb), + _ => Err(invalid("unknown friction model")), + } +} +#[cfg(feature = "dim3")] +pub(crate) fn friction_model_value(model: FrictionModel) -> u32 { + match model { + FrictionModel::Simplified => RPR_FRICTION_MODEL_SIMPLIFIED, + FrictionModel::Coulomb => RPR_FRICTION_MODEL_COULOMB, + } +} /// Return native default integration parameters. This POD value owns no resources. /// @ingroup worlds #[rapier_export] diff --git a/c/src/control.rs b/c/src/control.rs index 96a8a9149..fac608a33 100644 --- a/c/src/control.rs +++ b/c/src/control.rs @@ -17,11 +17,20 @@ pub struct RprKinematicCharacterController { #[repr(C)] #[derive(Copy, Clone, Default)] pub struct RprCharacterLength { - /// Value used when enabled is 1. + /// Nonnegative length: a fraction of the character shape height when relative is 1, a + /// world-space length otherwise. pub value: RprReal, /// 1 scales value by the character shape size; 0 uses an absolute length. pub relative: RprBool, } +impl From for RprCharacterLength { + fn from(value: CharacterLength) -> Self { + match value { + CharacterLength::Relative(value) => Self { value, relative: 1 }, + CharacterLength::Absolute(value) => Self { value, relative: 0 }, + } + } +} impl RprCharacterLength { fn raw(self) -> Result { nonnegative(self.value)?; @@ -191,6 +200,87 @@ pub unsafe extern "C" fn rpr_kinematic_character_controller_set_snap_to_ground( Ok(()) }) } +/// Return the normalized up direction. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_up( + controller: *const RprKinematicCharacterController, +) -> RprVector { + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { output(out, get(controller)?.inner.up.into()) }) + }) +} +/// Return the collision separation margin. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_offset( + controller: *const RprKinematicCharacterController, +) -> RprCharacterLength { + ffi_value(|out: *mut RprCharacterLength| { + ffi(|| unsafe { output(out, get(controller)?.inner.offset.into()) }) + }) +} +/// Copy of the automatic stepping settings. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprCharacterAutostep { + /// Whether automatic stepping is enabled. + pub enabled: RprBool, + /// Maximum height of the steps climbed automatically. + pub max_height: RprCharacterLength, + /// Minimum free width required on top of a step. + pub min_width: RprCharacterLength, + /// Whether the character can also step over dynamic bodies. + pub include_dynamic_bodies: RprBool, +} +/// Return the automatic stepping settings. When disabled, enabled is 0 and the other fields hold +/// Rapier's defaults. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_autostep( + controller: *const RprKinematicCharacterController, +) -> RprCharacterAutostep { + ffi_value(|out: *mut RprCharacterAutostep| { + ffi(|| unsafe { + let autostep = get(controller)?.inner.autostep; + let step = autostep.unwrap_or_default(); + output( + out, + RprCharacterAutostep { + enabled: autostep.is_some() as RprBool, + max_height: step.max_height.into(), + min_width: step.min_width.into(), + include_dynamic_bodies: step.include_dynamic_bodies as RprBool, + }, + ) + }) + }) +} +/// Set the small distance by which sliding motion is pushed along hit normals to avoid getting stuck; +/// it must be finite and nonnegative. Large values cause bumps when sliding on flat ground. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_set_normal_nudge_factor( + controller: *mut RprKinematicCharacterController, + value: RprReal, +) -> RprStatus { + ffi(|| unsafe { + let value = nonnegative(value)?; + get_mut(controller)?.inner.normal_nudge_factor = value; + Ok(()) + }) +} +/// Return the normal nudge factor set by SetNormalNudgeFactor. +/// @ingroup controllers +#[rapier_export(kinematic_character_controller)] +pub unsafe extern "C" fn rpr_kinematic_character_controller_normal_nudge_factor( + controller: *const RprKinematicCharacterController, +) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { output(out, get(controller)?.inner.normal_nudge_factor) }) + }) +} /// Computes movement without moving any collider. Use the returned translation to set the character /// target. /// NULL query options use the default filter. Query state reflects the latest Step or @@ -283,8 +373,55 @@ pub unsafe extern "C" fn rpr_kinematic_character_controller_collisions( ) } } -/// Applies impulses for the most recent move_shape collisions. Use the same world, shape, dt and -/// filter. +/// Query options of a query pipeline that mutably borrows the rigid bodies. The C predicate cannot +/// read the world while that borrow is live, so it is evaluated beforehand for every collider. +pub(crate) struct MutableQueryOptions { + filter: QueryFilter<'static>, + /// Predicate results indexed by collider index; None without predicate. + accepted: Option>, +} +impl MutableQueryOptions { + pub(crate) unsafe fn new( + owner: *mut RprWorld, + world: &PhysicsWorld, + options: *const RprQueryOptions, + ) -> Result { + unsafe { + let options = if options.is_null() { + RprQueryOptions::default() + } else { + *get(options)? + }; + let filter = options.filter.raw()?; + let accepted = options.predicate.map(|predicate| { + let read = RprReadContext::new(owner, &world.bodies, &world.colliders); + let mut accepted = vec![false; world.colliders.len()]; + for (handle, _) in world.colliders.iter() { + let index = handle.into_raw_parts().0 as usize; + if index >= accepted.len() { + accepted.resize(index + 1, false); + } + let handle = RprColliderHandle::from(handle).with_world(owner); + accepted[index] = predicate(options.userData, &read, handle) != 0; + } + accepted + }); + Ok(Self { filter, accepted }) + } + } + /// Whether the precomputed predicate accepts this collider. + fn accepts(&self, handle: ColliderHandle) -> bool { + self.accepted.as_ref().is_none_or(|accepted| { + let index = handle.into_raw_parts().0 as usize; + accepted.get(index).copied().unwrap_or(false) + }) + } +} + +/// Applies impulses to the dynamic bodies hit by the most recent MoveShape call. Use the same +/// shape, dt and query options as that call; NULL options use the default filter. +/// Unlike MoveShape, the options' predicate is called once per collider of the world before the +/// impulses are applied, while the world is locked for writing: it may only use Read* functions. /// @ingroup controllers #[rapier_export(kinematic_character_controller)] pub unsafe extern "C" fn rpr_kinematic_character_controller_solve_character_collision_impulses( @@ -292,34 +429,34 @@ pub unsafe extern "C" fn rpr_kinematic_character_controller_solve_character_coll shape: *const RprSharedShape, dt: RprReal, mass: RprReal, - filter: *const RprQueryFilter, + options: *const RprQueryOptions, ) -> RprStatus { ffi(|| unsafe { - let world = get(controller)?.world; - if !filter.is_null() { - get(filter)?.check_world(world)?; + let owner = get(controller)?.world; + if !options.is_null() { + get(options)?.check_world(owner)?; } - let access = get(world)?.write()?; + let access = get(owner)?.write()?; let raw = access.raw(); let world: *mut RprPhysicsWorld = raw; positive(dt)?; positive(mass)?; - let f = if filter.is_null() { - RprQueryFilter::default() - } else { - *get(filter)? - } - .raw()?; let c = get(controller)?; let shape = &*get(shape)?.0; + let options = MutableQueryOptions::new(owner, &get(world)?.0, options)?; + let predicate = |handle: ColliderHandle, _: &Collider| options.accepts(handle); + let mut filter = options.filter; + if options.accepted.is_some() { + filter.predicate = Some(&predicate); + } let w = &mut get_mut(world)?.0; let mut q = w.broad_phase.as_query_pipeline_mut( w.narrow_phase.query_dispatcher(), &mut w.bodies, &mut w.colliders, - f, + filter, ); c.inner .solve_character_collision_impulses(dt, &mut q, shape, mass, &c.collisions); @@ -536,43 +673,61 @@ mod vehicle { }) } /// Ray-cast wheel contacts and apply vehicle forces for dt seconds. Does not step the world. + /// NULL options use the default filter. The chassis colliders are always excluded, in addition + /// to the filter's own exclusions. The options' predicate is called once per collider of the + /// world before the update, while the world is locked for writing: it may only use Read* + /// functions. /// @ingroup controllers #[rapier_export(dynamic_ray_cast_vehicle_controller)] pub unsafe extern "C" fn rpr_dynamic_ray_cast_vehicle_controller_update_vehicle( controller: *mut RprDynamicRayCastVehicleController, dt: RprReal, - filter: *const RprQueryFilter, + options: *const RprQueryOptions, ) -> RprStatus { ffi(|| unsafe { - let world = get(controller)?.1; - if !filter.is_null() { - get(filter)?.check_world(world)?; + let owner = get(controller)?.1; + if !options.is_null() { + get(options)?.check_world(owner)?; } - let access = get(world)?.write()?; + let access = get(owner)?.write()?; let raw = access.raw(); let world: *mut RprPhysicsWorld = raw; positive(dt)?; let c = &mut get_mut(controller)?.0; - let mut f = if filter.is_null() { - RprQueryFilter::default() - } else { - *get(filter)? - } - .raw()?; - f.exclude_rigid_body = Some(c.chassis); - let w = &mut get_mut(world)?.0; - let chassis = w.bodies.get(c.chassis).ok_or_else(missing)?; + let chassis_handle = c.chassis; + let chassis = get(world)? + .0 + .bodies + .get(chassis_handle) + .ok_or_else(missing)?; ensure( chassis.is_dynamic() && chassis.soft_body().is_none(), "vehicle chassis must be an ordinary dynamic body", )?; + let options = MutableQueryOptions::new(owner, &get(world)?.0, options)?; + let mut filter = options.filter; + // Keep the caller's body exclusion; the chassis is then excluded by the predicate. + let exclude_chassis = filter + .exclude_rigid_body + .is_some_and(|h| h != chassis_handle); + if filter.exclude_rigid_body.is_none() { + filter.exclude_rigid_body = Some(chassis_handle); + } + let predicate = |handle: ColliderHandle, collider: &Collider| { + options.accepts(handle) + && !(exclude_chassis && collider.parent() == Some(chassis_handle)) + }; + if options.accepted.is_some() || exclude_chassis { + filter.predicate = Some(&predicate); + } + let w = &mut get_mut(world)?.0; let q = w.broad_phase.as_query_pipeline_mut( w.narrow_phase.query_dispatcher(), &mut w.bodies, &mut w.colliders, - f, + filter, ); c.update_vehicle(dt, q); Ok(()) @@ -641,7 +796,7 @@ pub use vehicle::*; /// PID controller with persistent integral state. /// Stateful proportional-integral-derivative controller. Release with the matching Free function. /// @ingroup controllers -pub struct RprPidController(rapier::control::PidController); +pub struct RprPidController(pub(crate) rapier::control::PidController); /// Per-axis proportional, integral, and derivative controller gains. /// @ingroup controllers @@ -662,8 +817,8 @@ pub struct RprPidGains { pub ang_kd: RprAngVector, } -/// Allocate a PID controller with supplied gains and controlled axes. Release with -/// rpr_free_pid_controller. +/// Allocate a PID controller with Rapier's defaults: kp = 60, ki = 1 and kd = 0.8 on every axis, all +/// axes controlled, and zero integrals. Release with rpr_free_pid_controller. /// @ingroup controllers #[rapier_export] pub unsafe extern "C" fn rpr_new_pid_controller() -> *mut RprPidController { @@ -736,7 +891,8 @@ pub unsafe extern "C" fn rpr_pid_controller_set_gains( Ok(()) }) } -/// AxesMask bits match Rapier: linear X/Y/Z are 1/2/4, angular X/Y/Z are 8/16/32. +/// Set the controlled axes, a combination of RPR_AXES_MASK_* bits. Gains are unchanged; unknown +/// bits are rejected. /// @ingroup controllers #[rapier_export(pid_controller)] pub unsafe extern "C" fn rpr_pid_controller_set_axes( @@ -744,14 +900,142 @@ pub unsafe extern "C" fn rpr_pid_controller_set_axes( axes: u32, ) -> RprStatus { ffi(|| unsafe { - let axes = u8::try_from(axes) - .ok() - .and_then(AxesMask::from_bits) - .ok_or_else(|| invalid("unknown PID axes"))?; + let axes = axes_mask(axes)?; get_mut(controller)?.0.set_axes(axes); Ok(()) }) } +/// Return the controlled axes as RPR_AXES_MASK_* bits. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_axes(controller: *const RprPidController) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { output(out, get(controller)?.0.axes().bits() as u32) }) + }) +} +/// Reset to zero the linear and angular errors accumulated by the integral term. +/// @ingroup controllers +#[rapier_export(pid_controller)] +pub unsafe extern "C" fn rpr_pid_controller_reset_integrals( + controller: *mut RprPidController, +) -> RprStatus { + ffi(|| unsafe { + get_mut(controller)?.0.reset_integrals(); + Ok(()) + }) +} +pub(crate) fn axes_mask(axes: u32) -> Result { + u8::try_from(axes) + .ok() + .and_then(AxesMask::from_bits) + .ok_or_else(|| invalid("unknown controller axes")) +} + +/// Stateless proportional-derivative controller: a PID controller without integral term, stored as +/// a plain value. Initialize with rpr_default_pd_controller. +/// @ingroup controllers +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprPdController { + /// Linear proportional gain per axis. + pub lin_kp: RprVector, + /// Linear derivative gain per axis. + pub lin_kd: RprVector, + /// Angular proportional gain per axis. + pub ang_kp: RprAngVector, + /// Angular derivative gain per axis. + pub ang_kd: RprAngVector, + /// Controlled axes, a combination of RPR_AXES_MASK_* bits. + pub axes: u32, +} +impl RprPdController { + pub(crate) fn raw(&self) -> Result { + Ok(rapier::control::PdController { + lin_kp: self.lin_kp.raw()?, + lin_kd: self.lin_kd.raw()?, + ang_kp: angular(self.ang_kp)?, + ang_kd: angular(self.ang_kd)?, + axes: axes_mask(self.axes)?, + }) + } +} +/// Return Rapier's default PD controller: kp = 60 and kd = 0.8 on every axis, all axes controlled. +/// This POD value owns no resources. +/// @ingroup controllers +#[rapier_export] +pub extern "C" fn rpr_default_pd_controller() -> RprPdController { + let pd = rapier::control::PdController::default(); + RprPdController { + lin_kp: pd.lin_kp.into(), + lin_kd: pd.lin_kd.into(), + ang_kp: angular_out(pd.ang_kp), + ang_kd: angular_out(pd.ang_kd), + axes: pd.axes.bits() as u32, + } +} +/// Compute the velocity change bringing the body toward the target pose and velocities. Neither the +/// body nor the controller is modified. +/// @ingroup controllers +#[rapier_export(pd_controller)] +pub unsafe extern "C" fn rpr_pd_controller_rigid_body_correction( + controller: *const RprPdController, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, +) -> RprVelocityCorrection { + let world = body.world; + ffi_value(|result: *mut RprVelocityCorrection| { + let linear = unsafe { std::ptr::addr_of_mut!((*result).linear) }; + let angular_velocity = unsafe { std::ptr::addr_of_mut!((*result).angularVelocity) }; + + ffi(|| unsafe { + body.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_pd_controller_rigid_body_correction( + controller, + std::ptr::addr_of!((*raw).0.bodies).cast(), + body, + target_pose, + target_linvel, + target_angvel, + linear, + angular_velocity, + )) + }) + }) +} + +pub(crate) unsafe fn native_pd_controller_rigid_body_correction( + controller: *const RprPdController, + bodies: *const RprRigidBodySet, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, + linear: *mut RprVector, + angular_velocity: *mut RprAngVector, +) -> RprStatus { + ffi(|| unsafe { + out_ptr(linear)?; + out_ptr(angular_velocity)?; + let pd = get(controller)?.raw()?; + let pose = target_pose.raw()?; + let velocity = RigidBodyVelocity { + linvel: target_linvel.raw()?, + angvel: angular(target_angvel)?, + }; + let correction = pd.rigid_body_correction( + get(bodies)?.0.get(body.raw()).ok_or_else(missing)?, + pose, + velocity, + ); + output(linear, correction.linvel.into())?; + output(angular_velocity, angular_out(correction.angvel)) + }) +} /// Compute a velocity correction, preserving the body's state and updating PID integrals. /// @ingroup controllers #[rapier_export(pid_controller)] @@ -844,14 +1128,13 @@ pub unsafe extern "C" fn rpr_kinematic_character_controller_settings( ffi_value(|out: *mut RprCharacterControllerSettings| { ffi(|| unsafe { let c = &get(controller)?.inner; - let snap_distance = match c.snap_to_ground { - Some(CharacterLength::Relative(value)) => RprCharacterLength { value, relative: 1 }, - Some(CharacterLength::Absolute(value)) => RprCharacterLength { value, relative: 0 }, - None => RprCharacterLength { + let snap_distance = c.snap_to_ground.map_or( + RprCharacterLength { value: 0.1, relative: 1, }, - }; + Into::into, + ); output( out, RprCharacterControllerSettings { diff --git a/c/src/descriptors.rs b/c/src/descriptors.rs index 4346d0b1a..e043e830a 100644 --- a/c/src/descriptors.rs +++ b/c/src/descriptors.rs @@ -202,6 +202,9 @@ pub const RPR_SHAPE_DESC_COMPOUND: u32 = 14; /// @ingroup shapes /// ShapeDesc kind selecting a round cylinder. pub const RPR_SHAPE_DESC_ROUND_CYLINDER: u32 = 15; +/// @ingroup shapes +/// ShapeDesc kind selecting a round cone. +pub const RPR_SHAPE_DESC_ROUND_CONE: u32 = 16; /// Non-owning shape description. Only fields selected by kind are read. /// a = cuboid half extents, capsule/segment endpoint, triangle vertex, or halfspace normal. @@ -224,7 +227,7 @@ pub struct RprShapeDesc { pub radius: RprReal, /// Half the height of a cylinder or cone. pub halfHeight: RprReal, - /// Rounding radius for a rounded shape. + /// Rounding radius of a round cylinder or round cone (the round cuboid reads radius instead). pub borderRadius: RprReal, /// Borrowed vertex positions. pub vertices: RprVectorView, @@ -330,6 +333,12 @@ impl RprShapeDesc { positive(self.radius)?, nonnegative(self.borderRadius)?, ), + #[cfg(feature = "dim3")] + RPR_SHAPE_DESC_ROUND_CONE => SharedShape::round_cone( + positive(self.halfHeight)?, + positive(self.radius)?, + nonnegative(self.borderRadius)?, + ), RPR_SHAPE_DESC_SHARED => unsafe { get(self.sharedShape)?.0.clone() }, RPR_SHAPE_DESC_COMPOUND => { let mut shapes = Vec::new(); @@ -506,9 +515,9 @@ pub struct RprColliderDesc { pub friction: RprReal, /// Nonnegative restitution coefficient. pub restitution: RprReal, - /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. pub frictionCombineRule: u32, - /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, or MAX. + /// RPR_COMBINE_AVERAGE, MIN, MULTIPLY, MAX, CLAMPED_SUM, or GEOMETRIC_MEAN. pub restitutionCombineRule: u32, /// 1 detects intersections without generating contact forces. pub isSensor: RprBool, diff --git a/c/src/dynamics.rs b/c/src/dynamics.rs index 3770cd3cc..1e50e23fc 100644 --- a/c/src/dynamics.rs +++ b/c/src/dynamics.rs @@ -796,3 +796,55 @@ pub unsafe extern "C" fn rpr_rigid_body_wake_up( Ok(()) }) } + +pub(crate) unsafe fn native_rigid_body_dominance_group( + object: *const RprRigidBody, + out: *mut i8, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.dominance_group()) + }) +} + +pub(crate) unsafe fn native_rigid_body_additional_solver_iterations( + object: *const RprRigidBody, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.additional_solver_iterations()) + }) +} + +pub(crate) unsafe fn native_rigid_body_additional_pgs_iterations( + object: *const RprRigidBody, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.additional_pgs_iterations()) + }) +} + +pub(crate) unsafe fn native_rigid_body_is_fast_rotation_allowed( + object: *const RprRigidBody, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.is_fast_rotation_allowed() as RprBool) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_allow_fast_rotation( + object: *mut RprRigidBody, + value: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let value = boolean(value)?; + let object = get_mut(object)?; + object.0.set_allow_fast_rotation(value); + Ok(()) + }) +} diff --git a/c/src/extra.rs b/c/src/extra.rs index 7505b75e5..a9a81c9ce 100644 --- a/c/src/extra.rs +++ b/c/src/extra.rs @@ -296,7 +296,7 @@ impl From<&ContactPair> for RprContactPair { } } /// Sensor intersection state for a collider pair. -/// @ingroup math +/// @ingroup events #[repr(C)] #[derive(Copy, Clone, Default)] pub struct RprIntersectionPair { @@ -332,7 +332,8 @@ pub unsafe extern "C" fn rpr_contact_pairs( } } -/// Return the narrow-phase contact pair for two colliders, or report RPR_NOT_FOUND. +/// Return the narrow-phase contact pair for two colliders, or report RPR_NOT_FOUND. Its collider1 and +/// collider2 follow the narrow-phase order, which may differ from the argument order. /// @ingroup events #[rapier_export] pub unsafe extern "C" fn rpr_contact_pair( @@ -408,12 +409,38 @@ pub struct RprContactPoint { pub distance: RprReal, /// Normal impulse applied at this contact. pub impulse: RprReal, + /// Friction impulse along the tangent basis of the contact. + #[cfg(feature = "dim2")] + pub tangent_impulse: [RprReal; 1], + /// Friction impulses along the two tangent basis vectors of the contact. + #[cfg(feature = "dim3")] + pub tangent_impulse: [RprReal; 2], +} +/// The geometric contacts of all the manifolds of a pair, in the pair's collider order. +pub(crate) fn contact_points(pair: &ContactPair) -> Vec { + pair.manifolds() + .iter() + .enumerate() + .flat_map(|(i, m)| { + m.points.iter().map(move |p| RprContactPoint { + manifold_index: i, + local_p1: p.local_p1.into(), + local_p2: p.local_p2.into(), + normal: m.data.normal.into(), + distance: p.dist, + impulse: p.data.impulse, + tangent_impulse: p.data.tangent_impulse.into(), + }) + }) + .collect() } /// Contact points in collider-local space; normal in world space. Geometric manifolds may be /// recycled. +/// local_p1/local_p2 follow the pair's own collider1/collider2 order (see rpr_contact_pair), which +/// may differ from the argument order. /// For clustered solver impulses use contact pair totals. Soft pairs have no rigid manifolds. /// @see @ref output_buffers -/// @ingroup worlds +/// @ingroup events #[rapier_export] pub unsafe extern "C" fn rpr_contact_points( collider1: RprColliderHandle, @@ -435,22 +462,7 @@ pub unsafe extern "C" fn rpr_contact_points( .0 .contact_pair(collider1.raw(), collider2.raw()) .ok_or((RPR_NOT_FOUND, "no contact pair".into()))?; - let v: Vec<_> = p - .manifolds() - .iter() - .enumerate() - .flat_map(|(i, m)| { - m.points.iter().map(move |p| RprContactPoint { - manifold_index: i, - local_p1: p.local_p1.into(), - local_p2: p.local_p2.into(), - normal: m.data.normal.into(), - distance: p.dist, - impulse: p.data.impulse, - }) - }) - .collect(); - copy_out(&v, buffer, capacity, count) + copy_out(&contact_points(p), buffer, capacity, count) }) }) } @@ -474,7 +486,7 @@ pub unsafe extern "C" fn rpr_multibody_joint_generalized_velocity( let set: *const RprMultibodyJointSet = std::ptr::addr_of!((*raw).0.multibody_joints).cast(); - let (m, _) = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + let (m, _) = multibody_joint_link(&get(set)?.0, handle)?; copy_out(m.generalized_velocity().as_slice(), buffer, capacity, count) }) }) @@ -501,6 +513,7 @@ pub unsafe extern "C" fn rpr_multibody_joint_set_generalized_velocity( for &x in v { finite(x)?; } + multibody_joint_link(&get(set)?.0, handle)?; let (m, _) = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; ensure( m.ndofs() == count, @@ -513,7 +526,11 @@ pub unsafe extern "C" fn rpr_multibody_joint_set_generalized_velocity( }) } -/// Check this before passing any dimension/precision-dependent structs across the ABI. +/// Check that the header matches the linked library before passing any structure across the ABI. +/// Pass RPR_ABI_VERSION, RPR_DIMENSION, the sizes of RprReal, RprVector and RprPose, and +/// RPR_ABI_FEATURES. Fails with RPR_INVALID_ARGUMENT when the version, dimension, precision, or +/// the RAPIER_FEM/RAPIER_ROBOTICS defines differ from the library, since they change structure +/// layouts. /// @ingroup errors #[rapier_export] pub unsafe extern "C" fn rpr_check_abi( @@ -522,6 +539,7 @@ pub unsafe extern "C" fn rpr_check_abi( real_size: usize, vector_size: usize, pose_size: usize, + features: u32, ) -> RprStatus { ffi(|| { ensure( @@ -531,6 +549,28 @@ pub unsafe extern "C" fn rpr_check_abi( && vector_size == std::mem::size_of::() && pose_size == std::mem::size_of::(), "header/library ABI mismatch", + )?; + let mismatch = features ^ RPR_ABI_FEATURES; + for (bit, define) in [ + (RPR_ABI_FEATURE_FEM, "RAPIER_FEM"), + (RPR_ABI_FEATURE_ROBOTICS, "RAPIER_ROBOTICS"), + ] { + if mismatch & bit != 0 { + // The bit differs, so the library has it exactly when the header does not. + let state = if features & bit != 0 { + "without" + } else { + "with" + }; + return Err(invalid(format!( + "header/library ABI mismatch: the library is built {state} the feature \ + selected by {define}; the header defines must match it" + ))); + } + } + ensure( + mismatch == 0, + "header/library ABI mismatch: unknown ABI features", ) }) } diff --git a/c/src/geometry.rs b/c/src/geometry.rs index 0d49c9615..122500734 100644 --- a/c/src/geometry.rs +++ b/c/src/geometry.rs @@ -218,8 +218,9 @@ pub(crate) unsafe fn impl_rpr_shared_shape_polyline( .map(RprVector::raw) .collect::>>()?; let indices = indices_array::<2>(indices, element_count, vertex_count)?; - ensure(!indices.is_empty(), "empty mesh")?; - let shape = SharedShape::polyline(points, Some(indices)); + ensure(points.len() >= 2, "not enough vertices")?; + // No edges connects the vertices in order, as a line strip. + let shape = SharedShape::polyline(points, (!indices.is_empty()).then_some(indices)); output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) }) } @@ -820,3 +821,73 @@ pub(crate) unsafe fn impl_rpr_shared_shape_trimesh_with_flags( output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) }) } + +pub(crate) unsafe fn native_collider_active_collision_types( + object: *const RprCollider, + out: *mut u16, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.active_collision_types().bits()) + }) +} + +pub(crate) unsafe fn native_collider_active_hooks( + object: *const RprCollider, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, object.0.active_hooks().bits()) + }) +} + +pub(crate) unsafe fn native_collider_friction_combine_rule( + object: *const RprCollider, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, combine_value(object.0.friction_combine_rule())) + }) +} + +pub(crate) unsafe fn native_collider_restitution_combine_rule( + object: *const RprCollider, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output(out, combine_value(object.0.restitution_combine_rule())) + }) +} + +pub(crate) unsafe fn native_collider_position_wrt_parent( + object: *const RprCollider, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let object = get(object)?; + output( + out, + object + .0 + .position_wrt_parent() + .copied() + .unwrap_or(*object.0.position()) + .into(), + ) + }) +} + +pub(crate) unsafe fn native_collider_set_rotation( + object: *mut RprCollider, + value: RprRotation, +) -> RprStatus { + ffi(|| unsafe { + let value = value.raw()?; + let object = get_mut(object)?; + object.0.set_rotation(value); + Ok(()) + }) +} diff --git a/c/src/geometry_views.rs b/c/src/geometry_views.rs index 9a08652f7..e4994c2a2 100644 --- a/c/src/geometry_views.rs +++ b/c/src/geometry_views.rs @@ -88,6 +88,34 @@ pub unsafe extern "C" fn rpr_convex_hull_shared_shape( }) }) } +/// Create an owned convex polyhedron from vertices and triangle indices assumed to form a convex +/// mesh (no convex hull is computed); fails on degenerate input. Release it with rpr_free_shared_shape. +/// Copies typed input geometry into an owned shared shape; arrays may be released on return. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_convex_mesh_shared_shape( + vertices: RprVectorView, + indices: RprTriangleView, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + crate::array_views::validate_view(vertices.data, vertices.count)?; + crate::array_views::validate_view(indices.data, indices.count)?; + out_ptr(out)?; + let points = input(vertices.data, vertices.count)? + .iter() + .copied() + .map(RprVector::raw) + .collect::>>()?; + let indices = indices_array::<3>(indices.data.cast(), indices.count, points.len())?; + ensure(!indices.is_empty(), "empty mesh")?; + let shape = SharedShape::convex_mesh(points, &indices) + .ok_or_else(|| invalid("degenerate convex mesh"))?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape)))) + }) + }) +} /// Create an owned triangle mesh from vertices and triangle indices. Release it with /// rpr_free_shared_shape. /// Copies typed input geometry into an owned shared shape; arrays may be released on return. @@ -112,6 +140,7 @@ pub unsafe extern "C" fn rpr_trimesh_shared_shape( }) } /// Create an owned polyline from vertices and edge indices. Release it with rpr_free_shared_shape. +/// Empty indices connect the vertices in order (a line strip). /// Copies typed input geometry into an owned shared shape; arrays may be released on return. /// @ingroup shapes #[rapier_export] diff --git a/c/src/handle_access.rs b/c/src/handle_access.rs index 6e6b8ab78..3dec67726 100644 --- a/c/src/handle_access.rs +++ b/c/src/handle_access.rs @@ -936,7 +936,8 @@ pub(crate) unsafe fn native_collider_set_get_parent( }) } -/// Set the collider world-space pose. +/// Set the collider world-space pose. For a collider attached to a rigid body, prefer +/// SetPositionWrtParent: the body pose overwrites it at the next step. /// @ingroup colliders #[rapier_export(collider)] pub unsafe extern "C" fn rpr_collider_set_position( @@ -959,7 +960,8 @@ pub unsafe extern "C" fn rpr_collider_set_position( }) } -/// Set the collider world-space translation. +/// Set the collider world-space translation. For a collider attached to a rigid body, prefer +/// SetPositionWrtParent: the body pose overwrites it at the next step. /// @ingroup colliders #[rapier_export(collider)] pub unsafe extern "C" fn rpr_collider_set_translation( @@ -1403,3 +1405,368 @@ pub unsafe extern "C" fn rpr_impulse_joint_set_desc( Ok(()) }) } + +/// Return the collider body-type collision activation bitmask (RPR_COLLISION_TYPES_* bits). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_active_collision_types(handle: RprColliderHandle) -> u16 { + let world = handle.world; + ffi_value(|out: *mut u16| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_active_collision_types( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_active_collision_types( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut u16, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_active_collision_types( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider physics-hook activation bitmask. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_active_hooks(handle: RprColliderHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_active_hooks( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_active_hooks( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_active_hooks( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider friction combination rule (RPR_COMBINE_*). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_friction_combine_rule(handle: RprColliderHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_friction_combine_rule( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_friction_combine_rule( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_friction_combine_rule( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider restitution combination rule (RPR_COMBINE_*). +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_restitution_combine_rule(handle: RprColliderHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_restitution_combine_rule( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_restitution_combine_rule( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut u32, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_restitution_combine_rule( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the collider pose relative to its parent rigid body, or its world-space pose if it has +/// no parent. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_position_wrt_parent(handle: RprColliderHandle) -> RprPose { + let world = handle.world; + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_collider_set_get_position_wrt_parent( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_collider_set_get_position_wrt_parent( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprPose, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_position_wrt_parent( + (element as *const Collider).cast(), + out, + )) + }) +} + +/// Return the rigid body signed dominance group. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_dominance_group(handle: RprRigidBodyHandle) -> i8 { + let world = handle.world; + ffi_value(|out: *mut i8| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_dominance_group( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_dominance_group( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut i8, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_dominance_group( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body additional solver iterations for connected bodies. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_additional_solver_iterations( + handle: RprRigidBodyHandle, +) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_additional_solver_iterations( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_additional_solver_iterations( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_additional_solver_iterations( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return the rigid body additional PGS iterations for connected bodies. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_additional_pgs_iterations( + handle: RprRigidBodyHandle, +) -> usize { + let world = handle.world; + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_additional_pgs_iterations( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_additional_pgs_iterations( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut usize, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_additional_pgs_iterations( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Return whether the rigid body may exceed the angular-velocity limit of its CCD. +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_is_fast_rotation_allowed( + handle: RprRigidBodyHandle, +) -> RprBool { + let world = handle.world; + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + crate::handle_access::forward(native_rigid_body_set_get_is_fast_rotation_allowed( + std::ptr::addr_of!((*raw).0.bodies).cast(), + handle, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_rigid_body_set_get_is_fast_rotation_allowed( + set: *const RprRigidBodySet, + handle: RprRigidBodyHandle, + out: *mut RprBool, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_rigid_body_is_fast_rotation_allowed( + (element as *const RigidBody).cast(), + out, + )) + }) +} + +/// Set the collider world-space rotation. For a collider attached to a rigid body, prefer +/// SetPositionWrtParent: the body pose overwrites it at the next step. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_set_rotation( + handle: RprColliderHandle, + value: RprRotation, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprColliderSet = std::ptr::addr_of_mut!((*raw).0.colliders).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_collider_set_rotation( + (element as *mut Collider).cast(), + value, + )) + }) +} + +/// Allow or disallow the rigid body to exceed the angular-velocity limit of its CCD (e.g. for +/// wheels). +/// @ingroup rigid_bodies +#[rapier_export(rigid_body)] +pub unsafe extern "C" fn rpr_rigid_body_set_allow_fast_rotation( + handle: RprRigidBodyHandle, + value: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprRigidBodySet = std::ptr::addr_of_mut!((*raw).0.bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + ensure( + element.soft_body().is_none(), + "mutate soft-body proxies through the soft-body API", + )?; + forward(native_rigid_body_set_allow_fast_rotation( + (element as *mut RigidBody).cast(), + value, + )) + }) +} diff --git a/c/src/handle_world.rs b/c/src/handle_world.rs index 732a987aa..5d5f3dd19 100644 --- a/c/src/handle_world.rs +++ b/c/src/handle_world.rs @@ -57,6 +57,10 @@ fields_world!(RprIntersectionPair, collider1, collider2); fields_world!(RprJointBodies, body1, body2); fields_world!(RprOptionalParticleDestination, body); fields_world!(RprOptionalRayHit, hit); +fields_world!(RprOptionalShapeCastHit, hit); +fields_world!(RprOptionalPointProjection, projection); +fields_world!(RprOptionalContactPair, pair); +fields_world!(RprOptionalIntersectionPair, pair); fields_world!(RprParticleDestination, body); fields_world!(RprPointProjection, collider); fields_world!(RprQueryFilter, exclude_collider, exclude_rigid_body); diff --git a/c/src/joint_access.rs b/c/src/joint_access.rs index 416bce0ab..3889bc2a0 100644 --- a/c/src/joint_access.rs +++ b/c/src/joint_access.rs @@ -254,7 +254,7 @@ pub unsafe extern "C" fn rpr_impulse_joint_set_enabled( } /// Set the joint desc joint spring coefficients. -/// @ingroup soft_bodies +/// @ingroup joints #[rapier_export(joint_desc)] pub unsafe extern "C" fn rpr_joint_desc_set_softness( desc: *mut RprJointDesc, @@ -268,7 +268,7 @@ pub unsafe extern "C" fn rpr_joint_desc_set_softness( } /// Set the impulse joint joint spring coefficients. /// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. -/// @ingroup soft_bodies +/// @ingroup joints #[rapier_export(impulse_joint)] pub unsafe extern "C" fn rpr_impulse_joint_set_softness( handle: RprImpulseJointHandle, diff --git a/c/src/joint_extras.rs b/c/src/joint_extras.rs new file mode 100644 index 000000000..9de007a5d --- /dev/null +++ b/c/src/joint_extras.rs @@ -0,0 +1,338 @@ +//! Joint impulses, counts, user data and live multibody-joint descriptions. +use crate::*; + +/// Impulses applied by an impulse joint during the last step, along the axes of its joint frame. +/// @ingroup joints +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprJointImpulses { + /// Impulse applied along the locked translational axes. + pub linear: RprVector, + /// Angular impulse applied along the locked rotational axes (a scalar in 2D). + pub angular: RprAngVector, + /// Impulse applied by the limit of each axis, in translation-then-rotation order. + pub limits: [RprReal; RPR_JOINT_DOF_COUNT], + /// Impulse applied by the motor of each axis, in translation-then-rotation order. + pub motors: [RprReal; RPR_JOINT_DOF_COUNT], +} + +impl From<&ImpulseJoint> for RprJointImpulses { + fn from(joint: &ImpulseJoint) -> Self { + let i = &joint.impulses; + #[cfg(feature = "dim2")] + let (linear, angular) = (Vector::new(i[0], i[1]), i[2]); + #[cfg(feature = "dim3")] + let (linear, angular) = ( + Vector::new(i[0], i[1], i[2]), + AngVector::new(i[3], i[4], i[5]), + ); + Self { + linear: linear.into(), + angular: angular_out(angular), + limits: joint.data.limits.map(|l| l.impulse), + motors: joint.data.motors.map(|m| m.impulse), + } + } +} + +/// Return the impulses applied by the impulse joint during the last step. They are zero before +/// its first step, and their components are expressed along the axes of the joint frame. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_impulses( + handle: RprImpulseJointHandle, +) -> RprJointImpulses { + let world = handle.world; + ffi_value(|out: *mut RprJointImpulses| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + + let joint = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + output(out, joint.into()) + }) + }) +} + +/// Return the impulse joint application-owned 128-bit user value. +/// @ingroup joints +#[rapier_export(impulse_joint)] +pub unsafe extern "C" fn rpr_impulse_joint_user_data(handle: RprImpulseJointHandle) -> RprUserData { + let world = handle.world; + ffi_value(|out: *mut RprUserData| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + + let joint = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + output(out, joint.data.user_data.into()) + }) + }) +} + +/// Return the number of impulse joints in the world. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_impulse_joint_count(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprImpulseJointSet = std::ptr::addr_of!((*raw).0.impulse_joints).cast(); + output(out, get(set)?.0.len()) + }) + }) +} + +/// Return the number of multibody joints in the world, which is the number of handles copied by +/// rpr_multibody_joint_handles. +/// @ingroup joints +#[rapier_export] +pub unsafe extern "C" fn rpr_multibody_joint_count(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + output(out, get(set)?.0.iter().count()) + }) + }) +} + +/// The link attached to its parent by a multibody joint. A handle resolving to a root link is +/// stale: it belonged to a removed joint whose child became the root of a new multibody. +pub(crate) fn multibody_joint_link( + set: &MultibodyJointSet, + handle: RprMultibodyJointHandle, +) -> Result<(&Multibody, &MultibodyLink)> { + let (multibody, id) = set.get(handle.raw()).ok_or_else(missing)?; + if id == 0 { + return Err(missing()); + } + Ok((multibody, multibody.link(id).ok_or_else(missing)?)) +} + +/// Copies the multibody joint configuration without returning a borrowed joint pointer. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_desc(handle: RprMultibodyJointHandle) -> RprJointDesc { + let world = handle.world; + ffi_value(|out: *mut RprJointDesc| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + + let (_, link) = multibody_joint_link(&get(set)?.0, handle)?; + output(out, link.joint.data.into()) + }) + }) +} + +/// Replaces the multibody joint configuration after validation. lockedAxes defines the degrees of +/// freedom of the multibody and cannot change: a different value reports INVALID_ARGUMENT. +/// wake_up = 1 wakes the two connected bodies; 0 preserves their sleep state. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_set_desc( + handle: RprMultibodyJointHandle, + desc: *const RprJointDesc, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let physics: *mut RprPhysicsWorld = raw; + + let desc = get(desc)?.raw()?; + let wake_up = boolean(wake_up)?; + let physics = &mut get_mut(physics)?.0; + let (multibody, link) = multibody_joint_link(&physics.multibody_joints, handle)?; + ensure( + link.joint.data.locked_axes == desc.locked_axes, + "the locked axes of a multibody joint cannot change", + )?; + let body2 = link.rigid_body_handle(); + let body1 = link + .parent_id() + .and_then(|id| multibody.link(id)) + .map(|l| l.rigid_body_handle()); + let (multibody, id) = physics + .multibody_joints + .get_mut(handle.raw()) + .ok_or_else(missing)?; + multibody.link_mut(id).ok_or_else(missing)?.joint.data = desc; + if wake_up { + for body in body1.into_iter().chain([body2]) { + if let Some(body) = physics.bodies.get_mut(body) { + body.wake_up(true); + } + } + } + Ok(()) + }) +} + +/// Return the two bodies connected by a multibody joint: its parent link, then its own link. +/// @ingroup joints +#[rapier_export(multibody_joint)] +pub unsafe extern "C" fn rpr_multibody_joint_bodies( + handle: RprMultibodyJointHandle, +) -> RprJointBodies { + let world = handle.world; + ffi_world_value(world, |result: *mut RprJointBodies| { + let body1 = unsafe { std::ptr::addr_of_mut!((*result).body1) }; + let body2 = unsafe { std::ptr::addr_of_mut!((*result).body2) }; + + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprMultibodyJointSet = + std::ptr::addr_of!((*raw).0.multibody_joints).cast(); + + out_ptr(body1)?; + out_ptr(body2)?; + let (multibody, link) = multibody_joint_link(&get(set)?.0, handle)?; + let parent = link + .parent_id() + .and_then(|id| multibody.link(id)) + .ok_or_else(missing)?; + output(body1, parent.rigid_body_handle().into())?; + output(body2, link.rigid_body_handle().into()) + }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ptr; + + unsafe fn body(world: *mut RprWorld, kind: u32, x: Real) -> RprRigidBodyHandle { + unsafe { + let mut desc = match kind { + RPR_FIXED => rpr_fixed_rigid_body_desc(), + _ => rpr_dynamic_rigid_body_desc(), + }; + desc.position.translation = (Vector::X * x).into(); + let handle = rpr_insert_rigid_body(world, &desc); + rpr_insert_collider(handle, &rpr_ball_collider_desc(0.1)); + assert_eq!(rpr_last_status(), RPR_OK); + handle + } + } + + #[test] + fn impulse_joint_impulses_user_data_and_counts() { + unsafe { + let world = rpr_new_world(); + let ground = body(world, RPR_FIXED, 0.0); + let bob = body(world, RPR_DYNAMIC, 1.0); + let mut desc = rpr_fixed_joint_desc(); + desc.localFrame1.translation = Vector::X.into(); + desc.userData = RprUserData { low: 7, high: 9 }; + let joint = rpr_insert_impulse_joint(ground, bob, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_impulse_joint_count(world), 1); + assert_eq!(rpr_multibody_joint_count(world), 0); + let user_data = rpr_impulse_joint_user_data(joint); + assert_eq!((user_data.low, user_data.high), (7, 9)); + + let impulses = rpr_impulse_joint_impulses(joint); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(impulses.linear.raw().unwrap(), Vector::ZERO); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + // The joint holds the body against gravity. + let impulses = rpr_impulse_joint_impulses(joint); + assert!(impulses.linear.raw().unwrap().length() > 0.0); + assert_eq!(impulses.limits, [0.0; RPR_JOINT_DOF_COUNT]); + + assert_eq!(rpr_remove_impulse_joint(joint, 1), RPR_OK); + assert_eq!(rpr_impulse_joint_count(world), 0); + rpr_impulse_joint_impulses(joint); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + rpr_impulse_joint_user_data(joint); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn multibody_joint_desc_bodies_and_stale_handles() { + unsafe { + let world = rpr_new_world(); + let root = body(world, RPR_FIXED, 0.0); + let link1 = body(world, RPR_DYNAMIC, 1.0); + let link2 = body(world, RPR_DYNAMIC, 2.0); + let desc = rpr_prismatic_joint_desc(Vector::X.into()); + let joint1 = rpr_insert_multibody_joint(root, link1, &desc); + let joint2 = rpr_insert_multibody_joint(link1, link2, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_multibody_joint_count(world), 2); + + let bodies = rpr_multibody_joint_bodies(joint2); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!((bodies.body1, bodies.body2), (link1, link2)); + + let mut changed = rpr_multibody_joint_desc(joint2); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(changed.lockedAxes, desc.lockedAxes); + assert_eq!( + rpr_joint_desc_set_motor_velocity(&mut changed, RPR_AXIS_LIN_X, 1.0, 0.5), + RPR_OK + ); + assert_eq!(rpr_rigid_body_sleep(link2), RPR_OK); + assert_eq!(rpr_multibody_joint_set_desc(joint2, &changed, 1), RPR_OK); + assert_eq!(rpr_rigid_body_is_sleeping(link2), 0); + assert_eq!(rpr_multibody_joint_desc(joint2).motors[0].targetVel, 1.0); + + // The locked axes define the multibody degrees of freedom. + let mut locked = changed; + locked.lockedAxes = rpr_fixed_joint_desc().lockedAxes; + assert_eq!( + rpr_multibody_joint_set_desc(joint2, &locked, 1), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_multibody_joint_desc(joint2).lockedAxes, desc.lockedAxes); + // Until the next step, the fixed root still counts as a free root. + assert!(rpr_multibody_joint_ndofs(joint2) > 2); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + assert_eq!(rpr_multibody_joint_ndofs(joint2), 2); + + // Removing the first joint makes link1 the root of a new multibody. Its old handle + // resolves to that root link and must be rejected. + assert_eq!(rpr_remove_multibody_joint(joint1, 1), RPR_OK); + assert_eq!(rpr_multibody_joint_count(world), 1); + let access = (*world).read().unwrap(); + let native = &(*access.raw()).0.multibody_joints; + assert_eq!(native.get(joint1.raw()).map(|(_, id)| id), Some(0)); + drop(access); + rpr_multibody_joint_desc(joint1); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + rpr_multibody_joint_bodies(joint1); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(rpr_remove_multibody_joint(joint1, 1), RPR_INVALID_HANDLE); + assert_eq!(rpr_multibody_joint_count(world), 1); + assert_eq!(rpr_multibody_joint_bodies(joint2).body1, link1); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } +} diff --git a/c/src/joints.rs b/c/src/joints.rs index c3f86c3f1..0f503ee68 100644 --- a/c/src/joints.rs +++ b/c/src/joints.rs @@ -297,7 +297,7 @@ pub unsafe extern "C" fn rpr_remove_multibody_joint( let wake_up = boolean(wake_up)?; let set = get_mut(set)?; - set.0.get(handle.raw()).ok_or_else(missing)?; + multibody_joint_link(&set.0, handle)?; set.0.remove(handle.raw(), wake_up); Ok(()) }) @@ -383,7 +383,9 @@ pub extern "C" fn rpr_default_inverse_kinematics_options() -> RprInverseKinemati } } -/// Return the articulation degrees of freedom associated with the joint. +/// Return the degrees of freedom of the whole multibody containing the joint (not of the joint +/// alone), including the free root of a dynamic multibody. After inserting a joint, the root's +/// contribution is only updated by the next step. /// @ingroup joints #[rapier_export(multibody_joint)] pub unsafe extern "C" fn rpr_multibody_joint_ndofs(handle: RprMultibodyJointHandle) -> usize { @@ -397,10 +399,7 @@ pub unsafe extern "C" fn rpr_multibody_joint_ndofs(handle: RprMultibodyJointHand let set: *const RprMultibodyJointSet = std::ptr::addr_of!((*raw).0.multibody_joints).cast(); - output( - out, - get(set)?.0.get(handle.raw()).ok_or_else(missing)?.0.ndofs(), - ) + output(out, multibody_joint_link(&get(set)?.0, handle)?.0.ndofs()) }) }) } @@ -430,8 +429,8 @@ pub unsafe extern "C" fn rpr_multibody_joint_inverse_kinematics( let set: *const RprMultibodyJointSet = std::ptr::addr_of!((*raw).0.multibody_joints).cast(); let bodies: *const RprRigidBodySet = std::ptr::addr_of!((*raw).0.bodies).cast(); + multibody_joint_link(&get(set)?.0, handle)?; let (multibody, link_id) = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; - ensure(multibody.link(link_id).is_some(), "invalid multibody link")?; ensure( count == multibody.ndofs(), "displacement count must equal articulation dofs", @@ -493,6 +492,7 @@ pub unsafe extern "C" fn rpr_multibody_joint_apply_displacements( for &value in values { finite(value)?; } + multibody_joint_link(&get(set)?.0, handle)?; let (multibody, _) = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; ensure( count == multibody.ndofs(), diff --git a/c/src/lib.rs b/c/src/lib.rs index bf5a0d6c5..9d58a1991 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -38,6 +38,9 @@ pub use types::*; mod extra; pub use extra::*; +mod world_extras; +pub use world_extras::*; + #[cfg(test)] mod tests; @@ -64,12 +67,20 @@ pub use shape_desc::*; mod soft_recipes; pub use soft_recipes::*; +mod soft_extras; +pub use soft_extras::*; +#[cfg(test)] +mod soft_extras_tests; + mod scoped_access; pub use scoped_access::*; mod joint_access; pub use joint_access::*; +mod joint_extras; +pub use joint_extras::*; + mod geometry_views; pub use geometry_views::*; @@ -87,5 +98,8 @@ pub use return_values::*; mod handle_world; use handle_world::*; +mod queries_extras; +pub use queries_extras::*; + #[cfg(test)] mod owner_handle_tests; diff --git a/c/src/objects.rs b/c/src/objects.rs index 948f477a6..819063e98 100644 --- a/c/src/objects.rs +++ b/c/src/objects.rs @@ -339,36 +339,37 @@ pub unsafe extern "C" fn rpr_soft_body_contains(handle: RprSoftBodyHandle) -> Rp } /// Remove a body and its joints, optionally keeping colliders as standalone objects. -/// Returns whether a body was removed; a stale handle returns false without error. +/// A removed or stale handle fails with RPR_INVALID_HANDLE, like the other Remove functions. +/// Removing a soft-body cluster proxy removes its cluster (see rpr_soft_body_remove_cluster). The +/// root body of a soft body is rejected: remove the soft body with rpr_remove_soft_body. /// @ingroup rigid_bodies #[rapier_export] pub unsafe extern "C" fn rpr_remove_rigid_body( handle: RprRigidBodyHandle, remove_attached_colliders: RprBool, -) -> RprBool { +) -> RprStatus { let world = handle.world; - ffi_value(|removed: *mut RprBool| { - ffi(|| unsafe { - handle.check_world(world)?; - let remove = boolean(remove_attached_colliders)?; - if !removed.is_null() { - out_ptr(removed)?; - } - let access = get(world)?.write()?; - let world = &mut (*access.raw()).0; - if let Some(body) = world.bodies.get(handle.raw()) { - ensure( - body.soft_body().is_none() || body.is_soft_frame(), - "remove a soft-body root through RemoveSoftBody", - )?; - } - let did_remove = world - .remove_body_with_colliders(handle.raw(), remove) - .is_some(); - if !removed.is_null() { - output(removed, did_remove as RprBool)?; - } - Ok(()) - }) + ffi(|| unsafe { + handle.check_world(world)?; + let remove = boolean(remove_attached_colliders)?; + let access = get(world)?.write()?; + let world = &mut (*access.raw()).0; + let body = world.bodies.get(handle.raw()).ok_or_else(missing)?; + // A soft-body root is also a cluster proxy, but removing it would silently remove the + // whole body (or re-root it); require the explicit soft-body calls instead. + if let Some(soft) = body.soft_body() { + ensure( + world + .soft_bodies + .get(soft) + .is_none_or(|sb| sb.root_body() != handle.raw()), + "the root body of a soft body cannot be removed; use RemoveSoftBody (or \ + SoftBody_RemoveCluster for its cluster)", + )?; + } + world + .remove_body_with_colliders(handle.raw(), remove) + .ok_or_else(missing)?; + Ok(()) }) } diff --git a/c/src/owner_handle_tests.rs b/c/src/owner_handle_tests.rs index db8cea10c..1d6ca03b0 100644 --- a/c/src/owner_handle_tests.rs +++ b/c/src/owner_handle_tests.rs @@ -25,7 +25,7 @@ fn collider_insertion_checks_parent_and_preserves_world_on_failure() { RprRigidBodyHandle::default() ); ok(); - assert_eq!(rpr_remove_rigid_body(parent, 1), 1); + assert_eq!(rpr_remove_rigid_body(parent, 1), RPR_OK); ok(); assert_eq!(rpr_collider_count(world), 1); ok(); @@ -146,7 +146,7 @@ fn returned_arrays_joints_and_query_hits_keep_the_owner() { ); ok(); assert_eq!(hit.collider, item_collider); - assert_eq!(rpr_remove_rigid_body(item_body, 1), 1); + assert_eq!(rpr_remove_rigid_body(item_body, 1), RPR_OK); ok(); rpr_rigid_body_translation(item_body); assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); diff --git a/c/src/pipeline.rs b/c/src/pipeline.rs index 29b3ce3a7..533afafd8 100644 --- a/c/src/pipeline.rs +++ b/c/src/pipeline.rs @@ -369,8 +369,7 @@ pub unsafe extern "C" fn rpr_set_num_solver_iterations( let object: *mut NativeIntegrationParameters = std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); - ensure(value > 0, "iteration count must be positive")?; - get_mut(object)?.0.num_solver_iterations = value; + get_mut(object)?.0.num_solver_iterations = iterations(value)?; Ok(()) }) } @@ -407,15 +406,14 @@ pub unsafe extern "C" fn rpr_set_num_internal_pgs_iterations( let object: *mut NativeIntegrationParameters = std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); - ensure(value > 0, "iteration count must be positive")?; - get_mut(object)?.0.num_internal_pgs_iterations = value; + get_mut(object)?.0.num_internal_pgs_iterations = iterations(value)?; Ok(()) }) } /// Return the world setting documented by /// RprIntegrationParameters::numInternalStabilizationIterations. -/// @ingroup errors +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_num_internal_stabilization_iterations( world: *const RprWorld, @@ -436,7 +434,7 @@ pub unsafe extern "C" fn rpr_num_internal_stabilization_iterations( /// Set the world setting documented by /// RprIntegrationParameters::numInternalStabilizationIterations. -/// @ingroup errors +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_set_num_internal_stabilization_iterations( world: *mut RprWorld, @@ -603,7 +601,7 @@ pub unsafe extern "C" fn rpr_set_friction_in_bias_pass( } /// Return the world setting documented by RprIntegrationParameters::warmstartJoints. -/// @ingroup joints +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_warmstart_joints(world: *const RprWorld) -> RprBool { ffi_value(|out: *mut RprBool| { @@ -621,7 +619,7 @@ pub unsafe extern "C" fn rpr_warmstart_joints(world: *const RprWorld) -> RprBool } /// Set the world setting documented by RprIntegrationParameters::warmstartJoints. -/// @ingroup joints +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_set_warmstart_joints( world: *mut RprWorld, @@ -641,7 +639,7 @@ pub unsafe extern "C" fn rpr_set_warmstart_joints( } /// Return the world setting documented by RprIntegrationParameters::contactSoftness. -/// @ingroup soft_bodies +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_contact_softness(world: *const RprWorld) -> RprSpringCoefficients { ffi_value(|out: *mut RprSpringCoefficients| { @@ -659,7 +657,7 @@ pub unsafe extern "C" fn rpr_contact_softness(world: *const RprWorld) -> RprSpri } /// Set the world setting documented by RprIntegrationParameters::contactSoftness. -/// @ingroup soft_bodies +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_set_contact_softness( world: *mut RprWorld, @@ -679,7 +677,7 @@ pub unsafe extern "C" fn rpr_set_contact_softness( } /// Return the world setting documented by RprIntegrationParameters::staticContactSoftness. -/// @ingroup soft_bodies +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_static_contact_softness( world: *const RprWorld, @@ -699,7 +697,7 @@ pub unsafe extern "C" fn rpr_static_contact_softness( } /// Set the world setting documented by RprIntegrationParameters::staticContactSoftness. -/// @ingroup soft_bodies +/// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_set_static_contact_softness( world: *mut RprWorld, @@ -734,7 +732,7 @@ pub struct RprCollisionEvent { pub collider2: RprColliderHandle, /// 1 for a starting event, 0 for a stopping event. pub started: RprBool, - /// Event flags: bit 0 sensor pair, bit 1 removed collider. + /// Bitmask of RPR_COLLISION_EVENT_SENSOR and RPR_COLLISION_EVENT_REMOVED. pub flags: u32, } /// Contact-force event, enabled by flags and the collider force threshold. @@ -754,82 +752,111 @@ pub struct RprContactForceEvent { pub max_force_direction: RprVector, /// Magnitude of the strongest contact force. pub max_force_magnitude: RprReal, - /// 1 for a starting event, 0 for a stopping event. + /// 1 for the first step the total force magnitude exceeds the threshold, 0 on the following + /// steps while it stays above it. No event is emitted when the force drops below it. pub started: RprBool, } -/// Events accumulate until clear. Copying events never drains them, allowing two-call buffer -/// sizing. +/// Events accumulate across steps until rpr_event_collector_clear: reading them never drains the +/// collector, allowing two-call buffer sizing. Optional callbacks also see each event during the +/// step. /// @ingroup events #[derive(Default)] pub struct RprEventCollector { collisions: Mutex>, forces: Mutex>, tears: Mutex>, + pub(crate) callbacks: Mutex, } struct WorldEvents<'a> { world: *mut RprWorld, events: &'a RprEventCollector, + // Copied when the operation starts, so callbacks may replace them for the next one. + callbacks: RprEventCallbacks, +} +impl<'a> WorldEvents<'a> { + fn new(world: *mut RprWorld, events: &'a RprEventCollector) -> Self { + let callbacks = *events.callbacks.lock().unwrap(); + Self { + world, + events, + callbacks, + } + } } // The address is only copied into handles; the step holds the world write guard. unsafe impl Sync for WorldEvents<'_> {} impl EventHandler for WorldEvents<'_> { fn handle_collision_event( &self, - _: &RigidBodySet, - _: &ColliderSet, + bodies: &RigidBodySet, + colliders: &ColliderSet, event: CollisionEvent, - _: Option<&ContactPair>, + pair: Option<&ContactPair>, ) { let (a, b, started, flags) = match event { CollisionEvent::Started(a, b, f) => (a, b, 1, f.bits()), CollisionEvent::Stopped(a, b, f) => (a, b, 0, f.bits()), }; - self.events - .collisions - .lock() - .unwrap() - .push(RprCollisionEvent { - collider1: RprColliderHandle::from(a).with_world(self.world), - collider2: RprColliderHandle::from(b).with_world(self.world), - started, - flags, - }); + let event = RprCollisionEvent { + collider1: RprColliderHandle::from(a).with_world(self.world), + collider2: RprColliderHandle::from(b).with_world(self.world), + started, + flags, + }; + self.events.collisions.lock().unwrap().push(event); + if let Some(callback) = self.callbacks.collision_event { + let contacts = pair.map(contact_points).unwrap_or_default(); + let read = RprReadContext::new(self.world, bodies, colliders); + unsafe { + callback( + self.callbacks.user_data, + &read, + &event, + contacts.as_ptr(), + contacts.len(), + ) + }; + } } fn handle_contact_force_event( &self, dt: Real, - _: &RigidBodySet, - _: &ColliderSet, + bodies: &RigidBodySet, + colliders: &ColliderSet, pair: &ContactPair, magnitude: Real, ) { let e = ContactForceEvent::from_contact_pair(dt, pair, magnitude); - self.events - .forces - .lock() - .unwrap() - .push(RprContactForceEvent { - collider1: RprColliderHandle::from(e.collider1).with_world(self.world), - collider2: RprColliderHandle::from(e.collider2).with_world(self.world), - total_force: e.total_force.into(), - total_force_magnitude: e.total_force_magnitude, - max_force_direction: e.max_force_direction.into(), - max_force_magnitude: e.max_force_magnitude, - started: e.started as u32, - }); + let event = RprContactForceEvent { + collider1: RprColliderHandle::from(e.collider1).with_world(self.world), + collider2: RprColliderHandle::from(e.collider2).with_world(self.world), + total_force: e.total_force.into(), + total_force_magnitude: e.total_force_magnitude, + max_force_direction: e.max_force_direction.into(), + max_force_magnitude: e.max_force_magnitude, + started: e.started as u32, + }; + self.events.forces.lock().unwrap().push(event); + if let Some(callback) = self.callbacks.contact_force_event { + let read = RprReadContext::new(self.world, bodies, colliders); + unsafe { callback(self.callbacks.user_data, &read, &event) }; + } } - fn handle_soft_body_tear_event(&self, _: &SoftBodySet, event: &SoftBodyTearEvent) { - self.events - .tears - .lock() - .unwrap() - .push(RprSoftBodyTearEvent(event.clone(), self.world)); + fn handle_soft_body_tear_event(&self, soft_bodies: &SoftBodySet, event: &SoftBodyTearEvent) { + let particles = soft_bodies + .get(event.soft_body) + .map_or(0, |b| b.num_particles()); + self.events.tears.lock().unwrap().push(RprSoftBodyTearEvent( + event.clone(), + self.world, + particles, + )); } } /// Pair callback: -1 rejects a contact pair; 0 detects contacts without impulses; 1 computes /// impulses. /// For sensor intersections only, zero rejects and any positive value accepts. -/// @ingroup math +/// @ingroup callbacks pub type RprPairFilter = Option< unsafe extern "C" fn( user_data: *mut c_void, @@ -868,9 +895,9 @@ pub type RprModifyContacts = Option< ), >; /// Borrowed native contact context. Valid only during its callback; never retain or free it. -/// @ingroup events +/// @ingroup callbacks pub struct RprContactModificationContext { - raw: *mut c_void, + pub(crate) raw: *mut c_void, } /// Modify individual solver contacts through a borrowed context, valid only during the callback. /// @ingroup callbacks @@ -1004,7 +1031,7 @@ impl PhysicsHooks for WorldHooks { } /// Applies Rapier's persistent one-way platform logic to the borrowed manifold. -/// @ingroup worlds +/// @ingroup callbacks #[rapier_export(contact_modification_context)] pub unsafe extern "C" fn rpr_contact_modification_context_update_as_oneway_platform( context: *mut RprContactModificationContext, @@ -1023,7 +1050,7 @@ pub unsafe extern "C" fn rpr_contact_modification_context_update_as_oneway_platf } /// Sets the tangent velocity of every rigid solver contact in this manifold. -/// @ingroup worlds +/// @ingroup callbacks #[rapier_export(contact_modification_context)] pub unsafe extern "C" fn rpr_contact_modification_context_set_tangent_velocity( context: *mut RprContactModificationContext, @@ -1067,7 +1094,7 @@ pub unsafe extern "C" fn rpr_free_event_collector(events: *mut RprEventCollector Ok(()) }) } -/// Discard all collected events. Does not change the world. +/// Discard all collected events. Does not change the world or the callbacks. /// @ingroup events #[rapier_export(event_collector)] pub unsafe extern "C" fn rpr_event_collector_clear(events: *mut RprEventCollector) -> RprStatus { @@ -1079,7 +1106,7 @@ pub unsafe extern "C" fn rpr_event_collector_clear(events: *mut RprEventCollecto Ok(()) }) } -/// Copy the collected collision start/stop events without removing them. +/// Copy the collision start/stop events collected since the last clear, without removing them. /// @see @ref output_buffers /// @ingroup events #[rapier_export(event_collector)] @@ -1099,7 +1126,7 @@ pub unsafe extern "C" fn rpr_event_collector_collision_events( }) }) } -/// Copy the collected contact-force events without removing them. +/// Copy the contact-force events collected since the last clear, without removing them. /// @see @ref output_buffers /// @ingroup events #[rapier_export(event_collector)] @@ -1119,7 +1146,7 @@ pub unsafe extern "C" fn rpr_event_collector_contact_force_events( }) }) } -/// Return the number of queued soft-body tear events. +/// Return the number of soft-body tear events collected since the last clear. /// @ingroup events #[rapier_export(event_collector)] pub unsafe extern "C" fn rpr_event_collector_tear_event_count( @@ -1132,7 +1159,12 @@ pub unsafe extern "C" fn rpr_event_collector_tear_event_count( /// Owned copy of a tear event. Read particle remapping before rebuilding render meshes. /// @ingroup events #[derive(Clone)] -pub struct RprSoftBodyTearEvent(pub(crate) SoftBodyTearEvent, pub(crate) *mut RprWorld); +pub struct RprSoftBodyTearEvent( + pub(crate) SoftBodyTearEvent, + pub(crate) *mut RprWorld, + /// Particle count of the torn body after the tear, for a tear that split nothing off. + pub(crate) usize, +); // Owned event data plus a non-owning world address, never dereferenced by the event. unsafe impl Send for RprSoftBodyTearEvent {} unsafe impl Sync for RprSoftBodyTearEvent {} @@ -1189,8 +1221,9 @@ pub unsafe extern "C" fn rpr_set_gravity(world: *mut RprWorld, value: RprVector) }) } -/// Hooks and events may be NULL. This call invalidates all borrowed set-element pointers. -/// Advance simulation by one timestep. Hooks and events may be NULL. +/// Advance simulation by one timestep. Hooks and events may be NULL. Events are appended to the +/// collector, which is never cleared automatically. This call invalidates all borrowed set-element +/// pointers. /// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_step( @@ -1209,10 +1242,7 @@ pub unsafe extern "C" fn rpr_step( let events = if events.is_null() { None } else { - Some(WorldEvents { - world, - events: get(events)?, - }) + Some(WorldEvents::new(world, get(events)?)) }; let events: &dyn EventHandler = events.as_ref().map_or(&() as &dyn EventHandler, |e| e); (*access.raw()).0.step_with_events(&hooks, events); @@ -1220,7 +1250,8 @@ pub unsafe extern "C" fn rpr_step( }) } -/// Refresh collision detection without advancing simulation. Hooks and events may be NULL. +/// Refresh collision detection without advancing simulation. Hooks and events may be NULL; events +/// are appended to the collector. /// @ingroup worlds #[rapier_export] pub unsafe extern "C" fn rpr_detect_collisions( @@ -1239,10 +1270,7 @@ pub unsafe extern "C" fn rpr_detect_collisions( let events = if events.is_null() { None } else { - Some(WorldEvents { - world, - events: get(events)?, - }) + Some(WorldEvents::new(world, get(events)?)) }; let events: &dyn EventHandler = events.as_ref().map_or(&() as &dyn EventHandler, |e| e); (*access.raw()).0.detect_collisions(&hooks, events); @@ -1362,7 +1390,7 @@ pub struct RprDebugLine { pub a: RprVector, /// World-space end point. pub b: RprVector, - /// RGBA color, four floats. + /// HSLA color: hue in degrees, then saturation, lightness and alpha in [0, 1]. pub color: [f32; 4], } struct Lines(Vec); @@ -1381,9 +1409,10 @@ impl rapier::pipeline::DebugRenderBackend for Lines { }); } } -/// Color is HSLA (hue in degrees), matching Rapier DebugColor. mode uses DebugRenderMode bits. +/// Copy the debug-render lines of the world with the default style. mode combines RPR_DEBUG_* bits; +/// colors are HSLA (hue in degrees), matching Rapier DebugColor. /// @see @ref output_buffers -/// @ingroup worlds +/// @ingroup events #[rapier_export] pub unsafe extern "C" fn rpr_debug_render( world: *const RprWorld, @@ -1398,15 +1427,27 @@ pub unsafe extern "C" fn rpr_debug_render( let world: *const RprPhysicsWorld = raw; - let mode = rapier::pipeline::DebugRenderMode::from_bits(mode) - .ok_or_else(|| invalid("unknown debug render flags"))?; - let mut pipeline = rapier::pipeline::DebugRenderPipeline::new(Default::default(), mode); - let mut lines = Lines(Vec::new()); - get(world)?.0.debug_render(&mut pipeline, &mut lines); - copy_out(&lines.0, buffer, capacity, count) + debug_render_lines(world, mode, Default::default(), buffer, capacity, count) }) }) } +pub(crate) unsafe fn debug_render_lines( + world: *const RprPhysicsWorld, + mode: u32, + style: rapier::pipeline::DebugRenderStyle, + buffer: *mut RprDebugLine, + capacity: usize, + count: *mut usize, +) -> Result { + let mode = rapier::pipeline::DebugRenderMode::from_bits(mode) + .ok_or_else(|| invalid("unknown debug render flags"))?; + let mut pipeline = rapier::pipeline::DebugRenderPipeline::new(style, mode); + let mut lines = Lines(Vec::new()); + unsafe { + get(world)?.0.debug_render(&mut pipeline, &mut lines); + copy_out(&lines.0, buffer, capacity, count) + } +} /// Set the world setting documented by RprSoftBodiesSettings::resweepStrain. /// @ingroup soft_bodies diff --git a/c/src/queries_extras.rs b/c/src/queries_extras.rs new file mode 100644 index 000000000..3025445b4 --- /dev/null +++ b/c/src/queries_extras.rs @@ -0,0 +1,1370 @@ +//! Additional scene queries, contact-graph access, event callbacks, per-contact hook edits and +//! debug-render styles. +use crate::*; +use rapier::geometry::ContactManifold; +use rapier::parry::query::NonlinearRigidMotion; +use rapier::pipeline::{ContactModificationContext, DebugRenderStyle}; +use std::ffi::c_void; + +/// @ingroup events +/// Collision event flag: at least one of the colliders was a sensor when the event fired. +pub const RPR_COLLISION_EVENT_SENSOR: u32 = 1; +/// @ingroup events +/// Collision event flag: the collision stopped because at least one collider was removed. +pub const RPR_COLLISION_EVENT_REMOVED: u32 = 2; + +pub(crate) fn shape_cast_hit( + collider: ColliderHandle, + hit: rapier::parry::query::ShapeCastHit, +) -> RprShapeCastHit { + RprShapeCastHit { + collider: collider.into(), + time_of_impact: hit.time_of_impact, + witness1: hit.witness1.into(), + witness2: hit.witness2.into(), + normal1: hit.normal1.into(), + normal2: hit.normal2.into(), + status: hit.status as u32, + } +} + +/// Copy the hits of every collider intersected by the ray, in no particular order. The ray is +/// origin + direction * t for 0 <= t <= max_toi; direction need not be normalized. solid treats an +/// interior origin as a hit at t = 0. +/// @see @ref output_buffers +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_intersect_ray( + world: *const RprWorld, + query_options: *const RprQueryOptions, + origin: RprVector, + direction: RprVector, + max_toi: RprReal, + solid: RprBool, + buffer: *mut RprRayHit, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + + let origin = origin.raw()?; + let direction = direction.raw()?; + positive(direction.length())?; + nonnegative(max_toi)?; + let solid = boolean(solid)?; + query.with_raw(|q| { + let values: Vec<_> = q + .intersect_ray(Ray::new(origin, direction), max_toi, solid) + .map(|(h, _, hit)| { + let (feature_type, feature_id) = feature(hit.feature); + RprRayHit { + collider: h.into(), + time_of_impact: hit.time_of_impact, + normal: hit.normal.into(), + feature_type, + feature_id, + } + }) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) + } +} + +/// Optional shape-cast result. A miss is found = 0 with status OK. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalShapeCastHit { + /// Shape-cast impact details. + pub hit: RprShapeCastHit, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// Optional point projection. A miss is found = 0 with status OK. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalPointProjection { + /// Closest projection details. + pub projection: RprPointProjection, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +unsafe fn optional_shape_cast( + world: *const RprWorld, + query_options: *const RprQueryOptions, + cast: impl FnOnce( + QueryPipeline<'_>, + ) -> Result>, +) -> RprOptionalShapeCastHit { + ffi_world_value(world, |result: *mut RprOptionalShapeCastHit| { + let out = unsafe { std::ptr::addr_of_mut!((*result).hit) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + out_ptr(out)?; + out_ptr(found)?; + query.with_raw(|q| match cast(q)? { + Some((h, hit)) => { + output(out, shape_cast_hit(h, hit))?; + output(found, 1) + } + None => { + output(out, RprShapeCastHit::default())?; + output(found, 0) + } + }) + }) + }) +} + +/// Sweep shape from pose along velocity and return the first hit, with found = 0 on a miss +/// (RPR_OK). Time is bounded by options.max_time_of_impact. The shape is borrowed for this call. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_try_cast_shape( + world: *const RprWorld, + query_options: *const RprQueryOptions, + pose: RprPose, + velocity: RprVector, + shape: *const RprSharedShape, + options: RprShapeCastOptions, +) -> RprOptionalShapeCastHit { + unsafe { + optional_shape_cast(world, query_options, |q| { + let p = pose.raw()?; + let v = velocity.raw()?; + let o = options.raw()?; + Ok(q.cast_shape(&p, v, &*get(shape)?.0, o)) + }) + } +} + +/// Return the closest surface projection within max_distance, with found = 0 if there is none +/// (RPR_OK). With solid = 1, an interior point projects to itself. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_try_project_point( + world: *const RprWorld, + query_options: *const RprQueryOptions, + point: RprVector, + max_distance: RprReal, + solid: RprBool, +) -> RprOptionalPointProjection { + ffi_world_value(world, |result: *mut RprOptionalPointProjection| { + let out = unsafe { std::ptr::addr_of_mut!((*result).projection) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + ffi(|| unsafe { + if !query_options.is_null() { + get(query_options)?.check_world(world)?; + } + let access = get(world)?.read()?; + let raw = access.raw(); + let query = QueryAccess::from_world(world as *mut RprWorld, raw, query_options)?; + out_ptr(out)?; + out_ptr(found)?; + let p = point.raw()?; + nonnegative(max_distance)?; + let solid = boolean(solid)?; + query.with_raw(|q| { + if let Some((h, p)) = q.project_point(p, max_distance, solid) { + output( + out, + RprPointProjection { + collider: h.into(), + point: p.point.into(), + is_inside: p.is_inside as u32, + }, + )?; + output(found, 1) + } else { + output(out, RprPointProjection::default())?; + output(found, 0) + } + }) + }) + }) +} + +/// Rigid motion with constant linear and angular velocities. At time t, the shape at start is +/// rotated by angvel * t around its local_center point, then translated by linvel * t. +/// @ingroup queries +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprNonlinearRigidMotion { + /// World-space pose at time zero. + pub start: RprPose, + /// Rotation center, in the local coordinates of the moving shape. + pub local_center: RprVector, + /// World-space linear velocity. + pub linvel: RprVector, + /// World-space angular velocity, in radians per second. + pub angvel: RprAngVector, +} +impl RprNonlinearRigidMotion { + fn raw(&self) -> Result { + Ok(NonlinearRigidMotion { + start: self.start.raw()?, + local_center: self.local_center.raw()?, + linvel: self.linvel.raw()?, + angvel: angular(self.angvel)?, + }) + } +} + +/// Return the world-space pose of the motion at the given time. +/// @ingroup queries +#[rapier_export(nonlinear_rigid_motion)] +pub unsafe extern "C" fn rpr_nonlinear_rigid_motion_position_at_time( + motion: *const RprNonlinearRigidMotion, + time: RprReal, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + let motion = get(motion)?.raw()?; + output(out, motion.position_at_time(finite(time)?).into()) + }) + }) +} + +/// Sweep shape along a rotating motion and return the first hit between start_time and end_time, +/// with found = 0 on a miss (RPR_OK). With stop_at_penetration = 1, a shape already intersecting a +/// collider at start_time hits it at start_time; with 0, that penetration is ignored while the +/// motion separates the shapes. witness1/normal1 are world-space; witness2/normal2 are local to the +/// shape, posed by rpr_nonlinear_rigid_motion_position_at_time at the time of impact. +/// NULL query options use the default filter. Query state reflects the latest Step or +/// DetectCollisions call. +/// @ingroup queries +#[rapier_export] +pub unsafe extern "C" fn rpr_try_cast_shape_nonlinear( + world: *const RprWorld, + query_options: *const RprQueryOptions, + motion: *const RprNonlinearRigidMotion, + shape: *const RprSharedShape, + start_time: RprReal, + end_time: RprReal, + stop_at_penetration: RprBool, +) -> RprOptionalShapeCastHit { + unsafe { + optional_shape_cast(world, query_options, |q| { + let motion = get(motion)?.raw()?; + finite(start_time)?; + finite(end_time)?; + ensure(start_time <= end_time, "start_time exceeds end_time")?; + let stop = boolean(stop_at_penetration)?; + Ok(q.cast_shape_nonlinear(&motion, &*get(shape)?.0, start_time, end_time, stop)) + }) + } +} + +/// Optional contact pair; check found before reading the pair. +/// @ingroup events +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalContactPair { + /// Contact pair summary. + pub pair: RprContactPair, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +/// Optional intersection pair; check found before reading the pair. +/// @ingroup events +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprOptionalIntersectionPair { + /// Intersection pair state. + pub pair: RprIntersectionPair, + /// Whether a result exists; other result fields are meaningful only when this is 1. + pub found: RprBool, +} + +unsafe fn narrow_phase<'a>(world: *const RprPhysicsWorld) -> Result<&'a NarrowPhase> { + unsafe { Ok(&get(world)?.0.narrow_phase) } +} + +/// Return the narrow-phase contact pair for two colliders, with found = 0 if the broad phase +/// found no potential contact between them (RPR_OK). The pair's collider1 and collider2 follow the +/// narrow-phase order, which may differ from the argument order. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_try_contact_pair( + collider1: RprColliderHandle, + collider2: RprColliderHandle, +) -> RprOptionalContactPair { + let world = collider1.world; + ffi_world_value(world, |result: *mut RprOptionalContactPair| { + let out = unsafe { std::ptr::addr_of_mut!((*result).pair) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + ffi(|| unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + out_ptr(out)?; + out_ptr(found)?; + match narrow_phase(access.raw())?.contact_pair(collider1.raw(), collider2.raw()) { + Some(p) => { + output(out, p.into())?; + output(found, 1) + } + None => { + output(out, RprContactPair::default())?; + output(found, 0) + } + } + }) + }) +} + +/// Return the intersection state of two colliders involving a sensor, or report RPR_NOT_FOUND if +/// the broad phase found no potential intersection. The result keeps the argument order. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_intersection_pair( + collider1: RprColliderHandle, + collider2: RprColliderHandle, +) -> RprIntersectionPair { + let world = collider1.world; + ffi_world_value(world, |out: *mut RprIntersectionPair| { + ffi(|| unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + let intersecting = narrow_phase(access.raw())? + .intersection_pair(collider1.raw(), collider2.raw()) + .ok_or((RPR_NOT_FOUND, "no intersection pair".into()))?; + output( + out, + RprIntersectionPair { + collider1, + collider2, + intersecting: intersecting as u32, + }, + ) + }) + }) +} + +/// Return the intersection state of two colliders involving a sensor, with found = 0 if the +/// broad phase found no potential intersection (RPR_OK). The pair keeps the argument order. +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_try_intersection_pair( + collider1: RprColliderHandle, + collider2: RprColliderHandle, +) -> RprOptionalIntersectionPair { + let world = collider1.world; + ffi_world_value(world, |result: *mut RprOptionalIntersectionPair| { + let out = unsafe { std::ptr::addr_of_mut!((*result).pair) }; + let found = unsafe { std::ptr::addr_of_mut!((*result).found) }; + ffi(|| unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + out_ptr(out)?; + out_ptr(found)?; + match narrow_phase(access.raw())?.intersection_pair(collider1.raw(), collider2.raw()) { + Some(intersecting) => { + output( + out, + RprIntersectionPair { + collider1, + collider2, + intersecting: intersecting as u32, + }, + )?; + output(found, 1) + } + None => { + output(out, RprIntersectionPair::default())?; + output(found, 0) + } + } + }) + }) +} + +/// Copy the narrow-phase contact pairs involving the collider, including pairs without active +/// solver contacts. The collider may be either collider1 or collider2 of each pair. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_contact_pairs( + handle: RprColliderHandle, + buffer: *mut RprContactPair, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + get(raw)? + .0 + .colliders + .get(handle.raw()) + .ok_or_else(missing)?; + let values: Vec = narrow_phase(raw)? + .contact_pairs_with(handle.raw()) + .map(Into::into) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Copy the intersection pairs involving the collider, in the narrow-phase order. The collider may +/// be either collider1 or collider2 of each pair. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_intersection_pairs( + handle: RprColliderHandle, + buffer: *mut RprIntersectionPair, + capacity: usize, +) -> usize { + let world = handle.world; + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + get(raw)? + .0 + .colliders + .get(handle.raw()) + .ok_or_else(missing)?; + let values: Vec<_> = narrow_phase(raw)? + .intersection_pairs_with(handle.raw()) + .map(|(a, b, hit)| RprIntersectionPair { + collider1: a.into(), + collider2: b.into(), + intersecting: hit as u32, + }) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Geometric contact manifold of a contact pair: contacts sharing one normal. Local data follow the +/// pair's own collider1/collider2 order (see rpr_contact_pair). +/// @ingroup events +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprContactManifold { + /// Contact normal in collider 1 local coordinates, pointing outward from it. + pub local_n1: RprVector, + /// Contact normal in collider 2 local coordinates, pointing outward from it. + pub local_n2: RprVector, + /// World-space contact normal, pointing from collider 1 toward collider 2. + pub normal: RprVector, + /// Index of the subshape of collider 1 (for composite shapes), zero otherwise. + pub subshape1: u32, + /// Index of the subshape of collider 2 (for composite shapes), zero otherwise. + pub subshape2: u32, + /// Number of geometric contact points; see rpr_contact_points. + pub num_points: usize, + /// Number of solver contacts; see rpr_solver_contacts. + pub num_solver_contacts: usize, + /// Application data, persistent across steps and editable by contact-modification hooks. + pub user_data: u32, +} +impl From<&ContactManifold> for RprContactManifold { + fn from(m: &ContactManifold) -> Self { + Self { + local_n1: m.local_n1.into(), + local_n2: m.local_n2.into(), + normal: m.data.normal.into(), + subshape1: m.subshape1, + subshape2: m.subshape2, + num_points: m.points.len(), + num_solver_contacts: m.data.solver_contacts.len(), + user_data: m.data.user_data, + } + } +} + +/// Contact seen by the constraint solver. Points are world-space, on each body's surface. +/// @ingroup events +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSolverContact { + /// World-space contact point on collider 1's body. + pub point1: RprVector, + /// World-space contact point on collider 2's body. + pub point2: RprVector, + /// Signed separation along the normal, contact skins deducted; negative means penetration. + pub distance: RprReal, + /// Desired world-space tangent relative velocity, e.g. for conveyor belts; zero by default. + pub tangent_velocity: RprVector, +} + +unsafe fn with_contact_pair( + collider1: RprColliderHandle, + collider2: RprColliderHandle, + f: impl FnOnce(&ContactPair, &RigidBodySet) -> Result, +) -> Result { + let world = collider1.world; + unsafe { + collider1.check_world(world)?; + collider2.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + let pair = narrow_phase(raw)? + .contact_pair(collider1.raw(), collider2.raw()) + .ok_or((RPR_NOT_FOUND, "no contact pair".into()))?; + f(pair, &get(raw)?.0.bodies) + } +} + +/// Copy the geometric contact manifolds of a contact pair, or report RPR_NOT_FOUND without a pair. +/// Their order matches the manifold_index of rpr_contact_points. Soft pairs have no rigid +/// manifolds. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_contact_manifolds( + collider1: RprColliderHandle, + collider2: RprColliderHandle, + buffer: *mut RprContactManifold, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + with_contact_pair(collider1, collider2, |pair, _| { + let values: Vec<_> = pair.manifolds().iter().map(Into::into).collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) +} + +/// Copy the solver contacts of one manifold of a contact pair, or report RPR_NOT_FOUND without a +/// pair. Points are resolved through the bodies' current poses. With contact clustering (3D +/// composite shapes), the solver may use merged manifolds instead; use contact pair totals then. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_solver_contacts( + collider1: RprColliderHandle, + collider2: RprColliderHandle, + manifold_index: usize, + buffer: *mut RprSolverContact, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + with_contact_pair(collider1, collider2, |pair, bodies| { + let m = pair + .manifolds() + .get(manifold_index) + .ok_or_else(|| invalid("manifold index out of range"))?; + let values: Vec<_> = m + .data + .solver_contacts + .iter() + .map(|c| { + let (point1, point2) = m.data.solver_contact_world_points(c, bodies); + RprSolverContact { + point1: point1.into(), + point2: point2.into(), + distance: c.dist, + tangent_velocity: c.tangent_velocity.into(), + } + }) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + }) +} + +// The context wraps a native context borrowed for the duration of the hook call. +unsafe fn context_ref<'a>( + context: *const RprContactModificationContext, +) -> Result<&'a ContactModificationContext<'a>> { + unsafe { Ok(&*get(context)?.raw.cast::>()) } +} +unsafe fn context_mut<'a>( + context: *mut RprContactModificationContext, +) -> Result<&'a mut ContactModificationContext<'a>> { + unsafe { + Ok(&mut *get_mut(context)? + .raw + .cast::>()) + } +} + +/// Return whether the context holds the contact candidates of two soft surfaces rather than a +/// manifold. Solver-contact accessors see no contacts in a soft context. +/// @ingroup callbacks +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_is_soft( + context: *const RprContactModificationContext, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { output(out, context_ref(context)?.rigid().is_none() as u32) }) + }) +} + +/// Return the number of solver contacts of the manifold; zero for a soft context. +/// @ingroup callbacks +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_solver_contact_count( + context: *const RprContactModificationContext, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let count = context_ref(context)? + .rigid() + .map_or(0, |m| m.solver_contacts.len()); + output(out, count) + }) + }) +} + +/// Return a solver contact of the manifold. Inside the hook, points are world-space. +/// @ingroup callbacks +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_solver_contact( + context: *const RprContactModificationContext, + index: usize, +) -> RprSolverContact { + ffi_value(|out: *mut RprSolverContact| { + ffi(|| unsafe { + let c = context_ref(context)? + .rigid() + .and_then(|m| m.solver_contacts.get(index)) + .ok_or_else(|| invalid("solver contact index out of range"))?; + output( + out, + RprSolverContact { + point1: c.anchor1.into(), + point2: c.anchor2.into(), + distance: c.dist, + tangent_velocity: c.tangent_velocity.into(), + }, + ) + }) + }) +} + +/// Replace the points, distance and tangent velocity of a solver contact of the manifold. Points +/// are world-space; a distance differing from their gap along the normal shifts the contact. +/// @ingroup callbacks +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_set_solver_contact( + context: *mut RprContactModificationContext, + index: usize, + contact: *const RprSolverContact, +) -> RprStatus { + ffi(|| unsafe { + let contact = *get(contact)?; + let point1 = contact.point1.raw()?; + let point2 = contact.point2.raw()?; + let distance = finite(contact.distance)?; + let tangent_velocity = contact.tangent_velocity.raw()?; + let c = context_mut(context)? + .rigid_mut() + .and_then(|m| m.solver_contacts.get_mut(index)) + .ok_or_else(|| invalid("solver contact index out of range"))?; + c.anchor1 = point1; + c.anchor2 = point2; + c.dist = distance; + c.tangent_velocity = tangent_velocity; + Ok(()) + }) +} + +/// Remove a solver contact of the manifold. The last solver contact takes its index. +/// @ingroup callbacks +#[rapier_export(contact_modification_context)] +pub unsafe extern "C" fn rpr_contact_modification_context_remove_solver_contact( + context: *mut RprContactModificationContext, + index: usize, +) -> RprStatus { + ffi(|| unsafe { + let contacts = context_mut(context)? + .rigid_mut() + .map(|m| &mut *m.solver_contacts) + .filter(|contacts| index < contacts.len()) + .ok_or_else(|| invalid("solver contact index out of range"))?; + contacts.swap_remove(index); + Ok(()) + }) +} + +/// Called during the step for each collision event, after it was added to the collector. contacts +/// holds the geometric contacts of the pair at that time (none for sensors), in the event's +/// collider order; it is borrowed for this call only. +/// @ingroup events +pub type RprCollisionEventCallback = Option< + unsafe extern "C" fn( + user_data: *mut c_void, + read: *const RprReadContext, + event: *const RprCollisionEvent, + contacts: *const RprContactPoint, + contact_count: usize, + ), +>; +/// Called during the step for each contact-force event, after it was added to the collector. +/// @ingroup events +pub type RprContactForceEventCallback = Option< + unsafe extern "C" fn( + user_data: *mut c_void, + read: *const RprReadContext, + event: *const RprContactForceEvent, + ), +>; + +/// Callbacks invoked while stepping, in addition to collecting the events. They follow the +/// RprPhysicsHooks rules: never unwind or retain arguments, read through the ReadContext, never +/// mutate the world, and be safe for concurrent invocation in parallel builds. NULL callbacks are +/// skipped. +/// @ingroup events +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct RprEventCallbacks { + /// Application data; Rapier does not own pointers encoded in it. + pub user_data: *mut c_void, + /// Optional collision start/stop callback. + pub collision_event: RprCollisionEventCallback, + /// Optional contact-force callback. + pub contact_force_event: RprContactForceEventCallback, +} +// SAFETY: The public callback contract requires thread-safe callbacks and user_data in parallel builds. +unsafe impl Send for RprEventCallbacks {} +unsafe impl Sync for RprEventCallbacks {} + +/// Replace the callbacks invoked while stepping with this collector; NULL removes them. They take +/// effect from the next Step or DetectCollisions call. +/// @ingroup events +#[rapier_export(event_collector)] +pub unsafe extern "C" fn rpr_event_collector_set_callbacks( + events: *mut RprEventCollector, + callbacks: *const RprEventCallbacks, +) -> RprStatus { + ffi(|| unsafe { + let callbacks = if callbacks.is_null() { + RprEventCallbacks::default() + } else { + *get(callbacks)? + }; + // Shared access: a callback may replace the callbacks of the collector being filled. + *get(events)?.callbacks.lock().unwrap() = callbacks; + Ok(()) + }) +} + +/// Debug-render colors and sizes. Colors are HSLA: hue in degrees, then saturation, lightness and +/// alpha in [0, 1]; multipliers scale each component. Initialize with +/// rpr_default_debug_render_style. +/// @ingroup events +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprDebugRenderStyle { + /// Positive number of subdivisions approximating curved shapes. + pub subdivisions: u32, + /// Positive number of subdivisions approximating the borders of round shapes. + pub border_subdivisions: u32, + /// Color of colliders attached to dynamic bodies. + pub collider_dynamic_color: [f32; 4], + /// Color of colliders attached to fixed bodies. + pub collider_fixed_color: [f32; 4], + /// Color of colliders attached to kinematic bodies. + pub collider_kinematic_color: [f32; 4], + /// Color of colliders without a parent body. + pub collider_parentless_color: [f32; 4], + /// Color of the lines from a body's center of mass to its impulse-joint anchors. + pub impulse_joint_anchor_color: [f32; 4], + /// Color of the line between the two anchors of an impulse joint. + pub impulse_joint_separation_color: [f32; 4], + /// Color of the lines from a body's center of mass to its multibody-joint anchors. + pub multibody_joint_anchor_color: [f32; 4], + /// Color of the line between the two anchors of a multibody joint. + pub multibody_joint_separation_color: [f32; 4], + /// Color multiplier for entities of sleeping bodies. + pub sleep_color_multiplier: [f32; 4], + /// Color multiplier for entities of awake bodies eligible for sleep. + pub sleep_eligible_color_multiplier: [f32; 4], + /// Color multiplier for entities of disabled bodies. + pub disabled_color_multiplier: [f32; 4], + /// Nonnegative length of the rendered body axes. + pub rigid_body_axes_length: RprReal, + /// Color of the segments joining the two points of a contact. + pub contact_depth_color: [f32; 4], + /// Color of the contact normals. + pub contact_normal_color: [f32; 4], + /// Nonnegative length of the contact normals. + pub contact_normal_length: RprReal, + /// Color of soft-body elements. + pub soft_body_element_color: [f32; 4], + /// Color of unloaded soft-body elements when coloring them by load. + pub soft_body_slack_color: [f32; 4], + /// Color of soft-body elements at their tear threshold when coloring them by load. + pub soft_body_loaded_color: [f32; 4], + /// Color of the soft-body cluster frames. + pub soft_body_frame_color: [f32; 4], + /// Color of the collider bounding boxes. + pub collider_aabb_color: [f32; 4], + /// Color of the vertex pseudo-normals of triangle meshes and polylines. + pub vertex_pseudo_normal_color: [f32; 4], + /// Color of the edge pseudo-normals of triangle meshes (3D only). + pub edge_pseudo_normal_color: [f32; 4], + /// Nonnegative length of the pseudo-normals. + pub pseudo_normal_length: RprReal, + /// Color of the normals of soft-body volume contacts. + pub volume_contact_normal_color: [f32; 4], + /// Color of the volume gradients drawn at the particles of a volume constraint. + pub volume_gradient_color: [f32; 4], +} +macro_rules! debug_style_fields { + ($m:ident) => { + $m!( + [subdivisions, border_subdivisions], + [ + collider_dynamic_color, + collider_fixed_color, + collider_kinematic_color, + collider_parentless_color, + impulse_joint_anchor_color, + impulse_joint_separation_color, + multibody_joint_anchor_color, + multibody_joint_separation_color, + sleep_color_multiplier, + sleep_eligible_color_multiplier, + disabled_color_multiplier, + contact_depth_color, + contact_normal_color, + soft_body_element_color, + soft_body_slack_color, + soft_body_loaded_color, + soft_body_frame_color, + collider_aabb_color, + vertex_pseudo_normal_color, + edge_pseudo_normal_color, + volume_contact_normal_color, + volume_gradient_color + ], + [ + rigid_body_axes_length, + contact_normal_length, + pseudo_normal_length + ] + ) + }; +} +impl From for RprDebugRenderStyle { + fn from(s: DebugRenderStyle) -> Self { + macro_rules! convert { + ([$($count:ident),*], [$($color:ident),*], [$($length:ident),*]) => { + Self { $($count: s.$count,)* $($color: s.$color,)* $($length: s.$length,)* } + }; + } + debug_style_fields!(convert) + } +} +impl RprDebugRenderStyle { + pub(crate) fn raw(&self) -> Result { + let s = self; + macro_rules! convert { + ([$($count:ident),*], [$($color:ident),*], [$($length:ident),*]) => {{ + $(ensure(s.$count > 0, concat!(stringify!($count), " must be positive"))?;)* + $(ensure( + s.$color.iter().all(|c| c.is_finite()), + concat!(stringify!($color), " must be finite"), + )?;)* + $(nonnegative(s.$length)?;)* + DebugRenderStyle { $($count: s.$count,)* $($color: s.$color,)* $($length: s.$length,)* } + }}; + } + Ok(debug_style_fields!(convert)) + } +} + +/// Return native default debug-render style. This POD value owns no resources. +/// @ingroup events +#[rapier_export] +pub extern "C" fn rpr_default_debug_render_style() -> RprDebugRenderStyle { + DebugRenderStyle::default().into() +} + +/// Copy the debug-render lines of the world drawn with the given style. mode combines RPR_DEBUG_* +/// bits. NULL style uses the default style. +/// @see @ref output_buffers +/// @ingroup events +#[rapier_export] +pub unsafe extern "C" fn rpr_debug_render_with_style( + world: *const RprWorld, + mode: u32, + style: *const RprDebugRenderStyle, + buffer: *mut RprDebugLine, + capacity: usize, +) -> usize { + ffi_value(|count: *mut usize| { + ffi(|| unsafe { + let style = if style.is_null() { + DebugRenderStyle::default() + } else { + get(style)?.raw()? + }; + let access = get(world)?.read()?; + debug_render_lines(access.raw(), mode, style, buffer, capacity, count) + }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ptr; + use std::sync::Mutex; + + // A fixed cube whose top face is at y = 0 and a dynamic ball resting on it. + unsafe fn ball_on_ground( + ball: RprColliderDesc, + ) -> (*mut RprWorld, RprColliderHandle, RprColliderHandle) { + unsafe { + let world = rpr_new_world(); + let mut ground = rpr_cuboid_collider_desc(Vector::splat(5.0).into()); + ground.position.translation = (-Vector::Y * 5.0).into(); + let ground = rpr_insert_collider_without_parent(world, &ground); + assert_eq!(rpr_last_status(), RPR_OK); + let mut body = rpr_dynamic_rigid_body_desc(); + body.position.translation = (Vector::Y * 0.49).into(); + let body = rpr_insert_rigid_body(world, &body); + let ball = rpr_insert_collider(body, &ball); + assert_eq!(rpr_last_status(), RPR_OK); + (world, ground, ball) + } + } + + #[test] + fn ray_shape_and_point_queries_report_all_hits_and_misses() { + unsafe { + let world = rpr_new_world(); + for x in [2.0, 4.0] { + let mut desc = rpr_ball_collider_desc(0.5); + desc.position.translation = (Vector::X * x).into(); + rpr_insert_collider_without_parent(world, &desc); + } + assert_eq!( + rpr_detect_collisions(world, ptr::null(), ptr::null()), + RPR_OK + ); + let (origin, dir) = (Vector::ZERO.into(), Vector::X.into()); + let count = + rpr_intersect_ray(world, ptr::null(), origin, dir, 10.0, 1, ptr::null_mut(), 0); + assert_eq!((rpr_last_status(), count), (RPR_OK, 2)); + let mut hits = [RprRayHit::default(); 2]; + rpr_intersect_ray( + world, + ptr::null(), + origin, + dir, + 10.0, + 1, + hits.as_mut_ptr(), + 2, + ); + assert_eq!(rpr_last_status(), RPR_OK); + let mut tois: Vec<_> = hits.iter().map(|h| h.time_of_impact).collect(); + tois.sort_by(|a, b| a.partial_cmp(b).unwrap()); + assert!((tois[0] - 1.5).abs() < 1.0e-4 && (tois[1] - 3.5).abs() < 1.0e-4); + assert!(hits.iter().all(|h| std::ptr::eq(h.collider.world, world))); + let short = + rpr_intersect_ray(world, ptr::null(), origin, dir, 2.0, 1, ptr::null_mut(), 0); + assert_eq!(short, 1); + + let shape = rpr_ball_shared_shape(0.25); + let mut options = rpr_default_shape_cast_options(); + options.max_time_of_impact = 10.0; + let pose = RprPose::from(Pose::IDENTITY); + let hit = rpr_try_cast_shape(world, ptr::null(), pose, dir, shape, options); + assert_eq!((rpr_last_status(), hit.found), (RPR_OK, 1)); + assert!((hit.hit.time_of_impact - 1.25).abs() < 1.0e-3); + assert!(std::ptr::eq(hit.hit.collider.world, world)); + let miss = + rpr_try_cast_shape(world, ptr::null(), pose, Vector::Y.into(), shape, options); + assert_eq!((rpr_last_status(), miss.found), (RPR_OK, 0)); + + let far = (Vector::Y * 5.0).into(); + let miss = rpr_try_project_point(world, ptr::null(), far, 1.0, 1); + assert_eq!((rpr_last_status(), miss.found), (RPR_OK, 0)); + let found = rpr_try_project_point(world, ptr::null(), far, 10.0, 1); + assert_eq!((rpr_last_status(), found.found), (RPR_OK, 1)); + assert!(std::ptr::eq(found.projection.collider.world, world)); + + let mut motion = RprNonlinearRigidMotion { + start: pose, + linvel: Vector::X.into(), + ..Default::default() + }; + let at = rpr_nonlinear_rigid_motion_position_at_time(&motion, 2.0); + assert_eq!(rpr_last_status(), RPR_OK); + assert!((at.translation.x - 2.0).abs() < 1.0e-6); + let hit = + rpr_try_cast_shape_nonlinear(world, ptr::null(), &motion, shape, 0.0, 10.0, 1); + assert_eq!((rpr_last_status(), hit.found), (RPR_OK, 1)); + assert!((hit.hit.time_of_impact - 1.25).abs() < 1.0e-2); + motion.linvel = Vector::Y.into(); + let miss = + rpr_try_cast_shape_nonlinear(world, ptr::null(), &motion, shape, 0.0, 10.0, 1); + assert_eq!((rpr_last_status(), miss.found), (RPR_OK, 0)); + rpr_try_cast_shape_nonlinear(world, ptr::null(), &motion, shape, 1.0, 0.0, 1); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_shared_shape(shape), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn contact_graph_exposes_manifolds_solver_contacts_and_pairs() { + unsafe { + let (world, ground, ball) = ball_on_ground(rpr_ball_collider_desc(0.5)); + for _ in 0..5 { + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + } + let pair = rpr_try_contact_pair(ball, ground); + assert_eq!((rpr_last_status(), pair.found), (RPR_OK, 1)); + assert_eq!(pair.pair.has_any_active_contact, 1); + assert!(std::ptr::eq(pair.pair.collider1.world, world)); + + let count = rpr_contact_manifolds(ball, ground, ptr::null_mut(), 0); + assert_eq!((rpr_last_status(), count), (RPR_OK, 1)); + let mut manifold = RprContactManifold::default(); + rpr_contact_manifolds(ball, ground, &mut manifold, 1); + assert_eq!(rpr_last_status(), RPR_OK); + let points = rpr_contact_points(ball, ground, ptr::null_mut(), 0); + assert_eq!(points, manifold.num_points); + let mut point = RprContactPoint::default(); + rpr_contact_points(ball, ground, &mut point, 1); + assert!(point.impulse > 0.0); + assert!(point.tangent_impulse.iter().all(|i| i.is_finite())); + + let count = rpr_solver_contacts(ball, ground, 0, ptr::null_mut(), 0); + assert_eq!( + (rpr_last_status(), count), + (RPR_OK, manifold.num_solver_contacts) + ); + assert!(count > 0); + let mut contacts = vec![RprSolverContact::default(); count]; + rpr_solver_contacts(ball, ground, 0, contacts.as_mut_ptr(), count); + assert_eq!(rpr_last_status(), RPR_OK); + // Both world-space points lie near the top face of the ground, under the ball. + for c in &contacts { + assert!(c.point1.y.abs() < 0.05 && c.point2.y.abs() < 0.05); + assert!(c.point1.x.abs() < 0.05 && c.point2.x.abs() < 0.05); + } + rpr_solver_contacts(ball, ground, 1, ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + + let mut pairs = [RprContactPair::default(); 1]; + let count = rpr_collider_contact_pairs(ball, pairs.as_mut_ptr(), 1); + assert_eq!((rpr_last_status(), count), (RPR_OK, 1)); + assert!(std::ptr::eq(pairs[0].collider1.world, world)); + let count = rpr_collider_intersection_pairs(ball, ptr::null_mut(), 0); + assert_eq!((rpr_last_status(), count), (RPR_OK, 0)); + let missing = rpr_try_intersection_pair(ball, ground); + assert_eq!((rpr_last_status(), missing.found), (RPR_OK, 0)); + rpr_intersection_pair(ball, ground); + assert_eq!(rpr_last_status(), RPR_NOT_FOUND); + + rpr_remove_collider(ground, 1); + rpr_collider_contact_pairs(ground, ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_HANDLE); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + let pair = rpr_try_contact_pair(ball, ground); + assert_eq!((rpr_last_status(), pair.found), (RPR_OK, 0)); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn intersection_graph_reports_sensor_pairs() { + unsafe { + let mut sensor = rpr_ball_collider_desc(0.5); + sensor.isSensor = 1; + let (world, ground, ball) = ball_on_ground(sensor); + assert_eq!( + rpr_detect_collisions(world, ptr::null(), ptr::null()), + RPR_OK + ); + let pair = rpr_try_intersection_pair(ball, ground); + assert_eq!( + (rpr_last_status(), pair.found, pair.pair.intersecting), + (RPR_OK, 1, 1) + ); + assert_eq!(pair.pair.collider1, ball); + let pair = rpr_intersection_pair(ground, ball); + assert_eq!((rpr_last_status(), pair.intersecting), (RPR_OK, 1)); + let mut pairs = [RprIntersectionPair::default(); 1]; + let count = rpr_collider_intersection_pairs(ground, pairs.as_mut_ptr(), 1); + assert_eq!( + (rpr_last_status(), count, pairs[0].intersecting), + (RPR_OK, 1, 1) + ); + let missing = rpr_try_contact_pair(ball, ground); + assert_eq!((rpr_last_status(), missing.found), (RPR_OK, 0)); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[derive(Default)] + struct Recorded { + collisions: Vec<(RprCollisionEvent, usize)>, + forces: Vec, + } + unsafe extern "C" fn on_collision( + data: *mut c_void, + read: *const RprReadContext, + event: *const RprCollisionEvent, + contacts: *const RprContactPoint, + count: usize, + ) { + unsafe { + let event = *event; + rpr_read_collider_translation(read, event.collider1); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(count == 0 || !contacts.is_null()); + let recorded = &*data.cast::>(); + recorded.lock().unwrap().collisions.push((event, count)); + } + } + unsafe extern "C" fn on_force( + data: *mut c_void, + _read: *const RprReadContext, + event: *const RprContactForceEvent, + ) { + unsafe { + let recorded = &*data.cast::>(); + recorded.lock().unwrap().forces.push((*event).started); + } + } + + #[test] + fn event_callbacks_run_during_the_step_besides_collection() { + unsafe { + let mut ball = rpr_ball_collider_desc(0.5); + ball.activeEvents = RPR_COLLISION_EVENTS | RPR_CONTACT_FORCE_EVENTS; + let (world, _ground, _ball) = ball_on_ground(ball); + let recorded = Mutex::new(Recorded::default()); + let events = rpr_new_event_collector(); + let callbacks = RprEventCallbacks { + user_data: (&recorded as *const Mutex).cast_mut().cast(), + collision_event: Some(on_collision), + contact_force_event: Some(on_force), + }; + assert_eq!( + rpr_event_collector_set_callbacks(events, &callbacks), + RPR_OK + ); + for _ in 0..3 { + assert_eq!(rpr_step(world, ptr::null(), events), RPR_OK); + } + { + let recorded = recorded.lock().unwrap(); + assert_eq!(recorded.collisions.len(), 1); + let (event, count) = recorded.collisions[0]; + assert_eq!((event.started, event.flags), (1, 0)); + assert!(count > 0); + assert!(std::ptr::eq(event.collider1.world, world)); + assert_eq!(recorded.forces.first(), Some(&1)); + assert!(recorded.forces.len() >= 2 && recorded.forces[1..].iter().all(|s| *s == 0)); + } + // Callbacks do not replace the collection, which keeps accumulating across steps. + let count = rpr_event_collector_collision_events(events, ptr::null_mut(), 0); + assert_eq!(count, 1); + let count = rpr_event_collector_contact_force_events(events, ptr::null_mut(), 0); + assert_eq!(count, recorded.lock().unwrap().forces.len()); + assert_eq!( + rpr_event_collector_set_callbacks(events, ptr::null()), + RPR_OK + ); + assert_eq!(rpr_step(world, ptr::null(), events), RPR_OK); + let forces = rpr_event_collector_contact_force_events(events, ptr::null_mut(), 0); + assert!(forces > recorded.lock().unwrap().forces.len()); + assert_eq!(rpr_free_event_collector(events), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn sensor_and_removal_flags_match_native_bits() { + assert_eq!( + RPR_COLLISION_EVENT_SENSOR, + CollisionEventFlags::SENSOR.bits() + ); + assert_eq!( + RPR_COLLISION_EVENT_REMOVED, + CollisionEventFlags::REMOVED.bits() + ); + unsafe { + let mut sensor = rpr_ball_collider_desc(0.5); + sensor.isSensor = 1; + sensor.activeEvents = RPR_COLLISION_EVENTS; + let (world, _ground, ball) = ball_on_ground(sensor); + let events = rpr_new_event_collector(); + assert_eq!(rpr_detect_collisions(world, ptr::null(), events), RPR_OK); + rpr_remove_collider(ball, 1); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_detect_collisions(world, ptr::null(), events), RPR_OK); + let mut collected = [RprCollisionEvent::default(); 2]; + let count = rpr_event_collector_collision_events(events, collected.as_mut_ptr(), 2); + assert_eq!((rpr_last_status(), count), (RPR_OK, 2)); + assert_eq!(collected[0].flags, RPR_COLLISION_EVENT_SENSOR); + assert_eq!( + collected[1].flags, + RPR_COLLISION_EVENT_SENSOR | RPR_COLLISION_EVENT_REMOVED + ); + assert_eq!(rpr_free_event_collector(events), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + struct HookState { + counts: Mutex>, + } + unsafe extern "C" fn edit_each_contact( + data: *mut c_void, + _read: *const RprReadContext, + _a: RprColliderHandle, + _b: RprColliderHandle, + context: *mut RprContactModificationContext, + ) { + unsafe { + assert_eq!(rpr_contact_modification_context_is_soft(context), 0); + let before = rpr_contact_modification_context_solver_contact_count(context); + rpr_contact_modification_context_solver_contact(context, before); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!( + rpr_contact_modification_context_remove_solver_contact(context, before), + RPR_INVALID_ARGUMENT + ); + if before > 1 { + assert_eq!( + rpr_contact_modification_context_remove_solver_contact(context, 0), + RPR_OK + ); + } + let after = rpr_contact_modification_context_solver_contact_count(context); + for i in 0..after { + let mut contact = rpr_contact_modification_context_solver_contact(context, i); + assert_eq!(rpr_last_status(), RPR_OK); + contact.tangent_velocity.x = 10.0; + assert_eq!( + rpr_contact_modification_context_set_solver_contact(context, i, &contact), + RPR_OK + ); + contact.distance = RprReal::NAN; + assert_eq!( + rpr_contact_modification_context_set_solver_contact(context, i, &contact), + RPR_INVALID_ARGUMENT + ); + } + let state = &*data.cast::(); + state.counts.lock().unwrap().push((before, after)); + } + } + + #[test] + fn hooks_edit_solver_contacts_one_by_one() { + unsafe { + let mut cube = rpr_cuboid_collider_desc(Vector::splat(0.5).into()); + cube.activeHooks = RPR_MODIFY_SOLVER_CONTACTS; + let (world, ground, cube) = ball_on_ground(cube); + let state = HookState { + counts: Mutex::new(Vec::new()), + }; + let hooks = RprPhysicsHooks { + user_data: (&state as *const HookState).cast_mut().cast(), + modify_solver_contacts_context: Some(edit_each_contact), + ..Default::default() + }; + for _ in 0..3 { + assert_eq!(rpr_step(world, &hooks, ptr::null()), RPR_OK); + } + let counts = state.counts.lock().unwrap().clone(); + assert!(!counts.is_empty()); + assert!( + counts + .iter() + .all(|(b, a)| *a == if *b > 1 { b - 1 } else { *b }) + ); + assert!(counts.iter().any(|(b, _)| *b > 1)); + let mut contacts = [RprSolverContact::default(); 8]; + let count = rpr_solver_contacts(cube, ground, 0, contacts.as_mut_ptr(), 8); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(count, counts.last().unwrap().1); + assert!( + contacts[..count] + .iter() + .all(|c| c.tangent_velocity.x == 10.0) + ); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn debug_render_style_round_trips_and_applies_colors() { + unsafe { + let style = rpr_default_debug_render_style(); + assert_eq!(style.raw().unwrap(), DebugRenderStyle::default()); + let (world, _ground, _ball) = ball_on_ground(rpr_ball_collider_desc(0.5)); + let mode = 1; // Collider shapes. + let default_count = rpr_debug_render(world, mode, ptr::null_mut(), 0); + let count = rpr_debug_render_with_style(world, mode, &style, ptr::null_mut(), 0); + assert_eq!((rpr_last_status(), count), (RPR_OK, default_count)); + let null_count = + rpr_debug_render_with_style(world, mode, ptr::null(), ptr::null_mut(), 0); + assert_eq!(null_count, default_count); + + let mut blue = style; + blue.collider_dynamic_color = [240.0, 1.0, 0.5, 1.0]; + let mut lines = vec![RprDebugLine::default(); count]; + rpr_debug_render_with_style(world, mode, &blue, lines.as_mut_ptr(), count); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(lines.iter().any(|l| l.color == blue.collider_dynamic_color)); + assert!(lines.iter().any(|l| l.color == style.collider_fixed_color)); + + let mut invalid = style; + invalid.subdivisions = 0; + rpr_debug_render_with_style(world, mode, &invalid, ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + invalid = style; + invalid.contact_normal_length = -1.0; + rpr_debug_render_with_style(world, mode, &invalid, ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } +} diff --git a/c/src/read_access.rs b/c/src/read_access.rs index bc34257f5..bcfe9b7a8 100644 --- a/c/src/read_access.rs +++ b/c/src/read_access.rs @@ -33,6 +33,37 @@ pub unsafe extern "C" fn rpr_read_pid_controller_rigid_body_correction( }) }) } +/// Compute a PD velocity correction from callback-visible body state. Neither the body nor the +/// controller is modified. The context is valid only during its callback. +/// @ingroup callbacks +#[rapier_export(read_pd_controller)] +pub unsafe extern "C" fn rpr_read_pd_controller_rigid_body_correction( + context: *const RprReadContext, + controller: *const RprPdController, + body: RprRigidBodyHandle, + target_pose: RprPose, + target_linvel: RprVector, + target_angvel: RprAngVector, +) -> RprVelocityCorrection { + ffi_value(|result: *mut RprVelocityCorrection| { + let linear = unsafe { std::ptr::addr_of_mut!((*result).linear) }; + let angular_velocity = unsafe { std::ptr::addr_of_mut!((*result).angularVelocity) }; + + ffi(|| unsafe { + body.check_world(read_context_world(context))?; + crate::handle_access::forward(native_pd_controller_rigid_body_correction( + controller, + get(context)?.bodies, + body, + target_pose, + target_linvel, + target_angvel, + linear, + angular_velocity, + )) + }) + }) +} /// Return the number of rigid body objects in the world. Uses only the callback-scoped read /// context; never retain the context. /// @ingroup callbacks @@ -1284,3 +1315,175 @@ pub unsafe extern "C" fn rpr_read_rigid_body_read_states( }) }) } +/// Return the collider body-type collision activation bitmask (RPR_COLLISION_TYPES_* bits). Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_active_collision_types( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> u16 { + ffi_value(|out: *mut u16| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_active_collision_types( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider physics-hook activation bitmask. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_active_hooks( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_active_hooks( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider friction combination rule (RPR_COMBINE_*). Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_friction_combine_rule( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_friction_combine_rule( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider restitution combination rule (RPR_COMBINE_*). Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_restitution_combine_rule( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_restitution_combine_rule( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the collider pose relative to its parent rigid body, or its world-space pose if it has +/// no parent. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_position_wrt_parent( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_collider_set_get_position_wrt_parent( + get(context)?.colliders, + handle, + out, + )) + }) + }) +} +/// Return the rigid body signed dominance group. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_dominance_group( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> i8 { + ffi_value(|out: *mut i8| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_dominance_group( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body additional solver iterations for connected bodies. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_additional_solver_iterations( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_additional_solver_iterations( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return the rigid body additional PGS iterations for connected bodies. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_additional_pgs_iterations( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_additional_pgs_iterations( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} +/// Return whether the rigid body may exceed the angular-velocity limit of its CCD. Uses only the callback-scoped read context; never +/// retain the context. +/// @ingroup callbacks +#[rapier_export(read_rigid_body)] +pub unsafe extern "C" fn rpr_read_rigid_body_is_fast_rotation_allowed( + context: *const RprReadContext, + handle: RprRigidBodyHandle, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + crate::handle_access::forward(native_rigid_body_set_get_is_fast_rotation_allowed( + get(context)?.bodies, + handle, + out, + )) + }) + }) +} diff --git a/c/src/render.rs b/c/src/render.rs index c7a5c2e8c..4bcef33ee 100644 --- a/c/src/render.rs +++ b/c/src/render.rs @@ -256,6 +256,30 @@ pub unsafe extern "C" fn rpr_round_cylinder_shared_shape( }) } +/// Create an owned round cone shape. Release it with rpr_free_shared_shape. +/// @ingroup shapes +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_round_cone_shared_shape( + half_height: RprReal, + radius: RprReal, + border_radius: RprReal, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + output( + out, + Box::into_raw(Box::new(RprSharedShape(SharedShape::round_cone( + positive(half_height)?, + positive(radius)?, + nonnegative(border_radius)?, + )))), + ) + }) + }) +} + /// Owned indexed geometry from Parry's shape tessellation, preserving its vertex order. /// @ingroup shapes #[cfg(feature = "dim3")] @@ -266,6 +290,8 @@ pub struct RprTriMeshData { /// Tessellate a ball or capsule with independent longitude/latitude subdivision counts. /// Cuboids, cones, cylinders, convex polyhedra, trimeshes, and heightfields are also supported. +/// ntheta (3 to 4096) is read by balls, capsules, cones and cylinders; nphi (2 to 4096) by balls +/// and capsules. Other shapes ignore them. /// @ingroup shapes #[cfg(feature = "dim3")] #[rapier_export(shared_shape)] @@ -277,10 +303,16 @@ pub unsafe extern "C" fn rpr_shared_shape_to_trimesh( ffi_value(|out: *mut *mut RprTriMeshData| { ffi(|| unsafe { out_ptr(out)?; - ensure( - (3..=4096).contains(&ntheta) && (2..=4096).contains(&nphi), - "invalid tessellation subdivisions", - )?; + let check_theta = || ensure((3..=4096).contains(&ntheta), "invalid ntheta"); + let check_phi = || ensure((2..=4096).contains(&nphi), "invalid nphi"); + match get(shape)?.0.as_typed_shape() { + TypedShape::Ball(_) | TypedShape::Capsule(_) => { + check_theta()?; + check_phi()?; + } + TypedShape::Cone(_) | TypedShape::Cylinder(_) => check_theta()?, + _ => {} + } let (vertices, indices) = match get(shape)?.0.as_typed_shape() { TypedShape::Ball(s) => s.to_trimesh(ntheta, nphi), TypedShape::Capsule(s) => s.to_trimesh(ntheta, nphi), diff --git a/c/src/robotics.rs b/c/src/robotics.rs index 7c795f50f..dddeef08d 100644 --- a/c/src/robotics.rs +++ b/c/src/robotics.rs @@ -1,10 +1,12 @@ //! Optional native URDF and MJCF importers (3D f32). #![allow(non_snake_case)] use crate::*; +use rapier::parry::shape::TriMeshFlags; +use rapier3d_mjcf::MjcfContactHooks; use rapier3d_mjcf::MjcfVisualMesh; use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot, MjcfRobotHandles}; use rapier3d_urdf::{UrdfLoaderOptions, UrdfMultibodyOptions, UrdfRobot, UrdfRobotHandles}; -use std::ffi::{CStr, c_char}; +use std::ffi::{CStr, c_char, c_void}; unsafe fn path_string<'a>(path: *const c_char) -> Result<&'a str> { ensure(!path.is_null(), "null path")?; @@ -230,7 +232,8 @@ pub unsafe extern "C" fn rpr_urdf_robot_insert_using_multibody_joints( }) } -/// Body handles in source order; absent MJCF bodies have invalid handles. +/// One body handle per imported URDF link, in source order. Links merged away by +/// squeezeEmptyFixedLinks have no entry. /// @see @ref output_buffers /// @ingroup robotics #[rapier_export(urdf_robot_handles)] @@ -952,6 +955,299 @@ pub unsafe extern "C" fn rpr_mjcf_visual_mesh_texture( }) } +/// Load a URDF robot from a NUL-terminated UTF-8 string. Relative mesh paths are resolved from +/// mesh_dir (NULL resolves them from the current directory). Options and their blueprint resources +/// are borrowed through this call; the robot is owned. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_urdf_robot_from_string( + urdf: *const c_char, + mesh_dir: *const c_char, + options: *const RprUrdfLoaderOptions, +) -> *mut RprUrdfRobot { + ffi_value(|out: *mut *mut RprUrdfRobot| { + ffi(|| unsafe { + out_ptr(out)?; + ensure(!urdf.is_null(), "null URDF string")?; + let urdf = CStr::from_ptr(urdf) + .to_str() + .map_err(|_| invalid("URDF string must be UTF-8"))?; + let mesh_dir = if mesh_dir.is_null() { + "." + } else { + path_string(mesh_dir)? + }; + let options = get(options)?.raw()?; + let (robot, _) = UrdfRobot::from_str(urdf, options, std::path::Path::new(mesh_dir)) + .map_err(|e| invalid(e.to_string()))?; + output(out, Box::into_raw(Box::new(RprUrdfRobot(robot)))) + }) + }) +} + +/// Owned physics hooks applying the `` rules (excluded pairs, pair friction) of an +/// inserted MJCF robot. Release with the matching Free function. +/// @ingroup robotics +pub struct RprMjcfContactHooks(pub(crate) MjcfContactHooks); +/// Release owned MJCF contact hooks. NULL is allowed. Do not free them while a step still uses +/// them, and do not free them twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_mjcf_contact_hooks(hooks: *mut RprMjcfContactHooks) -> RprStatus { + ffi(|| unsafe { + if !hooks.is_null() { + get(hooks)?; + drop(Box::from_raw(hooks)); + } + Ok(()) + }) +} +/// Build the contact rules of an inserted MJCF robot. robot must be the robot these handles were +/// inserted from. The rules refer to the inserted colliders; the returned hooks are owned. +/// @ingroup robotics +#[rapier_export(mjcf_robot_handles)] +pub unsafe extern "C" fn rpr_mjcf_robot_handles_contact_hooks( + handles: *const RprMjcfRobotHandles, + robot: *const RprMjcfRobot, +) -> *mut RprMjcfContactHooks { + ffi_value(|out: *mut *mut RprMjcfContactHooks| { + ffi(|| unsafe { + out_ptr(out)?; + let robot = &get(robot)?.0; + let hooks = match &get(handles)?.handles { + MjcfHandles::Impulse(h) => h.contact_hooks(robot), + MjcfHandles::Multibody(h) => h.contact_hooks(robot), + }; + output(out, Box::into_raw(Box::new(RprMjcfContactHooks(hooks)))) + }) + }) +} +/// Return physics hooks forwarding to these contact rules, with hooks as their user_data. Pass +/// them to rpr_step; hooks must outlive every step using them. The inserted colliders already +/// enable the contact-filtering and contact-modification hooks. +/// @ingroup robotics +#[rapier_export(mjcf_contact_hooks)] +pub unsafe extern "C" fn rpr_mjcf_contact_hooks_physics_hooks( + hooks: *const RprMjcfContactHooks, +) -> RprPhysicsHooks { + ffi_value(|out: *mut RprPhysicsHooks| { + ffi(|| unsafe { + let native: *const MjcfContactHooks = &get(hooks)?.0; + output( + out, + RprPhysicsHooks { + user_data: native.cast_mut().cast(), + filter_contact_pair: Some(forward_filter_contact_pair::), + filter_intersection_pair: Some( + forward_filter_intersection_pair::, + ), + modify_solver_contacts: None, + modify_solver_contacts_context: Some( + forward_modify_solver_contacts::, + ), + }, + ) + }) + }) +} + +// The C callbacks below forward to a native PhysicsHooks value given as user_data. +unsafe fn pair_filter_context<'a>( + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, +) -> Option> { + let read = unsafe { read.as_ref()? }; + let body = |h: RprRigidBodyHandle| (h.index != u32::MAX).then(|| h.raw()); + Some(PairFilterContext { + bodies: unsafe { &read.bodies.as_ref()?.0 }, + colliders: unsafe { &read.colliders.as_ref()?.0 }, + collider1: collider1.raw(), + collider2: collider2.raw(), + rigid_body1: body(body1), + rigid_body2: body(body2), + }) +} +unsafe extern "C" fn forward_filter_contact_pair( + user_data: *mut c_void, + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, +) -> i32 { + let hooks = unsafe { user_data.cast::().as_ref() }; + let context = unsafe { pair_filter_context(read, collider1, collider2, body1, body2) }; + let (Some(hooks), Some(context)) = (hooks, context) else { + return 1; + }; + match hooks.filter_contact_pair(&context) { + None => -1, + Some(flags) if flags.contains(SolverFlags::COMPUTE_RIGID_IMPULSES) => 1, + Some(_) => 0, + } +} +unsafe extern "C" fn forward_filter_intersection_pair( + user_data: *mut c_void, + read: *const RprReadContext, + collider1: RprColliderHandle, + collider2: RprColliderHandle, + body1: RprRigidBodyHandle, + body2: RprRigidBodyHandle, +) -> i32 { + let hooks = unsafe { user_data.cast::().as_ref() }; + let context = unsafe { pair_filter_context(read, collider1, collider2, body1, body2) }; + let (Some(hooks), Some(context)) = (hooks, context) else { + return 1; + }; + hooks.filter_intersection_pair(&context) as i32 +} +unsafe extern "C" fn forward_modify_solver_contacts( + user_data: *mut c_void, + _read: *const RprReadContext, + _collider1: RprColliderHandle, + _collider2: RprColliderHandle, + context: *mut RprContactModificationContext, +) { + let hooks = unsafe { user_data.cast::().as_ref() }; + let context = unsafe { context.as_mut() }; + if let (Some(hooks), Some(context)) = (hooks, context) { + let native = unsafe { &mut *context.raw.cast::>() }; + hooks.modify_solver_contacts(native); + } +} + +/// @ingroup robotics +/// Load each mesh as a triangle mesh, with the given trimesh flags. +pub const RPR_MESH_CONVERTER_TRIMESH: u32 = 0; +/// @ingroup robotics +/// Replace each mesh by its oriented bounding box. +pub const RPR_MESH_CONVERTER_OBB: u32 = 1; +/// @ingroup robotics +/// Replace each mesh by its axis-aligned bounding box. +pub const RPR_MESH_CONVERTER_AABB: u32 = 2; +/// @ingroup robotics +/// Replace each mesh by its convex hull. +pub const RPR_MESH_CONVERTER_CONVEX_HULL: u32 = 3; +/// @ingroup robotics +/// Replace each mesh by its convex decomposition. +pub const RPR_MESH_CONVERTER_CONVEX_DECOMPOSITION: u32 = 4; + +/// Shapes loaded from a mesh file (STL, COLLADA or Wavefront OBJ), one per mesh of the file. +/// Release with the matching Free function. +/// @ingroup robotics +pub struct RprLoadedMeshes { + meshes: Vec>, +} +/// Release owned loaded meshes. NULL is allowed. Do not pass borrowed pointers or free the object +/// twice. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_free_loaded_meshes(meshes: *mut RprLoadedMeshes) -> RprStatus { + ffi(|| unsafe { + if !meshes.is_null() { + get(meshes)?; + drop(Box::from_raw(meshes)); + } + Ok(()) + }) +} +/// Load every mesh of a file from a UTF-8 path and convert it into a shape with converter (an +/// RPR_MESH_CONVERTER_* value). trimesh_flags (RPR_TRIMESH_* bits) apply to +/// RPR_MESH_CONVERTER_TRIMESH and must be 0 otherwise. scale multiplies the vertices before +/// conversion. A mesh failing to convert does not fail the load; see rpr_loaded_meshes_clone_shape. +/// @ingroup robotics +#[rapier_export] +pub unsafe extern "C" fn rpr_loaded_meshes_from_file( + path: *const c_char, + converter: u32, + trimesh_flags: u32, + scale: RprVector, +) -> *mut RprLoadedMeshes { + ffi_value(|out: *mut *mut RprLoadedMeshes| { + ffi(|| unsafe { + out_ptr(out)?; + let path = path_string(path)?; + let scale = scale.raw()?; + ensure( + trimesh_flags == 0 || converter == RPR_MESH_CONVERTER_TRIMESH, + "trimesh flags require the trimesh converter", + )?; + let converter = match converter { + RPR_MESH_CONVERTER_TRIMESH if trimesh_flags == 0 => MeshConverter::TriMesh, + RPR_MESH_CONVERTER_TRIMESH => MeshConverter::TriMeshWithFlags( + u16::try_from(trimesh_flags) + .ok() + .and_then(TriMeshFlags::from_bits) + .ok_or_else(|| invalid("unknown trimesh flags"))?, + ), + RPR_MESH_CONVERTER_OBB => MeshConverter::Obb, + RPR_MESH_CONVERTER_AABB => MeshConverter::Aabb, + RPR_MESH_CONVERTER_CONVEX_HULL => MeshConverter::ConvexHull, + RPR_MESH_CONVERTER_CONVEX_DECOMPOSITION => MeshConverter::ConvexDecomposition, + _ => return Err(invalid("unknown mesh converter")), + }; + let meshes = rapier3d_meshloader::load_from_path(path, &converter, scale) + .map_err(|e| invalid(e.to_string()))? + .into_iter() + .map(|m| m.map(|m| (m.shape, m.pose)).map_err(|e| e.to_string())) + .collect(); + output(out, Box::into_raw(Box::new(RprLoadedMeshes { meshes }))) + }) + }) +} +/// Return the number of meshes read from the file, including those that failed to convert. +/// @ingroup robotics +#[rapier_export(loaded_meshes)] +pub unsafe extern "C" fn rpr_loaded_meshes_count(meshes: *const RprLoadedMeshes) -> usize { + ffi_value(|out: *mut usize| ffi(|| unsafe { output(out, get(meshes)?.meshes.len()) })) +} +unsafe fn loaded_mesh<'a>( + meshes: *const RprLoadedMeshes, + index: usize, +) -> Result<&'a (SharedShape, Pose)> { + let mesh = unsafe { get(meshes)? } + .meshes + .get(index) + .ok_or_else(|| invalid("mesh index out of range"))?; + mesh.as_ref() + .map_err(|e| invalid(format!("mesh conversion failed: {e}"))) +} +/// Return an owned shape wrapper sharing the geometry of a loaded mesh. Release it with +/// FreeSharedShape. Returns NULL with INVALID_ARGUMENT if the index is out of range or if that +/// mesh failed to convert. +/// @ingroup robotics +#[rapier_export(loaded_meshes)] +pub unsafe extern "C" fn rpr_loaded_meshes_clone_shape( + meshes: *const RprLoadedMeshes, + index: usize, +) -> *mut RprSharedShape { + ffi_value(|out: *mut *mut RprSharedShape| { + ffi(|| unsafe { + out_ptr(out)?; + let (shape, _) = loaded_mesh(meshes, index)?; + output(out, Box::into_raw(Box::new(RprSharedShape(shape.clone())))) + }) + }) +} +/// Return the pose to give the shape of a loaded mesh (for example the center of its bounding +/// box). Reports INVALID_ARGUMENT if the index is out of range or if that mesh failed to convert. +/// @ingroup robotics +#[rapier_export(loaded_meshes)] +pub unsafe extern "C" fn rpr_loaded_meshes_pose( + meshes: *const RprLoadedMeshes, + index: usize, +) -> RprPose { + ffi_value(|out: *mut RprPose| { + ffi(|| unsafe { + let (_, pose) = loaded_mesh(meshes, index)?; + output(out, (*pose).into()) + }) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -1178,4 +1474,168 @@ mod tests { assert_eq!(rpr_free_mjcf_robot(robot), RPR_OK); } } + + #[test] + fn urdf_loads_from_a_string() { + let urdf = std::ffi::CString::new( + r#" + + "#, + ) + .unwrap(); + unsafe { + let options = rpr_default_urdf_loader_options(); + let robot = rpr_urdf_robot_from_string(urdf.as_ptr(), std::ptr::null(), &options); + assert_eq!(rpr_last_status(), RPR_OK); + let world = rpr_new_world(); + let handles = rpr_urdf_robot_insert_using_impulse_joints(world, robot); + assert_eq!( + rpr_urdf_robot_handles_bodies(handles, std::ptr::null_mut(), 0), + 1 + ); + assert_eq!(rpr_collider_count(world), 1); + assert_eq!(rpr_free_urdf_robot_handles(handles), RPR_OK); + assert_eq!(rpr_free_urdf_robot(robot), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + + let invalid = c" Real { + let path = std::env::temp_dir().join(format!( + "rapier-c-hooks-{}-{with_hooks}.xml", + std::process::id() + )); + std::fs::write( + &path, + r#" + + + "#, + ) + .unwrap(); + let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); + unsafe { + let robot = + rpr_mjcf_robot_from_file(cpath.as_ptr(), &rpr_default_mjcf_loader_options()); + std::fs::remove_file(path).unwrap(); + assert_eq!(rpr_last_status(), RPR_OK); + let world = rpr_new_world(); + let handles = rpr_mjcf_robot_insert_using_impulse_joints(world, robot); + let contact_hooks = rpr_mjcf_robot_handles_contact_hooks(handles, robot); + assert_eq!(rpr_last_status(), RPR_OK); + let hooks = rpr_mjcf_contact_hooks_physics_hooks(contact_hooks); + assert_eq!(rpr_last_status(), RPR_OK); + for _ in 0..20 { + let hooks: *const RprPhysicsHooks = + if with_hooks { &hooks } else { std::ptr::null() }; + assert_eq!(rpr_step(world, hooks, std::ptr::null()), RPR_OK); + } + let mut bodies = [RprRigidBodyHandle::default(); 3]; + assert_eq!( + rpr_mjcf_robot_handles_bodies(handles, bodies.as_mut_ptr(), 3), + 3 + ); + let distance = (rpr_rigid_body_translation(bodies[2]).raw().unwrap() + - rpr_rigid_body_translation(bodies[1]).raw().unwrap()) + .length(); + assert_eq!(rpr_free_mjcf_contact_hooks(contact_hooks), RPR_OK); + assert_eq!(rpr_free_mjcf_robot_handles(handles), RPR_OK); + assert_eq!(rpr_free_mjcf_robot(robot), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + distance + } + } + + #[test] + fn mjcf_contact_hooks_exclude_pairs_through_c_hooks() { + unsafe { + // Without the hooks, the overlapping spheres push each other apart. + assert!(step_overlapping_mjcf_spheres(false) > 0.45); + assert!((step_overlapping_mjcf_spheres(true) - 0.4).abs() < 1.0e-4); + assert!( + rpr_mjcf_contact_hooks_physics_hooks(std::ptr::null()) + .filter_contact_pair + .is_none() + ); + assert_eq!(rpr_last_status(), RPR_NULL_POINTER); + } + } + + #[test] + fn mesh_files_load_as_shapes() { + let path = std::env::temp_dir().join(format!("rapier-c-meshes-{}.obj", std::process::id())); + std::fs::write( + &path, + "g box\nv 1 1 1\nv 3 1 1\nv 1 3 1\nv 1 1 3\nv 3 3 3\nf 1 2 3\nf 1 2 4\nf 1 3 4\nf 2 3 5\nf 2 4 5\nf 3 4 5\n\ + g point\nv 0 0 0\nv 0 0 0\nv 0 0 0\nf 6 7 8\n", + ) + .unwrap(); + let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); + unsafe { + let scale = RprVector::from(Vector::splat(2.0)); + let meshes = + rpr_loaded_meshes_from_file(cpath.as_ptr(), RPR_MESH_CONVERTER_AABB, 0, scale); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_loaded_meshes_count(meshes), 2); + let shape = rpr_loaded_meshes_clone_shape(meshes, 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert!((*shape).0.as_cuboid().is_some()); + assert_eq!(rpr_free_shared_shape(shape), RPR_OK); + let pose = rpr_loaded_meshes_pose(meshes, 0).raw().unwrap(); + assert_eq!(pose.translation, Vector::splat(4.0)); + assert!(rpr_loaded_meshes_clone_shape(meshes, 2).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_loaded_meshes(meshes), RPR_OK); + + // The convex hull of the point mesh fails without failing the whole file. + let meshes = rpr_loaded_meshes_from_file( + cpath.as_ptr(), + RPR_MESH_CONVERTER_CONVEX_HULL, + 0, + scale, + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_loaded_meshes_count(meshes), 2); + let shape = rpr_loaded_meshes_clone_shape(meshes, 0); + assert!((*shape).0.as_convex_polyhedron().is_some()); + assert_eq!(rpr_free_shared_shape(shape), RPR_OK); + assert!(rpr_loaded_meshes_clone_shape(meshes, 1).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + rpr_loaded_meshes_pose(meshes, 1); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_loaded_meshes(meshes), RPR_OK); + + let meshes = rpr_loaded_meshes_from_file( + cpath.as_ptr(), + RPR_MESH_CONVERTER_TRIMESH, + RPR_TRIMESH_ORIENTED, + scale, + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_free_loaded_meshes(meshes), RPR_OK); + for (converter, flags) in [ + (RPR_MESH_CONVERTER_CONVEX_HULL, RPR_TRIMESH_ORIENTED), + (RPR_MESH_CONVERTER_TRIMESH, 1 << 20), + (5, 0), + ] { + assert!( + rpr_loaded_meshes_from_file(cpath.as_ptr(), converter, flags, scale).is_null() + ); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } + std::fs::remove_file(&path).unwrap(); + assert!(rpr_loaded_meshes_from_file(cpath.as_ptr(), 0, 0, scale).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } + } } diff --git a/c/src/scoped_access.rs b/c/src/scoped_access.rs index 10410c62c..fa9d1984d 100644 --- a/c/src/scoped_access.rs +++ b/c/src/scoped_access.rs @@ -532,7 +532,9 @@ pub unsafe extern "C" fn rpr_soft_body_add_force( }) } -/// Apply a world-space linear impulse. +/// Add the same world-space velocity change to every free particle: the whole body is kicked at +/// the same velocity, whatever the particle masses (the value is not divided by the mass). Pinned +/// particles ignore it. /// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. /// @ingroup soft_bodies #[rapier_export(soft_body)] @@ -628,7 +630,9 @@ pub unsafe extern "C" fn rpr_soft_body_set_volume_factor( }) } -/// Attach a particle to a rigid body at the supplied body-local anchor. +/// Attach a particle to a rigid body by a two-way point-to-point constraint (unlike pinning). The +/// anchor is the particle's current position, expressed in the rigid body's local frame; a particle +/// attached twice keeps both attachments. Undo it with rpr_soft_body_detach_particle. /// @ingroup soft_bodies #[rapier_export(soft_body)] pub unsafe extern "C" fn rpr_soft_body_attach_particle( @@ -656,26 +660,30 @@ pub unsafe extern "C" fn rpr_soft_body_attach_particle( }) } -/// Remove a particle attachment to a rigid body. +/// Detach a particle from every rigid body it was attached to with rpr_soft_body_attach_particle. +/// Returns whether it was attached at all. /// @ingroup soft_bodies #[rapier_export(soft_body)] pub unsafe extern "C" fn rpr_soft_body_detach_particle( handle: RprSoftBodyHandle, index: usize, -) -> RprStatus { +) -> RprBool { let world = handle.world; - ffi(|| unsafe { - handle.check_world(world)?; - let access = get(world)?.write()?; - let raw = access.raw(); + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); - let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); - let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; - forward(native_soft_body_detach_particle( - (element as *mut SoftBody).cast(), - index, - )) + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_detach_particle( + (element as *mut SoftBody).cast(), + index, + out, + )) + }) }) } @@ -840,7 +848,9 @@ pub unsafe extern "C" fn rpr_soft_body_set_cluster_shape_matching_enabled( }) } -/// Set the soft body cluster shape-matching stiffness multiplier. +/// Scale the material stiffness (Young modulus) of every cell fully contained in a live cluster: +/// regional materials without a separate body. Cells straddling the cluster's boundary keep their +/// stiffness; use rpr_soft_body_set_cluster_edge_softness for edges. /// @ingroup soft_bodies #[rapier_export(soft_body)] pub unsafe extern "C" fn rpr_soft_body_set_cluster_stiffness_scale( @@ -1921,7 +1931,7 @@ pub(crate) unsafe fn native_rigid_body_set_get_kinetic_energy( }) } /// Return the rigid body soft-CCD prediction distance. -/// @ingroup soft_bodies +/// @ingroup rigid_bodies #[rapier_export(rigid_body)] pub unsafe extern "C" fn rpr_rigid_body_soft_ccd_prediction(handle: RprRigidBodyHandle) -> RprReal { let world = handle.world; @@ -2335,7 +2345,7 @@ pub unsafe extern "C" fn rpr_rigid_body_set_additional_mass( } /// Set the rigid body soft-CCD prediction distance. -/// @ingroup soft_bodies +/// @ingroup rigid_bodies #[rapier_export(rigid_body)] pub unsafe extern "C" fn rpr_rigid_body_set_soft_ccd_prediction( handle: RprRigidBodyHandle, @@ -3454,8 +3464,7 @@ pub(crate) unsafe fn native_collider_set_get_compute_aabb( )) }) } -/// Return an owned wrapper sharing the collider geometry. Release with rpr_free_shared_shape. -/// Returns an owned shape wrapper sharing the geometry. Release it with FreeSharedShape. +/// Return an owned wrapper sharing the collider geometry. Release it with rpr_free_shared_shape. /// @ingroup shapes #[rapier_export(collider)] pub unsafe extern "C" fn rpr_collider_clone_shape( diff --git a/c/src/shape_desc.rs b/c/src/shape_desc.rs index 5081c1996..da60b77c7 100644 --- a/c/src/shape_desc.rs +++ b/c/src/shape_desc.rs @@ -143,6 +143,27 @@ pub extern "C" fn rpr_round_cylinder_collider_desc( ..RprColliderDesc::default() } } +/// Return a rounded Y-aligned cone description; dimensions exclude border_radius. +/// Returns a description without allocating or validating. Build/insert validates its fields. +/// @ingroup colliders +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_round_cone_collider_desc( + half_height: RprReal, + radius: RprReal, + border_radius: RprReal, +) -> RprColliderDesc { + RprColliderDesc { + shape: RprShapeDesc { + kind: RPR_SHAPE_DESC_ROUND_CONE, + halfHeight: half_height, + radius, + borderRadius: border_radius, + ..RprShapeDesc::default() + }, + ..RprColliderDesc::default() + } +} /// Return a X-aligned capsule description; half_height is half the segment length, excluding caps. /// Returns a description without allocating or validating. Build/insert validates its fields. /// @ingroup colliders diff --git a/c/src/soft_body.rs b/c/src/soft_body.rs index 648f70b9a..724e44bf1 100644 --- a/c/src/soft_body.rs +++ b/c/src/soft_body.rs @@ -382,12 +382,13 @@ pub(crate) unsafe fn native_soft_body_attach_particle( pub(crate) unsafe fn native_soft_body_detach_particle( body: *mut RprSoftBody, index: usize, + out: *mut RprBool, ) -> RprStatus { ffi(|| unsafe { + out_ptr(out)?; let b = &mut get_mut(body)?.0; ensure(index < b.num_particles(), "particle index out of range")?; - b.detach_particle(index); - Ok(()) + output(out, b.detach_particle(index) as RprBool) }) } @@ -419,7 +420,9 @@ pub unsafe extern "C" fn rpr_soft_body_tear_event_soft_body( }, ) } -/// Copy the soft-body handles produced by the tear. +/// Copy the soft bodies the torn body is in after the tear, the one keeping the handle first: the +/// torn body alone when nothing was split off. Entry i holds the particles given by +/// rpr_soft_body_tear_event_piece_particles(event, i, ...). /// @see @ref output_buffers /// @ingroup soft_bodies #[rapier_export(soft_body_tear_event)] @@ -557,7 +560,9 @@ pub unsafe extern "C" fn rpr_soft_body_tear_event_inserted_particles( }) }) } -/// Copy original particle indices belonging to a resulting piece. +/// Copy the particles of the piece_index-th body of rpr_soft_body_tear_event_bodies, as indices in +/// the torn body after the tear (the indices the other event fields use); entry i is the piece's +/// particle i. piece_index must be less than rpr_soft_body_tear_event_piece_count. /// @see @ref output_buffers /// @ingroup soft_bodies #[rapier_export(soft_body_tear_event)] @@ -569,7 +574,14 @@ pub unsafe extern "C" fn rpr_soft_body_tear_event_piece_particles( ) -> usize { ffi_value(|count: *mut usize| { ffi(|| unsafe { - let p = get(event)? + let event = get(event)?; + if event.0.pieces.is_empty() { + // Nothing split off: the torn body is the only piece and keeps its particles. + ensure(piece_index == 0, "piece index out of range")?; + let all: Vec = (0..event.2 as u32).collect(); + return copy_out(&all, buffer, capacity, count); + } + let p = event .0 .pieces .get(piece_index) @@ -720,10 +732,13 @@ pub unsafe extern "C" fn rpr_soft_body_tear( &mut get_mut(impulse_joints)?.0, &mut get_mut(multibody_joints)?.0, ); + let particles = set.0.get(handle.raw()).map_or(0, |b| b.num_particles()); output( out, - e.map(|e| Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world)))) - .unwrap_or(std::ptr::null_mut()), + e.map(|e| { + Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world, particles))) + }) + .unwrap_or(std::ptr::null_mut()), ) }) }) @@ -1218,11 +1233,18 @@ pub unsafe extern "C" fn rpr_cut_soft_body( .map(RprVector::raw) .collect::>>()?; let blade: [Vector; rapier::math::DIM] = blade.try_into().unwrap(); - let event = get_mut(world)?.0.cut_soft_body(handle.raw(), &blade); + let world = &mut get_mut(world)?.0; + let event = world.cut_soft_body(handle.raw(), &blade); + let particles = world + .soft_bodies + .get(handle.raw()) + .map_or(0, |b| b.num_particles()); output( out, event - .map(|e| Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world)))) + .map(|e| { + Box::into_raw(Box::new(RprSoftBodyTearEvent(e, handle.world, particles))) + }) .unwrap_or(std::ptr::null_mut()), ) }) diff --git a/c/src/soft_desc.rs b/c/src/soft_desc.rs index 242362f2c..9b42042eb 100644 --- a/c/src/soft_desc.rs +++ b/c/src/soft_desc.rs @@ -31,6 +31,30 @@ pub const RPR_SOFT_DESC_CLOTH_TUBE: u32 = 8; /// @ingroup soft_bodies /// Soft-body selector: desc volumetric. pub const RPR_SOFT_DESC_VOLUMETRIC: u32 = 9; +/// @ingroup soft_bodies +/// Soft-body selector: closed counter-clockwise polygon of particles preserving its area (2D). +#[cfg(feature = "dim2")] +pub const RPR_SOFT_DESC_POLYGON: u32 = 10; +/// @ingroup soft_bodies +/// Soft-body selector: triangle mesh without cells, held by shape matching (2D). +#[cfg(feature = "dim2")] +pub const RPR_SOFT_DESC_TRIMESH: u32 = 11; +/// @ingroup soft_bodies +/// Soft-body selector: cloth with separate warp, weft and shear softness (3D). +#[cfg(feature = "dim3")] +pub const RPR_SOFT_DESC_CLOTH_ANISOTROPIC: u32 = 12; +/// Borrowed array of soft-body descriptions. count counts descriptions. +/// Data must remain live through the build/insert call that reads the description. +/// NULL is permitted only when count is zero. +/// @ingroup soft_bodies +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct RprSoftBodyDescView { + /// Borrowed pointer to contiguous elements; NULL is allowed when count is zero. + pub data: *const RprSoftBodyDesc, + /// Number of elements, not bytes unless the element type is a byte. + pub count: usize, +} /// Spring-coefficient override for one soft-body edge. /// @ingroup soft_bodies #[repr(C)] @@ -56,6 +80,8 @@ pub struct RprSoftEdgeTear { /// for topology arrays are element counts (edges, triangles, or tetrahedra). /// Nonempty topology overrides the generator's topology. Zero counts retain it. /// Generator inputs: a/b are rope ends or center/half-extents; cloth uses a/du/dv. +/// RPR_SOFT_DESC_POLYGON reads positions; RPR_SOFT_DESC_TRIMESH reads positions and cells (its +/// triangles become edges and a boundary, not cells). /// @ingroup soft_bodies #[repr(C)] #[derive(Clone, Copy)] @@ -70,6 +96,15 @@ pub struct RprSoftBodyDesc { pub du: RprVector, /// Cloth basis step along its second parameter axis. pub dv: RprVector, + #[cfg(feature = "dim3")] + /// Softness of the anisotropic cloth edges along du (warp). + pub warpSoftness: RprSpringCoefficients, + #[cfg(feature = "dim3")] + /// Softness of the anisotropic cloth edges along dv (weft). + pub weftSoftness: RprSpringCoefficients, + #[cfg(feature = "dim3")] + /// Softness of the anisotropic cloth diagonal edges (shear). + pub shearSoftness: RprSpringCoefficients, /// First recipe resolution; interpretation depends on kind. pub nx: usize, /// Second recipe resolution; interpretation depends on kind. @@ -116,6 +151,14 @@ pub struct RprSoftBodyDesc { pub skinVertices: RprVectorView, /// Borrowed skin topology. pub skinIndices: RprSurfaceElementView, + /// Borrowed descriptions merged into this body, their particles numbered after this one's in + /// order. Each contributes its particles, masses, pinned particles and elements (after its own + /// translation and total mass); every other setting comes from this description. Appended + /// descriptions cannot append others nor have a skin. + pub appended: RprSoftBodyDescView, + /// Borrowed structural edges added after appending (seams); indices count this body's particles + /// then the appended ones. Their rest length is the current distance of their particles. + pub addedEdges: RprEdgeView, /// Soft-body material coefficients. pub material: RprSoftBodyMaterial, /// RPR_SOFT_CELL_VOLUME, RPR_SOFT_CELL_COROTATIONAL, or RPR_SOFT_CELL_NEO_HOOKEAN. @@ -132,6 +175,9 @@ pub struct RprSoftBodyDesc { pub volumeFactor: RprReal, /// Optional shape-matching override; disabled retains recipe defaults. pub shapeMatching: RprOptionalBool, + /// Optional override of the ORIENTED flag of the generated collision surface (when disabled, a + /// closed surface is oriented). Set it to false for a shell whose inner side holds bodies. + pub oriented: RprOptionalBool, /// Whether self-collision is enabled. pub selfContacts: RprBool, /// Whether skin elements participate in collision detection. @@ -185,6 +231,12 @@ impl Default for RprSoftBodyDesc { b: Vector::ONE.into(), du: Vector::X.into(), dv: Vector::Y.into(), + #[cfg(feature = "dim3")] + warpSoftness: SoftBodyMaterial::default().edge_softness.into(), + #[cfg(feature = "dim3")] + weftSoftness: SoftBodyMaterial::default().edge_softness.into(), + #[cfg(feature = "dim3")] + shearSoftness: SoftBodyMaterial::default().edge_softness.into(), nx: 2, ny: 2, nz: 2, @@ -204,6 +256,8 @@ impl Default for RprSoftBodyDesc { wire: RprEdgeView::default(), skinVertices: RprVectorView::default(), skinIndices: RprSurfaceElementView::default(), + appended: RprSoftBodyDescView::default(), + addedEdges: RprEdgeView::default(), material: SoftBodyMaterial::default().into(), cellModel: match SoftBodyCellModel::default() { SoftBodyCellModel::Volume => 0, @@ -216,6 +270,7 @@ impl Default for RprSoftBodyDesc { volumePreservation: 0, volumeFactor: 1.0, shapeMatching: RprOptionalBool::default(), + oriented: RprOptionalBool::default(), selfContacts: 0, skinCollision: 0, collisionEnabled: 1, @@ -232,6 +287,52 @@ impl Default for RprSoftBodyDesc { } impl RprSoftBodyDesc { pub(crate) unsafe fn raw(&self) -> Result { + use crate::geometry::indices_array; + let mut b = unsafe { self.raw_piece()? }; + for piece in unsafe { input(self.appended.data, self.appended.count)? } { + ensure( + piece.appended.count == 0, + "appended soft-body descriptions cannot append others", + )?; + ensure( + piece.skinVertices.count == 0, + "appended soft-body descriptions cannot have a skin", + )?; + let mut other = unsafe { piece.raw()? }; + ensure( + b.positions + .len() + .checked_add(other.positions.len()) + .is_some_and(|n| n <= u32::MAX as usize), + "too many soft-body particles", + )?; + // `append` keeps this body's uniform mass unless the piece's masses are explicit. + if other.masses.is_empty() && other.particle_mass != b.particle_mass { + other.masses = vec![other.particle_mass; other.positions.len()]; + } + // `append` drops the piece's wire: carry it over, shifted. + #[cfg(feature = "dim3")] + { + let offset = b.positions.len() as u32; + let wire = std::mem::take(&mut other.wire); + b.wire + .extend(wire.into_iter().map(|w| [w[0] + offset, w[1] + offset])); + } + b = b.append(other); + } + if self.addedEdges.count != 0 { + let edges = unsafe { + indices_array::<2>( + self.addedEdges.data.cast(), + self.addedEdges.count, + b.positions.len(), + )? + }; + b = b.add_edges(edges); + } + Ok(b) + } + unsafe fn raw_piece(&self) -> Result { use crate::geometry::indices_array; let points = || { unsafe { input(self.positions.data, self.positions.count)? } @@ -264,6 +365,27 @@ impl RprSoftBodyDesc { } } #[cfg(feature = "dim2")] + RPR_SOFT_DESC_POLYGON => { + ensure( + (3..=u32::MAX as usize).contains(&self.positions.count), + "a soft polygon needs at least 3 points", + )?; + SoftBodyBuilder::polygon(points()?) + } + #[cfg(feature = "dim2")] + RPR_SOFT_DESC_TRIMESH => { + ensure( + self.positions.count > 0 && self.positions.count <= u32::MAX as usize, + "invalid particle count", + )?; + let vertices = points()?; + let idx = unsafe { + indices_array::<3>(self.cells.data.cast(), self.cells.count, vertices.len())? + }; + SoftBodyBuilder::trimesh(vertices, idx) + .ok_or_else(|| invalid("invalid soft triangle mesh"))? + } + #[cfg(feature = "dim2")] RPR_SOFT_DESC_DISK => { ensure( (3..=1_000_000).contains(&self.nx), @@ -344,7 +466,7 @@ impl RprSoftBodyDesc { SoftBodyBuilder::cuboid(self.a.raw()?, self.b.raw()?, self.nx, self.ny, self.nz) } #[cfg(feature = "dim3")] - RPR_SOFT_DESC_CLOTH => { + RPR_SOFT_DESC_CLOTH | RPR_SOFT_DESC_CLOTH_ANISOTROPIC => { ensure( self.nx >= 2 && self.ny >= 2 @@ -354,13 +476,26 @@ impl RprSoftBodyDesc { .is_some_and(|n| n <= u32::MAX as usize), "invalid cloth size", )?; - SoftBodyBuilder::cloth( - self.a.raw()?, - self.du.raw()?, - self.dv.raw()?, - self.nx, - self.ny, - ) + if self.kind == RPR_SOFT_DESC_CLOTH { + SoftBodyBuilder::cloth( + self.a.raw()?, + self.du.raw()?, + self.dv.raw()?, + self.nx, + self.ny, + ) + } else { + SoftBodyBuilder::cloth_anisotropic( + self.a.raw()?, + self.du.raw()?, + self.dv.raw()?, + self.nx, + self.ny, + self.warpSoftness.raw()?, + self.weftSoftness.raw()?, + self.shearSoftness.raw()?, + ) + } } _ => return Err(invalid("unsupported soft-body recipe")), }; @@ -384,7 +519,11 @@ impl RprSoftBodyDesc { b.bend_edges = unsafe { indices_array::<2>(self.bendEdges.data.cast(), self.bendEdges.count, n)? }; } - if self.cells.count != 0 { + #[cfg(feature = "dim2")] + let cells_are_generator_input = self.kind == RPR_SOFT_DESC_TRIMESH; + #[cfg(feature = "dim3")] + let cells_are_generator_input = false; + if self.cells.count != 0 && !cells_are_generator_input { b.cells = unsafe { indices_array::<{ rapier::math::DIM + 1 }>( self.cells.data.cast(), @@ -414,15 +553,20 @@ impl RprSoftBodyDesc { } } let edge_count = b.edges.len() + b.bend_edges.len(); - b.tension_only_edges = - unsafe { input(self.tensionOnlyEdges.data, self.tensionOnlyEdges.count)? }.to_vec(); + // The generator's per-edge overrides index its own edges: drop them with its edges. + if self.edges.count != 0 || self.bendEdges.count != 0 { + b.tension_only_edges.clear(); + b.edge_softness.clear(); + b.edge_tear_resistance.clear(); + } + let tension_only = + unsafe { input(self.tensionOnlyEdges.data, self.tensionOnlyEdges.count)? }; ensure( - b.tension_only_edges - .iter() - .all(|i| (*i as usize) < edge_count), + tension_only.iter().all(|i| (*i as usize) < edge_count), "tension edge out of bounds", )?; - b.edge_softness = unsafe { input(self.edgeSoftness.data, self.edgeSoftness.count)? } + b.tension_only_edges.extend_from_slice(tension_only); + let softness = unsafe { input(self.edgeSoftness.data, self.edgeSoftness.count)? } .iter() .map(|v| { ensure( @@ -431,15 +575,16 @@ impl RprSoftBodyDesc { )?; Ok((v.edge, v.softness.raw()?)) }) - .collect::>()?; - b.edge_tear_resistance = - unsafe { input(self.edgeTearResistance.data, self.edgeTearResistance.count)? } - .iter() - .map(|v| { - ensure((v.edge as usize) < edge_count, "tear edge out of bounds")?; - Ok((v.edge, nonnegative(v.resistance)?)) - }) - .collect::>()?; + .collect::>>()?; + b.edge_softness.extend(softness); + let tear = unsafe { input(self.edgeTearResistance.data, self.edgeTearResistance.count)? } + .iter() + .map(|v| { + ensure((v.edge as usize) < edge_count, "tear edge out of bounds")?; + Ok((v.edge, nonnegative(v.resistance)?)) + }) + .collect::>>()?; + b.edge_tear_resistance.extend(tear); if self.skinVertices.count != 0 { let vertices = unsafe { input(self.skinVertices.data, self.skinVertices.count)? } .iter() @@ -482,6 +627,9 @@ impl RprSoftBodyDesc { if boolean(self.shapeMatching.enabled)? { b.shape_matching = boolean(self.shapeMatching.value)?; } + if boolean(self.oriented.enabled)? { + b.oriented = Some(boolean(self.oriented.value)?); + } b.self_contacts = boolean(self.selfContacts)?; b.skin_collision = boolean(self.skinCollision)?; b.collider_template = if boolean(self.collisionEnabled)? { diff --git a/c/src/soft_extras.rs b/c/src/soft_extras.rs new file mode 100644 index 000000000..40b88ead2 --- /dev/null +++ b/c/src/soft_extras.rs @@ -0,0 +1,1035 @@ +//! Soft-body accessors, deferred tears, impulses and world soft-body settings. +use crate::handle_access::forward; +use crate::*; + +pub(crate) unsafe fn native_soft_body_particle_velocity( + body: *const RprSoftBody, + index: usize, + out: *mut RprVector, +) -> RprStatus { + ffi(|| unsafe { + let body = &get(body)?.0; + ensure(index < body.num_particles(), "particle index out of bounds")?; + output(out, body.particle_velocity(index).into()) + }) +} +/// Return the world-space velocity of the indexed particle. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_particle_velocity( + handle: RprSoftBodyHandle, + index: usize, +) -> RprVector { + let world = handle.world; + ffi_value(|out: *mut RprVector| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_particle_velocity( + (element as *const SoftBody).cast(), + index, + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_soft_body_solver(body: *const RprSoftBody, out: *mut u32) -> RprStatus { + ffi(|| unsafe { + let _body = &get(body)?.0; + #[cfg(feature = "fem")] + let solver = match _body.solver() { + SoftBodySolver::Constraints => RPR_SOFT_SOLVER_CONSTRAINTS, + SoftBodySolver::Fem => RPR_SOFT_SOLVER_FEM, + }; + #[cfg(not(feature = "fem"))] + let solver = RPR_SOFT_SOLVER_CONSTRAINTS; + output(out, solver) + }) +} +/// Return the solver simulating the soft body's elasticity (RPR_SOFT_SOLVER_*). Always +/// RPR_SOFT_SOLVER_CONSTRAINTS in a library built without FEM. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_solver(handle: RprSoftBodyHandle) -> u32 { + let world = handle.world; + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + let set: *const RprSoftBodySet = std::ptr::addr_of!((*raw).0.soft_bodies).cast(); + + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_solver( + (element as *const SoftBody).cast(), + out, + )) + }) + }) +} + +pub(crate) unsafe fn native_soft_body_set_cluster_edge_softness( + body: *mut RprSoftBody, + cluster: u32, + softness: *const RprSpringCoefficients, +) -> RprStatus { + ffi(|| unsafe { + let softness = if softness.is_null() { + None + } else { + Some(get(softness)?.raw()?) + }; + let b = &mut get_mut(body)?.0; + b.cluster(cluster) + .filter(|c| c.is_live()) + .ok_or_else(|| invalid("invalid cluster"))?; + b.set_cluster_edge_softness(cluster, softness); + Ok(()) + }) +} +/// Override the softness of every structural or bending edge fully contained in a live cluster: +/// regional stiffness for cloth and ropes. A NULL softness restores the body material's. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_set_cluster_edge_softness( + handle: RprSoftBodyHandle, + cluster: u32, + softness: *const RprSpringCoefficients, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_set_cluster_edge_softness( + (element as *mut SoftBody).cast(), + cluster, + softness, + )) + }) +} + +pub(crate) unsafe fn native_soft_body_apply_impulse_at_point( + body: *mut RprSoftBody, + impulse: RprVector, + point: RprVector, + falloff_radius: RprReal, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let impulse = impulse.raw()?; + let point = point.raw()?; + let falloff_radius = finite(falloff_radius)?; + let wake_up = boolean(wake_up)?; + get_mut(body)? + .0 + .apply_impulse_at_point(impulse, point, falloff_radius, wake_up); + Ok(()) + }) +} +/// Apply a world-space impulse to every free particle within falloff_radius of the world-space +/// point, scaled linearly from 1 at the point to 0 at that radius and divided by the particle's +/// mass. A falloff_radius of zero or less gives every free particle the whole impulse. Pinned +/// particles ignore it. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_apply_impulse_at_point( + handle: RprSoftBodyHandle, + impulse: RprVector, + point: RprVector, + falloff_radius: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_apply_impulse_at_point( + (element as *mut SoftBody).cast(), + impulse, + point, + falloff_radius, + wake_up, + )) + }) +} + +pub(crate) unsafe fn native_soft_body_apply_radial_impulse( + body: *mut RprSoftBody, + center: RprVector, + magnitude: RprReal, + falloff_radius: RprReal, + wake_up: RprBool, +) -> RprStatus { + ffi(|| unsafe { + let center = center.raw()?; + let magnitude = finite(magnitude)?; + let falloff_radius = finite(falloff_radius)?; + let wake_up = boolean(wake_up)?; + get_mut(body)? + .0 + .apply_radial_impulse(center, magnitude, falloff_radius, wake_up); + Ok(()) + }) +} +/// Apply an impulse of the given magnitude pointing away from the world-space center to every +/// free particle within falloff_radius, scaled linearly from 1 at the center to 0 at that +/// radius and divided by the particle's mass. A particle on the center gets nothing; a +/// falloff_radius of zero or less pushes every free particle fully. A negative magnitude pulls +/// toward the center. Pinned particles ignore it. +/// wake_up = 1 wakes affected bodies; 0 preserves their sleep state. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_apply_radial_impulse( + handle: RprSoftBodyHandle, + center: RprVector, + magnitude: RprReal, + falloff_radius: RprReal, + wake_up: RprBool, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_apply_radial_impulse( + (element as *mut SoftBody).cast(), + center, + magnitude, + falloff_radius, + wake_up, + )) + }) +} + +pub(crate) unsafe fn native_soft_body_reset_plasticity(body: *mut RprSoftBody) -> RprStatus { + ffi(|| unsafe { + get_mut(body)?.0.reset_plasticity(); + Ok(()) + }) +} +/// Undo every permanent (plastic) deformation: edge rest lengths, dihedral rest angles, cell +/// rest shapes and particle rest positions return to their creation state. The particles stay +/// put and spring back elastically. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_reset_plasticity(handle: RprSoftBodyHandle) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_reset_plasticity( + (element as *mut SoftBody).cast(), + )) + }) +} + +pub(crate) unsafe fn native_soft_body_tear_edge(body: *mut RprSoftBody, index: usize) -> RprStatus { + ffi(|| unsafe { + let b = &mut get_mut(body)?.0; + ensure(index < b.edges().len(), "edge index out of range")?; + b.tear_edge(index); + Ok(()) + }) +} +/// Mark the indexed edge as torn. The tear is applied at the end of the next step and reported +/// by a tear event; use rpr_soft_body_tear to tear immediately. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_tear_edge( + handle: RprSoftBodyHandle, + index: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_tear_edge( + (element as *mut SoftBody).cast(), + index, + )) + }) +} + +pub(crate) unsafe fn native_soft_body_tear_cell(body: *mut RprSoftBody, index: usize) -> RprStatus { + ffi(|| unsafe { + let b = &mut get_mut(body)?.0; + ensure(index < b.cells().len(), "cell index out of range")?; + b.tear_cell(index); + Ok(()) + }) +} +/// Mark the indexed cell as torn. The tear is applied at the end of the next step and reported +/// by a tear event: no cell is removed, one of its particles splits along the plane +/// perpendicular to the cell's principal rest stretch. +/// @ingroup soft_bodies +#[rapier_export(soft_body)] +pub unsafe extern "C" fn rpr_soft_body_tear_cell( + handle: RprSoftBodyHandle, + index: usize, +) -> RprStatus { + let world = handle.world; + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.write()?; + let raw = access.raw(); + + let set: *mut RprSoftBodySet = std::ptr::addr_of_mut!((*raw).0.soft_bodies).cast(); + + let element = get_mut(set)?.0.get_mut(handle.raw()).ok_or_else(missing)?; + forward(native_soft_body_tear_cell( + (element as *mut SoftBody).cast(), + index, + )) + }) +} + +pub(crate) unsafe fn native_collider_soft_body( + object: *const RprCollider, + out: *mut RprSoftBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + output( + out, + get(object)? + .0 + .deformable_mesh_ref() + .map(|m| m.body.into()) + .unwrap_or_default(), + ) + }) +} +pub(crate) unsafe fn native_collider_set_get_soft_body( + set: *const RprColliderSet, + handle: RprColliderHandle, + out: *mut RprSoftBodyHandle, +) -> RprStatus { + ffi(|| unsafe { + let element = get(set)?.0.get(handle.raw()).ok_or_else(missing)?; + forward(native_collider_soft_body( + (element as *const Collider).cast(), + out, + )) + }) +} +/// Return the soft body owning this deformable collider (a soft-body collision mesh), or an invalid +/// handle for any other collider. +/// @ingroup colliders +#[rapier_export(collider)] +pub unsafe extern "C" fn rpr_collider_soft_body(handle: RprColliderHandle) -> RprSoftBodyHandle { + let world = handle.world; + ffi_world_value(world, |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { + handle.check_world(world)?; + let access = get(world)?.read()?; + let raw = access.raw(); + + forward(native_collider_set_get_soft_body( + std::ptr::addr_of!((*raw).0.colliders).cast(), + handle, + out, + )) + }) + }) +} +/// Return the soft body owning this deformable collider, or an invalid handle for any other +/// collider. Uses only the callback-scoped read context; never retain the context. +/// @ingroup callbacks +#[rapier_export(read_collider)] +pub unsafe extern "C" fn rpr_read_collider_soft_body( + context: *const RprReadContext, + handle: RprColliderHandle, +) -> RprSoftBodyHandle { + ffi_world_value( + unsafe { read_context_world(context) }, + |out: *mut RprSoftBodyHandle| { + ffi(|| unsafe { + handle.check_world(read_context_world(context))?; + forward(native_collider_set_get_soft_body( + get(context)?.colliders, + handle, + out, + )) + }) + }, + ) +} + +/// Return the number of soft bodies the torn body is in after the tear: the length of +/// rpr_soft_body_tear_event_bodies, and the exclusive bound of the piece_index of +/// rpr_soft_body_tear_event_piece_particles. It is 1 when nothing was split off. +/// @ingroup soft_bodies +#[rapier_export(soft_body_tear_event)] +pub unsafe extern "C" fn rpr_soft_body_tear_event_piece_count( + event: *const RprSoftBodyTearEvent, +) -> usize { + ffi_value(|out: *mut usize| ffi(|| unsafe { output(out, get(event)?.0.pieces.len().max(1)) })) +} + +/// Return the world setting documented by RprSoftRecoverySettings::authoredVelocityMargin. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_authored_velocity_margin(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .authored_velocity_margin; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::edgeSpeculation. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_edge_speculation(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.edge_speculation; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::invertedCellDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_inverted_cell_detection(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .inverted_cell_detection; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::selfCrossingDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_self_crossing_detection(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .self_crossing_detection; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::detectionMotionGating. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_detection_motion_gating(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .detection_motion_gating; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::crossBodyDetection. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_cross_body_detection(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.cross_body_detection; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::selfStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_self_stand_down(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.self_stand_down; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::crossBodyExpelGate. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_cross_body_expel_gate(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .cross_body_expel_gate; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::edgeStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_edge_stand_down(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.edge_stand_down; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::crossingRepulsion. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_crossing_repulsion(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.crossing_repulsion; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::crossingRepulsionGuide. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_crossing_repulsion_guide(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .crossing_repulsion_guide; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::crossingRepulsionSelfGuide. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_crossing_repulsion_self_guide( + world: *const RprWorld, +) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .crossing_repulsion_self_guide; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::recoveryPace. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_recovery_pace(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.recovery_pace; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapConstraints. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_constraints(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_constraints; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapRigid. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_rigid(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_rigid; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapSkipSelfTangled. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_skip_self_tangled(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .overlap_skip_self_tangled; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapEdgeStandDown. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_edge_stand_down(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .overlap_edge_stand_down; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapConstraintPace. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_constraint_pace(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .overlap_constraint_pace; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapPatchConstraints. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_patch_constraints(world: *const RprWorld) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .overlap_patch_constraints; + output( + out, + match value { + SoftPatchConstraints::Keep => RPR_SOFT_PATCH_CONSTRAINTS_KEEP, + SoftPatchConstraints::StandDown => RPR_SOFT_PATCH_CONSTRAINTS_STAND_DOWN, + SoftPatchConstraints::AlongNormal => RPR_SOFT_PATCH_CONSTRAINTS_ALONG_NORMAL, + }, + ) + }) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapPatchConstraints. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_patch_constraints( + world: *mut RprWorld, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + let value = match value { + RPR_SOFT_PATCH_CONSTRAINTS_KEEP => SoftPatchConstraints::Keep, + RPR_SOFT_PATCH_CONSTRAINTS_STAND_DOWN => SoftPatchConstraints::StandDown, + RPR_SOFT_PATCH_CONSTRAINTS_ALONG_NORMAL => SoftPatchConstraints::AlongNormal, + _ => return Err(invalid("invalid overlap patch constraints")), + }; + get_mut(parameters)? + .0 + .soft_bodies + .recovery + .overlap_patch_constraints = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapSkinVolume. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_skin_volume(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_skin_volume; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapKeptDepth. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_kept_depth(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_kept_depth; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapSelfRegions. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_self_regions(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_self_regions; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapNormalPush. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_normal_push(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_normal_push; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapMultiVolume. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_multi_volume(world: *const RprWorld) -> RprBool { + ffi_value(|out: *mut RprBool| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_multi_volume; + output(out, value as RprBool) + }) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapSplit. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_split(world: *const RprWorld) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_split; + output(out, value) + }) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapSplit. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_split( + world: *mut RprWorld, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(parameters)?.0.soft_bodies.recovery.overlap_split = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapPatience. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_patience(world: *const RprWorld) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.recovery.overlap_patience; + output(out, value) + }) + }) +} + +/// Set the world setting documented by RprSoftRecoverySettings::overlapPatience. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_set_overlap_patience( + world: *mut RprWorld, + value: u32, +) -> RprStatus { + ffi(|| unsafe { + let access = get(world)?.write()?; + let raw = access.raw(); + + let parameters: *mut NativeIntegrationParameters = + std::ptr::addr_of_mut!((*raw).0.integration_parameters).cast(); + + get_mut(parameters)?.0.soft_bodies.recovery.overlap_patience = value; + Ok(()) + }) +} + +/// Return the world setting documented by RprSoftRecoverySettings::overlapProgressMargin. +/// @ingroup soft_bodies +#[rapier_export] +pub unsafe extern "C" fn rpr_recovery_overlap_progress_margin(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)? + .0 + .soft_bodies + .recovery + .overlap_progress_margin; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftFemParameters::linearTolerance. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_linear_tolerance(world: *const RprWorld) -> RprReal { + ffi_value(|out: *mut RprReal| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.fem.linear_tolerance; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftFemParameters::maxLinearIterations. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_max_linear_iterations(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.fem.max_linear_iterations; + output(out, value) + }) + }) +} + +/// Return the world setting documented by RprSoftFemParameters::maxDenseDofs. +/// @ingroup soft_bodies +#[cfg(feature = "fem")] +#[rapier_export] +pub unsafe extern "C" fn rpr_fem_max_dense_dofs(world: *const RprWorld) -> usize { + ffi_value(|out: *mut usize| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + + let parameters: *const NativeIntegrationParameters = + std::ptr::addr_of!((*raw).0.integration_parameters).cast(); + let value = get(parameters)?.0.soft_bodies.fem.max_dense_dofs; + output(out, value) + }) + }) +} diff --git a/c/src/soft_extras_tests.rs b/c/src/soft_extras_tests.rs new file mode 100644 index 000000000..fa6b4a639 --- /dev/null +++ b/c/src/soft_extras_tests.rs @@ -0,0 +1,430 @@ +use crate::*; + +fn same_recipe(desc: &RprSoftBodyDesc, native: SoftBodyBuilder) { + let actual = unsafe { desc.raw().unwrap() }; + assert_eq!(format!("{actual:?}"), format!("{native:?}")); +} + +unsafe fn rope(world: *mut RprWorld, particles: usize) -> RprSoftBodyHandle { + let desc = rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), particles); + let handle = unsafe { rpr_insert_soft_body(world, &desc) }; + assert_eq!(rpr_last_status(), RPR_OK); + handle +} + +#[test] +fn particle_velocity_impulses_and_solver() { + unsafe { + let world = rpr_new_world(); + let handle = rope(world, 3); + let v = rpr_soft_body_particle_velocity(handle, 3); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(v.raw().unwrap(), Vector::ZERO); + assert_eq!(rpr_soft_body_solver(handle), RPR_SOFT_SOLVER_CONSTRAINTS); + assert_eq!(rpr_last_status(), RPR_OK); + + // Linear falloff from the point: 1, 0.5 and 0 for the particles at x = 0, 0.5 and 1. + let up = Vector::Y * 2.0; + assert_eq!( + rpr_soft_body_apply_impulse_at_point(handle, up.into(), Vector::ZERO.into(), 1.0, 1), + RPR_OK + ); + for (i, expected) in [2.0, 1.0, 0.0].into_iter().enumerate() { + let v = rpr_soft_body_particle_velocity(handle, i); + assert_eq!(rpr_last_status(), RPR_OK); + assert!((v.raw().unwrap() - Vector::Y * expected).length() < 1.0e-5); + } + for i in 0..3 { + rpr_soft_body_set_particle_velocity(handle, i, Vector::ZERO.into()); + } + // Away from the middle particle, which gets nothing; no falloff pushes fully. + let center = Vector::X * 0.5; + assert_eq!( + rpr_soft_body_apply_radial_impulse(handle, center.into(), 3.0, 0.0, 1), + RPR_OK + ); + let v: Vec<_> = (0..3) + .map(|i| rpr_soft_body_particle_velocity(handle, i).raw().unwrap()) + .collect(); + assert!((v[0] + Vector::X * 3.0).length() < 1.0e-5); + assert_eq!(v[1], Vector::ZERO); + assert!((v[2] - Vector::X * 3.0).length() < 1.0e-5); + assert_eq!( + rpr_soft_body_apply_radial_impulse(handle, center.into(), Real::NAN, 0.0, 1), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_soft_body_reset_plasticity(handle), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn deferred_and_immediate_tears_report_consistent_pieces() { + unsafe { + let world = rpr_new_world(); + let handle = rope(world, 8); + assert_eq!(rpr_soft_body_tear_edge(handle, 100), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_soft_body_tear_cell(handle, 0), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_soft_body_tear_edge(handle, 3), RPR_OK); + assert_eq!(rpr_soft_body_count(world), 1); + assert_eq!(rpr_step(world, std::ptr::null(), std::ptr::null()), RPR_OK); + assert_eq!(rpr_soft_body_count(world), 2); + + // An immediate tear splitting a rope in two pieces. + let handle = rope(world, 8); + let edge = 3u32; + let event = rpr_soft_body_tear(handle, &edge, 1, std::ptr::null(), 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(!event.is_null()); + let count = rpr_soft_body_tear_event_piece_count(event); + assert_eq!(count, 2); + assert_eq!( + rpr_soft_body_tear_event_bodies(event, std::ptr::null_mut(), 0), + count + ); + let n = rpr_soft_body_tear_event_piece_particles(event, 1, std::ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(n > 0); + rpr_soft_body_tear_event_piece_particles(event, 2, std::ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_soft_body_tear_event(event), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn tear_splitting_nothing_has_one_piece_with_every_particle() { + // A synthetic event: nothing was split off a body of 4 particles. + let body = SoftBodyHandle::invalid(); + let event = RprSoftBodyTearEvent( + SoftBodyTearEvent { + soft_body: body, + ..Default::default() + }, + std::ptr::null_mut(), + 4, + ); + unsafe { + assert_eq!(rpr_soft_body_tear_event_piece_count(&event), 1); + assert_eq!( + rpr_soft_body_tear_event_bodies(&event, std::ptr::null_mut(), 0), + 1 + ); + let mut particles = [0u32; 4]; + assert_eq!( + rpr_soft_body_tear_event_piece_particles(&event, 0, particles.as_mut_ptr(), 4), + 4 + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(particles, [0, 1, 2, 3]); + rpr_soft_body_tear_event_piece_particles(&event, 1, std::ptr::null_mut(), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + } +} + +#[test] +fn clusters_attachments_and_root_removal() { + unsafe { + let world = rpr_new_world(); + let handle = rope(world, 3); + let root = rpr_soft_body_root_body(handle); + // The root is also a cluster proxy, but removing it would remove the whole body. + assert_eq!(rpr_remove_rigid_body(root, 1), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_soft_body_count(world), 1); + assert_eq!(rpr_rigid_body_contains(root), 1); + + let particle = 2u32; + let cluster = rpr_soft_body_add_cluster(handle, &particle, 1); + assert_eq!(rpr_last_status(), RPR_OK); + let softness = RprSpringCoefficients { + natural_frequency: 5.0, + damping_ratio: 1.0, + }; + assert_eq!( + rpr_soft_body_set_cluster_edge_softness(handle, cluster, &softness), + RPR_OK + ); + assert_eq!( + rpr_soft_body_set_cluster_edge_softness(handle, cluster, std::ptr::null()), + RPR_OK + ); + assert_eq!( + rpr_soft_body_set_cluster_edge_softness(handle, 99, &softness), + RPR_INVALID_ARGUMENT + ); + // Removing a cluster proxy removes its cluster. + let proxy = rpr_soft_body_cluster_proxy(handle, cluster); + assert_eq!(rpr_remove_rigid_body(proxy, 1), RPR_OK); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_soft_body_count(world), 1); + + let desc = rpr_fixed_rigid_body_desc(); + let anchor = rpr_insert_rigid_body(world, &desc); + assert_eq!(rpr_soft_body_detach_particle(handle, 0), 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_soft_body_attach_particle(handle, 0, anchor), RPR_OK); + assert_eq!(rpr_soft_body_detach_particle(handle, 0), 1); + assert_eq!(rpr_soft_body_detach_particle(handle, 0), 0); + assert_eq!(rpr_soft_body_detach_particle(handle, 99), 0); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn deformable_colliders_report_their_soft_body() { + unsafe { + let world = rpr_new_world(); + #[cfg(feature = "dim2")] + let desc = rpr_disk_soft_body_desc(Vector::ZERO.into(), 1.0, 12); + #[cfg(feature = "dim3")] + let desc = rpr_sphere_soft_body_desc(Vector::ZERO.into(), 1.0, 1); + let handle = rpr_insert_soft_body(world, &desc); + assert_eq!(rpr_last_status(), RPR_OK); + let mut collider = RprColliderHandle::default(); + assert_eq!(rpr_soft_body_mesh_colliders(handle, &mut collider, 1), 1); + assert_eq!(rpr_collider_soft_body(collider), handle); + assert_eq!(rpr_last_status(), RPR_OK); + + let rigid = RprColliderDesc::default(); + let rigid = rpr_insert_collider_without_parent(world, &rigid); + assert_eq!(rpr_collider_soft_body(rigid), RprSoftBodyHandle::default()); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn recovery_and_fem_settings_round_trip() { + unsafe { + let world = rpr_new_world(); + let defaults = SoftRecoverySettings::default(); + assert_eq!( + rpr_recovery_overlap_patience(world), + defaults.overlap_patience + ); + assert_eq!( + rpr_recovery_edge_speculation(world), + defaults.edge_speculation as RprBool + ); + assert_eq!(rpr_recovery_recovery_pace(world), defaults.recovery_pace); + assert_eq!(rpr_recovery_set_overlap_split(world, 5), RPR_OK); + assert_eq!(rpr_recovery_overlap_split(world), 5); + assert_eq!(rpr_recovery_set_overlap_patience(world, 7), RPR_OK); + assert_eq!(rpr_recovery_overlap_patience(world), 7); + assert_eq!( + rpr_recovery_set_overlap_patch_constraints(world, RPR_SOFT_PATCH_CONSTRAINTS_KEEP), + RPR_OK + ); + assert_eq!( + rpr_recovery_overlap_patch_constraints(world), + RPR_SOFT_PATCH_CONSTRAINTS_KEEP + ); + assert_eq!( + rpr_recovery_set_overlap_patch_constraints(world, 3), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + rpr_recovery_overlap_patch_constraints(world), + RPR_SOFT_PATCH_CONSTRAINTS_KEEP + ); + #[cfg(feature = "fem")] + { + assert_eq!(rpr_fem_set_max_dense_dofs(world, 12), RPR_OK); + assert_eq!(rpr_fem_max_dense_dofs(world), 12); + assert_eq!(rpr_fem_set_linear_tolerance(world, 0.5), RPR_OK); + assert_eq!(rpr_fem_linear_tolerance(world), 0.5); + assert_eq!( + rpr_fem_max_linear_iterations(world), + SoftFemParameters::default().max_linear_iterations + ); + } + assert_eq!(rpr_free_world(world), RPR_OK); + } +} + +#[test] +fn new_recipes_match_native_builders() { + #[cfg(feature = "dim2")] + { + let points: Vec = [Vector::ZERO, Vector::X, Vector::ONE, Vector::Y] + .into_iter() + .map(Into::into) + .collect(); + let view = RprVectorView { + data: points.as_ptr(), + count: 4, + }; + same_recipe( + &rpr_polygon_soft_body_desc(view), + SoftBodyBuilder::polygon(vec![Vector::ZERO, Vector::X, Vector::ONE, Vector::Y]), + ); + let short = RprVectorView { + data: points.as_ptr(), + count: 2, + }; + assert!(unsafe { rpr_polygon_soft_body_desc(short).raw() }.is_err()); + + let triangles = [ + RprTriangle { a: 0, b: 1, c: 2 }, + RprTriangle { a: 0, b: 2, c: 3 }, + ]; + let mut desc = rpr_default_soft_body_desc(); + let status = unsafe { + rpr_soft_body_desc_set_trimesh( + &mut desc, + view, + RprTriangleView { + data: triangles.as_ptr(), + count: 2, + }, + ) + }; + assert_eq!(status, RPR_OK); + same_recipe( + &desc, + SoftBodyBuilder::trimesh( + vec![Vector::ZERO, Vector::X, Vector::ONE, Vector::Y], + vec![[0, 1, 2], [0, 2, 3]], + ) + .unwrap(), + ); + } + #[cfg(feature = "dim3")] + { + let spring = |f| RprSpringCoefficients { + natural_frequency: f, + damping_ratio: 1.0, + }; + let mut desc = rpr_cloth_anisotropic_soft_body_desc( + Vector::ZERO.into(), + Vector::X.into(), + Vector::Z.into(), + 3, + 4, + spring(10.0), + spring(20.0), + spring(5.0), + ); + let native = |desc: &RprSoftBodyDesc| { + SoftBodyBuilder::cloth_anisotropic( + Vector::ZERO, + Vector::X, + Vector::Z, + 3, + 4, + SpringCoefficients::new(10.0, 1.0), + SpringCoefficients::new(20.0, 1.0), + SpringCoefficients::new(5.0, 1.0), + ) + .oriented(desc.oriented.value != 0) + }; + desc.oriented = RprOptionalBool { + enabled: 1, + value: 0, + }; + same_recipe(&desc, native(&desc)); + // Per-edge overrides of the description apply after the generator's. + let extra = [RprSoftEdgeSoftness { + edge: 0, + softness: spring(1.0), + }]; + desc.edgeSoftness = RprSoftEdgeSoftnessView { + data: extra.as_ptr(), + count: 1, + }; + let built = unsafe { desc.raw().unwrap() }; + assert_eq!( + built.edge_softness.last(), + Some(&(0, SpringCoefficients::new(1.0, 1.0))) + ); + assert_eq!( + built.edge_softness.len(), + native(&desc).edge_softness.len() + 1 + ); + } +} + +#[test] +fn appended_descriptions_are_merged_and_sewn() { + let a = rpr_rope_soft_body_desc(Vector::ZERO.into(), Vector::X.into(), 3); + let mut b = rpr_rope_soft_body_desc(Vector::Y.into(), (Vector::X + Vector::Y).into(), 4); + b.translation = (Vector::Y * 2.0).into(); + let pieces = [b]; + let seams = [RprEdge { a: 0, b: 3 }, RprEdge { a: 2, b: 6 }]; + let mut desc = a; + unsafe { + assert_eq!( + rpr_soft_body_desc_set_appended( + &mut desc, + RprSoftBodyDescView { + data: pieces.as_ptr(), + count: 1, + }, + ), + RPR_OK + ); + assert_eq!( + rpr_soft_body_desc_set_added_edges( + &mut desc, + RprEdgeView { + data: seams.as_ptr(), + count: 2, + }, + ), + RPR_OK + ); + } + let native_b = + SoftBodyBuilder::rope(Vector::Y, Vector::X + Vector::Y, 4).translated(Vector::Y * 2.0); + #[cfg(feature = "dim3")] + let wire: Vec<[u32; 2]> = native_b.wire.iter().map(|w| [w[0] + 3, w[1] + 3]).collect(); + #[allow(unused_mut)] + let mut native = SoftBodyBuilder::rope(Vector::ZERO, Vector::X, 3) + .append(native_b) + .add_edges([[0, 3], [2, 6]]); + #[cfg(feature = "dim3")] + native.wire.extend(wire); + same_recipe(&desc, native); + + // A piece with its own particle mass keeps it. + let mut heavy = pieces; + heavy[0].particleMass = 3.0; + desc.appended.data = heavy.as_ptr(); + let built = unsafe { desc.raw().unwrap() }; + assert_eq!(built.masses, vec![1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0]); + + // Seams are validated against the merged particles, and nesting is rejected. + let bad = [RprEdge { a: 0, b: 7 }]; + let mut invalid = desc; + invalid.addedEdges = RprEdgeView { + data: bad.as_ptr(), + count: 1, + }; + assert!(unsafe { invalid.raw() }.is_err()); + let nested = [desc]; + let mut outer = a; + outer.appended = RprSoftBodyDescView { + data: nested.as_ptr(), + count: 1, + }; + assert!(unsafe { outer.raw() }.is_err()); +} + +#[cfg(feature = "dim3")] +#[test] +fn to_trimesh_ignores_subdivisions_of_shapes_without_them() { + unsafe { + let cuboid = rpr_cuboid_shared_shape(Vector::ONE.into()); + let mesh = rpr_shared_shape_to_trimesh(cuboid, 0, 0); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(!mesh.is_null()); + assert_eq!(rpr_free_tri_mesh_data(mesh), RPR_OK); + let ball = rpr_ball_shared_shape(1.0); + assert!(rpr_shared_shape_to_trimesh(ball, 8, 1).is_null()); + assert_eq!(rpr_last_status(), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_free_shared_shape(cuboid), RPR_OK); + assert_eq!(rpr_free_shared_shape(ball), RPR_OK); + } +} diff --git a/c/src/soft_recipes.rs b/c/src/soft_recipes.rs index 5c64519a1..84292d77e 100644 --- a/c/src/soft_recipes.rs +++ b/c/src/soft_recipes.rs @@ -81,6 +81,51 @@ pub extern "C" fn rpr_cloth_soft_body_desc( ..RprSoftBodyDesc::default() } } +/// Return a cloth recipe like rpr_cloth_soft_body_desc whose edges along du (warp), along dv (weft) +/// and diagonal (shear) get their own softness; the material's bendSoftness still applies to the +/// bending edges. The softness is stored in warpSoftness, weftSoftness and shearSoftness. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim3")] +#[rapier_export] +pub extern "C" fn rpr_cloth_anisotropic_soft_body_desc( + origin: RprVector, + du: RprVector, + dv: RprVector, + nu: usize, + nv: usize, + warp: RprSpringCoefficients, + weft: RprSpringCoefficients, + shear: RprSpringCoefficients, +) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_CLOTH_ANISOTROPIC, + a: origin, + du, + dv, + nx: nu, + ny: nv, + warpSoftness: warp, + weftSoftness: weft, + shearSoftness: shear, + ..RprSoftBodyDesc::default() + } +} +/// Return a closed polygon recipe from at least 3 counter-clockwise points: structural edges along +/// the boundary, bending edges between second neighbors, and area preservation. The points are +/// borrowed until preview/insertion. +/// Initializes a recipe without allocating. Geometry is validated during preview/insertion. +/// @ingroup soft_bodies +#[cfg(feature = "dim2")] +#[rapier_export] +pub extern "C" fn rpr_polygon_soft_body_desc(points: RprVectorView) -> RprSoftBodyDesc { + RprSoftBodyDesc { + kind: RPR_SOFT_DESC_POLYGON, + volumePreservation: 1, + positions: points, + ..RprSoftBodyDesc::default() + } +} /// Return a closed regular polygon recipe with the specified boundary particle count and area /// preservation. /// Initializes a recipe without allocating. Geometry is validated during preview/insertion. diff --git a/c/src/types.rs b/c/src/types.rs index c337f1fcd..33d78619d 100644 --- a/c/src/types.rs +++ b/c/src/types.rs @@ -44,6 +44,33 @@ pub const RPR_COMBINE_MULTIPLY: u32 = 2; /// @ingroup colliders /// Use the larger of the two material coefficients. pub const RPR_COMBINE_MAX: u32 = 3; +/// @ingroup colliders +/// Use the sum of the two material coefficients, clamped to [0, 1]. +pub const RPR_COMBINE_CLAMPED_SUM: u32 = 4; +/// @ingroup colliders +/// Use the geometric mean (square root of the product) of the two material coefficients. +pub const RPR_COMBINE_GEOMETRIC_MEAN: u32 = 5; +/// @ingroup colliders +/// Active collision type bit: contacts between two dynamic bodies. +pub const RPR_COLLISION_TYPES_DYNAMIC_DYNAMIC: u16 = 0b0000_0000_0000_0001; +/// @ingroup colliders +/// Active collision type bit: contacts between a dynamic and a kinematic body. +pub const RPR_COLLISION_TYPES_DYNAMIC_KINEMATIC: u16 = 0b0000_0000_0000_1100; +/// @ingroup colliders +/// Active collision type bit: contacts between a dynamic and a fixed body (or a collider without parent). +pub const RPR_COLLISION_TYPES_DYNAMIC_FIXED: u16 = 0b0000_0000_0000_0010; +/// @ingroup colliders +/// Active collision type bit: contacts between two kinematic bodies. +pub const RPR_COLLISION_TYPES_KINEMATIC_KINEMATIC: u16 = 0b1100_1100_0000_0000; +/// @ingroup colliders +/// Active collision type bit: contacts between a kinematic and a fixed body (or a collider without parent). +pub const RPR_COLLISION_TYPES_KINEMATIC_FIXED: u16 = 0b0010_0010_0000_0000; +/// @ingroup colliders +/// Active collision type bit: contacts between two fixed bodies (or colliders without parent). +pub const RPR_COLLISION_TYPES_FIXED_FIXED: u16 = 0b0000_0000_0010_0000; +/// @ingroup colliders +/// Default active collision types: dynamic-dynamic, dynamic-kinematic, and dynamic-fixed. +pub const RPR_COLLISION_TYPES_DEFAULT: u16 = 0b0000_0000_0000_1111; /// Cartesian vector with two or three components. /// @ingroup math @@ -344,8 +371,11 @@ pub struct RprBuildFeatures { pub simd_lanes: u32, /// Whether this library exposes Rapier's parallel execution and thread-pool APIs. pub parallel: RprBool, + /// Whether the library is built with enhanced-determinism: the simulation, and the math + /// functions such as rpr_sin, give bit-identical results on every platform. + pub enhanced_determinism: RprBool, } -/// Return profiling, SIMD width, and parallelism of the linked library. +/// Return profiling, SIMD width, parallelism, and determinism of the linked library. /// @ingroup errors #[rapier_export] pub extern "C" fn rpr_build_features() -> RprBuildFeatures { @@ -353,6 +383,7 @@ pub extern "C" fn rpr_build_features() -> RprBuildFeatures { profiling: cfg!(feature = "profiler") as RprBool, simd_lanes: rapier::math::SIMD_WIDTH as u32, parallel: cfg!(feature = "parallel") as RprBool, + enhanced_determinism: cfg!(feature = "enhanced-determinism") as RprBool, } } @@ -371,9 +402,14 @@ pub(crate) fn combine(value: u32) -> Result { 1 => Ok(CoefficientCombineRule::Min), 2 => Ok(CoefficientCombineRule::Multiply), 3 => Ok(CoefficientCombineRule::Max), + 4 => Ok(CoefficientCombineRule::ClampedSum), + 5 => Ok(CoefficientCombineRule::GeometricMean), _ => Err(invalid("unknown combine rule")), } } +pub(crate) fn combine_value(rule: CoefficientCombineRule) -> u32 { + rule as u32 +} /// Copyable non-owning handle: world pointer plus entity index and generation. /// The world must remain alive throughout every use. Copying does not retain it. @@ -659,6 +695,24 @@ pub const RPR_SOFT_SOLVER_CONSTRAINTS: u32 = 0; /// @ingroup soft_bodies /// Use the finite-element solver; requires RAPIER_FEM. pub const RPR_SOFT_SOLVER_FEM: u32 = 1; +/// @ingroup soft_bodies +/// Edge plastic flow (RprSoftBodyMaterial::edgePlasticFlow): both a squeeze and a stretch set. +pub const RPR_SOFT_EDGE_PLASTIC_FLOW_BOTH: u32 = 0; +/// @ingroup soft_bodies +/// Edge plastic flow: only a squeeze sets; a stretched edge springs back. +pub const RPR_SOFT_EDGE_PLASTIC_FLOW_COMPRESSION: u32 = 1; +/// @ingroup soft_bodies +/// Edge plastic flow: only a stretch sets; a squeezed edge springs back. +pub const RPR_SOFT_EDGE_PLASTIC_FLOW_TENSION: u32 = 2; +/// @ingroup soft_bodies +/// Overlap patch constraints (RprSoftRecoverySettings::overlapPatchConstraints): keep them. +pub const RPR_SOFT_PATCH_CONSTRAINTS_KEEP: u32 = 0; +/// @ingroup soft_bodies +/// Overlap patch constraints: stand them down inside the patch. +pub const RPR_SOFT_PATCH_CONSTRAINTS_STAND_DOWN: u32 = 1; +/// @ingroup soft_bodies +/// Overlap patch constraints: align them with the overlap normal. +pub const RPR_SOFT_PATCH_CONSTRAINTS_ALONG_NORMAL: u32 = 2; /// @ingroup joints /// Joint axis index for translation along local X. pub const RPR_AXIS_LIN_X: u32 = 0; @@ -788,22 +842,41 @@ pub const RPR_MULTIBODY_JOINTS_ARE_KINEMATIC: u8 = 1; /// Disable contacts between colliders of the inserted articulation. pub const RPR_MULTIBODY_DISABLE_SELF_CONTACTS: u8 = 2; /// @ingroup joints -/// Skip joints that would close a loop in the articulation. +/// Do not insert MJCF equality constraints (loop closures) as impulse joints. MJCF only: URDF +/// insertion rejects it. pub const RPR_MULTIBODY_SKIP_LOOP_CLOSURES: u8 = 4; /// @ingroup joints -/// Do not import joint motors into the articulation. +/// Do not import joint motors into the articulation. MJCF only: URDF insertion rejects it. pub const RPR_MULTIBODY_SKIP_JOINT_MOTORS: u8 = 8; /// @ingroup joints -/// Do not import joint limits into the articulation. +/// Do not import joint limits into the articulation. MJCF only: URDF insertion rejects it. pub const RPR_MULTIBODY_SKIP_JOINT_LIMITS: u8 = 16; /// @ingroup joints -/// Do not import joint springs into the articulation. +/// Do not import joint springs into the articulation. MJCF only: URDF insertion rejects it. pub const RPR_MULTIBODY_SKIP_JOINT_SPRINGS: u8 = 32; +/// @ingroup shapes +/// Compute the half-edge topology of the triangle mesh. +pub const RPR_TRIMESH_HALF_EDGE_TOPOLOGY: u32 = 1; +/// @ingroup shapes +/// Compute the connected components of the triangle mesh. +pub const RPR_TRIMESH_CONNECTED_COMPONENTS: u32 = 2; +/// @ingroup shapes +/// Delete the triangles breaking the half-edge topology. +pub const RPR_TRIMESH_DELETE_BAD_TOPOLOGY_TRIANGLES: u32 = 4; +/// @ingroup shapes +/// Treat the triangle mesh as oriented (outward normals) and compute its pseudo-normals. +pub const RPR_TRIMESH_ORIENTED: u32 = 8; /// @ingroup shapes /// Merge triangle-mesh vertices with identical positions. pub const RPR_TRIMESH_MERGE_DUPLICATE_VERTICES: u32 = 16; /// @ingroup shapes +/// Delete the triangles with a zero area. +pub const RPR_TRIMESH_DELETE_DEGENERATE_TRIANGLES: u32 = 32; +/// @ingroup shapes +/// Delete the triangles sharing their three vertices with another triangle. +pub const RPR_TRIMESH_DELETE_DUPLICATE_TRIANGLES: u32 = 64; +/// @ingroup shapes /// Correct contact normals at internal mesh edges; includes duplicate-vertex merging. pub const RPR_TRIMESH_FIX_INTERNAL_EDGES: u32 = 144; /// @ingroup shapes diff --git a/c/src/world_extras.rs b/c/src/world_extras.rs new file mode 100644 index 000000000..666d0646c --- /dev/null +++ b/c/src/world_extras.rs @@ -0,0 +1,648 @@ +//! World diagnostics and settings, controller axes, ABI feature checks, and deterministic math. +use crate::*; +use rapier::na::{ComplexField, RealField}; + +/// @ingroup controllers +/// Controller axis bit: translation along X. +pub const RPR_AXES_MASK_LIN_X: u32 = 1; +/// @ingroup controllers +/// Controller axis bit: translation along Y. +pub const RPR_AXES_MASK_LIN_Y: u32 = 2; +/// @ingroup controllers +/// Controller axis bit: translation along Z. +#[cfg(feature = "dim3")] +pub const RPR_AXES_MASK_LIN_Z: u32 = 4; +/// @ingroup controllers +/// Controller axis bit: rotation about X. +#[cfg(feature = "dim3")] +pub const RPR_AXES_MASK_ANG_X: u32 = 8; +/// @ingroup controllers +/// Controller axis bit: rotation about Y. +#[cfg(feature = "dim3")] +pub const RPR_AXES_MASK_ANG_Y: u32 = 16; +/// @ingroup controllers +/// Controller axis bit: rotation about Z (the only rotation axis in 2D). +pub const RPR_AXES_MASK_ANG_Z: u32 = 32; + +/// @ingroup errors +/// ABI feature bit: RAPIER_FEM, which changes the layout of RprIntegrationParameters. +pub const RPR_ABI_FEATURE_FEM: u32 = 1; +/// @ingroup errors +/// ABI feature bit: RAPIER_ROBOTICS (3D, f32 only), which declares the URDF/MJCF API. +pub const RPR_ABI_FEATURE_ROBOTICS: u32 = 2; +/// @ingroup errors +/// RPR_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. +#[cfg(all( + feature = "fem", + feature = "robotics", + feature = "dim3", + feature = "f32" +))] +pub const RPR_ABI_FEATURES: u32 = 3; +/// @ingroup errors +/// RPR_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. +#[cfg(all( + feature = "fem", + not(all(feature = "robotics", feature = "dim3", feature = "f32")) +))] +pub const RPR_ABI_FEATURES: u32 = 1; +/// @ingroup errors +/// RPR_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. +#[cfg(all( + not(feature = "fem"), + feature = "robotics", + feature = "dim3", + feature = "f32" +))] +pub const RPR_ABI_FEATURES: u32 = 2; +/// @ingroup errors +/// RPR_ABI_FEATURE_* bits selected by the defines of this header; pass it to CheckAbi. +#[cfg(all( + not(feature = "fem"), + not(all(feature = "robotics", feature = "dim3", feature = "f32")) +))] +pub const RPR_ABI_FEATURES: u32 = 0; + +/// Copy the rigid bodies quarantined by the most recent Step because their pose or velocity became +/// non-finite (NaN or infinite). Rapier disabled them, restored their last valid pose when known, +/// and zeroed their velocities and forces; re-enable them with RigidBody_SetEnabled once the cause +/// is fixed. The list is cleared at the start of every Step and may hold handles removed since. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_quarantined_rigid_bodies( + world: *const RprWorld, + buffer: *mut RprRigidBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + let values: Vec = (*raw) + .0 + .quarantine() + .bodies() + .iter() + .map(|h| (*h).into()) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Copy the colliders quarantined by the most recent Step because their own pose or shape became +/// non-finite, independently of their parent. Rapier disabled them; re-enable them with +/// Collider_SetEnabled once fixed. The list is cleared at the start of every Step and may hold +/// handles removed since. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_quarantined_colliders( + world: *const RprWorld, + buffer: *mut RprColliderHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + let values: Vec = (*raw) + .0 + .quarantine() + .colliders() + .iter() + .map(|h| (*h).into()) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Copy the soft bodies quarantined by the most recent Step because a particle position or velocity +/// became non-finite. Rapier disabled them and zeroed their velocities but left the non-finite +/// positions: fix them with SoftBody_SetParticlePosition before SoftBody_SetEnabled. The list is +/// cleared at the start of every Step and may hold handles removed since. +/// @see @ref output_buffers +/// @ingroup worlds +#[rapier_export] +pub unsafe extern "C" fn rpr_quarantined_soft_bodies( + world: *const RprWorld, + buffer: *mut RprSoftBodyHandle, + capacity: usize, +) -> usize { + unsafe { + ffi_world_array(world, buffer, capacity, |count: *mut usize| { + ffi(|| { + let access = get(world)?.read()?; + let raw = access.raw(); + let values: Vec = (*raw) + .0 + .quarantine() + .soft_bodies() + .iter() + .map(|h| (*h).into()) + .collect(); + copy_out(&values, buffer, capacity, count) + }) + }) + } +} + +/// Return the world setting documented by RprIntegrationParameters::frictionModel. +/// @ingroup worlds +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_friction_model(world: *const RprWorld) -> u32 { + ffi_value(|out: *mut u32| { + ffi(|| unsafe { + let access = get(world)?.read()?; + let raw = access.raw(); + let model = (*raw).0.integration_parameters.friction_model; + output(out, friction_model_value(model)) + }) + }) +} + +/// Set the world setting documented by RprIntegrationParameters::frictionModel. +/// @ingroup worlds +#[cfg(feature = "dim3")] +#[rapier_export] +pub unsafe extern "C" fn rpr_set_friction_model(world: *mut RprWorld, value: u32) -> RprStatus { + ffi(|| unsafe { + let model = friction_model(value)?; + let access = get(world)?.write()?; + (*access.raw()).0.integration_parameters.friction_model = model; + Ok(()) + }) +} + +/// Sine of an angle in radians, computed by Rapier's math backend. With enhanced-determinism +/// (see BuildFeatures), the result is identical on every platform. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_sin(x: RprReal) -> RprReal { + ComplexField::sin(x) +} +/// Cosine of an angle in radians, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_cos(x: RprReal) -> RprReal { + ComplexField::cos(x) +} +/// Tangent of an angle in radians, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_tan(x: RprReal) -> RprReal { + ComplexField::tan(x) +} +/// Arcsine in radians, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_asin(x: RprReal) -> RprReal { + ComplexField::asin(x) +} +/// Arccosine in radians, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_acos(x: RprReal) -> RprReal { + ComplexField::acos(x) +} +/// Angle in radians of the point (x, y), in [-pi, pi], computed by Rapier's math backend. See +/// rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_atan2(y: RprReal, x: RprReal) -> RprReal { + RealField::atan2(y, x) +} +/// Exponential e^x, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_exp(x: RprReal) -> RprReal { + ComplexField::exp(x) +} +/// Natural logarithm, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_ln(x: RprReal) -> RprReal { + ComplexField::ln(x) +} +/// base raised to the power exponent, computed by Rapier's math backend. See rpr_sin. +/// @ingroup math +#[rapier_export] +pub extern "C" fn rpr_powf(base: RprReal, exponent: RprReal) -> RprReal { + ComplexField::powf(base, exponent) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ffi::c_void, ptr}; + + unsafe fn insert_body( + world: *mut RprWorld, + desc: RprRigidBodyDesc, + at: Vector, + ) -> RprRigidBodyHandle { + let mut desc = desc; + desc.position.translation = at.into(); + let handle = unsafe { rpr_insert_rigid_body(world, &desc) }; + assert_eq!(rpr_last_status(), RPR_OK); + handle + } + + unsafe fn insert_collider( + body: RprRigidBodyHandle, + desc: RprColliderDesc, + ) -> RprColliderHandle { + let handle = unsafe { rpr_insert_collider(body, &desc) }; + assert_eq!(rpr_last_status(), RPR_OK); + handle + } + + #[test] + fn quarantine_reports_the_last_step_only() { + unsafe { + let world = rpr_new_world(); + let body = insert_body(world, rpr_dynamic_rigid_body_desc(), Vector::ZERO); + insert_collider(body, rpr_ball_collider_desc(0.5)); + { + // The C API rejects non-finite velocities, so poison the body natively. + let access = get(world).unwrap().write().unwrap(); + let bodies = &mut (*access.raw()).0.bodies; + bodies[body.raw()].set_linvel(Vector::splat(Real::NAN), true); + } + assert_eq!(rpr_quarantined_rigid_bodies(world, ptr::null_mut(), 0), 0); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + let mut buffer = [RprRigidBodyHandle::default(); 2]; + assert_eq!( + rpr_quarantined_rigid_bodies(world, buffer.as_mut_ptr(), 2), + 1 + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert_eq!(buffer[0], body); + assert_eq!(rpr_quarantined_colliders(world, ptr::null_mut(), 0), 0); + assert_eq!(rpr_quarantined_soft_bodies(world, ptr::null_mut(), 0), 0); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + assert_eq!(rpr_quarantined_rigid_bodies(world, ptr::null_mut(), 0), 0); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn solver_iterations_are_validated_consistently() { + unsafe { + let world = rpr_new_world(); + let mut params = rpr_integration_parameters(world); + params.numSolverIterations = 0; + assert_eq!( + rpr_set_integration_parameters(world, ¶ms), + RPR_INVALID_ARGUMENT + ); + params.numSolverIterations = 4; + params.numInternalPgsIterations = 0; + assert_eq!( + rpr_set_integration_parameters(world, ¶ms), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + rpr_set_num_solver_iterations(world, 0), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + rpr_set_num_internal_pgs_iterations(world, 0), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[cfg(feature = "dim3")] + #[test] + fn friction_model_round_trips() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_friction_model(world), RPR_FRICTION_MODEL_SIMPLIFIED); + assert_eq!( + rpr_set_friction_model(world, RPR_FRICTION_MODEL_COULOMB), + RPR_OK + ); + assert_eq!(rpr_friction_model(world), RPR_FRICTION_MODEL_COULOMB); + assert_eq!( + rpr_integration_parameters(world).frictionModel, + RPR_FRICTION_MODEL_COULOMB + ); + assert_eq!(rpr_set_friction_model(world, 2), RPR_INVALID_ARGUMENT); + assert_eq!(rpr_friction_model(world), RPR_FRICTION_MODEL_COULOMB); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[test] + fn check_abi_rejects_a_feature_define_mismatch() { + unsafe { + let sizes = ( + size_of::(), + size_of::(), + size_of::(), + ); + let dim = rapier::math::DIM as u32; + let check = + |features| rpr_check_abi(RPR_ABI_VERSION, dim, sizes.0, sizes.1, sizes.2, features); + assert_eq!(check(RPR_ABI_FEATURES), RPR_OK); + assert_eq!( + check(RPR_ABI_FEATURES ^ RPR_ABI_FEATURE_FEM), + RPR_INVALID_ARGUMENT + ); + assert_eq!( + check(RPR_ABI_FEATURES ^ RPR_ABI_FEATURE_ROBOTICS), + RPR_INVALID_ARGUMENT + ); + assert_eq!(check(RPR_ABI_FEATURES | 64), RPR_INVALID_ARGUMENT); + assert_eq!( + RPR_ABI_FEATURES & RPR_ABI_FEATURE_FEM != 0, + cfg!(feature = "fem") + ); + } + } + + #[test] + fn math_functions_match_the_simulation_backend() { + let x: Real = 0.7; + assert_eq!(rpr_sin(x), ComplexField::sin(x)); + assert_eq!(rpr_cos(x), ComplexField::cos(x)); + assert_eq!(rpr_tan(x), ComplexField::tan(x)); + assert_eq!(rpr_asin(x), ComplexField::asin(x)); + assert_eq!(rpr_acos(x), ComplexField::acos(x)); + assert_eq!(rpr_atan2(1.0, -1.0), 3.0 * Real::frac_pi_4()); + assert_eq!(rpr_exp(0.0), 1.0); + assert_eq!(rpr_ln(1.0), 0.0); + assert_eq!(rpr_powf(2.0, 10.0), 1024.0); + } + + #[test] + fn pd_controller_defaults_and_correction() { + unsafe { + let pd = rpr_default_pd_controller(); + assert!(pd.raw().unwrap() == rapier::control::PdController::default()); + let mut bodies = RprRigidBodySet(RigidBodySet::new()); + let handle = bodies.0.insert(RigidBodyBuilder::dynamic()); + let mut only_x = pd; + only_x.axes = RPR_AXES_MASK_LIN_X; + let mut linear = RprVector::default(); + let mut angular = RprAngVector::default(); + let target = Pose::from_translation(Vector::X + Vector::Y); + let zero = angular_out(AngVector::default()); + assert_eq!( + native_pd_controller_rigid_body_correction( + &only_x, + &bodies, + handle.into(), + target.into(), + Vector::ZERO.into(), + zero, + &mut linear, + &mut angular, + ), + RPR_OK + ); + assert!(linear.x > 0.0); + assert_eq!(linear.y, 0.0); + let mut invalid = pd; + invalid.axes = 64; + assert_eq!( + native_pd_controller_rigid_body_correction( + &invalid, + &bodies, + handle.into(), + target.into(), + Vector::ZERO.into(), + zero, + &mut linear, + &mut angular, + ), + RPR_INVALID_ARGUMENT + ); + } + } + + #[test] + fn pid_axes_and_integrals() { + unsafe { + let pid = rpr_new_pid_controller(); + let all = RPR_AXES_MASK_LIN_X | RPR_AXES_MASK_LIN_Y | RPR_AXES_MASK_ANG_Z; + #[cfg(feature = "dim3")] + let all = all | RPR_AXES_MASK_LIN_Z | RPR_AXES_MASK_ANG_X | RPR_AXES_MASK_ANG_Y; + assert_eq!(rpr_pid_controller_axes(pid), all); + assert_eq!( + rpr_pid_controller_set_axes(pid, RPR_AXES_MASK_LIN_Y), + RPR_OK + ); + assert_eq!(rpr_pid_controller_axes(pid), RPR_AXES_MASK_LIN_Y); + #[cfg(feature = "dim2")] + assert_eq!(rpr_pid_controller_set_axes(pid, 4), RPR_INVALID_ARGUMENT); + (*pid).0.lin_integral = Vector::X; + assert_eq!(rpr_pid_controller_reset_integrals(pid), RPR_OK); + assert_eq!((*pid).0.lin_integral, Vector::ZERO); + assert_eq!(rpr_free_pid_controller(pid), RPR_OK); + } + } + + #[test] + fn character_controller_getters() { + unsafe { + let c = rpr_new_kinematic_character_controller(); + let up = rpr_kinematic_character_controller_up(c); + assert_eq!(up.y, 1.0); + let offset = rpr_kinematic_character_controller_offset(c); + assert_eq!((offset.value, offset.relative), (0.01, 1)); + let step = rpr_kinematic_character_controller_autostep(c); + assert_eq!(step.enabled, 0); + assert_eq!(step.include_dynamic_bodies, 1); + let height = RprCharacterLength { + value: 0.3, + relative: 0, + }; + let width = RprCharacterLength { + value: 0.2, + relative: 1, + }; + assert_eq!( + rpr_kinematic_character_controller_set_autostep(c, 1, height, width, 0), + RPR_OK + ); + let step = rpr_kinematic_character_controller_autostep(c); + assert_eq!((step.enabled, step.include_dynamic_bodies), (1, 0)); + assert_eq!((step.max_height.value, step.max_height.relative), (0.3, 0)); + assert_eq!((step.min_width.value, step.min_width.relative), (0.2, 1)); + assert_eq!( + rpr_kinematic_character_controller_normal_nudge_factor(c), + 1.0e-4 + ); + assert_eq!( + rpr_kinematic_character_controller_set_normal_nudge_factor(c, 0.01), + RPR_OK + ); + assert_eq!( + rpr_kinematic_character_controller_normal_nudge_factor(c), + 0.01 + ); + assert_eq!( + rpr_kinematic_character_controller_set_normal_nudge_factor(c, -1.0), + RPR_INVALID_ARGUMENT + ); + assert_eq!(rpr_free_kinematic_character_controller(c), RPR_OK); + } + } + + unsafe extern "C" fn reject_all( + calls: *mut c_void, + read: *const RprReadContext, + collider: RprColliderHandle, + ) -> RprBool { + unsafe { + // The world is locked for writing, but its read context stays usable. + let _ = rpr_read_collider_translation(read, collider); + assert_eq!(rpr_last_status(), RPR_OK); + *calls.cast::() += 1; + } + 0 + } + + #[test] + fn character_impulses_honor_the_query_predicate() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_set_gravity(world, Vector::ZERO.into()), RPR_OK); + let box_body = insert_body(world, rpr_dynamic_rigid_body_desc(), Vector::X * 2.0); + insert_collider( + box_body, + rpr_cuboid_collider_desc((Vector::ONE * 0.5).into()), + ); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + + let shape = rpr_ball_shared_shape(0.5); + let c = rpr_new_kinematic_character_controller(); + let movement = rpr_kinematic_character_controller_move_shape( + world, + ptr::null(), + c, + 1.0 / 60.0, + shape, + Pose::IDENTITY.into(), + (Vector::X * 2.0).into(), + ); + assert_eq!(rpr_last_status(), RPR_OK); + assert!(movement.translation.x < 1.5); + + let mut calls = 0usize; + let mut options = rpr_default_query_options(); + options.predicate = Some(reject_all); + options.userData = (&mut calls as *mut usize).cast(); + assert_eq!( + rpr_kinematic_character_controller_solve_character_collision_impulses( + c, + shape, + 1.0 / 60.0, + 1.0, + &options + ), + RPR_OK + ); + assert_eq!(calls, 1); + assert_eq!(rpr_rigid_body_linvel(box_body).x, 0.0); + assert_eq!( + rpr_kinematic_character_controller_solve_character_collision_impulses( + c, + shape, + 1.0 / 60.0, + 1.0, + ptr::null() + ), + RPR_OK + ); + assert!(rpr_rigid_body_linvel(box_body).x > 0.0); + assert_eq!(rpr_free_kinematic_character_controller(c), RPR_OK); + assert_eq!(rpr_free_shared_shape(shape), RPR_OK); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } + + #[cfg(feature = "dim3")] + unsafe extern "C" fn reject_collider( + rejected: *mut c_void, + _read: *const RprReadContext, + collider: RprColliderHandle, + ) -> RprBool { + unsafe { (collider != *rejected.cast::()) as RprBool } + } + + #[cfg(feature = "dim3")] + #[test] + fn vehicle_keeps_the_user_exclusions_and_the_chassis_exclusion() { + unsafe { + let world = rpr_new_world(); + assert_eq!(rpr_set_gravity(world, Vector::ZERO.into()), RPR_OK); + let ground_body = insert_body(world, rpr_fixed_rigid_body_desc(), Vector::ZERO); + let ground = insert_collider( + ground_body, + rpr_cuboid_collider_desc(Vector::new(10.0, 0.1, 10.0).into()), + ); + let bump_body = insert_body(world, rpr_fixed_rigid_body_desc(), Vector::Y * 0.3); + let bump = insert_collider( + bump_body, + rpr_cuboid_collider_desc(Vector::new(0.2, 0.05, 0.2).into()), + ); + let chassis = insert_body(world, rpr_dynamic_rigid_body_desc(), Vector::Y); + insert_collider(chassis, rpr_ball_collider_desc(0.3)); + assert_eq!(rpr_step(world, ptr::null(), ptr::null()), RPR_OK); + + let vehicle = rpr_new_dynamic_ray_cast_vehicle_controller(chassis); + let tuning = rpr_default_wheel_tuning(); + rpr_dynamic_ray_cast_vehicle_controller_add_wheel( + vehicle, + Vector::ZERO.into(), + (-Vector::Y).into(), + Vector::X.into(), + 1.0, + 0.1, + &tuning, + ); + assert_eq!(rpr_last_status(), RPR_OK); + let ground_object = |options: *const RprQueryOptions| { + assert_eq!( + rpr_dynamic_ray_cast_vehicle_controller_update_vehicle( + vehicle, + 1.0 / 60.0, + options + ), + RPR_OK + ); + let mut wheel = RprWheelState::default(); + assert_eq!( + rpr_dynamic_ray_cast_vehicle_controller_wheels(vehicle, &mut wheel, 1), + 1 + ); + wheel.ground_object + }; + assert_eq!(ground_object(ptr::null()), bump); + let mut options = rpr_default_query_options(); + options.filter.exclude_rigid_body = bump_body; + assert_eq!(ground_object(&options), ground); + let mut rejected = bump; + let mut options = rpr_default_query_options(); + options.predicate = Some(reject_collider); + options.userData = (&mut rejected as *mut RprColliderHandle).cast(); + assert_eq!(ground_object(&options), ground); + assert_eq!( + rpr_free_dynamic_ray_cast_vehicle_controller(vehicle), + RPR_OK + ); + assert_eq!(rpr_free_world(world), RPR_OK); + } + } +} diff --git a/c/src/world_queries.rs b/c/src/world_queries.rs index 896d8d7ef..db0c998cb 100644 --- a/c/src/world_queries.rs +++ b/c/src/world_queries.rs @@ -223,18 +223,7 @@ pub unsafe extern "C" fn rpr_cast_shape( let (h, r) = q .cast_shape(&p, v, &*get(shape)?.0, o) .ok_or((RPR_NOT_FOUND, "shape missed".into()))?; - output( - out, - RprShapeCastHit { - collider: h.into(), - time_of_impact: r.time_of_impact, - witness1: r.witness1.into(), - witness2: r.witness2.into(), - normal1: r.normal1.into(), - normal2: r.normal2.into(), - status: r.status as u32, - }, - ) + output(out, shape_cast_hit(h, r)) }) }) }) diff --git a/c/src/world_tests.rs b/c/src/world_tests.rs index e47851474..35c901698 100644 --- a/c/src/world_tests.rs +++ b/c/src/world_tests.rs @@ -208,16 +208,12 @@ fn removing_a_body_can_preserve_its_colliders() { assert_eq!(rpr_last_status(), RPR_OK); let collider = rpr_insert_collider(body, &shape); assert_eq!(rpr_last_status(), RPR_OK); - let mut removed = rpr_remove_rigid_body(body, 0); - assert_eq!(rpr_last_status(), RPR_OK); - assert_eq!(removed, 1); + assert_eq!(rpr_remove_rigid_body(body, 0), RPR_OK); let parent = rpr_collider_parent(collider); assert_eq!(rpr_last_status(), RPR_OK); assert_eq!(parent, RprRigidBodyHandle::default()); assert_eq!(rpr_rigid_body_validate_handle(body), RPR_INVALID_HANDLE); - removed = rpr_remove_rigid_body(body, 1); - assert_eq!(rpr_last_status(), RPR_OK); - assert_eq!(removed, 0); + assert_eq!(rpr_remove_rigid_body(body, 1), RPR_INVALID_HANDLE); assert_eq!(rpr_free_world(world), RPR_OK); } } diff --git a/c/testbed/examples2d/utils/character.h b/c/testbed/examples2d/utils/character.h index 18adb2e95..ad9c13522 100644 --- a/c/testbed/examples2d/utils/character.h +++ b/c/testbed/examples2d/utils/character.h @@ -102,7 +102,7 @@ static void updateCharacter(Testbed *viewer, R2World *world, CharacterControlMod tbBodyColor(viewer, characterHandle, movement.grounded ? .1f : .8f, movement.grounded ? .8f : .1f, .1f, 1); r2KinematicCharacterController_SolveCharacterCollisionImpulses(controller, shape, dt, - mass, &filter); + mass, &query); r2FreeSharedShape(shape); r2RigidBody_SetNextKinematicTranslation(characterHandle, diff --git a/c/testbed/examples3d/utils/character.h b/c/testbed/examples3d/utils/character.h index df30869fb..a2b47470b 100644 --- a/c/testbed/examples3d/utils/character.h +++ b/c/testbed/examples3d/utils/character.h @@ -105,7 +105,7 @@ static void updateCharacter(Testbed *viewer, R3World *world, CharacterControlMod tbBodyColor(viewer, characterHandle, movement.grounded ? .1f : .8f, movement.grounded ? .8f : .1f, .1f, 1); r3KinematicCharacterController_SolveCharacterCollisionImpulses(controller, shape, dt, - mass, &filter); + mass, &query); r3FreeSharedShape(shape); r3RigidBody_SetNextKinematicTranslation(characterHandle, diff --git a/c/testbed/examples3d/vehicle_controller3.c b/c/testbed/examples3d/vehicle_controller3.c index e1cdb9458..8c5f244b9 100644 --- a/c/testbed/examples3d/vehicle_controller3.c +++ b/c/testbed/examples3d/vehicle_controller3.c @@ -91,12 +91,12 @@ void tbVehicleController3(Testbed *testbed) { r3DynamicRayCastVehicleController_SetWheelControls(vehicle, i, steeringAngle, engineForce, 0); } - R3QueryFilter filter = r3DefaultQueryFilter(); - filter.flags = R3_QUERY_EXCLUDE_DYNAMIC; - filter.exclude_rigid_body = vehicleHandle; + /* The chassis colliders are always excluded by the controller. */ + R3QueryOptions options = r3DefaultQueryOptions(); + options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC; R3Real dt = r3TimeStep(world); - r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, dt, &filter); + r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, dt, &options); r3Step(world, NULL, NULL); } } diff --git a/c/testbed/grab.c b/c/testbed/grab.c index 3ee82e32b..5ceafa86e 100644 --- a/c/testbed/grab.c +++ b/c/testbed/grab.c @@ -39,8 +39,9 @@ RAPIER_TYPE(Status) tbGrabRelease(Testbed *t, TbGrab *grab) { if (contains(t, pulled)) { TRY(RAPIER_FN(RigidBody_WakeUp)(pulled, 1)); } - RAPIER_FN(RemoveRigidBody)(grab->mouseBody, 1); - TRY(RAPIER_FN(LastStatus)()); + if (contains(t, grab->mouseBody)) { /* A scene may have removed it already. */ + TRY(RAPIER_FN(RemoveRigidBody)(grab->mouseBody, 1)); + } if (grab->soft) { RAPIER_TYPE(RigidBodyHandle) proxies[] = {grab->body, pulled}; for (size_t i = 0; i < TB_COUNT(proxies); ++i) { @@ -53,8 +54,15 @@ RAPIER_TYPE(Status) tbGrabRelease(Testbed *t, TbGrab *grab) { isFrame = RAPIER_FN(RigidBody_IsSoftFrame)(proxies[i]); TRY(RAPIER_FN(LastStatus)()); if (isFrame) { - RAPIER_FN(RemoveRigidBody)(proxies[i], 1); + /* A soft-body root cannot be removed (a tear may have made the grab cluster one). */ + RAPIER_TYPE(SoftBodyHandle) soft = RAPIER_FN(RigidBody_SoftBody)(proxies[i]); + TRY(RAPIER_FN(LastStatus)()); + RAPIER_TYPE(RigidBodyHandle) root = RAPIER_FN(SoftBody_RootBody)(soft); TRY(RAPIER_FN(LastStatus)()); + if (root.index == proxies[i].index && root.generation == proxies[i].generation) { + continue; + } + TRY(RAPIER_FN(RemoveRigidBody)(proxies[i], 1)); } } } diff --git a/c/testbed/gui.c b/c/testbed/gui.c index bd6a87183..900fa5b9c 100644 --- a/c/testbed/gui.c +++ b/c/testbed/gui.c @@ -162,7 +162,7 @@ static int abi(void) { RAPIER_TYPE(Status) s = RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), - sizeof(RAPIER_TYPE(Pose))); + sizeof(RAPIER_TYPE(Pose)), RAPIER_CONST(ABI_FEATURES)); if (s) { fprintf(stderr, "ABI mismatch: %s\n", RAPIER_FN(LastError)()); } diff --git a/c/testbed/tests/grab.c b/c/testbed/tests/grab.c index e752800cf..f7bd6785c 100644 --- a/c/testbed/tests/grab.c +++ b/c/testbed/tests/grab.c @@ -83,9 +83,7 @@ static void rigidDrag(void) { pointCursor(&t, position); CHECK(tbGrabBegin(&t, &grab, (RAPIER_TYPE(Real))0.1)); assert(grab.active); - RAPIER_TYPE(Bool) removed = RAPIER_FN(RemoveRigidBody)(picked, 1); - CHECK(RAPIER_FN(LastStatus)()); - assert(removed); + CHECK(RAPIER_FN(RemoveRigidBody)(picked, 1)); CHECK(tbGrabUpdate(&t, &grab, V(0, 0, -1))); assert(!grab.active); checkCounts(&t, 2, 0); diff --git a/c/tests/handles.c b/c/tests/handles.c index 8dfcfad95..f49a268ec 100644 --- a/c/tests/handles.c +++ b/c/tests/handles.c @@ -95,9 +95,7 @@ int main(void) { assert(RAPIER_FN(RigidBody_SetTranslation)(root, position, 1) == RAPIER_CONST(INVALID_ARGUMENT)); - RAPIER_TYPE(Bool) removed = RAPIER_FN(RemoveRigidBody)(body, 1); - OK(RAPIER_FN(LastStatus)()); - assert(removed); + OK(RAPIER_FN(RemoveRigidBody)(body, 1)); RAPIER_TYPE(RigidBodyHandle) replacement = RAPIER_FN(InsertRigidBody)(world, &desc); OK(RAPIER_FN(LastStatus)()); assert(replacement.index == body.index && replacement.generation != body.generation); diff --git a/c/tests/integration.c b/c/tests/integration.c index daa236b0f..7308e25f5 100644 --- a/c/tests/integration.c +++ b/c/tests/integration.c @@ -124,9 +124,16 @@ static void test_world_pipeline(void) { int main(void) { OK(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), - sizeof(RAPIER_TYPE(Pose)))); + sizeof(RAPIER_TYPE(Pose)), RAPIER_CONST(ABI_FEATURES))); EXPECT(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), 99, sizeof(RAPIER_TYPE(Real)), - sizeof(RAPIER_TYPE(Vector)), sizeof(RAPIER_TYPE(Pose))), + sizeof(RAPIER_TYPE(Vector)), sizeof(RAPIER_TYPE(Pose)), + RAPIER_CONST(ABI_FEATURES)), + RAPIER_CONST(INVALID_ARGUMENT)); + /* A RAPIER_FEM define that differs from the library changes IntegrationParameters. */ + EXPECT(RAPIER_FN(CheckAbi)(RAPIER_CONST(ABI_VERSION), RAPIER_CONST(DIMENSION), + sizeof(RAPIER_TYPE(Real)), sizeof(RAPIER_TYPE(Vector)), + sizeof(RAPIER_TYPE(Pose)), + RAPIER_CONST(ABI_FEATURES) ^ RAPIER_CONST(ABI_FEATURE_FEM)), RAPIER_CONST(INVALID_ARGUMENT)); const char *version = RAPIER_FN(Version)(); assert(version && strstr(version, "+c.")); @@ -141,6 +148,7 @@ int main(void) { #endif RAPIER_TYPE(BuildFeatures) features = RAPIER_FN(BuildFeatures)(); assert(features.simd_lanes == 4 || features.simd_lanes == 8); + assert(features.enhanced_determinism == 0 || features.enhanced_determinism == 1); #ifdef RAPIER_EXPECTED_SIMD_LANES assert(features.simd_lanes == RAPIER_EXPECTED_SIMD_LANES); #endif @@ -439,8 +447,8 @@ int main(void) { count = RAPIER_FN(DebugRender)(world, 1, NULL, 0); OK(RAPIER_FN(LastStatus)()); assert(count > 0); - RAPIER_FN(RemoveRigidBody)(ball, 1); - OK(RAPIER_FN(LastStatus)()); + OK(RAPIER_FN(RemoveRigidBody)(ball, 1)); + EXPECT(RAPIER_FN(RemoveRigidBody)(ball, 1), RAPIER_CONST(INVALID_HANDLE)); EXPECT(RAPIER_FN(RigidBody_ValidateHandle)(ball), RAPIER_CONST(INVALID_HANDLE)); EXPECT(RAPIER_FN(Collider_ValidateHandle)(ball_collider), RAPIER_CONST(INVALID_HANDLE)); RAPIER_TYPE(RigidBodyHandle) replacement = add_body(world, RAPIER_CONST(DYNAMIC), 3); diff --git a/c/tools/generate-header.py b/c/tools/generate-header.py index 61e467a2e..987c9e6ff 100644 --- a/c/tools/generate-header.py +++ b/c/tools/generate-header.py @@ -53,7 +53,7 @@ for source in sources: for name, native in re.findall(r"pub struct (Rpr\w+)\(pub\(crate\) (\w+)\)", source.read_text()): text = text.replace(f"typedef {native} {name};", f"typedef struct {name} {name};") - for name in ["RprPairFilter", "RprModifyContacts", "RprErrorCallback", "RprModifyContactContext", "RprIkJointCanMove", "RprQueryPredicate"]: + for name in ["RprPairFilter", "RprModifyContacts", "RprErrorCallback", "RprModifyContactContext", "RprIkJointCanMove", "RprQueryPredicate", "RprCollisionEventCallback", "RprContactForceEventCallback"]: text = text.replace(f"(*{name})", f"(RAPIER_CALL *{name})") text = text.replace("\n#endif\n ;", ";\n#endif") diff --git a/c/tools/test-native.py b/c/tools/test-native.py index d0150cc80..aede9c0c0 100644 --- a/c/tools/test-native.py +++ b/c/tools/test-native.py @@ -139,7 +139,7 @@ def link_flags(libraries, linkage): source.write_text(f'''#include "rapier.h" int run{dim}(void) {{ R{dim}World *world = 0; - if (r{dim}CheckAbi(R{dim}_ABI_VERSION, {dim}, sizeof(R{dim}Real), sizeof(R{dim}Vector), sizeof(R{dim}Pose))) return 1; + if (r{dim}CheckAbi(R{dim}_ABI_VERSION, {dim}, sizeof(R{dim}Real), sizeof(R{dim}Vector), sizeof(R{dim}Pose), R{dim}_ABI_FEATURES)) return 1; world = r{dim}NewWorld(); if (r{dim}LastStatus()) return 2; R{dim}Status status = r{dim}Step(world, 0, 0); From fc23c670e6f08f523785203178cbb2ceadea75a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Thu, 24 Sep 2026 18:17:54 +0200 Subject: [PATCH 2/3] feat(python): add missing bindings --- CHANGELOG.md | 2 + crates/rapier3d-mjcf/src/hooks.rs | 10 + python/README.md | 14 +- python/docs/api/geometry.rst | 2 + python/docs/api/joints.rst | 1 + python/docs/api/loaders.rst | 11 +- python/docs/api/math.rst | 34 +- python/docs/api/pipeline.rst | 12 +- python/docs/api/soft_bodies.rst | 32 +- python/docs/api/world.rst | 7 +- python/docs/changelog.rst | 146 +- python/examples/character/stairs.py | 2 +- python/examples/vehicle/drive.py | 4 +- python/rapier-py-3d/Cargo.toml | 7 + python/rapier-py-3d/build.rs | 3 + .../rapier-py-3d/python/rapier3d/__init__.py | 22 +- .../rapier-py-3d/python/rapier3d/__init__.pyi | 405 +++- .../python/rapier3d/_event_handler.py | 31 +- .../python/rapier3d/_rapier3d.pyi | 1516 +++++++++++++-- .../python/rapier3d/loaders/mjcf.py | 4 + .../python/rapier3d/loaders/urdf.py | 2 + python/rapier-py-3d/python/rapier3d/math.py | 42 +- python/rapier-py-3d/python/rapier3d/math.pyi | 19 +- python/rapier-py-3d/src/build_info.rs | 63 + python/rapier-py-3d/src/controllers.rs | 623 ++++-- python/rapier-py-3d/src/debug_render.rs | 96 +- python/rapier-py-3d/src/dynamics.rs | 744 ++++--- python/rapier-py-3d/src/errors.rs | 24 + python/rapier-py-3d/src/events_hooks.rs | 695 +++++-- python/rapier-py-3d/src/geometry.rs | 1729 ++++++++++++----- python/rapier-py-3d/src/joints.rs | 799 ++++++-- python/rapier-py-3d/src/lib.rs | 2 + python/rapier-py-3d/src/loaders.rs | 500 ++++- python/rapier-py-3d/src/math.rs | 90 + python/rapier-py-3d/src/pipeline.rs | 1000 ++++++---- python/rapier-py-3d/src/serde_glue.rs | 166 +- python/rapier-py-3d/src/soft_body.rs | 1519 +++++++++++---- .../rapier-testbed/rapier_testbed/__init__.py | 11 +- .../rapier-testbed/rapier_testbed/_testbed.py | 58 +- python/tests/test_bodies_colliders.py | 485 +++++ python/tests/test_controllers.py | 356 ++++ python/tests/test_debug_render.py | 56 +- python/tests/test_dynamics.py | 28 +- python/tests/test_events_hooks.py | 342 ++++ python/tests/test_joints.py | 76 + python/tests/test_loaders.py | 213 ++ python/tests/test_multibody.py | 158 ++ python/tests/test_new_bindings.py | 20 +- python/tests/test_queries.py | 106 + python/tests/test_soft_bodies.py | 304 +++ python/tests/test_stubs.py | 114 ++ python/tests/test_world.py | 375 ++++ 52 files changed, 10594 insertions(+), 2486 deletions(-) create mode 100644 python/rapier-py-3d/src/build_info.rs create mode 100644 python/tests/test_bodies_colliders.py create mode 100644 python/tests/test_stubs.py create mode 100644 python/tests/test_world.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f046b51c9..96ff52223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ `Multibody::clear_dof_couplings` to remove DoF couplings. - `rapier3d-urdf`: `UrdfLink::urdf_link_index` and `UrdfJoint::urdf_joint_index` (and related) map the loaded links and joints to their URDF counterparts, even when empty links are squeezed. +- `rapier3d-mjcf`: `MjcfContactHooks::is_excluded` and `MjcfContactHooks::pair_override` to query the + contact rules of a model. ### Fixed diff --git a/crates/rapier3d-mjcf/src/hooks.rs b/crates/rapier3d-mjcf/src/hooks.rs index 774faf791..6766afb39 100644 --- a/crates/rapier3d-mjcf/src/hooks.rs +++ b/crates/rapier3d-mjcf/src/hooks.rs @@ -64,6 +64,16 @@ impl MjcfContactHooks { pub fn has_overrides(&self) -> bool { !self.overrides.is_empty() } + + /// `true` if contacts between the colliders `a` and `b` are excluded. + pub fn is_excluded(&self, a: ColliderHandle, b: ColliderHandle) -> bool { + self.exclude.contains(&(a, b)) + } + + /// The override registered for the pair of colliders `a` and `b`, if any. + pub fn pair_override(&self, a: ColliderHandle, b: ColliderHandle) -> Option { + self.overrides.get(&(a, b)).copied() + } } impl PhysicsHooks for MjcfContactHooks { diff --git a/python/README.md b/python/README.md index 7bb9bd91e..b34803dd3 100644 --- a/python/README.md +++ b/python/README.md @@ -65,19 +65,25 @@ python -c "import rapier3d; print(rapier3d.__version__)" # smoke check ### Threads The engine is always built multi-threaded, and `step()` releases the GIL while -it runs. A world defaults to one worker per logical CPU; each world owns its -pool, so worlds stepped from different Python threads don't compete: +it runs. By default, every world runs its parallel stages on rayon's global +pool (one worker per logical CPU), shared by all the worlds of the process. +`set_num_threads(n)` gives a world its own pool of `n` workers, so worlds +stepped from different Python threads don't compete for the same workers: ```python -world.set_num_threads(4) # four workers for this world +world.set_num_threads(4) # a pool of four workers for this world alone world.num_threads # -> 4 world.set_num_threads(1) # everything inline on the calling thread -world.set_num_threads(None) # back to one worker per logical CPU +world.set_num_threads(None) # back to the shared global pool ``` The worker count never changes the result: the same scene stepped with 1 and with 8 workers gives bit-identical states. +A world can be used from any Python thread, but not by two threads at once: +while it is being stepped, using it (or one of its sets or objects) from +another thread raises `RuntimeError`. + ### Run the test suite ```bash diff --git a/python/docs/api/geometry.rst b/python/docs/api/geometry.rst index bf7375a1d..424228bdd 100644 --- a/python/docs/api/geometry.rst +++ b/python/docs/api/geometry.rst @@ -37,6 +37,8 @@ Shapes .. autoclass:: Cylinder .. autoclass:: Cone .. autoclass:: ConvexPolyhedron +.. autoclass:: Voxels +.. autoclass:: FillMode Bounding volumes ---------------- diff --git a/python/docs/api/joints.rst b/python/docs/api/joints.rst index bddf64530..bd55c51a4 100644 --- a/python/docs/api/joints.rst +++ b/python/docs/api/joints.rst @@ -71,5 +71,6 @@ Runtime joint records --------------------- .. autoclass:: ImpulseJoint +.. autoclass:: MultibodyJoint .. autoclass:: MultibodyLink .. autoclass:: Multibody diff --git a/python/docs/api/loaders.rst b/python/docs/api/loaders.rst index 53a4cfb49..2122dbb20 100644 --- a/python/docs/api/loaders.rst +++ b/python/docs/api/loaders.rst @@ -23,5 +23,12 @@ MJCF :members: Parses MuJoCo MJCF (XML) models into rapier bodies, colliders, joints, -and ```` loop closures. Actuator, sensor, and keyframe -elements are not surfaced to Python. +and ```` loop closures. The +:class:`~rapier3d.loaders.mjcf.MjcfRobotHandles` returned by the insertion +drive the ```` elements +(:meth:`~rapier3d.loaders.mjcf.MjcfRobotHandles.apply_controls`), apply the +```` elements +(:meth:`~rapier3d.loaders.mjcf.MjcfRobotHandles.apply_keyframe`) and apply +the ```` rules +(:meth:`~rapier3d.loaders.mjcf.MjcfRobotHandles.contact_hooks`). Sensors +are not surfaced to Python. diff --git a/python/docs/api/math.rst b/python/docs/api/math.rst index 2ce9f210b..d6bd1d702 100644 --- a/python/docs/api/math.rst +++ b/python/docs/api/math.rst @@ -29,9 +29,31 @@ Free helpers .. autofunction:: rotation_from_angle -.. currentmodule:: rapier3d.math - -Pure-Python helpers re-exported under ``rapier3d.math``: - -.. automodule:: rapier3d.math - :no-index: +.. module:: rapier3d.math + +Deterministic functions +----------------------- + +Computed by Rapier's math backend, in the engine's precision (``f32``). With +the ``determinism`` feature (see :attr:`rapier3d.BuildFeatures.enhanced_determinism`), +they give bit-identical results on every platform, unlike Python's :mod:`math` +module or NumPy. + +.. autofunction:: sin +.. autofunction:: cos +.. autofunction:: tan +.. autofunction:: asin +.. autofunction:: acos +.. autofunction:: atan +.. autofunction:: atan2 +.. autofunction:: exp +.. autofunction:: log +.. autofunction:: pow +.. autofunction:: sqrt + +Helpers +------- + +.. autofunction:: lerp +.. autofunction:: linear_interp +.. autofunction:: wrap_to_pi diff --git a/python/docs/api/pipeline.rst b/python/docs/api/pipeline.rst index d5fa0acbe..137918ee1 100644 --- a/python/docs/api/pipeline.rst +++ b/python/docs/api/pipeline.rst @@ -8,8 +8,9 @@ the spatial / shape query machinery. Most programs should use :doc:`world` rather than wiring these pipelines up by hand. :class:`PhysicsWorld` owns a - :class:`PhysicsPipeline` and :class:`QueryPipeline` and steps them for - you; the types below are for advanced, hand-rolled simulation loops. + :class:`PhysicsPipeline`, a :class:`CollisionPipeline` and a + :class:`QueryPipeline` and steps them for you; the types below are for + advanced, hand-rolled simulation loops. .. currentmodule:: rapier3d @@ -18,6 +19,7 @@ Pipelines .. autoclass:: PhysicsPipeline .. autoclass:: CollisionPipeline +.. autoclass:: Quarantine Query pipeline & filters ------------------------ @@ -46,3 +48,9 @@ Performance counters .. autoclass:: CollisionDetectionCounters .. autoclass:: SolverCounters .. autoclass:: CCDCounters + +Build features +-------------- + +.. autofunction:: build_features +.. autoclass:: BuildFeatures diff --git a/python/docs/api/soft_bodies.rst b/python/docs/api/soft_bodies.rst index f1aaa6f9f..fa77448e7 100644 --- a/python/docs/api/soft_bodies.rst +++ b/python/docs/api/soft_bodies.rst @@ -44,15 +44,43 @@ Bodies and sets .. autoclass:: SoftBodySet .. autoclass:: SoftBodyHandle -Material and settings ---------------------- +Volume meshing +-------------- + +:meth:`SoftBody.volumetric` fills a closed triangle mesh (for example the one +:meth:`Cuboid.to_trimesh` or :meth:`Ball.to_trimesh` gives) with tetrahedral cells; +:meth:`SoftBody.volumetric_with` takes the meshing parameters spelled out:: + + vertices, indices = rp.Ball(0.5).to_trimesh(24, 24) + params = rp.VolumeMeshParameters(0.2, cover_subdivisions=1, cover_smoothing=4) + ball = world.add_soft_body(rp.SoftBody.volumetric_with(vertices, indices, params)) + +.. autoclass:: VolumeMeshParameters +.. autoclass:: MeshEnclosure + +Material, solver and settings +----------------------------- + +:attr:`SoftBody.material` and :attr:`IntegrationParameters.soft_bodies` (with its nested +``recovery`` and ``fem`` groups) are live views: setting one of their fields changes the body +or the parameters they were read from, and their ``copy()`` gives a detached copy. + +A body's elasticity is solved by constraints by default; :attr:`SoftBodySolver.FEM` solves it +over the whole body instead, so its stiffness does not depend on the solver iterations:: + + beam = world.add_soft_body( + rp.SoftBody.cuboid((0, 2, 0), (1.0, 0.1, 0.1), 11, 3, 3, solver=rp.SoftBodySolver.FEM) + ) + world.integration_parameters.soft_bodies.fem.max_linear_iterations = 40 .. autoclass:: SoftBodyMaterial .. autoclass:: SoftBodyCellModel .. autoclass:: SoftEdgePlasticFlow +.. autoclass:: SoftBodySolver .. autoclass:: SoftBodyParticleSettings .. autoclass:: SoftBodiesSettings .. autoclass:: SoftRecoverySettings +.. autoclass:: SoftFemParameters .. autoclass:: SoftPatchConstraints Elements diff --git a/python/docs/api/world.rst b/python/docs/api/world.rst index b196d5215..078df712c 100644 --- a/python/docs/api/world.rst +++ b/python/docs/api/world.rst @@ -6,9 +6,10 @@ PhysicsWorld — the main entry point :class:`PhysicsWorld` is the recommended starting point for almost every program. It bundles **all** of rapier's sub-state — the rigid-body, collider, and joint sets, the broad/narrow phase, island manager, CCD -solver, integration parameters, and the physics + query pipelines — into -a single object, and drives the whole simulation with one -:meth:`~PhysicsWorld.step` call. +solver, integration parameters, and the physics, collision and query +pipelines — into a single object, and drives the whole simulation with one +:meth:`~PhysicsWorld.step` call. Its gravity is zero unless given to the +constructor (unlike the Rust ``PhysicsWorld::new()``). Reach for the individual pieces (documented under :doc:`dynamics`, :doc:`geometry`, :doc:`joints`, and :doc:`pipeline`) diff --git a/python/docs/changelog.rst b/python/docs/changelog.rst index 28afd251a..ebb79ab37 100644 --- a/python/docs/changelog.rst +++ b/python/docs/changelog.rst @@ -10,6 +10,11 @@ release cadence. The authoritative changelog is the Cargo Unreleased ---------- +**Small fixes.** ``DebugRenderStyle.copy`` returns a detached style; +``BroadPhasePairEvent.is_added`` / ``is_removed`` give the event kind (the +``added`` constructor shadows the ``added`` flag on instances); the +``QueryPipeline.project_point`` stub lists ``max_dist``. + **Soft bodies.** ``PhysicsWorld.add_soft_body`` inserts a deformable body (``SoftBody.rope`` / ``cloth`` / ``cuboid`` / ``sphere`` / ``trimesh`` / ``volumetric`` generators, ``SoftBodyBuilder`` setters, ``SoftBodyMaterial``), @@ -32,6 +37,46 @@ take an optional ``soft_bodies`` argument; ``DebugRenderMode.SOFT_BODIES`` and related modes; snapshots include the soft bodies; ``SolverFlags.COMPUTE_RIGID_IMPULSES`` is the engine's new name for ``COMPUTE_IMPULSES``. +**Rigid bodies and colliders.** ``RigidBody.set_next_kinematic_translation`` / +``set_next_kinematic_rotation`` / ``set_next_kinematic_position`` drive position-based +kinematic bodies; ``RigidBody.allow_fast_rotation`` (and the matching builder method and +factory keyword) lifts the angular speed cap. ``Collider.position_wrt_parent`` / +``translation_wrt_parent`` / ``rotation_wrt_parent`` read and move a collider on its body. +New shapes: ``Collider.segment`` / ``polyline`` / ``voxelized_mesh`` and their +``SharedShape`` siblings, with ``FillMode`` selecting a hollow or solid voxelization. Every +``Collider`` shape factory now accepts the ``ColliderBuilder`` keyword arguments, and the +mesh-based ones accept nested lists as well as NumPy arrays. + +Fixes: ``SharedShape.heightfield`` / ``Collider.heightfield`` read row-major (C-ordered) +arrays correctly (rows along Z, columns along X; previously the heights were scrambled) and +accept ``float64`` arrays and nested lists. ``InteractionGroups.test_mode`` reads back as +``AND`` instead of ``DEFAULT`` (``DEFAULT`` and ``ONLY_DYNAMIC`` are now deprecated aliases +of ``AND``). The enums are hashable, and their stubs no longer claim they are +``enum.IntEnum`` subclasses. ``ColliderSet.remove`` takes ``wake_up`` (``wake_parent`` still +works but is deprecated) and ``RigidBody.add_gravitational_force`` takes ``wake_up``. The +force and torque docstrings now state that user forces persist until +``reset_forces`` / ``reset_torques``. + +Soft-body additions: the FEM solver is always built in (``SoftBodySolver``, +``SoftBodyBuilder.solver`` / ``solver=``, ``SoftBody.solver``, ``SoftFemParameters`` on +``SoftBodiesSettings.fem``); ``SoftBody.volumetric_with`` with ``VolumeMeshParameters`` and +``MeshEnclosure``; ``to_trimesh()`` on ``Ball``, ``Cuboid``, ``Capsule``, ``Cylinder``, +``Cone``, ``ConvexPolyhedron``, ``HeightField`` and ``Voxels``; +``SoftBody.set_cluster_shape_matching_target``, ``set_edge_tear_resistance``, +``set_cell_tear_resistance`` and ``set_particle_damaged``; ``SoftBody.particle_positions`` and +``particle_velocities`` are writable. ``SoftBody.material`` and +``IntegrationParameters.soft_bodies`` (with ``recovery`` and ``fem``) are now live views, with a +``copy()`` for a detached copy; assigning them still works. A view of a removed soft body raises +``InvalidHandle``; ``RigidBodySet.remove`` and ``ColliderSet.remove`` raise ``ValueError`` for a +soft-body proxy or collision mesh when ``soft_bodies`` is left out; the soft-body index arrays +(pinned particles, elements, cluster particles, torn edges and cells) accept any memory layout and +integer dtype, where a sliced array used to be silently ignored. + +**Breaking — ``SpringCoefficients`` keywords and defaults.** Its constructor takes ``natural_frequency`` and +``damping_ratio`` (``stiffness`` and ``damping`` remain as deprecated aliases, for the keywords and +the properties), and its defaults are now those of ``SpringCoefficients.contact_defaults()`` +(``30.0`` Hz and ``10.0``, instead of a damping ratio of ``5.0``). + **Breaking — repackaged as ``rapier3d``.** The single ``rapier`` umbrella package (with ``rapier.dim3`` submodules) has been replaced by the ``rapier3d`` package (3D / f32): @@ -54,6 +99,101 @@ package. setting: :meth:`~rapier3d.PhysicsWorld.set_num_threads` (also on :class:`~rapier3d.PhysicsPipeline`) picks how many workers a world's parallel stages run on, and :attr:`~rapier3d.PhysicsWorld.num_threads` -reports it. Each world owns its pool; ``None`` restores the default of one -worker per logical CPU and ``1`` runs everything inline on the calling -thread. Results are bit-identical whatever the worker count. +reports it. A worker count gives the world its own pool; ``None`` goes back +to rayon's global pool (one worker per logical CPU, shared by every world) +and ``1`` runs everything inline on the calling thread. Results are +bit-identical whatever the worker count. + +**Controllers.** ``Wheel.center`` / ``suspension`` / ``axle`` give the +world-space state of a vehicle wheel after the last update. +``PdController`` and ``PidController`` expose their gains as ``lin_kp`` / +``ang_kp`` / ``lin_kd`` / ``ang_kd`` (plus ``lin_ki`` / ``ang_ki`` and the +read-only ``lin_integral`` / ``ang_integral`` on the PID), and +``rigid_body_correction`` takes an optional ``target_vels`` +(``RigidBodyVelocity``). ``CharacterCollision.hit`` aliases ``toi``. +Fixes: scalar gains (``PidController(Kp=60.0)``) are applied to every axis +instead of raising ``TypeError``; the ``PidController.axes`` setter was +exposed as ``axes_attr``; ``update_vehicle`` and +``solve_character_collision_impulses`` now honor the ``QueryFilter`` +predicate (evaluated once per collider before the update) and +``update_vehicle`` always excludes the chassis colliders; +``KinematicCharacterController(snap_to_ground=None)`` (and +``autostep=None``) disables the feature; out-of-range wheel indices raise +``IndexError`` instead of ``TypeError``. The unused ``bodies`` / +``colliders`` arguments of ``move_shape`` and ``character_pos`` of +``solve_character_collision_impulses`` accept ``None``; passing sets other +than the query pipeline's now raises ``ValueError``. + +**Joints.** ``multibody_joints[handle]`` returns a live +:class:`~rapier3d.MultibodyJoint` (``data``, ``kinematic``, ``coords``, +``link_id``, ``multibody``); modifying its ``data`` wakes its bodies up and +changing its locked axes raises ``ValueError``. +:meth:`~rapier3d.Multibody.apply_displacements` applies the result of +``inverse_kinematics_for_link``, which now takes a ``joint_can_move`` +callback. ``PrismaticJointBuilder.motor``, the ``RopeJointBuilder`` motor +methods and ``RopeJoint.motor`` / ``set_motor_*``. The joint builders accept +``softness=`` as a keyword argument. Fixed: ``len(multibody_joints)`` now +counts joints (like iteration) instead of multibodies; +``InverseKinematicsOption`` shows its ``constrained_axes`` in its repr. + +**Loaders.** MJCF actuators, keyframes and contact rules: +``MjcfRobotHandles.actuators`` (:class:`~rapier3d.loaders.mjcf.MjcfActuatorHandle`), +``apply_controls``, ``apply_keyframe``, ``keyframe_controls``, +``keyframe_names`` and ``contact_hooks`` (a +:class:`~rapier3d.loaders.mjcf.MjcfContactHooks` that runs natively when +assigned to ``PhysicsWorld.physics_hooks``); ``MjcfRobot.gravity`` and +``keyframe_names``; ``MjcfMultibodyOptions.SKIP_JOINT_SPRINGS``. The +``Mjcf``/``UrdfMultibodyOptions`` flags support ``in``. Fixed: the repr of +``MjcfModel`` no longer shows ``Some(...)``, the URDF handles have a repr, +and the ``load_from_path`` docs list the supported formats. + +**Scene queries, contacts and hooks.** ``QueryPipeline.project_point`` takes a +``max_dist``. ``NarrowPhase.contact_pairs_with`` / ``intersection_pairs_with`` +list the pairs of one collider. ``ContactManifoldData.solver_contacts`` and +``solver_contact_world_points`` expose the solver contacts; +``SolverContact.tangent_velocity`` and +``ContactModificationContext.set_solver_contact`` / ``set_tangent_velocity`` +modify them from a hook. ``PairFilterContext`` / ``ContactModificationContext`` +expose the ``colliders`` / ``bodies`` being stepped, and event handlers receive +them (and the ``SoftBodySet``) instead of ``None``: during a callback, they and +the views into them can be read (not modified). ``QueryFilter``'s current groups +and predicate are read through ``interaction_groups`` / ``predicate_fn`` (the +former getters were shadowed by the builder methods). +``DebugRenderPipeline.style`` is now the same live object on every access. + +Fixes: ``PhysicsWorld.update_query_pipeline`` / ``QueryPipeline.update`` no +longer consume the collider changes, which made the next ``step()`` panic or +miss them; the query pipeline is up to date after every step, so +``auto_update_query`` now has no effect. Reading the world from a hook or event +handler no longer raises a ``PanicException`` (running a query or stepping from +there raises ``RuntimeError``). A hooks or event-handler object may omit any of its +methods. ``DebugLineCollector.lines()`` returns the documented ``(N, 2, 3)`` +array. ``ContactData.contact_id`` is the index of the point in its manifold +(it was always 0). ``ShapeCastHit`` documents its frames (``witness1`` / +``normal1`` on the hit collider in world space, ``witness2`` / ``normal2`` on +the cast shape in its local space). ``ContactPair``, ``ContactForceEvent``, +``ContactManifoldData`` and ``SolverContact`` have a ``repr``. + +**World and pipelines.** ``PhysicsWorld.quarantine`` / ``PhysicsPipeline.quarantine`` +report the objects neutralized by the last step because their state became non-finite +(:class:`~rapier3d.Quarantine`). ``PhysicsWorld.detect_collisions()`` updates the contacts +and the scene queries without stepping, with the world's new ``collision_pipeline``. +``PhysicsPipeline.counters`` is now a live view (``counters.disable()`` / ``enable()`` +act on the pipeline), and the wheels are built with rapier's ``profiler`` feature so +its timings are measured. ``build_features()`` (:class:`~rapier3d.BuildFeatures`) +reports the Cargo profile and the optional features of the loaded extension. +``IntegrationParameters.warmstart_joints`` and ``friction_in_bias_pass``; +``FrictionModel.SIMPLIFIED`` is the new name of ``COEFFICIENT`` (kept as a deprecated +alias). ``rapier3d.math`` gains ``sin``, ``cos``, ``tan``, ``asin``, ``acos``, ``atan``, +``atan2``, ``exp``, ``log``, ``pow`` and ``sqrt``, computed by Rapier's math backend +(cross-platform deterministic with the ``determinism`` feature). ``SoftBody.is_enabled`` +is writable. The testbed's ``set_world`` accepts a ``PhysicsWorld``. + +**Fixes.** ``PhysicsWorld.clear()`` also removes the soft bodies and resets the +pipelines. Using a rigid-body, collider, impulse-joint or multibody view after its object +was removed raises ``InvalidHandle`` instead of a ``PanicException``. A world and its sets +can be used from any thread: using them from another thread no longer panics, and +modifying them (or reading them outside of its callbacks) while another thread steps the +world raises ``RuntimeError``. The type stubs now +cover ``PhysicsWorld``, the pipelines, the counters and the whole package +(``__init__.pyi``), and ``Vec3`` / ``Point3`` coordinates are read-only. diff --git a/python/examples/character/stairs.py b/python/examples/character/stairs.py index ce464a70f..6e6126e97 100644 --- a/python/examples/character/stairs.py +++ b/python/examples/character/stairs.py @@ -15,7 +15,7 @@ def main() -> None: - world = rp.PhysicsWorld(gravity=(0, -9.81, 0), auto_update_query=True) + world = rp.PhysicsWorld(gravity=(0, -9.81, 0)) # Big flat ground. world.add_body( diff --git a/python/examples/vehicle/drive.py b/python/examples/vehicle/drive.py index af5d262f5..a4f5a1858 100644 --- a/python/examples/vehicle/drive.py +++ b/python/examples/vehicle/drive.py @@ -15,7 +15,7 @@ def main() -> None: - world = rp.PhysicsWorld(gravity=(0, -9.81, 0), auto_update_query=True) + world = rp.PhysicsWorld(gravity=(0, -9.81, 0)) # Ground. world.add_body( @@ -45,7 +45,6 @@ def main() -> None: # Settle. for _ in range(120): world.step() - world.update_query_pipeline() veh.update_vehicle(1.0 / 60.0, world.rigid_bodies, world.colliders, world.query_pipeline) # Accelerate via the rear wheels. 30N matches the Rust demo's arrow-key @@ -54,7 +53,6 @@ def main() -> None: veh.apply_engine_force(3, 30.0) for _ in range(240): world.step() - world.update_query_pipeline() veh.update_vehicle(1.0 / 60.0, world.rigid_bodies, world.colliders, world.query_pipeline) speed = veh.current_speed_km_hour() diff --git a/python/rapier-py-3d/Cargo.toml b/python/rapier-py-3d/Cargo.toml index 3ab418995..f63c3abe0 100644 --- a/python/rapier-py-3d/Cargo.toml +++ b/python/rapier-py-3d/Cargo.toml @@ -30,16 +30,23 @@ rust.unexpected_cfgs = { level = "warn", check-cfg = [ # `RAPIER_PY_DETERMINISM=1` translated to `--features determinism` by the # build.rs. determinism = ["rapier3d/enhanced-determinism"] +# Rapier's internal profiler: fills the timings of `Counters` (only measured while they are +# enabled, so it costs nothing otherwise). On by default. +default = ["profiler"] +profiler = ["rapier3d/profiler"] [dependencies] pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39", "multiple-pymethods"] } numpy = "0.22" nalgebra.workspace = true thiserror.workspace = true +# `fem` is always on: pip users can't pick cargo features, and the FEM soft-body solver is +# only used by the bodies that select it. rapier3d = { workspace = true, features = [ "serde-serialize", "debug-render", "parallel", + "fem", ] } # 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/build.rs b/python/rapier-py-3d/build.rs index 52ccd8468..53a965f84 100644 --- a/python/rapier-py-3d/build.rs +++ b/python/rapier-py-3d/build.rs @@ -9,6 +9,9 @@ //! - With neither → fast path with platform-native math. fn main() { + // Report the Cargo profile the extension is built with (`BuildFeatures.profile`). + let profile = std::env::var("PROFILE").expect("Cargo provides PROFILE"); + println!("cargo:rustc-env=RAPIER_PY_CARGO_PROFILE={profile}"); println!("cargo:rerun-if-env-changed=RAPIER_PY_DETERMINISM"); let env_set = std::env::var("RAPIER_PY_DETERMINISM").is_ok(); let feature_on = std::env::var("CARGO_FEATURE_DETERMINISM").is_ok(); diff --git a/python/rapier-py-3d/python/rapier3d/__init__.py b/python/rapier-py-3d/python/rapier3d/__init__.py index 9fce97c84..963071123 100644 --- a/python/rapier-py-3d/python/rapier3d/__init__.py +++ b/python/rapier-py-3d/python/rapier3d/__init__.py @@ -1,4 +1,4 @@ -"""3D bindings (f32 by default, with `rapier.dim3.f64` for double precision).""" +"""Python bindings of the Rapier physics engine (3D, f32).""" from __future__ import annotations @@ -56,10 +56,14 @@ SoftEdgePlasticFlow, SoftBodyEdgeKind, SoftPatchConstraints, + SoftBodySolver, SoftBodyMaterial, SoftBodyParticleSettings, SoftRecoverySettings, + SoftFemParameters, SoftBodiesSettings, + MeshEnclosure, + VolumeMeshParameters, SoftBodyBuilder, SoftBodyParticle, SoftBodyEdge, @@ -107,6 +111,7 @@ ImpulseJoint, MultibodyLink, Multibody, + MultibodyJoint, # ---- geometry ---- Collider, ColliderBuilder, @@ -128,6 +133,8 @@ TriMesh, HeightField, Compound, + Voxels, + FillMode, Cylinder, Cone, ConvexPolyhedron, @@ -175,6 +182,9 @@ CollisionDetectionCounters, SolverCounters, CCDCounters, + Quarantine, + BuildFeatures, + build_features, # ---- events / hooks ---- SolverFlags, PairFilterContext, @@ -261,10 +271,14 @@ "SoftEdgePlasticFlow", "SoftBodyEdgeKind", "SoftPatchConstraints", + "SoftBodySolver", "SoftBodyMaterial", "SoftBodyParticleSettings", "SoftRecoverySettings", + "SoftFemParameters", "SoftBodiesSettings", + "MeshEnclosure", + "VolumeMeshParameters", "SoftBodyBuilder", "SoftBodyParticle", "SoftBodyEdge", @@ -312,6 +326,7 @@ "ImpulseJoint", "MultibodyLink", "Multibody", + "MultibodyJoint", "Collider", "ColliderBuilder", "ColliderHandle", @@ -332,6 +347,8 @@ "TriMesh", "HeightField", "Compound", + "Voxels", + "FillMode", "Cylinder", "Cone", "ConvexPolyhedron", @@ -379,6 +396,9 @@ "CollisionDetectionCounters", "SolverCounters", "CCDCounters", + "Quarantine", + "BuildFeatures", + "build_features", # ---- events / hooks ---- "SolverFlags", "PairFilterContext", diff --git a/python/rapier-py-3d/python/rapier3d/__init__.pyi b/python/rapier-py-3d/python/rapier3d/__init__.pyi index b1d10bd1a..9f445623c 100644 --- a/python/rapier-py-3d/python/rapier3d/__init__.pyi +++ b/python/rapier-py-3d/python/rapier3d/__init__.pyi @@ -1,10 +1,22 @@ -"""Stub for `rapier.dim3` (3D, f32).""" +"""Type stubs for the `rapier3d` package: the Rapier physics engine (3D, f32). +Every public class and function of the compiled extension (`rapier3d._rapier3d`) is +re-exported here, with the pure-Python protocols and the `math` / `loaders` submodules. +""" + +from . import loaders as loaders +from . import math as math +from ._debug_render import DebugRenderBackend as DebugRenderBackend +from ._event_handler import EventHandler as EventHandler +from ._event_handler import PhysicsHooks as PhysicsHooks from ._rapier3d import ( - AngVector3 as AngVector3, - Isometry3 as Isometry3, + Vec3 as Vec3, Point3 as Point3, + Rotation3 as Rotation3, Quaternion as Quaternion, + Isometry3 as Isometry3, + AngVector3 as AngVector3, + rotation_from_angle as rotation_from_angle, RapierError as RapierError, InvalidHandle as InvalidHandle, MeshConversionError as MeshConversionError, @@ -13,10 +25,389 @@ from ._rapier3d import ( SerializationError as SerializationError, QueryFailure as QueryFailure, MeshLoaderError as MeshLoaderError, - Rotation3 as Rotation3, - Vec3 as Vec3, - rotation_from_angle as rotation_from_angle, + RigidBody as RigidBody, + RigidBodyBuilder as RigidBodyBuilder, + RigidBodyHandle as RigidBodyHandle, + RigidBodySet as RigidBodySet, + RigidBodyType as RigidBodyType, + LockedAxes as LockedAxes, + RigidBodyActivation as RigidBodyActivation, + RigidBodyDamping as RigidBodyDamping, + RigidBodyDominance as RigidBodyDominance, + RigidBodyCcd as RigidBodyCcd, + RigidBodyVelocity as RigidBodyVelocity, + RigidBodyForces as RigidBodyForces, + RigidBodyMassProps as RigidBodyMassProps, + RigidBodyPosition as RigidBodyPosition, + RigidBodyAdditionalMassProps as RigidBodyAdditionalMassProps, + MassProperties as MassProperties, + IntegrationParameters as IntegrationParameters, + SpringCoefficients as SpringCoefficients, + CoefficientCombineRule as CoefficientCombineRule, + FrictionModel as FrictionModel, + IslandManager as IslandManager, + CCDSolver as CCDSolver, + ImpulseJointSet as ImpulseJointSet, + MultibodyJointSet as MultibodyJointSet, + SoftBindingError as SoftBindingError, + SoftBodyHandle as SoftBodyHandle, + SoftBodyCellModel as SoftBodyCellModel, + SoftEdgePlasticFlow as SoftEdgePlasticFlow, + SoftBodyEdgeKind as SoftBodyEdgeKind, + SoftPatchConstraints as SoftPatchConstraints, + SoftBodySolver as SoftBodySolver, + SoftBodyMaterial as SoftBodyMaterial, + SoftBodyParticleSettings as SoftBodyParticleSettings, + SoftRecoverySettings as SoftRecoverySettings, + SoftFemParameters as SoftFemParameters, + SoftBodiesSettings as SoftBodiesSettings, + MeshEnclosure as MeshEnclosure, + VolumeMeshParameters as VolumeMeshParameters, + SoftBodyBuilder as SoftBodyBuilder, + SoftBodyParticle as SoftBodyParticle, + SoftBodyEdge as SoftBodyEdge, + SoftBodyDihedral as SoftBodyDihedral, + SoftBodyCell as SoftBodyCell, + SoftParticleAttachment as SoftParticleAttachment, + SoftVolumePiece as SoftVolumePiece, + SoftBodyCluster as SoftBodyCluster, + SoftMeshId as SoftMeshId, + SoftMeshRef as SoftMeshRef, + SoftCollisionMesh as SoftCollisionMesh, + SoftMeshBinding as SoftMeshBinding, + SoftBodyPiece as SoftBodyPiece, + SoftClusterSplit as SoftClusterSplit, + SoftJointMove as SoftJointMove, + SoftBodyTearEvent as SoftBodyTearEvent, + SoftBody as SoftBody, + SoftBodySet as SoftBodySet, + JointEnabled as JointEnabled, + MotorModel as MotorModel, + JointLimits as JointLimits, + JointMotor as JointMotor, + JointAxesMask as JointAxesMask, + JointAxis as JointAxis, + ImpulseJointHandle as ImpulseJointHandle, + MultibodyJointHandle as MultibodyJointHandle, + MultibodyIndex as MultibodyIndex, + MultibodyLinkId as MultibodyLinkId, + InverseKinematicsOption as InverseKinematicsOption, + FixedJoint as FixedJoint, + FixedJointBuilder as FixedJointBuilder, + RevoluteJoint as RevoluteJoint, + RevoluteJointBuilder as RevoluteJointBuilder, + PrismaticJoint as PrismaticJoint, + PrismaticJointBuilder as PrismaticJointBuilder, + SphericalJoint as SphericalJoint, + SphericalJointBuilder as SphericalJointBuilder, + RopeJoint as RopeJoint, + RopeJointBuilder as RopeJointBuilder, + SpringJoint as SpringJoint, + SpringJointBuilder as SpringJointBuilder, + GenericJoint as GenericJoint, + GenericJointBuilder as GenericJointBuilder, + ImpulseJoint as ImpulseJoint, + MultibodyLink as MultibodyLink, + Multibody as Multibody, + MultibodyJoint as MultibodyJoint, + Collider as Collider, + ColliderBuilder as ColliderBuilder, + ColliderHandle as ColliderHandle, + ColliderSet as ColliderSet, + ColliderType as ColliderType, + ColliderEnabled as ColliderEnabled, + ColliderMaterial as ColliderMaterial, + ColliderFlags as ColliderFlags, + ColliderParent as ColliderParent, + ColliderPosition as ColliderPosition, + ColliderMassProps as ColliderMassProps, + ShapeType as ShapeType, + SharedShape as SharedShape, + Ball as Ball, + Cuboid as Cuboid, + Capsule as Capsule, + Triangle as Triangle, + TriMesh as TriMesh, + HeightField as HeightField, + Compound as Compound, + Voxels as Voxels, + FillMode as FillMode, + Cylinder as Cylinder, + Cone as Cone, + ConvexPolyhedron as ConvexPolyhedron, + Aabb as Aabb, + BoundingSphere as BoundingSphere, + MeshConverter as MeshConverter, + TriMeshFlags as TriMeshFlags, + ActiveEvents as ActiveEvents, + ActiveHooks as ActiveHooks, + ActiveCollisionTypes as ActiveCollisionTypes, + Group as Group, + InteractionTestMode as InteractionTestMode, + InteractionGroups as InteractionGroups, + CollisionEventFlags as CollisionEventFlags, + CollisionEvent as CollisionEvent, + ContactForceEvent as ContactForceEvent, + ContactData as ContactData, + ContactManifold as ContactManifold, + ContactManifoldData as ContactManifoldData, + ContactPair as ContactPair, + IntersectionPair as IntersectionPair, + ColliderPair as ColliderPair, + BroadPhasePairEvent as BroadPhasePairEvent, + BroadPhaseBvh as BroadPhaseBvh, + BvhOptimizationStrategy as BvhOptimizationStrategy, + NarrowPhase as NarrowPhase, + SolverContact as SolverContact, + PhysicsPipeline as PhysicsPipeline, + CollisionPipeline as CollisionPipeline, + PhysicsWorld as PhysicsWorld, + QueryPipeline as QueryPipeline, + QueryFilter as QueryFilter, + QueryFilterFlags as QueryFilterFlags, + Ray as Ray, + RayIntersection as RayIntersection, + PointProjection as PointProjection, + ShapeCastHit as ShapeCastHit, + ShapeCastOptions as ShapeCastOptions, + ShapeCastStatus as ShapeCastStatus, + FeatureId as FeatureId, + NonlinearRigidMotion as NonlinearRigidMotion, + Counters as Counters, + StagesCounters as StagesCounters, + CollisionDetectionCounters as CollisionDetectionCounters, + SolverCounters as SolverCounters, + CCDCounters as CCDCounters, + Quarantine as Quarantine, + BuildFeatures as BuildFeatures, + build_features as build_features, + SolverFlags as SolverFlags, + PairFilterContext as PairFilterContext, + ContactModificationContext as ContactModificationContext, + ChannelEventCollector as ChannelEventCollector, + AxesMask as AxesMask, + CharacterLength as CharacterLength, + CharacterAutostep as CharacterAutostep, + CharacterCollision as CharacterCollision, + EffectiveCharacterMovement as EffectiveCharacterMovement, + KinematicCharacterController as KinematicCharacterController, + PdErrors as PdErrors, + PidCorrection as PidCorrection, + PdController as PdController, + PidController as PidController, + WheelTuning as WheelTuning, + RayCastInfo as RayCastInfo, + Wheel as Wheel, + DynamicRayCastVehicleController as DynamicRayCastVehicleController, + DebugRenderPipeline as DebugRenderPipeline, + DebugRenderStyle as DebugRenderStyle, + DebugRenderMode as DebugRenderMode, + DebugRenderObject as DebugRenderObject, + DebugColor as DebugColor, + DebugLineCollector as DebugLineCollector, ) -from . import f64 as f64, math as math __version__: str + +__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", + "SoftBindingError", + "SoftBodyHandle", + "SoftBodyCellModel", + "SoftEdgePlasticFlow", + "SoftBodyEdgeKind", + "SoftPatchConstraints", + "SoftBodySolver", + "SoftBodyMaterial", + "SoftBodyParticleSettings", + "SoftRecoverySettings", + "SoftFemParameters", + "SoftBodiesSettings", + "MeshEnclosure", + "VolumeMeshParameters", + "SoftBodyBuilder", + "SoftBodyParticle", + "SoftBodyEdge", + "SoftBodyDihedral", + "SoftBodyCell", + "SoftParticleAttachment", + "SoftVolumePiece", + "SoftBodyCluster", + "SoftMeshId", + "SoftMeshRef", + "SoftCollisionMesh", + "SoftMeshBinding", + "SoftBodyPiece", + "SoftClusterSplit", + "SoftJointMove", + "SoftBodyTearEvent", + "SoftBody", + "SoftBodySet", + "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", + "MultibodyJoint", + "Collider", + "ColliderBuilder", + "ColliderHandle", + "ColliderSet", + "ColliderType", + "ColliderEnabled", + "ColliderMaterial", + "ColliderFlags", + "ColliderParent", + "ColliderPosition", + "ColliderMassProps", + "ShapeType", + "SharedShape", + "Ball", + "Cuboid", + "Capsule", + "Triangle", + "TriMesh", + "HeightField", + "Compound", + "Voxels", + "FillMode", + "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", + "PhysicsPipeline", + "CollisionPipeline", + "PhysicsWorld", + "QueryPipeline", + "QueryFilter", + "QueryFilterFlags", + "Ray", + "RayIntersection", + "PointProjection", + "ShapeCastHit", + "ShapeCastOptions", + "ShapeCastStatus", + "FeatureId", + "NonlinearRigidMotion", + "Counters", + "StagesCounters", + "CollisionDetectionCounters", + "SolverCounters", + "CCDCounters", + "Quarantine", + "BuildFeatures", + "build_features", + "SolverFlags", + "PairFilterContext", + "ContactModificationContext", + "ChannelEventCollector", + "EventHandler", + "PhysicsHooks", + "AxesMask", + "CharacterLength", + "CharacterAutostep", + "CharacterCollision", + "EffectiveCharacterMovement", + "KinematicCharacterController", + "PdErrors", + "PidCorrection", + "PdController", + "PidController", + "WheelTuning", + "RayCastInfo", + "Wheel", + "DynamicRayCastVehicleController", + "DebugRenderPipeline", + "DebugRenderStyle", + "DebugRenderMode", + "DebugRenderObject", + "DebugColor", + "DebugLineCollector", + "DebugRenderBackend", + "math", + "__version__", +] diff --git a/python/rapier-py-3d/python/rapier3d/_event_handler.py b/python/rapier-py-3d/python/rapier3d/_event_handler.py index 3e68b358d..77b49a690 100644 --- a/python/rapier-py-3d/python/rapier3d/_event_handler.py +++ b/python/rapier-py-3d/python/rapier3d/_event_handler.py @@ -31,10 +31,12 @@ class EventHandler(Protocol): 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. + ``bodies`` and ``colliders`` are the ``RigidBodySet`` and ``ColliderSet`` + being stepped (``world.rigid_bodies`` / ``world.colliders``). During the + callback, they and the bodies / colliders read from them can be read but + not modified; the rest of the world (queries, joints, ...) can't be used + until ``step()`` returns. Every method is optional: a handler without one + of them doesn't receive the corresponding events. """ def handle_collision_event( @@ -73,9 +75,10 @@ def handle_soft_body_tear_event(self, soft_bodies: Any, event: Any) -> None: ``event`` is a ``SoftBodyTearEvent``: the torn elements, the split particles and the pieces that became soft bodies of their own. - ``soft_bodies`` is passed as ``None`` (see the note above). Immediate - ``SoftBodySet.tear`` / ``cut`` calls return their event instead. The - method is optional: a handler without it receives no tear events. + ``soft_bodies`` is the ``SoftBodySet`` being stepped, readable like + ``bodies`` above (``None`` with a ``PhysicsPipeline`` stepped without + soft bodies). Immediate ``SoftBodySet.tear`` / ``cut`` calls return + their event instead. """ ... @@ -89,9 +92,16 @@ class PhysicsHooks(Protocol): 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 + All three methods are optional: only define the ones you care about, a + missing one behaves like the default hook (the pair is kept, the contacts + are left unmodified). (But ``Protocol`` formally requires all three for type checkers.) + + The contexts expose the ``bodies`` / ``colliders`` sets being stepped. During + the callback, they and the bodies / colliders read from them can be read but + not modified. Read them through the context rather than through the world: + the hooks may run on the engine's worker threads, from which the + ``PhysicsWorld`` object itself can't be used. """ def filter_contact_pair(self, ctx: Any) -> Any: @@ -99,7 +109,8 @@ def filter_contact_pair(self, ctx: Any) -> Any: completely discard the pair). The default behavior corresponds to returning ``SolverFlags.COMPUTE_IMPULSES``. - ``ctx`` is a ``PairFilterContext`` view (read-only). + ``ctx`` is a ``PairFilterContext`` view (read-only), e.g. + ``ctx.colliders[ctx.collider1].user_data``. """ ... diff --git a/python/rapier-py-3d/python/rapier3d/_rapier3d.pyi b/python/rapier-py-3d/python/rapier3d/_rapier3d.pyi index 400b8619a..1b984e27f 100644 --- a/python/rapier-py-3d/python/rapier3d/_rapier3d.pyi +++ b/python/rapier-py-3d/python/rapier3d/_rapier3d.pyi @@ -1,11 +1,12 @@ -"""Type stubs for `rapier._rapier3d` (3D, f32). +"""Type stubs for `rapier3d._rapier3d`, the compiled extension of the `rapier3d` package (3D, f32). -Phase 02: math layer only. +Everything is re-exported by `rapier3d` (see `__init__.pyi`): import from there. """ from __future__ import annotations -from typing import Any, Iterator, Tuple, Union +import enum +from typing import Any, Iterator, Sequence, Tuple, Union import numpy as np @@ -38,6 +39,12 @@ RotationLike = Union[ "np.ndarray", ] IsometryLike = Union["Isometry3", Tuple[VectorLike, RotationLike]] +# An `(N, 3)` float ndarray, or a sequence of 3-vectors. +PointsLike = Union["np.ndarray", Sequence[VectorLike]] +# An `(M, K)` integer ndarray, or a sequence of length-K index sequences. +IndicesLike = Union["np.ndarray", Sequence[Sequence[int]]] +# An `(nrows, ncols)` float ndarray, or a sequence of equal-length rows. +HeightsLike = Union["np.ndarray", Sequence[Sequence[float]]] # ---- Vec3 ---------------------------------------------------------------- class Vec3: @@ -55,9 +62,13 @@ class Vec3: @staticmethod def from_ndarray(a: np.ndarray) -> Vec3: ... - x: float - y: float - z: float + # Vectors are immutable: build a new one instead of assigning a coordinate. + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... def dot(self, other: VectorLike) -> float: ... def cross(self, other: VectorLike) -> Vec3: ... @@ -81,6 +92,9 @@ class Vec3: def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> Vec3: ... # `AngVector3` is a Python-level alias for `Vec3`. AngVector3 = Vec3 @@ -95,10 +109,15 @@ class Point3: @staticmethod def from_ndarray(a: np.ndarray) -> Point3: ... - x: float - y: float - z: float - coords: Vec3 + # Points are immutable: build a new one instead of assigning a coordinate. + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def z(self) -> float: ... + @property + def coords(self) -> Vec3: ... def to_tuple(self) -> Tuple[float, float, float]: ... def to_ndarray(self) -> np.ndarray: ... @@ -113,6 +132,9 @@ class Point3: def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> Point3: ... # ---- Rotation3 / Quaternion --------------------------------------------- class Rotation3: @@ -159,6 +181,10 @@ class Rotation3: def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... + def to_tuple(self) -> Tuple[float, float, float, float]: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> Rotation3: ... # Alias: `Quaternion is Rotation3`. Quaternion = Rotation3 @@ -199,6 +225,9 @@ class Isometry3: def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> Isometry3: ... # ---- Free helpers -------------------------------------------------------- def rotation_from_angle(v: VectorLike) -> Rotation3: ... @@ -207,25 +236,42 @@ def rotation_from_angle(v: VectorLike) -> Rotation3: ... # Phase 03 — Dynamics # ===================================================================== -import enum +# The enums below are PyO3 enums, not `enum.IntEnum`s: their members are hashable, compare +# equal to their integer value and convert with `int()`, but the classes aren't iterable. -class RigidBodyType(enum.IntEnum): - DYNAMIC: int - FIXED: int - KINEMATIC_VELOCITY_BASED: int - KINEMATIC_POSITION_BASED: int - SOFT_FRAME: int +class RigidBodyType: + DYNAMIC: RigidBodyType + FIXED: RigidBodyType + KINEMATIC_VELOCITY_BASED: RigidBodyType + KINEMATIC_POSITION_BASED: RigidBodyType + SOFT_FRAME: RigidBodyType + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... -class CoefficientCombineRule(enum.IntEnum): - AVERAGE: int - MIN: int - MULTIPLY: int - MAX: int - CLAMPED_SUM: int +class CoefficientCombineRule: + AVERAGE: CoefficientCombineRule + MIN: CoefficientCombineRule + MULTIPLY: CoefficientCombineRule + MAX: CoefficientCombineRule + CLAMPED_SUM: CoefficientCombineRule + GEOMETRIC_MEAN: CoefficientCombineRule + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +class FrictionModel: + """Friction model of the contacts between rigid bodies.""" -class FrictionModel(enum.IntEnum): - COEFFICIENT: int - COULOMB: int + SIMPLIFIED: FrictionModel + """One friction constraint per group of up to four contacts, plus a twist constraint (default).""" + COULOMB: FrictionModel + """One Coulomb friction constraint per contact point.""" + COEFFICIENT: FrictionModel + """Deprecated alias of `SIMPLIFIED`.""" + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... class ColliderHandle: def __init__(self, index: int = 0, generation: int = 0) -> None: ... @@ -273,7 +319,10 @@ class JointMotor: class JointAxesMask: LIN_X: JointAxesMask LIN_Y: JointAxesMask + LIN_Z: JointAxesMask ANG_X: JointAxesMask + ANG_Y: JointAxesMask + ANG_Z: JointAxesMask LIN_AXES: JointAxesMask ANG_AXES: JointAxesMask LOCKED_REVOLUTE_AXES: JointAxesMask @@ -282,6 +331,8 @@ class JointAxesMask: FREE_REVOLUTE_AXES: JointAxesMask FREE_PRISMATIC_AXES: JointAxesMask FREE_FIXED_AXES: JointAxesMask + LOCKED_SPHERICAL_AXES: JointAxesMask + FREE_SPHERICAL_AXES: JointAxesMask def __init__(self, bits: int = 0) -> None: ... @staticmethod def empty() -> JointAxesMask: ... @@ -336,6 +387,9 @@ class MultibodyJointHandle: def __eq__(self, other: object) -> bool: ... class MultibodyIndex: + @staticmethod + def from_raw_parts(index: int, generation: int) -> MultibodyIndex: ... + def into_raw_parts(self) -> tuple[int, int]: ... @property def index(self) -> int: ... @property @@ -348,6 +402,9 @@ class MultibodyLinkId: def multibody(self) -> MultibodyIndex: ... @property def id(self) -> int: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> MultibodyLinkId: ... class InverseKinematicsOption: damping: float @@ -365,6 +422,12 @@ class GenericJoint: def __init__(self, locked_axes: JointAxesMask | None = None) -> None: ... @staticmethod def builder(locked_axes: JointAxesMask | None = None, **kwargs: Any) -> GenericJointBuilder: ... + local_frame1: Any + local_frame2: Any + local_anchor1: Any + local_anchor2: Any + local_axis1: Any + local_axis2: Any contacts_enabled: bool locked_axes: JointAxesMask coupled_axes: JointAxesMask @@ -388,6 +451,9 @@ class GenericJoint: def set_motor_max_force(self, axis: JointAxis, max_force: float) -> None: ... def set_motor_model(self, axis: JointAxis, model: MotorModel) -> None: ... def lock_axes(self, axes: JointAxesMask) -> None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> GenericJoint: ... class GenericJointBuilder: def __init__(self, locked_axes: JointAxesMask | None = None) -> None: ... @@ -409,6 +475,9 @@ class GenericJointBuilder: def user_data(self, d: int) -> GenericJointBuilder: ... def softness(self, v: SpringCoefficients) -> GenericJointBuilder: ... def build(self) -> GenericJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> GenericJointBuilder: ... class FixedJoint: def __init__(self) -> None: ... @@ -422,6 +491,9 @@ class FixedJoint: @property def data(self) -> GenericJoint: ... softness: SpringCoefficients + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> FixedJoint: ... class FixedJointBuilder: def __init__(self) -> None: ... @@ -432,6 +504,9 @@ class FixedJointBuilder: def contacts_enabled(self, b: bool) -> FixedJointBuilder: ... def softness(self, v: SpringCoefficients) -> FixedJointBuilder: ... def build(self) -> FixedJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> FixedJointBuilder: ... class RevoluteJoint: def __init__(self, axis: Any | None = None) -> None: ... @@ -458,6 +533,9 @@ class RevoluteJoint: def data(self) -> GenericJoint: ... softness: SpringCoefficients def angle(self, body_rotation1: Any, body_rotation2: Any) -> float: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RevoluteJoint: ... class RevoluteJointBuilder: def __init__(self, axis: Any | None = None) -> None: ... @@ -474,6 +552,9 @@ class RevoluteJointBuilder: def motor_model(self, model: MotorModel) -> RevoluteJointBuilder: ... def softness(self, v: SpringCoefficients) -> RevoluteJointBuilder: ... def build(self) -> RevoluteJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RevoluteJointBuilder: ... class PrismaticJoint: def __init__(self, axis: Any) -> None: ... @@ -495,6 +576,9 @@ class PrismaticJoint: @property def data(self) -> GenericJoint: ... softness: SpringCoefficients + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> PrismaticJoint: ... class PrismaticJointBuilder: def __init__(self, axis: Any) -> None: ... @@ -506,10 +590,14 @@ class PrismaticJointBuilder: def contacts_enabled(self, b: bool) -> PrismaticJointBuilder: ... def motor_velocity(self, target_vel: float, factor: float) -> PrismaticJointBuilder: ... def motor_position(self, target_pos: float, stiffness: float, damping: float) -> PrismaticJointBuilder: ... + def motor(self, target_pos: float, target_vel: float, stiffness: float, damping: float) -> PrismaticJointBuilder: ... def motor_max_force(self, max_force: float) -> PrismaticJointBuilder: ... def motor_model(self, model: MotorModel) -> PrismaticJointBuilder: ... def softness(self, v: SpringCoefficients) -> PrismaticJointBuilder: ... def build(self) -> PrismaticJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> PrismaticJointBuilder: ... class SphericalJoint: def __init__(self) -> None: ... @@ -531,6 +619,9 @@ class SphericalJoint: @property def data(self) -> GenericJoint: ... softness: SpringCoefficients + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> SphericalJoint: ... class SphericalJointBuilder: def __init__(self) -> None: ... @@ -547,6 +638,9 @@ class SphericalJointBuilder: def limits(self, axis: JointAxis, min: float, max: float) -> SphericalJointBuilder: ... def softness(self, v: SpringCoefficients) -> SphericalJointBuilder: ... def build(self) -> SphericalJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> SphericalJointBuilder: ... class RopeJoint: def __init__(self, max_distance: float = 1.0) -> None: ... @@ -558,18 +652,35 @@ class RopeJoint: local_anchor2: Any @property def min_distance(self) -> float: ... + def motor(self) -> JointMotor | None: ... + def set_motor_velocity(self, target_vel: float, factor: float) -> None: ... + def set_motor_position(self, target_pos: float, stiffness: float, damping: float) -> None: ... + def set_motor(self, target_pos: float, target_vel: float, stiffness: float, damping: float) -> None: ... + def set_motor_max_force(self, max_force: float) -> None: ... + def set_motor_model(self, model: MotorModel) -> None: ... @property def data(self) -> GenericJoint: ... softness: SpringCoefficients + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RopeJoint: ... class RopeJointBuilder: def __init__(self, max_distance: float = 1.0) -> None: ... def local_anchor1(self, p: Any) -> RopeJointBuilder: ... def local_anchor2(self, p: Any) -> RopeJointBuilder: ... def max_distance(self, d: float) -> RopeJointBuilder: ... + def motor_velocity(self, target_vel: float, factor: float) -> RopeJointBuilder: ... + def motor_position(self, target_pos: float, stiffness: float, damping: float) -> RopeJointBuilder: ... + def motor(self, target_pos: float, target_vel: float, stiffness: float, damping: float) -> RopeJointBuilder: ... + def motor_max_force(self, max_force: float) -> RopeJointBuilder: ... + def motor_model(self, model: MotorModel) -> RopeJointBuilder: ... def contacts_enabled(self, b: bool) -> RopeJointBuilder: ... def softness(self, v: SpringCoefficients) -> RopeJointBuilder: ... def build(self) -> RopeJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RopeJointBuilder: ... class SpringJoint: def __init__(self, rest_length: float = 0.0, stiffness: float = 1.0, damping: float = 0.0) -> None: ... @@ -589,6 +700,9 @@ class SpringJoint: def set_spring_model(self, model: MotorModel) -> None: ... @property def data(self) -> GenericJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> SpringJoint: ... class SpringJointBuilder: def __init__(self, rest_length: float = 0.0, stiffness: float = 1.0, damping: float = 0.0) -> None: ... @@ -597,6 +711,9 @@ class SpringJointBuilder: def contacts_enabled(self, b: bool) -> SpringJointBuilder: ... def spring_model(self, m: MotorModel) -> SpringJointBuilder: ... def build(self) -> SpringJoint: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> SpringJointBuilder: ... class ImpulseJoint: @property @@ -605,12 +722,17 @@ class ImpulseJoint: def body2(self) -> RigidBodyHandle: ... @property def data(self) -> GenericJoint: ... + @data.setter + def data(self, data: GenericJoint) -> None: ... @property def impulses(self) -> list[float]: ... class ImpulseJointSet: def __init__(self) -> None: ... def insert(self, body1: RigidBodyHandle, body2: RigidBodyHandle, joint: Any, wake_up: bool = True) -> ImpulseJointHandle: ... + def set_bodies( + self, handle: ImpulseJointHandle, body1: RigidBodyHandle, body2: RigidBodyHandle, wake_up: bool = True + ) -> bool: ... def remove(self, handle: ImpulseJointHandle, wake_up: bool = True) -> ImpulseJoint | None: ... def get(self, handle: ImpulseJointHandle) -> ImpulseJoint | None: ... def attached_joints(self, body: RigidBodyHandle) -> Any: ... @@ -620,6 +742,9 @@ class ImpulseJointSet: def __getitem__(self, handle: ImpulseJointHandle) -> ImpulseJoint: ... def __contains__(self, handle: ImpulseJointHandle) -> bool: ... def __iter__(self) -> Any: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> ImpulseJointSet: ... class MultibodyLink: @property @@ -629,6 +754,10 @@ class MultibodyLink: @property def parent_id(self) -> int | None: ... def is_root(self) -> bool: ... + @property + def coords(self) -> list[float]: ... + @property + def joint_rot(self) -> Rotation3: ... class Multibody: @property @@ -645,6 +774,7 @@ class Multibody: def set_damping(self, values: list[float]) -> None: ... def generalized_velocity(self) -> list[float]: ... def set_generalized_velocity(self, values: list[float]) -> None: ... + def apply_displacements(self, displacements: Sequence[float]) -> None: ... def generalized_acceleration(self) -> list[float]: ... def joint_velocity(self, link: MultibodyLink) -> list[float]: ... def body_jacobian(self, link_id: int) -> list[list[float]]: ... @@ -652,6 +782,20 @@ class Multibody: def update_rigid_bodies(self, bodies: RigidBodySet, update_mass_properties: bool) -> None: ... def forward_kinematics(self, bodies: RigidBodySet, read_root_pose_from_rigid_body: bool = False) -> None: ... +class MultibodyJoint: + @property + def data(self) -> GenericJoint: ... + @data.setter + def data(self, data: GenericJoint) -> None: ... + @property + def kinematic(self) -> bool: ... + @property + def coords(self) -> list[float]: ... + @property + def link_id(self) -> int: ... + @property + def multibody(self) -> Multibody: ... + class MultibodyJointSet: def __init__(self) -> None: ... def insert(self, parent: RigidBodyHandle, link_body: RigidBodyHandle, joint: Any, wake_up: bool = True) -> MultibodyJointHandle | None: ... @@ -666,11 +810,304 @@ class MultibodyJointSet: ) -> tuple[MultibodyJointHandle, Multibody, MultibodyLink] | None: ... def attached_joints(self, body: RigidBodyHandle) -> Any: ... def inverse_kinematics_for_link( - self, bodies: Any, handle: MultibodyJointHandle, - target_pose: Any, option: InverseKinematicsOption | None = None, + self, bodies: RigidBodySet, handle: MultibodyJointHandle, + target_pose: IsometryLike, option: InverseKinematicsOption | None = None, + joint_can_move: Callable[[MultibodyLink], bool] | None = None, ) -> list[float]: ... def __len__(self) -> int: ... - def __iter__(self) -> Any: ... + def __getitem__(self, handle: MultibodyJointHandle) -> MultibodyJoint: ... + def __iter__(self) -> Iterator[MultibodyJointHandle]: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> MultibodyJointSet: ... + +# ---- Loaders (re-exported by `rapier3d.loaders.mesh` / `.urdf` / `.mjcf`) ---- +from typing import Callable, Sequence + +class LoadedShape: + @property + def shape(self) -> SharedShape: ... + @property + def pose(self) -> Isometry3: ... + @property + def vertices(self) -> np.ndarray: ... + @property + def indices(self) -> np.ndarray: ... + +def load_from_path( + path: str, converter: MeshConverter | None = None, scale: float = 1.0 +) -> list[LoadedShape | MeshConversionError]: ... +def load_from_raw_mesh( + vertices: Any, indices: Any, converter: MeshConverter | None = None, scale: float = 1.0 +) -> LoadedShape: ... + +class UrdfMultibodyOptions: + JOINTS_ARE_KINEMATIC: UrdfMultibodyOptions + DISABLE_SELF_CONTACTS: UrdfMultibodyOptions + def __init__(self, bits: int = 0) -> None: ... + @staticmethod + def empty() -> UrdfMultibodyOptions: ... + @property + def bits(self) -> int: ... + def __or__(self, other: UrdfMultibodyOptions) -> UrdfMultibodyOptions: ... + def __and__(self, other: UrdfMultibodyOptions) -> UrdfMultibodyOptions: ... + def __contains__(self, other: UrdfMultibodyOptions) -> bool: ... + +class UrdfLoaderOptions: + 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: TriMeshFlags + mesh_converter: MeshConverter | None + shift: Isometry3 + scale: float + squeeze_empty_fixed_links: bool + collider_blueprint: ColliderBuilder | None + rigid_body_blueprint: RigidBodyBuilder | None + def __init__( + self, + create_colliders_from_collision_shapes: bool = True, + create_colliders_from_visual_shapes: bool = False, + apply_imported_mass_props: bool = True, + enable_joint_collisions: bool = False, + make_roots_fixed: bool = False, + trimesh_flags: TriMeshFlags | None = None, + mesh_converter: MeshConverter | None = None, + shift: IsometryLike | None = None, + scale: float = 1.0, + squeeze_empty_fixed_links: bool = True, + collider_blueprint: ColliderBuilder | None = None, + rigid_body_blueprint: RigidBodyBuilder | None = None, + ) -> None: ... + +class UrdfLink: + @property + def name(self) -> str: ... + @property + def n_visuals(self) -> int: ... + @property + def n_collisions(self) -> int: ... + @property + def mass(self) -> float: ... + +class UrdfJoint: + @property + def name(self) -> str: ... + @property + def joint_type(self) -> str: ... + @property + def parent(self) -> str: ... + @property + def child(self) -> str: ... + @property + def axis(self) -> list[float]: ... + +class Robot: + @property + def name(self) -> str: ... + @property + def links(self) -> list[UrdfLink]: ... + @property + def joints(self) -> list[UrdfJoint]: ... + +class UrdfVisual: + @property + def shape(self) -> SharedShape: ... + @property + def local_pose(self) -> Isometry3: ... + +class UrdfColliderHandle: + @property + def handle(self) -> ColliderHandle: ... + @property + def visual(self) -> UrdfVisual | None: ... + +class UrdfLinkHandle: + @property + def body(self) -> RigidBodyHandle: ... + @property + def colliders(self) -> list[UrdfColliderHandle]: ... + +class UrdfJointHandle: + @property + def joint(self) -> ImpulseJointHandle | MultibodyJointHandle | None: ... + @property + def link1(self) -> RigidBodyHandle: ... + @property + def link2(self) -> RigidBodyHandle: ... + +class UrdfRobotHandles: + @property + def links(self) -> list[UrdfLinkHandle]: ... + @property + def joints(self) -> list[UrdfJointHandle]: ... + +class UrdfRobot: + @staticmethod + def from_file( + path: str, options: UrdfLoaderOptions | None = None, mesh_dir: str | None = None + ) -> tuple[UrdfRobot, Robot]: ... + @staticmethod + def from_str( + xml: str, options: UrdfLoaderOptions | None = None, mesh_dir: str | None = None + ) -> tuple[UrdfRobot, Robot]: ... + @staticmethod + def from_robot( + robot: Robot, options: UrdfLoaderOptions | None = None, mesh_dir: str | None = None + ) -> UrdfRobot: ... + @property + def n_links(self) -> int: ... + @property + def n_joints(self) -> int: ... + def append_transform(self, iso: IsometryLike) -> None: ... + def insert_using_impulse_joints( + self, bodies: RigidBodySet, colliders: ColliderSet, joints: ImpulseJointSet + ) -> UrdfRobotHandles: ... + def insert_using_multibody_joints( + self, + bodies: RigidBodySet, + colliders: ColliderSet, + multibody_joints: MultibodyJointSet, + options: UrdfMultibodyOptions | None = None, + ) -> UrdfRobotHandles: ... + +class MjcfMultibodyOptions: + JOINTS_ARE_KINEMATIC: MjcfMultibodyOptions + DISABLE_SELF_CONTACTS: MjcfMultibodyOptions + SKIP_LOOP_CLOSURES: MjcfMultibodyOptions + SKIP_JOINT_MOTORS: MjcfMultibodyOptions + SKIP_JOINT_LIMITS: MjcfMultibodyOptions + SKIP_JOINT_SPRINGS: MjcfMultibodyOptions + def __init__(self, bits: int = 0) -> None: ... + @staticmethod + def empty() -> MjcfMultibodyOptions: ... + @property + def bits(self) -> int: ... + def __or__(self, other: MjcfMultibodyOptions) -> MjcfMultibodyOptions: ... + def __and__(self, other: MjcfMultibodyOptions) -> MjcfMultibodyOptions: ... + def __contains__(self, other: MjcfMultibodyOptions) -> bool: ... + +class ContactFilterMode: + Symmetric: ContactFilterMode + Asymmetric: ContactFilterMode + +class MjcfLoaderOptions: + 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: TriMeshFlags + mesh_converter: MeshConverter | None + shift: Isometry3 + scale: float + skip_plane_geoms: bool + disable_joint_motors: bool + collider_blueprint: ColliderBuilder | None + rigid_body_blueprint: RigidBodyBuilder | None + contact_filter_mode: ContactFilterMode + def __init__( + self, + create_colliders_from_collision_shapes: bool = True, + create_colliders_from_visual_shapes: bool = False, + apply_imported_mass_props: bool = True, + enable_joint_collisions: bool = False, + make_roots_fixed: bool = False, + trimesh_flags: TriMeshFlags | None = None, + mesh_converter: MeshConverter | None = None, + shift: IsometryLike | None = None, + scale: float = 1.0, + skip_plane_geoms: bool = True, + disable_joint_motors: bool = False, + collider_blueprint: ColliderBuilder | None = None, + rigid_body_blueprint: RigidBodyBuilder | None = None, + contact_filter_mode: ContactFilterMode = ..., + ) -> None: ... + +class MjcfModel: + @property + def name(self) -> str | None: ... + +class MjcfColliderHandle: + @property + def handle(self) -> ColliderHandle: ... + +class MjcfBodyHandle: + @property + def body(self) -> RigidBodyHandle: ... + @property + def colliders(self) -> list[MjcfColliderHandle]: ... + +class MjcfJointHandle: + @property + def joint(self) -> ImpulseJointHandle | MultibodyJointHandle | None: ... + @property + def link1(self) -> RigidBodyHandle: ... + @property + def link2(self) -> RigidBodyHandle: ... + +class MjcfActuatorHandle: + @property + def name(self) -> str | None: ... + @property + def joint(self) -> ImpulseJointHandle | MultibodyJointHandle | None: ... + +class MjcfContactHooks: + def filter_contact_pair(self, ctx: PairFilterContext) -> SolverFlags | None: ... + def modify_solver_contacts(self, ctx: ContactModificationContext) -> None: ... + +class MjcfRobotHandles: + @property + def bodies(self) -> list[MjcfBodyHandle | None]: ... + @property + def joints(self) -> list[MjcfJointHandle]: ... + @property + def equality_joints(self) -> list[MjcfJointHandle]: ... + @property + def actuators(self) -> list[MjcfActuatorHandle]: ... + @property + def keyframe_names(self) -> list[str | None]: ... + def apply_controls( + self, + bodies: RigidBodySet, + joints: ImpulseJointSet | MultibodyJointSet, + ctrl: Sequence[float], + gain_scale: float = 1.0, + ) -> None: ... + def apply_keyframe( + self, + bodies: RigidBodySet, + multibody_joints: MultibodyJointSet | None = None, + keyframe: int | str = 0, + ) -> None: ... + def keyframe_controls(self, keyframe: int | str) -> list[float]: ... + def contact_hooks(self) -> MjcfContactHooks: ... + +class MjcfRobot: + @staticmethod + def from_file(path: str, options: MjcfLoaderOptions | None = None) -> tuple[MjcfRobot, MjcfModel]: ... + @staticmethod + def from_str( + xml: str, options: MjcfLoaderOptions | None = None, base_dir: str | None = None + ) -> tuple[MjcfRobot, MjcfModel]: ... + @property + def gravity(self) -> Vec3: ... + @property + def keyframe_names(self) -> list[str | None]: ... + def append_transform(self, transform: IsometryLike) -> None: ... + def insert_using_impulse_joints( + self, bodies: RigidBodySet, colliders: ColliderSet, impulse_joints: ImpulseJointSet + ) -> MjcfRobotHandles: ... + def insert_using_multibody_joints( + self, + bodies: RigidBodySet, + colliders: ColliderSet, + multibody_joints: MultibodyJointSet, + impulse_joints: ImpulseJointSet, + options: MjcfMultibodyOptions | None = None, + ) -> MjcfRobotHandles: ... class RigidBodyHandle: def __init__(self, index: int = 0, generation: int = 0) -> None: ... @@ -725,11 +1162,11 @@ class 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: ... + stiffness: float # Deprecated alias of `natural_frequency`. + damping: float # Deprecated alias of `damping_ratio`. + def __init__(self, natural_frequency: float = 30.0, damping_ratio: float = 10.0) -> None: ... @staticmethod def contact_defaults() -> SpringCoefficients: ... @staticmethod @@ -768,6 +1205,11 @@ class MassProperties: def transform_by(self, iso: IsometryLike) -> MassProperties: ... def __add__(self, other: MassProperties) -> MassProperties: ... def __sub__(self, other: MassProperties) -> MassProperties: ... + @property + def inv_principal_inertia_sqrt(self) -> Vec3: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> MassProperties: ... class RigidBodyAdditionalMassProps: @staticmethod @@ -825,6 +1267,10 @@ class IntegrationParameters: contact_softness: SpringCoefficients static_contact_softness: SpringCoefficients friction_model: FrictionModel + friction_in_bias_pass: bool + """Also solve friction during the biased pass of each substep (default: False).""" + warmstart_joints: bool + """Warm-start the impulse joints like contacts (default: False).""" soft_bodies: SoftBodiesSettings def __init__(self) -> None: ... @staticmethod @@ -837,6 +1283,11 @@ class IntegrationParameters: def contact_spring_damping(self) -> float: ... def joint_spring_stiffness(self) -> float: ... def joint_spring_damping(self) -> float: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> IntegrationParameters: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... class IslandManager: def __init__(self) -> None: ... @@ -846,11 +1297,17 @@ class IslandManager: def contains_handle(self, h: RigidBodyHandle) -> bool: ... def same_island(self, h1: RigidBodyHandle, h2: RigidBodyHandle) -> bool: ... def active_set_offset(self, h: RigidBodyHandle) -> int: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> IslandManager: ... class CCDSolver: def __init__(self) -> None: ... def clear(self) -> None: ... def solve_ccd(self, *args: Any, **kwargs: Any) -> None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> CCDSolver: ... class RigidBodyBuilder: def __init__(self, body_type: RigidBodyType = ...) -> None: ... @@ -868,6 +1325,8 @@ class 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 additional_pgs_iterations(self, n: int) -> RigidBodyBuilder: ... + def allow_fast_rotation(self, b: bool) -> RigidBodyBuilder: ... def user_data(self, d: int) -> RigidBodyBuilder: ... def enabled(self, b: bool) -> RigidBodyBuilder: ... def additional_mass(self, m: float) -> RigidBodyBuilder: ... @@ -879,6 +1338,9 @@ class RigidBodyBuilder: def restrict_rotations(self, t: tuple[bool, bool, bool]) -> RigidBodyBuilder: ... def gyroscopic_forces(self, b: bool) -> RigidBodyBuilder: ... def build(self) -> RigidBody: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RigidBodyBuilder: ... class RigidBody: position: Isometry3 @@ -903,6 +1365,7 @@ class RigidBody: is_enabled: bool ccd_enabled: bool soft_ccd_prediction: float + allow_fast_rotation: bool gyroscopic_forces_enabled: bool activation: RigidBodyActivation @property @@ -949,7 +1412,10 @@ class RigidBody: 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 add_gravitational_force(self, gravity: VectorLike, wake_up: bool = True) -> None: ... + def set_next_kinematic_translation(self, translation: VectorLike) -> None: ... + def set_next_kinematic_rotation(self, rotation: RotationLike) -> None: ... + def set_next_kinematic_position(self, pose: IsometryLike) -> 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: ... @@ -963,6 +1429,9 @@ class RigidBody: 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: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RigidBody: ... class RigidBodySet: def __init__(self) -> None: ... @@ -984,6 +1453,9 @@ class RigidBodySet: def __contains__(self, handle: RigidBodyHandle) -> bool: ... def __iter__(self) -> Iterator[Tuple[RigidBodyHandle, RigidBody]]: ... def __len__(self) -> int: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> RigidBodySet: ... # =========================================================================== # Phase 04 — Geometry, Shapes, Broad/Narrow Phase @@ -1057,6 +1529,7 @@ class ActiveHooks: def contains(self, other: ActiveHooks) -> bool: ... def __or__(self, other: ActiveHooks) -> ActiveHooks: ... def __and__(self, other: ActiveHooks) -> ActiveHooks: ... + def is_empty(self) -> bool: ... class ActiveCollisionTypes: @@ -1130,12 +1603,17 @@ class Group: def __and__(self, other: Group) -> Group: ... def __xor__(self, other: Group) -> Group: ... def __invert__(self) -> Group: ... + def is_empty(self) -> bool: ... class InteractionTestMode: - DEFAULT: InteractionTestMode - ONLY_DYNAMIC: InteractionTestMode AND: InteractionTestMode OR: InteractionTestMode + # Deprecated aliases of `AND`. + DEFAULT: InteractionTestMode + ONLY_DYNAMIC: InteractionTestMode + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... class InteractionGroups: memberships: Group @@ -1155,6 +1633,9 @@ class InteractionGroups: def with_memberships(self, g: Group) -> InteractionGroups: ... def with_filter(self, g: Group) -> InteractionGroups: ... def test(self, other: InteractionGroups) -> bool: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> InteractionGroups: ... class CollisionEventFlags: SENSOR: CollisionEventFlags @@ -1163,6 +1644,9 @@ class CollisionEventFlags: @staticmethod def empty() -> CollisionEventFlags: ... def contains(self, other: CollisionEventFlags) -> bool: ... + @property + def bits(self) -> int: ... + def is_empty(self) -> bool: ... class TriMeshFlags: HALF_EDGE_TOPOLOGY: TriMeshFlags @@ -1173,9 +1657,18 @@ class TriMeshFlags: DELETE_DEGENERATE_TRIANGLES: TriMeshFlags DELETE_DUPLICATE_TRIANGLES: TriMeshFlags FIX_INTERNAL_EDGES: TriMeshFlags + DEFORMABLE: TriMeshFlags + FIX_INTERNAL_EDGES_TWO_SIDED: TriMeshFlags def __init__(self, bits: int = 0) -> None: ... @staticmethod def empty() -> TriMeshFlags: ... + @property + def bits(self) -> int: ... + def contains(self, other: TriMeshFlags) -> bool: ... + def is_empty(self) -> bool: ... + def __contains__(self, other: TriMeshFlags) -> bool: ... + def __or__(self, other: TriMeshFlags) -> TriMeshFlags: ... + def __and__(self, other: TriMeshFlags) -> TriMeshFlags: ... class ColliderMaterial: friction: float @@ -1192,6 +1685,7 @@ class ColliderFlags: @property def enabled(self) -> ColliderEnabled: ... def __init__(self) -> None: ... + active_collision_types: ActiveCollisionTypes class ColliderParent: handle: RigidBodyHandle @@ -1213,11 +1707,13 @@ class ColliderMassProps: class Ball: radius: float def __init__(self, radius: float) -> None: ... + def to_trimesh(self, ntheta_subdiv: int, nphi_subdiv: int) -> Tuple[np.ndarray, np.ndarray]: ... class Cuboid: @property def half_extents(self) -> Vec3: ... def __init__(self, half_extents: VectorLike) -> None: ... + def to_trimesh(self) -> Tuple[np.ndarray, np.ndarray]: ... class Capsule: @property @@ -1231,16 +1727,19 @@ class Capsule: @property def b(self) -> Point3: ... def __init__(self, a: VectorLike, b: VectorLike, radius: float) -> None: ... + def to_trimesh(self, ntheta_subdiv: int, nphi_subdiv: int) -> Tuple[np.ndarray, np.ndarray]: ... class Cylinder: half_height: float radius: float def __init__(self, half_height: float, radius: float) -> None: ... + def to_trimesh(self, nsubdiv: int) -> Tuple[np.ndarray, np.ndarray]: ... class Cone: half_height: float radius: float def __init__(self, half_height: float, radius: float) -> None: ... + def to_trimesh(self, nsubdiv: int) -> Tuple[np.ndarray, np.ndarray]: ... class Triangle: @property @@ -1264,6 +1763,13 @@ class TriMesh: class HeightField: @property def scale(self) -> Vec3: ... + @property + def heights(self) -> np.ndarray: ... + @property + def nrows(self) -> int: ... + @property + def ncols(self) -> int: ... + def to_trimesh(self) -> Tuple[np.ndarray, np.ndarray]: ... class ConvexPolyhedron: @property @@ -1271,6 +1777,7 @@ class ConvexPolyhedron: def num_points(self) -> int: ... @property def indices(self) -> np.ndarray: ... + def to_trimesh(self) -> Tuple[np.ndarray, np.ndarray]: ... class Compound: def num_shapes(self) -> int: ... @@ -1282,6 +1789,17 @@ class Voxels: @property def centers(self) -> np.ndarray: ... def num_voxels(self) -> int: ... + def to_trimesh(self) -> Tuple[np.ndarray, np.ndarray]: ... + +class FillMode: + SURFACE_ONLY: FillMode + @staticmethod + def flood_fill(detect_cavities: bool = False) -> FillMode: ... + @property + def is_flood_fill(self) -> bool: ... + @property + def detect_cavities(self) -> bool: ... + def __eq__(self, other: object) -> bool: ... class Aabb: @property @@ -1333,27 +1851,38 @@ class SharedShape: @staticmethod def halfspace(outward_normal: VectorLike) -> SharedShape: ... @staticmethod - def trimesh(vertices: np.ndarray, indices: np.ndarray, flags: TriMeshFlags | None = None) -> SharedShape: ... + def segment(a: VectorLike, b: VectorLike) -> SharedShape: ... @staticmethod - def convex_hull(points: np.ndarray) -> SharedShape: ... + def polyline(vertices: PointsLike, indices: IndicesLike | None = None) -> SharedShape: ... @staticmethod - def convex_polyhedron(points: np.ndarray) -> SharedShape: ... + def trimesh(vertices: PointsLike, indices: IndicesLike, flags: TriMeshFlags | None = None) -> SharedShape: ... @staticmethod - def convex_decomposition(vertices: np.ndarray, indices: np.ndarray) -> SharedShape: ... + def convex_hull(points: PointsLike) -> SharedShape: ... + @staticmethod + def convex_polyhedron(points: PointsLike) -> SharedShape: ... + @staticmethod + def convex_decomposition(vertices: PointsLike, indices: IndicesLike) -> SharedShape: ... @staticmethod def round_triangle(a: VectorLike, b: VectorLike, c: VectorLike, border_radius: float) -> SharedShape: ... @staticmethod - def round_convex_hull(points: np.ndarray, border_radius: float) -> SharedShape: ... + def round_convex_hull(points: PointsLike, border_radius: float) -> SharedShape: ... @staticmethod - def convex_mesh(vertices: np.ndarray, indices: np.ndarray) -> SharedShape: ... + def convex_mesh(vertices: PointsLike, indices: IndicesLike) -> SharedShape: ... @staticmethod - def round_convex_mesh(vertices: np.ndarray, indices: np.ndarray, border_radius: float) -> SharedShape: ... + def round_convex_mesh(vertices: PointsLike, indices: IndicesLike, border_radius: float) -> SharedShape: ... @staticmethod - def voxels(voxel_size: VectorLike, grid_coords: list[tuple[int, int, int]]) -> SharedShape: ... + def voxels(voxel_size: VectorLike, grid_coords: IndicesLike) -> SharedShape: ... @staticmethod - def voxels_from_points(voxel_size: VectorLike, points: np.ndarray) -> SharedShape: ... + def voxels_from_points(voxel_size: VectorLike, points: PointsLike) -> SharedShape: ... @staticmethod - def heightfield(heights: np.ndarray, scale: VectorLike) -> SharedShape: ... + def voxelized_mesh( + vertices: PointsLike, + indices: IndicesLike, + voxel_size: float, + fill_mode: FillMode | None = None, + ) -> SharedShape: ... + @staticmethod + def heightfield(heights: HeightsLike, scale: VectorLike) -> SharedShape: ... @staticmethod def compound(parts: list[tuple[IsometryLike, SharedShape]]) -> SharedShape: ... def compute_aabb(self, pose: IsometryLike) -> Aabb: ... @@ -1371,6 +1900,14 @@ class SharedShape: def as_convex_polyhedron(self) -> ConvexPolyhedron | None: ... def as_compound(self) -> Compound | None: ... def as_voxels(self) -> Voxels | None: ... + def as_round_cuboid(self) -> Cuboid | None: ... + def as_round_triangle(self) -> Triangle | None: ... + def as_round_cylinder(self) -> Cylinder | None: ... + def as_round_cone(self) -> Cone | None: ... + def as_round_convex_polyhedron(self) -> ConvexPolyhedron | None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> SharedShape: ... class MeshConverter: TRIMESH: MeshConverter @@ -1405,6 +1942,9 @@ class ColliderBuilder: def user_data(self, d: int) -> ColliderBuilder: ... def enabled(self, b: bool) -> ColliderBuilder: ... def build(self) -> Collider: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> ColliderBuilder: ... class Collider: position: Isometry3 @@ -1429,6 +1969,18 @@ class Collider: contact_force_event_threshold: float user_data: int @property + def position_wrt_parent(self) -> Isometry3 | None: ... + @position_wrt_parent.setter + def position_wrt_parent(self, p: IsometryLike) -> None: ... + @property + def translation_wrt_parent(self) -> Vec3 | None: ... + @translation_wrt_parent.setter + def translation_wrt_parent(self, v: VectorLike) -> None: ... + @property + def rotation_wrt_parent(self) -> Rotation3 | None: ... + @rotation_wrt_parent.setter + def rotation_wrt_parent(self, r: RotationLike) -> None: ... + @property def parent(self) -> RigidBodyHandle | None: ... @property def material(self) -> ColliderMaterial: ... @@ -1441,58 +1993,81 @@ class Collider: @staticmethod def cuboid(hx: float, hy: float, hz: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def capsule(half_height: float, radius: float) -> ColliderBuilder: ... + def capsule(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... + @staticmethod + def capsule_x(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... + @staticmethod + def capsule_y(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... + @staticmethod + def capsule_z(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def capsule_x(half_height: float, radius: float) -> ColliderBuilder: ... + def capsule_from_endpoints(a: VectorLike, b: VectorLike, radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def capsule_y(half_height: float, radius: float) -> ColliderBuilder: ... + def cylinder(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def capsule_z(half_height: float, radius: float) -> ColliderBuilder: ... + def cone(half_height: float, radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def capsule_from_endpoints(a: VectorLike, b: VectorLike, radius: float) -> ColliderBuilder: ... + def round_cuboid(hx: float, hy: float, hz: float, border_radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def cylinder(half_height: float, radius: float) -> ColliderBuilder: ... + def round_cylinder(half_height: float, radius: float, border_radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def cone(half_height: float, radius: float) -> ColliderBuilder: ... + def round_cone(half_height: float, radius: float, border_radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def round_cuboid(hx: float, hy: float, hz: float, border_radius: float) -> ColliderBuilder: ... + def triangle(a: VectorLike, b: VectorLike, c: VectorLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def round_cylinder(half_height: float, radius: float, border_radius: float) -> ColliderBuilder: ... + def segment(a: VectorLike, b: VectorLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def round_cone(half_height: float, radius: float, border_radius: float) -> ColliderBuilder: ... + def polyline(vertices: PointsLike, indices: IndicesLike | None = None, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def triangle(a: VectorLike, b: VectorLike, c: VectorLike) -> ColliderBuilder: ... + def trimesh( + vertices: PointsLike, indices: IndicesLike, flags: TriMeshFlags | None = None, **kwargs: Any + ) -> ColliderBuilder: ... @staticmethod - def trimesh(vertices: np.ndarray, indices: np.ndarray, flags: TriMeshFlags | None = None) -> ColliderBuilder: ... + def convex_hull(points: PointsLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def convex_hull(points: np.ndarray) -> ColliderBuilder: ... + def convex_polyhedron(points: PointsLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def convex_polyhedron(points: np.ndarray) -> ColliderBuilder: ... + def convex_decomposition(vertices: PointsLike, indices: IndicesLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def convex_decomposition(vertices: np.ndarray, indices: np.ndarray) -> ColliderBuilder: ... + def round_triangle( + a: VectorLike, b: VectorLike, c: VectorLike, border_radius: float, **kwargs: Any + ) -> ColliderBuilder: ... @staticmethod - def round_triangle(a: VectorLike, b: VectorLike, c: VectorLike, border_radius: float) -> ColliderBuilder: ... + def round_convex_hull(points: PointsLike, border_radius: float, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def round_convex_hull(points: np.ndarray, border_radius: float) -> ColliderBuilder: ... + def convex_mesh(vertices: PointsLike, indices: IndicesLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def convex_mesh(vertices: np.ndarray, indices: np.ndarray) -> ColliderBuilder: ... + def round_convex_mesh( + vertices: PointsLike, indices: IndicesLike, border_radius: float, **kwargs: Any + ) -> ColliderBuilder: ... @staticmethod - def round_convex_mesh(vertices: np.ndarray, indices: np.ndarray, border_radius: float) -> ColliderBuilder: ... + def voxels(voxel_size: VectorLike, grid_coords: IndicesLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def voxels(voxel_size: VectorLike, grid_coords: list[tuple[int, int, int]]) -> ColliderBuilder: ... + def voxels_from_points(voxel_size: VectorLike, points: PointsLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def voxels_from_points(voxel_size: VectorLike, points: np.ndarray) -> ColliderBuilder: ... + def voxelized_mesh( + vertices: PointsLike, + indices: IndicesLike, + voxel_size: float, + fill_mode: FillMode | None = None, + **kwargs: Any, + ) -> ColliderBuilder: ... @staticmethod - def converted_trimesh(vertices: np.ndarray, indices: np.ndarray, converter: MeshConverter) -> ColliderBuilder: ... + def converted_trimesh( + vertices: PointsLike, indices: IndicesLike, converter: MeshConverter, **kwargs: Any + ) -> ColliderBuilder: ... @staticmethod - def heightfield(heights: np.ndarray, scale: VectorLike) -> ColliderBuilder: ... + def heightfield(heights: HeightsLike, scale: VectorLike, **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def compound(parts: list[tuple[IsometryLike, SharedShape]]) -> ColliderBuilder: ... + def compound(parts: list[tuple[IsometryLike, SharedShape]], **kwargs: Any) -> ColliderBuilder: ... @staticmethod - def new(shape: SharedShape) -> ColliderBuilder: ... + def new(shape: SharedShape, **kwargs: Any) -> ColliderBuilder: ... def compute_aabb(self) -> Aabb: ... deformable_mesh_ref: SoftMeshRef | None + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> Collider: ... class ColliderSet: def __init__(self) -> None: ... @@ -1508,8 +2083,10 @@ class ColliderSet: handle: ColliderHandle, islands: IslandManager, bodies: RigidBodySet, - wake_parent: bool = True, + wake_up: bool = True, soft_bodies: SoftBodySet | None = None, + *, + wake_parent: bool | None = None, ) -> Collider | None: ... def insert_deformable( self, @@ -1527,6 +2104,9 @@ class ColliderSet: def __contains__(self, handle: ColliderHandle) -> bool: ... def __iter__(self) -> Iterator[Tuple[ColliderHandle, Collider]]: ... def __len__(self) -> int: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> ColliderSet: ... class ContactData: local_p1: Point3 @@ -1545,6 +2125,11 @@ class ContactManifoldData: num_active_contacts: int relative_dominance: int user_data: int + @property + def solver_contacts(self) -> list[SolverContact]: ... + def solver_contact_world_points( + self, contact: SolverContact, bodies: RigidBodySet + ) -> tuple[Point3, Point3]: ... class ContactManifold: data: ContactManifoldData @@ -1580,12 +2165,20 @@ class ColliderPair: def __len__(self) -> int: ... class BroadPhasePairEvent: - added: bool pair: ColliderPair @staticmethod def added(pair: ColliderPair) -> BroadPhasePairEvent: ... @staticmethod def removed(pair: ColliderPair) -> BroadPhasePairEvent: ... + @property + def removed_(self) -> bool: + """True if this is a "Removed" event.""" + @property + def is_added(self) -> bool: + """True if this is an "Added" event.""" + @property + def is_removed(self) -> bool: + """True if this is a "Removed" event.""" class CollisionEvent: started: bool @@ -1624,11 +2217,15 @@ class BroadPhaseBvh: @staticmethod def optimized_for(strategy: BvhOptimizationStrategy) -> BroadPhaseBvh: ... def clear(self) -> None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> BroadPhaseBvh: ... class SolverContact: point: Point3 point2: Point3 dist: float + tangent_velocity: Vec3 contact_id: int is_new: bool @@ -1638,7 +2235,14 @@ class NarrowPhase: def intersection_pair(self, h1: ColliderHandle, h2: ColliderHandle) -> bool | None: ... def contact_pairs(self) -> list[ContactPair]: ... def intersection_pairs(self) -> list[tuple[ColliderHandle, ColliderHandle, bool]]: ... + def contact_pairs_with(self, collider: ColliderHandle) -> list[ContactPair]: ... + def intersection_pairs_with( + self, collider: ColliderHandle + ) -> list[tuple[ColliderHandle, ColliderHandle, bool]]: ... def clear(self) -> None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> NarrowPhase: ... # ---- events / hooks (phase 07) ------------------------------------------- @@ -1657,8 +2261,14 @@ class SolverFlags: def __and__(self, other: SolverFlags) -> SolverFlags: ... def __xor__(self, other: SolverFlags) -> SolverFlags: ... def __invert__(self) -> SolverFlags: ... + @staticmethod + def default_() -> SolverFlags: ... class PairFilterContext: + @property + def colliders(self) -> ColliderSet: ... + @property + def bodies(self) -> RigidBodySet: ... @property def collider1(self) -> ColliderHandle: ... @property @@ -1669,6 +2279,10 @@ class PairFilterContext: def rigid_body2(self) -> RigidBodyHandle | None: ... class ContactModificationContext: + @property + def colliders(self) -> ColliderSet: ... + @property + def bodies(self) -> RigidBodySet: ... @property def collider1(self) -> ColliderHandle: ... @property @@ -1690,6 +2304,16 @@ class ContactModificationContext: def clear_solver_contacts(self) -> None: ... def num_solver_contacts(self) -> int: ... def remove_solver_contact(self, i: int) -> None: ... + def set_solver_contact( + self, + i: int, + *, + point: VectorLike | None = None, + point2: VectorLike | None = None, + dist: float | None = None, + tangent_velocity: VectorLike | None = None, + ) -> None: ... + def set_tangent_velocity(self, velocity: VectorLike) -> None: ... def update_as_oneway_platform(self, allowed_local_n1: Vec3 | tuple[float, float, float], angle: float) -> None: ... class ChannelEventCollector: @@ -1704,6 +2328,167 @@ class ChannelEventCollector: from typing import Callable, Literal +# ---- scene queries -------------------------------------------------------- + +class FeatureId: + kind: Literal["vertex", "edge", "face", "unknown"] + id: int + @staticmethod + def Vertex(id: int) -> FeatureId: ... + @staticmethod + def Edge(id: int) -> FeatureId: ... + @staticmethod + def Face(id: int) -> FeatureId: ... + @staticmethod + def Unknown() -> FeatureId: ... + @property + def is_vertex(self) -> bool: ... + @property + def is_edge(self) -> bool: ... + @property + def is_face(self) -> bool: ... + @property + def is_unknown(self) -> bool: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + +class Ray: + def __init__(self, origin: VectorLike, dir: VectorLike) -> None: ... + @property + def origin(self) -> Point3: ... + @property + def dir(self) -> Vec3: ... + def point_at(self, t: float) -> Point3: ... + +class RayIntersection: + toi: float + normal: Vec3 + feature: FeatureId + @property + def time_of_impact(self) -> float: ... + +class PointProjection: + is_inside: bool + point: Point3 + +class ShapeCastStatus(enum.IntEnum): + OUT_OF_ITERATIONS = ... + CONVERGED = ... + FAILED = ... + PENETRATING_OR_WITHIN_TARGET_DIST = ... + +class ShapeCastOptions: + max_time_of_impact: float + target_distance: float + stop_at_penetration: bool + compute_impact_geometry_on_penetration: bool + def __init__( + self, + max_time_of_impact: float = ..., + target_distance: float = 0.0, + stop_at_penetration: bool = True, + compute_impact_geometry_on_penetration: bool = True, + ) -> None: ... + @staticmethod + def with_max_time_of_impact(t: float) -> ShapeCastOptions: ... + +class ShapeCastHit: + time_of_impact: float + # On the hit collider, in world space. + witness1: Point3 + normal1: Vec3 + # On the cast shape, in its local space. + witness2: Point3 + normal2: Vec3 + status: ShapeCastStatus + +class NonlinearRigidMotion: + def __init__( + self, + start: IsometryLike | None = None, + local_center: VectorLike | None = None, + linvel: VectorLike | None = None, + angvel: VectorLike | None = None, + ) -> None: ... + @staticmethod + def identity() -> NonlinearRigidMotion: ... + @staticmethod + def constant_position(pos: IsometryLike) -> NonlinearRigidMotion: ... + @property + def start(self) -> Isometry3: ... + @property + def local_center(self) -> Point3: ... + @property + def linvel(self) -> Vec3: ... + @property + def angvel(self) -> Vec3: ... + def position_at_time(self, t: float) -> Isometry3: ... + +class QueryFilterFlags: + EXCLUDE_FIXED: QueryFilterFlags + EXCLUDE_KINEMATIC: QueryFilterFlags + EXCLUDE_DYNAMIC: QueryFilterFlags + EXCLUDE_SENSORS: QueryFilterFlags + EXCLUDE_SOLIDS: QueryFilterFlags + ONLY_DYNAMIC: QueryFilterFlags + ONLY_KINEMATIC: QueryFilterFlags + ONLY_FIXED: QueryFilterFlags + EXCLUDE_COLLISION_INVALID: QueryFilterFlags + def __init__(self, bits: int = 0) -> None: ... + @staticmethod + def empty() -> QueryFilterFlags: ... + @property + def bits(self) -> int: ... + def contains(self, other: QueryFilterFlags) -> bool: ... + def is_empty(self) -> bool: ... + def __contains__(self, other: QueryFilterFlags) -> bool: ... + def __or__(self, other: QueryFilterFlags) -> QueryFilterFlags: ... + def __and__(self, other: QueryFilterFlags) -> QueryFilterFlags: ... + def __xor__(self, other: QueryFilterFlags) -> QueryFilterFlags: ... + def __invert__(self) -> QueryFilterFlags: ... + def __sub__(self, other: QueryFilterFlags) -> QueryFilterFlags: ... + def __bool__(self) -> bool: ... + +class QueryFilter: + def __init__( + self, + flags: QueryFilterFlags | None = None, + groups: InteractionGroups | None = None, + exclude_collider: ColliderHandle | None = None, + exclude_rigid_body: RigidBodyHandle | None = None, + predicate: Callable[[ColliderHandle, Collider], bool] | None = None, + ) -> None: ... + @staticmethod + def new() -> QueryFilter: ... + @staticmethod + def exclude_fixed() -> QueryFilter: ... + @staticmethod + def exclude_kinematic() -> QueryFilter: ... + @staticmethod + def exclude_dynamic() -> QueryFilter: ... + @staticmethod + def only_dynamic() -> QueryFilter: ... + @staticmethod + def only_kinematic() -> QueryFilter: ... + @staticmethod + def only_fixed() -> QueryFilter: ... + def exclude_sensors(self) -> QueryFilter: ... + def exclude_solids(self) -> QueryFilter: ... + def groups(self, groups: InteractionGroups) -> QueryFilter: ... + def exclude_collider(self, h: ColliderHandle) -> QueryFilter: ... + def exclude_rigid_body(self, h: RigidBodyHandle) -> QueryFilter: ... + def predicate(self, fun: Callable[[ColliderHandle, Collider], bool]) -> QueryFilter: ... + @property + def flags(self) -> QueryFilterFlags: ... + @property + def interaction_groups(self) -> InteractionGroups | None: ... + @property + def exclude_collider_handle(self) -> ColliderHandle | None: ... + @property + def exclude_rigid_body_handle(self) -> RigidBodyHandle | None: ... + @property + def predicate_fn(self) -> Callable[[ColliderHandle, Collider], bool] | None: ... + class CharacterLength: kind: Literal["absolute", "relative"] value: float @@ -1745,6 +2530,8 @@ class CharacterCollision: def translation_remaining(self) -> Vec3: ... @property def toi(self) -> ShapeCastHit: ... + @property + def hit(self) -> ShapeCastHit: ... class KinematicCharacterController: up: Vec3 @@ -1761,17 +2548,17 @@ class KinematicCharacterController: up: Vec3 | tuple[float, float, float] | None = None, offset: CharacterLength | float | None = None, slide: bool | None = None, - autostep: CharacterAutostep | None = None, + autostep: CharacterAutostep | None = ..., max_slope_climb_angle: float | None = None, min_slope_slide_angle: float | None = None, - snap_to_ground: CharacterLength | float | None = None, + snap_to_ground: CharacterLength | float | None = ..., normal_nudge_factor: float | None = None, ) -> None: ... def move_shape( self, dt: float, - bodies: RigidBodySet, - colliders: ColliderSet, + bodies: RigidBodySet | None, + colliders: ColliderSet | None, queries: QueryPipeline, shape: SharedShape, shape_pos: Isometry3, @@ -1786,11 +2573,14 @@ class KinematicCharacterController: colliders: ColliderSet, queries: QueryPipeline, character_shape: SharedShape, - character_pos: Isometry3, + character_pos: Isometry3 | None, character_mass: float, collisions: list[CharacterCollision], filter: QueryFilter | None = None, ) -> None: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> KinematicCharacterController: ... class AxesMask: LIN_X: AxesMask @@ -1834,6 +2624,22 @@ class PidCorrection: class PdController: axes: AxesMask + @property + def lin_kp(self) -> Vec3: ... + @lin_kp.setter + def lin_kp(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def ang_kp(self) -> Vec3: ... + @ang_kp.setter + def ang_kp(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def lin_kd(self) -> Vec3: ... + @lin_kd.setter + def lin_kd(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def ang_kd(self) -> Vec3: ... + @ang_kd.setter + def ang_kd(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... def __init__( self, axes: AxesMask | None = None, @@ -1842,11 +2648,44 @@ class PdController: ) -> 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 rigid_body_correction( + self, body: RigidBody, target_pose: Isometry3, target_vels: RigidBodyVelocity | None = None + ) -> PidCorrection: ... def correction(self, pose_errors: PdErrors, vel_errors: PdErrors) -> PidCorrection: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> PdController: ... class PidController: axes: AxesMask + @property + def lin_kp(self) -> Vec3: ... + @lin_kp.setter + def lin_kp(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def ang_kp(self) -> Vec3: ... + @ang_kp.setter + def ang_kp(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def lin_ki(self) -> Vec3: ... + @lin_ki.setter + def lin_ki(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def ang_ki(self) -> Vec3: ... + @ang_ki.setter + def ang_ki(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def lin_kd(self) -> Vec3: ... + @lin_kd.setter + def lin_kd(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def ang_kd(self) -> Vec3: ... + @ang_kd.setter + def ang_kd(self, value: Vec3 | tuple[float, float, float] | float) -> None: ... + @property + def lin_integral(self) -> Vec3: ... + @property + def ang_integral(self) -> Vec3: ... def __init__( self, axes: AxesMask | None = None, @@ -1858,9 +2697,18 @@ class PidController: 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 rigid_body_correction( + self, + dt: float, + body: RigidBody, + target_pose: Isometry3, + target_vels: RigidBodyVelocity | None = None, + ) -> 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: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> PidController: ... class WheelTuning: suspension_stiffness: float @@ -1882,6 +2730,9 @@ class WheelTuning: ) -> None: ... @staticmethod def default() -> WheelTuning: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> WheelTuning: ... class RayCastInfo: @property @@ -1919,6 +2770,12 @@ class Wheel: wheel_suspension_force: float @property def raycast_info(self) -> RayCastInfo: ... + @property + def center(self) -> Vec3: ... + @property + def suspension(self) -> Vec3: ... + @property + def axle(self) -> Vec3: ... class DynamicRayCastVehicleController: index_up_axis: int @@ -1950,6 +2807,9 @@ class DynamicRayCastVehicleController: @property def current_vehicle_speed(self) -> float: ... def chassis(self) -> RigidBodyHandle: ... + def to_bytes(self) -> bytes: ... + @staticmethod + def from_bytes(blob: bytes) -> DynamicRayCastVehicleController: ... # ---- debug-render (phase 10) --------------------------------------------- @@ -2017,6 +2877,8 @@ class DebugRenderStyle: def __init__(self) -> None: ... @staticmethod def default() -> DebugRenderStyle: ... + def copy(self) -> DebugRenderStyle: + """A standalone copy of the style (detached from the pipeline it may belong to).""" subdivisions: int border_subdivisions: int collider_dynamic_color: DebugColor @@ -2048,6 +2910,7 @@ class DebugRenderStyle: class DebugLineCollector: def __init__(self) -> None: ... def clear(self) -> None: ... + # Shape (N, 2, 3): the two end points of each line. def lines(self) -> np.ndarray: ... def colors(self) -> np.ndarray: ... def objects(self) -> np.ndarray: ... @@ -2056,6 +2919,7 @@ class DebugLineCollector: class DebugRenderPipeline: mode: DebugRenderMode + # The same live object on every access; assigning copies the values. style: DebugRenderStyle def __init__( self, @@ -2087,7 +2951,7 @@ class DebugRenderPipeline: # =========================================================================== Softness = Union["SpringCoefficients", Tuple[float, float]] -IndicesLike = Union["np.ndarray", list, tuple] +IndexListLike = Union["np.ndarray", list, tuple] class SoftBodyHandle: @staticmethod @@ -2102,24 +2966,65 @@ class SoftBodyHandle: def __hash__(self) -> int: ... def __eq__(self, other: object) -> bool: ... -class SoftBodyCellModel(enum.IntEnum): - VOLUME: int - COROTATIONAL: int - NEO_HOOKEAN: int +class SoftBodyCellModel: + VOLUME: SoftBodyCellModel + COROTATIONAL: SoftBodyCellModel + NEO_HOOKEAN: SoftBodyCellModel + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... -class SoftEdgePlasticFlow(enum.IntEnum): - BOTH: int - COMPRESSION: int - TENSION: int +class SoftEdgePlasticFlow: + BOTH: SoftEdgePlasticFlow + COMPRESSION: SoftEdgePlasticFlow + TENSION: SoftEdgePlasticFlow + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... -class SoftBodyEdgeKind(enum.IntEnum): - STRUCTURAL: int - BEND: int +class SoftBodyEdgeKind: + STRUCTURAL: SoftBodyEdgeKind + BEND: SoftBodyEdgeKind + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... -class SoftPatchConstraints(enum.IntEnum): - KEEP: int - STAND_DOWN: int - ALONG_NORMAL: int +class SoftPatchConstraints: + KEEP: SoftPatchConstraints + STAND_DOWN: SoftPatchConstraints + ALONG_NORMAL: SoftPatchConstraints + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +class SoftBodySolver: + CONSTRAINTS: SoftBodySolver + FEM: SoftBodySolver + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +class MeshEnclosure: + COVER: MeshEnclosure + CRUST: MeshEnclosure + def __int__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +class VolumeMeshParameters: + cell_size: float + enclosure: MeshEnclosure + cover_smoothing: int + cover_guard: float + cover_subdivisions: int + def __init__( + self, + cell_size: float, + enclosure: MeshEnclosure = ..., + cover_smoothing: int = 0, + cover_guard: float = 0.15, + cover_subdivisions: int = 0, + ) -> None: ... class SoftBodyMaterial: edge_softness: SpringCoefficients @@ -2146,6 +3051,7 @@ class SoftBodyMaterial: def __init__(self, **kwargs: Any) -> None: ... @staticmethod def uniform(softness: Softness) -> SoftBodyMaterial: ... + def copy(self) -> SoftBodyMaterial: ... def tears(self) -> bool: ... def lame_parameters(self) -> Tuple[float, float]: ... @@ -2187,13 +3093,29 @@ class SoftRecoverySettings: overlap_patience: int overlap_progress_margin: float def __init__(self) -> None: ... + def copy(self) -> SoftRecoverySettings: ... + +class SoftFemParameters: + linear_tolerance: float + max_linear_iterations: int + max_dense_dofs: int + def __init__( + self, + *, + linear_tolerance: float = 1.0e-5, + max_linear_iterations: int = 20, + max_dense_dofs: int = 600, + ) -> None: ... + def copy(self) -> SoftFemParameters: ... class SoftBodiesSettings: recovery: SoftRecoverySettings + fem: SoftFemParameters resweep_strain: float max_extra_substeps: int contact_stiffening: float def __init__(self) -> None: ... + def copy(self) -> SoftBodiesSettings: ... class SoftBodyBuilder: def __init__(self, positions: Any, **kwargs: Any) -> None: ... @@ -2216,17 +3138,17 @@ class SoftBodyBuilder: def particle_mass(self, mass: float) -> SoftBodyBuilder: ... def mass(self, mass: float) -> SoftBodyBuilder: ... def masses(self, masses: list[float]) -> SoftBodyBuilder: ... - def pinned_particles(self, pinned: IndicesLike) -> SoftBodyBuilder: ... - def edges(self, edges: IndicesLike) -> SoftBodyBuilder: ... - def add_edges(self, edges: IndicesLike) -> SoftBodyBuilder: ... - def bend_edges(self, edges: IndicesLike) -> SoftBodyBuilder: ... + def pinned_particles(self, pinned: IndexListLike) -> SoftBodyBuilder: ... + def edges(self, edges: IndexListLike) -> SoftBodyBuilder: ... + def add_edges(self, edges: IndexListLike) -> SoftBodyBuilder: ... + def bend_edges(self, edges: IndexListLike) -> SoftBodyBuilder: ... def tension_only(self) -> SoftBodyBuilder: ... - def dihedrals(self, dihedrals: IndicesLike) -> SoftBodyBuilder: ... - def cells(self, cells: IndicesLike) -> SoftBodyBuilder: ... - def surface(self, surface: IndicesLike) -> SoftBodyBuilder: ... - def skin(self, vertices: Any, indices: IndicesLike) -> SoftBodyBuilder: ... + def dihedrals(self, dihedrals: IndexListLike) -> SoftBodyBuilder: ... + def cells(self, cells: IndexListLike) -> SoftBodyBuilder: ... + def surface(self, surface: IndexListLike) -> SoftBodyBuilder: ... + def skin(self, vertices: Any, indices: IndexListLike) -> SoftBodyBuilder: ... def skin_collision(self, enabled: bool) -> SoftBodyBuilder: ... - def wire(self, segments: IndicesLike) -> SoftBodyBuilder: ... + def wire(self, segments: IndexListLike) -> SoftBodyBuilder: ... def material(self, material: SoftBodyMaterial) -> SoftBodyBuilder: ... def softness(self, softness: Softness) -> SoftBodyBuilder: ... def tear_strain(self, strain: float) -> SoftBodyBuilder: ... @@ -2236,6 +3158,7 @@ class SoftBodyBuilder: def interior_strength(self, strength: float) -> SoftBodyBuilder: ... def edge_tear_resistance(self, resistances: list[Tuple[int, float]]) -> SoftBodyBuilder: ... def cell_model(self, model: SoftBodyCellModel) -> SoftBodyBuilder: ... + def solver(self, solver: SoftBodySolver) -> SoftBodyBuilder: ... def volume_preservation(self, enabled: bool) -> SoftBodyBuilder: ... def volume_factor(self, factor: float) -> SoftBodyBuilder: ... def shape_matching(self, enabled: bool) -> SoftBodyBuilder: ... @@ -2378,7 +3301,7 @@ class SoftCollisionMesh: class SoftMeshBinding: @staticmethod - def direct(particles: IndicesLike) -> SoftMeshBinding: ... + def direct(particles: IndexListLike) -> SoftMeshBinding: ... @staticmethod def direct_by_position(eps: float) -> SoftMeshBinding: ... @staticmethod @@ -2450,9 +3373,13 @@ class SoftBody: @staticmethod def sphere(center: VectorLike, radius: float, subdivisions: int, **kwargs: Any) -> SoftBodyBuilder: ... @staticmethod - def trimesh(vertices: Any, indices: IndicesLike, **kwargs: Any) -> SoftBodyBuilder: ... + def trimesh(vertices: Any, indices: IndexListLike, **kwargs: Any) -> SoftBodyBuilder: ... @staticmethod - def volumetric(vertices: Any, indices: IndicesLike, cell_size: float, skinned: bool = False, **kwargs: Any) -> SoftBodyBuilder: ... + def volumetric(vertices: Any, indices: IndexListLike, cell_size: float, skinned: bool = False, **kwargs: Any) -> SoftBodyBuilder: ... + @staticmethod + def volumetric_with( + vertices: Any, indices: IndexListLike, params: VolumeMeshParameters, skinned: bool = False, **kwargs: Any + ) -> SoftBodyBuilder: ... # particles @property def topology_version(self) -> int: ... @@ -2463,9 +3390,13 @@ class SoftBody: def particle_position(self, i: int) -> Vec3: ... @property def particle_positions(self) -> np.ndarray: ... + @particle_positions.setter + def particle_positions(self, positions: Any) -> None: ... def particle_velocity(self, i: int) -> Vec3: ... @property def particle_velocities(self) -> np.ndarray: ... + @particle_velocities.setter + def particle_velocities(self, velocities: Any) -> None: ... def set_particle_position(self, i: int, position: VectorLike) -> None: ... def set_particle_velocity(self, i: int, velocity: VectorLike) -> None: ... def set_particle_kinematic_target(self, i: int, position: VectorLike) -> None: ... @@ -2494,6 +3425,7 @@ class SoftBody: def boundary(self) -> np.ndarray: ... # material material: SoftBodyMaterial + solver: SoftBodySolver @property def cell_model(self) -> SoftBodyCellModel: ... @property @@ -2525,8 +3457,7 @@ class SoftBody: @property def is_sleeping(self) -> bool: ... def wake_up(self) -> None: ... - @property - def is_enabled(self) -> bool: ... + is_enabled: bool def set_enabled(self, enabled: bool) -> None: ... def set_additional_pgs_iterations(self, iterations: int) -> None: ... @property @@ -2546,6 +3477,9 @@ class SoftBody: @property def has_pending_tears(self) -> bool: ... def crossing_elements(self, blade: Tuple[VectorLike, VectorLike, VectorLike]) -> Tuple[list[int], list[int]]: ... + def set_edge_tear_resistance(self, i: int, resistance: float) -> None: ... + def set_cell_tear_resistance(self, i: int, resistance: float) -> None: ... + def set_particle_damaged(self, i: int, damaged: bool) -> None: ... # clusters @property def num_clusters(self) -> int: ... @@ -2561,6 +3495,7 @@ class SoftBody: def set_cluster_tear_resistance(self, i: int, resistance: float) -> None: ... def set_cluster_pinned(self, i: int, pinned: bool) -> None: ... def set_cluster_kinematic_target(self, i: int, pose: IsometryLike) -> None: ... + def set_cluster_shape_matching_target(self, i: int, target: IsometryLike | None) -> None: ... # meshes @property def meshes(self) -> list[SoftCollisionMesh]: ... @@ -2580,7 +3515,7 @@ class SoftBodySet: impulse_joints: ImpulseJointSet, multibody_joints: MultibodyJointSet, ) -> SoftBody | None: ... - def add_cluster(self, handle: SoftBodyHandle, particles: IndicesLike, bodies: RigidBodySet, colliders: ColliderSet) -> int | None: ... + def add_cluster(self, handle: SoftBodyHandle, particles: IndexListLike, bodies: RigidBodySet, colliders: ColliderSet) -> int | None: ... def remove_cluster( self, handle: SoftBodyHandle, @@ -2594,8 +3529,8 @@ class SoftBodySet: def tear( self, handle: SoftBodyHandle, - edges: IndicesLike, - cells: IndicesLike, + edges: IndexListLike, + cells: IndexListLike, islands: IslandManager, bodies: RigidBodySet, colliders: ColliderSet, @@ -2620,3 +3555,360 @@ class SoftBodySet: def __contains__(self, handle: SoftBodyHandle) -> bool: ... def __iter__(self) -> Iterator[Tuple[SoftBodyHandle, SoftBody]]: ... def __len__(self) -> int: ... + +# ===================================================================== +# World, pipelines, counters, quarantine and build features +# ===================================================================== + +from ._event_handler import EventHandler as _EventHandler +from ._event_handler import PhysicsHooks as _PhysicsHooks + +class StagesCounters: + """Per-stage timings of the last step, in milliseconds (a copy).""" + + @property + def update_time_ms(self) -> float: ... + @property + def collision_detection_time_ms(self) -> float: ... + @property + def island_construction_time_ms(self) -> float: ... + @property + def island_constraints_collection_time_ms(self) -> float: ... + @property + def solver_time_ms(self) -> float: ... + @property + def ccd_time_ms(self) -> float: ... + @property + def user_changes_time_ms(self) -> float: ... + +class CollisionDetectionCounters: + """Collision-detection statistics and timings of the last step (a copy).""" + + @property + def ncontact_pairs(self) -> int: ... + @property + def broad_phase_time_ms(self) -> float: ... + @property + def narrow_phase_time_ms(self) -> float: ... + @property + def final_broad_phase_time_ms(self) -> float: ... + +class SolverCounters: + """Solver statistics and timings of the last step (a copy).""" + + @property + def nconstraints(self) -> int: ... + @property + def ncontacts(self) -> int: ... + @property + def velocity_resolution_time_ms(self) -> float: ... + @property + def velocity_assembly_time_ms(self) -> float: ... + @property + def velocity_update_time_ms(self) -> float: ... + +class CCDCounters: + """Continuous-collision-detection statistics and timings of the last step (a copy).""" + + @property + def num_substeps(self) -> int: ... + @property + def toi_computation_time_ms(self) -> float: ... + @property + def solver_time_ms(self) -> float: ... + @property + def broad_phase_time_ms(self) -> float: ... + @property + def narrow_phase_time_ms(self) -> float: ... + +class Counters: + """Performance counters of a `PhysicsPipeline`. + + `PhysicsPipeline.counters` is a live view of the pipeline's counters (enabled by + default); `Counters()` owns its data and starts disabled. The timings are zero if the + bindings are built without the `profiler` feature (see `BuildFeatures.profiler`). + """ + + def __init__(self) -> None: ... + def enable(self) -> None: ... + def disable(self) -> None: ... + def reset(self) -> None: ... + @property + def enabled(self) -> bool: ... + @property + def step_time_ms(self) -> float: ... + @property + def custom_time_ms(self) -> float: ... + @property + def stages(self) -> StagesCounters: ... + @property + def cd(self) -> CollisionDetectionCounters: ... + @property + def solver(self) -> SolverCounters: ... + @property + def ccd(self) -> CCDCounters: ... + def print(self) -> None: ... + +class Quarantine: + """The objects neutralized during the last step because their state became non-finite. + + A copy, cleared by each step. Quarantined rigid-bodies are reset to their last valid pose + (when known), zeroed and disabled: set `RigidBody.is_enabled` back to `True` once the + cause is fixed. + """ + + @property + def bodies(self) -> list[RigidBodyHandle]: ... + @property + def colliders(self) -> list[ColliderHandle]: ... + @property + def soft_bodies(self) -> list[SoftBodyHandle]: ... + def is_empty(self) -> bool: ... + +class BuildFeatures: + """How the loaded extension was built (see `build_features()`).""" + + @property + def profile(self) -> str: + """The Cargo profile: `"release"` for an optimized build, `"debug"` otherwise.""" + @property + def enhanced_determinism(self) -> bool: + """Built with the `determinism` feature: cross-platform bit-identical results.""" + @property + def parallel(self) -> bool: + """The parallel stages of a step can run on several worker threads.""" + @property + def profiler(self) -> bool: + """The timings of the enabled `Counters` are measured.""" + +def build_features() -> BuildFeatures: + """How the loaded `rapier3d` extension was built.""" + +class PhysicsPipeline: + """Low-level physics-step driver; `PhysicsWorld` aggregates it with every other structure.""" + + def __init__(self) -> None: ... + @property + def counters(self) -> Counters: + """Live view of the pipeline's performance counters (enabled by default).""" + @property + def quarantine(self) -> Quarantine: + """The objects neutralized during the last step because their state became non-finite.""" + @property + def num_threads(self) -> int: + """The number of worker threads `step` runs its parallel stages on.""" + def set_num_threads(self, num_threads: int | None = None) -> None: + """Give the pipeline its own pool of `num_threads` workers (`None`: rayon's global pool).""" + def step( + self, + gravity: VectorLike, + integration_parameters: IntegrationParameters, + islands: IslandManager, + broad_phase: BroadPhaseBvh, + narrow_phase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + ccd_solver: CCDSolver, + hooks: _PhysicsHooks | None = None, + events: _EventHandler | None = None, + soft_bodies: SoftBodySet | None = None, + ) -> None: + """Advance the simulation by one step (releases the GIL while solving).""" + +class CollisionPipeline: + """Collision-detection-only step driver (no dynamics).""" + + def __init__(self) -> None: ... + def step( + self, + prediction_distance: float, + islands: IslandManager, + broad_phase: BroadPhaseBvh, + narrow_phase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + hooks: _PhysicsHooks | None = None, + events: _EventHandler | None = None, + ) -> None: + """Update the broad-phase and the narrow-phase without advancing the simulation.""" + +class QueryPipeline: + """Scene queries (ray casts, point projections, shape casts...) against a set of colliders.""" + + def __init__( + self, + broad_phase: BroadPhaseBvh, + narrow_phase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ) -> None: ... + def update(self, bodies: RigidBodySet, colliders: ColliderSet) -> None: ... + def cast_ray( + self, ray: Ray, max_toi: float, solid: bool, filter: QueryFilter | None = None + ) -> Tuple[ColliderHandle, float] | None: ... + def cast_ray_and_get_normal( + self, ray: Ray, max_toi: float, solid: bool, filter: QueryFilter | None = None + ) -> Tuple[ColliderHandle, RayIntersection] | None: ... + def intersect_ray( + self, + ray: Ray, + max_toi: float, + solid: bool, + callback: Callable[[ColliderHandle, RayIntersection], bool | None], + filter: QueryFilter | None = None, + ) -> None: ... + def project_point( + self, + point: VectorLike, + solid: bool, + filter: QueryFilter | None = None, + max_dist: float | None = None, + ) -> Tuple[ColliderHandle, PointProjection] | None: ... + def project_point_and_get_feature( + self, + point: VectorLike, + max_dist: float | None = None, + filter: QueryFilter | None = None, + ) -> Tuple[ColliderHandle, PointProjection, FeatureId] | None: ... + def intersect_point( + self, + point: VectorLike, + callback: Callable[[ColliderHandle], bool | None], + filter: QueryFilter | None = None, + ) -> None: ... + def cast_shape( + self, + shape_pose: IsometryLike, + shape_vel: VectorLike, + shape: SharedShape, + options: ShapeCastOptions, + filter: QueryFilter | None = None, + ) -> Tuple[ColliderHandle, ShapeCastHit] | None: ... + def cast_shape_nonlinear( + self, + motion: NonlinearRigidMotion, + shape: SharedShape, + options: ShapeCastOptions, + start_time: float = 0.0, + end_time: float = ..., + filter: QueryFilter | None = None, + ) -> Tuple[ColliderHandle, ShapeCastHit] | None: ... + def intersect_shape( + self, + shape_pose: IsometryLike, + shape: SharedShape, + callback: Callable[[ColliderHandle], bool | None], + filter: QueryFilter | None = None, + ) -> None: ... + def intersect_aabb_conservative( + self, + aabb: Aabb, + callback: Callable[[ColliderHandle], bool | None], + filter: QueryFilter | None = None, + ) -> None: ... + def colliders_with_aabb_intersecting_aabb( + self, + aabb: Aabb, + callback: Callable[[ColliderHandle], bool | None], + filter: QueryFilter | None = None, + ) -> None: ... + def test_aabb(self, aabb: Aabb, filter: QueryFilter | None = None) -> bool: ... + +class PhysicsWorld: + """Every structure of a simulation in one object, stepped with `step()`. + + The sub-structures are stable properties: the same Python object is returned on every + access. A world can be used from any thread, but not by two threads at once: while it is + being stepped, using it from another thread raises `RuntimeError`. + """ + + def __init__(self, gravity: VectorLike | None = None, auto_update_query: bool = False) -> None: + """Create an empty world. + + `gravity` defaults to zero (no gravity), unlike the Rust `PhysicsWorld::new()`: pass + `gravity=(0.0, -9.81, 0.0)` for the Earth's gravity. + """ + @property + def rigid_bodies(self) -> RigidBodySet: ... + @property + def colliders(self) -> ColliderSet: ... + @property + def impulse_joints(self) -> ImpulseJointSet: ... + @property + def multibody_joints(self) -> MultibodyJointSet: ... + @property + def soft_bodies(self) -> SoftBodySet: ... + @property + def broad_phase(self) -> BroadPhaseBvh: ... + @property + def narrow_phase(self) -> NarrowPhase: ... + @property + def islands(self) -> IslandManager: ... + @property + def ccd_solver(self) -> CCDSolver: ... + integration_parameters: IntegrationParameters + """The world's integration parameters, modified in place (assigning copies the values).""" + @property + def physics_pipeline(self) -> PhysicsPipeline: ... + @property + def collision_pipeline(self) -> CollisionPipeline: + """The collision pipeline used by `detect_collisions`.""" + @property + def query_pipeline(self) -> QueryPipeline: ... + @property + def quarantine(self) -> Quarantine: + """The objects neutralized during the last step because their state became non-finite.""" + @property + def num_threads(self) -> int: ... + def set_num_threads(self, num_threads: int | None = None) -> None: + """Give the world its own pool of `num_threads` workers (`None`: rayon's global pool).""" + gravity: Vec3 + """World-space gravity (accepts any vector-like when assigned).""" + event_handler: _EventHandler | None + physics_hooks: _PhysicsHooks | None + auto_update_query: bool + event_error_policy: Literal["defer", "strict"] + def step(self) -> None: + """Advance the simulation by one timestep.""" + def detect_collisions(self) -> None: + """Update the broad-phase and the narrow-phase without advancing the simulation.""" + def update_query_pipeline(self) -> None: ... + def wake_up(self, handle: RigidBodyHandle, strong: bool = True) -> None: ... + def wake_up_all(self, strong: bool = True) -> None: ... + def active_bodies(self) -> list[RigidBodyHandle]: ... + def clear(self) -> None: + """Remove every object and reset the simulation state, keeping the configuration.""" + def add_body( + self, + builder: RigidBody | RigidBodyBuilder, + colliders: list[Collider | ColliderBuilder] | None = None, + ) -> RigidBodyHandle: ... + def add_collider( + self, builder: Collider | ColliderBuilder, parent: RigidBodyHandle | None = None + ) -> ColliderHandle: ... + def remove_body(self, handle: RigidBodyHandle) -> RigidBody | None: ... + def remove_collider(self, handle: ColliderHandle) -> Collider | None: ... + def add_soft_body(self, builder: SoftBodyBuilder) -> SoftBodyHandle: ... + def remove_soft_body(self, handle: SoftBodyHandle) -> SoftBody | None: ... + def insert_deformable( + self, + builder: Collider | ColliderBuilder, + binding: SoftMeshBinding, + parent: RigidBodyHandle, + ) -> ColliderHandle: ... + def add_soft_body_cluster(self, handle: SoftBodyHandle, particles: IndexListLike) -> int | None: ... + def remove_soft_body_cluster(self, handle: SoftBodyHandle, cluster: int) -> bool: ... + def tear_soft_body( + self, handle: SoftBodyHandle, edges: IndexListLike, cells: IndexListLike + ) -> SoftBodyTearEvent | None: ... + def cut_soft_body( + self, handle: SoftBodyHandle, blade: Tuple[VectorLike, VectorLike, VectorLike] + ) -> SoftBodyTearEvent | None: ... + def snapshot(self) -> bytes: ... + @staticmethod + def restore(blob: bytes) -> PhysicsWorld: ... + def snapshot_json(self) -> str: ... + @staticmethod + def restore_json(s: str) -> PhysicsWorld: ... diff --git a/python/rapier-py-3d/python/rapier3d/loaders/mjcf.py b/python/rapier-py-3d/python/rapier3d/loaders/mjcf.py index 2dd563343..e7898f350 100644 --- a/python/rapier-py-3d/python/rapier3d/loaders/mjcf.py +++ b/python/rapier-py-3d/python/rapier3d/loaders/mjcf.py @@ -16,8 +16,10 @@ from .._rapier3d import ( # noqa: F401 ContactFilterMode, + MjcfActuatorHandle, MjcfBodyHandle, MjcfColliderHandle, + MjcfContactHooks, MjcfJointHandle, MjcfLoaderOptions, MjcfModel, @@ -30,8 +32,10 @@ __all__ = [ "ContactFilterMode", "MjcfError", + "MjcfActuatorHandle", "MjcfBodyHandle", "MjcfColliderHandle", + "MjcfContactHooks", "MjcfJointHandle", "MjcfLoaderOptions", "MjcfModel", diff --git a/python/rapier-py-3d/python/rapier3d/loaders/urdf.py b/python/rapier-py-3d/python/rapier3d/loaders/urdf.py index 9153ff3da..adba98b3f 100644 --- a/python/rapier-py-3d/python/rapier3d/loaders/urdf.py +++ b/python/rapier-py-3d/python/rapier3d/loaders/urdf.py @@ -17,6 +17,7 @@ UrdfMultibodyOptions, UrdfRobot, UrdfRobotHandles, + UrdfVisual, ) from .._rapier3d import UrdfError # noqa: F401 (error type) @@ -32,4 +33,5 @@ "UrdfMultibodyOptions", "UrdfRobot", "UrdfRobotHandles", + "UrdfVisual", ] diff --git a/python/rapier-py-3d/python/rapier3d/math.py b/python/rapier-py-3d/python/rapier3d/math.py index 6446b9e03..08ebb7a1a 100644 --- a/python/rapier-py-3d/python/rapier3d/math.py +++ b/python/rapier-py-3d/python/rapier3d/math.py @@ -1,4 +1,13 @@ -"""`rapier.dim3.math` — math helpers (3D, f32).""" +"""`rapier3d.math`: math helpers, and transcendental functions computed by Rapier. + +The functions :func:`sin`, :func:`cos`, :func:`tan`, :func:`asin`, :func:`acos`, +:func:`atan`, :func:`atan2`, :func:`exp`, :func:`log`, :func:`pow` and :func:`sqrt` +use the same math backend as the engine, in its precision (``f32``). When the bindings +are built with the ``determinism`` feature (see +:attr:`rapier3d.BuildFeatures.enhanced_determinism`), they give bit-identical results on +every platform, unlike Python's :mod:`math` module or NumPy: use them to compute the +initial state of a simulation that must be cross-platform deterministic. +""" from __future__ import annotations @@ -7,9 +16,38 @@ rotation_from_angle = _ext.rotation_from_angle +sin = _ext._math.sin +cos = _ext._math.cos +tan = _ext._math.tan +asin = _ext._math.asin +acos = _ext._math.acos +atan = _ext._math.atan +atan2 = _ext._math.atan2 +exp = _ext._math.exp +log = _ext._math.log +pow = _ext._math.pow # noqa: A001 (mirrors the name of `math.pow`) +sqrt = _ext._math.sqrt + def linear_interp(a, b, t): + """Alias of :func:`lerp`.""" return lerp(a, b, t) -__all__ = ["lerp", "linear_interp", "rotation_from_angle", "wrap_to_pi"] +__all__ = [ + "acos", + "asin", + "atan", + "atan2", + "cos", + "exp", + "lerp", + "linear_interp", + "log", + "pow", + "rotation_from_angle", + "sin", + "sqrt", + "tan", + "wrap_to_pi", +] diff --git a/python/rapier-py-3d/python/rapier3d/math.pyi b/python/rapier-py-3d/python/rapier3d/math.pyi index 301d1e595..8bc9ed98f 100644 --- a/python/rapier-py-3d/python/rapier3d/math.pyi +++ b/python/rapier-py-3d/python/rapier3d/math.pyi @@ -1,12 +1,27 @@ -"""Stub for `rapier.math`.""" +"""Stub for `rapier3d.math`.""" from typing import TypeVar from ._rapier3d import Rotation3 as _Rotation3 +from ._rapier3d import VectorLike as _VectorLike T = TypeVar("T") def lerp(a: T, b: T, t: float) -> T: ... def linear_interp(a: T, b: T, t: float) -> T: ... def wrap_to_pi(angle: float) -> float: ... -def rotation_from_angle(v) -> _Rotation3: ... +def rotation_from_angle(v: _VectorLike) -> _Rotation3: ... + +# Transcendental functions computed by Rapier's math backend (in f32), bit-identical on every +# platform when the bindings are built with the `determinism` feature. +def sin(x: float) -> float: ... +def cos(x: float) -> float: ... +def tan(x: float) -> float: ... +def asin(x: float) -> float: ... +def acos(x: float) -> float: ... +def atan(x: float) -> float: ... +def atan2(y: float, x: float) -> float: ... +def exp(x: float) -> float: ... +def log(x: float) -> float: ... +def pow(base: float, exponent: float) -> float: ... +def sqrt(x: float) -> float: ... diff --git a/python/rapier-py-3d/src/build_info.rs b/python/rapier-py-3d/src/build_info.rs new file mode 100644 index 000000000..28e54fc4c --- /dev/null +++ b/python/rapier-py-3d/src/build_info.rs @@ -0,0 +1,63 @@ +//! `BuildFeatures` / `build_features()`: how the loaded extension was built. + +use pyo3::prelude::*; + +/// How the loaded ``rapier3d`` extension was built (see :func:`build_features`). +#[pyclass(name = "BuildFeatures", module = "rapier", frozen)] +#[derive(Clone)] +pub struct BuildFeatures { + /// The Cargo profile of the extension: ``"release"`` for an optimized build, ``"debug"`` + /// otherwise (e.g. ``maturin develop`` without ``--release``), which is much slower. + #[pyo3(get)] + pub profile: &'static str, + /// ``True`` if built with the ``determinism`` feature (rapier's ``enhanced-determinism``): + /// the simulation and the functions of :mod:`rapier3d.math` give bit-identical results on + /// every platform. + #[pyo3(get)] + pub enhanced_determinism: bool, + /// ``True`` if the parallel stages of a step can run on several worker threads (see + /// :meth:`PhysicsWorld.set_num_threads`). Always ``True`` for the ``rapier3d`` wheels. + #[pyo3(get)] + pub parallel: bool, + /// ``True`` if built with rapier's ``profiler`` feature: the timings of the enabled + /// :class:`Counters` are measured (they are zero otherwise). + #[pyo3(get)] + pub profiler: bool, +} + +#[pymethods] +impl BuildFeatures { + fn __repr__(&self) -> String { + let b = |v: bool| if v { "True" } else { "False" }; + format!( + "BuildFeatures(profile='{}', enhanced_determinism={}, parallel={}, profiler={})", + self.profile, + b(self.enhanced_determinism), + b(self.parallel), + b(self.profiler) + ) + } +} + +/// How the loaded ``rapier3d`` extension was built: its Cargo profile and the optional engine +/// features compiled in. +/// +/// :: +/// +/// if rapier3d.build_features().profile != "release": +/// print("Warning: the rapier3d bindings are built without optimizations.") +#[pyfunction] +pub fn build_features() -> BuildFeatures { + BuildFeatures { + profile: env!("RAPIER_PY_CARGO_PROFILE"), + enhanced_determinism: cfg!(feature = "determinism"), + parallel: true, + profiler: cfg!(feature = "profiler"), + } +} + +pub fn register_build_info(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_function(wrap_pyfunction!(build_features, m)?)?; + Ok(()) +} diff --git a/python/rapier-py-3d/src/controllers.rs b/python/rapier-py-3d/src/controllers.rs index 474b9e335..fa8554017 100644 --- a/python/rapier-py-3d/src/controllers.rs +++ b/python/rapier-py-3d/src/controllers.rs @@ -10,7 +10,7 @@ use crate::*; use rapier3d as rapier; -use crate::pyo3::exceptions::PyTypeError; +use crate::pyo3::exceptions::{PyIndexError, PyTypeError, PyValueError}; use crate::pyo3::prelude::*; use crate::pyo3::pyclass::CompareOp; @@ -141,6 +141,18 @@ impl CharacterLength { } } +/// A keyword argument where an explicit ``None`` differs from an omitted argument. +pub(crate) enum OptionalArg<'py> { + Missing, + Given(Option>), +} + +impl<'py> FromPyObject<'py> for OptionalArg<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + Ok(Self::Given((!ob.is_none()).then(|| ob.clone()))) + } +} + // Accept a `(value, "kind")` tuple, a `CharacterLength` instance, or // a bare float (interpreted as Absolute). fn _extract_char_length(obj: &Bound<'_, PyAny>) -> PyResult { @@ -225,8 +237,14 @@ impl CharacterAutostep { /// 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 + "CharacterAutostep(max_height={}, min_width={}, include_dynamic_bodies={})", + self.max_height.__repr__(), + self.min_width.__repr__(), + if self.include_dynamic_bodies { + "True" + } else { + "False" + } ) } } @@ -295,7 +313,8 @@ impl EffectiveCharacterMovement { /// 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. +/// :ivar toi: ``ShapeCastHit`` with witness points and normals +/// (also available as ``hit``). #[pyclass(name = "CharacterCollision", module = "rapier", frozen)] #[derive(Debug, Clone, Copy)] pub struct CharacterCollision { @@ -313,6 +332,11 @@ pub struct CharacterCollision { #[pymethods] impl CharacterCollision { + /// Alias of :attr:`toi`, named after the Rust field. + #[getter] + fn hit(&self) -> ShapeCastHit { + self.toi + } /// Return the ``CharacterCollision(handle=..., toi=...)`` repr. fn __repr__(&self) -> String { format!( @@ -374,6 +398,60 @@ impl CharacterCollision { } } +/// Raises ``ValueError`` unless `bodies` and `colliders` are ``None`` or the query pipeline's sets. +fn check_query_sets( + queries: &QueryPipeline, + bodies: &Bound<'_, PyAny>, + colliders: &Bound<'_, PyAny>, +) -> PyResult<()> { + if !bodies.is_none() && !bodies.is(&queries.bodies) { + return Err(PyValueError::new_err( + "`bodies` must be the rigid-body set of the query pipeline", + )); + } + if !colliders.is_none() && !colliders.is(&queries.colliders) { + return Err(PyValueError::new_err( + "`colliders` must be the collider set of the query pipeline", + )); + } + Ok(()) +} + +/// Evaluates the filter's Python predicate on every collider, for operations that borrow the sets +/// mutably (the predicate cannot read them meanwhile). Returns the accepted colliders. +fn precompute_predicate( + py: Python<'_>, + filter: Option<&QueryFilter>, + colliders: &Py, +) -> PyResult>> { + let Some(predicate) = filter.and_then(|f| f.predicate.as_ref()) else { + return Ok(None); + }; + let handles: Vec<_> = crate::events_hooks::try_read(colliders, py)? + .0 + .iter() + .map(|(h, _)| h) + .collect(); + let mut accepted = std::collections::HashSet::with_capacity(handles.len()); + for handle in handles { + let collider = Collider { + backing: ColliderBacking::InSet { + set: colliders.clone_ref(py), + handle, + }, + }; + // Same convention as the scene queries: a non-bool result accepts the collider. + let keep = predicate + .call1(py, (ColliderHandle(handle), collider))? + .extract::(py) + .unwrap_or(true); + if keep { + accepted.insert(handle); + } + } + Ok(Some(accepted)) +} + // --------------------------------------------------------------- // KinematicCharacterController // --------------------------------------------------------------- @@ -408,14 +486,15 @@ impl KinematicCharacterController { /// 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. + /// ``None`` to disable (the default). /// :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. + /// a small drop. ``None`` disables it; when omitted it + /// defaults to ``CharacterLength.relative(0.2)``. /// :param normal_nudge_factor: Small bias along contact /// normals to avoid getting stuck on geometry. #[new] @@ -423,25 +502,23 @@ impl KinematicCharacterController { up=None, offset=None, slide=None, - autostep=None, + autostep=OptionalArg::Missing, max_slope_climb_angle=None, min_slope_slide_angle=None, - snap_to_ground=None, + snap_to_ground=OptionalArg::Missing, 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>>, + autostep: OptionalArg<'_>, max_slope_climb_angle: Option, min_slope_slide_angle: Option, - snap_to_ground: Option<&Bound<'_, PyAny>>, + snap_to_ground: OptionalArg<'_>, normal_nudge_factor: Option, ) -> PyResult { - let _ = py; let mut inner = rapier::control::KinematicCharacterController::default(); if let Some(u) = up { inner.up = u.0.into(); @@ -452,13 +529,11 @@ impl KinematicCharacterController { 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 OptionalArg::Given(a) = autostep { + inner.autostep = match a { + None => None, + Some(a) => Some(a.extract::()?.to_rapier()), + }; } if let Some(v) = max_slope_climb_angle { inner.max_slope_climb_angle = v; @@ -466,12 +541,11 @@ impl KinematicCharacterController { 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 OptionalArg::Given(s) = snap_to_ground { + inner.snap_to_ground = match s { + None => None, + Some(s) => Some(_extract_char_length(&s)?.to_rapier()), + }; } if let Some(v) = normal_nudge_factor { inner.normal_nudge_factor = v; @@ -613,13 +687,15 @@ impl KinematicCharacterController { /// 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``. + /// The sweep uses the sets referenced by ``queries``: the + /// ``bodies`` / ``colliders`` arguments are only kept for + /// backward compatibility and may be ``None``. /// /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (kept for API parity). - /// :param colliders: Collider set (kept for API parity). + /// :param bodies: ``None`` or the rigid-body set of ``queries`` + /// (unused). + /// :param colliders: ``None`` or the collider set of ``queries`` + /// (unused). /// :param queries: Up-to-date :class:`QueryPipeline`. /// :param shape: Character collision shape. /// :param shape_pos: World pose of the shape at frame start. @@ -628,6 +704,8 @@ impl KinematicCharacterController { /// :param events_callback: Optional callable receiving each /// :class:`CharacterCollision` as it is detected. /// :returns: :class:`EffectiveCharacterMovement`. + /// :raises ValueError: if ``bodies`` or ``colliders`` is neither + /// ``None`` nor the corresponding set of ``queries``. #[pyo3(signature = ( dt, bodies, colliders, queries, shape, shape_pos, desired_translation, filter=None, events_callback=None @@ -646,7 +724,7 @@ impl KinematicCharacterController { filter: Option<&QueryFilter>, events_callback: Option>, ) -> PyResult { - let _ = (bodies, colliders); // parity-only; queries owns them + check_query_sets(queries, bodies, colliders)?; let pose: rapier::math::Pose = shape_pos.0.into(); let desired: rapier::math::Vector = desired_translation.0.into(); let inner = &self.0; @@ -655,10 +733,10 @@ impl KinematicCharacterController { 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 bp = crate::events_hooks::try_read(&queries.broad_phase, py)?; + let np = crate::events_hooks::try_read(&queries.narrow_phase, py)?; + let bodies = crate::events_hooks::try_read(&queries.bodies, py)?; + let colliders = crate::events_hooks::try_read(&queries.colliders, py)?; let py_pred_obj: Option> = filter.and_then(|f| f.predicate.as_ref().map(|p| p.clone_ref(py))); @@ -736,16 +814,23 @@ impl KinematicCharacterController { /// reactive impulses sized for ``character_mass`` onto any /// dynamic colliders found. /// + /// The ``filter``'s predicate, if any, is called once per collider + /// before the impulses are applied (the sets are locked while they + /// are), so it must not modify them. + /// /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). + /// :param bodies: Rigid-body set of ``queries`` (mutated). + /// :param colliders: Collider set of ``queries`` (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_pos: Unused (each collision carries its own + /// character pose); kept for backward compatibility and may be + /// ``None``. /// :param character_mass: Mass used to scale impulses. /// :param collisions: List of :class:`CharacterCollision`. /// :param filter: Optional query filter. + /// :raises ValueError: if ``bodies`` or ``colliders`` is not the + /// corresponding set of ``queries``. #[pyo3(signature = ( dt, bodies, colliders, queries, character_shape, character_pos, character_mass, collisions, filter=None @@ -759,20 +844,26 @@ impl KinematicCharacterController { colliders: Py, queries: &QueryPipeline, character_shape: &SharedShape, - character_pos: PyIsometry, + character_pos: Option, 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(); + check_query_sets(queries, bodies.bind(py), colliders.bind(py))?; + let accepted = precompute_predicate(py, filter, &colliders)?; + let predicate = |h: rapier::geometry::ColliderHandle, _: &rapier::geometry::Collider| { + accepted.as_ref().is_none_or(|a| a.contains(&h)) + }; + // QueryPipelineMut holds `&mut RigidBodySet` and `&mut ColliderSet`. + let bp = crate::events_hooks::try_read(&queries.broad_phase, py)?; + let np = crate::events_hooks::try_read(&queries.narrow_phase, py)?; + let mut bodies_ref = crate::events_hooks::try_write(&bodies, py)?; + let mut colliders_ref = crate::events_hooks::try_write(&colliders, py)?; + let mut qf = filter.map(|f| f.as_rapier(None)).unwrap_or_default(); + if accepted.is_some() { + qf.predicate = Some(&predicate); + } let mut qpmut = bp.0.as_query_pipeline_mut( np.0.query_dispatcher(), &mut bodies_ref.0, @@ -792,23 +883,25 @@ impl KinematicCharacterController { /// Return a debug string summarizing the controller config. fn __repr__(&self) -> String { + let up: crate::na::Vector3 = self.0.up.into(); + let autostep = self + .autostep() + .map_or_else(|| "None".to_string(), |a| a.__repr__()); + let snap = self + .snap_to_ground() + .map_or_else(|| "None".to_string(), |l| l.__repr__()); 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" - }, + "KinematicCharacterController(up=({}, {}, {}), offset={}, slide={}, autostep={}, max_slope_climb_angle={}, min_slope_slide_angle={}, snap_to_ground={}, normal_nudge_factor={})", + up.x, + up.y, + up.z, + self.offset().__repr__(), + if self.0.slide { "True" } else { "False" }, + autostep, self.0.max_slope_climb_angle, self.0.min_slope_slide_angle, - if self.0.snap_to_ground.is_some() { - "set" - } else { - "None" - }, + snap, + self.0.normal_nudge_factor, ) } } @@ -816,15 +909,13 @@ impl KinematicCharacterController { // --------------------------------------------------------------- // 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). +// Bits: LIN_X, LIN_Y, LIN_Z, ANG_X, ANG_Y, ANG_Z. // --------------------------------------------------------------- /// 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). +/// The bits are ``LIN_X``, ``LIN_Y``, ``LIN_Z`` (translations along +/// each axis) and ``ANG_X``, ``ANG_Y``, ``ANG_Z`` (rotations around +/// each axis). /// /// Combine bits with the usual ``|``, ``&``, ``^``, ``-``, ``~`` /// operators. Distinct from :class:`LockedAxes`, which freezes @@ -946,10 +1037,9 @@ impl AxesMask { /// /// 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). +/// :ivar angular: Angular error vector. #[pyclass(name = "PdErrors", module = "rapier")] #[derive(Debug, Clone, Copy)] pub struct PdErrors { @@ -1026,8 +1116,7 @@ impl PdErrors { /// delta to the controlled rigid body each step. /// /// :ivar linear: Linear velocity correction. -/// :ivar angular: Angular velocity correction (vector in 3D, -/// scalar in 2D). +/// :ivar angular: Angular velocity correction. #[pyclass(name = "PidCorrection", module = "rapier", frozen)] #[derive(Debug, Clone, Copy)] pub struct PidCorrection { @@ -1062,6 +1151,26 @@ impl PidCorrection { } } +/// Extracts a controller gain: a float applied to every axis, or a per-axis 3-vector. +fn extract_gain(obj: &Bound<'_, PyAny>) -> PyResult> { + if let Ok(f) = obj.extract::() { + return Ok(crate::na::Vector3::repeat(f)); + } + obj.extract::() + .map(|v| v.0) + .map_err(|_| PyTypeError::new_err("expected a float or a 3-vector gain")) +} + +/// The target velocity of a `rigid_body_correction` call (zero when not given). +fn target_velocity(v: Option) -> rapier::dynamics::RigidBodyVelocity { + v.map_or_else(rapier::dynamics::RigidBodyVelocity::::zero, |v| { + rapier::dynamics::RigidBodyVelocity { + linvel: v.linvel.0.into(), + angvel: v.angvel.0.into(), + } + }) +} + // PdController — immutable PD. /// Proportional-derivative controller (PD). /// @@ -1077,11 +1186,14 @@ pub struct PdController(pub rapier::control::PdController); impl PdController { /// Build a PD controller with optional per-axis ``Kp`` / ``Kd``. /// + /// Each gain applies to both the linear and the angular axes. + /// /// :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``. + /// :param Kp: Proportional gain: a float applied to every axis, + /// or a per-axis 3-vector (defaults to ``60.0``). + /// :param Kd: Derivative gain, same shape as ``Kp`` (defaults + /// to ``0.8``). #[new] #[pyo3(signature = (axes=None, Kp=None, Kd=None))] #[allow(non_snake_case)] @@ -1095,34 +1207,14 @@ impl PdController { .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; + let k = extract_gain(k)?; + inner.lin_kp = k.into(); + inner.ang_kp = k.into(); } 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; + let k = extract_gain(k)?; + inner.lin_kd = k.into(); + inner.ang_kd = k.into(); } Ok(Self(inner)) } @@ -1138,6 +1230,47 @@ impl PdController { self.0.axes = v.0; } + /// Proportional gains of the linear axes (a float sets every axis). + #[getter] + fn lin_kp(&self) -> Vec3 { + Vec3(self.0.lin_kp.into()) + } + #[setter] + fn set_lin_kp(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.lin_kp = extract_gain(v)?.into(); + Ok(()) + } + /// Proportional gains of the angular axes (a float sets every axis). + #[getter] + fn ang_kp(&self) -> Vec3 { + Vec3(self.0.ang_kp.into()) + } + #[setter] + fn set_ang_kp(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.ang_kp = extract_gain(v)?.into(); + Ok(()) + } + /// Derivative gains of the linear axes (a float sets every axis). + #[getter] + fn lin_kd(&self) -> Vec3 { + Vec3(self.0.lin_kd.into()) + } + #[setter] + fn set_lin_kd(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.lin_kd = extract_gain(v)?.into(); + Ok(()) + } + /// Derivative gains of the angular axes (a float sets every axis). + #[getter] + fn ang_kd(&self) -> Vec3 { + Vec3(self.0.ang_kd.into()) + } + #[setter] + fn set_ang_kd(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.ang_kd = extract_gain(v)?.into(); + Ok(()) + } + /// Set the per-linear-axis proportional gain vector. fn set_axes_kp(&mut self, v: PyVector) { self.0.lin_kp = v.0.into(); @@ -1147,24 +1280,26 @@ impl PdController { self.0.lin_kd = v.0.into(); } - /// Velocity correction toward ``target_pose`` (zero target velocity). + /// Velocity correction toward ``target_pose`` and ``target_vels``. /// /// :param body: :class:`RigidBody` to drive. /// :param target_pose: Desired world pose. + /// :param target_vels: Desired :class:`RigidBodyVelocity` + /// (defaults to zero velocities). /// :returns: :class:`PidCorrection` to apply as a velocity delta. - #[pyo3(signature = (body, target_pose))] + #[pyo3(signature = (body, target_pose, target_vels=None))] fn rigid_body_correction( &self, - py: Python<'_>, body: &Bound<'_, PyAny>, target_pose: PyIsometry, + target_vels: Option, ) -> 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); + let owned = rb.to_owned_body()?; + let corr = self + .0 + .rigid_body_correction(&owned, target_pose, target_velocity(target_vels)); Ok(PidCorrection::from_velocity(corr)) } @@ -1197,10 +1332,15 @@ pub struct PidController(pub rapier::control::PidController); impl PidController { /// Build a PID with optional per-axis ``Kp`` / ``Ki`` / ``Kd``. /// + /// Each gain applies to both the linear and the angular axes. + /// /// :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). + /// :param Kp: Proportional gain: a float applied to every axis, + /// or a per-axis 3-vector (defaults to ``60.0``). + /// :param Ki: Integral gain, same shape as ``Kp`` (defaults to + /// ``1.0``). + /// :param Kd: Derivative gain, same shape as ``Kp`` (defaults + /// to ``0.8``). #[new] #[pyo3(signature = (axes=None, Kp=None, Ki=None, Kd=None))] #[allow(non_snake_case)] @@ -1216,46 +1356,19 @@ impl PidController { 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) - }?; + let k = extract_gain(k)?; + inner.pd.lin_kp = k.into(); + inner.pd.ang_kp = k.into(); } 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) - }?; + let k = extract_gain(k)?; + inner.lin_ki = k.into(); + inner.ang_ki = k.into(); + } + if let Some(k) = Kd { + let k = extract_gain(k)?; + inner.pd.lin_kd = k.into(); + inner.pd.ang_kd = k.into(); } Ok(Self(inner)) } @@ -1267,10 +1380,81 @@ impl PidController { } #[setter] /// Replace the :class:`AxesMask` selecting controlled DOFs. - fn set_axes_attr(&mut self, v: &AxesMask) { + fn set_axes(&mut self, v: &AxesMask) { self.0.set_axes(v.0); } + /// Proportional gains of the linear axes (a float sets every axis). + #[getter] + fn lin_kp(&self) -> Vec3 { + Vec3(self.0.pd.lin_kp.into()) + } + #[setter] + fn set_lin_kp(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.pd.lin_kp = extract_gain(v)?.into(); + Ok(()) + } + /// Proportional gains of the angular axes (a float sets every axis). + #[getter] + fn ang_kp(&self) -> Vec3 { + Vec3(self.0.pd.ang_kp.into()) + } + #[setter] + fn set_ang_kp(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.pd.ang_kp = extract_gain(v)?.into(); + Ok(()) + } + /// Integral gains of the linear axes (a float sets every axis). + #[getter] + fn lin_ki(&self) -> Vec3 { + Vec3(self.0.lin_ki.into()) + } + #[setter] + fn set_lin_ki(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.lin_ki = extract_gain(v)?.into(); + Ok(()) + } + /// Integral gains of the angular axes (a float sets every axis). + #[getter] + fn ang_ki(&self) -> Vec3 { + Vec3(self.0.ang_ki.into()) + } + #[setter] + fn set_ang_ki(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.ang_ki = extract_gain(v)?.into(); + Ok(()) + } + /// Derivative gains of the linear axes (a float sets every axis). + #[getter] + fn lin_kd(&self) -> Vec3 { + Vec3(self.0.pd.lin_kd.into()) + } + #[setter] + fn set_lin_kd(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.pd.lin_kd = extract_gain(v)?.into(); + Ok(()) + } + /// Derivative gains of the angular axes (a float sets every axis). + #[getter] + fn ang_kd(&self) -> Vec3 { + Vec3(self.0.pd.ang_kd.into()) + } + #[setter] + fn set_ang_kd(&mut self, v: &Bound<'_, PyAny>) -> PyResult<()> { + self.0.pd.ang_kd = extract_gain(v)?.into(); + Ok(()) + } + /// Linear error accumulated by the integral term (read-only; see :meth:`reset`). + #[getter] + fn lin_integral(&self) -> Vec3 { + Vec3(self.0.lin_integral.into()) + } + /// Angular error accumulated by the integral term (read-only; see :meth:`reset`). + #[getter] + fn ang_integral(&self) -> Vec3 { + Vec3(self.0.ang_integral.into()) + } + /// Set the per-linear-axis proportional gain vector. fn set_axes_kp(&mut self, v: PyVector) { self.0.pd.lin_kp = v.0.into(); @@ -1289,24 +1473,28 @@ impl PidController { self.0.reset_integrals(); } - /// One PID step: velocity correction toward ``target_pose``. + /// One PID step: velocity correction toward ``target_pose`` and ``target_vels``. /// /// :param dt: Time step in seconds. /// :param body: :class:`RigidBody` to drive. /// :param target_pose: Desired world pose. + /// :param target_vels: Desired :class:`RigidBodyVelocity` + /// (defaults to zero velocities). /// :returns: :class:`PidCorrection`. - #[pyo3(signature = (dt, body, target_pose))] + #[pyo3(signature = (dt, body, target_pose, target_vels=None))] fn rigid_body_correction( &mut self, dt: Real, body: &Bound<'_, PyAny>, target_pose: PyIsometry, + target_vels: Option, ) -> 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); + let owned = rb.to_owned_body()?; + let corr = self + .0 + .rigid_body_correction(dt, &owned, pose, target_velocity(target_vels)); Ok(PidCorrection::from_velocity(corr)) } @@ -1596,6 +1784,13 @@ impl RayCastInfo { /// :meth:`DynamicRayCastVehicleController.set_brake`, /// :meth:`set_steering`, and :meth:`apply_engine_force` to /// actually drive the wheel. +/// +/// :ivar center: World-space center of the wheel after the last +/// :meth:`DynamicRayCastVehicleController.update_vehicle`. +/// :ivar suspension: World-space direction of the suspension after the +/// last update. +/// :ivar axle: World-space direction of the (steered) axle after the +/// last update. #[pyclass(name = "Wheel", module = "rapier")] #[derive(Debug, Clone, Copy)] pub struct Wheel { @@ -1639,6 +1834,12 @@ pub struct Wheel { pub wheel_suspension_force: Real, #[pyo3(get)] pub raycast_info: RayCastInfo, + #[pyo3(get)] + pub center: Vec3, + #[pyo3(get)] + pub suspension: Vec3, + #[pyo3(get)] + pub axle: Vec3, } #[pymethods] @@ -1679,6 +1880,9 @@ impl Wheel { brake: w.brake, wheel_suspension_force: w.wheel_suspension_force, raycast_info: RayCastInfo::from_wheel(w), + center: Vec3(w.center().into()), + suspension: Vec3(w.suspension().into()), + axle: Vec3(w.axle().into()), } } } @@ -1747,14 +1951,19 @@ impl DynamicRayCastVehicleController { /// Requires a fresh :class:`QueryPipeline` — typically call /// ``world.update_query_pipeline()`` first. /// - /// The chassis body is excluded from the suspension raycasts (their origins - /// sit on its own collider), unless ``filter`` already excludes a body. + /// The chassis colliders are always excluded from the suspension + /// raycasts (their origins sit on them), in addition to the + /// ``filter``'s own exclusions. The ``filter``'s predicate, if any, + /// is called once per collider before the update (the sets are + /// locked during it), so it must not modify them. /// /// :param dt: Time step in seconds. - /// :param bodies: Rigid-body set (mutated). - /// :param colliders: Collider set (mutated). + /// :param bodies: Rigid-body set of ``queries`` (mutated). + /// :param colliders: Collider set of ``queries`` (mutated). /// :param queries: Up-to-date :class:`QueryPipeline`. /// :param filter: Optional query filter. + /// :raises ValueError: if ``bodies`` or ``colliders`` is not the + /// corresponding set of ``queries``. #[pyo3(signature = (dt, bodies, colliders, queries, filter=None))] fn update_vehicle( &mut self, @@ -1765,12 +1974,26 @@ impl DynamicRayCastVehicleController { 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); + check_query_sets(queries, bodies.bind(py), colliders.bind(py))?; + let accepted = precompute_predicate(py, filter, &colliders)?; + let chassis = self.0.chassis; let mut qf = filter.map(|f| f.as_rapier(None)).unwrap_or_default(); - qf.exclude_rigid_body = qf.exclude_rigid_body.or(Some(self.0.chassis)); + // Keep the caller's body exclusion; the chassis is then excluded by the predicate. + let exclude_chassis = qf.exclude_rigid_body.is_some_and(|h| h != chassis); + if qf.exclude_rigid_body.is_none() { + qf.exclude_rigid_body = Some(chassis); + } + let predicate = |h: rapier::geometry::ColliderHandle, co: &rapier::geometry::Collider| { + accepted.as_ref().is_none_or(|a| a.contains(&h)) + && !(exclude_chassis && co.parent() == Some(chassis)) + }; + if accepted.is_some() || exclude_chassis { + qf.predicate = Some(&predicate); + } + let bp = crate::events_hooks::try_read(&queries.broad_phase, py)?; + let np = crate::events_hooks::try_read(&queries.narrow_phase, py)?; + let mut bodies_ref = crate::events_hooks::try_write(&bodies, py)?; + let mut colliders_ref = crate::events_hooks::try_write(&colliders, py)?; let qpmut = bp.0.as_query_pipeline_mut( np.0.query_dispatcher(), &mut bodies_ref.0, @@ -1783,49 +2006,28 @@ impl DynamicRayCastVehicleController { /// 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; + /// :raises IndexError: if ``idx`` is out of range. + fn set_brake(&mut self, idx: isize, brake: Real) -> PyResult<()> { + let idx = self.wheel_index(idx)?; + self.0.wheels_mut()[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; + /// :raises IndexError: if ``idx`` is out of range. + fn set_steering(&mut self, idx: isize, steering: Real) -> PyResult<()> { + let idx = self.wheel_index(idx)?; + self.0.wheels_mut()[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; + /// :raises IndexError: if ``idx`` is out of range. + fn apply_engine_force(&mut self, idx: isize, force: Real) -> PyResult<()> { + let idx = self.wheel_index(idx)?; + self.0.wheels_mut()[idx].engine_force = force; Ok(()) } @@ -1836,16 +2038,10 @@ impl DynamicRayCastVehicleController { /// 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() - )) - }) + /// :raises IndexError: if ``idx`` is out of range. + fn wheel(&self, idx: isize) -> PyResult { + let idx = self.wheel_index(idx)?; + Ok(Wheel::from_rapier(&self.0.wheels()[idx])) } /// Current forward speed in km/h. @@ -1896,6 +2092,19 @@ impl DynamicRayCastVehicleController { } } +impl DynamicRayCastVehicleController { + /// Checks a wheel index, raising ``IndexError`` if it is out of range. + fn wheel_index(&self, idx: isize) -> PyResult { + let len = self.0.wheels().len(); + usize::try_from(idx) + .ok() + .filter(|&i| i < len) + .ok_or_else(|| { + PyIndexError::new_err(format!("wheel index {idx} out of range (len={len})")) + }) + } +} + // ============================================================================ // Registration. // ============================================================================ diff --git a/python/rapier-py-3d/src/debug_render.rs b/python/rapier-py-3d/src/debug_render.rs index bc59a5444..e668e4fa4 100644 --- a/python/rapier-py-3d/src/debug_render.rs +++ b/python/rapier-py-3d/src/debug_render.rs @@ -484,6 +484,10 @@ impl DebugRenderStyle { fn py_default() -> Self { Self(rapier::pipeline::DebugRenderStyle::default()) } + /// A standalone copy of the style (detached from the pipeline it may belong to). + fn copy(&self) -> Self { + *self + } /// Number of subdivisions used for curved shapes (cylinders, /// spheres, capsules). @@ -822,7 +826,7 @@ impl DebugRenderStyle { /// 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)`, +/// call `.lines()`, `.colors()`, `.objects()` for `(N, 2, 3)`, `(N, 4)`, /// `(N,)` NumPy arrays respectively (each call returns a fresh **copy** /// of the underlying data). #[pyclass(name = "DebugLineCollector", module = "rapier")] @@ -861,23 +865,10 @@ impl DebugLineCollector { ) } - /// 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; + /// Return a fresh `(N, 2, 3)` NumPy array of segment endpoints. + fn lines<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray3> { 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 + _lines_array(py, &lines) } /// Return a fresh `(N, 4)` NumPy array of RGBA (post-HSLA→RGBA) @@ -1045,9 +1036,11 @@ impl rapier::pipeline::DebugRenderBackend for _PyBackendAdapter { /// /// For purely-data use cases prefer :meth:`render_to_arrays`, /// which returns NumPy arrays directly. -#[pyclass(name = "DebugRenderPipeline", module = "rapier", unsendable)] +#[pyclass(name = "DebugRenderPipeline", module = "rapier")] pub struct DebugRenderPipeline { inner: rapier::pipeline::DebugRenderPipeline, + // The style `inner` renders with, copied in before each render. + style: Py, } impl DebugRenderPipeline { @@ -1062,6 +1055,7 @@ impl DebugRenderPipeline { soft_bodies: Option<&crate::soft_body::SoftBodySet>, backend: &mut dyn rapier::pipeline::DebugRenderBackend, ) { + self.inner.style = Python::with_gil(|py| self.style.borrow(py).0); let scratch = rapier::dynamics::SoftBodySet::new(); let soft_bodies = soft_bodies.map_or(&scratch, |s| &s.0); // The trait method `render` takes `&mut impl DebugRenderBackend` @@ -1104,12 +1098,17 @@ impl DebugRenderPipeline { /// configuration. Defaults to rapier's upstream defaults. #[new] #[pyo3(signature = (mode = None, style = None))] - fn new(mode: Option<&DebugRenderMode>, style: Option<&DebugRenderStyle>) -> Self { + fn new( + py: Python<'_>, + mode: Option<&DebugRenderMode>, + style: Option<&DebugRenderStyle>, + ) -> PyResult { let m = mode.map(|m| m.0).unwrap_or_default(); let s = style.map(|s| s.0).unwrap_or_default(); - Self { + Ok(Self { inner: rapier::pipeline::DebugRenderPipeline::new(s, m), - } + style: Py::new(py, DebugRenderStyle(s))?, + }) } /// Current :class:`DebugRenderMode` flag set. @@ -1122,15 +1121,19 @@ impl DebugRenderPipeline { fn set_mode(&mut self, v: &DebugRenderMode) { self.inner.mode = v.0; } - /// Current :class:`DebugRenderStyle`. + /// The :class:`DebugRenderStyle` used by the next renders. + /// + /// The same object is returned on every access, so modifying it + /// (``pipeline.style.subdivisions = 40``) changes the rendering. #[getter] - fn style(&self) -> DebugRenderStyle { - DebugRenderStyle(self.inner.style) + fn style(&self, py: Python<'_>) -> Py { + self.style.clone_ref(py) } #[setter] - /// Replace the current :class:`DebugRenderStyle`. - fn set_style(&mut self, v: &DebugRenderStyle) { - self.inner.style = v.0; + /// Copy the values of ``v`` into :attr:`style` (later changes to ``v`` + /// don't affect the pipeline). + fn set_style(&mut self, py: Python<'_>, v: &DebugRenderStyle) { + self.style.borrow_mut(py).0 = v.0; } /// Render the scene into the given ``backend``. @@ -1238,7 +1241,7 @@ impl DebugRenderPipeline { Bound<'py, crate::numpy::PyArray2>, Bound<'py, crate::numpy::PyArray1>, )> { - use crate::numpy::{PyArray1, PyArray2, PyArray3, PyArrayMethods}; + use crate::numpy::{PyArray1, PyArray2}; let buf: _DbgArc<_DbgMutex>> = _DbgArc::new(_DbgMutex::new(Vec::new())); { @@ -1259,19 +1262,7 @@ impl DebugRenderPipeline { 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 lines_arr = _lines_array(py, &lines); let mut col_flat: Vec> = Vec::with_capacity(n); for ln in lines.iter() { @@ -1296,15 +1287,22 @@ impl DebugRenderPipeline { } } -// 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]); +/// The `(N, 2, 3)` array of the end points of `lines`. +fn _lines_array<'py>( + py: Python<'py>, + lines: &[DebugLine], +) -> Bound<'py, crate::numpy::PyArray3> { + use crate::numpy::{PyArray3, PyArrayMethods}; + let arr = PyArray3::::zeros_bound(py, [lines.len(), 2, 3], false); + // SAFETY: the array was just created and is not shared yet. + let mut view = unsafe { arr.as_array_mut() }; + for (i, ln) in lines.iter().enumerate() { + for d in 0..3 { + view[[i, 0, d]] = ln.a[d]; + view[[i, 1, d]] = ln.b[d]; + } } - row + arr } pub fn register_debug_render( diff --git a/python/rapier-py-3d/src/dynamics.rs b/python/rapier-py-3d/src/dynamics.rs index 54bd46462..831a566da 100644 --- a/python/rapier-py-3d/src/dynamics.rs +++ b/python/rapier-py-3d/src/dynamics.rs @@ -15,7 +15,7 @@ use crate::*; use rapier3d as rapier; -use crate::pyo3::exceptions::{PyNotImplementedError, PyTypeError}; +use crate::pyo3::exceptions::{PyNotImplementedError, PyTypeError, PyValueError}; use crate::pyo3::prelude::*; use crate::pyo3::pyclass::CompareOp; @@ -145,8 +145,8 @@ impl RigidBodyHandle { /// ``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)] +#[pyclass(name = "RigidBodyType", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum RigidBodyType { /// Fully simulated: responds to forces, gravity and contacts. DYNAMIC, @@ -202,9 +202,16 @@ impl RigidBodyType { /// 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)] +/// (``GEOMETRIC_MEAN > CLAMPED_SUM > MAX > MULTIPLY > MIN > AVERAGE``). +#[pyclass( + name = "CoefficientCombineRule", + module = "rapier", + eq, + eq_int, + hash, + frozen +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CoefficientCombineRule { /// Arithmetic mean of the two coefficients. AVERAGE, @@ -366,33 +373,69 @@ impl RigidBodyActivation { // SpringCoefficients // ============================================================ -/// Soft-constraint spring parameters used for contact and joint -/// regularization. +/// Soft-constraint spring parameters used for contact, joint and soft-body regularization: a +/// natural frequency (in Hz) and a damping ratio (unitless, ``1`` is critically damped). /// -/// 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 +/// Rapier converts them 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); +/// Warns that a `SpringCoefficients` name is a deprecated alias. +fn warn_spring_alias(py: Python<'_>, old: &str, new: &str) -> PyResult<()> { + PyErr::warn_bound( + py, + &py.get_type_bound::(), + &format!("SpringCoefficients.{old} is deprecated, use {new}"), + 1, + ) +} + #[pymethods] impl SpringCoefficients { - /// Build a spring with the given natural frequency and damping - /// ratio. + /// Build a spring with the given natural frequency (Hz) and damping ratio; each one left + /// out takes its :meth:`contact_defaults` value (``30.0`` Hz and ``10.0``). /// - /// :param stiffness: natural frequency in Hz (default ``30.0``). - /// :param damping: damping ratio (default ``5.0``). + /// The ``stiffness`` and ``damping`` keywords are deprecated aliases of + /// ``natural_frequency`` and ``damping_ratio``. #[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, - }) + #[pyo3(signature = (natural_frequency=None, damping_ratio=None, *, stiffness=None, damping=None))] + fn new( + py: Python<'_>, + natural_frequency: Option, + damping_ratio: Option, + stiffness: Option, + damping: Option, + ) -> PyResult { + let defaults = rapier::dynamics::SpringCoefficients::::contact_defaults(); + let pick = |value: Option, alias: Option, name: &str, old: &str| match ( + value, alias, + ) { + (Some(_), Some(_)) => Err(PyTypeError::new_err(format!( + "SpringCoefficients: `{old}` is an alias of `{name}`; give only one of them" + ))), + (None, Some(v)) => { + warn_spring_alias(py, old, name)?; + Ok(Some(v)) + } + (v, None) => Ok(v), + }; + let natural_frequency = pick( + natural_frequency, + stiffness, + "natural_frequency", + "stiffness", + )? + .unwrap_or(defaults.natural_frequency); + let damping_ratio = pick(damping_ratio, damping, "damping_ratio", "damping")? + .unwrap_or(defaults.damping_ratio); + Ok(Self(rapier::dynamics::SpringCoefficients::new( + natural_frequency, + damping_ratio, + ))) } /// Return the default spring coefficients used for contact /// regularization. @@ -406,15 +449,17 @@ impl SpringCoefficients { fn joint_defaults() -> Self { Self(rapier::dynamics::SpringCoefficients::joint_defaults()) } - /// Spring natural frequency (alias for ``natural_frequency``). + /// Deprecated alias of :attr:`natural_frequency`. #[getter] - fn stiffness(&self) -> Real { - self.0.natural_frequency + fn stiffness(&self, py: Python<'_>) -> PyResult { + warn_spring_alias(py, "stiffness", "natural_frequency")?; + Ok(self.0.natural_frequency) } - /// Set the natural frequency. #[setter] - fn set_stiffness(&mut self, v: Real) { + fn set_stiffness(&mut self, py: Python<'_>, v: Real) -> PyResult<()> { + warn_spring_alias(py, "stiffness", "natural_frequency")?; self.0.natural_frequency = v; + Ok(()) } /// Natural frequency of the spring, in Hz. #[getter] @@ -426,15 +471,17 @@ impl SpringCoefficients { fn set_natural_frequency(&mut self, v: Real) { self.0.natural_frequency = v; } - /// Damping ratio (alias for ``damping_ratio``). + /// Deprecated alias of :attr:`damping_ratio`. #[getter] - fn damping(&self) -> Real { - self.0.damping_ratio + fn damping(&self, py: Python<'_>) -> PyResult { + warn_spring_alias(py, "damping", "damping_ratio")?; + Ok(self.0.damping_ratio) } - /// Set the damping ratio. #[setter] - fn set_damping(&mut self, v: Real) { + fn set_damping(&mut self, py: Python<'_>, v: Real) -> PyResult<()> { + warn_spring_alias(py, "damping", "damping_ratio")?; self.0.damping_ratio = v; + Ok(()) } /// Damping ratio (unitless, ``1.0`` is critically damped). #[getter] @@ -448,7 +495,7 @@ impl SpringCoefficients { } fn __repr__(&self) -> String { format!( - "SpringCoefficients(stiffness={}, damping={})", + "SpringCoefficients(natural_frequency={}, damping_ratio={})", self.0.natural_frequency, self.0.damping_ratio ) } @@ -464,7 +511,7 @@ impl SpringCoefficients { /// 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)] +#[pyclass(name = "IslandManager", module = "rapier")] pub struct IslandManager(pub rapier::dynamics::IslandManager); #[pymethods] @@ -575,7 +622,7 @@ impl RigidBodyHandleIter { /// 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)] +#[pyclass(name = "CCDSolver", module = "rapier")] pub struct CCDSolver(pub rapier::dynamics::CCDSolver); #[pymethods] @@ -612,33 +659,43 @@ impl CCDSolver { // FrictionModel (3D only) // ============================================================ -/// Friction model used by the 3D contact solver. +/// Friction model used by the contact solver between rigid bodies (multibodies always use +/// ``COULOMB``). /// -/// - ``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)] +/// - ``SIMPLIFIED`` (the default): one Coulomb friction constraint per group of up to four +/// contacts of a manifold, plus a rotational "twist" constraint. Much faster to solve, but +/// less accurate. ``COEFFICIENT`` is a deprecated alias. +/// - ``COULOMB``: one Coulomb friction constraint per contact point. +#[pyclass(name = "FrictionModel", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FrictionModel { - /// Cheap pyramidal approximation of the friction cone. - COEFFICIENT, - /// True circular Coulomb friction cone. + /// One friction constraint per group of contacts, plus a twist constraint (default). + SIMPLIFIED, + /// One Coulomb friction constraint per contact point. COULOMB, } +#[pymethods] +impl FrictionModel { + /// Deprecated alias of ``SIMPLIFIED``. + #[classattr] + fn COEFFICIENT() -> Self { + Self::SIMPLIFIED + } +} + impl FrictionModel { #[inline] pub(crate) fn to_rapier(self) -> rapier::dynamics::FrictionModel { match self { - Self::COEFFICIENT => rapier::dynamics::FrictionModel::Simplified, + Self::SIMPLIFIED => 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::Simplified => Self::SIMPLIFIED, rapier::dynamics::FrictionModel::Coulomb => Self::COULOMB, } } @@ -994,9 +1051,9 @@ impl RigidBodyVelocity { /// 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). +/// ``force`` and ``torque`` are the user forces: they are not cleared by the +/// simulation and keep being applied at every step until reset (unlike +/// impulses, which act once instantaneously). #[pyclass(name = "RigidBodyForces", module = "rapier")] #[derive(Debug, Clone, Copy)] pub struct RigidBodyForces { @@ -1217,14 +1274,21 @@ impl IntegrationParameters { fn set_dt(&mut self, v: Real) { self.0.dt = v; } - /// The settings shared by every soft body (a copy: assign it back to apply changes). + /// The settings shared by every soft body, as a live view: setting one of its fields + /// changes these parameters. Assigning a :class:`SoftBodiesSettings` replaces them all. #[getter] - fn soft_bodies(&self) -> crate::soft_body::SoftBodiesSettings { - crate::soft_body::SoftBodiesSettings(self.0.soft_bodies) + fn soft_bodies(slf: &Bound<'_, Self>) -> crate::soft_body::SoftBodiesSettings { + crate::soft_body::SoftBodiesSettings::in_params(slf.clone().unbind()) } #[setter] - fn set_soft_bodies(&mut self, v: &crate::soft_body::SoftBodiesSettings) { - self.0.soft_bodies = v.0; + fn set_soft_bodies( + slf: &Bound<'_, Self>, + v: &Bound<'_, crate::soft_body::SoftBodiesSettings>, + ) -> PyResult<()> { + // Read the value first: `v` may be a view of these very parameters. + let value = v.try_borrow()?.get()?; + slf.try_borrow_mut()?.0.soft_bodies = value; + Ok(()) } /// Minimum substep length used by CCD, in seconds. #[getter] @@ -1397,12 +1461,38 @@ impl IntegrationParameters { self.0.static_contact_softness = v.0; } - /// 3D friction model used by the contact solver. + /// If ``True``, friction is also solved during the biased (position-correcting) pass of + /// each substep instead of only during the unbiased one (default: ``False``). Leaving it off + /// makes contacts cheaper and keeps tall stacks stable. + #[getter] + fn friction_in_bias_pass(&self) -> bool { + self.0.friction_in_bias_pass + } + /// Solve friction during the biased pass too. + #[setter] + fn set_friction_in_bias_pass(&mut self, v: bool) { + self.0.friction_in_bias_pass = v; + } + /// If ``True``, the impulse joints are warm-started like contacts: the impulses of the + /// previous step, scaled by :attr:`warmstart_coefficient`, are re-applied at the start of + /// each substep (default: ``False``). Improves the convergence of stiff joint assemblies; + /// multibody joints are unaffected. + #[getter] + fn warmstart_joints(&self) -> bool { + self.0.warmstart_joints + } + /// Enable or disable the warm-starting of the impulse joints. + #[setter] + fn set_warmstart_joints(&mut self, v: bool) { + self.0.warmstart_joints = v; + } + + /// Friction model used by the contact solver (see :class:`FrictionModel`). #[getter] fn friction_model(&self) -> FrictionModel { FrictionModel::from_rapier(self.0.friction_model) } - /// Set the 3D friction model. + /// Set the friction model. #[setter] fn set_friction_model(&mut self, v: FrictionModel) { self.0.friction_model = v.to_rapier(); @@ -1534,40 +1624,39 @@ impl RigidBody { /// 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 { + /// already removed) raises `InvalidHandle`. + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::RigidBody) -> R) -> PyResult { match &self.backing { - RigidBodyBacking::Owned(b) => f(b), + RigidBodyBacking::Owned(b) => Ok(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) + RigidBodySet::read(set.bind(py), |set| set.get(*handle).map(f))? + .ok_or_else(|| crate::errors::stale_view("RigidBody")) }), } } /// 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 { + fn with_mut( + &mut self, + f: impl FnOnce(&mut rapier::dynamics::RigidBody) -> R, + ) -> PyResult { match &mut self.backing { - RigidBodyBacking::Owned(b) => f(b), + RigidBodyBacking::Owned(b) => Ok(f(b)), RigidBodyBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); + let mut set = crate::errors::try_borrow_mut(set.bind(py))?; let body = set .0 .get_mut(*handle) - .expect("RigidBody refers to a body that was removed from its set"); - f(body) + .ok_or_else(|| crate::errors::stale_view("RigidBody"))?; + Ok(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 { + pub fn to_owned_body(&self) -> PyResult { self.with_ref(|b| b.clone()) } } @@ -1578,7 +1667,7 @@ impl RigidBody { /// World-space pose of the body (read+write). #[getter] - fn position(&self) -> Isometry3 { + fn position(&self) -> PyResult { self.with_ref(|b| { let r = b.position(); let na_iso: crate::na::Isometry = (*r).into(); @@ -1588,14 +1677,15 @@ impl RigidBody { /// 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) { + fn set_position(&mut self, p: PyIsometry) -> PyResult<()> { let g: rapier::math::Pose = p.0.into(); - self.with_mut(|b| b.set_position(g, true)); + self.with_mut(|b| b.set_position(g, true))?; + Ok(()) } /// Translation portion of the pose (read+write). #[getter] - fn translation(&self) -> Vec3 { + fn translation(&self) -> PyResult { self.with_ref(|b| { let v: crate::na::SVector = b.translation().into(); Vec3(v) @@ -1603,14 +1693,15 @@ impl RigidBody { } /// Set the translation, leaving the rotation unchanged. #[setter] - fn set_translation(&mut self, v: PyVector) { + fn set_translation(&mut self, v: PyVector) -> PyResult<()> { let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_translation(g, true)); + self.with_mut(|b| b.set_translation(g, true))?; + Ok(()) } /// Rotation portion of the pose (read+write). #[getter] - fn rotation(&self) -> Rotation3 { + fn rotation(&self) -> PyResult { self.with_ref(|b| { let r = *b.rotation(); Rotation3(r.into()) @@ -1618,14 +1709,15 @@ impl RigidBody { } /// Set the rotation, leaving the translation unchanged. #[setter] - fn set_rotation(&mut self, r: PyRotation) { + fn set_rotation(&mut self, r: PyRotation) -> PyResult<()> { let g: rapier::math::Rotation = r.0.into(); - self.with_mut(|b| b.set_rotation(g, true)); + self.with_mut(|b| b.set_rotation(g, true))?; + Ok(()) } /// Linear velocity in world space (read+write). #[getter] - fn linvel(&self) -> Vec3 { + fn linvel(&self) -> PyResult { self.with_ref(|b| { let v: crate::na::SVector = b.linvel().into(); Vec3(v) @@ -1633,35 +1725,36 @@ impl RigidBody { } /// Set the linear velocity (wakes the body). #[setter] - fn set_linvel(&mut self, v: PyVector) { + fn set_linvel(&mut self, v: PyVector) -> PyResult<()> { let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_linvel(g, true)); + self.with_mut(|b| b.set_linvel(g, true))?; + Ok(()) } /// Total mass of the body (colliders + additional) (read-only). #[getter] - fn mass(&self) -> Real { + fn mass(&self) -> PyResult { self.with_ref(|b| b.mass()) } /// Inverse mass — ``0`` for fixed or infinite-mass bodies /// (read-only). #[getter] - fn inv_mass(&self) -> Real { + fn inv_mass(&self) -> PyResult { 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)) + fn center_of_mass(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|b| b.center_of_mass())?.into(); + Ok(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)) + fn local_center_of_mass(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|b| b.local_center_of_mass())?.into(); + Ok(Point3(crate::na::Point::from(v))) } /// Body-local mass properties (read-only). @@ -1670,140 +1763,143 @@ impl RigidBody { /// ``set_additional_mass_properties`` or /// ``recompute_mass_properties_from_colliders``. #[getter] - fn mass_properties(&self) -> MassProperties { + fn mass_properties(&self) -> PyResult { 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())) + fn body_type(&self) -> PyResult { + Ok(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) { + fn set_body_type(&mut self, t: RigidBodyType) -> PyResult<()> { let rt = t.to_rapier(); - self.with_mut(|b| b.set_body_type(rt, true)); + self.with_mut(|b| b.set_body_type(rt, true))?; + Ok(()) } /// Multiplier applied to the world gravity for this body /// (read+write). ``0`` disables gravity for the body. #[getter] - fn gravity_scale(&self) -> Real { + fn gravity_scale(&self) -> PyResult { 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)); + fn set_gravity_scale(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|b| b.set_gravity_scale(v, true)) } /// Per-second linear-velocity damping coefficient (read+write). #[getter] - fn linear_damping(&self) -> Real { + fn linear_damping(&self) -> PyResult { 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)); + fn set_linear_damping(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|b| b.set_linear_damping(v)) } /// Per-second angular-velocity damping coefficient (read+write). #[getter] - fn angular_damping(&self) -> Real { + fn angular_damping(&self) -> PyResult { 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)); + fn set_angular_damping(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|b| b.set_angular_damping(v)) } /// Dominance group of the body (read+write); see /// ``RigidBodyDominance``. #[getter] - fn dominance_group(&self) -> i8 { + fn dominance_group(&self) -> PyResult { 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)); + fn set_dominance_group(&mut self, v: i8) -> PyResult<()> { + 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 { + fn additional_solver_iterations(&self) -> PyResult { self.with_ref(|b| b.additional_solver_iterations()) } /// Extra internal PGS iterations run per substep for the island component containing /// this body (default ``0``); the component runs the largest request. #[getter] - fn additional_pgs_iterations(&self) -> usize { + fn additional_pgs_iterations(&self) -> PyResult { self.with_ref(|b| b.additional_pgs_iterations()) } #[setter] - fn set_additional_pgs_iterations(&mut self, v: usize) { + fn set_additional_pgs_iterations(&mut self, v: usize) -> PyResult<()> { self.with_mut(|b| b.set_additional_pgs_iterations(v)) } /// Is this body the proxy of a soft-body cluster? #[getter] - fn is_soft_frame(&self) -> bool { + fn is_soft_frame(&self) -> PyResult { self.with_ref(|b| b.is_soft_frame()) } /// The soft body this body is a cluster proxy of, if any. #[getter] - fn soft_body(&self) -> Option { + fn soft_body(&self) -> PyResult> { self.with_ref(|b| b.soft_body().map(crate::soft_body::SoftBodyHandle)) } /// The index of the cluster this proxy stands for in its soft body, if any. #[getter] - fn soft_cluster(&self) -> Option { + fn soft_cluster(&self) -> PyResult> { self.with_ref(|b| b.soft_cluster()) } /// 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)); + fn set_additional_solver_iterations(&mut self, v: usize) -> PyResult<()> { + 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())) + fn locked_axes(&self) -> PyResult { + Ok(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)); + fn set_locked_axes(&mut self, v: LockedAxes) -> PyResult<()> { + 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 { + fn is_sleeping(&self) -> PyResult { self.with_ref(|b| b.is_sleeping()) } /// Whether the body is currently moving (read-only). #[getter] - fn is_moving(&self) -> bool { + fn is_moving(&self) -> PyResult { self.with_ref(|b| b.is_moving()) } /// Whether the body is dynamic (read-only). #[getter] - fn is_dynamic(&self) -> bool { + fn is_dynamic(&self) -> PyResult { self.with_ref(|b| b.is_dynamic()) } /// Whether the body is kinematic (either variant) (read-only). #[getter] - fn is_kinematic(&self) -> bool { + fn is_kinematic(&self) -> PyResult { self.with_ref(|b| b.is_kinematic()) } /// Whether the body is fixed (read-only). #[getter] - fn is_fixed(&self) -> bool { + fn is_fixed(&self) -> PyResult { self.with_ref(|b| b.is_fixed()) } @@ -1811,77 +1907,94 @@ impl RigidBody { /// /// A disabled body is fully skipped by the solver and queries. #[getter] - fn is_enabled(&self) -> bool { + fn is_enabled(&self) -> PyResult { 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)); + fn set_is_enabled(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|b| b.set_enabled(v)) } /// Whether substep CCD is enabled for this body (read+write). #[getter] - fn ccd_enabled(&self) -> bool { + fn ccd_enabled(&self) -> PyResult { 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)); + fn set_ccd_enabled(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|b| b.enable_ccd(v)) } /// Soft-CCD prediction distance (read+write); ``0`` disables it. #[getter] - fn soft_ccd_prediction(&self) -> Real { + fn soft_ccd_prediction(&self) -> PyResult { 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)); + fn set_soft_ccd_prediction(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|b| b.set_soft_ccd_prediction(v)) + } + + /// Whether this body may exceed the angular speed cap (read+write). + /// + /// By default the angular velocity is clamped at each substep (about 45 degrees per + /// substep) to keep CCD reliable; enable this for bodies that must spin fast, e.g. wheels. + #[getter] + fn allow_fast_rotation(&self) -> PyResult { + self.with_ref(|b| b.is_fast_rotation_allowed()) + } + /// Allow or disallow this body to exceed the angular speed cap. + #[setter] + fn set_allow_fast_rotation(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|b| b.set_allow_fast_rotation(v))?; + Ok(()) } /// Application-defined ``u128`` payload attached to this body /// (read+write). #[getter] - fn user_data(&self) -> u128 { + fn user_data(&self) -> PyResult { 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); + fn set_user_data(&mut self, v: u128) -> PyResult<()> { + 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. + /// Force accumulated by ``add_force`` / ``add_force_at_point`` (read-only). + /// + /// It is not cleared by the simulation: it keeps being applied at every step until + /// ``reset_forces`` is called. Always zero for non-dynamic bodies. #[getter] - fn user_force(&self) -> Vec3 { - let v: crate::na::SVector = self.with_ref(|b| b.user_force()).into(); - Vec3(v) + fn user_force(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|b| b.user_force())?.into(); + Ok(Vec3(v)) } /// Handles of all colliders attached to this body (read-only). #[getter] - fn colliders(&self) -> Vec { + fn colliders(&self) -> PyResult> { self.with_ref(|b| b.colliders().iter().copied().map(ColliderHandle).collect()) } /// Activation / sleep state of this body (read+write). #[getter] - fn activation(&self) -> RigidBodyActivation { + fn activation(&self) -> PyResult { 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); + fn set_activation(&mut self, v: RigidBodyActivation) -> PyResult<()> { + 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 { + fn next_position(&self) -> PyResult { self.with_ref(|b| { let p: crate::na::Isometry = (*b.next_position()).into(); Isometry3(p) @@ -1890,33 +2003,42 @@ impl RigidBody { // ---- mutating methods ---- - /// Accumulate a force to be applied during the next step. + /// Add a persistent force to this body. /// - /// Forces accumulate over the step and are cleared automatically - /// before the next ``step()``. Use ``apply_impulse`` for an - /// instantaneous velocity change. + /// Successive calls accumulate, and the force is not cleared by the simulation: it keeps + /// being applied at every step until ``reset_forces`` is called (call it after stepping to + /// apply a force for a single step). Use ``apply_impulse`` for an instantaneous velocity + /// change. Only affects dynamic bodies. /// /// :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) { + fn add_force(&mut self, force: PyVector, wake_up: bool) -> PyResult<()> { let g: rapier::math::Vector = force.0.into(); - self.with_mut(|b| b.add_force(g, wake_up)); + self.with_mut(|b| b.add_force(g, wake_up))?; + Ok(()) } - /// Accumulate a force applied at a given world-space point. + /// Add a persistent 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``. + /// plus a torque ``(point - com) × force``. Like ``add_force``, both + /// persist until ``reset_forces`` / ``reset_torques`` are called. /// /// :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) { + fn add_force_at_point( + &mut self, + force: PyVector, + point: PyPoint, + wake_up: bool, + ) -> PyResult<()> { 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)); + self.with_mut(|b| b.add_force_at_point(f, p, wake_up))?; + Ok(()) } /// Apply an instantaneous linear impulse (units: ``N·s``). @@ -1924,9 +2046,10 @@ impl RigidBody { /// :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) { + fn apply_impulse(&mut self, impulse: PyVector, wake_up: bool) -> PyResult<()> { let g: rapier::math::Vector = impulse.0.into(); - self.with_mut(|b| b.apply_impulse(g, wake_up)); + self.with_mut(|b| b.apply_impulse(g, wake_up))?; + Ok(()) } /// Apply an instantaneous impulse at a given world-space point. @@ -1935,25 +2058,31 @@ impl RigidBody { /// :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) { + fn apply_impulse_at_point( + &mut self, + impulse: PyVector, + point: PyPoint, + wake_up: bool, + ) -> PyResult<()> { 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)); + self.with_mut(|b| b.apply_impulse_at_point(imp, p, wake_up))?; + Ok(()) } - /// Clear any accumulated forces on this body. + /// Clear the forces added with ``add_force`` / ``add_force_at_point``. /// /// :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)); + fn reset_forces(&mut self, wake_up: bool) -> PyResult<()> { + self.with_mut(|b| b.reset_forces(wake_up)) } - /// Clear any accumulated torques on this body. + /// Clear the torques added with ``add_torque`` / ``add_force_at_point``. /// /// :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)); + fn reset_torques(&mut self, wake_up: bool) -> PyResult<()> { + self.with_mut(|b| b.reset_torques(wake_up)) } /// Wake the body, allowing it to participate in the next step. @@ -1961,12 +2090,12 @@ impl RigidBody { /// :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)); + fn wake_up(&mut self, strong: bool) -> PyResult<()> { + self.with_mut(|b| b.wake_up(strong)) } /// Put the body to sleep immediately. - fn sleep(&mut self) { - self.with_mut(|b| b.sleep()); + fn sleep(&mut self) -> PyResult<()> { + self.with_mut(|b| b.sleep()) } /// World-space velocity at the given world-space point. @@ -1975,17 +2104,17 @@ impl RigidBody { /// ``v_point = linvel + angvel × (point - com)``. /// /// :param point: world-space point. - fn velocity_at_point(&self, point: PyPoint) -> Vec3 { + fn velocity_at_point(&self, point: PyPoint) -> PyResult { 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) + let v: crate::na::SVector = self.with_ref(|b| b.velocity_at_point(p))?.into(); + Ok(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 { + fn kinetic_energy(&self) -> PyResult { self.with_ref(|b| b.kinetic_energy()) } @@ -1996,7 +2125,7 @@ impl RigidBody { /// :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 { + fn gravitational_potential_energy(&self, dt: Real, gravity: PyVector) -> PyResult { let g: rapier::math::Vector = gravity.0.into(); self.with_ref(|b| b.gravitational_potential_energy(dt, g)) } @@ -2006,11 +2135,11 @@ impl RigidBody { /// is not modified. /// /// :param dt: prediction horizon in seconds. - fn predict_position_using_velocity(&self, dt: Real) -> Isometry3 { + fn predict_position_using_velocity(&self, dt: Real) -> PyResult { let p: crate::na::Isometry = self - .with_ref(|b| b.predict_position_using_velocity(dt)) + .with_ref(|b| b.predict_position_using_velocity(dt))? .into(); - Isometry3(p) + Ok(Isometry3(p)) } /// Predict the body's pose after ``dt`` seconds using its @@ -2018,7 +2147,7 @@ impl RigidBody { /// integrating (the body is not modified). /// /// :param dt: prediction horizon in seconds. - fn predict_position_using_velocity_and_forces(&self, dt: Real) -> Isometry3 { + fn predict_position_using_velocity_and_forces(&self, dt: Real) -> PyResult { self.with_ref(|b| { let p: crate::na::Isometry = b.predict_position_using_velocity_and_forces(dt).into(); @@ -2035,8 +2164,11 @@ impl RigidBody { /// /// :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)); + fn recompute_mass_properties_from_colliders( + &mut self, + colliders: &ColliderSet, + ) -> PyResult<()> { + self.with_mut(|b| b.recompute_mass_properties_from_colliders(&colliders.0)) } /// Add ``mass`` on top of the colliders' contribution. @@ -2044,8 +2176,8 @@ impl RigidBody { /// :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)); + fn set_additional_mass(&mut self, mass: Real, wake_up: bool) -> PyResult<()> { + self.with_mut(|b| b.set_additional_mass(mass, wake_up)) } /// Add full mass properties on top of the colliders' @@ -2054,29 +2186,78 @@ impl RigidBody { /// :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)); + fn set_additional_mass_properties( + &mut self, + mp: &MassProperties, + wake_up: bool, + ) -> PyResult<()> { + 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). + /// Add ``gravity * mass`` to this body's persistent force (see ``add_force``). + /// + /// Useful for a body with a custom gravity while the world gravity is zero. Like any + /// force added with ``add_force``, it keeps applying at every step until + /// ``reset_forces`` is called. /// /// :param gravity: gravity acceleration vector. - fn add_gravitational_force(&mut self, gravity: PyVector) { - // Add gravity * mass as a force. + /// :param wake_up: if ``True`` (default), wake the body up. + #[pyo3(signature = (gravity, wake_up=true))] + fn add_gravitational_force(&mut self, gravity: PyVector, wake_up: bool) -> PyResult<()> { 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)); + self.with_mut(|b| { + let m = b.mass(); + b.add_force(g * m, wake_up) + })?; + Ok(()) } - fn __repr__(&self) -> String { - let p = self.with_ref(|b| b.translation()); - format!( + /// Set the translation a position-based kinematic body will reach at the next step. + /// + /// Only affects ``KINEMATIC_POSITION_BASED`` bodies. The body is not moved right away: + /// its velocity is deduced from this target at the next step, which then moves it there + /// (pushing the dynamic bodies on its way). The target is readable from + /// ``next_position``. + /// + /// :param translation: world-space translation to reach. + fn set_next_kinematic_translation(&mut self, translation: PyVector) -> PyResult<()> { + let g: rapier::math::Vector = translation.0.into(); + self.with_mut(|b| b.set_next_kinematic_translation(g))?; + Ok(()) + } + + /// Set the rotation a position-based kinematic body will reach at the next step. + /// + /// Only affects ``KINEMATIC_POSITION_BASED`` bodies; its angular velocity is deduced + /// from this target at the next step. See ``set_next_kinematic_translation``. + /// + /// :param rotation: world-space rotation to reach. + fn set_next_kinematic_rotation(&mut self, rotation: PyRotation) -> PyResult<()> { + let g: rapier::math::Rotation = rotation.0.into(); + self.with_mut(|b| b.set_next_kinematic_rotation(g))?; + Ok(()) + } + + /// Set the pose a position-based kinematic body will reach at the next step. + /// + /// Only affects ``KINEMATIC_POSITION_BASED`` bodies; its linear and angular velocities + /// are deduced from this target at the next step. See + /// ``set_next_kinematic_translation``. + /// + /// :param pose: world-space pose to reach. + fn set_next_kinematic_position(&mut self, pose: PyIsometry) -> PyResult<()> { + let g: rapier::math::Pose = pose.0.into(); + self.with_mut(|b| b.set_next_kinematic_position(g))?; + Ok(()) + } + + fn __repr__(&self) -> PyResult { + let p = self.with_ref(|b| b.translation())?; + Ok(format!( "RigidBody(translation={:?}, type={:?})", p, - self.with_ref(|b| b.body_type()) - ) + self.with_ref(|b| b.body_type())? + )) } } @@ -2158,6 +2339,10 @@ impl RigidBodyBuilder { let f: Real = v.extract()?; self.builder = self.builder.clone().soft_ccd_prediction(f); } + "allow_fast_rotation" => { + let b: bool = v.extract()?; + self.builder = self.builder.clone().allow_fast_rotation(b); + } "dominance_group" => { let g: i8 = v.extract()?; self.builder = self.builder.clone().dominance_group(g); @@ -2304,6 +2489,15 @@ impl RigidBodyBuilder { builder: self.builder.clone().soft_ccd_prediction(f), } } + /// Allow the body to exceed the angular speed cap (default ``False``). + /// + /// By default the angular velocity is clamped at each substep (about 45 degrees per + /// substep) to keep CCD reliable; enable this for bodies that must spin fast, e.g. wheels. + fn allow_fast_rotation(&self, b: bool) -> Self { + Self { + builder: self.builder.clone().allow_fast_rotation(b), + } + } /// Set the dominance group for contact biasing. fn dominance_group(&self, g: i8) -> Self { Self { @@ -2428,72 +2622,77 @@ 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) + fn angvel(&self) -> PyResult { + let v: crate::na::Vector3 = self.with_ref(|b| b.angvel())?.into(); + Ok(Vec3(v)) } /// Set the angular velocity (wakes the body). #[setter] - fn set_angvel(&mut self, v: PyVector) { + fn set_angvel(&mut self, v: PyVector) -> PyResult<()> { let g: rapier::math::Vector = v.0.into(); - self.with_mut(|b| b.set_angvel(g, true)); + self.with_mut(|b| b.set_angvel(g, true))?; + Ok(()) } - /// Torque accumulated by user calls during the current step - /// (read-only). Cleared automatically at the next step. + /// Torque accumulated by ``add_torque`` / ``add_force_at_point`` (read-only). + /// + /// It is not cleared by the simulation: it keeps being applied at every step until + /// ``reset_torques`` is called. Always zero for non-dynamic bodies. #[getter] - fn user_torque(&self) -> Vec3 { - let v: crate::na::Vector3 = self.with_ref(|b| b.user_torque()).into(); - Vec3(v) + fn user_torque(&self) -> PyResult { + let v: crate::na::Vector3 = self.with_ref(|b| b.user_torque())?.into(); + Ok(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]) + fn enabled_rotations(&self) -> PyResult<(bool, bool, bool)> { + let arr = self.with_ref(|b| b.is_rotation_locked())?; + Ok((!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)); + fn set_enabled_rotations(&mut self, v: (bool, bool, bool)) -> PyResult<()> { + 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()); - ( + fn enabled_translations(&self) -> PyResult<(bool, bool, bool)> { + let la = self.with_ref(|b| b.locked_axes())?; + Ok(( !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)); + fn set_enabled_translations(&mut self, v: (bool, bool, bool)) -> PyResult<()> { + 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. + /// Add a persistent torque to this body. /// - /// The torque is cleared after the step (like ``add_force``); - /// use ``apply_torque_impulse`` for an instantaneous change of - /// angular velocity. + /// Successive calls accumulate, and the torque is not cleared by the simulation: it keeps + /// being applied at every step until ``reset_torques`` is called. Use + /// ``apply_torque_impulse`` for an instantaneous change of angular velocity. Only affects + /// dynamic bodies. /// /// :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) { + fn add_torque(&mut self, torque: PyVector, wake_up: bool) -> PyResult<()> { let g: rapier::math::Vector = torque.0.into(); - self.with_mut(|b| b.add_torque(g, wake_up)); + self.with_mut(|b| b.add_torque(g, wake_up))?; + Ok(()) } /// Apply an instantaneous angular impulse (units: ``N·m·s``). @@ -2501,22 +2700,23 @@ impl RigidBody { /// :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) { + fn apply_torque_impulse(&mut self, torque_impulse: PyVector, wake_up: bool) -> PyResult<()> { let g: rapier::math::Vector = torque_impulse.0.into(); - self.with_mut(|b| b.apply_torque_impulse(g, wake_up)); + self.with_mut(|b| b.apply_torque_impulse(g, wake_up))?; + Ok(()) } /// 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 { + fn gyroscopic_forces_enabled(&self) -> PyResult { 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)); + fn set_gyroscopic_forces_enabled(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|b| b.enable_gyroscopic_forces(v)) } /// Predict the angular velocity after ``dt`` seconds *with* @@ -2525,11 +2725,11 @@ impl RigidBody { /// The body is not modified. /// /// :param dt: integration timestep in seconds. - fn angvel_with_gyroscopic_forces(&self, dt: Real) -> Vec3 { + fn angvel_with_gyroscopic_forces(&self, dt: Real) -> PyResult { let v: crate::na::Vector3 = self - .with_ref(|b| b.angvel_with_gyroscopic_forces(dt)) + .with_ref(|b| b.angvel_with_gyroscopic_forces(dt))? .into(); - Vec3(v) + Ok(Vec3(v)) } // 3D-only builder static methods @@ -2652,9 +2852,25 @@ impl RigidBodyBuilder { /// 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)] +#[pyclass(name = "RigidBodySet", module = "rapier")] pub struct RigidBodySet(pub rapier::dynamics::RigidBodySet); +impl RigidBodySet { + /// Run `f` on the set, also while a step lends it to a physics hook or + /// event handler. + pub(crate) fn read( + slf: &Bound<'_, Self>, + f: impl FnOnce(&rapier::dynamics::RigidBodySet) -> R, + ) -> PyResult { + // A lent set is read first: the running step holds the set mutably borrowed, so + // `try_borrow` fails until it ends. + crate::events_hooks::with_lent_or(slf.as_ptr(), f, |f| match slf.try_borrow() { + Ok(set) => Ok(f(&set.0)), + Err(_) => Err(crate::events_hooks::stepping_error("RigidBodySet")), + }) + } +} + #[pymethods] impl RigidBodySet { /// Build an empty rigid-body set. @@ -2677,7 +2893,7 @@ impl RigidBodySet { return Ok(RigidBodyHandle(self.0.insert(rb))); } if let Ok(rb) = body.extract::>() { - let cloned = rb.to_owned_body(); + let cloned = rb.to_owned_body()?; return Ok(RigidBodyHandle(self.0.insert(cloned))); } Err(PyTypeError::new_err( @@ -2699,6 +2915,9 @@ impl RigidBodySet { /// ``soft_bodies`` is the world's :class:`SoftBodySet`: removing the proxy of a /// soft-body cluster removes that cluster. It can be left out for a world without soft /// bodies. + /// + /// :raises ValueError: if the body is the proxy of a soft-body cluster and + /// ``soft_bodies`` is left out. #[pyo3(signature = (handle, islands, colliders, impulse_joints, multibody_joints, remove_attached_colliders=true, soft_bodies=None))] #[allow(clippy::too_many_arguments)] fn remove( @@ -2710,10 +2929,27 @@ impl RigidBodySet { multibody_joints: &mut MultibodyJointSet, remove_attached_colliders: bool, soft_bodies: Option<&mut crate::soft_body::SoftBodySet>, - ) -> Option { + ) -> PyResult> { let mut scratch = rapier::dynamics::SoftBodySet::new(); - let soft_bodies = soft_bodies.map_or(&mut scratch, |s| &mut s.0); - self.0 + let soft_bodies = match soft_bodies { + Some(s) => &mut s.0, + None => { + // Removing a proxy without its soft body would leave the body without its cluster. + if self + .0 + .get(handle.0) + .is_some_and(|rb| rb.soft_body().is_some()) + { + return Err(PyValueError::new_err( + "the rigid body is the proxy of a soft-body cluster: pass the world's \ + SoftBodySet as `soft_bodies` to remove it", + )); + } + &mut scratch + } + }; + Ok(self + .0 .remove( handle.0, &mut islands.0, @@ -2723,7 +2959,7 @@ impl RigidBodySet { soft_bodies, remove_attached_colliders, ) - .map(RigidBody::new_owned) + .map(RigidBody::new_owned)) } /// Return a live **view** of the body identified by ``handle``, or @@ -2731,21 +2967,23 @@ impl RigidBodySet { /// /// 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 { + fn get(slf: &Bound<'_, Self>, handle: &RigidBodyHandle) -> PyResult> { + if !Self::read(slf, |set| set.contains(handle.0))? { + return Ok(None); + } + Ok(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() { + if !Self::read(slf, |set| set.contains(handle.0))? { return Err(crate::errors::InvalidHandle::new_err(format!( "no rigid body for {:?}", handle.0.into_raw_parts() @@ -2761,13 +2999,13 @@ impl RigidBodySet { /// ``handle in self`` — whether ``handle`` refers to a body /// stored in this set. - fn __contains__(&self, handle: &RigidBodyHandle) -> bool { - self.0.contains(handle.0) + fn __contains__(slf: &Bound<'_, Self>, handle: &RigidBodyHandle) -> PyResult { + Self::read(slf, |set| set.contains(handle.0)) } /// Number of bodies in the set. - fn __len__(&self) -> usize { - self.0.len() + fn __len__(slf: &Bound<'_, Self>) -> PyResult { + Self::read(slf, |set| set.len()) } /// Remove every body from the set. @@ -2781,7 +3019,7 @@ impl RigidBodySet { /// 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(); + Self::read(slf, |set| set.iter().map(|(h, _)| h).collect())?; Py::new( slf.py(), RigidBodySetIter { @@ -2793,8 +3031,10 @@ impl RigidBodySet { } /// 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(); + fn handles(slf: &Bound<'_, Self>) -> PyResult> { + let handles: Vec = Self::read(slf, |set| { + set.iter().map(|(h, _)| RigidBodyHandle(h)).collect() + })?; Py::new(slf.py(), RigidBodyHandleIter { handles, i: 0 }) } } diff --git a/python/rapier-py-3d/src/errors.rs b/python/rapier-py-3d/src/errors.rs index e37edfa72..60a2f30e7 100644 --- a/python/rapier-py-3d/src/errors.rs +++ b/python/rapier-py-3d/src/errors.rs @@ -77,3 +77,27 @@ pub fn register_errors(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> m.add("MeshLoaderError", py.get_type_bound::())?; Ok(()) } + +/// The error raised when a view is used after its object was removed from its set. +pub(crate) fn stale_view(kind: &str) -> PyErr { + InvalidHandle::new_err(format!( + "this {kind} was removed from its set: the view no longer refers to a live object" + )) +} + +/// Borrows a pyclass shared with a world, raising `RuntimeError` instead of panicking when a +/// running step holds it. +pub(crate) fn try_borrow<'py, T: pyo3::PyClass>(obj: &Bound<'py, T>) -> PyResult> { + obj.try_borrow() + .map_err(|_| crate::events_hooks::stepping_error(::NAME)) +} + +/// Mutably borrows a pyclass shared with a world, raising `RuntimeError` instead of panicking +/// when a running step (or a read) holds it. +pub(crate) fn try_borrow_mut<'py, T>(obj: &Bound<'py, T>) -> PyResult> +where + T: pyo3::PyClass, +{ + obj.try_borrow_mut() + .map_err(|_| crate::events_hooks::stepping_error(::NAME)) +} diff --git a/python/rapier-py-3d/src/events_hooks.rs b/python/rapier-py-3d/src/events_hooks.rs index 1c684db91..dd38e8157 100644 --- a/python/rapier-py-3d/src/events_hooks.rs +++ b/python/rapier-py-3d/src/events_hooks.rs @@ -31,14 +31,197 @@ //! 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). +//! +//! While a step runs, the Python sets it steps are mutably borrowed. Each +//! callback lends the shared references the engine gives it (see `LendGuard`), +//! so the Python code can still read those sets and the views into them. use crate::pyo3::exceptions::PyTypeError; use crate::pyo3::pyclass::CompareOp; use crate::*; use rapier3d as rapier; +use std::any::TypeId; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +// ===================================================================== +// Sets lent to the Python callbacks. +// +// A step holds the world's sets mutably borrowed on the Python side, but +// the engine hands its callbacks shared references to them. While a +// callback runs, these references are registered here, keyed by the +// Python object of the set, so that reading that set (or a view into it) +// goes through them instead of failing on the Python-side borrow. +// ===================================================================== + +struct LentSet { + guard: u64, + py_obj: usize, + type_id: TypeId, + ptr: usize, + // Reads in progress through this loan, and whether it is ending (no new reads). + readers: usize, + closing: bool, +} + +static LENT_SETS: Mutex> = Mutex::new(Vec::new()); +static NEXT_LEND_GUARD: AtomicU64 = AtomicU64::new(0); + +fn lent_sets() -> std::sync::MutexGuard<'static, Vec> { + LENT_SETS.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Registration of the sets lent to one callback; dropping it ends the loan. +/// +/// It must be dropped inside the engine callback that received the lent +/// references, while holding the GIL. +pub(crate) struct LendGuard(u64); + +impl LendGuard { + fn new() -> Self { + Self(NEXT_LEND_GUARD.fetch_add(1, Ordering::Relaxed)) + } + + /// Lend `set` as the rapier set behind the Python object `py_obj`. + fn lend(&self, py_obj: *mut crate::pyo3::ffi::PyObject, set: &T) { + lent_sets().push(LentSet { + guard: self.0, + py_obj: py_obj as usize, + type_id: TypeId::of::(), + ptr: set as *const T as usize, + readers: 0, + closing: false, + }); + } + + /// Remove the loans if no read is in progress through them. + fn try_end(&self) -> bool { + let mut sets = lent_sets(); + let mut busy = false; + for e in sets.iter_mut().filter(|e| e.guard == self.0) { + e.closing = true; + busy |= e.readers > 0; + } + if !busy { + sets.retain(|e| e.guard != self.0); + } + !busy + } +} + +impl Drop for LendGuard { + fn drop(&mut self) { + if !self.try_end() { + // A read from another Python thread gave up the GIL mid-way: let it finish. + Python::with_gil(|py| { + py.allow_threads(|| { + while !self.try_end() { + std::thread::yield_now(); + } + }) + }); + } + } +} + +/// Ends a read through a loan, even if the read panics. +struct LentRead(u64, usize); + +impl Drop for LentRead { + fn drop(&mut self) { + if let Some(e) = lent_sets() + .iter_mut() + .find(|e| e.guard == self.0 && e.py_obj == self.1) + { + e.readers -= 1; + } + } +} + +/// Run `f` on the rapier set a running callback lends for the Python object +/// `py_obj`, or pass `f` to `otherwise` if no callback lends it. +pub(crate) fn with_lent_or R>( + py_obj: *mut crate::pyo3::ffi::PyObject, + f: F, + otherwise: impl FnOnce(F) -> PyResult, +) -> PyResult { + let lent = { + let mut sets = lent_sets(); + sets.iter_mut() + .rev() + .find(|e| !e.closing && e.py_obj == py_obj as usize && e.type_id == TypeId::of::()) + .map(|e| { + e.readers += 1; + (e.ptr, LentRead(e.guard, e.py_obj)) + }) + }; + match lent { + // SAFETY: the pointer comes from a shared reference the engine passed to a + // callback, lent only while that callback runs: the callback doesn't return + // before this read ends (`LendGuard::drop` waits for it). + Some((ptr, _read)) => Ok(f(unsafe { &*(ptr as *const T) })), + None => otherwise(f), + } +} + +/// The error raised when a set is accessed while a step borrows it. +pub(crate) fn stepping_error(what: &str) -> PyErr { + crate::pyo3::exceptions::PyRuntimeError::new_err(format!( + "{what} is in use by a running step: from a physics hook or event handler, the \ + rigid-body, collider and soft-body sets can only be read" + )) +} + +/// Borrow `obj` immutably, raising `RuntimeError` if a step holds it. +pub(crate) fn try_read<'py, T: crate::pyo3::PyClass>( + obj: &'py Py, + py: Python<'py>, +) -> PyResult> { + obj.try_borrow(py) + .map_err(|_| stepping_error(::NAME)) +} + +/// Borrow `obj` mutably, raising `RuntimeError` if a step (or a read) holds it. +pub(crate) fn try_write< + 'py, + T: crate::pyo3::PyClass, +>( + obj: &'py Py, + py: Python<'py>, +) -> PyResult> { + obj.try_borrow_mut(py) + .map_err(|_| stepping_error(::NAME)) +} + +/// The Python sets a step hands to its callbacks, lent while each one runs. +pub struct CallbackSets { + pub bodies: Py, + pub colliders: Py, + pub soft_bodies: Option>, +} + +impl CallbackSets { + fn clone_ref(&self, py: Python<'_>) -> Self { + Self { + bodies: self.bodies.clone_ref(py), + colliders: self.colliders.clone_ref(py), + soft_bodies: self.soft_bodies.as_ref().map(|s| s.clone_ref(py)), + } + } + + fn lend( + &self, + bodies: &rapier::dynamics::RigidBodySet, + colliders: &rapier::geometry::ColliderSet, + ) -> LendGuard { + let guard = LendGuard::new(); + guard.lend(self.bodies.as_ptr(), bodies); + guard.lend(self.colliders.as_ptr(), colliders); + guard + } +} + // ===================================================================== // Deferred-error policy. // ===================================================================== @@ -182,20 +365,43 @@ impl SolverFlags { /// 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``. +/// Exposes the two colliders, their parent rigid bodies (when +/// applicable) and the sets holding them. The sets are the ones being +/// stepped: during the callback they (and the views into them) can be +/// read but not modified. #[pyclass(name = "PairFilterContext", module = "rapier", unsendable)] pub struct PairFilterContext { collider1: rapier::geometry::ColliderHandle, collider2: rapier::geometry::ColliderHandle, rigid_body1: Option, rigid_body2: Option, + sets: CallbackSets, +} + +impl PairFilterContext { + /// The raw handles of the two colliders of the pair. + pub(crate) fn collider_handles( + &self, + ) -> ( + rapier::geometry::ColliderHandle, + rapier::geometry::ColliderHandle, + ) { + (self.collider1, self.collider2) + } } #[pymethods] impl PairFilterContext { + /// The :class:`ColliderSet` being stepped, readable during the callback. + #[getter] + fn colliders(&self, py: Python<'_>) -> Py { + self.sets.colliders.clone_ref(py) + } + /// The :class:`RigidBodySet` being stepped, readable during the callback. + #[getter] + fn bodies(&self, py: Python<'_>) -> Py { + self.sets.bodies.clone_ref(py) + } /// Handle of the first collider in the pair. #[getter] fn collider1(&self) -> ColliderHandle { @@ -240,12 +446,14 @@ impl PairFilterContext { /// 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``. +/// The instance is a **transient view** valid only inside the callback: +/// accessing the contact data (``normal``, ``user_data``, +/// ``solver_contacts``, ...) after the callback returns raises +/// ``RuntimeError``. Like :class:`PairFilterContext`, it also exposes the +/// sets being stepped, readable during the callback. #[pyclass(name = "ContactModificationContext", module = "rapier", unsendable)] pub struct ContactModificationContext { + sets: CallbackSets, collider1: rapier::geometry::ColliderHandle, collider2: rapier::geometry::ColliderHandle, rigid_body1: Option, @@ -263,6 +471,16 @@ pub struct ContactModificationContext { } impl ContactModificationContext { + /// The raw handles of the two colliders of the pair. + pub(crate) fn collider_handles( + &self, + ) -> ( + rapier::geometry::ColliderHandle, + rapier::geometry::ColliderHandle, + ) { + (self.collider1, self.collider2) + } + #[inline] fn check_valid(&self) -> PyResult<()> { if !self.valid { @@ -276,6 +494,16 @@ impl ContactModificationContext { #[pymethods] impl ContactModificationContext { + /// The :class:`ColliderSet` being stepped, readable during the callback. + #[getter] + fn colliders(&self, py: Python<'_>) -> Py { + self.sets.colliders.clone_ref(py) + } + /// The :class:`RigidBodySet` being stepped, readable during the callback. + #[getter] + fn bodies(&self, py: Python<'_>) -> Py { + self.sets.bodies.clone_ref(py) + } /// Handle of the first collider in the pair. #[getter] fn collider1(&self) -> ColliderHandle { @@ -370,7 +598,7 @@ impl ContactModificationContext { /// /// :raises RuntimeError: If accessed outside the callback. #[setter] - fn set_friction(&mut self, v: Real) -> PyResult<()> { + pub(crate) fn set_friction(&mut self, v: Real) -> PyResult<()> { self.check_valid()?; // SAFETY: see `normal` getter. unsafe { @@ -403,31 +631,84 @@ impl ContactModificationContext { /// Return the current solver contacts as a list (read-only snapshot). /// + /// Inside the callback, their ``point`` / ``point2`` are world-space. + /// Use :meth:`set_solver_contact` to modify one. + /// /// :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({ - // Inside the modification hook the anchors hold the fresh - // world-space contact points; is-new lives in bit 31 of the - // contact id. - let p1: crate::na::Vector3 = sc.anchor1.into(); - let p2: crate::na::Vector3 = sc.anchor2.into(); - let id = sc.contact_id[0]; - SolverContact { - point: Point3(crate::na::Point3::from(p1)), - point2: Point3(crate::na::Point3::from(p2)), - dist: sc.dist, - contact_id: id & !rapier::geometry::NEW_CONTACT_BIT, - is_new: (id & rapier::geometry::NEW_CONTACT_BIT) != 0, - } - }); + Ok(vec_ref.iter().map(SolverContact::from_rapier).collect()) + } + + /// Modify the solver contact at index ``i``; the arguments left to + /// ``None`` are kept. + /// + /// :param i: Zero-based index into :attr:`solver_contacts`. + /// :param point: World-space contact point on the first collider. + /// :param point2: World-space contact point on the second collider. + /// :param dist: Separation along the normal (negative when + /// penetrating). A value differing from the gap between the two + /// points shifts the contact. + /// :param tangent_velocity: World-space tangent velocity of the second + /// collider's surface relative to the first one's that the solver + /// tries to reach at the contact (see :meth:`set_tangent_velocity`). + /// :raises IndexError: If ``i`` is out of range. + /// :raises RuntimeError: If accessed outside the callback. + #[pyo3(signature = (i, *, point=None, point2=None, dist=None, tangent_velocity=None))] + fn set_solver_contact( + &mut self, + i: usize, + point: Option, + point2: Option, + dist: Option, + tangent_velocity: Option, + ) -> PyResult<()> { + self.check_valid()?; + // SAFETY: see `normal` getter. + let contacts = unsafe { &mut *self.solver_contacts }; + let len = contacts.len(); + let sc = contacts.get_mut(i).ok_or_else(|| { + crate::pyo3::exceptions::PyIndexError::new_err(format!( + "solver contact index {i} out of range (the manifold has {len})" + )) + })?; + if let Some(p) = point { + sc.anchor1 = p.0.coords.into(); + } + if let Some(p) = point2 { + sc.anchor2 = p.0.coords.into(); + } + if let Some(d) = dist { + sc.dist = d; } - Ok(out) + if let Some(v) = tangent_velocity { + sc.tangent_velocity = v.0.into(); + } + Ok(()) + } + + /// Set the world-space tangent velocity of every solver contact of the + /// manifold, e.g. to make a conveyor belt drag what touches it. + /// + /// It is the velocity of the second collider's surface relative to the + /// first one's: a belt dragging objects along ``v`` sets ``v`` when it + /// is :attr:`collider1`, and ``-v`` when it is :attr:`collider2`. + /// Like every hook, it is only called while the pair is awake. + /// + /// :param velocity: World-space relative tangent velocity the solver + /// tries to reach at the contacts. + /// :raises RuntimeError: If accessed outside the callback. + fn set_tangent_velocity(&mut self, velocity: PyVector) -> PyResult<()> { + self.check_valid()?; + let v: rapier::math::Vector = velocity.0.into(); + // SAFETY: see `normal` getter. + for sc in unsafe { (*self.solver_contacts).iter_mut() } { + sc.tangent_velocity = v; + } + Ok(()) } /// Clear all solver contacts (the manifold won't contribute to the solver). @@ -542,33 +823,65 @@ impl ContactModificationContext { // `EventHandler` protocol to rapier's `EventHandler` trait. // ===================================================================== +/// Stash the first exception raised by a callback of a step. +fn stash_error(slot: &Mutex, e: PyErr) { + let mut s = slot.lock().unwrap(); + if s.err.is_none() { + s.err = Some(e); + } + if s.policy_strict { + s.aborted = true; + } +} + +fn is_aborted(slot: &Mutex) -> bool { + slot.lock().unwrap().aborted +} + /// Adapter wrapping an arbitrary Python `EventHandler`-protocol object. pub struct PyEventHandler { obj: Py, err_slot: Arc>, + sets: CallbackSets, + // Every method of the protocol is optional. + has_collision: bool, + has_contact_force: bool, + has_soft_body_tear: bool, } impl PyEventHandler { - pub fn new(obj: Py, err_slot: Arc>) -> Self { - Self { obj, err_slot } + pub fn new( + py: Python<'_>, + obj: Py, + err_slot: Arc>, + sets: CallbackSets, + ) -> Self { + let bound = obj.bind(py); + let has = |name: &str| bound.hasattr(name).unwrap_or(false); + Self { + has_collision: has("handle_collision_event"), + has_contact_force: has("handle_contact_force_event"), + has_soft_body_tear: has("handle_soft_body_tear_event"), + obj, + err_slot, + sets, + } } } impl rapier::pipeline::EventHandler for PyEventHandler { fn handle_collision_event( &self, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, + 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; - } + if !self.has_collision || is_aborted(&self.err_slot) { + return; } Python::with_gil(|py| { + let _loan = self.sets.lend(bodies, colliders); let py_event = CollisionEvent::from_rapier(event); let py_pair: PyObject = match contact_pair { Some(p) => Py::new(py, ContactPair(p.clone())) @@ -578,50 +891,44 @@ impl rapier::pipeline::EventHandler for PyEventHandler { }; let res = self.obj.bind(py).call_method1( "handle_collision_event", - (py.None(), py.None(), py_event, py_pair), + ( + self.sets.bodies.clone_ref(py), + self.sets.colliders.clone_ref(py), + 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; - } + stash_error(&self.err_slot, e); } }); } fn handle_soft_body_tear_event( &self, - _soft_bodies: &rapier::dynamics::SoftBodySet, + soft_bodies: &rapier::dynamics::SoftBodySet, event: &rapier::dynamics::SoftBodyTearEvent, ) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return; - } + if !self.has_soft_body_tear || is_aborted(&self.err_slot) { + return; } Python::with_gil(|py| { - let bound = self.obj.bind(py); - // The method is optional: handlers written before soft bodies existed keep working. - if !bound - .hasattr("handle_soft_body_tear_event") - .unwrap_or(false) - { - return; - } + let loan = LendGuard::new(); + let py_soft_bodies: PyObject = match &self.sets.soft_bodies { + Some(s) => { + loan.lend(s.as_ptr(), soft_bodies); + s.clone_ref(py).into_any() + } + None => py.None(), + }; let py_event = crate::soft_body::SoftBodyTearEvent(event.clone()); - let res = bound.call_method1("handle_soft_body_tear_event", (py.None(), py_event)); + let res = self + .obj + .bind(py) + .call_method1("handle_soft_body_tear_event", (py_soft_bodies, py_event)); + drop(loan); 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; - } + stash_error(&self.err_slot, e); } }); } @@ -629,43 +936,30 @@ impl rapier::pipeline::EventHandler for PyEventHandler { fn handle_contact_force_event( &self, dt: Real, - _bodies: &rapier::dynamics::RigidBodySet, - _colliders: &rapier::geometry::ColliderSet, + 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; - } + if !self.has_contact_force || is_aborted(&self.err_slot) { + 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), - ); + let _loan = self.sets.lend(bodies, colliders); + let res = Py::new(py, ContactPair(contact_pair.clone())).and_then(|py_pair| { + self.obj.bind(py).call_method1( + "handle_contact_force_event", + ( + dt, + self.sets.bodies.clone_ref(py), + self.sets.colliders.clone_ref(py), + 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; - } + stash_error(&self.err_slot, e); } }); } @@ -842,11 +1136,48 @@ impl rapier::pipeline::EventHandler for ChannelEventCollectorAdapter { pub struct PyPhysicsHooks { obj: Py, err_slot: Arc>, + sets: CallbackSets, + // Every method of the protocol is optional: a missing one behaves as + // the engine's default hook. + has_filter_contact: bool, + has_filter_intersection: bool, + has_modify: bool, } impl PyPhysicsHooks { - pub fn new(obj: Py, err_slot: Arc>) -> Self { - Self { obj, err_slot } + pub fn new( + py: Python<'_>, + obj: Py, + err_slot: Arc>, + sets: CallbackSets, + ) -> Self { + let bound = obj.bind(py); + let has = |name: &str| bound.hasattr(name).unwrap_or(false); + Self { + has_filter_contact: has("filter_contact_pair"), + has_filter_intersection: has("filter_intersection_pair"), + has_modify: has("modify_solver_contacts"), + obj, + err_slot, + sets, + } + } + + fn pair_filter_context( + &self, + py: Python<'_>, + context: &rapier::pipeline::PairFilterContext, + ) -> PyResult> { + Py::new( + py, + PairFilterContext { + collider1: context.collider1, + collider2: context.collider2, + rigid_body1: context.rigid_body1, + rigid_body2: context.rigid_body2, + sets: self.sets.clone_ref(py), + }, + ) } } @@ -855,47 +1186,19 @@ impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { &self, context: &rapier::pipeline::PairFilterContext, ) -> Option { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return Some(rapier::geometry::SolverFlags::default()); - } + if !self.has_filter_contact || is_aborted(&self.err_slot) { + 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,)); + let _loan = self.sets.lend(context.bodies, context.colliders); + let res = self.pair_filter_context(py, context).and_then(|ctx| { + 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; - } + stash_error(&self.err_slot, e); Some(rapier::geometry::SolverFlags::default()) } Ok(v) => { @@ -915,47 +1218,19 @@ impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { } fn filter_intersection_pair(&self, context: &rapier::pipeline::PairFilterContext) -> bool { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return true; - } + if !self.has_filter_intersection || is_aborted(&self.err_slot) { + 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,)) - { + let _loan = self.sets.lend(context.bodies, context.colliders); + let res = self.pair_filter_context(py, context).and_then(|ctx| { + self.obj + .bind(py) + .call_method1("filter_intersection_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; - } + stash_error(&self.err_slot, e); true } Ok(v) => v.extract::().unwrap_or(true), @@ -964,12 +1239,10 @@ impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { } fn modify_solver_contacts(&self, context: &mut rapier::pipeline::ContactModificationContext) { - { - let slot = self.err_slot.lock().unwrap(); - if slot.aborted { - return; - } + if !self.has_modify || is_aborted(&self.err_slot) { + return; } + let (bodies, colliders) = (context.bodies, context.colliders); // The contacts of two soft surfaces are candidates rather than a manifold: the Python // hook only sees rigid manifolds. let manifold = match &mut context.contacts { @@ -977,39 +1250,28 @@ impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { rapier::pipeline::ModifiableContacts::Soft(_) => return, }; Python::with_gil(|py| { - let manifold_local_n1 = manifold.manifold.local_n1; - let manifold_local_n2 = manifold.manifold.local_n2; - let normal_ptr: *mut rapier::math::Vector = manifold.normal; - let sc_ptr: *mut Vec = manifold.solver_contacts; - let friction_ptr: *mut Real = manifold.friction; - let restitution_ptr: *mut Real = manifold.restitution; - let ud_ptr: *mut u32 = manifold.user_data; + let _loan = self.sets.lend(bodies, colliders); let ctx_py = match Py::new( py, ContactModificationContext { + sets: self.sets.clone_ref(py), 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, - friction: friction_ptr, - restitution: restitution_ptr, - user_data: ud_ptr, + local_n1: manifold.manifold.local_n1, + local_n2: manifold.manifold.local_n2, + normal: manifold.normal, + solver_contacts: manifold.solver_contacts, + friction: manifold.friction, + restitution: manifold.restitution, + user_data: manifold.user_data, 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; - } + stash_error(&self.err_slot, e); return; } }; @@ -1019,18 +1281,9 @@ impl rapier::pipeline::PhysicsHooks for PyPhysicsHooks { .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; - } + ctx_py.borrow_mut(py).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; - } + stash_error(&self.err_slot, e); } }); } @@ -1049,6 +1302,7 @@ pub fn build_event_handler( py: Python<'_>, obj: Option<&Py>, err_slot: Arc>, + sets: &CallbackSets, ) -> Option> { let obj = obj?; let bound = obj.bind(py); @@ -1059,7 +1313,12 @@ pub fn build_event_handler( 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))) + Some(Box::new(PyEventHandler::new( + py, + obj.clone_ref(py), + err_slot, + sets.clone_ref(py), + ))) } /// Wrap an arbitrary Python object as a (boxed) rapier `PhysicsHooks`. @@ -1067,13 +1326,23 @@ pub fn build_physics_hooks( py: Python<'_>, obj: Option<&Py>, err_slot: Arc>, + sets: &CallbackSets, ) -> 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))) + // Hooks implemented in Rust run natively, without calling back into Python. + if let Ok(hooks) = bound.downcast::() { + return Some(Box::new(hooks.get().native())); + } + Some(Box::new(PyPhysicsHooks::new( + py, + obj.clone_ref(py), + err_slot, + sets.clone_ref(py), + ))) } pub fn register_events_hooks( diff --git a/python/rapier-py-3d/src/geometry.rs b/python/rapier-py-3d/src/geometry.rs index 19e3f0f65..dc5eb863f 100644 --- a/python/rapier-py-3d/src/geometry.rs +++ b/python/rapier-py-3d/src/geometry.rs @@ -9,91 +9,173 @@ use crate::*; use rapier3d as rapier; -use crate::numpy::{PyArray2, PyReadonlyArray2, PyUntypedArrayMethods}; +use crate::numpy::{PyArray2, PyReadonlyArray2}; 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(), - ))); +/// Read the rows of an `(M, N)` integer ndarray of element type `T`, or `None` if `obj` is +/// not such an array. Any memory layout is accepted. +fn int_ndarray_rows(obj: &Bound<'_, PyAny>) -> Option>> +where + T: crate::numpy::Element + Copy + TryInto + std::fmt::Display, +{ + let arr = obj.extract::>().ok()?; + let view = arr.as_array(); + if view.ncols() != N { + return Some(Err(PyValueError::new_err(format!( + "expected ndarray with shape (M, {N}); got (M, {})", + view.ncols() + )))); + } + let rows = view + .rows() + .into_iter() + .map(|row| { + let mut out = [0u32; N]; + for (o, x) in out.iter_mut().zip(row.iter()) { + *o = (*x) + .try_into() + .map_err(|_| PyValueError::new_err(format!("invalid index {x}")))?; } - Ok([c[0], c[1], c[2]]) + Ok(out) }) - .collect() + .collect(); + Some(rows) } -/// 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); +/// Read a 1D integer ndarray of element type `T`, or `None` if `obj` is not such an array. +/// Any memory layout is accepted. +fn int_ndarray_1d(obj: &Bound<'_, PyAny>) -> Option>> +where + T: crate::numpy::Element + Copy + TryInto + std::fmt::Display, +{ + let arr = obj.extract::>().ok()?; + let values = arr + .as_array() + .iter() + .map(|x| { + (*x).try_into() + .map_err(|_| PyValueError::new_err(format!("invalid index {x}"))) + }) + .collect(); + Some(values) +} + +/// Extract a flat index list: a 1D ndarray of any integer dtype, or a sequence of ints. +pub(crate) fn extract_index_list(obj: &Bound<'_, PyAny>) -> PyResult> { + if let Some(r) = int_ndarray_1d::(obj) + .or_else(|| int_ndarray_1d::(obj)) + .or_else(|| int_ndarray_1d::(obj)) + .or_else(|| int_ndarray_1d::(obj)) + .or_else(|| int_ndarray_1d::(obj)) + { + return r; + } + obj.extract::>() + .map_err(|_| PyTypeError::new_err("expected a 1D integer ndarray or a sequence of ints")) +} + +/// Extract an `(M, N)` index matrix: an integer ndarray of any integer dtype, or a sequence of +/// length-`N` sequences (lists or tuples). +pub(crate) fn extract_index_rows( + obj: &Bound<'_, PyAny>, +) -> PyResult> { + if let Some(r) = int_ndarray_rows::(obj) + .or_else(|| int_ndarray_rows::(obj)) + .or_else(|| int_ndarray_rows::(obj)) + .or_else(|| int_ndarray_rows::(obj)) + .or_else(|| int_ndarray_rows::(obj)) + { + return r; } 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 {}", + <[u32; N]>::try_from(c.as_slice()).map_err(|_| { + PyValueError::new_err(format!( + "expected inner sequence of length {N}; got {}", c.len(), - ))); - } - Ok([c[0], c[1]]) + )) + }) }) .collect() } +/// Extract an `Mx3` index matrix → `Vec<[u32; 3]>` (integer ndarray or nested sequences). +pub fn extract_indices(obj: &Bound<'_, PyAny>) -> PyResult> { + extract_index_rows::<3>(obj) +} + +/// Extract an `Mx2` index matrix → `Vec<[u32; 2]>` (integer ndarray or nested sequences). +pub fn extract_indices_2(obj: &Bound<'_, PyAny>) -> PyResult> { + extract_index_rows::<2>(obj) +} + +/// Reject the index buffers referencing vertices out of `0..num_vertices`. +fn check_indices_in_bounds( + indices: &[[u32; N]], + num_vertices: usize, +) -> PyResult<()> { + match indices + .iter() + .flatten() + .find(|i| **i as usize >= num_vertices) + { + Some(i) => Err(PyValueError::new_err(format!( + "vertex index {i} out of bounds for {num_vertices} vertices" + ))), + None => Ok(()), + } +} + +/// Reject the voxel sizes the voxelizer cannot handle. +fn check_voxel_size(voxel_size: Real) -> PyResult<()> { + if voxel_size > 0.0 && voxel_size.is_finite() { + Ok(()) + } else { + Err(PyValueError::new_err(format!( + "voxel_size must be positive and finite; got {voxel_size}" + ))) + } +} + +/// Extract a 3D heightfield's `(nrows, ncols)` height grid: a `float32`/`float64` ndarray of +/// any memory layout, or nested sequences. +/// +/// `heights[i, j]` is the height at row `i` (along Z) and column `j` (along X), converted to +/// parry's column-major storage. +fn extract_heights(obj: &Bound<'_, PyAny>) -> PyResult> { + fn from_view>( + view: crate::numpy::ndarray::ArrayView2<'_, T>, + ) -> rapier::parry::utils::Array2 { + let (nrows, ncols) = view.dim(); + rapier::parry::utils::Array2::from_fn(nrows, ncols, |i, j| view[[i, j]].into() as Real) + } + let heights = if let Ok(arr) = obj.extract::>() { + from_view(arr.as_array()) + } else if let Ok(arr) = obj.extract::>() { + from_view(arr.as_array()) + } else { + let rows: Vec> = obj.extract()?; + let ncols = rows.first().map_or(0, |r| r.len()); + if rows.iter().any(|r| r.len() != ncols) { + return Err(PyValueError::new_err( + "heights rows must all have the same length", + )); + } + rapier::parry::utils::Array2::from_fn(rows.len(), ncols, |i, j| rows[i][j]) + }; + if heights.nrows() < 2 || heights.ncols() < 2 { + return Err(PyValueError::new_err(format!( + "heights must have at least 2 rows and 2 columns; got ({}, {})", + heights.nrows(), + heights.ncols() + ))); + } + Ok(heights) +} + // ============================================================ // ColliderHandle (moved from dynamics) // ============================================================ @@ -173,8 +255,8 @@ impl ColliderHandle { /// 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)] +#[pyclass(name = "ShapeType", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ShapeType { BALL, CUBOID, @@ -241,8 +323,8 @@ impl ShapeType { /// /// 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)] +#[pyclass(name = "ColliderType", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ColliderType { SOLID, SENSOR, @@ -274,8 +356,8 @@ impl ColliderType { /// `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)] +#[pyclass(name = "ColliderEnabled", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ColliderEnabled { ENABLED, DISABLED_BY_PARENT, @@ -754,29 +836,49 @@ impl Group { /// /// `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)] +/// `ONLY_DYNAMIC` are deprecated aliases of `AND`, kept for compatibility. +#[pyclass( + name = "InteractionTestMode", + module = "rapier", + eq, + eq_int, + hash, + frozen +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InteractionTestMode { - DEFAULT, - ONLY_DYNAMIC, + /// Each collider must pass the other's filter. AND, + /// At least one collider must pass the other's filter. OR, } +#[pymethods] +impl InteractionTestMode { + /// Deprecated alias of ``AND``. + #[classattr] + #[allow(non_snake_case)] + fn DEFAULT() -> Self { + Self::AND + } + /// Deprecated alias of ``AND``. + #[classattr] + #[allow(non_snake_case)] + fn ONLY_DYNAMIC() -> Self { + Self::AND + } +} + 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::AND => 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::And => Self::AND, rapier::geometry::InteractionTestMode::Or => Self::OR, } } @@ -895,9 +997,10 @@ impl InteractionGroups { } fn __repr__(&self) -> String { format!( - "InteractionGroups(memberships={:#010x}, filter={:#010x})", + "InteractionGroups(memberships={:#010x}, filter={:#010x}, test_mode={:?})", self.0.memberships.bits(), self.0.filter.bits(), + InteractionTestMode::from_rapier(self.0.test_mode), ) } } @@ -1148,8 +1251,15 @@ impl ColliderMaterial { /// /// `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)] +#[pyclass( + name = "BvhOptimizationStrategy", + module = "rapier", + eq, + eq_int, + hash, + frozen +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BvhOptimizationStrategy { AUTO, NO_OPTIMIZATION, @@ -1181,7 +1291,7 @@ impl BvhOptimizationStrategy { /// /// Usually accessed via `world.broad_phase`. Create one explicitly /// only if you are driving the pipeline yourself. -#[pyclass(name = "BroadPhaseBvh", module = "rapier", unsendable)] +#[pyclass(name = "BroadPhaseBvh", module = "rapier")] pub struct BroadPhaseBvh(pub rapier::geometry::BroadPhaseBvh); #[pymethods] @@ -1215,7 +1325,7 @@ impl BroadPhaseBvh { /// Usually accessed via `world.narrow_phase`. Use it to query /// existing contact pairs and intersections between specific /// colliders. -#[pyclass(name = "NarrowPhase", module = "rapier", unsendable)] +#[pyclass(name = "NarrowPhase", module = "rapier")] pub struct NarrowPhase(pub rapier::geometry::NarrowPhase); #[pymethods] @@ -1257,6 +1367,37 @@ impl NarrowPhase { .collect() } + /// Snapshot every contact pair involving ``collider``. + /// + /// ``collider`` may be either collider of each returned pair: compare + /// it with their ``collider1`` / ``collider2``. + /// + /// :param collider: Handle of the collider. + /// :returns: List of `ContactPair` snapshots (empty for an unknown + /// handle). + fn contact_pairs_with(&self, collider: &ColliderHandle) -> Vec { + self.0 + .contact_pairs_with(collider.0) + .cloned() + .map(ContactPair) + .collect() + } + + /// Snapshot every sensor/intersection pair involving ``collider``. + /// + /// :param collider: Handle of the collider. + /// :returns: List of `(collider1, collider2, intersecting)` tuples + /// (empty for an unknown handle). + fn intersection_pairs_with( + &self, + collider: &ColliderHandle, + ) -> Vec<(ColliderHandle, ColliderHandle, bool)> { + self.0 + .intersection_pairs_with(collider.0) + .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. @@ -1348,6 +1489,17 @@ impl BroadPhasePairEvent { fn removed_(&self) -> bool { !self.added } + /// True iff this is an "Added" event. Unlike ``added``, which the ``added`` constructor shadows + /// on instances, this property is always the flag. + #[getter] + fn is_added(&self) -> bool { + self.added + } + /// True iff this is a "Removed" event. + #[getter] + fn is_removed(&self) -> bool { + !self.added + } fn __repr__(&self) -> String { if self.added { format!( @@ -1549,77 +1701,65 @@ impl ColliderFlags { /// 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)] +/// Accepts `float32` and `float64` ndarrays of any memory layout (cast to `Real`), as well +/// as any sequence of 3-vectors (tuples, lists, `Vec3`, `Point3`, ...). 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})" + fn from_view>( + view: crate::numpy::ndarray::ArrayView2<'_, T>, + ) -> PyResult> { + if view.ncols() != 3 { + return Err(PyValueError::new_err(format!( + "expected ndarray with shape (N, 3); got (N, {})", + view.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); + Ok(view + .rows() + .into_iter() + .map(|r| { + rapier::math::Vector::new( + r[0].into() as Real, + r[1].into() as Real, + r[2].into() as Real, + ) + }) + .collect()) } - // 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); + return from_view(arr.as_array()); } 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); + return from_view(arr.as_array()); } - // 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()) + let seq: Vec = obj.extract()?; + Ok(seq.into_iter().map(|v| v.0.into()).collect()) +} + +/// A `(vertices, indices)` triangle mesh as an `(N, 3)` float ndarray and an `(M, 3)` uint32 +/// ndarray. +type TriMeshArrays<'py> = (Bound<'py, PyArray2>, Bound<'py, PyArray2>); + +/// Checks the subdivisions of a curved shape's triangle mesh: at least `3` around its axis and +/// `2` from pole to pole (parry panics below). +fn check_subdivisions(around: u32, pole_to_pole: Option) -> PyResult<()> { + if around < 3 || pole_to_pole.is_some_and(|n| n < 2) { + return Err(PyValueError::new_err( + "a curved shape needs at least 3 subdivisions around its axis and 2 from pole to pole", + )); + } + Ok(()) +} + +fn trimesh_arrays<'py>( + py: Python<'py>, + (vertices, indices): (Vec, Vec<[u32; 3]>), +) -> TriMeshArrays<'py> { + ( + crate::soft_body::vectors_to_array(py, vertices.into_iter()), + crate::soft_body::elements_to_array(py, &indices), + ) } // ============================================================ @@ -1645,6 +1785,23 @@ impl Ball { fn radius(&self) -> Real { self.0.radius } + /// The sphere's surface as a triangle mesh, oriented outward, with ``ntheta_subdiv`` + /// subdivisions around its axis and ``nphi_subdiv`` from pole to pole: an ``(N, 3)`` float + /// ndarray of vertices and an ``(M, 3)`` uint32 ndarray of triangles. + /// + /// :raises ValueError: if ``ntheta_subdiv < 3`` or ``nphi_subdiv < 2``. + fn to_trimesh<'py>( + &self, + py: Python<'py>, + ntheta_subdiv: u32, + nphi_subdiv: u32, + ) -> PyResult> { + check_subdivisions(ntheta_subdiv, Some(nphi_subdiv))?; + Ok(trimesh_arrays( + py, + self.0.to_trimesh(ntheta_subdiv, nphi_subdiv), + )) + } fn __repr__(&self) -> String { format!("Ball(radius={})", self.0.radius) } @@ -1675,6 +1832,11 @@ impl Cuboid { let v: crate::na::SVector = self.0.half_extents.into(); Vec3(v) } + /// The box's surface as a triangle mesh, oriented outward: an ``(8, 3)`` float ndarray of + /// vertices and a ``(12, 3)`` uint32 ndarray of triangles. + fn to_trimesh<'py>(&self, py: Python<'py>) -> TriMeshArrays<'py> { + trimesh_arrays(py, self.0.to_trimesh()) + } fn __repr__(&self) -> String { format!("Cuboid(half_extents={:?})", self.0.half_extents) } @@ -1732,6 +1894,24 @@ impl Capsule { let v: crate::na::SVector = self.0.segment.b.into(); Point3(crate::na::Point::from(v)) } + /// The capsule's surface as a triangle mesh, oriented outward, with ``ntheta_subdiv`` + /// subdivisions around its axis and ``nphi_subdiv`` from pole to pole (split between the two + /// caps): an ``(N, 3)`` float ndarray of vertices and an ``(M, 3)`` uint32 ndarray of + /// triangles. + /// + /// :raises ValueError: if ``ntheta_subdiv < 3`` or ``nphi_subdiv < 2``. + fn to_trimesh<'py>( + &self, + py: Python<'py>, + ntheta_subdiv: u32, + nphi_subdiv: u32, + ) -> PyResult> { + check_subdivisions(ntheta_subdiv, Some(nphi_subdiv))?; + Ok(trimesh_arrays( + py, + self.0.to_trimesh(ntheta_subdiv, nphi_subdiv), + )) + } fn __repr__(&self) -> String { format!( "Capsule(half_height={}, radius={})", @@ -1877,6 +2057,86 @@ impl Voxels { fn num_voxels(&self) -> usize { self.0.voxels().filter(|vox| !vox.state.is_empty()).count() } + + /// The boundary of the filled voxels as a triangle mesh: an ``(N, 3)`` float ndarray of + /// vertices and an ``(M, 3)`` uint32 ndarray of triangles. + fn to_trimesh<'py>(&self, py: Python<'py>) -> TriMeshArrays<'py> { + trimesh_arrays(py, self.0.to_trimesh()) + } +} + +// ============================================================ +// FillMode +// ============================================================ + +/// How ``voxelized_mesh`` decides which voxels are filled. +/// +/// - ``FillMode.SURFACE_ONLY``: only the voxels intersecting the mesh surface are filled, +/// giving a hollow shell. +/// - ``FillMode.flood_fill(detect_cavities=False)``: the voxels intersecting the surface and +/// every voxel enclosed by it are filled, giving a solid. With ``detect_cavities=True``, +/// the enclosed holes of the solid are detected and left empty. +#[pyclass(name = "FillMode", module = "rapier", frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FillMode(pub rapier::parry::transformation::voxelization::FillMode); + +#[pymethods] +impl FillMode { + /// Only fill the voxels intersecting the surface of the mesh. + #[classattr] + const SURFACE_ONLY: FillMode = + FillMode(rapier::parry::transformation::voxelization::FillMode::SurfaceOnly); + + /// Fill the voxels intersecting the surface of the mesh and all the voxels it encloses. + /// + /// :param detect_cavities: whether to detect the holes enclosed by the solid and leave + /// them empty. + #[staticmethod] + #[pyo3(signature = (detect_cavities=false))] + fn flood_fill(detect_cavities: bool) -> Self { + Self(rapier::parry::transformation::voxelization::FillMode::FloodFill { detect_cavities }) + } + + /// Whether this is the flood-fill mode. + #[getter] + fn is_flood_fill(&self) -> bool { + matches!( + self.0, + rapier::parry::transformation::voxelization::FillMode::FloodFill { .. } + ) + } + + /// Whether the flood fill detects cavities (always ``False`` for ``SURFACE_ONLY``). + #[getter] + fn detect_cavities(&self) -> bool { + matches!( + self.0, + rapier::parry::transformation::voxelization::FillMode::FloodFill { + detect_cavities: true + } + ) + } + + 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("FillMode supports only == and !=")), + } + } + fn __repr__(&self) -> String { + match self.0 { + rapier::parry::transformation::voxelization::FillMode::SurfaceOnly => { + "FillMode.SURFACE_ONLY".to_string() + } + rapier::parry::transformation::voxelization::FillMode::FloodFill { + detect_cavities, + } => format!( + "FillMode.flood_fill(detect_cavities={})", + if detect_cavities { "True" } else { "False" } + ), + } + } } // ============================================================ @@ -1999,6 +2259,61 @@ impl SharedShape { Self(rapier::parry::shape::SharedShape::halfspace(n.into())) } + /// Build a segment between the points `a` and `b`. + #[staticmethod] + fn segment(a: PyVector, b: PyVector) -> Self { + Self(rapier::parry::shape::SharedShape::segment( + a.0.into(), + b.0.into(), + )) + } + + /// Build a polyline: a set of segments joining the given vertices. + /// + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 2)` integer ndarray (or a sequence of pairs) listing the + /// vertices of each segment. If ``None``, the vertices are joined in order into a + /// line strip. + #[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 = indices.map(extract_indices_2).transpose()?; + if let Some(idx) = &idx { + check_indices_in_bounds(idx, verts.len())?; + } + Ok(Self(rapier::parry::shape::SharedShape::polyline( + verts, idx, + ))) + } + + /// Build a voxels shape by voxelizing a triangle mesh. + /// + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 3)` integer ndarray (or a sequence of triples). + /// :param voxel_size: edge length of one (cubic) voxel. + /// :param fill_mode: which voxels are filled (a :class:`FillMode`); defaults to + /// ``FillMode.flood_fill()`` (a solid). + #[staticmethod] + #[pyo3(signature = (vertices, indices, voxel_size, fill_mode=None))] + fn voxelized_mesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + voxel_size: Real, + fill_mode: Option, + ) -> PyResult { + let verts = extract_verts_for_dim(vertices)?; + let idx = extract_indices(indices)?; + check_indices_in_bounds(&idx, verts.len())?; + check_voxel_size(voxel_size)?; + Ok(Self(rapier::parry::shape::SharedShape::voxelized_mesh( + &verts, + &idx, + voxel_size, + fill_mode.map(|m| m.0).unwrap_or_default(), + ))) + } + /// Build a triangle with rounded edges of radius `border_radius`. #[staticmethod] fn round_triangle(a: PyVector, b: PyVector, c: PyVector, border_radius: Real) -> Self { @@ -2292,10 +2607,10 @@ impl SharedShape { /// 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 raw: Vec<[i64; 3]> = grid_coords.extract()?; let coords: Vec = raw .iter() - .map(|&(x, y, z)| rapier::math::IVector::new(x as _, y as _, z as _)) + .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(), @@ -2320,21 +2635,21 @@ impl SharedShape { )) } - /// Build a 3D heightfield from a 2-D `heights` ndarray. + /// Build a 3D heightfield from a 2-D grid of heights. + /// + /// ``heights[i, j]`` is the height of the grid point at row ``i`` and column ``j``: rows + /// advance along the local Z axis and columns along the local X axis. The grid spans + /// ``[-0.5, 0.5]`` along X and Z before scaling, so the vertex ``(i, j)`` is at + /// ``((j / (ncols - 1) - 0.5) * scale.x, heights[i, j] * scale.y, + /// (i / (nrows - 1) - 0.5) * scale.z)``. /// - /// :param heights: `(rows, cols)` ndarray of `Real`. + /// :param heights: `(nrows, ncols)` ndarray (``float32`` or ``float64``, any memory + /// layout) or nested sequences, with at least 2 rows and 2 columns. /// :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, + extract_heights(heights)?, scale.0.into(), ))) } @@ -2547,10 +2862,10 @@ impl MeshConverter { /// 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. +/// Read-only snapshot. Holds the parent rigid-body handles (if any), +/// the world-space contact normal, the contacts seen by the solver +/// (`solver_contacts`, `num_active_contacts` of them), the relative +/// dominance used by the solver, and a user-data payload. #[pyclass(name = "ContactManifoldData", module = "rapier", frozen)] #[derive(Debug, Clone)] pub struct ContactManifoldData { @@ -2566,6 +2881,88 @@ pub struct ContactManifoldData { pub relative_dominance: i16, #[pyo3(get)] pub user_data: u32, + pub(crate) raw: rapier::geometry::ContactManifoldData, +} + +impl ContactManifoldData { + pub(crate) fn from_rapier(data: &rapier::geometry::ContactManifoldData) -> Self { + let normal: crate::na::SVector = data.normal.into(); + Self { + rigid_body1: data.rigid_body1.map(RigidBodyHandle), + rigid_body2: data.rigid_body2.map(RigidBodyHandle), + normal: Vec3(normal), + num_active_contacts: data.solver_contacts.len(), + relative_dominance: data.relative_dominance, + user_data: data.user_data, + raw: data.clone(), + } + } +} + +#[pymethods] +impl ContactManifoldData { + /// The contacts seen by the constraints solver (list of + /// :class:`SolverContact`). + /// + /// Their ``point`` / ``point2`` are anchors in the local frame of + /// the body they touch, centered at its center of mass (world-space + /// for a side without a solver body, e.g. a fixed one): use + /// :meth:`solver_contact_world_points` to get world-space points. + #[getter] + fn solver_contacts(&self) -> Vec { + self.raw + .solver_contacts + .iter() + .map(SolverContact::from_rapier) + .collect() + } + + /// The world-space contact points, one on each body's surface, of a + /// solver contact of this manifold. + /// + /// The anchors are resolved through the bodies' current poses. The + /// two points differ by roughly the contact distance along the + /// normal; their midpoint is the effective solver contact point. + /// + /// :param contact: A :class:`SolverContact` of :attr:`solver_contacts` + /// (not one read inside ``PhysicsHooks.modify_solver_contacts``, + /// whose points already are world-space). + /// :param bodies: The :class:`RigidBodySet` holding the manifold's + /// bodies. + /// :returns: ``(point1, point2)`` as :class:`Point3`. + fn solver_contact_world_points( + &self, + contact: &SolverContact, + bodies: &Bound<'_, RigidBodySet>, + ) -> PyResult<(Point3, Point3)> { + let (p1, p2) = RigidBodySet::read(bodies, |b| { + self.raw.solver_contact_world_points(&contact.raw, b) + })?; + let p1: crate::na::Vector3 = p1.into(); + let p2: crate::na::Vector3 = p2.into(); + Ok(( + Point3(crate::na::Point3::from(p1)), + Point3(crate::na::Point3::from(p2)), + )) + } + + fn __repr__(&self) -> String { + let body = |h: Option| { + h.map_or("None".to_string(), |h| { + format!("{:?}", h.0.into_raw_parts()) + }) + }; + let n = self.normal.0; + format!( + "ContactManifoldData(rigid_body1={}, rigid_body2={}, normal=({}, {}, {}), num_active_contacts={})", + body(self.rigid_body1), + body(self.rigid_body2), + n.x, + n.y, + n.z, + self.num_active_contacts, + ) + } } // ============================================================ @@ -2631,38 +3028,18 @@ impl ContactPair { .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, - } - }) + .enumerate() + .map(|(i, c)| ContactData::from_rapier(c, i)) .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, - }, + data: ContactManifoldData::from_rapier(&m.data), points, local_n1: Vec3(n1), local_n2: Vec3(n2), @@ -2681,19 +3058,13 @@ impl ContactPair { /// 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, - } + self.0.find_deepest_contact().map(|(m, c)| { + let index = m + .points + .iter() + .position(|p| std::ptr::eq(p, c)) + .unwrap_or_default(); + ContactData::from_rapier(c, index) }) } @@ -2704,10 +3075,26 @@ impl ContactPair { Vec3(nav) } - /// Magnitude of `total_impulse()`. + /// Sum of the magnitudes of the contact impulses of every manifold (the sum of the lengths, + /// not the length of :meth:`total_impulse`). This is what's compared with + /// :attr:`Collider.contact_force_event_threshold`. fn total_impulse_magnitude(&self) -> Real { self.0.total_impulse_magnitude() } + + fn __repr__(&self) -> String { + format!( + "ContactPair(collider1={:?}, collider2={:?}, manifolds={}, has_any_active_contact={})", + self.0.collider1.into_raw_parts(), + self.0.collider2.into_raw_parts(), + self.0.manifolds().len(), + if self.0.has_any_active_contact() { + "True" + } else { + "False" + }, + ) + } } // ============================================================ @@ -2759,6 +3146,19 @@ pub struct ContactForceEvent { pub max_force_magnitude: Real, } +#[pymethods] +impl ContactForceEvent { + fn __repr__(&self) -> String { + format!( + "ContactForceEvent(collider1={:?}, collider2={:?}, total_force_magnitude={}, max_force_magnitude={})", + self.collider1.0.into_raw_parts(), + self.collider2.0.into_raw_parts(), + self.total_force_magnitude, + self.max_force_magnitude, + ) + } +} + // ---- 3D-only shape views ---- /// Cylinder shape view, axis-aligned to Y. @@ -2785,6 +3185,15 @@ impl Cylinder { fn radius(&self) -> Real { self.0.radius } + /// The cylinder's surface as a triangle mesh, oriented outward, with ``nsubdiv`` + /// subdivisions around its axis: an ``(N, 3)`` float ndarray of vertices and an ``(M, 3)`` + /// uint32 ndarray of triangles. + /// + /// :raises ValueError: if ``nsubdiv < 3``. + fn to_trimesh<'py>(&self, py: Python<'py>, nsubdiv: u32) -> PyResult> { + check_subdivisions(nsubdiv, None)?; + Ok(trimesh_arrays(py, self.0.to_trimesh(nsubdiv))) + } fn __repr__(&self) -> String { format!( "Cylinder(half_height={}, radius={})", @@ -2817,6 +3226,15 @@ impl Cone { fn radius(&self) -> Real { self.0.radius } + /// The cone's surface as a triangle mesh, oriented outward, with ``nsubdiv`` subdivisions + /// around its axis: an ``(N, 3)`` float ndarray of vertices and an ``(M, 3)`` uint32 ndarray + /// of triangles. + /// + /// :raises ValueError: if ``nsubdiv < 3``. + fn to_trimesh<'py>(&self, py: Python<'py>, nsubdiv: u32) -> PyResult> { + check_subdivisions(nsubdiv, None)?; + Ok(trimesh_arrays(py, self.0.to_trimesh(nsubdiv))) + } fn __repr__(&self) -> String { format!( "Cone(half_height={}, radius={})", @@ -2852,6 +3270,12 @@ impl ConvexPolyhedron { self.0.points().len() } + /// The polyhedron's surface as a triangle mesh, oriented outward: an ``(N, 3)`` float + /// ndarray of vertices and an ``(M, 3)`` uint32 ndarray of triangles. + fn to_trimesh<'py>(&self, py: Python<'py>) -> TriMeshArrays<'py> { + trimesh_arrays(py, self.0.to_trimesh()) + } + /// Triangulated face indices as an `(M, 3)` ndarray of `u32`. /// /// Each (convex) face is fan-triangulated; the indices reference @@ -2881,11 +3305,12 @@ impl ConvexPolyhedron { #[pymethods] impl HeightField { - /// Height samples as an `(nrows, ncols)` float ndarray. + /// Height samples as an `(nrows, ncols)` float ndarray, laid out like the ``heights`` + /// given to :meth:`SharedShape.heightfield`. /// - /// 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]`. + /// Local vertex positions are + /// `(x * scale.x, heights[i, j] * scale.y, z * scale.z)` + /// where `x = j / (ncols - 1) - 0.5` and `z = i / (nrows - 1) - 0.5`. #[getter] fn heights<'py>(&self, py: Python<'py>) -> Bound<'py, crate::numpy::PyArray2> { let h = self.0.heights(); @@ -2896,16 +3321,21 @@ impl HeightField { .collect(); crate::numpy::PyArray2::from_vec2_bound(py, &rows).expect("contiguous 2D ndarray") } - /// Number of rows in the height grid (along the X axis). + /// Number of rows in the height grid (rows advance along the Z axis). #[getter] fn nrows(&self) -> usize { self.0.heights().nrows() } - /// Number of columns in the height grid (along the Z axis). + /// Number of columns in the height grid (columns advance along the X axis). #[getter] fn ncols(&self) -> usize { self.0.heights().ncols() } + /// The heightfield as a triangle mesh: an ``(N, 3)`` float ndarray of vertices and an + /// ``(M, 3)`` uint32 ndarray of triangles. + fn to_trimesh<'py>(&self, py: Python<'py>) -> TriMeshArrays<'py> { + trimesh_arrays(py, self.0.to_trimesh()) + } } // ---- ContactData (3D — tangent_impulse is 2 floats) ---- @@ -2916,7 +3346,9 @@ impl HeightField { /// 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. +/// tangent directions of the manifold's frame. `contact_id` is the +/// index of the point in the manifold's `points`, which the +/// `contact_id` of the solver contacts refers to. #[pyclass(name = "ContactData", module = "rapier", frozen)] #[derive(Debug, Clone)] pub struct ContactData { @@ -2938,6 +3370,23 @@ pub struct ContactData { pub contact_id: u32, } +impl ContactData { + pub(crate) fn from_rapier(c: &rapier::geometry::Contact, index: usize) -> Self { + 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: index as u32, + } + } +} + #[pymethods] impl ContactData { fn __repr__(&self) -> String { @@ -2950,10 +3399,18 @@ impl ContactData { /// One contact prepared for the constraints solver. /// /// Read-only snapshot. `point`/`point2` are the contact points on the -/// first and second body, in world coordinates while inside -/// ``PhysicsHooks.modify_solver_contacts``; `is_new` is true when the -/// contact is freshly generated this step. The manifold's friction and -/// restitution live on :class:`ContactModificationContext`. +/// first and second body: world-space inside +/// ``PhysicsHooks.modify_solver_contacts``; otherwise anchors in the +/// local frame of the body they touch, centered at its center of mass +/// (world-space for a side without a solver body, e.g. a fixed one), +/// see :meth:`ContactManifoldData.solver_contact_world_points`. `dist` +/// is their separation along the normal (negative if penetrating); +/// `tangent_velocity` the world-space tangent velocity of the second +/// collider's surface relative to the first one's that the solver tries +/// to reach at the contact; `contact_id` the index of the manifold point +/// it comes from; `is_new` is true when the contact didn't exist at the +/// previous contact update. The manifold's friction and restitution live +/// on :class:`ContactModificationContext`. #[pyclass(name = "SolverContact", module = "rapier", frozen)] #[derive(Debug, Clone)] pub struct SolverContact { @@ -2964,9 +3421,50 @@ pub struct SolverContact { #[pyo3(get)] pub dist: Real, #[pyo3(get)] + pub tangent_velocity: Vec3, + #[pyo3(get)] pub contact_id: u32, #[pyo3(get)] pub is_new: bool, + pub(crate) raw: rapier::geometry::SolverContact, +} + +impl SolverContact { + pub(crate) fn from_rapier(sc: &rapier::geometry::SolverContact) -> Self { + let p1: crate::na::Vector3 = sc.anchor1.into(); + let p2: crate::na::Vector3 = sc.anchor2.into(); + let tv: crate::na::Vector3 = sc.tangent_velocity.into(); + // The is-new flag lives in bit 31 of the contact id. + let id = sc.contact_id[0]; + SolverContact { + point: Point3(crate::na::Point3::from(p1)), + point2: Point3(crate::na::Point3::from(p2)), + dist: sc.dist, + tangent_velocity: Vec3(tv), + contact_id: id & !rapier::geometry::NEW_CONTACT_BIT, + is_new: (id & rapier::geometry::NEW_CONTACT_BIT) != 0, + raw: *sc, + } + } +} + +#[pymethods] +impl SolverContact { + fn __repr__(&self) -> String { + let (p1, p2) = (self.point.0, self.point2.0); + format!( + "SolverContact(point=({}, {}, {}), point2=({}, {}, {}), dist={}, contact_id={}, is_new={})", + p1.x, + p1.y, + p1.z, + p2.x, + p2.y, + p2.z, + self.dist, + self.contact_id, + if self.is_new { "True" } else { "False" }, + ) + } } // ============================================================ @@ -3029,39 +3527,35 @@ impl Collider { } /// Run `f` with a shared reference to the underlying collider. - fn with_ref(&self, f: impl FnOnce(&rapier::geometry::Collider) -> R) -> R { + fn with_ref(&self, f: impl FnOnce(&rapier::geometry::Collider) -> R) -> PyResult { match &self.backing { - ColliderBacking::Owned(c) => f(c), + ColliderBacking::Owned(c) => Ok(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) + ColliderSet::read(set.bind(py), |set| set.get(*handle).map(f))? + .ok_or_else(|| crate::errors::stale_view("Collider")) }), } } /// 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 { + fn with_mut(&mut self, f: impl FnOnce(&mut rapier::geometry::Collider) -> R) -> PyResult { match &mut self.backing { - ColliderBacking::Owned(c) => f(c), + ColliderBacking::Owned(c) => Ok(f(c)), ColliderBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); + let mut set = crate::errors::try_borrow_mut(set.bind(py))?; let c = set .0 .get_mut(*handle) - .expect("Collider refers to a collider that was removed from its set"); - f(c) + .ok_or_else(|| crate::errors::stale_view("Collider"))?; + Ok(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 { + pub fn to_owned_collider(&self) -> PyResult { self.with_ref(|c| c.clone()) } } @@ -3072,13 +3566,13 @@ impl Collider { /// Handle of the parent rigid-body, if any. #[getter] - fn parent(&self) -> Option { - self.with_ref(|c| c.parent()).map(RigidBodyHandle) + fn parent(&self) -> PyResult> { + Ok(self.with_ref(|c| c.parent())?.map(RigidBodyHandle)) } /// The soft-body collision mesh this collider holds, if it is a deformable collider. #[getter] - fn deformable_mesh_ref(&self) -> Option { + fn deformable_mesh_ref(&self) -> PyResult> { self.with_ref(|c| c.deformable_mesh_ref().map(crate::soft_body::SoftMeshRef)) } @@ -3086,82 +3580,144 @@ impl Collider { /// World-space pose of the collider. #[getter] - fn position(&self) -> Isometry3 { + fn position(&self) -> PyResult { 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())); + fn set_position(&mut self, p: PyIsometry) -> PyResult<()> { + self.with_mut(|c| c.set_position(p.0.into())) } /// World-space translation component of the pose. #[getter] - fn translation(&self) -> Vec3 { + fn translation(&self) -> PyResult { 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())); + fn set_translation(&mut self, v: PyVector) -> PyResult<()> { + self.with_mut(|c| c.set_translation(v.0.into())) } /// World-space rotation component of the pose. #[getter] - fn rotation(&self) -> Rotation3 { + fn rotation(&self) -> PyResult { 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())); + fn set_rotation(&mut self, r: PyRotation) -> PyResult<()> { + self.with_mut(|c| c.set_rotation(r.0.into())) } - // ---- shape ---- - - /// Underlying collision shape. + /// Pose of the collider relative to its parent rigid body, or ``None`` if it has no + /// parent (read+write). + /// + /// Setting it moves the collider on its body; the world-space ``position`` is updated at + /// the next step. Setting it on a collider without parent does nothing. #[getter] - fn shape(&self) -> SharedShape { - self.with_ref(|c| SharedShape(c.shared_shape().clone())) - } + fn position_wrt_parent(&self) -> PyResult> { + self.with_ref(|c| { + c.position_wrt_parent().map(|p| { + let iso: crate::na::Isometry = (*p).into(); + Isometry3(iso) + }) + }) + } + #[setter] + fn set_position_wrt_parent(&mut self, p: PyIsometry) -> PyResult<()> { + self.with_mut(|c| c.set_position_wrt_parent(p.0.into()))?; + Ok(()) + } + /// Translation of the collider relative to its parent rigid body, or ``None`` if it has + /// no parent (read+write). See ``position_wrt_parent``. + #[getter] + fn translation_wrt_parent(&self) -> PyResult> { + self.with_ref(|c| { + c.position_wrt_parent().map(|p| { + let v: crate::na::SVector = p.translation.into(); + Vec3(v) + }) + }) + } + #[setter] + fn set_translation_wrt_parent(&mut self, v: PyVector) -> PyResult<()> { + self.with_mut(|c| c.set_translation_wrt_parent(v.0.into()))?; + Ok(()) + } + /// Rotation of the collider relative to its parent rigid body, or ``None`` if it has no + /// parent (read+write). See ``position_wrt_parent``. + #[getter] + fn rotation_wrt_parent(&self) -> PyResult> { + self.with_ref(|c| { + c.position_wrt_parent() + .map(|p| Rotation3(p.rotation.into())) + }) + } #[setter] - fn set_shape(&mut self, s: SharedShape) { - self.with_mut(|c| c.set_shape(s.0)); + fn set_rotation_wrt_parent(&mut self, r: PyRotation) -> PyResult<()> { + self.with_mut(|c| { + if let Some(mut pose) = c.position_wrt_parent().copied() { + pose.rotation = r.0.into(); + c.set_position_wrt_parent(pose); + } + })?; + Ok(()) + } + + // ---- shape ---- + + /// Underlying collision shape. + #[getter] + fn shape(&self) -> PyResult { + self.with_ref(|c| SharedShape(c.shared_shape().clone())) + } + #[setter] + fn set_shape(&mut self, s: SharedShape) -> PyResult<()> { + 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. + /// Uniform density used to derive mass when no explicit mass is set (read+write). + /// + /// When the mass was set explicitly, reading it returns the density deduced from that + /// mass and the collider's volume. #[getter] - fn density(&self) -> Real { + fn density(&self) -> PyResult { self.with_ref(|c| c.density()) } #[setter] - fn set_density(&mut self, v: Real) { - self.with_mut(|c| c.set_density(v)); + fn set_density(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|c| c.set_density(v)) } - /// Explicit mass of the collider. + /// Mass of the collider (read+write). + /// + /// Reading it returns the mass this collider contributes to its parent body: the explicit + /// mass if one was set, otherwise the one computed from its density and volume. Setting it + /// gives the collider an explicit mass, replacing its density. #[getter] - fn mass(&self) -> Real { + fn mass(&self) -> PyResult { self.with_ref(|c| c.mass()) } #[setter] - fn set_mass(&mut self, v: Real) { - self.with_mut(|c| c.set_mass(v)); + fn set_mass(&mut self, v: Real) -> PyResult<()> { + 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())) + fn mass_properties(&self) -> PyResult { + Ok(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)); + fn set_mass_properties(&mut self, mp: MassProperties) -> PyResult<()> { + self.with_mut(|c| c.set_mass_properties(mp.0)) } /// Volume (3D) or area (2D) of the shape. #[getter] - fn volume(&self) -> Real { + fn volume(&self) -> PyResult { self.with_ref(|c| c.volume()) } @@ -3169,43 +3725,47 @@ impl Collider { /// Friction coefficient. #[getter] - fn friction(&self) -> Real { + fn friction(&self) -> PyResult { self.with_ref(|c| c.friction()) } #[setter] - fn set_friction(&mut self, v: Real) { - self.with_mut(|c| c.set_friction(v)); + fn set_friction(&mut self, v: Real) -> PyResult<()> { + self.with_mut(|c| c.set_friction(v)) } /// Restitution coefficient (bounciness). #[getter] - fn restitution(&self) -> Real { + fn restitution(&self) -> PyResult { self.with_ref(|c| c.restitution()) } #[setter] - fn set_restitution(&mut self, v: Real) { - self.with_mut(|c| c.set_restitution(v)); + fn set_restitution(&mut self, v: Real) -> PyResult<()> { + 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())) + fn friction_combine_rule(&self) -> PyResult { + Ok(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())); + fn set_friction_combine_rule(&mut self, v: CoefficientCombineRule) -> PyResult<()> { + 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())) + fn restitution_combine_rule(&self) -> PyResult { + Ok(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())); + fn set_restitution_combine_rule(&mut self, v: CoefficientCombineRule) -> PyResult<()> { + 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 { + fn material(&self) -> PyResult { self.with_ref(|c| ColliderMaterial(*c.material())) } @@ -3213,73 +3773,75 @@ impl Collider { /// True iff this collider is a sensor (no contact response). #[getter] - fn is_sensor(&self) -> bool { + fn is_sensor(&self) -> PyResult { self.with_ref(|c| c.is_sensor()) } #[setter] - fn set_is_sensor(&mut self, v: bool) { - self.with_mut(|c| c.set_sensor(v)); + fn set_is_sensor(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|c| c.set_sensor(v)) } /// True iff the collider is currently enabled. #[getter] - fn is_enabled(&self) -> bool { + fn is_enabled(&self) -> PyResult { self.with_ref(|c| c.is_enabled()) } #[setter] - fn set_is_enabled(&mut self, v: bool) { - self.with_mut(|c| c.set_enabled(v)); + fn set_is_enabled(&mut self, v: bool) -> PyResult<()> { + 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())) + fn active_events(&self) -> PyResult { + Ok(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)); + fn set_active_events(&mut self, v: ActiveEvents) -> PyResult<()> { + 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())) + fn active_hooks(&self) -> PyResult { + Ok(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)); + fn set_active_hooks(&mut self, v: ActiveHooks) -> PyResult<()> { + 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())) + fn active_collision_types(&self) -> PyResult { + Ok(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)); + fn set_active_collision_types(&mut self, v: ActiveCollisionTypes) -> PyResult<()> { + 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())) + fn collision_groups(&self) -> PyResult { + Ok(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)); + fn set_collision_groups(&mut self, v: InteractionGroups) -> PyResult<()> { + 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())) + fn solver_groups(&self) -> PyResult { + Ok(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)); + fn set_solver_groups(&mut self, v: InteractionGroups) -> PyResult<()> { + self.with_mut(|c| c.set_solver_groups(v.0)) } // ---- contact skin & event threshold ---- @@ -3287,42 +3849,42 @@ impl Collider { /// Thickness of the virtual "skin" around the collider, used to /// reduce jitter on resting contacts. #[getter] - fn contact_skin(&self) -> Real { + fn contact_skin(&self) -> PyResult { 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)); + fn set_contact_skin(&mut self, v: Real) -> PyResult<()> { + 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 { + fn contact_force_event_threshold(&self) -> PyResult { 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)); + fn set_contact_force_event_threshold(&mut self, v: Real) -> PyResult<()> { + 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 { + fn user_data(&self) -> PyResult { self.with_ref(|c| c.user_data) } #[setter] - fn set_user_data(&mut self, v: u128) { - self.with_mut(|c| c.user_data = v); + fn set_user_data(&mut self, v: u128) -> PyResult<()> { + 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())) + fn compute_aabb(&self) -> PyResult { + Ok(Aabb(self.with_ref(|c| c.compute_aabb())?)) } // ---- Builder forwards (mirror RigidBody.dynamic etc) ---- @@ -3342,15 +3904,69 @@ impl Collider { ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::ball(radius), kwargs) } /// Builder for a triangle collider with the given vertices. + /// + /// Like every shape factory of ``Collider``, it accepts optional `ColliderBuilder` + /// kwargs. #[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(), - ), - } + #[pyo3(signature = (a, b, c, **kwargs))] + fn triangle( + a: PyVector, + b: PyVector, + c: PyVector, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::triangle(a.0.into(), b.0.into(), c.0.into()), + kwargs, + ) + } + /// Builder for a segment collider between the points `a` and `b`. + #[staticmethod] + #[pyo3(signature = (a, b, **kwargs))] + fn segment( + a: PyVector, + b: PyVector, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::segment(a.0.into(), b.0.into()), + kwargs, + ) + } + /// Builder for a polyline collider: a set of segments joining the given vertices. + /// + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 2)` integer ndarray (or a sequence of pairs) listing the + /// vertices of each segment. If ``None``, the vertices are joined in order into a + /// line strip. + #[staticmethod] + #[pyo3(signature = (vertices, indices=None, **kwargs))] + fn polyline( + vertices: &Bound<'_, PyAny>, + indices: Option<&Bound<'_, PyAny>>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let shape = SharedShape::polyline(vertices, indices)?; + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::new(shape.0), kwargs) + } + /// Builder for a voxels collider obtained by voxelizing a triangle mesh. + /// + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 3)` integer ndarray (or a sequence of triples). + /// :param voxel_size: edge length of one (cubic) voxel. + /// :param fill_mode: which voxels are filled (a :class:`FillMode`); defaults to + /// ``FillMode.flood_fill()`` (a solid). + #[staticmethod] + #[pyo3(signature = (vertices, indices, voxel_size, fill_mode=None, **kwargs))] + fn voxelized_mesh( + vertices: &Bound<'_, PyAny>, + indices: &Bound<'_, PyAny>, + voxel_size: Real, + fill_mode: Option, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let shape = SharedShape::voxelized_mesh(vertices, indices, voxel_size, fill_mode)?; + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::new(shape.0), kwargs) } /// Builder for a half-space (infinite plane) collider. /// @@ -3376,33 +3992,37 @@ impl Collider { } /// Builder wrapping an arbitrary `SharedShape`. #[staticmethod] - #[pyo3(signature = (shape))] - fn new(shape: SharedShape) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::new(shape.0), - } + #[pyo3(signature = (shape, **kwargs))] + fn new( + shape: SharedShape, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::new(shape.0), kwargs) } /// Builder for a compound collider built from `(pose, sub_shape)` parts. #[staticmethod] - fn compound(parts: Vec<(PyIsometry, SharedShape)>) -> ColliderBuilder { + #[pyo3(signature = (parts, **kwargs))] + fn compound( + parts: Vec<(PyIsometry, SharedShape)>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { 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), - } + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::compound(parts), kwargs) } /// Builder for a triangle-mesh collider. /// - /// :param vertices: `(N, D)` ndarray of floats. - /// :param indices: `(M, 3)` ndarray of `u32`. + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 3)` integer ndarray (or a sequence of triples). /// :param flags: Optional `TriMeshFlags` preprocessing. /// :raises MeshConversionError: If the mesh cannot be built. #[staticmethod] - #[pyo3(signature = (vertices, indices, flags=None))] + #[pyo3(signature = (vertices, indices, flags=None, **kwargs))] fn trimesh( vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>, flags: Option, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> PyResult { let verts = extract_verts_for_dim(vertices)?; let idx = crate::geometry::extract_indices(indices)?; @@ -3411,69 +4031,78 @@ impl Collider { 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 }) + ColliderBuilder::from_kwargs(b, kwargs) } /// Builder for a collider wrapping the convex hull of a point cloud. /// + /// :param points: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). /// :raises MeshConversionError: If the convex hull cannot be built. #[staticmethod] - fn convex_hull(points: &Bound<'_, PyAny>) -> PyResult { + #[pyo3(signature = (points, **kwargs))] + fn convex_hull( + points: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> 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") - }) + let b = rapier::geometry::ColliderBuilder::convex_hull(&pts).ok_or_else(|| { + crate::errors::MeshConversionError::new_err("convex hull computation failed") + })?; + ColliderBuilder::from_kwargs(b, kwargs) } /// Builder for a triangle collider with rounded edges. #[staticmethod] + #[pyo3(signature = (a, b, c, border_radius, **kwargs))] fn round_triangle( a: PyVector, b: PyVector, c: PyVector, border_radius: Real, - ) -> ColliderBuilder { - ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::round_triangle( + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::round_triangle( a.0.into(), b.0.into(), c.0.into(), border_radius, ), - } + kwargs, + ) } /// Builder for a rounded convex-hull collider. /// /// :raises MeshConversionError: If the hull cannot be built. #[staticmethod] + #[pyo3(signature = (points, border_radius, **kwargs))] fn round_convex_hull( points: &Bound<'_, PyAny>, border_radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> PyResult { let pts = extract_verts_for_dim(points)?; - rapier::geometry::ColliderBuilder::round_convex_hull(&pts, border_radius) - .map(|b| ColliderBuilder { builder: b }) + let b = rapier::geometry::ColliderBuilder::round_convex_hull(&pts, border_radius) .ok_or_else(|| { crate::errors::MeshConversionError::new_err("round convex hull computation failed") - }) + })?; + ColliderBuilder::from_kwargs(b, kwargs) } /// 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. + /// :param points: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). #[staticmethod] + #[pyo3(signature = (voxel_size, points, **kwargs))] fn voxels_from_points( voxel_size: PyVector, points: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> PyResult { let pts = extract_verts_for_dim(points)?; - Ok(ColliderBuilder { - builder: rapier::geometry::ColliderBuilder::voxels_from_points( - voxel_size.0.into(), - &pts, - ), - }) + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::voxels_from_points(voxel_size.0.into(), &pts), + kwargs, + ) } /// Builder for a collider whose shape is produced from a triangle /// buffer by a `MeshConverter` (trimesh, hull, OBB, AABB, @@ -3481,59 +4110,86 @@ impl Collider { /// /// :raises MeshConversionError: If the conversion fails. #[staticmethod] + #[pyo3(signature = (vertices, indices, converter, **kwargs))] fn converted_trimesh( vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>, converter: &MeshConverter, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> 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}"))) + let b = rapier::geometry::ColliderBuilder::converted_trimesh(verts, idx, converter.0) + .map_err(|e| crate::errors::MeshConversionError::new_err(format!("{e}")))?; + ColliderBuilder::from_kwargs(b, kwargs) } // 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn capsule( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), + kwargs, + ) } /// 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn capsule_x( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::capsule_x(half_height, radius), + kwargs, + ) } /// 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn capsule_y( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::capsule_y(half_height, radius), + kwargs, + ) } /// 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( + #[pyo3(signature = (a, b, radius, **kwargs))] + fn capsule_from_endpoints( + a: PyVector, + b: PyVector, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::capsule_from_endpoints( a.0.into(), b.0.into(), radius, ), - } + kwargs, + ) } - fn __repr__(&self) -> String { - format!( + fn __repr__(&self) -> PyResult { + Ok(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()), - ) + 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()), + )) } } @@ -3563,142 +4219,191 @@ impl Collider { } /// 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn cylinder( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::cylinder(half_height, radius), + kwargs, + ) } /// 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn cone( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::cone(half_height, radius), + kwargs, + ) } /// 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), - } + #[pyo3(signature = (half_height, radius, **kwargs))] + fn capsule_z( + half_height: Real, + radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::capsule_z(half_height, radius), + kwargs, + ) } /// 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), - } + #[pyo3(signature = (hx, hy, hz, border_radius, **kwargs))] + fn round_cuboid( + hx: Real, + hy: Real, + hz: Real, + border_radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::round_cuboid(hx, hy, hz, border_radius), + kwargs, + ) } /// 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, - ), - } + #[pyo3(signature = (half_height, radius, border_radius, **kwargs))] + fn round_cylinder( + half_height: Real, + radius: Real, + border_radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::round_cylinder(half_height, radius, border_radius), + kwargs, + ) } /// 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, - ), - } + #[pyo3(signature = (half_height, radius, border_radius, **kwargs))] + fn round_cone( + half_height: Real, + radius: Real, + border_radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::round_cone(half_height, radius, border_radius), + kwargs, + ) } /// Builder for a convex-polyhedron collider (3D convex hull of `points`). #[staticmethod] - fn convex_polyhedron(points: &Bound<'_, PyAny>) -> PyResult { + #[pyo3(signature = (points, **kwargs))] + fn convex_polyhedron( + points: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { // 3D: convex_hull builds a ConvexPolyhedron internally; reuse it. - Self::convex_hull(points) + Self::convex_hull(points, kwargs) } /// Builder for a compound collider obtained by convex /// decomposition of a triangle mesh. + /// + /// :param vertices: `(N, 3)` ndarray of floats (or a sequence of 3-vectors). + /// :param indices: `(M, 3)` integer ndarray (or a sequence of triples). #[staticmethod] - #[pyo3(signature = (vertices, indices))] + #[pyo3(signature = (vertices, indices, **kwargs))] fn convex_decomposition( vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> 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), - }) + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::convex_decomposition(&verts, &idx), + kwargs, + ) } /// Builder for a convex-mesh collider (vertices assumed convex). /// /// :raises MeshConversionError: If the mesh is not a valid convex mesh. #[staticmethod] + #[pyo3(signature = (vertices, indices, **kwargs))] fn convex_mesh( vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> 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)", - ) - }) + let b = rapier::geometry::ColliderBuilder::convex_mesh(verts, &idx).ok_or_else(|| { + crate::errors::MeshConversionError::new_err( + "convex mesh construction failed (invalid convex mesh)", + ) + })?; + ColliderBuilder::from_kwargs(b, kwargs) } /// Builder for a rounded convex-mesh collider. /// /// :raises MeshConversionError: If the mesh is not a valid convex mesh. #[staticmethod] + #[pyo3(signature = (vertices, indices, border_radius, **kwargs))] fn round_convex_mesh( vertices: &Bound<'_, PyAny>, indices: &Bound<'_, PyAny>, border_radius: Real, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, ) -> 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 }) + let b = rapier::geometry::ColliderBuilder::round_convex_mesh(verts, &idx, border_radius) .ok_or_else(|| { crate::errors::MeshConversionError::new_err( "round convex mesh construction failed (invalid convex mesh)", ) - }) + })?; + ColliderBuilder::from_kwargs(b, kwargs) } /// 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), - }) + #[pyo3(signature = (voxel_size, grid_coords, **kwargs))] + fn voxels( + voxel_size: PyVector, + grid_coords: &Bound<'_, PyAny>, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + let shape = SharedShape::voxels(voxel_size, grid_coords)?; + ColliderBuilder::from_kwargs(rapier::geometry::ColliderBuilder::new(shape.0), kwargs) } /// Builder for a 3D heightfield collider. /// - /// :param heights: `(rows, cols)` ndarray of floats. + /// ``heights[i, j]`` is the height at row ``i`` (advancing along the local Z axis) and + /// column ``j`` (advancing along the local X axis); see :meth:`SharedShape.heightfield`. + /// + /// :param heights: `(nrows, ncols)` ndarray (``float32`` or ``float64``) or nested + /// sequences, with at least 2 rows and 2 columns. /// :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()), - }) + #[pyo3(signature = (heights, scale, **kwargs))] + fn heightfield( + heights: &Bound<'_, PyAny>, + scale: PyVector, + kwargs: Option<&Bound<'_, crate::pyo3::types::PyDict>>, + ) -> PyResult { + ColliderBuilder::from_kwargs( + rapier::geometry::ColliderBuilder::heightfield( + extract_heights(heights)?, + scale.0.into(), + ), + kwargs, + ) } } @@ -3830,13 +4535,19 @@ impl ColliderBuilder { #[pymethods] impl ColliderBuilder { - /// Set the world-space translation. Returns a new builder. + /// Set the translation. Returns a new builder. + /// + /// It is relative to the parent rigid body when the collider is inserted with a parent, + /// and in world space otherwise. fn translation(&self, v: PyVector) -> Self { Self { builder: self.builder.clone().translation(v.0.into()), } } - /// Set the world-space pose. Returns a new builder. + /// Set the pose (translation and rotation). Returns a new builder. + /// + /// It is relative to the parent rigid body when the collider is inserted with a parent, + /// and in world space otherwise. fn position(&self, p: PyIsometry) -> Self { Self { builder: self.builder.clone().position(p.0.into()), @@ -3948,8 +4659,8 @@ impl ColliderBuilder { /// 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). + /// The argument is a rotation vector (axis * angle). Like ``translation``, it is + /// relative to the parent rigid body when the collider is inserted with a parent. fn rotation(&self, v: &Bound<'_, PyAny>) -> PyResult { let mut b = self.builder.clone(); { @@ -3974,10 +4685,26 @@ impl ColliderBuilder { /// 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)] +/// (e.g. `set[h].is_sensor = True`) persists in place. +#[pyclass(name = "ColliderSet", module = "rapier")] pub struct ColliderSet(pub rapier::geometry::ColliderSet); +impl ColliderSet { + /// Run `f` on the set, also while a step lends it to a physics hook or + /// event handler. + pub(crate) fn read( + slf: &Bound<'_, Self>, + f: impl FnOnce(&rapier::geometry::ColliderSet) -> R, + ) -> PyResult { + // A lent set is read first: the running step holds the set mutably borrowed, so + // `try_borrow` fails until it ends. + crate::events_hooks::with_lent_or(slf.as_ptr(), f, |f| match slf.try_borrow() { + Ok(set) => Ok(f(&set.0)), + Err(_) => Err(crate::events_hooks::stepping_error("ColliderSet")), + }) + } +} + #[pymethods] impl ColliderSet { /// Construct an empty set. @@ -3996,7 +4723,7 @@ impl ColliderSet { 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()))); + return Ok(ColliderHandle(self.0.insert(c.to_owned_collider()?))); } Err(PyTypeError::new_err( "ColliderSet.insert expects a Collider or ColliderBuilder", @@ -4021,7 +4748,7 @@ impl ColliderSet { let coll = if let Ok(b) = builder.extract::>() { b.builder.clone().build() } else if let Ok(c) = builder.extract::>() { - c.to_owned_collider() + c.to_owned_collider()? } else { return Err(PyTypeError::new_err( "ColliderSet.insert_with_parent expects a Collider or ColliderBuilder", @@ -4036,29 +4763,66 @@ impl ColliderSet { /// 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. + /// :param wake_up: If True (default), wakes the parent rigid-body so + /// islands re-evaluate. + /// :param soft_bodies: The world's :class:`SoftBodySet`, needed to remove a deformable + /// collider (a collision mesh of a soft body); it can be left out otherwise. + /// :param wake_parent: Deprecated alias of ``wake_up``. /// :returns: The removed `Collider`, or `None` if `handle` is unknown. - #[pyo3(signature = (handle, islands, bodies, wake_parent=true, soft_bodies=None))] + /// :raises ValueError: if the collider is deformable and ``soft_bodies`` is left out. + #[pyo3(signature = (handle, islands, bodies, wake_up=true, soft_bodies=None, *, wake_parent=None))] + #[allow(clippy::too_many_arguments)] fn remove( &mut self, + py: Python<'_>, handle: &ColliderHandle, islands: &mut IslandManager, bodies: &mut RigidBodySet, - wake_parent: bool, + wake_up: bool, soft_bodies: Option<&mut crate::soft_body::SoftBodySet>, - ) -> Option { + wake_parent: Option, + ) -> PyResult> { + let wake_up = match wake_parent { + Some(w) => { + PyErr::warn_bound( + py, + &py.get_type_bound::(), + "ColliderSet.remove(wake_parent=...) is deprecated; use wake_up=... instead", + 1, + )?; + w + } + None => wake_up, + }; let mut scratch = rapier::dynamics::SoftBodySet::new(); - let soft_bodies = soft_bodies.map_or(&mut scratch, |s| &mut s.0); - self.0 + let soft_bodies = match soft_bodies { + Some(s) => &mut s.0, + None => { + // Removing a deformable collider without its soft body would leave the body + // with a mesh bound to a collider that no longer exists. + if self + .0 + .get(handle.0) + .is_some_and(|c| c.deformable_mesh_ref().is_some()) + { + return Err(PyValueError::new_err( + "the collider is a soft-body collision mesh: pass the world's \ + SoftBodySet as `soft_bodies` to remove it", + )); + } + &mut scratch + } + }; + Ok(self + .0 .remove( handle.0, &mut islands.0, &mut bodies.0, soft_bodies, - wake_parent, + wake_up, ) - .map(Collider::new_owned) + .map(Collider::new_owned)) } /// Insert a collider holding a soft body's deformable collision mesh: a triangle mesh @@ -4079,7 +4843,7 @@ impl ColliderSet { let coll = if let Ok(b) = builder.extract::>() { b.builder.clone().build() } else if let Ok(c) = builder.extract::>() { - c.to_owned_collider() + c.to_owned_collider()? } else { return Err(PyTypeError::new_err( "ColliderSet.insert_deformable expects a Collider or ColliderBuilder", @@ -4100,18 +4864,20 @@ impl ColliderSet { /// 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 { + fn get(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> PyResult> { + if !Self::read(slf, |set| set.contains(handle.0))? { + return Ok(None); + } + Ok(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() { + if !Self::read(slf, |set| set.contains(handle.0))? { return Err(crate::errors::InvalidHandle::new_err(format!( "no collider for {:?}", handle.0.into_raw_parts(), @@ -4125,15 +4891,15 @@ impl ColliderSet { }) } - fn __contains__(&self, handle: &ColliderHandle) -> bool { - self.0.contains(handle.0) + fn __contains__(slf: &Bound<'_, Self>, handle: &ColliderHandle) -> PyResult { + Self::read(slf, |set| set.contains(handle.0)) } - fn __len__(&self) -> usize { - self.0.len() + fn __len__(slf: &Bound<'_, Self>) -> PyResult { + Self::read(slf, |set| set.len()) } /// True iff the set contains no colliders. - fn is_empty(&self) -> bool { - self.0.is_empty() + fn is_empty(slf: &Bound<'_, Self>) -> PyResult { + Self::read(slf, |set| set.is_empty()) } /// Remove every collider from the set. @@ -4143,7 +4909,7 @@ impl ColliderSet { fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { let handles: Vec = - slf.borrow().0.iter().map(|(h, _)| h).collect(); + Self::read(slf, |set| set.iter().map(|(h, _)| h).collect())?; Py::new( slf.py(), ColliderSetIter { @@ -4155,8 +4921,10 @@ impl ColliderSet { } /// 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(); + fn handles(slf: &Bound<'_, Self>) -> PyResult> { + let h: Vec = Self::read(slf, |set| { + set.iter().map(|(h, _)| ColliderHandle(h)).collect() + })?; Py::new(slf.py(), ColliderHandleIter { handles: h, i: 0 }) } } @@ -4336,6 +5104,7 @@ pub fn register_geometry( m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/python/rapier-py-3d/src/joints.rs b/python/rapier-py-3d/src/joints.rs index 8a1db4cff..3815fede4 100644 --- a/python/rapier-py-3d/src/joints.rs +++ b/python/rapier-py-3d/src/joints.rs @@ -40,8 +40,8 @@ use rapier3d as rapier; /// ``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)] +#[pyclass(name = "JointEnabled", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum JointEnabled { /// Joint participates normally in the constraint solver. ENABLED, @@ -101,8 +101,8 @@ impl JointEnabled { /// 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)] +#[pyclass(name = "MotorModel", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MotorModel { /// PID output is interpreted as a desired acceleration; the /// engine scales by effective mass before applying. Behaviour @@ -432,8 +432,8 @@ impl JointAxesMask { /// (``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)] +#[pyclass(name = "JointAxis", module = "rapier", eq, eq_int, hash, frozen)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum JointAxis { /// Translation along the body-local X axis. LIN_X, @@ -789,8 +789,12 @@ impl InverseKinematicsOption { /// 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 + "InverseKinematicsOption(damping={}, max_iters={}, constrained_axes=JointAxesMask(bits={:#010b}), epsilon_linear={}, epsilon_angular={})", + self.damping, + self.max_iters, + self.constrained_axes.0.bits(), + self.epsilon_linear, + self.epsilon_angular ) } } @@ -837,7 +841,7 @@ impl FixedJoint { /// :param kwargs: Optional keyword args forwarded to the /// builder; accepted keys are ``local_anchor1``, /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, - /// ``contacts_enabled``. + /// ``contacts_enabled``, ``softness``. #[staticmethod] #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] fn builder( @@ -971,6 +975,10 @@ impl FixedJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown FixedJointBuilder kwarg: '{}'", @@ -1067,7 +1075,8 @@ impl RevoluteJoint { /// :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``. + /// ``local_anchor2``, ``limits``, ``contacts_enabled``, + /// ``softness``. #[staticmethod] #[pyo3(signature = (axis=None, **kwargs))] fn builder( @@ -1231,6 +1240,10 @@ impl RevoluteJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown RevoluteJointBuilder kwarg: '{}'", @@ -1345,7 +1358,7 @@ impl PrismaticJoint { /// :param kwargs: Optional keyword args forwarded to the /// builder; accepted keys include ``local_anchor1``, /// ``local_anchor2``, ``local_axis1``, ``local_axis2``, - /// ``limits``, ``contacts_enabled``. + /// ``limits``, ``contacts_enabled``, ``softness``. #[staticmethod] #[pyo3(signature = (axis, **kwargs))] fn builder( @@ -1519,6 +1532,10 @@ impl PrismaticJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown PrismaticJointBuilder kwarg: '{}'", @@ -1578,6 +1595,11 @@ impl PrismaticJointBuilder { 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.set_motor(target_pos, target_vel, 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)) @@ -1627,7 +1649,7 @@ impl RopeJoint { /// :param max_distance: Maximum rope length (world units). /// :param kwargs: Optional keyword args; accepted keys are /// ``local_anchor1``, ``local_anchor2``, ``max_distance``, - /// ``contacts_enabled``. + /// ``contacts_enabled``, ``softness``. #[staticmethod] #[pyo3(signature = (max_distance, **kwargs))] fn builder( @@ -1705,6 +1727,37 @@ impl RopeJoint { .unwrap_or(0.0) } + /// Return the motor acting on the rope length, if configured. + fn motor(&self) -> Option { + self.0 + .motor(rapier::dynamics::JointAxis::LinX) + .map(JointMotor::from_rapier) + } + /// Configure a velocity-target motor on the rope length, with damping. + /// + /// :param target_vel: Desired stretching 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 pulling the rope length 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 { @@ -1759,6 +1812,10 @@ impl RopeJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown RopeJointBuilder kwarg: '{}'", @@ -1796,6 +1853,28 @@ impl RopeJointBuilder { fn max_distance(&self, d: Real) -> Self { Self(self.0.max_distance(d)) } + /// Configure a velocity-target motor on the rope length, 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 pulling the rope length 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)) + } + /// 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.set_motor(target_pos, target_vel, 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())) + } /// Enable or disable contacts between the attached bodies. fn contacts_enabled(&self, b: bool) -> Self { Self(self.0.contacts_enabled(b)) @@ -2078,7 +2157,7 @@ pub struct GenericJoint { /// 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). +/// or a `MultibodyJointSet` (so `joint.data.set_limits(..)` persists in place). #[derive(Debug)] pub enum GenericJointBacking { Owned(Box), @@ -2086,6 +2165,10 @@ pub enum GenericJointBacking { set: Py, handle: rapier::dynamics::ImpulseJointHandle, }, + MultibodyJointData { + set: Py, + handle: rapier::dynamics::MultibodyJointHandle, + }, } impl Clone for GenericJointBacking { @@ -2098,6 +2181,12 @@ impl Clone for GenericJointBacking { handle: *handle, }) } + GenericJointBacking::MultibodyJointData { set, handle } => { + Python::with_gil(|py| GenericJointBacking::MultibodyJointData { + set: set.clone_ref(py), + handle: *handle, + }) + } } } } @@ -2115,34 +2204,66 @@ impl GenericJoint { backing: GenericJointBacking::Owned(Box::new(joint)), } } - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::GenericJoint) -> R) -> R { + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::GenericJoint) -> R) -> PyResult { match &self.backing { - GenericJointBacking::Owned(g) => f(g), + GenericJointBacking::Owned(g) => Ok(f(g)), GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); + let set = crate::errors::try_borrow(set.bind(py))?; let joint = set .0 .get(*handle) - .expect("GenericJoint refers to a joint that was removed from its set"); - f(&joint.data) + .ok_or_else(|| crate::errors::stale_view("GenericJoint"))?; + Ok(f(&joint.data)) + }), + GenericJointBacking::MultibodyJointData { set, handle } => Python::with_gil(|py| { + let set = crate::errors::try_borrow(set.bind(py))?; + let link = set + .0 + .get(*handle) + .and_then(|(mb, id)| mb.link(id)) + .ok_or_else(|| crate::errors::stale_view("GenericJoint"))?; + Ok(f(&link.joint.data)) }), } } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::GenericJoint) -> R) -> R { + fn with_mut( + &mut self, + f: impl FnOnce(&mut rapier::dynamics::GenericJoint) -> R, + ) -> PyResult { match &mut self.backing { - GenericJointBacking::Owned(g) => f(g), + GenericJointBacking::Owned(g) => Ok(f(g)), GenericJointBacking::ImpulseJointData { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); + let mut set = crate::errors::try_borrow_mut(set.bind(py))?; let joint = set .0 .get_mut(*handle, true) - .expect("GenericJoint refers to a joint that was removed from its set"); - f(&mut joint.data) + .ok_or_else(|| crate::errors::stale_view("GenericJoint"))?; + Ok(f(&mut joint.data)) }), + GenericJointBacking::MultibodyJointData { set, handle } => Python::with_gil(|py| { + let mut set = crate::errors::try_borrow_mut(set.bind(py))?; + set.modify_joint(*handle, |joint| f(&mut joint.data)) + .ok_or_else(|| crate::errors::stale_view("GenericJoint")) + }), + } + } + /// Reject a change of the locked axes of a multibody joint: they define + /// its degrees of freedom, so changing them would corrupt the multibody. + fn ensure_locked_axes_unchanged( + &self, + new_locked_axes: rapier::dynamics::JointAxesMask, + ) -> PyResult<()> { + if matches!(self.backing, GenericJointBacking::MultibodyJointData { .. }) + && self.with_ref(|g| g.locked_axes)? != new_locked_axes + { + return Err(PyValueError::new_err( + "the locked axes of a multibody joint cannot change (they define its degrees of freedom)", + )); } + Ok(()) } /// Copy the underlying joint out (it is `Copy` upstream). - pub fn to_owned_generic(&self) -> rapier::dynamics::GenericJoint { + pub fn to_owned_generic(&self) -> PyResult { self.with_ref(|g| *g) } } @@ -2167,7 +2288,8 @@ impl GenericJoint { /// :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``. + /// ``locked_axes``, ``coupled_axes``, ``contacts_enabled``, + /// ``softness``. #[staticmethod] #[pyo3(signature = (locked_axes=None, **kwargs))] fn builder( @@ -2182,192 +2304,203 @@ impl GenericJoint { /// 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) + fn local_frame1(&self) -> PyResult { + let pose: crate::na::Isometry = self.with_ref(|g| g.local_frame1)?.into(); + Ok(Isometry3(pose)) } /// Set the body-local frame on body 1. #[setter] - fn set_local_frame1(&mut self, iso: PyIsometry) { + fn set_local_frame1(&mut self, iso: PyIsometry) -> PyResult<()> { let p: rapier::math::Pose = iso.0.into(); self.with_mut(|g| { g.set_local_frame1(p); - }); + })?; + Ok(()) } /// 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) + fn local_frame2(&self) -> PyResult { + let pose: crate::na::Isometry = self.with_ref(|g| g.local_frame2)?.into(); + Ok(Isometry3(pose)) } /// Set the body-local frame on body 2. #[setter] - fn set_local_frame2(&mut self, iso: PyIsometry) { + fn set_local_frame2(&mut self, iso: PyIsometry) -> PyResult<()> { let p: rapier::math::Pose = iso.0.into(); self.with_mut(|g| { g.set_local_frame2(p); - }); + })?; + Ok(()) } /// 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)) + fn local_anchor1(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|g| g.local_anchor1())?.into(); + Ok(Point3(crate::na::Point::from(v))) } /// Set the body-local anchor on body 1. #[setter] - fn set_local_anchor1(&mut self, p: PyPoint) { + fn set_local_anchor1(&mut self, p: PyPoint) -> PyResult<()> { let g: rapier::math::Vector = p.0.coords.into(); self.with_mut(|gj| { gj.set_local_anchor1(g); - }); + })?; + Ok(()) } /// 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)) + fn local_anchor2(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|g| g.local_anchor2())?.into(); + Ok(Point3(crate::na::Point::from(v))) } /// Set the body-local anchor on body 2. #[setter] - fn set_local_anchor2(&mut self, p: PyPoint) { + fn set_local_anchor2(&mut self, p: PyPoint) -> PyResult<()> { let g: rapier::math::Vector = p.0.coords.into(); self.with_mut(|gj| { gj.set_local_anchor2(g); - }); + })?; + Ok(()) } /// 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) + fn local_axis1(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|g| g.local_axis1())?.into(); + Ok(Vec3(v)) } /// Set the reference axis on body 1. #[setter] - fn set_local_axis1(&mut self, v: PyVector) { + fn set_local_axis1(&mut self, v: PyVector) -> PyResult<()> { let g: rapier::math::Vector = v.0.into(); self.with_mut(|gj| { gj.set_local_axis1(g); - }); + })?; + Ok(()) } /// 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) + fn local_axis2(&self) -> PyResult { + let v: crate::na::SVector = self.with_ref(|g| g.local_axis2())?.into(); + Ok(Vec3(v)) } /// Set the reference axis on body 2. #[setter] - fn set_local_axis2(&mut self, v: PyVector) { + fn set_local_axis2(&mut self, v: PyVector) -> PyResult<()> { let g: rapier::math::Vector = v.0.into(); self.with_mut(|gj| { gj.set_local_axis2(g); - }); + })?; + Ok(()) } /// Mask of axes that are rigidly locked by this joint. #[getter] - fn locked_axes(&self) -> JointAxesMask { - JointAxesMask(self.with_ref(|g| g.locked_axes)) + fn locked_axes(&self) -> PyResult { + Ok(JointAxesMask(self.with_ref(|g| g.locked_axes)?)) } /// Replace the set of locked axes. + /// + /// :raises ValueError: If this is the live data of a multibody joint and + /// the locked axes change (they define its degrees of freedom). #[setter] - fn set_locked_axes(&mut self, v: JointAxesMask) { - self.with_mut(|g| g.locked_axes = v.0); + fn set_locked_axes(&mut self, v: JointAxesMask) -> PyResult<()> { + self.ensure_locked_axes_unchanged(v.0)?; + self.with_mut(|g| g.locked_axes = v.0)?; + Ok(()) } /// Mask of axes that carry a :class:`JointLimits` configuration. #[getter] - fn limit_axes(&self) -> JointAxesMask { - JointAxesMask(self.with_ref(|g| g.limit_axes)) + fn limit_axes(&self) -> PyResult { + Ok(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)) + fn motor_axes(&self) -> PyResult { + Ok(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)) + fn coupled_axes(&self) -> PyResult { + Ok(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); + fn set_coupled_axes(&mut self, v: JointAxesMask) -> PyResult<()> { + 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)) + fn softness(&self) -> PyResult { + Ok(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); + fn set_softness(&mut self, v: SpringCoefficients) -> PyResult<()> { + self.with_mut(|g| g.softness = v.0) } /// Whether collision detection is enabled between the /// attached bodies. #[getter] - fn contacts_enabled(&self) -> bool { + fn contacts_enabled(&self) -> PyResult { self.with_ref(|g| g.contacts_enabled) } /// Enable or disable contacts between the attached bodies. #[setter] - fn set_contacts_enabled(&mut self, v: bool) { + fn set_contacts_enabled(&mut self, v: bool) -> PyResult<()> { 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)) + fn enabled(&self) -> PyResult { + Ok(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) { + fn set_enabled(&mut self, b: bool) -> PyResult<()> { self.with_mut(|g| { g.set_enabled(b); - }); + }) } /// Return ``True`` iff ``enabled == ENABLED``. - fn is_enabled(&self) -> bool { + fn is_enabled(&self) -> PyResult { self.with_ref(|g| g.is_enabled()) } /// Opaque user-data integer carried by the joint. #[getter] - fn user_data(&self) -> u128 { + fn user_data(&self) -> PyResult { 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); + fn set_user_data(&mut self, v: u128) -> PyResult<()> { + self.with_mut(|g| g.user_data = v) } /// Return the limits configured on ``axis``, if any. - fn limits(&self, axis: JointAxis) -> Option { + fn limits(&self, axis: JointAxis) -> PyResult> { 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) { + fn set_limits(&mut self, axis: JointAxis, min: Real, max: Real) -> PyResult<()> { 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 { + fn motor(&self, axis: JointAxis) -> PyResult> { self.with_ref(|g| g.motor(axis.to_rapier()).map(JointMotor::from_rapier)) } /// Fully configure the motor on ``axis``. @@ -2378,16 +2511,21 @@ impl GenericJoint { target_vel: Real, stiffness: Real, damping: Real, - ) { + ) -> PyResult<()> { 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) { + fn set_motor_velocity( + &mut self, + axis: JointAxis, + target_vel: Real, + factor: Real, + ) -> PyResult<()> { self.with_mut(|g| { g.set_motor_velocity(axis.to_rapier(), target_vel, factor); - }); + }) } /// Configure ``axis`` as a spring-damper motor toward /// ``target_pos``. @@ -2397,41 +2535,47 @@ impl GenericJoint { target_pos: Real, stiffness: Real, damping: Real, - ) { + ) -> PyResult<()> { 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) { + fn set_motor_max_force(&mut self, axis: JointAxis, max_force: Real) -> PyResult<()> { 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) { + fn set_motor_model(&mut self, axis: JointAxis, model: MotorModel) -> PyResult<()> { 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 { + fn motor_model(&self, axis: JointAxis) -> PyResult> { 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) { + /// + /// :raises ValueError: If this is the live data of a multibody joint and + /// the locked axes change (they define its degrees of freedom). + fn lock_axes(&mut self, axes: JointAxesMask) -> PyResult<()> { + let new_locked_axes = self.with_ref(|g| g.locked_axes)? | axes.0; + self.ensure_locked_axes_unchanged(new_locked_axes)?; self.with_mut(|g| { g.lock_axes(axes.0); - }); + })?; + Ok(()) } /// Return a developer-readable representation. - fn __repr__(&self) -> String { - format!( + fn __repr__(&self) -> PyResult { + Ok(format!( "GenericJoint(locked_axes={:#010b}, enabled={:?})", - self.with_ref(|g| g.locked_axes).bits(), - self.with_ref(|g| g.enabled) - ) + self.with_ref(|g| g.locked_axes)?.bits(), + self.with_ref(|g| g.enabled)? + )) } } @@ -2496,6 +2640,10 @@ impl GenericJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown GenericJointBuilder kwarg: '{}'", @@ -2677,29 +2825,32 @@ impl ImpulseJoint { backing: ImpulseJointBacking::Owned(Box::new(joint)), } } - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::ImpulseJoint) -> R) -> R { + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::ImpulseJoint) -> R) -> PyResult { match &self.backing { - ImpulseJointBacking::Owned(j) => f(j), + ImpulseJointBacking::Owned(j) => Ok(f(j)), ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { - let set = set.bind(py).borrow(); + let set = crate::errors::try_borrow(set.bind(py))?; let j = set .0 .get(*handle) - .expect("ImpulseJoint refers to a joint that was removed from its set"); - f(j) + .ok_or_else(|| crate::errors::stale_view("ImpulseJoint"))?; + Ok(f(j)) }), } } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::ImpulseJoint) -> R) -> R { + fn with_mut( + &mut self, + f: impl FnOnce(&mut rapier::dynamics::ImpulseJoint) -> R, + ) -> PyResult { match &mut self.backing { - ImpulseJointBacking::Owned(j) => f(j), + ImpulseJointBacking::Owned(j) => Ok(f(j)), ImpulseJointBacking::InSet { set, handle } => Python::with_gil(|py| { - let mut set = set.bind(py).borrow_mut(); + let mut set = crate::errors::try_borrow_mut(set.bind(py))?; let j = set .0 .get_mut(*handle, true) - .expect("ImpulseJoint refers to a joint that was removed from its set"); - f(j) + .ok_or_else(|| crate::errors::stale_view("ImpulseJoint"))?; + Ok(f(j)) }), } } @@ -2709,12 +2860,12 @@ impl ImpulseJoint { impl ImpulseJoint { /// Handle of the first attached body. #[getter] - fn body1(&self) -> RigidBodyHandle { + fn body1(&self) -> PyResult { self.with_ref(|j| RigidBodyHandle(j.body1())) } /// Handle of the second attached body. #[getter] - fn body2(&self) -> RigidBodyHandle { + fn body2(&self) -> PyResult { self.with_ref(|j| RigidBodyHandle(j.body2())) } /// Underlying :class:`GenericJoint` description (read+write). @@ -2737,23 +2888,24 @@ impl ImpulseJoint { } /// 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); + fn set_data(&mut self, data: GenericJoint) -> PyResult<()> { + let g = data.to_owned_generic()?; + self.with_mut(|j| j.data = g)?; + Ok(()) } /// 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 { + fn impulses(&self) -> PyResult> { self.with_ref(|j| { let v = j.impulses; v.to_vec() }) } /// Return a developer-readable representation. - fn __repr__(&self) -> String { + fn __repr__(&self) -> PyResult { self.with_ref(|j| { let (i1, g1) = j.body1().0.into_raw_parts(); let (i2, g2) = j.body2().0.into_raw_parts(); @@ -2778,7 +2930,7 @@ impl ImpulseJoint { /// /// Supports ``len()``, ``in``, ``set[handle]``, and iteration /// (yielding ``(ImpulseJointHandle, ImpulseJoint)`` pairs). -#[pyclass(name = "ImpulseJointSet", module = "rapier", unsendable)] +#[pyclass(name = "ImpulseJointSet", module = "rapier")] pub struct ImpulseJointSet(pub rapier::dynamics::ImpulseJointSet); #[pymethods] @@ -2812,7 +2964,7 @@ impl ImpulseJointSet { let obj = joint; let result: crate::pyo3::PyResult = (|| { if let Ok(j) = obj.extract::>() { - return Ok(j.to_owned_generic()); + return j.to_owned_generic(); } if let Ok(b) = obj.extract::>() { return Ok(b.0.build()); @@ -2911,14 +3063,16 @@ impl ImpulseJointSet { /// 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 { + fn get(slf: &Bound<'_, Self>, handle: &ImpulseJointHandle) -> PyResult> { + if crate::errors::try_borrow(slf)?.0.get(handle.0).is_none() { + return Ok(None); + } + Ok(Some(ImpulseJoint { backing: ImpulseJointBacking::InSet { set: slf.clone().unbind(), handle: handle.0, }, - }) + })) } /// Indexed access (``self[handle]``) — returns a live view. @@ -2926,7 +3080,7 @@ impl ImpulseJointSet { /// :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() { + if crate::errors::try_borrow(slf)?.0.get(handle.0).is_none() { return Err(crate::errors::InvalidHandle::new_err(format!( "no impulse joint for {:?}", handle.0.into_raw_parts() @@ -2957,8 +3111,11 @@ impl ImpulseJointSet { /// Iterate over ``(handle, joint)`` pairs. fn __iter__(slf: &Bound<'_, Self>) -> PyResult> { - let handles: Vec = - slf.borrow().0.iter().map(|(h, _)| h).collect(); + let handles: Vec = crate::errors::try_borrow(slf)? + .0 + .iter() + .map(|(h, _)| h) + .collect(); Py::new( slf.py(), ImpulseJointSetIter { @@ -3148,26 +3305,29 @@ impl Clone for Multibody { } impl Multibody { - fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::Multibody) -> R) -> R { + fn with_ref(&self, f: impl FnOnce(&rapier::dynamics::Multibody) -> R) -> PyResult { Python::with_gil(|py| { - let set = self.set.bind(py).borrow(); + let set = crate::errors::try_borrow(self.set.bind(py))?; 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) + .ok_or_else(|| crate::errors::stale_view("Multibody"))?; + Ok(f(mb)) }) } - fn with_mut(&mut self, f: impl FnOnce(&mut rapier::dynamics::Multibody) -> R) -> R { + fn with_mut( + &mut self, + f: impl FnOnce(&mut rapier::dynamics::Multibody) -> R, + ) -> PyResult { Python::with_gil(|py| { - let mut set = self.set.bind(py).borrow_mut(); + let mut set = crate::errors::try_borrow_mut(self.set.bind(py))?; 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) + .ok_or_else(|| crate::errors::stale_view("Multibody"))?; + Ok(f(mb)) }) } } @@ -3176,44 +3336,44 @@ impl Multibody { impl Multibody { /// Handle of the root link's rigid body. #[getter] - fn root_handle(&self) -> RigidBodyHandle { + fn root_handle(&self) -> PyResult { self.with_ref(|mb| RigidBodyHandle(mb.root().rigid_body_handle())) } /// Total number of links in the articulation. #[getter] - fn num_links(&self) -> usize { + fn num_links(&self) -> PyResult { self.with_ref(|mb| mb.num_links()) } /// Total number of degrees of freedom across all joints. #[getter] - fn ndofs(&self) -> usize { + fn ndofs(&self) -> PyResult { 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 { + fn get_link(&self, idx: usize) -> PyResult> { 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()); + 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 { + fn self_contacts_enabled(&self) -> PyResult { 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)); + fn set_self_contacts_enabled(&mut self, v: bool) -> PyResult<()> { + self.with_mut(|mb| mb.set_self_contacts_enabled(v)) } /// Per-degree-of-freedom joint damping coefficients /// (length :attr:`ndofs`). - fn damping(&self) -> Vec { + fn damping(&self) -> PyResult> { self.with_ref(|mb| mb.damping().iter().copied().collect()) } /// Set the per-DOF joint damping coefficients. @@ -3233,10 +3393,10 @@ impl Multibody { d[i] = *v; } Ok(()) - }) + })? } /// Generalized velocity vector (one entry per DOF). - fn generalized_velocity(&self) -> Vec { + fn generalized_velocity(&self) -> PyResult> { self.with_ref(|mb| mb.generalized_velocity().iter().copied().collect()) } /// Set the generalized velocity vector. @@ -3256,20 +3416,42 @@ impl Multibody { v[i] = *x; } Ok(()) - }) + })? + } + /// Add ``displacements`` (one entry per degree of freedom, e.g. the + /// result of :meth:`MultibodyJointSet.inverse_kinematics_for_link`) to + /// the generalized coordinates of the joints. + /// + /// The link poses and their rigid-bodies are updated by the next + /// simulation step, or right away by :meth:`forward_kinematics` + /// followed by :meth:`update_rigid_bodies`. + /// + /// :raises ValueError: If ``displacements`` length differs from `ndofs`. + fn apply_displacements(&mut self, displacements: Vec) -> PyResult<()> { + self.with_mut(|mb| { + if displacements.len() != mb.ndofs() { + return Err(PyValueError::new_err(format!( + "expected {} displacements (ndofs), got {}", + mb.ndofs(), + displacements.len() + ))); + } + mb.apply_displacements(&displacements); + Ok(()) + })? } /// Generalized acceleration vector from the last solver step. - fn generalized_acceleration(&self) -> Vec { + fn generalized_acceleration(&self) -> PyResult> { self.with_ref(|mb| mb.generalized_acceleration().iter().copied().collect()) } /// Velocity DOFs belonging to a single ``link``. - fn joint_velocity(&self, link: &MultibodyLink) -> Vec { + fn joint_velocity(&self, link: &MultibodyLink) -> PyResult> { 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> { + fn body_jacobian(&self, link_id: usize) -> PyResult>> { self.with_ref(|mb| { let j = mb.body_jacobian(link_id); (0..j.nrows()) @@ -3279,13 +3461,17 @@ impl Multibody { } /// Link indices on the path from the root to ``link_id`` /// (inclusive). - fn kinematic_branch(&self, link_id: usize) -> Vec { + fn kinematic_branch(&self, link_id: usize) -> PyResult> { 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)); + fn update_rigid_bodies( + &self, + bodies: &mut RigidBodySet, + update_mass_properties: bool, + ) -> PyResult<()> { + self.with_ref(|mb| mb.update_rigid_bodies(&mut bodies.0, update_mass_properties)) } /// Recompute link poses from the generalized coordinates /// (forward kinematics). @@ -3294,8 +3480,12 @@ impl Multibody { /// :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)); + fn forward_kinematics( + &mut self, + bodies: &RigidBodySet, + read_root_pose_from_rigid_body: bool, + ) -> PyResult<()> { + self.with_mut(|mb| mb.forward_kinematics(&bodies.0, read_root_pose_from_rigid_body)) } } @@ -3324,6 +3514,127 @@ impl MultibodyLinkIter { } } +// ================================================================= +// MultibodyJoint (view) +// ================================================================= +fn invalid_multibody_joint(handle: rapier::dynamics::MultibodyJointHandle) -> PyErr { + crate::errors::InvalidHandle::new_err(format!( + "no multibody joint for {:?}", + handle.0.into_raw_parts() + )) +} + +/// Live view of one joint stored in a :class:`MultibodyJointSet`, +/// returned by ``multibody_joints[handle]``. +/// +/// Modifying :attr:`data` modifies the joint stored in the set (and +/// wakes up its rigid-bodies at the next step), like for +/// :class:`ImpulseJoint`. The locked axes of a multibody joint define +/// its degrees of freedom so they can't be changed. +/// +/// :raises InvalidHandle: When accessed after the joint was removed. +#[pyclass(name = "MultibodyJoint", module = "rapier")] +pub struct MultibodyJoint { + set: Py, + handle: rapier::dynamics::MultibodyJointHandle, +} + +impl MultibodyJoint { + fn with_ref( + &self, + py: Python<'_>, + f: impl FnOnce(&rapier::dynamics::Multibody, &rapier::dynamics::MultibodyLink) -> R, + ) -> PyResult { + let set = crate::errors::try_borrow(self.set.bind(py))?; + let (mb, id) = set + .0 + .get(self.handle) + .ok_or_else(|| invalid_multibody_joint(self.handle))?; + let link = mb + .link(id) + .ok_or_else(|| invalid_multibody_joint(self.handle))?; + Ok(f(mb, link)) + } +} + +#[pymethods] +impl MultibodyJoint { + /// The :class:`GenericJoint` description of the joint, as a live view. + /// + /// Mutating it in place persists, e.g. + /// ``joint.data.set_motor_velocity(JointAxis.ANG_X, 1.0, 0.5)``. + /// Assigning a whole :class:`GenericJoint` (``joint.data = gj``) + /// also works. + /// + /// :raises ValueError: On assignment or mutation, if the locked axes + /// change. + #[getter] + fn data(&self, py: Python<'_>) -> PyResult { + self.with_ref(py, |_, _| ())?; + Ok(GenericJoint { + backing: GenericJointBacking::MultibodyJointData { + set: self.set.clone_ref(py), + handle: self.handle, + }, + }) + } + /// Replace the joint description (same locked axes required). + #[setter] + fn set_data(&mut self, py: Python<'_>, data: &GenericJoint) -> PyResult<()> { + let new_data = data.to_owned_generic()?; + let locked_axes = self.with_ref(py, |_, link| link.joint.data.locked_axes)?; + if new_data.locked_axes != locked_axes { + return Err(PyValueError::new_err( + "the locked axes of a multibody joint cannot change (they define its degrees of freedom)", + )); + } + crate::errors::try_borrow_mut(self.set.bind(py))? + .modify_joint(self.handle, |joint| joint.data = new_data) + .ok_or_else(|| invalid_multibody_joint(self.handle)) + } + /// Whether the joint is kinematic (inserted with + /// :meth:`MultibodyJointSet.insert_kinematic`): its velocity is never + /// changed by the physics engine. + #[getter] + fn kinematic(&self, py: Python<'_>) -> PyResult { + self.with_ref(py, |_, link| link.joint.kinematic) + } + /// Generalized coordinates of the joint: ``[lin_x, lin_y, lin_z, + /// ang_x, ang_y, ang_z]``, only meaningful on its free axes (e.g. the + /// angle of a revolute joint is ``coords[3]``). + #[getter] + fn coords(&self, py: Python<'_>) -> PyResult> { + self.with_ref(py, |_, link| link.joint.coords().to_vec()) + } + /// Index of the link the joint attaches to its parent, within its + /// :attr:`multibody`. + #[getter] + fn link_id(&self, py: Python<'_>) -> PyResult { + self.with_ref(py, |_, link| link.link_id()) + } + /// The :class:`Multibody` (live view) the joint belongs to. + #[getter] + fn multibody(&self, py: Python<'_>) -> PyResult { + self.with_ref(py, |_, _| ())?; + Ok(Multibody { + set: self.set.clone_ref(py), + key: MultibodyKey::Joint(self.handle), + }) + } + /// Return a developer-readable representation. + fn __repr__(&self, py: Python<'_>) -> String { + let (i, g) = self.handle.0.into_raw_parts(); + let handle = format!("MultibodyJointHandle(index={i}, generation={g})"); + match self.with_ref(py, |_, link| (link.link_id(), link.joint.kinematic)) { + Ok((link_id, kinematic)) => format!( + "MultibodyJoint(handle={handle}, link_id={link_id}, kinematic={})", + if kinematic { "True" } else { "False" } + ), + Err(_) => format!("MultibodyJoint(handle={handle}, removed)"), + } + } +} + // ================================================================= // MultibodyJointSet // ================================================================= @@ -3337,16 +3648,57 @@ impl MultibodyLinkIter { /// 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); +/// Supports ``len()``, ``set[handle]`` (a live :class:`MultibodyJoint`) +/// and iteration over joint handles. +#[pyclass(name = "MultibodyJointSet", module = "rapier")] +pub struct MultibodyJointSet( + pub rapier::dynamics::MultibodyJointSet, + /// Rigid-bodies attached to joints modified through a live view, woken + /// up by the next step (`MultibodyJointSet::get_mut` doesn't track them). + pub Vec, +); + +impl MultibodyJointSet { + pub(crate) fn wrap(set: rapier::dynamics::MultibodyJointSet) -> Self { + Self(set, Vec::new()) + } + + /// Mutate the joint `handle`, then queue its two rigid-bodies for waking up. + pub(crate) fn modify_joint( + &mut self, + handle: rapier::dynamics::MultibodyJointHandle, + f: impl FnOnce(&mut rapier::dynamics::MultibodyJoint) -> R, + ) -> Option { + let (mb, id) = self.0.get_mut(handle)?; + let parent = mb + .link(id)? + .parent_id() + .and_then(|p| mb.link(p)) + .map(|l| l.rigid_body_handle()); + let link = mb.link_mut(id)?; + let body = link.rigid_body_handle(); + let result = f(&mut link.joint); + self.1.push(body); + self.1.extend(parent); + Some(result) + } + + /// Wake up the rigid-bodies queued by `modify_joint`. + pub(crate) fn wake_up_modified_bodies(&mut self, bodies: &mut rapier::dynamics::RigidBodySet) { + for handle in self.1.drain(..) { + if let Some(body) = bodies.get_mut(handle) { + body.wake_up(true); + } + } + } +} #[pymethods] impl MultibodyJointSet { /// Construct an empty :class:`MultibodyJointSet`. #[new] fn new() -> Self { - Self(rapier::dynamics::MultibodyJointSet::new()) + Self::wrap(rapier::dynamics::MultibodyJointSet::new()) } /// Insert a dynamic joint between ``parent`` and ``link_body``. @@ -3373,7 +3725,7 @@ impl MultibodyJointSet { let obj = joint; let result: crate::pyo3::PyResult = (|| { if let Ok(j) = obj.extract::>() { - return Ok(j.to_owned_generic()); + return j.to_owned_generic(); } if let Ok(b) = obj.extract::>() { return Ok(b.0.build()); @@ -3455,7 +3807,7 @@ impl MultibodyJointSet { let obj = joint; let result: crate::pyo3::PyResult = (|| { if let Ok(j) = obj.extract::>() { - return Ok(j.to_owned_generic()); + return j.to_owned_generic(); } if let Ok(b) = obj.extract::>() { return Ok(b.0.build()); @@ -3525,24 +3877,38 @@ impl MultibodyJointSet { /// 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(( + fn get( + slf: &Bound<'_, Self>, + handle: &MultibodyJointHandle, + ) -> PyResult> { + let Some(id) = crate::errors::try_borrow(slf)? + .0 + .get(handle.0) + .map(|(_, id)| id) + else { + return Ok(None); + }; + Ok(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 { + fn multibody( + slf: &Bound<'_, Self>, + handle: &MultibodyJointHandle, + ) -> PyResult> { + if crate::errors::try_borrow(slf)?.0.get(handle.0).is_none() { + return Ok(None); + } + Ok(Some(Multibody { set: slf.clone().unbind(), key: MultibodyKey::Joint(handle.0), - }) + })) } /// Return the :class:`MultibodyLinkId` of ``body`` if it @@ -3552,12 +3918,18 @@ impl MultibodyJointSet { } /// 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 { + fn get_multibody(slf: &Bound<'_, Self>, index: &MultibodyIndex) -> PyResult> { + if crate::errors::try_borrow(slf)? + .0 + .get_multibody(index.0) + .is_none() + { + return Ok(None); + } + Ok(Some(Multibody { set: slf.clone().unbind(), key: MultibodyKey::Index(index.0), - }) + })) } /// Find the multibody joint connecting two bodies, if they are @@ -3585,9 +3957,27 @@ impl MultibodyJointSet { )) } - /// Number of articulations currently stored. + /// Number of joints currently stored (the number of handles yielded + /// by iteration). fn __len__(&self) -> usize { - self.0.multibodies().count() + self.0.iter().count() + } + + /// Indexed access (``self[handle]``): a live view of the joint. + /// + /// :raises InvalidHandle: If ``handle`` does not point to a + /// joint in this set. + fn __getitem__( + slf: &Bound<'_, Self>, + handle: &MultibodyJointHandle, + ) -> PyResult { + if crate::errors::try_borrow(slf)?.0.get(handle.0).is_none() { + return Err(invalid_multibody_joint(handle.0)); + } + Ok(MultibodyJoint { + set: slf.clone().unbind(), + handle: handle.0, + }) } /// Iterate over every joint handle in the set. @@ -3630,28 +4020,33 @@ impl MultibodyJointSet { /// :param target_pose: Desired world-space pose for the link. /// :param option: Solver tuning; defaults to a sensible /// starting point if omitted. + /// :param joint_can_move: Optional callable + /// ``joint_can_move(link: MultibodyLink) -> bool`` called once for + /// each link from the root to the target link; the joints of the + /// links it returns ``False`` for are left unchanged. By default + /// every joint can move. /// :returns: Flat list of length :py:attr:`Multibody.ndofs` /// giving the joint-coordinate displacement that achieves - /// the IK target. + /// the IK target (apply it with + /// :meth:`Multibody.apply_displacements`). /// :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))] + #[pyo3(signature = (bodies, handle, target_pose, option=None, joint_can_move=None))] fn inverse_kinematics_for_link( &self, bodies: &RigidBodySet, handle: &MultibodyJointHandle, target_pose: PyIsometry, option: Option<&InverseKinematicsOption>, + joint_can_move: Option<&Bound<'_, PyAny>>, ) -> 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 (mb, link_id) = self + .0 + .get(handle.0) + .ok_or_else(|| invalid_multibody_joint(handle.0))?; let opts = option.copied().unwrap_or_else(|| InverseKinematicsOption { damping: 1.0, max_iters: 10, @@ -3662,14 +4057,37 @@ impl MultibodyJointSet { let rapier_opts = opts.to_rapier(); let target: rapier::math::Pose = target_pose.0.into(); let mut displacements = crate::na::DVector::::zeros(mb.ndofs()); + // The first error raised by `joint_can_move` is re-raised once the solver returns. + let callback_err: std::cell::RefCell> = std::cell::RefCell::new(None); + let can_move = |link: &rapier::dynamics::MultibodyLink| -> bool { + let Some(callback) = joint_can_move else { + return true; + }; + if callback_err.borrow().is_some() { + return false; + } + match callback + .call1((MultibodyLink(*link),)) + .and_then(|r| r.is_truthy()) + { + Ok(can_move) => can_move, + Err(e) => { + *callback_err.borrow_mut() = Some(e); + false + } + } + }; mb.inverse_kinematics( &bodies.0, link_id, &rapier_opts, &target, - |_| true, + can_move, &mut displacements, ); + if let Some(e) = callback_err.into_inner() { + return Err(e); + } Ok(displacements.iter().copied().collect()) } } @@ -3768,7 +4186,7 @@ impl SphericalJoint { /// :param kwargs: Optional keyword args forwarded to the /// builder; accepted keys are ``local_anchor1``, /// ``local_anchor2``, ``local_frame1``, ``local_frame2``, - /// ``contacts_enabled``. + /// ``contacts_enabled``, ``softness``. #[staticmethod] #[pyo3(signature = (body_a=None, body_b=None, **kwargs))] fn builder( @@ -3968,6 +4386,10 @@ impl SphericalJointBuilder { let b: bool = v.extract()?; me.0 = me.0.contacts_enabled(b); } + "softness" => { + let c: SpringCoefficients = v.extract()?; + me.0 = me.0.softness(c.0); + } _ => { return Err(PyTypeError::new_err(format!( "unknown SphericalJointBuilder kwarg: '{}'", @@ -4168,6 +4590,7 @@ pub fn register_joints( m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/python/rapier-py-3d/src/lib.rs b/python/rapier-py-3d/src/lib.rs index e24b73b44..be5223a79 100644 --- a/python/rapier-py-3d/src/lib.rs +++ b/python/rapier-py-3d/src/lib.rs @@ -36,6 +36,7 @@ pub use numpy; pub use pyo3; pub use serde_json; +pub mod build_info; pub mod conv; pub mod errors; pub mod serde_io; @@ -82,6 +83,7 @@ fn _rapier3d(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { register_controllers(py, m)?; register_debug_render(py, m)?; register_soft_bodies(py, m)?; + build_info::register_build_info(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 index d045314f9..02c5ac2c8 100644 --- a/python/rapier-py-3d/src/loaders.rs +++ b/python/rapier-py-3d/src/loaders.rs @@ -100,13 +100,13 @@ fn _loaded_shape_from_meshloader(loaded: rapier3d_meshloader::LoadedShape) -> Lo /// 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``. +/// The file (STL, COLLADA ``.dae`` or Wavefront ``.obj``, picked from +/// its extension) is parsed into one or more meshes; each mesh 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`). +/// :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`` @@ -149,7 +149,8 @@ fn loaders_mesh_load_from_path( /// /// :param vertices: Sequence of 3D vertex positions. /// :param indices: Sequence of triangle indices ``(i, j, k)``. -/// :param converter: :class:`MeshConverter` (defaults to TriMesh). +/// :param converter: :class:`MeshConverter` (defaults to +/// :attr:`MeshConverter.TRIMESH`). /// :param scale: Uniform scale applied during conversion. /// :returns: A :class:`LoadedShape`. /// :raises MeshConversionError: if the conversion failed. @@ -235,6 +236,10 @@ impl UrdfMultibodyOptions { fn __and__(&self, other: &UrdfMultibodyOptions) -> Self { Self(self.0 & other.0) } + /// ``other in self``: ``True`` if every flag of ``other`` is set here. + fn __contains__(&self, other: &UrdfMultibodyOptions) -> bool { + self.0.contains(other.0) + } /// Return ``UrdfMultibodyOptions(bits=0b...)`` repr. fn __repr__(&self) -> String { format!("UrdfMultibodyOptions(bits={:#06b})", self.0.bits()) @@ -263,7 +268,7 @@ impl UrdfMultibodyOptions { /// :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 +/// 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 @@ -547,7 +552,7 @@ impl UrdfRobotSource { /// /// Populated by the loader only when a non-default /// :attr:`UrdfLoaderOptions.mesh_converter` (e.g. -/// ``MeshConverter.Obb()``) replaced the source mesh with a cheap +/// ``MeshConverter.OBB``) replaced the source mesh with a cheap /// proxy collider — this keeps the original high-resolution mesh /// available for rendering. /// @@ -586,6 +591,18 @@ impl UrdfColliderHandle { local_pose: Isometry3(v.local_pose.into()), }) } + /// Return the ``UrdfColliderHandle(...)`` repr. + fn __repr__(&self) -> String { + let (i, g) = self.handle.0.into_raw_parts(); + format!( + "UrdfColliderHandle(handle=ColliderHandle(index={i}, generation={g}), has_visual={})", + if self.visual.is_some() { + "True" + } else { + "False" + } + ) + } } /// Handle of one URDF link (its rigid body plus its colliders). @@ -601,6 +618,18 @@ pub struct UrdfLinkHandle { pub colliders: Vec, } +#[pymethods] +impl UrdfLinkHandle { + /// Return the ``UrdfLinkHandle(...)`` repr. + fn __repr__(&self) -> String { + let (i, g) = self.body.0.into_raw_parts(); + format!( + "UrdfLinkHandle(body=RigidBodyHandle(index={i}, generation={g}), n_colliders={})", + self.colliders.len() + ) + } +} + /// Handle of one URDF joint after insertion. /// /// :ivar joint: Either an :class:`ImpulseJointHandle` (when the @@ -631,6 +660,18 @@ pub struct UrdfRobotHandles { pub joints: Vec>, } +#[pymethods] +impl UrdfRobotHandles { + /// Return the ``UrdfRobotHandles(...)`` repr. + fn __repr__(&self) -> String { + format!( + "UrdfRobotHandles(n_links={}, n_joints={})", + self.links.len(), + self.joints.len() + ) + } +} + // ----- UrdfRobot --------------------------------------------------- /// A URDF robot, ready to be inserted into the simulation. @@ -999,6 +1040,11 @@ impl MjcfMultibodyOptions { #[classattr] const SKIP_JOINT_LIMITS: MjcfMultibodyOptions = MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_LIMITS); + /// Don't install the ```` passive springs on the + /// multibody joints. + #[classattr] + const SKIP_JOINT_SPRINGS: MjcfMultibodyOptions = + MjcfMultibodyOptions(rapier3d_mjcf::MjcfMultibodyOptions::SKIP_JOINT_SPRINGS); /// Raw bits as an unsigned int. #[getter] fn bits(&self) -> u8 { @@ -1012,6 +1058,10 @@ impl MjcfMultibodyOptions { fn __and__(&self, other: &MjcfMultibodyOptions) -> Self { Self(self.0 & other.0) } + /// ``other in self``: ``True`` if every flag of ``other`` is set here. + fn __contains__(&self, other: &MjcfMultibodyOptions) -> bool { + self.0.contains(other.0) + } /// Return ``MjcfMultibodyOptions(bits=0b...)`` repr. fn __repr__(&self) -> String { format!("MjcfMultibodyOptions(bits={:#08b})", self.0.bits()) @@ -1020,8 +1070,15 @@ impl MjcfMultibodyOptions { /// How MJCF ``contype`` / ``conaffinity`` masks map onto rapier /// :class:`InteractionGroups`. -#[pyclass(name = "ContactFilterMode", module = "rapier", eq, eq_int)] -#[derive(Clone, Copy, PartialEq, Eq)] +#[pyclass( + name = "ContactFilterMode", + module = "rapier", + eq, + eq_int, + hash, + frozen +)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum ContactFilterMode { /// ``memberships = filter = contype | conaffinity`` (default). Symmetric, @@ -1215,7 +1272,10 @@ impl MjcfModel { } /// Return the ``MjcfModel(...)`` repr. fn __repr__(&self) -> String { - format!("MjcfModel(name={:?})", self.raw.name) + match &self.raw.name { + Some(name) => format!("MjcfModel(name={name:?})"), + None => "MjcfModel(name=None)".to_string(), + } } } @@ -1258,14 +1318,64 @@ pub struct MjcfJointHandle { pub link2: RigidBodyHandle, } +/// Handle of one MJCF ```` after insertion. +/// +/// :ivar name: The actuator's ``name`` attribute, if any. +/// :ivar joint: The joint the actuator drives: an +/// :class:`ImpulseJointHandle` (impulse-joint path), a +/// :class:`MultibodyJointHandle` (multibody path), or ``None`` if it +/// drives no joint (e.g. a tendon) or its joint was dropped as a loop +/// closure. +#[pyclass(name = "MjcfActuatorHandle", module = "rapier")] +pub struct MjcfActuatorHandle { + #[pyo3(get)] + pub name: Option, + #[pyo3(get)] + pub joint: crate::pyo3::PyObject, +} + +#[pymethods] +impl MjcfActuatorHandle { + /// Return the ``MjcfActuatorHandle(...)`` repr. + fn __repr__(&self, py: crate::pyo3::Python<'_>) -> crate::pyo3::PyResult { + let name = match &self.name { + Some(name) => format!("{name:?}"), + None => "None".to_string(), + }; + let joint = self.joint.bind(py).repr()?; + Ok(format!("MjcfActuatorHandle(name={name}, joint={joint})")) + } +} + +/// A keyframe of an MJCF model, by index (negative indices count from the end) or by name. +#[derive(crate::pyo3::FromPyObject)] +pub enum MjcfKeyframeRef { + Index(isize), + Name(String), +} + +/// The handles returned by the insertion, with the joint type of the path used. +enum MjcfInsertedHandles { + Impulse(rapier3d_mjcf::MjcfRobotHandles), + Multibody(rapier3d_mjcf::MjcfRobotHandles>), +} + /// Aggregate handle set returned by MJCF insertion functions. /// +/// Besides the handles, it keeps the model data needed by the runtime +/// helpers: :meth:`apply_controls` (actuators), :meth:`apply_keyframe` / +/// :meth:`keyframe_controls` (````) and :meth:`contact_hooks` +/// (````). +/// /// :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). +/// :ivar actuators: One :class:`MjcfActuatorHandle` per ````, +/// in model order (the order of the ``ctrl`` values of +/// :meth:`apply_controls`). #[pyclass(name = "MjcfRobotHandles", module = "rapier")] pub struct MjcfRobotHandles { #[pyo3(get)] @@ -1274,6 +1384,305 @@ pub struct MjcfRobotHandles { pub joints: Vec>, #[pyo3(get)] pub equality_joints: Vec>, + #[pyo3(get)] + pub actuators: Vec>, + /// The inserted robot, without its colliders and visual meshes (only its + /// metadata is needed by the runtime helpers). + robot: Box, + inserted: MjcfInsertedHandles, +} + +impl MjcfRobotHandles { + /// Keep what the runtime helpers need from `robot` before it is consumed by an insertion. + fn robot_metadata(robot: &rapier3d_mjcf::MjcfRobot) -> Box { + let mut robot = robot.clone(); + for body in &mut robot.bodies { + body.colliders.clear(); + body.visual_meshes.clear(); + } + Box::new(robot) + } + + fn keyframe( + &self, + keyframe: &MjcfKeyframeRef, + ) -> crate::pyo3::PyResult<&rapier3d_mjcf::mjcf_rs::extras::Keyframe> { + let keyframes = &self.robot.keyframes; + let index = match keyframe { + MjcfKeyframeRef::Name(name) => { + return self.robot.keyframe_by_name(name).ok_or_else(|| { + crate::pyo3::exceptions::PyKeyError::new_err(format!( + "the MJCF model has no keyframe named {name:?}" + )) + }); + } + MjcfKeyframeRef::Index(index) => *index, + }; + let resolved = if index < 0 { + index + keyframes.len() as isize + } else { + index + }; + usize::try_from(resolved) + .ok() + .and_then(|i| keyframes.get(i)) + .ok_or_else(|| { + crate::pyo3::exceptions::PyIndexError::new_err(format!( + "keyframe index {index} out of range (the MJCF model has {} keyframes)", + keyframes.len() + )) + }) + } +} + +#[pymethods] +impl MjcfRobotHandles { + /// The ``name`` of each ```` of the model (``None`` for + /// an unnamed key), in model order. + #[getter] + fn keyframe_names(&self) -> Vec> { + self.robot + .keyframes + .iter() + .map(|k| k.name.clone()) + .collect() + } + + /// Drive the actuators of the model: one control value per actuator. + /// + /// Follows MuJoCo's semantics for each actuator kind: ```` + /// applies the force ``ctrl * gear``, ```` and + /// ```` configure a joint motor toward the ``ctrl`` + /// position or velocity (with the ``kp``/``kv`` gains and + /// ``forcerange``), ```` damps the joint, and an affine + /// ```` becomes a position servo. Other actuators are + /// ignored. Call it before each step whose controls changed. + /// + /// :param bodies: :class:`RigidBodySet` of the robot (its driven + /// bodies are woken up). + /// :param joints: The joint set the robot was inserted with: the + /// :class:`ImpulseJointSet` (``insert_using_impulse_joints``) or + /// the :class:`MultibodyJointSet` (``insert_using_multibody_joints``). + /// :param ctrl: One control value per actuator, in the order of + /// :attr:`actuators`. + /// :param gain_scale: Uniform scale of the actuators' strength + /// (gains and force limits). Default ``1.0``. + /// :raises ValueError: If ``len(ctrl) != len(actuators)``. + /// :raises TypeError: If ``joints`` isn't the kind of joint set the + /// robot was inserted with. + #[pyo3(signature = (bodies, joints, ctrl, gain_scale=1.0))] + fn apply_controls( + &self, + bodies: &mut RigidBodySet, + joints: &crate::pyo3::Bound<'_, crate::pyo3::PyAny>, + ctrl: Vec, + gain_scale: Real, + ) -> crate::pyo3::PyResult<()> { + if ctrl.len() != self.actuators.len() { + return Err(crate::pyo3::exceptions::PyValueError::new_err(format!( + "expected {} control values (one per actuator), got {}", + self.actuators.len(), + ctrl.len() + ))); + } + match &self.inserted { + MjcfInsertedHandles::Impulse(handles) => { + let mut joints = joints + .extract::>() + .map_err(|_| { + crate::pyo3::exceptions::PyTypeError::new_err( + "the robot was inserted with impulse joints: pass its ImpulseJointSet", + ) + })?; + handles.apply_controls_scaled(&mut joints.0, &ctrl, gain_scale); + } + MjcfInsertedHandles::Multibody(handles) => { + let mut joints = joints + .extract::>() + .map_err(|_| { + crate::pyo3::exceptions::PyTypeError::new_err( + "the robot was inserted with multibody joints: pass its MultibodyJointSet", + ) + })?; + handles.apply_controls_multibody_scaled( + &mut bodies.0, + &mut joints.0, + &ctrl, + gain_scale, + ); + } + } + Ok(()) + } + + /// Reset the robot to one of the model's keyframes. + /// + /// Applies the keyframe's joint positions (``qpos``) and velocities + /// (``qvel``), and the poses of its mocap bodies. With impulse joints + /// the body poses are set by forward kinematics and only the velocity + /// of a floating base is applied. The actuators are left unchanged: + /// drive them with :meth:`keyframe_controls` to hold the pose. + /// + /// :param bodies: :class:`RigidBodySet` of the robot. + /// :param multibody_joints: The :class:`MultibodyJointSet` the robot + /// was inserted with (required for a robot inserted with + /// ``insert_using_multibody_joints``, ignored otherwise). + /// :param keyframe: The keyframe's index (negative indices count from + /// the end) or name. Default ``0``. + /// :raises IndexError: If the index is out of range. + /// :raises KeyError: If no keyframe has this name. + /// :raises TypeError: If ``multibody_joints`` is missing for a robot + /// inserted with multibody joints. + #[pyo3(signature = (bodies, multibody_joints=None, keyframe=MjcfKeyframeRef::Index(0)))] + fn apply_keyframe( + &self, + bodies: &mut RigidBodySet, + multibody_joints: Option<&mut MultibodyJointSet>, + keyframe: MjcfKeyframeRef, + ) -> crate::pyo3::PyResult<()> { + let key = self.keyframe(&keyframe)?; + match &self.inserted { + MjcfInsertedHandles::Impulse(handles) => { + handles.apply_keyframe(&mut bodies.0, &self.robot, key); + } + MjcfInsertedHandles::Multibody(handles) => { + let multibody_joints = multibody_joints.ok_or_else(|| { + crate::pyo3::exceptions::PyTypeError::new_err( + "the robot was inserted with multibody joints: pass its MultibodyJointSet", + ) + })?; + handles.apply_keyframe(&mut bodies.0, &mut multibody_joints.0, &self.robot, key); + } + } + Ok(()) + } + + /// The control values holding one of the model's keyframes: one per + /// actuator, to pass to :meth:`apply_controls`. + /// + /// Each value is the keyframe's ``ctrl`` entry if it has one, + /// otherwise the keyframe position of the hinge or slide joint the + /// actuator drives, otherwise ``0``. + /// + /// :param keyframe: The keyframe's index (negative indices count from + /// the end) or name. + /// :raises IndexError: If the index is out of range. + /// :raises KeyError: If no keyframe has this name. + fn keyframe_controls(&self, keyframe: MjcfKeyframeRef) -> crate::pyo3::PyResult> { + Ok(self.robot.keyframe_controls(self.keyframe(&keyframe)?)) + } + + /// The physics hooks applying the model's ```` rules: + /// ```` (no contacts between two bodies) and ```` + /// (friction of a pair of geoms). + /// + /// Assign the result to :attr:`PhysicsWorld.physics_hooks` (or pass + /// it to :meth:`PhysicsPipeline.step`). + fn contact_hooks(&self) -> MjcfContactHooks { + let hooks = match &self.inserted { + MjcfInsertedHandles::Impulse(handles) => handles.contact_hooks(&self.robot), + MjcfInsertedHandles::Multibody(handles) => handles.contact_hooks(&self.robot), + }; + MjcfContactHooks(std::sync::Arc::new(hooks)) + } + + /// Return the ``MjcfRobotHandles(...)`` repr. + fn __repr__(&self) -> String { + format!( + "MjcfRobotHandles(n_bodies={}, n_joints={}, n_equality_joints={}, n_actuators={}, n_keyframes={})", + self.bodies.len(), + self.joints.len(), + self.equality_joints.len(), + self.actuators.len(), + self.robot.keyframes.len(), + ) + } +} + +/// Physics hooks applying the ```` rules of an MJCF model: +/// ```` removes the contacts between the colliders of two +/// bodies and ```` overrides the friction of a pair of geoms. +/// +/// Returned by :meth:`MjcfRobotHandles.contact_hooks`. Assign it to +/// :attr:`PhysicsWorld.physics_hooks`: the rules are then applied +/// natively, without calling back into Python. The colliders created by +/// the loader already enable the hooks +/// (``ActiveHooks.FILTER_CONTACT_PAIRS | ActiveHooks.MODIFY_SOLVER_CONTACTS``). +/// Its methods can also be called by your own hooks object, to combine +/// these rules with others. +#[pyclass(name = "MjcfContactHooks", module = "rapier", frozen)] +#[derive(Clone)] +pub struct MjcfContactHooks(pub std::sync::Arc); + +/// Native `PhysicsHooks` adapter sharing the hooks of a `MjcfContactHooks`. +pub struct SharedMjcfContactHooks(std::sync::Arc); + +impl rapier::pipeline::PhysicsHooks for SharedMjcfContactHooks { + fn filter_contact_pair( + &self, + context: &rapier::pipeline::PairFilterContext, + ) -> Option { + self.0.filter_contact_pair(context) + } + + fn filter_intersection_pair(&self, context: &rapier::pipeline::PairFilterContext) -> bool { + self.0.filter_intersection_pair(context) + } + + fn modify_solver_contacts(&self, context: &mut rapier::pipeline::ContactModificationContext) { + self.0.modify_solver_contacts(context) + } +} + +impl MjcfContactHooks { + pub(crate) fn native(&self) -> SharedMjcfContactHooks { + SharedMjcfContactHooks(self.0.clone()) + } +} + +#[pymethods] +impl MjcfContactHooks { + /// ``None`` (no contacts) if the pair of colliders is excluded by an + /// ```` rule, ``SolverFlags.COMPUTE_RIGID_IMPULSES`` otherwise. + fn filter_contact_pair(&self, ctx: &PairFilterContext) -> Option { + let (collider1, collider2) = ctx.collider_handles(); + if self.0.is_excluded(collider1, collider2) { + None + } else { + Some(SolverFlags( + rapier::geometry::SolverFlags::COMPUTE_RIGID_IMPULSES, + )) + } + } + + /// Apply the friction of the ```` rule matching the pair of + /// colliders, if any. + /// + /// :raises RuntimeError: If ``ctx`` is used outside of its callback. + fn modify_solver_contacts( + &self, + mut ctx: crate::pyo3::PyRefMut<'_, ContactModificationContext>, + ) -> crate::pyo3::PyResult<()> { + let (collider1, collider2) = ctx.collider_handles(); + if let Some(friction) = self + .0 + .pair_override(collider1, collider2) + .and_then(|o| o.friction) + { + ctx.set_friction(friction)?; + } + Ok(()) + } + + /// Return the ``MjcfContactHooks(...)`` repr. + fn __repr__(&self) -> String { + let flag = |b: bool| if b { "True" } else { "False" }; + format!( + "MjcfContactHooks(has_excludes={}, has_overrides={})", + flag(self.0.has_excludes()), + flag(self.0.has_overrides()) + ) + } } /// A MuJoCo MJCF model, ready to be inserted into the simulation. @@ -1352,6 +1761,37 @@ impl MjcfRobot { Ok(()) } + /// The gravity declared by the model (``