phys-collision 2.0.1-beta.0

Provides collision detection ability
// Copyright (C) 2020-2025 phys-collision authors. All Rights Reserved.
//
// 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.

#![allow(warnings)]

use approx_det::{assert_relative_eq, relative_eq};
use glam::{Dot, UnitQuat, UnitVec3, Vec2, Vec3};
use wasm_bindgen_test::*;

use super::common::{
    build_displaced_convexhull_test, get_input_wide, CollisionTestShape, Convex1ContactManifold,
    ConvexHullInput, Mvec3,
};
use crate::collision_tasks::tests::common::{
    convex4manifold_wide2convex4contact_manifold, Convex4ContactManifold, TestInput,
};
use crate::collision_tasks::ShapeWideTester;
use crate::shapes::{ConvexHullWide, CylinderWide};
use crate::traits::PairWideTest;
use crate::{ConvexHull, ConvexHullId, Cylinder, ShapeContainer};

wasm_bindgen_test_configure!(run_in_browser);

type InputTypeData = TestInput<Cylinder, ConvexHullInput>;
type InputType = TestInput<Cylinder, ConvexHullId>;
const ADJUST_DEPTH_FACTOR: f32 = 1.0;

// Convex has a box shape in these tests.

fn test_cylinder_convex(input_data_bytes: &[u8], output_bytes: &[u8]) {
    let input_data: InputTypeData =
        serde_json::from_slice(input_data_bytes).expect("file should be proper JSON");
    let hull_points = input_data.b.vertices;
    let convex_hull = ConvexHull::new_unchecked(&hull_points);
    let mut container = ShapeContainer::default();
    let hull_id = container.add(convex_hull);

    let input = InputType {
        a: input_data.a,
        b: hull_id,
        speculative_margin: input_data.speculative_margin,
        offset_b: input_data.offset_b,
        orientation_a: input_data.orientation_a,
        orientation_b: input_data.orientation_b,
    };
    let array = [input];

    let mut output: Convex4ContactManifold =
        serde_json::from_slice(output_bytes).expect("file should be proper JSON");
    output.adjust_offset_a_to_shape_surface(ADJUST_DEPTH_FACTOR);
    let outputs = [output];

    let pair_count = array.len();
    TestWide!(
        CylinderWide,
        ConvexHullWide,
        array,
        pair_count,
        outputs,
        convex4manifold_wide2convex4contact_manifold,
        Some(&container),
        compare
    );
}
fn project(v: &Mvec3, x_axis: UnitVec3, y_axis: UnitVec3) -> Vec2 {
    let offset = Vec3::new(v.x, v.y, v.z);
    let x = x_axis.dot(offset);
    let y = y_axis.dot(offset);
    Vec2::new(x, y)
}

fn compare(v0: &Convex4ContactManifold, v1: &Convex4ContactManifold) {
    let normal = UnitVec3::from_array_unchecked([v0.Normal.x, v0.Normal.y, v0.Normal.z]);
    let (x_axis, y_axis) = normal.any_orthogonal_pair();
    let v0 = v0.to_array();
    let v1 = v1.to_array();
    all_elements_offset_match_unique(&v0, &v1, x_axis, y_axis);
}

