From 83aee02540ceeb140f78cbacbf8c41e7344ea9a7 Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Wed, 3 Dec 2025 16:24:08 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E4=BE=9D=E8=B5=96=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- thirdparty/rapier.js/.github/FUNDING.yml | 12 + .../rapier.js/.github/workflows/main.yml | 154 ++ thirdparty/rapier.js/.gitignore | 21 + thirdparty/rapier.js/.prettierignore | 7 + thirdparty/rapier.js/.prettierrc | 5 + thirdparty/rapier.js/CHANGELOG.md | 937 ++++++++ thirdparty/rapier.js/Cargo.lock | 1711 ++++++++++++++ thirdparty/rapier.js/Cargo.toml | 23 + thirdparty/rapier.js/LICENSE | 201 ++ thirdparty/rapier.js/README.md | 82 + .../builds/prepare_builds/Cargo.toml | 9 + .../rapier.js/builds/prepare_builds/README.md | 17 + .../prepare_builds/build_all_projects.sh | 19 + .../prepare_builds/prepare_all_projects.sh | 11 + .../builds/prepare_builds/src/main.rs | 182 ++ .../prepare_builds/templates/Cargo.toml.tera | 60 + .../builds/prepare_builds/templates/LICENSE | 201 ++ .../prepare_builds/templates/README.md.tera | 70 + .../templates/build_rust.sh.tera | 15 + .../templates/build_typescript.sh.tera | 13 + .../templates/package.json.tera | 25 + .../prepare_builds/templates/tsconfig.json | 13 + .../templates/tsconfig_typedoc.json | 10 + .../templates/typedoc.json.tera | 42 + thirdparty/rapier.js/package.json | 12 + thirdparty/rapier.js/publish_all_canary.sh | 19 + thirdparty/rapier.js/publish_all_prod.sh | 19 + thirdparty/rapier.js/rapier-compat/README.md | 16 + .../rapier.js/rapier-compat/build-rust.sh | 50 + .../rapier.js/rapier-compat/fix_raw_file.sh | 11 + thirdparty/rapier.js/rapier-compat/gen_src.sh | 58 + .../rapier.js/rapier-compat/package.json | 59 + .../rapier.js/rapier-compat/rollup.config.js | 85 + .../rapier.js/rapier-compat/src2d/init.ts | 60 + .../rapier.js/rapier-compat/src2d/raw.ts | 1 + .../rapier.js/rapier-compat/src3d/init.ts | 12 + .../rapier.js/rapier-compat/src3d/raw.ts | 1 + .../rapier-compat/tests/World2d.test.ts | 23 + .../rapier-compat/tests/World3d.test.ts | 23 + .../rapier-compat/tests/math2d.test.ts | 15 + .../rapier-compat/tests/math3d.test.ts | 17 + .../rapier-compat/tsconfig.common.json | 15 + .../rapier.js/rapier-compat/tsconfig.json | 10 + .../rapier-compat/tsconfig.pkg2d.json | 8 + .../rapier-compat/tsconfig.pkg3d.json | 8 + thirdparty/rapier.js/src.ts/coarena.ts | 70 + .../src.ts/control/character_controller.ts | 386 ++++ thirdparty/rapier.js/src.ts/control/index.ts | 6 + .../src.ts/control/pid_controller.ts | 207 ++ .../control/ray_cast_vehicle_controller.ts | 481 ++++ .../rapier.js/src.ts/dynamics/ccd_solver.ts | 25 + .../dynamics/coefficient_combine_rule.ts | 13 + .../src.ts/dynamics/impulse_joint.ts | 681 ++++++ .../src.ts/dynamics/impulse_joint_set.ts | 165 ++ thirdparty/rapier.js/src.ts/dynamics/index.ts | 10 + .../src.ts/dynamics/integration_parameters.ts | 126 ++ .../src.ts/dynamics/island_manager.ts | 37 + .../src.ts/dynamics/multibody_joint.ts | 222 ++ .../src.ts/dynamics/multibody_joint_set.ts | 157 ++ .../rapier.js/src.ts/dynamics/rigid_body.ts | 1691 ++++++++++++++ .../src.ts/dynamics/rigid_body_set.ts | 238 ++ thirdparty/rapier.js/src.ts/exports.ts | 27 + .../rapier.js/src.ts/geometry/broad_phase.ts | 520 +++++ .../rapier.js/src.ts/geometry/collider.ts | 1983 +++++++++++++++++ .../rapier.js/src.ts/geometry/collider_set.ts | 213 ++ .../rapier.js/src.ts/geometry/contact.ts | 62 + .../rapier.js/src.ts/geometry/feature.ts | 16 + thirdparty/rapier.js/src.ts/geometry/index.ts | 11 + .../src.ts/geometry/interaction_groups.ts | 18 + .../rapier.js/src.ts/geometry/narrow_phase.ts | 203 ++ thirdparty/rapier.js/src.ts/geometry/point.ts | 98 + thirdparty/rapier.js/src.ts/geometry/ray.ts | 192 ++ thirdparty/rapier.js/src.ts/geometry/shape.ts | 1558 +++++++++++++ thirdparty/rapier.js/src.ts/geometry/toi.ts | 105 + thirdparty/rapier.js/src.ts/init.ts | 2 + thirdparty/rapier.js/src.ts/math.ts | 263 +++ .../src.ts/pipeline/debug_render_pipeline.ts | 85 + .../rapier.js/src.ts/pipeline/event_queue.ts | 158 ++ thirdparty/rapier.js/src.ts/pipeline/index.ts | 7 + .../src.ts/pipeline/physics_hooks.ts | 54 + .../src.ts/pipeline/physics_pipeline.ts | 85 + .../src.ts/pipeline/query_pipeline.ts | 57 + .../src.ts/pipeline/serialization_pipeline.ts | 84 + thirdparty/rapier.js/src.ts/pipeline/world.ts | 1312 +++++++++++ thirdparty/rapier.js/src.ts/rapier.ts | 3 + thirdparty/rapier.js/src.ts/raw.ts | 1 + .../src/control/character_controller.rs | 290 +++ thirdparty/rapier.js/src/control/mod.rs | 11 + .../rapier.js/src/control/pid_controller.rs | 264 +++ .../control/ray_cast_vehicle_controller.rs | 336 +++ .../rapier.js/src/dynamics/ccd_solver.rs | 13 + .../rapier.js/src/dynamics/impulse_joint.rs | 215 ++ .../src/dynamics/impulse_joint_set.rs | 92 + .../src/dynamics/integration_parameters.rs | 101 + .../rapier.js/src/dynamics/island_manager.rs | 31 + thirdparty/rapier.js/src/dynamics/joint.rs | 314 +++ thirdparty/rapier.js/src/dynamics/mod.rs | 20 + .../rapier.js/src/dynamics/multibody_joint.rs | 196 ++ .../src/dynamics/multibody_joint_set.rs | 85 + .../rapier.js/src/dynamics/rigid_body.rs | 753 +++++++ .../rapier.js/src/dynamics/rigid_body_set.rs | 237 ++ .../rapier.js/src/geometry/broad_phase.rs | 462 ++++ thirdparty/rapier.js/src/geometry/collider.rs | 993 +++++++++ .../rapier.js/src/geometry/collider_set.rs | 293 +++ thirdparty/rapier.js/src/geometry/contact.rs | 31 + thirdparty/rapier.js/src/geometry/feature.rs | 47 + thirdparty/rapier.js/src/geometry/mod.rs | 49 + .../rapier.js/src/geometry/narrow_phase.rs | 217 ++ thirdparty/rapier.js/src/geometry/point.rs | 53 + thirdparty/rapier.js/src/geometry/ray.rs | 74 + thirdparty/rapier.js/src/geometry/shape.rs | 527 +++++ thirdparty/rapier.js/src/geometry/toi.rs | 65 + thirdparty/rapier.js/src/lib.rs | 32 + thirdparty/rapier.js/src/math.rs | 266 +++ .../src/pipeline/debug_render_pipeline.rs | 152 ++ .../rapier.js/src/pipeline/event_queue.rs | 140 ++ thirdparty/rapier.js/src/pipeline/mod.rs | 11 + .../rapier.js/src/pipeline/physics_hooks.rs | 217 ++ .../src/pipeline/physics_pipeline.rs | 170 ++ .../src/pipeline/serialization_pipeline.rs | 145 ++ thirdparty/rapier.js/src/utils.rs | 79 + thirdparty/rapier.js/testbed2d/.npmignore | 3 + thirdparty/rapier.js/testbed2d/package.json | 32 + thirdparty/rapier.js/testbed2d/publish.sh | 5 + .../rapier.js/testbed2d/src/Graphics.ts | 269 +++ thirdparty/rapier.js/testbed2d/src/Gui.ts | 113 + thirdparty/rapier.js/testbed2d/src/Testbed.ts | 189 ++ .../src/demos/characterController.ts | 89 + .../testbed2d/src/demos/collisionGroups.ts | 79 + .../testbed2d/src/demos/convexPolygons.ts | 68 + .../rapier.js/testbed2d/src/demos/cubes.ts | 56 + .../testbed2d/src/demos/heightfield.ts | 65 + .../rapier.js/testbed2d/src/demos/keva.ts | 92 + .../testbed2d/src/demos/lockedRotations.ts | 51 + .../testbed2d/src/demos/pidController.ts | 100 + .../rapier.js/testbed2d/src/demos/polyline.ts | 64 + .../testbed2d/src/demos/revoluteJoints.ts | 59 + .../rapier.js/testbed2d/src/demos/voxels.ts | 90 + thirdparty/rapier.js/testbed2d/src/index.ts | 30 + .../rapier.js/testbed2d/static/.htaccess | 3 + .../rapier.js/testbed2d/static/index.html | 18 + thirdparty/rapier.js/testbed2d/tsconfig.json | 11 + .../rapier.js/testbed2d/webpack.config.js | 55 + thirdparty/rapier.js/testbed3d/.npmignore | 3 + thirdparty/rapier.js/testbed3d/package.json | 32 + thirdparty/rapier.js/testbed3d/publish.sh | 6 + .../rapier.js/testbed3d/src/Graphics.ts | 576 +++++ thirdparty/rapier.js/testbed3d/src/Gui.ts | 167 ++ thirdparty/rapier.js/testbed3d/src/Testbed.ts | 211 ++ .../rapier.js/testbed3d/src/demos/ccd.ts | 75 + .../src/demos/characterController.ts | 106 + .../testbed3d/src/demos/collisionGroups.ts | 73 + .../testbed3d/src/demos/convexPolyhedron.ts | 111 + .../rapier.js/testbed3d/src/demos/damping.ts | 42 + .../rapier.js/testbed3d/src/demos/fountain.ts | 78 + .../testbed3d/src/demos/glbToTrimesh.ts | 68 + .../testbed3d/src/demos/glbtoConvexHull.ts | 68 + .../testbed3d/src/demos/heightfield.ts | 126 ++ .../rapier.js/testbed3d/src/demos/joints.ts | 292 +++ .../rapier.js/testbed3d/src/demos/keva.ts | 139 ++ .../testbed3d/src/demos/lockedRotations.ts | 58 + .../testbed3d/src/demos/pidController.ts | 127 ++ .../rapier.js/testbed3d/src/demos/platform.ts | 153 ++ .../rapier.js/testbed3d/src/demos/pyramid.ts | 51 + .../rapier.js/testbed3d/src/demos/trimesh.ts | 143 ++ .../rapier.js/testbed3d/src/demos/voxels.ts | 130 ++ thirdparty/rapier.js/testbed3d/src/index.ts | 42 + .../rapier.js/testbed3d/static/.htaccess | 3 + .../rapier.js/testbed3d/static/index.html | 18 + .../static/suzanne_blender_monkey.glb | Bin 0 -> 60820 bytes thirdparty/rapier.js/testbed3d/tsconfig.json | 11 + .../rapier.js/testbed3d/webpack.config.js | 55 + 172 files changed, 27480 insertions(+) create mode 100644 thirdparty/rapier.js/.github/FUNDING.yml create mode 100644 thirdparty/rapier.js/.github/workflows/main.yml create mode 100644 thirdparty/rapier.js/.gitignore create mode 100644 thirdparty/rapier.js/.prettierignore create mode 100644 thirdparty/rapier.js/.prettierrc create mode 100644 thirdparty/rapier.js/CHANGELOG.md create mode 100644 thirdparty/rapier.js/Cargo.lock create mode 100644 thirdparty/rapier.js/Cargo.toml create mode 100644 thirdparty/rapier.js/LICENSE create mode 100644 thirdparty/rapier.js/README.md create mode 100644 thirdparty/rapier.js/builds/prepare_builds/Cargo.toml create mode 100644 thirdparty/rapier.js/builds/prepare_builds/README.md create mode 100644 thirdparty/rapier.js/builds/prepare_builds/build_all_projects.sh create mode 100644 thirdparty/rapier.js/builds/prepare_builds/prepare_all_projects.sh create mode 100644 thirdparty/rapier.js/builds/prepare_builds/src/main.rs create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/Cargo.toml.tera create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/LICENSE create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/README.md.tera create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/build_rust.sh.tera create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/build_typescript.sh.tera create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/package.json.tera create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig.json create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig_typedoc.json create mode 100644 thirdparty/rapier.js/builds/prepare_builds/templates/typedoc.json.tera create mode 100644 thirdparty/rapier.js/package.json create mode 100644 thirdparty/rapier.js/publish_all_canary.sh create mode 100644 thirdparty/rapier.js/publish_all_prod.sh create mode 100644 thirdparty/rapier.js/rapier-compat/README.md create mode 100644 thirdparty/rapier.js/rapier-compat/build-rust.sh create mode 100644 thirdparty/rapier.js/rapier-compat/fix_raw_file.sh create mode 100644 thirdparty/rapier.js/rapier-compat/gen_src.sh create mode 100644 thirdparty/rapier.js/rapier-compat/package.json create mode 100644 thirdparty/rapier.js/rapier-compat/rollup.config.js create mode 100644 thirdparty/rapier.js/rapier-compat/src2d/init.ts create mode 100644 thirdparty/rapier.js/rapier-compat/src2d/raw.ts create mode 100644 thirdparty/rapier.js/rapier-compat/src3d/init.ts create mode 100644 thirdparty/rapier.js/rapier-compat/src3d/raw.ts create mode 100644 thirdparty/rapier.js/rapier-compat/tests/World2d.test.ts create mode 100644 thirdparty/rapier.js/rapier-compat/tests/World3d.test.ts create mode 100644 thirdparty/rapier.js/rapier-compat/tests/math2d.test.ts create mode 100644 thirdparty/rapier.js/rapier-compat/tests/math3d.test.ts create mode 100644 thirdparty/rapier.js/rapier-compat/tsconfig.common.json create mode 100644 thirdparty/rapier.js/rapier-compat/tsconfig.json create mode 100644 thirdparty/rapier.js/rapier-compat/tsconfig.pkg2d.json create mode 100644 thirdparty/rapier.js/rapier-compat/tsconfig.pkg3d.json create mode 100644 thirdparty/rapier.js/src.ts/coarena.ts create mode 100644 thirdparty/rapier.js/src.ts/control/character_controller.ts create mode 100644 thirdparty/rapier.js/src.ts/control/index.ts create mode 100644 thirdparty/rapier.js/src.ts/control/pid_controller.ts create mode 100644 thirdparty/rapier.js/src.ts/control/ray_cast_vehicle_controller.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/ccd_solver.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/coefficient_combine_rule.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/impulse_joint.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/impulse_joint_set.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/index.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/integration_parameters.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/island_manager.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/multibody_joint.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/multibody_joint_set.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/rigid_body.ts create mode 100644 thirdparty/rapier.js/src.ts/dynamics/rigid_body_set.ts create mode 100644 thirdparty/rapier.js/src.ts/exports.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/broad_phase.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/collider.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/collider_set.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/contact.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/feature.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/index.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/interaction_groups.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/narrow_phase.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/point.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/ray.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/shape.ts create mode 100644 thirdparty/rapier.js/src.ts/geometry/toi.ts create mode 100644 thirdparty/rapier.js/src.ts/init.ts create mode 100644 thirdparty/rapier.js/src.ts/math.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/debug_render_pipeline.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/event_queue.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/index.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/physics_hooks.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/physics_pipeline.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/query_pipeline.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/serialization_pipeline.ts create mode 100644 thirdparty/rapier.js/src.ts/pipeline/world.ts create mode 100644 thirdparty/rapier.js/src.ts/rapier.ts create mode 100644 thirdparty/rapier.js/src.ts/raw.ts create mode 100644 thirdparty/rapier.js/src/control/character_controller.rs create mode 100644 thirdparty/rapier.js/src/control/mod.rs create mode 100644 thirdparty/rapier.js/src/control/pid_controller.rs create mode 100644 thirdparty/rapier.js/src/control/ray_cast_vehicle_controller.rs create mode 100644 thirdparty/rapier.js/src/dynamics/ccd_solver.rs create mode 100644 thirdparty/rapier.js/src/dynamics/impulse_joint.rs create mode 100644 thirdparty/rapier.js/src/dynamics/impulse_joint_set.rs create mode 100644 thirdparty/rapier.js/src/dynamics/integration_parameters.rs create mode 100644 thirdparty/rapier.js/src/dynamics/island_manager.rs create mode 100644 thirdparty/rapier.js/src/dynamics/joint.rs create mode 100644 thirdparty/rapier.js/src/dynamics/mod.rs create mode 100644 thirdparty/rapier.js/src/dynamics/multibody_joint.rs create mode 100644 thirdparty/rapier.js/src/dynamics/multibody_joint_set.rs create mode 100644 thirdparty/rapier.js/src/dynamics/rigid_body.rs create mode 100644 thirdparty/rapier.js/src/dynamics/rigid_body_set.rs create mode 100644 thirdparty/rapier.js/src/geometry/broad_phase.rs create mode 100644 thirdparty/rapier.js/src/geometry/collider.rs create mode 100644 thirdparty/rapier.js/src/geometry/collider_set.rs create mode 100644 thirdparty/rapier.js/src/geometry/contact.rs create mode 100644 thirdparty/rapier.js/src/geometry/feature.rs create mode 100644 thirdparty/rapier.js/src/geometry/mod.rs create mode 100644 thirdparty/rapier.js/src/geometry/narrow_phase.rs create mode 100644 thirdparty/rapier.js/src/geometry/point.rs create mode 100644 thirdparty/rapier.js/src/geometry/ray.rs create mode 100644 thirdparty/rapier.js/src/geometry/shape.rs create mode 100644 thirdparty/rapier.js/src/geometry/toi.rs create mode 100644 thirdparty/rapier.js/src/lib.rs create mode 100644 thirdparty/rapier.js/src/math.rs create mode 100644 thirdparty/rapier.js/src/pipeline/debug_render_pipeline.rs create mode 100644 thirdparty/rapier.js/src/pipeline/event_queue.rs create mode 100644 thirdparty/rapier.js/src/pipeline/mod.rs create mode 100644 thirdparty/rapier.js/src/pipeline/physics_hooks.rs create mode 100644 thirdparty/rapier.js/src/pipeline/physics_pipeline.rs create mode 100644 thirdparty/rapier.js/src/pipeline/serialization_pipeline.rs create mode 100644 thirdparty/rapier.js/src/utils.rs create mode 100644 thirdparty/rapier.js/testbed2d/.npmignore create mode 100644 thirdparty/rapier.js/testbed2d/package.json create mode 100644 thirdparty/rapier.js/testbed2d/publish.sh create mode 100644 thirdparty/rapier.js/testbed2d/src/Graphics.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/Gui.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/Testbed.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/characterController.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/collisionGroups.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/convexPolygons.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/cubes.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/heightfield.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/keva.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/lockedRotations.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/pidController.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/polyline.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/revoluteJoints.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/demos/voxels.ts create mode 100644 thirdparty/rapier.js/testbed2d/src/index.ts create mode 100644 thirdparty/rapier.js/testbed2d/static/.htaccess create mode 100644 thirdparty/rapier.js/testbed2d/static/index.html create mode 100644 thirdparty/rapier.js/testbed2d/tsconfig.json create mode 100644 thirdparty/rapier.js/testbed2d/webpack.config.js create mode 100644 thirdparty/rapier.js/testbed3d/.npmignore create mode 100644 thirdparty/rapier.js/testbed3d/package.json create mode 100644 thirdparty/rapier.js/testbed3d/publish.sh create mode 100644 thirdparty/rapier.js/testbed3d/src/Graphics.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/Gui.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/Testbed.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/ccd.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/characterController.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/collisionGroups.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/convexPolyhedron.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/damping.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/fountain.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/glbToTrimesh.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/glbtoConvexHull.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/heightfield.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/joints.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/keva.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/lockedRotations.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/pidController.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/platform.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/pyramid.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/trimesh.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/demos/voxels.ts create mode 100644 thirdparty/rapier.js/testbed3d/src/index.ts create mode 100644 thirdparty/rapier.js/testbed3d/static/.htaccess create mode 100644 thirdparty/rapier.js/testbed3d/static/index.html create mode 100644 thirdparty/rapier.js/testbed3d/static/suzanne_blender_monkey.glb create mode 100644 thirdparty/rapier.js/testbed3d/tsconfig.json create mode 100644 thirdparty/rapier.js/testbed3d/webpack.config.js diff --git a/thirdparty/rapier.js/.github/FUNDING.yml b/thirdparty/rapier.js/.github/FUNDING.yml new file mode 100644 index 00000000..97039335 --- /dev/null +++ b/thirdparty/rapier.js/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: ["dimforge"] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/thirdparty/rapier.js/.github/workflows/main.yml b/thirdparty/rapier.js/.github/workflows/main.yml new file mode 100644 index 00000000..c7977f5e --- /dev/null +++ b/thirdparty/rapier.js/.github/workflows/main.yml @@ -0,0 +1,154 @@ +name: main + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + check-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22.x" + - run: npm ci + - name: Check formatting + run: npm run fmt -- --check + - name: Check Rust formatting + run: cargo fmt -- --check + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - run: npm ci; + - name: Prepare no-compat builds + run: | + ./builds/prepare_builds/prepare_all_projects.sh + - name: Build all no-compat projects + run: | + ./builds/prepare_builds/build_all_projects.sh + # Install dependencies to check simd for builds + - name: Install wabt + run: | + sudo apt install wabt; + if: matrix.os == 'ubuntu-latest' + - name: Install wabt + run: | + brew install wabt; + if: matrix.os == 'macos-latest' + # Check simd for rust builds + - name: Check 2d simd rust build + run: | + if ! wasm-objdump -d builds/rapier2d-simd/pkg/rapier_wasm2d_bg.wasm | grep :\\sfd ; then + >&2 echo "ERROR: 2d simd compat build does not include simd opcode prefix." && exit 1 + fi + - name: Check 3d simd compat build + run: | + if ! wasm-objdump -d builds/rapier3d-simd/pkg/rapier_wasm3d_bg.wasm | grep :\\sfd ; then + >&2 echo "ERROR: 3d simd compat build does not include simd opcode prefix." && exit 1 + fi + - uses: actions/upload-artifact@v4 + with: + name: pkg no-compat ${{ matrix.os }} + path: | + builds/*/pkg + overwrite: true + build-compat: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - run: npm ci; + - name: Prepare compat builds + run: | + ./builds/prepare_builds/prepare_all_projects.sh + - name: Build rapier-compat + run: | + cd rapier-compat; + npm ci; + npm run build; + npm run test; + # Install dependencies to check simd for builds + - name: Install wabt + run: | + sudo apt install wabt; + if: matrix.os == 'ubuntu-latest' + - name: Install wabt + run: | + brew install wabt; + if: matrix.os == 'macos-latest' + # Check simd for compat builds + - name: Check 2d simd compat build + run: | + if ! wasm-objdump -d rapier-compat/builds/2d-simd/pkg/rapier_wasm2d_bg.wasm | grep :\\sfd ; then + >&2 echo "ERROR: 2d simd compat build does not include simd opcode prefix." && exit 1; + fi + - name: Check 3d simd compat build + run: | + if ! wasm-objdump -d rapier-compat/builds/3d-simd/pkg/rapier_wasm3d_bg.wasm | grep :\\sfd ; then + >&2 echo "ERROR: 3d simd compat build does not include simd opcode prefix." && exit 1; + fi + # Upload + - uses: actions/upload-artifact@v4 + with: + name: pkg compat ${{ matrix.os }} + path: | + rapier-compat/builds/*/pkg + overwrite: true + publish: + runs-on: ubuntu-latest + needs: [build, build-compat] + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: pkg no-compat ubuntu-latest + path: builds + - uses: actions/download-artifact@v4 + with: + name: pkg compat ubuntu-latest + path: rapier-compat/builds + - uses: actions/setup-node@v4 + with: + node-version: "22.x" + registry-url: "https://registry.npmjs.org" + - name: Publish projects + run: | + ./publish_all_canary.sh + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/thirdparty/rapier.js/.gitignore b/thirdparty/rapier.js/.gitignore new file mode 100644 index 00000000..b6a5a6f5 --- /dev/null +++ b/thirdparty/rapier.js/.gitignore @@ -0,0 +1,21 @@ +rapier2d/compat +rapier3d/compat +**/*.rs.bk +node_modules +target +.idea +.DS_Store +dist +pkg +gen2d/ +gen3d/ +docs +*.swp +*.d.ts + +# Made by the prepare_builds crate +builds/* +!builds/prepare_builds + +# compat generated builds +rapier-compat/builds/ \ No newline at end of file diff --git a/thirdparty/rapier.js/.prettierignore b/thirdparty/rapier.js/.prettierignore new file mode 100644 index 00000000..24ed3cdd --- /dev/null +++ b/thirdparty/rapier.js/.prettierignore @@ -0,0 +1,7 @@ +pkg +gen2d +gen3d +docs +target +**/CHANGELOG.md +**/README.md \ No newline at end of file diff --git a/thirdparty/rapier.js/.prettierrc b/thirdparty/rapier.js/.prettierrc new file mode 100644 index 00000000..5a3e1430 --- /dev/null +++ b/thirdparty/rapier.js/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "bracketSpacing": false, + "trailingComma": "all" +} diff --git a/thirdparty/rapier.js/CHANGELOG.md b/thirdparty/rapier.js/CHANGELOG.md new file mode 100644 index 00000000..413325bf --- /dev/null +++ b/thirdparty/rapier.js/CHANGELOG.md @@ -0,0 +1,937 @@ +## 0.19.3 (05 Nov. 2025) + +- Significantly improve performances of `combineVoxelStates`. + +### 0.19.2 (17 Oct. 2025) + +- Fix bug where kinematic bodies would not wake up when setting its velocity. +- Fix bug where slow-moving kinematic bodies would fall asleep. +- Fix point-projection on voxels shapes. + +### 0.19.1 (03 Oct. 2025) + +### Modified + +- Update to Rapier 0.30.0. The only change is a [switch to a sparse storage](https://github.com/dimforge/parry/pull/380) + for the Voxels shapes. This allows support for orders of magnitudes larger maps without reaching the 4GB WASM memory + limit. + +### 0.19.0 (05 Sept. 2025) + +### Modified + +- Update to Rapier 0.29.0 which includes performance improvements for scenes involving a lot of contact constraints. + See https://github.com/dimforge/rapier/pull/876 for details. +- Renamed the `RigidBody.invPrincipalInertiaSqrt` and `.effectiveWorldInvInertiaSqrt` methods to + `RigidBody.invPrincipalInertia` and `.effectiveWorldInvInertia` (removed the `Sqrt` suffix). These methods will now + return the actual inverse angular inertia matrix rather than its square root. +- Removed methods related to the legacy PGS solver: `World.numAdditionalFrictionIterations`, + `switchToStandardPgsSolver`, `switchToSmallStepsPgsSolver`, `switchToSmallStepsPgsSolverWithoutWarmstart`. + +### 0.18.2 (13 August 2025) + +### Fixed + +- Fix rollup configuration adding `types: "./rapier.d.ts"` to the export config. + +### 0.18.1 (8 August 2025) + +### Modified + +- Update to Rapier 0.28.0 which includes performance improvements when CCD is active and when + the user applies modification to a collider or rigid-body. + +### Fix + +- Another attempt to fix bundlerless module import with rapier-compat. + +### 0.18.0 (24 July 2025) + +### Added + +- Add `World.timing*` functions to access the internal performances measurements if the internal + profiler is enabled with `World.profilerEnabled = true`. +- Add `World.maxCcdSubsteps` to get/set the max number of CCD substeps run by the engine. + +### Fix + +- Fixed crash that would happen when removing colliders in a particular order (e.g. in the same order + as their insertion). + +### 0.18.0-beta.0 (12 July 2025) + +#### Modified + +- Update to Rapier 0.27.0-beta.1 which includes a fully reworked broad-phase tha supports scene queries. + This implies a performance gain on large scenes by avoiding the need to re-build the underlying acceleration + structure at each frame. +- Un-deprecate methods for reading shape properties (for example `collider.radius()`). It turned out that these + methods are more convenient as they are guaranteed to always be in sync with rapier’s state on wasm. +- Add `collider.translationWrtParent()` and `collider.rotationWrtParent()` to get the collider’s translation/rotation + relative to its parent rigid-body. + +#### Fix + +- rapier-compat top level javascript files extensions have been changed from `.cjs.js` and `.es.js` to `.cjs` and `mjs` + respectively. This results in better compatibility with NPM. + +### 0.17.3 (30 May 2025) + +#### Fix + +- The published package for 0.17.2 had a broken package.json. It is fixed on this release. + +### 0.17.2 (30 May 2025) + +#### Added + +- Added the function `RAPIER.reserveMemory` to instruct the internal allocator to pre-allocate more memory in preparation + for future operations. This typically called only once after initializing the WASM module. + +### 0.17.1 (23 May 2025) + +#### Added + +- Added optional arguments to `World.debugRender(filterFlags, filterPredicate)` to prevent some colliders from being + rendered. +- Added `Collider.combineVoxelStates` to ensure two adjacent voxels colliders don’t suffer from the internal edges + problem, and `Collider.propagateVoxelChange` to maintain that coupling after modifications with `.setVoxel`. + +### 0.17.0 (16 May 2025) + +#### Fixed + +- Fix sensor events not triggering when hitting a voxels collider. + +#### Added + +- Added support for voxels colliders attached to dynamic rigid-bodies. +- Added force calculation between colliding voxels/voxels and voxels/compound shapes. + +### 0.16.2 (5 May 2025) + +#### Fixed + +- Fixed infinite loop in `collider.setVoxel`. + +### 0.16.1 (2 May 2025) + +#### Added + +- Added `Collider.clearShapeCache` to release the reference to the JS shape stored in the collider, oor too force its + recomputation the next time the collider shape is accessed. +- Added support for shape-casting involving Voxels colliders. +- Added support for CCD involving Voxels colliders. + +### 0.16.0 (24 April 2025) + +#### Added + +- Added `ColliderDesc.voxels` to create a collider with a shape optimized for voxels. +- Added `Collider.setVoxel` for adding or removing a voxel from a collider with a voxel shape. +- Added the `Voxels` shape class. + +The support or voxels is still experimental. In particular the following features will currently **not** work on +colliders with voxel shapes: + +- Voxels colliders attached to dynamic rigid-bodies will not run the automatic mass/angular inertia calculation. +- Shape-casting on voxel shapes/colliders. +- Collision-detection between two-voxels colliders, or a voxels collider and a mesh, polyline, or heightfield. + +See [parry#336](https://github.com/dimforge/parry/pull/336) for additional information. + +### 0.15.1 (10 April 2025) + +#### Added + +- Added `RigidBody.velocityAtPoint` function to retrieve the velocity of a given world-space point on a rigid-body. + +#### Modified + +- Update to Rapier 0.25. The main notable change is that the `TriMeshFlags.FIX_INTERNAL_EDGES` flag will no longer + imply that the `TriMeshFlags.ORIENTED` flag is set, avoiding edge-cases where enabling `FIX_INTERNAL_EDGES` + results in unexpected collisions between open meshes and balls. + +#### Fixed + +- Fixed `*-simd-compat` builds not actually emitting SIMD instructions. + +### 0.15.0 (06 March 2025) + +#### Added + +- Added `PidController`, `World.createPidController`, `World.removePidController` to create a Position-Integral-Derivative + controller; a building block for building velocity-based dynamic character controllers. + +#### Modified + +- Update to Rapier 0.24. +- Package tree shaking has been disabled to avoid a crash on certain build configuration (#289). +- Multiple packages with different feature sets are now released, and their build process has been automated (#309) + - Released packages are: + - rapier2d + - rapier2d-compat + - rapier2d-simd + - rapier2d-simd-compat + - rapier2d-deterministic + - rapier2d-deterministic-compat + - rapier3d + - rapier3d-compat + - rapier2d + - rapier2d-compat + - rapier3d-simd + - rapier3d-simd-compat + - rapier3d-deterministic + - rapier3d-deterministic-compat + - :warning: To this occasion, existing packages `rapier2d` and `rapier3d` are now built without `enhanced-determinism` feature enabled, + so if you rely on that feature, you should migrate to the new `-deterministic` flavor. + + +### 0.14.0 (20 July 2024) + +#### Modified + +- Update from the rust library of rapier 0.19 to 0.22, see [rapier's changelog](https://github.com/dimforge/rapier/blob/master/CHANGELOG.md#v0210-23-june-2024) for more context. + +#### Added + +- Added `RigidBody.userForce` function to retrieve the constant force(s) the user added to a rigid-body +- Added `RigidBody.userTorque` function to retrieve the constant torque(s) the user added to a rigid-body + +### 0.13.1 (2024-05-08) + +#### Fixed + +- Fix regression that could cause missed contact between a ball and another shape type. + +### 0.13.0 (2024-05-05) + +Several stability improvements are added as part of this release. +See [rapier#625](https://github.com/dimforge/rapier/pull/625) for overviews of the most important improvements. + +#### Modified + +- The `castShape` and `castCollider` functions have been modified to add a `targetDistance` parameter. This parameter + indicates the distance below which a hit is detected. +- Rename `RayIntersection.toi` to `.timeOfImpact` for better clarity. +- Rename `RayColliderIntersection.toi` to `.timeOfImpact` for better clarity. +- Rename `RayColliderToi` to `RayColliderHit`. +- Rename `RayColliderHit.toi` to `.timeOfImpact` for better clarity. +- Rename `ShapeTOI` to `ShapeCastHit`. +- Rename `ShapeColliderTOI` to `ColliderShapeCastHit`. +- Rename `ShapeCastHit.toi` to `.timeOfImpact`. + +#### Added + +- Fix the kinematic character controller getting stuck against vertical walls. +- Add `KinematicCharacterController.normalNudgeFactor` and `.setNormalNudgeFactor` so set a coefficient used for + avoiding having the character controller get stuck on penetrations. +- Add `RigidBody.softCcdPrediction`, `.setSoftCcdPrediction`, and `RigidBodyDesc.setSoftCcdPrediction` for configuring + soft-ccd on the rigid-body. See [rapier#625](https://github.com/dimforge/rapier/pull/625) for additional details on + soft-ccd. +- 3D version only: add `TriMeshFlags::FIX_INTERNAL_EDGES` and `HeightFieldFlags::FIX_INTERNAL_EDGES` for enabling + internal edges correction (which is no longer enabled by default). The flags have been added as an optional parameter + when building the shapes. +- Add `Collider.contactSkin`, `.setContactSkin`, and `ColliderDesc.setContactSkin` for configuring the collider’s + contact skin. See [rapier#625](https://github.com/dimforge/rapier/pull/625) for additional details on contact skins. +- Add `World.lengthUnit` which can be used to indicate the typical size of dynamic objects (e.g. 100 pixels instead of + 1 meter). This helps the physics engine adjust internal thresholds for better results. + +#### Fixed + +- Fix an issue where the reported contact force are lower than their actual value. + +### 0.12.0 (2024-01-28) + +The main highlight of this release is the implementation of a new non-linear constraints solver for better stability +and increased convergence rates. See [#579](https://github.com/dimforge/rapier/pull/579) for additional information. + +In order to adjust the number of iterations of the new solver, simply adjust `World.numSolverIterations`. +If recovering the old solver behavior is useful to you, call `World.switchToStandardPgsSolver()`. + +It is now possible to specify some additional solver iteration for specific rigid-bodies (and everything interacting +with it directly or indirectly through contacts and joints): `RigidBodyDesc.additionalSolverIterations` and +`RigidBody::setAdditionalSolverIterations`. This allows for higher-accuracy on subsets of the physics scene +without affecting performance of the other parts of the simulation. + +#### Modified + +- Renamed `CharacterController.translationApplied`, `.translationRemaining` and the `desiredTranslation` + method argument to `CharacterController.translationDeltaApplied`, `.translationDeltaRemaining` and the + `desiredTranslationDelta` to avoid confusion with the usage of the `translation` world in `RigidBody.translation()`. + +#### Added + +- Added `DynamicRayCastVehicleController` to simulate vehicles based on ray-casting. +- Added `JointData.generic` (3D only) to create a generic 6-dof joint and manually specify the locked axes. + +### 0.11.2 + +#### Fixed + +- Fix bug that made dynamic rigid-bodies behave like kinematic bodies after being disabled and then re-enabled. +- Fix issue with convex polyhedron jitter due to missing contacts. +- Fix character controller getting stuck against vertical walls. +- Fix character controller’s snapping to ground not triggering sometimes. +- Fix character controller’s horizontal offset being mostly ignored and some instances of vertical offset being ignored. + +#### Added + +- Add `JointData.spring` and `JointData.rope` joints. +- Add access to the mass-properties of a rigid-body: `RigidBody.effectiveInvMass`, `.invMass()`, `.localCom()`, + `.worldCom()`, `.invPrincipalInertiaSqrt()`, `.principalInertia()`, `.principalInertiaLocalFrame()`, + `.effectiveWorldInvInertiaSqrt()`, `.effectiveAngularInertia()`. +- Add `DynamicRayCastVehicleController.siteFrictionStiffness` to customize the side friction applied to the vehicle + controller’s wheel. + +#### Modified + +- Rename `World.contactsWith` to `World.contactPairsWith`. +- Rename `World.intersectionsWith` to `World.intersectionPairsWith`. + +### 0.11.1 (2023-01-16) + +#### Fixed + +- Fix bug that disabled all colliders at construction time. + +### 0.11.0 (2023-01-15) + +#### Added + +- Add `World.propagateModifiedBodyPositionsToColliders` to propagate rigid-bodies position changes to their attached + colliders. +- Add `World.updateSceneQueries` to update the scene queries data structures without stepping the whole simulation. +- Add `RigidBody.isEnabled, RigidBody.setEnabled, RigidBodyDesc.setEnabled` to disable a rigid-body (and all its + attached colliders) without removing it from the physics world. +- Add `Collider.isEnabled, Collider.setEnabled, ColliderDesc.setEnabled` to disable a collider without removing it + from the physics world. +- Add shape-specific methods to modify a collider’s + size: `Collider.setRadius, setHalfExtents, setRoundRadius, setHalfHeight`. + +#### Modified + +- Add a boolean argument to `RigidBody.setBodyType` to indicate if the rigid-body should awaken after changing + its type. + +#### Fixed + +- Fix rigid-bodies automatically waking up at creation even if they were explicitly created sleeping. + +### 0.10.0 (2022-11-06) + +#### Added + +- Add `World.createCharacterController`, `World.removeCharacterController` to create/remove a kinematic character + controller. +- Add a character-controller implementation with the `KinematicCharacterController` class and its method + `KinematicCharacterController.computeColliderMovement`. The character controller features currently supported are: + - Slide on uneven terrains + - Interaction with dynamic bodies. + - Climb stairs automatically. + - Automatically snap the body to the floor when going downstairs. + - Prevent sliding up slopes that are too steep + - Prevent sliding down slopes that are not steep enough + - Interactions with moving platforms. + - Report information on the obstacles it hit on its path. +- Add the `HalfSpace` (infinite plane) shape. Colliders with this shape can be built using `ColliderDesc.halfspace`. + +#### Modified + +- Change the signature of `Collider.castShape` and World.castShape by adding a boolean argument `stop_at_penetration` + before the filter-related arguments. Set this argument to `true` to get the same result as before. If this is set to + `false` and the shape being cast starts it path already intersecting another shape, then a hit won’t be returned + with that intersecting shape unless the casting movement would result in more penetrations. +- Reduce rounding errors in 3D when setting the rotation of a rigid-body or collider. + +#### Fixed + +- Fix incorrect application of torque if the torque is applies right after setting the rigid-body’s + position, but before calling `World.step`. + +### 0.9.0 (2022-10-07) + +#### Fixed + +- Fix unpredictable broad-phase panic when using small colliders in the simulation. +- Fix collision events being incorrectly generated for any shape that produces multiple + contact manifolds (like triangle meshes). + +#### Modified + +- The `RigidBodyDesc.setAdditionalMass` method will now result in the additional angular inertia + being automatically computed based on the shapes of the colliders attached to the rigid-body. +- Removed the method `RigidBodyDesc.setAdditionalPrincipalAngularInertia`. Use + `RigidBodyDesc.setAdditionalMassProperties` instead. +- The methods of `RigidBodyDesc` and `ColliderDesc` will now always copy the object provided by + the user instead of storing the object itself. +- The following method will now copy the user’s objects instead of storing it: `ColliderDesc.setRotation`, + `ColliderDesc.setMassProperties`, `RigidBodyDesc.setRotation`, `RigidBodyDesc.setAdditionalMassProperties`, + `RigidBodyDesc.setAngvel`. +- Rename `RigidBody.restrictRotations` and `RigidBody.restrictTranslations` to + `RigidBody.setEnabledRotations` and `RigidBody.setEnabledTranslations`. +- Rename `RigidBodyDesc.restrictRotations` and `RigidBodyDesc.restrictTranslations` to + `RigidBodyDesc.enabledRotations` and `RigidBodyDesc.enabledTranslations`. + +#### Added + +- Add `ImpulseJoint.setContactsEnabled`, and `MultibodyJoint.setContactsEnabled` to set whether + contacts are enabled between colliders attached to rigid-bodies linked by this joint. +- Add `UnitImpulseJoint.setLimits` to set the limits of a unit joint. +- Add `RigidBody.recomputeMassPropertiesFromColliders` to force the immediate computation + of a rigid-body’s mass properties (instead of waiting for them to be recomputed during the next + timestep). This is useful to be able to read immediately the result of a change of a rigid-body + additional mass-properties or a change of one of its collider’s mass-properties. +- Add `RigidBody::setAdditionalMass` to set the additional mass for the rigid-body. The additional + angular inertia is automatically computed based on the attached colliders shapes. If this automatic + angular inertia computation isn’t desired, use `RigidBody::setAdditionalMassProperties` instead. +- Add `Collider.setDensity`, `.setMass`, `.setMassProperties`, to alter a collider’s mass + properties. Note that `.setMass` will result in the collider’s angular inertia being automatically + computed based on this mass and on its shape. +- Add `ColliderDesc.setMass` to set the mass of the collider instead of its density. Its angular + inertia tensor will be automatically computed based on this mass and its shape. +- Add more filtering options for scene-queries. All the scene query methods now take additional optional + arguments to indicate if one particular collider, rigid-body, or type of colliders/rigid-bodies have to + be ignored by the query. +- Add force reporting based on contact force events. The `EventHandler` trait has been modified to include + the method `EventQueue.drainContactForceEvents`. Contact force events are generated whenever the sum of the + magnitudes of all the forces between two colliders is greater than any of their + `Collider.contactForceEventThreshold` values (only the colliders with the `ActiveEvents.CONTACT_FORCE_EVENT` + flag set are taken into account for this threshold). + +### 0.8.1 (2022-04-06) + +Starting with this release, all the examples in `testbed2d` and `testbed3d` have been updated to `webpack 5`, +and are written in Typescript. In addition, canary `0.0.0` releases will be generated automatically after each merge +to the `master` branch. + +#### Fixed + +- Fix bug causing `World.intersectionPair` to always return `false`. + +### 0.8.0 (2022-03-31) + +#### Breaking changes + +- Most APIs that relied on rigid-body handles or collider handles have been modified to rely on `RigidBody` + or `Collider` objects instead. Handles are now only needed for events and hooks. +- Rename STATIC to FIXED in the `ActiveCollisionTypes` flags. +- The `RigidBody.applyForce` and `.applyTorque` methods have been replaced by `.addForce` and `.addTorque`. These + force/torques are no longer automatically zeroed after a timestep. To zero them manually, call `.resetForce` or + `.resetTorque`. +- The `EventQueue.drainContactEvents` and `EventQueue.drainIntersectionEvents` have been merged into a single + method: `EventQueue:drainCollisionEvents`. + +#### Added + +- Add a lines-based debug-renderer. See [#119](https://github.com/dimforge/rapier.js/pull/119) for examples of + integration of the debug-renderer with `THREE.js` and `PIXI.js`. +- Each rigid-body, collider, impulse joint, and multibody joint now have a unique object instance. This instance + in only valid as long as the corresponding objects is inserted to the `World`. +- Add a `wakeUp: bool` argument to the `World.createImpulseJoint` and `MultibodyJointSet::createMultibodyJoint` to + automatically wake-up the rigid-bodies attached to the inserted joint. +- Add a `filter` callback to all the scene queries. Use this for filtering more fine-grained than collision groups. +- Add `collider.shape` that represents the shape of a collider. This is a more convenient way of reading the collider’s + shape properties. Modifying this shape will have no effect unless `collider.setShape` is called with the modified + shape. +- Add `Collider.containsPoint`, `.projectPoint`, `.intersectsRay`, `.castShape`, `.castCollider`, `.intersectsShape`, + `.contactShape`, `.contactCollider`, `.castRay`, `.castRayAndGetNormal`. +- Add `Shape.containsPoint`, `.projectPoint`, `.intersectsRay`, `.castShape`, `.intersectsShape`, + `.contactShape`, `.castRay`, `.castRayAndGetNormal`. +- Add `World.projectPointAndGetFeature` which includes the feature Id the projected point lies on. +- Add `RigidBodyDesc.sleeping` to initialize the rigid-body in a sleeping state. + +### 0.8.0-alpha.2 (2022-03-20) + +The changelog hasn’t been updated yet. +For an overview of the changes, refer to the changelog for the unreleased Rust version: +https://github.com/dimforge/rapier/blob/master/CHANGELOG.md#unreleased + +### 0.8.0-alpha.1 (2022-01-28) + +#### Fixed + +- Fix a crash when calling `collider.setShape`. +- Fix a crash when calling `world.collidersWithAabbIntersectingAabb`. +- Fix damping not being applied properly to some rigid-bodies. + +### 0.8.0-alpha.0 (2022-01-16) + +This release updates to Rapier 0.12.0-alpha.0 which contains: + +- A **complete rewrite** of the joint and contact constraint solver. +- The support for locking individual translation axes. +- The support for multibody joint. + +This is an **alpha** release because the new solver still needs some tuning, +and the spherical joint motors/limits is currently not working. + +#### Breaking changes + +- In 3D: renamed `BallJoint` to `SphericalJoint`. +- In 2D: renamed `BallJoint` to `RevoluteJoint`. +- Remove the joint motors and limits for the spherical joint (this is a temporary removal until with leave alpha). +- All the joint-related structures and methods (`RevoluteJoint`, `PrismaticJoint`, `createJoint`, etc.) have renamed to + include `impulse` in the names: `RevoluteImpulseJoint`, `PrismaticImpulseJoint`, `createImpulseJoint`, etc. This is + to differentiate them from the new multibody joints. +- Remove from the integration parameters all the parameters that are no longer meaningful (`maxPositionIterations`, + `jointErp`, `warmstartCoeff`, `allowedAngularError`, `maxLinearCorrection`, `maxAngularCorrection`). + +#### Added + +- Add multibody joints. They are created the same way as impulse joints, except that they are created + by `world.createMultibodyJoint` instead of `world.createImpulseJoint`. +- Add the ability to lock individual translation axes. Use `rigidBody.restrictTranslation`. + +#### Fixed + +- Fixed an issue with velocity-based kinematic bodies applying kinematic rotation improperly. +- Fixed the second witness points and normals returned by shape-casts. +- Fixed the second local contact normal and points returned by contact manifolds. + +### 0.7.6 + +This release updates to Rapier 0.11.1 which contains several bug fixes. + +#### Fixed + +- Fix a bug causing large moving colliders to miss some collisions after some time. +- Fix invalid forces generated by contacts with position-based kinematic bodies. +- Fix a bug where two colliders without parent would not have their collision computed even if the + appropriate flags were set. + +### 0.7.5 + +#### Fixed + +- Fixed an issue where a collider with no parent attached would not be created + under some conditions. + +### 0.7.4 + +This release was broken and has been unpublished. + +### 0.7.3 + +#### Fixed + +- The `collider.halfExtents()` methods now returns a valid value for round cuboids. + +### 0.7.2 (rapier-compat only) + +#### Modified + +- The `rapier-compat` packages don’t need the `Buffer` polyfill anymore. + +### 0.7.1 + +#### Modified + +- Update to use Rapier 0.11.0. +- The `rapier-compat` packages are now generated by rollup. + +### v0.7.0 + +The typescripts bindings for Rapier have +a [brand new user-guide](https://rapier.rs/docs/user_guides/javascript/getting_started_js) +covering all the features of the physics engine! + +### Breaking change + +- The `World.castRay` and `World.castRayAndGetNormal` methods have a different signature now, making them + more flexible. +- Rename `ActiveHooks::FILTER_CONTACT_PAIR` to `ActiveHooks.FILTER_CONTACT_PAIRS`. +- Rename `ActiveHooks::FILTER_INTERSECTION_PAIR` to `ActiveHooks.FILTER_INTERSECTION_PAIRS`. +- Rename `BodyStatus` to `RigidBodyType`. +- Rename `RigidBody.bodyStatus()` to `RigidBody.bodyType()`. +- Rename `RigidBodyDesc.setMassProperties` to `RigidBodyDesc.setAdditionalMassProperties`. +- Rename `RigidBodyDesc.setPrincipalAngularInertia` to `RigidBodyDesc.setAdditionalPrincipalAngularInertia`. +- Rename `ColliderDesc.setIsSensor` to `ColliderDesc.setSensor. + +#### Added + +- Add `Ray.pointAt(t)` that conveniently computes `ray.origin + ray.dir * t`. +- Add access to joint motors by defining each joint with its own class deriving from `Joint`. Each joint now + have its relevant motor configuration + methods: `configurMotorModel, configureMotorVelocity, configureMotorPosition, configureMotor`. +- Add `World.collidersWithAabbIntersectingAabb` for retrieving the handles of all the colliders intersecting the given + AABB. +- Add many missing methods for reading/modifying a rigid-body state after its creation: + - `RigidBody.lockTranslations` + - `RigidBody.lockRotations` + - `RigidBody.restrictRotations` + - `RigidBody.dominanceGroup` + - `RigidBody.setDominanceGroup` + - `RigidBody.enableCcd` +- Add `RigidBodyDesc.setDominanceGroup` for setting the dominance group of the rigid-body being built. +- Add many missing methods for reading/modifying a collider state after its creation: + - `Collider.setSendor` + - `Collider.setShape` + - `Collider.setRestitution` + - `Collider.setFriction` + - `Collider.frictionCombineRule` + - `Collider.setFrictionCombineRule` + - `Collider.restitutionCombineRule` + - `Collider.setRestitutionCombineRule` + - `Collider.setCollisionGroups` + - `Collider.setSolverGroups` + - `Collider.activeHooks` + - `Collider.setActiveHooks` + - `Collider.activeEvents` + - `Collider.setActiveEvents` + - `Collider.activeCollisionTypes` + - `Collider.setTranslation` + - `Collider.setTranslationWrtParent` + - `Collider.setRotation` + - `Collider.setRotationWrtParent` +- Add `ColliderDesc.setMassProperties` for setting explicitly the mass properties of the collider being built (instead + of relying on density). + +#### Modified + +- Colliders are no longer required to be attached to a rigid-body. Therefore, the second argument + of `World.createCollider` + is now optional. + +### v0.6.0 + +#### Breaking changes + +- The `BodyStatus::Kinematic` variant has been replaced by `BodyStatus::KinematicPositionBased` and + `BodyStatus::KinematicVelocityBased`. Position-based kinematic bodies are controlled by setting (at each frame) the + next + kinematic position of the rigid-body (just like before), and the velocity-based kinematic bodies are controlled + by setting (at each frame) the velocity of the rigid-body. +- The `RigidBodyDesc.newKinematic` has been replaced by `RigidBodyDesc.newKinematicPositionBased` and + `RigidBodyDesc.newKinematicVelocityBased` in order to build a position-based or velocity-based kinematic body. +- All contact and intersection events are now disabled by default. The can be enabled for each collider individually + by setting + its `ActiveEvents`: `ColliderDesc.setActiveEvents(ActiveEvents.PROXIMITY_EVENTS | ActiveEvents.ContactEvents)`. + +#### Added + +- It is now possible to read contact information from the narrow-phase using: + - `world.narrowPhase.contactsWith(collider1, callback)` + - `world.narrowPhase.intersectionsWith(collider1, callback)` + - `world.narrowPhase.contactPair(collider1, collider2, callback)` + - `world.narrowPhase.intersectionPair(collider1, collider2, callback)` +- It is now possible to apply custom collision-detection filtering rules (more flexible than collision groups) + based on a JS object by doing the following: + - Enable physics hooks for the colliders you want the custom rules to apply to: + `ColliderDesc.setActiveHooks(ActiveHooks.FILTER_CONTACT_PAIR | ActiveHooks.FILTER_CONTACT_PAIR)` + - Define an object that implements the `PhysicsHooks` interface. + - Pass and instance of your physics hooks object as the second argument of the `World.step` method. +- It is now possible to enable collision-detection between two non-dynamic bodies (e.g. between a kinematic + body and a fixed body) by setting the active collision types of a collider: + `ColliderDesc.setActiveCollisionTypes(ActiveCollisionTypes.ALL)` + +### v0.5.3 + +- Fix a crash when loading the WASM file on iOS safari. + +### v0.5.2 + +- Fix a crash that could happen after adding and then removing a collider right away, + before stepping the simulation. + +### v0.5.1 + +- Fix a determinism issue after snapshot restoration. + +### v0.5.0 + +- Significantly improve the broad-phase performances when very large colliders are used. +- Add `RigidBodyDesc::setCcdEnabled` to enable Continuous Collision Detection (CCD) for this rigid-body. CCD ensures + that a fast-moving rigid-body doesn't miss a collision (the tunneling problem). + +#### Breaking changes: + +- Removed multiple fields from `IntegrationParameters`. These were unused parameters. + +### v0.4.0 + +- Fix a bug in ball/convex shape collision detection causing the ball to ignore penetrations with depths greater than + its radius. + +Breaking changes: + +- Removed `IntegrationParameters.restitutionVelocityThreshold` + and `IntegrationParameters.set_restitutionVelocityThreshold`. + +### v0.3.1 + +- Fix crash happening when creating a collider with a `ColliderDesc.convexHull` shape. +- Actually remove the second argument of `RigidBodyDesc.setMass` as mentioned in the 0.3.0 changelog. +- Improve snapshotting performances. + +### v0.3.0 + +#### Added + +- Added a `RAPIER.version()` function at the root of the package to retrieve the version of Rapier as a string. + +Several geometric queries have been added (the same methods have been added to the +`QueryPipeline` too): + +- `World.intersectionsWithRay`: get all colliders intersecting a ray. +- `World.intersectionWithShape`: get one collider intersecting a shape. +- `World.projectPoint`: get the projection of a point on the closest collider. +- `World.intersectionsWithPoint`: get all the colliders containing a point. +- `World.castShape`: get the first collider intersecting a shape moving linearly + (aka. sweep test). +- `World.intersectionsWithShape`: get all the colliders intersecting a shape. + +Several new shape types are now supported: + +- `RoundCuboid`, `Segment`, `Triangle`, `RoundTriangle`, `Polyline`, `ConvexPolygon` (2D only), + `RoundConvexPolygon` (2D only), `ConvexPolyhedron` (3D only), `RoundConvexPolyhedron` (3D only), + `RoundCone` (3D only). + +It is possible to build `ColliderDesc` using these new shapes: + +- `ColliderDesc.roundCuboid`, `ColliderDesc.segment`, `ColliderDesc.triangle`, `ColliderDesc.roundTriangle`, + `ColliderDesc.convexHull`, `ColliderDesc.roundConvexHull`, `ColliderDesc.Polyline`, + `ColliderDesc.convexPolyline` (2D only), `ColliderDesc.roundConvexPolyline` (2D only), + `ColliderDesc.convexMesh` (3D only),`ColliderDesc.roundConvexMesh` (3D only), `ColliderDesc.roundCone` (3D only). + +It is possible to specify different rules for combining friction and restitution coefficients of the two colliders +involved in a contact with: + +- `ColliderDesc.frictionCombineRule`, and `ColliderDesc.restitutionCombineRule`. + +Various RigidBody-related getter and setters have been added: + +- `RigidBodyDesc.newStatic`, `RigidBodyDesc.newDynamic`, and `RigidBodyDesc.newKinematic` are new static method, short + equivalent to `new RigidBodyDesc(BodyStatus.Static)`, etc. +- `RigidBodyDesc.setGravityScale`, `RigidBody.gravityScale`, `RigidBody.setGravityScale` to get/set the scale factor + applied to the gravity affecting a rigid-body. Setting this to 0.0 will make the rigid-body ignore gravity. +- `RigidBody.setLinearDamping` and `RigidBody.setAngularDamping` to set the linear and angular damping of the + rigid-body. +- `RigidBodyDesc.restrictRotations` to prevent rotations along specific coordinate axes. This replaces the three boolean + arguments previously passed to `.setPrincipalAngularInertia`. + +#### Breaking changes + +Breaking changes related to rigid-bodies: + +- The `RigidBodyDesc.setTranslation` and `RigidBodyDesc.setLinvel` methods now take the components of the translation + directly as arguments instead of a single `Vector`. +- The `RigidBodyDesc.setMass` takes only one argument now. Use `RigidBodyDesc.lockTranslations` to lock the + translational motion of the rigid-body. +- The `RigidBodyDesc.setPrincipalAngularInertia` no longer have boolean parameters to lock rotations. + Use `RigidBodyDesc.lockRotations` or `RigidBodyDesc.restrictRotations` to lock the rotational motion of the + rigid-body. + +Breaking changes related to colliders: + +- The `ColliderDesc.setTranslation` method now take the components of the translation directly as arguments instead of a + single `Vector`. +- The `roundRadius` fields have been renamed `borderRadius`. +- The `RawShapeType.Polygon` no longer exists. For a 2D convex polygon, use `RawShapeType.ConvexPolygon` + instead. +- All occurrences of `Trimesh` have been replaced by `TriMesh` (note the change in case). + +Breaking changes related to events: + +- Rename all occurrences of `Proximity` to `Intersection`. +- The `Proximity` enum has been removed. +- The `drainIntersectionEvents` (previously called `drainProximityEvent`) will now call a callback with + arguments `(number, number, boolean)`: two collider handles, and a boolean indicating if they are intersecting. + +Breaking changes related to scene queries: + +- The `QueryPipeline.castRay` method now takes two additional parameters: a boolean indicating if the ray should not + propagate if it starts inside of a shape, and a bit mask indicating the group the ray is part of and these it + interacts with. +- The `World.castRay` and `QueryPipeline.castRay` now return a struct of type `RayColliderHit` + which no longer contains the normal at the hit point. Use the new methods `World.castRayAndGetNormal` + or `QueryPipeline.castRayAndGetNormal` in order to retrieve the normal too. + +### v0.2.13 + +- Fix a bug where `RigidBodyDesc.setMass(m)` with `m != 0.0` would cause the rotations of the created rigid-body to be + locked. + +### v0.2.12 + +- Add a boolean argument to `RigidBodyDesc.setMass` to indicate if the mass contribution of colliders should be enabled + for this rigid-body or not. +- Add a `RigidBody.lockRotations` method to lock all the rigid-body rotations resulting from forces. +- Add a `RigidBody.lockTranslations` method to lock all the rigid-body translations resulting from forces. +- Add a `RigidBody.setPrincipalAngularInertia` method to set the principal inertia of the rigid-body. This gives the + ability to lock individual rotation axes of one rigid-body. + +### v0.2.11 + +- Fix a bug where removing when immediately adding a collider would cause collisions to fail with the new collider. +- Fix a regression causing some colliders added after a few timesteps not to be properly taken into account. + +### v0.2.10 + +- Fix a bug where removing a collider would result in a rigid-body being removed instead. +- Fix a determinism issue when running simulation on the Apple M1 processor. +- Add `JointData.prismatic` and `JointData.fixed` for creating prismatic joint or fixed joints. + +### v0.2.9 + +- Add `RigidBody.setLinvel` to set the linear velocity of a rigid-body. +- Add `RigidBody.setAngvel` to set the angular velocity of a rigid-body. + +### v0.2.8 + +- Add `RigidBodySet.remove` to remove a rigid-body from the set. +- Add `ColliderSet.remove` to remove a collider from the set. +- Add `JointSet.remove` to remove a joint from the set. +- Add `RigidBodyDesc.setLinearDamping` and `RigidBodyDesc.setAngularDamping` for setting the linear and angular damping + coefficient of the rigid-body to create. +- Add `RigidBodyDesc.setMass`, and `RigidBodyDesc.setMassProperties` for setting the initial mass properties of a + rigid-body. +- Add `ColliderDesc.setCollisionGroups` to use bit-masks for collision filtering between some pairs of colliders. +- Add `ColliderDesc.setSolverGroups` to use bit-masks for making the constraints solver ignore contacts between some + pairs of colliders. +- Add `ColliderDesc.heightfield` to build a collider with an heightfield shape. +- Add `ColliderDesc.trimesh` to build a collider with a triangle mesh shape. + +### v0.2.7 + +- Reduce snapshot size and computation times. + +### v0.2.6 + +- Fix bug causing an unbounded memory usage when an objects falls indefinitely. + +### v0.2.5 + +- Fix wrong result given by `RigidBody.isKinematic()` and `RigidBody.isDynamic()`. + +### v0.2.4 + +- Add the support for round cylinder colliders (i.e. cylinders with round edges). + +### v0.2.3 + +- Add the support for cone, cylinder, and capsule colliders. + +### v0.2.2 + +- Fix regression causing the density and `isSensor` properties of `ColliderDesc` to not be taken into account. +- Throw an exception when the parent handle passed to `world.createCollider` is not a number. + +### v0.2.1 + +This is a significant rewrite of the JavaScript bindings for rapier. The objective of this rewrite is to make the API +closer to Rapier's and remove most need for manual memory management. + +- Calling `.free()` is now required only for objects that live for the whole duration of the simulation. This means that + it is no longer necessary to `.free()` vectors, rays, ray intersections, colliders, rigid-bodies, etc. Object that + continue to require an explicit `.free()` are: + - `World` and `EventQueue`. + - Or, if you are not using the `World` directly: + `RigidBodySet`, `ColliderSet`, `JointSet`, `IntegrationParameters`, `PhysicsPipeline`, `QueryPipeline` + , `SerializationPipeline`, and `EventQueue`. +- Collider.parent() now returns the `RigidBodyHandle` of its parent (instead of the `RigidBody` directly). +- Colliders are now built with `world.createCollider`, i.e., `body.createCollider(colliderDesc)` + becomes `world.createCollider(colliderDesc, bodyHandle)`. +- Shape types are not an enumeration instead of strings: `ShapeType.Ball` and `ShapeType.Cuboid` instead of `"ball"` + and `"cuboid"`. +- `collider.handle` is now a field instead of a function. +- `body.handle` is now a field instead of a function. +- The world's gravity is now a `Vector` instead of individual components, i.e., `let world = new RAPIER.World(x, y, z);` + becomes `let world = new RAPIER.World(new RAPIER.Vector3(x, y, z))`. +- Most methods that took individual components as argument (`setPosition`, `setKinematicPosition`, `setRotation`, etc.) + now take a `Vector` or `Rotation` structure instead. For example `rigid_body.setKinematicPosition(x, y, z)` + becomes `rigid_body.setKinematicPosition(new RAPIER.Vector3(x, y, z))`. +- `world.stepWithEvents` becomes `world.step` (the event queue is the last optional argument). +- `RigidBodyDesc` and `ColliderDesc` now use the builder pattern. For example + `let bodyDesc = new RAPIER.RigidBodyDesc("dynamic"); bodyDesc.setTranslation(x, y, z)` becomes + `new RigidBodyDesc(BodyStatus.Dynamic).setTranslation(new Vector(x, y, z))`. +- `ray.dir` and `ray.origin` are now fields instead of methods. +- 2D rotations are now just a `number` instead of a `Rotation` struct. So instead of doing `rotation.angle`, single use + the number as the rotation angle. +- 3D rotations are now represented by the interface `Rotation` (with fields `{x,y,z,w}`) or the class `Quaternion`. Any + object with these `{x, y, z, w}` fields can be used wherever a `Rotation` is required. +- 2D vectors are now represented by the interface `Vector` (with fields `{x,y}`) or the class `Vector2`). Any object + with these `{x,y}` fields can be used wherever a `Vector` is required. +- 3D vectors are now represented by the interface `Vector` (with fields `{x,y,z}`) or the class `Vector3`). Any object + with these `{x,y,z}` fields can be used wherever a `Vector` is required. + +### v0.2.0 + +See changelogs for v0.2.1 instead. The NPM package for v0.2.0 were missing some files. + +### v0.1.17 + +- Fix bug when ghost forces and/or crashes could be observed when a kinematic body touches a fixed body. + +### v0.1.16 + +- Fix kinematic rigid-body not waking up dynamic bodies it touches. +- Added `new Ray(origin, direction)` constructor instead of `Ray.new(origin, direction)`. + +### v0.1.15 + +- Fix crash when removing a kinematic rigid-body from the World. + +### v0.1.14 + +- Fix issues where force application functions took ownership of the JS vector, preventing the user from freeing + with `Vector.free()` afterwards. + +### v0.1.13 + +- Added `rigidBody.setNextKinematicTranslation` to set the translation of a kinematic rigid-body at the next timestep. +- Added `rigidBody.setNextKinematicRotation` to set the rotation of a kinematic rigid-body at the next timestep. +- Added `rigidBody.predictedTranslation` to get the translation of a kinematic rigid-body at the next timestep. +- Added `rigidBody.predictedRotation` to set the rotation of a kinematic rigid-body at the next timestep. +- Added `Ray` and `RayIntersection` structures for ray-casting. +- Added `world.castRay` to compute the first hit of a ray with the physics scene. +- Fix a bug causing a kinematic rigid-body not to teleport as expected after a `rigidBody.setPosition`. + +### v0.1.12 + +- Added `world.removeCollider(collider)` to remove a collider from the physics world. +- Added `colliderDesc.setTranslation(...)` to set the relative translation of the collider to build wrt. the rigid-body + it is attached to. +- Added `colliderDesc.setRotation(...)` to set the relative rotation of the collider to build wrt. the rigid-body it is + attached to. + +### v0.1.11 + +- Fix a bug causing a crash when the broad-phase proxy handles were recycled. + +### v0.1.10 + +- Fix a determinism problem that could cause rigid-body handle allocation to be non-deterministic after a snapshot + restoration. + +### v0.1.9 + +- Added `world.getCollider(handle)` that retrieves a collider from its integer handle. +- Added `joint.handle()` that returns the integer handle of the joint. + +### v0.1.8 + +- Added `world.forEachRigidBodyHandle(f)` to apply a closure on the integer handle of each rigid-body on the world. +- Added `world.forEachActiveRigidBody(f)` to apply a closure on each rigid-body on the world. +- Added `world.forEachActiveRigidBodyHandle(f)` to apply a closure on the integer handle of each rigid-body on the + world. +- Added `rigidBody.applyForce`, `.applyTorque`, `.applyImpulse`, `.applyTorqueImpulse`, `.applyForceAtPoint`, and + `.applyImpulseAtPoint` to apply a manual force or torque to a rigid-body. +- Added the `EventQueue` structure that can be used to collect and iterate through physics events. +- Added the `Proximity` enum that represents the proximity state of a sensor collider and another collider. +- Added the `world.stepWithEvents(eventQueue)` which executes a physics timestep and collects the physics events into + the given event queue. + +### v0.1.7 + +- Added `world.getRigidBody(handle)` to retrieve a rigid-body from its handle. +- Added `world.getJoint(handle)` to retrieve a joint from its handle. +- Added `rigidBody.rotation()` to retrieve its world-space orientation as a quaternion. +- Added `rigidBody.setTranslation(...)` to set the translation of a rigid-body. +- Added `rigidBody.setRotation(...)` to set the orientation of a rigid-body. +- Added `rigidBody.wakeUp()` to manually wake up a rigid-body. +- Added `rigidBody_desc.setRotation(...)` to set tho orientation of the rigid-body to be created. + +### v0.1.6 + +- Added `world.removeRigidBody(...)` to remove a rigid-body from the world. diff --git a/thirdparty/rapier.js/Cargo.lock b/thirdparty/rapier.js/Cargo.lock new file mode 100644 index 00000000..11154411 --- /dev/null +++ b/thirdparty/rapier.js/Cargo.lock @@ -0,0 +1,1711 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dimforge_rapier2d-deterministic" +version = "0.19.3" +dependencies = [ + "bincode", + "js-sys", + "nalgebra", + "palette", + "parry2d", + "rapier2d", + "ref-cast", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glam" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" + +[[package]] +name = "glam" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b" + +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" + +[[package]] +name = "glam" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34" + +[[package]] +name = "glam" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca" + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "glam" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "nalgebra" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" +dependencies = [ + "approx", + "glam 0.14.0", + "glam 0.15.2", + "glam 0.16.0", + "glam 0.17.3", + "glam 0.18.0", + "glam 0.19.0", + "glam 0.20.5", + "glam 0.21.3", + "glam 0.22.0", + "glam 0.23.0", + "glam 0.24.2", + "glam 0.25.0", + "glam 0.27.0", + "glam 0.28.0", + "glam 0.29.3", + "glam 0.30.9", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "serde", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parry2d" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef681740349cec3ab9b5996b03b459b383b6998e1ffcb2804e8b57eb1e8491d9" +dependencies = [ + "approx", + "arrayvec", + "bitflags", + "downcast-rs", + "either", + "ena", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap", + "log", + "nalgebra", + "num-derive", + "num-traits", + "ordered-float", + "serde", + "serde_arrays", + "simba", + "slab", + "smallvec", + "spade", + "thiserror", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prepare_builds" +version = "0.1.0" +dependencies = [ + "clap", + "clap_derive", + "tera", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rapier2d" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12a465faea032e025eb97a3b0763422391c3edb98200ed5efd516307df550" +dependencies = [ + "approx", + "arrayvec", + "bit-vec", + "bitflags", + "downcast-rs", + "log", + "nalgebra", + "num-derive", + "num-traits", + "ordered-float", + "parry2d", + "profiling", + "rustc-hash", + "serde", + "simba", + "static_assertions", + "thiserror", + "web-time", + "wide", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "libm", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spade" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990" +dependencies = [ + "hashbrown 0.15.5", + "num-traits", + "robust", + "serde", + "smallvec", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/thirdparty/rapier.js/Cargo.toml b/thirdparty/rapier.js/Cargo.toml new file mode 100644 index 00000000..6691d264 --- /dev/null +++ b/thirdparty/rapier.js/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = ["builds/*"] +resolver = "2" + +[profile.release] +debug = false +codegen-units = 1 +#lto = true +strip = true # Workaround for https://github.com/bevyengine/bevy/issues/16030 (the bug only happens in 2D) + +[patch.crates-io] +#rapier2d = { path = "../rapier/crates/rapier2d" } +#rapier3d = { path = "../rapier/crates/rapier3d" } +#parry2d = { path = "../parry/crates/parry2d" } +#parry3d = { path = "../parry/crates/parry3d" } +#nalgebra = { path = "../nalgebra" } +#simba = { path = "../simba" } + +#nalgebra = { git = "https://github.com/dimforge/nalgebra", branch = "dev" } +#rapier2d = { git = "https://github.com/dimforge/rapier", rev = "82416e3ca66dcdc34c0f350cec570ef1019a199f" } +#rapier3d = { git = "https://github.com/dimforge/rapier", rev = "82416e3ca66dcdc34c0f350cec570ef1019a199f" } +#parry2d = { git = "https://github.com/dimforge/parry" } +#parry3d = { git = "https://github.com/dimforge/parry" } diff --git a/thirdparty/rapier.js/LICENSE b/thirdparty/rapier.js/LICENSE new file mode 100644 index 00000000..e579674d --- /dev/null +++ b/thirdparty/rapier.js/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Dimforge EURL + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/thirdparty/rapier.js/README.md b/thirdparty/rapier.js/README.md new file mode 100644 index 00000000..d04cc4bf --- /dev/null +++ b/thirdparty/rapier.js/README.md @@ -0,0 +1,82 @@ +

+ crates.io +

+

+ + + + + Build status + + + + +

+

+ + Website | Documentation | + 2D examples (sources) | + 3D examples (sources) + +

+ +--- + +

+2D and 3D physics engines +for the JavaScript programming language (official bindings). +

+ +--- + +## Building packages manually + +From the root of the repository, run: + +```shell +./builds/prepare_builds/prepare_all_projects.sh +./builds/prepare_builds/build_all_projects.sh +``` + +Note that `prepare_all_projects.sh` only needs to be run once. It needs to be re-run if any file from the +`builds/prepare_builds` directory (and subdirectories) are modified. + +The built packages will be in `builds/rapier2d/pkg`, `builds/rapier3d/pkg`, etc. To build the `-compat` variant of the +packages, run `npm run build` in the `rapier-compat` directory. Note that this will only work if you already ran +`prepare_all_projects.sh`. The compat packages are then generated in, e.g., `rapier-compat/builds/3d/pkg`. + +## Feature selection + +Multiple NPM packages exist for Rapier, depending on your needs: +- [`@dimforge/rapier2d`](https://www.npmjs.com/package/@dimforge/rapier2d) or + [`@dimforge/rapier3d`](https://www.npmjs.com/package/@dimforge/rapier3d): + The main build of the Rapier physics engine for 2D or 3D physics simulation. This should have wide browser + support while offering great performances. This does **not** guarantee cross-platform determinism of the physics + simulation (but it is still locally deterministic, on the same machine). +- [`@dimforge/rapier2d-simd`](https://www.npmjs.com/package/@dimforge/rapier2d-simd) or + [`@dimforge/rapier3d-simd`](https://www.npmjs.com/package/@dimforge/rapier3d-simd): + A build with internal SIMD optimizations enabled. More limited browser support (requires support for [simd128](https://caniuse.com/?search=simd)). +- [`@dimforge/rapier2d-deterministic`](https://www.npmjs.com/package/@dimforge/rapier2d-deterministic) or + [`@dimforge/rapier3d-deterministic`](https://www.npmjs.com/package/@dimforge/rapier3d-deterministic): + A less optimized build but with a guarantee of a cross-platform deterministic execution of the physics simulation. + +## Bundler support + +Some bundlers will struggle with the `.wasm` file package into the builds above. Alternative `-compat` versions exist +which embed the `.wasm` file into the `.js` sources encoded with base64. This results in a bigger package size, but +much wider bundler support. + +Just append `-compat` to the build you are interested in: +[`rapier2d-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-compat), +[`rapier2d-simd-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-simd-compat), +[`rapier2d-deterministic-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-deterministic-compat), +[`rapier3d-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-compat), +[`rapier3d-simd-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-simd-compat), +[`rapier3d-deterministic-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-deterministic-compat). + +## Nightly builds + +Each time a new Pull Request is merged to the `main` branch of the [`rapier.js` repository](https://github.com/dimforge/rapier.js), +an automatic _canary_ build is triggered. Builds published to npmjs under the _canary_ tag does not come with any +stability guarantee and does not follow semver versioning. But it can be a useful solution to try out the latest +features until a proper release is cut. \ No newline at end of file diff --git a/thirdparty/rapier.js/builds/prepare_builds/Cargo.toml b/thirdparty/rapier.js/builds/prepare_builds/Cargo.toml new file mode 100644 index 00000000..905de28f --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "prepare_builds" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5" } +clap_derive = { version = "4.5" } +tera = "1.20" diff --git a/thirdparty/rapier.js/builds/prepare_builds/README.md b/thirdparty/rapier.js/builds/prepare_builds/README.md new file mode 100644 index 00000000..4d05a2fa --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/README.md @@ -0,0 +1,17 @@ +# Prepare builds + +This project helps with making specific package: it takes a few parameter to create a (1) folder ready to compile. + +It uses clap so you can pass `-h` to get more info about its parameters. + +## usage + +At workspace root: `cargo run -p prepare_builds -- -d dim2 -f simd`. + +Or use provided scripts: `./builds/prepare_builds/prepare_all_projects.sh && ./builds/prepare_builds/build_all_projects.sh` + +## Technical considerations + +Askama/rinja was not chosen because compiled templates make it difficult to iterate on a folder and parse all templates. + +This folder is in `builds/` only for the workspace member glob to not complain if `builds/` is inexistent or empty. diff --git a/thirdparty/rapier.js/builds/prepare_builds/build_all_projects.sh b/thirdparty/rapier.js/builds/prepare_builds/build_all_projects.sh new file mode 100644 index 00000000..a8ed7840 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/build_all_projects.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +for entry in builds/* +do + if [[ "$(basename "$entry")" == $(basename $(dirname "${BASH_SOURCE[0]}")) ]]; then + echo "skipping directory: $entry" + continue; + fi + ( + cd $entry + echo "building $entry" + # FIXME: ideally we'd use `npm ci` + # but we'd need to have generated the `package-lock` beforehand and committed them in the repository. + # I'm not sure yet how to store those `package-lock`s yet though. + # They should proably be similar to all packages, but I'm not sure. + npm i; + npm run build; + ) +done diff --git a/thirdparty/rapier.js/builds/prepare_builds/prepare_all_projects.sh b/thirdparty/rapier.js/builds/prepare_builds/prepare_all_projects.sh new file mode 100644 index 00000000..bed9c147 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/prepare_all_projects.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +features=(non-deterministic deterministic simd) +dims=(dim2 dim3) + +for feature in ${features[@]}; do + for dim in ${dims[@]}; do + echo "preparing dimension $dim with feature $feature" + cargo run -p prepare_builds -- -d ${dim} -f ${feature} + done +done diff --git a/thirdparty/rapier.js/builds/prepare_builds/src/main.rs b/thirdparty/rapier.js/builds/prepare_builds/src/main.rs new file mode 100644 index 00000000..56675e8e --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/src/main.rs @@ -0,0 +1,182 @@ +use std::{ + error::Error, + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, +}; + +use clap::Parser; +use clap_derive::{Parser, ValueEnum}; +use tera::{Context, Tera}; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Args { + /// Dimension to use + #[arg(short, long)] + dim: Dimension, + + /// Features to enable + #[arg(short, long)] + feature_set: FeatureSet, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum Dimension { + Dim2, + Dim3, +} + +#[derive(ValueEnum, Default, Debug, Clone, Copy)] +pub enum FeatureSet { + #[default] + NonDeterministic, + Deterministic, + Simd, +} + +/// Values to use when creating the new build folder. +pub struct BuildValues { + /// Only the number of dimensions, as sometimes it will be prefixed by "dim" and sometimes post-fixed by "d". + pub dim: String, + /// real name of the additional features to enable in the project + pub feature_set: Vec, + pub target_dir: PathBuf, + pub template_dir: PathBuf, + pub additional_rust_flags: String, + pub additional_wasm_opt_flags: Vec, + pub js_package_name: String, +} + +impl BuildValues { + pub fn new(args: Args) -> Self { + let dim = match args.dim { + Dimension::Dim2 => "2", + Dimension::Dim3 => "3", + }; + let feature_set = match args.feature_set { + FeatureSet::NonDeterministic => vec![], + FeatureSet::Deterministic => vec!["enhanced-determinism"], + FeatureSet::Simd => vec!["simd-stable"], + }; + let js_package_name = match args.feature_set { + FeatureSet::NonDeterministic => format!("rapier{dim}d"), + FeatureSet::Deterministic => format!("rapier{dim}d-deterministic"), + FeatureSet::Simd => format!("rapier{dim}d-simd"), + }; + + let root: PathBuf = env!("CARGO_MANIFEST_DIR").into(); + + Self { + dim: dim.to_string(), + feature_set: feature_set.iter().map(|f| f.to_string()).collect(), + template_dir: root.join("templates/").clone(), + target_dir: root.parent().unwrap().join(&js_package_name).into(), + additional_rust_flags: match args.feature_set { + FeatureSet::Simd => "RUSTFLAGS='-C target-feature=+simd128'".to_string(), + _ => "".to_string(), + }, + additional_wasm_opt_flags: match args.feature_set { + FeatureSet::Simd => vec!["--enable-simd".to_string()], + _ => vec![], + }, + js_package_name, + } + } +} + +fn main() { + let args = Args::parse(); + dbg!(&args); + + let build_values = BuildValues::new(args); + copy_top_level_files_in_directory(&build_values.template_dir, &build_values.target_dir) + .expect("Failed to copy directory"); + process_templates(&build_values).expect("Failed to process templates"); +} + +fn copy_top_level_files_in_directory( + src: impl AsRef, + dest: impl AsRef, +) -> std::io::Result<()> { + let src = src.as_ref(); + let dest = Path::new(&dest); + if dest.exists() { + fs::remove_dir_all(&dest)?; + } + fs::create_dir_all(&dest)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dest.join(path.file_name().unwrap()); + + if !path.is_dir() { + fs::copy(&path, &dest_path)?; + } + } + Ok(()) +} + +/// Process all tera templates in the target directory: +/// - Remove the extension +/// - Render the templates to +/// +fn process_templates(build_values: &BuildValues) -> std::io::Result<()> { + let target_dir = build_values.target_dir.clone(); + + let mut context = Context::new(); + context.insert("dimension", &build_values.dim); + context.insert("additional_features", &build_values.feature_set); + context.insert("additional_rust_flags", &build_values.additional_rust_flags); + context.insert( + "additional_wasm_opt_flags", + &build_values.additional_wasm_opt_flags, + ); + context.insert("js_package_name", &build_values.js_package_name); + + let tera = match Tera::new(target_dir.join("**/*.tera").to_str().unwrap()) { + Ok(t) => t, + Err(e) => { + eprintln!("Parsing error(s): {}", e); + ::std::process::exit(1); + } + }; + dbg!(tera.templates.keys(), &context); + + for entry in fs::read_dir(target_dir)? { + let entry = entry?; + let path = entry.path(); + // For tera templates, remove extension. + if path.extension() == Some(std::ffi::OsStr::new("tera")) { + let mut i = path.iter(); + + // Get path from target directory + for _ in i + .by_ref() + .take_while(|c| *c != build_values.target_dir.file_name().unwrap()) + {} + let path_template = i.as_path(); + match tera.render(path_template.to_str().unwrap(), &context) { + Ok(s) => { + let old_path = path.clone(); + let new_path = path.with_extension(""); + let mut file = File::create(path.join(new_path))?; + file.write_all(s.as_bytes())?; + std::fs::remove_file(old_path)?; + } + Err(e) => { + eprintln!("Error: {}", e); + let mut cause = e.source(); + while let Some(e) = cause { + eprintln!("Reason: {}", e); + cause = e.source(); + } + } + }; + } + } + + Ok(()) +} diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/Cargo.toml.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/Cargo.toml.tera new file mode 100644 index 00000000..af9756d1 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/Cargo.toml.tera @@ -0,0 +1,60 @@ +[package] +name = "dimforge_{{ js_package_name }}" # Can't be named rapier{{ dimension }}d which conflicts with the dependency. +version = "0.19.3" +authors = ["Sébastien Crozet "] +description = "{{ dimension }}-dimensional physics engine in Rust - official JS bindings." +documentation = "https://rapier.rs/rustdoc/rapier{{ dimension }}d/index.html" +homepage = "https://rapier.rs" +repository = "https://github.com/dimforge/rapier.js" +readme = "README.md" +keywords = ["physics", "dynamics", "rigid", "real-time", "joints"] +license = "Apache-2.0" +edition = "2018" + +[features] +default = ["dim{{ dimension }}"] +dim{{ dimension }} = [] + +[lib] +name = "rapier_wasm{{ dimension }}d" +path = "../../src/lib.rs" +crate-type = ["cdylib", "rlib"] +required-features = ["dim{{ dimension }}"] + +[lints] +rust.unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(feature, values("dim{% if dimension == "2" %}3{% else %}2{% endif %}"))', +] } + +[dependencies] +rapier{{ dimension }}d = { version = "0.30.1", features = [ + "serde-serialize", + "debug-render", + "profiler", + {%- for feature in additional_features %} + "{{ feature }}", + {%- endfor %} +] } +# The explicit dependency to parry only needed to pin the patch version. +parry{{ dimension }}d = { version = "^0.25.3" } +ref-cast = "1" +wasm-bindgen = "0.2.100" +js-sys = "0.3" +nalgebra = "0.34" +serde = { version = "1", features = ["derive", "rc"] } +bincode = "1" +palette = "0.7" + +[package.metadata.wasm-pack.profile.release] +# add -g to keep debug symbols +wasm-opt = [ + '-O4', + '--dce', + # The two options below are needed because of: https://github.com/rustwasm/wasm-pack/issues/1501 + '--enable-bulk-memory', + '--enable-nontrapping-float-to-int', + {%- for flag in additional_wasm_opt_flags %} + '{{ flag }}', + {%- endfor %} +] +#wasm-opt = ['-g'] diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/LICENSE b/thirdparty/rapier.js/builds/prepare_builds/templates/LICENSE new file mode 100644 index 00000000..e579674d --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Dimforge EURL + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/README.md.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/README.md.tera new file mode 100644 index 00000000..3455eab2 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/README.md.tera @@ -0,0 +1,70 @@ +

+ crates.io +

+

+ + + + + Build status + + + crates.io + + + npm version + + + + +

+

+ + Website | Documentation + +

+ +--- + +

+{{ dimension }}D physics engine +for the JavaScript programming language (official bindings). +

+ +--- + +## Feature selection + +Multiple NPM packages exist for Rapier, depending on your needs: +- [`@dimforge/rapier2d`](https://www.npmjs.com/package/@dimforge/rapier2d) or + [`@dimforge/rapier3d`](https://www.npmjs.com/package/@dimforge/rapier3d): + The main build of the Rapier physics engine for 2D or 3D physics simulation. This should have wide browser + support while offering great performances. This does **not** guarantee cross-platform determinism of the physics + simulation (but it is still locally deterministic, on the same machine). +- [`@dimforge/rapier2d-simd`](https://www.npmjs.com/package/@dimforge/rapier2d-simd) or + [`@dimforge/rapier3d-simd`](https://www.npmjs.com/package/@dimforge/rapier3d-simd): + A build with internal SIMD optimizations enabled. More limited browser support (requires support for [simd128](https://caniuse.com/?search=simd)). +- [`@dimforge/rapier2d-deterministic`](https://www.npmjs.com/package/@dimforge/rapier2d-deterministic) or + [`@dimforge/rapier3d-deterministic`](https://www.npmjs.com/package/@dimforge/rapier3d-deterministic): + A less optimized build but with a guarantee of a cross-platform deterministic execution of the physics simulation. + +## Bundler support + +Some bundlers will struggle with the `.wasm` file package into the builds above. Alternative `-compat` versions exist +which embed the `.wasm` file into the `.js` sources encoded with base64. This results in a bigger package size, but +much wider bundler support. + +Just append `-compat` to the build you are interested in: +[`rapier2d-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-compat), +[`rapier2d-simd-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-simd-compat), +[`rapier2d-deterministic-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-deterministic-compat), +[`rapier3d-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-compat), +[`rapier3d-simd-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-simd-compat), +[`rapier3d-deterministic-compat`](https://www.npmjs.com/package/@dimforge/rapier3d-deterministic-compat). + +## Nightly builds + +Each time a new Pull Request is merged to the `main` branch of the [`rapier.js` repository](https://github.com/dimforge/rapier.js), +an automatic _canary_ build is triggered. Builds published to npmjs under the _canary_ tag does not come with any +stability guarantee and does not follow semver versioning. But it can be a useful solution to try out the latest +features until a proper release is cut. \ No newline at end of file diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/build_rust.sh.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/build_rust.sh.tera new file mode 100644 index 00000000..6fd6ae82 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/build_rust.sh.tera @@ -0,0 +1,15 @@ +#!/bin/sh + +# Cleaning rust because changing rust flags may lead to different build results. +cargo clean + +{{ additional_rust_flags }} npx wasm-pack build +sed -i.bak 's#dimforge_rapier#@dimforge/rapier#g' pkg/package.json +sed -i.bak 's/"rapier_wasm{{ dimension }}d_bg.wasm"/"*"/g' pkg/package.json +( + cd pkg + npm pkg delete sideEffects + npm pkg set 'sideEffects[0]'="./*.js" +) +rm pkg/*.bak +rm pkg/.gitignore diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/build_typescript.sh.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/build_typescript.sh.tera new file mode 100644 index 00000000..8fcce98b --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/build_typescript.sh.tera @@ -0,0 +1,13 @@ +#! /bin/sh + +mkdir -p ./pkg/src +cp -r ../../src.ts/* pkg/src/. +rm -f ./pkg/raw.ts +echo 'export * from "./rapier_wasm{{ dimension }}d"' > pkg/src/raw.ts +# See https://serverfault.com/a/137848 +find pkg/ -type f -print0 | LC_ALL=C xargs -0 sed -i.bak '\:#if DIM{% if dimension == "2" %}3{% else %}2{% endif %}:,\:#endif:d' +npx tsc +# NOTE: we keep the typescripts files into the NPM package for source mapping: see #3 +sed -i.bak 's/"module": "rapier_wasm{{ dimension }}d.js"/"module": "rapier.js"/g' pkg/package.json +sed -i.bak 's/"types": "rapier_wasm{{ dimension }}d.d.ts"/"types": "rapier.d.ts"/g' pkg/package.json +find pkg/ -type f -name '*.bak' | xargs rm diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/package.json.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/package.json.tera new file mode 100644 index 00000000..95cc5f79 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/package.json.tera @@ -0,0 +1,25 @@ +{ + "name": "@dimforge/{{ js_package_name }}", + "description": "", + "private": true, + "exports": "./pkg", + "types": "./pkg/rapier.d.ts", + "scripts": { + "build": "npm run clean && npm run build:wasm && npm run build:ts && npm run build:doc", + "build:doc": "typedoc --tsconfig tsconfig_typedoc.json", + "build:wasm": "sh ./build_rust.sh", + "build:ts": "sh ./build_typescript.sh", + "clean": "rimraf pkg" + }, + "//": "Better keep rimraf version in sync with wasm-pack, see https://github.com/rustwasm/wasm-pack/issues/1444", + "devDependencies": { + "rimraf": "^3.0.2", + "typedoc": "^0.25.13" + }, + "dependencies": { + "wasm-pack": "^0.12.1" + }, + "sideEffects": [ + "./*.js" + ] +} \ No newline at end of file diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig.json b/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig.json new file mode 100644 index 00000000..ecdfc4ce --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "outDir": "./pkg", + "module": "ES6", + "target": "es6", + "lib": ["es6"], + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "rootDirs": ["./pkg", "./pkg/src"] + }, + "files": ["./pkg/src/rapier.ts"] +} diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig_typedoc.json b/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig_typedoc.json new file mode 100644 index 00000000..eea45699 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/tsconfig_typedoc.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "./pkg", + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "lib": ["es6"] + }, + "files": ["./pkg/rapier.d.ts"] +} diff --git a/thirdparty/rapier.js/builds/prepare_builds/templates/typedoc.json.tera b/thirdparty/rapier.js/builds/prepare_builds/templates/typedoc.json.tera new file mode 100644 index 00000000..d96bb7a6 --- /dev/null +++ b/thirdparty/rapier.js/builds/prepare_builds/templates/typedoc.json.tera @@ -0,0 +1,42 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": [ + "./pkg/rapier.d.ts" + ], + "out": "./docs", + "readme": "none", + "excludeExternals": true, + "excludeNotDocumented": false, + "intentionallyNotExported": [ + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawBroadPhase", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawCCDSolver", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawColliderSet", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawEventQueue", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawImpulseJointSet", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawMultibodyJointSet", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawIntegrationParameters", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawIslandManager", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawNarrowPhase", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawPhysicsPipeline", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawRigidBodySet", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawSerializationPipeline", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawContactManifold", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawShape", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawGenericJoint", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawJointAxis", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawRotation", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawVector", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawPointColliderProjection", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawPointProjection", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawRayColliderIntersection", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawRayColliderHit", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawRayIntersection", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawColliderShapeCastHit", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawShapeContact", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawShapeCastHit", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawQueryPipeline", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawDeserializedWorld", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawDebugRenderPipeline", + "pkg/rapier_wasm{{ dimension }}d.d.ts:RawContactForceEvent" + ] +} \ No newline at end of file diff --git a/thirdparty/rapier.js/package.json b/thirdparty/rapier.js/package.json new file mode 100644 index 00000000..ce0eec04 --- /dev/null +++ b/thirdparty/rapier.js/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "fmt": "prettier ." + }, + "devDependencies": { + "prettier": "2.7.1", + "typedoc": "0.23.19", + "typescript": "4.8.4", + "wasm-opt": "1.4.0", + "wasm-pack": "0.12.1" + } +} diff --git a/thirdparty/rapier.js/publish_all_canary.sh b/thirdparty/rapier.js/publish_all_canary.sh new file mode 100644 index 00000000..21155429 --- /dev/null +++ b/thirdparty/rapier.js/publish_all_canary.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +for entry in builds/*/pkg +do + ( + echo "Publishing $entry" + cd $entry; npm version 0.0.0-$(git rev-parse --short HEAD)-$(date '+%Y%m%d') --git-tag-version false; + npm publish --tag canary --access public; + ) +done; + +for entry in rapier-compat/builds/*/pkg +do + ( + echo "Publishing $entry" + cd $entry; npm version 0.0.0-$(git rev-parse --short HEAD)-$(date '+%Y%m%d') --git-tag-version false; + npm publish --tag canary --access public; + ) +done; \ No newline at end of file diff --git a/thirdparty/rapier.js/publish_all_prod.sh b/thirdparty/rapier.js/publish_all_prod.sh new file mode 100644 index 00000000..f04915cb --- /dev/null +++ b/thirdparty/rapier.js/publish_all_prod.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +for entry in builds/*/pkg +do + ( + echo "Publishing $entry" + cd $entry; + npm publish --access public; + ) +done; + +for entry in rapier-compat/builds/*/pkg +do + ( + echo "Publishing $entry" + cd $entry; + npm publish --access public; + ) +done; \ No newline at end of file diff --git a/thirdparty/rapier.js/rapier-compat/README.md b/thirdparty/rapier.js/rapier-compat/README.md new file mode 100644 index 00000000..5d440ec2 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/README.md @@ -0,0 +1,16 @@ +# tsconfig json files + +- tsconfig.common.json - shared TypeScript options +- tsconfig.pkg2d.json - config for compiling rapier2d-compat +- tsconfig.pkg3d.json - config for compiling rapier3d-compat +- tconfig.json - for IDE (VSCode) and unit tests. Includes Jest types. + +## Generation steps + +Check `./package.json scripts`, which are used by CI. + +Summary: + +- build rust wasm projects into their dedicated folder in `./builds/` +- copy common javascript and generate dimension specific javascript into a common `./pkg` folder +- copy that `pkg` folder in each folder from `./builds` diff --git a/thirdparty/rapier.js/rapier-compat/build-rust.sh b/thirdparty/rapier.js/rapier-compat/build-rust.sh new file mode 100644 index 00000000..2acd18f8 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/build-rust.sh @@ -0,0 +1,50 @@ +#!/bin/bash + + +help() +{ + printf "Usage: %s: [-d 2|3] [-f deterministim|non-deterministic|simd]\n" $0 +} + +while getopts :d:f: name +do + case $name in + d) + dimension="$OPTARG";; + f) + feature="$OPTARG";; + ?) help ; exit 1;; + esac +done + +if [[ -z "$dimension" ]]; then + help; exit 2; +fi +if [[ -z "$feature" ]]; then + help; exit 3; +fi + +if [[ $feature == "non-deterministic" ]]; then + feature_postfix="" +else + feature_postfix="-${feature}" +fi + +rust_source_directory="../builds/rapier${dimension}d${feature_postfix}" + +if [ ! -d "$rust_source_directory" ]; then + echo "Directory $rust_source_directory does not exist"; + echo "You may want to generate rust projects first."; + help + exit 4; +fi + +# Working dir in wasm-pack is the project root so we need that "../../" + +if [[ $feature == "simd" ]]; then + export additional_rustflags='-C target-feature=+simd128' +else + export additional_rustflags='' +fi + +RUSTFLAGS="${additional_rustflags}" wasm-pack --verbose build --target web --out-dir "../../rapier-compat/builds/${dimension}d${feature_postfix}/wasm-build" "$rust_source_directory" diff --git a/thirdparty/rapier.js/rapier-compat/fix_raw_file.sh b/thirdparty/rapier.js/rapier-compat/fix_raw_file.sh new file mode 100644 index 00000000..66f8bfaf --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/fix_raw_file.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +for feature in \ +2d 2d-deterministic 2d-simd \ +3d 3d-deterministic 3d-simd +do + +echo 'export * from "'"./rapier_wasm$feature"'"' > builds/${feature}/pkg/raw.d.ts +echo 'export * from "'"./rapier_wasm$feature"'"' > builds/${feature}/pkg/raw.d.ts + +done; \ No newline at end of file diff --git a/thirdparty/rapier.js/rapier-compat/gen_src.sh b/thirdparty/rapier.js/rapier-compat/gen_src.sh new file mode 100644 index 00000000..24ccc441 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/gen_src.sh @@ -0,0 +1,58 @@ +# Copy source and remove #if sections - similar to script in ../rapierXd +set -e + +gen_js() { + DIM=$1 + GENOUT="./gen${DIM}" + + # Make output directories + mkdir -p ${GENOUT} + + # Copy common sources + cp -r ../src.ts/* $GENOUT + + # Copy compat mode override sources + rm -f "${GENOUT}/raw.ts" "${GENOUT}/init.ts" + cp -r ./src${DIM}/* $GENOUT +} + +gen_js "2d" +gen_js "3d" + +# See https://serverfault.com/a/137848 +find gen2d/ -type f -print0 | LC_ALL=C xargs -0 sed -i.bak '\:#if DIM3:,\:#endif:d' +find gen3d/ -type f -print0 | LC_ALL=C xargs -0 sed -i.bak '\:#if DIM2:,\:#endif:d' + +# Clean up backup files. +find gen2d/ -type f -name '*.bak' | xargs rm +find gen3d/ -type f -name '*.bak' | xargs rm + +for features_set in \ +"2" "2 deterministic" "2 simd" \ +"3" "3 deterministic" "3 simd" +do + + set -- $features_set # Convert the "tuple" into the param args $1 $2... + dimension=$1 + if [ -z "$2" ]; then + feature="${1}d"; + else + feature="${1}d-${2}"; + fi + + mkdir -p ./builds/${feature}/pkg/ + + cp ./builds/${feature}/wasm-build/rapier_wasm* ./builds/${feature}/pkg/ + cp -r ./gen${dimension}d ./builds/${feature}/ + + # copy tsconfig, as they contain paths + cp ./tsconfig.common.json ./tsconfig.json ./builds/${feature}/ + cp ./tsconfig.pkg${dimension}d.json ./builds/${feature}/tsconfig.pkg.json + + # "import.meta" causes Babel to choke, but the code path is never taken so just remove it. + sed -i.bak 's/import.meta.url/""/g' ./builds/${feature}/pkg/rapier_wasm${dimension}d.js + + # Clean up backup files. + find ./builds/${feature}/pkg/ -type f -name '*.bak' | xargs rm + +done diff --git a/thirdparty/rapier.js/rapier-compat/package.json b/thirdparty/rapier.js/rapier-compat/package.json new file mode 100644 index 00000000..f97e9cf4 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/package.json @@ -0,0 +1,59 @@ +{ + "name": "build-rapier", + "description": "Build scripts for compatibility package with inlined webassembly as base64.", + "private": true, + "scripts": { + "build-rust-2d-non-deterministic": "./build-rust.sh -f non-deterministic -d 2", + "build-rust-2d-deterministic": "./build-rust.sh -f deterministic -d 2", + "build-rust-2d-simd": "./build-rust.sh -f simd -d 2", + "build-rust-2d-all": "npm run build-rust-2d-non-deterministic && npm run build-rust-2d-deterministic && npm run build-rust-2d-simd", + "build-rust-3d-non-deterministic": "./build-rust.sh -f non-deterministic -d 3", + "build-rust-3d-deterministic": "./build-rust.sh -f deterministic -d 3", + "build-rust-3d-simd": "./build-rust.sh -f simd -d 3", + "build-rust-3d-all": "npm run build-rust-3d-non-deterministic && npm run build-rust-3d-deterministic && npm run build-rust-3d-simd", + "build-rust-all": "npm run build-rust-2d-all && npm run build-rust-3d-all", + "build-genjs": "sh ./gen_src.sh", + "build-js": "rollup --config rollup.config.js --bundleConfigAsCjs", + "fix-raw-file": "sh ./fix_raw_file.sh", + "build": "npm run clean && npm run build-rust-all && npm run build-genjs && npm run build-js && npm run fix-raw-file", + "clean": "rimraf gen2d gen3d builds", + "test": "jest --detectOpenHandles", + "all": "npm run build" + }, + "dependencies": { + "base64-js": "^1.5.1" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "@rollup/plugin-terser": "0.1.0", + "@types/jest": "^29.2.1", + "jest": "^29.2.2", + "rimraf": "^3.0.2", + "rollup": "^3.2.5", + "rollup-plugin-base64": "^1.0.1", + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-filesize": "^9.1.2", + "ts-jest": "^29.0.3", + "tslib": "^2.4.1", + "typescript": "^4.8.4" + }, + "jest": { + "roots": [ + "/tests" + ], + "preset": "ts-jest", + "collectCoverageFrom": [ + "builds/**/pkg/**/*.js" + ], + "transformIgnorePatterns": [ + "[/\\\\]node_modules[/\\\\].+\\.(js|ts)$", + "builds/.*/[/\\\\]pkg[/\\\\].+\\.(js|ts)$" + ], + "moduleFileExtensions": [ + "ts", + "js" + ] + } +} diff --git a/thirdparty/rapier.js/rapier-compat/rollup.config.js b/thirdparty/rapier.js/rapier-compat/rollup.config.js new file mode 100644 index 00000000..d127c671 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/rollup.config.js @@ -0,0 +1,85 @@ +import commonjs from "@rollup/plugin-commonjs"; +import {nodeResolve} from "@rollup/plugin-node-resolve"; +import typescript from "@rollup/plugin-typescript"; +import terser from "@rollup/plugin-terser"; +import path from "path"; +import {base64} from "rollup-plugin-base64"; +import copy from "rollup-plugin-copy"; +import filesize from "rollup-plugin-filesize"; + +const config = (dim, features_postfix) => ({ + input: `builds/${features_postfix}/gen${dim}/rapier.ts`, + output: [ + { + file: `builds/${features_postfix}/pkg/rapier.mjs`, + format: "es", + sourcemap: true, + exports: "named", + }, + { + file: `builds/${features_postfix}/pkg/rapier.cjs`, + format: "cjs", + sourcemap: true, + exports: "named", + }, + ], + plugins: [ + copy({ + targets: [ + { + src: `builds/${features_postfix}/wasm-build/package.json`, + dest: `builds/${features_postfix}/pkg/`, + transform(content) { + let config = JSON.parse(content.toString()); + config.name = `@dimforge/rapier${features_postfix}-compat`; + config.description += + " Compatibility package with inlined webassembly as base64."; + config.types = "rapier.d.ts"; + config.main = "rapier.cjs"; + config.module = "rapier.mjs"; + config.exports = { + ".": { + types: "./rapier.d.ts", + require: "./rapier.cjs", + import: "./rapier.mjs", + }, + }; + // delete config.module; + config.files = ["*"]; + return JSON.stringify(config, undefined, 2); + }, + }, + { + src: `../rapier${features_postfix}/LICENSE`, + dest: `builds/${features_postfix}/pkg`, + }, + { + src: `../rapier${features_postfix}/README.md`, + dest: `builds/${features_postfix}/pkg`, + }, + ], + }), + base64({include: "**/*.wasm"}), + terser(), + nodeResolve(), + commonjs(), + typescript({ + tsconfig: path.resolve( + __dirname, + `builds/${features_postfix}/tsconfig.pkg.json`, + ), + sourceMap: true, + inlineSources: true, + }), + filesize(), + ], +}); + +export default [ + config("2d", "2d"), + config("2d", "2d-deterministic"), + config("2d", "2d-simd"), + config("3d", "3d"), + config("3d", "3d-deterministic"), + config("3d", "3d-simd"), +]; diff --git a/thirdparty/rapier.js/rapier-compat/src2d/init.ts b/thirdparty/rapier.js/rapier-compat/src2d/init.ts new file mode 100644 index 00000000..c1f2b3f2 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/src2d/init.ts @@ -0,0 +1,60 @@ +/** + * RAPIER initialization module with dynamic WASM loading support. + * RAPIER 初始化模块,支持动态 WASM 加载。 + */ + +import wasmInit from "../pkg/rapier_wasm2d"; + +/** + * Input types for WASM initialization. + * WASM 初始化的输入类型。 + */ +export type InitInput = + | RequestInfo // URL string or Request object + | URL // URL object + | Response // Fetch Response object + | BufferSource // ArrayBuffer or TypedArray + | WebAssembly.Module; // Pre-compiled module + +let initialized = false; + +/** + * Initializes RAPIER. + * Has to be called and awaited before using any library methods. + * + * 初始化 RAPIER。 + * 必须在使用任何库方法之前调用并等待。 + * + * @param input - WASM source (required). Can be URL, Response, ArrayBuffer, etc. + * WASM 源(必需)。可以是 URL、Response、ArrayBuffer 等。 + * + * @example + * // Load from URL | 从 URL 加载 + * await RAPIER.init('wasm/rapier_wasm2d_bg.wasm'); + * + * @example + * // Load from fetch response | 从 fetch 响应加载 + * const response = await fetch('wasm/rapier_wasm2d_bg.wasm'); + * await RAPIER.init(response); + * + * @example + * // Load from ArrayBuffer | 从 ArrayBuffer 加载 + * const buffer = await fetch('wasm/rapier_wasm2d_bg.wasm').then(r => r.arrayBuffer()); + * await RAPIER.init(buffer); + */ +export async function init(input?: InitInput): Promise { + if (initialized) { + return; + } + + await wasmInit(input); + initialized = true; +} + +/** + * Check if RAPIER is already initialized. + * 检查 RAPIER 是否已初始化。 + */ +export function isInitialized(): boolean { + return initialized; +} diff --git a/thirdparty/rapier.js/rapier-compat/src2d/raw.ts b/thirdparty/rapier.js/rapier-compat/src2d/raw.ts new file mode 100644 index 00000000..4eadaffa --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/src2d/raw.ts @@ -0,0 +1 @@ +export * from "../pkg/rapier_wasm2d"; diff --git a/thirdparty/rapier.js/rapier-compat/src3d/init.ts b/thirdparty/rapier.js/rapier-compat/src3d/init.ts new file mode 100644 index 00000000..15b14ada --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/src3d/init.ts @@ -0,0 +1,12 @@ +// @ts-ignore +import wasmBase64 from "../pkg/rapier_wasm3d_bg.wasm"; +import wasmInit from "../pkg/rapier_wasm3d"; +import base64 from "base64-js"; + +/** + * Initializes RAPIER. + * Has to be called and awaited before using any library methods. + */ +export async function init() { + await wasmInit(base64.toByteArray(wasmBase64 as unknown as string).buffer); +} diff --git a/thirdparty/rapier.js/rapier-compat/src3d/raw.ts b/thirdparty/rapier.js/rapier-compat/src3d/raw.ts new file mode 100644 index 00000000..b123ae6b --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/src3d/raw.ts @@ -0,0 +1 @@ +export * from "../pkg/rapier_wasm3d"; diff --git a/thirdparty/rapier.js/rapier-compat/tests/World2d.test.ts b/thirdparty/rapier.js/rapier-compat/tests/World2d.test.ts new file mode 100644 index 00000000..45a3bb72 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tests/World2d.test.ts @@ -0,0 +1,23 @@ +import {init, Vector2, World} from "../builds/2d-deterministic/pkg"; + +describe("2d/World", () => { + let world: World; + + beforeAll(init); + + afterAll(async () => { + await Promise.resolve(); + }); + + beforeEach(() => { + world = new World(new Vector2(0, 9.8)); + }); + + afterEach(() => { + world.free(); + }); + + test("constructor", () => { + expect(world.colliders.len()).toBe(0); + }); +}); diff --git a/thirdparty/rapier.js/rapier-compat/tests/World3d.test.ts b/thirdparty/rapier.js/rapier-compat/tests/World3d.test.ts new file mode 100644 index 00000000..e0ddd546 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tests/World3d.test.ts @@ -0,0 +1,23 @@ +import {init, Vector3, World} from "../builds/3d-deterministic/pkg"; + +describe("3d/World", () => { + let world: World; + + beforeAll(init); + + afterAll(async () => { + await Promise.resolve(); + }); + + beforeEach(() => { + world = new World(new Vector3(0, 9.8, 0)); + }); + + afterEach(() => { + world.free(); + }); + + test("constructor", () => { + expect(world.colliders.len()).toBe(0); + }); +}); diff --git a/thirdparty/rapier.js/rapier-compat/tests/math2d.test.ts b/thirdparty/rapier.js/rapier-compat/tests/math2d.test.ts new file mode 100644 index 00000000..a7560565 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tests/math2d.test.ts @@ -0,0 +1,15 @@ +import {Vector2, VectorOps} from "../builds/2d-deterministic/pkg"; + +describe("2d/math", () => { + test("Vector2", () => { + const v = new Vector2(0, 1); + expect(v.x).toBe(0); + expect(v.y).toBe(1); + }); + + test("VectorOps", () => { + const v = VectorOps.new(0, 1); + expect(v.x).toBe(0); + expect(v.y).toBe(1); + }); +}); diff --git a/thirdparty/rapier.js/rapier-compat/tests/math3d.test.ts b/thirdparty/rapier.js/rapier-compat/tests/math3d.test.ts new file mode 100644 index 00000000..2ff194cc --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tests/math3d.test.ts @@ -0,0 +1,17 @@ +import {Vector3, VectorOps} from "../builds/3d-deterministic/pkg"; + +describe("3d/math", () => { + test("Vector3", () => { + const v = new Vector3(0, 1, 2); + expect(v.x).toBe(0); + expect(v.y).toBe(1); + expect(v.z).toBe(2); + }); + + test("VectorOps", () => { + const v = VectorOps.new(0, 1, 2); + expect(v.x).toBe(0); + expect(v.y).toBe(1); + expect(v.z).toBe(2); + }); +}); diff --git a/thirdparty/rapier.js/rapier-compat/tsconfig.common.json b/thirdparty/rapier.js/rapier-compat/tsconfig.common.json new file mode 100644 index 00000000..37bd7d7f --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tsconfig.common.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ES6", + "target": "es6", + "noEmitOnError": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "lib": ["es6", "DOM"], + "moduleResolution": "node", + "esModuleInterop": true, + "sourceMap": true, + "declaration": true + } +} diff --git a/thirdparty/rapier.js/rapier-compat/tsconfig.json b/thirdparty/rapier.js/rapier-compat/tsconfig.json new file mode 100644 index 00000000..beeca944 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.common.json", + "compilerOptions": { + "lib": ["es6", "DOM"], + "esModuleInterop": true, + "types": ["jest"], + "outDir": "./dist" + }, + "include": ["./tests/**/*.test.ts"] +} diff --git a/thirdparty/rapier.js/rapier-compat/tsconfig.pkg2d.json b/thirdparty/rapier.js/rapier-compat/tsconfig.pkg2d.json new file mode 100644 index 00000000..77349ed2 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tsconfig.pkg2d.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.common.json", + "compilerOptions": { + "rootDir": "./gen2d", + "outDir": "./2d" + }, + "files": ["./gen2d/rapier.ts"] +} diff --git a/thirdparty/rapier.js/rapier-compat/tsconfig.pkg3d.json b/thirdparty/rapier.js/rapier-compat/tsconfig.pkg3d.json new file mode 100644 index 00000000..3d7323e8 --- /dev/null +++ b/thirdparty/rapier.js/rapier-compat/tsconfig.pkg3d.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.common.json", + "compilerOptions": { + "rootDir": "./gen3d", + "outDir": "." + }, + "files": ["./gen3d/rapier.ts"] +} diff --git a/thirdparty/rapier.js/src.ts/coarena.ts b/thirdparty/rapier.js/src.ts/coarena.ts new file mode 100644 index 00000000..9bf978a5 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/coarena.ts @@ -0,0 +1,70 @@ +export class Coarena { + fconv: Float64Array; + uconv: Uint32Array; + data: Array; + size: number; + + public constructor() { + this.fconv = new Float64Array(1); + this.uconv = new Uint32Array(this.fconv.buffer); + this.data = new Array(); + this.size = 0; + } + + public set(handle: number, data: T) { + let i = this.index(handle); + while (this.data.length <= i) { + this.data.push(null); + } + + if (this.data[i] == null) this.size += 1; + this.data[i] = data; + } + + public len(): number { + return this.size; + } + + public delete(handle: number) { + let i = this.index(handle); + if (i < this.data.length) { + if (this.data[i] != null) this.size -= 1; + this.data[i] = null; + } + } + + public clear() { + this.data = new Array(); + } + + public get(handle: number): T | null { + let i = this.index(handle); + if (i < this.data.length) { + return this.data[i]; + } else { + return null; + } + } + + public forEach(f: (elt: T) => void) { + for (const elt of this.data) { + if (elt != null) f(elt); + } + } + + public getAll(): Array { + return this.data.filter((elt) => elt != null); + } + + private index(handle: number): number { + /// Extracts the index part of a handle (the lower 32 bits). + /// This is done by first injecting the handle into an Float64Array + /// which is itself injected into an Uint32Array (at construction time). + /// The 0-th value of the Uint32Array will become the `number` integer + /// representation of the lower 32 bits. + /// Also `this.uconv[1]` then contains the generation number as a `number`, + /// which we don’t really need. + this.fconv[0] = handle; + return this.uconv[0]; + } +} diff --git a/thirdparty/rapier.js/src.ts/control/character_controller.ts b/thirdparty/rapier.js/src.ts/control/character_controller.ts new file mode 100644 index 00000000..8e536392 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/control/character_controller.ts @@ -0,0 +1,386 @@ +import {RawKinematicCharacterController, RawCharacterCollision} from "../raw"; +import {Rotation, Vector, VectorOps} from "../math"; +import { + BroadPhase, + Collider, + ColliderSet, + InteractionGroups, + NarrowPhase, + Shape, +} from "../geometry"; +import {QueryFilterFlags, World} from "../pipeline"; +import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics"; + +/** + * A collision between the character and an obstacle hit on its path. + */ +export class CharacterCollision { + /** The collider involved in the collision. Null if the collider no longer exists in the physics world. */ + public collider: Collider | null; + /** The translation delta applied to the character before this collision took place. */ + public translationDeltaApplied: Vector; + /** The translation delta the character would move after this collision if there is no other obstacles. */ + public translationDeltaRemaining: Vector; + /** The time-of-impact between the character and the obstacles. */ + public toi: number; + /** The world-space contact point on the collider when the collision happens. */ + public witness1: Vector; + /** The local-space contact point on the character when the collision happens. */ + public witness2: Vector; + /** The world-space outward contact normal on the collider when the collision happens. */ + public normal1: Vector; + /** The local-space outward contact normal on the character when the collision happens. */ + public normal2: Vector; +} + +/** + * A character controller for controlling kinematic bodies and parentless colliders by hitting + * and sliding against obstacles. + */ +export class KinematicCharacterController { + private raw: RawKinematicCharacterController; + private rawCharacterCollision: RawCharacterCollision; + + private params: IntegrationParameters; + private broadPhase: BroadPhase; + private narrowPhase: NarrowPhase; + private bodies: RigidBodySet; + private colliders: ColliderSet; + private _applyImpulsesToDynamicBodies: boolean; + private _characterMass: number | null; + + constructor( + offset: number, + params: IntegrationParameters, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ) { + this.params = params; + this.bodies = bodies; + this.colliders = colliders; + this.broadPhase = broadPhase; + this.narrowPhase = narrowPhase; + this.raw = new RawKinematicCharacterController(offset); + this.rawCharacterCollision = new RawCharacterCollision(); + this._applyImpulsesToDynamicBodies = false; + this._characterMass = null; + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + this.rawCharacterCollision.free(); + } + + this.raw = undefined; + this.rawCharacterCollision = undefined; + } + + /** + * The direction that goes "up". Used to determine where the floor is, and the floor’s angle. + */ + public up(): Vector { + return this.raw.up(); + } + + /** + * Sets the direction that goes "up". Used to determine where the floor is, and the floor’s angle. + */ + public setUp(vector: Vector) { + let rawVect = VectorOps.intoRaw(vector); + return this.raw.setUp(rawVect); + rawVect.free(); + } + + public applyImpulsesToDynamicBodies(): boolean { + return this._applyImpulsesToDynamicBodies; + } + + public setApplyImpulsesToDynamicBodies(enabled: boolean) { + this._applyImpulsesToDynamicBodies = enabled; + } + + /** + * Returns the custom value of the character mass, if it was set by `this.setCharacterMass`. + */ + public characterMass(): number | null { + return this._characterMass; + } + + /** + * Set the mass of the character to be used for impulse resolution if `self.applyImpulsesToDynamicBodies` + * is set to `true`. + * + * If no character mass is set explicitly (or if it is set to `null`) it is automatically assumed to be equal + * to the mass of the rigid-body the character collider is attached to; or equal to 0 if the character collider + * isn’t attached to any rigid-body. + * + * @param mass - The mass to set. + */ + public setCharacterMass(mass: number | null) { + this._characterMass = mass; + } + + /** + * A small gap to preserve between the character and its surroundings. + * + * This value should not be too large to avoid visual artifacts, but shouldn’t be too small + * (must not be zero) to improve numerical stability of the character controller. + */ + public offset(): number { + return this.raw.offset(); + } + + /** + * Sets a small gap to preserve between the character and its surroundings. + * + * This value should not be too large to avoid visual artifacts, but shouldn’t be too small + * (must not be zero) to improve numerical stability of the character controller. + */ + public setOffset(value: number) { + this.raw.setOffset(value); + } + + /// Increase this number if your character appears to get stuck when sliding against surfaces. + /// + /// This is a small distance applied to the movement toward the contact normals of shapes hit + /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating + /// state during the sliding calculation. + /// + /// This value should remain fairly small since it can introduce artificial "bumps" when sliding + /// along a flat surface. + public normalNudgeFactor(): number { + return this.raw.normalNudgeFactor(); + } + + /// Increase this number if your character appears to get stuck when sliding against surfaces. + /// + /// This is a small distance applied to the movement toward the contact normals of shapes hit + /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating + /// state during the sliding calculation. + /// + /// This value should remain fairly small since it can introduce artificial "bumps" when sliding + /// along a flat surface. + public setNormalNudgeFactor(value: number) { + this.raw.setNormalNudgeFactor(value); + } + + /** + * Is sliding against obstacles enabled? + */ + public slideEnabled(): boolean { + return this.raw.slideEnabled(); + } + + /** + * Enable or disable sliding against obstacles. + */ + public setSlideEnabled(enabled: boolean) { + this.raw.setSlideEnabled(enabled); + } + + /** + * The maximum step height a character can automatically step over. + */ + public autostepMaxHeight(): number | null { + return this.raw.autostepMaxHeight(); + } + + /** + * The minimum width of free space that must be available after stepping on a stair. + */ + public autostepMinWidth(): number | null { + return this.raw.autostepMinWidth(); + } + + /** + * Can the character automatically step over dynamic bodies too? + */ + public autostepIncludesDynamicBodies(): boolean | null { + return this.raw.autostepIncludesDynamicBodies(); + } + + /** + * Is automatically stepping over small objects enabled? + */ + public autostepEnabled(): boolean { + return this.raw.autostepEnabled(); + } + + /** + * Enabled automatically stepping over small objects. + * + * @param maxHeight - The maximum step height a character can automatically step over. + * @param minWidth - The minimum width of free space that must be available after stepping on a stair. + * @param includeDynamicBodies - Can the character automatically step over dynamic bodies too? + */ + public enableAutostep( + maxHeight: number, + minWidth: number, + includeDynamicBodies: boolean, + ) { + this.raw.enableAutostep(maxHeight, minWidth, includeDynamicBodies); + } + + /** + * Disable automatically stepping over small objects. + */ + public disableAutostep() { + return this.raw.disableAutostep(); + } + + /** + * The maximum angle (radians) between the floor’s normal and the `up` vector that the + * character is able to climb. + */ + public maxSlopeClimbAngle(): number { + return this.raw.maxSlopeClimbAngle(); + } + + /** + * Sets the maximum angle (radians) between the floor’s normal and the `up` vector that the + * character is able to climb. + */ + public setMaxSlopeClimbAngle(angle: number) { + this.raw.setMaxSlopeClimbAngle(angle); + } + + /** + * The minimum angle (radians) between the floor’s normal and the `up` vector before the + * character starts to slide down automatically. + */ + public minSlopeSlideAngle(): number { + return this.raw.minSlopeSlideAngle(); + } + + /** + * Sets the minimum angle (radians) between the floor’s normal and the `up` vector before the + * character starts to slide down automatically. + */ + public setMinSlopeSlideAngle(angle: number) { + this.raw.setMinSlopeSlideAngle(angle); + } + + /** + * If snap-to-ground is enabled, should the character be automatically snapped to the ground if + * the distance between the ground and its feet are smaller than the specified threshold? + */ + public snapToGroundDistance(): number | null { + return this.raw.snapToGroundDistance(); + } + + /** + * Enables automatically snapping the character to the ground if the distance between + * the ground and its feet are smaller than the specified threshold. + */ + public enableSnapToGround(distance: number) { + this.raw.enableSnapToGround(distance); + } + + /** + * Disables automatically snapping the character to the ground. + */ + public disableSnapToGround() { + this.raw.disableSnapToGround(); + } + + /** + * Is automatically snapping the character to the ground enabled? + */ + public snapToGroundEnabled(): boolean { + return this.raw.snapToGroundEnabled(); + } + + /** + * Computes the movement the given collider is able to execute after hitting and sliding on obstacles. + * + * @param collider - The collider to move. + * @param desiredTranslationDelta - The desired collider movement. + * @param filterFlags - Flags for excluding whole subsets of colliders from the obstacles taken into account. + * @param filterGroups - Groups for excluding colliders with incompatible collision groups from the obstacles + * taken into account. + * @param filterPredicate - Any collider for which this closure returns `false` will be excluded from the + * obstacles taken into account. + */ + public computeColliderMovement( + collider: Collider, + desiredTranslationDelta: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterPredicate?: (collider: Collider) => boolean, + ) { + let rawTranslationDelta = VectorOps.intoRaw(desiredTranslationDelta); + this.raw.computeColliderMovement( + this.params.dt, + this.broadPhase.raw, + this.narrowPhase.raw, + this.bodies.raw, + this.colliders.raw, + collider.handle, + rawTranslationDelta, + this._applyImpulsesToDynamicBodies, + this._characterMass, + filterFlags, + filterGroups, + this.colliders.castClosure(filterPredicate), + ); + rawTranslationDelta.free(); + } + + /** + * The movement computed by the last call to `this.computeColliderMovement`. + */ + public computedMovement(): Vector { + return VectorOps.fromRaw(this.raw.computedMovement()); + } + + /** + * The result of ground detection computed by the last call to `this.computeColliderMovement`. + */ + public computedGrounded(): boolean { + return this.raw.computedGrounded(); + } + + /** + * The number of collisions against obstacles detected along the path of the last call + * to `this.computeColliderMovement`. + */ + public numComputedCollisions(): number { + return this.raw.numComputedCollisions(); + } + + /** + * Returns the collision against one of the obstacles detected along the path of the last + * call to `this.computeColliderMovement`. + * + * @param i - The i-th collision will be returned. + * @param out - If this argument is set, it will be filled with the collision information. + */ + public computedCollision( + i: number, + out?: CharacterCollision, + ): CharacterCollision | null { + if (!this.raw.computedCollision(i, this.rawCharacterCollision)) { + return null; + } else { + let c = this.rawCharacterCollision; + out = out ?? new CharacterCollision(); + out.translationDeltaApplied = VectorOps.fromRaw( + c.translationDeltaApplied(), + ); + out.translationDeltaRemaining = VectorOps.fromRaw( + c.translationDeltaRemaining(), + ); + out.toi = c.toi(); + out.witness1 = VectorOps.fromRaw(c.worldWitness1()); + out.witness2 = VectorOps.fromRaw(c.worldWitness2()); + out.normal1 = VectorOps.fromRaw(c.worldNormal1()); + out.normal2 = VectorOps.fromRaw(c.worldNormal2()); + out.collider = this.colliders.get(c.handle()); + return out; + } + } +} diff --git a/thirdparty/rapier.js/src.ts/control/index.ts b/thirdparty/rapier.js/src.ts/control/index.ts new file mode 100644 index 00000000..43e495a5 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/control/index.ts @@ -0,0 +1,6 @@ +export * from "./character_controller"; +export * from "./pid_controller"; + +// #if DIM3 +export * from "./ray_cast_vehicle_controller"; +// #endif diff --git a/thirdparty/rapier.js/src.ts/control/pid_controller.ts b/thirdparty/rapier.js/src.ts/control/pid_controller.ts new file mode 100644 index 00000000..52b2f006 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/control/pid_controller.ts @@ -0,0 +1,207 @@ +import {RawPidController} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import {Collider, ColliderSet, InteractionGroups, Shape} from "../geometry"; +import {QueryFilterFlags, World} from "../pipeline"; +import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics"; + +// TODO: unify with the JointAxesMask +/** + * An enum representing the possible joint axes controlled by a PidController. + * They can be ORed together, like: + * PidAxesMask.LinX || PidAxesMask.LinY + * to get a pid controller that only constraints the translational X and Y axes. + * + * Possible axes are: + * + * - `X`: X translation axis + * - `Y`: Y translation axis + * - `Z`: Z translation axis + * - `AngX`: X angular rotation axis (3D only) + * - `AngY`: Y angular rotation axis (3D only) + * - `AngZ`: Z angular rotation axis + */ +export enum PidAxesMask { + None = 0, + LinX = 1 << 0, + LinY = 1 << 1, + LinZ = 1 << 2, + // #if DIM3 + AngX = 1 << 3, + AngY = 1 << 4, + // #endif + AngZ = 1 << 5, + // #if DIM2 + AllLin = PidAxesMask.LinX | PidAxesMask.LinY, + AllAng = PidAxesMask.AngZ, + // #endif + // #if DIM3 + AllLin = PidAxesMask.LinX | PidAxesMask.LinY | PidAxesMask.LinZ, + AllAng = PidAxesMask.AngX | PidAxesMask.AngY | PidAxesMask.AngZ, + // #endif + All = PidAxesMask.AllLin | PidAxesMask.AllAng, +} + +/** + * A controller for controlling dynamic bodies using the + * Proportional-Integral-Derivative correction model. + */ +export class PidController { + private raw: RawPidController; + + private params: IntegrationParameters; + private bodies: RigidBodySet; + + constructor( + params: IntegrationParameters, + bodies: RigidBodySet, + kp: number, + ki: number, + kd: number, + axes: PidAxesMask, + ) { + this.params = params; + this.bodies = bodies; + this.raw = new RawPidController(kp, ki, kd, axes); + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + + this.raw = undefined; + } + + public setKp(kp: number, axes: PidAxesMask) { + this.raw.set_kp(kp, axes); + } + + public setKi(ki: number, axes: PidAxesMask) { + this.raw.set_kp(ki, axes); + } + + public setKd(kd: number, axes: PidAxesMask) { + this.raw.set_kp(kd, axes); + } + + public setAxes(axes: PidAxesMask) { + this.raw.set_axes_mask(axes); + } + + public resetIntegrals() { + this.raw.reset_integrals(); + } + + public applyLinearCorrection( + body: RigidBody, + targetPosition: Vector, + targetLinvel: Vector, + ) { + let rawPos = VectorOps.intoRaw(targetPosition); + let rawVel = VectorOps.intoRaw(targetLinvel); + this.raw.apply_linear_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + } + + // #if DIM2 + public applyAngularCorrection( + body: RigidBody, + targetRotation: number, + targetAngVel: number, + ) { + this.raw.apply_angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + targetRotation, + targetAngVel, + ); + } + // #endif + + // #if DIM3 + public applyAngularCorrection( + body: RigidBody, + targetRotation: Rotation, + targetAngVel: Vector, + ) { + let rawPos = RotationOps.intoRaw(targetRotation); + let rawVel = VectorOps.intoRaw(targetAngVel); + this.raw.apply_angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + } + // #endif + + public linearCorrection( + body: RigidBody, + targetPosition: Vector, + targetLinvel: Vector, + ): Vector { + let rawPos = VectorOps.intoRaw(targetPosition); + let rawVel = VectorOps.intoRaw(targetLinvel); + let correction = this.raw.linear_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + + return VectorOps.fromRaw(correction); + } + + // #if DIM2 + public angularCorrection( + body: RigidBody, + targetRotation: number, + targetAngVel: number, + ): number { + return this.raw.angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + targetRotation, + targetAngVel, + ); + } + // #endif + + // #if DIM3 + public angularCorrection( + body: RigidBody, + targetRotation: Rotation, + targetAngVel: Vector, + ): Vector { + let rawPos = RotationOps.intoRaw(targetRotation); + let rawVel = VectorOps.intoRaw(targetAngVel); + let correction = this.raw.angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + + return VectorOps.fromRaw(correction); + } + // #endif +} diff --git a/thirdparty/rapier.js/src.ts/control/ray_cast_vehicle_controller.ts b/thirdparty/rapier.js/src.ts/control/ray_cast_vehicle_controller.ts new file mode 100644 index 00000000..fa65b3e3 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/control/ray_cast_vehicle_controller.ts @@ -0,0 +1,481 @@ +import {RawDynamicRayCastVehicleController} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + BroadPhase, + Collider, + ColliderSet, + InteractionGroups, + NarrowPhase, +} from "../geometry"; +import {QueryFilterFlags} from "../pipeline"; +import {RigidBody, RigidBodyHandle, RigidBodySet} from "../dynamics"; + +/** + * A character controller to simulate vehicles using ray-casting for the wheels. + */ +export class DynamicRayCastVehicleController { + private raw: RawDynamicRayCastVehicleController; + private broadPhase: BroadPhase; + private narrowPhase: NarrowPhase; + private bodies: RigidBodySet; + private colliders: ColliderSet; + private _chassis: RigidBody; + + constructor( + chassis: RigidBody, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ) { + this.raw = new RawDynamicRayCastVehicleController(chassis.handle); + this.broadPhase = broadPhase; + this.narrowPhase = narrowPhase; + this.bodies = bodies; + this.colliders = colliders; + this._chassis = chassis; + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + + this.raw = undefined; + } + + /** + * Updates the vehicle’s velocity based on its suspension, engine force, and brake. + * + * This directly updates the velocity of its chassis rigid-body. + * + * @param dt - Time increment used to integrate forces. + * @param filterFlags - Flag to exclude categories of objects from the wheels’ ray-cast. + * @param filterGroups - Only colliders compatible with these groups will be hit by the wheels’ ray-casts. + * @param filterPredicate - Callback to filter out which collider will be hit by the wheels’ ray-casts. + */ + public updateVehicle( + dt: number, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.raw.update_vehicle( + dt, + this.broadPhase.raw, + this.narrowPhase.raw, + this.bodies.raw, + this.colliders.raw, + filterFlags, + filterGroups, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * The current forward speed of the vehicle. + */ + public currentVehicleSpeed(): number { + return this.raw.current_vehicle_speed(); + } + + /** + * The rigid-body used as the chassis. + */ + public chassis(): RigidBody { + return this._chassis; + } + + /** + * The chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`). + */ + get indexUpAxis(): number { + return this.raw.index_up_axis(); + } + + /** + * Sets the chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`). + */ + set indexUpAxis(axis: number) { + this.raw.set_index_up_axis(axis); + } + + /** + * The chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`). + */ + get indexForwardAxis(): number { + return this.raw.index_forward_axis(); + } + + /** + * Sets the chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`). + */ + set setIndexForwardAxis(axis: number) { + this.raw.set_index_forward_axis(axis); + } + + /** + * Adds a new wheel attached to this vehicle. + * @param chassisConnectionCs - The position of the wheel relative to the chassis. + * @param directionCs - The direction of the wheel’s suspension, relative to the chassis. The ray-casting will + * happen following this direction to detect the ground. + * @param axleCs - The wheel’s axle axis, relative to the chassis. + * @param suspensionRestLength - The rest length of the wheel’s suspension spring. + * @param radius - The wheel’s radius. + */ + public addWheel( + chassisConnectionCs: Vector, + directionCs: Vector, + axleCs: Vector, + suspensionRestLength: number, + radius: number, + ) { + let rawChassisConnectionCs = VectorOps.intoRaw(chassisConnectionCs); + let rawDirectionCs = VectorOps.intoRaw(directionCs); + let rawAxleCs = VectorOps.intoRaw(axleCs); + + this.raw.add_wheel( + rawChassisConnectionCs, + rawDirectionCs, + rawAxleCs, + suspensionRestLength, + radius, + ); + + rawChassisConnectionCs.free(); + rawDirectionCs.free(); + rawAxleCs.free(); + } + + /** + * The number of wheels attached to this vehicle. + */ + public numWheels(): number { + return this.raw.num_wheels(); + } + + /* + * + * Access to wheel properties. + * + */ + /* + * Getters + setters + */ + /** + * The position of the i-th wheel, relative to the chassis. + */ + public wheelChassisConnectionPointCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_chassis_connection_point_cs(i)); + } + + /** + * Sets the position of the i-th wheel, relative to the chassis. + */ + public setWheelChassisConnectionPointCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_chassis_connection_point_cs(i, rawValue); + rawValue.free(); + } + + /** + * The rest length of the i-th wheel’s suspension spring. + */ + public wheelSuspensionRestLength(i: number): number | null { + return this.raw.wheel_suspension_rest_length(i); + } + + /** + * Sets the rest length of the i-th wheel’s suspension spring. + */ + public setWheelSuspensionRestLength(i: number, value: number) { + this.raw.set_wheel_suspension_rest_length(i, value); + } + + /** + * The maximum distance the i-th wheel suspension can travel before and after its resting length. + */ + public wheelMaxSuspensionTravel(i: number): number | null { + return this.raw.wheel_max_suspension_travel(i); + } + + /** + * Sets the maximum distance the i-th wheel suspension can travel before and after its resting length. + */ + public setWheelMaxSuspensionTravel(i: number, value: number) { + this.raw.set_wheel_max_suspension_travel(i, value); + } + + /** + * The i-th wheel’s radius. + */ + public wheelRadius(i: number): number | null { + return this.raw.wheel_radius(i); + } + + /** + * Sets the i-th wheel’s radius. + */ + public setWheelRadius(i: number, value: number) { + this.raw.set_wheel_radius(i, value); + } + + /** + * The i-th wheel’s suspension stiffness. + * + * Increase this value if the suspension appears to not push the vehicle strong enough. + */ + public wheelSuspensionStiffness(i: number): number | null { + return this.raw.wheel_suspension_stiffness(i); + } + + /** + * Sets the i-th wheel’s suspension stiffness. + * + * Increase this value if the suspension appears to not push the vehicle strong enough. + */ + public setWheelSuspensionStiffness(i: number, value: number) { + this.raw.set_wheel_suspension_stiffness(i, value); + } + + /** + * The i-th wheel’s suspension’s damping when it is being compressed. + */ + public wheelSuspensionCompression(i: number): number | null { + return this.raw.wheel_suspension_compression(i); + } + + /** + * The i-th wheel’s suspension’s damping when it is being compressed. + */ + public setWheelSuspensionCompression(i: number, value: number) { + this.raw.set_wheel_suspension_compression(i, value); + } + + /** + * The i-th wheel’s suspension’s damping when it is being released. + * + * Increase this value if the suspension appears to overshoot. + */ + public wheelSuspensionRelaxation(i: number): number | null { + return this.raw.wheel_suspension_relaxation(i); + } + + /** + * Sets the i-th wheel’s suspension’s damping when it is being released. + * + * Increase this value if the suspension appears to overshoot. + */ + public setWheelSuspensionRelaxation(i: number, value: number) { + this.raw.set_wheel_suspension_relaxation(i, value); + } + + /** + * The maximum force applied by the i-th wheel’s suspension. + */ + public wheelMaxSuspensionForce(i: number): number | null { + return this.raw.wheel_max_suspension_force(i); + } + + /** + * Sets the maximum force applied by the i-th wheel’s suspension. + */ + public setWheelMaxSuspensionForce(i: number, value: number) { + this.raw.set_wheel_max_suspension_force(i, value); + } + + /** + * The maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle. + */ + public wheelBrake(i: number): number | null { + return this.raw.wheel_brake(i); + } + + /** + * Set the maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle. + */ + public setWheelBrake(i: number, value: number) { + this.raw.set_wheel_brake(i, value); + } + + /** + * The steering angle (radians) for the i-th wheel. + */ + public wheelSteering(i: number): number | null { + return this.raw.wheel_steering(i); + } + + /** + * Sets the steering angle (radians) for the i-th wheel. + */ + public setWheelSteering(i: number, value: number) { + this.raw.set_wheel_steering(i, value); + } + + /** + * The forward force applied by the i-th wheel on the chassis. + */ + public wheelEngineForce(i: number): number | null { + return this.raw.wheel_engine_force(i); + } + + /** + * Sets the forward force applied by the i-th wheel on the chassis. + */ + public setWheelEngineForce(i: number, value: number) { + this.raw.set_wheel_engine_force(i, value); + } + + /** + * The direction of the i-th wheel’s suspension, relative to the chassis. + * + * The ray-casting will happen following this direction to detect the ground. + */ + public wheelDirectionCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_direction_cs(i)); + } + + /** + * Sets the direction of the i-th wheel’s suspension, relative to the chassis. + * + * The ray-casting will happen following this direction to detect the ground. + */ + public setWheelDirectionCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_direction_cs(i, rawValue); + rawValue.free(); + } + + /** + * The i-th wheel’s axle axis, relative to the chassis. + * + * The axis index defined as 0 = X, 1 = Y, 2 = Z. + */ + public wheelAxleCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_axle_cs(i)); + } + + /** + * Sets the i-th wheel’s axle axis, relative to the chassis. + * + * The axis index defined as 0 = X, 1 = Y, 2 = Z. + */ + public setWheelAxleCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_axle_cs(i, rawValue); + rawValue.free(); + } + + /** + * Parameter controlling how much traction the tire has. + * + * The larger the value, the more instantaneous braking will happen (with the risk of + * causing the vehicle to flip if it’s too strong). + */ + public wheelFrictionSlip(i: number): number | null { + return this.raw.wheel_friction_slip(i); + } + + /** + * Sets the parameter controlling how much traction the tire has. + * + * The larger the value, the more instantaneous braking will happen (with the risk of + * causing the vehicle to flip if it’s too strong). + */ + public setWheelFrictionSlip(i: number, value: number) { + this.raw.set_wheel_friction_slip(i, value); + } + + /** + * The multiplier of friction between a tire and the collider it’s on top of. + * + * The larger the value, the stronger side friction will be. + */ + public wheelSideFrictionStiffness(i: number): number | null { + return this.raw.wheel_side_friction_stiffness(i); + } + + /** + * The multiplier of friction between a tire and the collider it’s on top of. + * + * The larger the value, the stronger side friction will be. + */ + public setWheelSideFrictionStiffness(i: number, value: number) { + this.raw.set_wheel_side_friction_stiffness(i, value); + } + + /* + * Getters only. + */ + + /** + * The i-th wheel’s current rotation angle (radians) on its axle. + */ + public wheelRotation(i: number): number | null { + return this.raw.wheel_rotation(i); + } + + /** + * The forward impulses applied by the i-th wheel on the chassis. + */ + public wheelForwardImpulse(i: number): number | null { + return this.raw.wheel_forward_impulse(i); + } + + /** + * The side impulses applied by the i-th wheel on the chassis. + */ + public wheelSideImpulse(i: number): number | null { + return this.raw.wheel_side_impulse(i); + } + + /** + * The force applied by the i-th wheel suspension. + */ + public wheelSuspensionForce(i: number): number | null { + return this.raw.wheel_suspension_force(i); + } + + /** + * The (world-space) contact normal between the i-th wheel and the floor. + */ + public wheelContactNormal(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_contact_normal_ws(i)); + } + + /** + * The (world-space) point hit by the wheel’s ray-cast for the i-th wheel. + */ + public wheelContactPoint(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_contact_point_ws(i)); + } + + /** + * The suspension length for the i-th wheel. + */ + public wheelSuspensionLength(i: number): number | null { + return this.raw.wheel_suspension_length(i); + } + + /** + * The (world-space) starting point of the ray-cast for the i-th wheel. + */ + public wheelHardPoint(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_hard_point_ws(i)); + } + + /** + * Is the i-th wheel in contact with the ground? + */ + public wheelIsInContact(i: number): boolean { + return this.raw.wheel_is_in_contact(i); + } + + /** + * The collider hit by the ray-cast for the i-th wheel. + */ + public wheelGroundObject(i: number): Collider | null { + return this.colliders.get(this.raw.wheel_ground_object(i)); + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/ccd_solver.ts b/thirdparty/rapier.js/src.ts/dynamics/ccd_solver.ts new file mode 100644 index 00000000..7f794222 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/ccd_solver.ts @@ -0,0 +1,25 @@ +import {RawCCDSolver} from "../raw"; + +/** + * The CCD solver responsible for resolving Continuous Collision Detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()` + * once you are done using it. + */ +export class CCDSolver { + raw: RawCCDSolver; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawCCDSolver) { + this.raw = raw || new RawCCDSolver(); + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/coefficient_combine_rule.ts b/thirdparty/rapier.js/src.ts/dynamics/coefficient_combine_rule.ts new file mode 100644 index 00000000..680b402a --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/coefficient_combine_rule.ts @@ -0,0 +1,13 @@ +/** + * A rule applied to combine coefficients. + * + * Use this when configuring the `ColliderDesc` to specify + * how friction and restitution coefficient should be combined + * in a contact. + */ +export enum CoefficientCombineRule { + Average = 0, + Min = 1, + Multiply = 2, + Max = 3, +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/impulse_joint.ts b/thirdparty/rapier.js/src.ts/dynamics/impulse_joint.ts new file mode 100644 index 00000000..0c637ddf --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/impulse_joint.ts @@ -0,0 +1,681 @@ +import {Rotation, Vector, VectorOps, RotationOps} from "../math"; +import { + RawGenericJoint, + RawImpulseJointSet, + RawRigidBodySet, + RawJointAxis, + RawJointType, + RawMotorModel, +} from "../raw"; +import {RigidBody, RigidBodyHandle} from "./rigid_body"; +import {RigidBodySet} from "./rigid_body_set"; +// #if DIM3 +import {Quaternion} from "../math"; +// #endif + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type ImpulseJointHandle = number; + +/** + * An enum grouping all possible types of joints: + * + * - `Revolute`: A revolute joint that removes all degrees of freedom between the affected + * bodies except for the rotation along one axis. + * - `Fixed`: A fixed joint that removes all relative degrees of freedom between the affected bodies. + * - `Prismatic`: A prismatic joint that removes all degrees of freedom between the affected + * bodies except for the translation along one axis. + * - `Spherical`: (3D only) A spherical joint that removes all relative linear degrees of freedom between the affected bodies. + * - `Generic`: (3D only) A joint with customizable degrees of freedom, allowing any of the 6 axes to be locked. + */ +export enum JointType { + Revolute, + Fixed, + Prismatic, + Rope, + Spring, + // #if DIM3 + Spherical, + Generic, + // #endif +} + +export enum MotorModel { + AccelerationBased, + ForceBased, +} + +/** + * An enum representing the possible joint axes of a generic joint. + * They can be ORed together, like: + * JointAxesMask.LinX || JointAxesMask.LinY + * to get a joint that is only free in the X and Y translational (positional) axes. + * + * Possible free axes are: + * + * - `X`: X translation axis + * - `Y`: Y translation axis + * - `Z`: Z translation axis + * - `AngX`: X angular rotation axis + * - `AngY`: Y angular rotations axis + * - `AngZ`: Z angular rotation axis + */ +export enum JointAxesMask { + LinX = 1 << 0, + LinY = 1 << 1, + LinZ = 1 << 2, + AngX = 1 << 3, + AngY = 1 << 4, + AngZ = 1 << 5, +} + +export class ImpulseJoint { + protected rawSet: RawImpulseJointSet; // The ImpulseJoint won't need to free this. + protected bodySet: RigidBodySet; // The ImpulseJoint won’t need to free this. + handle: ImpulseJointHandle; + + constructor( + rawSet: RawImpulseJointSet, + bodySet: RigidBodySet, + handle: ImpulseJointHandle, + ) { + this.rawSet = rawSet; + this.bodySet = bodySet; + this.handle = handle; + } + + public static newTyped( + rawSet: RawImpulseJointSet, + bodySet: RigidBodySet, + handle: ImpulseJointHandle, + ): ImpulseJoint { + switch (rawSet.jointType(handle)) { + case RawJointType.Revolute: + return new RevoluteImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Prismatic: + return new PrismaticImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Fixed: + return new FixedImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Spring: + return new SpringImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Rope: + return new RopeImpulseJoint(rawSet, bodySet, handle); + // #if DIM3 + case RawJointType.Spherical: + return new SphericalImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Generic: + return new GenericImpulseJoint(rawSet, bodySet, handle); + // #endif + default: + return new ImpulseJoint(rawSet, bodySet, handle); + } + } + + /** @internal */ + public finalizeDeserialization(bodySet: RigidBodySet) { + this.bodySet = bodySet; + } + + /** + * Checks if this joint is still valid (i.e. that it has + * not been deleted from the joint set yet). + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + /** + * The first rigid-body this joint it attached to. + */ + public body1(): RigidBody { + return this.bodySet.get(this.rawSet.jointBodyHandle1(this.handle)); + } + + /** + * The second rigid-body this joint is attached to. + */ + public body2(): RigidBody { + return this.bodySet.get(this.rawSet.jointBodyHandle2(this.handle)); + } + + /** + * The type of this joint given as a string. + */ + public type(): JointType { + return this.rawSet.jointType(this.handle) as number as JointType; + } + + // #if DIM3 + /** + * The rotation quaternion that aligns this joint's first local axis to the `x` axis. + */ + public frameX1(): Rotation { + return RotationOps.fromRaw(this.rawSet.jointFrameX1(this.handle)); + } + + // #endif + + // #if DIM3 + /** + * The rotation matrix that aligns this joint's second local axis to the `x` axis. + */ + public frameX2(): Rotation { + return RotationOps.fromRaw(this.rawSet.jointFrameX2(this.handle)); + } + + // #endif + + /** + * The position of the first anchor of this joint. + * + * The first anchor gives the position of the application point on the + * local frame of the first rigid-body it is attached to. + */ + public anchor1(): Vector { + return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle)); + } + + /** + * The position of the second anchor of this joint. + * + * The second anchor gives the position of the application point on the + * local frame of the second rigid-body it is attached to. + */ + public anchor2(): Vector { + return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle)); + } + + /** + * Sets the position of the first anchor of this joint. + * + * The first anchor gives the position of the application point on the + * local frame of the first rigid-body it is attached to. + */ + public setAnchor1(newPos: Vector) { + const rawPoint = VectorOps.intoRaw(newPos); + this.rawSet.jointSetAnchor1(this.handle, rawPoint); + rawPoint.free(); + } + + /** + * Sets the position of the second anchor of this joint. + * + * The second anchor gives the position of the application point on the + * local frame of the second rigid-body it is attached to. + */ + public setAnchor2(newPos: Vector) { + const rawPoint = VectorOps.intoRaw(newPos); + this.rawSet.jointSetAnchor2(this.handle, rawPoint); + rawPoint.free(); + } + + /** + * Controls whether contacts are computed between colliders attached + * to the rigid-bodies linked by this joint. + */ + public setContactsEnabled(enabled: boolean) { + this.rawSet.jointSetContactsEnabled(this.handle, enabled); + } + + /** + * Indicates if contacts are enabled between colliders attached + * to the rigid-bodies linked by this joint. + */ + public contactsEnabled(): boolean { + return this.rawSet.jointContactsEnabled(this.handle); + } +} + +export class UnitImpulseJoint extends ImpulseJoint { + /** + * The axis left free by this joint. + */ + protected rawAxis?(): RawJointAxis; + + /** + * Are the limits enabled for this joint? + */ + public limitsEnabled(): boolean { + return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis()); + } + + /** + * The min limit of this joint. + */ + public limitsMin(): number { + return this.rawSet.jointLimitsMin(this.handle, this.rawAxis()); + } + + /** + * The max limit of this joint. + */ + public limitsMax(): number { + return this.rawSet.jointLimitsMax(this.handle, this.rawAxis()); + } + + /** + * Sets the limits of this joint. + * + * @param min - The minimum bound of this joint’s free coordinate. + * @param max - The maximum bound of this joint’s free coordinate. + */ + public setLimits(min: number, max: number) { + this.rawSet.jointSetLimits(this.handle, this.rawAxis(), min, max); + } + + public configureMotorModel(model: MotorModel) { + this.rawSet.jointConfigureMotorModel( + this.handle, + this.rawAxis(), + model as number as RawMotorModel, + ); + } + + public configureMotorVelocity(targetVel: number, factor: number) { + this.rawSet.jointConfigureMotorVelocity( + this.handle, + this.rawAxis(), + targetVel, + factor, + ); + } + + public configureMotorPosition( + targetPos: number, + stiffness: number, + damping: number, + ) { + this.rawSet.jointConfigureMotorPosition( + this.handle, + this.rawAxis(), + targetPos, + stiffness, + damping, + ); + } + + public configureMotor( + targetPos: number, + targetVel: number, + stiffness: number, + damping: number, + ) { + this.rawSet.jointConfigureMotor( + this.handle, + this.rawAxis(), + targetPos, + targetVel, + stiffness, + damping, + ); + } +} + +export class FixedImpulseJoint extends ImpulseJoint {} + +export class RopeImpulseJoint extends ImpulseJoint {} + +export class SpringImpulseJoint extends ImpulseJoint {} + +export class PrismaticImpulseJoint extends UnitImpulseJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.LinX; + } +} + +export class RevoluteImpulseJoint extends UnitImpulseJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.AngX; + } +} + +// #if DIM3 +export class GenericImpulseJoint extends ImpulseJoint {} + +export class SphericalImpulseJoint extends ImpulseJoint { + /* Unsupported by this alpha release. + public configureMotorModel(model: MotorModel) { + this.rawSet.jointConfigureMotorModel(this.handle, model); + } + + public configureMotorVelocity(targetVel: Vector, factor: number) { + this.rawSet.jointConfigureBallMotorVelocity(this.handle, targetVel.x, targetVel.y, targetVel.z, factor); + } + + public configureMotorPosition(targetPos: Quaternion, stiffness: number, damping: number) { + this.rawSet.jointConfigureBallMotorPosition(this.handle, targetPos.w, targetPos.x, targetPos.y, targetPos.z, stiffness, damping); + } + + public configureMotor(targetPos: Quaternion, targetVel: Vector, stiffness: number, damping: number) { + this.rawSet.jointConfigureBallMotor(this.handle, + targetPos.w, targetPos.x, targetPos.y, targetPos.z, + targetVel.x, targetVel.y, targetVel.z, + stiffness, damping); + } + */ +} +// #endif + +export class JointData { + anchor1: Vector; + anchor2: Vector; + axis: Vector; + frame1: Rotation; + frame2: Rotation; + jointType: JointType; + limitsEnabled: boolean; + limits: Array; + axesMask: JointAxesMask; + stiffness: number; + damping: number; + length: number; + + private constructor() {} + + /** + * Creates a new joint descriptor that builds a Fixed joint. + * + * A fixed joint removes all the degrees of freedom between the affected bodies, ensuring their + * anchor and local frames coincide in world-space. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param frame1 - The reference orientation of the joint wrt. the first rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param frame2 - The reference orientation of the joint wrt. the second rigid-body. + */ + public static fixed( + anchor1: Vector, + frame1: Rotation, + anchor2: Vector, + frame2: Rotation, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.frame1 = frame1; + res.frame2 = frame2; + res.jointType = JointType.Fixed; + return res; + } + + public static spring( + rest_length: number, + stiffness: number, + damping: number, + anchor1: Vector, + anchor2: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.length = rest_length; + res.stiffness = stiffness; + res.damping = damping; + res.jointType = JointType.Spring; + return res; + } + + public static rope( + length: number, + anchor1: Vector, + anchor2: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.length = length; + res.jointType = JointType.Rope; + return res; + } + + // #if DIM2 + + /** + * Create a new joint descriptor that builds revolute joints. + * + * A revolute joint allows three relative rotational degrees of freedom + * by preventing any relative translation between the anchors of the + * two attached rigid-bodies. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + */ + public static revolute(anchor1: Vector, anchor2: Vector): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.jointType = JointType.Revolute; + return res; + } + + /** + * Creates a new joint descriptor that builds a Prismatic joint. + * + * A prismatic joint removes all the degrees of freedom between the + * affected bodies, except for the translation along one axis. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param axis - Axis of the joint, expressed in the local-space of the rigid-bodies it is attached to. + */ + public static prismatic( + anchor1: Vector, + anchor2: Vector, + axis: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.axis = axis; + res.jointType = JointType.Prismatic; + return res; + } + + // #endif + + // #if DIM3 + /** + * Create a new joint descriptor that builds generic joints. + * + * A generic joint allows customizing its degrees of freedom + * by supplying a mask of the joint axes that should remain locked. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param axis - The X axis of the joint, expressed in the local-space of the rigid-bodies it is attached to. + * @param axesMask - Mask representing the locked axes of the joint. You can use logical OR to select these from + * the JointAxesMask enum. For example, passing (JointAxesMask.AngX || JointAxesMask.AngY) will + * create a joint locked in the X and Y rotational axes. + */ + public static generic( + anchor1: Vector, + anchor2: Vector, + axis: Vector, + axesMask: JointAxesMask, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.axis = axis; + res.axesMask = axesMask; + res.jointType = JointType.Generic; + return res; + } + + /** + * Create a new joint descriptor that builds spherical joints. + * + * A spherical joint allows three relative rotational degrees of freedom + * by preventing any relative translation between the anchors of the + * two attached rigid-bodies. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + */ + public static spherical(anchor1: Vector, anchor2: Vector): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.jointType = JointType.Spherical; + return res; + } + + /** + * Creates a new joint descriptor that builds a Prismatic joint. + * + * A prismatic joint removes all the degrees of freedom between the + * affected bodies, except for the translation along one axis. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param axis - Axis of the joint, expressed in the local-space of the rigid-bodies it is attached to. + */ + public static prismatic( + anchor1: Vector, + anchor2: Vector, + axis: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.axis = axis; + res.jointType = JointType.Prismatic; + return res; + } + + /** + * Create a new joint descriptor that builds Revolute joints. + * + * A revolute joint removes all degrees of freedom between the affected + * bodies except for the rotation along one axis. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param axis - Axis of the joint, expressed in the local-space of the rigid-bodies it is attached to. + */ + public static revolute( + anchor1: Vector, + anchor2: Vector, + axis: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.axis = axis; + res.jointType = JointType.Revolute; + return res; + } + // #endif + + public intoRaw(): RawGenericJoint { + let rawA1 = VectorOps.intoRaw(this.anchor1); + let rawA2 = VectorOps.intoRaw(this.anchor2); + let rawAx; + let result; + let limitsEnabled = false; + let limitsMin = 0.0; + let limitsMax = 0.0; + + switch (this.jointType) { + case JointType.Fixed: + let rawFra1 = RotationOps.intoRaw(this.frame1); + let rawFra2 = RotationOps.intoRaw(this.frame2); + result = RawGenericJoint.fixed(rawA1, rawFra1, rawA2, rawFra2); + rawFra1.free(); + rawFra2.free(); + break; + case JointType.Spring: + result = RawGenericJoint.spring( + this.length, + this.stiffness, + this.damping, + rawA1, + rawA2, + ); + break; + case JointType.Rope: + result = RawGenericJoint.rope(this.length, rawA1, rawA2); + break; + case JointType.Prismatic: + rawAx = VectorOps.intoRaw(this.axis); + + if (!!this.limitsEnabled) { + limitsEnabled = true; + limitsMin = this.limits[0]; + limitsMax = this.limits[1]; + } + + // #if DIM2 + result = RawGenericJoint.prismatic( + rawA1, + rawA2, + rawAx, + limitsEnabled, + limitsMin, + limitsMax, + ); + // #endif + + // #if DIM3 + result = RawGenericJoint.prismatic( + rawA1, + rawA2, + rawAx, + limitsEnabled, + limitsMin, + limitsMax, + ); + // #endif + + rawAx.free(); + break; + // #if DIM2 + case JointType.Revolute: + result = RawGenericJoint.revolute(rawA1, rawA2); + break; + // #endif + // #if DIM3 + case JointType.Generic: + rawAx = VectorOps.intoRaw(this.axis); + // implicit type cast: axesMask is a JointAxesMask bitflag enum, + // we're treating it as a u8 on the Rust side + let rawAxesMask = this.axesMask; + result = RawGenericJoint.generic( + rawA1, + rawA2, + rawAx, + rawAxesMask, + ); + break; + case JointType.Spherical: + result = RawGenericJoint.spherical(rawA1, rawA2); + break; + case JointType.Revolute: + rawAx = VectorOps.intoRaw(this.axis); + result = RawGenericJoint.revolute(rawA1, rawA2, rawAx); + rawAx.free(); + break; + // #endif + } + + rawA1.free(); + rawA2.free(); + + return result; + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/impulse_joint_set.ts b/thirdparty/rapier.js/src.ts/dynamics/impulse_joint_set.ts new file mode 100644 index 00000000..2d382514 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/impulse_joint_set.ts @@ -0,0 +1,165 @@ +import {RawImpulseJointSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RigidBodySet} from "./rigid_body_set"; +import { + RevoluteImpulseJoint, + FixedImpulseJoint, + ImpulseJoint, + ImpulseJointHandle, + JointData, + JointType, + PrismaticImpulseJoint, + // #if DIM3 + SphericalImpulseJoint, + // #endif +} from "./impulse_joint"; +import {IslandManager} from "./island_manager"; +import {RigidBodyHandle} from "./rigid_body"; +import {Collider, ColliderHandle} from "../geometry"; + +/** + * A set of joints. + * + * To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()` + * once you are done using it (and all the joints it created). + */ +export class ImpulseJointSet { + raw: RawImpulseJointSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this joint set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawImpulseJointSet) { + this.raw = raw || new RawImpulseJointSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachJointHandle((handle: ImpulseJointHandle) => { + this.map.set(handle, ImpulseJoint.newTyped(raw, null, handle)); + }); + } + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + this.map.forEach((joint) => joint.finalizeDeserialization(bodies)); + } + + /** + * Creates a new joint and return its integer handle. + * + * @param bodies - The set of rigid-bodies containing the bodies the joint is attached to. + * @param desc - The joint's parameters. + * @param parent1 - The handle of the first rigid-body this joint is attached to. + * @param parent2 - The handle of the second rigid-body this joint is attached to. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createJoint( + bodies: RigidBodySet, + desc: JointData, + parent1: RigidBodyHandle, + parent2: RigidBodyHandle, + wakeUp: boolean, + ): ImpulseJoint { + const rawParams = desc.intoRaw(); + const handle = this.raw.createJoint( + rawParams, + parent1, + parent2, + wakeUp, + ); + rawParams.free(); + let joint = ImpulseJoint.newTyped(this.raw, bodies, handle); + this.map.set(handle, joint); + return joint; + } + + /** + * Remove a joint from this set. + * + * @param handle - The integer handle of the joint. + * @param wakeUp - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically. + */ + public remove(handle: ImpulseJointHandle, wakeUp: boolean) { + this.raw.remove(handle, wakeUp); + this.unmap(handle); + } + + /** + * Calls the given closure with the integer handle of each impulse joint attached to this rigid-body. + * + * @param f - The closure called with the integer handle of each impulse joint attached to the rigid-body. + */ + public forEachJointHandleAttachedToRigidBody( + handle: RigidBodyHandle, + f: (handle: ImpulseJointHandle) => void, + ) { + this.raw.forEachJointAttachedToRigidBody(handle, f); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: ImpulseJointHandle) { + this.map.delete(handle); + } + + /** + * The number of joints on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a joint with the given handle? + * + * @param handle - The joint handle to check. + */ + public contains(handle: ImpulseJointHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the joint with the given handle. + * + * Returns `null` if no joint with the specified handle exists. + * + * @param handle - The integer handle of the joint to retrieve. + */ + public get(handle: ImpulseJointHandle): ImpulseJoint | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each joint contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (joint: ImpulseJoint) => void) { + this.map.forEach(f); + } + + /** + * Gets all joints in the list. + * + * @returns joint list. + */ + public getAll(): ImpulseJoint[] { + return this.map.getAll(); + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/index.ts b/thirdparty/rapier.js/src.ts/dynamics/index.ts new file mode 100644 index 00000000..4141658e --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/index.ts @@ -0,0 +1,10 @@ +export * from "./rigid_body"; +export * from "./rigid_body_set"; +export * from "./integration_parameters"; +export * from "./impulse_joint"; +export * from "./impulse_joint_set"; +export * from "./multibody_joint"; +export * from "./multibody_joint_set"; +export * from "./coefficient_combine_rule"; +export * from "./ccd_solver"; +export * from "./island_manager"; diff --git a/thirdparty/rapier.js/src.ts/dynamics/integration_parameters.ts b/thirdparty/rapier.js/src.ts/dynamics/integration_parameters.ts new file mode 100644 index 00000000..b7253e21 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/integration_parameters.ts @@ -0,0 +1,126 @@ +import {RawIntegrationParameters} from "../raw"; + +export class IntegrationParameters { + raw: RawIntegrationParameters; + + constructor(raw?: RawIntegrationParameters) { + this.raw = raw || new RawIntegrationParameters(); + } + + /** + * Free the WASM memory used by these integration parameters. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * The timestep length (default: `1.0 / 60.0`) + */ + get dt(): number { + return this.raw.dt; + } + + /** + * The Error Reduction Parameter in `[0, 1]` is the proportion of + * the positional error to be corrected at each time step (default: `0.2`). + */ + get contact_erp(): number { + return this.raw.contact_erp; + } + + get lengthUnit(): number { + return this.raw.lengthUnit; + } + + /** + * Normalized amount of penetration the engine won’t attempt to correct (default: `0.001m`). + * + * This threshold considered by the physics engine is this value multiplied by the `lengthUnit`. + */ + get normalizedAllowedLinearError(): number { + return this.raw.normalizedAllowedLinearError; + } + + /** + * The maximal normalized distance separating two objects that will generate predictive contacts (default: `0.002`). + * + * This threshold considered by the physics engine is this value multiplied by the `lengthUnit`. + */ + get normalizedPredictionDistance(): number { + return this.raw.normalizedPredictionDistance; + } + + /** + * The number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + get numSolverIterations(): number { + return this.raw.numSolverIterations; + } + + /** + * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + get numInternalPgsIterations(): number { + return this.raw.numInternalPgsIterations; + } + + /** + * Minimum number of dynamic bodies in each active island (default: `128`). + */ + get minIslandSize(): number { + return this.raw.minIslandSize; + } + + /** + * Maximum number of substeps performed by the solver (default: `1`). + */ + get maxCcdSubsteps(): number { + return this.raw.maxCcdSubsteps; + } + + set dt(value: number) { + this.raw.dt = value; + } + + set contact_natural_frequency(value: number) { + this.raw.contact_natural_frequency = value; + } + + set lengthUnit(value: number) { + this.raw.lengthUnit = value; + } + + set normalizedAllowedLinearError(value: number) { + this.raw.normalizedAllowedLinearError = value; + } + + set normalizedPredictionDistance(value: number) { + this.raw.normalizedPredictionDistance = value; + } + + /** + * Sets the number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + set numSolverIterations(value: number) { + this.raw.numSolverIterations = value; + } + + /** + * Sets the number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + set numInternalPgsIterations(value: number) { + this.raw.numInternalPgsIterations = value; + } + + set minIslandSize(value: number) { + this.raw.minIslandSize = value; + } + + set maxCcdSubsteps(value: number) { + this.raw.maxCcdSubsteps = value; + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/island_manager.ts b/thirdparty/rapier.js/src.ts/dynamics/island_manager.ts new file mode 100644 index 00000000..65ed21dc --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/island_manager.ts @@ -0,0 +1,37 @@ +import {RawIslandManager} from "../raw"; +import {RigidBodyHandle} from "./rigid_body"; + +/** + * The CCD solver responsible for resolving Continuous Collision Detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()` + * once you are done using it. + */ +export class IslandManager { + raw: RawIslandManager; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawIslandManager) { + this.raw = raw || new RawIslandManager(); + } + + /** + * Applies the given closure to the handle of each active rigid-bodies contained by this set. + * + * A rigid-body is active if it is not sleeping, i.e., if it moved recently. + * + * @param f - The closure to apply. + */ + public forEachActiveRigidBodyHandle(f: (handle: RigidBodyHandle) => void) { + this.raw.forEachActiveRigidBodyHandle(f); + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/multibody_joint.ts b/thirdparty/rapier.js/src.ts/dynamics/multibody_joint.ts new file mode 100644 index 00000000..5608aec7 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/multibody_joint.ts @@ -0,0 +1,222 @@ +import { + RawImpulseJointSet, + RawJointAxis, + RawJointType, + RawMultibodyJointSet, +} from "../raw"; +import { + FixedImpulseJoint, + ImpulseJointHandle, + JointType, + MotorModel, + PrismaticImpulseJoint, + RevoluteImpulseJoint, +} from "./impulse_joint"; + +// #if DIM3 +import {Quaternion} from "../math"; +import {SphericalImpulseJoint} from "./impulse_joint"; +// #endif + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type MultibodyJointHandle = number; + +export class MultibodyJoint { + protected rawSet: RawMultibodyJointSet; // The MultibodyJoint won't need to free this. + handle: MultibodyJointHandle; + + constructor(rawSet: RawMultibodyJointSet, handle: MultibodyJointHandle) { + this.rawSet = rawSet; + this.handle = handle; + } + + public static newTyped( + rawSet: RawMultibodyJointSet, + handle: MultibodyJointHandle, + ): MultibodyJoint { + switch (rawSet.jointType(handle)) { + case RawJointType.Revolute: + return new RevoluteMultibodyJoint(rawSet, handle); + case RawJointType.Prismatic: + return new PrismaticMultibodyJoint(rawSet, handle); + case RawJointType.Fixed: + return new FixedMultibodyJoint(rawSet, handle); + // #if DIM3 + case RawJointType.Spherical: + return new SphericalMultibodyJoint(rawSet, handle); + // #endif + default: + return new MultibodyJoint(rawSet, handle); + } + } + + /** + * Checks if this joint is still valid (i.e. that it has + * not been deleted from the joint set yet). + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + // /** + // * The unique integer identifier of the first rigid-body this joint it attached to. + // */ + // public bodyHandle1(): RigidBodyHandle { + // return this.rawSet.jointBodyHandle1(this.handle); + // } + // + // /** + // * The unique integer identifier of the second rigid-body this joint is attached to. + // */ + // public bodyHandle2(): RigidBodyHandle { + // return this.rawSet.jointBodyHandle2(this.handle); + // } + // + // /** + // * The type of this joint given as a string. + // */ + // public type(): JointType { + // return this.rawSet.jointType(this.handle); + // } + // + // // #if DIM3 + // /** + // * The rotation quaternion that aligns this joint's first local axis to the `x` axis. + // */ + // public frameX1(): Rotation { + // return RotationOps.fromRaw(this.rawSet.jointFrameX1(this.handle)); + // } + // + // // #endif + // + // // #if DIM3 + // /** + // * The rotation matrix that aligns this joint's second local axis to the `x` axis. + // */ + // public frameX2(): Rotation { + // return RotationOps.fromRaw(this.rawSet.jointFrameX2(this.handle)); + // } + // + // // #endif + // + // /** + // * The position of the first anchor of this joint. + // * + // * The first anchor gives the position of the points application point on the + // * local frame of the first rigid-body it is attached to. + // */ + // public anchor1(): Vector { + // return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle)); + // } + // + // /** + // * The position of the second anchor of this joint. + // * + // * The second anchor gives the position of the points application point on the + // * local frame of the second rigid-body it is attached to. + // */ + // public anchor2(): Vector { + // return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle)); + // } + + /** + * Controls whether contacts are computed between colliders attached + * to the rigid-bodies linked by this joint. + */ + public setContactsEnabled(enabled: boolean) { + this.rawSet.jointSetContactsEnabled(this.handle, enabled); + } + + /** + * Indicates if contacts are enabled between colliders attached + * to the rigid-bodies linked by this joint. + */ + public contactsEnabled(): boolean { + return this.rawSet.jointContactsEnabled(this.handle); + } +} + +export class UnitMultibodyJoint extends MultibodyJoint { + /** + * The axis left free by this joint. + */ + protected rawAxis?(): RawJointAxis; + + // /** + // * Are the limits enabled for this joint? + // */ + // public limitsEnabled(): boolean { + // return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis()); + // } + // + // /** + // * The min limit of this joint. + // */ + // public limitsMin(): number { + // return this.rawSet.jointLimitsMin(this.handle, this.rawAxis()); + // } + // + // /** + // * The max limit of this joint. + // */ + // public limitsMax(): number { + // return this.rawSet.jointLimitsMax(this.handle, this.rawAxis()); + // } + // + // public configureMotorModel(model: MotorModel) { + // this.rawSet.jointConfigureMotorModel(this.handle, this.rawAxis(), model); + // } + // + // public configureMotorVelocity(targetVel: number, factor: number) { + // this.rawSet.jointConfigureMotorVelocity(this.handle, this.rawAxis(), targetVel, factor); + // } + // + // public configureMotorPosition(targetPos: number, stiffness: number, damping: number) { + // this.rawSet.jointConfigureMotorPosition(this.handle, this.rawAxis(), targetPos, stiffness, damping); + // } + // + // public configureMotor(targetPos: number, targetVel: number, stiffness: number, damping: number) { + // this.rawSet.jointConfigureMotor(this.handle, this.rawAxis(), targetPos, targetVel, stiffness, damping); + // } +} + +export class FixedMultibodyJoint extends MultibodyJoint {} + +export class PrismaticMultibodyJoint extends UnitMultibodyJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.LinX; + } +} + +export class RevoluteMultibodyJoint extends UnitMultibodyJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.AngX; + } +} + +// #if DIM3 +export class SphericalMultibodyJoint extends MultibodyJoint { + /* Unsupported by this alpha release. + public configureMotorModel(model: MotorModel) { + this.rawSet.jointConfigureMotorModel(this.handle, model); + } + + public configureMotorVelocity(targetVel: Vector, factor: number) { + this.rawSet.jointConfigureBallMotorVelocity(this.handle, targetVel.x, targetVel.y, targetVel.z, factor); + } + + public configureMotorPosition(targetPos: Quaternion, stiffness: number, damping: number) { + this.rawSet.jointConfigureBallMotorPosition(this.handle, targetPos.w, targetPos.x, targetPos.y, targetPos.z, stiffness, damping); + } + + public configureMotor(targetPos: Quaternion, targetVel: Vector, stiffness: number, damping: number) { + this.rawSet.jointConfigureBallMotor(this.handle, + targetPos.w, targetPos.x, targetPos.y, targetPos.z, + targetVel.x, targetVel.y, targetVel.z, + stiffness, damping); + } + */ +} +// #endif diff --git a/thirdparty/rapier.js/src.ts/dynamics/multibody_joint_set.ts b/thirdparty/rapier.js/src.ts/dynamics/multibody_joint_set.ts new file mode 100644 index 00000000..2fbb3403 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/multibody_joint_set.ts @@ -0,0 +1,157 @@ +import {RawMultibodyJointSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RigidBodySet} from "./rigid_body_set"; +import { + MultibodyJoint, + MultibodyJointHandle, + RevoluteMultibodyJoint, + FixedMultibodyJoint, + PrismaticMultibodyJoint, + // #if DIM3 + SphericalMultibodyJoint, + // #endif +} from "./multibody_joint"; +import {ImpulseJointHandle, JointData, JointType} from "./impulse_joint"; +import {IslandManager} from "./island_manager"; +import {ColliderHandle} from "../geometry"; +import {RigidBodyHandle} from "./rigid_body"; + +/** + * A set of joints. + * + * To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()` + * once you are done using it (and all the joints it created). + */ +export class MultibodyJointSet { + raw: RawMultibodyJointSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this joint set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawMultibodyJointSet) { + this.raw = raw || new RawMultibodyJointSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachJointHandle((handle: MultibodyJointHandle) => { + this.map.set(handle, MultibodyJoint.newTyped(this.raw, handle)); + }); + } + } + + /** + * Creates a new joint and return its integer handle. + * + * @param desc - The joint's parameters. + * @param parent1 - The handle of the first rigid-body this joint is attached to. + * @param parent2 - The handle of the second rigid-body this joint is attached to. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createJoint( + desc: JointData, + parent1: RigidBodyHandle, + parent2: RigidBodyHandle, + wakeUp: boolean, + ): MultibodyJoint { + const rawParams = desc.intoRaw(); + const handle = this.raw.createJoint( + rawParams, + parent1, + parent2, + wakeUp, + ); + rawParams.free(); + let joint = MultibodyJoint.newTyped(this.raw, handle); + this.map.set(handle, joint); + return joint; + } + + /** + * Remove a joint from this set. + * + * @param handle - The integer handle of the joint. + * @param wake_up - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically. + */ + public remove(handle: MultibodyJointHandle, wake_up: boolean) { + this.raw.remove(handle, wake_up); + this.map.delete(handle); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: MultibodyJointHandle) { + this.map.delete(handle); + } + + /** + * The number of joints on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a joint with the given handle? + * + * @param handle - The joint handle to check. + */ + public contains(handle: MultibodyJointHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the joint with the given handle. + * + * Returns `null` if no joint with the specified handle exists. + * + * @param handle - The integer handle of the joint to retrieve. + */ + public get(handle: MultibodyJointHandle): MultibodyJoint | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each joint contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (joint: MultibodyJoint) => void) { + this.map.forEach(f); + } + + /** + * Calls the given closure with the integer handle of each multibody joint attached to this rigid-body. + * + * @param f - The closure called with the integer handle of each multibody joint attached to the rigid-body. + */ + public forEachJointHandleAttachedToRigidBody( + handle: RigidBodyHandle, + f: (handle: MultibodyJointHandle) => void, + ) { + this.raw.forEachJointAttachedToRigidBody(handle, f); + } + + /** + * Gets all joints in the list. + * + * @returns joint list. + */ + public getAll(): MultibodyJoint[] { + return this.map.getAll(); + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/rigid_body.ts b/thirdparty/rapier.js/src.ts/dynamics/rigid_body.ts new file mode 100644 index 00000000..8f52dbe5 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/rigid_body.ts @@ -0,0 +1,1691 @@ +import {RawRigidBodySet, RawRigidBodyType} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +// #if DIM3 +import {SdpMatrix3, SdpMatrix3Ops} from "../math"; +// #endif +import {Collider, ColliderSet} from "../geometry"; + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type RigidBodyHandle = number; + +/** + * The simulation status of a rigid-body. + */ +// TODO: rename this to RigidBodyType +export enum RigidBodyType { + /** + * A `RigidBodyType::Dynamic` body can be affected by all external forces. + */ + Dynamic = 0, + /** + * A `RigidBodyType::Fixed` body cannot be affected by external forces. + */ + Fixed, + /** + * A `RigidBodyType::KinematicPositionBased` body cannot be affected by any external forces but can be controlled + * by the user at the position level while keeping realistic one-way interaction with dynamic bodies. + * + * One-way interaction means that a kinematic body can push a dynamic body, but a kinematic body + * cannot be pushed by anything. In other words, the trajectory of a kinematic body can only be + * modified by the user and is independent from any contact or joint it is involved in. + */ + KinematicPositionBased, + /** + * A `RigidBodyType::KinematicVelocityBased` body cannot be affected by any external forces but can be controlled + * by the user at the velocity level while keeping realistic one-way interaction with dynamic bodies. + * + * One-way interaction means that a kinematic body can push a dynamic body, but a kinematic body + * cannot be pushed by anything. In other words, the trajectory of a kinematic body can only be + * modified by the user and is independent from any contact or joint it is involved in. + */ + KinematicVelocityBased, +} + +/** + * A rigid-body. + */ +export class RigidBody { + private rawSet: RawRigidBodySet; // The RigidBody won't need to free this. + private colliderSet: ColliderSet; + readonly handle: RigidBodyHandle; + + /** + * An arbitrary user-defined object associated with this rigid-body. + */ + public userData?: unknown; + + constructor( + rawSet: RawRigidBodySet, + colliderSet: ColliderSet, + handle: RigidBodyHandle, + ) { + this.rawSet = rawSet; + this.colliderSet = colliderSet; + this.handle = handle; + } + + /** @internal */ + public finalizeDeserialization(colliderSet: ColliderSet) { + this.colliderSet = colliderSet; + } + + /** + * Checks if this rigid-body is still valid (i.e. that it has + * not been deleted from the rigid-body set yet. + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + /** + * Locks or unlocks the ability of this rigid-body to translate. + * + * @param locked - If `true`, this rigid-body will no longer translate due to forces and impulses. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public lockTranslations(locked: boolean, wakeUp: boolean) { + return this.rawSet.rbLockTranslations(this.handle, locked, wakeUp); + } + + /** + * Locks or unlocks the ability of this rigid-body to rotate. + * + * @param locked - If `true`, this rigid-body will no longer rotate due to torques and impulses. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public lockRotations(locked: boolean, wakeUp: boolean) { + return this.rawSet.rbLockRotations(this.handle, locked, wakeUp); + } + + // #if DIM2 + /** + * Locks or unlocks the ability of this rigid-body to translation along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public setEnabledTranslations( + enableX: boolean, + enableY: boolean, + wakeUp: boolean, + ) { + return this.rawSet.rbSetEnabledTranslations( + this.handle, + enableX, + enableY, + wakeUp, + ); + } + + /** + * Locks or unlocks the ability of this rigid-body to translation along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + * @deprecated use `this.setEnabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + enableX: boolean, + enableY: boolean, + wakeUp: boolean, + ) { + this.setEnabledTranslations(enableX, enableX, wakeUp); + } + + // #endif + // #if DIM3 + /** + * Locks or unlocks the ability of this rigid-body to translate along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer translate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer translate due to torques and impulses, along the Y coordinate axis. + * @param enableZ - If `false`, this rigid-body will no longer translate due to torques and impulses, along the Z coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public setEnabledTranslations( + enableX: boolean, + enableY: boolean, + enableZ: boolean, + wakeUp: boolean, + ) { + return this.rawSet.rbSetEnabledTranslations( + this.handle, + enableX, + enableY, + enableZ, + wakeUp, + ); + } + + /** + * Locks or unlocks the ability of this rigid-body to translate along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer translate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer translate due to torques and impulses, along the Y coordinate axis. + * @param enableZ - If `false`, this rigid-body will no longer translate due to torques and impulses, along the Z coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + * @deprecated use `this.setEnabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + enableX: boolean, + enableY: boolean, + enableZ: boolean, + wakeUp: boolean, + ) { + this.setEnabledTranslations(enableX, enableY, enableZ, wakeUp); + } + + /** + * Locks or unlocks the ability of this rigid-body to rotate along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param enableZ - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Z coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public setEnabledRotations( + enableX: boolean, + enableY: boolean, + enableZ: boolean, + wakeUp: boolean, + ) { + return this.rawSet.rbSetEnabledRotations( + this.handle, + enableX, + enableY, + enableZ, + wakeUp, + ); + } + + /** + * Locks or unlocks the ability of this rigid-body to rotate along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param enableZ - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Z coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + * @deprecated use `this.setEnabledRotations` with the same arguments instead. + */ + public restrictRotations( + enableX: boolean, + enableY: boolean, + enableZ: boolean, + wakeUp: boolean, + ) { + this.setEnabledRotations(enableX, enableY, enableZ, wakeUp); + } + + // #endif + + /** + * The dominance group, in [-127, +127] this rigid-body is part of. + */ + public dominanceGroup(): number { + return this.rawSet.rbDominanceGroup(this.handle); + } + + /** + * Sets the dominance group of this rigid-body. + * + * @param group - The dominance group of this rigid-body. Must be a signed integer in the range [-127, +127]. + */ + public setDominanceGroup(group: number) { + this.rawSet.rbSetDominanceGroup(this.handle, group); + } + + /** + * The number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + */ + public additionalSolverIterations(): number { + return this.rawSet.rbAdditionalSolverIterations(this.handle); + } + + /** + * Sets the number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + * + * Compared to increasing the global `World.numSolverIteration`, setting this + * value lets you increase accuracy on only a subset of the scene, resulting in reduced + * performance loss. + * + * @param iters - The new number of additional solver iterations (default: 0). + */ + public setAdditionalSolverIterations(iters: number) { + this.rawSet.rbSetAdditionalSolverIterations(this.handle, iters); + } + + /** + * Enable or disable CCD (Continuous Collision Detection) for this rigid-body. + * + * @param enabled - If `true`, CCD will be enabled for this rigid-body. + */ + public enableCcd(enabled: boolean) { + this.rawSet.rbEnableCcd(this.handle, enabled); + } + + /** + * Sets the soft-CCD prediction distance for this rigid-body. + * + * See the documentation of `RigidBodyDesc.setSoftCcdPrediction` for + * additional details. + */ + public setSoftCcdPrediction(distance: number) { + this.rawSet.rbSetSoftCcdPrediction(this.handle, distance); + } + + /** + * Gets the soft-CCD prediction distance for this rigid-body. + * + * See the documentation of `RigidBodyDesc.setSoftCcdPrediction` for + * additional details. + */ + public softCcdPrediction(): number { + return this.rawSet.rbSoftCcdPrediction(this.handle); + } + + /** + * The world-space translation of this rigid-body. + */ + public translation(): Vector { + let res = this.rawSet.rbTranslation(this.handle); + return VectorOps.fromRaw(res); + } + + /** + * The world-space orientation of this rigid-body. + */ + public rotation(): Rotation { + let res = this.rawSet.rbRotation(this.handle); + return RotationOps.fromRaw(res); + } + + /** + * The world-space next translation of this rigid-body. + * + * If this rigid-body is kinematic this value is set by the `setNextKinematicTranslation` + * method and is used for estimating the kinematic body velocity at the next timestep. + * For non-kinematic bodies, this value is currently unspecified. + */ + public nextTranslation(): Vector { + let res = this.rawSet.rbNextTranslation(this.handle); + return VectorOps.fromRaw(res); + } + + /** + * The world-space next orientation of this rigid-body. + * + * If this rigid-body is kinematic this value is set by the `setNextKinematicRotation` + * method and is used for estimating the kinematic body velocity at the next timestep. + * For non-kinematic bodies, this value is currently unspecified. + */ + public nextRotation(): Rotation { + let res = this.rawSet.rbNextRotation(this.handle); + return RotationOps.fromRaw(res); + } + + /** + * Sets the translation of this rigid-body. + * + * @param tra - The world-space position of the rigid-body. + * @param wakeUp - Forces the rigid-body to wake-up so it is properly affected by forces if it + * wasn't moving before modifying its position. + */ + public setTranslation(tra: Vector, wakeUp: boolean) { + // #if DIM2 + this.rawSet.rbSetTranslation(this.handle, tra.x, tra.y, wakeUp); + // #endif + // #if DIM3 + this.rawSet.rbSetTranslation(this.handle, tra.x, tra.y, tra.z, wakeUp); + // #endif + } + + /** + * Sets the linear velocity of this rigid-body. + * + * @param vel - The linear velocity to set. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setLinvel(vel: Vector, wakeUp: boolean) { + let rawVel = VectorOps.intoRaw(vel); + this.rawSet.rbSetLinvel(this.handle, rawVel, wakeUp); + rawVel.free(); + } + + /** + * The scale factor applied to the gravity affecting + * this rigid-body. + */ + public gravityScale(): number { + return this.rawSet.rbGravityScale(this.handle); + } + + /** + * Sets the scale factor applied to the gravity affecting + * this rigid-body. + * + * @param factor - The scale factor to set. A value of 0.0 means + * that this rigid-body will on longer be affected by gravity. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setGravityScale(factor: number, wakeUp: boolean) { + this.rawSet.rbSetGravityScale(this.handle, factor, wakeUp); + } + + // #if DIM3 + /** + * Sets the rotation quaternion of this rigid-body. + * + * This does nothing if a zero quaternion is provided. + * + * @param rotation - The rotation to set. + * @param wakeUp - Forces the rigid-body to wake-up so it is properly affected by forces if it + * wasn't moving before modifying its position. + */ + public setRotation(rot: Rotation, wakeUp: boolean) { + this.rawSet.rbSetRotation( + this.handle, + rot.x, + rot.y, + rot.z, + rot.w, + wakeUp, + ); + } + + /** + * Sets the angular velocity fo this rigid-body. + * + * @param vel - The angular velocity to set. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setAngvel(vel: Vector, wakeUp: boolean) { + let rawVel = VectorOps.intoRaw(vel); + this.rawSet.rbSetAngvel(this.handle, rawVel, wakeUp); + rawVel.free(); + } + + // #endif + + // #if DIM2 + /** + * Sets the rotation angle of this rigid-body. + * + * @param angle - The rotation angle, in radians. + * @param wakeUp - Forces the rigid-body to wake-up so it is properly affected by forces if it + * wasn't moving before modifying its position. + */ + public setRotation(angle: number, wakeUp: boolean) { + this.rawSet.rbSetRotation(this.handle, angle, wakeUp); + } + + /** + * Sets the angular velocity fo this rigid-body. + * + * @param vel - The angular velocity to set. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setAngvel(vel: number, wakeUp: boolean) { + this.rawSet.rbSetAngvel(this.handle, vel, wakeUp); + } + + // #endif + + /** + * If this rigid body is kinematic, sets its future translation after the next timestep integration. + * + * This should be used instead of `rigidBody.setTranslation` to make the dynamic object + * interacting with this kinematic body behave as expected. Internally, Rapier will compute + * an artificial velocity for this rigid-body from its current position and its next kinematic + * position. This velocity will be used to compute forces on dynamic bodies interacting with + * this body. + * + * @param t - The kinematic translation to set. + */ + public setNextKinematicTranslation(t: Vector) { + // #if DIM2 + this.rawSet.rbSetNextKinematicTranslation(this.handle, t.x, t.y); + // #endif + // #if DIM3 + this.rawSet.rbSetNextKinematicTranslation(this.handle, t.x, t.y, t.z); + // #endif + } + + // #if DIM3 + /** + * If this rigid body is kinematic, sets its future rotation after the next timestep integration. + * + * This should be used instead of `rigidBody.setRotation` to make the dynamic object + * interacting with this kinematic body behave as expected. Internally, Rapier will compute + * an artificial velocity for this rigid-body from its current position and its next kinematic + * position. This velocity will be used to compute forces on dynamic bodies interacting with + * this body. + * + * @param rot - The kinematic rotation to set. + */ + public setNextKinematicRotation(rot: Rotation) { + this.rawSet.rbSetNextKinematicRotation( + this.handle, + rot.x, + rot.y, + rot.z, + rot.w, + ); + } + + // #endif + + // #if DIM2 + /** + * If this rigid body is kinematic, sets its future rotation after the next timestep integration. + * + * This should be used instead of `rigidBody.setRotation` to make the dynamic object + * interacting with this kinematic body behave as expected. Internally, Rapier will compute + * an artificial velocity for this rigid-body from its current position and its next kinematic + * position. This velocity will be used to compute forces on dynamic bodies interacting with + * this body. + * + * @param angle - The kinematic rotation angle, in radians. + */ + public setNextKinematicRotation(angle: number) { + this.rawSet.rbSetNextKinematicRotation(this.handle, angle); + } + + // #endif + + /** + * The linear velocity of this rigid-body. + */ + public linvel(): Vector { + return VectorOps.fromRaw(this.rawSet.rbLinvel(this.handle)); + } + + /** + * The velocity of the given world-space point on this rigid-body. + */ + public velocityAtPoint(point: Vector): Vector { + const rawPoint = VectorOps.intoRaw(point); + let result = VectorOps.fromRaw( + this.rawSet.rbVelocityAtPoint(this.handle, rawPoint), + ); + rawPoint.free(); + return result; + } + + // #if DIM3 + /** + * The angular velocity of this rigid-body. + */ + public angvel(): Vector { + return VectorOps.fromRaw(this.rawSet.rbAngvel(this.handle)); + } + + // #endif + + // #if DIM2 + /** + * The angular velocity of this rigid-body. + */ + public angvel(): number { + return this.rawSet.rbAngvel(this.handle); + } + + // #endif + + /** + * The mass of this rigid-body. + */ + public mass(): number { + return this.rawSet.rbMass(this.handle); + } + + /** + * The inverse mass taking into account translation locking. + */ + public effectiveInvMass(): Vector { + return VectorOps.fromRaw(this.rawSet.rbEffectiveInvMass(this.handle)); + } + + /** + * The inverse of the mass of a rigid-body. + * + * If this is zero, the rigid-body is assumed to have infinite mass. + */ + public invMass(): number { + return this.rawSet.rbInvMass(this.handle); + } + + /** + * The center of mass of a rigid-body expressed in its local-space. + */ + public localCom(): Vector { + return VectorOps.fromRaw(this.rawSet.rbLocalCom(this.handle)); + } + + /** + * The world-space center of mass of the rigid-body. + */ + public worldCom(): Vector { + return VectorOps.fromRaw(this.rawSet.rbWorldCom(this.handle)); + } + + // #if DIM2 + /** + * The inverse of the principal angular inertia of the rigid-body. + * + * Components set to zero are assumed to be infinite along the corresponding principal axis. + */ + public invPrincipalInertia(): number { + return this.rawSet.rbInvPrincipalInertia(this.handle); + } + + // #endif + + // #if DIM3 + /** + * The inverse of the principal angular inertia of the rigid-body. + * + * Components set to zero are assumed to be infinite along the corresponding principal axis. + */ + public invPrincipalInertia(): Vector { + return VectorOps.fromRaw( + this.rawSet.rbInvPrincipalInertia(this.handle), + ); + } + + // #endif + + // #if DIM2 + /** + * The angular inertia along the principal inertia axes of the rigid-body. + */ + public principalInertia(): number { + return this.rawSet.rbPrincipalInertia(this.handle); + } + + // #endif + + // #if DIM3 + /** + * The angular inertia along the principal inertia axes of the rigid-body. + */ + public principalInertia(): Vector { + return VectorOps.fromRaw(this.rawSet.rbPrincipalInertia(this.handle)); + } + + // #endif + + // #if DIM3 + /** + * The principal vectors of the local angular inertia tensor of the rigid-body. + */ + public principalInertiaLocalFrame(): Rotation { + return RotationOps.fromRaw( + this.rawSet.rbPrincipalInertiaLocalFrame(this.handle), + ); + } + + // #endif + + // #if DIM2 + /** + * The world-space inverse angular inertia tensor of the rigid-body, + * taking into account rotation locking. + */ + public effectiveWorldInvInertia(): number { + return this.rawSet.rbEffectiveWorldInvInertia(this.handle); + } + + // #endif + + // #if DIM3 + /** + * The world-space inverse angular inertia tensor of the rigid-body, + * taking into account rotation locking. + */ + public effectiveWorldInvInertia(): SdpMatrix3 { + return SdpMatrix3Ops.fromRaw( + this.rawSet.rbEffectiveWorldInvInertia(this.handle), + ); + } + + // #endif + + // #if DIM2 + /** + * The effective world-space angular inertia (that takes the potential rotation locking into account) of + * this rigid-body. + */ + public effectiveAngularInertia(): number { + return this.rawSet.rbEffectiveAngularInertia(this.handle); + } + + // #endif + + // #if DIM3 + /** + * The effective world-space angular inertia (that takes the potential rotation locking into account) of + * this rigid-body. + */ + public effectiveAngularInertia(): SdpMatrix3 { + return SdpMatrix3Ops.fromRaw( + this.rawSet.rbEffectiveAngularInertia(this.handle), + ); + } + + // #endif + + /** + * Put this rigid body to sleep. + * + * A sleeping body no longer moves and is no longer simulated by the physics engine unless + * it is waken up. It can be woken manually with `this.wakeUp()` or automatically due to + * external forces like contacts. + */ + public sleep() { + this.rawSet.rbSleep(this.handle); + } + + /** + * Wakes this rigid-body up. + * + * A dynamic rigid-body that does not move during several consecutive frames will + * be put to sleep by the physics engine, i.e., it will stop being simulated in order + * to avoid useless computations. + * This methods forces a sleeping rigid-body to wake-up. This is useful, e.g., before modifying + * the position of a dynamic body so that it is properly simulated afterwards. + */ + public wakeUp() { + this.rawSet.rbWakeUp(this.handle); + } + + /** + * Is CCD enabled for this rigid-body? + */ + public isCcdEnabled(): boolean { + return this.rawSet.rbIsCcdEnabled(this.handle); + } + + /** + * The number of colliders attached to this rigid-body. + */ + public numColliders(): number { + return this.rawSet.rbNumColliders(this.handle); + } + + /** + * Retrieves the `i-th` collider attached to this rigid-body. + * + * @param i - The index of the collider to retrieve. Must be a number in `[0, this.numColliders()[`. + * This index is **not** the same as the unique identifier of the collider. + */ + public collider(i: number): Collider { + return this.colliderSet.get(this.rawSet.rbCollider(this.handle, i)); + } + + /** + * Sets whether this rigid-body is enabled or not. + * + * @param enabled - Set to `false` to disable this rigid-body and all its attached colliders. + */ + public setEnabled(enabled: boolean) { + this.rawSet.rbSetEnabled(this.handle, enabled); + } + + /** + * Is this rigid-body enabled? + */ + public isEnabled(): boolean { + return this.rawSet.rbIsEnabled(this.handle); + } + + /** + * The status of this rigid-body: static, dynamic, or kinematic. + */ + public bodyType(): RigidBodyType { + return this.rawSet.rbBodyType(this.handle) as number as RigidBodyType; + } + + /** + * Set a new status for this rigid-body: static, dynamic, or kinematic. + */ + public setBodyType(type: RigidBodyType, wakeUp: boolean) { + return this.rawSet.rbSetBodyType( + this.handle, + type as number as RawRigidBodyType, + wakeUp, + ); + } + + /** + * Is this rigid-body sleeping? + */ + public isSleeping(): boolean { + return this.rawSet.rbIsSleeping(this.handle); + } + + /** + * Is the velocity of this rigid-body not zero? + */ + public isMoving(): boolean { + return this.rawSet.rbIsMoving(this.handle); + } + + /** + * Is this rigid-body static? + */ + public isFixed(): boolean { + return this.rawSet.rbIsFixed(this.handle); + } + + /** + * Is this rigid-body kinematic? + */ + public isKinematic(): boolean { + return this.rawSet.rbIsKinematic(this.handle); + } + + /** + * Is this rigid-body dynamic? + */ + public isDynamic(): boolean { + return this.rawSet.rbIsDynamic(this.handle); + } + + /** + * The linear damping coefficient of this rigid-body. + */ + public linearDamping(): number { + return this.rawSet.rbLinearDamping(this.handle); + } + + /** + * The angular damping coefficient of this rigid-body. + */ + public angularDamping(): number { + return this.rawSet.rbAngularDamping(this.handle); + } + + /** + * Sets the linear damping factor applied to this rigid-body. + * + * @param factor - The damping factor to set. + */ + public setLinearDamping(factor: number) { + this.rawSet.rbSetLinearDamping(this.handle, factor); + } + + /** + * Recompute the mass-properties of this rigid-bodies based on its currently attached colliders. + */ + public recomputeMassPropertiesFromColliders() { + this.rawSet.rbRecomputeMassPropertiesFromColliders( + this.handle, + this.colliderSet.raw, + ); + } + + /** + * Sets the rigid-body's additional mass. + * + * The total angular inertia of the rigid-body will be scaled automatically based on this additional mass. If this + * scaling effect isn’t desired, use Self::additional_mass_properties instead of this method. + * + * This is only the "additional" mass because the total mass of the rigid-body is equal to the sum of this + * additional mass and the mass computed from the colliders (with non-zero densities) attached to this rigid-body. + * + * That total mass (which includes the attached colliders’ contributions) will be updated at the name physics step, + * or can be updated manually with `this.recomputeMassPropertiesFromColliders`. + * + * This will override any previous additional mass-properties set by `this.setAdditionalMass`, + * `this.setAdditionalMassProperties`, `RigidBodyDesc::setAdditionalMass`, or + * `RigidBodyDesc.setAdditionalMassfProperties` for this rigid-body. + * + * @param mass - The additional mass to set. + * @param wakeUp - If `true` then the rigid-body will be woken up if it was put to sleep because it did not move for a while. + */ + public setAdditionalMass(mass: number, wakeUp: boolean) { + this.rawSet.rbSetAdditionalMass(this.handle, mass, wakeUp); + } + + // #if DIM3 + /** + * Sets the rigid-body's additional mass-properties. + * + * This is only the "additional" mass-properties because the total mass-properties of the rigid-body is equal to the + * sum of this additional mass-properties and the mass computed from the colliders (with non-zero densities) attached + * to this rigid-body. + * + * That total mass-properties (which include the attached colliders’ contributions) will be updated at the name + * physics step, or can be updated manually with `this.recomputeMassPropertiesFromColliders`. + * + * This will override any previous mass-properties set by `this.setAdditionalMass`, + * `this.setAdditionalMassProperties`, `RigidBodyDesc.setAdditionalMass`, or `RigidBodyDesc.setAdditionalMassProperties` + * for this rigid-body. + * + * If `wake_up` is true then the rigid-body will be woken up if it was put to sleep because it did not move for a while. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: Vector, + angularInertiaLocalFrame: Rotation, + wakeUp: boolean, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + let rawPrincipalInertia = VectorOps.intoRaw(principalAngularInertia); + let rawInertiaFrame = RotationOps.intoRaw(angularInertiaLocalFrame); + + this.rawSet.rbSetAdditionalMassProperties( + this.handle, + mass, + rawCom, + rawPrincipalInertia, + rawInertiaFrame, + wakeUp, + ); + + rawCom.free(); + rawPrincipalInertia.free(); + rawInertiaFrame.free(); + } + + // #endif + + // #if DIM2 + /** + * Sets the rigid-body's additional mass-properties. + * + * This is only the "additional" mass-properties because the total mass-properties of the rigid-body is equal to the + * sum of this additional mass-properties and the mass computed from the colliders (with non-zero densities) attached + * to this rigid-body. + * + * That total mass-properties (which include the attached colliders’ contributions) will be updated at the name + * physics step, or can be updated manually with `this.recomputeMassPropertiesFromColliders`. + * + * This will override any previous mass-properties set by `this.setAdditionalMass`, + * `this.setAdditionalMassProperties`, `RigidBodyDesc.setAdditionalMass`, or `RigidBodyDesc.setAdditionalMassProperties` + * for this rigid-body. + * + * If `wake_up` is true then the rigid-body will be woken up if it was put to sleep because it did not move for a while. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + wakeUp: boolean, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + this.rawSet.rbSetAdditionalMassProperties( + this.handle, + mass, + rawCom, + principalAngularInertia, + wakeUp, + ); + rawCom.free(); + } + + // #endif + + /** + * Sets the linear damping factor applied to this rigid-body. + * + * @param factor - The damping factor to set. + */ + public setAngularDamping(factor: number) { + this.rawSet.rbSetAngularDamping(this.handle, factor); + } + + /** + * Resets to zero the user forces (but not torques) applied to this rigid-body. + * + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public resetForces(wakeUp: boolean) { + this.rawSet.rbResetForces(this.handle, wakeUp); + } + + /** + * Resets to zero the user torques applied to this rigid-body. + * + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public resetTorques(wakeUp: boolean) { + this.rawSet.rbResetTorques(this.handle, wakeUp); + } + + /** + * Adds a force at the center-of-mass of this rigid-body. + * + * @param force - the world-space force to add to the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addForce(force: Vector, wakeUp: boolean) { + const rawForce = VectorOps.intoRaw(force); + this.rawSet.rbAddForce(this.handle, rawForce, wakeUp); + rawForce.free(); + } + + /** + * Applies an impulse at the center-of-mass of this rigid-body. + * + * @param impulse - the world-space impulse to apply on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyImpulse(impulse: Vector, wakeUp: boolean) { + const rawImpulse = VectorOps.intoRaw(impulse); + this.rawSet.rbApplyImpulse(this.handle, rawImpulse, wakeUp); + rawImpulse.free(); + } + + // #if DIM2 + /** + * Adds a torque at the center-of-mass of this rigid-body. + * + * @param torque - the torque to add to the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addTorque(torque: number, wakeUp: boolean) { + this.rawSet.rbAddTorque(this.handle, torque, wakeUp); + } + + // #endif + + // #if DIM3 + /** + * Adds a torque at the center-of-mass of this rigid-body. + * + * @param torque - the world-space torque to add to the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addTorque(torque: Vector, wakeUp: boolean) { + const rawTorque = VectorOps.intoRaw(torque); + this.rawSet.rbAddTorque(this.handle, rawTorque, wakeUp); + rawTorque.free(); + } + + // #endif + + // #if DIM2 + /** + * Applies an impulsive torque at the center-of-mass of this rigid-body. + * + * @param torqueImpulse - the torque impulse to apply on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyTorqueImpulse(torqueImpulse: number, wakeUp: boolean) { + this.rawSet.rbApplyTorqueImpulse(this.handle, torqueImpulse, wakeUp); + } + + // #endif + + // #if DIM3 + /** + * Applies an impulsive torque at the center-of-mass of this rigid-body. + * + * @param torqueImpulse - the world-space torque impulse to apply on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyTorqueImpulse(torqueImpulse: Vector, wakeUp: boolean) { + const rawTorqueImpulse = VectorOps.intoRaw(torqueImpulse); + this.rawSet.rbApplyTorqueImpulse(this.handle, rawTorqueImpulse, wakeUp); + rawTorqueImpulse.free(); + } + + // #endif + + /** + * Adds a force at the given world-space point of this rigid-body. + * + * @param force - the world-space force to add to the rigid-body. + * @param point - the world-space point where the impulse is to be applied on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addForceAtPoint(force: Vector, point: Vector, wakeUp: boolean) { + const rawForce = VectorOps.intoRaw(force); + const rawPoint = VectorOps.intoRaw(point); + this.rawSet.rbAddForceAtPoint(this.handle, rawForce, rawPoint, wakeUp); + rawForce.free(); + rawPoint.free(); + } + + /** + * Applies an impulse at the given world-space point of this rigid-body. + * + * @param impulse - the world-space impulse to apply on the rigid-body. + * @param point - the world-space point where the impulse is to be applied on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyImpulseAtPoint( + impulse: Vector, + point: Vector, + wakeUp: boolean, + ) { + const rawImpulse = VectorOps.intoRaw(impulse); + const rawPoint = VectorOps.intoRaw(point); + this.rawSet.rbApplyImpulseAtPoint( + this.handle, + rawImpulse, + rawPoint, + wakeUp, + ); + rawImpulse.free(); + rawPoint.free(); + } + + /** + * Retrieves the constant force(s) the user added to this rigid-body + * Returns zero if the rigid-body is not dynamic. + */ + public userForce(): Vector { + return VectorOps.fromRaw(this.rawSet.rbUserForce(this.handle)); + } + + // #if DIM2 + /** + * Retrieves the constant torque(s) the user added to this rigid-body + * Returns zero if the rigid-body is not dynamic. + */ + public userTorque(): number { + return this.rawSet.rbUserTorque(this.handle); + } + // #endif + + // #if DIM3 + /** + * Retrieves the constant torque(s) the user added to this rigid-body + * Returns zero if the rigid-body is not dynamic. + */ + public userTorque(): Vector { + return VectorOps.fromRaw(this.rawSet.rbUserTorque(this.handle)); + } + // #endif +} + +export class RigidBodyDesc { + enabled: boolean; + translation: Vector; + rotation: Rotation; + gravityScale: number; + mass: number; + massOnly: boolean; + centerOfMass: Vector; + translationsEnabledX: boolean; + translationsEnabledY: boolean; + linvel: Vector; + // #if DIM2 + angvel: number; + principalAngularInertia: number; + rotationsEnabled: boolean; + // #endif + // #if DIM3 + angvel: Vector; + principalAngularInertia: Vector; + angularInertiaLocalFrame: Rotation; + translationsEnabledZ: boolean; + rotationsEnabledX: boolean; + rotationsEnabledY: boolean; + rotationsEnabledZ: boolean; + // #endif + linearDamping: number; + angularDamping: number; + status: RigidBodyType; + canSleep: boolean; + sleeping: boolean; + ccdEnabled: boolean; + softCcdPrediction: number; + dominanceGroup: number; + additionalSolverIterations: number; + userData?: unknown; + + constructor(status: RigidBodyType) { + this.enabled = true; + this.status = status; + this.translation = VectorOps.zeros(); + this.rotation = RotationOps.identity(); + this.gravityScale = 1.0; + this.linvel = VectorOps.zeros(); + this.mass = 0.0; + this.massOnly = false; + this.centerOfMass = VectorOps.zeros(); + this.translationsEnabledX = true; + this.translationsEnabledY = true; + // #if DIM2 + this.angvel = 0.0; + this.principalAngularInertia = 0.0; + this.rotationsEnabled = true; + // #endif + // #if DIM3 + this.angvel = VectorOps.zeros(); + this.principalAngularInertia = VectorOps.zeros(); + this.angularInertiaLocalFrame = RotationOps.identity(); + this.translationsEnabledZ = true; + this.rotationsEnabledX = true; + this.rotationsEnabledY = true; + this.rotationsEnabledZ = true; + // #endif + this.linearDamping = 0.0; + this.angularDamping = 0.0; + this.canSleep = true; + this.sleeping = false; + this.ccdEnabled = false; + this.softCcdPrediction = 0.0; + this.dominanceGroup = 0; + this.additionalSolverIterations = 0; + } + + /** + * A rigid-body descriptor used to build a dynamic rigid-body. + */ + public static dynamic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Dynamic); + } + + /** + * A rigid-body descriptor used to build a position-based kinematic rigid-body. + */ + public static kinematicPositionBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicPositionBased); + } + + /** + * A rigid-body descriptor used to build a velocity-based kinematic rigid-body. + */ + public static kinematicVelocityBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicVelocityBased); + } + + /** + * A rigid-body descriptor used to build a fixed rigid-body. + */ + public static fixed(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Fixed); + } + + /** + * A rigid-body descriptor used to build a dynamic rigid-body. + * + * @deprecated The method has been renamed to `.dynamic()`. + */ + public static newDynamic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Dynamic); + } + + /** + * A rigid-body descriptor used to build a position-based kinematic rigid-body. + * + * @deprecated The method has been renamed to `.kinematicPositionBased()`. + */ + public static newKinematicPositionBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicPositionBased); + } + + /** + * A rigid-body descriptor used to build a velocity-based kinematic rigid-body. + * + * @deprecated The method has been renamed to `.kinematicVelocityBased()`. + */ + public static newKinematicVelocityBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicVelocityBased); + } + + /** + * A rigid-body descriptor used to build a fixed rigid-body. + * + * @deprecated The method has been renamed to `.fixed()`. + */ + public static newStatic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Fixed); + } + + public setDominanceGroup(group: number): RigidBodyDesc { + this.dominanceGroup = group; + return this; + } + + /** + * Sets the number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + * + * Compared to increasing the global `World.numSolverIteration`, setting this + * value lets you increase accuracy on only a subset of the scene, resulting in reduced + * performance loss. + * + * @param iters - The new number of additional solver iterations (default: 0). + */ + public setAdditionalSolverIterations(iters: number): RigidBodyDesc { + this.additionalSolverIterations = iters; + return this; + } + + /** + * Sets whether the created rigid-body will be enabled or disabled. + * @param enabled − If set to `false` the rigid-body will be disabled at creation. + */ + public setEnabled(enabled: boolean): RigidBodyDesc { + this.enabled = enabled; + return this; + } + + // #if DIM2 + /** + * Sets the initial translation of the rigid-body to create. + */ + public setTranslation(x: number, y: number): RigidBodyDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y}; + return this; + } + + // #endif + + // #if DIM3 + /** + * Sets the initial translation of the rigid-body to create. + * + * @param tra - The translation to set. + */ + public setTranslation(x: number, y: number, z: number): RigidBodyDesc { + if ( + typeof x != "number" || + typeof y != "number" || + typeof z != "number" + ) + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y, z: z}; + return this; + } + + // #endif + + /** + * Sets the initial rotation of the rigid-body to create. + * + * @param rot - The rotation to set. + */ + public setRotation(rot: Rotation): RigidBodyDesc { + // #if DIM2 + this.rotation = rot; + // #endif + // #if DIM3 + RotationOps.copy(this.rotation, rot); + // #endif + return this; + } + + /** + * Sets the scale factor applied to the gravity affecting + * the rigid-body being built. + * + * @param scale - The scale factor. Set this to `0.0` if the rigid-body + * needs to ignore gravity. + */ + public setGravityScale(scale: number): RigidBodyDesc { + this.gravityScale = scale; + return this; + } + + /** + * Sets the initial mass of the rigid-body being built, before adding colliders' contributions. + * + * @param mass − The initial mass of the rigid-body to create. + */ + public setAdditionalMass(mass: number): RigidBodyDesc { + this.mass = mass; + this.massOnly = true; + return this; + } + + // #if DIM2 + /** + * Sets the initial linear velocity of the rigid-body to create. + * + * @param x - The linear velocity to set along the `x` axis. + * @param y - The linear velocity to set along the `y` axis. + */ + public setLinvel(x: number, y: number): RigidBodyDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The linvel components must be numbers."); + + this.linvel = {x: x, y: y}; + return this; + } + + /** + * Sets the initial angular velocity of the rigid-body to create. + * + * @param vel - The angular velocity to set. + */ + public setAngvel(vel: number): RigidBodyDesc { + this.angvel = vel; + return this; + } + + /** + * Sets the mass properties of the rigid-body being built. + * + * Note that the final mass properties of the rigid-bodies depends + * on the initial mass-properties of the rigid-body (set by this method) + * to which is added the contributions of all the colliders with non-zero density + * attached to this rigid-body. + * + * Therefore, if you want your provided mass properties to be the final + * mass properties of your rigid-body, don't attach colliders to it, or + * only attach colliders with densities equal to zero. + * + * @param mass − The initial mass of the rigid-body to create. + * @param centerOfMass − The initial center-of-mass of the rigid-body to create. + * @param principalAngularInertia − The initial principal angular inertia of the rigid-body to create. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ): RigidBodyDesc { + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + this.principalAngularInertia = principalAngularInertia; + this.massOnly = false; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + */ + public enabledTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + ): RigidBodyDesc { + this.translationsEnabledX = translationsEnabledX; + this.translationsEnabledY = translationsEnabledY; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + * @deprecated use `this.enabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + ): RigidBodyDesc { + return this.enabledTranslations( + translationsEnabledX, + translationsEnabledY, + ); + } + + /** + * Locks all translations that would have resulted from forces on + * the created rigid-body. + */ + public lockTranslations(): RigidBodyDesc { + return this.restrictTranslations(false, false); + } + + /** + * Locks all rotations that would have resulted from forces on + * the created rigid-body. + */ + public lockRotations(): RigidBodyDesc { + this.rotationsEnabled = false; + return this; + } + + // #endif + + // #if DIM3 + /** + * Sets the initial linear velocity of the rigid-body to create. + * + * @param x - The linear velocity to set along the `x` axis. + * @param y - The linear velocity to set along the `y` axis. + * @param z - The linear velocity to set along the `z` axis. + */ + public setLinvel(x: number, y: number, z: number): RigidBodyDesc { + if ( + typeof x != "number" || + typeof y != "number" || + typeof z != "number" + ) + throw TypeError("The linvel components must be numbers."); + + this.linvel = {x: x, y: y, z: z}; + return this; + } + + /** + * Sets the initial angular velocity of the rigid-body to create. + * + * @param vel - The angular velocity to set. + */ + public setAngvel(vel: Vector): RigidBodyDesc { + VectorOps.copy(this.angvel, vel); + return this; + } + + /** + * Sets the mass properties of the rigid-body being built. + * + * Note that the final mass properties of the rigid-bodies depends + * on the initial mass-properties of the rigid-body (set by this method) + * to which is added the contributions of all the colliders with non-zero density + * attached to this rigid-body. + * + * Therefore, if you want your provided mass properties to be the final + * mass properties of your rigid-body, don't attach colliders to it, or + * only attach colliders with densities equal to zero. + * + * @param mass − The initial mass of the rigid-body to create. + * @param centerOfMass − The initial center-of-mass of the rigid-body to create. + * @param principalAngularInertia − The initial principal angular inertia of the rigid-body to create. + * These are the eigenvalues of the angular inertia matrix. + * @param angularInertiaLocalFrame − The initial local angular inertia frame of the rigid-body to create. + * These are the eigenvectors of the angular inertia matrix. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: Vector, + angularInertiaLocalFrame: Rotation, + ): RigidBodyDesc { + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + VectorOps.copy(this.principalAngularInertia, principalAngularInertia); + RotationOps.copy( + this.angularInertiaLocalFrame, + angularInertiaLocalFrame, + ); + this.massOnly = false; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + * @param translationsEnabledZ - Are translations along the Z axis enabled? + */ + public enabledTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + translationsEnabledZ: boolean, + ): RigidBodyDesc { + this.translationsEnabledX = translationsEnabledX; + this.translationsEnabledY = translationsEnabledY; + this.translationsEnabledZ = translationsEnabledZ; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + * @param translationsEnabledZ - Are translations along the Z axis enabled? + * @deprecated use `this.enabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + translationsEnabledZ: boolean, + ): RigidBodyDesc { + return this.enabledTranslations( + translationsEnabledX, + translationsEnabledY, + translationsEnabledZ, + ); + } + + /** + * Locks all translations that would have resulted from forces on + * the created rigid-body. + */ + public lockTranslations(): RigidBodyDesc { + return this.enabledTranslations(false, false, false); + } + + /** + * Allow rotation of this rigid-body only along specific axes. + * @param rotationsEnabledX - Are rotations along the X axis enabled? + * @param rotationsEnabledY - Are rotations along the y axis enabled? + * @param rotationsEnabledZ - Are rotations along the Z axis enabled? + */ + public enabledRotations( + rotationsEnabledX: boolean, + rotationsEnabledY: boolean, + rotationsEnabledZ: boolean, + ): RigidBodyDesc { + this.rotationsEnabledX = rotationsEnabledX; + this.rotationsEnabledY = rotationsEnabledY; + this.rotationsEnabledZ = rotationsEnabledZ; + return this; + } + + /** + * Allow rotation of this rigid-body only along specific axes. + * @param rotationsEnabledX - Are rotations along the X axis enabled? + * @param rotationsEnabledY - Are rotations along the y axis enabled? + * @param rotationsEnabledZ - Are rotations along the Z axis enabled? + * @deprecated use `this.enabledRotations` with the same arguments instead. + */ + public restrictRotations( + rotationsEnabledX: boolean, + rotationsEnabledY: boolean, + rotationsEnabledZ: boolean, + ): RigidBodyDesc { + return this.enabledRotations( + rotationsEnabledX, + rotationsEnabledY, + rotationsEnabledZ, + ); + } + + /** + * Locks all rotations that would have resulted from forces on + * the created rigid-body. + */ + public lockRotations(): RigidBodyDesc { + return this.restrictRotations(false, false, false); + } + + // #endif + + /** + * Sets the linear damping of the rigid-body to create. + * + * This will progressively slowdown the translational movement of the rigid-body. + * + * @param damping - The angular damping coefficient. Should be >= 0. The higher this + * value is, the stronger the translational slowdown will be. + */ + public setLinearDamping(damping: number): RigidBodyDesc { + this.linearDamping = damping; + return this; + } + + /** + * Sets the angular damping of the rigid-body to create. + * + * This will progressively slowdown the rotational movement of the rigid-body. + * + * @param damping - The angular damping coefficient. Should be >= 0. The higher this + * value is, the stronger the rotational slowdown will be. + */ + public setAngularDamping(damping: number): RigidBodyDesc { + this.angularDamping = damping; + return this; + } + + /** + * Sets whether or not the rigid-body to create can sleep. + * + * @param can - true if the rigid-body can sleep, false if it can't. + */ + public setCanSleep(can: boolean): RigidBodyDesc { + this.canSleep = can; + return this; + } + + /** + * Sets whether or not the rigid-body is to be created asleep. + * + * @param can - true if the rigid-body should be in sleep, default false. + */ + setSleeping(sleeping: boolean): RigidBodyDesc { + this.sleeping = sleeping; + return this; + } + + /** + * Sets whether Continuous Collision Detection (CCD) is enabled for this rigid-body. + * + * @param enabled - true if the rigid-body has CCD enabled. + */ + public setCcdEnabled(enabled: boolean): RigidBodyDesc { + this.ccdEnabled = enabled; + return this; + } + + /** + * Sets the maximum prediction distance Soft Continuous Collision-Detection. + * + * When set to 0, soft-CCD is disabled. Soft-CCD helps prevent tunneling especially of + * slow-but-thin to moderately fast objects. The soft CCD prediction distance indicates how + * far in the object’s path the CCD algorithm is allowed to inspect. Large values can impact + * performance badly by increasing the work needed from the broad-phase. + * + * It is a generally cheaper variant of regular CCD (that can be enabled with + * `RigidBodyDesc::setCcdEnabled` since it relies on predictive constraints instead of + * shape-cast and substeps. + */ + public setSoftCcdPrediction(distance: number): RigidBodyDesc { + this.softCcdPrediction = distance; + return this; + } + + /** + * Sets the user-defined object of this rigid-body. + * + * @param userData - The user-defined object to set. + */ + public setUserData(data?: unknown): RigidBodyDesc { + this.userData = data; + return this; + } +} diff --git a/thirdparty/rapier.js/src.ts/dynamics/rigid_body_set.ts b/thirdparty/rapier.js/src.ts/dynamics/rigid_body_set.ts new file mode 100644 index 00000000..4c2e47bd --- /dev/null +++ b/thirdparty/rapier.js/src.ts/dynamics/rigid_body_set.ts @@ -0,0 +1,238 @@ +import {RawRigidBodySet, RawRigidBodyType} from "../raw"; +import {Coarena} from "../coarena"; +import {VectorOps, RotationOps} from "../math"; +import { + RigidBody, + RigidBodyDesc, + RigidBodyHandle, + RigidBodyType, +} from "./rigid_body"; +import {ColliderSet} from "../geometry"; +import {ImpulseJointSet} from "./impulse_joint_set"; +import {MultibodyJointSet} from "./multibody_joint_set"; +import {IslandManager} from "./island_manager"; + +/** + * A set of rigid bodies that can be handled by a physics pipeline. + * + * To avoid leaking WASM resources, this MUST be freed manually with `rigidBodySet.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class RigidBodySet { + raw: RawRigidBodySet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this rigid-body set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawRigidBodySet) { + this.raw = raw || new RawRigidBodySet(); + this.map = new Coarena(); + // deserialize + if (raw) { + raw.forEachRigidBodyHandle((handle: RigidBodyHandle) => { + this.map.set(handle, new RigidBody(raw, null, handle)); + }); + } + } + + /** + * Internal method, do not call this explicitly. + */ + public finalizeDeserialization(colliderSet: ColliderSet) { + this.map.forEach((rb) => rb.finalizeDeserialization(colliderSet)); + } + + /** + * Creates a new rigid-body and return its integer handle. + * + * @param desc - The description of the rigid-body to create. + */ + public createRigidBody( + colliderSet: ColliderSet, + desc: RigidBodyDesc, + ): RigidBody { + let rawTra = VectorOps.intoRaw(desc.translation); + let rawRot = RotationOps.intoRaw(desc.rotation); + let rawLv = VectorOps.intoRaw(desc.linvel); + let rawCom = VectorOps.intoRaw(desc.centerOfMass); + + // #if DIM3 + let rawAv = VectorOps.intoRaw(desc.angvel); + let rawPrincipalInertia = VectorOps.intoRaw( + desc.principalAngularInertia, + ); + let rawInertiaFrame = RotationOps.intoRaw( + desc.angularInertiaLocalFrame, + ); + // #endif + + let handle = this.raw.createRigidBody( + desc.enabled, + rawTra, + rawRot, + desc.gravityScale, + desc.mass, + desc.massOnly, + rawCom, + rawLv, + // #if DIM2 + desc.angvel, + desc.principalAngularInertia, + desc.translationsEnabledX, + desc.translationsEnabledY, + desc.rotationsEnabled, + // #endif + // #if DIM3 + rawAv, + rawPrincipalInertia, + rawInertiaFrame, + desc.translationsEnabledX, + desc.translationsEnabledY, + desc.translationsEnabledZ, + desc.rotationsEnabledX, + desc.rotationsEnabledY, + desc.rotationsEnabledZ, + // #endif + desc.linearDamping, + desc.angularDamping, + desc.status as number as RawRigidBodyType, + desc.canSleep, + desc.sleeping, + desc.softCcdPrediction, + desc.ccdEnabled, + desc.dominanceGroup, + desc.additionalSolverIterations, + ); + + rawTra.free(); + rawRot.free(); + rawLv.free(); + rawCom.free(); + + // #if DIM3 + rawAv.free(); + rawPrincipalInertia.free(); + rawInertiaFrame.free(); + // #endif + + const body = new RigidBody(this.raw, colliderSet, handle); + body.userData = desc.userData; + + this.map.set(handle, body); + + return body; + } + + /** + * Removes a rigid-body from this set. + * + * This will also remove all the colliders and joints attached to the rigid-body. + * + * @param handle - The integer handle of the rigid-body to remove. + * @param colliders - The set of colliders that may contain colliders attached to the removed rigid-body. + * @param impulseJoints - The set of impulse joints that may contain joints attached to the removed rigid-body. + * @param multibodyJoints - The set of multibody joints that may contain joints attached to the removed rigid-body. + */ + public remove( + handle: RigidBodyHandle, + islands: IslandManager, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ) { + // Unmap the entities that will be removed automatically because of the rigid-body removals. + for (let i = 0; i < this.raw.rbNumColliders(handle); i += 1) { + colliders.unmap(this.raw.rbCollider(handle, i)); + } + + impulseJoints.forEachJointHandleAttachedToRigidBody(handle, (handle) => + impulseJoints.unmap(handle), + ); + multibodyJoints.forEachJointHandleAttachedToRigidBody( + handle, + (handle) => multibodyJoints.unmap(handle), + ); + + // Remove the rigid-body. + this.raw.remove( + handle, + islands.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ); + this.map.delete(handle); + } + + /** + * The number of rigid-bodies on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a rigid-body with the given handle? + * + * @param handle - The rigid-body handle to check. + */ + public contains(handle: RigidBodyHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the rigid-body with the given handle. + * + * @param handle - The handle of the rigid-body to retrieve. + */ + public get(handle: RigidBodyHandle): RigidBody | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each rigid-body contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (body: RigidBody) => void) { + this.map.forEach(f); + } + + /** + * Applies the given closure to each active rigid-bodies contained by this set. + * + * A rigid-body is active if it is not sleeping, i.e., if it moved recently. + * + * @param f - The closure to apply. + */ + public forEachActiveRigidBody( + islands: IslandManager, + f: (body: RigidBody) => void, + ) { + islands.forEachActiveRigidBodyHandle((handle) => { + f(this.get(handle)); + }); + } + + /** + * Gets all rigid-bodies in the list. + * + * @returns rigid-bodies list. + */ + public getAll(): RigidBody[] { + return this.map.getAll(); + } +} diff --git a/thirdparty/rapier.js/src.ts/exports.ts b/thirdparty/rapier.js/src.ts/exports.ts new file mode 100644 index 00000000..237bd545 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/exports.ts @@ -0,0 +1,27 @@ +import {version as vers, reserve_memory as reserve} from "./raw"; + +export function version(): string { + return vers(); +} + +/// Reserves additional memory in WASM land. +/// +/// This will grow the internal WASM memory buffer so that it can fit at least +/// the specified amount of extra bytes. This can help reduce future runtime +/// overhead due to dynamic internal memory growth once the limit of the +/// pre-allocated memory is reached. +/// +/// This feature is still experimental. Due to the nature of the internal +/// allocator, there can be situations where the allocator decides to perform +/// additional internal memory growth even though not all `extraBytesCount` +/// are occupied yet. +export function reserveMemory(extraBytesCount: number) { + reserve(extraBytesCount); +} + +export * from "./math"; +export * from "./dynamics"; +export * from "./geometry"; +export * from "./pipeline"; +export * from "./init"; +export * from "./control"; diff --git a/thirdparty/rapier.js/src.ts/geometry/broad_phase.ts b/thirdparty/rapier.js/src.ts/geometry/broad_phase.ts new file mode 100644 index 00000000..addaee92 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/broad_phase.ts @@ -0,0 +1,520 @@ +import {RawBroadPhase, RawRayColliderIntersection} from "../raw"; +import {RigidBodyHandle, RigidBodySet} from "../dynamics"; +import {ColliderSet} from "./collider_set"; +import {Ray, RayColliderHit, RayColliderIntersection} from "./ray"; +import {InteractionGroups} from "./interaction_groups"; +import {ColliderHandle} from "./collider"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import {Shape} from "./shape"; +import {PointColliderProjection} from "./point"; +import {ColliderShapeCastHit} from "./toi"; +import {QueryFilterFlags} from "../pipeline"; +import {NarrowPhase} from "./narrow_phase"; + +/** + * The broad-phase used for coarse collision-detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `broadPhase.free()` + * once you are done using it. + */ +export class BroadPhase { + raw: RawBroadPhase; + + /** + * Release the WASM memory occupied by this broad-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawBroadPhase) { + this.raw = raw || new RawBroadPhase(); + } + + /** + * Find the closest intersection between a ray and a set of collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param filter - The callback to filter out which collider will be hit. + */ + public castRay( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): RayColliderHit | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayColliderHit.fromRaw( + colliders, + this.raw.castRay( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Find the closest intersection between a ray and a set of collider. + * + * This also computes the normal at the hit point. + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + */ + public castRayAndGetNormal( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): RayColliderIntersection | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayColliderIntersection.fromRaw( + colliders, + this.raw.castRayAndGetNormal( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Cast a ray and collects all the intersections between a ray and the scene. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param callback - The callback called once per hit (in no particular order) between a ray and a collider. + * If this callback returns `false`, then the cast will stop and no further hits will be detected/reported. + */ + public intersectionsWithRay( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + callback: (intersect: RayColliderIntersection) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let rawCallback = (rawInter: RawRayColliderIntersection) => { + return callback( + RayColliderIntersection.fromRaw(colliders, rawInter), + ); + }; + + this.raw.intersectionsWithRay( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + rawCallback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawOrig.free(); + rawDir.free(); + } + + /** + * Gets the handle of up to one collider intersecting the given shape. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The position of the shape used for the intersection test. + * @param shapeRot - The orientation of the shape used for the intersection test. + * @param shape - The shape used for the intersection test. + * @param groups - The bit groups and filter associated to the ray, in order to only + * hit the colliders with collision groups compatible with the ray's group. + */ + public intersectionWithShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): ColliderHandle | null { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawShape = shape.intoRaw(); + let result = this.raw.intersectionWithShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawShape, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPos.free(); + rawRot.free(); + rawShape.free(); + + return result; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPoint( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): PointColliderProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointColliderProjection.fromRaw( + colliders, + this.raw.projectPoint( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPoint.free(); + + return result; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point to project. + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPointAndGetFeature( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): PointColliderProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointColliderProjection.fromRaw( + colliders, + this.raw.projectPointAndGetFeature( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPoint.free(); + + return result; + } + + /** + * Find all the colliders containing the given point. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point used for the containment test. + * @param groups - The bit groups and filter associated to the point to test, in order to only + * test on colliders with collision groups compatible with the ray's group. + * @param callback - A function called with the handles of each collider with a shape + * containing the `point`. + */ + public intersectionsWithPoint( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + callback: (handle: ColliderHandle) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawPoint = VectorOps.intoRaw(point); + + this.raw.intersectionsWithPoint( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + callback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPoint.free(); + } + + /** + * Casts a shape at a constant linear velocity and retrieve the first collider it hits. + * This is similar to ray-casting except that we are casting a whole shape instead of + * just a point (the ray origin). + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The initial position of the shape to cast. + * @param shapeRot - The initial rotation of the shape to cast. + * @param shapeVel - The constant velocity of the shape to cast (i.e. the cast direction). + * @param shape - The shape to cast. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @param groups - The bit groups and filter associated to the shape to cast, in order to only + * test on colliders with collision groups compatible with this group. + */ + public castShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shapeVel: Vector, + shape: Shape, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): ColliderShapeCastHit | null { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawVel = VectorOps.intoRaw(shapeVel); + let rawShape = shape.intoRaw(); + + let result = ColliderShapeCastHit.fromRaw( + colliders, + this.raw.castShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawVel, + rawShape, + targetDistance, + maxToi, + stopAtPenetration, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPos.free(); + rawRot.free(); + rawVel.free(); + rawShape.free(); + + return result; + } + + /** + * Retrieve all the colliders intersecting the given shape. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The position of the shape to test. + * @param shapeRot - The orientation of the shape to test. + * @param shape - The shape to test. + * @param groups - The bit groups and filter associated to the shape to test, in order to only + * test on colliders with collision groups compatible with this group. + * @param callback - A function called with the handles of each collider intersecting the `shape`. + */ + public intersectionsWithShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + callback: (handle: ColliderHandle) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawShape = shape.intoRaw(); + + this.raw.intersectionsWithShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawShape, + callback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPos.free(); + rawRot.free(); + rawShape.free(); + } + + /** + * Finds the handles of all the colliders with an AABB intersecting the given AABB. + * + * @param aabbCenter - The center of the AABB to test. + * @param aabbHalfExtents - The half-extents of the AABB to test. + * @param callback - The callback that will be called with the handles of all the colliders + * currently intersecting the given AABB. + */ + public collidersWithAabbIntersectingAabb( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + aabbCenter: Vector, + aabbHalfExtents: Vector, + callback: (handle: ColliderHandle) => boolean, + ) { + let rawCenter = VectorOps.intoRaw(aabbCenter); + let rawHalfExtents = VectorOps.intoRaw(aabbHalfExtents); + this.raw.collidersWithAabbIntersectingAabb( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawCenter, + rawHalfExtents, + callback, + ); + rawCenter.free(); + rawHalfExtents.free(); + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/collider.ts b/thirdparty/rapier.js/src.ts/geometry/collider.ts new file mode 100644 index 00000000..25817362 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/collider.ts @@ -0,0 +1,1983 @@ +import {RawColliderSet} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import { + CoefficientCombineRule, + RigidBody, + RigidBodyHandle, + RigidBodySet, +} from "../dynamics"; +import {ActiveHooks, ActiveEvents} from "../pipeline"; +import {InteractionGroups} from "./interaction_groups"; +import { + Shape, + Cuboid, + Ball, + ShapeType, + Capsule, + Voxels, + TriMesh, + Polyline, + Heightfield, + Segment, + Triangle, + RoundTriangle, + RoundCuboid, + HalfSpace, + TriMeshFlags, + // #if DIM2 + ConvexPolygon, + RoundConvexPolygon, + // #endif + // #if DIM3 + Cylinder, + RoundCylinder, + Cone, + RoundCone, + ConvexPolyhedron, + RoundConvexPolyhedron, + HeightFieldFlags, + // #endif +} from "./shape"; +import {Ray, RayIntersection} from "./ray"; +import {PointProjection} from "./point"; +import {ColliderShapeCastHit, ShapeCastHit} from "./toi"; +import {ShapeContact} from "./contact"; +import {ColliderSet} from "./collider_set"; + +/** + * Flags affecting whether collision-detection happens between two colliders + * depending on the type of rigid-bodies they are attached to. + */ +export enum ActiveCollisionTypes { + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a dynamic body. + */ + DYNAMIC_DYNAMIC = 0b0000_0000_0000_0001, + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a kinematic body. + */ + DYNAMIC_KINEMATIC = 0b0000_0000_0000_1100, + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a fixed body (or not attached to any body). + */ + DYNAMIC_FIXED = 0b0000_0000_0000_0010, + /** + * Enable collision-detection between a collider attached to a kinematic body + * and another collider attached to a kinematic body. + */ + KINEMATIC_KINEMATIC = 0b1100_1100_0000_0000, + + /** + * Enable collision-detection between a collider attached to a kinematic body + * and another collider attached to a fixed body (or not attached to any body). + */ + KINEMATIC_FIXED = 0b0010_0010_0000_0000, + + /** + * Enable collision-detection between a collider attached to a fixed body (or + * not attached to any body) and another collider attached to a fixed body (or + * not attached to any body). + */ + FIXED_FIXED = 0b0000_0000_0010_0000, + /** + * The default active collision types, enabling collisions between a dynamic body + * and another body of any type, but not enabling collisions between two non-dynamic bodies. + */ + DEFAULT = DYNAMIC_KINEMATIC | DYNAMIC_DYNAMIC | DYNAMIC_FIXED, + /** + * Enable collisions between any kind of rigid-bodies (including between two non-dynamic bodies). + */ + ALL = DYNAMIC_KINEMATIC | + DYNAMIC_DYNAMIC | + DYNAMIC_FIXED | + KINEMATIC_KINEMATIC | + KINEMATIC_FIXED | + KINEMATIC_KINEMATIC, +} + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type ColliderHandle = number; + +/** + * A geometric entity that can be attached to a body so it can be affected + * by contacts and proximity queries. + */ +export class Collider { + private colliderSet: ColliderSet; // The Collider won't need to free this. + readonly handle: ColliderHandle; + private _shape: Shape; // TODO: deprecate/remove this since it isn’t a reliable way of getting the latest shape properties. + private _parent: RigidBody | null; + + constructor( + colliderSet: ColliderSet, + handle: ColliderHandle, + parent: RigidBody | null, + shape?: Shape, + ) { + this.colliderSet = colliderSet; + this.handle = handle; + this._parent = parent; + this._shape = shape; + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + if (this.handle != null) { + this._parent = bodies.get( + this.colliderSet.raw.coParent(this.handle), + ); + } + } + + private ensureShapeIsCached() { + if (!this._shape) + this._shape = Shape.fromRaw(this.colliderSet.raw, this.handle); + } + + /** + * The shape of this collider. + */ + public get shape(): Shape { + this.ensureShapeIsCached(); + return this._shape; + } + + /** + * Set the internal cached JS shape to null. + * + * This can be useful if you want to free some memory (assuming you are not + * holding any other references to the shape object), or in order to force + * the recalculation of the JS shape (the next time the `shape` getter is + * accessed) from the WASM source of truth. + */ + public clearShapeCache() { + this._shape = null; + } + + /** + * Checks if this collider is still valid (i.e. that it has + * not been deleted from the collider set yet). + */ + public isValid(): boolean { + return this.colliderSet.raw.contains(this.handle); + } + + /** + * The world-space translation of this collider. + */ + public translation(): Vector { + return VectorOps.fromRaw( + this.colliderSet.raw.coTranslation(this.handle), + ); + } + + /** + * The translation of this collider relative to its parent rigid-body. + * + * Returns `null` if the collider doesn’t have a parent rigid-body. + */ + public translationWrtParent(): Vector | null { + return VectorOps.fromRaw( + this.colliderSet.raw.coTranslationWrtParent(this.handle), + ); + } + + /** + * The world-space orientation of this collider. + */ + public rotation(): Rotation { + return RotationOps.fromRaw( + this.colliderSet.raw.coRotation(this.handle), + ); + } + + /** + * The orientation of this collider relative to its parent rigid-body. + * + * Returns `null` if the collider doesn’t have a parent rigid-body. + */ + public rotationWrtParent(): Rotation | null { + return RotationOps.fromRaw( + this.colliderSet.raw.coRotationWrtParent(this.handle), + ); + } + + /** + * Is this collider a sensor? + */ + public isSensor(): boolean { + return this.colliderSet.raw.coIsSensor(this.handle); + } + + /** + * Sets whether this collider is a sensor. + * @param isSensor - If `true`, the collider will be a sensor. + */ + public setSensor(isSensor: boolean) { + this.colliderSet.raw.coSetSensor(this.handle, isSensor); + } + + /** + * Sets the new shape of the collider. + * @param shape - The collider’s new shape. + */ + public setShape(shape: Shape) { + let rawShape = shape.intoRaw(); + this.colliderSet.raw.coSetShape(this.handle, rawShape); + rawShape.free(); + this._shape = shape; + } + + /** + * Sets whether this collider is enabled or not. + * + * @param enabled - Set to `false` to disable this collider (its parent rigid-body won’t be disabled automatically by this). + */ + public setEnabled(enabled: boolean) { + this.colliderSet.raw.coSetEnabled(this.handle, enabled); + } + + /** + * Is this collider enabled? + */ + public isEnabled(): boolean { + return this.colliderSet.raw.coIsEnabled(this.handle); + } + + /** + * Sets the restitution coefficient of the collider to be created. + * + * @param restitution - The restitution coefficient in `[0, 1]`. A value of 0 (the default) means no bouncing behavior + * while 1 means perfect bouncing (though energy may still be lost due to numerical errors of the + * constraints solver). + */ + public setRestitution(restitution: number) { + this.colliderSet.raw.coSetRestitution(this.handle, restitution); + } + + /** + * Sets the friction coefficient of the collider to be created. + * + * @param friction - The friction coefficient. Must be greater or equal to 0. This is generally smaller than 1. The + * higher the coefficient, the stronger friction forces will be for contacts with the collider + * being built. + */ + public setFriction(friction: number) { + this.colliderSet.raw.coSetFriction(this.handle, friction); + } + + /** + * Gets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + */ + public frictionCombineRule(): CoefficientCombineRule { + return this.colliderSet.raw.coFrictionCombineRule(this.handle); + } + + /** + * Sets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setFrictionCombineRule(rule: CoefficientCombineRule) { + this.colliderSet.raw.coSetFrictionCombineRule(this.handle, rule); + } + + /** + * Gets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + */ + public restitutionCombineRule(): CoefficientCombineRule { + return this.colliderSet.raw.coRestitutionCombineRule(this.handle); + } + + /** + * Sets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setRestitutionCombineRule(rule: CoefficientCombineRule) { + this.colliderSet.raw.coSetRestitutionCombineRule(this.handle, rule); + } + + /** + * Sets the collision groups used by this collider. + * + * Two colliders will interact iff. their collision groups are compatible. + * See the documentation of `InteractionGroups` for details on teh used bit pattern. + * + * @param groups - The collision groups used for the collider being built. + */ + public setCollisionGroups(groups: InteractionGroups) { + this.colliderSet.raw.coSetCollisionGroups(this.handle, groups); + } + + /** + * Sets the solver groups used by this collider. + * + * Forces between two colliders in contact will be computed iff their solver + * groups are compatible. + * See the documentation of `InteractionGroups` for details on the used bit pattern. + * + * @param groups - The solver groups used for the collider being built. + */ + public setSolverGroups(groups: InteractionGroups) { + this.colliderSet.raw.coSetSolverGroups(this.handle, groups); + } + + /** + * Sets the contact skin for this collider. + * + * See the documentation of `ColliderDesc.setContactSkin` for additional details. + */ + public contactSkin(): number { + return this.colliderSet.raw.coContactSkin(this.handle); + } + + /** + * Sets the contact skin for this collider. + * + * See the documentation of `ColliderDesc.setContactSkin` for additional details. + * + * @param thickness - The contact skin thickness. + */ + public setContactSkin(thickness: number) { + return this.colliderSet.raw.coSetContactSkin(this.handle, thickness); + } + + /** + * Get the physics hooks active for this collider. + */ + public activeHooks(): ActiveHooks { + return this.colliderSet.raw.coActiveHooks(this.handle); + } + + /** + * Set the physics hooks active for this collider. + * + * Use this to enable custom filtering rules for contact/intersecstion pairs involving this collider. + * + * @param activeHooks - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveHooks(activeHooks: ActiveHooks) { + this.colliderSet.raw.coSetActiveHooks(this.handle, activeHooks); + } + + /** + * The events active for this collider. + */ + public activeEvents(): ActiveEvents { + return this.colliderSet.raw.coActiveEvents(this.handle); + } + + /** + * Set the events active for this collider. + * + * Use this to enable contact and/or intersection event reporting for this collider. + * + * @param activeEvents - The events active for contact/intersection pairs involving this collider. + */ + public setActiveEvents(activeEvents: ActiveEvents) { + this.colliderSet.raw.coSetActiveEvents(this.handle, activeEvents); + } + + /** + * Gets the collision types active for this collider. + */ + public activeCollisionTypes(): ActiveCollisionTypes { + return this.colliderSet.raw.coActiveCollisionTypes(this.handle); + } + + /** + * Sets the total force magnitude beyond which a contact force event can be emitted. + * + * @param threshold - The new force threshold. + */ + public setContactForceEventThreshold(threshold: number) { + return this.colliderSet.raw.coSetContactForceEventThreshold( + this.handle, + threshold, + ); + } + + /** + * The total force magnitude beyond which a contact force event can be emitted. + */ + public contactForceEventThreshold(): number { + return this.colliderSet.raw.coContactForceEventThreshold(this.handle); + } + + /** + * Set the collision types active for this collider. + * + * @param activeCollisionTypes - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveCollisionTypes(activeCollisionTypes: ActiveCollisionTypes) { + this.colliderSet.raw.coSetActiveCollisionTypes( + this.handle, + activeCollisionTypes, + ); + } + + /** + * Sets the uniform density of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + * + * The mass and angular inertia of this collider will be computed automatically based on its + * shape. + */ + public setDensity(density: number) { + this.colliderSet.raw.coSetDensity(this.handle, density); + } + + /** + * Sets the mass of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + * + * The angular inertia of this collider will be computed automatically based on its shape + * and this mass value. + */ + public setMass(mass: number) { + this.colliderSet.raw.coSetMass(this.handle, mass); + } + + // #if DIM3 + /** + * Sets the mass of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: Vector, + angularInertiaLocalFrame: Rotation, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + let rawPrincipalInertia = VectorOps.intoRaw(principalAngularInertia); + let rawInertiaFrame = RotationOps.intoRaw(angularInertiaLocalFrame); + + this.colliderSet.raw.coSetMassProperties( + this.handle, + mass, + rawCom, + rawPrincipalInertia, + rawInertiaFrame, + ); + + rawCom.free(); + rawPrincipalInertia.free(); + rawInertiaFrame.free(); + } + + // #endif + + // #if DIM2 + /** + * Sets the mass of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + this.colliderSet.raw.coSetMassProperties( + this.handle, + mass, + rawCom, + principalAngularInertia, + ); + rawCom.free(); + } + + // #endif + + /** + * Sets the translation of this collider. + * + * @param tra - The world-space position of the collider. + */ + public setTranslation(tra: Vector) { + // #if DIM2 + this.colliderSet.raw.coSetTranslation(this.handle, tra.x, tra.y); + // #endif + // #if DIM3 + this.colliderSet.raw.coSetTranslation(this.handle, tra.x, tra.y, tra.z); + // #endif + } + + /** + * Sets the translation of this collider relative to its parent rigid-body. + * + * Does nothing if this collider isn't attached to a rigid-body. + * + * @param tra - The new translation of the collider relative to its parent. + */ + public setTranslationWrtParent(tra: Vector) { + // #if DIM2 + this.colliderSet.raw.coSetTranslationWrtParent( + this.handle, + tra.x, + tra.y, + ); + // #endif + // #if DIM3 + this.colliderSet.raw.coSetTranslationWrtParent( + this.handle, + tra.x, + tra.y, + tra.z, + ); + // #endif + } + + // #if DIM3 + /** + * Sets the rotation quaternion of this collider. + * + * This does nothing if a zero quaternion is provided. + * + * @param rotation - The rotation to set. + */ + public setRotation(rot: Rotation) { + this.colliderSet.raw.coSetRotation( + this.handle, + rot.x, + rot.y, + rot.z, + rot.w, + ); + } + + /** + * Sets the rotation quaternion of this collider relative to its parent rigid-body. + * + * This does nothing if a zero quaternion is provided or if this collider isn't + * attached to a rigid-body. + * + * @param rotation - The rotation to set. + */ + public setRotationWrtParent(rot: Rotation) { + this.colliderSet.raw.coSetRotationWrtParent( + this.handle, + rot.x, + rot.y, + rot.z, + rot.w, + ); + } + + // #endif + // #if DIM2 + /** + * Sets the rotation angle of this collider. + * + * @param angle - The rotation angle, in radians. + */ + public setRotation(angle: number) { + this.colliderSet.raw.coSetRotation(this.handle, angle); + } + + /** + * Sets the rotation angle of this collider relative to its parent rigid-body. + * + * Does nothing if this collider isn't attached to a rigid-body. + * + * @param angle - The rotation angle, in radians. + */ + public setRotationWrtParent(angle: number) { + this.colliderSet.raw.coSetRotationWrtParent(this.handle, angle); + } + + // #endif + + /** + * The type of the shape of this collider. + */ + public shapeType(): ShapeType { + return this.colliderSet.raw.coShapeType( + this.handle, + ) as number as ShapeType; + } + + /** + * The half-extents of this collider if it is a cuboid shape. + */ + public halfExtents(): Vector { + return VectorOps.fromRaw( + this.colliderSet.raw.coHalfExtents(this.handle), + ); + } + + /** + * Sets the half-extents of this collider if it is a cuboid shape. + * + * @param newHalfExtents - desired half extents. + */ + public setHalfExtents(newHalfExtents: Vector) { + const rawPoint = VectorOps.intoRaw(newHalfExtents); + this.colliderSet.raw.coSetHalfExtents(this.handle, rawPoint); + } + + /** + * The radius of this collider if it is a ball, cylinder, capsule, or cone shape. + */ + public radius(): number { + return this.colliderSet.raw.coRadius(this.handle); + } + + /** + * Sets the radius of this collider if it is a ball, cylinder, capsule, or cone shape. + * + * @param newRadius - desired radius. + */ + public setRadius(newRadius: number): void { + this.colliderSet.raw.coSetRadius(this.handle, newRadius); + } + + /** + * The radius of the round edges of this collider if it is a round cylinder. + */ + public roundRadius(): number { + return this.colliderSet.raw.coRoundRadius(this.handle); + } + + /** + * Sets the radius of the round edges of this collider if it has round edges. + * + * @param newBorderRadius - desired round edge radius. + */ + public setRoundRadius(newBorderRadius: number) { + this.colliderSet.raw.coSetRoundRadius(this.handle, newBorderRadius); + } + + /** + * The half height of this collider if it is a cylinder, capsule, or cone shape. + */ + public halfHeight(): number { + return this.colliderSet.raw.coHalfHeight(this.handle); + } + + /** + * Sets the half height of this collider if it is a cylinder, capsule, or cone shape. + * + * @param newHalfheight - desired half height. + */ + public setHalfHeight(newHalfheight: number) { + this.colliderSet.raw.coSetHalfHeight(this.handle, newHalfheight); + } + + /** + * If this collider has a Voxels shape, this will mark the voxel at the + * given grid coordinates as filled or empty (depending on the `filled` + * argument). + * + * Each input value is assumed to be an integer. + * + * The operation is O(1), unless the provided coordinates are out of the + * bounds of the currently allocated internal grid in which case the grid + * will be grown automatically. + */ + public setVoxel( + ix: number, + iy: number, + // #if DIM3 + iz: number, + // #endif + filled: boolean, + ) { + this.colliderSet.raw.coSetVoxel( + this.handle, + ix, + iy, + // #if DIM3 + iz, + // #endif + filled, + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this and `voxels2` are voxel colliders, and a voxel from `this` was + * modified with `setVoxel`, this will ensure that a + * moving object transitioning across the boundaries of these colliders + * won’t suffer from the "internal edges" artifact. + * + * The indices `ix, iy, iz` indicate the integer coordinates of the voxel in + * the local coordinate frame of `this`. + * + * If the voxels in `voxels2` live in a different coordinate space from `this`, + * then the `shift_*` argument indicate the distance, in voxel units, between + * the origin of `this` to the origin of `voxels2`. + * + * This method is intended to be called between `this` and all the other + * voxels colliders with a domain intersecting `this` or sharing a domain + * boundary. This is an incremental maintenance of the effect of + * `combineVoxelStates`. + */ + public propagateVoxelChange( + voxels2: Collider, + ix: number, + iy: number, + // #if DIM3 + iz: number, + // #endif + shift_x: number, + shift_y: number, + // #if DIM3 + shift_z: number, + // #endif + ) { + this.colliderSet.raw.coPropagateVoxelChange( + this.handle, + voxels2.handle, + ix, + iy, + // #if DIM3 + iz, + // #endif + shift_x, + shift_y, + // #if DIM3 + shift_z, + // #endif + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this and `voxels2` are voxel colliders, this will ensure that a + * moving object transitioning across the boundaries of these colliders + * won’t suffer from the "internal edges" artifact. + * + * If the voxels in `voxels2` live in a different coordinate space from `this`, + * then the `shift_*` argument indicate the distance, in voxel units, between + * the origin of `this` to the origin of `voxels2`. + * + * This method is intended to be called once between all pairs of voxels + * colliders with intersecting domains or shared boundaries. + * + * If either voxels collider is then modified with `setVoxel`, the + * `propagateVoxelChange` method must be called to maintain the coupling + * between the voxels shapes after the modification. + */ + public combineVoxelStates( + voxels2: Collider, + shift_x: number, + shift_y: number, + // #if DIM3 + shift_z: number, + // #endif + ) { + this.colliderSet.raw.coCombineVoxelStates( + this.handle, + voxels2.handle, + shift_x, + shift_y, + // #if DIM3 + shift_z, + // #endif + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this collider has a triangle mesh, polyline, convex polygon, or convex polyhedron shape, + * this returns the vertex buffer of said shape. + */ + public vertices(): Float32Array { + return this.colliderSet.raw.coVertices(this.handle); + } + + /** + * If this collider has a triangle mesh, polyline, or convex polyhedron shape, + * this returns the index buffer of said shape. + */ + public indices(): Uint32Array | undefined { + return this.colliderSet.raw.coIndices(this.handle); + } + + /** + * If this collider has a heightfield shape, this returns the heights buffer of + * the heightfield. + * In 3D, the returned height matrix is provided in column-major order. + */ + public heightfieldHeights(): Float32Array { + return this.colliderSet.raw.coHeightfieldHeights(this.handle); + } + + /** + * If this collider has a heightfield shape, this returns the scale + * applied to it. + */ + public heightfieldScale(): Vector { + let scale = this.colliderSet.raw.coHeightfieldScale(this.handle); + return VectorOps.fromRaw(scale); + } + + // #if DIM3 + /** + * If this collider has a heightfield shape, this returns the number of + * rows of its height matrix. + */ + public heightfieldNRows(): number { + return this.colliderSet.raw.coHeightfieldNRows(this.handle); + } + + /** + * If this collider has a heightfield shape, this returns the number of + * columns of its height matrix. + */ + public heightfieldNCols(): number { + return this.colliderSet.raw.coHeightfieldNCols(this.handle); + } + + // #endif + + /** + * The rigid-body this collider is attached to. + */ + public parent(): RigidBody | null { + return this._parent; + } + + /** + * The friction coefficient of this collider. + */ + public friction(): number { + return this.colliderSet.raw.coFriction(this.handle); + } + + /** + * The restitution coefficient of this collider. + */ + public restitution(): number { + return this.colliderSet.raw.coRestitution(this.handle); + } + + /** + * The density of this collider. + */ + public density(): number { + return this.colliderSet.raw.coDensity(this.handle); + } + + /** + * The mass of this collider. + */ + public mass(): number { + return this.colliderSet.raw.coMass(this.handle); + } + + /** + * The volume of this collider. + */ + public volume(): number { + return this.colliderSet.raw.coVolume(this.handle); + } + + /** + * The collision groups of this collider. + */ + public collisionGroups(): InteractionGroups { + return this.colliderSet.raw.coCollisionGroups(this.handle); + } + + /** + * The solver groups of this collider. + */ + public solverGroups(): InteractionGroups { + return this.colliderSet.raw.coSolverGroups(this.handle); + } + + /** + * Tests if this collider contains a point. + * + * @param point - The point to test. + */ + public containsPoint(point: Vector): boolean { + let rawPoint = VectorOps.intoRaw(point); + let result = this.colliderSet.raw.coContainsPoint( + this.handle, + rawPoint, + ); + + rawPoint.free(); + + return result; + } + + /** + * Find the projection of a point on this collider. + * + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + */ + public projectPoint(point: Vector, solid: boolean): PointProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointProjection.fromRaw( + this.colliderSet.raw.coProjectPoint(this.handle, rawPoint, solid), + ); + + rawPoint.free(); + + return result; + } + + /** + * Tests if this collider intersects the given ray. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + */ + public intersectsRay(ray: Ray, maxToi: number): boolean { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = this.colliderSet.raw.coIntersectsRay( + this.handle, + rawOrig, + rawDir, + maxToi, + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /* + * Computes the smallest time between this and the given shape under translational movement are separated by a distance smaller or equal to distance. + * + * @param collider1Vel - The constant velocity of the current shape to cast (i.e. the cast direction). + * @param shape2 - The shape to cast against. + * @param shape2Pos - The position of the second shape. + * @param shape2Rot - The rotation of the second shape. + * @param shape2Vel - The constant velocity of the second shape. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `collider1Vel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + */ + public castShape( + collider1Vel: Vector, + shape2: Shape, + shape2Pos: Vector, + shape2Rot: Rotation, + shape2Vel: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ShapeCastHit | null { + let rawCollider1Vel = VectorOps.intoRaw(collider1Vel); + let rawShape2Pos = VectorOps.intoRaw(shape2Pos); + let rawShape2Rot = RotationOps.intoRaw(shape2Rot); + let rawShape2Vel = VectorOps.intoRaw(shape2Vel); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeCastHit.fromRaw( + this.colliderSet, + this.colliderSet.raw.coCastShape( + this.handle, + rawCollider1Vel, + rawShape2, + rawShape2Pos, + rawShape2Rot, + rawShape2Vel, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawCollider1Vel.free(); + rawShape2Pos.free(); + rawShape2Rot.free(); + rawShape2Vel.free(); + rawShape2.free(); + + return result; + } + + /* + * Computes the smallest time between this and the given collider under translational movement are separated by a distance smaller or equal to distance. + * + * @param collider1Vel - The constant velocity of the current collider to cast (i.e. the cast direction). + * @param collider2 - The collider to cast against. + * @param collider2Vel - The constant velocity of the second collider. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + */ + public castCollider( + collider1Vel: Vector, + collider2: Collider, + collider2Vel: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ColliderShapeCastHit | null { + let rawCollider1Vel = VectorOps.intoRaw(collider1Vel); + let rawCollider2Vel = VectorOps.intoRaw(collider2Vel); + + let result = ColliderShapeCastHit.fromRaw( + this.colliderSet, + this.colliderSet.raw.coCastCollider( + this.handle, + rawCollider1Vel, + collider2.handle, + rawCollider2Vel, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawCollider1Vel.free(); + rawCollider2Vel.free(); + + return result; + } + + public intersectsShape( + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + ): boolean { + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + let rawShape2 = shape2.intoRaw(); + + let result = this.colliderSet.raw.coIntersectsShape( + this.handle, + rawShape2, + rawPos2, + rawRot2, + ); + + rawPos2.free(); + rawRot2.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between the shape owned by this collider and the given shape. + * + * @param shape2 - The second shape. + * @param shape2Pos - The initial position of the second shape. + * @param shape2Rot - The rotation of the second shape. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactShape( + shape2: Shape, + shape2Pos: Vector, + shape2Rot: Rotation, + prediction: number, + ): ShapeContact | null { + let rawPos2 = VectorOps.intoRaw(shape2Pos); + let rawRot2 = RotationOps.intoRaw(shape2Rot); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeContact.fromRaw( + this.colliderSet.raw.coContactShape( + this.handle, + rawShape2, + rawPos2, + rawRot2, + prediction, + ), + ); + + rawPos2.free(); + rawRot2.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between the collider and the given collider. + * + * @param collider2 - The second collider. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactCollider( + collider2: Collider, + prediction: number, + ): ShapeContact | null { + let result = ShapeContact.fromRaw( + this.colliderSet.raw.coContactCollider( + this.handle, + collider2.handle, + prediction, + ), + ); + + return result; + } + + /** + * Find the closest intersection between a ray and this collider. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @returns The time-of-impact between this collider and the ray, or `-1` if there is no intersection. + */ + public castRay(ray: Ray, maxToi: number, solid: boolean): number { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = this.colliderSet.raw.coCastRay( + this.handle, + rawOrig, + rawDir, + maxToi, + solid, + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Find the closest intersection between a ray and this collider. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + */ + public castRayAndGetNormal( + ray: Ray, + maxToi: number, + solid: boolean, + ): RayIntersection | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayIntersection.fromRaw( + this.colliderSet.raw.coCastRayAndGetNormal( + this.handle, + rawOrig, + rawDir, + maxToi, + solid, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } +} + +export enum MassPropsMode { + Density, + Mass, + MassProps, +} + +export class ColliderDesc { + enabled: boolean; + shape: Shape; + massPropsMode: MassPropsMode; + mass: number; + centerOfMass: Vector; + // #if DIM2 + principalAngularInertia: number; + rotationsEnabled: boolean; + // #endif + // #if DIM3 + principalAngularInertia: Vector; + angularInertiaLocalFrame: Rotation; + // #endif + density: number; + friction: number; + restitution: number; + rotation: Rotation; + translation: Vector; + isSensor: boolean; + collisionGroups: InteractionGroups; + solverGroups: InteractionGroups; + frictionCombineRule: CoefficientCombineRule; + restitutionCombineRule: CoefficientCombineRule; + activeEvents: ActiveEvents; + activeHooks: ActiveHooks; + activeCollisionTypes: ActiveCollisionTypes; + contactForceEventThreshold: number; + contactSkin: number; + + /** + * Initializes a collider descriptor from the collision shape. + * + * @param shape - The shape of the collider being built. + */ + constructor(shape: Shape) { + this.enabled = true; + this.shape = shape; + this.massPropsMode = MassPropsMode.Density; + this.density = 1.0; + this.friction = 0.5; + this.restitution = 0.0; + this.rotation = RotationOps.identity(); + this.translation = VectorOps.zeros(); + this.isSensor = false; + this.collisionGroups = 0xffff_ffff; + this.solverGroups = 0xffff_ffff; + this.frictionCombineRule = CoefficientCombineRule.Average; + this.restitutionCombineRule = CoefficientCombineRule.Average; + this.activeCollisionTypes = ActiveCollisionTypes.DEFAULT; + this.activeEvents = ActiveEvents.NONE; + this.activeHooks = ActiveHooks.NONE; + this.mass = 0.0; + this.centerOfMass = VectorOps.zeros(); + this.contactForceEventThreshold = 0.0; + this.contactSkin = 0.0; + + // #if DIM2 + this.principalAngularInertia = 0.0; + this.rotationsEnabled = true; + // #endif + // #if DIM3 + this.principalAngularInertia = VectorOps.zeros(); + this.angularInertiaLocalFrame = RotationOps.identity(); + // #endif + } + + /** + * Create a new collider descriptor with a ball shape. + * + * @param radius - The radius of the ball. + */ + public static ball(radius: number): ColliderDesc { + const shape = new Ball(radius); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a capsule shape. + * + * @param halfHeight - The half-height of the capsule, along the `y` axis. + * @param radius - The radius of the capsule basis. + */ + public static capsule(halfHeight: number, radius: number): ColliderDesc { + const shape = new Capsule(halfHeight, radius); + return new ColliderDesc(shape); + } + + /** + * Creates a new segment shape. + * + * @param a - The first point of the segment. + * @param b - The second point of the segment. + */ + public static segment(a: Vector, b: Vector): ColliderDesc { + const shape = new Segment(a, b); + return new ColliderDesc(shape); + } + + /** + * Creates a new triangle shape. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + */ + public static triangle(a: Vector, b: Vector, c: Vector): ColliderDesc { + const shape = new Triangle(a, b, c); + return new ColliderDesc(shape); + } + + /** + * Creates a new triangle shape with round corners. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + * @param borderRadius - The radius of the borders of this triangle. In 3D, + * this is also equal to half the thickness of the triangle. + */ + public static roundTriangle( + a: Vector, + b: Vector, + c: Vector, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundTriangle(a, b, c, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a polyline shape. + * + * @param vertices - The coordinates of the polyline's vertices. + * @param indices - The indices of the polyline's segments. If this is `undefined` or `null`, + * the vertices are assumed to describe a line strip. + */ + public static polyline( + vertices: Float32Array, + indices?: Uint32Array | null, + ): ColliderDesc { + const shape = new Polyline(vertices, indices); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a shape made of voxels. + * + * @param data - Defines the set of voxels. If this is a `Int32Array` then + * each voxel is defined from its (signed) grid coordinates, + * with 3 (resp 2) contiguous integers per voxel in 3D (resp 2D). + * If this is a `Float32Array`, each voxel will be such that + * they contain at least one point from this array (where each + * point is defined from 3 (resp 2) contiguous numbers per point + * in 3D (resp 2D). + * @param voxelSize - The size of each voxel. + */ + public static voxels( + voxels: Float32Array | Int32Array, + voxelSize: Vector, + ): ColliderDesc { + const shape = new Voxels(voxels, voxelSize); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a triangle mesh shape. + * + * @param vertices - The coordinates of the triangle mesh's vertices. + * @param indices - The indices of the triangle mesh's triangles. + */ + public static trimesh( + vertices: Float32Array, + indices: Uint32Array, + flags?: TriMeshFlags, + ): ColliderDesc { + const shape = new TriMesh(vertices, indices, flags); + return new ColliderDesc(shape); + } + + // #if DIM2 + /** + * Creates a new collider descriptor with a rectangular shape. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + */ + public static cuboid(hx: number, hy: number): ColliderDesc { + const shape = new Cuboid(hx, hy); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a rectangular shape with round borders. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + * @param borderRadius - The radius of the cuboid's borders. + */ + public static roundCuboid( + hx: number, + hy: number, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundCuboid(hx, hy, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider description with a halfspace (infinite plane) shape. + * + * @param normal - The outward normal of the plane. + */ + public static halfspace(normal: Vector): ColliderDesc { + const shape = new HalfSpace(normal); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a heightfield shape. + * + * @param heights - The heights of the heightfield, along its local `y` axis. + * @param scale - The scale factor applied to the heightfield. + */ + public static heightfield( + heights: Float32Array, + scale: Vector, + ): ColliderDesc { + const shape = new Heightfield(heights, scale); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polygon as the shape for this new collider descriptor. + * + * @param points - The point that will be used to compute the convex-hull. + */ + public static convexHull(points: Float32Array): ColliderDesc | null { + const shape = new ConvexPolygon(points, false); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + */ + public static convexPolyline(vertices: Float32Array): ColliderDesc | null { + const shape = new ConvexPolygon(vertices, true); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polygon as the shape for this new collider descriptor. A + * border is added to that convex polygon to give it round corners. + * + * @param points - The point that will be used to compute the convex-hull. + * @param borderRadius - The radius of the round border added to the convex polygon. + */ + public static roundConvexHull( + points: Float32Array, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolygon(points, borderRadius, false); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a round convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + * @param borderRadius - The radius of the round border added to the convex polyline. + */ + public static roundConvexPolyline( + vertices: Float32Array, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolygon(vertices, borderRadius, true); + return new ColliderDesc(shape); + } + + // #endif + + // #if DIM3 + /** + * Creates a new collider descriptor with a cuboid shape. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + * @param hz - The half-width of the rectangle along its local `z` axis. + */ + public static cuboid(hx: number, hy: number, hz: number): ColliderDesc { + const shape = new Cuboid(hx, hy, hz); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a rectangular shape with round borders. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + * @param hz - The half-width of the rectangle along its local `z` axis. + * @param borderRadius - The radius of the cuboid's borders. + */ + public static roundCuboid( + hx: number, + hy: number, + hz: number, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundCuboid(hx, hy, hz, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a heightfield shape. + * + * @param nrows − The number of rows in the heights matrix. + * @param ncols - The number of columns in the heights matrix. + * @param heights - The heights of the heightfield along its local `y` axis, + * provided as a matrix stored in column-major order. + * @param scale - The scale factor applied to the heightfield. + */ + public static heightfield( + nrows: number, + ncols: number, + heights: Float32Array, + scale: Vector, + flags?: HeightFieldFlags, + ): ColliderDesc { + const shape = new Heightfield(nrows, ncols, heights, scale, flags); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a cylinder shape. + * + * @param halfHeight - The half-height of the cylinder, along the `y` axis. + * @param radius - The radius of the cylinder basis. + */ + public static cylinder(halfHeight: number, radius: number): ColliderDesc { + const shape = new Cylinder(halfHeight, radius); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a cylinder shape with rounded corners. + * + * @param halfHeight - The half-height of the cylinder, along the `y` axis. + * @param radius - The radius of the cylinder basis. + * @param borderRadius - The radius of the cylinder's rounded edges and vertices. + */ + public static roundCylinder( + halfHeight: number, + radius: number, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundCylinder(halfHeight, radius, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a cone shape. + * + * @param halfHeight - The half-height of the cone, along the `y` axis. + * @param radius - The radius of the cone basis. + */ + public static cone(halfHeight: number, radius: number): ColliderDesc { + const shape = new Cone(halfHeight, radius); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a cone shape with rounded corners. + * + * @param halfHeight - The half-height of the cone, along the `y` axis. + * @param radius - The radius of the cone basis. + * @param borderRadius - The radius of the cone's rounded edges and vertices. + */ + public static roundCone( + halfHeight: number, + radius: number, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundCone(halfHeight, radius, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polyhedron as the shape for this new collider descriptor. + * + * @param points - The point that will be used to compute the convex-hull. + */ + public static convexHull(points: Float32Array): ColliderDesc | null { + const shape = new ConvexPolyhedron(points, null); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + */ + public static convexMesh( + vertices: Float32Array, + indices?: Uint32Array | null, + ): ColliderDesc | null { + const shape = new ConvexPolyhedron(vertices, indices); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polyhedron as the shape for this new collider descriptor. A + * border is added to that convex polyhedron to give it round corners. + * + * @param points - The point that will be used to compute the convex-hull. + * @param borderRadius - The radius of the round border added to the convex polyhedron. + */ + public static roundConvexHull( + points: Float32Array, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolyhedron(points, null, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a round convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + * @param borderRadius - The radius of the round border added to the convex polyline. + */ + public static roundConvexMesh( + vertices: Float32Array, + indices: Uint32Array | null, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolyhedron( + vertices, + indices, + borderRadius, + ); + return new ColliderDesc(shape); + } + + // #endif + + // #if DIM2 + /** + * Sets the position of the collider to be created relative to the rigid-body it is attached to. + */ + public setTranslation(x: number, y: number): ColliderDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y}; + return this; + } + + // #endif + + // #if DIM3 + /** + * Sets the position of the collider to be created relative to the rigid-body it is attached to. + */ + public setTranslation(x: number, y: number, z: number): ColliderDesc { + if ( + typeof x != "number" || + typeof y != "number" || + typeof z != "number" + ) + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y, z: z}; + return this; + } + + // #endif + + /** + * Sets the rotation of the collider to be created relative to the rigid-body it is attached to. + * + * @param rot - The rotation of the collider to be created relative to the rigid-body it is attached to. + */ + public setRotation(rot: Rotation): ColliderDesc { + // #if DIM2 + this.rotation = rot; + // #endif + // #if DIM3 + RotationOps.copy(this.rotation, rot); + // #endif + return this; + } + + /** + * Sets whether or not the collider being created is a sensor. + * + * A sensor collider does not take part of the physics simulation, but generates + * proximity events. + * + * @param sensor - Set to `true` of the collider built is to be a sensor. + */ + public setSensor(sensor: boolean): ColliderDesc { + this.isSensor = sensor; + return this; + } + + /** + * Sets whether the created collider will be enabled or disabled. + * @param enabled − If set to `false` the collider will be disabled at creation. + */ + public setEnabled(enabled: boolean): ColliderDesc { + this.enabled = enabled; + return this; + } + + /** + * Sets the contact skin of the collider. + * + * The contact skin acts as if the collider was enlarged with a skin of width `skin_thickness` + * around it, keeping objects further apart when colliding. + * + * A non-zero contact skin can increase performance, and in some cases, stability. However + * it creates a small gap between colliding object (equal to the sum of their skin). If the + * skin is sufficiently small, this might not be visually significant or can be hidden by the + * rendering assets. + */ + public setContactSkin(thickness: number): ColliderDesc { + this.contactSkin = thickness; + return this; + } + + /** + * Sets the density of the collider being built. + * + * The mass and angular inertia tensor will be computed automatically based on this density and the collider’s shape. + * + * @param density - The density to set, must be greater or equal to 0. A density of 0 means that this collider + * will not affect the mass or angular inertia of the rigid-body it is attached to. + */ + public setDensity(density: number): ColliderDesc { + this.massPropsMode = MassPropsMode.Density; + this.density = density; + return this; + } + + /** + * Sets the mass of the collider being built. + * + * The angular inertia tensor will be computed automatically based on this mass and the collider’s shape. + * + * @param mass - The mass to set, must be greater or equal to 0. + */ + public setMass(mass: number): ColliderDesc { + this.massPropsMode = MassPropsMode.Mass; + this.mass = mass; + return this; + } + + // #if DIM2 + /** + * Sets the mass properties of the collider being built. + * + * This replaces the mass-properties automatically computed from the collider's density and shape. + * These mass-properties will be added to the mass-properties of the rigid-body this collider will be attached to. + * + * @param mass − The mass of the collider to create. + * @param centerOfMass − The center-of-mass of the collider to create. + * @param principalAngularInertia − The principal angular inertia of the collider to create. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ): ColliderDesc { + this.massPropsMode = MassPropsMode.MassProps; + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + this.principalAngularInertia = principalAngularInertia; + return this; + } + + // #endif + + // #if DIM3 + /** + * Sets the mass properties of the collider being built. + * + * This replaces the mass-properties automatically computed from the collider's density and shape. + * These mass-properties will be added to the mass-properties of the rigid-body this collider will be attached to. + * + * @param mass − The mass of the collider to create. + * @param centerOfMass − The center-of-mass of the collider to create. + * @param principalAngularInertia − The initial principal angular inertia of the collider to create. + * These are the eigenvalues of the angular inertia matrix. + * @param angularInertiaLocalFrame − The initial local angular inertia frame of the collider to create. + * These are the eigenvectors of the angular inertia matrix. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: Vector, + angularInertiaLocalFrame: Rotation, + ): ColliderDesc { + this.massPropsMode = MassPropsMode.MassProps; + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + VectorOps.copy(this.principalAngularInertia, principalAngularInertia); + RotationOps.copy( + this.angularInertiaLocalFrame, + angularInertiaLocalFrame, + ); + return this; + } + + // #endif + + /** + * Sets the restitution coefficient of the collider to be created. + * + * @param restitution - The restitution coefficient in `[0, 1]`. A value of 0 (the default) means no bouncing behavior + * while 1 means perfect bouncing (though energy may still be lost due to numerical errors of the + * constraints solver). + */ + public setRestitution(restitution: number): ColliderDesc { + this.restitution = restitution; + return this; + } + + /** + * Sets the friction coefficient of the collider to be created. + * + * @param friction - The friction coefficient. Must be greater or equal to 0. This is generally smaller than 1. The + * higher the coefficient, the stronger friction forces will be for contacts with the collider + * being built. + */ + public setFriction(friction: number): ColliderDesc { + this.friction = friction; + return this; + } + + /** + * Sets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setFrictionCombineRule(rule: CoefficientCombineRule): ColliderDesc { + this.frictionCombineRule = rule; + return this; + } + + /** + * Sets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setRestitutionCombineRule( + rule: CoefficientCombineRule, + ): ColliderDesc { + this.restitutionCombineRule = rule; + return this; + } + + /** + * Sets the collision groups used by this collider. + * + * Two colliders will interact iff. their collision groups are compatible. + * See the documentation of `InteractionGroups` for details on teh used bit pattern. + * + * @param groups - The collision groups used for the collider being built. + */ + public setCollisionGroups(groups: InteractionGroups): ColliderDesc { + this.collisionGroups = groups; + return this; + } + + /** + * Sets the solver groups used by this collider. + * + * Forces between two colliders in contact will be computed iff their solver + * groups are compatible. + * See the documentation of `InteractionGroups` for details on the used bit pattern. + * + * @param groups - The solver groups used for the collider being built. + */ + public setSolverGroups(groups: InteractionGroups): ColliderDesc { + this.solverGroups = groups; + return this; + } + + /** + * Set the physics hooks active for this collider. + * + * Use this to enable custom filtering rules for contact/intersecstion pairs involving this collider. + * + * @param activeHooks - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveHooks(activeHooks: ActiveHooks): ColliderDesc { + this.activeHooks = activeHooks; + return this; + } + + /** + * Set the events active for this collider. + * + * Use this to enable contact and/or intersection event reporting for this collider. + * + * @param activeEvents - The events active for contact/intersection pairs involving this collider. + */ + public setActiveEvents(activeEvents: ActiveEvents): ColliderDesc { + this.activeEvents = activeEvents; + return this; + } + + /** + * Set the collision types active for this collider. + * + * @param activeCollisionTypes - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveCollisionTypes( + activeCollisionTypes: ActiveCollisionTypes, + ): ColliderDesc { + this.activeCollisionTypes = activeCollisionTypes; + return this; + } + + /** + * Sets the total force magnitude beyond which a contact force event can be emitted. + * + * @param threshold - The force threshold to set. + */ + public setContactForceEventThreshold(threshold: number): ColliderDesc { + this.contactForceEventThreshold = threshold; + return this; + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/collider_set.ts b/thirdparty/rapier.js/src.ts/geometry/collider_set.ts new file mode 100644 index 00000000..832b837a --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/collider_set.ts @@ -0,0 +1,213 @@ +import {RawColliderSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RotationOps, VectorOps} from "../math"; +import {Collider, ColliderDesc, ColliderHandle} from "./collider"; +import {ImpulseJointHandle, IslandManager, RigidBodyHandle} from "../dynamics"; +import {RigidBodySet} from "../dynamics"; + +/** + * A set of rigid bodies that can be handled by a physics pipeline. + * + * To avoid leaking WASM resources, this MUST be freed manually with `colliderSet.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class ColliderSet { + raw: RawColliderSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this collider set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawColliderSet) { + this.raw = raw || new RawColliderSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachColliderHandle((handle: ColliderHandle) => { + this.map.set(handle, new Collider(this, handle, null)); + }); + } + } + + /** @internal */ + public castClosure( + f?: (collider: Collider) => Res, + ): (handle: ColliderHandle) => Res | undefined { + return (handle) => { + if (!!f) { + return f(this.get(handle)); + } else { + return undefined; + } + }; + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + this.map.forEach((collider) => + collider.finalizeDeserialization(bodies), + ); + } + + /** + * Creates a new collider and return its integer handle. + * + * @param bodies - The set of bodies where the collider's parent can be found. + * @param desc - The collider's description. + * @param parentHandle - The integer handle of the rigid-body this collider is attached to. + */ + public createCollider( + bodies: RigidBodySet, + desc: ColliderDesc, + parentHandle: RigidBodyHandle, + ): Collider { + let hasParent = parentHandle != undefined && parentHandle != null; + + if (hasParent && isNaN(parentHandle)) + throw Error( + "Cannot create a collider with a parent rigid-body handle that is not a number.", + ); + + let rawShape = desc.shape.intoRaw(); + let rawTra = VectorOps.intoRaw(desc.translation); + let rawRot = RotationOps.intoRaw(desc.rotation); + let rawCom = VectorOps.intoRaw(desc.centerOfMass); + + // #if DIM3 + let rawPrincipalInertia = VectorOps.intoRaw( + desc.principalAngularInertia, + ); + let rawInertiaFrame = RotationOps.intoRaw( + desc.angularInertiaLocalFrame, + ); + // #endif + + let handle = this.raw.createCollider( + desc.enabled, + rawShape, + rawTra, + rawRot, + desc.massPropsMode, + desc.mass, + rawCom, + // #if DIM2 + desc.principalAngularInertia, + // #endif + // #if DIM3 + rawPrincipalInertia, + rawInertiaFrame, + // #endif + desc.density, + desc.friction, + desc.restitution, + desc.frictionCombineRule, + desc.restitutionCombineRule, + desc.isSensor, + desc.collisionGroups, + desc.solverGroups, + desc.activeCollisionTypes, + desc.activeHooks, + desc.activeEvents, + desc.contactForceEventThreshold, + desc.contactSkin, + hasParent, + hasParent ? parentHandle : 0, + bodies.raw, + ); + + rawShape.free(); + rawTra.free(); + rawRot.free(); + rawCom.free(); + + // #if DIM3 + rawPrincipalInertia.free(); + rawInertiaFrame.free(); + // #endif + + let parent = hasParent ? bodies.get(parentHandle) : null; + let collider = new Collider(this, handle, parent, desc.shape); + this.map.set(handle, collider); + return collider; + } + + /** + * Remove a collider from this set. + * + * @param handle - The integer handle of the collider to remove. + * @param bodies - The set of rigid-body containing the rigid-body the collider is attached to. + * @param wakeUp - If `true`, the rigid-body the removed collider is attached to will be woken-up automatically. + */ + public remove( + handle: ColliderHandle, + islands: IslandManager, + bodies: RigidBodySet, + wakeUp: boolean, + ) { + this.raw.remove(handle, islands.raw, bodies.raw, wakeUp); + this.unmap(handle); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: ImpulseJointHandle) { + this.map.delete(handle); + } + + /** + * Gets the rigid-body with the given handle. + * + * @param handle - The handle of the rigid-body to retrieve. + */ + public get(handle: ColliderHandle): Collider | null { + return this.map.get(handle); + } + + /** + * The number of colliders on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a collider with the given handle? + * + * @param handle - The collider handle to check. + */ + public contains(handle: ColliderHandle): boolean { + return this.get(handle) != null; + } + + /** + * Applies the given closure to each collider contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (collider: Collider) => void) { + this.map.forEach(f); + } + + /** + * Gets all colliders in the list. + * + * @returns collider list. + */ + public getAll(): Collider[] { + return this.map.getAll(); + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/contact.ts b/thirdparty/rapier.js/src.ts/geometry/contact.ts new file mode 100644 index 00000000..802f57f4 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/contact.ts @@ -0,0 +1,62 @@ +import {Vector, VectorOps} from "../math"; +import {RawShapeContact} from "../raw"; + +/** + * The contact info between two shapes. + */ +export class ShapeContact { + /** + * Distance between the two contact points. + * If this is negative, this contact represents a penetration. + */ + distance: number; + + /** + * Position of the contact on the first shape. + */ + point1: Vector; + + /** + * Position of the contact on the second shape. + */ + point2: Vector; + + /** + * Contact normal, pointing towards the exterior of the first shape. + */ + normal1: Vector; + + /** + * Contact normal, pointing towards the exterior of the second shape. + * If these contact data are expressed in world-space, this normal is equal to -normal1. + */ + normal2: Vector; + + constructor( + dist: number, + point1: Vector, + point2: Vector, + normal1: Vector, + normal2: Vector, + ) { + this.distance = dist; + this.point1 = point1; + this.point2 = point2; + this.normal1 = normal1; + this.normal2 = normal2; + } + + public static fromRaw(raw: RawShapeContact): ShapeContact { + if (!raw) return null; + + const result = new ShapeContact( + raw.distance(), + VectorOps.fromRaw(raw.point1()), + VectorOps.fromRaw(raw.point2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/feature.ts b/thirdparty/rapier.js/src.ts/geometry/feature.ts new file mode 100644 index 00000000..bec9560d --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/feature.ts @@ -0,0 +1,16 @@ +// #if DIM2 +export enum FeatureType { + Vertex, + Face, + Unknown, +} +// #endif + +// #if DIM3 +export enum FeatureType { + Vertex, + Edge, + Face, + Unknown, +} +// #endif diff --git a/thirdparty/rapier.js/src.ts/geometry/index.ts b/thirdparty/rapier.js/src.ts/geometry/index.ts new file mode 100644 index 00000000..1bd19e50 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/index.ts @@ -0,0 +1,11 @@ +export * from "./broad_phase"; +export * from "./narrow_phase"; +export * from "./shape"; +export * from "./collider"; +export * from "./collider_set"; +export * from "./feature"; +export * from "./ray"; +export * from "./point"; +export * from "./toi"; +export * from "./interaction_groups"; +export * from "./contact"; diff --git a/thirdparty/rapier.js/src.ts/geometry/interaction_groups.ts b/thirdparty/rapier.js/src.ts/geometry/interaction_groups.ts new file mode 100644 index 00000000..4d73fd31 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/interaction_groups.ts @@ -0,0 +1,18 @@ +/** + * Pairwise filtering using bit masks. + * + * This filtering method is based on two 16-bit values: + * - The interaction groups (the 16 left-most bits of `self.0`). + * - The interaction mask (the 16 right-most bits of `self.0`). + * + * An interaction is allowed between two filters `a` and `b` two conditions + * are met simultaneously: + * - The interaction groups of `a` has at least one bit set to `1` in common with the interaction mask of `b`. + * - The interaction groups of `b` has at least one bit set to `1` in common with the interaction mask of `a`. + * In other words, interactions are allowed between two filter iff. the following condition is met: + * + * ``` + * ((a >> 16) & b) != 0 && ((b >> 16) & a) != 0 + * ``` + */ +export type InteractionGroups = number; diff --git a/thirdparty/rapier.js/src.ts/geometry/narrow_phase.ts b/thirdparty/rapier.js/src.ts/geometry/narrow_phase.ts new file mode 100644 index 00000000..af04a227 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/narrow_phase.ts @@ -0,0 +1,203 @@ +import {RawNarrowPhase, RawContactManifold} from "../raw"; +import {ColliderHandle} from "./collider"; +import {Vector, VectorOps} from "../math"; + +/** + * The narrow-phase used for precise collision-detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `narrowPhase.free()` + * once you are done using it. + */ +export class NarrowPhase { + raw: RawNarrowPhase; + tempManifold: TempContactManifold; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawNarrowPhase) { + this.raw = raw || new RawNarrowPhase(); + this.tempManifold = new TempContactManifold(null); + } + + /** + * Enumerates all the colliders potentially in contact with the given collider. + * + * @param collider1 - The second collider involved in the contact. + * @param f - Closure that will be called on each collider that is in contact with `collider1`. + */ + public contactPairsWith( + collider1: ColliderHandle, + f: (collider2: ColliderHandle) => void, + ) { + this.raw.contact_pairs_with(collider1, f); + } + + /** + * Enumerates all the colliders intersecting the given colliders, assuming one of them + * is a sensor. + */ + public intersectionPairsWith( + collider1: ColliderHandle, + f: (collider2: ColliderHandle) => void, + ) { + this.raw.intersection_pairs_with(collider1, f); + } + + /** + * Iterates through all the contact manifolds between the given pair of colliders. + * + * @param collider1 - The first collider involved in the contact. + * @param collider2 - The second collider involved in the contact. + * @param f - Closure that will be called on each contact manifold between the two colliders. If the second argument + * passed to this closure is `true`, then the contact manifold data is flipped, i.e., methods like `localNormal1` + * actually apply to the `collider2` and fields like `localNormal2` apply to the `collider1`. + */ + public contactPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + f: (manifold: TempContactManifold, flipped: boolean) => void, + ) { + const rawPair = this.raw.contact_pair(collider1, collider2); + + if (!!rawPair) { + const flipped = rawPair.collider1() != collider1; + + let i; + for (i = 0; i < rawPair.numContactManifolds(); ++i) { + this.tempManifold.raw = rawPair.contactManifold(i); + if (!!this.tempManifold.raw) { + f(this.tempManifold, flipped); + } + + // SAFETY: The RawContactManifold stores a raw pointer that will be invalidated + // at the next timestep. So we must be sure to free the pair here + // to avoid unsoundness in the Rust code. + this.tempManifold.free(); + } + rawPair.free(); + } + } + + /** + * Returns `true` if `collider1` and `collider2` intersect and at least one of them is a sensor. + * @param collider1 − The first collider involved in the intersection. + * @param collider2 − The second collider involved in the intersection. + */ + public intersectionPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + ): boolean { + return this.raw.intersection_pair(collider1, collider2); + } +} + +export class TempContactManifold { + raw: RawContactManifold; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw: RawContactManifold) { + this.raw = raw; + } + + public normal(): Vector { + return VectorOps.fromRaw(this.raw.normal()); + } + + public localNormal1(): Vector { + return VectorOps.fromRaw(this.raw.local_n1()); + } + + public localNormal2(): Vector { + return VectorOps.fromRaw(this.raw.local_n2()); + } + + public subshape1(): number { + return this.raw.subshape1(); + } + + public subshape2(): number { + return this.raw.subshape2(); + } + + public numContacts(): number { + return this.raw.num_contacts(); + } + + public localContactPoint1(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.contact_local_p1(i)); + } + + public localContactPoint2(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.contact_local_p2(i)); + } + + public contactDist(i: number): number { + return this.raw.contact_dist(i); + } + + public contactFid1(i: number): number { + return this.raw.contact_fid1(i); + } + + public contactFid2(i: number): number { + return this.raw.contact_fid2(i); + } + + public contactImpulse(i: number): number { + return this.raw.contact_impulse(i); + } + + // #if DIM2 + public contactTangentImpulse(i: number): number { + return this.raw.contact_tangent_impulse(i); + } + // #endif + + // #if DIM3 + public contactTangentImpulseX(i: number): number { + return this.raw.contact_tangent_impulse_x(i); + } + + public contactTangentImpulseY(i: number): number { + return this.raw.contact_tangent_impulse_y(i); + } + // #endif + + public numSolverContacts(): number { + return this.raw.num_solver_contacts(); + } + + public solverContactPoint(i: number): Vector { + return VectorOps.fromRaw(this.raw.solver_contact_point(i)); + } + + public solverContactDist(i: number): number { + return this.raw.solver_contact_dist(i); + } + + public solverContactFriction(i: number): number { + return this.raw.solver_contact_friction(i); + } + + public solverContactRestitution(i: number): number { + return this.raw.solver_contact_restitution(i); + } + + public solverContactTangentVelocity(i: number): Vector { + return VectorOps.fromRaw(this.raw.solver_contact_tangent_velocity(i)); + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/point.ts b/thirdparty/rapier.js/src.ts/geometry/point.ts new file mode 100644 index 00000000..d272b7e8 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/point.ts @@ -0,0 +1,98 @@ +import {Collider, ColliderHandle} from "./collider"; +import {Vector, VectorOps} from "../math"; +import { + RawFeatureType, + RawPointColliderProjection, + RawPointProjection, +} from "../raw"; +import {FeatureType} from "./feature"; +import {ColliderSet} from "./collider_set"; + +/** + * The projection of a point on a collider. + */ +export class PointProjection { + /** + * The projection of the point on the collider. + */ + point: Vector; + /** + * Is the point inside of the collider? + */ + isInside: boolean; + + constructor(point: Vector, isInside: boolean) { + this.point = point; + this.isInside = isInside; + } + + public static fromRaw(raw: RawPointProjection): PointProjection { + if (!raw) return null; + + const result = new PointProjection( + VectorOps.fromRaw(raw.point()), + raw.isInside(), + ); + raw.free(); + return result; + } +} + +/** + * The projection of a point on a collider (includes the collider handle). + */ +export class PointColliderProjection { + /** + * The collider hit by the ray. + */ + collider: Collider; + /** + * The projection of the point on the collider. + */ + point: Vector; + /** + * Is the point inside of the collider? + */ + isInside: boolean; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + collider: Collider, + point: Vector, + isInside: boolean, + featureType?: FeatureType, + featureId?: number, + ) { + this.collider = collider; + this.point = point; + this.isInside = isInside; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawPointColliderProjection, + ): PointColliderProjection { + if (!raw) return null; + + const result = new PointColliderProjection( + colliderSet.get(raw.colliderHandle()), + VectorOps.fromRaw(raw.point()), + raw.isInside(), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/ray.ts b/thirdparty/rapier.js/src.ts/geometry/ray.ts new file mode 100644 index 00000000..36df5ee3 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/ray.ts @@ -0,0 +1,192 @@ +import {Vector, VectorOps} from "../math"; +import { + RawFeatureType, + RawRayColliderIntersection, + RawRayColliderHit, + RawRayIntersection, +} from "../raw"; +import {Collider} from "./collider"; +import {FeatureType} from "./feature"; +import {ColliderSet} from "./collider_set"; + +/** + * A ray. This is a directed half-line. + */ +export class Ray { + /** + * The starting point of the ray. + */ + public origin: Vector; + /** + * The direction of propagation of the ray. + */ + public dir: Vector; + + /** + * Builds a ray from its origin and direction. + * + * @param origin - The ray's starting point. + * @param dir - The ray's direction of propagation. + */ + constructor(origin: Vector, dir: Vector) { + this.origin = origin; + this.dir = dir; + } + + public pointAt(t: number): Vector { + return { + x: this.origin.x + this.dir.x * t, + y: this.origin.y + this.dir.y * t, + // #if DIM3 + z: this.origin.z + this.dir.z * t, + // #endif + }; + } +} + +/** + * The intersection between a ray and a collider. + */ +export class RayIntersection { + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + /** + * The normal of the collider at the hit point. + */ + normal: Vector; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + timeOfImpact: number, + normal: Vector, + featureType?: FeatureType, + featureId?: number, + ) { + this.timeOfImpact = timeOfImpact; + this.normal = normal; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw(raw: RawRayIntersection): RayIntersection { + if (!raw) return null; + + const result = new RayIntersection( + raw.time_of_impact(), + VectorOps.fromRaw(raw.normal()), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} + +/** + * The intersection between a ray and a collider (includes the collider handle). + */ +export class RayColliderIntersection { + /** + * The collider hit by the ray. + */ + collider: Collider; + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + /** + * The normal of the collider at the hit point. + */ + normal: Vector; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + collider: Collider, + timeOfImpact: number, + normal: Vector, + featureType?: FeatureType, + featureId?: number, + ) { + this.collider = collider; + this.timeOfImpact = timeOfImpact; + this.normal = normal; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawRayColliderIntersection, + ): RayColliderIntersection { + if (!raw) return null; + + const result = new RayColliderIntersection( + colliderSet.get(raw.colliderHandle()), + raw.time_of_impact(), + VectorOps.fromRaw(raw.normal()), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} + +/** + * The time of impact between a ray and a collider. + */ +export class RayColliderHit { + /** + * The handle of the collider hit by the ray. + */ + collider: Collider; + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + + constructor(collider: Collider, timeOfImpact: number) { + this.collider = collider; + this.timeOfImpact = timeOfImpact; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawRayColliderHit, + ): RayColliderHit { + if (!raw) return null; + + const result = new RayColliderHit( + colliderSet.get(raw.colliderHandle()), + raw.timeOfImpact(), + ); + raw.free(); + return result; + } +} diff --git a/thirdparty/rapier.js/src.ts/geometry/shape.ts b/thirdparty/rapier.js/src.ts/geometry/shape.ts new file mode 100644 index 00000000..f3546982 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/shape.ts @@ -0,0 +1,1558 @@ +import {Vector, VectorOps, Rotation, RotationOps} from "../math"; +import {RawColliderSet, RawShape, RawShapeType} from "../raw"; +import {ShapeContact} from "./contact"; +import {PointProjection} from "./point"; +import {Ray, RayIntersection} from "./ray"; +import {ShapeCastHit} from "./toi"; +import {ColliderHandle} from "./collider"; + +export abstract class Shape { + public abstract intoRaw(): RawShape; + + /** + * The concrete type of this shape. + */ + public abstract get type(): ShapeType; + + /** + * instant mode without cache + */ + public static fromRaw( + rawSet: RawColliderSet, + handle: ColliderHandle, + ): Shape { + const rawType = rawSet.coShapeType(handle); + + let extents: Vector; + let borderRadius: number; + let vs: Float32Array; + let indices: Uint32Array; + let halfHeight: number; + let radius: number; + let normal: Vector; + + switch (rawType) { + case RawShapeType.Ball: + return new Ball(rawSet.coRadius(handle)); + case RawShapeType.Cuboid: + extents = rawSet.coHalfExtents(handle); + // #if DIM2 + return new Cuboid(extents.x, extents.y); + // #endif + + // #if DIM3 + return new Cuboid(extents.x, extents.y, extents.z); + // #endif + + case RawShapeType.RoundCuboid: + extents = rawSet.coHalfExtents(handle); + borderRadius = rawSet.coRoundRadius(handle); + + // #if DIM2 + return new RoundCuboid(extents.x, extents.y, borderRadius); + // #endif + + // #if DIM3 + return new RoundCuboid( + extents.x, + extents.y, + extents.z, + borderRadius, + ); + // #endif + + case RawShapeType.Capsule: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + return new Capsule(halfHeight, radius); + case RawShapeType.Segment: + vs = rawSet.coVertices(handle); + + // #if DIM2 + return new Segment( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + ); + // #endif + + // #if DIM3 + return new Segment( + VectorOps.new(vs[0], vs[1], vs[2]), + VectorOps.new(vs[3], vs[4], vs[5]), + ); + // #endif + + case RawShapeType.Polyline: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + return new Polyline(vs, indices); + case RawShapeType.Triangle: + vs = rawSet.coVertices(handle); + + // #if DIM2 + return new Triangle( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + VectorOps.new(vs[4], vs[5]), + ); + // #endif + + // #if DIM3 + return new Triangle( + VectorOps.new(vs[0], vs[1], vs[2]), + VectorOps.new(vs[3], vs[4], vs[5]), + VectorOps.new(vs[6], vs[7], vs[8]), + ); + // #endif + + case RawShapeType.RoundTriangle: + vs = rawSet.coVertices(handle); + borderRadius = rawSet.coRoundRadius(handle); + + // #if DIM2 + return new RoundTriangle( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + VectorOps.new(vs[4], vs[5]), + borderRadius, + ); + // #endif + + // #if DIM3 + return new RoundTriangle( + VectorOps.new(vs[0], vs[1], vs[2]), + VectorOps.new(vs[3], vs[4], vs[5]), + VectorOps.new(vs[6], vs[7], vs[8]), + borderRadius, + ); + // #endif + + case RawShapeType.HalfSpace: + normal = VectorOps.fromRaw(rawSet.coHalfspaceNormal(handle)); + return new HalfSpace(normal); + + case RawShapeType.Voxels: + const vox_data = rawSet.coVoxelData(handle); + const vox_size = rawSet.coVoxelSize(handle); + return new Voxels(vox_data, vox_size); + + case RawShapeType.TriMesh: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + const tri_flags = rawSet.coTriMeshFlags(handle); + return new TriMesh(vs, indices, tri_flags); + + case RawShapeType.HeightField: + const scale = rawSet.coHeightfieldScale(handle); + const heights = rawSet.coHeightfieldHeights(handle); + + // #if DIM2 + return new Heightfield(heights, scale); + // #endif + + // #if DIM3 + const nrows = rawSet.coHeightfieldNRows(handle); + const ncols = rawSet.coHeightfieldNCols(handle); + const hf_flags = rawSet.coHeightFieldFlags(handle); + return new Heightfield(nrows, ncols, heights, scale, hf_flags); + // #endif + + // #if DIM2 + case RawShapeType.ConvexPolygon: + vs = rawSet.coVertices(handle); + return new ConvexPolygon(vs, false); + case RawShapeType.RoundConvexPolygon: + vs = rawSet.coVertices(handle); + borderRadius = rawSet.coRoundRadius(handle); + return new RoundConvexPolygon(vs, borderRadius, false); + // #endif + + // #if DIM3 + case RawShapeType.ConvexPolyhedron: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + return new ConvexPolyhedron(vs, indices); + case RawShapeType.RoundConvexPolyhedron: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + borderRadius = rawSet.coRoundRadius(handle); + return new RoundConvexPolyhedron(vs, indices, borderRadius); + case RawShapeType.Cylinder: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + return new Cylinder(halfHeight, radius); + case RawShapeType.RoundCylinder: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + borderRadius = rawSet.coRoundRadius(handle); + return new RoundCylinder(halfHeight, radius, borderRadius); + case RawShapeType.Cone: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + return new Cone(halfHeight, radius); + case RawShapeType.RoundCone: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + borderRadius = rawSet.coRoundRadius(handle); + return new RoundCone(halfHeight, radius, borderRadius); + // #endif + + default: + throw new Error("unknown shape type: " + rawType); + } + } + + /** + * Computes the time of impact between two moving shapes. + * @param shapePos1 - The initial position of this sahpe. + * @param shapeRot1 - The rotation of this shape. + * @param shapeVel1 - The velocity of this shape. + * @param shape2 - The second moving shape. + * @param shapePos2 - The initial position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @param shapeVel2 - The velocity of the second shape. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time when the impact can happen. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @returns If the two moving shapes collider at some point along their trajectories, this returns the + * time at which the two shape collider as well as the contact information during the impact. Returns + * `null`if the two shapes never collide along their paths. + */ + public castShape( + shapePos1: Vector, + shapeRot1: Rotation, + shapeVel1: Vector, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + shapeVel2: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ShapeCastHit | null { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawVel1 = VectorOps.intoRaw(shapeVel1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + let rawVel2 = VectorOps.intoRaw(shapeVel2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeCastHit.fromRaw( + null, + rawShape1.castShape( + rawPos1, + rawRot1, + rawVel1, + rawShape2, + rawPos2, + rawRot2, + rawVel2, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawPos1.free(); + rawRot1.free(); + rawVel1.free(); + rawPos2.free(); + rawRot2.free(); + rawVel2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + /** + * Tests if this shape intersects another shape. + * + * @param shapePos1 - The position of this shape. + * @param shapeRot1 - The rotation of this shape. + * @param shape2 - The second shape to test. + * @param shapePos2 - The position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @returns `true` if the two shapes intersect, `false` if they don’t. + */ + public intersectsShape( + shapePos1: Vector, + shapeRot1: Rotation, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + ): boolean { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = rawShape1.intersectsShape( + rawPos1, + rawRot1, + rawShape2, + rawPos2, + rawRot2, + ); + + rawPos1.free(); + rawRot1.free(); + rawPos2.free(); + rawRot2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between two shapes. + * + * @param shapePos1 - The initial position of this sahpe. + * @param shapeRot1 - The rotation of this shape. + * @param shape2 - The second shape. + * @param shapePos2 - The initial position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactShape( + shapePos1: Vector, + shapeRot1: Rotation, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + prediction: number, + ): ShapeContact | null { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeContact.fromRaw( + rawShape1.contactShape( + rawPos1, + rawRot1, + rawShape2, + rawPos2, + rawRot2, + prediction, + ), + ); + + rawPos1.free(); + rawRot1.free(); + rawPos2.free(); + rawRot2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + containsPoint( + shapePos: Vector, + shapeRot: Rotation, + point: Vector, + ): boolean { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawPoint = VectorOps.intoRaw(point); + let rawShape = this.intoRaw(); + + let result = rawShape.containsPoint(rawPos, rawRot, rawPoint); + + rawPos.free(); + rawRot.free(); + rawPoint.free(); + rawShape.free(); + + return result; + } + + projectPoint( + shapePos: Vector, + shapeRot: Rotation, + point: Vector, + solid: boolean, + ): PointProjection { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawPoint = VectorOps.intoRaw(point); + let rawShape = this.intoRaw(); + + let result = PointProjection.fromRaw( + rawShape.projectPoint(rawPos, rawRot, rawPoint, solid), + ); + + rawPos.free(); + rawRot.free(); + rawPoint.free(); + rawShape.free(); + + return result; + } + + intersectsRay( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + ): boolean { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = rawShape.intersectsRay( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } + + castRay( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + solid: boolean, + ): number { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = rawShape.castRay( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + solid, + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } + + castRayAndGetNormal( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + solid: boolean, + ): RayIntersection { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = RayIntersection.fromRaw( + rawShape.castRayAndGetNormal( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + solid, + ), + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } +} + +// #if DIM2 +/** + * An enumeration representing the type of a shape. + */ +export enum ShapeType { + Ball = 0, + Cuboid = 1, + Capsule = 2, + Segment = 3, + Polyline = 4, + Triangle = 5, + TriMesh = 6, + HeightField = 7, + // Compound = 8, + ConvexPolygon = 9, + RoundCuboid = 10, + RoundTriangle = 11, + RoundConvexPolygon = 12, + HalfSpace = 13, + Voxels = 14, +} + +// #endif + +// #if DIM3 + +/** + * An enumeration representing the type of a shape. + */ +export enum ShapeType { + Ball = 0, + Cuboid = 1, + Capsule = 2, + Segment = 3, + Polyline = 4, + Triangle = 5, + TriMesh = 6, + HeightField = 7, + // Compound = 8, + ConvexPolyhedron = 9, + Cylinder = 10, + Cone = 11, + RoundCuboid = 12, + RoundTriangle = 13, + RoundCylinder = 14, + RoundCone = 15, + RoundConvexPolyhedron = 16, + HalfSpace = 17, + Voxels = 18, +} + +// NOTE: this **must** match the bits in the HeightFieldFlags on the rust side. +/** + * Flags controlling the behavior of some operations involving heightfields. + */ +export enum HeightFieldFlags { + /** + * If set, a special treatment will be applied to contact manifold calculation to eliminate + * or fix contacts normals that could lead to incorrect bumps in physics simulation (especially + * on flat surfaces). + * + * This is achieved by taking into account adjacent triangle normals when computing contact + * points for a given triangle. + */ + FIX_INTERNAL_EDGES = 0b0000_0001, +} + +// #endif + +// NOTE: this **must** match the TriMeshFlags on the rust side. +/** + * Flags controlling the behavior of the triangle mesh creation and of some + * operations involving triangle meshes. + */ +export enum TriMeshFlags { + // NOTE: these two flags are not really useful in JS. + // + // /** + // * If set, the half-edge topology of the trimesh will be computed if possible. + // */ + // HALF_EDGE_TOPOLOGY = 0b0000_0001, + // /** If set, the half-edge topology and connected components of the trimesh will be computed if possible. + // * + // * Because of the way it is currently implemented, connected components can only be computed on + // * a mesh where the half-edge topology computation succeeds. It will no longer be the case in the + // * future once we decouple the computations. + // */ + // CONNECTED_COMPONENTS = 0b0000_0010, + /** + * If set, any triangle that results in a failing half-hedge topology computation will be deleted. + */ + DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100, + /** + * If set, the trimesh will be assumed to be oriented (with outward normals). + * + * The pseudo-normals of its vertices and edges will be computed. + */ + ORIENTED = 0b0000_1000, + /** + * If set, the duplicate vertices of the trimesh will be merged. + * + * Two vertices with the exact same coordinates will share the same entry on the + * vertex buffer and the index buffer is adjusted accordingly. + */ + MERGE_DUPLICATE_VERTICES = 0b0001_0000, + /** + * If set, the triangles sharing two vertices with identical index values will be removed. + * + * Because of the way it is currently implemented, this methods implies that duplicate + * vertices will be merged. It will no longer be the case in the future once we decouple + * the computations. + */ + DELETE_DEGENERATE_TRIANGLES = 0b0010_0000, + /** + * If set, two triangles sharing three vertices with identical index values (in any order) + * will be removed. + * + * Because of the way it is currently implemented, this methods implies that duplicate + * vertices will be merged. It will no longer be the case in the future once we decouple + * the computations. + */ + DELETE_DUPLICATE_TRIANGLES = 0b0100_0000, + /** + * If set, a special treatment will be applied to contact manifold calculation to eliminate + * or fix contacts normals that could lead to incorrect bumps in physics simulation + * (especially on flat surfaces). + * + * This is achieved by taking into account adjacent triangle normals when computing contact + * points for a given triangle. + * + * /!\ NOT SUPPORTED IN THE 2D VERSION OF RAPIER. + */ + FIX_INTERNAL_EDGES = 0b1000_0000 | TriMeshFlags.MERGE_DUPLICATE_VERTICES, +} + +/** + * A shape that is a sphere in 3D and a circle in 2D. + */ +export class Ball extends Shape { + readonly type = ShapeType.Ball; + + /** + * The balls radius. + */ + radius: number; + + /** + * Creates a new ball with the given radius. + * @param radius - The balls radius. + */ + constructor(radius: number) { + super(); + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.ball(this.radius); + } +} + +export class HalfSpace extends Shape { + readonly type = ShapeType.HalfSpace; + + /** + * The outward normal of the half-space. + */ + normal: Vector; + + /** + * Creates a new halfspace delimited by an infinite plane. + * + * @param normal - The outward normal of the plane. + */ + constructor(normal: Vector) { + super(); + this.normal = normal; + } + + public intoRaw(): RawShape { + let n = VectorOps.intoRaw(this.normal); + let result = RawShape.halfspace(n); + n.free(); + return result; + } +} + +/** + * A shape that is a box in 3D and a rectangle in 2D. + */ +export class Cuboid extends Shape { + readonly type = ShapeType.Cuboid; + + /** + * The half extent of the cuboid along each coordinate axis. + */ + halfExtents: Vector; + + // #if DIM2 + /** + * Creates a new 2D rectangle. + * @param hx - The half width of the rectangle. + * @param hy - The helf height of the rectangle. + */ + constructor(hx: number, hy: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy); + } + + // #endif + + // #if DIM3 + /** + * Creates a new 3D cuboid. + * @param hx - The half width of the cuboid. + * @param hy - The half height of the cuboid. + * @param hz - The half depth of the cuboid. + */ + constructor(hx: number, hy: number, hz: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy, hz); + } + + // #endif + + public intoRaw(): RawShape { + // #if DIM2 + return RawShape.cuboid(this.halfExtents.x, this.halfExtents.y); + // #endif + + // #if DIM3 + return RawShape.cuboid( + this.halfExtents.x, + this.halfExtents.y, + this.halfExtents.z, + ); + // #endif + } +} + +/** + * A shape that is a box in 3D and a rectangle in 2D, with round corners. + */ +export class RoundCuboid extends Shape { + readonly type = ShapeType.RoundCuboid; + + /** + * The half extent of the cuboid along each coordinate axis. + */ + halfExtents: Vector; + + /** + * The radius of the cuboid's round border. + */ + borderRadius: number; + + // #if DIM2 + /** + * Creates a new 2D rectangle. + * @param hx - The half width of the rectangle. + * @param hy - The helf height of the rectangle. + * @param borderRadius - The radius of the borders of this cuboid. This will + * effectively increase the half-extents of the cuboid by this radius. + */ + constructor(hx: number, hy: number, borderRadius: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy); + this.borderRadius = borderRadius; + } + + // #endif + + // #if DIM3 + /** + * Creates a new 3D cuboid. + * @param hx - The half width of the cuboid. + * @param hy - The half height of the cuboid. + * @param hz - The half depth of the cuboid. + * @param borderRadius - The radius of the borders of this cuboid. This will + * effectively increase the half-extents of the cuboid by this radius. + */ + constructor(hx: number, hy: number, hz: number, borderRadius: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy, hz); + this.borderRadius = borderRadius; + } + + // #endif + + public intoRaw(): RawShape { + // #if DIM2 + return RawShape.roundCuboid( + this.halfExtents.x, + this.halfExtents.y, + this.borderRadius, + ); + // #endif + + // #if DIM3 + return RawShape.roundCuboid( + this.halfExtents.x, + this.halfExtents.y, + this.halfExtents.z, + this.borderRadius, + ); + // #endif + } +} + +/** + * A shape that is a capsule. + */ +export class Capsule extends Shape { + readonly type = ShapeType.Capsule; + + /** + * The radius of the capsule's basis. + */ + radius: number; + + /** + * The capsule's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * Creates a new capsule with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + */ + constructor(halfHeight: number, radius: number) { + super(); + this.halfHeight = halfHeight; + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.capsule(this.halfHeight, this.radius); + } +} + +/** + * A shape that is a segment. + */ +export class Segment extends Shape { + readonly type = ShapeType.Segment; + + /** + * The first point of the segment. + */ + a: Vector; + + /** + * The second point of the segment. + */ + b: Vector; + + /** + * Creates a new segment shape. + * @param a - The first point of the segment. + * @param b - The second point of the segment. + */ + constructor(a: Vector, b: Vector) { + super(); + this.a = a; + this.b = b; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let result = RawShape.segment(ra, rb); + ra.free(); + rb.free(); + return result; + } +} + +/** + * A shape that is a segment. + */ +export class Triangle extends Shape { + readonly type = ShapeType.Triangle; + + /** + * The first point of the triangle. + */ + a: Vector; + + /** + * The second point of the triangle. + */ + b: Vector; + + /** + * The second point of the triangle. + */ + c: Vector; + + /** + * Creates a new triangle shape. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + */ + constructor(a: Vector, b: Vector, c: Vector) { + super(); + this.a = a; + this.b = b; + this.c = c; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let rc = VectorOps.intoRaw(this.c); + let result = RawShape.triangle(ra, rb, rc); + ra.free(); + rb.free(); + rc.free(); + return result; + } +} + +/** + * A shape that is a triangle with round borders and a non-zero thickness. + */ +export class RoundTriangle extends Shape { + readonly type = ShapeType.RoundTriangle; + + /** + * The first point of the triangle. + */ + a: Vector; + + /** + * The second point of the triangle. + */ + b: Vector; + + /** + * The second point of the triangle. + */ + c: Vector; + + /** + * The radius of the triangles's rounded edges and vertices. + * In 3D, this is also equal to half the thickness of the round triangle. + */ + borderRadius: number; + + /** + * Creates a new triangle shape with round corners. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + * @param borderRadius - The radius of the borders of this triangle. In 3D, + * this is also equal to half the thickness of the triangle. + */ + constructor(a: Vector, b: Vector, c: Vector, borderRadius: number) { + super(); + this.a = a; + this.b = b; + this.c = c; + this.borderRadius = borderRadius; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let rc = VectorOps.intoRaw(this.c); + let result = RawShape.roundTriangle(ra, rb, rc, this.borderRadius); + ra.free(); + rb.free(); + rc.free(); + return result; + } +} + +/** + * A shape that is a triangle mesh. + */ +export class Polyline extends Shape { + readonly type = ShapeType.Polyline; + + /** + * The vertices of the polyline. + */ + vertices: Float32Array; + + /** + * The indices of the segments. + */ + indices: Uint32Array; + + /** + * Creates a new polyline shape. + * + * @param vertices - The coordinates of the polyline's vertices. + * @param indices - The indices of the polyline's segments. If this is `null` or not provided, then + * the vertices are assumed to form a line strip. + */ + constructor(vertices: Float32Array, indices?: Uint32Array) { + super(); + this.vertices = vertices; + this.indices = indices ?? new Uint32Array(0); + } + + public intoRaw(): RawShape { + return RawShape.polyline(this.vertices, this.indices); + } +} + +/** + * A shape made of voxels. + */ +export class Voxels extends Shape { + readonly type = ShapeType.Voxels; + + /** + * The points or grid coordinates used to initialize the voxels. + */ + data: Float32Array | Int32Array; + + /** + * The dimensions of each voxel. + */ + voxelSize: Vector; + + /** + * Creates a new shape made of voxels. + * + * @param data - Defines the set of voxels. If this is a `Int32Array` then + * each voxel is defined from its (signed) grid coordinates, + * with 3 (resp 2) contiguous integers per voxel in 3D (resp 2D). + * If this is a `Float32Array`, each voxel will be such that + * they contain at least one point from this array (where each + * point is defined from 3 (resp 2) contiguous numbers per point + * in 3D (resp 2D). + * @param voxelSize - The size of each voxel. + */ + constructor(data: Float32Array | Int32Array, voxelSize: Vector) { + super(); + this.data = data; + this.voxelSize = voxelSize; + } + + public intoRaw(): RawShape { + let voxelSize = VectorOps.intoRaw(this.voxelSize); + + let result; + if (this.data instanceof Int32Array) { + result = RawShape.voxels(voxelSize, this.data); + } else { + result = RawShape.voxelsFromPoints(voxelSize, this.data); + } + + voxelSize.free(); + return result; + } +} + +/** + * A shape that is a triangle mesh. + */ +export class TriMesh extends Shape { + readonly type = ShapeType.TriMesh; + + /** + * The vertices of the triangle mesh. + */ + vertices: Float32Array; + + /** + * The indices of the triangles. + */ + indices: Uint32Array; + + /** + * The triangle mesh flags. + */ + flags: TriMeshFlags; + + /** + * Creates a new triangle mesh shape. + * + * @param vertices - The coordinates of the triangle mesh's vertices. + * @param indices - The indices of the triangle mesh's triangles. + */ + constructor( + vertices: Float32Array, + indices: Uint32Array, + flags?: TriMeshFlags, + ) { + super(); + this.vertices = vertices; + this.indices = indices; + this.flags = flags; + } + + public intoRaw(): RawShape { + return RawShape.trimesh(this.vertices, this.indices, this.flags); + } +} + +// #if DIM2 +/** + * A shape that is a convex polygon. + */ +export class ConvexPolygon extends Shape { + readonly type = ShapeType.ConvexPolygon; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * Do we want to assume the vertices already form a convex hull? + */ + skipConvexHullComputation: boolean; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param skipConvexHullComputation - If set to `true`, the input points will + * be assumed to form a convex polyline and no convex-hull computation will + * be done automatically. + */ + constructor(vertices: Float32Array, skipConvexHullComputation: boolean) { + super(); + this.vertices = vertices; + this.skipConvexHullComputation = !!skipConvexHullComputation; + } + + public intoRaw(): RawShape { + if (this.skipConvexHullComputation) { + return RawShape.convexPolyline(this.vertices); + } else { + return RawShape.convexHull(this.vertices); + } + } +} + +/** + * A shape that is a convex polygon. + */ +export class RoundConvexPolygon extends Shape { + readonly type = ShapeType.RoundConvexPolygon; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * Do we want to assume the vertices already form a convex hull? + */ + skipConvexHullComputation: boolean; + + /** + * The radius of the convex polygon's rounded edges and vertices. + */ + borderRadius: number; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param borderRadius - The radius of the borders of this convex polygon. + * @param skipConvexHullComputation - If set to `true`, the input points will + * be assumed to form a convex polyline and no convex-hull computation will + * be done automatically. + */ + constructor( + vertices: Float32Array, + borderRadius: number, + skipConvexHullComputation: boolean, + ) { + super(); + this.vertices = vertices; + this.borderRadius = borderRadius; + this.skipConvexHullComputation = !!skipConvexHullComputation; + } + + public intoRaw(): RawShape { + if (this.skipConvexHullComputation) { + return RawShape.roundConvexPolyline( + this.vertices, + this.borderRadius, + ); + } else { + return RawShape.roundConvexHull(this.vertices, this.borderRadius); + } + } +} + +/** + * A shape that is a heightfield. + */ +export class Heightfield extends Shape { + readonly type = ShapeType.HeightField; + + /** + * The heights of the heightfield, along its local `y` axis. + */ + heights: Float32Array; + + /** + * The heightfield's length along its local `x` axis. + */ + scale: Vector; + + /** + * Creates a new heightfield shape. + * + * @param heights - The heights of the heightfield, along its local `y` axis. + * @param scale - The scale factor applied to the heightfield. + */ + constructor(heights: Float32Array, scale: Vector) { + super(); + this.heights = heights; + this.scale = scale; + } + + public intoRaw(): RawShape { + let rawScale = VectorOps.intoRaw(this.scale); + let rawShape = RawShape.heightfield(this.heights, rawScale); + rawScale.free(); + return rawShape; + } +} + +// #endif + +// #if DIM3 +/** + * A shape that is a convex polygon. + */ +export class ConvexPolyhedron extends Shape { + readonly type = ShapeType.ConvexPolyhedron; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * The indices of the convex polygon. + */ + indices?: Uint32Array | null; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param indices - The index buffer of this convex mesh. If this is `null` + * or `undefined`, the convex-hull of the input vertices will be computed + * automatically. Otherwise, it will be assumed that the mesh you provide + * is already convex. + */ + constructor(vertices: Float32Array, indices?: Uint32Array | null) { + super(); + this.vertices = vertices; + this.indices = indices; + } + + public intoRaw(): RawShape { + if (!!this.indices) { + return RawShape.convexMesh(this.vertices, this.indices); + } else { + return RawShape.convexHull(this.vertices); + } + } +} + +/** + * A shape that is a convex polygon. + */ +export class RoundConvexPolyhedron extends Shape { + readonly type = ShapeType.RoundConvexPolyhedron; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * The indices of the convex polygon. + */ + indices?: Uint32Array; + + /** + * The radius of the convex polyhedron's rounded edges and vertices. + */ + borderRadius: number; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param indices - The index buffer of this convex mesh. If this is `null` + * or `undefined`, the convex-hull of the input vertices will be computed + * automatically. Otherwise, it will be assumed that the mesh you provide + * is already convex. + * @param borderRadius - The radius of the borders of this convex polyhedron. + */ + constructor( + vertices: Float32Array, + indices: Uint32Array | null | undefined, + borderRadius: number, + ) { + super(); + this.vertices = vertices; + this.indices = indices; + this.borderRadius = borderRadius; + } + + public intoRaw(): RawShape { + if (!!this.indices) { + return RawShape.roundConvexMesh( + this.vertices, + this.indices, + this.borderRadius, + ); + } else { + return RawShape.roundConvexHull(this.vertices, this.borderRadius); + } + } +} + +/** + * A shape that is a heightfield. + */ +export class Heightfield extends Shape { + readonly type = ShapeType.HeightField; + + /** + * The number of rows in the heights matrix. + */ + nrows: number; + + /** + * The number of columns in the heights matrix. + */ + ncols: number; + + /** + * The heights of the heightfield along its local `y` axis, + * provided as a matrix stored in column-major order. + */ + heights: Float32Array; + + /** + * The dimensions of the heightfield's local `x,z` plane. + */ + scale: Vector; + + /** + * Flags applied to the heightfield. + */ + flags: HeightFieldFlags; + + /** + * Creates a new heightfield shape. + * + * @param nrows − The number of rows in the heights matrix. + * @param ncols - The number of columns in the heights matrix. + * @param heights - The heights of the heightfield along its local `y` axis, + * provided as a matrix stored in column-major order. + * @param scale - The dimensions of the heightfield's local `x,z` plane. + */ + constructor( + nrows: number, + ncols: number, + heights: Float32Array, + scale: Vector, + flags?: HeightFieldFlags, + ) { + super(); + this.nrows = nrows; + this.ncols = ncols; + this.heights = heights; + this.scale = scale; + this.flags = flags; + } + + public intoRaw(): RawShape { + let rawScale = VectorOps.intoRaw(this.scale); + let rawShape = RawShape.heightfield( + this.nrows, + this.ncols, + this.heights, + rawScale, + this.flags, + ); + rawScale.free(); + return rawShape; + } +} + +/** + * A shape that is a 3D cylinder. + */ +export class Cylinder extends Shape { + readonly type = ShapeType.Cylinder; + + /** + * The radius of the cylinder's basis. + */ + radius: number; + + /** + * The cylinder's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * Creates a new cylinder with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + */ + constructor(halfHeight: number, radius: number) { + super(); + this.halfHeight = halfHeight; + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.cylinder(this.halfHeight, this.radius); + } +} + +/** + * A shape that is a 3D cylinder with round corners. + */ +export class RoundCylinder extends Shape { + readonly type = ShapeType.RoundCylinder; + + /** + * The radius of the cylinder's basis. + */ + radius: number; + + /** + * The cylinder's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * The radius of the cylinder's rounded edges and vertices. + */ + borderRadius: number; + + /** + * Creates a new cylinder with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + * @param borderRadius - The radius of the borders of this cylinder. + */ + constructor(halfHeight: number, radius: number, borderRadius: number) { + super(); + this.borderRadius = borderRadius; + this.halfHeight = halfHeight; + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.roundCylinder( + this.halfHeight, + this.radius, + this.borderRadius, + ); + } +} + +/** + * A shape that is a 3D cone. + */ +export class Cone extends Shape { + readonly type = ShapeType.Cone; + + /** + * The radius of the cone's basis. + */ + radius: number; + + /** + * The cone's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * Creates a new cone with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + */ + constructor(halfHeight: number, radius: number) { + super(); + this.halfHeight = halfHeight; + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.cone(this.halfHeight, this.radius); + } +} + +/** + * A shape that is a 3D cone with round corners. + */ +export class RoundCone extends Shape { + readonly type = ShapeType.RoundCone; + + /** + * The radius of the cone's basis. + */ + radius: number; + + /** + * The cone's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * The radius of the cylinder's rounded edges and vertices. + */ + borderRadius: number; + + /** + * Creates a new cone with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + * @param borderRadius - The radius of the borders of this cone. + */ + constructor(halfHeight: number, radius: number, borderRadius: number) { + super(); + this.halfHeight = halfHeight; + this.radius = radius; + this.borderRadius = borderRadius; + } + + public intoRaw(): RawShape { + return RawShape.roundCone( + this.halfHeight, + this.radius, + this.borderRadius, + ); + } +} + +// #endif diff --git a/thirdparty/rapier.js/src.ts/geometry/toi.ts b/thirdparty/rapier.js/src.ts/geometry/toi.ts new file mode 100644 index 00000000..ade13f17 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/geometry/toi.ts @@ -0,0 +1,105 @@ +import {Collider} from "./collider"; +import {Vector, VectorOps} from "../math"; +import {RawShapeCastHit, RawColliderShapeCastHit} from "../raw"; +import {ColliderSet} from "./collider_set"; + +/** + * The intersection between a ray and a collider. + */ +export class ShapeCastHit { + /** + * The time of impact of the two shapes. + */ + time_of_impact: number; + /** + * The local-space contact point on the first shape, at + * the time of impact. + */ + witness1: Vector; + /** + * The local-space contact point on the second shape, at + * the time of impact. + */ + witness2: Vector; + /** + * The local-space normal on the first shape, at + * the time of impact. + */ + normal1: Vector; + /** + * The local-space normal on the second shape, at + * the time of impact. + */ + normal2: Vector; + + constructor( + time_of_impact: number, + witness1: Vector, + witness2: Vector, + normal1: Vector, + normal2: Vector, + ) { + this.time_of_impact = time_of_impact; + this.witness1 = witness1; + this.witness2 = witness2; + this.normal1 = normal1; + this.normal2 = normal2; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawShapeCastHit, + ): ShapeCastHit { + if (!raw) return null; + + const result = new ShapeCastHit( + raw.time_of_impact(), + VectorOps.fromRaw(raw.witness1()), + VectorOps.fromRaw(raw.witness2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} + +/** + * The intersection between a ray and a collider. + */ +export class ColliderShapeCastHit extends ShapeCastHit { + /** + * The handle of the collider hit by the ray. + */ + collider: Collider; + + constructor( + collider: Collider, + time_of_impact: number, + witness1: Vector, + witness2: Vector, + normal1: Vector, + normal2: Vector, + ) { + super(time_of_impact, witness1, witness2, normal1, normal2); + this.collider = collider; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawColliderShapeCastHit, + ): ColliderShapeCastHit { + if (!raw) return null; + + const result = new ColliderShapeCastHit( + colliderSet.get(raw.colliderHandle()), + raw.time_of_impact(), + VectorOps.fromRaw(raw.witness1()), + VectorOps.fromRaw(raw.witness2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} diff --git a/thirdparty/rapier.js/src.ts/init.ts b/thirdparty/rapier.js/src.ts/init.ts new file mode 100644 index 00000000..e9e66aad --- /dev/null +++ b/thirdparty/rapier.js/src.ts/init.ts @@ -0,0 +1,2 @@ +// Placeholder for the `init` function used in rapier-compat. +export {}; diff --git a/thirdparty/rapier.js/src.ts/math.ts b/thirdparty/rapier.js/src.ts/math.ts new file mode 100644 index 00000000..77669258 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/math.ts @@ -0,0 +1,263 @@ +import {RawVector, RawRotation} from "./raw"; +// #if DIM3 +import {RawSdpMatrix3} from "./raw"; +// #endif + +// #if DIM2 +export interface Vector { + x: number; + y: number; +} + +/** + * A 2D vector. + */ +export class Vector2 implements Vector { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +export class VectorOps { + public static new(x: number, y: number): Vector { + return new Vector2(x, y); + } + + public static zeros(): Vector { + return VectorOps.new(0.0, 0.0); + } + + // FIXME: type ram: RawVector? + public static fromRaw(raw: RawVector): Vector | null { + if (!raw) return null; + + let res = VectorOps.new(raw.x, raw.y); + raw.free(); + return res; + } + + public static intoRaw(v: Vector): RawVector { + return new RawVector(v.x, v.y); + } + + public static copy(out: Vector, input: Vector) { + out.x = input.x; + out.y = input.y; + } +} + +/** + * A rotation angle in radians. + */ +export type Rotation = number; + +export class RotationOps { + public static identity(): number { + return 0.0; + } + + public static fromRaw(raw: RawRotation): Rotation | null { + if (!raw) return null; + + let res = raw.angle; + raw.free(); + return res; + } + + public static intoRaw(angle: Rotation): RawRotation { + return RawRotation.fromAngle(angle); + } +} + +// #endif + +// #if DIM3 +export interface Vector { + x: number; + y: number; + z: number; +} + +/** + * A 3D vector. + */ +export class Vector3 implements Vector { + x: number; + y: number; + z: number; + + constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } +} + +export class VectorOps { + public static new(x: number, y: number, z: number): Vector { + return new Vector3(x, y, z); + } + + public static intoRaw(v: Vector): RawVector { + return new RawVector(v.x, v.y, v.z); + } + + public static zeros(): Vector { + return VectorOps.new(0.0, 0.0, 0.0); + } + + // FIXME: type ram: RawVector? + public static fromRaw(raw: RawVector): Vector | null { + if (!raw) return null; + + let res = VectorOps.new(raw.x, raw.y, raw.z); + raw.free(); + return res; + } + + public static copy(out: Vector, input: Vector) { + out.x = input.x; + out.y = input.y; + out.z = input.z; + } +} + +export interface Rotation { + x: number; + y: number; + z: number; + w: number; +} + +/** + * A quaternion. + */ +export class Quaternion implements Rotation { + x: number; + y: number; + z: number; + w: number; + + constructor(x: number, y: number, z: number, w: number) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } +} + +export class RotationOps { + public static identity(): Rotation { + return new Quaternion(0.0, 0.0, 0.0, 1.0); + } + + public static fromRaw(raw: RawRotation): Rotation | null { + if (!raw) return null; + + let res = new Quaternion(raw.x, raw.y, raw.z, raw.w); + raw.free(); + return res; + } + + public static intoRaw(rot: Rotation): RawRotation { + return new RawRotation(rot.x, rot.y, rot.z, rot.w); + } + + public static copy(out: Rotation, input: Rotation) { + out.x = input.x; + out.y = input.y; + out.z = input.z; + out.w = input.w; + } +} + +/** + * A 3D symmetric-positive-definite matrix. + */ +export class SdpMatrix3 { + /** + * Row major list of the upper-triangular part of the symmetric matrix. + */ + elements: Float32Array; + + /** + * Matrix element at row 1, column 1. + */ + public get m11(): number { + return this.elements[0]; + } + + /** + * Matrix element at row 1, column 2. + */ + public get m12(): number { + return this.elements[1]; + } + + /** + * Matrix element at row 2, column 1. + */ + public get m21(): number { + return this.m12; + } + + /** + * Matrix element at row 1, column 3. + */ + public get m13(): number { + return this.elements[2]; + } + + /** + * Matrix element at row 3, column 1. + */ + public get m31(): number { + return this.m13; + } + + /** + * Matrix element at row 2, column 2. + */ + public get m22(): number { + return this.elements[3]; + } + + /** + * Matrix element at row 2, column 3. + */ + public get m23(): number { + return this.elements[4]; + } + + /** + * Matrix element at row 3, column 2. + */ + public get m32(): number { + return this.m23; + } + + /** + * Matrix element at row 3, column 3. + */ + public get m33(): number { + return this.elements[5]; + } + + constructor(elements: Float32Array) { + this.elements = elements; + } +} + +export class SdpMatrix3Ops { + public static fromRaw(raw: RawSdpMatrix3): SdpMatrix3 { + const sdpMatrix3 = new SdpMatrix3(raw.elements()); + raw.free(); + return sdpMatrix3; + } +} + +// #endif diff --git a/thirdparty/rapier.js/src.ts/pipeline/debug_render_pipeline.ts b/thirdparty/rapier.js/src.ts/pipeline/debug_render_pipeline.ts new file mode 100644 index 00000000..bf0d7057 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/debug_render_pipeline.ts @@ -0,0 +1,85 @@ +import {RawDebugRenderPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + IslandManager, + ImpulseJointSet, + MultibodyJointSet, + RigidBodySet, +} from "../dynamics"; +import {BroadPhase, Collider, ColliderSet, NarrowPhase} from "../geometry"; +import {QueryFilterFlags} from "./query_pipeline"; + +/** + * The vertex and color buffers for debug-redering the physics scene. + */ +export class DebugRenderBuffers { + /** + * The lines to render. This is a flat array containing all the lines + * to render. Each line is described as two consecutive point. Each + * point is described as two (in 2D) or three (in 3D) consecutive + * floats. For example, in 2D, the array: `[1, 2, 3, 4, 5, 6, 7, 8]` + * describes the two segments `[[1, 2], [3, 4]]` and `[[5, 6], [7, 8]]`. + */ + public vertices: Float32Array; + /** + * The color buffer. There is one color per vertex, and each color + * has four consecutive components (in RGBA format). + */ + public colors: Float32Array; + + constructor(vertices: Float32Array, colors: Float32Array) { + this.vertices = vertices; + this.colors = colors; + } +} + +/** + * A pipeline for rendering the physics scene. + * + * To avoid leaking WASM resources, this MUST be freed manually with `debugRenderPipeline.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class DebugRenderPipeline { + raw: RawDebugRenderPipeline; + public vertices: Float32Array; + public colors: Float32Array; + + /** + * Release the WASM memory occupied by this serialization pipeline. + */ + free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + this.vertices = undefined; + this.colors = undefined; + } + + constructor(raw?: RawDebugRenderPipeline) { + this.raw = raw || new RawDebugRenderPipeline(); + } + + public render( + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + narrow_phase: NarrowPhase, + filterFlags?: QueryFilterFlags, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.raw.render( + bodies.raw, + colliders.raw, + impulse_joints.raw, + multibody_joints.raw, + narrow_phase.raw, + filterFlags, + colliders.castClosure(filterPredicate), + ); + this.vertices = this.raw.vertices(); + this.colors = this.raw.colors(); + } +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/event_queue.ts b/thirdparty/rapier.js/src.ts/pipeline/event_queue.ts new file mode 100644 index 00000000..e39c1582 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/event_queue.ts @@ -0,0 +1,158 @@ +import {RawContactForceEvent, RawEventQueue} from "../raw"; +import {RigidBodyHandle} from "../dynamics"; +import {Collider, ColliderHandle} from "../geometry"; +import {Vector, VectorOps} from "../math"; + +/** + * Flags indicating what events are enabled for colliders. + */ +export enum ActiveEvents { + NONE = 0, + /** + * Enable collision events. + */ + COLLISION_EVENTS = 0b0001, + /** + * Enable contact force events. + */ + CONTACT_FORCE_EVENTS = 0b0010, +} + +/** + * Event occurring when the sum of the magnitudes of the + * contact forces between two colliders exceed a threshold. + * + * This object should **not** be stored anywhere. Its properties can only be + * read from within the closure given to `EventHandler.drainContactForceEvents`. + */ +export class TempContactForceEvent { + raw: RawContactForceEvent; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * The first collider involved in the contact. + */ + public collider1(): ColliderHandle { + return this.raw.collider1(); + } + + /** + * The second collider involved in the contact. + */ + public collider2(): ColliderHandle { + return this.raw.collider2(); + } + + /** + * The sum of all the forces between the two colliders. + */ + public totalForce(): Vector { + return VectorOps.fromRaw(this.raw.total_force()); + } + + /** + * The sum of the magnitudes of each force between the two colliders. + * + * Note that this is **not** the same as the magnitude of `self.total_force`. + * Here we are summing the magnitude of all the forces, instead of taking + * the magnitude of their sum. + */ + public totalForceMagnitude(): number { + return this.raw.total_force_magnitude(); + } + + /** + * The world-space (unit) direction of the force with strongest magnitude. + */ + public maxForceDirection(): Vector { + return VectorOps.fromRaw(this.raw.max_force_direction()); + } + + /** + * The magnitude of the largest force at a contact point of this contact pair. + */ + public maxForceMagnitude(): number { + return this.raw.max_force_magnitude(); + } +} + +/** + * A structure responsible for collecting events generated + * by the physics engine. + * + * To avoid leaking WASM resources, this MUST be freed manually with `eventQueue.free()` + * once you are done using it. + */ +export class EventQueue { + raw: RawEventQueue; + + /** + * Creates a new event collector. + * + * @param autoDrain -setting this to `true` is strongly recommended. If true, the collector will + * be automatically drained before each `world.step(collector)`. If false, the collector will + * keep all events in memory unless it is manually drained/cleared; this may lead to unbounded use of + * RAM if no drain is performed. + */ + constructor(autoDrain: boolean, raw?: RawEventQueue) { + this.raw = raw || new RawEventQueue(autoDrain); + } + + /** + * Release the WASM memory occupied by this event-queue. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * Applies the given javascript closure on each collision event of this collector, then clear + * the internal collision event buffer. + * + * @param f - JavaScript closure applied to each collision event. The + * closure must take three arguments: two integers representing the handles of the colliders + * involved in the collision, and a boolean indicating if the collision started (true) or stopped + * (false). + */ + public drainCollisionEvents( + f: ( + handle1: ColliderHandle, + handle2: ColliderHandle, + started: boolean, + ) => void, + ) { + this.raw.drainCollisionEvents(f); + } + + /** + * Applies the given javascript closure on each contact force event of this collector, then clear + * the internal collision event buffer. + * + * @param f - JavaScript closure applied to each collision event. The + * closure must take one `TempContactForceEvent` argument. + */ + public drainContactForceEvents(f: (event: TempContactForceEvent) => void) { + let event = new TempContactForceEvent(); + this.raw.drainContactForceEvents((raw: RawContactForceEvent) => { + event.raw = raw; + f(event); + event.free(); + }); + } + + /** + * Removes all events contained by this collector + */ + public clear() { + this.raw.clear(); + } +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/index.ts b/thirdparty/rapier.js/src.ts/pipeline/index.ts new file mode 100644 index 00000000..092c1a59 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/index.ts @@ -0,0 +1,7 @@ +export * from "./world"; +export * from "./physics_pipeline"; +export * from "./serialization_pipeline"; +export * from "./event_queue"; +export * from "./physics_hooks"; +export * from "./debug_render_pipeline"; +export * from "./query_pipeline"; diff --git a/thirdparty/rapier.js/src.ts/pipeline/physics_hooks.ts b/thirdparty/rapier.js/src.ts/pipeline/physics_hooks.ts new file mode 100644 index 00000000..20e12ad9 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/physics_hooks.ts @@ -0,0 +1,54 @@ +import {RigidBodyHandle} from "../dynamics"; +import {ColliderHandle} from "../geometry"; + +export enum ActiveHooks { + NONE = 0, + FILTER_CONTACT_PAIRS = 0b0001, + FILTER_INTERSECTION_PAIRS = 0b0010, + // MODIFY_SOLVER_CONTACTS = 0b0100, /* Not supported yet in JS. */ +} + +export enum SolverFlags { + EMPTY = 0b000, + COMPUTE_IMPULSE = 0b001, +} + +export interface PhysicsHooks { + /** + * Function that determines if contacts computation should happen between two colliders, and how the + * constraints solver should behave for these contacts. + * + * This will only be executed and taken into account if at least one of the involved colliders contains the + * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks. + * + * @param collider1 − Handle of the first collider involved in the potential contact. + * @param collider2 − Handle of the second collider involved in the potential contact. + * @param body1 − Handle of the first body involved in the potential contact. + * @param body2 − Handle of the second body involved in the potential contact. + */ + filterContactPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + body1: RigidBodyHandle, + body2: RigidBodyHandle, + ): SolverFlags | null; + + /** + * Function that determines if intersection computation should happen between two colliders (where at least + * one is a sensor). + * + * This will only be executed and taken into account if `one of the involved colliders contains the + * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks. + * + * @param collider1 − Handle of the first collider involved in the potential contact. + * @param collider2 − Handle of the second collider involved in the potential contact. + * @param body1 − Handle of the first body involved in the potential contact. + * @param body2 − Handle of the second body involved in the potential contact. + */ + filterIntersectionPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + body1: RigidBodyHandle, + body2: RigidBodyHandle, + ): boolean; +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/physics_pipeline.ts b/thirdparty/rapier.js/src.ts/pipeline/physics_pipeline.ts new file mode 100644 index 00000000..a3319205 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/physics_pipeline.ts @@ -0,0 +1,85 @@ +import {RawPhysicsPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + ImpulseJointSet, + MultibodyJointSet, + RigidBodyHandle, + RigidBodySet, + CCDSolver, + IslandManager, +} from "../dynamics"; +import { + BroadPhase, + ColliderHandle, + ColliderSet, + NarrowPhase, +} from "../geometry"; +import {EventQueue} from "./event_queue"; +import {PhysicsHooks} from "./physics_hooks"; + +export class PhysicsPipeline { + raw: RawPhysicsPipeline; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawPhysicsPipeline) { + this.raw = raw || new RawPhysicsPipeline(); + } + + public step( + gravity: Vector, + integrationParameters: IntegrationParameters, + islands: IslandManager, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ccdSolver: CCDSolver, + eventQueue?: EventQueue, + hooks?: PhysicsHooks, + ) { + let rawG = VectorOps.intoRaw(gravity); + + if (!!eventQueue) { + this.raw.stepWithEvents( + rawG, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ccdSolver.raw, + eventQueue.raw, + hooks, + !!hooks ? hooks.filterContactPair : null, + !!hooks ? hooks.filterIntersectionPair : null, + ); + } else { + this.raw.step( + rawG, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ccdSolver.raw, + ); + } + + rawG.free(); + } +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/query_pipeline.ts b/thirdparty/rapier.js/src.ts/pipeline/query_pipeline.ts new file mode 100644 index 00000000..5610ff31 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/query_pipeline.ts @@ -0,0 +1,57 @@ +import {RawRayColliderIntersection} from "../raw"; +import { + ColliderHandle, + ColliderSet, + InteractionGroups, + PointColliderProjection, + Ray, + RayColliderIntersection, + RayColliderHit, + Shape, + ColliderShapeCastHit, +} from "../geometry"; +import {IslandManager, RigidBodyHandle, RigidBodySet} from "../dynamics"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; + +// NOTE: must match the bits in the QueryFilterFlags on the Rust side. +/** + * Flags for excluding whole sets of colliders from a scene query. + */ +export enum QueryFilterFlags { + /** + * Exclude from the query any collider attached to a fixed rigid-body and colliders with no rigid-body attached. + */ + EXCLUDE_FIXED = 0b0000_0001, + /** + * Exclude from the query any collider attached to a dynamic rigid-body. + */ + EXCLUDE_KINEMATIC = 0b0000_0010, + /** + * Exclude from the query any collider attached to a kinematic rigid-body. + */ + EXCLUDE_DYNAMIC = 0b0000_0100, + /** + * Exclude from the query any collider that is a sensor. + */ + EXCLUDE_SENSORS = 0b0000_1000, + /** + * Exclude from the query any collider that is not a sensor. + */ + EXCLUDE_SOLIDS = 0b0001_0000, + /** + * Excludes all colliders not attached to a dynamic rigid-body. + */ + ONLY_DYNAMIC = QueryFilterFlags.EXCLUDE_FIXED | + QueryFilterFlags.EXCLUDE_KINEMATIC, + /** + * Excludes all colliders not attached to a kinematic rigid-body. + */ + ONLY_KINEMATIC = QueryFilterFlags.EXCLUDE_DYNAMIC | + QueryFilterFlags.EXCLUDE_FIXED, + /** + * Exclude all colliders attached to a non-fixed rigid-body + * (this will not exclude colliders not attached to any rigid-body). + */ + ONLY_FIXED = QueryFilterFlags.EXCLUDE_DYNAMIC | + QueryFilterFlags.EXCLUDE_KINEMATIC, +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/serialization_pipeline.ts b/thirdparty/rapier.js/src.ts/pipeline/serialization_pipeline.ts new file mode 100644 index 00000000..87fde398 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/serialization_pipeline.ts @@ -0,0 +1,84 @@ +import {RawSerializationPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + IslandManager, + ImpulseJointSet, + MultibodyJointSet, + RigidBodySet, +} from "../dynamics"; +import {BroadPhase, ColliderSet, NarrowPhase} from "../geometry"; +import {World} from "./world"; + +/** + * A pipeline for serializing the physics scene. + * + * To avoid leaking WASM resources, this MUST be freed manually with `serializationPipeline.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class SerializationPipeline { + raw: RawSerializationPipeline; + + /** + * Release the WASM memory occupied by this serialization pipeline. + */ + free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawSerializationPipeline) { + this.raw = raw || new RawSerializationPipeline(); + } + + /** + * Serialize a complete physics state into a single byte array. + * @param gravity - The current gravity affecting the simulation. + * @param integrationParameters - The integration parameters of the simulation. + * @param broadPhase - The broad-phase of the simulation. + * @param narrowPhase - The narrow-phase of the simulation. + * @param bodies - The rigid-bodies taking part into the simulation. + * @param colliders - The colliders taking part into the simulation. + * @param impulseJoints - The impulse joints taking part into the simulation. + * @param multibodyJoints - The multibody joints taking part into the simulation. + */ + public serializeAll( + gravity: Vector, + integrationParameters: IntegrationParameters, + islands: IslandManager, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ): Uint8Array { + let rawGra = VectorOps.intoRaw(gravity); + + const res = this.raw.serializeAll( + rawGra, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ); + rawGra.free(); + + return res; + } + + /** + * Deserialize the complete physics state from a single byte array. + * + * @param data - The byte array to deserialize. + */ + public deserializeAll(data: Uint8Array): World { + return World.fromRaw(this.raw.deserializeAll(data)); + } +} diff --git a/thirdparty/rapier.js/src.ts/pipeline/world.ts b/thirdparty/rapier.js/src.ts/pipeline/world.ts new file mode 100644 index 00000000..9ee85616 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/pipeline/world.ts @@ -0,0 +1,1312 @@ +import { + RawBroadPhase, + RawCCDSolver, + RawColliderSet, + RawDeserializedWorld, + RawIntegrationParameters, + RawIslandManager, + RawImpulseJointSet, + RawMultibodyJointSet, + RawNarrowPhase, + RawPhysicsPipeline, + RawRigidBodySet, + RawSerializationPipeline, + RawDebugRenderPipeline, +} from "../raw"; + +import { + BroadPhase, + Collider, + ColliderDesc, + ColliderHandle, + ColliderSet, + InteractionGroups, + NarrowPhase, + PointColliderProjection, + Ray, + RayColliderIntersection, + RayColliderHit, + Shape, + ColliderShapeCastHit, + TempContactManifold, +} from "../geometry"; +import { + CCDSolver, + IntegrationParameters, + IslandManager, + ImpulseJoint, + ImpulseJointHandle, + MultibodyJoint, + MultibodyJointHandle, + JointData, + ImpulseJointSet, + MultibodyJointSet, + RigidBody, + RigidBodyDesc, + RigidBodyHandle, + RigidBodySet, +} from "../dynamics"; +import {Rotation, Vector, VectorOps} from "../math"; +import {PhysicsPipeline} from "./physics_pipeline"; +import {QueryFilterFlags} from "./query_pipeline"; +import {SerializationPipeline} from "./serialization_pipeline"; +import {EventQueue} from "./event_queue"; +import {PhysicsHooks} from "./physics_hooks"; +import {DebugRenderBuffers, DebugRenderPipeline} from "./debug_render_pipeline"; +import { + KinematicCharacterController, + PidAxesMask, + PidController, +} from "../control"; +import {Coarena} from "../coarena"; + +// #if DIM3 +import {DynamicRayCastVehicleController} from "../control"; + +// #endif + +/** + * The physics world. + * + * This contains all the data-structures necessary for creating and simulating + * bodies with contacts, joints, and external forces. + */ +export class World { + public gravity: Vector; + integrationParameters: IntegrationParameters; + islands: IslandManager; + broadPhase: BroadPhase; + narrowPhase: NarrowPhase; + bodies: RigidBodySet; + colliders: ColliderSet; + impulseJoints: ImpulseJointSet; + multibodyJoints: MultibodyJointSet; + ccdSolver: CCDSolver; + physicsPipeline: PhysicsPipeline; + serializationPipeline: SerializationPipeline; + debugRenderPipeline: DebugRenderPipeline; + characterControllers: Set; + pidControllers: Set; + + // #if DIM3 + vehicleControllers: Set; + + // #endif + + /** + * Release the WASM memory occupied by this physics world. + * + * All the fields of this physics world will be freed as well, + * so there is no need to call their `.free()` methods individually. + */ + public free() { + this.integrationParameters.free(); + this.islands.free(); + this.broadPhase.free(); + this.narrowPhase.free(); + this.bodies.free(); + this.colliders.free(); + this.impulseJoints.free(); + this.multibodyJoints.free(); + this.ccdSolver.free(); + this.physicsPipeline.free(); + this.serializationPipeline.free(); + this.debugRenderPipeline.free(); + this.characterControllers.forEach((controller) => controller.free()); + this.pidControllers.forEach((controller) => controller.free()); + + // #if DIM3 + this.vehicleControllers.forEach((controller) => controller.free()); + // #endif + + this.integrationParameters = undefined; + this.islands = undefined; + this.broadPhase = undefined; + this.narrowPhase = undefined; + this.bodies = undefined; + this.colliders = undefined; + this.ccdSolver = undefined; + this.impulseJoints = undefined; + this.multibodyJoints = undefined; + this.physicsPipeline = undefined; + this.serializationPipeline = undefined; + this.debugRenderPipeline = undefined; + this.characterControllers = undefined; + this.pidControllers = undefined; + + // #if DIM3 + this.vehicleControllers = undefined; + // #endif + } + + constructor( + gravity: Vector, + rawIntegrationParameters?: RawIntegrationParameters, + rawIslands?: RawIslandManager, + rawBroadPhase?: RawBroadPhase, + rawNarrowPhase?: RawNarrowPhase, + rawBodies?: RawRigidBodySet, + rawColliders?: RawColliderSet, + rawImpulseJoints?: RawImpulseJointSet, + rawMultibodyJoints?: RawMultibodyJointSet, + rawCCDSolver?: RawCCDSolver, + rawPhysicsPipeline?: RawPhysicsPipeline, + rawSerializationPipeline?: RawSerializationPipeline, + rawDebugRenderPipeline?: RawDebugRenderPipeline, + ) { + this.gravity = gravity; + this.integrationParameters = new IntegrationParameters( + rawIntegrationParameters, + ); + this.islands = new IslandManager(rawIslands); + this.broadPhase = new BroadPhase(rawBroadPhase); + this.narrowPhase = new NarrowPhase(rawNarrowPhase); + this.bodies = new RigidBodySet(rawBodies); + this.colliders = new ColliderSet(rawColliders); + this.impulseJoints = new ImpulseJointSet(rawImpulseJoints); + this.multibodyJoints = new MultibodyJointSet(rawMultibodyJoints); + this.ccdSolver = new CCDSolver(rawCCDSolver); + this.physicsPipeline = new PhysicsPipeline(rawPhysicsPipeline); + this.serializationPipeline = new SerializationPipeline( + rawSerializationPipeline, + ); + this.debugRenderPipeline = new DebugRenderPipeline( + rawDebugRenderPipeline, + ); + this.characterControllers = new Set(); + this.pidControllers = new Set(); + + // #if DIM3 + this.vehicleControllers = new Set(); + // #endif + + this.impulseJoints.finalizeDeserialization(this.bodies); + this.bodies.finalizeDeserialization(this.colliders); + this.colliders.finalizeDeserialization(this.bodies); + } + + public static fromRaw(raw: RawDeserializedWorld): World { + if (!raw) return null; + + return new World( + VectorOps.fromRaw(raw.takeGravity()), + raw.takeIntegrationParameters(), + raw.takeIslandManager(), + raw.takeBroadPhase(), + raw.takeNarrowPhase(), + raw.takeBodies(), + raw.takeColliders(), + raw.takeImpulseJoints(), + raw.takeMultibodyJoints(), + ); + } + + /** + * Takes a snapshot of this world. + * + * Use `World.restoreSnapshot` to create a new physics world with a state identical to + * the state when `.takeSnapshot()` is called. + */ + public takeSnapshot(): Uint8Array { + return this.serializationPipeline.serializeAll( + this.gravity, + this.integrationParameters, + this.islands, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + ); + } + + /** + * Creates a new physics world from a snapshot. + * + * This new physics world will be an identical copy of the snapshoted physics world. + */ + public static restoreSnapshot(data: Uint8Array): World { + let deser = new SerializationPipeline(); + return deser.deserializeAll(data); + } + + /** + * Computes all the lines (and their colors) needed to render the scene. + * + * @param filterFlags - Flags for excluding whole subsets of colliders from rendering. + * @param filterPredicate - Any collider for which this closure returns `false` will be excluded from the + * debug rendering. + */ + public debugRender( + filterFlags?: QueryFilterFlags, + filterPredicate?: (collider: Collider) => boolean, + ): DebugRenderBuffers { + this.debugRenderPipeline.render( + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + this.narrowPhase, + filterFlags, + filterPredicate, + ); + return new DebugRenderBuffers( + this.debugRenderPipeline.vertices, + this.debugRenderPipeline.colors, + ); + } + + /** + * Advance the simulation by one time step. + * + * All events generated by the physics engine are ignored. + * + * @param EventQueue - (optional) structure responsible for collecting + * events generated by the physics engine. + */ + public step(eventQueue?: EventQueue, hooks?: PhysicsHooks) { + this.physicsPipeline.step( + this.gravity, + this.integrationParameters, + this.islands, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + this.ccdSolver, + eventQueue, + hooks, + ); + } + + /** + * Update colliders positions after rigid-bodies moved. + * + * When a rigid-body moves, the positions of the colliders attached to it need to be updated. This update is + * generally automatically done at the beginning and the end of each simulation step with World.step. + * If the positions need to be updated without running a simulation step this method can be called manually. + */ + public propagateModifiedBodyPositionsToColliders() { + this.bodies.raw.propagateModifiedBodyPositionsToColliders( + this.colliders.raw, + ); + } + + // TODO: This needs to trigger a broad-phase update but without emitting collision events? + // /** + // * Ensure subsequent scene queries take into account the collider positions set before this method is called. + // * + // * This does not step the physics simulation forward. + // */ + // public updateSceneQueries() { + // this.propagateModifiedBodyPositionsToColliders(); + // this.queryPipeline.update(this.colliders); + // } + + /** + * The current simulation timestep. + */ + get timestep(): number { + return this.integrationParameters.dt; + } + + /** + * Sets the new simulation timestep. + * + * The simulation timestep governs by how much the physics state of the world will + * be integrated. A simulation timestep should: + * - be as small as possible. Typical values evolve around 0.016 (assuming the chosen unit is milliseconds, + * corresponds to the time between two frames of a game running at 60FPS). + * - not vary too much during the course of the simulation. A timestep with large variations may + * cause instabilities in the simulation. + * + * @param dt - The timestep length, in seconds. + */ + set timestep(dt: number) { + this.integrationParameters.dt = dt; + } + + /** + * The approximate size of most dynamic objects in the scene. + * + * See the documentation of the `World.lengthUnit` setter for further details. + */ + get lengthUnit(): number { + return this.integrationParameters.lengthUnit; + } + + /** + * The approximate size of most dynamic objects in the scene. + * + * This value is used internally to estimate some length-based tolerance. In particular, the + * values `IntegrationParameters.allowedLinearError`, + * `IntegrationParameters.maxPenetrationCorrection`, + * `IntegrationParameters.predictionDistance`, `RigidBodyActivation.linearThreshold` + * are scaled by this value implicitly. + * + * This value can be understood as the number of units-per-meter in your physical world compared + * to a human-sized world in meter. For example, in a 2d game, if your typical object size is 100 + * pixels, set the `[`Self::length_unit`]` parameter to 100.0. The physics engine will interpret + * it as if 100 pixels is equivalent to 1 meter in its various internal threshold. + * (default `1.0`). + */ + set lengthUnit(unitsPerMeter: number) { + this.integrationParameters.lengthUnit = unitsPerMeter; + } + + /** + * The number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + get numSolverIterations(): number { + return this.integrationParameters.numSolverIterations; + } + + /** + * Sets the number of solver iterations run by the constraints solver for calculating forces (default: `4`). + * + * The greater this value is, the most rigid and realistic the physics simulation will be. + * However a greater number of iterations is more computationally intensive. + * + * @param niter - The new number of solver iterations. + */ + set numSolverIterations(niter: number) { + this.integrationParameters.numSolverIterations = niter; + } + + /** + * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + get numInternalPgsIterations(): number { + return this.integrationParameters.numInternalPgsIterations; + } + + /** + * Sets the Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + * + * Increasing this parameter will improve stability of the simulation. It will have a lesser effect than + * increasing `numSolverIterations` but is also less computationally expensive. + * + * @param niter - The new number of internal PGS iterations. + */ + set numInternalPgsIterations(niter: number) { + this.integrationParameters.numInternalPgsIterations = niter; + } + + /** + * The number of substeps continuous collision-detection can run (default: `1`). + */ + get maxCcdSubsteps(): number { + return this.integrationParameters.maxCcdSubsteps; + } + + /** + * Sets the number of substeps continuous collision-detection can run (default: `1`). + * + * CCD operates using a "motion clamping" mechanism where all fast-moving object trajectories will + * be truncated to their first impact on their path. The number of CCD substeps beyond 1 indicate how + * many times that trajectory will be updated and continued after a hit. This can results in smoother + * paths, but at a significant computational cost. + * + * @param niter - The new maximum number of CCD substeps. Setting to `0` disables CCD entirely. + */ + set maxCcdSubsteps(substeps: number) { + this.integrationParameters.maxCcdSubsteps = substeps; + } + + /** + * Creates a new rigid-body from the given rigid-body descriptor. + * + * @param body - The description of the rigid-body to create. + */ + public createRigidBody(body: RigidBodyDesc): RigidBody { + return this.bodies.createRigidBody(this.colliders, body); + } + + /** + * Creates a new character controller. + * + * @param offset - The artificial gap added between the character’s chape and its environment. + */ + public createCharacterController( + offset: number, + ): KinematicCharacterController { + let controller = new KinematicCharacterController( + offset, + this.integrationParameters, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + ); + this.characterControllers.add(controller); + return controller; + } + + /** + * Removes a character controller from this world. + * + * @param controller - The character controller to remove. + */ + public removeCharacterController(controller: KinematicCharacterController) { + this.characterControllers.delete(controller); + controller.free(); + } + + /** + * Creates a new PID (Proportional-Integral-Derivative) controller. + * + * @param kp - The Proportional gain applied to the instantaneous linear position errors. + * This is usually set to a multiple of the inverse of simulation step time + * (e.g. `60` if the delta-time is `1.0 / 60.0`). + * @param ki - The linear gain applied to the Integral part of the PID controller. + * @param kd - The Derivative gain applied to the instantaneous linear velocity errors. + * This is usually set to a value in `[0.0, 1.0]` where `0.0` implies no damping + * (no correction of velocity errors) and `1.0` implies complete damping (velocity errors + * are corrected in a single simulation step). + * @param axes - The axes affected by this controller. + * Only coordinate axes with a bit flags set to `true` will be taken into + * account when calculating the errors and corrections. + */ + public createPidController( + kp: number, + ki: number, + kd: number, + axes: PidAxesMask, + ): PidController { + let controller = new PidController( + this.integrationParameters, + this.bodies, + kp, + ki, + kd, + axes, + ); + this.pidControllers.add(controller); + return controller; + } + + /** + * Removes a PID controller from this world. + * + * @param controller - The PID controller to remove. + */ + public removePidController(controller: PidController) { + this.pidControllers.delete(controller); + controller.free(); + } + + // #if DIM3 + /** + * Creates a new vehicle controller. + * + * @param chassis - The rigid-body used as the chassis of the vehicle controller. When the vehicle + * controller is updated, it will change directly the rigid-body’s velocity. This + * rigid-body must be a dynamic or kinematic-velocity-based rigid-body. + */ + public createVehicleController( + chassis: RigidBody, + ): DynamicRayCastVehicleController { + let controller = new DynamicRayCastVehicleController( + chassis, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + ); + this.vehicleControllers.add(controller); + return controller; + } + + /** + * Removes a vehicle controller from this world. + * + * @param controller - The vehicle controller to remove. + */ + public removeVehicleController( + controller: DynamicRayCastVehicleController, + ) { + this.vehicleControllers.delete(controller); + controller.free(); + } + + // #endif + + /** + * Creates a new collider. + * + * @param desc - The description of the collider. + * @param parent - The rigid-body this collider is attached to. + */ + public createCollider(desc: ColliderDesc, parent?: RigidBody): Collider { + let parentHandle = parent ? parent.handle : undefined; + return this.colliders.createCollider(this.bodies, desc, parentHandle); + } + + /** + * Creates a new impulse joint from the given joint descriptor. + * + * @param params - The description of the joint to create. + * @param parent1 - The first rigid-body attached to this joint. + * @param parent2 - The second rigid-body attached to this joint. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createImpulseJoint( + params: JointData, + parent1: RigidBody, + parent2: RigidBody, + wakeUp: boolean, + ): ImpulseJoint { + return this.impulseJoints.createJoint( + this.bodies, + params, + parent1.handle, + parent2.handle, + wakeUp, + ); + } + + /** + * Creates a new multibody joint from the given joint descriptor. + * + * @param params - The description of the joint to create. + * @param parent1 - The first rigid-body attached to this joint. + * @param parent2 - The second rigid-body attached to this joint. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createMultibodyJoint( + params: JointData, + parent1: RigidBody, + parent2: RigidBody, + wakeUp: boolean, + ): MultibodyJoint { + return this.multibodyJoints.createJoint( + params, + parent1.handle, + parent2.handle, + wakeUp, + ); + } + + /** + * Retrieves a rigid-body from its handle. + * + * @param handle - The integer handle of the rigid-body to retrieve. + */ + public getRigidBody(handle: RigidBodyHandle): RigidBody { + return this.bodies.get(handle); + } + + /** + * Retrieves a collider from its handle. + * + * @param handle - The integer handle of the collider to retrieve. + */ + public getCollider(handle: ColliderHandle): Collider { + return this.colliders.get(handle); + } + + /** + * Retrieves an impulse joint from its handle. + * + * @param handle - The integer handle of the impulse joint to retrieve. + */ + public getImpulseJoint(handle: ImpulseJointHandle): ImpulseJoint { + return this.impulseJoints.get(handle); + } + + /** + * Retrieves an multibody joint from its handle. + * + * @param handle - The integer handle of the multibody joint to retrieve. + */ + public getMultibodyJoint(handle: MultibodyJointHandle): MultibodyJoint { + return this.multibodyJoints.get(handle); + } + + /** + * Removes the given rigid-body from this physics world. + * + * This will remove this rigid-body as well as all its attached colliders and joints. + * Every other bodies touching or attached by joints to this rigid-body will be woken-up. + * + * @param body - The rigid-body to remove. + */ + public removeRigidBody(body: RigidBody) { + if (this.bodies) { + this.bodies.remove( + body.handle, + this.islands, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + ); + } + } + + /** + * Removes the given collider from this physics world. + * + * @param collider - The collider to remove. + * @param wakeUp - If set to `true`, the rigid-body this collider is attached to will be awaken. + */ + public removeCollider(collider: Collider, wakeUp: boolean) { + if (this.colliders) { + this.colliders.remove( + collider.handle, + this.islands, + this.bodies, + wakeUp, + ); + } + } + + /** + * Removes the given impulse joint from this physics world. + * + * @param joint - The impulse joint to remove. + * @param wakeUp - If set to `true`, the rigid-bodies attached by this joint will be awaken. + */ + public removeImpulseJoint(joint: ImpulseJoint, wakeUp: boolean) { + if (this.impulseJoints) { + this.impulseJoints.remove(joint.handle, wakeUp); + } + } + + /** + * Removes the given multibody joint from this physics world. + * + * @param joint - The multibody joint to remove. + * @param wakeUp - If set to `true`, the rigid-bodies attached by this joint will be awaken. + */ + public removeMultibodyJoint(joint: MultibodyJoint, wakeUp: boolean) { + if (this.impulseJoints) { + this.multibodyJoints.remove(joint.handle, wakeUp); + } + } + + /** + * Applies the given closure to each collider managed by this physics world. + * + * @param f(collider) - The function to apply to each collider managed by this physics world. Called as `f(collider)`. + */ + public forEachCollider(f: (collider: Collider) => void) { + this.colliders.forEach(f); + } + + /** + * Applies the given closure to each rigid-body managed by this physics world. + * + * @param f(body) - The function to apply to each rigid-body managed by this physics world. Called as `f(collider)`. + */ + public forEachRigidBody(f: (body: RigidBody) => void) { + this.bodies.forEach(f); + } + + /** + * Applies the given closure to each active rigid-body managed by this physics world. + * + * After a short time of inactivity, a rigid-body is automatically deactivated ("asleep") by + * the physics engine in order to save computational power. A sleeping rigid-body never moves + * unless it is moved manually by the user. + * + * @param f - The function to apply to each active rigid-body managed by this physics world. Called as `f(collider)`. + */ + public forEachActiveRigidBody(f: (body: RigidBody) => void) { + this.bodies.forEachActiveRigidBody(this.islands, f); + } + + /** + * Find the closest intersection between a ray and the physics world. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param filter - The callback to filter out which collider will be hit. + */ + public castRay( + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): RayColliderHit | null { + return this.broadPhase.castRay( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find the closest intersection between a ray and the physics world. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + */ + public castRayAndGetNormal( + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): RayColliderIntersection | null { + return this.broadPhase.castRayAndGetNormal( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Cast a ray and collects all the intersections between a ray and the scene. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param callback - The callback called once per hit (in no particular order) between a ray and a collider. + * If this callback returns `false`, then the cast will stop and no further hits will be detected/reported. + */ + public intersectionsWithRay( + ray: Ray, + maxToi: number, + solid: boolean, + callback: (intersect: RayColliderIntersection) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithRay( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + callback, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Gets the handle of up to one collider intersecting the given shape. + * + * @param shapePos - The position of the shape used for the intersection test. + * @param shapeRot - The orientation of the shape used for the intersection test. + * @param shape - The shape used for the intersection test. + * @param groups - The bit groups and filter associated to the ray, in order to only + * hit the colliders with collision groups compatible with the ray's group. + */ + public intersectionWithShape( + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): Collider | null { + let handle = this.broadPhase.intersectionWithShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shape, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + return handle != null ? this.colliders.get(handle) : null; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPoint( + point: Vector, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): PointColliderProjection | null { + return this.broadPhase.projectPoint( + this.narrowPhase, + this.bodies, + this.colliders, + point, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find the projection of a point on the closest collider. + * + * @param point - The point to project. + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPointAndGetFeature( + point: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): PointColliderProjection | null { + return this.broadPhase.projectPointAndGetFeature( + this.narrowPhase, + this.bodies, + this.colliders, + point, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find all the colliders containing the given point. + * + * @param point - The point used for the containment test. + * @param groups - The bit groups and filter associated to the point to test, in order to only + * test on colliders with collision groups compatible with the ray's group. + * @param callback - A function called with the handles of each collider with a shape + * containing the `point`. + */ + public intersectionsWithPoint( + point: Vector, + callback: (handle: Collider) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithPoint( + this.narrowPhase, + this.bodies, + this.colliders, + point, + this.colliders.castClosure(callback), + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Casts a shape at a constant linear velocity and retrieve the first collider it hits. + * This is similar to ray-casting except that we are casting a whole shape instead of + * just a point (the ray origin). + * + * @param shapePos - The initial position of the shape to cast. + * @param shapeRot - The initial rotation of the shape to cast. + * @param shapeVel - The constant velocity of the shape to cast (i.e. the cast direction). + * @param shape - The shape to cast. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @param groups - The bit groups and filter associated to the shape to cast, in order to only + * test on colliders with collision groups compatible with this group. + */ + public castShape( + shapePos: Vector, + shapeRot: Rotation, + shapeVel: Vector, + shape: Shape, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): ColliderShapeCastHit | null { + return this.broadPhase.castShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shapeVel, + shape, + targetDistance, + maxToi, + stopAtPenetration, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Retrieve all the colliders intersecting the given shape. + * + * @param shapePos - The position of the shape to test. + * @param shapeRot - The orientation of the shape to test. + * @param shape - The shape to test. + * @param groups - The bit groups and filter associated to the shape to test, in order to only + * test on colliders with collision groups compatible with this group. + * @param callback - A function called with the handles of each collider intersecting the `shape`. + */ + public intersectionsWithShape( + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + callback: (collider: Collider) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shape, + this.colliders.castClosure(callback), + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Finds the handles of all the colliders with an AABB intersecting the given AABB. + * + * @param aabbCenter - The center of the AABB to test. + * @param aabbHalfExtents - The half-extents of the AABB to test. + * @param callback - The callback that will be called with the handles of all the colliders + * currently intersecting the given AABB. + */ + public collidersWithAabbIntersectingAabb( + aabbCenter: Vector, + aabbHalfExtents: Vector, + callback: (handle: Collider) => boolean, + ) { + this.broadPhase.collidersWithAabbIntersectingAabb( + this.narrowPhase, + this.bodies, + this.colliders, + aabbCenter, + aabbHalfExtents, + this.colliders.castClosure(callback), + ); + } + + /** + * Enumerates all the colliders potentially in contact with the given collider. + * + * @param collider1 - The second collider involved in the contact. + * @param f - Closure that will be called on each collider that is in contact with `collider1`. + */ + public contactPairsWith( + collider1: Collider, + f: (collider2: Collider) => void, + ) { + this.narrowPhase.contactPairsWith( + collider1.handle, + this.colliders.castClosure(f), + ); + } + + /** + * Enumerates all the colliders intersecting the given colliders, assuming one of them + * is a sensor. + */ + public intersectionPairsWith( + collider1: Collider, + f: (collider2: Collider) => void, + ) { + this.narrowPhase.intersectionPairsWith( + collider1.handle, + this.colliders.castClosure(f), + ); + } + + /** + * Iterates through all the contact manifolds between the given pair of colliders. + * + * @param collider1 - The first collider involved in the contact. + * @param collider2 - The second collider involved in the contact. + * @param f - Closure that will be called on each contact manifold between the two colliders. If the second argument + * passed to this closure is `true`, then the contact manifold data is flipped, i.e., methods like `localNormal1` + * actually apply to the `collider2` and fields like `localNormal2` apply to the `collider1`. + */ + public contactPair( + collider1: Collider, + collider2: Collider, + f: (manifold: TempContactManifold, flipped: boolean) => void, + ) { + this.narrowPhase.contactPair(collider1.handle, collider2.handle, f); + } + + /** + * Returns `true` if `collider1` and `collider2` intersect and at least one of them is a sensor. + * @param collider1 − The first collider involved in the intersection. + * @param collider2 − The second collider involved in the intersection. + */ + public intersectionPair(collider1: Collider, collider2: Collider): boolean { + return this.narrowPhase.intersectionPair( + collider1.handle, + collider2.handle, + ); + } + + /** + * Sets whether internal performance profiling is enabled (default: false). + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + set profilerEnabled(enabled: boolean) { + this.physicsPipeline.raw.set_profiler_enabled(enabled); + } + + /** + * Indicates if the internal performance profiling is enabled. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + get profilerEnabled(): boolean { + return this.physicsPipeline.raw.is_profiler_enabled(); + } + + /** + * The time spent in milliseconds by the last step to run the entire simulation step. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingStep(): number { + return this.physicsPipeline.raw.timing_step(); + } + + /** + * The time spent in milliseconds by the last step to run the collision-detection + * (broad-phase + narrow-phase). + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCollisionDetection(): number { + return this.physicsPipeline.raw.timing_collision_detection(); + } + + /** + * The time spent in milliseconds by the last step to run the broad-phase. + * + * This timing is included in `timingCollisionDetection`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingBroadPhase(): number { + return this.physicsPipeline.raw.timing_broad_phase(); + } + + /** + * The time spent in milliseconds by the last step to run the narrow-phase. + * + * This timing is included in `timingCollisionDetection`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingNarrowPhase(): number { + return this.physicsPipeline.raw.timing_narrow_phase(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint solver. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingSolver(): number { + return this.physicsPipeline.raw.timing_solver(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint + * initialization. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityAssembly(): number { + return this.physicsPipeline.raw.timing_velocity_assembly(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint + * resolution. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityResolution(): number { + return this.physicsPipeline.raw.timing_velocity_resolution(); + } + + /** + * The time spent in milliseconds by the last step to run the rigid-body + * velocity update. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityUpdate(): number { + return this.physicsPipeline.raw.timing_velocity_update(); + } + + /** + * The time spent in milliseconds by writing rigid-body velocities + * calculated by the solver back into the rigid-bodies. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityWriteback(): number { + return this.physicsPipeline.raw.timing_velocity_writeback(); + } + + /** + * The total time spent in CCD detection and resolution. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcd(): number { + return this.physicsPipeline.raw.timing_ccd(); + } + + /** + * The total time spent searching for the continuous hits during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdToiComputation(): number { + return this.physicsPipeline.raw.timing_ccd_toi_computation(); + } + + /** + * The total time spent in the broad-phase during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdBroadPhase(): number { + return this.physicsPipeline.raw.timing_ccd_broad_phase(); + } + + /** + * The total time spent in the narrow-phase during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdNarrowPhase(): number { + return this.physicsPipeline.raw.timing_ccd_narrow_phase(); + } + + /** + * The total time spent in the constraints resolution during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdSolver(): number { + return this.physicsPipeline.raw.timing_ccd_solver(); + } + + /** + * The total time spent in the islands calculation during CCD. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingIslandConstruction(): number { + return this.physicsPipeline.raw.timing_island_construction(); + } + + /** + * The total time spent propagating detected user changes. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingUserChanges(): number { + return this.physicsPipeline.raw.timing_user_changes(); + } +} diff --git a/thirdparty/rapier.js/src.ts/rapier.ts b/thirdparty/rapier.js/src.ts/rapier.ts new file mode 100644 index 00000000..dbf4d053 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/rapier.ts @@ -0,0 +1,3 @@ +import * as RAPIER from "./exports"; +export * from "./exports"; +export default RAPIER; diff --git a/thirdparty/rapier.js/src.ts/raw.ts b/thirdparty/rapier.js/src.ts/raw.ts new file mode 100644 index 00000000..51f837b7 --- /dev/null +++ b/thirdparty/rapier.js/src.ts/raw.ts @@ -0,0 +1 @@ +export * from "../rapier3d/pkg/rapier_wasm3d"; diff --git a/thirdparty/rapier.js/src/control/character_controller.rs b/thirdparty/rapier.js/src/control/character_controller.rs new file mode 100644 index 00000000..03cbdc5d --- /dev/null +++ b/thirdparty/rapier.js/src/control/character_controller.rs @@ -0,0 +1,290 @@ +use crate::dynamics::RawRigidBodySet; +use crate::geometry::{RawBroadPhase, RawColliderSet, RawNarrowPhase}; +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use na::{Isometry, Unit}; +use rapier::control::{ + CharacterAutostep, CharacterCollision, CharacterLength, EffectiveCharacterMovement, + KinematicCharacterController, +}; +use rapier::geometry::{ColliderHandle, ShapeCastHit}; +use rapier::math::{Point, Real, Vector}; +use rapier::parry::query::ShapeCastStatus; +use rapier::pipeline::{QueryFilter, QueryFilterFlags}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawKinematicCharacterController { + controller: KinematicCharacterController, + result: EffectiveCharacterMovement, + events: Vec, +} + +fn length_value(length: CharacterLength) -> Real { + match length { + CharacterLength::Absolute(val) => val, + CharacterLength::Relative(val) => val, + } +} +#[wasm_bindgen] +impl RawKinematicCharacterController { + #[wasm_bindgen(constructor)] + pub fn new(offset: Real) -> Self { + let controller = KinematicCharacterController { + offset: CharacterLength::Absolute(offset), + autostep: None, + snap_to_ground: None, + ..KinematicCharacterController::default() + }; + + Self { + controller, + result: EffectiveCharacterMovement { + translation: Vector::zeros(), + grounded: false, + is_sliding_down_slope: false, + }, + events: vec![], + } + } + + pub fn up(&self) -> RawVector { + self.controller.up.into_inner().into() + } + + pub fn setUp(&mut self, vector: &RawVector) { + self.controller.up = Unit::new_normalize(vector.0); + } + + pub fn normalNudgeFactor(&self) -> Real { + self.controller.normal_nudge_factor + } + + pub fn setNormalNudgeFactor(&mut self, value: Real) { + self.controller.normal_nudge_factor = value; + } + + pub fn offset(&self) -> Real { + length_value(self.controller.offset) + } + + pub fn setOffset(&mut self, value: Real) { + self.controller.offset = CharacterLength::Absolute(value); + } + + pub fn slideEnabled(&self) -> bool { + self.controller.slide + } + + pub fn setSlideEnabled(&mut self, enabled: bool) { + self.controller.slide = enabled + } + + pub fn autostepMaxHeight(&self) -> Option { + self.controller.autostep.map(|e| length_value(e.max_height)) + } + + pub fn autostepMinWidth(&self) -> Option { + self.controller.autostep.map(|e| length_value(e.min_width)) + } + + pub fn autostepIncludesDynamicBodies(&self) -> Option { + self.controller.autostep.map(|e| e.include_dynamic_bodies) + } + + pub fn autostepEnabled(&self) -> bool { + self.controller.autostep.is_some() + } + + pub fn enableAutostep(&mut self, maxHeight: Real, minWidth: Real, includeDynamicBodies: bool) { + self.controller.autostep = Some(CharacterAutostep { + min_width: CharacterLength::Absolute(minWidth), + max_height: CharacterLength::Absolute(maxHeight), + include_dynamic_bodies: includeDynamicBodies, + }) + } + + pub fn disableAutostep(&mut self) { + self.controller.autostep = None; + } + + pub fn maxSlopeClimbAngle(&self) -> Real { + self.controller.max_slope_climb_angle + } + + pub fn setMaxSlopeClimbAngle(&mut self, angle: Real) { + self.controller.max_slope_climb_angle = angle; + } + + pub fn minSlopeSlideAngle(&self) -> Real { + self.controller.min_slope_slide_angle + } + + pub fn setMinSlopeSlideAngle(&mut self, angle: Real) { + self.controller.min_slope_slide_angle = angle + } + + pub fn snapToGroundDistance(&self) -> Option { + self.controller.snap_to_ground.map(length_value) + } + + pub fn enableSnapToGround(&mut self, distance: Real) { + self.controller.snap_to_ground = Some(CharacterLength::Absolute(distance)); + } + + pub fn disableSnapToGround(&mut self) { + self.controller.snap_to_ground = None; + } + + pub fn snapToGroundEnabled(&self) -> bool { + self.controller.snap_to_ground.is_some() + } + + pub fn computeColliderMovement( + &mut self, + dt: Real, + broad_phase: &RawBroadPhase, + narrow_phase: &RawNarrowPhase, + bodies: &mut RawRigidBodySet, + colliders: &mut RawColliderSet, + collider_handle: FlatHandle, + desired_translation_delta: &RawVector, + apply_impulses_to_dynamic_bodies: bool, + character_mass: Option, + filter_flags: u32, + filter_groups: Option, + filter_predicate: &js_sys::Function, + ) { + let handle = crate::utils::collider_handle(collider_handle); + if let Some(collider) = colliders.0.get(handle) { + let collider_pose = *collider.position(); + let collider_shape = collider.shared_shape().clone(); + let collider_parent = collider.parent(); + + crate::utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: Some(handle), + exclude_rigid_body: collider_parent, + predicate, + }; + + let character_mass = character_mass + .or_else(|| { + collider_parent + .and_then(|h| bodies.0.get(h)) + .map(|b| b.mass()) + }) + .unwrap_or(0.0); + + let mut query_pipeline = broad_phase.0.as_query_pipeline_mut( + narrow_phase.0.query_dispatcher(), + &mut bodies.0, + &mut colliders.0, + query_filter, + ); + + self.events.clear(); + let events = &mut self.events; + self.result = self.controller.move_shape( + dt, + &query_pipeline.as_ref(), + &*collider_shape, + &collider_pose, + desired_translation_delta.0, + |event| events.push(event), + ); + + if apply_impulses_to_dynamic_bodies { + self.controller.solve_character_collision_impulses( + dt, + &mut query_pipeline, + &*collider_shape, + character_mass, + self.events.iter(), + ); + } + }); + } else { + self.result.translation.fill(0.0); + } + } + + pub fn computedMovement(&self) -> RawVector { + self.result.translation.into() + } + + pub fn computedGrounded(&self) -> bool { + self.result.grounded + } + + pub fn numComputedCollisions(&self) -> usize { + self.events.len() + } + + pub fn computedCollision(&self, i: usize, collision: &mut RawCharacterCollision) -> bool { + if let Some(coll) = self.events.get(i) { + collision.0 = *coll; + } + + i < self.events.len() + } +} + +#[wasm_bindgen] +pub struct RawCharacterCollision(CharacterCollision); + +#[wasm_bindgen] +impl RawCharacterCollision { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(CharacterCollision { + handle: ColliderHandle::invalid(), + character_pos: Isometry::identity(), + translation_applied: Vector::zeros(), + translation_remaining: Vector::zeros(), + hit: ShapeCastHit { + time_of_impact: 0.0, + witness1: Point::origin(), + witness2: Point::origin(), + normal1: Vector::y_axis(), + normal2: Vector::y_axis(), + status: ShapeCastStatus::Failed, + }, + }) + } + + pub fn handle(&self) -> FlatHandle { + utils::flat_handle(self.0.handle.0) + } + + pub fn translationDeltaApplied(&self) -> RawVector { + self.0.translation_applied.into() + } + + pub fn translationDeltaRemaining(&self) -> RawVector { + self.0.translation_remaining.into() + } + + pub fn toi(&self) -> Real { + self.0.hit.time_of_impact + } + + pub fn worldWitness1(&self) -> RawVector { + self.0.hit.witness1.coords.into() // Already in world-space. + } + + pub fn worldWitness2(&self) -> RawVector { + (self.0.character_pos * self.0.hit.witness2).coords.into() + } + + pub fn worldNormal1(&self) -> RawVector { + self.0.hit.normal1.into_inner().into() // Already in world-space. + } + + pub fn worldNormal2(&self) -> RawVector { + (self.0.character_pos * self.0.hit.normal2.into_inner()).into() + } +} diff --git a/thirdparty/rapier.js/src/control/mod.rs b/thirdparty/rapier.js/src/control/mod.rs new file mode 100644 index 00000000..74352c2b --- /dev/null +++ b/thirdparty/rapier.js/src/control/mod.rs @@ -0,0 +1,11 @@ +pub use self::character_controller::RawKinematicCharacterController; +pub use self::pid_controller::RawPidController; + +#[cfg(feature = "dim3")] +pub use self::ray_cast_vehicle_controller::RawDynamicRayCastVehicleController; + +mod character_controller; +mod pid_controller; + +#[cfg(feature = "dim3")] +mod ray_cast_vehicle_controller; diff --git a/thirdparty/rapier.js/src/control/pid_controller.rs b/thirdparty/rapier.js/src/control/pid_controller.rs new file mode 100644 index 00000000..890e3391 --- /dev/null +++ b/thirdparty/rapier.js/src/control/pid_controller.rs @@ -0,0 +1,264 @@ +use crate::dynamics::RawRigidBodySet; +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::control::PidController; +use rapier::dynamics::AxesMask; +use rapier::math::Vector; +use wasm_bindgen::prelude::*; + +#[cfg(feature = "dim3")] +use crate::math::RawRotation; +#[cfg(feature = "dim2")] +use rapier::math::Rotation; + +#[wasm_bindgen] +pub struct RawPidController { + controller: PidController, +} + +#[wasm_bindgen] +impl RawPidController { + #[wasm_bindgen(constructor)] + pub fn new(kp: f32, ki: f32, kd: f32, axes_mask: u8) -> Self { + let controller = PidController::new( + kp, + ki, + kd, + AxesMask::from_bits(axes_mask).unwrap_or(AxesMask::all()), + ); + Self { controller } + } + + pub fn set_kp(&mut self, kp: f32, axes: u8) { + let axes = AxesMask::from_bits(axes).unwrap_or(AxesMask::all()); + if axes.contains(AxesMask::LIN_X) { + self.controller.pd.lin_kp.x = kp; + } + if axes.contains(AxesMask::LIN_Y) { + self.controller.pd.lin_kp.y = kp; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::LIN_Z) { + self.controller.pd.lin_kp.z = kp; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_X) { + self.controller.pd.ang_kp.x = kp; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_Y) { + self.controller.pd.ang_kp.y = kp; + } + if axes.contains(AxesMask::ANG_Z) { + #[cfg(feature = "dim2")] + { + self.controller.pd.ang_kp = kp; + } + #[cfg(feature = "dim3")] + { + self.controller.pd.ang_kp.z = kp; + } + } + } + + pub fn set_ki(&mut self, ki: f32, axes: u8) { + let axes = AxesMask::from_bits(axes).unwrap_or(AxesMask::all()); + if axes.contains(AxesMask::LIN_X) { + self.controller.lin_ki.x = ki; + } + if axes.contains(AxesMask::LIN_Y) { + self.controller.lin_ki.y = ki; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::LIN_Z) { + self.controller.lin_ki.z = ki; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_X) { + self.controller.ang_ki.x = ki; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_Y) { + self.controller.ang_ki.y = ki; + } + if axes.contains(AxesMask::ANG_Z) { + #[cfg(feature = "dim2")] + { + self.controller.ang_ki = ki; + } + #[cfg(feature = "dim3")] + { + self.controller.ang_ki.z = ki; + } + } + } + + pub fn set_kd(&mut self, kd: f32, axes: u8) { + let axes = AxesMask::from_bits(axes).unwrap_or(AxesMask::all()); + if axes.contains(AxesMask::LIN_X) { + self.controller.pd.lin_kd.x = kd; + } + if axes.contains(AxesMask::LIN_Y) { + self.controller.pd.lin_kd.x = kd; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::LIN_Z) { + self.controller.pd.lin_kd.x = kd; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_X) { + self.controller.pd.ang_kd.x = kd; + } + #[cfg(feature = "dim3")] + if axes.contains(AxesMask::ANG_Y) { + self.controller.pd.ang_kd.y = kd; + } + if axes.contains(AxesMask::ANG_Z) { + #[cfg(feature = "dim2")] + { + self.controller.pd.ang_kd = kd; + } + #[cfg(feature = "dim3")] + { + self.controller.pd.ang_kd.z = kd; + } + } + } + + pub fn set_axes_mask(&mut self, axes_mask: u8) { + if let Some(mask) = AxesMask::from_bits(axes_mask) { + self.controller.pd.axes = mask; + } + } + + pub fn reset_integrals(&mut self) { + self.controller.reset_integrals(); + } + + pub fn apply_linear_correction( + &mut self, + dt: f32, + bodies: &mut RawRigidBodySet, + rb_handle: FlatHandle, + target_translation: &RawVector, + target_linvel: &RawVector, + ) { + let rb_handle = utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get_mut(rb_handle) else { + return; + }; + + let correction = self.controller.linear_rigid_body_correction( + dt, + rb, + target_translation.0.into(), + target_linvel.0, + ); + rb.set_linvel(*rb.linvel() + correction, true); + } + + #[cfg(feature = "dim2")] + pub fn apply_angular_correction( + &mut self, + dt: f32, + bodies: &mut RawRigidBodySet, + rb_handle: FlatHandle, + target_rotation: f32, + target_angvel: f32, + ) { + let rb_handle = crate::utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get_mut(rb_handle) else { + return; + }; + + let correction = self.controller.angular_rigid_body_correction( + dt, + rb, + Rotation::new(target_rotation), + target_angvel, + ); + rb.set_angvel(rb.angvel() + correction, true); + } + + #[cfg(feature = "dim3")] + pub fn apply_angular_correction( + &mut self, + dt: f32, + bodies: &mut RawRigidBodySet, + rb_handle: FlatHandle, + target_rotation: &RawRotation, + target_angvel: &RawVector, + ) { + let rb_handle = crate::utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get_mut(rb_handle) else { + return; + }; + + let correction = self.controller.angular_rigid_body_correction( + dt, + rb, + target_rotation.0, + target_angvel.0, + ); + rb.set_angvel(rb.angvel() + correction, true); + } + + pub fn linear_correction( + &mut self, + dt: f32, + bodies: &RawRigidBodySet, + rb_handle: FlatHandle, + target_translation: &RawVector, + target_linvel: &RawVector, + ) -> RawVector { + let rb_handle = crate::utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get(rb_handle) else { + return RawVector(Vector::zeros()); + }; + + self.controller + .linear_rigid_body_correction(dt, rb, target_translation.0.into(), target_linvel.0) + .into() + } + + #[cfg(feature = "dim2")] + pub fn angular_correction( + &mut self, + dt: f32, + bodies: &RawRigidBodySet, + rb_handle: FlatHandle, + target_rotation: f32, + target_angvel: f32, + ) -> f32 { + let rb_handle = crate::utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get(rb_handle) else { + return 0.0; + }; + + self.controller.angular_rigid_body_correction( + dt, + rb, + Rotation::new(target_rotation), + target_angvel, + ) + } + + #[cfg(feature = "dim3")] + pub fn angular_correction( + &mut self, + dt: f32, + bodies: &RawRigidBodySet, + rb_handle: FlatHandle, + target_rotation: &RawRotation, + target_angvel: &RawVector, + ) -> RawVector { + let rb_handle = crate::utils::body_handle(rb_handle); + let Some(rb) = bodies.0.get(rb_handle) else { + return RawVector(Vector::zeros()); + }; + + self.controller + .angular_rigid_body_correction(dt, rb, target_rotation.0, target_angvel.0) + .into() + } +} diff --git a/thirdparty/rapier.js/src/control/ray_cast_vehicle_controller.rs b/thirdparty/rapier.js/src/control/ray_cast_vehicle_controller.rs new file mode 100644 index 00000000..36bd7f61 --- /dev/null +++ b/thirdparty/rapier.js/src/control/ray_cast_vehicle_controller.rs @@ -0,0 +1,336 @@ +use crate::dynamics::RawRigidBodySet; +use crate::geometry::{RawBroadPhase, RawColliderSet, RawNarrowPhase}; +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::control::{DynamicRayCastVehicleController, WheelTuning}; +use rapier::math::Real; +use rapier::pipeline::{QueryFilter, QueryFilterFlags}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawDynamicRayCastVehicleController { + controller: DynamicRayCastVehicleController, +} + +#[wasm_bindgen] +impl RawDynamicRayCastVehicleController { + #[wasm_bindgen(constructor)] + pub fn new(chassis: FlatHandle) -> Self { + Self { + controller: DynamicRayCastVehicleController::new(utils::body_handle(chassis)), + } + } + + pub fn current_vehicle_speed(&self) -> Real { + self.controller.current_vehicle_speed + } + + pub fn chassis(&self) -> FlatHandle { + utils::flat_handle(self.controller.chassis.0) + } + + pub fn index_up_axis(&self) -> usize { + self.controller.index_up_axis + } + pub fn set_index_up_axis(&mut self, axis: usize) { + self.controller.index_up_axis = axis; + } + + pub fn index_forward_axis(&self) -> usize { + self.controller.index_forward_axis + } + pub fn set_index_forward_axis(&mut self, axis: usize) { + self.controller.index_forward_axis = axis; + } + + pub fn add_wheel( + &mut self, + chassis_connection_cs: &RawVector, + direction_cs: &RawVector, + axle_cs: &RawVector, + suspension_rest_length: Real, + radius: Real, + ) { + self.controller.add_wheel( + chassis_connection_cs.0.into(), + direction_cs.0, + axle_cs.0, + suspension_rest_length, + radius, + &WheelTuning::default(), + ); + } + + pub fn num_wheels(&self) -> usize { + self.controller.wheels().len() + } + + pub fn update_vehicle( + &mut self, + dt: Real, + broad_phase: &RawBroadPhase, + narrow_phase: &RawNarrowPhase, + bodies: &mut RawRigidBodySet, + colliders: &mut RawColliderSet, + filter_flags: u32, + filter_groups: Option, + filter_predicate: &js_sys::Function, + ) { + crate::utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + predicate, + exclude_rigid_body: Some(self.controller.chassis), + exclude_collider: None, + }; + + let query_pipeline = broad_phase.0.as_query_pipeline_mut( + narrow_phase.0.query_dispatcher(), + &mut bodies.0, + &mut colliders.0, + query_filter, + ); + + self.controller.update_vehicle(dt, query_pipeline); + }); + } + + /* + * + * Access to wheel properties. + * + */ + /* + * Getters + setters + */ + pub fn wheel_chassis_connection_point_cs(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.chassis_connection_point_cs.into()) + } + pub fn set_wheel_chassis_connection_point_cs(&mut self, i: usize, value: &RawVector) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.chassis_connection_point_cs = value.0.into(); + } + } + + pub fn wheel_suspension_rest_length(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.suspension_rest_length) + } + pub fn set_wheel_suspension_rest_length(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.suspension_rest_length = value; + } + } + + pub fn wheel_max_suspension_travel(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.max_suspension_travel) + } + pub fn set_wheel_max_suspension_travel(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.max_suspension_travel = value; + } + } + + pub fn wheel_radius(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.radius) + } + pub fn set_wheel_radius(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.radius = value; + } + } + + pub fn wheel_suspension_stiffness(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.suspension_stiffness) + } + pub fn set_wheel_suspension_stiffness(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.suspension_stiffness = value; + } + } + + pub fn wheel_suspension_compression(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.damping_compression) + } + pub fn set_wheel_suspension_compression(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.damping_compression = value; + } + } + + pub fn wheel_suspension_relaxation(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.damping_relaxation) + } + pub fn set_wheel_suspension_relaxation(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.damping_relaxation = value; + } + } + + pub fn wheel_max_suspension_force(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.max_suspension_force) + } + pub fn set_wheel_max_suspension_force(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.max_suspension_force = value; + } + } + + pub fn wheel_brake(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.brake) + } + pub fn set_wheel_brake(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.brake = value; + } + } + + pub fn wheel_steering(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.steering) + } + pub fn set_wheel_steering(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.steering = value; + } + } + + pub fn wheel_engine_force(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.engine_force) + } + pub fn set_wheel_engine_force(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.engine_force = value; + } + } + + pub fn wheel_direction_cs(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.direction_cs.into()) + } + pub fn set_wheel_direction_cs(&mut self, i: usize, value: &RawVector) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.direction_cs = value.0; + } + } + + pub fn wheel_axle_cs(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.axle_cs.into()) + } + pub fn set_wheel_axle_cs(&mut self, i: usize, value: &RawVector) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.axle_cs = value.0; + } + } + + pub fn wheel_friction_slip(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.friction_slip) + } + pub fn set_wheel_friction_slip(&mut self, i: usize, value: Real) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.friction_slip = value; + } + } + + pub fn wheel_side_friction_stiffness(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.side_friction_stiffness) + } + + pub fn set_wheel_side_friction_stiffness(&mut self, i: usize, stiffness: f32) { + if let Some(wheel) = self.controller.wheels_mut().get_mut(i) { + wheel.side_friction_stiffness = stiffness; + } + } + + /* + * Getters only. + */ + pub fn wheel_rotation(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.rotation) + } + + pub fn wheel_forward_impulse(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.forward_impulse) + } + + pub fn wheel_side_impulse(&self, i: usize) -> Option { + self.controller.wheels().get(i).map(|w| w.side_impulse) + } + + pub fn wheel_suspension_force(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.wheel_suspension_force) + } + + pub fn wheel_contact_normal_ws(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.raycast_info().contact_normal_ws.into()) + } + + pub fn wheel_contact_point_ws(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.raycast_info().contact_point_ws.into()) + } + + pub fn wheel_suspension_length(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.raycast_info().suspension_length) + } + + pub fn wheel_hard_point_ws(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .map(|w| w.raycast_info().hard_point_ws.into()) + } + + pub fn wheel_is_in_contact(&self, i: usize) -> bool { + self.controller + .wheels() + .get(i) + .map(|w| w.raycast_info().is_in_contact) + .unwrap_or(false) + } + + pub fn wheel_ground_object(&self, i: usize) -> Option { + self.controller + .wheels() + .get(i) + .and_then(|w| w.raycast_info().ground_object) + .map(|h| utils::flat_handle(h.0)) + } +} diff --git a/thirdparty/rapier.js/src/dynamics/ccd_solver.rs b/thirdparty/rapier.js/src/dynamics/ccd_solver.rs new file mode 100644 index 00000000..4c8b54f8 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/ccd_solver.rs @@ -0,0 +1,13 @@ +use rapier::dynamics::CCDSolver; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawCCDSolver(pub(crate) CCDSolver); + +#[wasm_bindgen] +impl RawCCDSolver { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawCCDSolver(CCDSolver::new()) + } +} diff --git a/thirdparty/rapier.js/src/dynamics/impulse_joint.rs b/thirdparty/rapier.js/src/dynamics/impulse_joint.rs new file mode 100644 index 00000000..bfbdf128 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/impulse_joint.rs @@ -0,0 +1,215 @@ +use crate::dynamics::{RawImpulseJointSet, RawJointAxis, RawJointType, RawMotorModel}; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use rapier::dynamics::JointAxis; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +impl RawImpulseJointSet { + /// The type of this joint. + pub fn jointType(&self, handle: FlatHandle) -> RawJointType { + self.map(handle, |j| j.data.locked_axes.into()) + } + + /// The unique integer identifier of the first rigid-body this joint it attached to. + pub fn jointBodyHandle1(&self, handle: FlatHandle) -> FlatHandle { + self.map(handle, |j| utils::flat_handle(j.body1.0)) + } + + /// The unique integer identifier of the second rigid-body this joint is attached to. + pub fn jointBodyHandle2(&self, handle: FlatHandle) -> FlatHandle { + self.map(handle, |j| utils::flat_handle(j.body2.0)) + } + + /// The angular part of the joint’s local frame relative to the first rigid-body it is attached to. + pub fn jointFrameX1(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |j| j.data.local_frame1.rotation.into()) + } + + /// The angular part of the joint’s local frame relative to the second rigid-body it is attached to. + pub fn jointFrameX2(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |j| j.data.local_frame2.rotation.into()) + } + + /// The position of the first anchor of this joint. + /// + /// The first anchor gives the position of the points application point on the + /// local frame of the first rigid-body it is attached to. + pub fn jointAnchor1(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |j| j.data.local_frame1.translation.vector.into()) + } + + /// The position of the second anchor of this joint. + /// + /// The second anchor gives the position of the points application point on the + /// local frame of the second rigid-body it is attached to. + pub fn jointAnchor2(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |j| j.data.local_frame2.translation.vector.into()) + } + + /// Sets the position of the first local anchor + pub fn jointSetAnchor1(&mut self, handle: FlatHandle, newPos: &RawVector) { + self.map_mut(handle, |j| { + j.data.set_local_anchor1(newPos.0.into()); + }); + } + + /// Sets the position of the second local anchor + pub fn jointSetAnchor2(&mut self, handle: FlatHandle, newPos: &RawVector) { + self.map_mut(handle, |j| { + j.data.set_local_anchor2(newPos.0.into()); + }) + } + + /// Are contacts between the rigid-bodies attached by this joint enabled? + pub fn jointContactsEnabled(&self, handle: FlatHandle) -> bool { + self.map(handle, |j| j.data.contacts_enabled) + } + + /// Sets whether contacts are enabled between the rigid-bodies attached by this joint. + pub fn jointSetContactsEnabled(&mut self, handle: FlatHandle, enabled: bool) { + self.map_mut(handle, |j| { + j.data.contacts_enabled = enabled; + }); + } + + /// Are the limits for this joint enabled? + pub fn jointLimitsEnabled(&self, handle: FlatHandle, axis: RawJointAxis) -> bool { + self.map(handle, |j| { + j.data.limit_axes.contains(JointAxis::from(axis).into()) + }) + } + + /// Return the lower limit along the given joint axis. + pub fn jointLimitsMin(&self, handle: FlatHandle, axis: RawJointAxis) -> f32 { + self.map(handle, |j| j.data.limits[axis as usize].min) + } + + /// If this is a prismatic joint, returns its upper limit. + pub fn jointLimitsMax(&self, handle: FlatHandle, axis: RawJointAxis) -> f32 { + self.map(handle, |j| j.data.limits[axis as usize].max) + } + + /// Enables and sets the joint limits + pub fn jointSetLimits(&mut self, handle: FlatHandle, axis: RawJointAxis, min: f32, max: f32) { + self.map_mut(handle, |j| { + j.data.set_limits(axis.into(), [min, max]); + }); + } + + pub fn jointConfigureMotorModel( + &mut self, + handle: FlatHandle, + axis: RawJointAxis, + model: RawMotorModel, + ) { + self.map_mut(handle, |j| { + j.data.motors[axis as usize].model = model.into() + }) + } + + /* + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotorVelocity( + &mut self, + handle: FlatHandle, + vx: f32, + vy: f32, + vz: f32, + factor: f32, + ) { + let targetVel = Vector3::new(vx, vy, vz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => j.configure_motor_velocity(targetVel, factor), + _ => {} + }) + } + + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotorPosition( + &mut self, + handle: FlatHandle, + qw: f32, + qx: f32, + qy: f32, + qz: f32, + stiffness: f32, + damping: f32, + ) { + let quat = Quaternion::new(qw, qx, qy, qz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => { + if let Some(unit_quat) = UnitQuaternion::try_new(quat, 1.0e-5) { + j.configure_motor_position(unit_quat, stiffness, damping) + } + } + _ => {} + }) + } + + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotor( + &mut self, + handle: FlatHandle, + qw: f32, + qx: f32, + qy: f32, + qz: f32, + vx: f32, + vy: f32, + vz: f32, + stiffness: f32, + damping: f32, + ) { + let quat = Quaternion::new(qw, qx, qy, qz); + let vel = Vector3::new(vx, vy, vz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => { + if let Some(unit_quat) = UnitQuaternion::try_new(quat, 1.0e-5) { + j.configure_motor(unit_quat, vel, stiffness, damping) + } + } + _ => {} + }) + } + */ + + pub fn jointConfigureMotorVelocity( + &mut self, + handle: FlatHandle, + axis: RawJointAxis, + targetVel: f32, + factor: f32, + ) { + self.jointConfigureMotor(handle, axis, 0.0, targetVel, 0.0, factor) + } + + pub fn jointConfigureMotorPosition( + &mut self, + handle: FlatHandle, + axis: RawJointAxis, + targetPos: f32, + stiffness: f32, + damping: f32, + ) { + self.jointConfigureMotor(handle, axis, targetPos, 0.0, stiffness, damping) + } + + pub fn jointConfigureMotor( + &mut self, + handle: FlatHandle, + axis: RawJointAxis, + targetPos: f32, + targetVel: f32, + stiffness: f32, + damping: f32, + ) { + self.map_mut(handle, |j| { + j.data + .set_motor(axis.into(), targetPos, targetVel, stiffness, damping); + }) + } +} diff --git a/thirdparty/rapier.js/src/dynamics/impulse_joint_set.rs b/thirdparty/rapier.js/src/dynamics/impulse_joint_set.rs new file mode 100644 index 00000000..b7ba1bf0 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/impulse_joint_set.rs @@ -0,0 +1,92 @@ +use crate::dynamics::RawGenericJoint; +use crate::utils::{self, FlatHandle}; +use rapier::dynamics::{ImpulseJoint, ImpulseJointSet}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawImpulseJointSet(pub(crate) ImpulseJointSet); + +impl RawImpulseJointSet { + pub(crate) fn map(&self, handle: FlatHandle, f: impl FnOnce(&ImpulseJoint) -> T) -> T { + let body = self.0.get(utils::impulse_joint_handle(handle)).expect( + "Invalid ImpulseJoint reference. It may have been removed from the physics World.", + ); + f(body) + } + + pub(crate) fn map_mut( + &mut self, + handle: FlatHandle, + f: impl FnOnce(&mut ImpulseJoint) -> T, + ) -> T { + let body = self + .0 + .get_mut(utils::impulse_joint_handle(handle), true) + .expect( + "Invalid ImpulseJoint reference. It may have been removed from the physics World.", + ); + f(body) + } +} + +#[wasm_bindgen] +impl RawImpulseJointSet { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawImpulseJointSet(ImpulseJointSet::new()) + } + + pub fn createJoint( + &mut self, + params: &RawGenericJoint, + parent1: FlatHandle, + parent2: FlatHandle, + wake_up: bool, + ) -> FlatHandle { + utils::flat_handle( + self.0 + .insert( + utils::body_handle(parent1), + utils::body_handle(parent2), + params.0.clone(), + wake_up, + ) + .0, + ) + } + + pub fn remove(&mut self, handle: FlatHandle, wakeUp: bool) { + let handle = utils::impulse_joint_handle(handle); + self.0.remove(handle, wakeUp); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn contains(&self, handle: FlatHandle) -> bool { + self.0.get(utils::impulse_joint_handle(handle)).is_some() + } + + /// Applies the given JavaScript function to the integer handle of each joint managed by this physics world. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each joint managed by this set. Called as `f(collider)`. + pub fn forEachJointHandle(&self, f: &js_sys::Function) { + let this = JsValue::null(); + for (handle, _) in self.0.iter() { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } + + /// Applies the given JavaScript function to the integer handle of each joint attached to the given rigid-body. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each joint attached to the rigid-body. Called as `f(collider)`. + pub fn forEachJointAttachedToRigidBody(&self, body: FlatHandle, f: &js_sys::Function) { + let this = JsValue::null(); + for (_, _, handle, _) in self.0.attached_joints(utils::body_handle(body)) { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } +} diff --git a/thirdparty/rapier.js/src/dynamics/integration_parameters.rs b/thirdparty/rapier.js/src/dynamics/integration_parameters.rs new file mode 100644 index 00000000..b273e057 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/integration_parameters.rs @@ -0,0 +1,101 @@ +use rapier::dynamics::IntegrationParameters; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawIntegrationParameters(pub(crate) IntegrationParameters); + +#[wasm_bindgen] +impl RawIntegrationParameters { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawIntegrationParameters(IntegrationParameters::default()) + } + + #[wasm_bindgen(getter)] + pub fn dt(&self) -> f32 { + self.0.dt + } + + #[wasm_bindgen(getter)] + pub fn contact_erp(&self) -> f32 { + self.0.contact_erp() + } + + #[wasm_bindgen(getter)] + pub fn normalizedAllowedLinearError(&self) -> f32 { + self.0.normalized_allowed_linear_error + } + + #[wasm_bindgen(getter)] + pub fn normalizedPredictionDistance(&self) -> f32 { + self.0.normalized_prediction_distance + } + + #[wasm_bindgen(getter)] + pub fn numSolverIterations(&self) -> usize { + self.0.num_solver_iterations + } + + #[wasm_bindgen(getter)] + pub fn numInternalPgsIterations(&self) -> usize { + self.0.num_internal_pgs_iterations + } + + #[wasm_bindgen(getter)] + pub fn minIslandSize(&self) -> usize { + self.0.min_island_size + } + + #[wasm_bindgen(getter)] + pub fn maxCcdSubsteps(&self) -> usize { + self.0.max_ccd_substeps + } + + #[wasm_bindgen(getter)] + pub fn lengthUnit(&self) -> f32 { + self.0.length_unit + } + + #[wasm_bindgen(setter)] + pub fn set_dt(&mut self, value: f32) { + self.0.dt = value; + } + + #[wasm_bindgen(setter)] + pub fn set_contact_natural_frequency(&mut self, value: f32) { + self.0.contact_natural_frequency = value + } + + #[wasm_bindgen(setter)] + pub fn set_normalizedAllowedLinearError(&mut self, value: f32) { + self.0.normalized_allowed_linear_error = value + } + + #[wasm_bindgen(setter)] + pub fn set_normalizedPredictionDistance(&mut self, value: f32) { + self.0.normalized_prediction_distance = value + } + + #[wasm_bindgen(setter)] + pub fn set_numSolverIterations(&mut self, value: usize) { + self.0.num_solver_iterations = value; + } + #[wasm_bindgen(setter)] + pub fn set_numInternalPgsIterations(&mut self, value: usize) { + self.0.num_internal_pgs_iterations = value; + } + #[wasm_bindgen(setter)] + pub fn set_minIslandSize(&mut self, value: usize) { + self.0.min_island_size = value + } + + #[wasm_bindgen(setter)] + pub fn set_maxCcdSubsteps(&mut self, value: usize) { + self.0.max_ccd_substeps = value + } + + #[wasm_bindgen(setter)] + pub fn set_lengthUnit(&mut self, value: f32) { + self.0.length_unit = value + } +} diff --git a/thirdparty/rapier.js/src/dynamics/island_manager.rs b/thirdparty/rapier.js/src/dynamics/island_manager.rs new file mode 100644 index 00000000..7b4cfae6 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/island_manager.rs @@ -0,0 +1,31 @@ +use crate::utils; +use rapier::dynamics::IslandManager; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawIslandManager(pub(crate) IslandManager); + +#[wasm_bindgen] +impl RawIslandManager { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawIslandManager(IslandManager::new()) + } + + /// Applies the given JavaScript function to the integer handle of each active rigid-body + /// managed by this island manager. + /// + /// After a short time of inactivity, a rigid-body is automatically deactivated ("asleep") by + /// the physics engine in order to save computational power. A sleeping rigid-body never moves + /// unless it is moved manually by the user. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each active rigid-body managed by this + /// set. Called as `f(collider)`. + pub fn forEachActiveRigidBodyHandle(&self, f: &js_sys::Function) { + let this = JsValue::null(); + for handle in self.0.active_bodies() { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } +} diff --git a/thirdparty/rapier.js/src/dynamics/joint.rs b/thirdparty/rapier.js/src/dynamics/joint.rs new file mode 100644 index 00000000..74ab2b3d --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/joint.rs @@ -0,0 +1,314 @@ +use crate::math::{RawRotation, RawVector}; +use na::Unit; +use rapier::dynamics::{ + FixedJointBuilder, GenericJoint, JointAxesMask, JointAxis, MotorModel, PrismaticJointBuilder, + RevoluteJointBuilder, RopeJointBuilder, SpringJointBuilder, +}; +#[cfg(feature = "dim3")] +use rapier::dynamics::{GenericJointBuilder, SphericalJointBuilder}; +use rapier::math::Isometry; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[cfg(feature = "dim2")] +pub enum RawJointType { + Revolute, + Fixed, + Prismatic, + Rope, + Spring, + Generic, +} + +#[wasm_bindgen] +#[cfg(feature = "dim3")] +pub enum RawJointType { + Revolute, + Fixed, + Prismatic, + Rope, + Spring, + Spherical, + Generic, +} + +/// The type of this joint. +#[cfg(feature = "dim2")] +impl From for RawJointType { + fn from(ty: JointAxesMask) -> RawJointType { + let rev_axes = JointAxesMask::LIN_X | JointAxesMask::LIN_Y; + let pri_axes = JointAxesMask::LIN_Y | JointAxesMask::ANG_X; + let fix_axes = JointAxesMask::LIN_X | JointAxesMask::LIN_Y | JointAxesMask::ANG_X; + + if ty == rev_axes { + RawJointType::Revolute + } else if ty == pri_axes { + RawJointType::Prismatic + } else if ty == fix_axes { + RawJointType::Fixed + } else { + RawJointType::Generic + } + } +} + +/// The type of this joint. +#[cfg(feature = "dim3")] +impl From for RawJointType { + fn from(ty: JointAxesMask) -> RawJointType { + let rev_axes = JointAxesMask::LIN_X + | JointAxesMask::LIN_Y + | JointAxesMask::LIN_Z + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + let pri_axes = JointAxesMask::LIN_Y + | JointAxesMask::LIN_Z + | JointAxesMask::ANG_X + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + let sph_axes = JointAxesMask::ANG_X | JointAxesMask::ANG_Y | JointAxesMask::ANG_Z; + let fix_axes = JointAxesMask::LIN_X + | JointAxesMask::LIN_Y + | JointAxesMask::LIN_Z + | JointAxesMask::ANG_X + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + + if ty == rev_axes { + RawJointType::Revolute + } else if ty == pri_axes { + RawJointType::Prismatic + } else if ty == sph_axes { + RawJointType::Spherical + } else if ty == fix_axes { + RawJointType::Fixed + } else { + RawJointType::Generic + } + } +} + +#[wasm_bindgen] +pub enum RawMotorModel { + AccelerationBased, + ForceBased, +} + +impl From for MotorModel { + fn from(model: RawMotorModel) -> MotorModel { + match model { + RawMotorModel::AccelerationBased => MotorModel::AccelerationBased, + RawMotorModel::ForceBased => MotorModel::ForceBased, + } + } +} + +#[cfg(feature = "dim2")] +#[wasm_bindgen] +#[derive(Copy, Clone)] +pub enum RawJointAxis { + LinX, + LinY, + AngX, +} + +#[cfg(feature = "dim3")] +#[wasm_bindgen] +#[derive(Copy, Clone)] +pub enum RawJointAxis { + LinX, + LinY, + LinZ, + AngX, + AngY, + AngZ, +} + +impl From for JointAxis { + fn from(axis: RawJointAxis) -> JointAxis { + match axis { + RawJointAxis::LinX => JointAxis::LinX, + RawJointAxis::LinY => JointAxis::LinY, + #[cfg(feature = "dim3")] + RawJointAxis::LinZ => JointAxis::LinZ, + RawJointAxis::AngX => JointAxis::AngX, + #[cfg(feature = "dim3")] + RawJointAxis::AngY => JointAxis::AngY, + #[cfg(feature = "dim3")] + RawJointAxis::AngZ => JointAxis::AngZ, + } + } +} + +#[wasm_bindgen] +pub struct RawGenericJoint(pub(crate) GenericJoint); + +#[wasm_bindgen] +impl RawGenericJoint { + /// Creates a new joint descriptor that builds generic joints. + /// + /// Generic joints allow arbitrary axes of freedom to be selected + /// for the joint from the available 6 degrees of freedom. + #[cfg(feature = "dim3")] + pub fn generic( + anchor1: &RawVector, + anchor2: &RawVector, + axis: &RawVector, + lockedAxes: u8, + ) -> Option { + let axesMask: JointAxesMask = JointAxesMask::from_bits(lockedAxes)?; + let axis = Unit::try_new(axis.0, 0.0)?; + let joint: GenericJoint = GenericJointBuilder::new(axesMask) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .local_axis1(axis) + .local_axis2(axis) + .into(); + Some(Self(joint)) + } + + pub fn spring( + rest_length: f32, + stiffness: f32, + damping: f32, + anchor1: &RawVector, + anchor2: &RawVector, + ) -> Self { + Self( + SpringJointBuilder::new(rest_length, stiffness, damping) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .into(), + ) + } + + pub fn rope(length: f32, anchor1: &RawVector, anchor2: &RawVector) -> Self { + Self( + RopeJointBuilder::new(length) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .into(), + ) + } + + /// Create a new joint descriptor that builds spherical joints. + /// + /// A spherical joints allows three relative rotational degrees of freedom + /// by preventing any relative translation between the anchors of the + /// two attached rigid-bodies. + #[cfg(feature = "dim3")] + pub fn spherical(anchor1: &RawVector, anchor2: &RawVector) -> Self { + Self( + SphericalJointBuilder::new() + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .into(), + ) + } + + /// Creates a new joint descriptor that builds a Prismatic joint. + /// + /// A prismatic joint removes all the degrees of freedom between the + /// affected bodies, except for the translation along one axis. + /// + /// Returns `None` if any of the provided axes cannot be normalized. + #[cfg(feature = "dim2")] + pub fn prismatic( + anchor1: &RawVector, + anchor2: &RawVector, + axis: &RawVector, + limitsEnabled: bool, + limitsMin: f32, + limitsMax: f32, + ) -> Option { + let axis = Unit::try_new(axis.0, 0.0)?; + let mut joint = PrismaticJointBuilder::new(axis) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()); + + if limitsEnabled { + joint = joint.limits([limitsMin, limitsMax]); + } + + Some(Self(joint.into())) + } + + /// Creates a new joint descriptor that builds a Prismatic joint. + /// + /// A prismatic joint removes all the degrees of freedom between the + /// affected bodies, except for the translation along one axis. + /// + /// Returns `None` if any of the provided axes cannot be normalized. + #[cfg(feature = "dim3")] + pub fn prismatic( + anchor1: &RawVector, + anchor2: &RawVector, + axis: &RawVector, + limitsEnabled: bool, + limitsMin: f32, + limitsMax: f32, + ) -> Option { + let axis = Unit::try_new(axis.0, 0.0)?; + let mut joint = PrismaticJointBuilder::new(axis) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()); + + if limitsEnabled { + joint = joint.limits([limitsMin, limitsMax]); + } + + Some(Self(joint.into())) + } + + /// Creates a new joint descriptor that builds a Fixed joint. + /// + /// A fixed joint removes all the degrees of freedom between the affected bodies. + pub fn fixed( + anchor1: &RawVector, + axes1: &RawRotation, + anchor2: &RawVector, + axes2: &RawRotation, + ) -> RawGenericJoint { + let pos1 = Isometry::from_parts(anchor1.0.into(), axes1.0); + let pos2 = Isometry::from_parts(anchor2.0.into(), axes2.0); + Self( + FixedJointBuilder::new() + .local_frame1(pos1) + .local_frame2(pos2) + .into(), + ) + } + + /// Create a new joint descriptor that builds Revolute joints. + /// + /// A revolute joint removes all degrees of freedom between the affected + /// bodies except for the rotation. + #[cfg(feature = "dim2")] + pub fn revolute(anchor1: &RawVector, anchor2: &RawVector) -> Option { + Some(Self( + RevoluteJointBuilder::new() + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .into(), + )) + } + + /// Create a new joint descriptor that builds Revolute joints. + /// + /// A revolute joint removes all degrees of freedom between the affected + /// bodies except for the rotation along one axis. + #[cfg(feature = "dim3")] + pub fn revolute( + anchor1: &RawVector, + anchor2: &RawVector, + axis: &RawVector, + ) -> Option { + let axis = Unit::try_new(axis.0, 0.0)?; + Some(Self( + RevoluteJointBuilder::new(axis) + .local_anchor1(anchor1.0.into()) + .local_anchor2(anchor2.0.into()) + .into(), + )) + } +} diff --git a/thirdparty/rapier.js/src/dynamics/mod.rs b/thirdparty/rapier.js/src/dynamics/mod.rs new file mode 100644 index 00000000..36cc7eaa --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/mod.rs @@ -0,0 +1,20 @@ +//! Structures related to dynamics: bodies, joints, etc. + +pub use self::ccd_solver::*; +pub use self::impulse_joint_set::*; +pub use self::integration_parameters::*; +pub use self::island_manager::*; +pub use self::joint::*; +pub use self::multibody_joint_set::*; +pub use self::rigid_body_set::*; + +mod ccd_solver; +mod impulse_joint; +mod impulse_joint_set; +mod integration_parameters; +mod island_manager; +mod joint; +mod multibody_joint; +mod multibody_joint_set; +mod rigid_body; +mod rigid_body_set; diff --git a/thirdparty/rapier.js/src/dynamics/multibody_joint.rs b/thirdparty/rapier.js/src/dynamics/multibody_joint.rs new file mode 100644 index 00000000..ff6e816e --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/multibody_joint.rs @@ -0,0 +1,196 @@ +use crate::dynamics::{RawJointAxis, RawJointType, RawMultibodyJointSet}; +use crate::math::{RawRotation, RawVector}; +use crate::utils::FlatHandle; +use rapier::dynamics::JointAxis; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +impl RawMultibodyJointSet { + /// The type of this joint. + pub fn jointType(&self, handle: FlatHandle) -> RawJointType { + self.map(handle, |j| j.data.locked_axes.into()) + } + + // /// The unique integer identifier of the first rigid-body this joint it attached to. + // pub fn jointBodyHandle1(&self, handle: FlatHandle) -> u32 { + // self.map(handle, |j| j.body1.into_raw_parts().0) + // } + // + // /// The unique integer identifier of the second rigid-body this joint is attached to. + // pub fn jointBodyHandle2(&self, handle: FlatHandle) -> u32 { + // self.map(handle, |j| j.body2.into_raw_parts().0) + // } + + /// The angular part of the joint’s local frame relative to the first rigid-body it is attached to. + pub fn jointFrameX1(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |j| j.data.local_frame1.rotation.into()) + } + + /// The angular part of the joint’s local frame relative to the second rigid-body it is attached to. + pub fn jointFrameX2(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |j| j.data.local_frame2.rotation.into()) + } + + /// The position of the first anchor of this joint. + /// + /// The first anchor gives the position of the points application point on the + /// local frame of the first rigid-body it is attached to. + pub fn jointAnchor1(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |j| j.data.local_frame1.translation.vector.into()) + } + + /// The position of the second anchor of this joint. + /// + /// The second anchor gives the position of the points application point on the + /// local frame of the second rigid-body it is attached to. + pub fn jointAnchor2(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |j| j.data.local_frame2.translation.vector.into()) + } + + /// Are contacts between the rigid-bodies attached by this joint enabled? + pub fn jointContactsEnabled(&self, handle: FlatHandle) -> bool { + self.map(handle, |j| j.data.contacts_enabled) + } + + /// Sets whether contacts are enabled between the rigid-bodies attached by this joint. + pub fn jointSetContactsEnabled(&mut self, handle: FlatHandle, enabled: bool) { + self.map_mut(handle, |j| { + j.data.contacts_enabled = enabled; + }); + } + + /// Are the limits for this joint enabled? + pub fn jointLimitsEnabled(&self, handle: FlatHandle, axis: RawJointAxis) -> bool { + self.map(handle, |j| { + j.data.limit_axes.contains(JointAxis::from(axis).into()) + }) + } + + /// Return the lower limit along the given joint axis. + pub fn jointLimitsMin(&self, handle: FlatHandle, axis: RawJointAxis) -> f32 { + self.map(handle, |j| j.data.limits[axis as usize].min) + } + + /// If this is a prismatic joint, returns its upper limit. + pub fn jointLimitsMax(&self, handle: FlatHandle, axis: RawJointAxis) -> f32 { + self.map(handle, |j| j.data.limits[axis as usize].max) + } + + // pub fn jointConfigureMotorModel( + // &mut self, + // handle: FlatHandle, + // axis: RawJointAxis, + // model: RawMotorModel, + // ) { + // self.map_mut(handle, |j| { + // j.data.motors[axis as usize].model = model.into() + // }) + // } + + /* + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotorVelocity( + &mut self, + handle: FlatHandle, + vx: f32, + vy: f32, + vz: f32, + factor: f32, + ) { + let targetVel = Vector3::new(vx, vy, vz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => j.configure_motor_velocity(targetVel, factor), + _ => {} + }) + } + + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotorPosition( + &mut self, + handle: FlatHandle, + qw: f32, + qx: f32, + qy: f32, + qz: f32, + stiffness: f32, + damping: f32, + ) { + let quat = Quaternion::new(qw, qx, qy, qz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => { + if let Some(unit_quat) = UnitQuaternion::try_new(quat, 1.0e-5) { + j.configure_motor_position(unit_quat, stiffness, damping) + } + } + _ => {} + }) + } + + #[cfg(feature = "dim3")] + pub fn jointConfigureBallMotor( + &mut self, + handle: FlatHandle, + qw: f32, + qx: f32, + qy: f32, + qz: f32, + vx: f32, + vy: f32, + vz: f32, + stiffness: f32, + damping: f32, + ) { + let quat = Quaternion::new(qw, qx, qy, qz); + let vel = Vector3::new(vx, vy, vz); + + self.map_mut(handle, |j| match &mut j.params { + JointData::SphericalJoint(j) => { + if let Some(unit_quat) = UnitQuaternion::try_new(quat, 1.0e-5) { + j.configure_motor(unit_quat, vel, stiffness, damping) + } + } + _ => {} + }) + } + */ + + // pub fn jointConfigureMotorVelocity( + // &mut self, + // handle: FlatHandle, + // axis: RawJointAxis, + // targetVel: f32, + // factor: f32, + // ) { + // self.jointConfigureMotor(handle, axis, 0.0, targetVel, 0.0, factor) + // } + // + // pub fn jointConfigureMotorPosition( + // &mut self, + // handle: FlatHandle, + // axis: RawJointAxis, + // targetPos: f32, + // stiffness: f32, + // damping: f32, + // ) { + // self.jointConfigureMotor(handle, axis, targetPos, 0.0, stiffness, damping) + // } + + // pub fn jointConfigureMotor( + // &mut self, + // handle: FlatHandle, + // axis: RawJointAxis, + // targetPos: f32, + // targetVel: f32, + // stiffness: f32, + // damping: f32, + // ) { + // self.map_mut(handle, |j| { + // j.data.motors[axis as usize].target_pos = targetPos; + // j.data.motors[axis as usize].target_vel = targetVel; + // j.data.motors[axis as usize].stiffness = stiffness; + // j.data.motors[axis as usize].damping = damping; + // }) + // } +} diff --git a/thirdparty/rapier.js/src/dynamics/multibody_joint_set.rs b/thirdparty/rapier.js/src/dynamics/multibody_joint_set.rs new file mode 100644 index 00000000..c7f4839e --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/multibody_joint_set.rs @@ -0,0 +1,85 @@ +use crate::dynamics::RawGenericJoint; +use crate::utils::{self, FlatHandle}; +use rapier::dynamics::{MultibodyJoint, MultibodyJointSet}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawMultibodyJointSet(pub(crate) MultibodyJointSet); + +impl RawMultibodyJointSet { + pub(crate) fn map(&self, handle: FlatHandle, f: impl FnOnce(&MultibodyJoint) -> T) -> T { + let (body, link_id) = self + .0 + .get(utils::multibody_joint_handle(handle)) + .expect("Invalid Joint reference. It may have been removed from the physics World."); + f(body.link(link_id).unwrap().joint()) + } + + pub(crate) fn map_mut( + &mut self, + handle: FlatHandle, + f: impl FnOnce(&mut MultibodyJoint) -> T, + ) -> T { + let (body, link_id) = self + .0 + .get_mut(utils::multibody_joint_handle(handle)) + .expect("Invalid Joint reference. It may have been removed from the physics World."); + f(&mut body.link_mut(link_id).unwrap().joint) + } +} + +#[wasm_bindgen] +impl RawMultibodyJointSet { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawMultibodyJointSet(MultibodyJointSet::new()) + } + + pub fn createJoint( + &mut self, + params: &RawGenericJoint, + parent1: FlatHandle, + parent2: FlatHandle, + wakeUp: bool, + ) -> FlatHandle { + // TODO: avoid the unwrap? + let parent1 = utils::body_handle(parent1); + let parent2 = utils::body_handle(parent2); + + self.0 + .insert(parent1, parent2, params.0.clone(), wakeUp) + .map(|h| utils::flat_handle(h.0)) + .unwrap_or(FlatHandle::MAX) + } + + pub fn remove(&mut self, handle: FlatHandle, wakeUp: bool) { + let handle = utils::multibody_joint_handle(handle); + self.0.remove(handle, wakeUp); + } + + pub fn contains(&self, handle: FlatHandle) -> bool { + self.0.get(utils::multibody_joint_handle(handle)).is_some() + } + + /// Applies the given JavaScript function to the integer handle of each joint managed by this physics world. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each joint managed by this set. Called as `f(collider)`. + pub fn forEachJointHandle(&self, f: &js_sys::Function) { + let this = JsValue::null(); + for (handle, _, _, _) in self.0.iter() { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } + + /// Applies the given JavaScript function to the integer handle of each joint attached to the given rigid-body. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each joint attached to the rigid-body. Called as `f(collider)`. + pub fn forEachJointAttachedToRigidBody(&self, body: FlatHandle, f: &js_sys::Function) { + let this = JsValue::null(); + for (_, _, handle) in self.0.attached_joints(utils::body_handle(body)) { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } +} diff --git a/thirdparty/rapier.js/src/dynamics/rigid_body.rs b/thirdparty/rapier.js/src/dynamics/rigid_body.rs new file mode 100644 index 00000000..d7174d77 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/rigid_body.rs @@ -0,0 +1,753 @@ +use crate::dynamics::{RawRigidBodySet, RawRigidBodyType}; +use crate::geometry::RawColliderSet; +#[cfg(feature = "dim3")] +use crate::math::RawSdpMatrix3; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use na::Point; +use rapier::dynamics::MassProperties; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +impl RawRigidBodySet { + /// The world-space translation of this rigid-body. + pub fn rbTranslation(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| RawVector(rb.position().translation.vector)) + } + + /// The world-space orientation of this rigid-body. + pub fn rbRotation(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |rb| RawRotation(rb.position().rotation)) + } + + /// Put the given rigid-body to sleep. + pub fn rbSleep(&mut self, handle: FlatHandle) { + self.map_mut(handle, |rb| rb.sleep()); + } + + /// Is this rigid-body sleeping? + pub fn rbIsSleeping(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_sleeping()) + } + + /// Is the velocity of this rigid-body not zero? + pub fn rbIsMoving(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_moving()) + } + + /// The world-space predicted translation of this rigid-body. + /// + /// If this rigid-body is kinematic this value is set by the `setNextKinematicTranslation` + /// method and is used for estimating the kinematic body velocity at the next timestep. + /// For non-kinematic bodies, this value is currently unspecified. + pub fn rbNextTranslation(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| { + RawVector(rb.next_position().translation.vector) + }) + } + + /// The world-space predicted orientation of this rigid-body. + /// + /// If this rigid-body is kinematic this value is set by the `setNextKinematicRotation` + /// method and is used for estimating the kinematic body velocity at the next timestep. + /// For non-kinematic bodies, this value is currently unspecified. + pub fn rbNextRotation(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |rb| RawRotation(rb.next_position().rotation)) + } + + /// Sets the translation of this rigid-body. + /// + /// # Parameters + /// - `x`: the world-space position of the rigid-body along the `x` axis. + /// - `y`: the world-space position of the rigid-body along the `y` axis. + /// - `z`: the world-space position of the rigid-body along the `z` axis. + /// - `wakeUp`: forces the rigid-body to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim3")] + pub fn rbSetTranslation(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_translation(na::Vector3::new(x, y, z), wakeUp); + }) + } + + /// Sets the translation of this rigid-body. + /// + /// # Parameters + /// - `x`: the world-space position of the rigid-body along the `x` axis. + /// - `y`: the world-space position of the rigid-body along the `y` axis. + /// - `wakeUp`: forces the rigid-body to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim2")] + pub fn rbSetTranslation(&mut self, handle: FlatHandle, x: f32, y: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_translation(na::Vector2::new(x, y), wakeUp); + }) + } + + /// Sets the rotation quaternion of this rigid-body. + /// + /// This does nothing if a zero quaternion is provided. + /// + /// # Parameters + /// - `x`: the first vector component of the quaternion. + /// - `y`: the second vector component of the quaternion. + /// - `z`: the third vector component of the quaternion. + /// - `w`: the scalar component of the quaternion. + /// - `wakeUp`: forces the rigid-body to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim3")] + pub fn rbSetRotation( + &mut self, + handle: FlatHandle, + x: f32, + y: f32, + z: f32, + w: f32, + wakeUp: bool, + ) { + if let Some(q) = na::Unit::try_new(na::Quaternion::new(w, x, y, z), 0.0) { + self.map_mut(handle, |rb| rb.set_rotation(q, wakeUp)) + } + } + + /// Sets the rotation angle of this rigid-body. + /// + /// # Parameters + /// - `angle`: the rotation angle, in radians. + /// - `wakeUp`: forces the rigid-body to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim2")] + pub fn rbSetRotation(&mut self, handle: FlatHandle, angle: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_rotation(na::UnitComplex::new(angle), wakeUp) + }) + } + + /// Sets the linear velocity of this rigid-body. + pub fn rbSetLinvel(&mut self, handle: FlatHandle, linvel: &RawVector, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_linvel(linvel.0, wakeUp); + }); + } + + /// Sets the angular velocity of this rigid-body. + #[cfg(feature = "dim2")] + pub fn rbSetAngvel(&mut self, handle: FlatHandle, angvel: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_angvel(angvel, wakeUp); + }); + } + + /// Sets the angular velocity of this rigid-body. + #[cfg(feature = "dim3")] + pub fn rbSetAngvel(&mut self, handle: FlatHandle, angvel: &RawVector, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.set_angvel(angvel.0, wakeUp); + }); + } + + /// If this rigid body is kinematic, sets its future translation after the next timestep integration. + /// + /// This should be used instead of `rigidBody.setTranslation` to make the dynamic object + /// interacting with this kinematic body behave as expected. Internally, Rapier will compute + /// an artificial velocity for this rigid-body from its current position and its next kinematic + /// position. This velocity will be used to compute forces on dynamic bodies interacting with + /// this body. + /// + /// # Parameters + /// - `x`: the world-space position of the rigid-body along the `x` axis. + /// - `y`: the world-space position of the rigid-body along the `y` axis. + /// - `z`: the world-space position of the rigid-body along the `z` axis. + #[cfg(feature = "dim3")] + pub fn rbSetNextKinematicTranslation(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32) { + self.map_mut(handle, |rb| { + rb.set_next_kinematic_translation(na::Vector3::new(x, y, z)); + }) + } + + /// If this rigid body is kinematic, sets its future translation after the next timestep integration. + /// + /// This should be used instead of `rigidBody.setTranslation` to make the dynamic object + /// interacting with this kinematic body behave as expected. Internally, Rapier will compute + /// an artificial velocity for this rigid-body from its current position and its next kinematic + /// position. This velocity will be used to compute forces on dynamic bodies interacting with + /// this body. + /// + /// # Parameters + /// - `x`: the world-space position of the rigid-body along the `x` axis. + /// - `y`: the world-space position of the rigid-body along the `y` axis. + #[cfg(feature = "dim2")] + pub fn rbSetNextKinematicTranslation(&mut self, handle: FlatHandle, x: f32, y: f32) { + self.map_mut(handle, |rb| { + rb.set_next_kinematic_translation(na::Vector2::new(x, y)); + }) + } + + /// If this rigid body is kinematic, sets its future rotation after the next timestep integration. + /// + /// This should be used instead of `rigidBody.setRotation` to make the dynamic object + /// interacting with this kinematic body behave as expected. Internally, Rapier will compute + /// an artificial velocity for this rigid-body from its current position and its next kinematic + /// position. This velocity will be used to compute forces on dynamic bodies interacting with + /// this body. + /// + /// # Parameters + /// - `x`: the first vector component of the quaternion. + /// - `y`: the second vector component of the quaternion. + /// - `z`: the third vector component of the quaternion. + /// - `w`: the scalar component of the quaternion. + #[cfg(feature = "dim3")] + pub fn rbSetNextKinematicRotation( + &mut self, + handle: FlatHandle, + x: f32, + y: f32, + z: f32, + w: f32, + ) { + if let Some(q) = na::Unit::try_new(na::Quaternion::new(w, x, y, z), 0.0) { + self.map_mut(handle, |rb| { + rb.set_next_kinematic_rotation(q); + }) + } + } + + /// If this rigid body is kinematic, sets its future rotation after the next timestep integration. + /// + /// This should be used instead of `rigidBody.setRotation` to make the dynamic object + /// interacting with this kinematic body behave as expected. Internally, Rapier will compute + /// an artificial velocity for this rigid-body from its current position and its next kinematic + /// position. This velocity will be used to compute forces on dynamic bodies interacting with + /// this body. + /// + /// # Parameters + /// - `angle`: the rotation angle, in radians. + #[cfg(feature = "dim2")] + pub fn rbSetNextKinematicRotation(&mut self, handle: FlatHandle, angle: f32) { + self.map_mut(handle, |rb| { + rb.set_next_kinematic_rotation(na::UnitComplex::new(angle)); + }) + } + + pub fn rbRecomputeMassPropertiesFromColliders( + &mut self, + handle: FlatHandle, + colliders: &RawColliderSet, + ) { + self.map_mut(handle, |rb| { + rb.recompute_mass_properties_from_colliders(&colliders.0) + }) + } + + pub fn rbSetAdditionalMass(&mut self, handle: FlatHandle, mass: f32, wake_up: bool) { + self.map_mut(handle, |rb| { + rb.set_additional_mass(mass, wake_up); + }) + } + + #[cfg(feature = "dim3")] + pub fn rbSetAdditionalMassProperties( + &mut self, + handle: FlatHandle, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: &RawVector, + angularInertiaFrame: &RawRotation, + wake_up: bool, + ) { + self.map_mut(handle, |rb| { + let mprops = MassProperties::with_principal_inertia_frame( + centerOfMass.0.into(), + mass, + principalAngularInertia.0, + angularInertiaFrame.0, + ); + rb.set_additional_mass_properties(mprops, wake_up) + }) + } + + #[cfg(feature = "dim2")] + pub fn rbSetAdditionalMassProperties( + &mut self, + handle: FlatHandle, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: f32, + wake_up: bool, + ) { + self.map_mut(handle, |rb| { + let props = MassProperties::new(centerOfMass.0.into(), mass, principalAngularInertia); + rb.set_additional_mass_properties(props, wake_up) + }) + } + + /// The linear velocity of this rigid-body. + pub fn rbLinvel(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| RawVector(*rb.linvel())) + } + + /// The angular velocity of this rigid-body. + #[cfg(feature = "dim2")] + pub fn rbAngvel(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.angvel()) + } + + /// The angular velocity of this rigid-body. + #[cfg(feature = "dim3")] + pub fn rbAngvel(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| RawVector(*rb.angvel())) + } + + /// The velocity of the given world-space point on this rigid-body. + pub fn rbVelocityAtPoint(&self, handle: FlatHandle, point: &RawVector) -> RawVector { + self.map(handle, |rb| { + rb.velocity_at_point(&Point::from(point.0)).into() + }) + } + + pub fn rbLockTranslations(&mut self, handle: FlatHandle, locked: bool, wake_up: bool) { + self.map_mut(handle, |rb| rb.lock_translations(locked, wake_up)) + } + + #[cfg(feature = "dim2")] + pub fn rbSetEnabledTranslations( + &mut self, + handle: FlatHandle, + allow_x: bool, + allow_y: bool, + wake_up: bool, + ) { + self.map_mut(handle, |rb| { + rb.set_enabled_translations(allow_x, allow_y, wake_up) + }) + } + + #[cfg(feature = "dim3")] + pub fn rbSetEnabledTranslations( + &mut self, + handle: FlatHandle, + allow_x: bool, + allow_y: bool, + allow_z: bool, + wake_up: bool, + ) { + self.map_mut(handle, |rb| { + rb.set_enabled_translations(allow_x, allow_y, allow_z, wake_up) + }) + } + + pub fn rbLockRotations(&mut self, handle: FlatHandle, locked: bool, wake_up: bool) { + self.map_mut(handle, |rb| rb.lock_rotations(locked, wake_up)) + } + + #[cfg(feature = "dim3")] + pub fn rbSetEnabledRotations( + &mut self, + handle: FlatHandle, + allow_x: bool, + allow_y: bool, + allow_z: bool, + wake_up: bool, + ) { + self.map_mut(handle, |rb| { + rb.set_enabled_rotations(allow_x, allow_y, allow_z, wake_up) + }) + } + + pub fn rbDominanceGroup(&self, handle: FlatHandle) -> i8 { + self.map(handle, |rb| rb.dominance_group()) + } + + pub fn rbSetDominanceGroup(&mut self, handle: FlatHandle, group: i8) { + self.map_mut(handle, |rb| rb.set_dominance_group(group)) + } + + pub fn rbEnableCcd(&mut self, handle: FlatHandle, enabled: bool) { + self.map_mut(handle, |rb| rb.enable_ccd(enabled)) + } + + pub fn rbSetSoftCcdPrediction(&mut self, handle: FlatHandle, prediction: f32) { + self.map_mut(handle, |rb| rb.set_soft_ccd_prediction(prediction)) + } + + /// The mass of this rigid-body. + pub fn rbMass(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.mass()) + } + + /// The inverse of the mass of a rigid-body. + /// + /// If this is zero, the rigid-body is assumed to have infinite mass. + pub fn rbInvMass(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.mass_properties().local_mprops.inv_mass) + } + + /// The inverse mass taking into account translation locking. + pub fn rbEffectiveInvMass(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| rb.mass_properties().effective_inv_mass.into()) + } + + /// The center of mass of a rigid-body expressed in its local-space. + pub fn rbLocalCom(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| { + rb.mass_properties().local_mprops.local_com.into() + }) + } + + /// The world-space center of mass of the rigid-body. + pub fn rbWorldCom(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| rb.mass_properties().world_com.into()) + } + + /// The inverse of the principal angular inertia of the rigid-body. + /// + /// Components set to zero are assumed to be infinite along the corresponding principal axis. + #[cfg(feature = "dim2")] + pub fn rbInvPrincipalInertia(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| { + rb.mass_properties() + .local_mprops + .inv_principal_inertia + .into() + }) + } + + /// The inverse of the principal angular inertia of the rigid-body. + /// + /// Components set to zero are assumed to be infinite along the corresponding principal axis. + #[cfg(feature = "dim3")] + pub fn rbInvPrincipalInertia(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| { + rb.mass_properties() + .local_mprops + .inv_principal_inertia + .into() + }) + } + + #[cfg(feature = "dim3")] + /// The principal vectors of the local angular inertia tensor of the rigid-body. + pub fn rbPrincipalInertiaLocalFrame(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |rb| { + RawRotation::from( + rb.mass_properties() + .local_mprops + .principal_inertia_local_frame, + ) + }) + } + + /// The angular inertia along the principal inertia axes of the rigid-body. + #[cfg(feature = "dim2")] + pub fn rbPrincipalInertia(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| { + rb.mass_properties().local_mprops.principal_inertia().into() + }) + } + + /// The angular inertia along the principal inertia axes of the rigid-body. + #[cfg(feature = "dim3")] + pub fn rbPrincipalInertia(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| { + rb.mass_properties().local_mprops.principal_inertia().into() + }) + } + + /// The world-space inverse angular inertia tensor of the rigid-body, + /// taking into account rotation locking. + #[cfg(feature = "dim2")] + pub fn rbEffectiveWorldInvInertia(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| { + rb.mass_properties().effective_world_inv_inertia.into() + }) + } + + /// The world-space inverse angular inertia tensor of the rigid-body, + /// taking into account rotation locking. + #[cfg(feature = "dim3")] + pub fn rbEffectiveWorldInvInertia(&self, handle: FlatHandle) -> RawSdpMatrix3 { + self.map(handle, |rb| { + rb.mass_properties().effective_world_inv_inertia.into() + }) + } + + /// The effective world-space angular inertia (that takes the potential rotation locking into account) of + /// this rigid-body. + #[cfg(feature = "dim2")] + pub fn rbEffectiveAngularInertia(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| { + rb.mass_properties().effective_angular_inertia().into() + }) + } + + /// The effective world-space angular inertia (that takes the potential rotation locking into account) of + /// this rigid-body. + #[cfg(feature = "dim3")] + pub fn rbEffectiveAngularInertia(&self, handle: FlatHandle) -> RawSdpMatrix3 { + self.map(handle, |rb| { + rb.mass_properties().effective_angular_inertia().into() + }) + } + + /// Wakes this rigid-body up. + /// + /// A dynamic rigid-body that does not move during several consecutive frames will + /// be put to sleep by the physics engine, i.e., it will stop being simulated in order + /// to avoid useless computations. + /// This method forces a sleeping rigid-body to wake-up. This is useful, e.g., before modifying + /// the position of a dynamic body so that it is properly simulated afterwards. + pub fn rbWakeUp(&mut self, handle: FlatHandle) { + self.map_mut(handle, |rb| rb.wake_up(true)) + } + + /// Is Continuous Collision Detection enabled for this rigid-body? + pub fn rbIsCcdEnabled(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_ccd_enabled()) + } + pub fn rbSoftCcdPrediction(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.soft_ccd_prediction()) + } + + /// The number of colliders attached to this rigid-body. + pub fn rbNumColliders(&self, handle: FlatHandle) -> usize { + self.map(handle, |rb| rb.colliders().len()) + } + + /// Retrieves the `i-th` collider attached to this rigid-body. + /// + /// # Parameters + /// - `at`: The index of the collider to retrieve. Must be a number in `[0, this.numColliders()[`. + /// This index is **not** the same as the unique identifier of the collider. + pub fn rbCollider(&self, handle: FlatHandle, at: usize) -> FlatHandle { + self.map(handle, |rb| utils::flat_handle(rb.colliders()[at].0)) + } + + /// The status of this rigid-body: fixed, dynamic, or kinematic. + pub fn rbBodyType(&self, handle: FlatHandle) -> RawRigidBodyType { + self.map(handle, |rb| rb.body_type().into()) + } + + /// Set a new status for this rigid-body: fixed, dynamic, or kinematic. + pub fn rbSetBodyType(&mut self, handle: FlatHandle, status: RawRigidBodyType, wake_up: bool) { + self.map_mut(handle, |rb| rb.set_body_type(status.into(), wake_up)); + } + + /// Is this rigid-body fixed? + pub fn rbIsFixed(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_fixed()) + } + + /// Is this rigid-body kinematic? + pub fn rbIsKinematic(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_kinematic()) + } + + /// Is this rigid-body dynamic? + pub fn rbIsDynamic(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_dynamic()) + } + + /// The linear damping coefficient of this rigid-body. + pub fn rbLinearDamping(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.linear_damping()) + } + + /// The angular damping coefficient of this rigid-body. + pub fn rbAngularDamping(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.angular_damping()) + } + + pub fn rbSetLinearDamping(&mut self, handle: FlatHandle, factor: f32) { + self.map_mut(handle, |rb| rb.set_linear_damping(factor)); + } + + pub fn rbSetAngularDamping(&mut self, handle: FlatHandle, factor: f32) { + self.map_mut(handle, |rb| rb.set_angular_damping(factor)); + } + + pub fn rbSetEnabled(&mut self, handle: FlatHandle, enabled: bool) { + self.map_mut(handle, |rb| rb.set_enabled(enabled)) + } + + pub fn rbIsEnabled(&self, handle: FlatHandle) -> bool { + self.map(handle, |rb| rb.is_enabled()) + } + + pub fn rbGravityScale(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.gravity_scale()) + } + + pub fn rbSetGravityScale(&mut self, handle: FlatHandle, factor: f32, wakeUp: bool) { + self.map_mut(handle, |rb| rb.set_gravity_scale(factor, wakeUp)); + } + + /// Resets to zero all user-added forces added to this rigid-body. + pub fn rbResetForces(&mut self, handle: FlatHandle, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.reset_forces(wakeUp); + }) + } + + /// Resets to zero all user-added torques added to this rigid-body. + pub fn rbResetTorques(&mut self, handle: FlatHandle, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.reset_torques(wakeUp); + }) + } + + /// Adds a force at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `force`: the world-space force to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + pub fn rbAddForce(&mut self, handle: FlatHandle, force: &RawVector, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.add_force(force.0, wakeUp); + }) + } + + /// Applies an impulse at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `impulse`: the world-space impulse to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + pub fn rbApplyImpulse(&mut self, handle: FlatHandle, impulse: &RawVector, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.apply_impulse(impulse.0, wakeUp); + }) + } + + /// Adds a torque at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `torque`: the torque to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + #[cfg(feature = "dim2")] + pub fn rbAddTorque(&mut self, handle: FlatHandle, torque: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.add_torque(torque, wakeUp); + }) + } + + /// Adds a torque at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `torque`: the world-space torque to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + #[cfg(feature = "dim3")] + pub fn rbAddTorque(&mut self, handle: FlatHandle, torque: &RawVector, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.add_torque(torque.0, wakeUp); + }) + } + + /// Applies an impulsive torque at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `torque impulse`: the torque impulse to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + #[cfg(feature = "dim2")] + pub fn rbApplyTorqueImpulse(&mut self, handle: FlatHandle, torque_impulse: f32, wakeUp: bool) { + self.map_mut(handle, |rb| { + rb.apply_torque_impulse(torque_impulse, wakeUp); + }) + } + + /// Applies an impulsive torque at the center-of-mass of this rigid-body. + /// + /// # Parameters + /// - `torque impulse`: the world-space torque impulse to apply on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + #[cfg(feature = "dim3")] + pub fn rbApplyTorqueImpulse( + &mut self, + handle: FlatHandle, + torque_impulse: &RawVector, + wakeUp: bool, + ) { + self.map_mut(handle, |rb| { + rb.apply_torque_impulse(torque_impulse.0, wakeUp); + }) + } + + /// Adds a force at the given world-space point of this rigid-body. + /// + /// # Parameters + /// - `force`: the world-space force to apply on the rigid-body. + /// - `point`: the world-space point where the impulse is to be applied on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + pub fn rbAddForceAtPoint( + &mut self, + handle: FlatHandle, + force: &RawVector, + point: &RawVector, + wakeUp: bool, + ) { + self.map_mut(handle, |rb| { + rb.add_force_at_point(force.0, point.0.into(), wakeUp); + }) + } + + /// Applies an impulse at the given world-space point of this rigid-body. + /// + /// # Parameters + /// - `impulse`: the world-space impulse to apply on the rigid-body. + /// - `point`: the world-space point where the impulse is to be applied on the rigid-body. + /// - `wakeUp`: should the rigid-body be automatically woken-up? + pub fn rbApplyImpulseAtPoint( + &mut self, + handle: FlatHandle, + impulse: &RawVector, + point: &RawVector, + wakeUp: bool, + ) { + self.map_mut(handle, |rb| { + rb.apply_impulse_at_point(impulse.0, point.0.into(), wakeUp); + }) + } + + pub fn rbAdditionalSolverIterations(&self, handle: FlatHandle) -> usize { + self.map(handle, |rb| rb.additional_solver_iterations()) + } + + pub fn rbSetAdditionalSolverIterations(&mut self, handle: FlatHandle, iters: usize) { + self.map_mut(handle, |rb| { + rb.set_additional_solver_iterations(iters as usize); + }) + } + + /// An arbitrary user-defined 32-bit integer + pub fn rbUserData(&self, handle: FlatHandle) -> u32 { + self.map(handle, |rb| rb.user_data as u32) + } + + /// Sets the user-defined 32-bit integer of this rigid-body. + /// + /// # Parameters + /// - `data`: an arbitrary user-defined 32-bit integer. + pub fn rbSetUserData(&mut self, handle: FlatHandle, data: u32) { + self.map_mut(handle, |rb| { + rb.user_data = data as u128; + }) + } + + /// Retrieves the constant force(s) the user added to this rigid-body. + /// Returns zero if the rigid-body is not dynamic. + pub fn rbUserForce(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| rb.user_force().into()) + } + + /// Retrieves the constant torque(s) the user added to this rigid-body. + /// Returns zero if the rigid-body is not dynamic. + #[cfg(feature = "dim2")] + pub fn rbUserTorque(&self, handle: FlatHandle) -> f32 { + self.map(handle, |rb| rb.user_torque()) + } + + /// Retrieves the constant torque(s) the user added to this rigid-body. + /// Returns zero if the rigid-body is not dynamic. + #[cfg(feature = "dim3")] + pub fn rbUserTorque(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |rb| rb.user_torque().into()) + } +} diff --git a/thirdparty/rapier.js/src/dynamics/rigid_body_set.rs b/thirdparty/rapier.js/src/dynamics/rigid_body_set.rs new file mode 100644 index 00000000..83b963e3 --- /dev/null +++ b/thirdparty/rapier.js/src/dynamics/rigid_body_set.rs @@ -0,0 +1,237 @@ +use crate::dynamics::{RawImpulseJointSet, RawIslandManager, RawMultibodyJointSet}; +use crate::geometry::RawColliderSet; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use rapier::dynamics::{MassProperties, RigidBody, RigidBodyBuilder, RigidBodySet, RigidBodyType}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub enum RawRigidBodyType { + Dynamic, + Fixed, + KinematicPositionBased, + KinematicVelocityBased, +} + +impl Into for RawRigidBodyType { + fn into(self) -> RigidBodyType { + match self { + RawRigidBodyType::Dynamic => RigidBodyType::Dynamic, + RawRigidBodyType::Fixed => RigidBodyType::Fixed, + RawRigidBodyType::KinematicPositionBased => RigidBodyType::KinematicPositionBased, + RawRigidBodyType::KinematicVelocityBased => RigidBodyType::KinematicVelocityBased, + } + } +} + +impl Into for RigidBodyType { + fn into(self) -> RawRigidBodyType { + match self { + RigidBodyType::Dynamic => RawRigidBodyType::Dynamic, + RigidBodyType::Fixed => RawRigidBodyType::Fixed, + RigidBodyType::KinematicPositionBased => RawRigidBodyType::KinematicPositionBased, + RigidBodyType::KinematicVelocityBased => RawRigidBodyType::KinematicVelocityBased, + } + } +} + +#[wasm_bindgen] +pub struct RawRigidBodySet(pub(crate) RigidBodySet); + +impl RawRigidBodySet { + pub(crate) fn map(&self, handle: FlatHandle, f: impl FnOnce(&RigidBody) -> T) -> T { + let body = self.0.get(utils::body_handle(handle)).expect( + "Invalid RigidBody reference. It may have been removed from the physics World.", + ); + f(body) + } + + pub(crate) fn map_mut( + &mut self, + handle: FlatHandle, + f: impl FnOnce(&mut RigidBody) -> T, + ) -> T { + let body = self.0.get_mut(utils::body_handle(handle)).expect( + "Invalid RigidBody reference. It may have been removed from the physics World.", + ); + f(body) + } +} + +#[wasm_bindgen] +impl RawRigidBodySet { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawRigidBodySet(RigidBodySet::new()) + } + + #[cfg(feature = "dim3")] + pub fn createRigidBody( + &mut self, + enabled: bool, + translation: &RawVector, + rotation: &RawRotation, + gravityScale: f32, + mass: f32, + massOnly: bool, + centerOfMass: &RawVector, + linvel: &RawVector, + angvel: &RawVector, + principalAngularInertia: &RawVector, + angularInertiaFrame: &RawRotation, + translationEnabledX: bool, + translationEnabledY: bool, + translationEnabledZ: bool, + rotationEnabledX: bool, + rotationEnabledY: bool, + rotationEnabledZ: bool, + linearDamping: f32, + angularDamping: f32, + rb_type: RawRigidBodyType, + canSleep: bool, + sleeping: bool, + softCcdPrediction: f32, + ccdEnabled: bool, + dominanceGroup: i8, + additional_solver_iterations: usize, + ) -> FlatHandle { + let pos = na::Isometry3::from_parts(translation.0.into(), rotation.0); + + let mut rigid_body = RigidBodyBuilder::new(rb_type.into()) + .enabled(enabled) + .pose(pos) + .gravity_scale(gravityScale) + .enabled_translations( + translationEnabledX, + translationEnabledY, + translationEnabledZ, + ) + .enabled_rotations(rotationEnabledX, rotationEnabledY, rotationEnabledZ) + .linvel(linvel.0) + .angvel(angvel.0) + .linear_damping(linearDamping) + .angular_damping(angularDamping) + .can_sleep(canSleep) + .sleeping(sleeping) + .ccd_enabled(ccdEnabled) + .dominance_group(dominanceGroup) + .additional_solver_iterations(additional_solver_iterations) + .soft_ccd_prediction(softCcdPrediction); + + rigid_body = if massOnly { + rigid_body.additional_mass(mass) + } else { + let props = MassProperties::with_principal_inertia_frame( + centerOfMass.0.into(), + mass, + principalAngularInertia.0, + angularInertiaFrame.0, + ); + rigid_body.additional_mass_properties(props) + }; + + utils::flat_handle(self.0.insert(rigid_body.build()).0) + } + + #[cfg(feature = "dim2")] + pub fn createRigidBody( + &mut self, + enabled: bool, + translation: &RawVector, + rotation: &RawRotation, + gravityScale: f32, + mass: f32, + massOnly: bool, + centerOfMass: &RawVector, + linvel: &RawVector, + angvel: f32, + principalAngularInertia: f32, + translationEnabledX: bool, + translationEnabledY: bool, + rotationsEnabled: bool, + linearDamping: f32, + angularDamping: f32, + rb_type: RawRigidBodyType, + canSleep: bool, + sleeping: bool, + softCcdPrediciton: f32, + ccdEnabled: bool, + dominanceGroup: i8, + additional_solver_iterations: usize, + ) -> FlatHandle { + let pos = na::Isometry2::from_parts(translation.0.into(), rotation.0); + let mut rigid_body = RigidBodyBuilder::new(rb_type.into()) + .enabled(enabled) + .pose(pos) + .gravity_scale(gravityScale) + .enabled_translations(translationEnabledX, translationEnabledY) + .linvel(linvel.0) + .angvel(angvel) + .linear_damping(linearDamping) + .angular_damping(angularDamping) + .can_sleep(canSleep) + .sleeping(sleeping) + .ccd_enabled(ccdEnabled) + .dominance_group(dominanceGroup) + .additional_solver_iterations(additional_solver_iterations) + .soft_ccd_prediction(softCcdPrediciton); + + rigid_body = if massOnly { + rigid_body.additional_mass(mass) + } else { + let props = MassProperties::new(centerOfMass.0.into(), mass, principalAngularInertia); + rigid_body.additional_mass_properties(props) + }; + + if !rotationsEnabled { + rigid_body = rigid_body.lock_rotations(); + } + + utils::flat_handle(self.0.insert(rigid_body.build()).0) + } + + pub fn remove( + &mut self, + handle: FlatHandle, + islands: &mut RawIslandManager, + colliders: &mut RawColliderSet, + joints: &mut RawImpulseJointSet, + articulations: &mut RawMultibodyJointSet, + ) { + let handle = utils::body_handle(handle); + self.0.remove( + handle, + &mut islands.0, + &mut colliders.0, + &mut joints.0, + &mut articulations.0, + true, + ); + } + + /// The number of rigid-bodies on this set. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Checks if a rigid-body with the given integer handle exists. + pub fn contains(&self, handle: FlatHandle) -> bool { + self.0.get(utils::body_handle(handle)).is_some() + } + + /// Applies the given JavaScript function to the integer handle of each rigid-body managed by this set. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each rigid-body managed by this set. Called as `f(collider)`. + pub fn forEachRigidBodyHandle(&self, f: &js_sys::Function) { + let this = JsValue::null(); + for (handle, _) in self.0.iter() { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } + + pub fn propagateModifiedBodyPositionsToColliders(&mut self, colliders: &mut RawColliderSet) { + self.0 + .propagate_modified_body_positions_to_colliders(&mut colliders.0); + } +} diff --git a/thirdparty/rapier.js/src/geometry/broad_phase.rs b/thirdparty/rapier.js/src/geometry/broad_phase.rs new file mode 100644 index 00000000..6f81a862 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/broad_phase.rs @@ -0,0 +1,462 @@ +use crate::dynamics::RawRigidBodySet; +use crate::geometry::{ + RawColliderSet, RawColliderShapeCastHit, RawNarrowPhase, RawPointColliderProjection, + RawRayColliderHit, RawRayColliderIntersection, RawShape, +}; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use rapier::geometry::DefaultBroadPhase; +use rapier::geometry::{Aabb, ColliderHandle, Ray}; +use rapier::math::{Isometry, Point}; +use rapier::parry::query::ShapeCastOptions; +use rapier::pipeline::{QueryFilter, QueryFilterFlags}; +use rapier::prelude::FeatureId; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawBroadPhase(pub(crate) DefaultBroadPhase); + +#[wasm_bindgen] +impl RawBroadPhase { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawBroadPhase(DefaultBroadPhase::new()) + } + + pub fn castRay( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + let (handle, timeOfImpact) = utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let ray = Ray::new(rayOrig.0.into(), rayDir.0); + query_pipeline.cast_ray(&ray, maxToi, solid) + })?; + + Some(RawRayColliderHit { + handle, + timeOfImpact, + }) + } + + pub fn castRayAndGetNormal( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + let (handle, inter) = utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let ray = Ray::new(rayOrig.0.into(), rayDir.0); + query_pipeline.cast_ray_and_get_normal(&ray, maxToi, solid) + })?; + + Some(RawRayColliderIntersection { handle, inter }) + } + + // The callback is of type (RawRayColliderIntersection) => bool + pub fn intersectionsWithRay( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + callback: &js_sys::Function, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let ray = Ray::new(rayOrig.0.into(), rayDir.0); + let rcallback = |handle, inter| { + let result = RawRayColliderIntersection { handle, inter }; + match callback.call1(&JsValue::null(), &JsValue::from(result)) { + Err(_) => true, + Ok(val) => val.as_bool().unwrap_or(true), + } + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + for (handle, _, inter) in query_pipeline.intersect_ray(ray, maxToi, solid) { + if !rcallback(handle, inter) { + break; + } + } + }); + } + + pub fn intersectionWithShape( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + shapePos: &RawVector, + shapeRot: &RawRotation, + shape: &RawShape, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + // TODO: take a callback as argument so we can yield all the intersecting shapes? + for (handle, _) in query_pipeline.intersect_shape(pos, &*shape.0) { + // Return the first intersection we find. + return Some(utils::flat_handle(handle.0)); + } + + None + }) + } + + pub fn projectPoint( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + point: &RawVector, + solid: bool, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + query_pipeline + .project_point(&point.0.into(), f32::MAX, solid) + .map(|(handle, proj)| RawPointColliderProjection { + handle, + proj, + feature: FeatureId::Unknown, + }) + }) + } + + pub fn projectPointAndGetFeature( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + point: &RawVector, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + query_pipeline + .project_point_and_get_feature(&point.0.into()) + .map(|(handle, proj, feature)| RawPointColliderProjection { + handle, + proj, + feature, + }) + }) + } + + // The callback is of type (u32) => bool + pub fn intersectionsWithPoint( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + point: &RawVector, + callback: &js_sys::Function, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let rcallback = |handle: ColliderHandle| match callback.call1( + &JsValue::null(), + &JsValue::from(utils::flat_handle(handle.0)), + ) { + Err(_) => true, + Ok(val) => val.as_bool().unwrap_or(true), + }; + + for (handle, _) in query_pipeline.intersect_point(point.0.into()) { + if !rcallback(handle) { + break; + } + } + }); + } + + pub fn castShape( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + shapePos: &RawVector, + shapeRot: &RawRotation, + shapeVel: &RawVector, + shape: &RawShape, + target_distance: f32, + maxToi: f32, + stop_at_penetration: bool, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) -> Option { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + query_pipeline + .cast_shape( + &pos, + &shapeVel.0, + &*shape.0, + ShapeCastOptions { + max_time_of_impact: maxToi, + stop_at_penetration, + compute_impact_geometry_on_penetration: true, + target_distance, + }, + ) + .map(|(handle, hit)| RawColliderShapeCastHit { handle, hit }) + }) + } + + // The callback has type (u32) => boolean + pub fn intersectionsWithShape( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + shapePos: &RawVector, + shapeRot: &RawRotation, + shape: &RawShape, + callback: &js_sys::Function, + filter_flags: u32, + filter_groups: Option, + filter_exclude_collider: Option, + filter_exclude_rigid_body: Option, + filter_predicate: &js_sys::Function, + ) { + utils::with_filter(filter_predicate, |predicate| { + let query_filter = QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: filter_groups.map(crate::geometry::unpack_interaction_groups), + exclude_collider: filter_exclude_collider.map(crate::utils::collider_handle), + exclude_rigid_body: filter_exclude_rigid_body.map(crate::utils::body_handle), + predicate, + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + query_filter, + ); + + let rcallback = |handle: ColliderHandle| match callback.call1( + &JsValue::null(), + &JsValue::from(utils::flat_handle(handle.0)), + ) { + Err(_) => true, + Ok(val) => val.as_bool().unwrap_or(true), + }; + + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + for (handle, _) in query_pipeline.intersect_shape(pos, &*shape.0) { + if !rcallback(handle) { + break; + } + } + }) + } + + pub fn collidersWithAabbIntersectingAabb( + &self, + narrow_phase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + aabbCenter: &RawVector, + aabbHalfExtents: &RawVector, + callback: &js_sys::Function, + ) { + let rcallback = |handle: &ColliderHandle| match callback.call1( + &JsValue::null(), + &JsValue::from(utils::flat_handle(handle.0)), + ) { + Err(_) => true, + Ok(val) => val.as_bool().unwrap_or(true), + }; + + let query_pipeline = self.0.as_query_pipeline( + narrow_phase.0.query_dispatcher(), + &bodies.0, + &colliders.0, + Default::default(), + ); + + let center = Point::from(aabbCenter.0); + let aabb = Aabb::new(center - aabbHalfExtents.0, center + aabbHalfExtents.0); + + for (handle, _) in query_pipeline.intersect_aabb_conservative(aabb) { + if !rcallback(&handle) { + break; + } + } + } +} diff --git a/thirdparty/rapier.js/src/geometry/collider.rs b/thirdparty/rapier.js/src/geometry/collider.rs new file mode 100644 index 00000000..e8019044 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/collider.rs @@ -0,0 +1,993 @@ +use crate::geometry::shape::SharedShapeUtility; +use crate::geometry::{ + RawColliderSet, RawColliderShapeCastHit, RawPointProjection, RawRayIntersection, RawShape, + RawShapeCastHit, RawShapeContact, RawShapeType, +}; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use rapier::dynamics::MassProperties; +use rapier::geometry::{ActiveCollisionTypes, ShapeType}; +use rapier::math::{Isometry, Point, Real, Vector}; +use rapier::parry::query; +use rapier::parry::query::ShapeCastOptions; +use rapier::pipeline::{ActiveEvents, ActiveHooks}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +impl RawColliderSet { + /// The world-space translation of this collider. + pub fn coTranslation(&self, handle: FlatHandle) -> RawVector { + self.map(handle, |co| co.position().translation.vector.into()) + } + + /// The world-space orientation of this collider. + pub fn coRotation(&self, handle: FlatHandle) -> RawRotation { + self.map(handle, |co| co.position().rotation.into()) + } + + /// The translation of this collider relative to its parent rigid-body. + /// + /// Returns the `None` if it doesn’t have a parent. + pub fn coTranslationWrtParent(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.position_wrt_parent() + .map(|pose| pose.translation.vector.into()) + }) + } + + /// The orientation of this collider relative to its parent rigid-body. + /// + /// Returns the `None` if it doesn’t have a parent. + pub fn coRotationWrtParent(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.position_wrt_parent().map(|pose| pose.rotation.into()) + }) + } + + /// Sets the translation of this collider. + /// + /// # Parameters + /// - `x`: the world-space position of the collider along the `x` axis. + /// - `y`: the world-space position of the collider along the `y` axis. + /// - `z`: the world-space position of the collider along the `z` axis. + /// - `wakeUp`: forces the collider to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim3")] + pub fn coSetTranslation(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32) { + self.map_mut(handle, |co| { + co.set_translation(na::Vector3::new(x, y, z)); + }) + } + + /// Sets the translation of this collider. + /// + /// # Parameters + /// - `x`: the world-space position of the collider along the `x` axis. + /// - `y`: the world-space position of the collider along the `y` axis. + /// - `wakeUp`: forces the collider to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim2")] + pub fn coSetTranslation(&mut self, handle: FlatHandle, x: f32, y: f32) { + self.map_mut(handle, |co| { + co.set_translation(na::Vector2::new(x, y)); + }) + } + + #[cfg(feature = "dim3")] + pub fn coSetTranslationWrtParent(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32) { + self.map_mut(handle, |co| { + co.set_translation_wrt_parent(na::Vector3::new(x, y, z)); + }) + } + + #[cfg(feature = "dim2")] + pub fn coSetTranslationWrtParent(&mut self, handle: FlatHandle, x: f32, y: f32) { + self.map_mut(handle, |co| { + co.set_translation_wrt_parent(na::Vector2::new(x, y)); + }) + } + + /// Sets the rotation quaternion of this collider. + /// + /// This does nothing if a zero quaternion is provided. + /// + /// # Parameters + /// - `x`: the first vector component of the quaternion. + /// - `y`: the second vector component of the quaternion. + /// - `z`: the third vector component of the quaternion. + /// - `w`: the scalar component of the quaternion. + /// - `wakeUp`: forces the collider to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim3")] + pub fn coSetRotation(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32, w: f32) { + if let Some(q) = na::Unit::try_new(na::Quaternion::new(w, x, y, z), 0.0) { + self.map_mut(handle, |co| co.set_rotation(q)) + } + } + + /// Sets the rotation angle of this collider. + /// + /// # Parameters + /// - `angle`: the rotation angle, in radians. + /// - `wakeUp`: forces the collider to wake-up so it is properly affected by forces if it + /// wasn't moving before modifying its position. + #[cfg(feature = "dim2")] + pub fn coSetRotation(&mut self, handle: FlatHandle, angle: f32) { + self.map_mut(handle, |co| co.set_rotation(na::UnitComplex::new(angle))) + } + + #[cfg(feature = "dim3")] + pub fn coSetRotationWrtParent(&mut self, handle: FlatHandle, x: f32, y: f32, z: f32, w: f32) { + if let Some(q) = na::Unit::try_new(na::Quaternion::new(w, x, y, z), 0.0) { + self.map_mut(handle, |co| co.set_rotation_wrt_parent(q.scaled_axis())) + } + } + + #[cfg(feature = "dim2")] + pub fn coSetRotationWrtParent(&mut self, handle: FlatHandle, angle: f32) { + self.map_mut(handle, |co| co.set_rotation_wrt_parent(angle)) + } + + /// Is this collider a sensor? + pub fn coIsSensor(&self, handle: FlatHandle) -> bool { + self.map(handle, |co| co.is_sensor()) + } + + /// The type of the shape of this collider. + pub fn coShapeType(&self, handle: FlatHandle) -> RawShapeType { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::Ball => RawShapeType::Ball, + ShapeType::Cuboid => RawShapeType::Cuboid, + ShapeType::Capsule => RawShapeType::Capsule, + ShapeType::Segment => RawShapeType::Segment, + ShapeType::Polyline => RawShapeType::Polyline, + ShapeType::Triangle => RawShapeType::Triangle, + ShapeType::TriMesh => RawShapeType::TriMesh, + ShapeType::HeightField => RawShapeType::HeightField, + ShapeType::Compound => RawShapeType::Compound, + ShapeType::HalfSpace => RawShapeType::HalfSpace, + ShapeType::Voxels => RawShapeType::Voxels, + #[cfg(feature = "dim3")] + ShapeType::ConvexPolyhedron => RawShapeType::ConvexPolyhedron, + #[cfg(feature = "dim2")] + ShapeType::ConvexPolygon => RawShapeType::ConvexPolygon, + #[cfg(feature = "dim3")] + ShapeType::Cylinder => RawShapeType::Cylinder, + #[cfg(feature = "dim3")] + ShapeType::Cone => RawShapeType::Cone, + ShapeType::RoundCuboid => RawShapeType::RoundCuboid, + ShapeType::RoundTriangle => RawShapeType::RoundTriangle, + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => RawShapeType::RoundCylinder, + #[cfg(feature = "dim3")] + ShapeType::RoundCone => RawShapeType::RoundCone, + #[cfg(feature = "dim3")] + ShapeType::RoundConvexPolyhedron => RawShapeType::RoundConvexPolyhedron, + #[cfg(feature = "dim2")] + ShapeType::RoundConvexPolygon => RawShapeType::RoundConvexPolygon, + ShapeType::Custom => panic!("Not yet implemented."), + }) + } + + pub fn coHalfspaceNormal(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.shape() + .as_halfspace() + .map(|h| h.normal.into_inner().into()) + }) + } + + /// The half-extents of this collider if it is has a cuboid shape. + pub fn coHalfExtents(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.shape() + .as_cuboid() + .map(|c| c.half_extents.into()) + .or_else(|| { + co.shape() + .as_round_cuboid() + .map(|c| c.inner_shape.half_extents.into()) + }) + }) + } + + /// Set the half-extents of this collider if it has a cuboid shape. + pub fn coSetHalfExtents(&mut self, handle: FlatHandle, newHalfExtents: &RawVector) { + self.map_mut(handle, |co| match co.shape().shape_type() { + ShapeType::Cuboid => co + .shape_mut() + .as_cuboid_mut() + .map(|b| b.half_extents = newHalfExtents.0.into()), + ShapeType::RoundCuboid => co + .shape_mut() + .as_round_cuboid_mut() + .map(|b| b.inner_shape.half_extents = newHalfExtents.0.into()), + _ => None, + }); + } + + /// The radius of this collider if it is a ball, capsule, cylinder, or cone shape. + pub fn coRadius(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::Ball => co.shape().as_ball().map(|b| b.radius), + ShapeType::Capsule => co.shape().as_capsule().map(|b| b.radius), + #[cfg(feature = "dim3")] + ShapeType::Cylinder => co.shape().as_cylinder().map(|b| b.radius), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => { + co.shape().as_round_cylinder().map(|b| b.inner_shape.radius) + } + #[cfg(feature = "dim3")] + ShapeType::Cone => co.shape().as_cone().map(|b| b.radius), + _ => None, + }) + } + + /// Set the radius of this collider if it is a ball, capsule, cylinder, or cone shape. + pub fn coSetRadius(&mut self, handle: FlatHandle, newRadius: Real) { + self.map_mut(handle, |co| match co.shape().shape_type() { + ShapeType::Ball => co.shape_mut().as_ball_mut().map(|b| b.radius = newRadius), + ShapeType::Capsule => co + .shape_mut() + .as_capsule_mut() + .map(|b| b.radius = newRadius), + #[cfg(feature = "dim3")] + ShapeType::Cylinder => co + .shape_mut() + .as_cylinder_mut() + .map(|b| b.radius = newRadius), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => co + .shape_mut() + .as_round_cylinder_mut() + .map(|b| b.inner_shape.radius = newRadius), + #[cfg(feature = "dim3")] + ShapeType::Cone => co.shape_mut().as_cone_mut().map(|b| b.radius = newRadius), + _ => None, + }); + } + + /// The half height of this collider if it is a capsule, cylinder, or cone shape. + pub fn coHalfHeight(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::Capsule => co.shape().as_capsule().map(|b| b.half_height()), + #[cfg(feature = "dim3")] + ShapeType::Cylinder => co.shape().as_cylinder().map(|b| b.half_height), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => co + .shape() + .as_round_cylinder() + .map(|b| b.inner_shape.half_height), + #[cfg(feature = "dim3")] + ShapeType::Cone => co.shape().as_cone().map(|b| b.half_height), + _ => None, + }) + } + + /// Set the half height of this collider if it is a capsule, cylinder, or cone shape. + pub fn coSetHalfHeight(&mut self, handle: FlatHandle, newHalfheight: Real) { + self.map_mut(handle, |co| match co.shape().shape_type() { + ShapeType::Capsule => { + let point = Point::from(Vector::y() * newHalfheight); + co.shape_mut().as_capsule_mut().map(|b| { + b.segment.a = -point; + b.segment.b = point; + }) + } + #[cfg(feature = "dim3")] + ShapeType::Cylinder => co + .shape_mut() + .as_cylinder_mut() + .map(|b| b.half_height = newHalfheight), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => co + .shape_mut() + .as_round_cylinder_mut() + .map(|b| b.inner_shape.half_height = newHalfheight), + #[cfg(feature = "dim3")] + ShapeType::Cone => co + .shape_mut() + .as_cone_mut() + .map(|b| b.half_height = newHalfheight), + _ => None, + }); + } + + /// The radius of the round edges of this collider. + pub fn coRoundRadius(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::RoundCuboid => co.shape().as_round_cuboid().map(|b| b.border_radius), + ShapeType::RoundTriangle => co.shape().as_round_triangle().map(|b| b.border_radius), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => co.shape().as_round_cylinder().map(|b| b.border_radius), + #[cfg(feature = "dim3")] + ShapeType::RoundCone => co.shape().as_round_cone().map(|b| b.border_radius), + #[cfg(feature = "dim3")] + ShapeType::RoundConvexPolyhedron => co + .shape() + .as_round_convex_polyhedron() + .map(|b| b.border_radius), + #[cfg(feature = "dim2")] + ShapeType::RoundConvexPolygon => co + .shape() + .as_round_convex_polygon() + .map(|b| b.border_radius), + _ => None, + }) + } + + /// Set the radius of the round edges of this collider. + pub fn coSetRoundRadius(&mut self, handle: FlatHandle, newBorderRadius: Real) { + self.map_mut(handle, |co| match co.shape().shape_type() { + ShapeType::RoundCuboid => co + .shape_mut() + .as_round_cuboid_mut() + .map(|b| b.border_radius = newBorderRadius), + ShapeType::RoundTriangle => co + .shape_mut() + .as_round_triangle_mut() + .map(|b| b.border_radius = newBorderRadius), + #[cfg(feature = "dim3")] + ShapeType::RoundCylinder => co + .shape_mut() + .as_round_cylinder_mut() + .map(|b| b.border_radius = newBorderRadius), + #[cfg(feature = "dim3")] + ShapeType::RoundCone => co + .shape_mut() + .as_round_cone_mut() + .map(|b| b.border_radius = newBorderRadius), + #[cfg(feature = "dim3")] + ShapeType::RoundConvexPolyhedron => co + .shape_mut() + .as_round_convex_polyhedron_mut() + .map(|b| b.border_radius = newBorderRadius), + #[cfg(feature = "dim2")] + ShapeType::RoundConvexPolygon => co + .shape_mut() + .as_round_convex_polygon_mut() + .map(|b| b.border_radius = newBorderRadius), + _ => None, + }); + } + + pub fn coVoxelData(&self, handle: FlatHandle) -> Option> { + self.map(handle, |co| { + let vox = co.shape().as_voxels()?; + let coords = vox + .voxels() + .filter_map(|vox| (!vox.state.is_empty()).then_some(vox.grid_coords)) + .flat_map(|ids| ids.coords.data.0[0]) + .collect(); + Some(coords) + }) + } + + pub fn coVoxelSize(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + let vox = co.shape().as_voxels()?; + Some(RawVector(vox.voxel_size())) + }) + } + + #[cfg(feature = "dim2")] + pub fn coSetVoxel(&mut self, handle: FlatHandle, ix: i32, iy: i32, filled: bool) { + self.map_mut(handle, |co| { + if let Some(vox) = co.shape_mut().as_voxels_mut() { + vox.set_voxel(Point::new(ix, iy), filled); + } + }) + } + + #[cfg(feature = "dim3")] + pub fn coSetVoxel(&mut self, handle: FlatHandle, ix: i32, iy: i32, iz: i32, filled: bool) { + self.map_mut(handle, |co| { + if let Some(vox) = co.shape_mut().as_voxels_mut() { + vox.set_voxel(Point::new(ix, iy, iz), filled); + } + }) + } + + #[cfg(feature = "dim2")] + pub fn coPropagateVoxelChange( + &mut self, + handle1: FlatHandle, + handle2: FlatHandle, + ix: i32, + iy: i32, + shift_x: i32, + shift_y: i32, + ) { + self.map_pair_mut(handle1, handle2, |co1, co2| { + if let (Some(co1), Some(co2)) = (co1, co2) { + if let (Some(vox1), Some(vox2)) = ( + co1.shape_mut().as_voxels_mut(), + co2.shape_mut().as_voxels_mut(), + ) { + vox1.propagate_voxel_change( + vox2, + Point::new(ix, iy), + Vector::new(shift_x, shift_y), + ); + } + } + }) + } + + #[cfg(feature = "dim3")] + pub fn coPropagateVoxelChange( + &mut self, + handle1: FlatHandle, + handle2: FlatHandle, + ix: i32, + iy: i32, + iz: i32, + shift_x: i32, + shift_y: i32, + shift_z: i32, + ) { + self.map_pair_mut(handle1, handle2, |co1, co2| { + if let (Some(co1), Some(co2)) = (co1, co2) { + if let (Some(vox1), Some(vox2)) = ( + co1.shape_mut().as_voxels_mut(), + co2.shape_mut().as_voxels_mut(), + ) { + vox1.propagate_voxel_change( + vox2, + Point::new(ix, iy, iz), + Vector::new(shift_x, shift_y, shift_z), + ); + } + } + }) + } + + #[cfg(feature = "dim2")] + pub fn coCombineVoxelStates( + &mut self, + handle1: FlatHandle, + handle2: FlatHandle, + shift_x: i32, + shift_y: i32, + ) { + self.map_pair_mut(handle1, handle2, |co1, co2| { + if let (Some(co1), Some(co2)) = (co1, co2) { + if let (Some(vox1), Some(vox2)) = ( + co1.shape_mut().as_voxels_mut(), + co2.shape_mut().as_voxels_mut(), + ) { + vox1.combine_voxel_states(vox2, Vector::new(shift_x, shift_y)); + } + } + }) + } + + #[cfg(feature = "dim3")] + pub fn coCombineVoxelStates( + &mut self, + handle1: FlatHandle, + handle2: FlatHandle, + shift_x: i32, + shift_y: i32, + shift_z: i32, + ) { + self.map_pair_mut(handle1, handle2, |co1, co2| { + if let (Some(co1), Some(co2)) = (co1, co2) { + if let (Some(vox1), Some(vox2)) = ( + co1.shape_mut().as_voxels_mut(), + co2.shape_mut().as_voxels_mut(), + ) { + vox1.combine_voxel_states(vox2, Vector::new(shift_x, shift_y, shift_z)); + } + } + }) + } + + /// The vertices of this triangle mesh, polyline, convex polyhedron, segment, triangle or convex polyhedron, if it is one. + pub fn coVertices(&self, handle: FlatHandle) -> Option> { + let flatten = + |vertices: &[Point]| vertices.iter().flat_map(|p| p.iter()).copied().collect(); + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::TriMesh => co.shape().as_trimesh().map(|t| flatten(t.vertices())), + #[cfg(feature = "dim2")] + ShapeType::Polyline => co.shape().as_polyline().map(|p| flatten(p.vertices())), + #[cfg(feature = "dim3")] + ShapeType::ConvexPolyhedron => co + .shape() + .as_convex_polyhedron() + .map(|p| flatten(p.points())), + #[cfg(feature = "dim3")] + ShapeType::RoundConvexPolyhedron => co + .shape() + .as_round_convex_polyhedron() + .map(|p| flatten(p.inner_shape.points())), + #[cfg(feature = "dim2")] + ShapeType::ConvexPolygon => co.shape().as_convex_polygon().map(|p| flatten(p.points())), + #[cfg(feature = "dim2")] + ShapeType::RoundConvexPolygon => co + .shape() + .as_round_convex_polygon() + .map(|p| flatten(p.inner_shape.points())), + ShapeType::Segment => co.shape().as_segment().map(|s| flatten(&[s.a, s.b])), + ShapeType::RoundTriangle => co + .shape() + .as_round_triangle() + .map(|t| flatten(&[t.inner_shape.a, t.inner_shape.b, t.inner_shape.c])), + ShapeType::Triangle => co.shape().as_triangle().map(|t| flatten(&[t.a, t.b, t.c])), + _ => None, + }) + } + + /// The indices of this triangle mesh, polyline, or convex polyhedron, if it is one. + pub fn coIndices(&self, handle: FlatHandle) -> Option> { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::TriMesh => co + .shape() + .as_trimesh() + .map(|t| t.indices().iter().flat_map(|p| p.iter()).copied().collect()), + ShapeType::Polyline => co + .shape() + .as_polyline() + .map(|p| p.indices().iter().flat_map(|p| p.iter()).copied().collect()), + #[cfg(feature = "dim3")] + ShapeType::ConvexPolyhedron => co.shape().as_convex_polyhedron().map(|p| { + // TODO: avoid the `.to_trimesh()`. + p.to_trimesh() + .1 + .iter() + .flat_map(|p| p.iter()) + .copied() + .collect() + }), + #[cfg(feature = "dim3")] + ShapeType::RoundConvexPolyhedron => co.shape().as_round_convex_polyhedron().map(|p| { + // TODO: avoid the `.to_trimesh()`. + p.inner_shape + .to_trimesh() + .1 + .iter() + .flat_map(|p| p.iter()) + .copied() + .collect() + }), + _ => None, + }) + } + + pub fn coTriMeshFlags(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.shape().as_trimesh().map(|tri| tri.flags().bits() as u32) + }) + } + + #[cfg(feature = "dim3")] + pub fn coHeightFieldFlags(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| { + co.shape() + .as_heightfield() + .map(|hf| hf.flags().bits() as u32) + }) + } + + /// The height of this heightfield if it is one. + pub fn coHeightfieldHeights(&self, handle: FlatHandle) -> Option> { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::HeightField => co + .shape() + .as_heightfield() + .map(|h| h.heights().as_slice().to_vec()), + _ => None, + }) + } + + /// The scaling factor applied of this heightfield if it is one. + pub fn coHeightfieldScale(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::HeightField => co.shape().as_heightfield().map(|h| RawVector(*h.scale())), + _ => None, + }) + } + + /// The number of rows on this heightfield's height matrix, if it is one. + #[cfg(feature = "dim3")] + pub fn coHeightfieldNRows(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::HeightField => co.shape().as_heightfield().map(|h| h.nrows()), + _ => None, + }) + } + + /// The number of columns on this heightfield's height matrix, if it is one. + #[cfg(feature = "dim3")] + pub fn coHeightfieldNCols(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| match co.shape().shape_type() { + ShapeType::HeightField => co.shape().as_heightfield().map(|h| h.ncols()), + _ => None, + }) + } + + /// The unique integer identifier of the collider this collider is attached to. + pub fn coParent(&self, handle: FlatHandle) -> Option { + self.map(handle, |co| co.parent().map(|p| utils::flat_handle(p.0))) + } + + pub fn coSetEnabled(&mut self, handle: FlatHandle, enabled: bool) { + self.map_mut(handle, |co| co.set_enabled(enabled)) + } + + pub fn coIsEnabled(&self, handle: FlatHandle) -> bool { + self.map(handle, |co| co.is_enabled()) + } + + pub fn coSetContactSkin(&mut self, handle: FlatHandle, contact_skin: f32) { + self.map_mut(handle, |co| co.set_contact_skin(contact_skin)) + } + + pub fn coContactSkin(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.contact_skin()) + } + + /// The friction coefficient of this collider. + pub fn coFriction(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.material().friction) + } + /// The restitution coefficient of this collider. + pub fn coRestitution(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.material().restitution) + } + + /// The density of this collider. + pub fn coDensity(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.density()) + } + + /// The mass of this collider. + pub fn coMass(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.mass()) + } + + /// The volume of this collider. + pub fn coVolume(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.volume()) + } + + /// The collision groups of this collider. + pub fn coCollisionGroups(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| { + super::pack_interaction_groups(co.collision_groups()) + }) + } + + /// The solver groups of this collider. + pub fn coSolverGroups(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| { + super::pack_interaction_groups(co.solver_groups()) + }) + } + + /// The physics hooks enabled for this collider. + pub fn coActiveHooks(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| co.active_hooks().bits()) + } + + /// The collision types enabled for this collider. + pub fn coActiveCollisionTypes(&self, handle: FlatHandle) -> u16 { + self.map(handle, |co| co.active_collision_types().bits()) + } + + /// The events enabled for this collider. + pub fn coActiveEvents(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| co.active_events().bits()) + } + + /// The total force magnitude beyond which a contact force event can be emitted. + pub fn coContactForceEventThreshold(&self, handle: FlatHandle) -> f32 { + self.map(handle, |co| co.contact_force_event_threshold()) + } + + pub fn coContainsPoint(&self, handle: FlatHandle, point: &RawVector) -> bool { + self.map(handle, |co| { + co.shared_shape() + .containsPoint(co.position(), &point.0.into()) + }) + } + + pub fn coCastShape( + &self, + handle: FlatHandle, + colliderVel: &RawVector, + shape2: &RawShape, + shape2Pos: &RawVector, + shape2Rot: &RawRotation, + shape2Vel: &RawVector, + target_distance: f32, + maxToi: f32, + stop_at_penetration: bool, + ) -> Option { + let pos2 = Isometry::from_parts(shape2Pos.0.into(), shape2Rot.0); + + self.map(handle, |co| { + let pos1 = co.position(); + co.shared_shape().castShape( + pos1, + &colliderVel.0.into(), + &*shape2.0, + &pos2, + &shape2Vel.0.into(), + target_distance, + maxToi, + stop_at_penetration, + ) + }) + } + + pub fn coCastCollider( + &self, + handle: FlatHandle, + collider1Vel: &RawVector, + collider2handle: FlatHandle, + collider2Vel: &RawVector, + target_distance: f32, + max_toi: f32, + stop_at_penetration: bool, + ) -> Option { + let handle2 = utils::collider_handle(collider2handle); + let co2 = self + .0 + .get(handle2) + .expect("Invalid Collider reference. It may have been removed from the physics World."); + + self.map(handle, |co| { + query::cast_shapes( + co.position(), + &collider1Vel.0, + co.shape(), + co2.position(), + &collider2Vel.0, + co2.shape(), + ShapeCastOptions { + max_time_of_impact: max_toi, + stop_at_penetration, + target_distance, + compute_impact_geometry_on_penetration: true, + }, + ) + .unwrap_or(None) + .map_or(None, |hit| { + Some(RawColliderShapeCastHit { + handle: handle2, + hit, + }) + }) + }) + } + + pub fn coIntersectsShape( + &self, + handle: FlatHandle, + shape2: &RawShape, + shapePos2: &RawVector, + shapeRot2: &RawRotation, + ) -> bool { + let pos2 = Isometry::from_parts(shapePos2.0.into(), shapeRot2.0); + + self.map(handle, |co| { + co.shared_shape() + .intersectsShape(co.position(), &*shape2.0, &pos2) + }) + } + + pub fn coContactShape( + &self, + handle: FlatHandle, + shape2: &RawShape, + shapePos2: &RawVector, + shapeRot2: &RawRotation, + prediction: f32, + ) -> Option { + let pos2 = Isometry::from_parts(shapePos2.0.into(), shapeRot2.0); + + self.map(handle, |co| { + co.shared_shape() + .contactShape(co.position(), &*shape2.0, &pos2, prediction) + }) + } + + pub fn coContactCollider( + &self, + handle: FlatHandle, + collider2handle: FlatHandle, + prediction: f32, + ) -> Option { + let co2 = self + .0 + .get(utils::collider_handle(collider2handle)) + .expect("Invalid Collider reference. It may have been removed from the physics World."); + + self.map(handle, |co| { + query::contact( + co.position(), + co.shape(), + &co2.position(), + co2.shape(), + prediction, + ) + .ok() + .flatten() + .map(|contact| RawShapeContact { contact }) + }) + } + + pub fn coProjectPoint( + &self, + handle: FlatHandle, + point: &RawVector, + solid: bool, + ) -> RawPointProjection { + self.map(handle, |co| { + co.shared_shape() + .projectPoint(co.position(), &point.0.into(), solid) + }) + } + + pub fn coIntersectsRay( + &self, + handle: FlatHandle, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + ) -> bool { + self.map(handle, |co| { + co.shared_shape().intersectsRay( + co.position(), + rayOrig.0.into(), + rayDir.0.into(), + maxToi, + ) + }) + } + + pub fn coCastRay( + &self, + handle: FlatHandle, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + ) -> f32 { + self.map(handle, |co| { + co.shared_shape().castRay( + co.position(), + rayOrig.0.into(), + rayDir.0.into(), + maxToi, + solid, + ) + }) + } + + pub fn coCastRayAndGetNormal( + &self, + handle: FlatHandle, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + ) -> Option { + self.map(handle, |co| { + co.shared_shape().castRayAndGetNormal( + co.position(), + rayOrig.0.into(), + rayDir.0.into(), + maxToi, + solid, + ) + }) + } + + pub fn coSetSensor(&mut self, handle: FlatHandle, is_sensor: bool) { + self.map_mut(handle, |co| co.set_sensor(is_sensor)) + } + + pub fn coSetRestitution(&mut self, handle: FlatHandle, restitution: f32) { + self.map_mut(handle, |co| co.set_restitution(restitution)) + } + + pub fn coSetFriction(&mut self, handle: FlatHandle, friction: f32) { + self.map_mut(handle, |co| co.set_friction(friction)) + } + + pub fn coFrictionCombineRule(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| co.friction_combine_rule() as u32) + } + + pub fn coSetFrictionCombineRule(&mut self, handle: FlatHandle, rule: u32) { + let rule = super::combine_rule_from_u32(rule); + self.map_mut(handle, |co| co.set_friction_combine_rule(rule)) + } + + pub fn coRestitutionCombineRule(&self, handle: FlatHandle) -> u32 { + self.map(handle, |co| co.restitution_combine_rule() as u32) + } + + pub fn coSetRestitutionCombineRule(&mut self, handle: FlatHandle, rule: u32) { + let rule = super::combine_rule_from_u32(rule); + self.map_mut(handle, |co| co.set_restitution_combine_rule(rule)) + } + + pub fn coSetCollisionGroups(&mut self, handle: FlatHandle, groups: u32) { + let groups = super::unpack_interaction_groups(groups); + self.map_mut(handle, |co| co.set_collision_groups(groups)) + } + + pub fn coSetSolverGroups(&mut self, handle: FlatHandle, groups: u32) { + let groups = super::unpack_interaction_groups(groups); + self.map_mut(handle, |co| co.set_solver_groups(groups)) + } + + pub fn coSetActiveHooks(&mut self, handle: FlatHandle, hooks: u32) { + let hooks = ActiveHooks::from_bits(hooks).unwrap_or(ActiveHooks::empty()); + self.map_mut(handle, |co| co.set_active_hooks(hooks)); + } + + pub fn coSetActiveEvents(&mut self, handle: FlatHandle, events: u32) { + let events = ActiveEvents::from_bits(events).unwrap_or(ActiveEvents::empty()); + self.map_mut(handle, |co| co.set_active_events(events)) + } + + pub fn coSetActiveCollisionTypes(&mut self, handle: FlatHandle, types: u16) { + let types = ActiveCollisionTypes::from_bits(types).unwrap_or(ActiveCollisionTypes::empty()); + self.map_mut(handle, |co| co.set_active_collision_types(types)); + } + + pub fn coSetShape(&mut self, handle: FlatHandle, shape: &RawShape) { + self.map_mut(handle, |co| co.set_shape(shape.0.clone())); + } + + pub fn coSetContactForceEventThreshold(&mut self, handle: FlatHandle, threshold: f32) { + self.map_mut(handle, |co| co.set_contact_force_event_threshold(threshold)) + } + + pub fn coSetDensity(&mut self, handle: FlatHandle, density: f32) { + self.map_mut(handle, |co| co.set_density(density)) + } + + pub fn coSetMass(&mut self, handle: FlatHandle, mass: f32) { + self.map_mut(handle, |co| co.set_mass(mass)) + } + + #[cfg(feature = "dim3")] + pub fn coSetMassProperties( + &mut self, + handle: FlatHandle, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: &RawVector, + angularInertiaFrame: &RawRotation, + ) { + self.map_mut(handle, |co| { + let mprops = MassProperties::with_principal_inertia_frame( + centerOfMass.0.into(), + mass, + principalAngularInertia.0, + angularInertiaFrame.0, + ); + + co.set_mass_properties(mprops) + }) + } + + #[cfg(feature = "dim2")] + pub fn coSetMassProperties( + &mut self, + handle: FlatHandle, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: f32, + ) { + self.map_mut(handle, |co| { + let props = MassProperties::new(centerOfMass.0.into(), mass, principalAngularInertia); + co.set_mass_properties(props) + }) + } +} diff --git a/thirdparty/rapier.js/src/geometry/collider_set.rs b/thirdparty/rapier.js/src/geometry/collider_set.rs new file mode 100644 index 00000000..6e31d8eb --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/collider_set.rs @@ -0,0 +1,293 @@ +use crate::dynamics::{RawIslandManager, RawRigidBodySet}; +use crate::geometry::RawShape; +use crate::math::{RawRotation, RawVector}; +use crate::utils::{self, FlatHandle}; +use rapier::prelude::*; +use wasm_bindgen::prelude::*; + +// NOTE: this MUST match the same enum on the TS side. +enum MassPropsMode { + Density = 0, + Mass, + MassProps, +} + +#[wasm_bindgen] +pub struct RawColliderSet(pub(crate) ColliderSet); + +impl RawColliderSet { + pub(crate) fn map(&self, handle: FlatHandle, f: impl FnOnce(&Collider) -> T) -> T { + let collider = self + .0 + .get(utils::collider_handle(handle)) + .expect("Invalid Collider reference. It may have been removed from the physics World."); + f(collider) + } + + pub(crate) fn map_mut( + &mut self, + handle: FlatHandle, + f: impl FnOnce(&mut Collider) -> T, + ) -> T { + let collider = self + .0 + .get_mut(utils::collider_handle(handle)) + .expect("Invalid Collider reference. It may have been removed from the physics World."); + f(collider) + } + + pub(crate) fn map_pair_mut( + &mut self, + handle1: FlatHandle, + handle2: FlatHandle, + f: impl FnOnce(Option<&mut Collider>, Option<&mut Collider>) -> T, + ) -> T { + let (collider1, collider2) = self.0.get_pair_mut( + utils::collider_handle(handle1), + utils::collider_handle(handle2), + ); + f(collider1, collider2) + } +} + +impl RawColliderSet { + // This is a workaround because wasm-bindgen doesn't support the `cfg(feature = ...)` + // for the method arguments. + pub fn do_create_collider( + &mut self, + enabled: bool, + shape: &RawShape, + translation: &RawVector, + rotation: &RawRotation, + massPropsMode: u32, + mass: f32, + centerOfMass: &RawVector, + #[cfg(feature = "dim2")] principalAngularInertia: f32, + #[cfg(feature = "dim3")] principalAngularInertia: &RawVector, + #[cfg(feature = "dim3")] angularInertiaFrame: &RawRotation, + density: f32, + friction: f32, + restitution: f32, + frictionCombineRule: u32, + restitutionCombineRule: u32, + isSensor: bool, + collisionGroups: u32, + solverGroups: u32, + activeCollisionTypes: u16, + activeHooks: u32, + activeEvents: u32, + contactForceEventThreshold: f32, + contactSkin: f32, + hasParent: bool, + parent: FlatHandle, + bodies: &mut RawRigidBodySet, + ) -> Option { + let pos = Isometry::from_parts(translation.0.into(), rotation.0); + let mut builder = ColliderBuilder::new(shape.0.clone()) + .enabled(enabled) + .position(pos) + .friction(friction) + .restitution(restitution) + .collision_groups(super::unpack_interaction_groups(collisionGroups)) + .solver_groups(super::unpack_interaction_groups(solverGroups)) + .active_hooks(ActiveHooks::from_bits(activeHooks).unwrap_or(ActiveHooks::empty())) + .active_events(ActiveEvents::from_bits(activeEvents).unwrap_or(ActiveEvents::empty())) + .active_collision_types( + ActiveCollisionTypes::from_bits(activeCollisionTypes) + .unwrap_or(ActiveCollisionTypes::empty()), + ) + .sensor(isSensor) + .friction_combine_rule(super::combine_rule_from_u32(frictionCombineRule)) + .restitution_combine_rule(super::combine_rule_from_u32(restitutionCombineRule)) + .contact_force_event_threshold(contactForceEventThreshold) + .contact_skin(contactSkin); + + if massPropsMode == MassPropsMode::MassProps as u32 { + #[cfg(feature = "dim2")] + let mprops = MassProperties::new(centerOfMass.0.into(), mass, principalAngularInertia); + #[cfg(feature = "dim3")] + let mprops = MassProperties::with_principal_inertia_frame( + centerOfMass.0.into(), + mass, + principalAngularInertia.0, + angularInertiaFrame.0, + ); + builder = builder.mass_properties(mprops); + } else if massPropsMode == MassPropsMode::Density as u32 { + builder = builder.density(density); + } else { + assert_eq!(massPropsMode, MassPropsMode::Mass as u32); + builder = builder.mass(mass); + }; + + let collider = builder.build(); + + if hasParent { + Some(utils::flat_handle( + self.0 + .insert_with_parent(collider, utils::body_handle(parent), &mut bodies.0) + .0, + )) + } else { + Some(utils::flat_handle(self.0.insert(collider).0)) + } + } +} + +#[wasm_bindgen] +impl RawColliderSet { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawColliderSet(ColliderSet::new()) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn contains(&self, handle: FlatHandle) -> bool { + self.0.get(utils::collider_handle(handle)).is_some() + } + + #[cfg(feature = "dim2")] + pub fn createCollider( + &mut self, + enabled: bool, + shape: &RawShape, + translation: &RawVector, + rotation: &RawRotation, + massPropsMode: u32, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: f32, + density: f32, + friction: f32, + restitution: f32, + frictionCombineRule: u32, + restitutionCombineRule: u32, + isSensor: bool, + collisionGroups: u32, + solverGroups: u32, + activeCollisionTypes: u16, + activeHooks: u32, + activeEvents: u32, + contactForceEventThreshold: f32, + contactSkin: f32, + hasParent: bool, + parent: FlatHandle, + bodies: &mut RawRigidBodySet, + ) -> Option { + self.do_create_collider( + enabled, + shape, + translation, + rotation, + massPropsMode, + mass, + centerOfMass, + principalAngularInertia, + density, + friction, + restitution, + frictionCombineRule, + restitutionCombineRule, + isSensor, + collisionGroups, + solverGroups, + activeCollisionTypes, + activeHooks, + activeEvents, + contactForceEventThreshold, + contactSkin, + hasParent, + parent, + bodies, + ) + } + + #[cfg(feature = "dim3")] + pub fn createCollider( + &mut self, + enabled: bool, + shape: &RawShape, + translation: &RawVector, + rotation: &RawRotation, + massPropsMode: u32, + mass: f32, + centerOfMass: &RawVector, + principalAngularInertia: &RawVector, + angularInertiaFrame: &RawRotation, + density: f32, + friction: f32, + restitution: f32, + frictionCombineRule: u32, + restitutionCombineRule: u32, + isSensor: bool, + collisionGroups: u32, + solverGroups: u32, + activeCollisionTypes: u16, + activeHooks: u32, + activeEvents: u32, + contactForceEventThreshold: f32, + contactSkin: f32, + hasParent: bool, + parent: FlatHandle, + bodies: &mut RawRigidBodySet, + ) -> Option { + self.do_create_collider( + enabled, + shape, + translation, + rotation, + massPropsMode, + mass, + centerOfMass, + principalAngularInertia, + angularInertiaFrame, + density, + friction, + restitution, + frictionCombineRule, + restitutionCombineRule, + isSensor, + collisionGroups, + solverGroups, + activeCollisionTypes, + activeHooks, + activeEvents, + contactForceEventThreshold, + contactSkin, + hasParent, + parent, + bodies, + ) + } + + /// Removes a collider from this set and wake-up the rigid-body it is attached to. + pub fn remove( + &mut self, + handle: FlatHandle, + islands: &mut RawIslandManager, + bodies: &mut RawRigidBodySet, + wakeUp: bool, + ) { + let handle = utils::collider_handle(handle); + self.0.remove(handle, &mut islands.0, &mut bodies.0, wakeUp); + } + + /// Checks if a collider with the given integer handle exists. + pub fn isHandleValid(&self, handle: FlatHandle) -> bool { + self.0.get(utils::collider_handle(handle)).is_some() + } + + /// Applies the given JavaScript function to the integer handle of each collider managed by this collider set. + /// + /// # Parameters + /// - `f(handle)`: the function to apply to the integer handle of each collider managed by this collider set. Called as `f(handle)`. + pub fn forEachColliderHandle(&self, f: &js_sys::Function) { + let this = JsValue::null(); + for (handle, _) in self.0.iter() { + let _ = f.call1(&this, &JsValue::from(utils::flat_handle(handle.0))); + } + } +} diff --git a/thirdparty/rapier.js/src/geometry/contact.rs b/thirdparty/rapier.js/src/geometry/contact.rs new file mode 100644 index 00000000..47043a27 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/contact.rs @@ -0,0 +1,31 @@ +use crate::math::RawVector; +use rapier::parry::query; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawShapeContact { + pub(crate) contact: query::Contact, +} + +#[wasm_bindgen] +impl RawShapeContact { + pub fn distance(&self) -> f32 { + self.contact.dist + } + + pub fn point1(&self) -> RawVector { + self.contact.point1.coords.into() + } + + pub fn point2(&self) -> RawVector { + self.contact.point2.coords.into() + } + + pub fn normal1(&self) -> RawVector { + self.contact.normal1.into_inner().into() + } + + pub fn normal2(&self) -> RawVector { + self.contact.normal2.into_inner().into() + } +} diff --git a/thirdparty/rapier.js/src/geometry/feature.rs b/thirdparty/rapier.js/src/geometry/feature.rs new file mode 100644 index 00000000..96e8f511 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/feature.rs @@ -0,0 +1,47 @@ +use rapier::prelude::FeatureId; +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg(feature = "dim2")] +#[wasm_bindgen] +#[derive(Copy, Clone)] +pub enum RawFeatureType { + Vertex, + Face, + Unknown, +} + +#[cfg(feature = "dim3")] +#[wasm_bindgen] +#[derive(Copy, Clone)] +pub enum RawFeatureType { + Vertex, + Edge, + Face, + Unknown, +} + +pub trait IntoTypeValue { + fn into_type(self) -> RawFeatureType; + fn into_value(self) -> Option; +} + +impl IntoTypeValue for FeatureId { + fn into_type(self) -> RawFeatureType { + match self { + FeatureId::Vertex(_) => RawFeatureType::Vertex, + #[cfg(feature = "dim3")] + FeatureId::Edge(_) => RawFeatureType::Edge, + FeatureId::Face(_) => RawFeatureType::Face, + _ => RawFeatureType::Unknown, + } + } + + fn into_value(self) -> Option { + match self { + FeatureId::Vertex(id) | FeatureId::Face(id) => Some(id), + #[cfg(feature = "dim3")] + FeatureId::Edge(id) => Some(id), + _ => None, + } + } +} diff --git a/thirdparty/rapier.js/src/geometry/mod.rs b/thirdparty/rapier.js/src/geometry/mod.rs new file mode 100644 index 00000000..08918b88 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/mod.rs @@ -0,0 +1,49 @@ +//! Structures related to geometry: colliders, shapes, etc. + +pub use self::broad_phase::*; +pub use self::collider_set::*; +pub use self::contact::*; +pub use self::feature::*; +pub use self::narrow_phase::*; +pub use self::point::*; +pub use self::ray::*; +pub use self::shape::*; +pub use self::toi::*; + +mod broad_phase; +mod collider; +mod collider_set; +mod contact; +mod feature; +mod narrow_phase; +mod point; +mod ray; +mod shape; +mod toi; + +use rapier::dynamics::CoefficientCombineRule; +use rapier::geometry::InteractionGroups; +use rapier::prelude::Group; + +pub const fn unpack_interaction_groups(memberships_filter: u32) -> InteractionGroups { + InteractionGroups::new( + Group::from_bits_retain((memberships_filter >> 16) as u32), + Group::from_bits_retain((memberships_filter & 0x0000_ffff) as u32), + ) +} + +pub const fn pack_interaction_groups(groups: InteractionGroups) -> u32 { + (groups.memberships.bits() << 16) | groups.filter.bits() +} + +pub const fn combine_rule_from_u32(rule: u32) -> CoefficientCombineRule { + if rule == CoefficientCombineRule::Average as u32 { + CoefficientCombineRule::Average + } else if rule == CoefficientCombineRule::Min as u32 { + CoefficientCombineRule::Min + } else if rule == CoefficientCombineRule::Multiply as u32 { + CoefficientCombineRule::Multiply + } else { + CoefficientCombineRule::Max + } +} diff --git a/thirdparty/rapier.js/src/geometry/narrow_phase.rs b/thirdparty/rapier.js/src/geometry/narrow_phase.rs new file mode 100644 index 00000000..be3fee25 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/narrow_phase.rs @@ -0,0 +1,217 @@ +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::geometry::{ContactManifold, ContactPair, NarrowPhase}; +use rapier::math::Real; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawNarrowPhase(pub(crate) NarrowPhase); + +#[wasm_bindgen] +impl RawNarrowPhase { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawNarrowPhase(NarrowPhase::new()) + } + + pub fn contact_pairs_with(&self, handle1: FlatHandle, f: js_sys::Function) { + let this = JsValue::null(); + let handle1 = utils::collider_handle(handle1); + for pair in self.0.contact_pairs_with(handle1) { + let handle2 = if pair.collider1 == handle1 { + utils::flat_handle(pair.collider2.0) + } else { + utils::flat_handle(pair.collider1.0) + }; + + let _ = f.call1(&this, &JsValue::from(handle2)); + } + } + + pub fn contact_pair(&self, handle1: FlatHandle, handle2: FlatHandle) -> Option { + let handle1 = utils::collider_handle(handle1); + let handle2 = utils::collider_handle(handle2); + self.0 + .contact_pair(handle1, handle2) + .map(|p| RawContactPair(p as *const ContactPair)) + } + + pub fn intersection_pairs_with(&self, handle1: FlatHandle, f: js_sys::Function) { + let this = JsValue::null(); + let handle1 = utils::collider_handle(handle1); + for (h1, h2, inter) in self.0.intersection_pairs_with(handle1) { + if inter { + let handle2 = if h1 == handle1 { + utils::flat_handle(h2.0) + } else { + utils::flat_handle(h1.0) + }; + + let _ = f.call1(&this, &JsValue::from(handle2)); + } + } + } + + pub fn intersection_pair(&self, handle1: FlatHandle, handle2: FlatHandle) -> bool { + let handle1 = utils::collider_handle(handle1); + let handle2 = utils::collider_handle(handle2); + self.0.intersection_pair(handle1, handle2) == Some(true) + } +} + +#[wasm_bindgen] +pub struct RawContactPair(*const ContactPair); +#[wasm_bindgen] +pub struct RawContactManifold(*const ContactManifold); + +// SAFETY: the use of a raw pointer is very unsafe. +// We need this because wasm-bindgen doesn't support +// lifetimes. So for the moment, we have to make sure +// that our TypeScript wrapper properly free the pair +// before the user has a chance to invalidate this pointer. +#[wasm_bindgen] +impl RawContactPair { + pub fn collider1(&self) -> FlatHandle { + unsafe { utils::flat_handle((*self.0).collider1.0) } + } + + pub fn collider2(&self) -> FlatHandle { + unsafe { utils::flat_handle((*self.0).collider2.0) } + } + + pub fn numContactManifolds(&self) -> usize { + unsafe { (*self.0).manifolds.len() } + } + pub fn contactManifold(&self, i: usize) -> Option { + unsafe { + (&(*self.0).manifolds) + .get(i) + .map(|m| RawContactManifold(m as *const ContactManifold)) + } + } +} + +#[wasm_bindgen] +impl RawContactManifold { + pub fn normal(&self) -> RawVector { + unsafe { RawVector((*self.0).data.normal) } + } + + // pub fn user_data(&self) -> u32 { + // unsafe { (*self.0).data.user_data } + // } + + pub fn local_n1(&self) -> RawVector { + unsafe { (*self.0).local_n1.into() } + } + + pub fn local_n2(&self) -> RawVector { + unsafe { (*self.0).local_n2.into() } + } + + pub fn subshape1(&self) -> u32 { + unsafe { (*self.0).subshape1 } + } + + pub fn subshape2(&self) -> u32 { + unsafe { (*self.0).subshape2 } + } + + pub fn num_contacts(&self) -> usize { + unsafe { (*self.0).points.len() } + } + + pub fn contact_local_p1(&self, i: usize) -> Option { + unsafe { (&(*self.0).points).get(i).map(|c| c.local_p1.coords.into()) } + } + + pub fn contact_local_p2(&self, i: usize) -> Option { + unsafe { (&(*self.0).points).get(i).map(|c| c.local_p2.coords.into()) } + } + + pub fn contact_dist(&self, i: usize) -> Real { + unsafe { (&(*self.0).points).get(i).map(|c| c.dist).unwrap_or(0.0) } + } + + pub fn contact_fid1(&self, i: usize) -> u32 { + unsafe { (&(*self.0).points).get(i).map(|c| c.fid1.0).unwrap_or(0) } + } + + pub fn contact_fid2(&self, i: usize) -> u32 { + unsafe { (&(*self.0).points).get(i).map(|c| c.fid2.0).unwrap_or(0) } + } + + pub fn contact_impulse(&self, i: usize) -> Real { + unsafe { + (&(*self.0).points) + .get(i) + .map(|c| c.data.impulse) + .unwrap_or(0.0) + } + } + + #[cfg(feature = "dim2")] + pub fn contact_tangent_impulse(&self, i: usize) -> Real { + unsafe { + (&(*self.0).points) + .get(i) + .map(|c| c.data.tangent_impulse.x) + .unwrap_or(0.0) + } + } + + #[cfg(feature = "dim3")] + pub fn contact_tangent_impulse_x(&self, i: usize) -> Real { + unsafe { + (&(*self.0).points) + .get(i) + .map(|c| c.data.tangent_impulse.x) + .unwrap_or(0.0) + } + } + + #[cfg(feature = "dim3")] + pub fn contact_tangent_impulse_y(&self, i: usize) -> Real { + unsafe { + (&(*self.0).points) + .get(i) + .map(|c| c.data.tangent_impulse.y) + .unwrap_or(0.0) + } + } + + pub fn num_solver_contacts(&self) -> usize { + unsafe { (*self.0).data.solver_contacts.len() } + } + + pub fn solver_contact_point(&self, i: usize) -> Option { + unsafe { + (&(*self.0).data) + .solver_contacts + .get(i) + .map(|c| c.point.coords.into()) + } + } + + pub fn solver_contact_dist(&self, i: usize) -> Real { + unsafe { + (&(*self.0).data) + .solver_contacts + .get(i) + .map(|c| c.dist) + .unwrap_or(0.0) + } + } + + pub fn solver_contact_friction(&self, i: usize) -> Real { + unsafe { (&(*self.0).data).solver_contacts[i].friction } + } + + pub fn solver_contact_restitution(&self, i: usize) -> Real { + unsafe { (&(*self.0).data).solver_contacts[i].restitution } + } + + pub fn solver_contact_tangent_velocity(&self, i: usize) -> RawVector { + unsafe { (&(*self.0).data).solver_contacts[i].tangent_velocity.into() } + } +} diff --git a/thirdparty/rapier.js/src/geometry/point.rs b/thirdparty/rapier.js/src/geometry/point.rs new file mode 100644 index 00000000..e7bb4a93 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/point.rs @@ -0,0 +1,53 @@ +use crate::geometry::feature::IntoTypeValue; +use crate::geometry::RawFeatureType; +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::{ + geometry::{ColliderHandle, PointProjection}, + prelude::FeatureId, +}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawPointProjection(pub(crate) PointProjection); + +#[wasm_bindgen] +impl RawPointProjection { + pub fn point(&self) -> RawVector { + self.0.point.coords.into() + } + + pub fn isInside(&self) -> bool { + self.0.is_inside + } +} + +#[wasm_bindgen] +pub struct RawPointColliderProjection { + pub(crate) handle: ColliderHandle, + pub(crate) proj: PointProjection, + pub(crate) feature: FeatureId, +} + +#[wasm_bindgen] +impl RawPointColliderProjection { + pub fn colliderHandle(&self) -> FlatHandle { + utils::flat_handle(self.handle.0) + } + + pub fn point(&self) -> RawVector { + self.proj.point.coords.into() + } + + pub fn isInside(&self) -> bool { + self.proj.is_inside + } + + pub fn featureType(&self) -> RawFeatureType { + self.feature.into_type() + } + + pub fn featureId(&self) -> Option { + self.feature.into_value() + } +} diff --git a/thirdparty/rapier.js/src/geometry/ray.rs b/thirdparty/rapier.js/src/geometry/ray.rs new file mode 100644 index 00000000..5285d5da --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/ray.rs @@ -0,0 +1,74 @@ +use crate::geometry::feature::IntoTypeValue; +use crate::geometry::RawFeatureType; +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::geometry::{ColliderHandle, RayIntersection}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawRayIntersection(pub(crate) RayIntersection); + +#[wasm_bindgen] +impl RawRayIntersection { + pub fn normal(&self) -> RawVector { + self.0.normal.into() + } + + pub fn time_of_impact(&self) -> f32 { + self.0.time_of_impact + } + + pub fn featureType(&self) -> RawFeatureType { + self.0.feature.into_type() + } + + pub fn featureId(&self) -> Option { + self.0.feature.into_value() + } +} + +#[wasm_bindgen] +pub struct RawRayColliderIntersection { + pub(crate) handle: ColliderHandle, + pub(crate) inter: RayIntersection, +} + +#[wasm_bindgen] +impl RawRayColliderIntersection { + pub fn colliderHandle(&self) -> FlatHandle { + utils::flat_handle(self.handle.0) + } + + pub fn normal(&self) -> RawVector { + self.inter.normal.into() + } + + pub fn time_of_impact(&self) -> f32 { + self.inter.time_of_impact + } + + pub fn featureType(&self) -> RawFeatureType { + self.inter.feature.into_type() + } + + pub fn featureId(&self) -> Option { + self.inter.feature.into_value() + } +} + +#[wasm_bindgen] +pub struct RawRayColliderHit { + pub(crate) handle: ColliderHandle, + pub(crate) timeOfImpact: f32, +} + +#[wasm_bindgen] +impl RawRayColliderHit { + pub fn colliderHandle(&self) -> FlatHandle { + utils::flat_handle(self.handle.0) + } + + pub fn timeOfImpact(&self) -> f32 { + self.timeOfImpact + } +} diff --git a/thirdparty/rapier.js/src/geometry/shape.rs b/thirdparty/rapier.js/src/geometry/shape.rs new file mode 100644 index 00000000..5f01d9f5 --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/shape.rs @@ -0,0 +1,527 @@ +use crate::geometry::{RawPointProjection, RawRayIntersection, RawShapeCastHit, RawShapeContact}; +use crate::math::{RawRotation, RawVector}; +#[cfg(feature = "dim3")] +use na::DMatrix; +#[cfg(feature = "dim2")] +use na::DVector; +use na::Unit; +use rapier::geometry::{Shape, SharedShape, TriMeshFlags}; +use rapier::math::{Isometry, Point, Real, Vector, DIM}; +use rapier::parry::query; +use rapier::parry::query::{Ray, ShapeCastOptions}; +use wasm_bindgen::prelude::*; + +pub trait SharedShapeUtility { + fn castShape( + &self, + shapePos1: &Isometry, + shapeVel1: &Vector, + shape2: &dyn Shape, + shapePos2: &Isometry, + shapeVel2: &Vector, + target_distance: f32, + maxToi: f32, + stop_at_penetration: bool, + ) -> Option; + + fn intersectsShape( + &self, + shapePos1: &Isometry, + shape2: &dyn Shape, + shapePos2: &Isometry, + ) -> bool; + + fn contactShape( + &self, + shapePos1: &Isometry, + shape2: &dyn Shape, + shapePos2: &Isometry, + prediction: f32, + ) -> Option; + + fn containsPoint(&self, shapePos: &Isometry, point: &Point) -> bool; + + fn projectPoint( + &self, + shapePos: &Isometry, + point: &Point, + solid: bool, + ) -> RawPointProjection; + + fn intersectsRay( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + ) -> bool; + + fn castRay( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + solid: bool, + ) -> f32; + + fn castRayAndGetNormal( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + solid: bool, + ) -> Option; +} + +// for RawShape & Collider +impl SharedShapeUtility for SharedShape { + fn castShape( + &self, + shapePos1: &Isometry, + shapeVel1: &Vector, + shape2: &dyn Shape, + shapePos2: &Isometry, + shapeVel2: &Vector, + target_distance: f32, + maxToi: f32, + stop_at_penetration: bool, + ) -> Option { + query::cast_shapes( + shapePos1, + shapeVel1, + &*self.0, + shapePos2, + &shapeVel2, + shape2, + ShapeCastOptions { + max_time_of_impact: maxToi, + target_distance, + stop_at_penetration, + compute_impact_geometry_on_penetration: true, + }, + ) + .ok() + .flatten() + .map(|hit| RawShapeCastHit { hit }) + } + + fn intersectsShape( + &self, + shapePos1: &Isometry, + shape2: &dyn Shape, + shapePos2: &Isometry, + ) -> bool { + query::intersection_test(shapePos1, &*self.0, shapePos2, shape2).unwrap_or(false) + } + + fn contactShape( + &self, + shapePos1: &Isometry, + shape2: &dyn Shape, + shapePos2: &Isometry, + prediction: f32, + ) -> Option { + query::contact(shapePos1, &*self.0, shapePos2, shape2, prediction) + .ok() + .flatten() + .map(|contact| RawShapeContact { contact }) + } + + fn containsPoint(&self, shapePos: &Isometry, point: &Point) -> bool { + self.as_ref().contains_point(shapePos, point) + } + + fn projectPoint( + &self, + shapePos: &Isometry, + point: &Point, + solid: bool, + ) -> RawPointProjection { + RawPointProjection(self.as_ref().project_point(shapePos, point, solid)) + } + + fn intersectsRay( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + ) -> bool { + self.as_ref() + .intersects_ray(shapePos, &Ray::new(rayOrig, rayDir), maxToi) + } + + fn castRay( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + solid: bool, + ) -> f32 { + self.as_ref() + .cast_ray(shapePos, &Ray::new(rayOrig, rayDir), maxToi, solid) + .unwrap_or(-1.0) // Negative value = no hit. + } + + fn castRayAndGetNormal( + &self, + shapePos: &Isometry, + rayOrig: Point, + rayDir: Vector, + maxToi: f32, + solid: bool, + ) -> Option { + self.as_ref() + .cast_ray_and_get_normal(shapePos, &Ray::new(rayOrig, rayDir), maxToi, solid) + .map(|inter| RawRayIntersection(inter)) + } +} + +#[wasm_bindgen] +#[cfg(feature = "dim2")] +pub enum RawShapeType { + Ball = 0, + Cuboid = 1, + Capsule = 2, + Segment = 3, + Polyline = 4, + Triangle = 5, + TriMesh = 6, + HeightField = 7, + Compound = 8, + ConvexPolygon = 9, + RoundCuboid = 10, + RoundTriangle = 11, + RoundConvexPolygon = 12, + HalfSpace = 13, + Voxels = 14, +} + +#[wasm_bindgen] +#[cfg(feature = "dim3")] +pub enum RawShapeType { + Ball = 0, + Cuboid = 1, + Capsule = 2, + Segment = 3, + Polyline = 4, + Triangle = 5, + TriMesh = 6, + HeightField = 7, + Compound = 8, + ConvexPolyhedron = 9, + Cylinder = 10, + Cone = 11, + RoundCuboid = 12, + RoundTriangle = 13, + RoundCylinder = 14, + RoundCone = 15, + RoundConvexPolyhedron = 16, + HalfSpace = 17, + Voxels = 18, +} + +#[wasm_bindgen] +pub struct RawShape(pub(crate) SharedShape); + +#[wasm_bindgen] +impl RawShape { + #[cfg(feature = "dim2")] + pub fn cuboid(hx: f32, hy: f32) -> Self { + Self(SharedShape::cuboid(hx, hy)) + } + + #[cfg(feature = "dim3")] + pub fn cuboid(hx: f32, hy: f32, hz: f32) -> Self { + Self(SharedShape::cuboid(hx, hy, hz)) + } + + #[cfg(feature = "dim2")] + pub fn roundCuboid(hx: f32, hy: f32, borderRadius: f32) -> Self { + Self(SharedShape::round_cuboid(hx, hy, borderRadius)) + } + + #[cfg(feature = "dim3")] + pub fn roundCuboid(hx: f32, hy: f32, hz: f32, borderRadius: f32) -> Self { + Self(SharedShape::round_cuboid(hx, hy, hz, borderRadius)) + } + + pub fn ball(radius: f32) -> Self { + Self(SharedShape::ball(radius)) + } + + pub fn halfspace(normal: &RawVector) -> Self { + Self(SharedShape::halfspace(Unit::new_normalize(normal.0))) + } + + pub fn capsule(halfHeight: f32, radius: f32) -> Self { + let p2 = Point::from(Vector::y() * halfHeight); + let p1 = -p2; + Self(SharedShape::capsule(p1, p2, radius)) + } + + #[cfg(feature = "dim3")] + pub fn cylinder(halfHeight: f32, radius: f32) -> Self { + Self(SharedShape::cylinder(halfHeight, radius)) + } + + #[cfg(feature = "dim3")] + pub fn roundCylinder(halfHeight: f32, radius: f32, borderRadius: f32) -> Self { + Self(SharedShape::round_cylinder( + halfHeight, + radius, + borderRadius, + )) + } + + #[cfg(feature = "dim3")] + pub fn cone(halfHeight: f32, radius: f32) -> Self { + Self(SharedShape::cone(halfHeight, radius)) + } + + #[cfg(feature = "dim3")] + pub fn roundCone(halfHeight: f32, radius: f32, borderRadius: f32) -> Self { + Self(SharedShape::round_cone(halfHeight, radius, borderRadius)) + } + + pub fn voxels(voxel_size: &RawVector, grid_coords: Vec) -> Self { + let grid_coords: Vec<_> = grid_coords + .chunks_exact(DIM) + .map(Point::from_slice) + .collect(); + Self(SharedShape::voxels(voxel_size.0, &grid_coords)) + } + + pub fn voxelsFromPoints(voxel_size: &RawVector, points: Vec) -> Self { + let points: Vec<_> = points.chunks_exact(DIM).map(Point::from_slice).collect(); + Self(SharedShape::voxels_from_points(voxel_size.0, &points)) + } + + pub fn polyline(vertices: Vec, indices: Vec) -> Self { + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + let indices: Vec<_> = indices.chunks(2).map(|v| [v[0], v[1]]).collect(); + if indices.is_empty() { + Self(SharedShape::polyline(vertices, None)) + } else { + Self(SharedShape::polyline(vertices, Some(indices))) + } + } + + pub fn trimesh(vertices: Vec, indices: Vec, flags: u32) -> Option { + let flags = TriMeshFlags::from_bits(flags as u16).unwrap_or_default(); + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + let indices = indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect(); + SharedShape::trimesh_with_flags(vertices, indices, flags) + .ok() + .map(Self) + } + + #[cfg(feature = "dim2")] + pub fn heightfield(heights: Vec, scale: &RawVector) -> Self { + let heights = DVector::from_vec(heights); + Self(SharedShape::heightfield(heights, scale.0)) + } + + #[cfg(feature = "dim3")] + pub fn heightfield( + nrows: u32, + ncols: u32, + heights: Vec, + scale: &RawVector, + flags: u32, + ) -> Self { + let flags = + rapier::parry::shape::HeightFieldFlags::from_bits(flags as u8).unwrap_or_default(); + let heights = DMatrix::from_vec(nrows as usize + 1, ncols as usize + 1, heights); + Self(SharedShape::heightfield_with_flags(heights, scale.0, flags)) + } + + pub fn segment(p1: &RawVector, p2: &RawVector) -> Self { + Self(SharedShape::segment(p1.0.into(), p2.0.into())) + } + + pub fn triangle(p1: &RawVector, p2: &RawVector, p3: &RawVector) -> Self { + Self(SharedShape::triangle(p1.0.into(), p2.0.into(), p3.0.into())) + } + + pub fn roundTriangle( + p1: &RawVector, + p2: &RawVector, + p3: &RawVector, + borderRadius: f32, + ) -> Self { + Self(SharedShape::round_triangle( + p1.0.into(), + p2.0.into(), + p3.0.into(), + borderRadius, + )) + } + + pub fn convexHull(points: Vec) -> Option { + let points: Vec<_> = points.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + SharedShape::convex_hull(&points).map(|s| Self(s)) + } + + pub fn roundConvexHull(points: Vec, borderRadius: f32) -> Option { + let points: Vec<_> = points.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + SharedShape::round_convex_hull(&points, borderRadius).map(|s| Self(s)) + } + + #[cfg(feature = "dim2")] + pub fn convexPolyline(vertices: Vec) -> Option { + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + SharedShape::convex_polyline(vertices).map(|s| Self(s)) + } + + #[cfg(feature = "dim2")] + pub fn roundConvexPolyline(vertices: Vec, borderRadius: f32) -> Option { + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + SharedShape::round_convex_polyline(vertices, borderRadius).map(|s| Self(s)) + } + + #[cfg(feature = "dim3")] + pub fn convexMesh(vertices: Vec, indices: Vec) -> Option { + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + let indices: Vec<_> = indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect(); + SharedShape::convex_mesh(vertices, &indices).map(|s| Self(s)) + } + + #[cfg(feature = "dim3")] + pub fn roundConvexMesh( + vertices: Vec, + indices: Vec, + borderRadius: f32, + ) -> Option { + let vertices = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); + let indices: Vec<_> = indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect(); + SharedShape::round_convex_mesh(vertices, &indices, borderRadius).map(|s| Self(s)) + } + + pub fn castShape( + &self, + shapePos1: &RawVector, + shapeRot1: &RawRotation, + shapeVel1: &RawVector, + shape2: &RawShape, + shapePos2: &RawVector, + shapeRot2: &RawRotation, + shapeVel2: &RawVector, + target_distance: f32, + maxToi: f32, + stop_at_penetration: bool, + ) -> Option { + let pos1 = Isometry::from_parts(shapePos1.0.into(), shapeRot1.0); + let pos2 = Isometry::from_parts(shapePos2.0.into(), shapeRot2.0); + + self.0.castShape( + &pos1, + &shapeVel1.0, + &*shape2.0, + &pos2, + &shapeVel2.0, + target_distance, + maxToi, + stop_at_penetration, + ) + } + + pub fn intersectsShape( + &self, + shapePos1: &RawVector, + shapeRot1: &RawRotation, + shape2: &RawShape, + shapePos2: &RawVector, + shapeRot2: &RawRotation, + ) -> bool { + let pos1 = Isometry::from_parts(shapePos1.0.into(), shapeRot1.0); + let pos2 = Isometry::from_parts(shapePos2.0.into(), shapeRot2.0); + + self.0.intersectsShape(&pos1, &*shape2.0, &pos2) + } + + pub fn contactShape( + &self, + shapePos1: &RawVector, + shapeRot1: &RawRotation, + shape2: &RawShape, + shapePos2: &RawVector, + shapeRot2: &RawRotation, + prediction: f32, + ) -> Option { + let pos1 = Isometry::from_parts(shapePos1.0.into(), shapeRot1.0); + let pos2 = Isometry::from_parts(shapePos2.0.into(), shapeRot2.0); + + self.0.contactShape(&pos1, &*shape2.0, &pos2, prediction) + } + + pub fn containsPoint( + &self, + shapePos: &RawVector, + shapeRot: &RawRotation, + point: &RawVector, + ) -> bool { + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + self.0.containsPoint(&pos, &point.0.into()) + } + + pub fn projectPoint( + &self, + shapePos: &RawVector, + shapeRot: &RawRotation, + point: &RawVector, + solid: bool, + ) -> RawPointProjection { + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + self.0.projectPoint(&pos, &point.0.into(), solid) + } + + pub fn intersectsRay( + &self, + shapePos: &RawVector, + shapeRot: &RawRotation, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + ) -> bool { + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + self.0 + .intersectsRay(&pos, rayOrig.0.into(), rayDir.0.into(), maxToi) + } + + pub fn castRay( + &self, + shapePos: &RawVector, + shapeRot: &RawRotation, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + ) -> f32 { + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + self.0 + .castRay(&pos, rayOrig.0.into(), rayDir.0.into(), maxToi, solid) + } + + pub fn castRayAndGetNormal( + &self, + shapePos: &RawVector, + shapeRot: &RawRotation, + rayOrig: &RawVector, + rayDir: &RawVector, + maxToi: f32, + solid: bool, + ) -> Option { + let pos = Isometry::from_parts(shapePos.0.into(), shapeRot.0); + + self.0 + .castRayAndGetNormal(&pos, rayOrig.0.into(), rayDir.0.into(), maxToi, solid) + } +} diff --git a/thirdparty/rapier.js/src/geometry/toi.rs b/thirdparty/rapier.js/src/geometry/toi.rs new file mode 100644 index 00000000..1e00675d --- /dev/null +++ b/thirdparty/rapier.js/src/geometry/toi.rs @@ -0,0 +1,65 @@ +use crate::math::RawVector; +use crate::utils::{self, FlatHandle}; +use rapier::geometry::{ColliderHandle, ShapeCastHit}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawShapeCastHit { + pub(crate) hit: ShapeCastHit, +} + +#[wasm_bindgen] +impl RawShapeCastHit { + pub fn time_of_impact(&self) -> f32 { + self.hit.time_of_impact + } + + pub fn witness1(&self) -> RawVector { + self.hit.witness1.coords.into() + } + + pub fn witness2(&self) -> RawVector { + self.hit.witness2.coords.into() + } + + pub fn normal1(&self) -> RawVector { + self.hit.normal1.into_inner().into() + } + + pub fn normal2(&self) -> RawVector { + self.hit.normal2.into_inner().into() + } +} + +#[wasm_bindgen] +pub struct RawColliderShapeCastHit { + pub(crate) handle: ColliderHandle, + pub(crate) hit: ShapeCastHit, +} + +#[wasm_bindgen] +impl RawColliderShapeCastHit { + pub fn colliderHandle(&self) -> FlatHandle { + utils::flat_handle(self.handle.0) + } + + pub fn time_of_impact(&self) -> f32 { + self.hit.time_of_impact + } + + pub fn witness1(&self) -> RawVector { + self.hit.witness1.coords.into() + } + + pub fn witness2(&self) -> RawVector { + self.hit.witness2.coords.into() + } + + pub fn normal1(&self) -> RawVector { + self.hit.normal1.into_inner().into() + } + + pub fn normal2(&self) -> RawVector { + self.hit.normal2.into_inner().into() + } +} diff --git a/thirdparty/rapier.js/src/lib.rs b/thirdparty/rapier.js/src/lib.rs new file mode 100644 index 00000000..132809dc --- /dev/null +++ b/thirdparty/rapier.js/src/lib.rs @@ -0,0 +1,32 @@ +//! # Rapier +//! Fast and deterministic WASM physics engine. + +#![allow(non_snake_case)] // JS uses camelCase, so we will follow its convention for the generated bindings. + // #![deny(missing_docs)] + +extern crate nalgebra as na; +#[cfg(feature = "dim2")] +extern crate rapier2d as rapier; +#[cfg(feature = "dim3")] +extern crate rapier3d as rapier; +#[macro_use] +extern crate serde; + +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} + +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn reserve_memory(extra_bytes_count: u32) { + let mut unused: Vec = vec![]; + unused.reserve(extra_bytes_count as usize); + std::hint::black_box(&unused); +} + +pub mod control; +pub mod dynamics; +pub mod geometry; +pub mod math; +pub mod pipeline; +pub mod utils; diff --git a/thirdparty/rapier.js/src/math.rs b/thirdparty/rapier.js/src/math.rs new file mode 100644 index 00000000..5c50c142 --- /dev/null +++ b/thirdparty/rapier.js/src/math.rs @@ -0,0 +1,266 @@ +//! Linear algebra primitives. + +#[cfg(feature = "dim3")] +use js_sys::Float32Array; +#[cfg(feature = "dim3")] +use na::{Quaternion, Unit}; +#[cfg(feature = "dim3")] +use rapier::math::Real; +use rapier::math::{Point, Rotation, Vector}; +#[cfg(feature = "dim3")] +use rapier::parry::utils::SdpMatrix3; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[repr(transparent)] +#[derive(Copy, Clone)] +/// A rotation quaternion. +pub struct RawRotation(pub(crate) Rotation); + +impl From> for RawRotation { + fn from(v: Rotation) -> Self { + RawRotation(v) + } +} + +#[wasm_bindgen] +#[cfg(feature = "dim2")] +/// A unit complex number describing the orientation of a Rapier entity. +impl RawRotation { + /// The identity rotation. + pub fn identity() -> Self { + Self(Rotation::identity()) + } + + /// The rotation with thegiven angle. + pub fn fromAngle(angle: f32) -> Self { + Self(Rotation::new(angle)) + } + + /// The imaginary part of this complex number. + #[wasm_bindgen(getter)] + pub fn im(&self) -> f32 { + self.0.im + } + + /// The real part of this complex number. + #[wasm_bindgen(getter)] + pub fn re(&self) -> f32 { + self.0.re + } + + /// The rotation angle in radians. + #[wasm_bindgen(getter)] + pub fn angle(&self) -> f32 { + self.0.angle() + } +} + +#[wasm_bindgen] +#[cfg(feature = "dim3")] +/// A unit quaternion describing the orientation of a Rapier entity. +impl RawRotation { + #[wasm_bindgen(constructor)] + pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { + RawRotation(Unit::new_unchecked(Quaternion::new(w, x, y, z))) + } + + /// The identity quaternion. + pub fn identity() -> Self { + Self(Rotation::identity()) + } + + /// The `x` component of this quaternion. + #[wasm_bindgen(getter)] + pub fn x(&self) -> f32 { + self.0.i + } + + /// The `y` component of this quaternion. + #[wasm_bindgen(getter)] + pub fn y(&self) -> f32 { + self.0.j + } + + /// The `z` component of this quaternion. + #[wasm_bindgen(getter)] + pub fn z(&self) -> f32 { + self.0.k + } + + /// The `w` component of this quaternion. + #[wasm_bindgen(getter)] + pub fn w(&self) -> f32 { + self.0.w + } +} + +#[wasm_bindgen] +#[repr(transparent)] +#[derive(Copy, Clone)] +/// A vector. +pub struct RawVector(pub(crate) Vector); + +impl From> for RawVector { + fn from(v: Vector) -> Self { + RawVector(v) + } +} + +impl From> for RawVector { + fn from(pt: Point) -> Self { + pt.coords.into() + } +} + +#[wasm_bindgen] +impl RawVector { + /// Creates a new vector filled with zeros. + pub fn zero() -> Self { + Self(Vector::zeros()) + } + + /// Creates a new 2D vector from its two components. + /// + /// # Parameters + /// - `x`: the `x` component of this 2D vector. + /// - `y`: the `y` component of this 2D vector. + #[cfg(feature = "dim2")] + #[wasm_bindgen(constructor)] + pub fn new(x: f32, y: f32) -> Self { + Self(Vector::new(x, y)) + } + + /// Creates a new 3D vector from its two components. + /// + /// # Parameters + /// - `x`: the `x` component of this 3D vector. + /// - `y`: the `y` component of this 3D vector. + /// - `z`: the `z` component of this 3D vector. + #[cfg(feature = "dim3")] + #[wasm_bindgen(constructor)] + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self(Vector::new(x, y, z)) + } + + /// The `x` component of this vector. + #[wasm_bindgen(getter)] + pub fn x(&self) -> f32 { + self.0.x + } + + /// Sets the `x` component of this vector. + #[wasm_bindgen(setter)] + pub fn set_x(&mut self, x: f32) { + self.0.x = x + } + + /// The `y` component of this vector. + #[wasm_bindgen(getter)] + pub fn y(&self) -> f32 { + self.0.y + } + + /// Sets the `y` component of this vector. + #[wasm_bindgen(setter)] + pub fn set_y(&mut self, y: f32) { + self.0.y = y + } + + /// The `z` component of this vector. + #[cfg(feature = "dim3")] + #[wasm_bindgen(getter)] + pub fn z(&self) -> f32 { + self.0.z + } + + /// Sets the `z` component of this vector. + #[cfg(feature = "dim3")] + #[wasm_bindgen(setter)] + pub fn set_z(&mut self, z: f32) { + self.0.z = z + } + + /// Create a new 2D vector from this vector with its components rearranged as `{x, y}`. + #[cfg(feature = "dim2")] + pub fn xy(&self) -> Self { + Self(self.0.xy()) + } + + /// Create a new 2D vector from this vector with its components rearranged as `{y, x}`. + #[cfg(feature = "dim2")] + pub fn yx(&self) -> Self { + Self(self.0.yx()) + } + + /// Create a new 2D vector from this vector with its components rearranged as `{z, y}`. + #[cfg(feature = "dim2")] + #[cfg(feature = "dim3")] + pub fn zy(&self) -> Self { + Self(self.0.zy()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{x, y, z}`. + /// + /// This will effectively return a copy of `this`. This method exist for completeness with the + /// other swizzling functions. + #[cfg(feature = "dim3")] + pub fn xyz(&self) -> Self { + Self(self.0.xyz()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{y, x, z}`. + #[cfg(feature = "dim3")] + pub fn yxz(&self) -> Self { + Self(self.0.yxz()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{z, x, y}`. + #[cfg(feature = "dim3")] + pub fn zxy(&self) -> Self { + Self(self.0.zxy()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{x, z, y}`. + #[cfg(feature = "dim3")] + pub fn xzy(&self) -> Self { + Self(self.0.xzy()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{y, z, x}`. + #[cfg(feature = "dim3")] + pub fn yzx(&self) -> Self { + Self(self.0.yzx()) + } + + /// Create a new 3D vector from this vector with its components rearranged as `{z, y, x}`. + #[cfg(feature = "dim3")] + pub fn zyx(&self) -> Self { + Self(self.0.zyx()) + } +} + +#[wasm_bindgen] +#[repr(transparent)] +#[derive(Copy, Clone)] +#[cfg(feature = "dim3")] +pub struct RawSdpMatrix3(pub(crate) SdpMatrix3); + +#[cfg(feature = "dim3")] +impl From> for RawSdpMatrix3 { + fn from(v: SdpMatrix3) -> Self { + RawSdpMatrix3(v) + } +} + +#[wasm_bindgen] +#[cfg(feature = "dim3")] +impl RawSdpMatrix3 { + /// Row major list of the upper-triangular part of the symmetric matrix. + pub fn elements(&self) -> Float32Array { + let m = self.0; + let output = Float32Array::new_with_length(6); + output.copy_from(&[m.m11, m.m12, m.m13, m.m22, m.m23, m.m33]); + output + } +} diff --git a/thirdparty/rapier.js/src/pipeline/debug_render_pipeline.rs b/thirdparty/rapier.js/src/pipeline/debug_render_pipeline.rs new file mode 100644 index 00000000..1676e59b --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/debug_render_pipeline.rs @@ -0,0 +1,152 @@ +use crate::dynamics::{RawImpulseJointSet, RawMultibodyJointSet, RawRigidBodySet}; +use crate::geometry::{RawColliderSet, RawNarrowPhase}; +use js_sys::Float32Array; +use palette::convert::IntoColorUnclamped; +use palette::rgb::Rgba; +use palette::Hsla; +use rapier::dynamics::{RigidBody, RigidBodySet}; +use rapier::geometry::ColliderSet; +use rapier::math::{Point, Real}; +use rapier::pipeline::{DebugRenderBackend, DebugRenderObject, DebugRenderPipeline}; +use rapier::prelude::{QueryFilter, QueryFilterFlags}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawDebugRenderPipeline { + pub(crate) raw: DebugRenderPipeline, + vertices: Vec, + colors: Vec, +} + +#[wasm_bindgen] +impl RawDebugRenderPipeline { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawDebugRenderPipeline { + raw: DebugRenderPipeline::default(), + vertices: vec![], + colors: vec![], + } + } + + pub fn vertices(&self) -> Float32Array { + let output = Float32Array::new_with_length(self.vertices.len() as u32); + output.copy_from(&self.vertices); + output + } + + pub fn colors(&self) -> Float32Array { + let output = Float32Array::new_with_length(self.colors.len() as u32); + output.copy_from(&self.colors); + output + } + + pub fn render( + &mut self, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + impulse_joints: &RawImpulseJointSet, + multibody_joints: &RawMultibodyJointSet, + narrow_phase: &RawNarrowPhase, + filter_flags: u32, + filter_predicate: &js_sys::Function, + ) { + self.vertices.clear(); + self.colors.clear(); + + crate::utils::with_filter(filter_predicate, |predicate| { + let mut backend = CopyToBuffersBackend { + filter: QueryFilter { + flags: QueryFilterFlags::from_bits(filter_flags) + .unwrap_or(QueryFilterFlags::empty()), + groups: None, + exclude_collider: None, + exclude_rigid_body: None, + predicate, + }, + bodies: &bodies.0, + colliders: &colliders.0, + vertices: &mut self.vertices, + colors: &mut self.colors, + }; + + self.raw.render( + &mut backend, + &bodies.0, + &colliders.0, + &impulse_joints.0, + &multibody_joints.0, + &narrow_phase.0, + ) + }) + } +} + +struct CopyToBuffersBackend<'a> { + filter: QueryFilter<'a>, + bodies: &'a RigidBodySet, + colliders: &'a ColliderSet, + vertices: &'a mut Vec, + colors: &'a mut Vec, +} + +impl<'a> DebugRenderBackend for CopyToBuffersBackend<'a> { + fn filter_object(&self, object: DebugRenderObject) -> bool { + let test_rigid_body = |rb: &RigidBody| { + rb.colliders().iter().all(|handle| { + let Some(co) = self.colliders.get(*handle) else { + return false; + }; + self.filter.test(self.bodies, *handle, co) + }) + }; + + match object { + DebugRenderObject::Collider(handle, co) + | DebugRenderObject::ColliderAabb(handle, co, _) => { + self.filter.test(self.bodies, handle, co) + } + DebugRenderObject::ContactPair(pair, co1, co2) => { + self.filter.test(self.bodies, pair.collider1, co1) + && self.filter.test(self.bodies, pair.collider2, co2) + } + DebugRenderObject::ImpulseJoint(_, joint) => { + let Some(rb1) = self.bodies.get(joint.body1) else { + return false; + }; + let Some(rb2) = self.bodies.get(joint.body2) else { + return false; + }; + test_rigid_body(rb1) && test_rigid_body(rb2) + } + DebugRenderObject::MultibodyJoint(_, _, link) => { + let Some(rb) = self.bodies.get(link.rigid_body_handle()) else { + return false; + }; + test_rigid_body(rb) + } + DebugRenderObject::RigidBody(_, rb) => test_rigid_body(rb), + } + } + + /// Draws a colored line. + /// + /// Note that this method can be called multiple time for the same `object`. + fn draw_line( + &mut self, + _object: DebugRenderObject, + a: Point, + b: Point, + color: [f32; 4], + ) { + self.vertices.extend_from_slice(a.coords.as_slice()); + self.vertices.extend_from_slice(b.coords.as_slice()); + + // Convert to RGB which will be easier to handle in JS. + let hsl = Hsla::new(color[0], color[1], color[2], color[3]); + let rgb: Rgba = hsl.into_color_unclamped(); + self.colors.extend_from_slice(&[ + rgb.red, rgb.green, rgb.blue, rgb.alpha, rgb.red, rgb.green, rgb.blue, rgb.alpha, + ]); + } +} diff --git a/thirdparty/rapier.js/src/pipeline/event_queue.rs b/thirdparty/rapier.js/src/pipeline/event_queue.rs new file mode 100644 index 00000000..fb6034d9 --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/event_queue.rs @@ -0,0 +1,140 @@ +use crate::math::RawVector; +use crate::utils; +use crate::utils::FlatHandle; +use rapier::geometry::{CollisionEvent, ContactForceEvent}; +use rapier::pipeline::ChannelEventCollector; +use std::sync::mpsc::Receiver; +use wasm_bindgen::prelude::*; + +/// A structure responsible for collecting events generated +/// by the physics engine. +#[wasm_bindgen] +pub struct RawEventQueue { + pub(crate) collector: ChannelEventCollector, + collision_events: Receiver, + contact_force_events: Receiver, + pub(crate) auto_drain: bool, +} + +#[wasm_bindgen] +pub struct RawContactForceEvent(ContactForceEvent); + +#[wasm_bindgen] +impl RawContactForceEvent { + /// The first collider involved in the contact. + pub fn collider1(&self) -> FlatHandle { + crate::utils::flat_handle(self.0.collider1.0) + } + + /// The second collider involved in the contact. + pub fn collider2(&self) -> FlatHandle { + crate::utils::flat_handle(self.0.collider2.0) + } + + /// The sum of all the forces between the two colliders. + pub fn total_force(&self) -> RawVector { + RawVector(self.0.total_force) + } + + /// The sum of the magnitudes of each force between the two colliders. + /// + /// Note that this is **not** the same as the magnitude of `self.total_force`. + /// Here we are summing the magnitude of all the forces, instead of taking + /// the magnitude of their sum. + pub fn total_force_magnitude(&self) -> f32 { + self.0.total_force_magnitude + } + + /// The world-space (unit) direction of the force with strongest magnitude. + pub fn max_force_direction(&self) -> RawVector { + RawVector(self.0.max_force_direction) + } + + /// The magnitude of the largest force at a contact point of this contact pair. + pub fn max_force_magnitude(&self) -> f32 { + self.0.max_force_magnitude + } +} + +// #[wasm_bindgen] +// /// The proximity state of a sensor collider and another collider. +// pub enum RawIntersection { +// /// The sensor is intersecting the other collider. +// Intersecting = 0, +// /// The sensor is within tolerance margin of the other collider. +// WithinMargin = 1, +// /// The sensor is disjoint from the other collider. +// Disjoint = 2, +// } + +#[wasm_bindgen] +impl RawEventQueue { + /// Creates a new event collector. + /// + /// # Parameters + /// - `autoDrain`: setting this to `true` is strongly recommended. If true, the collector will + /// be automatically drained before each `world.step(collector)`. If false, the collector will + /// keep all events in memory unless it is manually drained/cleared; this may lead to unbounded use of + /// RAM if no drain is performed. + #[wasm_bindgen(constructor)] + pub fn new(autoDrain: bool) -> Self { + let collision_channel = std::sync::mpsc::channel(); + let contact_force_channel = std::sync::mpsc::channel(); + let collector = ChannelEventCollector::new(collision_channel.0, contact_force_channel.0); + + Self { + collector, + collision_events: collision_channel.1, + contact_force_events: contact_force_channel.1, + auto_drain: autoDrain, + } + } + + /// Applies the given javascript closure on each collision event of this collector, then clear + /// the internal collision event buffer. + /// + /// # Parameters + /// - `f(handle1, handle2, started)`: JavaScript closure applied to each collision event. The + /// closure should take three arguments: two integers representing the handles of the colliders + /// involved in the collision, and a boolean indicating if the collision started (true) or stopped + /// (false). + pub fn drainCollisionEvents(&mut self, f: &js_sys::Function) { + let this = JsValue::null(); + while let Ok(event) = self.collision_events.try_recv() { + match event { + CollisionEvent::Started(co1, co2, _) => { + let h1 = utils::flat_handle(co1.0); + let h2 = utils::flat_handle(co2.0); + let _ = f.call3( + &this, + &JsValue::from(h1), + &JsValue::from(h2), + &JsValue::from_bool(true), + ); + } + CollisionEvent::Stopped(co1, co2, _) => { + let h1 = utils::flat_handle(co1.0); + let h2 = utils::flat_handle(co2.0); + let _ = f.call3( + &this, + &JsValue::from(h1), + &JsValue::from(h2), + &JsValue::from_bool(false), + ); + } + } + } + } + + pub fn drainContactForceEvents(&mut self, f: &js_sys::Function) { + let this = JsValue::null(); + while let Ok(event) = self.contact_force_events.try_recv() { + let _ = f.call1(&this, &JsValue::from(RawContactForceEvent(event))); + } + } + + /// Removes all events contained by this collector. + pub fn clear(&self) { + while let Ok(_) = self.collision_events.try_recv() {} + } +} diff --git a/thirdparty/rapier.js/src/pipeline/mod.rs b/thirdparty/rapier.js/src/pipeline/mod.rs new file mode 100644 index 00000000..23e19bfb --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/mod.rs @@ -0,0 +1,11 @@ +pub use self::debug_render_pipeline::*; +pub use self::event_queue::*; +pub use self::physics_hooks::*; +pub use self::physics_pipeline::*; +pub use self::serialization_pipeline::*; + +mod debug_render_pipeline; +mod event_queue; +mod physics_hooks; +mod physics_pipeline; +mod serialization_pipeline; diff --git a/thirdparty/rapier.js/src/pipeline/physics_hooks.rs b/thirdparty/rapier.js/src/pipeline/physics_hooks.rs new file mode 100644 index 00000000..2bbc8034 --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/physics_hooks.rs @@ -0,0 +1,217 @@ +use crate::utils; +use rapier::geometry::SolverFlags; +use rapier::pipeline::{ContactModificationContext, PairFilterContext, PhysicsHooks}; +use wasm_bindgen::prelude::*; + +pub struct RawPhysicsHooks { + pub this: js_sys::Object, + pub filter_contact_pair: js_sys::Function, + pub filter_intersection_pair: js_sys::Function, + // pub modify_solver_contacts: &'a js_sys::Function, +} + +// HACK: the RawPhysicsHooks is no longer Send+Sync because the JS objects are +// no longer Send+Sync since https://github.com/rustwasm/wasm-bindgen/pull/955 +// As far as this is confined to the bindings this should be fine since we +// never use threading in wasm. +unsafe impl Send for RawPhysicsHooks {} +unsafe impl Sync for RawPhysicsHooks {} + +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + +impl PhysicsHooks for RawPhysicsHooks { + fn filter_contact_pair(&self, ctxt: &PairFilterContext) -> Option { + let rb1 = ctxt + .rigid_body1 + .map(|rb| JsValue::from(utils::flat_handle(rb.0))) + .unwrap_or(JsValue::NULL); + let rb2 = ctxt + .rigid_body2 + .map(|rb| JsValue::from(utils::flat_handle(rb.0))) + .unwrap_or(JsValue::NULL); + + let result = self + .filter_contact_pair + .bind2( + &self.this, + &JsValue::from(utils::flat_handle(ctxt.collider1.0)), + &JsValue::from(utils::flat_handle(ctxt.collider2.0)), + ) + .call2(&self.this, &rb1, &rb2) + .ok()?; + let flags = result.as_f64()?; + // TODO: not sure exactly why we have to do `flags as u32` instead + // of `flags.to_bits() as u32`. + SolverFlags::from_bits(flags as u32) + } + + fn filter_intersection_pair(&self, ctxt: &PairFilterContext) -> bool { + let rb1 = ctxt + .rigid_body1 + .map(|rb| JsValue::from(utils::flat_handle(rb.0))) + .unwrap_or(JsValue::NULL); + let rb2 = ctxt + .rigid_body2 + .map(|rb| JsValue::from(utils::flat_handle(rb.0))) + .unwrap_or(JsValue::NULL); + + self.filter_intersection_pair + .bind2( + &self.this, + &JsValue::from(utils::flat_handle(ctxt.collider1.0)), + &JsValue::from(utils::flat_handle(ctxt.collider2.0)), + ) + .call2(&self.this, &rb1, &rb2) + .ok() + .and_then(|res| res.as_bool()) + .unwrap_or(false) + } + + fn modify_solver_contacts(&self, _ctxt: &mut ContactModificationContext) {} +} + +/* NOTE: the following is an attempt to make contact modification work. + * +#[wasm_bindgen] +#[derive(Copy, Clone, Debug)] +pub struct RawContactManifold(*const ContactManifold); +pub struct RawSolverContact(*const SolverContact); + +#[wasm_bindgen] +pub struct RawContactModificationContext { + pub collider1: u32, + pub collider2: u32, + pub rigid_body1: Option, + pub rigid_body2: Option, + pub manifold: *const ContactManifold, + pub solver_contacts: *mut Vec, + normal: *mut Vector, + user_data: *mut u32, +} + +#[wasm_bindgen] +impl RawContactModificationContext { + pub fn collider1(&self) -> u32 { + self.collider1 + } + + pub fn collider2(&self) -> u32 { + self.collider2 + } + + #[wasm_bindgen(getter)] + pub fn normal(&self) -> RawVector { + unsafe { RawVector(*self.normal) } + } + + #[wasm_bindgen(setter)] + pub fn set_normal(&mut self, normal: RawVector) { + unsafe { + *self.normal = normal.0; + } + } + + #[wasm_bindgen(getter)] + pub fn user_data(&self) -> u32 { + unsafe { *self.user_data } + } + + #[wasm_bindgen(setter)] + pub fn set_user_data(&mut self, user_data: u32) { + unsafe { + *self.user_data = user_data; + } + } + + pub fn num_solver_contacts(&self) -> usize { + unsafe { (*self.solver_contacts).len() } + } + + pub fn clear_solver_contacts(&mut self) { + unsafe { (*self.solver_contacts).clear() } + } + + pub fn remove_solver_contact(&mut self, i: usize) { + unsafe { + if i < self.num_solver_contacts() { + (*self.solver_contacts).swap_remove(i); + } + } + } + + pub fn solver_contact_point(&self, i: usize) -> Option { + unsafe { + (*self.solver_contacts) + .get(i) + .map(|c| c.point.coords.into()) + } + } + + pub fn set_solver_contact_point(&mut self, i: usize, pt: &RawVector) { + unsafe { + if let Some(c) = (*self.solver_contacts).get_mut(i) { + c.point = pt.0.into() + } + } + } + + pub fn solver_contact_dist(&self, i: usize) -> Real { + unsafe { + (*self.solver_contacts) + .get(i) + .map(|c| c.dist) + .unwrap_or(0.0) + } + } + + pub fn set_solver_contact_dist(&mut self, i: usize, dist: Real) { + unsafe { + if let Some(c) = (*self.solver_contacts).get_mut(i) { + c.dist = dist + } + } + } + + pub fn solver_contact_friction(&self, i: usize) -> Real { + unsafe { (*self.solver_contacts)[i].friction } + } + + pub fn set_solver_contact_friction(&mut self, i: usize, friction: Real) { + unsafe { + if let Some(c) = (*self.solver_contacts).get_mut(i) { + c.friction = friction + } + } + } + + pub fn solver_contact_restitution(&self, i: usize) -> Real { + unsafe { (*self.solver_contacts)[i].restitution } + } + + pub fn set_solver_contact_restitution(&mut self, i: usize, restitution: Real) { + unsafe { + if let Some(c) = (*self.solver_contacts).get_mut(i) { + c.restitution = restitution + } + } + } + + pub fn solver_contact_tangent_velocity(&self, i: usize) -> RawVector { + unsafe { (*self.solver_contacts)[i].tangent_velocity.into() } + } + + pub fn set_solver_contact_tangent_velocity(&mut self, i: usize, vel: &RawVector) { + unsafe { + if let Some(c) = (*self.solver_contacts).get_mut(i) { + c.tangent_velocity = vel.0.into() + } + } + } +} +*/ diff --git a/thirdparty/rapier.js/src/pipeline/physics_pipeline.rs b/thirdparty/rapier.js/src/pipeline/physics_pipeline.rs new file mode 100644 index 00000000..287cfeea --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/physics_pipeline.rs @@ -0,0 +1,170 @@ +use crate::dynamics::{ + RawCCDSolver, RawImpulseJointSet, RawIntegrationParameters, RawIslandManager, + RawMultibodyJointSet, RawRigidBodySet, +}; +use crate::geometry::{RawBroadPhase, RawColliderSet, RawNarrowPhase}; +use crate::math::RawVector; +use crate::pipeline::{RawEventQueue, RawPhysicsHooks}; +use crate::rapier::pipeline::PhysicsPipeline; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct RawPhysicsPipeline(pub(crate) PhysicsPipeline); + +#[wasm_bindgen] +impl RawPhysicsPipeline { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + let mut pipeline = PhysicsPipeline::new(); + pipeline.counters.disable(); // Disable perf counters by default. + RawPhysicsPipeline(pipeline) + } + + pub fn set_profiler_enabled(&mut self, enabled: bool) { + if enabled { + self.0.counters.enable(); + } else { + self.0.counters.disable(); + } + } + + pub fn is_profiler_enabled(&self) -> bool { + self.0.counters.enabled() + } + + pub fn timing_step(&self) -> f64 { + self.0.counters.step_time_ms() + } + + pub fn timing_collision_detection(&self) -> f64 { + self.0.counters.collision_detection_time_ms() + } + + pub fn timing_broad_phase(&self) -> f64 { + self.0.counters.broad_phase_time_ms() + } + + pub fn timing_narrow_phase(&self) -> f64 { + self.0.counters.narrow_phase_time_ms() + } + + pub fn timing_solver(&self) -> f64 { + self.0.counters.solver_time_ms() + } + + pub fn timing_velocity_assembly(&self) -> f64 { + self.0.counters.solver.velocity_assembly_time.time_ms() + } + + pub fn timing_velocity_resolution(&self) -> f64 { + self.0.counters.velocity_resolution_time_ms() + } + + pub fn timing_velocity_update(&self) -> f64 { + self.0.counters.velocity_update_time_ms() + } + + pub fn timing_velocity_writeback(&self) -> f64 { + self.0.counters.solver.velocity_writeback_time.time_ms() + } + + pub fn timing_ccd(&self) -> f64 { + self.0.counters.ccd_time_ms() + } + + pub fn timing_ccd_toi_computation(&self) -> f64 { + self.0.counters.ccd.toi_computation_time.time_ms() + } + + pub fn timing_ccd_broad_phase(&self) -> f64 { + self.0.counters.ccd.broad_phase_time.time_ms() + } + + pub fn timing_ccd_narrow_phase(&self) -> f64 { + self.0.counters.ccd.narrow_phase_time.time_ms() + } + + pub fn timing_ccd_solver(&self) -> f64 { + self.0.counters.ccd.solver_time.time_ms() + } + + pub fn timing_island_construction(&self) -> f64 { + self.0.counters.island_construction_time_ms() + } + + pub fn timing_user_changes(&self) -> f64 { + self.0.counters.stages.user_changes.time_ms() + } + + pub fn step( + &mut self, + gravity: &RawVector, + integrationParameters: &RawIntegrationParameters, + islands: &mut RawIslandManager, + broadPhase: &mut RawBroadPhase, + narrowPhase: &mut RawNarrowPhase, + bodies: &mut RawRigidBodySet, + colliders: &mut RawColliderSet, + joints: &mut RawImpulseJointSet, + articulations: &mut RawMultibodyJointSet, + ccd_solver: &mut RawCCDSolver, + ) { + self.0.step( + &gravity.0, + &integrationParameters.0, + &mut islands.0, + &mut broadPhase.0, + &mut narrowPhase.0, + &mut bodies.0, + &mut colliders.0, + &mut joints.0, + &mut articulations.0, + &mut ccd_solver.0, + &(), + &(), + ); + } + + pub fn stepWithEvents( + &mut self, + gravity: &RawVector, + integrationParameters: &RawIntegrationParameters, + islands: &mut RawIslandManager, + broadPhase: &mut RawBroadPhase, + narrowPhase: &mut RawNarrowPhase, + bodies: &mut RawRigidBodySet, + colliders: &mut RawColliderSet, + joints: &mut RawImpulseJointSet, + articulations: &mut RawMultibodyJointSet, + ccd_solver: &mut RawCCDSolver, + eventQueue: &mut RawEventQueue, + hookObject: js_sys::Object, + hookFilterContactPair: js_sys::Function, + hookFilterIntersectionPair: js_sys::Function, + ) { + if eventQueue.auto_drain { + eventQueue.clear(); + } + + let hooks = RawPhysicsHooks { + this: hookObject, + filter_contact_pair: hookFilterContactPair, + filter_intersection_pair: hookFilterIntersectionPair, + }; + + self.0.step( + &gravity.0, + &integrationParameters.0, + &mut islands.0, + &mut broadPhase.0, + &mut narrowPhase.0, + &mut bodies.0, + &mut colliders.0, + &mut joints.0, + &mut articulations.0, + &mut ccd_solver.0, + &hooks, + &eventQueue.collector, + ); + } +} diff --git a/thirdparty/rapier.js/src/pipeline/serialization_pipeline.rs b/thirdparty/rapier.js/src/pipeline/serialization_pipeline.rs new file mode 100644 index 00000000..97076f63 --- /dev/null +++ b/thirdparty/rapier.js/src/pipeline/serialization_pipeline.rs @@ -0,0 +1,145 @@ +use crate::dynamics::{ + RawImpulseJointSet, RawIntegrationParameters, RawIslandManager, RawMultibodyJointSet, + RawRigidBodySet, +}; +use crate::geometry::{RawBroadPhase, RawColliderSet, RawNarrowPhase}; +use crate::math::RawVector; +use js_sys::Uint8Array; +use rapier::dynamics::{ + ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, RigidBodySet, +}; +use rapier::geometry::{ColliderSet, DefaultBroadPhase, NarrowPhase}; +use rapier::math::Vector; +use wasm_bindgen::prelude::*; + +#[derive(Serialize)] +struct SerializableWorld<'a> { + gravity: &'a Vector, + integration_parameters: &'a IntegrationParameters, + islands: &'a IslandManager, + broad_phase: &'a DefaultBroadPhase, + narrow_phase: &'a NarrowPhase, + bodies: &'a RigidBodySet, + colliders: &'a ColliderSet, + impulse_joints: &'a ImpulseJointSet, + multibody_joints: &'a MultibodyJointSet, +} + +#[derive(Deserialize)] +struct DeserializableWorld { + gravity: Vector, + integration_parameters: IntegrationParameters, + islands: IslandManager, + broad_phase: DefaultBroadPhase, + narrow_phase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, +} + +#[wasm_bindgen] +pub struct RawDeserializedWorld { + gravity: Option, + integrationParameters: Option, + islands: Option, + broadPhase: Option, + narrowPhase: Option, + bodies: Option, + colliders: Option, + impulse_joints: Option, + multibody_joints: Option, +} + +#[wasm_bindgen] +impl RawDeserializedWorld { + pub fn takeGravity(&mut self) -> Option { + self.gravity.take() + } + + pub fn takeIntegrationParameters(&mut self) -> Option { + self.integrationParameters.take() + } + + pub fn takeIslandManager(&mut self) -> Option { + self.islands.take() + } + + pub fn takeBroadPhase(&mut self) -> Option { + self.broadPhase.take() + } + + pub fn takeNarrowPhase(&mut self) -> Option { + self.narrowPhase.take() + } + + pub fn takeBodies(&mut self) -> Option { + self.bodies.take() + } + + pub fn takeColliders(&mut self) -> Option { + self.colliders.take() + } + + pub fn takeImpulseJoints(&mut self) -> Option { + self.impulse_joints.take() + } + + pub fn takeMultibodyJoints(&mut self) -> Option { + self.multibody_joints.take() + } +} + +#[wasm_bindgen] +pub struct RawSerializationPipeline; + +#[wasm_bindgen] +impl RawSerializationPipeline { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawSerializationPipeline + } + + pub fn serializeAll( + &self, + gravity: &RawVector, + integrationParameters: &RawIntegrationParameters, + islands: &RawIslandManager, + broadPhase: &RawBroadPhase, + narrowPhase: &RawNarrowPhase, + bodies: &RawRigidBodySet, + colliders: &RawColliderSet, + impulse_joints: &RawImpulseJointSet, + multibody_joints: &RawMultibodyJointSet, + ) -> Option { + let to_serialize = SerializableWorld { + gravity: &gravity.0, + integration_parameters: &integrationParameters.0, + islands: &islands.0, + broad_phase: &broadPhase.0, + narrow_phase: &narrowPhase.0, + bodies: &bodies.0, + colliders: &colliders.0, + impulse_joints: &impulse_joints.0, + multibody_joints: &multibody_joints.0, + }; + let snap = bincode::serialize(&to_serialize).ok()?; + Some(Uint8Array::from(&snap[..])) + } + + pub fn deserializeAll(&self, data: Uint8Array) -> Option { + let data = data.to_vec(); + let d: DeserializableWorld = bincode::deserialize(&data[..]).ok()?; + Some(RawDeserializedWorld { + gravity: Some(RawVector(d.gravity)), + integrationParameters: Some(RawIntegrationParameters(d.integration_parameters)), + islands: Some(RawIslandManager(d.islands)), + broadPhase: Some(RawBroadPhase(d.broad_phase)), + narrowPhase: Some(RawNarrowPhase(d.narrow_phase)), + bodies: Some(RawRigidBodySet(d.bodies)), + colliders: Some(RawColliderSet(d.colliders)), + impulse_joints: Some(RawImpulseJointSet(d.impulse_joints)), + multibody_joints: Some(RawMultibodyJointSet(d.multibody_joints)), + }) + } +} diff --git a/thirdparty/rapier.js/src/utils.rs b/thirdparty/rapier.js/src/utils.rs new file mode 100644 index 00000000..1b46e7cc --- /dev/null +++ b/thirdparty/rapier.js/src/utils.rs @@ -0,0 +1,79 @@ +use rapier::data::Index; +use rapier::dynamics::{ImpulseJointHandle, MultibodyJointHandle, RigidBodyHandle}; +use rapier::geometry::{Collider, ColliderHandle}; +use wasm_bindgen::JsValue; + +pub type FlatHandle = f64; + +#[inline(always)] +pub fn collider_handle(id: FlatHandle) -> ColliderHandle { + ColliderHandle::from_raw_parts(id.to_bits() as u32, (id.to_bits() >> 32) as u32) +} + +#[inline(always)] +pub fn body_handle(id: FlatHandle) -> RigidBodyHandle { + RigidBodyHandle::from_raw_parts(id.to_bits() as u32, (id.to_bits() >> 32) as u32) +} + +#[inline(always)] +pub fn impulse_joint_handle(id: FlatHandle) -> ImpulseJointHandle { + ImpulseJointHandle::from_raw_parts(id.to_bits() as u32, (id.to_bits() >> 32) as u32) +} + +#[inline(always)] +pub fn multibody_joint_handle(id: FlatHandle) -> MultibodyJointHandle { + MultibodyJointHandle::from_raw_parts(id.to_bits() as u32, (id.to_bits() >> 32) as u32) +} + +#[inline(always)] +pub fn flat_handle(id: Index) -> FlatHandle { + let (i, g) = id.into_raw_parts(); + FlatHandle::from_bits(i as u64 | ((g as u64) << 32)) +} + +// pub type FlatHandle = u32; +// +// #[inline(always)] +// pub fn collider_handle(id: FlatHandle) -> ColliderHandle { +// ColliderHandle::from_raw_parts(id as u32, (id >> 16) as u32) +// } +// +// #[inline(always)] +// pub fn body_handle(id: FlatHandle) -> RigidBodyHandle { +// RigidBodyHandle::from_raw_parts(id as u32, (id >> 16) as u32) +// } +// +// #[inline(always)] +// pub fn impulse_joint_handle(id: FlatHandle) -> ImpulseJointHandle { +// ImpulseJointHandle::from_raw_parts(id as u32, (id >> 16) as u32) +// } +// +// #[inline(always)] +// pub fn multibody_joint_handle(id: FlatHandle) -> MultibodyJointHandle { +// MultibodyJointHandle::from_raw_parts(id as u32, (id >> 16) as u32) +// } +// +// #[inline(always)] +// pub fn flat_handle(id: Index) -> FlatHandle { +// let (i, g) = id.into_raw_parts(); +// i as u32 | ((g as u32) << 16) +// } + +#[inline(always)] +pub fn with_filter( + filter: &js_sys::Function, + f: impl FnOnce(Option<&dyn Fn(ColliderHandle, &Collider) -> bool>) -> T, +) -> T { + if filter.is_function() { + let filtercb = move |handle: ColliderHandle, _: &Collider| match filter + .call1(&JsValue::null(), &JsValue::from(flat_handle(handle.0))) + { + Err(_) => true, + Ok(val) => val.as_bool().unwrap_or(true), + }; + + f(Some(&filtercb)) + } else { + f(None) + } +} diff --git a/thirdparty/rapier.js/testbed2d/.npmignore b/thirdparty/rapier.js/testbed2d/.npmignore new file mode 100644 index 00000000..488f9125 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/.npmignore @@ -0,0 +1,3 @@ +webpack.config2d.js +src +*.tgz \ No newline at end of file diff --git a/thirdparty/rapier.js/testbed2d/package.json b/thirdparty/rapier.js/testbed2d/package.json new file mode 100644 index 00000000..9152daa0 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/package.json @@ -0,0 +1,32 @@ +{ + "author": "sebcrozet ", + "name": "rapier-testbed2d", + "version": "0.1.0", + "description": "JavaScript testbed for rapier.", + "license": "Apache-2.0", + "main": "dist/rapier-testbed2d.js", + "scripts": { + "build": "rimraf dist pkg && webpack", + "start": "rimraf dist pkg && webpack serve --open --node-env development" + }, + "dependencies": { + "@dimforge/rapier2d": "file:../builds/rapier2d", + "hash-wasm": "^4.12.0", + "lil-gui": "^0.17.0", + "pixi-viewport": "^4.37.0", + "pixi.js": "^6.3.2", + "seedrandom": "^3.0.5", + "stats.js": "^0.17.0" + }, + "devDependencies": { + "@types/seedrandom": "^3.0.2", + "@types/stats.js": "^0.17.0", + "copy-webpack-plugin": "^11.0.0", + "rimraf": "^3.0.2", + "ts-loader": "^9.4.1", + "typescript": "^4.8.4", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.1" + } +} diff --git a/thirdparty/rapier.js/testbed2d/publish.sh b/thirdparty/rapier.js/testbed2d/publish.sh new file mode 100644 index 00000000..087c63be --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/publish.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +npm run build +rsync -av dist/ crozet@ssh.cluster003.hosting.ovh.net:/home/crozet/rapier/demos2d +# rsync -av dist/Box2D_v2.3.1_min.wasm.wasm crozet@ssh.cluster003.hosting.ovh.net:/home/crozet/rapier/Box2D_v2.3.1_min.wasm.wasm diff --git a/thirdparty/rapier.js/testbed2d/src/Graphics.ts b/thirdparty/rapier.js/testbed2d/src/Graphics.ts new file mode 100644 index 00000000..4f9448f8 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/Graphics.ts @@ -0,0 +1,269 @@ +import * as PIXI from "pixi.js"; +import {Viewport} from "pixi-viewport"; +import type * as RAPIER from "@dimforge/rapier2d"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +const BOX_INSTANCE_INDEX = 0; +const BALL_INSTANCE_INDEX = 1; + +var kk = 0; + +export class Graphics { + coll2gfx: Map; + colorIndex: number; + colorPalette: Array; + renderer: PIXI.Renderer; + scene: PIXI.Container; + viewport: Viewport; + instanceGroups: Array>; + lines: PIXI.Graphics; + + constructor() { + // High pixel Ratio make the rendering extremely slow, so we cap it. + // const pixelRatio = window.devicePixelRatio ? Math.min(window.devicePixelRatio, 1.5) : 1; + + this.coll2gfx = new Map(); + this.colorIndex = 0; + this.colorPalette = [0xf3d9b1, 0x98c1d9, 0x053c5e, 0x1f7a8c]; + this.renderer = new PIXI.Renderer({ + backgroundColor: 0x292929, + antialias: true, + // resolution: pixelRatio, + width: window.innerWidth, + height: window.innerHeight, + }); + + this.scene = new PIXI.Container(); + document.body.appendChild(this.renderer.view); + + this.viewport = new Viewport({ + screenWidth: window.innerWidth, + screenHeight: window.innerHeight, + worldWidth: 1000, + worldHeight: 1000, + interaction: this.renderer.plugins.interaction, + }); + + this.scene.addChild(this.viewport); + this.viewport.drag().pinch().wheel().decelerate(); + + let me = this; + + function onWindowResize() { + me.renderer.resize(window.innerWidth, window.innerHeight); + } + + function onContextMenu(event: UIEvent) { + event.preventDefault(); + } + + document.oncontextmenu = onContextMenu; + document.body.oncontextmenu = onContextMenu; + + window.addEventListener("resize", onWindowResize, false); + + this.initInstances(); + } + + initInstances() { + this.instanceGroups = []; + this.instanceGroups.push( + this.colorPalette.map((color) => { + let graphics = new PIXI.Graphics(); + graphics.beginFill(color); + graphics.drawRect(-1.0, 1.0, 2.0, -2.0); + graphics.endFill(); + return graphics; + }), + ); + + this.instanceGroups.push( + this.colorPalette.map((color) => { + let graphics = new PIXI.Graphics(); + graphics.beginFill(color); + graphics.drawCircle(0.0, 0.0, 1.0); + graphics.endFill(); + return graphics; + }), + ); + } + + render(world: RAPIER.World, debugRender: Boolean) { + kk += 1; + + if (!this.lines) { + this.lines = new PIXI.Graphics(); + this.viewport.addChild(this.lines); + } + + if (debugRender) { + let buffers = world.debugRender(); + let vtx = buffers.vertices; + let cls = buffers.colors; + + this.lines.clear(); + + for (let i = 0; i < vtx.length / 4; i += 1) { + let color = PIXI.utils.rgb2hex([ + cls[i * 8], + cls[i * 8 + 1], + cls[i * 8 + 2], + ]); + this.lines.lineStyle(1.0, color, cls[i * 8 + 3], 0.5, true); + this.lines.moveTo(vtx[i * 4], -vtx[i * 4 + 1]); + this.lines.lineTo(vtx[i * 4 + 2], -vtx[i * 4 + 3]); + } + } else { + this.lines.clear(); + } + + this.updatePositions(world); + this.renderer.render(this.scene); + } + + lookAt(pos: {zoom: number; target: {x: number; y: number}}) { + this.viewport.setZoom(pos.zoom); + this.viewport.moveCenter(pos.target.x, pos.target.y); + } + + updatePositions(world: RAPIER.World) { + world.forEachCollider((elt) => { + let gfx = this.coll2gfx.get(elt.handle); + let translation = elt.translation(); + let rotation = elt.rotation(); + + if (!!gfx) { + gfx.position.x = translation.x; + gfx.position.y = -translation.y; + gfx.rotation = -rotation; + } + }); + } + + reset() { + this.coll2gfx.forEach((gfx) => { + this.viewport.removeChild(gfx); + gfx.destroy(); + }); + this.coll2gfx = new Map(); + this.colorIndex = 0; + } + + addCollider( + RAPIER: RAPIER_API, + world: RAPIER.World, + collider: RAPIER.Collider, + ) { + let i; + let parent = collider.parent(); + let instance; + let graphics; + let vertices; + let instanceId = parent.isFixed() ? 0 : this.colorIndex + 1; + + switch (collider.shapeType()) { + case RAPIER.ShapeType.Cuboid: + let hext = collider.halfExtents(); + instance = this.instanceGroups[BOX_INSTANCE_INDEX][instanceId]; + graphics = instance.clone(); + graphics.scale.x = hext.x; + graphics.scale.y = hext.y; + this.viewport.addChild(graphics); + break; + case RAPIER.ShapeType.Ball: + let rad = collider.radius(); + instance = this.instanceGroups[BALL_INSTANCE_INDEX][instanceId]; + graphics = instance.clone(); + graphics.scale.x = rad; + graphics.scale.y = rad; + this.viewport.addChild(graphics); + break; + case RAPIER.ShapeType.Polyline: + vertices = Array.from(collider.vertices()); + graphics = new PIXI.Graphics(); + graphics + .lineStyle(0.2, this.colorPalette[instanceId]) + .moveTo(vertices[0], -vertices[1]); + + for (i = 2; i < vertices.length; i += 2) { + graphics.lineTo(vertices[i], -vertices[i + 1]); + } + + this.viewport.addChild(graphics); + break; + case RAPIER.ShapeType.HeightField: + let heights = Array.from(collider.heightfieldHeights()); + let scale = collider.heightfieldScale(); + let step = scale.x / (heights.length - 1); + + graphics = new PIXI.Graphics(); + graphics + .lineStyle(0.2, this.colorPalette[instanceId]) + .moveTo(-scale.x / 2.0, -heights[0] * scale.y); + + for (i = 1; i < heights.length; i += 1) { + graphics.lineTo( + -scale.x / 2.0 + i * step, + -heights[i] * scale.y, + ); + } + + this.viewport.addChild(graphics); + break; + case RAPIER.ShapeType.ConvexPolygon: + vertices = Array.from(collider.vertices()); + graphics = new PIXI.Graphics(); + graphics.beginFill(this.colorPalette[instanceId], 1.0); + graphics.moveTo(vertices[0], -vertices[1]); + + for (i = 2; i < vertices.length; i += 2) { + graphics.lineTo(vertices[i], -vertices[i + 1]); + } + + this.viewport.addChild(graphics); + break; + case RAPIER.ShapeType.Voxels: + graphics = new PIXI.Graphics(); + graphics.beginFill(this.colorPalette[instanceId], 1.0); + collider.clearShapeCache(); + let shape = collider.shape as RAPIER.Voxels; + let gridCoords = shape.data; + let sz = shape.voxelSize; + + for (i = 0; i < gridCoords.length; i += 2) { + let minx = gridCoords[i] * sz.x; + let miny = gridCoords[i + 1] * sz.y; + let maxx = minx + sz.x; + let maxy = miny + sz.y; + + graphics.moveTo(minx, -miny); + graphics.lineTo(maxx, -miny); + graphics.lineTo(maxx, -maxy); + graphics.lineTo(minx, -maxy); + } + + this.viewport.addChild(graphics); + break; + default: + console.log("Unknown shape to render: ", collider.shapeType()); + return; + } + + let t = collider.translation(); + let r = collider.rotation(); + // dummy.position.set(t.x, t.y, t.z); + // dummy.quaternion.set(r.x, r.y, r.z, r.w); + // dummy.scale.set(instanceDesc.scale.x, instanceDesc.scale.y, instanceDesc.scale.z); + // dummy.updateMatrix(); + // instance.setMatrixAt(instanceDesc.elementId, dummy.matrix); + // instance.instanceMatrix.needsUpdate = true; + graphics.position.x = t.x; + graphics.position.y = -t.y; + graphics.rotation = r; + + this.coll2gfx.set(collider.handle, graphics); + this.colorIndex = + (this.colorIndex + 1) % (this.colorPalette.length - 1); + } +} diff --git a/thirdparty/rapier.js/testbed2d/src/Gui.ts b/thirdparty/rapier.js/testbed2d/src/Gui.ts new file mode 100644 index 00000000..a410607e --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/Gui.ts @@ -0,0 +1,113 @@ +import GUI from "lil-gui"; +import * as Stats from "stats.js"; +import type {Testbed} from "./Testbed"; + +export interface DebugInfos { + token: number; + stepId: number; + worldHash: string; + worldHashTime: number; + snapshotTime: number; +} + +export class Gui { + stats: Stats; + rapierVersion: string; + maxTimePanelValue: number; + stepTimePanel: Stats.Panel; + gui: GUI; + debugText: HTMLDivElement; + + constructor(testbed: Testbed, simulationParameters: Testbed["parameters"]) { + // Timings + this.stats = new Stats(); + this.rapierVersion = testbed.RAPIER.version(); + this.maxTimePanelValue = 16.0; + this.stepTimePanel = this.stats.addPanel( + new Stats.Panel("ms (step)", "#ff8", "#221"), + ); + this.stats.showPanel(this.stats.dom.children.length - 1); + document.body.appendChild(this.stats.dom); + + var backends = simulationParameters.backends; + var demos = Array.from(simulationParameters.builders.keys()); + var me = this; + + // For configuring simulation parameters. + this.gui = new GUI({ + title: "Rapier JS demos", + }); + var currDemo = this.gui + .add(simulationParameters, "demo", demos) + .onChange((demo: string) => { + testbed.switchToDemo(demo); + }); + this.gui + .add(simulationParameters, "numSolverIters", 0, 20) + .step(1) + .listen(); + this.gui + .add(simulationParameters, "debugInfos") + .listen() + .onChange((value: boolean) => { + me.debugText.style.visibility = value ? "visible" : "hidden"; + }); + this.gui.add(simulationParameters, "debugRender").listen(); + this.gui.add(simulationParameters, "running").listen(); + this.gui.add(simulationParameters, "step"); + simulationParameters.step = () => { + simulationParameters.stepping = true; + }; + this.gui.add(simulationParameters, "takeSnapshot"); + simulationParameters.takeSnapshot = () => { + testbed.takeSnapshot(); + }; + this.gui.add(simulationParameters, "restoreSnapshot"); + simulationParameters.restoreSnapshot = () => { + testbed.restoreSnapshot(); + }; + this.gui.add(simulationParameters, "restart"); + simulationParameters.restart = () => { + testbed.switchToDemo(currDemo.getValue()); + }; + + /* + * Block of text for debug infos. + */ + this.debugText = document.createElement("div"); + this.debugText.style.position = "absolute"; + this.debugText.innerHTML = ""; + this.debugText.style.top = 50 + "px"; + this.debugText.style.visibility = "visible"; + this.debugText.style.color = "#fff"; + document.body.appendChild(this.debugText); + } + + setDebugInfos(infos: DebugInfos) { + let text = "Version " + this.rapierVersion; + text += "
[Step " + infos.stepId + "]"; + + if (infos.worldHash) { + text += + "
World hash (xxHash128): " + infos.worldHash.toString(); + text += + "
World hash time (xxHash128): " + + infos.worldHashTime + + "ms"; + text += "
Snapshot time: " + infos.snapshotTime + "ms"; + } + this.debugText.innerHTML = text; + } + + setTiming(timing: number) { + if (!!timing) { + this.maxTimePanelValue = Math.max(this.maxTimePanelValue, timing); + this.stepTimePanel.update(timing, this.maxTimePanelValue); + } + } + + resetTiming() { + this.maxTimePanelValue = 1.0; + this.stepTimePanel.update(0.0, 16.0); + } +} diff --git a/thirdparty/rapier.js/testbed2d/src/Testbed.ts b/thirdparty/rapier.js/testbed2d/src/Testbed.ts new file mode 100644 index 00000000..61279ab2 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/Testbed.ts @@ -0,0 +1,189 @@ +import {Graphics} from "./Graphics"; +import {Gui} from "./Gui"; +import type {DebugInfos} from "./Gui"; +import {xxhash128} from "hash-wasm"; +import type * as RAPIER from "@dimforge/rapier2d"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +type Builders = Map void>; + +class SimulationParameters { + backend: string; + prevBackend: string; + demo: string; + numSolverIters: number; + running: boolean; + stepping: boolean; + debugRender: boolean; + step: () => void; + restart: () => void; + takeSnapshot: () => void; + restoreSnapshot: () => void; + backends: Array; + builders: Builders; + debugInfos: boolean; + + constructor(backends: Array, builders: Builders) { + this.backend = "rapier"; + this.prevBackend = "rapier"; + this.demo = "collision groups"; + this.numSolverIters = 4; + this.running = true; + this.stepping = false; + this.debugRender = false; + this.step = function () {}; + this.restart = function () {}; + this.takeSnapshot = function () {}; + this.restoreSnapshot = function () {}; + this.backends = backends; + this.builders = builders; + this.debugInfos = false; + } +} + +export class Testbed { + RAPIER: RAPIER_API; + gui: Gui; + graphics: Graphics; + inhibitLookAt: boolean; + parameters: SimulationParameters; + demoToken: number; + mouse: {x: number; y: number}; + events: RAPIER.EventQueue; + world: RAPIER.World; + preTimestepAction?: (gfx: Graphics) => void; + stepId: number; + prevDemo: string; + lastMessageTime: number; + snap: Uint8Array; + snapStepId: number; + + constructor(RAPIER: RAPIER_API, builders: Builders) { + let backends = ["rapier"]; + this.RAPIER = RAPIER; + let parameters = new SimulationParameters(backends, builders); + this.gui = new Gui(this, parameters); + this.graphics = new Graphics(); + this.inhibitLookAt = false; + this.parameters = parameters; + this.demoToken = 0; + this.mouse = {x: 0, y: 0}; + this.events = new RAPIER.EventQueue(true); + this.switchToDemo(builders.keys().next().value); + + window.addEventListener("mousemove", (event) => { + this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + this.mouse.y = 1 - (event.clientY / window.innerHeight) * 2; + }); + } + + setpreTimestepAction(action: (gfx: Graphics) => void) { + this.preTimestepAction = action; + } + + setWorld(world: RAPIER.World) { + document.onkeyup = null; + document.onkeydown = null; + this.preTimestepAction = null; + this.world = world; + this.world.numSolverIterations = this.parameters.numSolverIters; + this.demoToken += 1; + this.stepId = 0; + this.gui.resetTiming(); + + world.forEachCollider((coll) => { + this.graphics.addCollider(this.RAPIER, world, coll); + }); + + this.lastMessageTime = new Date().getTime(); + } + + lookAt(pos: Parameters[0]) { + if (!this.inhibitLookAt) { + this.graphics.lookAt(pos); + } + + this.inhibitLookAt = false; + } + + switchToDemo(demo: string) { + if (demo == this.prevDemo) { + this.inhibitLookAt = true; + } + + this.prevDemo = demo; + this.graphics.reset(); + this.stepId = 0; + + this.parameters.prevBackend = this.parameters.backend; + this.parameters.builders.get(demo)(this.RAPIER, this); + } + + switchToBackend(backend: string) { + this.switchToDemo(this.parameters.demo); + } + + takeSnapshot() { + this.snap = this.world.takeSnapshot(); + this.snapStepId = this.stepId; + } + + restoreSnapshot() { + if (!!this.snap) { + this.world.free(); + this.world = this.RAPIER.World.restoreSnapshot(this.snap); + this.stepId = this.snapStepId; + } + } + + run() { + if (this.parameters.running || this.parameters.stepping) { + this.world.numSolverIterations = this.parameters.numSolverIters; + + if (!!this.preTimestepAction) { + this.preTimestepAction(this.graphics); + } + + let t0 = new Date().getTime(); + this.world.step(this.events); + this.gui.setTiming(new Date().getTime() - t0); + this.stepId += 1; + + if (!!this.parameters.debugInfos) { + let t0 = performance.now(); + let snapshot = this.world.takeSnapshot(); + let t1 = performance.now(); + let snapshotTime = t1 - t0; + + let debugInfos: DebugInfos = { + token: this.demoToken, + stepId: this.stepId, + worldHash: "", + worldHashTime: 0, + snapshotTime: 0, + }; + t0 = performance.now(); + xxhash128(snapshot).then((hash) => { + debugInfos.worldHash = hash; + t1 = performance.now(); + let worldHashTime = t1 - t0; + debugInfos.worldHashTime = worldHashTime; + debugInfos.snapshotTime = snapshotTime; + this.gui.setDebugInfos(debugInfos); + }); + } + } + + if (this.parameters.stepping) { + this.parameters.running = false; + this.parameters.stepping = false; + } + + this.gui.stats.begin(); + this.graphics.render(this.world, this.parameters.debugRender); + this.gui.stats.end(); + + requestAnimationFrame(() => this.run()); + } +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/characterController.ts b/thirdparty/rapier.js/testbed2d/src/demos/characterController.ts new file mode 100644 index 00000000..7877db12 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/characterController.ts @@ -0,0 +1,89 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.1); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let rad = 0.5; + let num = 5; + let i, j, k; + let shift = rad * 2.5; + let center = num * rad; + let height = 5.0; + + for (i = 0; i < num; ++i) { + for (k = i; k < num; ++k) { + let x = (i * shift) / 2.0 + (k - i) * shift - center; + let y = (i * shift) / 2.0 + height; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad / 2.0); + world.createCollider(colliderDesc, body); + } + } + + // Character. + let characterDesc = + RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation( + -10.0, + 4.0, + ); + let character = world.createRigidBody(characterDesc); + let characterColliderDesc = RAPIER.ColliderDesc.cuboid(0.6, 1.2); + let characterCollider = world.createCollider( + characterColliderDesc, + character, + ); + + let characterController = world.createCharacterController(0.1); + characterController.enableAutostep(0.7, 0.3, true); + characterController.enableSnapToGround(0.7); + + let speed = 0.2; + let movementDirection = {x: 0.0, y: -speed}; + + let updateCharacter = () => { + characterController.computeColliderMovement( + characterCollider, + movementDirection, + ); + + let movement = characterController.computedMovement(); + let newPos = character.translation(); + newPos.x += movement.x; + newPos.y += movement.y; + character.setNextKinematicTranslation(newPos); + console.log("here"); + }; + + testbed.setWorld(world); + testbed.setpreTimestepAction(updateCharacter); + + document.onkeydown = function (event: KeyboardEvent) { + if (event.key == "ArrowLeft") movementDirection.x = -speed; + if (event.key == "ArrowRight") movementDirection.x = speed; + if (event.key == " ") movementDirection.y = speed; + }; + + document.onkeyup = function (event: KeyboardEvent) { + if (event.key == "ArrowLeft") movementDirection.x = 0.0; + if (event.key == "ArrowRight") movementDirection.x = 0.0; + if (event.key == " ") movementDirection.y = -speed; // Gravity + }; + + testbed.lookAt({ + target: {x: 0.0, y: -1.0}, + zoom: 50.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/collisionGroups.ts b/thirdparty/rapier.js/testbed2d/src/demos/collisionGroups.ts new file mode 100644 index 00000000..51163618 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/collisionGroups.ts @@ -0,0 +1,79 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + /* + * Ground + */ + let ground_size = 5.0; + let ground_height = 0.1; + + let groundBodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + 0.0, + -ground_height, + ); + let groundBody = world.createRigidBody(groundBodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(ground_size, ground_height); + world.createCollider(colliderDesc, groundBody); + + /* + * Setup groups + */ + let group1 = 0x00010001; + let group2 = 0x00020002; + + /* + * A green floor that will collide with the first group only. + */ + colliderDesc = RAPIER.ColliderDesc.cuboid(1.0, 0.1) + .setTranslation(0.0, 1.0) + .setCollisionGroups(group1); + world.createCollider(colliderDesc, groundBody); + + /* + * A blue floor that will collide with the second group only. + */ + colliderDesc = RAPIER.ColliderDesc.cuboid(1.0, 0.1) + .setTranslation(0.0, 2.0) + .setCollisionGroups(group2); + world.createCollider(colliderDesc, groundBody); + + /* + * Create the cubes + */ + let num = 8; + let rad = 0.1; + + let shift = rad * 2.0; + let centerx = shift * (num / 2); + let centery = 2.5; + let i, j; + + for (j = 0; j < 4; ++j) { + for (i = 0; i < num; ++i) { + let x = i * shift - centerx; + let y = j * shift + centery; + + // Alternate between the green and blue groups. + let group = i % 2 == 0 ? group1 : group2; + + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + ).setCollisionGroups(group); + world.createCollider(colliderDesc, body); + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: 0.0, y: -1.0}, + zoom: 100.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/convexPolygons.ts b/thirdparty/rapier.js/testbed2d/src/demos/convexPolygons.ts new file mode 100644 index 00000000..6edad202 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/convexPolygons.ts @@ -0,0 +1,68 @@ +import seedrandom from "seedrandom"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + /* + * Ground + */ + // Create Ground. + let groundSize = 30.0; + let grounds = [ + {x: 0.0, y: 0.0, hx: groundSize, hy: 1.2}, + {x: -groundSize, y: groundSize, hx: 1.2, hy: groundSize}, + {x: groundSize, y: groundSize, hx: 1.2, hy: groundSize}, + ]; + + grounds.forEach((ground) => { + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + ground.x, + ground.y, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(ground.hx, ground.hy); + world.createCollider(colliderDesc, body); + }); + + /* + * Create the convex polygons + */ + let num = 14; + let scale = 4.0; + + let shift = scale; + let centerx = (shift * num) / 2.0; + let centery = shift / 2.0; + + let i, j, k; + let rng = seedrandom("convexPolygon"); + + for (i = 0; i < num; ++i) { + for (j = 0; j < num * 2; ++j) { + let x = i * shift - centerx; + let y = j * shift * 2.0 + centery + 2.0; + + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + + let points = []; + for (k = 0; k < 10; ++k) { + points.push(rng() * scale, rng() * scale); + } + let colliderDesc = RAPIER.ColliderDesc.convexHull( + new Float32Array(points), + ); + world.createCollider(colliderDesc, body); + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: -10.0, y: -30.0}, + zoom: 7.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/cubes.ts b/thirdparty/rapier.js/testbed2d/src/demos/cubes.ts new file mode 100644 index 00000000..5214b485 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/cubes.ts @@ -0,0 +1,56 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + // Create Ground. + let groundSize = 40.0; + let grounds = [ + {x: 0.0, y: 0.0, hx: groundSize, hy: 0.1}, + {x: -groundSize, y: groundSize, hx: 0.1, hy: groundSize}, + {x: groundSize, y: groundSize, hx: 0.1, hy: groundSize}, + ]; + + grounds.forEach((ground) => { + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + ground.x, + ground.y, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(ground.hx, ground.hy); + world.createCollider(colliderDesc, body); + }); + + // Dynamic cubes. + let num = 20; + let numy = 50; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centerx = shift * (num / 2); + let centery = shift / 2.0; + + let i, j; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + let x = i * shift - centerx; + let y = j * shift + centery + 3.0; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad); + world.createCollider(colliderDesc, body); + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: -10.0, y: -30.0}, + zoom: 7.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/heightfield.ts b/thirdparty/rapier.js/testbed2d/src/demos/heightfield.ts new file mode 100644 index 00000000..d055499b --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/heightfield.ts @@ -0,0 +1,65 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + let i, j; + + /* + * Ground + */ + let ground_size = {x: 50.0, y: 1.0}; + let nsubdivs = 100; + let heights = []; + + heights.push(40.0); + for (i = 1; i < nsubdivs; ++i) { + heights.push(Math.cos((i * ground_size.x) / nsubdivs) * 2.0); + } + heights.push(40.0); + + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.heightfield( + new Float32Array(heights), + ground_size, + ); + world.createCollider(colliderDesc, body); + + /* + * Create the cubes + */ + let num = 15; + let rad = 0.5; + + let shift = rad * 2.0; + let centerx = shift * (num / 2); + let centery = shift / 2.0; + + for (i = 0; i < num; ++i) { + for (j = 0; j < num * 5; ++j) { + let x = i * shift - centerx; + let y = j * shift + centery + 3.0; + + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + + if (j % 2 == 0) { + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad); + world.createCollider(colliderDesc, body); + } else { + let colliderDesc = RAPIER.ColliderDesc.ball(rad); + world.createCollider(colliderDesc, body); + } + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: -10.0, y: -15.0}, + zoom: 10.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/keva.ts b/thirdparty/rapier.js/testbed2d/src/demos/keva.ts new file mode 100644 index 00000000..6ca57c0c --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/keva.ts @@ -0,0 +1,92 @@ +import type * as RAPIER from "@dimforge/rapier2d"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +function buildBlock( + RAPIER: RAPIER_API, + world: RAPIER.World, + halfExtents: RAPIER.Vector, + shift: RAPIER.Vector, + numx: number, + numy: number, + numz: number, +) { + let halfExtents_yx = {x: halfExtents.y, y: halfExtents.x}; + let dimensions = [halfExtents, halfExtents_yx]; + let spacing = (halfExtents.y * numx - halfExtents.x) / (numz - 1.0); + + let i; + let j; + let y = 0.0; + + for (i = 0; i <= numy; ++i) { + let dim = dimensions[i % 2]; + [numx, numz] = [numz, numx]; + + for (j = 0; j < numx; ++j) { + let x = i % 2 == 0 ? spacing * j * 2.0 : dim.x * j * 2.0; + + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x + dim.x + shift.x, + y + dim.y + shift.y, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(dim.x, dim.y); + world.createCollider(colliderDesc, body); + } + + y += dim.y * 2.0; + } +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + // Create Ground. + let groundSize = 150.0; + let groundHeight = 0.1; + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + 0.0, + -groundHeight, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(groundSize, groundHeight); + world.createCollider(colliderDesc, body); + + // Keva tower. + let halfExtents = new RAPIER.Vector2(0.5, 2.0); + let blockHeight = 0.0; + // These should only be set to odd values otherwise + // the blocks won't align in the nicest way. + let numyArr = [0, 3, 5, 5, 7, 9]; + let numBlocksBuilt = 0; + let i; + + for (i = 5; i >= 1; --i) { + let numx = i * 15; + let numy = numyArr[i]; + let numz = numx * 2 + 1; + let blockWidth = numx * halfExtents.y * 2.0; + buildBlock( + RAPIER, + world, + halfExtents, + new RAPIER.Vector2(-blockWidth / 2.0, blockHeight), + numx, + numy, + numz, + ); + blockHeight += (numy + 1) * (halfExtents.x + halfExtents.y); + numBlocksBuilt += numx * numy; + } + + testbed.setWorld(world); + + testbed.lookAt({ + target: {x: -10.0, y: -5.0}, + zoom: 4.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/lockedRotations.ts b/thirdparty/rapier.js/testbed2d/src/demos/lockedRotations.ts new file mode 100644 index 00000000..8600f95f --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/lockedRotations.ts @@ -0,0 +1,51 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + /* + * The ground + */ + let ground_size = 1.8; + let ground_height = 1.0; + + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + 0.0, + -ground_height, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(ground_size, ground_height); + world.createCollider(colliderDesc, body); + + /* + * A rectangle that only rotates along the `x` axis. + */ + bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(0.0, 3.0) + .lockTranslations(); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.cuboid(2.0, 0.6); + world.createCollider(colliderDesc, body); + + /* + * A cuboid that cannot rotate. + */ + bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(0.4, 5.0) + .lockRotations(); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.cuboid(0.4, 0.6); + world.createCollider(colliderDesc, body); + + /* + * Setup the testbed. + */ + testbed.setWorld(world); + testbed.lookAt({ + target: {x: 0.0, y: -2.0}, + zoom: 50.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/pidController.ts b/thirdparty/rapier.js/testbed2d/src/demos/pidController.ts new file mode 100644 index 00000000..fac2a7c4 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/pidController.ts @@ -0,0 +1,100 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.1); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let rad = 0.5; + let num = 5; + let i, j, k; + let shift = rad * 2.5; + let center = num * rad; + let height = 5.0; + + for (i = 0; i < num; ++i) { + for (k = i; k < num; ++k) { + let x = (i * shift) / 2.0 + (k - i) * shift - center; + let y = (i * shift) / 2.0 + height; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad / 2.0); + world.createCollider(colliderDesc, body); + } + } + + // Character. + let characterDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(-10.0, 4.0) + .setGravityScale(10.0) + .setSoftCcdPrediction(10.0); + let character = world.createRigidBody(characterDesc); + let characterColliderDesc = RAPIER.ColliderDesc.cuboid(0.6, 1.2); + world.createCollider(characterColliderDesc, character); + + let pidController = world.createPidController( + 60.0, + 0.0, + 1.0, + RAPIER.PidAxesMask.AllAng, + ); + let speed = 0.2; + let movementDirection = {x: 0.0, y: 0.0}; + let targetVelocity = {x: 0.0, y: 0.0}; + let targetRotation = 0.0; + + let updateCharacter = () => { + if (movementDirection.x == 0.0 && movementDirection.y == 0.0) { + // Only adjust the rotation, but let translation. + pidController.setAxes(RAPIER.PidAxesMask.AllAng); + } else if (movementDirection.y == 0.0) { + // Don’t control the linear Y axis so the player can fall down due to gravity. + pidController.setAxes( + RAPIER.PidAxesMask.AllAng | RAPIER.PidAxesMask.LinX, + ); + } else { + pidController.setAxes(RAPIER.PidAxesMask.All); + } + + let targetPoint = character.translation(); + targetPoint.x += movementDirection.x; + targetPoint.y += movementDirection.y; + + pidController.applyLinearCorrection( + character, + targetPoint, + targetVelocity, + ); + pidController.applyAngularCorrection(character, 0.0, 0.0); + }; + + testbed.setWorld(world); + testbed.setpreTimestepAction(updateCharacter); + + document.onkeydown = function (event: KeyboardEvent) { + if (event.key == "ArrowLeft") movementDirection.x = -speed; + if (event.key == "ArrowRight") movementDirection.x = speed; + if (event.key == " ") movementDirection.y = speed; + }; + + document.onkeyup = function (event: KeyboardEvent) { + if (event.key == "ArrowLeft") movementDirection.x = 0.0; + if (event.key == "ArrowRight") movementDirection.x = 0.0; + if (event.key == " ") movementDirection.y = 0.0; + }; + + testbed.lookAt({ + target: {x: 0.0, y: -1.0}, + zoom: 50.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/polyline.ts b/thirdparty/rapier.js/testbed2d/src/demos/polyline.ts new file mode 100644 index 00000000..47d82540 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/polyline.ts @@ -0,0 +1,64 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + let i, j; + + /* + * Ground + */ + let nsubdivs = 100; + let points = []; + let groundSize = 30.0; + let stepSize = groundSize / nsubdivs; + + points.push(-groundSize / 2.0, 25.0); + for (i = 1; i < nsubdivs; ++i) { + let x = -groundSize / 2.0 + i * stepSize; + let y = Math.cos(i * stepSize) * 2.0; + points.push(x, y); + } + points.push(groundSize / 2.0, 25.0); + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.polyline(new Float32Array(points)); + world.createCollider(colliderDesc, body); + + /* + * Create the cubes + */ + let num = 10; + let rad = 0.5; + + let shift = rad * 2.0; + let centerx = shift * (num / 2); + let centery = shift / 2.0; + + for (i = 0; i < num; ++i) { + for (j = 0; j < num * 5; ++j) { + let x = i * shift - centerx; + let y = j * shift + centery + 3.0; + + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + + if (j % 2 == 0) { + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad); + world.createCollider(colliderDesc, body); + } else { + let colliderDesc = RAPIER.ColliderDesc.ball(rad); + world.createCollider(colliderDesc, body); + } + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: -10.0, y: -15.0}, + zoom: 10.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/revoluteJoints.ts b/thirdparty/rapier.js/testbed2d/src/demos/revoluteJoints.ts new file mode 100644 index 00000000..2d7923fd --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/revoluteJoints.ts @@ -0,0 +1,59 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + let bodies = []; + + let rad = 0.4; + let numi = 30; // Num vertical nodes. + let numk = 30; // Num horizontal nodes. + let shift = 1.0; + let i, k; + + for (k = 0; k < numk; ++k) { + for (i = 0; i < numi; ++i) { + let status = + k >= numk / 2 - 3 && k <= numk / 2 + 3 && i == 0 + ? RAPIER.RigidBodyType.Fixed + : RAPIER.RigidBodyType.Dynamic; + + let bodyDesc = new RAPIER.RigidBodyDesc(status).setTranslation( + k * shift, + -i * shift, + ); + let child = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.ball(rad); + world.createCollider(colliderDesc, child); + + // Vertical joint. + if (i > 0) { + let parent = bodies[bodies.length - 1]; + let anchor1 = new RAPIER.Vector2(0.0, 0.0); + let anchor2 = new RAPIER.Vector2(0.0, shift); + let JointData = RAPIER.JointData.revolute(anchor1, anchor2); + world.createImpulseJoint(JointData, parent, child, true); + } + + // Horizontal joint. + if (k > 0) { + let parentIndex = bodies.length - numi; + let parent = bodies[parentIndex]; + let anchor1 = new RAPIER.Vector2(0.0, 0.0); + let anchor2 = new RAPIER.Vector2(-shift, 0.0); + let JointData = RAPIER.JointData.revolute(anchor1, anchor2); + world.createImpulseJoint(JointData, parent, child, true); + } + + bodies.push(child); + } + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: 30.0, y: 30.0}, + zoom: 10.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/demos/voxels.ts b/thirdparty/rapier.js/testbed2d/src/demos/voxels.ts new file mode 100644 index 00000000..6a6fc366 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/demos/voxels.ts @@ -0,0 +1,90 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier2d"); + +function generateVoxels(n: number) { + let points = []; + + let i; + for (i = 0; i <= n; ++i) { + let y = Math.max(-0.8, Math.min(Math.sin((i / n) * 10.0), 0.8)) * 8.0; + points.push(i - n / 2.0, y); + } + return { + points: new Float32Array(points), + voxelSize: {x: 1.0, y: 1.2}, + }; +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector2(0.0, -9.81); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let voxels = generateVoxels(100); + let colliderDesc = RAPIER.ColliderDesc.voxels( + voxels.points, + voxels.voxelSize, + ); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let num = 10; + let numy = 4; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centery = shift / 2.0; + + let offset = -num * (rad * 2.0 + rad) * 0.5; + let i, j; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + let x = i * shift + offset; + let y = j * shift + centery + 10.0; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y); + let body = world.createRigidBody(bodyDesc); + let colliderDesc; + + switch (j % 3) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad / 2.0, + ); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + ).setTranslation(rad, 0.0); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + ).setTranslation(-rad, 0.0); + break; + } + + world.createCollider(colliderDesc, body); + } + + offset -= 0.05 * rad * (num - 1.0); + } + + testbed.setWorld(world); + testbed.lookAt({ + target: {x: 0.0, y: 0.0}, + zoom: 20.0, + }); +} diff --git a/thirdparty/rapier.js/testbed2d/src/index.ts b/thirdparty/rapier.js/testbed2d/src/index.ts new file mode 100644 index 00000000..24bd18f4 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/src/index.ts @@ -0,0 +1,30 @@ +import {Testbed} from "./Testbed"; +import * as CollisionGroups from "./demos/collisionGroups"; +import * as Cubes from "./demos/cubes"; +import * as Keva from "./demos/keva"; +import * as Heightfield from "./demos/heightfield"; +import * as Polyline from "./demos/polyline"; +import * as RevoluteJoints from "./demos/revoluteJoints"; +import * as LockedRotations from "./demos/lockedRotations"; +import * as ConvexPolygons from "./demos/convexPolygons"; +import * as CharacterController from "./demos/characterController"; +import * as PidController from "./demos/pidController"; +import * as Voxels from "./demos/voxels"; + +import("@dimforge/rapier2d").then((RAPIER) => { + let builders = new Map([ + ["collision groups", CollisionGroups.initWorld], + ["character controller", CharacterController.initWorld], + ["convex polygons", ConvexPolygons.initWorld], + ["cubes", Cubes.initWorld], + ["heightfield", Heightfield.initWorld], + ["joints: revolute", RevoluteJoints.initWorld], + ["keva tower", Keva.initWorld], + ["locked rotations", LockedRotations.initWorld], + ["pid controller", PidController.initWorld], + ["polyline", Polyline.initWorld], + ["voxels", Voxels.initWorld], + ]); + let testbed = new Testbed(RAPIER, builders); + testbed.run(); +}); diff --git a/thirdparty/rapier.js/testbed2d/static/.htaccess b/thirdparty/rapier.js/testbed2d/static/.htaccess new file mode 100644 index 00000000..9bdb356b --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/static/.htaccess @@ -0,0 +1,3 @@ +RewriteEngine On +RewriteCond %{SERVER_PORT} 80 +RewriteRule ^(.*)$ https://www.rapier.rs/$1 [R,L] diff --git a/thirdparty/rapier.js/testbed2d/static/index.html b/thirdparty/rapier.js/testbed2d/static/index.html new file mode 100644 index 00000000..6df5b069 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/static/index.html @@ -0,0 +1,18 @@ + + + + + Rapier2D JS bindings demo + + + + + + diff --git a/thirdparty/rapier.js/testbed2d/tsconfig.json b/thirdparty/rapier.js/testbed2d/tsconfig.json new file mode 100644 index 00000000..5d07ecd8 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es2022", + "target": "es2022", + "allowJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/thirdparty/rapier.js/testbed2d/webpack.config.js b/thirdparty/rapier.js/testbed2d/webpack.config.js new file mode 100644 index 00000000..0e5c9839 --- /dev/null +++ b/thirdparty/rapier.js/testbed2d/webpack.config.js @@ -0,0 +1,55 @@ +const webpack = require("webpack"); +const path = require("path"); +const CopyPlugin = require("copy-webpack-plugin"); + +const isDev = process.env.NODE_ENV === "development"; +const dist = path.resolve(__dirname, "dist"); + +/** + * @type {import('webpack-dev-server').Configuration} + */ +const devServer = { + static: { + directory: dist, + }, + allowedHosts: "all", + compress: true, +}; + +/** + * @type {import('webpack').Configuration} + */ +const webpackConfig = { + mode: isDev ? "development" : "production", + entry: "./src/index.ts", + devtool: "inline-source-map", + output: { + path: dist, + filename: "index.js", + }, + resolve: { + extensions: [".ts", ".js"], + }, + devServer, + performance: false, + experiments: { + asyncWebAssembly: true, + syncWebAssembly: true, + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [{from: "static", to: dist}], + }), + ], +}; + +module.exports = webpackConfig; diff --git a/thirdparty/rapier.js/testbed3d/.npmignore b/thirdparty/rapier.js/testbed3d/.npmignore new file mode 100644 index 00000000..488f9125 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/.npmignore @@ -0,0 +1,3 @@ +webpack.config2d.js +src +*.tgz \ No newline at end of file diff --git a/thirdparty/rapier.js/testbed3d/package.json b/thirdparty/rapier.js/testbed3d/package.json new file mode 100644 index 00000000..516ee275 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/package.json @@ -0,0 +1,32 @@ +{ + "author": "sebcrozet ", + "name": "rapier-testbed3d", + "version": "0.1.0", + "description": "JavaScript testbed for rapier.", + "license": "Apache-2.0", + "main": "dist/rapier-testbed3d.js", + "scripts": { + "build": "rimraf dist pkg && webpack", + "start": "rimraf dist pkg && webpack serve --open --node-env development" + }, + "dependencies": { + "@dimforge/rapier3d": "file:../builds/rapier3d", + "hash-wasm": "^4.12.0", + "lil-gui": "^0.17.0", + "seedrandom": "^3.0.5", + "stats.js": "^0.17.0", + "three": "^0.146.0" + }, + "devDependencies": { + "@types/seedrandom": "^3.0.2", + "@types/stats.js": "^0.17.0", + "@types/three": "^0.144.0", + "copy-webpack-plugin": "^11.0.0", + "rimraf": "^3.0.2", + "ts-loader": "^9.4.1", + "typescript": "^4.8.4", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.1" + } +} diff --git a/thirdparty/rapier.js/testbed3d/publish.sh b/thirdparty/rapier.js/testbed3d/publish.sh new file mode 100644 index 00000000..e8bc248f --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/publish.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +npm run build +rsync -av --delete-after dist/ crozet@ssh.cluster003.hosting.ovh.net:/home/crozet/rapier/demos3d +# rsync -av dist/ammo.wasm.wasm crozet@ssh.cluster003.hosting.ovh.net:/home/crozet/rapier/ammo.wasm.wasm +# rsync -av dist/physx.release.wasm crozet@ssh.cluster003.hosting.ovh.net:/home/crozet/rapier/physx.release.wasm diff --git a/thirdparty/rapier.js/testbed3d/src/Graphics.ts b/thirdparty/rapier.js/testbed3d/src/Graphics.ts new file mode 100644 index 00000000..f52d6c96 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/Graphics.ts @@ -0,0 +1,576 @@ +import * as THREE from "three"; +import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; +import RAPIER from "@dimforge/rapier3d"; + +const BOX_INSTANCE_INDEX = 0; +const BALL_INSTANCE_INDEX = 1; +const CYLINDER_INSTANCE_INDEX = 2; +const CONE_INSTANCE_INDEX = 3; + +var dummy = new THREE.Object3D(); +var kk = 0; + +interface InstanceDesc { + groupId: number; + instanceId: number; + elementId: number; + highlighted: boolean; + scale?: THREE.Vector3; +} + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +// NOTE: this is a very naive voxels -> mesh conversion. Proper +// conversions should use something like greedy meshing instead. +function genVoxelsGeometry(collider: RAPIER.Collider) { + // Clear the cached shape so it gets recomputed from the source of truth, + // and so we’ll be sure that the data contain grid coordinates even if the + // voxels were initialized with floating points. + collider.clearShapeCache(); + let shape = collider.shape as RAPIER.Voxels; + let gridCoords = shape.data; + let sz = shape.voxelSize; + let vertices = []; + let indices = []; + + let i: number; + for (i = 0; i < gridCoords.length; i += 3) { + let minx = gridCoords[i] * sz.x; + let miny = gridCoords[i + 1] * sz.y; + let minz = gridCoords[i + 2] * sz.z; + let maxx = minx + sz.x; + let maxy = miny + sz.y; + let maxz = minz + sz.z; + + let k: number = vertices.length / 3; + vertices.push(minx, miny, maxz); + vertices.push(minx, miny, minz); + vertices.push(maxx, miny, minz); + vertices.push(maxx, miny, maxz); + vertices.push(minx, maxy, maxz); + vertices.push(minx, maxy, minz); + vertices.push(maxx, maxy, minz); + vertices.push(maxx, maxy, maxz); + + indices.push(k + 4, k + 5, k + 0); + indices.push(k + 5, k + 1, k + 0); + indices.push(k + 5, k + 6, k + 1); + indices.push(k + 6, k + 2, k + 1); + indices.push(k + 6, k + 7, k + 3); + indices.push(k + 2, k + 6, k + 3); + indices.push(k + 7, k + 4, k + 0); + indices.push(k + 3, k + 7, k + 0); + indices.push(k + 0, k + 1, k + 2); + indices.push(k + 3, k + 0, k + 2); + indices.push(k + 7, k + 6, k + 5); + indices.push(k + 4, k + 7, k + 5); + } + + return { + vertices: new Float32Array(vertices), + indices: new Uint32Array(indices), + }; +} + +function genHeightfieldGeometry(collider: RAPIER.Collider) { + let heights = collider.heightfieldHeights(); + let nrows = collider.heightfieldNRows(); + let ncols = collider.heightfieldNCols(); + let scale = collider.heightfieldScale(); + + let vertices = []; + let indices = []; + let eltWX = 1.0 / nrows; + let eltWY = 1.0 / ncols; + + let i: number; + let j: number; + for (j = 0; j <= ncols; ++j) { + for (i = 0; i <= nrows; ++i) { + let x = (j * eltWX - 0.5) * scale.x; + let y = heights[j * (nrows + 1) + i] * scale.y; + let z = (i * eltWY - 0.5) * scale.z; + + vertices.push(x, y, z); + } + } + + for (j = 0; j < ncols; ++j) { + for (i = 0; i < nrows; ++i) { + let i1 = (i + 0) * (ncols + 1) + (j + 0); + let i2 = (i + 0) * (ncols + 1) + (j + 1); + let i3 = (i + 1) * (ncols + 1) + (j + 0); + let i4 = (i + 1) * (ncols + 1) + (j + 1); + + indices.push(i1, i3, i2); + indices.push(i3, i4, i2); + } + } + + return { + vertices: new Float32Array(vertices), + indices: new Uint32Array(indices), + }; +} + +export class Graphics { + raycaster: THREE.Raycaster; + highlightedCollider: null | number; + coll2instance: Map; + coll2mesh: Map; + rb2colls: Map>; + colorIndex: number; + colorPalette: Array; + scene: THREE.Scene; + camera: THREE.PerspectiveCamera; + renderer: THREE.WebGLRenderer; + light: THREE.PointLight; + lines: THREE.LineSegments; + controls: OrbitControls; + instanceGroups: Array>; + + constructor() { + this.raycaster = new THREE.Raycaster(); + this.highlightedCollider = null; + this.coll2instance = new Map(); + this.coll2mesh = new Map(); + this.rb2colls = new Map(); + this.colorIndex = 0; + this.colorPalette = [0xf3d9b1, 0x98c1d9, 0x053c5e, 0x1f7a8c, 0xff0000]; + this.scene = new THREE.Scene(); + this.camera = new THREE.PerspectiveCamera( + 45, + window.innerWidth / window.innerHeight, + 0.1, + 10000, + ); + this.renderer = new THREE.WebGLRenderer({antialias: true}); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setClearColor(0x292929, 1); + // High pixel Ratio make the rendering extremely slow, so we cap it. + const pixelRatio = window.devicePixelRatio + ? Math.min(window.devicePixelRatio, 1.5) + : 1; + this.renderer.setPixelRatio(pixelRatio); + document.body.appendChild(this.renderer.domElement); + + let ambientLight = new THREE.AmbientLight(0x606060); + this.scene.add(ambientLight); + this.light = new THREE.PointLight(0xffffff, 1, 1000); + this.scene.add(this.light); + + // For the debug-renderer. + { + let material = new THREE.LineBasicMaterial({ + color: 0xffffff, + vertexColors: true, + }); + let geometry = new THREE.BufferGeometry(); + this.lines = new THREE.LineSegments(geometry, material); + this.scene.add(this.lines); + } + let me = this; + + function onWindowResize() { + if (!!me.camera) { + me.camera.aspect = window.innerWidth / window.innerHeight; + me.camera.updateProjectionMatrix(); + me.renderer.setSize(window.innerWidth, window.innerHeight); + } + } + + window.addEventListener("resize", onWindowResize, false); + + this.controls = new OrbitControls( + this.camera, + this.renderer.domElement, + ); + this.controls.enableDamping = true; + this.controls.dampingFactor = 0.2; + this.controls.maxPolarAngle = Math.PI / 2; + this.initInstances(); + } + + initInstances() { + this.instanceGroups = []; + this.instanceGroups.push( + this.colorPalette.map((color) => { + let box = new THREE.BoxGeometry(2.0, 2.0, 2.0); + let mat = new THREE.MeshPhongMaterial({ + color: color, + flatShading: true, + }); + return new THREE.InstancedMesh(box, mat, 1000); + }), + ); + + this.instanceGroups.push( + this.colorPalette.map((color) => { + let ball = new THREE.SphereGeometry(1.0); + let mat = new THREE.MeshPhongMaterial({ + color: color, + flatShading: true, + }); + return new THREE.InstancedMesh(ball, mat, 1000); + }), + ); + + this.instanceGroups.push( + this.colorPalette.map((color) => { + let cylinder = new THREE.CylinderGeometry(1.0, 1.0); + let mat = new THREE.MeshPhongMaterial({ + color: color, + flatShading: true, + }); + return new THREE.InstancedMesh(cylinder, mat, 100); + }), + ); + + this.instanceGroups.push( + this.colorPalette.map((color) => { + let cone = new THREE.ConeGeometry(1.0, 1.0); + let mat = new THREE.MeshPhongMaterial({ + color: color, + flatShading: true, + }); + return new THREE.InstancedMesh(cone, mat, 100); + }), + ); + + this.instanceGroups.forEach((groups) => { + groups.forEach((instance) => { + instance.userData.elementId2coll = new Map(); + instance.count = 0; + instance.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + this.scene.add(instance); + }); + }); + } + + render(world: RAPIER.World, debugRender: boolean) { + kk += 1; + this.controls.update(); + // if (kk % 100 == 0) { + // console.log(this.camera.position); + // console.log(this.controls.target); + // } + + this.light.position.set( + this.camera.position.x, + this.camera.position.y, + this.camera.position.z, + ); + + if (debugRender) { + let buffers = world.debugRender(); + this.lines.visible = true; + this.lines.geometry.setAttribute( + "position", + new THREE.BufferAttribute(buffers.vertices, 3), + ); + this.lines.geometry.setAttribute( + "color", + new THREE.BufferAttribute(buffers.colors, 4), + ); + } else { + this.lines.visible = false; + } + + this.updatePositions(world); + this.renderer.render(this.scene, this.camera); + } + + rayAtMousePosition(pos: {x: number; y: number}) { + this.raycaster.setFromCamera(pos, this.camera); + return this.raycaster.ray; + } + + lookAt(pos: { + target: {x: number; y: number; z: number}; + eye: {x: number; y: number; z: number}; + }) { + this.camera.position.set(pos.eye.x, pos.eye.y, pos.eye.z); + this.controls.target.set(pos.target.x, pos.target.y, pos.target.z); + this.controls.update(); + } + + highlightInstanceId() { + return this.colorPalette.length - 1; + } + + highlightCollider(handle: number) { + if (handle == this.highlightedCollider) + // Avoid flickering when moving the mouse on a single collider. + return; + + if (this.highlightedCollider != null) { + let desc = this.coll2instance.get(this.highlightedCollider); + + if (!!desc) { + desc.highlighted = false; + this.instanceGroups[desc.groupId][ + this.highlightInstanceId() + ].count = 0; + } + } + if (handle != null) { + let desc = this.coll2instance.get(handle); + + if (!!desc) { + if (desc.instanceId != 0) + // Don't highlight static/kinematic bodies. + desc.highlighted = true; + } + } + this.highlightedCollider = handle; + } + + updatePositions(world: RAPIER.World) { + world.forEachCollider((elt) => { + let gfx = this.coll2instance.get(elt.handle); + let translation = elt.translation(); + let rotation = elt.rotation(); + + if (!!gfx) { + let instance = this.instanceGroups[gfx.groupId][gfx.instanceId]; + dummy.scale.set(gfx.scale.x, gfx.scale.y, gfx.scale.z); + dummy.position.set(translation.x, translation.y, translation.z); + dummy.quaternion.set( + rotation.x, + rotation.y, + rotation.z, + rotation.w, + ); + dummy.updateMatrix(); + instance.setMatrixAt(gfx.elementId, dummy.matrix); + + let highlightInstance = + this.instanceGroups[gfx.groupId][ + this.highlightInstanceId() + ]; + if (gfx.highlighted) { + highlightInstance.count = 1; + highlightInstance.setMatrixAt(0, dummy.matrix); + } + + instance.instanceMatrix.needsUpdate = true; + highlightInstance.instanceMatrix.needsUpdate = true; + } + + let mesh = this.coll2mesh.get(elt.handle); + + if (!!mesh) { + mesh.position.set(translation.x, translation.y, translation.z); + mesh.quaternion.set( + rotation.x, + rotation.y, + rotation.z, + rotation.w, + ); + mesh.updateMatrix(); + } + }); + } + + reset() { + this.instanceGroups.forEach((groups) => { + groups.forEach((instance) => { + instance.userData.elementId2coll = new Map(); + instance.count = 0; + }); + }); + + this.coll2mesh.forEach((mesh) => { + this.scene.remove(mesh); + }); + + this.coll2instance = new Map(); + this.rb2colls = new Map(); + this.colorIndex = 0; + } + + // applyModifications(RAPIER: RAPIER_API, world: RAPIER.World, modifications) { + // if (!!modifications) { + // modifications.addCollider.forEach(coll => { + // let collider = world.getCollider(coll.handle); + // this.addCollider(RAPIER, world, collider); + // }); + // modifications.removeRigidBody.forEach(body => { + // if (!!this.rb2colls.get(body.handle)) { + // this.rb2colls.get(body.handle).forEach(coll => this.removeCollider(coll)); + // this.rb2colls.delete(body.handle); + // } + // }); + // } + // } + + removeRigidBody(body: RAPIER.RigidBody) { + if (!!this.rb2colls.get(body.handle)) { + this.rb2colls + .get(body.handle) + .forEach((coll) => this.removeCollider(coll)); + this.rb2colls.delete(body.handle); + } + } + + removeCollider(collider: RAPIER.Collider) { + let gfx = this.coll2instance.get(collider.handle); + let instance = this.instanceGroups[gfx.groupId][gfx.instanceId]; + + if (instance.count > 1) { + let coll2 = instance.userData.elementId2coll.get( + instance.count - 1, + ); + instance.userData.elementId2coll.delete(instance.count - 1); + instance.userData.elementId2coll.set(gfx.elementId, coll2); + + let gfx2 = this.coll2instance.get(coll2.handle); + gfx2.elementId = gfx.elementId; + } + + instance.count -= 1; + this.coll2instance.delete(collider.handle); + } + + addCollider( + RAPIER: RAPIER_API, + world: RAPIER.World, + collider: RAPIER.Collider, + ) { + this.colorIndex = + (this.colorIndex + 1) % (this.colorPalette.length - 2); + let parent = collider.parent(); + if (!this.rb2colls.get(parent.handle)) { + this.rb2colls.set(parent.handle, [collider]); + } else { + this.rb2colls.get(parent.handle).push(collider); + } + + let instance; + let instanceDesc: InstanceDesc = { + groupId: 0, + instanceId: parent.isFixed() ? 0 : this.colorIndex + 1, + elementId: 0, + highlighted: false, + }; + + switch (collider.shapeType()) { + case RAPIER.ShapeType.Cuboid: + let hext = collider.halfExtents(); + instance = + this.instanceGroups[BOX_INSTANCE_INDEX][ + instanceDesc.instanceId + ]; + instanceDesc.groupId = BOX_INSTANCE_INDEX; + instanceDesc.scale = new THREE.Vector3(hext.x, hext.y, hext.z); + break; + case RAPIER.ShapeType.Ball: + let rad = collider.radius(); + instance = + this.instanceGroups[BALL_INSTANCE_INDEX][ + instanceDesc.instanceId + ]; + instanceDesc.groupId = BALL_INSTANCE_INDEX; + instanceDesc.scale = new THREE.Vector3(rad, rad, rad); + break; + case RAPIER.ShapeType.Cylinder: + case RAPIER.ShapeType.RoundCylinder: + let cyl_rad = collider.radius(); + let cyl_height = collider.halfHeight() * 2.0; + instance = + this.instanceGroups[CYLINDER_INSTANCE_INDEX][ + instanceDesc.instanceId + ]; + instanceDesc.groupId = CYLINDER_INSTANCE_INDEX; + instanceDesc.scale = new THREE.Vector3( + cyl_rad, + cyl_height, + cyl_rad, + ); + break; + case RAPIER.ShapeType.Cone: + let cone_rad = collider.radius(); + let cone_height = collider.halfHeight() * 2.0; + instance = + this.instanceGroups[CONE_INSTANCE_INDEX][ + instanceDesc.instanceId + ]; + instanceDesc.groupId = CONE_INSTANCE_INDEX; + instanceDesc.scale = new THREE.Vector3( + cone_rad, + cone_height, + cone_rad, + ); + break; + case RAPIER.ShapeType.TriMesh: + case RAPIER.ShapeType.HeightField: + case RAPIER.ShapeType.ConvexPolyhedron: + case RAPIER.ShapeType.RoundConvexPolyhedron: + case RAPIER.ShapeType.Voxels: + let geometry = new THREE.BufferGeometry(); + let vertices; + let indices; + + if (collider.shapeType() == RAPIER.ShapeType.HeightField) { + let g = genHeightfieldGeometry(collider); + vertices = g.vertices; + indices = g.indices; + } else if (collider.shapeType() == RAPIER.ShapeType.Voxels) { + let g = genVoxelsGeometry(collider); + vertices = g.vertices; + indices = g.indices; + } else { + vertices = collider.vertices(); + indices = collider.indices(); + } + + geometry.setIndex(Array.from(indices)); + geometry.setAttribute( + "position", + new THREE.BufferAttribute(vertices, 3), + ); + let color = parent.isFixed() ? 0 : this.colorIndex + 1; + + let material = new THREE.MeshPhongMaterial({ + color: this.colorPalette[color], + side: THREE.DoubleSide, + flatShading: true, + }); + + let mesh = new THREE.Mesh(geometry, material); + this.scene.add(mesh); + this.coll2mesh.set(collider.handle, mesh); + return; + default: + console.log("Unknown shape to render."); + return; + } + + if (!!instance) { + instanceDesc.elementId = instance.count; + instance.userData.elementId2coll.set(instance.count, collider); + instance.count += 1; + } + + let highlightInstance = + this.instanceGroups[instanceDesc.groupId][ + this.highlightInstanceId() + ]; + highlightInstance.count = 0; + + let t = collider.translation(); + let r = collider.rotation(); + dummy.position.set(t.x, t.y, t.z); + dummy.quaternion.set(r.x, r.y, r.z, r.w); + dummy.scale.set( + instanceDesc.scale.x, + instanceDesc.scale.y, + instanceDesc.scale.z, + ); + dummy.updateMatrix(); + instance.setMatrixAt(instanceDesc.elementId, dummy.matrix); + instance.instanceMatrix.needsUpdate = true; + + this.coll2instance.set(collider.handle, instanceDesc); + } +} diff --git a/thirdparty/rapier.js/testbed3d/src/Gui.ts b/thirdparty/rapier.js/testbed3d/src/Gui.ts new file mode 100644 index 00000000..e89b2f47 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/Gui.ts @@ -0,0 +1,167 @@ +import GUI from "lil-gui"; +import * as Stats from "stats.js"; +import type {Testbed} from "./Testbed"; + +export interface DebugInfos { + token: number; + stepId: number; + worldHash: string; + worldHashTime: number; + snapshotTime: number; + + timingStep: number; + timingCollisionDetection: number; + timingBroadPhase: number; + timingNarrowPhase: number; + timingSolver: number; + timingVelocityAssembly: number; + timingVelocityResolution: number; + timingVelocityUpdate: number; + timingVelocityWriteback: number; + timingCcd: number; + timingCcdToiComputation: number; + timingCcdBroadPhase: number; + timingCcdNarrowPhase: number; + timingCcdSolver: number; + timingIslandConstruction: number; + timingUserChanges: number; +} + +export class Gui { + stats: Stats; + rapierVersion: string; + maxTimePanelValue: number; + stepTimePanel: Stats.Panel; + gui: GUI; + debugText: HTMLDivElement; + + constructor(testbed: Testbed, simulationParameters: Testbed["parameters"]) { + // Timings + this.stats = new Stats(); + this.rapierVersion = testbed.RAPIER.version(); + this.maxTimePanelValue = 16.0; + this.stepTimePanel = this.stats.addPanel( + new Stats.Panel("ms (step)", "#ff8", "#221"), + ); + this.stats.showPanel(this.stats.dom.children.length - 1); + document.body.appendChild(this.stats.dom); + + var backends = simulationParameters.backends; + var demos = Array.from(simulationParameters.builders.keys()); + var me = this; + + // For configuring simulation parameters. + this.gui = new GUI({ + title: "Rapier JS demos", + }); + var currDemo = this.gui + .add(simulationParameters, "demo", demos) + .onChange((demo: string) => { + testbed.switchToDemo(demo); + }); + this.gui + .add(simulationParameters, "numSolverIters", 0, 20) + .step(1) + .listen(); + this.gui + .add(simulationParameters, "debugInfos") + .listen() + .onChange((value: boolean) => { + me.debugText.style.visibility = value ? "visible" : "hidden"; + }); + this.gui.add(simulationParameters, "debugRender").listen(); + this.gui.add(simulationParameters, "running").listen(); + this.gui.add(simulationParameters, "step"); + simulationParameters.step = () => { + simulationParameters.stepping = true; + }; + this.gui.add(simulationParameters, "takeSnapshot"); + simulationParameters.takeSnapshot = () => { + testbed.takeSnapshot(); + }; + this.gui.add(simulationParameters, "restoreSnapshot"); + simulationParameters.restoreSnapshot = () => { + testbed.restoreSnapshot(); + }; + this.gui.add(simulationParameters, "restart"); + simulationParameters.restart = () => { + testbed.switchToDemo(currDemo.getValue()); + }; + + /* + * Block of text for debug infos. + */ + this.debugText = document.createElement("div"); + this.debugText.style.position = "absolute"; + this.debugText.innerHTML = ""; + this.debugText.style.top = 50 + "px"; + this.debugText.style.visibility = "visible"; + this.debugText.style.color = "#fff"; + document.body.appendChild(this.debugText); + } + + setDebugInfos(infos: DebugInfos) { + let text = "Version " + this.rapierVersion; + text += "
[Step " + infos.stepId + "]"; + + if (infos.worldHash) { + text += + "
World hash (xxHash128): " + infos.worldHash.toString(); + text += + "
World hash time (xxHash128): " + + infos.worldHashTime + + "ms"; + text += "
Snapshot time: " + infos.snapshotTime + "ms"; + } + + text += "
timingStep: " + infos.timingStep + "ms"; + text += + "
timingCollisionDetection: " + + infos.timingCollisionDetection + + "ms"; + text += "
timingBroadPhase: " + infos.timingBroadPhase + "ms"; + text += "
timingNarrowPhase: " + infos.timingNarrowPhase + "ms"; + text += "
timingSolver: " + infos.timingSolver + "ms"; + text += + "
timingVelocityAssembly: " + + infos.timingVelocityAssembly + + "ms"; + text += + "
timingVelocityResolution: " + + infos.timingVelocityResolution + + "ms"; + text += + "
timingVelocityUpdate: " + infos.timingVelocityUpdate + "ms"; + text += + "
timingVelocityWriteback: " + + infos.timingVelocityWriteback + + "ms"; + text += "
timingCcd: " + infos.timingCcd + "ms"; + text += + "
timingCcdToiComputation: " + + infos.timingCcdToiComputation + + "ms"; + text += "
timingCcdBroadPhase: " + infos.timingCcdBroadPhase + "ms"; + text += + "
timingCcdNarrowPhase: " + infos.timingCcdNarrowPhase + "ms"; + text += "
timingCcdSolver: " + infos.timingCcdSolver + "ms"; + text += + "
timingIslandConstruction: " + + infos.timingIslandConstruction + + "ms"; + text += "
timingUserChanges: " + infos.timingUserChanges + "ms"; + this.debugText.innerHTML = text; + } + + setTiming(timing: number) { + if (!!timing) { + this.maxTimePanelValue = Math.max(this.maxTimePanelValue, timing); + this.stepTimePanel.update(timing, this.maxTimePanelValue); + } + } + + resetTiming() { + this.maxTimePanelValue = 1.0; + this.stepTimePanel.update(0.0, 16.0); + } +} diff --git a/thirdparty/rapier.js/testbed3d/src/Testbed.ts b/thirdparty/rapier.js/testbed3d/src/Testbed.ts new file mode 100644 index 00000000..5d3afd88 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/Testbed.ts @@ -0,0 +1,211 @@ +import {Graphics} from "./Graphics"; +import {Gui} from "./Gui"; +import type {DebugInfos} from "./Gui"; +import {xxhash128} from "hash-wasm"; +import type * as RAPIER from "@dimforge/rapier3d"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +type Builders = Map void>; + +class SimulationParameters { + backend: string; + prevBackend: string; + demo: string; + numSolverIters: number; + running: boolean; + stepping: boolean; + debugInfos: boolean; + debugRender: boolean; + step: () => void; + restart: () => void; + takeSnapshot: () => void; + restoreSnapshot: () => void; + backends: Array; + builders: Builders; + + constructor(backends: Array, builders: Builders) { + this.backend = "rapier"; + this.prevBackend = "rapier"; + this.demo = "collision groups"; + this.numSolverIters = 4; + this.running = true; + this.stepping = false; + this.debugRender = false; + this.step = () => {}; + this.restart = () => {}; + this.takeSnapshot = () => {}; + this.restoreSnapshot = () => {}; + this.backends = backends; + this.builders = builders; + this.debugInfos = false; + } +} + +export class Testbed { + RAPIER: RAPIER_API; + gui: Gui; + graphics: Graphics; + inhibitLookAt: boolean; + parameters: SimulationParameters; + demoToken: number; + mouse: {x: number; y: number}; + events: RAPIER.EventQueue; + world: RAPIER.World; + preTimestepAction?: (gfx: Graphics) => void; + stepId: number; + prevDemo: string; + lastMessageTime: number; + snap: Uint8Array; + snapStepId: number; + + constructor(RAPIER: RAPIER_API, builders: Builders) { + let backends = ["rapier"]; + this.RAPIER = RAPIER; + let parameters = new SimulationParameters(backends, builders); + this.gui = new Gui(this, parameters); + this.graphics = new Graphics(); + this.inhibitLookAt = false; + this.parameters = parameters; + this.demoToken = 0; + this.mouse = {x: 0, y: 0}; + this.events = new RAPIER.EventQueue(true); + + this.switchToDemo(builders.keys().next().value); + + window.addEventListener("mousemove", (event) => { + this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + this.mouse.y = 1 - (event.clientY / window.innerHeight) * 2; + }); + } + + setpreTimestepAction(action: (gfx: Graphics) => void) { + this.preTimestepAction = action; + } + + setWorld(world: RAPIER.World) { + document.onkeydown = null; // Reset key events. + document.onkeyup = null; // Reset key events. + + this.preTimestepAction = null; + this.world = world; + this.world.numSolverIterations = this.parameters.numSolverIters; + this.demoToken += 1; + this.stepId = 0; + this.gui.resetTiming(); + + world.forEachCollider((coll) => { + this.graphics.addCollider(this.RAPIER, world, coll); + }); + + this.lastMessageTime = new Date().getTime(); + } + + lookAt(pos: Parameters[0]) { + if (!this.inhibitLookAt) { + this.graphics.lookAt(pos); + } + + this.inhibitLookAt = false; + } + + switchToDemo(demo: string) { + if (demo == this.prevDemo) { + this.inhibitLookAt = true; + } + + this.prevDemo = demo; + this.graphics.reset(); + + this.parameters.prevBackend = this.parameters.backend; + this.parameters.builders.get(demo)(this.RAPIER, this); + } + + switchToBackend(backend: string) { + this.switchToDemo(this.parameters.demo); + } + + takeSnapshot() { + this.snap = this.world.takeSnapshot(); + this.snapStepId = this.stepId; + } + + restoreSnapshot() { + if (!!this.snap) { + this.world.free(); + this.world = this.RAPIER.World.restoreSnapshot(this.snap); + this.stepId = this.snapStepId; + } + } + + run() { + if (this.parameters.running || this.parameters.stepping) { + this.world.numSolverIterations = this.parameters.numSolverIters; + + if (!!this.preTimestepAction) { + this.preTimestepAction(this.graphics); + } + + let t0 = new Date().getTime(); + this.world.step(this.events); + this.gui.setTiming(new Date().getTime() - t0); + this.stepId += 1; + + if (!!this.parameters.debugInfos) { + let t0 = performance.now(); + let snapshot = this.world.takeSnapshot(); + let t1 = performance.now(); + let snapshotTime = t1 - t0; + + let debugInfos: DebugInfos = { + token: this.demoToken, + stepId: this.stepId, + worldHash: "", + worldHashTime: 0, + snapshotTime: 0, + timingStep: this.world.timingStep(), + timingCollisionDetection: + this.world.timingCollisionDetection(), + timingBroadPhase: this.world.timingBroadPhase(), + timingNarrowPhase: this.world.timingNarrowPhase(), + timingSolver: this.world.timingSolver(), + timingVelocityAssembly: this.world.timingVelocityAssembly(), + timingVelocityResolution: + this.world.timingVelocityResolution(), + timingVelocityUpdate: this.world.timingVelocityUpdate(), + timingVelocityWriteback: + this.world.timingVelocityWriteback(), + timingCcd: this.world.timingCcd(), + timingCcdToiComputation: + this.world.timingCcdToiComputation(), + timingCcdBroadPhase: this.world.timingCcdBroadPhase(), + timingCcdNarrowPhase: this.world.timingCcdNarrowPhase(), + timingCcdSolver: this.world.timingCcdSolver(), + timingIslandConstruction: + this.world.timingIslandConstruction(), + timingUserChanges: this.world.timingUserChanges(), + }; + t0 = performance.now(); + xxhash128(snapshot).then((hash) => { + debugInfos.worldHash = hash; + t1 = performance.now(); + let worldHashTime = t1 - t0; + debugInfos.worldHashTime = worldHashTime; + debugInfos.snapshotTime = snapshotTime; + this.gui.setDebugInfos(debugInfos); + }); + } + } + + if (this.parameters.stepping) { + this.parameters.running = false; + this.parameters.stepping = false; + } + + this.gui.stats.begin(); + this.graphics.render(this.world, this.parameters.debugRender); + this.gui.stats.end(); + + requestAnimationFrame(() => this.run()); + } +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/ccd.ts b/thirdparty/rapier.js/testbed3d/src/demos/ccd.ts new file mode 100644 index 00000000..823c9013 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/ccd.ts @@ -0,0 +1,75 @@ +import type RAPIER from "@dimforge/rapier3d"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function createWall( + RAPIER: RAPIER_API, + testbed: Testbed, + world: RAPIER.World, + offset: {x: number; y: number; z: number}, + stackHeight: number, +) { + let i, j; + + let shiftY = 1.0; + let shiftZ = 2.0; + + for (i = 0; i < stackHeight; ++i) { + for (j = i; j < stackHeight; ++j) { + let x = offset.x; + let y = i * shiftY + offset.y; + let z = + (i * shiftZ) / 2.0 + (j - i) * shiftZ + offset.z - stackHeight; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 1.0); + world.createCollider(colliderDesc, body); + } + } +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let groundHeight = 0.1; + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(30.0, 0.1, 30.0); + world.createCollider(colliderDesc, body); + + let numX = 5; + let numZ = 8; + let shiftY = groundHeight + 0.5; + + let i; + for (i = 0; i < numX; ++i) { + let x = i * 6.0; + createWall(RAPIER, testbed, world, {x: x, y: shiftY, z: 0.0}, numZ); + } + + // A very fast rigid-body with CCD enabled. + // Create dynamic cube. + bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(-20.0, shiftY + 2.0, 0.0) + .setLinvel(1000.0, 0.0, 0.0) + .setCcdEnabled(true); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.ball(1.0).setDensity(10.0); + world.createCollider(colliderDesc, body); + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: -31.96000000000001, y: 19.730000000000008, z: -27.86}, + target: {x: -0.0505, y: -0.4126, z: -0.0229}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/characterController.ts b/thirdparty/rapier.js/testbed3d/src/demos/characterController.ts new file mode 100644 index 00000000..2406ebcc --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/characterController.ts @@ -0,0 +1,106 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.1, 15.0); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let rad = 0.5; + let num = 5; + let i, j, k; + let shift = rad * 2.5; + let center = num * rad; + let height = 5.0; + + for (i = 0; i < num; ++i) { + for (j = i; j < num; ++j) { + for (k = i; k < num; ++k) { + let x = (i * shift) / 2.0 + (k - i) * shift - center; + let y = (i * shift) / 2.0 + height; + let z = (i * shift) / 2.0 + (j - i) * shift - center; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad / 2.0, + rad, + ); + world.createCollider(colliderDesc, body); + } + } + } + + // Character. + let characterDesc = + RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation( + -10.0, + 4.0, + -10.0, + ); + let character = world.createRigidBody(characterDesc); + let characterColliderDesc = RAPIER.ColliderDesc.cylinder(1.2, 0.6); + let characterCollider = world.createCollider( + characterColliderDesc, + character, + ); + + let characterController = world.createCharacterController(0.1); + characterController.enableAutostep(0.7, 0.3, true); + characterController.enableSnapToGround(0.7); + + let speed = 0.2; + let movementDirection = {x: 0.0, y: -speed, z: 0.0}; + + let updateCharacter = () => { + characterController.computeColliderMovement( + characterCollider, + movementDirection, + ); + + let movement = characterController.computedMovement(); + let newPos = character.translation(); + newPos.x += movement.x; + newPos.y += movement.y; + newPos.z += movement.z; + character.setNextKinematicTranslation(newPos); + }; + + testbed.setWorld(world); + testbed.setpreTimestepAction(updateCharacter); + + document.onkeydown = function (event: KeyboardEvent) { + if (event.key == "ArrowUp") movementDirection.x = speed; + if (event.key == "ArrowDown") movementDirection.x = -speed; + if (event.key == "ArrowLeft") movementDirection.z = -speed; + if (event.key == "ArrowRight") movementDirection.z = speed; + if (event.key == " ") movementDirection.y = speed; + }; + + document.onkeyup = function (event: KeyboardEvent) { + if (event.key == "ArrowUp") movementDirection.x = 0.0; + if (event.key == "ArrowDown") movementDirection.x = 0.0; + if (event.key == "ArrowLeft") movementDirection.z = 0.0; + if (event.key == "ArrowRight") movementDirection.z = 0.0; + if (event.key == " ") movementDirection.y = -speed; // Gravity + }; + + let cameraPosition = { + eye: {x: -40.0, y: 19.730000000000008, z: 0.0}, + target: {x: 0.0, y: -0.4126, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/collisionGroups.ts b/thirdparty/rapier.js/testbed3d/src/demos/collisionGroups.ts new file mode 100644 index 00000000..49c9ddb7 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/collisionGroups.ts @@ -0,0 +1,73 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(5.0, 0.1, 5.0); + world.createCollider(colliderDesc, groundBody); + + // Setup groups. + let group1 = 0x00010001; + let group2 = 0x00020002; + + // Add one floor that collides with the first group only. + colliderDesc = RAPIER.ColliderDesc.cuboid(1.0, 0.1, 1.0) + .setTranslation(0.0, 1.0, 0.0) + .setCollisionGroups(group1); + world.createCollider(colliderDesc, groundBody); + + // Add one floor that collides with the second group only. + colliderDesc = RAPIER.ColliderDesc.cuboid(1.0, 0.1, 1.0) + .setTranslation(0.0, 2.0, 0.0) + .setCollisionGroups(group2); + world.createCollider(colliderDesc, groundBody); + + // Dynamic cubes. + let num = 8; + let rad = 0.1; + + let shift = rad * 2.0; + let centerx = shift * (num / 2); + let centery = 2.5; + let centerz = shift * (num / 2); + let i, j, k; + + for (j = 0; j < 4; j++) { + for (i = 0; i < num; i++) { + for (k = 0; k < num; k++) { + let x = i * shift - centerx; + let y = j * shift + centery; + let z = k * shift - centerz; + + // Alternate between the green and blue groups. + let group = k % 2 == 0 ? group1 : group2; + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + rad, + ).setCollisionGroups(group); + world.createCollider(colliderDesc, body); + } + } + } + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: 10.0, y: 5.0, z: 10.0}, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/convexPolyhedron.ts b/thirdparty/rapier.js/testbed3d/src/demos/convexPolyhedron.ts new file mode 100644 index 00000000..0ec4b903 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/convexPolyhedron.ts @@ -0,0 +1,111 @@ +import type {Testbed} from "../Testbed"; +import seedrandom from "seedrandom"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function generateTriMesh(nsubdivs: number, wx: number, wy: number, wz: number) { + let vertices = []; + let indices = []; + + let elementWidth = 1.0 / nsubdivs; + let rng = seedrandom("trimesh"); + + let i, j; + for (i = 0; i <= nsubdivs; ++i) { + for (j = 0; j <= nsubdivs; ++j) { + let x = (j * elementWidth - 0.5) * wx; + let y = rng() * wy; + let z = (i * elementWidth - 0.5) * wz; + + vertices.push(x, y, z); + } + } + + for (i = 0; i < nsubdivs; ++i) { + for (j = 0; j < nsubdivs; ++j) { + let i1 = (i + 0) * (nsubdivs + 1) + (j + 0); + let i2 = (i + 0) * (nsubdivs + 1) + (j + 1); + let i3 = (i + 1) * (nsubdivs + 1) + (j + 0); + let i4 = (i + 1) * (nsubdivs + 1) + (j + 1); + + indices.push(i1, i3, i2); + indices.push(i3, i4, i2); + } + } + + return { + vertices: new Float32Array(vertices), + indices: new Uint32Array(indices), + }; +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let trimesh = generateTriMesh(20, 40.0, 4.0, 40.0); + let colliderDesc = RAPIER.ColliderDesc.trimesh( + trimesh.vertices, + trimesh.indices, + ); + world.createCollider(colliderDesc, body); + + /* + * Create the polyhedra + */ + let num = 5; + let scale = 2.0; + let border_rad = 0.1; + + let shift = border_rad * 2.0 + scale; + let centerx = shift * (num / 2); + let centery = shift / 2.0; + let centerz = shift * (num / 2); + + let rng = seedrandom("convexPolyhedron"); + let i, j, k, l; + + for (j = 0; j < 15; ++j) { + for (i = 0; i < num; ++i) { + for (k = 0; k < num; ++k) { + let x = i * shift - centerx; + let y = j * shift + centery + 3.0; + let z = k * shift - centerz; + + let vertices = []; + for (l = 0; l < 10; ++l) { + vertices.push(rng() * scale, rng() * scale, rng() * scale); + } + let v = new Float32Array(vertices); + + // Build the rigid body. + bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.roundConvexHull( + v, + border_rad, + ); + world.createCollider(colliderDesc, body); + } + } + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/damping.ts b/thirdparty/rapier.js/testbed3d/src/demos/damping.ts new file mode 100644 index 00000000..c6295f66 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/damping.ts @@ -0,0 +1,42 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, 0.0, 0.0); + let world = new RAPIER.World(gravity); + + /* + * Create the cubes + */ + let num = 10; + let rad = 0.2; + + let subdiv = 1.0 / num; + + let i; + for (i = 0; i < num; ++i) { + let x = Math.sin(i * subdiv * Math.PI * 2.0); + let y = Math.cos(i * subdiv * Math.PI * 2.0); + + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(x, y, 0.0) + .setLinvel(x * 10.0, y * 10.0, 0.0) + .setAngvel(new RAPIER.Vector3(0.0, 0.0, 100.0)) + .setLinearDamping((i + 1) * subdiv * 10.0) + .setAngularDamping((num - i) * subdiv * 10.0); + let body = world.createRigidBody(bodyDesc); + + // Build the collider. + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, body); + } + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: 0, y: 2.0, z: 20}, + target: {x: 0, y: 2.0, z: 0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/fountain.ts b/thirdparty/rapier.js/testbed3d/src/demos/fountain.ts new file mode 100644 index 00000000..00d8dced --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/fountain.ts @@ -0,0 +1,78 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + let removableBodies = new Array(); + + // Create Ground. + let groundBodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(groundBodyDesc); + let groundColliderDesc = RAPIER.ColliderDesc.cuboid(40.0, 0.1, 40.0); + world.createCollider(groundColliderDesc, groundBody); + + // Dynamic cubes. + let rad = 1.0; + let j = 0; + let spawn_interval = 5; + + let spawnBodies = (graphics: Testbed["graphics"]) => { + j += 1; + if (j % spawn_interval != 0) { + return; + } + + let bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setLinvel(0.0, 15.0, 0.0) + .setTranslation(0.0, 10.0, 0.0); + let colliderDesc; + + switch ((j / spawn_interval) % 4) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.roundCylinder( + rad, + rad, + rad / 10.0, + ); + break; + case 3: + colliderDesc = RAPIER.ColliderDesc.cone(rad, rad); + break; + } + + let body = world.createRigidBody(bodyDesc); + let collider = world.createCollider(colliderDesc, body); + graphics.addCollider(RAPIER, world, collider); + + removableBodies.push(body); + + // We reached the max number, delete the oldest rigid-body. + if (removableBodies.length > 400) { + let rb = removableBodies[0]; + world.removeRigidBody(rb); + graphics.removeRigidBody(rb); + removableBodies.shift(); + } + }; + + testbed.setWorld(world); + testbed.setpreTimestepAction(spawnBodies); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 10.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/glbToTrimesh.ts b/thirdparty/rapier.js/testbed3d/src/demos/glbToTrimesh.ts new file mode 100644 index 00000000..fa65a7cd --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/glbToTrimesh.ts @@ -0,0 +1,68 @@ +import type {Testbed} from "../Testbed"; +import {Vector3, Object3D, Mesh, BufferGeometry, BufferAttribute} from "three"; +import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + testbed.parameters.debugRender = true; + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(5.0, 0.1, 5.0); + world.createCollider(colliderDesc, groundBody); + + // Adding the 3d model + + let loader = new GLTFLoader(); + + loader.load("./suzanne_blender_monkey.glb", (gltf) => { + gltf.scene.position.set(0, 1.2, 0); + gltf.scene.scale.set(3, 3, 3); + testbed.graphics.scene.add(gltf.scene); + gltf.scene.updateMatrixWorld(true); // ensure world matrix is up to date + gltf.scene.traverse((child: Object3D) => { + if ((child as Mesh).isMesh && (child as Mesh).geometry) { + const mesh = child as Mesh; + const geometry = mesh.geometry as BufferGeometry; + + const vertices: number[] = []; + const indices = new Uint32Array(geometry.index!.array); // assume index is non-null + const positionAttribute = geometry.getAttribute( + "position", + ) as BufferAttribute; + + mesh.updateWorldMatrix(true, true); + + const v = new Vector3(); + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + v.fromBufferAttribute(positionAttribute, i); + v.applyMatrix4(mesh.matrixWorld); + vertices.push(v.x, v.y, v.z); + } + + const verticesArray = new Float32Array(vertices); + + const rigidBodyDesc = RAPIER.RigidBodyDesc.fixed(); + const rigidBody = world.createRigidBody(rigidBodyDesc); + + const colliderDesc = RAPIER.ColliderDesc.trimesh( + verticesArray, + indices, + ); + world.createCollider(colliderDesc, rigidBody); + } + }); + }); + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: 10.0, y: 5.0, z: 10.0}, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/glbtoConvexHull.ts b/thirdparty/rapier.js/testbed3d/src/demos/glbtoConvexHull.ts new file mode 100644 index 00000000..a4d2619e --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/glbtoConvexHull.ts @@ -0,0 +1,68 @@ +import type {Testbed} from "../Testbed"; +import { + Vector3, + Object3D, + Mesh, + BufferGeometry, + BufferAttribute, + TriangleStripDrawMode, +} from "three"; +import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(5.0, 0.1, 5.0); + world.createCollider(colliderDesc, groundBody); + + // Adding the 3d model + + let loader = new GLTFLoader(); + + loader.load("./suzanne_blender_monkey.glb", (gltf) => { + gltf.scene.position.set(0, 1.2, 0); + gltf.scene.scale.set(3, 3, 3); + testbed.graphics.scene.add(gltf.scene); + testbed.parameters.debugRender = true; + gltf.scene.updateMatrixWorld(true); // ensure world matrix is up to date + + const v = new Vector3(); + const positions: number[] = []; + + gltf.scene.traverse((child: Object3D) => { + if ((child as Mesh).isMesh && (child as Mesh).geometry) { + const mesh = child as Mesh; + const geometry = mesh.geometry as BufferGeometry; + const positionAttribute = geometry.getAttribute( + "position", + ) as BufferAttribute; + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + v.fromBufferAttribute(positionAttribute, i); + v.applyMatrix4(mesh.matrixWorld); + positions.push(v.x, v.y, v.z); + } + } + }); + + const rigidBodyDesc = RAPIER.RigidBodyDesc.fixed(); + const rigidBody = world.createRigidBody(rigidBodyDesc); + + const colliderDesc = RAPIER.ColliderDesc.convexHull( + new Float32Array(positions), + ); + world.createCollider(colliderDesc, rigidBody); + }); + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: 10.0, y: 5.0, z: 10.0}, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/heightfield.ts b/thirdparty/rapier.js/testbed3d/src/demos/heightfield.ts new file mode 100644 index 00000000..5a2feece --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/heightfield.ts @@ -0,0 +1,126 @@ +import seedrandom from "seedrandom"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function generateHeightfield(nsubdivs: number) { + let heights = []; + + let rng = seedrandom("heightfield"); + + let i, j; + for (i = 0; i <= nsubdivs; ++i) { + for (j = 0; j <= nsubdivs; ++j) { + heights.push(rng()); + } + } + + return new Float32Array(heights); +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let nsubdivs = 20; + let scale = new RAPIER.Vector3(70.0, 4.0, 70.0); + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let heights = generateHeightfield(nsubdivs); + let colliderDesc = RAPIER.ColliderDesc.heightfield( + nsubdivs, + nsubdivs, + heights, + scale, + ); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let num = 4; + let numy = 10; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centery = shift / 2.0; + + let offset = -num * (rad * 2.0 + rad) * 0.5; + let i, j, k; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + for (k = 0; k < num; ++k) { + let x = i * shift + offset; + let y = j * shift + centery + 3.0; + let z = k * shift + offset; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc; + + switch (j % 5) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + rad, + ); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.roundCylinder( + rad, + rad, + rad / 10.0, + ); + break; + case 3: + colliderDesc = RAPIER.ColliderDesc.cone(rad, rad); + break; + case 4: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad / 2.0, + rad / 2.0, + ); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(rad, 0.0, 0.0); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(-rad, 0.0, 0.0); + break; + } + + world.createCollider(colliderDesc, body); + } + } + + offset -= 0.05 * rad * (num - 1.0); + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/joints.ts b/thirdparty/rapier.js/testbed3d/src/demos/joints.ts new file mode 100644 index 00000000..56546d7f --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/joints.ts @@ -0,0 +1,292 @@ +import type RAPIER from "@dimforge/rapier3d"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function createPrismaticJoints( + RAPIER: RAPIER_API, + world: RAPIER.World, + origin: RAPIER.Vector, + num: number, +) { + let rad = 0.4; + let shift = 1.0; + + let groundDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + origin.x, + origin.y, + origin.z, + ); + let currParent = world.createRigidBody(groundDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, currParent); + + let i; + let z; + + for (i = 0; i < num; ++i) { + z = origin.z + (i + 1) * shift; + let rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + origin.x, + origin.y, + z, + ); + let currChild = world.createRigidBody(rigidBodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, currChild); + + let axis; + + if (i % 2 == 0) { + axis = new RAPIER.Vector3(1.0, 1.0, 0.0); + } else { + axis = new RAPIER.Vector3(-1.0, 1.0, 0.0); + } + + z = new RAPIER.Vector3(0.0, 0.0, 1.0); + let prism = RAPIER.JointData.prismatic( + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Vector3(0.0, 0.0, -shift), + axis, + ); + prism.limitsEnabled = true; + prism.limits = [-2.0, 2.0]; + world.createImpulseJoint(prism, currParent, currChild, true); + + currParent = currChild; + } +} + +function createRevoluteJoints( + RAPIER: RAPIER_API, + world: RAPIER.World, + origin: RAPIER.Vector3, + num: number, +) { + let rad = 0.4; + let shift = 2.0; + + let groundDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + origin.x, + origin.y, + 0.0, + ); + let currParent = world.createRigidBody(groundDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, currParent); + + let i, k; + let z; + + for (i = 0; i < num; ++i) { + // Create four bodies. + z = origin.z + i * shift * 2.0 + shift; + + let positions = [ + new RAPIER.Vector3(origin.x, origin.y, z), + new RAPIER.Vector3(origin.x + shift, origin.y, z), + new RAPIER.Vector3(origin.x + shift, origin.y, z + shift), + new RAPIER.Vector3(origin.x, origin.y, z + shift), + ]; + + let parents = [currParent, currParent, currParent, currParent]; + + for (k = 0; k < 4; ++k) { + let rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + positions[k].x, + positions[k].y, + positions[k].z, + ); + let rigidBody = world.createRigidBody(rigidBodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, rigidBody); + + parents[k] = rigidBody; + } + + // Setup four joints. + let o = new RAPIER.Vector3(0.0, 0.0, 0.0); + let x = new RAPIER.Vector3(1.0, 0.0, 0.0); + z = new RAPIER.Vector3(0.0, 0.0, 1.0); + + let revs = [ + RAPIER.JointData.revolute( + o, + new RAPIER.Vector3(0.0, 0.0, -shift), + z, + ), + RAPIER.JointData.revolute( + o, + new RAPIER.Vector3(-shift, 0.0, 0.0), + x, + ), + RAPIER.JointData.revolute( + o, + new RAPIER.Vector3(0.0, 0.0, -shift), + z, + ), + RAPIER.JointData.revolute( + o, + new RAPIER.Vector3(shift, 0.0, 0.0), + x, + ), + ]; + + world.createImpulseJoint(revs[0], currParent, parents[0], true); + world.createImpulseJoint(revs[1], parents[0], parents[1], true); + world.createImpulseJoint(revs[2], parents[1], parents[2], true); + world.createImpulseJoint(revs[3], parents[2], parents[3], true); + + currParent = parents[3]; + } +} + +function createFixedJoints( + RAPIER: RAPIER_API, + world: RAPIER.World, + origin: RAPIER.Vector3, + num: number, +) { + let rad = 0.4; + let shift = 1.0; + let i, k; + let parents = []; + + for (k = 0; k < num; ++k) { + for (i = 0; i < num; ++i) { + let fk = k; + let fi = i; + + // NOTE: the num - 2 test is to avoid two consecutive + // fixed bodies. Because physx will crash if we add + // a joint between these. + let bodyType; + + if (i == 0 && ((k % 4 == 0 && k != num - 2) || k == num - 1)) { + bodyType = RAPIER.RigidBodyType.Fixed; + } else { + bodyType = RAPIER.RigidBodyType.Dynamic; + } + + let rigidBody = new RAPIER.RigidBodyDesc(bodyType).setTranslation( + origin.x + fk * shift, + origin.y, + origin.z + fi * shift, + ); + let child = world.createRigidBody(rigidBody); + let colliderDesc = RAPIER.ColliderDesc.ball(rad); + world.createCollider(colliderDesc, child); + + // Vertical joint. + if (i > 0) { + let parent = parents[parents.length - 1]; + let params = RAPIER.JointData.fixed( + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Vector3(0.0, 0.0, -shift), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ); + + world.createImpulseJoint(params, parent, child, true); + } + + // Horizontal joint. + if (k > 0) { + let parent_index = parents.length - num; + let parent = parents[parent_index]; + let params = RAPIER.JointData.fixed( + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Vector3(-shift, 0.0, 0.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ); + + world.createImpulseJoint(params, parent, child, true); + } + + parents.push(child); + } + } +} + +function createBallJoints( + RAPIER: RAPIER_API, + world: RAPIER.World, + num: number, +) { + let rad = 0.4; + let shift = 1.0; + let i, k; + let parents = []; + + for (k = 0; k < num; ++k) { + for (i = 0; i < num; ++i) { + let fk = k; + let fi = i; + + let bodyType; + + if (i == 0 && (k % 4 == 0 || k == num - 1)) { + bodyType = RAPIER.RigidBodyType.Fixed; + } else { + bodyType = RAPIER.RigidBodyType.Dynamic; + } + + let bodyDesc = new RAPIER.RigidBodyDesc(bodyType).setTranslation( + fk * shift, + 0.0, + fi * shift, + ); + let child = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.ball(rad); + world.createCollider(colliderDesc, child); + + // Vertical joint. + let o = new RAPIER.Vector3(0.0, 0.0, 0.0); + + if (i > 0) { + let parent = parents[parents.length - 1]; + let params = RAPIER.JointData.spherical( + o, + new RAPIER.Vector3(0.0, 0.0, -shift), + ); + world.createImpulseJoint(params, parent, child, true); + } + + // Horizontal joint. + if (k > 0) { + let parent_index = parents.length - num; + let parent = parents[parent_index]; + let params = RAPIER.JointData.spherical( + o, + new RAPIER.Vector3(-shift, 0.0, 0.0), + ); + world.createImpulseJoint(params, parent, child, true); + } + + parents.push(child); + } + } +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + createPrismaticJoints( + RAPIER, + world, + new RAPIER.Vector3(20.0, 10.0, 0.0), + 5, + ); + createFixedJoints(RAPIER, world, new RAPIER.Vector3(0.0, 10.0, 0.0), 5); + createRevoluteJoints(RAPIER, world, new RAPIER.Vector3(20.0, 0.0, 0.0), 3); + createBallJoints(RAPIER, world, 15); + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: 15.0, y: 5.0, z: 42.0}, + target: {x: 13.0, y: 1.0, z: 1.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/keva.ts b/thirdparty/rapier.js/testbed3d/src/demos/keva.ts new file mode 100644 index 00000000..d4e27cb0 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/keva.ts @@ -0,0 +1,139 @@ +import type RAPIER from "@dimforge/rapier3d"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function buildBlock( + RAPIER: RAPIER_API, + world: RAPIER.World, + halfExtents: RAPIER.Vector, + shift: RAPIER.Vector, + numx: number, + numy: number, + numz: number, +) { + let half_extents_zyx = { + x: halfExtents.z, + y: halfExtents.y, + z: halfExtents.x, + }; + let dimensions = [halfExtents, half_extents_zyx]; + let blockWidth = 2.0 * halfExtents.z * numx; + let blockHeight = 2.0 * halfExtents.y * numy; + let spacing = (halfExtents.z * numx - halfExtents.x) / (numz - 1.0); + + let i; + let j; + let k; + + for (i = 0; i < numy; ++i) { + [numx, numz] = [numz, numx]; + let dim = dimensions[i % 2]; + let y = dim.y * i * 2.0; + + for (j = 0; j < numx; ++j) { + let x = i % 2 == 0 ? spacing * j * 2.0 : dim.x * j * 2.0; + + for (k = 0; k < numz; ++k) { + let z = i % 2 == 0 ? dim.z * k * 2.0 : spacing * k * 2.0; + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x + dim.x + shift.x, + y + dim.y + shift.y, + z + dim.z + shift.z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + dim.x, + dim.y, + dim.z, + ); + world.createCollider(colliderDesc, body); + } + } + } + + // Close the top. + let dim = {x: halfExtents.z, y: halfExtents.x, z: halfExtents.y}; + + for (i = 0; i < blockWidth / (dim.x * 2.0); ++i) { + for (j = 0; j < blockWidth / (dim.z * 2.0); ++j) { + // Build the rigid body. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + i * dim.x * 2.0 + dim.x + shift.x, + dim.y + shift.y + blockHeight, + j * dim.z * 2.0 + dim.z + shift.z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(dim.x, dim.y, dim.z); + world.createCollider(colliderDesc, body); + } + } +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let groundSize = 50.0; + let groundHeight = 0.1; + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + 0.0, + -groundHeight, + 0.0, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + groundSize, + groundHeight, + groundSize, + ); + world.createCollider(colliderDesc, body); + + // Keva tower. + let halfExtents = new RAPIER.Vector3(0.1, 0.5, 2.0); + let blockHeight = 0.0; + // These should only be set to odd values otherwise + // the blocks won't align in the nicest way. + let numyArr = [0, 3, 5, 5, 7, 9]; + let numBlocksBuilt = 0; + let i; + + for (i = 5; i >= 1; --i) { + let numx = i; + let numy = numyArr[i]; + let numz = numx * 3 + 1; + let blockWidth = numx * halfExtents.z * 2.0; + buildBlock( + RAPIER, + world, + halfExtents, + new RAPIER.Vector3( + -blockWidth / 2.0, + blockHeight, + -blockWidth / 2.0, + ), + numx, + numy, + numz, + ); + blockHeight += numy * halfExtents.y * 2.0 + halfExtents.x * 2.0; + numBlocksBuilt += numx * numy * numz; + } + + testbed.setWorld(world); + let cameraPosition = { + eye: { + x: -70.38553832116718, + y: 17.893810295517365, + z: 29.34767842147597, + }, + target: { + x: 0.5890869353464383, + y: 3.132044603021203, + z: -0.2899937806661885, + }, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/lockedRotations.ts b/thirdparty/rapier.js/testbed3d/src/demos/lockedRotations.ts new file mode 100644 index 00000000..e3b4c538 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/lockedRotations.ts @@ -0,0 +1,58 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + /* + * The ground + */ + let ground_size = 1.7; + let ground_height = 0.1; + + let bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + 0.0, + -ground_height, + 0.0, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + ground_size, + ground_height, + ground_size, + ); + world.createCollider(colliderDesc, body); + + /* + * A rectangle that only rotates along the `x` axis. + */ + bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(0.0, 3.0, 0.0) + .lockTranslations() + .enabledRotations(true, false, false); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.cuboid(0.2, 0.6, 2.0); + world.createCollider(colliderDesc, body); + + /* + * A cylinder that cannot rotate. + */ + bodyDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(0.2, 5.0, 0.4) + .lockRotations(); + body = world.createRigidBody(bodyDesc); + colliderDesc = RAPIER.ColliderDesc.cylinder(0.6, 0.4); + world.createCollider(colliderDesc, body); + + /* + * Setup the testbed. + */ + testbed.setWorld(world); + let cameraPosition = { + eye: {x: -10.0, y: 3.0, z: 0.0}, + target: {x: 0.0, y: 3.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/pidController.ts b/thirdparty/rapier.js/testbed3d/src/demos/pidController.ts new file mode 100644 index 00000000..ea3e1286 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/pidController.ts @@ -0,0 +1,127 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.1, 15.0); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let rad = 0.5; + let num = 5; + let i, j, k; + let shift = rad * 2.5; + let center = num * rad; + let height = 5.0; + + for (i = 0; i < num; ++i) { + for (j = i; j < num; ++j) { + for (k = i; k < num; ++k) { + let x = (i * shift) / 2.0 + (k - i) * shift - center; + let y = (i * shift) / 2.0 + height; + let z = (i * shift) / 2.0 + (j - i) * shift - center; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad / 2.0, + rad, + ); + world.createCollider(colliderDesc, body); + } + } + } + + // Character. + let characterDesc = RAPIER.RigidBodyDesc.dynamic() + .setTranslation(-10.0, 4.0, -10.0) + .setGravityScale(10.0) + .setSoftCcdPrediction(10.0); + let character = world.createRigidBody(characterDesc); + let characterColliderDesc = RAPIER.ColliderDesc.cylinder(1.2, 0.6); + world.createCollider(characterColliderDesc, character); + + let pidController = world.createPidController( + 60.0, + 0.0, + 1.0, + RAPIER.PidAxesMask.AllAng, + ); + let speed = 0.2; + let movementDirection = {x: 0.0, y: 0.0, z: 0.0}; + let targetVelocity = {x: 0.0, y: 0.0, z: 0.0}; + let targetRotation = new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0); + + let updateCharacter = () => { + if ( + movementDirection.x == 0.0 && + movementDirection.y == 0.0 && + movementDirection.z == 0.0 + ) { + // Only adjust the rotation, but let translation. + pidController.setAxes(RAPIER.PidAxesMask.AllAng); + } else if (movementDirection.y == 0.0) { + // Don’t control the linear Y axis so the player can fall down due to gravity. + pidController.setAxes( + RAPIER.PidAxesMask.AllAng | + RAPIER.PidAxesMask.LinX | + RAPIER.PidAxesMask.LinZ, + ); + } else { + pidController.setAxes(RAPIER.PidAxesMask.All); + } + + let targetPoint = character.translation(); + targetPoint.x += movementDirection.x; + targetPoint.y += movementDirection.y; + targetPoint.z += movementDirection.z; + + pidController.applyLinearCorrection( + character, + targetPoint, + targetVelocity, + ); + pidController.applyAngularCorrection( + character, + targetRotation, + targetVelocity, + ); + }; + + testbed.setWorld(world); + testbed.setpreTimestepAction(updateCharacter); + + document.onkeydown = function (event: KeyboardEvent) { + if (event.key == "ArrowUp") movementDirection.x = speed; + if (event.key == "ArrowDown") movementDirection.x = -speed; + if (event.key == "ArrowLeft") movementDirection.z = -speed; + if (event.key == "ArrowRight") movementDirection.z = speed; + if (event.key == " ") movementDirection.y = speed; + }; + + document.onkeyup = function (event: KeyboardEvent) { + if (event.key == "ArrowUp") movementDirection.x = 0.0; + if (event.key == "ArrowDown") movementDirection.x = 0.0; + if (event.key == "ArrowLeft") movementDirection.z = 0.0; + if (event.key == "ArrowRight") movementDirection.z = 0.0; + if (event.key == " ") movementDirection.y = 0.0; + }; + + let cameraPosition = { + eye: {x: -40.0, y: 19.730000000000008, z: 0.0}, + target: {x: 0.0, y: -0.4126, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/platform.ts b/thirdparty/rapier.js/testbed3d/src/demos/platform.ts new file mode 100644 index 00000000..fe51d9ae --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/platform.ts @@ -0,0 +1,153 @@ +import seedrandom from "seedrandom"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function generateTriMesh(nsubdivs: number, wx: number, wy: number, wz: number) { + let vertices = []; + let indices = []; + + let elementWidth = 1.0 / nsubdivs; + let rng = seedrandom("trimesh"); + + let i, j; + for (i = 0; i <= nsubdivs; ++i) { + for (j = 0; j <= nsubdivs; ++j) { + let x = (j * elementWidth - 0.5) * wx; + let y = rng() * wy; + let z = (i * elementWidth - 0.5) * wz; + + vertices.push(x, y, z); + } + } + + for (i = 0; i < nsubdivs; ++i) { + for (j = 0; j < nsubdivs; ++j) { + let i1 = (i + 0) * (nsubdivs + 1) + (j + 0); + let i2 = (i + 0) * (nsubdivs + 1) + (j + 1); + let i3 = (i + 1) * (nsubdivs + 1) + (j + 0); + let i4 = (i + 1) * (nsubdivs + 1) + (j + 1); + + indices.push(i1, i3, i2); + indices.push(i3, i4, i2); + } + } + + return { + vertices: new Float32Array(vertices), + indices: new Uint32Array(indices), + }; +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.kinematicVelocityBased(); + let platformBody = world.createRigidBody(bodyDesc); + let trimesh = generateTriMesh(20, 70.0, 4.0, 70.0); + let colliderDesc = RAPIER.ColliderDesc.trimesh( + trimesh.vertices, + trimesh.indices, + ); + world.createCollider(colliderDesc, platformBody); + let t = 0.0; + + let movePlatform = () => { + t += 0.016; + let dy = Math.sin(t) * 10.0; + let dang = Math.sin(t) * 0.2; + platformBody.setLinvel({x: 0.0, y: dy, z: 0.0}, true); + platformBody.setAngvel({x: 0.0, y: dang, z: 0.0}, true); + }; + + // Dynamic cubes. + let num = 4; + let numy = 10; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centery = shift / 2.0; + + let offset = -num * (rad * 2.0 + rad) * 0.5; + let i, j, k; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + for (k = 0; k < num; ++k) { + let x = i * shift + offset; + let y = j * shift + centery + 3.0; + let z = k * shift + offset; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc; + + switch (j % 5) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + rad, + ); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.roundCylinder( + rad, + rad, + rad / 10.0, + ); + break; + case 3: + colliderDesc = RAPIER.ColliderDesc.cone(rad, rad); + break; + case 4: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad / 2.0, + rad / 2.0, + ); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(rad, 0.0, 0.0); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(-rad, 0.0, 0.0); + break; + } + + world.createCollider(colliderDesc, body); + } + } + + offset -= 0.05 * rad * (num - 1.0); + } + + testbed.setWorld(world); + testbed.setpreTimestepAction(movePlatform); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/pyramid.ts b/thirdparty/rapier.js/testbed3d/src/demos/pyramid.ts new file mode 100644 index 00000000..9a63e47e --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/pyramid.ts @@ -0,0 +1,51 @@ +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(30.0, 0.1, 30.0); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let rad = 0.5; + let num = 10; + let i, j, k; + let shift = rad * 2.5; + let center = num * rad; + let height = 10.0; + + for (i = 0; i < num; ++i) { + for (j = i; j < num; ++j) { + for (k = i; k < num; ++k) { + let x = + (i * shift) / 2.0 + (k - i) * shift - height * rad - center; + let y = i * shift + height; + let z = + (i * shift) / 2.0 + (j - i) * shift - height * rad - center; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(rad, rad, rad); + world.createCollider(colliderDesc, body); + } + } + } + + testbed.setWorld(world); + let cameraPosition = { + eye: {x: -31.96000000000001, y: 19.730000000000008, z: -27.86}, + target: {x: -0.0505, y: -0.4126, z: -0.0229}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/trimesh.ts b/thirdparty/rapier.js/testbed3d/src/demos/trimesh.ts new file mode 100644 index 00000000..5a50bd3b --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/trimesh.ts @@ -0,0 +1,143 @@ +import seedrandom from "seedrandom"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function generateTriMesh(nsubdivs: number, wx: number, wy: number, wz: number) { + let vertices = []; + let indices = []; + + let elementWidth = 1.0 / nsubdivs; + let rng = seedrandom("trimesh"); + + let i, j; + for (i = 0; i <= nsubdivs; ++i) { + for (j = 0; j <= nsubdivs; ++j) { + let x = (j * elementWidth - 0.5) * wx; + let y = rng() * wy; + let z = (i * elementWidth - 0.5) * wz; + + vertices.push(x, y, z); + } + } + + for (i = 0; i < nsubdivs; ++i) { + for (j = 0; j < nsubdivs; ++j) { + let i1 = (i + 0) * (nsubdivs + 1) + (j + 0); + let i2 = (i + 0) * (nsubdivs + 1) + (j + 1); + let i3 = (i + 1) * (nsubdivs + 1) + (j + 0); + let i4 = (i + 1) * (nsubdivs + 1) + (j + 1); + + indices.push(i1, i3, i2); + indices.push(i3, i4, i2); + } + } + + return { + vertices: new Float32Array(vertices), + indices: new Uint32Array(indices), + }; +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let trimesh = generateTriMesh(20, 70.0, 4.0, 70.0); + let colliderDesc = RAPIER.ColliderDesc.trimesh( + trimesh.vertices, + trimesh.indices, + ); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let num = 4; + let numy = 10; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centery = shift / 2.0; + + let offset = -num * (rad * 2.0 + rad) * 0.5; + let i, j, k; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + for (k = 0; k < num; ++k) { + let x = i * shift + offset; + let y = j * shift + centery + 3.0; + let z = k * shift + offset; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc; + + switch (j % 5) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + rad, + ); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.roundCylinder( + rad, + rad, + rad / 10.0, + ); + break; + case 3: + colliderDesc = RAPIER.ColliderDesc.cone(rad, rad); + break; + case 4: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad / 2.0, + rad / 2.0, + ); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(rad, 0.0, 0.0); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(-rad, 0.0, 0.0); + break; + } + + world.createCollider(colliderDesc, body); + } + } + + offset -= 0.05 * rad * (num - 1.0); + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/demos/voxels.ts b/thirdparty/rapier.js/testbed3d/src/demos/voxels.ts new file mode 100644 index 00000000..936b8730 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/demos/voxels.ts @@ -0,0 +1,130 @@ +import seedrandom from "seedrandom"; +import type {Testbed} from "../Testbed"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function generateVoxels(n: number) { + let points = []; + + let i, j; + for (i = 0; i <= n; ++i) { + for (j = 0; j <= n; ++j) { + let y = + Math.max( + -0.8, + Math.min( + Math.sin((i / n) * 10.0) * Math.cos((j / n) * 10.0), + 0.8, + ), + ) * 8.0; + points.push(i - n / 2.0, y, j - n / 2.0); + } + } + return { + points: new Float32Array(points), + voxelSize: {x: 1.0, y: 1.2, z: 1.5}, + }; +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let body = world.createRigidBody(bodyDesc); + let voxels = generateVoxels(100); + let colliderDesc = RAPIER.ColliderDesc.voxels( + voxels.points, + voxels.voxelSize, + ); + world.createCollider(colliderDesc, body); + + // Dynamic cubes. + let num = 10; + let numy = 4; + let rad = 1.0; + + let shift = rad * 2.0 + rad; + let centery = shift / 2.0; + + let offset = -num * (rad * 2.0 + rad) * 0.5; + let i, j, k; + + for (j = 0; j < numy; ++j) { + for (i = 0; i < num; ++i) { + for (k = 0; k < num; ++k) { + let x = i * shift + offset; + let y = j * shift + centery + 10.0; + let z = k * shift + offset; + + // Create dynamic cube. + let bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + let body = world.createRigidBody(bodyDesc); + let colliderDesc; + + switch (j % 5) { + case 0: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad, + rad, + rad, + ); + break; + case 1: + colliderDesc = RAPIER.ColliderDesc.ball(rad); + break; + case 2: + colliderDesc = RAPIER.ColliderDesc.roundCylinder( + rad, + rad, + rad / 10.0, + ); + break; + case 3: + colliderDesc = RAPIER.ColliderDesc.cone(rad, rad); + break; + case 4: + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad / 2.0, + rad / 2.0, + ); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(rad, 0.0, 0.0); + world.createCollider(colliderDesc, body); + colliderDesc = RAPIER.ColliderDesc.cuboid( + rad / 2.0, + rad, + rad / 2.0, + ).setTranslation(-rad, 0.0, 0.0); + break; + } + + world.createCollider(colliderDesc, body); + } + } + + offset -= 0.05 * rad * (num - 1.0); + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: { + x: -88.48024008669711, + y: 46.911325612198354, + z: 83.56055570254844, + }, + target: {x: 0.0, y: 0.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/thirdparty/rapier.js/testbed3d/src/index.ts b/thirdparty/rapier.js/testbed3d/src/index.ts new file mode 100644 index 00000000..accfad53 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/src/index.ts @@ -0,0 +1,42 @@ +import {Testbed} from "./Testbed"; +import * as Trimesh from "./demos/trimesh"; +import * as Voxels from "./demos/voxels"; +import * as CollisionGroups from "./demos/collisionGroups"; +import * as Pyramid from "./demos/pyramid"; +import * as Keva from "./demos/keva"; +import * as Joints from "./demos/joints"; +import * as Fountain from "./demos/fountain"; +import * as Damping from "./demos/damping"; +import * as Heightfield from "./demos/heightfield"; +import * as LockedRotations from "./demos/lockedRotations"; +import * as ConvexPolyhedron from "./demos/convexPolyhedron"; +import * as CCD from "./demos/ccd"; +import * as Platform from "./demos/platform"; +import * as CharacterController from "./demos/characterController"; +import * as PidController from "./demos/pidController"; +import * as glbToTrimesh from "./demos/glbToTrimesh"; +import * as glbToConvexHull from "./demos/glbtoConvexHull"; + +import("@dimforge/rapier3d").then((RAPIER) => { + let builders = new Map([ + ["collision groups", CollisionGroups.initWorld], + ["character controller", CharacterController.initWorld], + ["convex polyhedron", ConvexPolyhedron.initWorld], + ["CCD", CCD.initWorld], + ["damping", Damping.initWorld], + ["fountain", Fountain.initWorld], + ["heightfield", Heightfield.initWorld], + ["joints", Joints.initWorld], + ["keva tower", Keva.initWorld], + ["locked rotations", LockedRotations.initWorld], + ["pid controller", PidController.initWorld], + ["platform", Platform.initWorld], + ["pyramid", Pyramid.initWorld], + ["triangle mesh", Trimesh.initWorld], + ["voxels", Voxels.initWorld], + ["GLTF to convexHull", glbToConvexHull.initWorld], + ["GLTF to trimesh", glbToTrimesh.initWorld], + ]); + let testbed = new Testbed(RAPIER, builders); + testbed.run(); +}); diff --git a/thirdparty/rapier.js/testbed3d/static/.htaccess b/thirdparty/rapier.js/testbed3d/static/.htaccess new file mode 100644 index 00000000..9bdb356b --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/static/.htaccess @@ -0,0 +1,3 @@ +RewriteEngine On +RewriteCond %{SERVER_PORT} 80 +RewriteRule ^(.*)$ https://www.rapier.rs/$1 [R,L] diff --git a/thirdparty/rapier.js/testbed3d/static/index.html b/thirdparty/rapier.js/testbed3d/static/index.html new file mode 100644 index 00000000..d2a61221 --- /dev/null +++ b/thirdparty/rapier.js/testbed3d/static/index.html @@ -0,0 +1,18 @@ + + + + + Rapier3D JS bindings demo + + + + + + diff --git a/thirdparty/rapier.js/testbed3d/static/suzanne_blender_monkey.glb b/thirdparty/rapier.js/testbed3d/static/suzanne_blender_monkey.glb new file mode 100644 index 0000000000000000000000000000000000000000..6b8819b0e9103c3192b0ec5991642c3900f09231 GIT binary patch literal 60820 zcmb822YeO9`}G5;R8cxg4@i@ekU|2vy9h{8I;bEP0)!e8OhWHXR8UY*Kv57;ET|}` zsN5YC6}w^q8;T84QBkp@dC$2syZhYx=g02L=kxVBOwK+tJ3BKwyY~ji<`3ynHYq7- z^$$r&FPBeBx@d6!ezTHuM~%uWE-ox8PVO{(R`SRxW5(naT{=E*T5_kc4w)>RQc!{)XJ_IslX9oyz|?kWnH^KJQ&LjW(y}vBvQjeJq_oS* zNX^K|&W0@|Jv%i!9Yxt$>1kPTWp>O?NzF`6y%Gt=7a&2~lVoUnk`yRANsf|O>yj_M zuv=R4oHqaGEJn^K$?HF6OmQAs-61U_yTkv_iKWDvotfS-twUyqwA7T84q2%z%I=tw znw8NpBQrBAEiFTyNJ<-=4gNz@hjjSS4_T=h9b>LjH8tgA?aJ^vyhC!F7DL6eeuM+ zl2PNvGx`Qu0B6%^wNCwJ@Cw(GFA{kpYHZ2lAwZEA;hnH}4uAj8zWqT=y|7G4|_Qck~`swaPaiWD;Qfc4nrPe7Gu63cM|I#lV6xy($$+V#cWz`(O8bMwDj!E zG=C)ipG4^$Q?qa_<-jz#Sv0t$X#8l@kcxpCuC5Lo2l^z;35Zr_WTE?Ln3P+RS2R91 zUt2~OPC?%fMopuWJCzho$@3~3R9IMolbJlSsBd0LZa%K`pu#C*#}#0m;NTjWTb$Re zFu$;e?U&9!AKipEbG z&%vjEo?C*>89Akdf1Q=wum7OF7xalORRj7D?meV;|9`J;>SxD>?}rCija7dFoScsjJ}&>quZgU-O)ZOKtwL4a?j!lx-S|(c7p-nm)tw&$%B5{ZbLmGSDZ93K;tQc9D zm^$tsuFBaN8L1uJ0?aC|QzF%y02{z+|v?n%i=&uEj;E;}VnoJ;H{erNQ_ zoisVtTc&fozl+X^tDA|uot`xUIDLy*TC!G4e%y-3%m{Xfp@^W;63m@*bfeX55R}uBk(c!1RMmP zg3rJq@HzMbdr!G)lQ(TqJoFVGuYWHjT&pbzK^`WemG9}EBk!62g<2ZJHt z5^$-}jF*9-U>LaEXvX2-3UDR3%4o)`!3dBGMjFjH3XBGMV2sg>W5GBu9!xNraU#eE zlR$ydjD=t_xCRs%%~%Xdz!WgmXvS$^I+y`w8qGKh%m#D7T%#H1f%#woSZFlkBCr@N z0oNMMxD;Fmt_L?5&A1FK2RDM7jAmQ`R)U+sEk-l00;|ET;5MTf*MPNP9k|_S#yh}z za3{FSXvPg-Be)xEGMe!ouo>J7?lYQk3%DOV03I}&@gcAkJPaN&n(S9?4OBOp zu?DCKYJu8DGu8oh!8zbuqZ#Xg`k(=5Xf$IZ&=@oU$wo6a1%Ml*H>IUoWoqZw_`1zZ5S8qL@Z zbO#rL9!4|v1ie6SaFNlB7lS^aFX(49V}CFJ3a-$iC zgDb$5;3}gTuLdJPE*NPv<0vp1kA4~!TMl%+I$>17LWHe(j zC;?NzRHGTEf$3ldm}xZQEHE3)0dtLJoCoHE1z@4kjElfxumoIdG~-fm9k?FcU^L@0 zupHb7ZZev21y~7g2Dcc^xC*QWw}RV@W?Tc-f_30_qZ#i2>%pDiE~6PYfQ{g8u*qn~ zd%$LJFSyTW#x3A}@BnzwXvT-YR`4)*#AwDx!8Y(1c-&~l?O+FZ0z7Fn<5OTKcp5xo zG~=^i7l;DKXhsEggXh5WMlL z+JW{U#c0M<&;g`@bfXzFKqlx2vW#ZT2Ax1>kYhAs1X#cZU5sYD0CWZ2KzE}VF9bb6 zPteO~#@^r}a53m(G-F@T5A+8EjAk4N27$p~h|!FffJ?z;V5rfI!@%WWIJm-S#w)>9 z;A$|!XvSPH5{v?)jb_XPW58H2&S=K*U;>y3@{MMk1PVYQm~1rTHJ}I-gA$_|r+}$o z8klZ0;|wqp%mTBGW}E}&f_Y%R(ToegLa+!dHkxq>xE3r0*BQ-tJ-7iZ1IvwOyb;_4 zR)Cd8Gu{kt0jt1jqZw}nw}CZat|w8NUVJf$zal zqZxky$H8ylccU5q0DpqNz~4qQ{sT^cf5Cr7GnN9rv?v400&2!`;52YLIKybh@}L5! z2r3!PSQ%6SXM(CmGoA&`2Gu}yqZw;}nxGb_Z8T#YP#2s7&NZ5`9;go*fQCjhHUf=7 z6Oe2)V^h!!oClg4&Da981g$`8qZ!W!Z9rSl&S=K=AO)m?4n{Mkfpm}oGL2^J2(myn z=wviwXOIITz%rWA23^1fpsUf0-9UG6A?RT=V^7cv^ad9h&3G~B1Nwq~Ml<#Y1HeEq z$Y{pFUIJfFb<3d z6O3k@2=c)sP+&A;A(#xV0YyeL7K0Kn1xz)XaT=HoW`LPSGtL6D!5lExXvTS9K3D)2 z8qK%}ECx%!wMH{81=oS=!3{<;E(6QKjo>Du8CQUn;AU`((TuCWYH%yK&1l9oU@ce& zZa13o4zM2F3GOnQaRb;0?gpETX1oV%2KR#djAq;d?gtNm2aRTY2y6upgGY>Jd=zX0 zkAcUHX50>TfG5C{Ml(JIc7mtDGe$E$3wD7haExYDU^jRUJa07P9`FKq5xitH<6iJG zcm=#_G~;XFb?^pw(`d%Gz}sLSc*khQcfot$eX!qX#slC3@FDofXvUAhC*UCX)M&=f zz#;HC_`+z$FTq#fYjD_T#&5t8@GbbxXvXisQSbx!(P+k>z%lSM_{C_(U%_$k8~EL5 z#y`NH;4kpE(Tx9q6X0L)pV5q^An7!$|DY_OW-JFz1E+&CjAkqkDu9ZhlF^KnK^1T& zsA@FhS>S9?4OBOpu?DCKYJu8DGu8oh!8zbuqZ#Xg`k(=5Xf$IZ&=@oU$wo6a1%Ml*H>IUoWo zqZw_`1zZ5S8qL@ZbO#rL9!4|v1ie6SaFNlB7lS^aFX(49V}CFJ3a-$iCgDb$5;3}gTuLdJPE*NPv<0vp1kA4~!T zMl%+I$>17LWHe(jC;?NzRHGTEf$3ldm}xZQEHE3)0dtLJoCoHE1z@4kjElfxumoId zG~-fm9k?FcU^L@0upHb7ZZev21y~7g2Dcc^xC*QWw}RV@W?Tc-f_30_qZ#i2>%pDi zE~6PYfQ{g8u*qn~d%$LJFSyTW#x3A}@BnzwXvT-YR`4)*#AwDx!8Y(1c-&~l?O+FZ z0z7Fn<5OTKcp5xoG~=^i7l;DKXhsEggXh5WMl!G(9USa_8|OfU<~Hkxq`m<#5C`9?D?05^dZV5QNFH-lTiDzMsU##_N{U=3JnG~+sOJGcX^ zH=6NIa2MDBHX6-%H`oO30h^6xycgUDwt)MMW_$oV2p$4kjb?lp>;O-ICyi!&3hV?= zgJ+Cpd=~5iQQ#QOsK9RU9C+Sn#y#K#@FIB0XvV$ZW$+4k)o8}o!0X@*@TSp>Z-KYL zKJbpwjPHW?!24jo(ToSc2jD~Sk`9#jAoK_#OZD}yTFOiVk8?xkfYA1NA`z(9mecMxZfh0+NkpYzmrz^FVW>8C!sspcQCsG~@Z84QLD6 z8O_)pq<~b=!Dz-bkPb3HrqPTYK^Djcos4Ge401pOSVlA2pbNMFbTyi>8|V%$1U-yq z>KLj6v zkHIHKGadw=g3rJqqZvO3Uw|*cS4J~_4Gx2Ez!9SvzXjic@4-={8Gisjf}g-KqZxk& zzkpxCaibZ31HXemz@J7l{ssO9|9}%lGyV(y1EnCTyw;3mKv_@@oMtrR>EH}d9#k-z zu_CAhDuXIUGoA^mg0sNcMl)6e)jKe^>4mcOo1NDt&YycX9Mxe3L zj7>l?XbPGc&3GPY4qAYgMl-eot-<-AjnRy4K|9bMq!`VZ3Oax^kZv?%2FL^*L6*^s z*`O2X404QSi~tMRpo`Ir7l5vy8|ZE{a~lsnLv= zfuUd+xZG&Q;ou5zCAi9H#;d^ykPAi{%{U5-26A2m;{)J9@DSK)G~>enD~7ed^a6E%(sU~+>5#Rxw3`}Q+5(ziKTW|`@ODQq zEA1gkZH$Ls%X7;QIlljxefjOm38{=U9v8eJ8n6(>&G!TPM=qN8~i@e`Fo*%%A=qB zx)R#p<<$ALAAKwO%CAf29Fnx$8j{o}*8ki7>yP$ZXw&Z4xw!2_Iohww;dflWtV?UZ zAH=azMoR23*&cX3Y3?p*-rA z!;d`LFYA(RkbS~2%JXHEbtSaH%L!ksfAn#FU&*>;PQR`#$glfx?=fR?)U!| zOV$;CUVfj*oNoVf&L#8}@;AqIKg2e8b;+FZedxE7*OAwHXz4ex@yTnnwX~cq>*95E z%iU{AzpTscf3`vPiO$~({lC*2|K?bW*O7f9b84;qvae)aGAHZP>&X_JhhBe7PVcwH`S-#)z7_gcT;CqCd53bx zEl2yj{zo2f{f8g@qy0PwagAa*-SN{psUO$3gt|CB-Eywadi=<`WE}=79FvLT$Mwsca;+FzIyRP{WwI`m$MUmWt_iX(*#?=@ud9WZlXEwr zu2_DS`#D?0)g{{?bGm)y-oJQ`1IiqWxb0_7_JdpQ_6hy6F1P*0AG@aX_}99HcTHW7 zpHq(?miyO~etoS?+q^c&oN}xtjLBFVbX{`n`C~G^F24=zldYwL-LWn6V9dB<-p}cd zJ#$`;f2`!;@xaTfB!XMWL?|fx@60Qr{qpuhH%GC+iLge?)D`EV^#x2_>kCkJF z`9+&8SG(=unh>AUAM6U556_Y9=iHCa>Ca8~9QFF}zw*d6%Fh{ptnTMix93;1 z$Gz4vr#x1U0lz)>CuQN-syLVFv3=YA(k7~PQUU&sy>@9+wZHTlqibL-njKe;eKzK@ z_9G9jsm`O1)&8NSTaf>ZSe^l8Cga*QfgT@MS3(=SobboA_HRLcok!L+Bxx7MdPm~NE1?ZuPWX{i z`*}^>JhCo48O8E*e&3%o6n&M6{Jd^{T?uXQa>9>3as3-(`Sm*C*Tp>>pEdC0jpMi{ zsGztP8fmv>yqWvzTdA)wt??5xVCega81zXdyLOJ(7JzQ{)D!f zHXp-%ss;K)=aKnkPQ6xJsOwkM71!rpU9xZCM^4v2->XZuLH7lp<+=9AG3n<|s8?J& zIaWV$^m~9qI2XTOj&-i#oTmxn+#CN1ZScmw9Oqm!+&u7mb#Z+2dG`LKwYU!cnjq^+ zXan+i^BV2aYX$G6ZXQ_|##Jmo#u@rpu4#T<32pFlvL5E*Sm4~%&)r?H-|_3>Sm%1q zv7fL`qTjq@WgEPla;@h4b@Rx&^!_GImH&QTPQp5G)5rH?32i`I+;VxZ*^ND$o5!z< z=f*LHdN5z(?o+Yz()TYugSp4boc?`^W75y#*Cq4o_l8~2pJ$;zIe)Wxe~hbFmUB*V zeV{)+zhAGMzjB`HJbHbAM*F*A-B@qx_0|n{L zPPQ-g{qv)49@gvn`8>mW8}CW7E}1iaujn5Ozn2qvu&2>^$me{@uOee2iP|m+hDP=Y*WHF7H^`PT79hh5==^q5pZ0ScT*C{f_H5HRn#^dC78W z9!o#+p#OCqd0w&~Wd9`QVV|^4Y8M+l`hn{j-%~B;If}m8jkf7>&MDu|wMFNVeZsYc{e!)+!*=pm9v5F%TpPTc zY^Uqz-oVX+HhXpP99hn12kh@-`T28MS3(=SoO~W&ex1+HlTa6a%=7ArI@$ky;O`Pw z7t2K(;&QrvdSo8g&r)hLmSjK2&V~I?58KJTB(6&=zqu~4J~{aqcKvb3dO0~J^}7Pj zIkur+Y%XvN`{jOJ^!RP?{VbK|$mb2-bM*abK!RykO;xRT>Uk9j{Q+m<*lc&uA4>ykO;ypZFZb-BmJ_rE{aWdF;$ zWKKB-WFN}>@?7M#NNls)9+^k>n=GdmJK>6dxrd1;THzjB_U-1490ru_+Zd2P_gdg~tgMBfYayCXik zZp2#W*QMVP>2kSFB9B*>Y=c~TIcC@%{qDfO|H``L{gl_j^~<_s8~8lM_dk60-CDW` zeZqHT8}Tk9E7m_+$JG_e>f$A;|lgnE(FyYAj~=O*+U$RrXE*k5+Rytn+rxced|fi0Wo1 z>*x9|sKc*|IYl|NJFet4jcWt)xE}VFJ0_|5jL-dztV@*sye%>zHmUBCYDhJ#*yWeZnvh{Eb7HL6{yVE0(CxVY$2sx+qt40iQn}@PZ{+%AU9t`FIpw>%+O}Tz zRw2J%SL>wO7^hrUS?<3#lXb~9z!PgT$6owymAdIq@unocjGOKVRg0mlGDYmX-o#E$+e22CZWh*gO`2Ov6mdEn2T-GIX!tc2Lgmd@$MCPQ2>maW3nUIq^AOY%a4- zd92JS$0R@Z;~b|yVeWW!$(-0T+WKAeO7xF^tjsCr6!(~}KVj~B^Hk>KXJ+(sOvz(q zPPr!VJ6EnhVeN|b1J^W}b4b!r^h;XoePR5u{#qy3H25(Fv_E0(#reByub-11?lJgG zBaf9i<(f?G`s43Y{yHyn@;g6zUh{d*KUU_H_ccEI`Tn?jACA}eKADsEIF4uD^X0KJ zXTm+v@%?f4?pS`V)w~9J{&LUY&t<-Ea?9ntUe4wCIiBU^lzS59XB*gmvM%O#%jI50 z)+O5@bKHw*Kg6AGdsd)b+>b@%>nzy_{@^zV2Bz+sx0Cum`neU2L0M4!;>ES(ul8UGZ)3 zbIQ+-xpwfgc-+h5>f)N_mdAfa?AImR5TEk|a_Y~=`I$Y}0=-W7b)g*Xcgs(pF720f z$u@BR<9hJ?hx62*?_>SLnsdV8_fchCdR^4zd|q_@^tyRu8)Q!T-2=QkME;{V=53sZ zUl-pgy5;fTG4Sh>ZIC(fPRrKsNa`YI51h+xw7&(`I$2ljxjk0ScVMnx)+O5@bK;o> z>%aca2KPn!Uc&L~*Tr$_mh0yv?U!}wIicGobL!8e_4iw}K8F6`vyNXE-`DWFp)A*0 z`(<6S4V)Le_WB(S|F+WM_2Kgc-{Y9J;u`OSry}k#St#?$JwJ~Wd2qb;vmC#rhB4O< zVd1izhh+9+_+;!PA?DN=?g#Jy&T1{Gh+GOx@1oMjB^N{YxwLJcdYAY+uS_x zqyMy@{lNLc@0-!X`>6Z9GU2yezsw{3>^C=$%&+&K-uJ}3x@4Q_;dhM8_t0e9L{9E? zbsp|TU4Q&{y|{n&JYG)yEU^d2XX1X~A4~1}<#)jNxx&^`?Ul6A?P+yi4ThIS`m z?EA;Y=k&h=20!MX=f^nIb;)arT;BMR`BCnTA9<~1z5gqJLO*-gRMwkt?Yw@L`DMMl zcVi7hfARaaem}_eC(Jdk{j#0Br@MLL=io-meZMNNg{&)~4We&lpKrwY((ij^UGhCG z-;dxvjCOF17>e^chFoJE=GS9;KFjfQT@F9`NBg;EXwOFajpk=ee*5Fw>DQIe1}~>< zr+m*W>(YC(voUwOB(~FQzxE_?{}$JN*-pK0(RsAiewkn9!FmwOZ`NUGZ{5Z@>-9My z56X3Zxo%^e@Ut*`+kY#tH|8@h_ah^)XW0f_(dOr;^k6^FeFXG2Tt{8b`?&A-^Thk( zj`ectJ?K)4{YpQ_7Uv<#an0Rwzplibu0P>e%RQFkhjYoybJ)7zx*m=7zgr$(m){2F zbp2AxoC$LZHt)WMxv1}79II|!GLP2UFUO?JDeFp@7hX;|Pvgg`KQ9vY2Hu_l@9%ld zqx!w(FkbU0+rszye!0Jwh~F~|!}~ey=egi}cQ(cr_fxorY0&&$n;c92*pOp}*TeP8 zJk;)()Or=>^B9c3Z?KQ&`-NdK%`uDbz@R?-jw(K*!hMtFj$2Oc`eh#Jmzw#-u^f+B zcdm-%=kuy*1KO<1WnD54wdU(qA_(CZWCcl|Pt^e60Hy*(|SU!l3ze;aeI z3C>|9#?3d-+1Ov`a=Fixds^=0e1F`rUQX_5(GKK48)K?zZ2a(AGN)TE>ykP7u8-q~ z{)A(_oXoF3H)MrxzJEx68cIb~hq*oJt1(tbU^be{M&_;X5*ADKU)UN65K zKXR_|`O>Xd=A?GVKl6+8_4z8%WvJDB(o){nc*;D3EsMni2a!$!-QdzH_%lv(c zYRK)kU-lI}-2b`jkJ~5zE06Riyj$QHjlFZ=`s}_3kmsn^2JgKA>h#`GM295l_a5?G zWd8U({<$Qaqu1vNdA$76ueJB?1#7(f?uBcn?ldX4hv9r@f9dCa`Mm7k`{LJ3 z{~jmreVm(ap7{06&%<{DxUb@KME-t&zfQ{h%!78v)=xhByMBI$-MzQU{4ys#U&p<> zKh_*RxB2U&tc&^GwTqv}`+mPJ*#^0eF^^aSy>&RDUN1lMxPG~g$$I5}M(+JE|1AA8 z?k(Sju8TRJ5*s6Kx!k9r%-jF)dBykp`D|cnHrl+(`o45)EI-$Clp~KWmvzaU z@W=AgpKz>~vsTg_F|EH_w>dWcvF=Au9?Q>iS(nU-eN%_Z@OU{{mwv~``*tq+e5l2L??u+d--~d|nbY;lx?~&T`+03_AB(jyBI`k`+O-+6WY{N2`Adyq%A;eyz)oa6EN{d&3Y<+1$!fc{-rH;&1TNK7#wCt);~_ z*JjQ~_OoB^UoW|S!jF1Azn4>v4PH~7Cxtn^y4cTdxg0a{`pP!QoN_$ldtR}edTh(O zy2bMA`QndfS(j{s%qiz1*B`c%`z2Wy*Aur~&PO?S_#HHQ%$zmzG&aX$n!m%#^_d=O zSr?zv-Ey{*b5r~Mxi8zmes(?aYvTYn|1Mb<*C>-8Yns1a#n&a<;IFrG??lbHtk+># zS8i;Mvs|vj@pbuaNVrF%&HCOg$0YYu+~e!_pZ?gBV^ZF`xqoy0a=#(#k~z5#;qwp2 zv^-Ykl=~CzLtMYyFUh*%b8f^nIuTnZp_ zkC#)9C3&pODaR7EJN6Rh4%)Bh6#fNJ)Wvs^^D%#UpW0&aog&K7PF*hNnw(R5Ezz)hleN$2IcJzM=>qGus>?`2cC3C`$YoYxK$9g$yB`rnHJ;>1o_vHmxpLfUB zYLw$xT`udAIk~5G{Rzi)gg)7eF*y|b=Aob0)SYW` z&c@g4=a+fpeT3KA%_H-(|9MS$pOSqg^C#5noxAK4_P?7))+_UKE!7@<&vM6%tXEz~ zu9?E`<&Upd-bds(ly$}3%i?mnZHOPA{{4k(KHFf{TUrs{QB>YNjZC( zIrrt5m-9=`UpeOE$7kG{fbpZ-Dc3GJ=4D+8ZSdxooC|VYieE$gbuxa;`|~TI4d{P8 zzvNty>#D4aYo>cINSH^s7J44ZwOZzsy^jKoN`Vv566=Jy-)wW z%Kp;cv2v_e4n4f@@OLXwj%&1q>zE^-H<{DTL+$$I`yyGF%!z+(80%OCK9}-6iFYiu zn^V3of**6)_51J2WL+|6t)xehKMB`kE!yoLD|5>CUGO8n_VeEAzPpp}*5Y$=ELA}r z$FOGl$I6_%=el|D-Vx{M`u+ErvMzW;PQEAMK7`-F@sE``xhHYoXX^cl_H!K*?>x~) z{mxVGd9;Vm`yAUu9xHQleRK2h*+lrgw#jQM$B)b}$B(?$vflrdU-pT-zOvr<>+AQG z%rEPeIpeRXKPF{Pd3|xLcOCV+8vS{?e14SuFZ)6EC!ZJG=SMxK+|T9xewO)V9@)?G z{lDyI*`G4MeE%=sWhLbC)&boH{T@~3PsoEgt3UJ7b!q>JoYr<~%Pe*8&3&DaS_yxk zUU10}yKC)Z&N~HnI3cwX{y_cn^Y2;v-}urgt$ncysU6`D)C(@&X+6@emwKW{Srt+{ z!XKzVYjAh8*Ik#YWpyg4kUAp#f%^R_H#mQM@src->dRC}Z3%y%ex>(?YJyWqwb}5H z6H?p4AE-;~9&_5=dWD+*cn>F}wuC=WzgO$Hv-#oSs%iIbR!D6Lf1ti*Y`Yas z;T9{TwuL`XpZ4#3Xa9Ytsk?Fp+99$Sg~b>q6*A+;m?fx7oMgYD}sKJFZ8y~YZumGB4ZDhJQE z|2&+j>b%q23aORw2kKuo_qUgy|Cd#v!F^Upt%N^N@2Nh>`e=Ctdt!}2c1Z0Af1s|O z{ectt;(M$52R-eO+7kXi{qS#t)Z7_ATGyX*uN6|;!XK#LA22}W*Z9%0ZhG7ascqp8 z)S^#RNbU9s{efCsQx#IX*OdN1J@>&rcG=!Q|O5BoK%0CKQDUGgn8yl9TlFF>f0vW zVYSVetUkT5m30d32+v9NYoEVvwQVs$)jxki{t>QC-zX#>@)_A^dMt%N^NKk{8=`;N|C)RnJC zt&my?f1uuXUm1H%t?Fv^?DOo9S_yxk9{NW;`>g-YQFrz@%?_!R@CWM07N$qWJ_M>LK&9Ro=w<>bws44s$qd3xA-tuCJ}g`)#V)A$81W{DJzZP8IEsm)mMk zWJ_M>hBKSA3gD` zt@8V~R3UXl_yhG9?K?-#TQ^7@s!>ga)KTFN)cH5vWG&cTsIpsJ?u68i@CWKMJMFOs zl^v%J?&{=()Q<26>N5^MWEHN@Q}0~1$_c3*;SbbvH|@5LKANu@9GMyksU6`D)J>;V za@ut$QKQnXw?b-5_yhI%ufJ+dAJ<=1%{l0V)Q<26>h9$%E4{c_?HfAB38@|757fi1 zOwMs?&Qf14>gj~kyM#YbcU-qBGWgj{wd>LjDx{7If1v*8Ez5eSu#Z}_sJaTN9pMku zUF7Gtlu^ls-DlC?u68i z@CWK6e_Xw5$BpyU%+3=dA$5-M2kI(Uw0SnKXr7u~d~GzO?kxO)8lTtU&wPGvH=inm zo^L!cv#X!(GSKe(dwRsFR6~W-O85iy;)=y~<;BCT*{Kt)kXi|UpkDO-{gImIy=yJ* zI?fKMqrxAk&&wF8UM;iU*;3|7E2OrCKTy{jd%)_@Vw97bnyW%;NB9GENuyQPf>zyC zNuwGdly-zaP#>PzLftpxJ7;{YbM27Y7XCneeBpt}eP5lcnpC{V4ymKUAE>{r)75U! zx0ZUb@2yryt%N^N?`yo*T0gXwQrBgwklGRcK>fjzNaW@hr>o^h^R19ND*St4hV5@SA$3&v19i8fUpkxS<*GZrXy$~}mhcDavQw^hhL>Ee%1-&r38^jN57ZY% zKDEZ$qt)K?PIE$PNB9GE%@gaa;a$dly@dvoMDI5w(tk)5c{&ky&Jx>6#FwD z#|G-6&g-oP-~Jq(a^xBnQai#QsJ~7-o>M(iq8`3?trJr368=D)e|;tEzqymt;YN=; zr_he@oK)Z5zO0&iV1O!D@kS@4wuL`X*V+Aov-+HTRddJr(U95_{$PF4C5}p)sfIQ` zCsHSzwuC=We^XptHT`hui`9onyHbmbWtI-CH#SU#Nd6_8Zs168=DaUWICQK%OBoph18Dl2kQF&+`Q|IgVR*uw0TxYog@5#`k%F1tW^z$DW~uIPDt$tf1vJv zWQBF##C$b+;}R#Nc7#7r*FA5c^YLjNRn^_iRY+|Kf1p0I<@>w#EUlmx6c1G)b&l`{ z>g>iloHnh-sI*B-oRHcQ{y_a*?ha?)&3NBFDt%W-Z3%y%?)lak&X8>*)Q+RyTOqY2 z{DFF4>nhPDvwExJk2J7D>WJ_M>R*d@T2uRUQoF|0vO{V|_ycu?5vA6mTiUBh50$q= zYDf43wew(W8>re3KInwhO85iyhvf#^3pZ3(+tsbnkXi|Upx(KomtCV?eRc4&A0i>O z68=El@j!Vswd!nZ$EN;vNNo#$pzi*~FtzNsV@+riwL)rJ_yhF~Q)W63KX=etQR^x@ zq_%`VP}jY$j-Ax(4y#E$+YYIf@CWLvAKzsa-@U=g=rF<#sU6`D)T>KYTEmK0SqCnh zV29L>@CWLF=MT3>FDocC$){taUB3RYT&Yu6nemedMm1Dx`LVKT!8oGpz&9O|xsfUJwnb9pMku|1|tDvZCWi z`-k(7I3aaZ_ycvzAHR>RY*p1#ug_8;byWBR_0EaEM6MY<)_QyCG!;@ug+EYV`PdZu zzuhaW$&)JY3aORw2kJ5_3hc}i_gcx%H@8A+CH#SU#G>ZbqU&edsjD8y38@|757fnb z*6o`2_6+;KZ?AGf>Kx$@)HnRm#a_L!w*CIww>lxU68=E_*My1os!MiR?Yo?5h15#; z19f`20(9eg?NUelFP`BDQ*=~Q^^vJry4_YC$ z68=E_#ej6{`Ml4qbMtalNbLxJpssTuYDHJ}v3Cym#R;h$;Sbae&fZ`xUh|`MVXYx5 zq;`ZqP~Uy}3hUy%<*b4=#VVwBgg;QZf1pmjrL48$)`)d;(F{AJc7#7r*Lw4Dt5R;ZUDv8>ht!Vn2kNKpc+a|H za82v@w|RC*?FfIM{^gGxYttj`)!6#!c1Z0Af1pk}qn>^GBh8!{OM2QNwG#e7y}5F_ zeW?8)YeP;uJET^^AE*cY*V^tirKA1o^?j|7S_yxko;m1W>(d=w?2&^%wL)q~_ycvH z!e(}*Vd>Vv)+w|lJSWvl9++w0^V@~dHI*;2 zLTV-af%?|`BKyHDcRH(T?}~)fO85hH>*!4T>XN0AgY~;vA+-|zKwWvGmEA4e80 zEX@h2mGB4Z*IH)UPgL$3wI`&hkXi|Upx(R8v68NQIC}i9d=*kV!XK#jZ1~EmaZ|oC z_2Mg4NbLxJpnkenJ^RD1Qyu5u3sgw0gg;P^T-@2NQ#94N`HZIO6j}+-N%f}kORWtB zm#aa;4m+pNj_{mR?^#yW-d)(;o_b)D6H+VT57ceyC)s~JFwoxr>B{s7xcZy z>e2gv1W!wIRC z@CWKo#vQimUVN^a{TP1lFr0RTKTt1UdY$u6wZEO6+lSa8wI%$4dU3TOs`W=@Ro2}T ztdQCk{y=^CgfrBG`z}-;-~OZ%Qrp5GsJFdz#CquG_UhX5)l^9B2!EiibWWPxrCC)q zy8B@#q*lTosGoX2%l`7w!;y~9w^Jdt68=EFdqH>S@THU0;R&}`A+;s^f%>+)$J)O< zFwiR7N;x6568=Da&l~SXzF0U*b^fTK6;elqKTuC>u{ARBxgAcO`%saRnzwGh=$Zk_yhGH_k3dAb8~mKfBP?1NbLxJpni5^U;FoNk2xLN{ON?$O85iy z6))7ZUs#r=HeLR%6H+VT57fV1T*v-&b34^^+$T;*t%N^NXAPZYukKkC$=jLbgw#s- z1NHV=p5S{=44_sg>{t>g0W`?5dMSMz5%BtB_g=f1sYYrt+>eXWbcHpEJ)6 zsdI!sP*;Djk^RAw4>{lMPFEqd68=El{-f#aXR8}dEU+qm)yod4ZQ&2p6EpLo_Ok1v z4Nk}S38y2%AE-C}QNw!Ao};GLdom}ac7#7rKe=U#RWRrpwX^QxNJ#Anf1vJKd!x0v zPO&QYOf)B?c7#7rC)aIn{nTW(vXT$wgw&4k2kNU9J#Njvb+p>_@y%99?FfIMex~jl z)}MJ3RHgf?L_=yv_yhHbM;2M<9+|2@G^BQfKTvm`vM`c9ZJyd#+ICk+9TomS{Y>UF zPW5*6Ro9GkJEXRRKTsdGo7inO_E4*>YLSpy34frz{?D(hrLC@3D=PPOLTX3&19jf} zW$Yn4FHvuH8197BO85hH$Mt8}d(ZE$_SC!93aORw2kK@wt+l?W)K#S}t7)G?JHm5P z{q-^@((mBrNQ0_#RY)Bb{y^QZd6NC)>ax4?@3>Nh)JpgRb>{X-cKyeeJ7+yM&k3oO z@CWLaUJ>@0z}(ksVUo!XK!s|60lZF@J!4S=wMHq*lTosM9Z<