Output the type that was incorrect to make it more obvious what the user is doing wrong without them adding logging. To avoid string building during lots of vector math, introduced a private precond module that only builds the strings when an error occurs. It uses error() to put the resulting error message at the caller to cpml. Before: lua: ~/cpml/modules/vec2.lua:52: new: Wrong argument type for x (<number> expected) lua: ~/cpml/modules/vec2.lua:424: __add: Wrong argument type for right hand operand. (<cpml.vec2> expected) example stack traceback: [C]: in function 'assert' ~/cpml/modules/vec2.lua:424: in metamethod '__add' test_cpml.lua:32: in main chunk [C]: in ? After: lua: test_cpml.lua:31: new: Wrong argument type for x: string (<number> expected) lua: test_cpml.lua:32: __add: Wrong argument type 'string' for right hand operand. (<cpml.vec2> expected) example stack traceback: [C]: in function 'error' ~/cpml/modules/_private_precond.lua:13: in function 'modules._private_precond.assert' ~/cpml/modules/vec2.lua:425: in metamethod '__add' test_cpml.lua:32: in main chunk [C]: in ? The tracebacks are longer, but the initial error is at the location of the mistake and the output includes the input type.
418 lines
11 KiB
Lua
418 lines
11 KiB
Lua
--- A 3 component vector.
|
|
-- @module vec3
|
|
|
|
local modules = (...):gsub('%.[^%.]+$', '') .. "."
|
|
local precond = require(modules .. "_private_precond")
|
|
local private = require(modules .. "_private_utils")
|
|
local sqrt = math.sqrt
|
|
local cos = math.cos
|
|
local sin = math.sin
|
|
local vec3 = {}
|
|
local vec3_mt = {}
|
|
|
|
-- Private constructor.
|
|
local function new(x, y, z)
|
|
return setmetatable({
|
|
x = x or 0,
|
|
y = y or 0,
|
|
z = z or 0
|
|
}, vec3_mt)
|
|
end
|
|
|
|
-- Do the check to see if JIT is enabled. If so use the optimized FFI structs.
|
|
local status, ffi
|
|
if type(jit) == "table" and jit.status() then
|
|
status, ffi = pcall(require, "ffi")
|
|
if status then
|
|
ffi.cdef "typedef struct { double x, y, z;} cpml_vec3;"
|
|
new = ffi.typeof("cpml_vec3")
|
|
end
|
|
end
|
|
|
|
--- Constants
|
|
-- @table vec3
|
|
-- @field unit_x X axis of rotation
|
|
-- @field unit_y Y axis of rotation
|
|
-- @field unit_z Z axis of rotation
|
|
-- @field zero Empty vector
|
|
vec3.unit_x = new(1, 0, 0)
|
|
vec3.unit_y = new(0, 1, 0)
|
|
vec3.unit_z = new(0, 0, 1)
|
|
vec3.zero = new(0, 0, 0)
|
|
|
|
--- The public constructor.
|
|
-- @param x Can be of three types: </br>
|
|
-- number X component
|
|
-- table {x, y, z} or {x=x, y=y, z=z}
|
|
-- scalar To fill the vector eg. {x, x, x}
|
|
-- @tparam number y Y component
|
|
-- @tparam number z Z component
|
|
-- @treturn vec3 out
|
|
function vec3.new(x, y, z)
|
|
-- number, number, number
|
|
if x and y and z then
|
|
precond.typeof(x, "number", "new: Wrong argument type for x")
|
|
precond.typeof(y, "number", "new: Wrong argument type for y")
|
|
precond.typeof(z, "number", "new: Wrong argument type for z")
|
|
|
|
return new(x, y, z)
|
|
|
|
-- {x, y, z} or {x=x, y=y, z=z}
|
|
elseif type(x) == "table" or type(x) == "cdata" then -- table in vanilla lua, cdata in luajit
|
|
local xx, yy, zz = x.x or x[1], x.y or x[2], x.z or x[3]
|
|
precond.typeof(xx, "number", "new: Wrong argument type for x")
|
|
precond.typeof(yy, "number", "new: Wrong argument type for y")
|
|
precond.typeof(zz, "number", "new: Wrong argument type for z")
|
|
|
|
return new(xx, yy, zz)
|
|
|
|
-- number
|
|
elseif type(x) == "number" then
|
|
return new(x, x, x)
|
|
else
|
|
return new()
|
|
end
|
|
end
|
|
|
|
--- Clone a vector.
|
|
-- @tparam vec3 a Vector to be cloned
|
|
-- @treturn vec3 out
|
|
function vec3.clone(a)
|
|
return new(a.x, a.y, a.z)
|
|
end
|
|
|
|
--- Add two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.add(a, b)
|
|
return new(
|
|
a.x + b.x,
|
|
a.y + b.y,
|
|
a.z + b.z
|
|
)
|
|
end
|
|
|
|
--- Subtract one vector from another.
|
|
-- Order: If a and b are positions, computes the direction and distance from b
|
|
-- to a.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.sub(a, b)
|
|
return new(
|
|
a.x - b.x,
|
|
a.y - b.y,
|
|
a.z - b.z
|
|
)
|
|
end
|
|
|
|
--- Multiply a vector by another vectorr.
|
|
-- Component-size multiplication not matrix multiplication.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.mul(a, b)
|
|
return new(
|
|
a.x * b.x,
|
|
a.y * b.y,
|
|
a.z * b.z
|
|
)
|
|
end
|
|
|
|
--- Divide a vector by a scalar.
|
|
-- Component-size inv multiplication. Like a non-uniform scale().
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.div(a, b)
|
|
return new(
|
|
a.x / b.x,
|
|
a.y / b.y,
|
|
a.z / b.z
|
|
)
|
|
end
|
|
|
|
--- Get the normal of a vector.
|
|
-- @tparam vec3 a Vector to normalize
|
|
-- @treturn vec3 out
|
|
function vec3.normalize(a)
|
|
if a:is_zero() then
|
|
return new()
|
|
end
|
|
return a:scale(1 / a:len())
|
|
end
|
|
|
|
--- Trim a vector to a given length
|
|
-- @tparam vec3 a Vector to be trimmed
|
|
-- @tparam number len Length to trim the vector to
|
|
-- @treturn vec3 out
|
|
function vec3.trim(a, len)
|
|
return a:normalize():scale(math.min(a:len(), len))
|
|
end
|
|
|
|
--- Get the cross product of two vectors.
|
|
-- Resulting direction is right-hand rule normal of plane defined by a and b.
|
|
-- Magnitude is the area spanned by the parallelograms that a and b span.
|
|
-- Order: Direction determined by right-hand rule.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.cross(a, b)
|
|
return new(
|
|
a.y * b.z - a.z * b.y,
|
|
a.z * b.x - a.x * b.z,
|
|
a.x * b.y - a.y * b.x
|
|
)
|
|
end
|
|
|
|
--- Get the dot product of two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn number dot
|
|
function vec3.dot(a, b)
|
|
return a.x * b.x + a.y * b.y + a.z * b.z
|
|
end
|
|
|
|
--- Get the length of a vector.
|
|
-- @tparam vec3 a Vector to get the length of
|
|
-- @treturn number len
|
|
function vec3.len(a)
|
|
return sqrt(a.x * a.x + a.y * a.y + a.z * a.z)
|
|
end
|
|
|
|
--- Get the squared length of a vector.
|
|
-- @tparam vec3 a Vector to get the squared length of
|
|
-- @treturn number len
|
|
function vec3.len2(a)
|
|
return a.x * a.x + a.y * a.y + a.z * a.z
|
|
end
|
|
|
|
--- Get the distance between two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn number dist
|
|
function vec3.dist(a, b)
|
|
local dx = a.x - b.x
|
|
local dy = a.y - b.y
|
|
local dz = a.z - b.z
|
|
return sqrt(dx * dx + dy * dy + dz * dz)
|
|
end
|
|
|
|
--- Get the squared distance between two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn number dist
|
|
function vec3.dist2(a, b)
|
|
local dx = a.x - b.x
|
|
local dy = a.y - b.y
|
|
local dz = a.z - b.z
|
|
return dx * dx + dy * dy + dz * dz
|
|
end
|
|
|
|
--- Scale a vector by a scalar.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam number b Right hand operand
|
|
-- @treturn vec3 out
|
|
function vec3.scale(a, b)
|
|
return new(
|
|
a.x * b,
|
|
a.y * b,
|
|
a.z * b
|
|
)
|
|
end
|
|
|
|
--- Rotate vector about an axis.
|
|
-- @tparam vec3 a Vector to rotate
|
|
-- @tparam number phi Angle to rotate vector by (in radians)
|
|
-- @tparam vec3 axis Axis to rotate by
|
|
-- @treturn vec3 out
|
|
function vec3.rotate(a, phi, axis)
|
|
if not vec3.is_vec3(axis) then
|
|
return a
|
|
end
|
|
|
|
local u = axis:normalize()
|
|
local c = cos(phi)
|
|
local s = sin(phi)
|
|
|
|
-- Calculate generalized rotation matrix
|
|
local m1 = new((c + u.x * u.x * (1 - c)), (u.x * u.y * (1 - c) - u.z * s), (u.x * u.z * (1 - c) + u.y * s))
|
|
local m2 = new((u.y * u.x * (1 - c) + u.z * s), (c + u.y * u.y * (1 - c)), (u.y * u.z * (1 - c) - u.x * s))
|
|
local m3 = new((u.z * u.x * (1 - c) - u.y * s), (u.z * u.y * (1 - c) + u.x * s), (c + u.z * u.z * (1 - c)) )
|
|
|
|
return new(
|
|
a:dot(m1),
|
|
a:dot(m2),
|
|
a:dot(m3)
|
|
)
|
|
end
|
|
|
|
--- Get the perpendicular vector of a vector.
|
|
-- @tparam vec3 a Vector to get perpendicular axes from
|
|
-- @treturn vec3 out
|
|
function vec3.perpendicular(a)
|
|
return new(-a.y, a.x, 0)
|
|
end
|
|
|
|
--- Lerp between two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @tparam number s Step value
|
|
-- @treturn vec3 out
|
|
function vec3.lerp(a, b, s)
|
|
return a + (b - a) * s
|
|
end
|
|
|
|
-- Round all components to nearest int (or other precision).
|
|
-- @tparam vec3 a Vector to round.
|
|
-- @tparam precision Digits after the decimal (round numebr if unspecified)
|
|
-- @treturn vec3 Rounded vector
|
|
function vec3.round(a, precision)
|
|
return vec3.new(private.round(a.x, precision), private.round(a.y, precision), private.round(a.z, precision))
|
|
end
|
|
|
|
--- Unpack a vector into individual components.
|
|
-- @tparam vec3 a Vector to unpack
|
|
-- @treturn number x
|
|
-- @treturn number y
|
|
-- @treturn number z
|
|
function vec3.unpack(a)
|
|
return a.x, a.y, a.z
|
|
end
|
|
|
|
--- Return the component-wise minimum of two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors.
|
|
function vec3.component_min(a, b)
|
|
return new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z))
|
|
end
|
|
|
|
--- Return the component-wise maximum of two vectors.
|
|
-- @tparam vec3 a Left hand operand
|
|
-- @tparam vec3 b Right hand operand
|
|
-- @treturn vec3 A vector where each component is the lesser value for that component between the two given vectors.
|
|
function vec3.component_max(a, b)
|
|
return new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z))
|
|
end
|
|
|
|
-- Negate x axis only of vector.
|
|
-- @tparam vec2 a Vector to x-flip.
|
|
-- @treturn vec2 x-flipped vector
|
|
function vec3.flip_x(a)
|
|
return vec3.new(-a.x, a.y, a.z)
|
|
end
|
|
|
|
-- Negate y axis only of vector.
|
|
-- @tparam vec2 a Vector to y-flip.
|
|
-- @treturn vec2 y-flipped vector
|
|
function vec3.flip_y(a)
|
|
return vec3.new(a.x, -a.y, a.z)
|
|
end
|
|
|
|
-- Negate z axis only of vector.
|
|
-- @tparam vec2 a Vector to z-flip.
|
|
-- @treturn vec2 z-flipped vector
|
|
function vec3.flip_z(a)
|
|
return vec3.new(a.x, a.y, -a.z)
|
|
end
|
|
|
|
--- Return a boolean showing if a table is or is not a vec3.
|
|
-- @tparam vec3 a Vector to be tested
|
|
-- @treturn boolean is_vec3
|
|
function vec3.is_vec3(a)
|
|
if type(a) == "cdata" then
|
|
return ffi.istype("cpml_vec3", a)
|
|
end
|
|
|
|
return
|
|
type(a) == "table" and
|
|
type(a.x) == "number" and
|
|
type(a.y) == "number" and
|
|
type(a.z) == "number"
|
|
end
|
|
|
|
--- Return a boolean showing if a table is or is not a zero vec3.
|
|
-- @tparam vec3 a Vector to be tested
|
|
-- @treturn boolean is_zero
|
|
function vec3.is_zero(a)
|
|
return a.x == 0 and a.y == 0 and a.z == 0
|
|
end
|
|
|
|
--- Return whether any component is NaN
|
|
-- @tparam vec3 a Vector to be tested
|
|
-- @treturn boolean if x,y, or z are nan
|
|
function vec3.has_nan(a)
|
|
return private.is_nan(a.x) or
|
|
private.is_nan(a.y) or
|
|
private.is_nan(a.z)
|
|
end
|
|
|
|
--- Return a formatted string.
|
|
-- @tparam vec3 a Vector to be turned into a string
|
|
-- @treturn string formatted
|
|
function vec3.to_string(a)
|
|
return string.format("(%+0.3f,%+0.3f,%+0.3f)", a.x, a.y, a.z)
|
|
end
|
|
|
|
vec3_mt.__index = vec3
|
|
vec3_mt.__tostring = vec3.to_string
|
|
|
|
function vec3_mt.__call(_, x, y, z)
|
|
return vec3.new(x, y, z)
|
|
end
|
|
|
|
function vec3_mt.__unm(a)
|
|
return new(-a.x, -a.y, -a.z)
|
|
end
|
|
|
|
function vec3_mt.__eq(a, b)
|
|
if not vec3.is_vec3(a) or not vec3.is_vec3(b) then
|
|
return false
|
|
end
|
|
return a.x == b.x and a.y == b.y and a.z == b.z
|
|
end
|
|
|
|
function vec3_mt.__add(a, b)
|
|
precond.assert(vec3.is_vec3(a), "__add: Wrong argument type '%s' for left hand operand. (<cpml.vec3> expected)", type(a))
|
|
precond.assert(vec3.is_vec3(b), "__add: Wrong argument type '%s' for right hand operand. (<cpml.vec3> expected)", type(b))
|
|
return a:add(b)
|
|
end
|
|
|
|
function vec3_mt.__sub(a, b)
|
|
precond.assert(vec3.is_vec3(a), "__sub: Wrong argument type '%s' for left hand operand. (<cpml.vec3> expected)", type(a))
|
|
precond.assert(vec3.is_vec3(b), "__sub: Wrong argument type '%s' for right hand operand. (<cpml.vec3> expected)", type(b))
|
|
return a:sub(b)
|
|
end
|
|
|
|
function vec3_mt.__mul(a, b)
|
|
precond.assert(vec3.is_vec3(a), "__mul: Wrong argument type '%s' for left hand operand. (<cpml.vec3> expected)", type(a))
|
|
precond.assert(vec3.is_vec3(b) or type(b) == "number", "__mul: Wrong argument type '%s' for right hand operand. (<cpml.vec3> or <number> expected)", type(b))
|
|
|
|
if vec3.is_vec3(b) then
|
|
return a:mul(b)
|
|
end
|
|
|
|
return a:scale(b)
|
|
end
|
|
|
|
function vec3_mt.__div(a, b)
|
|
precond.assert(vec3.is_vec3(a), "__div: Wrong argument type '%s' for left hand operand. (<cpml.vec3> expected)", type(a))
|
|
precond.assert(vec3.is_vec3(b) or type(b) == "number", "__div: Wrong argument type '%s' for right hand operand. (<cpml.vec3> or <number> expected)", type(b))
|
|
|
|
if vec3.is_vec3(b) then
|
|
return a:div(b)
|
|
end
|
|
|
|
return a:scale(1 / b)
|
|
end
|
|
|
|
if status then
|
|
xpcall(function() -- Allow this to silently fail; assume failure means someone messed with package.loaded
|
|
ffi.metatype(new, vec3_mt)
|
|
end, function() end)
|
|
end
|
|
|
|
return setmetatable({}, vec3_mt)
|