fn all_elements_offset_match_unique(
    a: &[Convex1ContactManifold],
    b: &[Convex1ContactManifold],
    x_axis: UnitVec3,
    y_axis: UnitVec3,
) -> bool {
    let mut used_flags = vec![false; b.len()];

    for v0 in a {
        if !v0.contact_exists {
            continue;
        }
        let mut matched = false;
        for (j, v1) in b.iter().enumerate() {
            if !v1.contact_exists {
                continue;
            }
            if used_flags[j] {
                continue;
            }
            let offset_equal = {
                let v0_a0 = project(&v0.offset_a, x_axis, y_axis);
                let v1_a0 = project(&v1.offset_a, x_axis, y_axis);
                let distance = v0_a0.distance(v1_a0);
                distance < 1e-3
            };
            let normal_equal = relative_eq!(v0.normal, v1.normal, epsilon = 1e-4);
            let _ = v0.feature_id == v1.feature_id;
            let depth_equal = relative_eq!(v0.depth, v1.depth, epsilon = 1e-5);
            let contact_exist_equal = v0.contact_exists == v1.contact_exists;

            if contact_exist_equal && depth_equal && normal_equal {
                used_flags[j] = true;
                matched = true;
                break;
            }
        }
        assert!(matched, "v0:{v0:?} \n b:{b:?}");
        if !matched {
            return false; // 有一个 v0 没找到匹配
        }
    }

    true
}
#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_01_cylinder_cap_close_to_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();
    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input01_cylinder_cap_close_to_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output01.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_02_cylinder_cap_tangent_to_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input02_cylinder_cap_tangent_to_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output02.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_03_cylinder_cap_penetrate_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input03_cylinder_cap_penetrate_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output03.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_04_cylinder_x_90_edge_close_to_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input04_cylinder_x_90_edge_close_to_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output04.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_05_cylinder_x_90_edge_tangent_to_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input05_cylinder_x_90_edge_tangent_to_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output05.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_06_cylinder_x_90_edge_penetrate_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input06_cylinder_x_90_edge_penetrate_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output06.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_07_cylinder_edge_penetrate_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input07_cylinder_edge_penetrate_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output07.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_08_no_contact() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!("resource/cylinder_convex_hull/input08_no_contact.json");
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output08.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_09_cylinder_bottom_cap_close_to_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input09_cylinder_bottom_cap_close_to_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output09.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_10_cylinder_cap_close_to_big_convex_face() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input10_cylinder_cap_close_to_big_convex_face.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output10.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_11_cylinder_cap_close_to_convex_vertex() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input11_cylinder_cap_close_to_convex_vertex.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output11.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_12_cylinder_cap_tangent_convex_vertex() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input12_cylinder_cap_tangent_convex_vertex.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output12.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_13_cylinder_cap_penetrate_convex_vertex() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input13_cylinder_cap_penetrate_convex_vertex.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output13.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

// this test is filtered by not(feature = "qhull") for some reasons
// 1. there are three best faces that can be chosen for the epsilon,and the manifold calculated by
//    every face is different
// 2. the face sequence is not equal in qhull or not(qhull) feature,so the best face will be
//    differently in the two feature,that result in different contact point count in manifold
// ignore this test for unstable algorithm issue 2177
#[test]
#[wasm_bindgen_test]
#[cfg(not(feature = "qhull"))]
#[ignore]
fn test_cylinder_convex_14_cylinder_edge_penetrate_convex_vertex() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input14_cylinder_edge_penetrate_convex_vertex.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output14.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_15_create_min_contact() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes =
        include_bytes!("resource/cylinder_convex_hull/input15_create_min_contact.json");
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output15.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_16_near_zero_angle_between_axis_and_edge_plane() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input16_near_zero_angle_between_axis_and_edge_plane.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output16.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_17_cylinder_face_and_convex_face_one_contact_point() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes = include_bytes!(
        "resource/cylinder_convex_hull/input17_cylinder_face_and_convex_face_one_contact_point.json"
    );
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output17.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_18_center_overlap() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes =
        include_bytes!("resource/cylinder_convex_hull/input18_center_overlap.json");
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output18.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

#[test]
#[wasm_bindgen_test]
fn test_cylinder_convex_19_empty_candidates() {
    let _ = env_logger::builder().is_test(true).try_init();

    let input_data_bytes =
        include_bytes!("resource/cylinder_convex_hull/input19_empty_candidates.json");
    let output_bytes = include_bytes!("resource/cylinder_convex_hull/output19.json");
    test_cylinder_convex(input_data_bytes, output_bytes);
}

//qhull has no "shift to center" param
#[test]
#[cfg(not(feature = "qhull"))]
fn test_cylinder_convex_shift() {
    let shape_a = Cylinder::new(1., 2.);
    let convexhull_cuboid_length = Vec3::new(4., 5., 6.);
    let convexhull_displacement_vec = Vec3::new(1e0, 1e4, 1e2);
    let desired_offset_b = Vec3::new(0., 4.1, 0.);
    let rotation_a = UnitQuat::from_euler_default(-1., -2., -3.);
    let rotation_b = UnitQuat::from_euler_default(1., 2., 3.);

    build_displaced_convexhull_test(
        &CollisionTestShape::Cylinder(shape_a),
        convexhull_cuboid_length,
        convexhull_displacement_vec,
        desired_offset_b,
        rotation_a,
        rotation_b,
    );
}