use gizmo_core::world::World;
use gizmo_math::Vec3;
use gizmo_physics_core::{BoxShape, Collider, ColliderShape, Transform};
use gizmo_physics_rigid::components::RigidBody;
pub const MIN_HE: f32 = 1e-4;
pub fn derived_box_half_extents(scale: Vec3, base: Vec3) -> Vec3 {
(scale * base).abs().max(Vec3::splat(MIN_HE))
}
#[derive(Debug, Clone, Copy)]
pub struct AutoBoxCollider {
pub base: Vec3,
}
impl AutoBoxCollider {
pub fn new() -> Self {
Self { base: Vec3::ONE }
}
pub fn scaled(base: Vec3) -> Self {
Self { base }
}
}
impl Default for AutoBoxCollider {
fn default() -> Self {
Self::new()
}
}
gizmo_core::impl_component!(AutoBoxCollider);
pub struct AutoBoxColliderSystem;
impl gizmo_core::system::System for AutoBoxColliderSystem {
fn access_info(&self) -> gizmo_core::system::AccessInfo {
let mut info = gizmo_core::system::AccessInfo::new();
info.is_exclusive = true;
info
}
#[tracing::instrument(skip_all, level = "trace", name = "auto_box_collider")]
fn run(&mut self, world: &World, dt: f32) {
use gizmo_core::commands::Commands;
use gizmo_core::query::{Added, Mut};
use gizmo_core::system::SystemParam;
let mut commands = Commands::fetch(world, dt).ok();
if commands.is_none() {
tracing::trace!(
"AutoBoxColliderSystem: Commands (CommandQueue) yok — işaretler boyutlanacak ama kaldırılamayacak"
);
}
let mut resolved = 0usize;
let mut skipped_non_box = 0usize;
let mut inertia_refreshed = 0usize;
if let Some(mut q) = unsafe {
world
.query_unchecked::<(&Transform, Mut<Collider>, &AutoBoxCollider, Added<AutoBoxCollider>)>()
} {
for (id, (t, mut col, cfg, _)) in q.iter_mut() {
if !matches!(col.shape, ColliderShape::Box(_)) {
skipped_non_box += 1;
tracing::warn!(
entity = id,
"AutoBoxCollider kutu-olmayan collider'a takılı — atlanıyor"
);
continue;
}
let he = derived_box_half_extents(t.scale, cfg.base);
col.shape = ColliderShape::Box(BoxShape { half_extents: he });
resolved += 1;
if let (Some(cmds), Some(e)) = (commands.as_mut(), world.entity(id)) {
cmds.entity(e).remove::<AutoBoxCollider>();
}
}
}
if let Some(mut q) = unsafe {
world
.query_unchecked::<(&Transform, Mut<RigidBody>, &Collider, &AutoBoxCollider, Added<AutoBoxCollider>)>()
} {
for (_id, (t, mut rb, col, cfg, _)) in q.iter_mut() {
if !matches!(col.shape, ColliderShape::Box(_)) {
continue;
}
let he = derived_box_half_extents(t.scale, cfg.base);
rb.update_inertia_from_collider(&Collider::box_collider(he));
inertia_refreshed += 1;
}
}
if resolved > 0 || skipped_non_box > 0 {
tracing::debug!(
resolved,
inertia_refreshed,
skipped_non_box,
"AutoBoxCollider: taze işaretler Transform.scale'den çözüldü"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use gizmo_core::commands::CommandQueue;
use gizmo_core::system::System;
fn world_with_commands() -> World {
let mut world = World::new();
world.insert_resource(CommandQueue::default());
world
}
#[derive(Default)]
struct ProbeCapture {
he: Option<Vec3>,
}
struct ProbeSystem;
impl System for ProbeSystem {
fn access_info(&self) -> gizmo_core::system::AccessInfo {
let mut i = gizmo_core::system::AccessInfo::new();
i.is_exclusive = true;
i
}
fn run(&mut self, world: &World, _dt: f32) {
let mut captured = None;
if let Some(q) = world.query::<&Collider>() {
for (_id, col) in q.iter() {
if let ColliderShape::Box(b) = &col.shape {
captured = Some(b.half_extents);
break;
}
}
}
if let Some(mut cap) = world.get_resource_mut::<ProbeCapture>() {
cap.he = captured;
}
}
}
struct NoopSystem;
impl System for NoopSystem {
fn access_info(&self) -> gizmo_core::system::AccessInfo {
gizmo_core::system::AccessInfo::new()
}
fn run(&mut self, _w: &World, _dt: f32) {}
}
#[test]
fn marker_spawned_after_schedule_run_is_missed_by_added_gate() {
use gizmo_core::system::{Schedule, SystemConfig};
let mut world = world_with_commands();
let mut schedule = Schedule::new();
schedule.add_di_system(
SystemConfig::new(Box::new(AutoBoxColliderSystem))
.label("auto_box_collider")
.before("physics_step"),
);
schedule.add_di_system(SystemConfig::new(Box::new(NoopSystem)).label("physics_step"));
schedule.run(&mut world, 0.016);
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::new(12.0, 0.6, 10.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new_static());
world.add_component(e, AutoBoxCollider::new());
world.apply_commands();
schedule.run(&mut world, 0.016);
schedule.run(&mut world, 0.016);
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(
b.half_extents,
Vec3::ONE,
"update-hook marker'ı Added ile çözülmez — tuzak belgelendi"
),
_ => panic!("kutu olmalı"),
}
}
#[test]
fn resolver_runs_before_physics_step_label() {
use gizmo_core::system::{Schedule, SystemConfig};
let mut world = world_with_commands();
world.insert_resource(ProbeCapture::default());
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::new(2.0, 3.0, 4.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::new());
let mut schedule = Schedule::new();
schedule.add_di_system(
SystemConfig::new(Box::new(AutoBoxColliderSystem))
.label("auto_box_collider")
.before("physics_step"),
);
schedule.add_di_system(SystemConfig::new(Box::new(ProbeSystem)).label("physics_step"));
schedule.run(&mut world, 0.016);
let cap = world.get_resource::<ProbeCapture>().unwrap();
assert_eq!(
cap.he,
Some(Vec3::new(2.0, 3.0, 4.0)),
"physics_step çalıştığında collider ölçekli olmalıydı (resolver .before ile bağlanmadı mı?)"
);
}
#[test]
fn resolves_box_from_scale_and_derives_inertia() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::new(2.0, 3.0, 4.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE)); world.add_component(e, RigidBody::new(10.0, true));
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0); AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::new(2.0, 3.0, 4.0)),
_ => panic!("kutu olmalı"),
}
let mut reference = RigidBody::new(10.0, true);
reference.update_inertia_from_collider(&Collider::box_collider(Vec3::new(2.0, 3.0, 4.0)));
let rb = world.borrow::<RigidBody>().get(e.id()).cloned().unwrap();
assert_eq!(rb.local_inertia, reference.local_inertia);
assert!(world.borrow::<AutoBoxCollider>().get(e.id()).is_none());
}
#[test]
fn base_factor_halves_extents() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::splat(4.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::scaled(Vec3::splat(0.5)));
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::splat(2.0)),
_ => panic!("kutu olmalı"),
}
}
#[test]
fn runs_once_via_added_gate_without_commands() {
let mut world = World::new(); let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::new(2.0, 2.0, 2.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
assert!(world.borrow::<AutoBoxCollider>().get(e.id()).is_some(), "işaret kalmalı");
{
let mut q = world.borrow_mut::<Collider>();
let mut c = q.get_mut(e.id()).unwrap();
c.shape = ColliderShape::Box(BoxShape { half_extents: Vec3::splat(9.0) });
}
let prev = world.tick;
world.begin_change_frame(prev);
AutoBoxColliderSystem.run(&world, 0.016);
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::splat(9.0)),
_ => panic!("kutu olmalı"),
}
}
#[test]
fn non_box_collider_is_skipped() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::splat(3.0)));
world.add_component(e, Collider::sphere(0.5));
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
assert!(matches!(col.shape, ColliderShape::Sphere(_)), "küre korunmalı");
}
#[test]
fn degenerate_scale_is_clamped() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::new(0.0, 2.0, 3.0)));
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => {
assert_eq!(b.half_extents.x, MIN_HE);
assert_eq!(b.half_extents.y, 2.0);
}
_ => panic!("kutu olmalı"),
}
}
#[test]
fn resize_preserves_material() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::splat(2.0)));
world.add_component(
e,
Collider::box_collider(Vec3::ONE)
.with_friction(0.85)
.with_restitution(0.3),
);
world.add_component(e, RigidBody::new(1.0, true));
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::splat(2.0)),
_ => panic!("kutu olmalı"),
}
assert_eq!(col.material.static_friction, 0.85);
assert_eq!(col.material.restitution, 0.3);
}
#[test]
fn trigger_only_without_rigidbody_no_panic() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(e, Transform::new(Vec3::ZERO).with_scale(Vec3::splat(5.0)));
let mut trig = Collider::box_collider(Vec3::ONE);
trig.is_trigger = true;
world.add_component(e, trig);
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016); world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::splat(5.0)),
_ => panic!("kutu olmalı"),
}
assert!(col.is_trigger, "trigger bayrağı korunmalı");
}
#[test]
fn static_body_resizes_without_panic() {
let mut world = world_with_commands();
let e = world.spawn();
world.add_component(
e,
Transform::new(Vec3::ZERO).with_scale(Vec3::new(600.0, 1.0, 600.0)),
);
world.add_component(e, Collider::box_collider(Vec3::ONE));
world.add_component(e, RigidBody::new_static());
world.add_component(e, AutoBoxCollider::new());
world.begin_change_frame(0);
AutoBoxColliderSystem.run(&world, 0.016);
world.apply_commands();
let col = world.borrow::<Collider>().get(e.id()).cloned().unwrap();
match col.shape {
ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::new(600.0, 1.0, 600.0)),
_ => panic!("kutu olmalı"),
}
}
#[test]
fn derived_helper_is_pure_and_guards() {
assert_eq!(
derived_box_half_extents(Vec3::new(2.0, 0.5, 2.0), Vec3::ONE),
Vec3::new(2.0, 0.5, 2.0)
);
assert_eq!(
derived_box_half_extents(Vec3::splat(4.0), Vec3::splat(0.5)),
Vec3::splat(2.0)
);
assert_eq!(
derived_box_half_extents(Vec3::new(-3.0, 2.0, 1.0), Vec3::ONE),
Vec3::new(3.0, 2.0, 1.0)
);
assert_eq!(derived_box_half_extents(Vec3::ZERO, Vec3::ONE), Vec3::splat(MIN_HE));
}
}