├── src ├── v2 │ ├── broad_phase_interop │ │ ├── mod.rs │ │ └── bvh_arena.rs │ ├── minkowski.rs │ ├── math │ │ ├── glam.rs │ │ ├── array.rs │ │ └── mod.rs │ ├── ray.rs │ ├── transform │ │ └── mod.rs │ ├── mod.rs │ ├── epa.rs │ ├── shapes.rs │ └── gjk.rs ├── v3 │ ├── interop │ │ ├── mod.rs │ │ └── glam_0_24.rs │ ├── range.rs │ ├── point.rs │ ├── aabb.rs │ ├── mod.rs │ └── vector.rs └── lib.rs ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── create-release.yml │ └── check.yml ├── .cargo └── fast_compiles_config ├── release.toml ├── .gitpod.yml ├── UNLICENSE ├── tests ├── v3_collision_spec.rs ├── v3_ray_cast_spec.rs └── collision_spec.rs ├── Cargo.toml ├── justfile ├── CHANGELOG.md ├── README.md ├── examples └── bevy.rs └── Cargo.lock /src/v2/broad_phase_interop/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "bvh-arena")] 2 | mod bvh_arena; 3 | -------------------------------------------------------------------------------- /src/v3/interop/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "unstable-v3-glam-0-24")] 2 | mod glam_0_24; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target 3 | /.cargo/*.toml 4 | 5 | # IDE 6 | /.idea 7 | /.vscode 8 | *.iml 9 | 10 | # Operating systems 11 | .DS_Store 12 | .trashes 13 | *.db 14 | 15 | # NodeJS tools 16 | /node_modules 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create github release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | changelog: CHANGELOG.md 19 | token: ${{ github.token }} 20 | -------------------------------------------------------------------------------- /.cargo/fast_compiles_config: -------------------------------------------------------------------------------- 1 | # Copy the content of this file to `.cargo/config.toml` 2 | 3 | [target.x86_64-unknown-linux-gnu] 4 | linker = "clang" 5 | rustflags = ["-Clink-arg=-fuse-ld=mold"] 6 | 7 | [target.x86_64-apple-darwin] 8 | rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld"] 9 | 10 | [target.aarch64-apple-darwin] 11 | rustflags = ["-C", "link-arg=-fuse-ld=/opt/homebrew/bin/zld"] 12 | 13 | [target.x86_64-pc-windows-msvc] 14 | linker = "rust-lld.exe" 15 | -------------------------------------------------------------------------------- /src/v2/minkowski.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Neg, Sub}; 2 | 3 | use super::Support; 4 | 5 | pub(crate) struct Difference<'a, S1, S2> { 6 | pub(crate) shape1: &'a S1, 7 | pub(crate) shape2: &'a S2, 8 | } 9 | 10 | impl Support for Difference<'_, S1, S2> 11 | where 12 | V: Copy + Sub + Neg + Into, 13 | S1: Support, 14 | S2: Support, 15 | { 16 | fn support(&self, direction: V) -> V { 17 | self.shape1.support(direction) - self.shape2.support(-direction) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | verify: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: dtolnay/rust-toolchain@stable 16 | with: 17 | components: clippy, rustfmt 18 | - uses: Swatinem/rust-cache@v2 19 | - uses: taiki-e/install-action@v2 20 | with: 21 | tool: cargo-hack@0.6,just@1,cargo-msrv@0.16,cargo-deny@0.16 22 | - run: just verify 23 | -------------------------------------------------------------------------------- /src/v3/interop/glam_0_24.rs: -------------------------------------------------------------------------------- 1 | use crate::v3::{Point, Vec2}; 2 | use glam; 3 | 4 | impl From for Vec2 { 5 | fn from(v: glam::Vec2) -> Self { 6 | Vec2::new(v.x, v.y) 7 | } 8 | } 9 | 10 | impl From for glam::Vec2 { 11 | fn from(v: Vec2) -> Self { 12 | glam::Vec2::new(v.x, v.y) 13 | } 14 | } 15 | 16 | impl From for Point { 17 | fn from(v: glam::Vec2) -> Self { 18 | Self::new(v.x, v.y) 19 | } 20 | } 21 | 22 | impl From for glam::Vec2 { 23 | fn from(p: Point) -> Self { 24 | glam::Vec2::new(p.x(), p.y()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/v2/math/glam.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec2; 2 | 3 | use super::{Cross, Dot, Normalize, Perp}; 4 | 5 | impl Dot for Vec2 { 6 | type Output = f32; 7 | fn dot(self, other: Self) -> Self::Output { 8 | Vec2::dot(self, other) 9 | } 10 | } 11 | 12 | impl Cross for Vec2 { 13 | type Output = f32; 14 | fn cross(self, other: Self) -> Self::Output { 15 | Vec2::perp_dot(self, other) 16 | } 17 | } 18 | 19 | impl Perp for Vec2 { 20 | fn perp(self) -> Self { 21 | Vec2::perp(self) 22 | } 23 | } 24 | 25 | impl Normalize for Vec2 { 26 | fn normalize(self) -> Option { 27 | Vec2::try_normalize(self) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["main"] 2 | pre-release-commit-message = "release {{version}}" 3 | 4 | [[pre-release-replacements]] 5 | file = "CHANGELOG.md" 6 | search = "## \\[Unreleased\\]" 7 | replace = "## [Unreleased]\n\n\n## [{{version}}] - {{date}}" 8 | prerelease = true 9 | exactly = 1 10 | 11 | [[pre-release-replacements]] 12 | file = "CHANGELOG.md" 13 | search = "\\.\\.\\.HEAD" 14 | replace = "...v{{version}}" 15 | prerelease = true 16 | exactly = 1 17 | 18 | [[pre-release-replacements]] 19 | file = "CHANGELOG.md" 20 | search = "\\[Unreleased\\]:" 21 | replace = "[Unreleased]: https://github.com/jcornaz/beancount_parser_2/compare/v{{version}}...HEAD\n[{{version}}]:" 22 | prerelease = true 23 | exactly = 1 24 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - before: rustup component add clippy 3 | init: | 4 | cargo update 5 | cargo clippy --all-features --all-targets 6 | cargo doc --all-features --no-deps 7 | cargo test --tests --all-features 8 | command: cargo watch -x 'test --all-features' -x 'clippy --all-targets --all-features' -x 'doc --all-features --no-deps' 9 | 10 | vscode: 11 | extensions: 12 | - belfz.search-crates-io 13 | - matklad.rust-analyzer 14 | - serayuzgur.crates 15 | - bungcip.better-toml 16 | 17 | github: 18 | prebuilds: 19 | master: true 20 | branches: true 21 | pullRequests: true 22 | pullRequestsFromForks: true 23 | addCheck: true 24 | addComment: false 25 | addBadge: false 26 | -------------------------------------------------------------------------------- /src/v2/math/array.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Add, Mul}; 2 | 3 | use super::Dot; 4 | 5 | impl + Mul> Dot for [S; 2] { 6 | type Output = S; 7 | fn dot(self, other: Self) -> Self::Output { 8 | (self[0] * other[0]) + (self[1] * other[1]) 9 | } 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | use approx::assert_ulps_eq; 16 | use rstest::rstest; 17 | 18 | #[rstest] 19 | #[case([0.0, 0.0], [0.0, 0.0], 0.0)] 20 | #[case([1.0, 0.0], [2.0, 0.0], 2.0)] 21 | #[case([2.0, 0.0], [3.0, 0.0], 6.0)] 22 | #[case([2.0, 1.0], [3.0, 1.0], 7.0)] 23 | #[case([2.0, 3.0], [3.0, 4.0], 18.0)] 24 | #[case([2.0, 3.0], [-3.0, 4.0], 6.0)] 25 | fn test_dot(#[case] v1: [f32; 2], #[case] v2: [f32; 2], #[case] expected: f32) { 26 | assert_ulps_eq!(v1.dot(v2), expected); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/v2/ray.rs: -------------------------------------------------------------------------------- 1 | use crate::CollisionShape; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq)] 4 | struct Ray { 5 | origin: V, 6 | vector: V, 7 | } 8 | 9 | impl Ray { 10 | fn new(origin: V, vector: V) -> Self { 11 | Self { origin, vector } 12 | } 13 | 14 | #[allow(clippy::unused_self)] 15 | fn cast(self, _shape: &CollisionShape) -> Option { 16 | None 17 | } 18 | } 19 | 20 | mod tests { 21 | use rstest::rstest; 22 | 23 | use crate::{CollisionShape, Transform}; 24 | 25 | type Vector = [f32; 2]; 26 | type Ray = super::Ray; 27 | 28 | #[rstest] 29 | #[case(Ray::new([0.0, 0.0], [0.0, 0.0]), CollisionShape::new_circle(1.0).with_transform(Transform::from_translation([1.0, 1.0])))] 30 | fn returns_none(#[case] ray: Ray, #[case] shape: CollisionShape) { 31 | assert_eq!(ray.cast(&shape), None); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/v2/broad_phase_interop/bvh_arena.rs: -------------------------------------------------------------------------------- 1 | use bvh_arena::volumes::Aabb; 2 | use glam::Vec2; 3 | 4 | use crate::v2::{CollisionShape, Support}; 5 | 6 | impl From<&CollisionShape> for Aabb<2> { 7 | fn from(shape: &CollisionShape) -> Self { 8 | let min = [ 9 | shape.support(Vec2::new(-1.0, 0.0)).x, 10 | shape.support(Vec2::new(0.0, -1.0)).y, 11 | ]; 12 | let max = [ 13 | shape.support(Vec2::new(1.0, 0.0)).x, 14 | shape.support(Vec2::new(0.0, 1.0)).y, 15 | ]; 16 | Self::from_min_max(min, max) 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | use glam::Vec2; 24 | 25 | #[test] 26 | fn from_rectangle() { 27 | let expected = Aabb::from_min_max(Vec2::new(-0.5, -1.0), Vec2::new(0.5, 1.0)); 28 | let actual = Aabb::from(&CollisionShape::new_rectangle(1.0, 2.0)); 29 | assert_eq!(expected, actual); 30 | } 31 | 32 | #[test] 33 | fn from_circle() { 34 | let expected = Aabb::from_min_max(Vec2::new(-1.0, -1.0), Vec2::new(1.0, 1.0)); 35 | let actual = Aabb::from(&CollisionShape::new_circle(1.0)); 36 | assert_eq!(expected, actual); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /tests/v3_collision_spec.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "unstable-v3-aabb")] 2 | 3 | use rstest::rstest; 4 | 5 | use impacted::v3::{Aabb, Collides, Point, Shape, Vec2}; 6 | 7 | #[rstest] 8 | #[case( 9 | Aabb::from_size(Vec2::new(2.0, 2.0)), 10 | Aabb::from_size(Vec2::new(2.0, 2.0)) 11 | )] 12 | #[case( 13 | Aabb::from_size(Vec2::new(2.0, 2.0)), 14 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 0.0)) 15 | )] 16 | #[case( 17 | Aabb::from_size(Vec2::new(2.0, 2.0)), 18 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 1.9)) 19 | )] 20 | #[case(Point::ORIGIN, Aabb::from_size(Vec2::new(2.0, 2.0)))] 21 | #[case( 22 | Point::new(10.9, 9.1), 23 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(10.0, 10.0)) 24 | )] 25 | fn test_collides(#[case] shape1: impl Shape, #[case] shape2: impl Shape) { 26 | assert!(shape1.collides(&shape2)); 27 | } 28 | 29 | #[rstest] 30 | #[case( 31 | Aabb::from_size(Vec2::new(2.0, 2.0)), 32 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(2.1, 0.0)) 33 | )] 34 | #[case( 35 | Aabb::from_size(Vec2::new(2.0, 2.0)), 36 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 2.1)) 37 | )] 38 | #[case( 39 | Point::new(12.1, 9.1), 40 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(10.0, 10.0)) 41 | )] 42 | fn test_not_collides(#[case] shape1: impl Shape, #[case] shape2: impl Shape) { 43 | assert!(!shape1.collides(&shape2)); 44 | } 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "impacted" 3 | version = "2.0.3" 4 | license = "Unlicense" 5 | authors = ["Jonathan Cornaz"] 6 | edition = "2021" 7 | rust-version = "1.68.2" 8 | description = "2d collision test for arbitrary convex shapes" 9 | repository = "https://github.com/jcornaz/impacted" 10 | categories = ["game-development"] 11 | keywords = ["collision", "2d", "geometry", "gamedev", "gjk"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [features] 17 | default = ["std"] 18 | std = ["glam/std", "bvh-arena?/std"] 19 | unstable-v3 = [] 20 | unstable-v3-aabb = ["unstable-v3", "libm"] 21 | unstable-v3-glam-0-24 = [] 22 | glam-0-24 = [] # Deprecated 23 | 24 | [build-dependencies] 25 | rustc_version = "0.4" 26 | 27 | [dependencies] 28 | # Public 29 | bvh-arena = { version = "1.1", default-features = false, optional = true } 30 | 31 | # Private 32 | glam = { version = "0.29", default-features = false, features = ["libm"] } 33 | sealed = { version = "0.6", default-features = false } 34 | smallvec = { version = "1.9", default-features = false } 35 | libm = { version = "0.2", default-features = false, optional = true } 36 | 37 | [dev-dependencies] 38 | rstest = { version = "0.26.1", default-features = false } 39 | bevy = { version = "0.12.1", default-features = false, features = ["bevy_render", "bevy_winit", "bevy_core_pipeline", "bevy_sprite", "bevy_asset", "x11"]} 40 | glam = { version = "0.29.0", features = ["libm", "approx"] } 41 | approx = "0.5.1" 42 | 43 | [lints.rust] 44 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(nightly)'] } 45 | 46 | [[example]] 47 | name = "bevy" 48 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | @_choose: 4 | just --choose --unsorted 5 | 6 | # Perform all verifications (compile, test, lint, etc.) 7 | verify: test lint doc 8 | cargo msrv verify 9 | 10 | # Watch the source files and run `just verify` when source changes 11 | watch: 12 | cargo watch --delay 0.1 --clear --why -- just verify 13 | 14 | # Run the bevy example 15 | run-bevy-example: 16 | cargo run --example bevy 17 | 18 | # Run the tests 19 | test: 20 | cargo hack check --feature-powerset 21 | cargo hack test --tests --each-feature 22 | cargo test --examples --all-features 23 | cargo test --doc --all-features 24 | 25 | # Run the static code analysis 26 | lint: 27 | cargo fmt -- --check 28 | cargo hack clippy --each-feature --all-targets 29 | 30 | # Build the documentation 31 | doc *args: 32 | cargo doc --all-features --no-deps {{args}} 33 | 34 | # Open the documentation page 35 | doc-open: (doc "--open") 36 | 37 | # Clean up compilation output 38 | clean: 39 | rm -rf target 40 | rm -f Cargo.lock 41 | rm -rf node_modules 42 | 43 | # Install cargo dev-tools used by the `verify` recipe (requires rustup to be already installed) 44 | install-dev-tools: 45 | rustup install stable 46 | rustup override set stable 47 | cargo install cargo-hack cargo-watch cargo-msrv 48 | 49 | # Install a git hook to run tests before every commits 50 | install-git-hooks: 51 | echo '#!/usr/bin/env sh' > .git/hooks/pre-push 52 | echo 'just verify' >> .git/hooks/pre-push 53 | chmod +x .git/hooks/pre-push 54 | 55 | release *args: verify 56 | test $GITHUB_TOKEN 57 | test $CARGO_REGISTRY_TOKEN 58 | cargo release {{args}} 59 | -------------------------------------------------------------------------------- /src/v2/math/mod.rs: -------------------------------------------------------------------------------- 1 | mod array; 2 | mod glam; 3 | 4 | pub(crate) trait Dot { 5 | type Output; 6 | fn dot(self, other: Self) -> Self::Output; 7 | } 8 | 9 | pub(crate) trait Cross { 10 | type Output; 11 | fn cross(self, other: Self) -> Self::Output; 12 | } 13 | 14 | pub(crate) trait Perp { 15 | /// Rotate the vector by 90 degrees 16 | /// 17 | /// The rotation direction should be so that the perpendicular of the x-axis is the y-axis. 18 | fn perp(self) -> Self; 19 | } 20 | 21 | pub(crate) trait Normalize: Sized { 22 | fn normalize(self) -> Option; 23 | } 24 | 25 | pub(crate) trait CmpToZero: Copy { 26 | fn is_negative(self) -> bool; 27 | fn is_zero(self) -> bool; 28 | fn is_positive(self) -> bool; 29 | } 30 | 31 | pub(crate) trait MagnitudeSquared { 32 | type Scalar; 33 | fn magnitude_squared(self) -> Self::Scalar; 34 | } 35 | 36 | impl MagnitudeSquared for V { 37 | type Scalar = ::Output; 38 | fn magnitude_squared(self) -> Self::Scalar { 39 | self.dot(self) 40 | } 41 | } 42 | 43 | impl CmpToZero for f32 { 44 | fn is_negative(self) -> bool { 45 | self < 0.0 46 | } 47 | 48 | fn is_zero(self) -> bool { 49 | self == 0.0 50 | } 51 | 52 | fn is_positive(self) -> bool { 53 | self > 0.0 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | use approx::assert_ulps_eq; 61 | use rstest::rstest; 62 | 63 | #[rstest] 64 | #[case([0.0, 0.0], 0.0)] 65 | #[case([2.0, 3.0], 13.0)] 66 | #[case([3.0, 4.0], 25.0)] 67 | #[case([4.0, 3.0], 25.0)] 68 | fn test_magnitude_squared(#[case] vector: [f32; 2], #[case] expected: f32) { 69 | assert_ulps_eq!(vector.magnitude_squared(), expected); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/v3/range.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone, PartialEq)] 2 | pub struct Range { 3 | pub(super) min: f32, 4 | pub(super) max: f32, 5 | } 6 | 7 | impl Range { 8 | pub(super) fn from_min_max(min: f32, max: f32) -> Self { 9 | debug_assert!(min <= max); 10 | Self { min, max } 11 | } 12 | 13 | pub(super) fn overlaps(self, other: Range) -> bool { 14 | self.min <= other.max && self.max >= other.min 15 | } 16 | 17 | #[cfg(test)] 18 | pub(super) fn contains(self, point: f32) -> bool { 19 | point >= self.min && point <= self.max 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | impl approx::AbsDiffEq for Range { 25 | type Epsilon = f32; 26 | fn default_epsilon() -> Self::Epsilon { 27 | f32::default_epsilon() 28 | } 29 | fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 30 | self.min.abs_diff_eq(&other.min, epsilon) && self.max.abs_diff_eq(&other.max, epsilon) 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | use rstest::rstest; 38 | 39 | #[rstest] 40 | #[case((0.0, 1.0), (0.5, 1.5))] 41 | #[case((0.0, 1.0), (1.0, 1.5))] 42 | #[case((0.0, 1.0), (-1.0, 0.5))] 43 | #[case((0.0, 1.0), (-1.0, 0.0))] 44 | #[case((0.0, 1.0), (0.1, 0.9))] 45 | #[case((0.0, 1.0), (-1.0, 2.0))] 46 | #[case((0.0, 0.0), (0.0, 0.0))] 47 | fn range_should_overlap(#[case] r1: (f32, f32), #[case] r2: (f32, f32)) { 48 | let r1 = Range::from_min_max(r1.0, r1.1); 49 | let r2 = Range::from_min_max(r2.0, r2.1); 50 | assert!(r1.overlaps(r2), "{r1:?} does not overlap {r2:?}"); 51 | } 52 | 53 | #[rstest] 54 | #[case((0.0, 1.0), (1.1, 1.5))] 55 | #[case((2.0, 3.0), (0.0, 1.0))] 56 | #[case((2.0, 3.0), (0.0, 1.0))] 57 | fn range_should_not_overlap(#[case] r1: (f32, f32), #[case] r2: (f32, f32)) { 58 | let r1 = Range::from_min_max(r1.0, r1.1); 59 | let r2 = Range::from_min_max(r2.0, r2.1); 60 | assert!(!r1.overlaps(r2), "{r1:?} overlaps {r2:?}"); 61 | } 62 | 63 | #[rstest] 64 | #[case((0.0, 1.0), 0.0)] 65 | #[case((0.0, 1.0), 1.0)] 66 | #[case((0.0, 1.0), 0.5)] 67 | fn range_should_contain(#[case] range: (f32, f32), #[case] point: f32) { 68 | assert!(Range::from_min_max(range.0, range.1).contains(point)); 69 | } 70 | 71 | #[rstest] 72 | #[case((0.0, 1.0), -1.0)] 73 | #[case((0.0, 1.0), 2.0)] 74 | fn range_should_not_contain(#[case] range: (f32, f32), #[case] point: f32) { 75 | assert!(!Range::from_min_max(range.0, range.1).contains(point)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | 8 | ## [Unreleased] 9 | 10 | ### Dependencies 11 | 12 | * Bump rust MSRV to 1.68.2 13 | * Update 'sealed' to version `0.6.0` 14 | * Releax minimum version of dependencies: 15 | * `bvh-arena`: `^1.1` (instead of `^1.1.3`) 16 | * `smallvec`: `^1.9` (instead of `^1.13.2`) 17 | * `libm`: `^0.2` (instead of `^0.2.8`) 18 | 19 | 20 | ## [2.0.3] - 2024-09-02 21 | 22 | ### Dependencies 23 | 24 | * Update glam to `0.29` 25 | 26 | 27 | ## [2.0.2] - 2024-06-12 28 | 29 | ### Breaking changes to `unstable-v3` 30 | 31 | * Change `Shape` trait methods to return `impl Iterator` 32 | * Rename feature `glam-0-24` to `unstable-glam-0-24` 33 | 34 | ### Dependencies 35 | 36 | * Update glam to 0.28 37 | * Bump rust MSRV to 1.68 38 | 39 | 40 | ## [2.0.1] - 2023-11-01 41 | 42 | ### Added 43 | 44 | * `unstable-v3` feature flag and related `v3` module: an exploration of how could look like the next major version 45 | * `unstable-v3-aabb` Axis-Aligned-Bounding-Box shape for the v3 module 46 | 47 | 48 | ## [2.0.0] - 2023-08-03 49 | 50 | ### Breaking changes 51 | 52 | * Removed all bevy interoperability features. 53 | Note that this crate, despite being now engine agnostic, is still usable from bevy, and an example of usage from bevy is still provided. 54 | * Removed deprecated and unused `Error` type 55 | 56 | 57 | ### Bug fixes 58 | 59 | * Fix panic of contact generation that could occur if a circle was tested against its rotated version 60 | 61 | 62 | ### Dependencies 63 | 64 | * Update glam to 0.23 65 | 66 | 67 | ### Documentation 68 | 69 | * List feature flags in API documentation 70 | 71 | 72 | ## [1.5.3] - 2023-01-01 73 | 74 | 75 | ### Documentation 76 | 77 | * lower minimum supported rust version (msrv) to 1.60 ([259b7a5](https://github.com/jcornaz/impacted/commit/259b7a57ee36a602d12eb86e083d2a2df6897649)) 78 | * **readme:** fix build badge ([1f91bf8](https://github.com/jcornaz/impacted/commit/1f91bf88ee4a57eddc4a1ed4b47fc5ffea04e85d)) 79 | 80 | 81 | [Unreleased]: https://github.com/jcornaz/beancount_parser_2/compare/v2.0.3...HEAD 82 | [2.0.3]: https://github.com/jcornaz/beancount_parser_2/compare/v2.0.2...v2.0.3 83 | [2.0.2]: https://github.com/jcornaz/beancount_parser_2/compare/v2.0.1...v2.0.2 84 | [2.0.1]: https://github.com/jcornaz/beancount_parser_2/compare/v2.0.0...v2.0.1 85 | [2.0.0]: https://github.com/jcornaz/beancount_parser_2/compare/v1.5.3...v2.0.0 86 | [1.5.4]: https://github.com/jcornaz/impacted/compare/v1.5.3...v1.5.4 87 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(future_incompatible)] 2 | #![warn(nonstandard_style, rust_2018_idioms, missing_docs, clippy::pedantic)] 3 | #![deny(unsafe_code)] 4 | #![allow(clippy::wildcard_imports)] 5 | #![cfg_attr(not(feature = "std"), no_std)] 6 | #![cfg_attr(nightly, feature(doc_auto_cfg))] 7 | 8 | //! 2d collision test for game-development in rust 9 | //! 10 | //! This provides a low-level "narrow-phase" collision-detection logic. 11 | //! 12 | //! If you want to pair it with a broad-phase, you may look at [bvh-arena] or [broccoli]. 13 | //! 14 | //! [bvh-arena]: https://github.com/jcornaz/bvh-arena 15 | //! [broccoli]: https://github.com/tiby312/broccoli 16 | //! 17 | //! # Usage 18 | //! 19 | //! The central type is [`CollisionShape`]. Once a collision shape is created and positioned (with a [`Transform`]) 20 | //! is is possible to call [`CollisionShape::is_collided_with`] to test for collision with another shape. 21 | //! 22 | //! ``` 23 | //! # use approx::assert_ulps_eq; 24 | //! use impacted::{CollisionShape, Transform, Contact}; 25 | //! 26 | //! // The examples of this crate use glam. 27 | //! // But you may use another math library instead. 28 | //! use glam::Vec2; 29 | //! 30 | //! // Create a circle 31 | //! let circle = CollisionShape::new_circle(1.0); 32 | //! 33 | //! // Create a rectangle 34 | //! let mut rect1 = CollisionShape::new_rectangle(4.0, 4.0) 35 | //! .with_transform(Transform::from_translation(Vec2::new(2.0, 0.0))); 36 | //! 37 | //! // Create another rectangle 38 | //! let mut rect2 = rect1.clone() 39 | //! .with_transform(Transform::from_translation(Vec2::new(0.0, 4.0))); 40 | //! 41 | //! // Then we can test for collision 42 | //! assert!(circle.is_collided_with(&rect1)); 43 | //! assert!(!circle.is_collided_with(&rect2)); 44 | //! 45 | //! // And generate contact data 46 | //! // (It returns `None` if there is no contact) 47 | //! let contact = circle.contact_with(&rect1).unwrap(); 48 | //! let normal: Vec2 = contact.normal.into(); 49 | //! assert_ulps_eq!(normal, -Vec2::X); 50 | //! assert_ulps_eq!(contact.penetration, 1.0); 51 | //! ``` 52 | //! 53 | //! ## Feature flags 54 | //! 55 | //! * `std` (enabled by default) Allow to use rust the standard library (need to be disabled for `no_std` apps) 56 | //! * `bvh-arena` Integration with [bvh-arena](https://crates.io/crates/bvh-arena) bounding volumes 57 | //! 58 | //! 59 | //! ## Unstable feature flags 60 | //! 61 | //! **The following features may receive breaking changes or be removed in a patch release!** 62 | //! 63 | //! * `unstable-v3` `v3` module, an exploration of what could be the next major version of the API 64 | //! * `unstable-v3-aabb` Axis-Aligned-Bounding-Box shape for the v3 module 65 | //! 66 | 67 | #[cfg(all(test, feature = "unstable-v3-aabb"))] 68 | extern crate alloc; 69 | 70 | mod v2; 71 | #[cfg(feature = "unstable-v3")] 72 | pub mod v3; 73 | 74 | pub use v2::*; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # impacted 2 | 3 | [![License](https://img.shields.io/crates/l/impacted)](#Unlicense) 4 | [![Crates.io](https://img.shields.io/crates/v/impacted)](https://crates.io/crates/impacted) 5 | ![rustc](https://img.shields.io/badge/rustc-1.68+-blue?logo=rust) 6 | [![Docs](https://docs.rs/impacted/badge.svg)](https://docs.rs/impacted) 7 | ![Maintenance](https://img.shields.io/maintenance/passively/2025) 8 | 9 | 2d collision test for game-development in rust 10 | 11 | This provides a low-level "narrow-phase" collision-detection logic. 12 | 13 | If you want to pair it with a broad phrase, you may look at [bvh-arena] or [broccoli]. 14 | 15 | [bvh-arena]: https://github.com/jcornaz/bvh-arena 16 | [broccoli]: https://github.com/tiby312/broccoli 17 | 18 | > [!NOTE] 19 | > 20 | > If you only need simple shapes (i.e. axis-aligned bounding boxes) you may prefer to look at this simpler 2d collision library I'm working on: [collision2d](https://github.com/jcornaz/collision2d) 21 | 22 | ## Usage example 23 | 24 | ```rust 25 | use impacted::{CollisionShape, Transform}; 26 | use glam::Vec2; // <-- use any math library you like 27 | 28 | // Create a circle 29 | let circle = CollisionShape::new_circle(1.0); 30 | 31 | // Create a rectangle 32 | let mut rect = CollisionShape::new_rectangle(4.0, 4.0) 33 | .with_transform(Transform::from_translation(Vec2::new(2.0, 0.0))); 34 | 35 | // Test for collision 36 | assert!(circle.is_collided_with(&rect)); 37 | ``` 38 | 39 | You may also look in the [examples](https://github.com/jcornaz/impacted/tree/main/examples) directory 40 | for more complete/concrete usage examples. 41 | 42 | 43 | ## Installation 44 | 45 | ```sh 46 | cargo add impacted 47 | ``` 48 | 49 | ## Cargo features 50 | 51 | * `std` (enabled by default) Allow to use rust the standard library (need to be disabled for `no_std` apps) 52 | * `bvh-arena` Integration with [bvh-arena](https://crates.io/crates/bvh-arena) bounding volumes 53 | 54 | ## Unstable feature flags 55 | 56 | **The following features may receive breaking changes or be removed in a patch release!** 57 | 58 | * `unstable-v3`: enable the `v3` module containing an exploration of what could be the next major version of the API 59 | * `unstable-v3-aabb`: Axis-Aligned-Bounding-Box shape for the `v3` module 60 | 61 | 62 | 63 | ## MSRV 64 | 65 | The minimum supported rust version is currently: `1.68` 66 | 67 | Bumping the minimum supported rust version to a newer stable version 68 | is not considered a breaking change. 69 | 70 | 71 | ## Unlicense 72 | 73 | This is free and unencumbered software released into the public domain. 74 | 75 | Anyone is free to copy, modify, publish, use, compile, sell, or 76 | distribute this software, either in source code form or as a compiled 77 | binary, for any purpose, commercial or non-commercial, and by any 78 | means. 79 | 80 | In jurisdictions that recognize copyright laws, the author or authors 81 | of this software dedicate any and all copyright interest in the 82 | software to the public domain. We make this dedication for the benefit 83 | of the public at large and to the detriment of our heirs and 84 | successors. We intend this dedication to be an overt act of 85 | relinquishment in perpetuity of all present and future rights to this 86 | software under copyright law. 87 | 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 91 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 92 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 93 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 94 | OTHER DEALINGS IN THE SOFTWARE. 95 | 96 | For more information, please refer to 97 | -------------------------------------------------------------------------------- /src/v3/point.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | iter, 3 | ops::{Add, AddAssign, Sub, SubAssign}, 4 | }; 5 | 6 | use sealed::sealed; 7 | 8 | use super::{vector::Vec2, Range, Shape, __seal_shape}; 9 | 10 | #[derive(Debug, Copy, Clone, PartialEq)] 11 | pub struct Point(Vec2); 12 | 13 | impl Point { 14 | pub const ORIGIN: Self = Self(Vec2::ZERO); 15 | 16 | #[must_use] 17 | pub fn new(x: f32, y: f32) -> Self { 18 | Self(Vec2::new(x, y)) 19 | } 20 | 21 | #[must_use] 22 | pub fn x(self) -> f32 { 23 | self.0.x 24 | } 25 | 26 | #[must_use] 27 | pub fn y(self) -> f32 { 28 | self.0.y 29 | } 30 | } 31 | 32 | impl From<[f32; 2]> for Point { 33 | fn from([x, y]: [f32; 2]) -> Self { 34 | Self::new(x, y) 35 | } 36 | } 37 | 38 | impl From for [f32; 2] { 39 | fn from(Point(v): Point) -> Self { 40 | v.into() 41 | } 42 | } 43 | 44 | impl From<(f32, f32)> for Point { 45 | fn from((x, y): (f32, f32)) -> Self { 46 | Point::new(x, y) 47 | } 48 | } 49 | 50 | impl From for (f32, f32) { 51 | fn from(Point(v): Point) -> Self { 52 | v.into() 53 | } 54 | } 55 | 56 | impl From for Point { 57 | fn from(value: Vec2) -> Self { 58 | Self(value) 59 | } 60 | } 61 | 62 | impl From for Vec2 { 63 | fn from(Point(v): Point) -> Self { 64 | v 65 | } 66 | } 67 | 68 | #[sealed] 69 | impl Shape for Point { 70 | fn axes(&self) -> impl Iterator { 71 | iter::empty() 72 | } 73 | 74 | fn focals(&self) -> impl Iterator { 75 | iter::empty() 76 | } 77 | 78 | fn vertices(&self) -> impl Iterator { 79 | iter::once(*self) 80 | } 81 | 82 | fn project_on(&self, axis: Vec2) -> Range { 83 | let p = self.0.dot(axis); 84 | Range::from_min_max(p, p) 85 | } 86 | } 87 | 88 | impl AddAssign for Point { 89 | fn add_assign(&mut self, rhs: Vec2) { 90 | self.0 += rhs; 91 | } 92 | } 93 | 94 | impl Add for Point { 95 | type Output = Self; 96 | fn add(mut self, rhs: Vec2) -> Self::Output { 97 | self += rhs; 98 | self 99 | } 100 | } 101 | 102 | impl SubAssign for Point { 103 | fn sub_assign(&mut self, rhs: Vec2) { 104 | self.0 -= rhs; 105 | } 106 | } 107 | 108 | impl Sub for Point { 109 | type Output = Self; 110 | fn sub(mut self, rhs: Vec2) -> Self::Output { 111 | self -= rhs; 112 | self 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | impl approx::AbsDiffEq for Point { 118 | type Epsilon = f32; 119 | fn default_epsilon() -> Self::Epsilon { 120 | f32::default_epsilon() 121 | } 122 | fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 123 | self.0.abs_diff_eq(&other.0, epsilon) 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use approx::assert_abs_diff_eq; 130 | use rstest::rstest; 131 | 132 | use super::*; 133 | 134 | #[rstest] 135 | #[case(Vec2::ZERO, Vec2::ZERO, 0.0)] 136 | #[case(Vec2::ZERO, Vec2::X, 0.0)] 137 | #[case(Vec2::X, Vec2::X, 1.0)] 138 | #[case(Vec2::X, Vec2::Y, 0.0)] 139 | #[case(Vec2::Y, Vec2::Y, 1.0)] 140 | #[case(Vec2::Y, Vec2::X, 0.0)] 141 | #[case(Vec2::new(3.0, 4.0), Vec2::X, 3.0)] 142 | #[case(Vec2::new(3.0, 4.0), Vec2::Y, 4.0)] 143 | #[case(Vec2::new(3.0, 4.0),Vec2::new(3.0/5.0, 4.0/5.0),5.0)] 144 | #[case(Vec2::new(3.0, 3.0),Vec2::new(2f32.sqrt(), -(2f32.sqrt())),0.0)] 145 | fn test_axis_projection( 146 | #[case] point: impl Into, 147 | #[case] axis: Vec2, 148 | #[case] expected: f32, 149 | ) { 150 | let expected = Range::from_min_max(expected, expected); 151 | assert_abs_diff_eq!(point.into().project_on(axis), expected); 152 | } 153 | 154 | #[test] 155 | fn test_sat_axes() { 156 | assert_eq!(Point::from(Vec2::ZERO).axes().next(), None); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/v3_ray_cast_spec.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "unstable-v3-aabb")] 2 | 3 | use approx::assert_abs_diff_eq; 4 | use impacted::v3::{ray_cast, Aabb, Point, Shape, Vec2}; 5 | use rstest::rstest; 6 | 7 | #[rstest] 8 | #[case( 9 | Vec2::ZERO, 10 | Vec2::X, 11 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 0.0)), 12 | Vec2::new(0.9, 0.0), 13 | )] 14 | #[case( 15 | Vec2::ZERO, 16 | -Vec2::X, 17 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(-1.9, 0.0)), 18 | Vec2::new(-0.9, 0.0) 19 | )] 20 | #[case( 21 | Vec2::X, 22 | Vec2::X, 23 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(2.9, 0.0)), 24 | Vec2::new(1.9, 0.0) 25 | )] 26 | #[case( 27 | Vec2::ZERO, 28 | Vec2::X * 2.0, 29 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(2.9, 0.0)), 30 | Vec2::new(1.9, 0.0) 31 | )] 32 | #[case( 33 | Vec2::ZERO, 34 | Vec2::Y, 35 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 1.9)), 36 | Vec2::new(0.0, 0.9) 37 | )] 38 | #[case( 39 | Vec2::Y, 40 | Vec2::Y, 41 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 2.9)), 42 | Vec2::new(0.0, 1.9) 43 | )] 44 | #[case( 45 | Vec2::ZERO, 46 | Vec2::Y * 2.0, 47 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 2.9)), 48 | Vec2::new(0.0, 1.9) 49 | )] 50 | #[case( 51 | Vec2::ZERO, 52 | Vec2::new(1.0, 1.0), 53 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 1.9)), 54 | Vec2::new(0.9, 0.9), 55 | )] 56 | #[case( 57 | Vec2::ZERO, 58 | Vec2::new(1.0, 1.0), 59 | Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.5, 1.9)), 60 | Vec2::new(0.9, 0.9), 61 | )] 62 | fn should_find_contact_time( 63 | #[case] origin: impl Into, 64 | #[case] vector: Vec2, 65 | #[case] target: impl Shape, 66 | #[case] expected_point: impl Into, 67 | ) { 68 | let origin = origin.into(); 69 | let expected_point = expected_point.into(); 70 | let time = ray_cast(origin, vector, &target).unwrap().time; 71 | let point = origin + (vector * time); 72 | assert_abs_diff_eq!(point.x(), expected_point.x()); 73 | assert_abs_diff_eq!(point.y(), expected_point.y()); 74 | } 75 | 76 | #[rstest] 77 | #[case(Point::ORIGIN, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 0.0)), -Vec2::X)] 78 | #[case(Point::ORIGIN, -Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(-1.9, 0.0)), Vec2::X)] 79 | #[case(Point::ORIGIN, Vec2::Y, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, 1.9)), -Vec2::Y)] 80 | #[case(Point::ORIGIN, -Vec2::Y, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(0.0, -1.9)), Vec2::Y)] 81 | #[case(Point::ORIGIN, Vec2::new(1.0, 1.0), Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 0.0)), -Vec2::X)] 82 | fn should_find_contact_normal( 83 | #[case] origin: impl Into, 84 | #[case] vector: Vec2, 85 | #[case] target: impl Shape, 86 | #[case] expected_normal: Vec2, 87 | ) { 88 | let origin = origin.into(); 89 | let normal = ray_cast(origin, vector, &target).unwrap().normal; 90 | assert_abs_diff_eq!(normal.x(), expected_normal.x()); 91 | assert_abs_diff_eq!(normal.y(), expected_normal.y()); 92 | } 93 | 94 | #[rstest] 95 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(2.1, 0.0)))] 96 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(-2.1, 0.0)))] 97 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::ORIGIN))] 98 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(1.0, 1.0)).with_center_at(Point::ORIGIN))] 99 | #[case(-Vec2::X, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.1, 0.0)))] 100 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, 5.0)))] 101 | #[case(Vec2::ZERO, Vec2::X, Aabb::from_size(Vec2::new(2.0, 2.0)).with_center_at(Point::new(1.9, -5.0)))] 102 | fn should_return_none_when_there_is_no_hit( 103 | #[case] origin: impl Into, 104 | #[case] vector: Vec2, 105 | #[case] target: impl Shape, 106 | ) { 107 | let result = ray_cast(origin.into(), vector, &target); 108 | assert_eq!(result, None); 109 | } 110 | -------------------------------------------------------------------------------- /src/v2/transform/mod.rs: -------------------------------------------------------------------------------- 1 | use glam::{Affine2, Mat2, Vec2}; 2 | 3 | use super::{CollisionShape, Support}; 4 | 5 | /// Transform that can be used for a [`CollisionShape`] 6 | #[derive(Debug, Clone)] 7 | pub struct Transform { 8 | local_to_world: Affine2, 9 | world_to_local: Mat2, 10 | } 11 | 12 | impl Transform { 13 | pub(crate) fn new(local_to_world: Affine2) -> Self { 14 | let world_to_local = local_to_world.matrix2.inverse(); 15 | Self { 16 | local_to_world, 17 | world_to_local, 18 | } 19 | } 20 | 21 | /// Create a translation transform 22 | /// 23 | /// # Panics 24 | /// 25 | /// Panic if the translation is not finite 26 | /// 27 | /// # Example with glam 28 | /// 29 | /// ```rust 30 | /// use impacted::Transform; 31 | /// use glam::Vec2; 32 | /// let translation = Transform::from_translation(Vec2::new(1.0, 2.0)); 33 | /// ``` 34 | #[inline] 35 | #[must_use] 36 | pub fn from_translation(translation: impl Into<[f32; 2]>) -> Self { 37 | Self::new(Affine2::from_translation(translation.into().into())) 38 | } 39 | 40 | /// Create a translation and rotation transform 41 | /// 42 | /// # Panics 43 | /// 44 | /// Panic if the translation or angle is not finite 45 | /// 46 | /// # Example with glam 47 | /// 48 | /// ```rust 49 | /// use impacted::Transform; 50 | /// use core::f32::consts; 51 | /// use glam::Vec2; 52 | /// let translation = Transform::from_angle_translation(consts::FRAC_PI_4, Vec2::new(1.0, 2.0)); 53 | /// ``` 54 | #[inline] 55 | #[must_use] 56 | pub fn from_angle_translation(angle: f32, translation: impl Into<[f32; 2]>) -> Self { 57 | Self::new(Affine2::from_angle_translation( 58 | angle, 59 | translation.into().into(), 60 | )) 61 | } 62 | 63 | /// Create a translation, rotation and scale transform 64 | /// 65 | /// The scale must not be zero 66 | /// 67 | /// # Panics 68 | /// 69 | /// Panic if a component of the scale is zero, or if the translation or angle is not finite 70 | /// 71 | /// # Example with glam 72 | /// 73 | /// ```rust 74 | /// use impacted::Transform; 75 | /// use core::f32::consts; 76 | /// use glam::Vec2; 77 | /// let translation = Transform::from_scale_angle_translation( 78 | /// Vec2::splat(2.0), 79 | /// consts::FRAC_PI_4, 80 | /// Vec2::new(1.0, 2.0) 81 | /// ); 82 | /// ``` 83 | #[inline] 84 | #[must_use] 85 | pub fn from_scale_angle_translation( 86 | scale: impl Into<[f32; 2]>, 87 | angle: f32, 88 | translation: impl Into<[f32; 2]>, 89 | ) -> Self { 90 | Self::new(Affine2::from_scale_angle_translation( 91 | scale.into().into(), 92 | angle, 93 | translation.into().into(), 94 | )) 95 | } 96 | 97 | pub(crate) fn position(&self) -> Vec2 { 98 | self.local_to_world.translation 99 | } 100 | } 101 | 102 | impl Default for Transform { 103 | /// The default transform is the identity transform 104 | #[inline] 105 | fn default() -> Self { 106 | Self { 107 | local_to_world: Affine2::IDENTITY, 108 | world_to_local: Mat2::IDENTITY, 109 | } 110 | } 111 | } 112 | 113 | impl Support for CollisionShape { 114 | fn support(&self, direction: Vec2) -> Vec2 { 115 | let local_direction = self.transform.world_to_local * direction; 116 | let local_support = self.data.support(local_direction); 117 | self.transform 118 | .local_to_world 119 | .transform_point2(local_support) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use core::f32::consts; 126 | 127 | use approx::assert_ulps_eq; 128 | use glam::Vec2; 129 | 130 | use super::*; 131 | 132 | #[test] 133 | fn transformed_shape_support() { 134 | let transform: Transform = Transform::from_scale_angle_translation( 135 | Vec2::splat(2.0), // sized doubled 136 | consts::FRAC_PI_4, // rotated by 45° 137 | Vec2::new(1., 2.), // translated 138 | ); 139 | 140 | let support_point = CollisionShape::new_rectangle(2.0, 2.0) 141 | .with_transform(transform) 142 | .support(Vec2::X); 143 | assert!((3.5..4.0).contains(&support_point.x)); 144 | assert_ulps_eq!(2.0, support_point.y); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/v2/mod.rs: -------------------------------------------------------------------------------- 1 | mod broad_phase_interop; 2 | mod epa; 3 | mod gjk; 4 | mod math; 5 | mod minkowski; 6 | #[cfg(test)] 7 | mod ray; 8 | pub mod shapes; 9 | mod transform; 10 | 11 | use shapes::ShapeData; 12 | pub use transform::Transform; 13 | 14 | /// A collision shape 15 | /// 16 | /// This is the entry point for collision detection. 17 | /// 18 | /// See [crate](crate) level documentation for more info and examples. 19 | #[derive(Debug, Clone)] 20 | pub struct CollisionShape { 21 | transform: Transform, 22 | data: ShapeData, 23 | } 24 | 25 | impl> From for CollisionShape { 26 | fn from(shape: S) -> Self { 27 | Self { 28 | transform: Transform::default(), 29 | data: shape.into(), 30 | } 31 | } 32 | } 33 | 34 | impl CollisionShape { 35 | /// Create a circle from its radius 36 | /// 37 | /// The origin is in the center of the circle 38 | #[inline] 39 | #[must_use] 40 | pub fn new_circle(radius: f32) -> Self { 41 | shapes::Circle::new(radius).into() 42 | } 43 | 44 | /// Create a rectangle from its width and height 45 | /// 46 | /// The origin is in the center of the rectangle 47 | #[inline] 48 | #[must_use] 49 | pub fn new_rectangle(width: f32, height: f32) -> Self { 50 | shapes::Rectangle::new(width, height).into() 51 | } 52 | 53 | /// Create a segment from two points 54 | #[inline] 55 | #[must_use] 56 | pub fn new_segment(p1: impl Into<[f32; 2]>, p2: impl Into<[f32; 2]>) -> Self { 57 | shapes::Segment::new(p1, p2).into() 58 | } 59 | 60 | /// Set the transform (translation, rotation and scale) 61 | /// 62 | /// This is equivalent to [`set_transform`](Self::set_transform), but in a builder style, 63 | /// useful to set the transform directly at creation 64 | #[inline] 65 | #[must_use] 66 | pub fn with_transform(mut self, transform: impl Into) -> Self { 67 | self.set_transform(transform); 68 | self 69 | } 70 | 71 | /// Set the transform (translation, rotation and scale) 72 | #[inline] 73 | pub fn set_transform(&mut self, transform: impl Into) { 74 | self.transform = transform.into(); 75 | } 76 | 77 | /// Returns true if the two convex shapes geometries are overlapping 78 | #[must_use] 79 | pub fn is_collided_with(&self, other: &Self) -> bool { 80 | let difference = minkowski::Difference { 81 | shape1: self, 82 | shape2: other, 83 | }; 84 | let initial_axis = other.transform.position() - self.transform.position(); 85 | gjk::find_simplex_enclosing_origin(&difference, initial_axis).is_some() 86 | } 87 | 88 | /// Returns contact data with the other shape if they collide. Returns `None` if they don't collide. 89 | /// 90 | /// The normal of the contact data is pointing toward this shape. 91 | /// In other words, ff this shape is moved by `contact.normal * contact.penetration` 92 | /// the two shapes will no longer be inter-penetrating. 93 | #[must_use] 94 | pub fn contact_with(&self, other: &Self) -> Option { 95 | let difference = minkowski::Difference { 96 | shape1: self, 97 | shape2: other, 98 | }; 99 | let initial_axis = other.transform.position() - self.transform.position(); 100 | let simplex = gjk::find_simplex_enclosing_origin(&difference, initial_axis)?; 101 | let Contact { 102 | normal, 103 | penetration, 104 | } = epa::generate_contact(&difference, simplex); 105 | Some(Contact:: { 106 | normal: normal.into(), 107 | penetration, 108 | }) 109 | } 110 | 111 | /// Returns the shape data of the collider 112 | #[must_use] 113 | pub fn shape_data(&self) -> &ShapeData { 114 | &self.data 115 | } 116 | } 117 | 118 | /// Contact data between two shapes 119 | /// 120 | /// See [`CollisionShape::contact_with`] 121 | #[non_exhaustive] 122 | #[derive(Debug, Clone, PartialEq)] 123 | pub struct Contact { 124 | /// Contact normal 125 | /// 126 | /// This is the direction on which the first shape should be moved to resolve inter-penetration 127 | /// This is also on that direction that impulse should be applied to the first shape to resolve velocities 128 | pub normal: V, 129 | /// Penetration 130 | /// 131 | /// This is "how much" the two shapes are inter-penetrating 132 | pub penetration: S, 133 | } 134 | 135 | trait Support { 136 | /// Returns the farthest point of the shape in the given direction. 137 | /// 138 | /// More formaly: For a direction `v` return the point `p` of the shape that maximize the dot product `p . v` 139 | /// 140 | /// If many points are equaly far in the given direction (have the same dot product `p . v`), 141 | /// then one of the is choosen arbitrarily. 142 | /// 143 | /// Note the direction may not be normalized, and may have a magnitude of zero. 144 | fn support(&self, direction: V) -> V; 145 | } 146 | -------------------------------------------------------------------------------- /src/v3/aabb.rs: -------------------------------------------------------------------------------- 1 | use core::iter; 2 | 3 | use sealed::sealed; 4 | 5 | use super::{__seal_shape, vector::Vec2, Point, Range, Shape}; 6 | 7 | #[derive(Debug, Copy, Clone)] 8 | pub struct Aabb { 9 | center: Point, 10 | half_size: Vec2, 11 | } 12 | 13 | impl Aabb { 14 | #[must_use] 15 | pub fn from_size(size: impl Into) -> Self { 16 | Self { 17 | center: Point::ORIGIN, 18 | half_size: size.into() / 2.0, 19 | } 20 | } 21 | 22 | pub fn set_center_at(&mut self, center: impl Into) { 23 | self.center = center.into(); 24 | } 25 | 26 | #[must_use] 27 | pub fn with_center_at(mut self, center: impl Into) -> Self { 28 | self.set_center_at(center); 29 | self 30 | } 31 | } 32 | 33 | #[sealed] 34 | impl Shape for Aabb { 35 | fn axes(&self) -> impl Iterator { 36 | [Vec2::X, Vec2::Y].into_iter() 37 | } 38 | 39 | fn focals(&self) -> impl Iterator { 40 | iter::empty() 41 | } 42 | 43 | fn vertices(&self) -> impl Iterator { 44 | [ 45 | (self.center - self.half_size), 46 | (self.center + Vec2::new(self.half_size.x, -self.half_size.y)), 47 | (self.center + self.half_size), 48 | (self.center + Vec2::new(-self.half_size.x, self.half_size.y)), 49 | ] 50 | .into_iter() 51 | } 52 | 53 | fn project_on(&self, axis: Vec2) -> Range { 54 | let r1 = abs(self.half_size.dot(axis)); 55 | let r2 = abs(Vec2::new(-self.half_size.x, self.half_size.y).dot(axis)); 56 | let r = r1.max(r2); 57 | let shift = Vec2::from(self.center).dot(axis); 58 | Range::from_min_max(shift - r, shift + r) 59 | } 60 | } 61 | 62 | #[cfg(feature = "std")] 63 | fn abs(v: f32) -> f32 { 64 | v.abs() 65 | } 66 | 67 | #[cfg(not(feature = "std"))] 68 | fn abs(v: f32) -> f32 { 69 | libm::fabsf(v) 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use alloc::vec::Vec; 75 | 76 | use approx::assert_abs_diff_eq; 77 | use rstest::rstest; 78 | 79 | use super::*; 80 | 81 | #[rstest] 82 | #[case(Aabb::from_size(Vec2::new(0.0, 0.0)), Vec2::new(1.0, 0.0), 0.0, 0.0)] 83 | #[case(Aabb::from_size(Vec2::new(2.0, 0.0)), Vec2::new(1.0, 0.0), -1.0, 1.0)] 84 | #[case(Aabb::from_size(Vec2::new(2.0, 0.0)), Vec2::new(-1.0, 0.0), -1.0, 1.0)] 85 | #[case(Aabb::from_size(Vec2::new(0.0, 2.0)), Vec2::new(0.0, 1.0), -1.0, 1.0)] 86 | #[case(Aabb::from_size(Vec2::new(0.0, 2.0)), Vec2::new(0.0, -1.0), -1.0, 1.0)] 87 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)), Vec2::new(1.0, 0.0), -1.5, 1.5)] 88 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)), Vec2::new(0.0, 1.0), -2.0, 2.0)] 89 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)), Vec2::new(1.5, 2.0), -6.25, 6.25)] 90 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)), Vec2::new(1.5, -2.0), -6.25, 6.25)] 91 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)), Vec2::new(-1.5, 2.0), -6.25, 6.25)] 92 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)).with_center_at(Point::new(0.0, 0.0)), Vec2::new(1.0, 0.0), -1.5, 1.5)] 93 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)).with_center_at(Point::new(1.0, 0.0)), Vec2::new(1.0, 0.0), -0.5, 2.5)] 94 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)).with_center_at(Point::new(0.0, 1.0)), Vec2::new(1.0, 0.0), -1.5, 1.5)] 95 | #[case(Aabb::from_size(Vec2::new(3.0, 4.0)).with_center_at(Point::new(0.0, 1.0)), Vec2::new(0.0, 1.0), -1.0, 3.0)] 96 | fn test_axis_project( 97 | #[case] shape: Aabb, 98 | #[case] axis: Vec2, 99 | #[case] expected_min: f32, 100 | #[case] expected_max: f32, 101 | ) { 102 | let range = shape.project_on(axis); 103 | assert_abs_diff_eq!(range.min, expected_min); 104 | assert_abs_diff_eq!(range.max, expected_max); 105 | } 106 | 107 | #[rstest] 108 | fn test_sat_axes( 109 | #[values( 110 | Aabb::from_size(Vec2::ZERO), 111 | Aabb::from_size(Vec2::new(2.0, 3.0)).with_center_at(Point::new(4.0, 5.0)) 112 | )] 113 | shape: Aabb, 114 | ) { 115 | let mut iterator = shape.axes(); 116 | assert_eq!(iterator.next(), Some(Vec2::X)); 117 | assert_eq!(iterator.next(), Some(Vec2::Y)); 118 | assert!(iterator.next().is_none(), "too many axes returned"); 119 | } 120 | 121 | #[test] 122 | fn test_sat_vertices() { 123 | let aabb = Aabb::from_size(Vec2::new(2.0, 3.0)).with_center_at(Point::new(4.0, 5.0)); 124 | let vertices: Vec<_> = aabb.vertices().collect(); 125 | assert_eq!(vertices.len(), 4); 126 | assert!( 127 | vertices.iter().copied().any(|p| p == Point::new(3.0, 3.5)), 128 | "top left corner not is incorrect: {vertices:?}" 129 | ); 130 | assert!( 131 | vertices.iter().copied().any(|p| p == Point::new(5.0, 3.5)), 132 | "top left corner not is incorrect: {vertices:?}" 133 | ); 134 | assert!( 135 | vertices.iter().copied().any(|p| p == Point::new(5.0, 6.5)), 136 | "top left corner not is incorrect: {vertices:?}" 137 | ); 138 | assert!( 139 | vertices.iter().copied().any(|p| p == Point::new(3.0, 6.5)), 140 | "top left corner not is incorrect: {vertices:?}" 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/v2/epa.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | mem, 3 | ops::{Neg, Sub}, 4 | }; 5 | 6 | use smallvec::{smallvec, SmallVec}; 7 | 8 | use super::{gjk, math::*, Contact, Support}; 9 | 10 | pub(super) fn generate_contact( 11 | difference: &impl Support, 12 | simplex: gjk::Simplex, 13 | ) -> Contact 14 | where 15 | V: Copy 16 | + Default 17 | + Sub 18 | + Neg 19 | + Dot 20 | + Cross 21 | + Perp 22 | + Normalize, 23 | S: PartialOrd + Sub + CmpToZero, 24 | { 25 | let mut simplex: Simplex = simplex.into(); 26 | for _ in 0..1000 { 27 | let edge = simplex.closest_edge(); 28 | let support = difference.support(edge.normal); 29 | let penetration = support.dot(edge.normal); 30 | if (penetration - edge.distance).is_negative() { 31 | return edge.into(); 32 | } 33 | simplex.insert(edge.index, support); 34 | } 35 | simplex.closest_edge().into() 36 | } 37 | 38 | struct Edge { 39 | index: usize, 40 | normal: V, 41 | distance: ::Output, 42 | } 43 | 44 | impl From> for Contact 45 | where 46 | V: Neg + Dot, 47 | { 48 | fn from(edge: Edge) -> Self { 49 | Contact { 50 | normal: -edge.normal, 51 | penetration: edge.distance, 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone, PartialEq)] 57 | struct Simplex { 58 | points: SmallVec<[V; 10]>, 59 | } 60 | 61 | impl Simplex 62 | where 63 | V: Dot + Copy + Sub + Perp + Normalize + Default, 64 | ::Output: PartialOrd, 65 | { 66 | fn closest_edge(&self) -> Edge { 67 | (0..self.points.len()) 68 | .map(|index| self.edge(index)) 69 | .min_by(|e1, e2| { 70 | e1.distance 71 | .partial_cmp(&e2.distance) 72 | .unwrap_or(core::cmp::Ordering::Equal) 73 | }) 74 | .expect("no edge in epa simplex") 75 | } 76 | 77 | fn edge(&self, index: usize) -> Edge { 78 | let p1 = self.points[index]; 79 | let p2 = self 80 | .points 81 | .get(index + 1) 82 | .copied() 83 | .unwrap_or_else(|| self.points[0]); 84 | let edge = p2 - p1; 85 | let normal = edge 86 | .perp() 87 | .normalize() 88 | .or_else(|| p1.normalize()) 89 | .unwrap_or_default(); 90 | let distance = p1.dot(normal); 91 | Edge { 92 | index, 93 | normal, 94 | distance, 95 | } 96 | } 97 | } 98 | 99 | impl Simplex { 100 | fn insert(&mut self, index: usize, point: V) { 101 | self.points.insert(index + 1, point); 102 | } 103 | } 104 | 105 | impl From> for Simplex 106 | where 107 | V: Copy + Sub + Cross, 108 | ::Output: CmpToZero, 109 | { 110 | fn from(simplex: gjk::Simplex) -> Self { 111 | Self { 112 | points: match simplex { 113 | gjk::Simplex::Point(p) => smallvec![p], 114 | gjk::Simplex::Line(p1, p2) => smallvec![p1, p2], 115 | gjk::Simplex::Triangle(p1, mut p2, mut p3) => { 116 | if (p2 - p1).cross(p3 - p2).is_negative() { 117 | mem::swap(&mut p2, &mut p3); 118 | } 119 | smallvec![p1, p2, p3] 120 | } 121 | }, 122 | } 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use approx::assert_ulps_eq; 129 | use glam::Vec2; 130 | 131 | use super::*; 132 | 133 | mod simplex { 134 | use super::*; 135 | 136 | #[test] 137 | fn starts_with_left_winding() { 138 | let expected = [Vec2::ZERO, Vec2::X, Vec2::Y]; 139 | let simplex1: Simplex = 140 | gjk::Simplex::Triangle(Vec2::ZERO, Vec2::X, Vec2::Y).into(); 141 | assert_eq!(&simplex1.points[..], &expected); 142 | let simplex2: Simplex = 143 | gjk::Simplex::Triangle(Vec2::ZERO, Vec2::Y, Vec2::X).into(); 144 | assert_eq!(&simplex2.points[..], &expected); 145 | } 146 | 147 | #[test] 148 | fn next_returns_feature_index_and_outward_direction() { 149 | let simplex = Simplex { 150 | points: smallvec![ 151 | Vec2::Y * 9.0, 152 | Vec2::X * 5.0 - Vec2::Y, 153 | -Vec2::X * 5.0 - Vec2::Y 154 | ], 155 | }; 156 | let Edge { 157 | index, 158 | normal, 159 | distance, 160 | } = simplex.closest_edge(); 161 | assert_eq!(index, 1); 162 | assert_ulps_eq!(distance, 1.0); 163 | assert_eq!(normal, -Vec2::Y); 164 | } 165 | 166 | #[test] 167 | fn insert_point() { 168 | let mut simplex = Simplex { 169 | points: smallvec![Vec2::Y * 2.0, Vec2::X - Vec2::Y, -Vec2::X - Vec2::Y], 170 | }; 171 | simplex.insert(1, -Vec2::Y); 172 | assert_eq!( 173 | &simplex.points[..], 174 | &[ 175 | Vec2::Y * 2.0, 176 | Vec2::X - Vec2::Y, 177 | -Vec2::Y, 178 | -Vec2::X - Vec2::Y 179 | ] 180 | ); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/v2/shapes.rs: -------------------------------------------------------------------------------- 1 | //! Collection of shape data that can be used to create a [`CollisionShape`](crate::CollisionShape) 2 | 3 | use glam::Vec2; 4 | 5 | use super::Support; 6 | 7 | /// Geometric information about a shape 8 | #[non_exhaustive] 9 | #[derive(Debug, Clone)] 10 | pub enum ShapeData { 11 | /// A circle 12 | /// 13 | /// See [`Circle`] 14 | Circle(Circle), 15 | /// A circle 16 | /// 17 | /// See [`Rectangle`] 18 | Rectangle(Rectangle), 19 | /// A segment 20 | /// 21 | /// See [`Segment`] 22 | Segment(Segment), 23 | } 24 | 25 | impl Support for ShapeData { 26 | fn support(&self, direction: Vec2) -> Vec2 { 27 | match self { 28 | ShapeData::Circle(circle) => circle.support(direction), 29 | ShapeData::Rectangle(rect) => rect.support(direction), 30 | ShapeData::Segment(segment) => segment.support(direction), 31 | } 32 | } 33 | } 34 | 35 | /// A circle 36 | #[derive(Debug, Copy, Clone, PartialEq)] 37 | pub struct Circle { 38 | radius: f32, 39 | } 40 | 41 | impl Circle { 42 | /// Create a circle from its radius 43 | #[inline] 44 | #[must_use] 45 | pub fn new(radius: f32) -> Self { 46 | Self { radius } 47 | } 48 | 49 | /// Returns the radius of the circle 50 | #[must_use] 51 | pub fn radius(&self) -> f32 { 52 | self.radius 53 | } 54 | } 55 | 56 | impl From for ShapeData { 57 | #[inline] 58 | fn from(circle: Circle) -> Self { 59 | Self::Circle(circle) 60 | } 61 | } 62 | 63 | impl Support for Circle { 64 | fn support(&self, direction: Vec2) -> Vec2 { 65 | let point = direction.clamp_length(self.radius, self.radius); 66 | if point.is_nan() { 67 | Vec2::new(self.radius, 0.0) 68 | } else { 69 | point 70 | } 71 | } 72 | } 73 | 74 | /// A rectangle 75 | #[derive(Debug, Copy, Clone, PartialEq)] 76 | pub struct Rectangle { 77 | half_extents: Vec2, 78 | } 79 | 80 | impl Rectangle { 81 | /// Creates a rectangle from its width and height 82 | /// 83 | /// The origin is in the center of the rectangle 84 | #[inline] 85 | #[must_use] 86 | pub fn new(width: f32, height: f32) -> Self { 87 | Self { 88 | half_extents: Vec2::new(width * 0.5, height * 0.5).abs(), 89 | } 90 | } 91 | 92 | /// Returns the half width and height of the rectangle 93 | #[must_use] 94 | pub fn half_extents(&self) -> [f32; 2] { 95 | self.half_extents.into() 96 | } 97 | } 98 | 99 | impl From for ShapeData { 100 | #[inline] 101 | fn from(rect: Rectangle) -> Self { 102 | Self::Rectangle(rect) 103 | } 104 | } 105 | 106 | impl Support for Rectangle { 107 | fn support(&self, direction: Vec2) -> Vec2 { 108 | let mut support = self.half_extents; 109 | if direction.x < 0.0 { 110 | support.x = -support.x; 111 | } 112 | if direction.y < 0.0 { 113 | support.y = -support.y; 114 | } 115 | support 116 | } 117 | } 118 | 119 | /// A segment 120 | #[derive(Debug, Clone)] 121 | pub struct Segment { 122 | p1: Vec2, 123 | p2: Vec2, 124 | } 125 | 126 | impl Segment { 127 | /// Returns a new segment 128 | pub fn new(p1: impl Into<[f32; 2]>, p2: impl Into<[f32; 2]>) -> Self { 129 | Self { 130 | p1: p1.into().into(), 131 | p2: p2.into().into(), 132 | } 133 | } 134 | } 135 | 136 | impl From for ShapeData { 137 | fn from(segment: Segment) -> Self { 138 | Self::Segment(segment) 139 | } 140 | } 141 | 142 | impl Support for Segment { 143 | fn support(&self, direction: Vec2) -> Vec2 { 144 | if self.p1.dot(direction) > self.p2.dot(direction) { 145 | self.p1 146 | } else { 147 | self.p2 148 | } 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | 156 | use approx::assert_ulps_eq; 157 | 158 | #[test] 159 | fn circle() { 160 | assert_eq!(Circle::new(2.0).support(Vec2::X), Vec2::X * 2.0); 161 | assert_eq!(Circle::new(3.0).support(Vec2::Y), Vec2::Y * 3.0); 162 | assert_eq!(Circle::new(0.0).support(Vec2::X), Vec2::ZERO); 163 | } 164 | 165 | #[test] 166 | fn circle_with_invalid_direction() { 167 | assert_ulps_eq!( 168 | Circle::new(1.) 169 | .support(Vec2::splat(f32::NAN)) 170 | .length_squared(), 171 | 1.0 172 | ); 173 | } 174 | 175 | #[test] 176 | fn rectangle() { 177 | let rectangle = Rectangle::new(6.0, 4.0); 178 | assert_eq!(rectangle.support(Vec2::new(1., 1.)), Vec2::new(3., 2.)); 179 | assert_eq!(rectangle.support(Vec2::new(-1., 1.)), Vec2::new(-3., 2.)); 180 | assert_eq!(rectangle.support(Vec2::new(1., -1.)), Vec2::new(3., -2.)); 181 | assert_eq!(rectangle.support(Vec2::new(-1., -1.)), Vec2::new(-3., -2.)); 182 | } 183 | 184 | #[test] 185 | fn rectangle_with_invalid_direction() { 186 | assert_ulps_eq!( 187 | Rectangle::new(2., 2.) 188 | .support(Vec2::splat(f32::NAN)) 189 | .length_squared(), 190 | 2.0 191 | ); 192 | } 193 | 194 | #[test] 195 | fn line() { 196 | let segment = Segment::new(Vec2::ZERO, Vec2::X * 2.0); 197 | assert_eq!(segment.support(Vec2::X), Vec2::X * 2.0); 198 | assert_eq!(segment.support(Vec2::X + Vec2::X), Vec2::X * 2.0); 199 | assert_eq!(segment.support(-Vec2::X), Vec2::ZERO); 200 | assert_eq!(segment.support(-Vec2::X - Vec2::X), Vec2::ZERO); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/v2/gjk.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Neg, Sub}; 2 | 3 | use super::{math::*, Support}; 4 | 5 | pub(super) fn find_simplex_enclosing_origin( 6 | shape: &impl Support, 7 | initial_direction: V, 8 | ) -> Option> 9 | where 10 | V: Copy + Dot + Perp + Neg + Sub, 11 | ::Output: CmpToZero, 12 | { 13 | let mut simplex = { 14 | let first_point = shape.support(initial_direction); 15 | if is_negative_or_invalid(first_point.dot(initial_direction)) { 16 | return None; 17 | } 18 | Simplex::new(first_point) 19 | }; 20 | 21 | while let Some(direction) = simplex.next() { 22 | let point = shape.support(direction); 23 | if is_negative_or_invalid(point.dot(direction)) { 24 | return None; 25 | } 26 | simplex.insert(point); 27 | } 28 | Some(simplex) 29 | } 30 | 31 | fn is_negative_or_invalid(dot: impl CmpToZero) -> bool { 32 | !dot.is_positive() && !dot.is_zero() 33 | } 34 | 35 | #[derive(Debug, Copy, Clone, PartialEq)] 36 | pub(crate) enum Simplex

{ 37 | Point(P), 38 | Line(P, P), 39 | Triangle(P, P, P), 40 | } 41 | 42 | impl

Simplex

{ 43 | pub(crate) fn new(point: P) -> Self { 44 | Self::Point(point) 45 | } 46 | } 47 | 48 | impl Simplex

{ 49 | pub(crate) fn insert(&mut self, new_point: P) { 50 | match *self { 51 | Self::Point(p) => *self = Self::Line(p, new_point), 52 | Self::Line(p1, p2) => *self = Self::Triangle(p1, p2, new_point), 53 | Self::Triangle(_, _, _) => { 54 | panic!("Cannot expand 2d simplex further than triangle") 55 | } 56 | } 57 | } 58 | } 59 | 60 | impl Simplex 61 | where 62 | V: Copy + Dot + Perp + Neg + Sub, 63 | ::Output: CmpToZero, 64 | { 65 | /// Set to the simpler simplex that is closest to the origin. 66 | /// 67 | /// If the origin is inside the simplex returns None. Otherwise returns the next direction to test. 68 | pub(crate) fn next(&mut self) -> Option { 69 | match *self { 70 | Self::Point(point) => { 71 | if point.magnitude_squared().is_positive() { 72 | Some(-point) 73 | } else { 74 | None 75 | } 76 | } 77 | Self::Line(p1, p2) => { 78 | let mut dir = (p2 - p1).perp(); 79 | if dir.dot(p1).is_positive() { 80 | dir = -dir; 81 | } 82 | if dir.dot(p1).is_negative() { 83 | Some(dir) 84 | } else { 85 | None 86 | } 87 | } 88 | Self::Triangle(p1, p2, p3) => { 89 | let mut dir = perp(p3 - p1, p3 - p2); 90 | if dir.dot(-p3).is_positive() { 91 | *self = Self::Line(p1, p3); 92 | return Some(dir); 93 | } 94 | dir = perp(p3 - p2, p3 - p1); 95 | if dir.dot(-p3).is_positive() { 96 | *self = Self::Line(p2, p3); 97 | Some(dir) 98 | } else { 99 | None 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | /// Returns a perpendicular to `axis` that has a positive dot product with `direction` 107 | fn perp(axis: V, direction: V) -> V 108 | where 109 | V: Copy + Perp + Neg + Dot, 110 | ::Output: CmpToZero, 111 | { 112 | let perp = axis.perp(); 113 | if perp.dot(direction).is_negative() { 114 | -perp 115 | } else { 116 | perp 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | #[cfg(feature = "std")] 123 | use rstest::rstest; 124 | 125 | use super::*; 126 | use glam::Vec2; 127 | 128 | struct InvalidSupport; 129 | impl Support for InvalidSupport { 130 | fn support(&self, _: Vec2) -> Vec2 { 131 | Vec2::NAN 132 | } 133 | } 134 | 135 | #[test] 136 | fn invalid_support() { 137 | assert!(find_simplex_enclosing_origin(&InvalidSupport, Vec2::X).is_none()); 138 | } 139 | 140 | #[rstest] 141 | #[case(Simplex::Point(Vec2::default()))] 142 | #[case(Simplex::Line(-Vec2::X, Vec2::X))] 143 | #[case(Simplex::Line(Vec2::X, Vec2::default()))] 144 | #[case(Simplex::Line(Vec2::default(), Vec2::Y))] 145 | #[case(Simplex::Triangle(Vec2::X, Vec2::Y, Vec2::default()))] 146 | #[case(Simplex::Triangle(Vec2::new(-1.0, -1.0), Vec2::new(1.0, -1.0), Vec2::Y))] 147 | #[cfg(feature = "std")] 148 | fn contains_origin(#[case] simplex: Simplex) { 149 | let mut modified = simplex; 150 | assert!(modified.next().is_none()); 151 | assert_eq!(modified, simplex); 152 | } 153 | 154 | #[test] 155 | fn point() { 156 | let mut simplex = Simplex::new(Vec2::X); 157 | assert_eq!(simplex.next(), Some(-Vec2::X)); 158 | assert_eq!(simplex, Simplex::new(Vec2::X)); 159 | } 160 | 161 | #[test] 162 | fn line() { 163 | let mut simplex = Simplex::Line(Vec2::Y, Vec2::new(1.0, 1.0)); 164 | assert_eq!(simplex.next(), Some(-Vec2::Y)); 165 | assert_eq!(simplex, Simplex::Line(Vec2::Y, Vec2::new(1.0, 1.0))); 166 | } 167 | 168 | #[test] 169 | fn triangle() { 170 | let mut simplex = Simplex::Triangle(Vec2::Y, Vec2::new(1.0, 1.0), Vec2::X); 171 | assert_eq!( 172 | simplex.next().map(glam::Vec2::normalize), 173 | Some(Vec2::new(-1.0, -1.0).normalize()) 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/collision_spec.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "std")] 2 | 3 | use std::f32::consts; 4 | 5 | use approx::assert_abs_diff_eq; 6 | use glam::Vec2; 7 | use rstest::*; 8 | 9 | use impacted::{CollisionShape, Transform}; 10 | 11 | #[rstest] 12 | #[case(CollisionShape::new_circle(1.0), CollisionShape::new_circle(1.0))] 13 | #[case( 14 | CollisionShape::new_circle(1.0), 15 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::ZERO)), 16 | )] 17 | #[case( 18 | CollisionShape::new_circle(1.0), 19 | CollisionShape::new_circle(1.0).with_transform(Transform::from_angle_translation(2.0, Vec2::ZERO)), 20 | )] 21 | #[case( 22 | CollisionShape::new_circle(1.0), 23 | CollisionShape::new_circle(1.0).with_transform(Transform::from_scale_angle_translation(Vec2::splat(2.0), 2.0, Vec2::ZERO)), 24 | )] 25 | #[case( 26 | CollisionShape::new_circle(1.0), 27 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::new(2.0, 0.0))), 28 | )] 29 | #[case( 30 | CollisionShape::new_circle(1.0), 31 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::X * 1.0)), 32 | )] 33 | #[case( 34 | CollisionShape::new_circle(1.0), 35 | CollisionShape::new_circle(1.5).with_transform(Transform::from_translation(Vec2::Y * 2.1)), 36 | )] 37 | #[case( 38 | CollisionShape::new_circle(1.0), 39 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_translation(Vec2::X * 1.9)), 40 | )] 41 | #[case( 42 | CollisionShape::new_circle(1.0), 43 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_angle_translation(consts::FRAC_PI_4, Vec2::X * 2.3)), 44 | )] 45 | #[case( 46 | CollisionShape::new_circle(1.0), 47 | CollisionShape::new_segment(Vec2::ZERO, Vec2::X) 48 | )] 49 | fn collides(#[case] shape1: CollisionShape, #[case] shape2: CollisionShape) { 50 | assert!(shape1.is_collided_with(&shape2)); 51 | let contact = shape1.contact_with(&shape2); 52 | assert!(contact.is_some(), "{contact:?}"); 53 | } 54 | 55 | #[rstest] 56 | #[case( 57 | CollisionShape::new_circle(1.0), 58 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::X * 2.1)), 59 | )] 60 | #[case( 61 | CollisionShape::new_circle(1.0), 62 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::Y * 2.1)), 63 | )] 64 | #[case( 65 | CollisionShape::new_circle(1.0), 66 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_translation(Vec2::X * 2.1)), 67 | )] 68 | #[case( 69 | CollisionShape::new_circle(1.0), 70 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_angle_translation(consts::FRAC_PI_4, Vec2::X * 2.5)), 71 | )] 72 | #[case( 73 | CollisionShape::new_circle(1.0), 74 | CollisionShape::new_segment(Vec2::X * 2.0, Vec2::X * 3.0), 75 | )] 76 | fn does_not_collide(#[case] shape1: CollisionShape, #[case] shape2: CollisionShape) { 77 | assert!(!shape1.is_collided_with(&shape2)); 78 | let contact = shape1.contact_with(&shape2); 79 | assert!(contact.is_none(), "{contact:?}"); 80 | } 81 | 82 | #[rstest] 83 | #[case( 84 | CollisionShape::new_circle(1.0), 85 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::X * 1.95)), 86 | Vec2::new(-1.0, 0.0) 87 | )] 88 | #[case( 89 | CollisionShape::new_circle(1.0), 90 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::Y * 1.95)), 91 | Vec2::new(0.0, -1.0) 92 | )] 93 | #[case( 94 | CollisionShape::new_rectangle(1.0, 1.0), 95 | CollisionShape::new_rectangle(1.0, 1.0).with_transform(Transform::from_translation(Vec2::X * -0.95)), 96 | Vec2::new(1.0, 0.0) 97 | )] 98 | #[case( 99 | CollisionShape::new_rectangle(2.0, 2.0), 100 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_angle_translation(consts::FRAC_PI_4 + 0.1, Vec2::X * 2.3)), 101 | Vec2::new(-1.0, 0.0) 102 | )] 103 | #[case( 104 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_angle_translation(consts::FRAC_PI_4 + 0.1, Vec2::X * 2.3)), 105 | CollisionShape::new_rectangle(2.0, 2.0), 106 | Vec2::new(1.0, 0.0) 107 | )] 108 | fn contact_normal( 109 | #[case] shape1: CollisionShape, 110 | #[case] shape2: CollisionShape, 111 | #[case] expected_normal: Vec2, 112 | ) { 113 | let contact = shape1.contact_with(&shape2).unwrap(); 114 | assert_abs_diff_eq!(Vec2::from(contact.normal), expected_normal, epsilon = 0.001); 115 | } 116 | 117 | #[rstest] 118 | #[case( 119 | CollisionShape::new_circle(1.0), 120 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::X * 1.95)), 121 | 0.05 122 | )] 123 | #[case( 124 | CollisionShape::new_circle(1.0), 125 | CollisionShape::new_circle(1.0).with_transform(Transform::from_translation(Vec2::Y * 1.0)), 126 | 1.0 127 | )] 128 | #[case( 129 | CollisionShape::new_rectangle(1.0, 1.0), 130 | CollisionShape::new_rectangle(1.0, 1.0).with_transform(Transform::from_translation(Vec2::X * -0.95)), 131 | 0.05 132 | )] 133 | #[case( 134 | CollisionShape::new_rectangle(1.0, 1.0), 135 | CollisionShape::new_rectangle(1.0, 1.0).with_transform(Transform::from_translation(Vec2::X * -0.5)), 136 | 0.5 137 | )] 138 | #[case( 139 | CollisionShape::new_rectangle(2.0, 2.0), 140 | CollisionShape::new_rectangle(2.0, 2.0).with_transform(Transform::from_angle_translation(consts::FRAC_PI_4, Vec2::X * 2.3)), 141 | 0.1142 142 | )] 143 | fn contact_penetration( 144 | #[case] shape1: CollisionShape, 145 | #[case] shape2: CollisionShape, 146 | #[case] expected_penetration: f32, 147 | ) { 148 | let contact = shape1.contact_with(&shape2).unwrap(); 149 | assert_abs_diff_eq!(contact.penetration, expected_penetration, epsilon = 0.0001); 150 | } 151 | -------------------------------------------------------------------------------- /examples/bevy.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts; 2 | 3 | use bevy::prelude::*; 4 | 5 | #[derive(Debug, Component, Deref, DerefMut)] 6 | struct CollisionShape(impacted::CollisionShape); 7 | 8 | /// As simple "tag" component to mark the entity controlled by keyboard 9 | /// (Nothing specific about collision detection) 10 | #[derive(Component)] 11 | struct Controlled; 12 | 13 | fn main() { 14 | App::new() 15 | .add_plugins(DefaultPlugins) 16 | .add_systems(Startup, startup) 17 | .add_systems(PreUpdate, control_shape) 18 | .add_systems(Update, (update_shape_transforms, update_color).chain()) 19 | .run(); 20 | } 21 | 22 | /// Initialize the "game" 23 | fn startup(mut commands: Commands) { 24 | // Camera 25 | commands.spawn(Camera2dBundle::default()); 26 | 27 | // Left shape (controlled) 28 | commands 29 | // Add a sprite so we can see it (nothing specific about collision detection) 30 | .spawn(SpriteBundle { 31 | sprite: Sprite { 32 | custom_size: Some(Vec2::splat(100.0)), 33 | color: Color::BLUE, 34 | ..Default::default() 35 | }, 36 | transform: Transform::from_translation(Vec3::new(-200., 0.0, 0.0)), 37 | ..Default::default() 38 | }) 39 | .insert(( 40 | // Add the collision shape 41 | CollisionShape(impacted::CollisionShape::new_rectangle(100.0, 100.0)), 42 | // Mark this shape as the one being controlled (nothing specific to collision detection) 43 | Controlled, 44 | )); 45 | 46 | // Right shape (static) 47 | commands 48 | // Add a sprite so we can see it (nothing specific about collision detection) 49 | .spawn(SpriteBundle { 50 | sprite: Sprite { 51 | custom_size: Some(Vec2::splat(100.0)), 52 | color: Color::BLUE, 53 | ..Default::default() 54 | }, 55 | transform: Transform::from_translation(Vec3::new(200., 0.0, 0.0)), 56 | ..Default::default() 57 | }) 58 | // Add the collision shape 59 | .insert(CollisionShape(impacted::CollisionShape::new_rectangle( 60 | 100.0, 100.0, 61 | ))); 62 | } 63 | 64 | /// Update the `CollisionShape` transform if the `GlobalTransform` has changed 65 | fn update_shape_transforms( 66 | mut shapes: Query<(&mut CollisionShape, &GlobalTransform), Changed>, 67 | ) { 68 | for (mut shape, transform) in shapes.iter_mut() { 69 | let (scale, rotation, translation) = transform.to_scale_rotation_translation(); 70 | shape.set_transform(impacted::Transform::from_scale_angle_translation( 71 | scale.truncate(), 72 | angle_2d_from_quat(rotation), 73 | translation.truncate(), 74 | )); 75 | } 76 | } 77 | 78 | fn angle_2d_from_quat(quat: Quat) -> f32 { 79 | if quat.is_near_identity() { 80 | return 0.0; 81 | } 82 | let projected = quat.to_scaled_axis().project_onto(Vec3::Z); 83 | let angle = projected.length(); 84 | if projected.z < 0.0 { 85 | -angle 86 | } else { 87 | angle 88 | } 89 | } 90 | 91 | /// Detect collision and update shape colors 92 | /// 93 | /// Notice, that it only looks at the shapes that have moved to avoid unnecessary collision test 94 | /// 95 | /// It still tests each moved shape against each other shape as this library only provide narrow-phase collision test. 96 | /// For many small games it should be fine. 97 | /// For bigger games, you may consider to pair it with a broad-phase (like [bvh-arena] or [broccoli]) 98 | /// to reduce the number of collision test to perform. 99 | /// 100 | /// Also, remember that this implementation is quite generic, 101 | /// and it might be simplified for your use-case. 102 | /// Example: check for collision between each enemy that has moved and the player. 103 | /// You may even have many of this kind of system for different aspect of the game logic, and bevy can run them in parallel! 104 | /// 105 | /// [bvh-arena]: https://crates.io/crates/bvh-arena 106 | /// [broccoli]: https://crates.io/crates/broccoli 107 | fn update_color( 108 | mut all_shapes: Query<(Entity, &mut Sprite, &CollisionShape)>, 109 | moved_shapes: Query<(Entity, &CollisionShape), Changed>, 110 | ) { 111 | for (moved_entity, moved_shape) in moved_shapes.iter() { 112 | let mut is_collided = false; 113 | for (other_entity, mut other_sprite, other_shape) in all_shapes.iter_mut() { 114 | if other_entity == moved_entity { 115 | continue; // Don't test collision with self 116 | } 117 | 118 | // Test for collision, and update the other shape color 119 | if moved_shape.is_collided_with(other_shape) { 120 | other_sprite.color = Color::RED; 121 | is_collided = true; 122 | } else { 123 | other_sprite.color = Color::BLUE; 124 | } 125 | } 126 | 127 | // Update the moved shape color 128 | let (_, mut sprite, _) = all_shapes.get_mut(moved_entity).unwrap(); 129 | sprite.color = if is_collided { Color::RED } else { Color::BLUE } 130 | } 131 | } 132 | 133 | /// Simple control system to move the shape with the arrows keys, and rotate with `A` and `D` 134 | /// (Nothing specific about collision detection here) 135 | fn control_shape( 136 | input: Res>, 137 | time: Res