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()