fix(physics): restore standard tangent sign response

The left relative slingshot probe proved the spin projection, but its geometry
did not distinguish the sign rule. A stopped-Wine probe on symmetric record 72
did: the binary uses the standard dot-product tangent for both spin and the
opposing direction of the current tangential response. The earlier
swapped-component interpretation happened to agree for vertical walls and a
centered circle but selected the wrong sign on the right slingshot.

Use `tv=(vx*nx+vy*ny)/length`, update spin with
`tv*0.02*response_tangent`, and apply
`-sign(tv)*abs(normal_velocity)*response_tangent`, retaining the binary's
negative direction when tv is zero. Seal record 72's exact position, velocity,
and `+5.46` Real48 spin in C and Rust. Keep the two-minute autoplay assertion
focused on actual launch, press/release, target, bumper, and finite-state
activity after the corrected trajectory.

Test Plan:
- stopped Wine 11.15 record-72 injection/trace -- matched position, velocity, and spin
- `cargo test --workspace --all-targets --all-features` -- 124 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed
- `bash original/tools/test_reconstructed_c.sh` -- passed
- `python3 original/tools/audit_reconstruction.py --require-complete` -- passed with zero incomplete or unclassified units
- `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 20:33:20 +02:00
parent f79652bc9f
commit 23195cb9d1
9 changed files with 124 additions and 68 deletions
+8 -22
View File
@@ -241,21 +241,7 @@ fn normal_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, lengt
.round_i32()
}
fn tangent_direction(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
if length <= 0 {
return 0;
}
Real48::ZERO
.subtract(
Real48::from_i32(velocity.x)
.multiply(normal_y)
.add(Real48::from_i32(velocity.y).multiply(normal_x)),
)
.divide(Real48::from_i32(length))
.round_i32()
}
fn spin_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
fn tangent_velocity(velocity: MilliVec, normal_x: Real48, normal_y: Real48, length: i32) -> i32 {
if length <= 0 {
return 0;
}
@@ -295,16 +281,16 @@ fn apply_response(
x: normal_x.round_i32(),
y: normal_y.round_i32(),
});
// Raw 1000:ef19..f16a uses the swapped-component value only as the
// response direction; the standard dot product independently updates spin.
let tangent_direction = tangent_direction(velocity, normal_x, normal_y, length);
let spin_projection = spin_velocity(velocity, normal_x, normal_y, length);
let spin_delta = Real48::from_i32(spin_projection)
// Raw 1000:ef19..f16a uses the standard dot-product tangent both to
// oppose the current tangential motion and to update retained spin.
let tangent_velocity = tangent_velocity(velocity, normal_x, normal_y, length);
let spin_delta = Real48::from_i32(tangent_velocity)
.multiply(Real48::from_bytes([0x7b, 0x71, 0x3d, 0x0a, 0xd7, 0x23]))
.multiply(coefficients.tangent);
let mut tangent_response = Real48::from_i32(collision_velocity.wrapping_abs())
.multiply(coefficients.tangent);
if tangent_direction < 0 {
.multiply(coefficients.tangent)
.negate();
if tangent_velocity < 0 {
tangent_response = tangent_response.negate();
}
let projection = spin