-- Copyright (c) 2022 David Vogel -- -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT -- Just 2D vector stuff. Mainly for Noita. -- State: Some stuff is untested. ---Metatable of the Vec2 object that is returned by this lib. ---This will only contain a __call field pointing to the constructor. ---@class Vec2Meta local libMetaTable = {} ---@class Vec2 ---@field [1] number ---@field [2] number ---@field x number ---@field y number local Vec2 = setmetatable({}, libMetaTable) ----------------- -- Constructor -- ----------------- ---Creates a new vector. --- --- Vec2() -- Returns a vector with zeroed coordinates. --- Vec2(v) -- Returns a copy of the given Vec2 object. --- Vec2("1.2, 3.4") -- Returns a vector with x = 1.2 and y = 3.4. --- Vec2(1.2, 3.4) -- Returns a vector with x = 1.2 and y = 3.4. ---@param ... any ---@return Vec2 function libMetaTable:__call(...) local n = select("#", ...) if n == 0 then return setmetatable({ 0, 0 }, Vec2) -- Zero initialized vector. elseif n == 1 then local param = select(1, ...) if type(param) == "string" then local vector = {} for field in string.gmatch(param, "[^,%s]+") do table.insert(vector, tonumber(field)) end assert(#vector == 2, string.format("parsed vector contains an invalid number of fields: %d, expected %d", #vector, 2)) return setmetatable(vector, Vec2) -- Vector initialized with the given coordinates. elseif getmetatable(param) == Vec2 then return Vec2(param[1], param[2]) end error(string.format("unsupported argument type %q", type(param))) elseif n == 2 then assert(type(select(1, ...)) == "number", string.format("first argument has type %q, expects %q", type(select(1, ...)), "number")) assert(type(select(2, ...)) == "number", string.format("first argument has type %q, expects %q", type(select(2, ...)), "number")) return setmetatable({ ... }, Vec2) -- Vector initialized with the given coordinates. end error(string.format("called Vec2 constructor with %d argument(s)", n)) end ----------------- -- Metamethods -- ----------------- ---Handle special fields, like x and y. ---@param key any ---@return any function Vec2:__index(key) if type(key) == "number" then return rawget(self, key) end if key == "x" then return rawget(self, 1) end if key == "y" then return rawget(self, 2) end return rawget(Vec2, key) end ---Handle special fields, like x and y. ---@param key any function Vec2:__newindex(key, value) if type(key) == "number" then return rawset(self, key, value) end if key == "x" then rawset(self, 1, value) end if key == "y" then rawset(self, 2, value) end -- There should no way to manipulate any other keys of the object or its metatable. end ---Returns a string representation of this vector. ---This supports a round-trip via Vec2(tostring(v)) without loss of information. ---@return string function Vec2:__tostring() return string.format("%.16g, %.16g", self[1], self[2]) end ---------------------------- -- Mathematic metamethods -- ---------------------------- ---Returns a new vector that is the sum v1 + v2. --- ---This will not mutate any vector. ---@param v1 Vec2 ---@param v2 Vec2 ---@return Vec2 function Vec2.__add(v1, v2) assert(getmetatable(v1) == Vec2 and getmetatable(v2) == Vec2, "wrong argument types. Expected two Vec2 objects") return Vec2(v1[1] + v2[1], v1[2] + v2[2]) end ---Returns a new vector that is the difference v1 - v2. --- ---This will not mutate any vector. ---@param v1 Vec2 ---@param v2 Vec2 ---@return Vec2 function Vec2.__sub(v1, v2) assert(getmetatable(v1) == Vec2 and getmetatable(v2) == Vec2, "wrong argument types. Expected two Vec2 objects") return Vec2(v1[1] - v2[1], v1[2] - v2[2]) end ---Returns a new vector that is the multiplication of a vector with a scalar. --- ---This will not mutate any value. ---@param a number|Vec2 ---@param b number|Vec2 ---@return Vec2 function Vec2.__mul(a, b) if type(a) == "number" and getmetatable(b) == Vec2 then return Vec2(b[1] * a, b[2] * a) elseif getmetatable(a) == Vec2 and type(b) == "number" then return Vec2(a[1] * b, a[2] * b) end error(string.format("invalid combination of argument types for multiplication: %q and %q", type(a), type(b))) end ---Returns a new vector that is the division of a vector by a scalar. --- ---This will not mutate any value. ---@param v Vec2 ---@param s number ---@return Vec2 function Vec2.__div(v, s) if getmetatable(v) == Vec2 and type(s) == "number" then return Vec2(v[1] / s, v[2] / s) end error(string.format("invalid combination of argument types for division: %q and %q", type(v), type(s))) end ---Returns the negated vector. --- ---This will not mutate any value. ---@return Vec2 function Vec2:__unm() return Vec2(-self[1], -self[2]) end ---Returns whether the two vectors are equal. ---Will return false if one of the values is not a vector. ---@param v1 Vec2 ---@param v2 Vec2 ---@return boolean function Vec2.__eq(v1, v2) if getmetatable(v1) ~= Vec2 or getmetatable(v2) ~= Vec2 then return false end return v1[1] == v2[1] and v1[2] == v2[2] end ------------- -- Methods -- ------------- ---Adds v to the vector. --- ---This mutates self. ---@param v Vec2 function Vec2:Add(v) assert(getmetatable(v) == Vec2, string.format("wrong argument type %q, expected Vec2 object", type(v))) self[1], self[2] = self[1] + v[1], self[2] + v[2] end ---Subtracts v from the vector. --- ---This mutates self. ---@param v Vec2 function Vec2:Sub(v) assert(getmetatable(v) == Vec2, string.format("wrong argument type %q, expected Vec2 object", type(v))) self[1], self[2] = self[1] - v[1], self[2] - v[2] end ---Multiplies self with the given scalar. --- ---This mutates self. ---@param s number function Vec2:Mul(s) assert(type(s) == "number", string.format("wrong argument type %q, expected number", type(s))) self[1], self[2] = self[1] * s, self[2] * s end ---Divides self by the given scalar. --- ---This mutates self. ---@param s number function Vec2:Div(s) assert(type(s) == "number", string.format("wrong argument type %q, expected number", type(s))) self[1], self[2] = self[1] / s, self[2] / s end ---Returns a copy of self. ---@return Vec2 function Vec2:Copy() return Vec2(self) end ---Returns the vector fields as parameters. ---@return number ---@return number function Vec2:Unpack() return self[1], self[2] end ---Sets the vector fields to the given coordinates. ---@param x number ---@param y number function Vec2:Set(x, y) assert(type(x) == "number", string.format("wrong argument type %q, expected number", type(x))) assert(type(y) == "number", string.format("wrong argument type %q, expected number", type(y))) self[1], self[2] = x, y end ---Returns the squared length of the vector. ---@return number function Vec2:LengthSqr() return self[1] ^ 2 + self[2] ^ 2 end ---Returns the length of the vector. ---@return number function Vec2:Length() return math.sqrt(self:LengthSqr()) end ---Returns the squared distance of self to the given vector. ---@param v Vec2 ---@return number function Vec2:DistanceSqr(v) return (v - self):LengthSqr() end ---Returns the distance of self to the given vector. ---@param v Vec2 ---@return number function Vec2:Distance(v) return (v - self):Length() end ---Sets the length of the vector to 1. --- ---This mutates self. function Vec2:Normalize() local len = self:Length() self:Div(len) end ---Returns a copy of the vector with its length set to 1. ---@return Vec2 function Vec2:Normalized() local len = self:Length() return self / len end ---Compares this vector to the given one. ---@param v Vec2 ---@param tolerance number -- Tolerance per field. ---@return boolean function Vec2:EqualTo(v, tolerance) if math.abs(v[1] - self[1]) > tolerance then return false end if math.abs(v[2] - self[2]) > tolerance then return false end return true end ---Round returns v rounded by the given method. ---@param x number ---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest". ---@return integer local function round(x, method) method = method or "nearest" if method == "nearest" then return math.floor(x + 0.5) elseif method == "floor" then return math.floor(x) elseif method == "ceil" then return math.ceil(x) elseif method == "to-zero" then if x >= 0 then return math.floor(x) else return math.ceil(x) end elseif method == "away-zero" then if x >= 0 then return math.ceil(x) else return math.floor(x) end end error(string.format("invalid rounding method %q", method)) end ---Round rounds all vector fields individually by the given rounding method. ---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest". function Vec2:Round(method) self[1], self[2] = round(self[1], method), round(self[2], method) end ---Round rounds all vector fields individually by the given rounding method. ---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest". ---@return Vec2 function Vec2:Rounded(method) return Vec2(round(self[1], method), round(self[2], method)) end ------------------------- -- JSON Implementation -- ------------------------- ---MarshalJSON implements the JSON marshaler interface. ---@return string function Vec2:MarshalJSON() return string.format("[%.16g, %.16g]", self[1], self[2]) -- Encode as JSON array. -- TODO: Handle NaN, +Inf, -Inf, ... correctly end return Vec2