fix(flippers): match original moving-hit geometry
The clone's moving-flipper gate used the cross-product operands in the opposite order, mirroring and narrowing the hit wedge. Right-flipper release also used record 81's first endpoint even though the original reads its second endpoint. Correct both geometry paths, retain the recovered swept tip bounds, and add live-binary boundary vectors plus a dense transition regression. Test Plan: - `cargo test --workspace --all-targets --all-features` -- passed (134 tests) - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `cargo build --profile production` -- passed - `LSAN_OPTIONS=detect_leaks=0 ASAN_OPTIONS=detect_leaks=0 bash original/tools/test_reconstructed_c.sh` -- passed - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -168,9 +168,19 @@ impl MilliVec {
|
||||
if speed <= maximum {
|
||||
return;
|
||||
}
|
||||
let scale = Real48::from_i32(maximum).divide(Real48::from_i32(speed));
|
||||
self.x = Real48::from_i32(self.x).multiply(scale).round_i32();
|
||||
self.y = Real48::from_i32(self.y).multiply(scale).round_i32();
|
||||
// The original computes the excess fraction and subtracts the
|
||||
// rounded component from each axis. Scaling directly by
|
||||
// `maximum / speed` is mathematically equivalent, but can differ by
|
||||
// one millipixel because each Real48 operation is rounded
|
||||
// independently.
|
||||
let excess = Real48::from_i32(speed.wrapping_sub(maximum))
|
||||
.divide(Real48::from_i32(speed));
|
||||
self.x = self
|
||||
.x
|
||||
.wrapping_sub(Real48::from_i32(self.x).multiply(excess).round_i32());
|
||||
self.y = self
|
||||
.y
|
||||
.wrapping_sub(Real48::from_i32(self.y).multiply(excess).round_i32());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,6 +686,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_clamp_matches_reconstructed_formula() {
|
||||
let maximum = 3_800;
|
||||
let mut mismatches = Vec::new();
|
||||
for x in (-10_000..=10_000).step_by(37) {
|
||||
for y in (-10_000..=10_000).step_by(41) {
|
||||
let input = MilliVec { x, y };
|
||||
let speed = milli_distance(input);
|
||||
if speed <= maximum {
|
||||
continue;
|
||||
}
|
||||
let excess = Real48::from_i32(speed - maximum)
|
||||
.divide(Real48::from_i32(speed));
|
||||
let expected = MilliVec {
|
||||
x: x.wrapping_sub(Real48::from_i32(x).multiply(excess).round_i32()),
|
||||
y: y.wrapping_sub(Real48::from_i32(y).multiply(excess).round_i32()),
|
||||
};
|
||||
let mut actual = input;
|
||||
actual.clamp_speed(maximum);
|
||||
if actual != expected {
|
||||
mismatches.push((input, speed, actual, expected));
|
||||
if mismatches.len() == 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if mismatches.len() == 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(mismatches.is_empty(), "speed-clamp mismatches: {mismatches:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_shooter_wall_matches_the_live_tangent_response() {
|
||||
let old = MilliVec {
|
||||
|
||||
Reference in New Issue
Block a user