[vector_math] Fix Quaternion.rotate() applying the inverse rotation - #41
[vector_math] Fix Quaternion.rotate() applying the inverse rotation#41AdityaJagtap18 wants to merge 3 commits into
Conversation
Quaternion.rotate() (and the rotated() wrapper) implemented `conjugate(this) * [v,0] * this` instead of the standard sandwich product `this * [v,0] * conjugate(this)`, so every quaternion rotation in both the 32-bit (vector_math) and 64-bit (vector_math_64) variants rotated vectors by the inverse of the intended rotation. For example, a 90-degree rotation about +Z applied to (1,0,0) must give (0,1,0) by the right-hand rule (and matches Matrix4.rotationZ(pi/2) and Quaternion.asRotationMatrix()), but the old code returned (0,-1,0). Verified by: - Deriving the correct sandwich-product formula by hand and comparing against Matrix4.rotationZ/asRotationMatrix() ground truth. - Adding test/fuzz_invariants_test.dart, a randomized invariant fuzzer covering normalize, inverse round-trips for Matrix2/3/4 and Quaternion, and — the check that surfaced this bug — cross-verifying Quaternion.rotated() against Quaternion.asRotationMatrix() on the same vector. - Correcting the 12 hardcoded expected vectors in quaternion_test.dart's testQuaternionNormalize(), which encoded the old (buggy) rotation direction; new values were independently derived from the standard rotation-matrix formulas. - Running the full vector_math test suite (261 tests) after the fix: all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per this repo's version-and-CHANGELOG contribution requirement for packages/vector_math following the Quaternion.rotate() inverse-rotation fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request fixes a bug in Quaternion.rotate() and rotated() where the inverse of the intended rotation was being applied in both the 32-bit and 64-bit implementations. It updates the corresponding unit tests, bumps the package version to 2.4.3, and introduces a new fuzz test suite to verify mathematical invariants. The review feedback suggests using the built-in normalized() method instead of cloning and normalizing vectors manually, and removing redundant vector cloning when calling Matrix3.transformed().
| for (var i = 0; i < n; i++) { | ||
| final Vector2 v2 = randV2(); | ||
| if (v2.length >= 1e-6) { | ||
| final Vector2 copy2 = v2.clone()..normalize(); |
There was a problem hiding this comment.
In vector_math, Vector2, Vector3, and Vector4 all provide a built-in normalized() method that returns a normalized copy of the vector. Using v2.normalized() is more idiomatic and concise than v2.clone()..normalize().\n\nThe same applies to:\n- Line 57: v3.clone()..normalize() -> v3.normalized()\n- Line 62: v4.clone()..normalize() -> v4.normalized()\n- Line 72: v3.clone()..normalize() -> v3.normalized()\n- Line 82: v4.clone()..normalize() -> v4.normalized()
| final Vector2 copy2 = v2.clone()..normalize(); | |
| final Vector2 copy2 = v2.normalized(); |
| final Vector3 v = randV3(); | ||
| final Vector3 viaQuat = q.rotated(v); | ||
| final Matrix3 m = q.asRotationMatrix(); | ||
| final Vector3 viaMatrix = m.transformed(v.clone()); |
There was a problem hiding this comment.
In vector_math, Matrix3.transformed(Vector3 arg, [Vector3? out]) does not modify the input vector arg. If the out parameter is omitted, it automatically creates a copy of arg internally before transforming it. Therefore, calling v.clone() here is redundant and results in an extra unnecessary allocation. You can pass v directly.
| final Vector3 viaMatrix = m.transformed(v.clone()); | |
| final Vector3 viaMatrix = m.transformed(v); |
- v.clone()..normalize() -> v.normalized(), matching this codebase's own idiom. - Matrix3.transformed(v.clone()) -> Matrix3.transformed(v): the method already copies its argument internally when no `out` is given. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quaternion.rotate()/rotated()computedconjugate(this) * [v,0] * thisinstead of the standard sandwich product
this * [v,0] * conjugate(this),so every quaternion rotation (in both the 32-bit
vector_math.dartand64-bit
vector_math_64.dartvariants) rotated vectors by the inverse ofthe intended rotation.
Example: rotating
(1, 0, 0)by 90° about+Zmust give(0, 1, 0)bythe right-hand rule, and matches
Matrix4.rotationZ/Quaternion.asRotationMatrix(), but the old code returned(0, -1, 0).Fixes flutter/flutter#192350.
What changed
lib/src/vector_math/quaternion.dartandlib/src/vector_math_64/quaternion.dart: correctedrotate()to usethe standard
this * [v,0] * conjugate(this)sandwich product.test/fuzz_invariants_test.dart(new): a randomized invariant fuzzercovering normalize,
normalizeIntoconsistency, componentwisemin/max/mix, inverse round-trips for Matrix2/3/4 and Quaternion, cross
product orthogonality, and — the check that caught this bug —
Quaternion.rotated()cross-verified againstQuaternion.asRotationMatrix()on the same vector.test/quaternion_test.dart: corrected the 12 hardcoded expectedvectors in
testQuaternionNormalize(), which encoded the old (buggy)rotation direction; new values were independently derived from the
standard rotation-matrix formulas.
vector_mathto 2.4.3 with a CHANGELOG entry.Verification
against
Matrix4.rotationZ/asRotationMatrix()ground truth forseveral axes/angles.
vector_mathtest suite after the fix: all 261 testspass (
dart test).dart format --output=none --set-exit-if-changedanddart analyzeare clean on all changed files.
Transparency
I used Claude Code to help find this bug (via the fuzz test), derive
and verify the fix, and draft this PR. I reviewed the diff and the math
by hand before submitting, and I'm available to address review feedback.
Pre-Review Checklist
[vector_math]///).🤖 Generated with Claude Code