Minetest-WorldEditAdditions/worldeditadditions_core/utils/vector3.lua
Starbeamrainbowlabs 9c66288827
Vector3: add rotate3d.
Thanks to GitHub Copilot for the implementation, it just needed
improving around the edges to make it work perfectly.
For the record, copilot generated it in JS, and then
I manually ported it to Lua.
If the Copilot code looks sufficiently similar to your own source,
get in touch and I will give you full credit.
2023-01-20 23:43:07 +00:00

498 lines
19 KiB
Lua

--- A 3-dimensional vector.
-- @class
local Vector3 = {}
Vector3.__index = Vector3
Vector3.__name = "Vector3" -- A hack to allow identification in wea.inspect
--- Creates a new Vector3 instance.
-- @param x number The x co-ordinate value.
-- @param y number The y co-ordinate value.
-- @param z number The z co-ordinate value.
function Vector3.new(x, y, z)
x = x or 0
y = y or 0
z = z or 0
local result = {
x = x,
y = y,
z = z
}
setmetatable(result, Vector3)
return result
end
--- Returns a new instance of this vector.
-- @param a Vector3 The vector to clone.
-- @returns Vector3 A new vector whose values are identical to those of the original vector.
function Vector3.clone(a)
return Vector3.new(a.x, a.y, a.z)
end
--- Adds the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be added to each of the components of the vector.
-- @param a Vector3|number The first item to add.
-- @param a Vector3|number The second item to add.
-- @returns Vector3 The result as a new Vector3 object.
function Vector3.add(a, b)
if type(a) == "number" then
return Vector3.new(b.x + a, b.y + a, b.z + a)
elseif type(b) == "number" then
return Vector3.new(a.x + b, a.y + b, a.z + b)
end
return Vector3.new(a.x + b.x, a.y + b.y, a.z + b.z)
end
--- Subtracts the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be subtracted to each of the components of the vector.
-- @param a Vector3|number The first item to subtract.
-- @param a Vector3|number The second item to subtract.
-- @returns Vector3 The result as a new Vector3 object.
function Vector3.subtract(a, b)
if type(a) == "number" then
return Vector3.new(b.x - a, b.y - a, b.z - a)
elseif type(b) == "number" then
return Vector3.new(a.x - b, a.y - b, a.z - b)
end
return Vector3.new(a.x - b.x, a.y - b.y, a.z - b.z)
end
--- Alias for Vector3.subtract.
function Vector3.sub(a, b) return Vector3.subtract(a, b) end
--- Multiplies the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be multiplied to each of the components of the vector.
--
-- If both of the inputs are vectors, then the components are multiplied
-- by each other (NOT the cross product). In other words:
-- a.x * b.x, a.y * b.y, a.z * b.z
--
-- @param a Vector3|number The first item to multiply.
-- @param a Vector3|number The second item to multiply.
-- @returns Vector3 The result as a new Vector3 object.
function Vector3.multiply(a, b)
if type(a) == "number" then
return Vector3.new(b.x * a, b.y * a, b.z * a)
elseif type(b) == "number" then
return Vector3.new(a.x * b, a.y * b, a.z * b)
end
return Vector3.new(a.x * b.x, a.y * b.y, a.z * b.z)
end
--- Alias for Vector3.multiply.
function Vector3.mul(a, b) return Vector3.multiply(a, b) end
--- Divides the specified vectors or numbers together.
-- Returns the result as a new vector.
-- If 1 of the inputs is a number and the other a vector, then the number will
-- be divided to each of the components of the vector.
-- @param a Vector3|number The first item to divide.
-- @param a Vector3|number The second item to divide.
-- @returns Vector3 The result as a new Vector3 object.
function Vector3.divide(a, b)
if type(a) == "number" then
return Vector3.new(a / b.x, a/ b.y, a / b.z)
-- return Vector3.new(b.x / a, b.y / a, b.z / a) -- BUG: is this a bug?
elseif type(b) == "number" then
return Vector3.new(a.x / b, a.y / b, a.z / b)
end
return Vector3.new(a.x / b.x, a.y / b.y, a.z / b.z)
end
--- Alias for Vector3.divide.
function Vector3.div(a, b) return Vector3.divide(a, b) end
--- Rounds the components of this vector down.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 A new instance with the x/y/z components rounded down.
function Vector3.floor(a)
return Vector3.new(math.floor(a.x), math.floor(a.y), math.floor(a.z))
end
--- Rounds the components of this vector up.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 A new instance with the x/y/z components rounded up.
function Vector3.ceil(a)
return Vector3.new(math.ceil(a.x), math.ceil(a.y), math.ceil(a.z))
end
--- Rounds the components of this vector.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 A new instance with the x/y/z components rounded.
function Vector3.round(a)
return Vector3.new(math.floor(a.x+0.5), math.floor(a.y+0.5), math.floor(a.z+0.5))
end
--- Rounds the components of this vector to the specified number of decimal places.
-- @param a Vector3 The vector to round.
-- @param dp number The number of decimal places to round to.
-- @returns Vector3 A new instance with the components rounded to the specified number of decimal places.
function Vector3.round_dp(a, dp)
local power = 10^dp
return (a * power):round() / power
end
--- Snaps this Vector3 to an imaginary square grid with the specified sized
-- squares.
-- @param a Vector3 The vector to operate on.
-- @param number grid_size The size of the squares on the imaginary grid to which to snap.
-- @returns Vector3 A new Vector3 instance snapped to an imaginary grid of the specified size.
function Vector3.snap_to(a, grid_size)
return (a / grid_size):round() * grid_size
end
--- Returns the area of this vector.
-- In other words, multiplies all the components together and returns a scalar value.
-- @param a Vector3 The vector to return the area of.
-- @returns number The area of this vector.
function Vector3.area(a)
return a.x * a.y * a.z
end
--- Returns the scalar length of this vector squared.
-- @param a Vector3 The vector to operate on.
-- @returns number The length squared of this vector as a scalar value.
function Vector3.length_squared(a)
return a.x * a.x + a.y * a.y + a.z * a.z
end
--- Square roots each component of this vector.
-- @param a Vector3 The vector to operate on.
-- @returns number A new vector with each component square rooted.
function Vector3.sqrt(a)
return Vector3.new(math.sqrt(a.x), math.sqrt(a.y), math.sqrt(a.z))
end
--- Calculates the scalar length of this vector.
-- @param a Vector3 The vector to operate on.
-- @returns number The length of this vector as a scalar value.
function Vector3.length(a)
return math.sqrt(a:length_squared())
end
--- Calculates the volume of the region bounded by 1 points.
-- @param a Vector3 The first point bounding the target region.
-- @param b Vector3 The second point bounding the target region.
-- @returns number The volume of the defined region.
function Vector3.volume(a, b)
local pos1, pos2 = Vector3.sort(a, b)
local vol = pos2 - pos1
return vol.x * vol.y * vol.z
end
--- Calculates the dot product of this vector and another vector.
-- @param a Vector3 The first vector to operate on.
-- @param a Vector3 The second vector to operate on.
-- @returns number The dot product of this vector as a scalar value.
function Vector3.dot(a, b)
return a.x * b.x + a.y * b.y + a.z * b.z;
end
--- Alias of Vector3.dot.
function Vector3.dot_product(a, b)
return Vector3.dot(a, b)
end
--- Determines if 2 vectors are equal to each other.
-- 2 vectors are equal if their values are identical.
-- @param a Vector3 The first vector to test.
-- @param a Vector3 The second vector to test.
-- @returns bool Whether the 2 vectors are equal or not.
function Vector3.equals(a, b)
return a.x == b.x
and a.y == b.y
and a.z == b.z
end
--- Returns a new vector whose length clamped to the given length.
-- The direction in which the vector is pointing is not changed.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 A new Vector3 instance limited to the specified length.
function Vector3.limit_to(a, length)
if type(length) ~= "number" then error("Error: Expected number, but found "..type(length)..".") end
if a:length() > length then
return (a / a:length()) * length
end
return a:clone()
end
--- Returns a new vector whose length clamped to the given length.
-- The direction in which the vector is pointing is not changed.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 A new Vector3 instance limited to the specified length.
function Vector3.set_to(a, length)
if type(length) ~= "number" then error("Error: Expected number, but found "..type(length)..".") end
return (a / a:length()) * length
end
--- Returns the unit vector of this vector.
-- The unit vector is a vector with a length of 1.
-- Returns a new vector.
-- Does not change the direction of the vector.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 The unit vector of this vector.
function Vector3.unit(a)
return a / a:length()
end
--- Alias of Vector3.unit.
function Vector3.normalise(a) return a:unit() end
--- Return a vector that is amount distance towards b from a.
-- @param a Vector3 The vector to move from.
-- @param b Vector3 The vector to move towards.
-- @param amount number The amount to move.
function Vector3.move_towards(a, b, amount)
return a + (b - a):limit_to(amount)
end
--- Returns the value of the minimum component of the vector.
-- Returns a scalar value.
-- @param a Vector3 The vector to operate on.
-- @returns number The value of the minimum component of the vector.
function Vector3.min_component(a)
return math.min(a.x, a.y, a.z)
end
--- Returns the value of the maximum component of the vector.
-- Returns a scalar value.
-- @param a Vector3 The vector to operate on.
-- @returns number The value of the maximum component of the vector.
function Vector3.max_component(a)
return math.max(a.x, a.y, a.z)
end
--- Returns the absolute form of this vector.
-- In other words, it removes the minus sign from all components of the vector.
-- @param a Vector3 The vector to operate on.
-- @returns Vector3 The absolute form of the given vector.
function Vector3.abs(a)
return Vector3.new(math.abs(a.x), math.abs(a.y), math.abs(a.z))
end
--- Sorts the components of the given vectors.
-- pos1 will contain the minimum values, and pos2 the maximum values.
-- Returns 2 new vectors.
-- Note that for this specific function
-- the vectors provided do not *have* to be instances of Vector3.
-- It is only required that they have the keys x, y, and z.
-- Vector3 instances are always returned.
-- This enables convenient ingesting of positions from outside.
-- @param pos1 Vector3 The first vector to operate on.
-- @param pos2 Vector3 The second vector to operate on.
-- @returns Vector3,Vector3 The 2 sorted vectors (min, max).
function Vector3.sort(pos1, pos2)
-- Cloning is important because Vector3's API does not mutate
local pos1_new = Vector3.clone(pos1) -- This way we can accept non-Vector3 instances
local pos2_new = Vector3.clone(pos2) -- This way we can accept non-Vector3 instances
if pos1_new.x > pos2_new.x then
pos1_new.x, pos2_new.x = pos2_new.x, pos1_new.x
end
if pos1_new.y > pos2_new.y then
pos1_new.y, pos2_new.y = pos2_new.y, pos1_new.y
end
if pos1_new.z > pos2_new.z then
pos1_new.z, pos2_new.z = pos2_new.z, pos1_new.z
end
return pos1_new, pos2_new
end
--- Determines if this vector is contained within the region defined by the given vectors.
-- @param a Vector3 The target vector to check.
-- @param pos1 Vector3 pos1 of the defined region.
-- @param pos2 Vector3 pos2 of the defined region.
-- @return boolean Whether the given target is contained within the defined worldedit region.
function Vector3.is_contained(target, pos1, pos2)
local pos1, pos2 = Vector3.sort(pos1, pos2)
return pos1.x <= target.x
and pos1.y <= target.y
and pos1.z <= target.z
and pos2.x >= target.x
and pos2.y >= target.y
and pos2.z >= target.z
end
--- Clamps the given point to fall within the region defined by 2 points.
-- @param a Vector3 The target vector to clamp.
-- @param pos1 Vector3 pos1 of the defined region.
-- @param pos2 Vector3 pos2 of the defined region.
-- @returns Vector3 The target vector, clamped to fall within the defined region.
function Vector3.clamp(a, pos1, pos2)
pos1, pos2 = Vector3.sort(pos1, pos2)
return Vector3.min(Vector3.max(a, pos1), pos2)
end
--- Expands the defined region to include the given point.
-- @param target Vector3 The target vector to include.
-- @param pos1 Vector3 pos1 of the defined region.
-- @param pos2 Vector3 pos2 of the defined region.
-- @returns Vector3,Vector3 2 vectors that represent the expand_region.
function Vector3.expand_region(target, pos1, pos2)
local pos1, pos2 = Vector3.sort(pos1, pos2)
if target.x < pos1.x then pos1.x = target.x end
if target.y < pos1.y then pos1.y = target.y end
if target.z < pos1.z then pos1.z = target.z end
if target.x > pos2.x then pos2.x = target.x end
if target.y > pos2.y then pos2.y = target.y end
if target.z > pos2.z then pos2.z = target.z end
return pos1, pos2
end
--- Returns the mean (average) of 2 positions.
-- In other words, returns the centre of 2 points.
-- @param pos1 Vector3|number pos1 of the defined region.
-- @param pos2 Vector3|number pos2 of the defined region.
-- @param target Vector3 Centre coordinates.
function Vector3.mean(pos1, pos2)
return (pos1 + pos2) / 2
end
--- Returns a vector of the min components of 2 vectors.
-- @param pos1 Vector3|number The first vector to operate on.
-- @param pos2 Vector3|number The second vector to operate on.
-- @return Vector3 The minimum values from the input vectors
function Vector3.min(pos1, pos2)
if type(pos1) == "number" then
pos1 = Vector3.new(pos1, pos1, pos1)
end
if type(pos2) == "number" then
pos2 = Vector3.new(pos2, pos2, pos2)
end
return Vector3.new(
math.min(pos1.x, pos2.x),
math.min(pos1.y, pos2.y),
math.min(pos1.z, pos2.z)
)
end
--- Returns a vector of the max values of 2 vectors.
-- @param pos1 Vector3 The first vector to operate on.
-- @param pos2 Vector3 The second vector to operate on.
-- @return Vector3 The maximum values from the input vectors.
function Vector3.max(pos1, pos2)
if type(pos1) == "number" then
pos1 = Vector3.new(pos1, pos1, pos1)
end
if type(pos2) == "number" then
pos2 = Vector3.new(pos2, pos2, pos2)
end
return Vector3.new(
math.max(pos1.x, pos2.x),
math.max(pos1.y, pos2.y),
math.max(pos1.z, pos2.z)
)
end
--- Given 2 angles and a length, return a Vector3 pointing in that direction.
-- Consider a sphere, with the 2 angles defining a point on the sphere's surface.
-- This function returns that point as a Vector3.
-- @source https://math.stackexchange.com/a/1881767/221181
-- @param angle_x number The X angle.
-- @param angle_y number The Y angle.
-- @param length number The radius of the sphere in question.
-- @returns Vector3 The point on the sphere defined by the aforementioned parameters.
function Vector3.fromBearing(angle_x, angle_y, length)
return Vector3.new( -- X and Y swapped
length * math.cos(angle_x),
length * math.sin(angle_x) * math.sin(angle_y),
length * math.sin(angle_x) * math.cos(angle_y)
)
end
--- Rotate a given point around an origin point in 3d space.
-- Consider 3 axes (X, Y, and Z) that are centred on origin. This function
-- rotates point around these axes in the aforementioned order.
-- NOTE: This function is not as intuitive as it sounds.
-- A whiteboard and a head for mathematics is recommended before using this
-- function. Either that, or Blender 3 (https://blender.org/) is quite useful to visualise what's going on.
-- @source GitHub Copilot, generated 2023-01-17
-- @warning Not completely tested! Pending a thorough evaluation. Seems to basically work, after some tweaks to the fluff around the edges?
-- @param {Vector3} origin The origin point to rotate around
-- @param {Vector3} point The point to rotate.
-- @param {Vector3} rotate Rotate this much around the 3 different axes, x, y, and z. Axial rotations are handled in this order: X→Y→Z.
-- @param {Number} x Rotate this much around the X axis (yz plane), in radians.
-- @param {Number} y Rotate this much around the Y axis (xz plane), in radians.
-- @param {Number} z Rotate this much around the Z axis (xy plane), in radians.
-- @return {Vector3} The rotated point.
function Vector3.rotate3d(origin, point, rotate)
local point_norm = point - origin
-- rotate around x
local x1 = point_norm.x
local y1 = point_norm.y * math.cos(rotate.x) - point_norm.z * math.sin(rotate.x)
local z1 = point_norm.y * math.sin(rotate.x) + point_norm.z * math.cos(rotate.x)
-- rotate around y
local x2 = x1 * math.cos(rotate.y) + z1 * math.sin(rotate.y)
local y2 = y1
local z2 = -x1 * math.sin(rotate.y) + z1 * math.cos(rotate.y)
-- rotate around z
local x3 = x2 * math.cos(rotate.z) - y2 * math.sin(rotate.z)
local y3 = x2 * math.sin(rotate.z) + y2 * math.cos(rotate.z)
local z3 = z2
return Vector3.new(x3, y3, z3) + origin
-- return [x3+origin[0], y3+origin[1], z3+origin[2]];
end
-- ██████ ██████ ███████ ██████ █████ ████████ ██████ ██████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██████ █████ ██████ ███████ ██ ██ ██ ██████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ██ ███████ ██ ██ ██ ██ ██ ██████ ██ ██
--
-- ██████ ██ ██ ███████ ██████ ██████ ██ ██████ ███████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ █████ ██████ ██████ ██ ██ ██ █████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ████ ███████ ██ ██ ██ ██ ██ ██████ ███████ ███████
function Vector3.__call(x, y, z) return Vector3.new(x, y, z) end
function Vector3.__add(a, b)
return Vector3.add(a, b)
end
function Vector3.__sub(a, b)
return Vector3.sub(a, b)
end
function Vector3.__mul(a, b)
return Vector3.mul(a, b)
end
function Vector3.__div(a, b)
return Vector3.divide(a, b)
end
function Vector3.__eq(a, b)
return Vector3.equals(a, b)
end
--- Returns the current Vector3 as a string.
function Vector3.__tostring(a)
return "("..a.x..", "..a.y..", "..a.z..")"
end
function Vector3.__concat(a, b)
if type(a) ~= "string" then a = tostring(a) end
if type(b) ~= "string" then b = tostring(b) end
return a .. b
end
return Vector3