From 975fb27494c5fefd8f5c90a3e2d2b7d3f5909ce8 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 23 Aug 2026 18:28:02 +0200 Subject: [PATCH] refactor(physics): defer static collision responses Split type-one and type-two detection from response resolution so the retained candidate can be applied to the motion state that exists after the complete record scan. Existing response functions remain compatibility wrappers, and a focused test proves late motion changes participate in resolution. Test Plan: - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - git diff --check --- tdkpin-rs/src/original_physics.rs | 114 ++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 28 deletions(-) diff --git a/tdkpin-rs/src/original_physics.rs b/tdkpin-rs/src/original_physics.rs index a0b7bcd..ac182b5 100644 --- a/tdkpin-rs/src/original_physics.rs +++ b/tdkpin-rs/src/original_physics.rs @@ -24,6 +24,15 @@ pub struct CollisionResponse { pub auxiliary_fired: bool, } +#[derive(Clone, Copy, Debug)] +pub struct StaticCollisionCandidate { + pub surface_distance: i32, + normal_x: Real48, + normal_y: Real48, + normal_velocity: i32, + material: CollisionMaterial, +} + #[derive(Clone, Copy, Debug)] pub struct CollisionMaterial { pub normal_rebound: f64, @@ -87,6 +96,25 @@ impl CollisionMaterial { } } +impl StaticCollisionCandidate { + pub fn resolve(self, velocity: MilliVec, spin: Real48) -> CollisionResponse { + let (velocity, spin, auxiliary_fired) = apply_response( + velocity, + spin, + self.normal_x, + self.normal_y, + self.normal_velocity, + self.material.real48(), + ); + CollisionResponse { + surface_distance: self.surface_distance, + velocity, + spin, + auxiliary_fired, + } + } +} + #[derive(Clone, Copy)] struct ResponseCoefficients { normal: Real48, @@ -293,14 +321,13 @@ fn apply_response( } /// Calculate the original type-2 response in the registered segment's basis. -pub fn line_collision_response( +pub fn line_collision_candidate( old_position: MilliVec, velocity: MilliVec, line_start: Vec2, line_end: Vec2, material: CollisionMaterial, - spin: Real48, -) -> Option { +) -> Option { let start = MilliVec::from_position(line_start); let end = MilliVec::from_position(line_end); let predicted = old_position.add(velocity); @@ -337,22 +364,27 @@ pub fn line_collision_response( { return None; } - let (velocity, spin, auxiliary_fired) = apply_response( - velocity, - spin, + Some(StaticCollisionCandidate { + surface_distance: distance, normal_x, normal_y, - collision_velocity, - material.real48(), - ); - Some(CollisionResponse { - surface_distance: distance, - velocity, - spin, - auxiliary_fired, + normal_velocity: collision_velocity, + material, }) } +pub fn line_collision_response( + old_position: MilliVec, + velocity: MilliVec, + start: Vec2, + end: Vec2, + material: CollisionMaterial, + spin: Real48, +) -> Option { + line_collision_candidate(old_position, velocity, start, end, material) + .map(|candidate| candidate.resolve(velocity, spin)) +} + #[cfg(test)] pub fn collide_with_line( old_position: MilliVec, @@ -377,14 +409,13 @@ pub fn collide_with_line( } /// Calculate the original type-1 circle response for a path entering it. -pub fn circle_collision_response( +pub fn circle_collision_candidate( old_position: MilliVec, velocity: MilliVec, center: Vec2, radius: f32, material: CollisionMaterial, - spin: Real48, -) -> Option { +) -> Option { let center = MilliVec::from_position(center); let radius_milli = (radius * 1_000.0).round() as i32; let mut surface_distance = @@ -411,22 +442,27 @@ pub fn circle_collision_response( return None; } surface_distance = surface_distance.wrapping_add(3_000); - let (velocity, spin, auxiliary_fired) = apply_response( - velocity, - spin, + Some(StaticCollisionCandidate { + surface_distance, normal_x, normal_y, - collision_velocity, - material.real48(), - ); - Some(CollisionResponse { - surface_distance, - velocity, - spin, - auxiliary_fired, + normal_velocity: collision_velocity, + material, }) } +pub fn circle_collision_response( + old_position: MilliVec, + velocity: MilliVec, + center: Vec2, + radius: f32, + material: CollisionMaterial, + spin: Real48, +) -> Option { + circle_collision_candidate(old_position, velocity, center, radius, material) + .map(|candidate| candidate.resolve(velocity, spin)) +} + pub fn ball_collision_response( old_position: MilliVec, velocity: MilliVec, @@ -562,6 +598,28 @@ mod tests { ); } + #[test] + fn detected_candidate_resolves_against_the_later_motion_state() { + let old = MilliVec { + x: 326_000, + y: 200_000, + }; + let detected_velocity = MilliVec { x: 3_000, y: 0 }; + let candidate = line_collision_candidate( + old, + detected_velocity, + vec2(328.0, 422.0), + vec2(328.0, 58.0), + CollisionMaterial::line(0.6, 0.1), + ) + .expect("the candidate must be retained during the record scan"); + + let response = candidate.resolve(MilliVec { x: 3_000, y: 1_000 }, Real48::ZERO); + + assert_eq!(response.velocity, MilliVec { x: -1_800, y: 1_000 }); + assert_eq!(response.surface_distance, 2_000); + } + #[test] fn auxiliary_kick_uses_the_recovered_negative_speed_threshold() { let material = CollisionMaterial::line_with_kick(0.5, 0.1, -0.4, 0.4);