From 39aee9a4212b167df0220d1363b7de4008e59347 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Tue, 15 Jun 2021 11:44:34 -0700 Subject: [PATCH] Fix angle_to to produce signed angle angle_to was producing the angle from +x to the difference between a,b which is unexpected. Instead, it should produce the smallest absolute angle between the two vectors and be signed to indicate the direction of rotation. By using the old angle_to implementation and modifying equal() to print out the failures, you can see the numbers that it was producing before didn't make much sense: right:angle_to(down) = 45.0 right:angle_to(left) = 0.0 right:angle_to(up) = -45.0 down:angle_to(right) = -135.0 down:angle_to(left) = -45.0 down:angle_to(up) = -90.0 left:angle_to(down) = 135.0 left:angle_to(up) = -135.0 up:angle_to(right) = 135.0 up:angle_to(down) = 90.0 up:angle_to(left) = 45.0 Now it produces numbers you'd expect: right:angle_to(down) = -90.0 right:angle_to(left) = 180.0 right:angle_to(up) = 90.0 down:angle_to(right) = 90.0 down:angle_to(left) = -90.0 down:angle_to(up) = 180.0 left:angle_to(down) = 90.0 left:angle_to(up) = -90.0 up:angle_to(right) = -90.0 up:angle_to(down) = 180.0 up:angle_to(left) = 90.0 See also https://stackoverflow.com/questions/21483999/using-atan2-to-find-angle-between-two-vectors Also added tests for angle_between. --- modules/vec2.lua | 19 +++++++++++---- spec/utils_spec.lua | 2 -- spec/vec2_spec.lua | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/modules/vec2.lua b/modules/vec2.lua index ca153d9..e4d7e3c 100644 --- a/modules/vec2.lua +++ b/modules/vec2.lua @@ -228,22 +228,31 @@ function vec2.perpendicular(a) return new(-a.y, a.x) end ---- Angle from one vector to another. +--- Signed angle from one vector to another. +-- Rotations from +x to +y are positive. -- @tparam vec2 a Vector -- @tparam vec2 b Vector --- @treturn number angle +-- @treturn number angle in (-pi, pi] function vec2.angle_to(a, b) if b then - return atan2(a.y - b.y, a.x - b.x) + local angle = atan2(b.y, b.x) - atan2(a.y, a.x) + -- convert to (-pi, pi] + if angle > math.pi then + angle = angle - 2 * math.pi + elseif angle <= -math.pi then + angle = angle + 2 * math.pi + end + return angle end return atan2(a.y, a.x) end ---- Angle between two vectors. +--- Unsigned angle between two vectors. +-- Directionless and thus commutative. -- @tparam vec2 a Vector -- @tparam vec2 b Vector --- @treturn number angle +-- @treturn number angle in [0, pi] function vec2.angle_between(a, b) if b then if vec2.is_vec2(a) then diff --git a/spec/utils_spec.lua b/spec/utils_spec.lua index b113b9a..1b7a8eb 100644 --- a/spec/utils_spec.lua +++ b/spec/utils_spec.lua @@ -45,6 +45,4 @@ project_from(out, a, b) mirror_on(out, a, b) reflect(out, i, n) refract(out, i, n, ior) -angle_to(a, b) -angle_between(a, b) --]] diff --git a/spec/vec2_spec.lua b/spec/vec2_spec.lua index f79c807..a163b0b 100644 --- a/spec/vec2_spec.lua +++ b/spec/vec2_spec.lua @@ -203,6 +203,62 @@ describe("vec2:", function() assert.is.equal(temp, vec2(-1, -2)) end) + it("finds angle from one 2-vector to another", function() + local d = { + right = vec2(1, 0), + down = vec2(0, -1), + left = vec2(-1, 0), + up = vec2(0, 1), + } + assert.is.equal(math.deg(d.right:angle_to(d.right)), 0.0) + assert.is.equal(math.deg(d.right:angle_to(d.down)), -90.0) + assert.is.equal(math.deg(d.right:angle_to(d.left)), 180.0) + assert.is.equal(math.deg(d.right:angle_to(d.up)), 90.0) + + assert.is.equal(math.deg(d.down:angle_to(d.right)), 90.0) + assert.is.equal(math.deg(d.down:angle_to(d.down)), 0.0) + assert.is.equal(math.deg(d.down:angle_to(d.left)), -90.0) + assert.is.equal(math.deg(d.down:angle_to(d.up)), 180.0) + + assert.is.equal(math.deg(d.left:angle_to(d.right)), 180.0) + assert.is.equal(math.deg(d.left:angle_to(d.down)), 90.0) + assert.is.equal(math.deg(d.left:angle_to(d.left)), 0.0) + assert.is.equal(math.deg(d.left:angle_to(d.up)), -90.0) + + assert.is.equal(math.deg(d.up:angle_to(d.right)), -90.0) + assert.is.equal(math.deg(d.up:angle_to(d.down)), 180.0) + assert.is.equal(math.deg(d.up:angle_to(d.left)), 90.0) + assert.is.equal(math.deg(d.up:angle_to(d.up)), 0.0) + end) + + it("finds angle between two 2-vectors", function() + local d = { + right = vec2(1, 0), + down = vec2(0, -1), + left = vec2(-1, 0), + up = vec2(0, 1), + } + assert.is.equal(math.deg(d.right:angle_between(d.right)), 0.0) + assert.is.equal(math.deg(d.right:angle_between(d.down)), 90.0) + assert.is.equal(math.deg(d.right:angle_between(d.left)), 180.0) + assert.is.equal(math.deg(d.right:angle_between(d.up)), 90.0) + + assert.is.equal(math.deg(d.down:angle_between(d.right)), 90.0) + assert.is.equal(math.deg(d.down:angle_between(d.down)), 0.0) + assert.is.equal(math.deg(d.down:angle_between(d.left)), 90.0) + assert.is.equal(math.deg(d.down:angle_between(d.up)), 180.0) + + assert.is.equal(math.deg(d.left:angle_between(d.right)), 180.0) + assert.is.equal(math.deg(d.left:angle_between(d.down)), 90.0) + assert.is.equal(math.deg(d.left:angle_between(d.left)), 0.0) + assert.is.equal(math.deg(d.left:angle_between(d.up)), 90.0) + + assert.is.equal(math.deg(d.up:angle_between(d.right)), 90.0) + assert.is.equal(math.deg(d.up:angle_between(d.down)), 180.0) + assert.is.equal(math.deg(d.up:angle_between(d.left)), 90.0) + assert.is.equal(math.deg(d.up:angle_between(d.up)), 0.0) + end) + -- Do this last, to insulate tests from accidental state contamination -- Do vec3 tests last, to insulate tests from accidental state contamination it("converts a 2-vector to a 3-vector", function()