- contact solver wide ops + V32 now have real SSE2 and NEON paths selected by target arch; scalar fallback behind the disable-simd feature. All three paths are bit-identical (cross-arch determinism verified: same ragdoll hash on NEON, SSE2 under Rosetta, and scalar). - double-precision feature (C BOX3D_DOUBLE_PRECISION): f64 world positions with the exact C boundary-function semantics; enables the far-from-origin test halves (157 tests in DP mode, 151 default). - world snapshots: recording substrate subset (buffer/writers/geometry registry/readers) + world_snapshot.c port; bit-identical continuation after restore, corrupt-image rejection. - examples/benchmark.rs: all 10 C benchmark scenarios; serial Rust runs 1.05-1.55x slower than C -O2 at one worker (geomean ~1.3x with fat LTO). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1146 lines
39 KiB
Rust
1146 lines
39 KiB
Rust
// Port of box3d/test/test_world.c (+ the scene helpers it uses from
|
|
// box3d/shared/benchmarks.c, stability.c, overflow_color.c).
|
|
//
|
|
// Adaptations from C (documented per test):
|
|
// - The Rust port has no global world registry: destroy_world consumes the
|
|
// World, so the C `b3World_IsValid(worldId) == false` checks after destroy
|
|
// are not expressible and are dropped.
|
|
// - Worker counts are ignored (always serial): world_set_worker_count is a
|
|
// no-op stub and world_get_worker_count always returns 1.
|
|
// - user data is u64 instead of void*.
|
|
|
|
use makepad_box3d::body::*;
|
|
use makepad_box3d::compound::create_compound;
|
|
use makepad_box3d::constants::GRAPH_COLOR_COUNT;
|
|
use makepad_box3d::ensure;
|
|
use makepad_box3d::ensure_small;
|
|
use makepad_box3d::hull::{create_cylinder, create_rock, make_box_hull, make_cube_hull, make_offset_box_hull};
|
|
use makepad_box3d::id::WorldId;
|
|
use makepad_box3d::math_functions::{compute_cos_sin, length, pos, vec3, Pos, Quat, Transform, Vec3, WorldTransform, PI};
|
|
use makepad_box3d::mesh::create_wave_mesh;
|
|
use makepad_box3d::physics_world::*;
|
|
use makepad_box3d::shape::*;
|
|
use makepad_box3d::test_utils::{random_vec3_uniform, set_random_seed};
|
|
use makepad_box3d::types::*;
|
|
|
|
fn world_id_of(world: &World) -> WorldId {
|
|
WorldId { index1: world.world_id + 1, generation: world.generation }
|
|
}
|
|
|
|
// This is a simple example of building and running a simulation
|
|
// using Box3D. Here we create a large ground box and a small dynamic box.
|
|
#[test]
|
|
fn hello_world() {
|
|
// Construct a world object, which will hold and simulate the rigid bodies.
|
|
let mut world_def = default_world_def();
|
|
world_def.gravity = vec3(0.0, -10.0, 0.0);
|
|
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_is_valid(&world, world_id_of(&world)));
|
|
|
|
// Define the ground body.
|
|
let mut ground_body_def = default_body_def();
|
|
ground_body_def.position = pos(0.0, -10.0, 0.0);
|
|
|
|
let ground_id = create_body(&mut world, &ground_body_def);
|
|
ensure!(body_is_valid(&world, ground_id));
|
|
|
|
// Define the ground box shape. The extents are the half-widths of the box.
|
|
let ground_box = make_box_hull(50.0, 10.0, 50.0);
|
|
|
|
// Add the box shape to the ground body.
|
|
let ground_shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
|
|
|
|
// Define the dynamic body. We set its position and call the body factory.
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = pos(0.0, 4.0, 0.0);
|
|
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
// Define another box shape for our dynamic body.
|
|
let dynamic_box = make_cube_hull(1.0);
|
|
|
|
// Define the dynamic body shape
|
|
let mut shape_def = default_shape_def();
|
|
|
|
// Set the box density to be non-zero, so it will be dynamic.
|
|
shape_def.density = 1.0;
|
|
|
|
// Override the default friction.
|
|
shape_def.base_material.friction = 0.3;
|
|
|
|
// Add the shape to the body.
|
|
create_hull_shape(&mut world, body_id, &shape_def, &dynamic_box);
|
|
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 4;
|
|
|
|
let mut position = body_get_position(&world, body_id);
|
|
let mut rotation = body_get_rotation(&world, body_id);
|
|
|
|
// This is our little game loop.
|
|
for _ in 0..90 {
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
|
|
position = body_get_position(&world, body_id);
|
|
rotation = body_get_rotation(&world, body_id);
|
|
}
|
|
|
|
destroy_world(world);
|
|
|
|
ensure_small!(position.y - 1.00, 0.01);
|
|
ensure_small!(rotation.v.x, 0.01);
|
|
ensure_small!(rotation.v.z, 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_world() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_is_valid(&world, world_id_of(&world)));
|
|
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 1;
|
|
|
|
for _ in 0..60 {
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
}
|
|
|
|
destroy_world(world);
|
|
// C: ENSURE(b3World_IsValid(worldId) == false) — not expressible with an owned World.
|
|
}
|
|
|
|
const BODY_COUNT: usize = 10;
|
|
|
|
#[test]
|
|
fn destroy_all_bodies_world() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut count = 0usize;
|
|
let mut creating = true;
|
|
|
|
let mut body_ids = [makepad_box3d::id::NULL_BODY_ID; BODY_COUNT];
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
let cube = make_cube_hull(0.5);
|
|
|
|
for _ in 0..(2 * BODY_COUNT + 10) {
|
|
if creating {
|
|
if count < BODY_COUNT {
|
|
body_ids[count] = create_body(&mut world, &body_def);
|
|
|
|
let shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, body_ids[count], &shape_def, &cube);
|
|
count += 1;
|
|
} else {
|
|
creating = false;
|
|
}
|
|
} else if count > 0 {
|
|
destroy_body(&mut world, body_ids[count - 1]);
|
|
body_ids[count - 1] = makepad_box3d::id::NULL_BODY_ID;
|
|
count -= 1;
|
|
}
|
|
|
|
world_step(&mut world, 1.0 / 60.0, 3);
|
|
}
|
|
|
|
let counters = world_get_counters(&world);
|
|
ensure!(counters.body_count == 0);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_valid() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_is_valid(&world, world_id_of(&world)));
|
|
|
|
let body_def = default_body_def();
|
|
|
|
let body_id1 = create_body(&mut world, &body_def);
|
|
ensure!(body_is_valid(&world, body_id1) == true);
|
|
|
|
let body_id2 = create_body(&mut world, &body_def);
|
|
ensure!(body_is_valid(&world, body_id2) == true);
|
|
|
|
destroy_body(&mut world, body_id1);
|
|
ensure!(body_is_valid(&world, body_id1) == false);
|
|
|
|
destroy_body(&mut world, body_id2);
|
|
ensure!(body_is_valid(&world, body_id2) == false);
|
|
|
|
destroy_world(world);
|
|
// C checks world/body validity after world destroy — not expressible with an owned World.
|
|
}
|
|
|
|
const WORLD_COUNT: usize = 128 / 2; // B3_MAX_WORLDS / 2
|
|
|
|
#[test]
|
|
fn test_world_recycle() {
|
|
let count = 100;
|
|
|
|
for _ in 0..count {
|
|
let world_def = default_world_def();
|
|
let mut worlds: Vec<World> = Vec::with_capacity(WORLD_COUNT);
|
|
for _ in 0..WORLD_COUNT {
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_is_valid(&world, world_id_of(&world)));
|
|
|
|
let body_def = default_body_def();
|
|
create_body(&mut world, &body_def);
|
|
worlds.push(world);
|
|
}
|
|
|
|
for world in worlds.iter_mut() {
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 1;
|
|
|
|
for _ in 0..10 {
|
|
world_step(world, time_step, sub_step_count);
|
|
}
|
|
}
|
|
|
|
while let Some(world) = worlds.pop() {
|
|
destroy_world(world);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This test is here to ensure all API functions link correctly.
|
|
#[test]
|
|
fn test_world_coverage() {
|
|
let world_def = default_world_def();
|
|
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_is_valid(&world, world_id_of(&world)));
|
|
|
|
world_enable_sleeping(&mut world, true);
|
|
world_enable_sleeping(&mut world, false);
|
|
let mut flag = world_is_sleeping_enabled(&world);
|
|
ensure!(flag == false);
|
|
|
|
world_enable_continuous(&mut world, false);
|
|
world_enable_continuous(&mut world, true);
|
|
flag = world_is_continuous_enabled(&world);
|
|
ensure!(flag == true);
|
|
|
|
world_set_restitution_threshold(&mut world, 0.0);
|
|
world_set_restitution_threshold(&mut world, 2.0);
|
|
let mut value = world_get_restitution_threshold(&world);
|
|
ensure!(value == 2.0);
|
|
|
|
world_set_hit_event_threshold(&mut world, 0.0);
|
|
world_set_hit_event_threshold(&mut world, 100.0);
|
|
value = world_get_hit_event_threshold(&world);
|
|
ensure!(value == 100.0);
|
|
|
|
// C passes fn pointers with a NULL context; the port takes closures.
|
|
world_set_custom_filter_callback(&mut world, Some(Box::new(|_a, _b| true)));
|
|
world_set_pre_solve_callback(&mut world, Some(Box::new(|_a, _b, _point, _normal| false)));
|
|
|
|
let g = vec3(1.0, 2.0, 0.0);
|
|
world_set_gravity(&mut world, g);
|
|
let v = world_get_gravity(&world);
|
|
ensure!(v.x == g.x);
|
|
ensure!(v.y == g.y);
|
|
|
|
let explosion_def = default_explosion_def();
|
|
world_explode(&mut world, &explosion_def);
|
|
|
|
world_set_contact_tuning(&mut world, 10.0, 2.0, 4.0);
|
|
|
|
world_set_maximum_linear_speed(&mut world, 10.0);
|
|
value = world_get_maximum_linear_speed(&world);
|
|
ensure!(value == 10.0);
|
|
|
|
world_enable_warm_starting(&mut world, true);
|
|
flag = world_is_warm_starting_enabled(&world);
|
|
ensure!(flag == true);
|
|
|
|
let count = world_get_awake_body_count(&world);
|
|
ensure!(count == 0);
|
|
|
|
world_set_user_data(&mut world, 0xDEADBEEF);
|
|
let user_data = world_get_user_data(&world);
|
|
ensure!(user_data == 0xDEADBEEF);
|
|
|
|
world_step(&mut world, 1.0, 1);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sensor() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Wall from x = 1 to x = 2
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
body_def.position = pos(1.5, 11.0, 0.0);
|
|
let wall_id = create_body(&mut world, &body_def);
|
|
let box_hull = make_box_hull(0.5, 10.0, 1.0);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.enable_sensor_events = true;
|
|
create_hull_shape(&mut world, wall_id, &shape_def, &box_hull);
|
|
|
|
// Bullet fired towards the wall
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.is_bullet = true;
|
|
body_def.gravity_scale = 0.0;
|
|
body_def.position = pos(7.39814, 4.0, 0.0);
|
|
body_def.linear_velocity = vec3(-20.0, 0.0, 0.0);
|
|
let bullet_id = create_body(&mut world, &body_def);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.is_sensor = true;
|
|
shape_def.enable_sensor_events = true;
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.1 };
|
|
create_sphere_shape(&mut world, bullet_id, &shape_def, &sphere);
|
|
|
|
let mut begin_count = 0;
|
|
let mut end_count = 0;
|
|
|
|
loop {
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 4;
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
|
|
let bullet_pos = body_get_position(&world, bullet_id);
|
|
|
|
let events = world_get_sensor_events(&world);
|
|
|
|
if !events.begin_events.is_empty() {
|
|
begin_count += 1;
|
|
}
|
|
|
|
if !events.end_events.is_empty() {
|
|
end_count += 1;
|
|
}
|
|
|
|
if bullet_pos.x < -1.0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(begin_count == 1);
|
|
ensure!(end_count == 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contact_events() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Static ground
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
body_def.position = pos(0.0, -0.5, 0.0);
|
|
let ground_id = create_body(&mut world, &body_def);
|
|
let ground_box = make_box_hull(10.0, 0.5, 10.0);
|
|
let ground_shape_def = default_shape_def();
|
|
let ground_shape_id = create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
|
|
|
|
// Dynamic sphere dropped onto the ground; restitution causes it to bounce so we get end events
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = pos(0.0, 5.0, 0.0);
|
|
let sphere_body_id = create_body(&mut world, &body_def);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = 1.0;
|
|
shape_def.enable_contact_events = true;
|
|
shape_def.base_material.restitution = 0.6;
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
let sphere_shape_id = create_sphere_shape(&mut world, sphere_body_id, &shape_def, &sphere);
|
|
|
|
let mut begin_count = 0;
|
|
let mut end_count = 0;
|
|
let mut ids_checked = false;
|
|
|
|
for _ in 0..120 {
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
|
|
let (first_begin, begin_len, end_len) = {
|
|
let events = world_get_contact_events(&world);
|
|
(
|
|
events.begin_events.first().copied(),
|
|
events.begin_events.len(),
|
|
events.end_events.len(),
|
|
)
|
|
};
|
|
|
|
if begin_len > 0 && ids_checked == false {
|
|
let be = first_begin.unwrap();
|
|
let a_is_sphere = be.shape_id_a == sphere_shape_id;
|
|
let b_is_sphere = be.shape_id_b == sphere_shape_id;
|
|
let a_is_ground = be.shape_id_a == ground_shape_id;
|
|
let b_is_ground = be.shape_id_b == ground_shape_id;
|
|
ensure!((a_is_sphere && b_is_ground) || (a_is_ground && b_is_sphere));
|
|
ensure!(contact_is_valid(&world, be.contact_id));
|
|
ids_checked = true;
|
|
}
|
|
|
|
begin_count += begin_len;
|
|
end_count += end_len;
|
|
}
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(ids_checked);
|
|
ensure!(begin_count >= 1);
|
|
ensure!(end_count >= 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_hit_events() {
|
|
let mut world_def = default_world_def();
|
|
world_def.hit_event_threshold = 1.0;
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Static ground
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
body_def.position = pos(0.0, -0.5, 0.0);
|
|
let ground_id = create_body(&mut world, &body_def);
|
|
let ground_box = make_box_hull(10.0, 0.5, 10.0);
|
|
let ground_shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
|
|
|
|
// Sphere driven into the ground fast enough to clear the hit threshold
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.gravity_scale = 0.0;
|
|
body_def.position = pos(0.0, 2.0, 0.0);
|
|
body_def.linear_velocity = vec3(0.0, -30.0, 0.0);
|
|
let sphere_body_id = create_body(&mut world, &body_def);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = 1.0;
|
|
shape_def.enable_hit_events = true;
|
|
shape_def.base_material.user_material_id = 7;
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
create_sphere_shape(&mut world, sphere_body_id, &shape_def, &sphere);
|
|
|
|
let mut hit_count = 0;
|
|
let mut captured_speed = 0.0;
|
|
let mut captured_material_a = 0u64;
|
|
let mut captured_material_b = 0u64;
|
|
let mut captured_normal = vec3(0.0, 0.0, 0.0);
|
|
|
|
for _ in 0..30 {
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
|
|
let events = world_get_contact_events(&world);
|
|
if !events.hit_events.is_empty() && hit_count == 0 {
|
|
let hit = events.hit_events[0];
|
|
captured_speed = hit.approach_speed;
|
|
captured_normal = hit.normal;
|
|
captured_material_a = hit.user_material_id_a;
|
|
captured_material_b = hit.user_material_id_b;
|
|
}
|
|
|
|
hit_count += events.hit_events.len();
|
|
}
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(hit_count >= 1);
|
|
ensure!(captured_speed > 1.0);
|
|
// Head-on vertical impact: normal lies along Y
|
|
ensure_small!(captured_normal.x, 0.01);
|
|
ensure_small!(captured_normal.z, 0.01);
|
|
// One side of the contact carries the sphere's user material
|
|
ensure!(captured_material_a == 7 || captured_material_b == 7);
|
|
}
|
|
|
|
// Hit-event material lookup must respect the compound child that participated in the
|
|
// contact. Two children with distinct userMaterialIds at separated positions, dropped
|
|
// sphere strikes one specifically.
|
|
#[test]
|
|
fn test_compound_hit_events() {
|
|
const HULL_MATERIAL_A: u64 = 11;
|
|
const HULL_MATERIAL_B: u64 = 22;
|
|
const SPHERE_MATERIAL: u64 = 99;
|
|
const HULL_CENTER_X: f32 = 3.0;
|
|
|
|
for side in 0..2 {
|
|
let expected_hull_material = if side == 0 { HULL_MATERIAL_A } else { HULL_MATERIAL_B };
|
|
let spawn_x = if side == 0 { -HULL_CENTER_X } else { HULL_CENTER_X };
|
|
|
|
let mut world_def = default_world_def();
|
|
world_def.hit_event_threshold = 1.0;
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Build a compound with two hulls at opposite x positions, distinct userMaterialIds
|
|
let box_a = make_box_hull(1.0, 1.0, 1.0);
|
|
let box_b = make_box_hull(1.0, 1.0, 1.0);
|
|
|
|
let mut mat_a = default_surface_material();
|
|
mat_a.user_material_id = HULL_MATERIAL_A;
|
|
|
|
let mut mat_b = default_surface_material();
|
|
mat_b.user_material_id = HULL_MATERIAL_B;
|
|
|
|
let compound_def = CompoundDef {
|
|
hulls: vec![
|
|
CompoundHullDef {
|
|
hull: box_a.clone(),
|
|
transform: Transform { p: vec3(-HULL_CENTER_X, 0.0, 0.0), q: Quat::IDENTITY },
|
|
material: mat_a,
|
|
},
|
|
CompoundHullDef {
|
|
hull: box_b.clone(),
|
|
transform: Transform { p: vec3(HULL_CENTER_X, 0.0, 0.0), q: Quat::IDENTITY },
|
|
material: mat_b,
|
|
},
|
|
],
|
|
..Default::default()
|
|
};
|
|
let compound = create_compound(&compound_def);
|
|
|
|
// Static body holds the compound
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
let compound_body_id = create_body(&mut world, &body_def);
|
|
let compound_shape_def = default_shape_def();
|
|
create_compound_shape(&mut world, compound_body_id, &compound_shape_def, &compound);
|
|
|
|
// Sphere driven straight down onto the chosen child
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.gravity_scale = 0.0;
|
|
body_def.position = pos(spawn_x, 3.0, 0.0);
|
|
body_def.linear_velocity = vec3(0.0, -30.0, 0.0);
|
|
let sphere_body_id = create_body(&mut world, &body_def);
|
|
let mut sphere_shape_def = default_shape_def();
|
|
sphere_shape_def.density = 1.0;
|
|
sphere_shape_def.enable_hit_events = true;
|
|
sphere_shape_def.base_material.user_material_id = SPHERE_MATERIAL;
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
create_sphere_shape(&mut world, sphere_body_id, &sphere_shape_def, &sphere);
|
|
|
|
let mut hit_count = 0;
|
|
let mut captured_material_a = 0u64;
|
|
let mut captured_material_b = 0u64;
|
|
|
|
for _ in 0..30 {
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
|
|
let events = world_get_contact_events(&world);
|
|
if !events.hit_events.is_empty() && hit_count == 0 {
|
|
let hit = events.hit_events[0];
|
|
captured_material_a = hit.user_material_id_a;
|
|
captured_material_b = hit.user_material_id_b;
|
|
}
|
|
hit_count += events.hit_events.len();
|
|
}
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(hit_count >= 1);
|
|
// Sphere material on one side
|
|
ensure!(captured_material_a == SPHERE_MATERIAL || captured_material_b == SPHERE_MATERIAL);
|
|
// Struck compound child's material on the other side.
|
|
ensure!(captured_material_a == expected_hull_material || captured_material_b == expected_hull_material);
|
|
}
|
|
}
|
|
|
|
struct JunkyardData {
|
|
pusher_id: makepad_box3d::id::BodyId,
|
|
degrees: f32,
|
|
radius: f32,
|
|
}
|
|
|
|
// Port of CreateJunkyard from box3d/shared/benchmarks.c using the debug body
|
|
// count (2 layers instead of 24) — the C test build uses the same reduction.
|
|
fn create_junkyard(world: &mut World) -> JunkyardData {
|
|
let ground_id;
|
|
{
|
|
let mut body_def = default_body_def();
|
|
body_def.position.y = -1.0;
|
|
ground_id = create_body(world, &body_def);
|
|
}
|
|
|
|
{
|
|
let shape_def = default_shape_def();
|
|
{
|
|
let box_hull = make_box_hull(120.0, 1.0, 120.0);
|
|
create_hull_shape(world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
{
|
|
let box_hull = make_offset_box_hull(1.0, 8.0, 50.0, vec3(-50.0, 8.0, 0.0));
|
|
create_hull_shape(world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
{
|
|
let box_hull = make_offset_box_hull(1.0, 8.0, 50.0, vec3(50.0, 8.0, 0.0));
|
|
create_hull_shape(world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
{
|
|
let box_hull = make_offset_box_hull(50.0, 8.0, 1.0, vec3(0.0, 8.0, -50.0));
|
|
create_hull_shape(world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
{
|
|
let box_hull = make_offset_box_hull(50.0, 8.0, 1.0, vec3(0.0, 8.0, 50.0));
|
|
create_hull_shape(world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
}
|
|
{
|
|
let rock_hull = create_rock(1.5);
|
|
|
|
let count = 2; // BENCHMARK_DEBUG variant
|
|
let height = 24.0;
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
let shape_def = default_shape_def();
|
|
for y in 0..count {
|
|
for x in 0..=20 {
|
|
for z in 0..=20 {
|
|
let px = -40.0 + 4.0 * x as f32;
|
|
let py = 4.0 * y as f32 + height + 1.0;
|
|
let pz = -40.0 + 4.0 * z as f32; body_def.position = pos(px, py, pz);
|
|
let body_id = create_body(world, &body_def);
|
|
create_hull_shape(world, body_id, &shape_def, &rock_hull);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let radius = 35.0;
|
|
let m_height = 24.0;
|
|
|
|
let hull = create_cylinder(m_height, 4.0, 0.0, 16);
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Kinematic;
|
|
body_def.position = pos(radius, 0.0, 0.0);
|
|
let pusher_id = create_body(world, &body_def);
|
|
let shape_def = default_shape_def();
|
|
create_hull_shape(world, pusher_id, &shape_def, &hull);
|
|
|
|
JunkyardData { pusher_id, degrees: 0.0, radius }
|
|
}
|
|
|
|
// Port of StepJunkyard: note the C version only drives the kinematic pusher; it
|
|
// does not step the world.
|
|
fn step_junkyard(world: &mut World, data: &mut JunkyardData) {
|
|
let time_step = 1.0 / 60.0;
|
|
let omega = -6.0;
|
|
data.degrees += omega * time_step;
|
|
let cs = compute_cos_sin(data.degrees * PI / 180.0);
|
|
let r = data.radius;
|
|
let target_pos: Pos = pos(r * cs.cosine, 0.0, r * cs.sine);
|
|
let target = WorldTransform { p: target_pos, q: Quat::IDENTITY };
|
|
body_set_target_transform(world, data.pusher_id, target, time_step, false);
|
|
}
|
|
|
|
// Adapted: the Rust port is always serial. world_set_worker_count is a no-op
|
|
// stub and world_get_worker_count always returns 1, so the C expectations of
|
|
// 4 / B3_MAX_WORKERS collapse to 1.
|
|
#[test]
|
|
fn test_set_worker_count() {
|
|
let mut world_def = default_world_def();
|
|
world_def.worker_count = 1;
|
|
let mut world = create_world(&world_def);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
let mut junkyard = create_junkyard(&mut world);
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
world_set_worker_count(&mut world, 4);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
world_set_worker_count(&mut world, 4);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
world_set_worker_count(&mut world, 0);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
world_set_worker_count(&mut world, -5);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
world_set_worker_count(&mut world, makepad_box3d::constants::MAX_WORKERS as i32 + 10);
|
|
ensure!(world_get_worker_count(&world) == 1);
|
|
|
|
step_junkyard(&mut world, &mut junkyard);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
// This tests continuous collision and mesh contact stability.
|
|
// Port of CreateMeshDrop from box3d/shared/stability.c.
|
|
#[test]
|
|
fn test_mesh_drop() {
|
|
let world_def = default_world_def();
|
|
// C requests 4 workers; the port is always serial.
|
|
|
|
let mut world = create_world(&world_def);
|
|
|
|
// CreateMeshDrop
|
|
{
|
|
let body_def = default_body_def();
|
|
let ground_id = create_body(&mut world, &body_def);
|
|
|
|
let grid_count = 40;
|
|
let cell_width = 1.0;
|
|
let row_hz = 0.1;
|
|
let column_hz = 0.2;
|
|
let ground_amplitude = 0.5;
|
|
|
|
let mesh = create_wave_mesh(grid_count, grid_count, cell_width, ground_amplitude, row_hz, column_hz);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.filter.category_bits = 1;
|
|
create_mesh_shape(&mut world, ground_id, &shape_def, &mesh, Vec3::ONE);
|
|
}
|
|
|
|
{
|
|
let box_hull = make_box_hull(0.02, 0.2, 0.04);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.base_material.rolling_resistance = 0.1;
|
|
|
|
// Don't allow shapes to collide with each other.
|
|
shape_def.filter.category_bits = 2;
|
|
shape_def.filter.mask_bits = 1;
|
|
|
|
set_random_seed(3963634789);
|
|
|
|
let grid_count = 32; // MESH_DROP_GRID_COUNT
|
|
|
|
for i in 0..grid_count {
|
|
for j in 0..grid_count {
|
|
let linear_velocity = random_vec3_uniform(-1.0, 1.0);
|
|
let angular_velocity = random_vec3_uniform(-5.0, 5.0);
|
|
|
|
body_def.position = pos(
|
|
0.5 * (i as f32 - 0.5 * grid_count as f32),
|
|
5.0,
|
|
0.5 * (j as f32 - 0.5 * grid_count as f32),
|
|
);
|
|
body_def.linear_velocity = linear_velocity;
|
|
body_def.angular_velocity = angular_velocity;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
create_hull_shape(&mut world, body_id, &shape_def, &box_hull);
|
|
}
|
|
}
|
|
}
|
|
|
|
let time_step = 1.0 / 60.0;
|
|
|
|
let mut step_index = 0;
|
|
let step_limit = 400;
|
|
|
|
while step_index < step_limit {
|
|
let sub_step_count = 4;
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
|
|
let move_count = world_get_body_events(&world).move_events.len();
|
|
if move_count == 0 {
|
|
// All bodies sleeping
|
|
break;
|
|
}
|
|
|
|
step_index += 1;
|
|
}
|
|
|
|
println!(" test_mesh_drop step_index = {}", step_index);
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(step_index < step_limit);
|
|
}
|
|
|
|
// Verifies the overflow solver path. The scene puts more dyn-dyn contacts on a
|
|
// single hub body than there are dynamic graph colors, so several land in the
|
|
// overflow color. Port of CreateOverflowColorPile from box3d/shared/overflow_color.c.
|
|
#[test]
|
|
fn test_overflow_color_pile() {
|
|
const RING_COUNT: usize = 5;
|
|
const PER_RING: usize = 5;
|
|
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
// Static ground (top surface at y = 0)
|
|
{
|
|
let mut body_def = default_body_def();
|
|
body_def.position = pos(0.0, -1.0, 0.0);
|
|
let ground_id = create_body(&mut world, &body_def);
|
|
|
|
let box_hull = make_box_hull(20.0, 1.0, 20.0);
|
|
let shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, ground_id, &shape_def, &box_hull);
|
|
}
|
|
|
|
// Tall, heavy hub.
|
|
let hub_half_x = 0.5f32;
|
|
let hub_half_y = 2.5f32;
|
|
let hub_half_z = 0.5f32;
|
|
{
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = pos(0.0, hub_half_y, 0.0);
|
|
let hub_id = create_body(&mut world, &body_def);
|
|
|
|
let box_hull = make_box_hull(hub_half_x, hub_half_y, hub_half_z);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = 50.0;
|
|
create_hull_shape(&mut world, hub_id, &shape_def, &box_hull);
|
|
}
|
|
|
|
// Neighbors: vertical rings around the hub, each box slightly overlapping
|
|
// the hub so a contact exists on the very first step.
|
|
let neighbor_half = 0.2f32;
|
|
let ring_radius = hub_half_x + neighbor_half - 0.03;
|
|
|
|
let neighbor_box = make_box_hull(neighbor_half, neighbor_half, neighbor_half);
|
|
let neighbor_shape = default_shape_def();
|
|
|
|
let ring_spacing = 0.5f32;
|
|
let base_y = neighbor_half + 0.05;
|
|
|
|
let _ = hub_half_z;
|
|
|
|
for ring in 0..RING_COUNT {
|
|
let y = base_y + ring_spacing * ring as f32;
|
|
|
|
// Offset alternate rings by half a slot.
|
|
let theta_offset = if (ring & 1) != 0 { PI / PER_RING as f32 } else { 0.0 };
|
|
|
|
for slot in 0..PER_RING {
|
|
let theta = theta_offset + (2.0 * PI * slot as f32) / PER_RING as f32;
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = pos(ring_radius * theta.cos(), y, ring_radius * theta.sin());
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
create_hull_shape(&mut world, body_id, &neighbor_shape, &neighbor_box);
|
|
}
|
|
}
|
|
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 4;
|
|
|
|
let step_count = 10;
|
|
for _ in 0..step_count {
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
}
|
|
|
|
// Confirm the scene actually populated the overflow color.
|
|
let counters = world_get_counters(&world);
|
|
let overflow_contacts = counters.color_counts[GRAPH_COLOR_COUNT - 1];
|
|
|
|
destroy_world(world);
|
|
|
|
ensure!(overflow_contacts > 0);
|
|
}
|
|
|
|
// b3Body_EnableSleep must sync bodySim/bodyState flags; the flag-sync assertion
|
|
// in validate_solver_sets fires on the next step otherwise.
|
|
#[test]
|
|
fn enable_sleep_flag_sync_test() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.enable_sleep = false;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
ensure!(body_is_sleep_enabled(&world, body_id) == false);
|
|
|
|
body_enable_sleep(&mut world, body_id, true);
|
|
ensure!(body_is_sleep_enabled(&world, body_id) == true);
|
|
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
// b3Body_SetBullet must not drift against b3SyncBodyFlags.
|
|
#[test]
|
|
fn set_bullet_drift_test() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
{
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.is_bullet = false;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
ensure!(body_is_bullet(&world, body_id) == false);
|
|
|
|
body_set_bullet(&mut world, body_id, true);
|
|
ensure!(body_is_bullet(&world, body_id) == true);
|
|
|
|
let mut locks = MotionLocks::default();
|
|
locks.linear_x = true;
|
|
body_set_motion_locks(&mut world, body_id, locks);
|
|
|
|
ensure!(body_is_bullet(&world, body_id) == true);
|
|
}
|
|
|
|
{
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.is_bullet = true;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
ensure!(body_is_bullet(&world, body_id) == true);
|
|
|
|
body_set_bullet(&mut world, body_id, false);
|
|
ensure!(body_is_bullet(&world, body_id) == false);
|
|
|
|
let mut locks = MotionLocks::default();
|
|
locks.linear_x = true;
|
|
body_set_motion_locks(&mut world, body_id, locks);
|
|
|
|
ensure!(body_is_bullet(&world, body_id) == false);
|
|
}
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
// Regression: b3Body_EnableSleep used to leak the world lock on a no-op change.
|
|
#[test]
|
|
fn enable_sleep_noop_unlock_test() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.enable_sleep = true;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
// No-op: enableSleep is already true. Must not leak the world lock.
|
|
body_enable_sleep(&mut world, body_id, true);
|
|
|
|
// Would assert in the unlocked-world guard if the lock had leaked.
|
|
body_enable_sleep(&mut world, body_id, false);
|
|
ensure!(body_is_sleep_enabled(&world, body_id) == false);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
#[test]
|
|
fn enable_contact_recycling_test() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
|
|
// Default is enabled
|
|
let body_a = create_body(&mut world, &body_def);
|
|
ensure!(body_is_contact_recycling_enabled(&world, body_a) == true);
|
|
|
|
body_enable_contact_recycling(&mut world, body_a, false);
|
|
ensure!(body_is_contact_recycling_enabled(&world, body_a) == false);
|
|
|
|
body_enable_contact_recycling(&mut world, body_a, true);
|
|
ensure!(body_is_contact_recycling_enabled(&world, body_a) == true);
|
|
|
|
// Per-def opt-out at creation
|
|
body_def.enable_contact_recycling = false;
|
|
let body_b = create_body(&mut world, &body_def);
|
|
ensure!(body_is_contact_recycling_enabled(&world, body_b) == false);
|
|
|
|
// Stepping after toggling must not trip the flag-sync validator
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
|
|
destroy_world(world);
|
|
}
|
|
|
|
// Identical hull data is shared through a reference counted world database.
|
|
#[test]
|
|
fn test_hull_database() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let box_hull = make_box_hull(0.5, 0.5, 0.5);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
let body_a = create_body(&mut world, &body_def);
|
|
let body_b = create_body(&mut world, &body_def);
|
|
|
|
let shape_def = default_shape_def();
|
|
|
|
// Two shapes built from identical data share one owned copy in the world database.
|
|
let shape_a = create_hull_shape(&mut world, body_a, &shape_def, &box_hull);
|
|
let shape_b = create_hull_shape(&mut world, body_b, &shape_def, &box_hull);
|
|
|
|
let got_a = shape_get_hull(&world, shape_a);
|
|
let got_b = shape_get_hull(&world, shape_b);
|
|
|
|
// Both shapes point at the single shared copy
|
|
ensure!(std::sync::Arc::ptr_eq(&got_a, &got_b));
|
|
|
|
// C also checks the shared copy is not the caller's stack hull. The Rust
|
|
// port stores the caller's Arc on first insert by design (Arc handles the
|
|
// lifetime), so that check does not apply.
|
|
|
|
// A box built independently must de-duplicate to the same shared copy.
|
|
let box2 = make_box_hull(0.5, 0.5, 0.5);
|
|
let body_c = create_body(&mut world, &body_def);
|
|
let shape_c = create_hull_shape(&mut world, body_c, &shape_def, &box2);
|
|
ensure!(std::sync::Arc::ptr_eq(&shape_get_hull(&world, shape_c), &got_a));
|
|
destroy_shape(&mut world, shape_c, true);
|
|
|
|
// Setting a shape's hull to its own sole shared copy must not free it mid update.
|
|
let box3 = make_box_hull(0.3, 0.3, 0.3);
|
|
let body_d = create_body(&mut world, &body_def);
|
|
let shape_d = create_hull_shape(&mut world, body_d, &shape_def, &box3);
|
|
let got_d = shape_get_hull(&world, shape_d);
|
|
shape_set_hull(&mut world, shape_d, &got_d);
|
|
ensure!(std::sync::Arc::ptr_eq(&shape_get_hull(&world, shape_d), &got_d));
|
|
destroy_shape(&mut world, shape_d, true);
|
|
|
|
// Releasing one reference keeps the other alive
|
|
destroy_shape(&mut world, shape_a, true);
|
|
let still_b = shape_get_hull(&world, shape_b);
|
|
ensure!(std::sync::Arc::ptr_eq(&still_b, &got_b));
|
|
|
|
destroy_shape(&mut world, shape_b, true);
|
|
|
|
// World destroy asserts the database drained to zero references
|
|
destroy_world(world);
|
|
}
|
|
|
|
struct ExplosionResult {
|
|
linear_velocity: Vec3,
|
|
angular_velocity: Vec3,
|
|
}
|
|
|
|
// Explode just off the +x side of a centered sphere and capture the impulse it
|
|
// receives. The result must not depend on how far the body sits from the origin.
|
|
fn run_explosion(base: Pos) -> ExplosionResult {
|
|
let mut world_def = default_world_def();
|
|
world_def.gravity = Vec3::ZERO;
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.position = base;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
let sphere = Sphere { center: Vec3::ZERO, radius: 1.0 };
|
|
let shape_def = default_shape_def();
|
|
create_sphere_shape(&mut world, body_id, &shape_def, &sphere);
|
|
|
|
// Blast sits 3 units along +x, so the body is pushed back along -x
|
|
let mut explosion_def = default_explosion_def();
|
|
explosion_def.position = makepad_box3d::math_functions::offset_pos(base, vec3(3.0, 0.0, 0.0));
|
|
explosion_def.radius = 5.0;
|
|
explosion_def.falloff = 0.0;
|
|
explosion_def.impulse_per_area = 10.0;
|
|
world_explode(&mut world, &explosion_def);
|
|
|
|
let result = ExplosionResult {
|
|
linear_velocity: body_get_linear_velocity(&world, body_id),
|
|
angular_velocity: body_get_angular_velocity(&world, body_id),
|
|
};
|
|
|
|
destroy_world(world);
|
|
result
|
|
}
|
|
|
|
#[test]
|
|
fn test_explosion() {
|
|
let origin = run_explosion(makepad_box3d::math_functions::POS_ZERO);
|
|
|
|
// Pushed away from the blast along -x. A centered sphere has no transverse
|
|
// or angular component.
|
|
ensure!(origin.linear_velocity.x < -1.0e-4);
|
|
ensure_small!(origin.linear_velocity.y, 1.0e-6);
|
|
ensure_small!(origin.linear_velocity.z, 1.0e-6);
|
|
ensure_small!(length(origin.angular_velocity), 1.0e-6);
|
|
|
|
// The same blast far from the origin must produce the same impulse.
|
|
let far = run_explosion(pos(1.0e7, 1.0e7, 1.0e7));
|
|
ensure_small!(far.linear_velocity.x - origin.linear_velocity.x, 1.0e-5);
|
|
ensure_small!(far.linear_velocity.y - origin.linear_velocity.y, 1.0e-5);
|
|
ensure_small!(far.linear_velocity.z - origin.linear_velocity.z, 1.0e-5);
|
|
}
|
|
|
|
// Ensure correct move events from bodies involved in CCD.
|
|
#[test]
|
|
fn test_continuous_move_event() {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
world_enable_continuous(&mut world, true);
|
|
|
|
// Thin static wall, near face at x = 0.1
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Static;
|
|
body_def.position = pos(0.0, 0.0, 0.0);
|
|
let wall_id = create_body(&mut world, &body_def);
|
|
let wall_box = make_box_hull(0.1, 5.0, 5.0);
|
|
let shape_def = default_shape_def();
|
|
create_hull_shape(&mut world, wall_id, &shape_def, &wall_box);
|
|
|
|
// Fast dynamic sphere fired at the wall.
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
body_def.gravity_scale = 0.0;
|
|
body_def.position = pos(3.0, 0.0, 0.0);
|
|
body_def.linear_velocity = vec3(-30.0, 0.0, 0.0);
|
|
let ball_id = create_body(&mut world, &body_def);
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = 1.0;
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.25 };
|
|
create_sphere_shape(&mut world, ball_id, &shape_def, &sphere);
|
|
|
|
let time_step = 1.0 / 60.0;
|
|
let sub_step_count = 4;
|
|
let mut have_move = false;
|
|
|
|
for _ in 0..30 {
|
|
world_step(&mut world, time_step, sub_step_count);
|
|
|
|
let xf = body_get_transform(&world, ball_id);
|
|
|
|
let events = world_get_body_events(&world);
|
|
for event in events.move_events {
|
|
if event.body_id != ball_id {
|
|
continue;
|
|
}
|
|
|
|
have_move = true;
|
|
|
|
// The move event must carry the same pose the body reports, CCD rewind included
|
|
ensure!(event.transform.p.x == xf.p.x);
|
|
ensure!(event.transform.p.y == xf.p.y);
|
|
ensure!(event.transform.p.z == xf.p.z);
|
|
ensure!(event.transform.q.v.x == xf.q.v.x);
|
|
ensure!(event.transform.q.v.y == xf.q.v.y);
|
|
ensure!(event.transform.q.v.z == xf.q.v.z);
|
|
ensure!(event.transform.q.s == xf.q.s);
|
|
}
|
|
}
|
|
|
|
ensure!(have_move == true);
|
|
|
|
// Tunnel check
|
|
let final_pos = body_get_position(&world, ball_id);
|
|
ensure!(0.2 < final_pos.x && final_pos.x < 0.8);
|
|
|
|
destroy_world(world);
|
|
}
|