Add vector2 lua API for 2D vectors (#16929)

This commit is contained in:
kromka-chleba
2026-03-19 12:37:39 +01:00
committed by GitHub
parent 787b2a4df8
commit b3de90634a
12 changed files with 1012 additions and 146 deletions
+6 -1
View File
@@ -15,6 +15,7 @@ read_globals = {
"dump", "dump2",
"fgettext", "fgettext_ne",
"vector",
"vector2",
"VoxelArea",
"VoxelManip",
"profiler",
@@ -24,7 +25,7 @@ read_globals = {
string = {fields = {"split", "trim"}},
table = {fields = {"copy", "copy_with_metatables", "getn", "indexof", "keyof", "insert_all", "shuffle"}},
math = {fields = {"hypot", "round", "isfinite"}},
math = {fields = {"hypot", "round", "isfinite", "sign"}},
}
globals = {
@@ -71,6 +72,10 @@ files["builtin/common/vector.lua"] = {
globals = { "vector", "math" },
}
files["builtin/common/vector2.lua"] = {
globals = { "vector2", "math" },
}
files["builtin/game/voxelarea.lua"] = {
globals = { "VoxelArea" },
}
+401
View File
@@ -0,0 +1,401 @@
_G.vector = {}
_G.vector2 = {}
dofile("builtin/common/math.lua")
dofile("builtin/common/vector.lua")
dofile("builtin/common/vector2.lua")
-- Custom assertion for comparing floating-point numbers with tolerance
local function number_close(state, arguments)
if #arguments < 2 then
return false
end
local expected = arguments[1]
local actual = arguments[2]
local tolerance = arguments[3] or 0.000001
if type(expected) == "number" and type(actual) == "number" then
return math.abs(expected - actual) < tolerance
end
return false
end
-- Custom assertion for comparing vectors with tolerance
-- Uses component-wise comparison to be self-contained
local function vector2_close(state, arguments)
if #arguments < 2 then
return false
end
local expected = arguments[1]
local actual = arguments[2]
local tolerance = arguments[3] or 0.000001
if type(expected) == "table" and type(actual) == "table" then
return math.abs(expected.x - actual.x) < tolerance and
math.abs(expected.y - actual.y) < tolerance
end
return false
end
assert:register("assertion", "number_close", number_close)
assert:register("assertion", "vector2_close", vector2_close)
describe("vector2", function()
describe("new()", function()
it("constructs", function()
assert.same({x = 1, y = 2}, vector2.new(1, 2))
assert.is_true(vector2.check(vector2.new(1, 2)))
end)
it("throws on invalid input", function()
assert.has.errors(function()
vector2.new()
end)
assert.has.errors(function()
vector2.new({ x = 3, y = 2 })
end)
assert.has.errors(function()
vector2.new({ x = 3 })
end)
assert.has.errors(function()
vector2.new({ d = 3 })
end)
end)
end)
it("zero()", function()
assert.same({x = 0, y = 0}, vector2.zero())
assert.is_true(vector2.check(vector2.zero()))
end)
it("copy()", function()
local v = vector2.new(1, 2)
assert.same(v, vector2.copy(v))
assert.is_true(vector2.check(vector2.copy(v)))
end)
it("indexes", function()
local some_vector = vector2.new(24, 42)
assert.equal(24, some_vector[1])
assert.equal(24, some_vector.x)
assert.equal(42, some_vector[2])
assert.equal(42, some_vector.y)
some_vector[1] = 100
assert.equal(100, some_vector.x)
some_vector.x = 101
assert.equal(101, some_vector[1])
some_vector[2] = 100
assert.equal(100, some_vector.y)
some_vector.y = 102
assert.equal(102, some_vector[2])
end)
it("direction()", function()
local a = vector2.new(1, 0)
local b = vector2.new(1, 42)
local dir1 = vector2.direction(a, b)
assert.number_close(0, dir1.x)
assert.number_close(1, dir1.y)
local dir2 = a:direction(b)
assert.number_close(0, dir2.x)
assert.number_close(1, dir2.y)
end)
it("distance()", function()
local a = vector2.new(1, 0)
local b = vector2.new(4, 4)
assert.number_close(5, vector2.distance(a, b))
assert.number_close(5, a:distance(b))
assert.number_close(0, vector2.distance(a, a))
assert.number_close(0, b:distance(b))
end)
it("length()", function()
local a = vector2.new(3, 4)
assert.number_close(0, vector2.length(vector2.zero()))
assert.number_close(5, vector2.length(a))
assert.number_close(5, a:length())
end)
it("normalize()", function()
local a = vector2.new(0, -5)
local norm1 = vector2.normalize(a)
assert.number_close(0, norm1.x)
assert.number_close(-1, norm1.y)
local norm2 = a:normalize()
assert.number_close(0, norm2.x)
assert.number_close(-1, norm2.y)
local norm3 = vector2.normalize(vector2.zero())
assert.number_close(0, norm3.x)
assert.number_close(0, norm3.y)
end)
it("floor()", function()
local a = vector2.new(0.1, 0.9)
assert.same(vector2.new(0, 0), vector2.floor(a))
assert.same(vector2.new(0, 0), a:floor())
end)
it("round()", function()
local a = vector2.new(0.1, 0.9)
assert.same(vector2.new(0, 1), vector2.round(a))
assert.same(vector2.new(0, 1), a:round())
end)
it("ceil()", function()
local a = vector2.new(0.1, 0.9)
assert.same(vector2.new(1, 1), vector2.ceil(a))
assert.same(vector2.new(1, 1), a:ceil())
end)
it("sign()", function()
local a = vector2.new(-120.3, 231.5)
assert.same(vector2.new(-1, 1), vector2.sign(a))
assert.same(vector2.new(-1, 1), a:sign())
assert.same(vector2.new(0, 1), vector2.sign(a, 200))
assert.same(vector2.new(0, 1), a:sign(200))
end)
it("abs()", function()
local a = vector2.new(-123.456, 13)
assert.same(vector2.new(123.456, 13), vector2.abs(a))
assert.same(vector2.new(123.456, 13), a:abs())
end)
it("apply()", function()
local f = function(x)
return x * 2
end
local f2 = function(x, opt1, opt2)
return x + opt1 + opt2
end
local a = vector2.new(0.1, 0.9)
assert.same(vector2.new(1, 1), vector2.apply(a, math.ceil))
assert.same(vector2.new(1, 1), a:apply(math.ceil))
assert.same(vector2.new(0.1, 0.9), vector2.apply(a, math.abs))
assert.same(vector2.new(0.1, 0.9), a:apply(math.abs))
assert.same(vector2.new(0.2, 1.8), vector2.apply(a, f))
assert.same(vector2.new(0.2, 1.8), a:apply(f))
local b = vector2.new(1, 2)
assert.same(vector2.new(3, 4), vector2.apply(b, f2, 1, 1))
assert.same(vector2.new(3, 4), b:apply(f2, 1, 1))
end)
it("combine()", function()
local a = vector2.new(1, 4)
local b = vector2.new(2, 3)
assert.same(vector2.add(a, b), vector2.combine(a, b, function(x, y) return x + y end))
assert.same(vector2.new(2, 4), vector2.combine(a, b, math.max))
assert.same(vector2.new(1, 3), vector2.combine(a, b, math.min))
end)
it("equals()", function()
assert.is_true(vector2.equals({x = 0, y = 0}, {x = 0, y = 0}))
assert.is_true(vector2.equals({x = -1, y = 0}, vector2.new(-1, 0)))
assert.is_false(vector2.equals({x = 1, y = 2}, {x = 1, y = 3}))
local a = vector2.new(1, 2)
assert.is_true(a:equals(a))
assert.is_true(vector2.new(1, 2) == vector2.new(1, 2))
assert.is_false(vector2.new(1, 2) == vector2.new(1, 3))
end)
it("metatable is same", function()
local a = vector2.zero()
local b = vector2.new(1, 2)
assert.equal(true, vector2.check(a))
assert.equal(true, vector2.check(b))
assert.equal(vector2.metatable, getmetatable(a))
assert.equal(vector2.metatable, getmetatable(b))
assert.equal(vector2.metatable, a.metatable)
end)
it("sort()", function()
local a = vector2.new(1, 2)
local b = vector2.new(0.5, 232)
local sorted = {vector2.new(0.5, 2), vector2.new(1, 232)}
assert.same(sorted, {vector2.sort(a, b)})
assert.same(sorted, {a:sort(b)})
end)
it("angle()", function()
assert.number_close(math.pi, vector2.angle(vector2.new(-1, -2), vector2.new(1, 2)))
assert.number_close(math.pi/2, vector2.new(0, 1):angle(vector2.new(1, 0)))
end)
it("dot()", function()
assert.equal(-5, vector2.dot(vector2.new(-1, -2), vector2.new(1, 2)))
assert.equal(0, vector2.zero():dot(vector2.new(1, 2)))
end)
it("offset()", function()
assert.same({x = 41, y = 52}, vector2.offset(vector2.new(1, 2), 40, 50))
assert.same(vector2.new(41, 52), vector2.offset(vector2.new(1, 2), 40, 50))
assert.same(vector2.new(41, 52), vector2.new(1, 2):offset(40, 50))
end)
it("check()", function()
assert.is_false(vector2.check(nil))
assert.is_false(vector2.check(1))
assert.is_false(vector2.check({x = 1, y = 2}))
local real = vector2.new(1, 2)
assert.is_true(vector2.check(real))
assert.is_true(real:check())
end)
it("abusing works", function()
local v = vector2.new(1, 2)
v.a = 1
assert.equal(1, v.a)
local a_is_there = false
for key, value in pairs(v) do
if key == "a" then
a_is_there = true
assert.equal(value, 1)
break
end
end
assert.is_true(a_is_there)
end)
it("add()", function()
local a = vector2.new(1, 2)
local b = vector2.new(1, 4)
local c = vector2.new(2, 6)
assert.same(c, vector2.add(a, {x = 1, y = 4}))
assert.same(c, vector2.add(a, b))
assert.same(c, a:add(b))
assert.same(c, a + b)
assert.same(c, b + a)
end)
it("subtract()", function()
local a = vector2.new(1, 2)
local b = vector2.new(2, 4)
local c = vector2.new(-1, -2)
assert.same(c, vector2.subtract(a, {x = 2, y = 4}))
assert.same(c, vector2.subtract(a, b))
assert.same(c, a:subtract(b))
assert.same(c, a - b)
assert.same(c, -b + a)
end)
it("multiply()", function()
local a = vector2.new(1, 2)
local s = 2
local d = vector2.new(2, 4)
assert.same(d, vector2.multiply(a, s))
assert.same(d, a:multiply(s))
assert.same(d, a * s)
assert.same(d, s * a)
assert.same(-a, -1 * a)
end)
it("divide()", function()
local a = vector2.new(1, 2)
local s = 2
local d = vector2.new(0.5, 1)
assert.same(d, vector2.divide(a, s))
assert.same(d, a:divide(s))
assert.same(d, a / s)
assert.same(d, 1/s * a)
assert.same(-a, a / -1)
end)
it("to_string()", function()
local v = vector2.new(1, 2)
local str1 = vector2.to_string(v)
local str2 = v:to_string()
local str3 = tostring(v)
-- All should produce the same string
assert.same(str1, str2)
assert.same(str1, str3)
-- Verify the string format
assert.same("(1, 2)", str1)
-- Test edge cases for %g format
assert.same("(0, 0)", vector2.to_string(vector2.new(0, 0)))
assert.same("(-1, -2)", vector2.to_string(vector2.new(-1, -2)))
assert.same("(0.0001, 1e+10)", vector2.to_string(vector2.new(0.0001, 1e10)))
assert.same("(3.14159, 1.41421)", vector2.to_string(vector2.new(math.pi, math.sqrt(2))))
end)
it("from_string()", function()
local v = vector2.new(1, 2)
assert.is_true(vector2.check(vector2.from_string("(1, 2)")))
assert.same({v, 7}, {vector2.from_string("(1, 2)")})
assert.same({v, 7}, {vector2.from_string("(1,2 )")})
assert.same({v, 7}, {vector2.from_string("(1,2,)")})
assert.same({v, 6}, {vector2.from_string("(1 2)")})
assert.same({v, 9}, {vector2.from_string("( 1, 2 )")})
assert.same({v, 9}, {vector2.from_string(" ( 1, 2) ")})
assert.same({vector2.zero(), 6}, {vector2.from_string("(0,0) ( 1, 2) ")})
assert.same({v, 14}, {vector2.from_string("(0,0) ( 1, 2) ", 6)})
assert.same({v, 14}, {vector2.from_string("(0,0) ( 1, 2) ", 7)})
assert.is_nil(vector2.from_string("nothing"))
end)
describe("from_angle()", function()
it("creates unit vector from angle", function()
assert.vector2_close(vector2.new(1, 0), vector2.from_angle(0))
assert.vector2_close(vector2.new(0, 1), vector2.from_angle(math.pi / 2))
assert.vector2_close(vector2.new(-1, 0), vector2.from_angle(math.pi))
end)
it("throws on invalid input", function()
assert.has.errors(function()
vector2.from_angle()
end)
end)
end)
describe("to_angle()", function()
it("returns angle of vector", function()
assert.number_close(0, vector2.to_angle(vector2.new(1, 0)))
assert.number_close(math.pi / 2, vector2.to_angle(vector2.new(0, 1)))
assert.number_close(math.pi / 4, vector2.to_angle(vector2.new(1, 1)))
end)
it("is inverse of from_angle", function()
local angle = math.pi / 3
local v = vector2.from_angle(angle)
assert.number_close(angle, vector2.to_angle(v))
end)
end)
describe("rotate()", function()
it("rotates vector by angle in radians", function()
assert.vector2_close(vector2.new(0, 1), vector2.rotate(vector2.new(1, 0), math.pi / 2))
assert.vector2_close(vector2.new(-1, 0), vector2.rotate(vector2.new(1, 0), math.pi))
assert.vector2_close(vector2.new(0, 1), vector2.new(1, 0):rotate(math.pi / 2))
end)
it("preserves length", function()
local v = vector2.new(3, 4)
local rotated = vector2.rotate(v, math.pi / 3)
assert.number_close(vector2.length(v), vector2.length(rotated))
end)
end)
it("in_area()", function()
assert.is_true(vector2.in_area(vector2.zero(), vector2.new(-10, -10), vector2.new(10, 10)))
assert.is_true(vector2.in_area(vector2.new(-2, 5), vector2.new(-10, -10), vector2.new(10, 10)))
assert.is_true(vector2.in_area(vector2.new(-10, -10), vector2.new(-10, -10), vector2.new(10, 10)))
assert.is_false(vector2.in_area(vector2.new(-11, -10), vector2.new(-10, -10), vector2.new(10, 10)))
end)
end)
+275
View File
@@ -0,0 +1,275 @@
--[[
2D Vector helpers
Note: The vector2.*-functions must be able to accept old vectors that had no metatables
]]
-- localize functions
local setmetatable = setmetatable
local math = math
vector2 = {}
local metatable = {}
vector2.metatable = metatable
local xy = {"x", "y"}
-- only called when rawget(v, key) returns nil
function metatable.__index(v, key)
return rawget(v, xy[key]) or vector2[key]
end
-- only called when rawget(v, key) returns nil
function metatable.__newindex(v, key, value)
rawset(v, xy[key] or key, value)
end
-- constructors
local function fast_new(x, y)
return setmetatable({x = x, y = y}, metatable)
end
function vector2.new(x, y)
assert(x and y, "Invalid arguments for vector2.new()")
return fast_new(x, y)
end
function vector2.zero()
return fast_new(0, 0)
end
function vector2.copy(v)
assert(v.x and v.y, "Invalid vector passed to vector2.copy()")
return fast_new(v.x, v.y)
end
function vector2.from_angle(angle)
assert(angle, "Invalid argument for vector2.from_angle()")
return fast_new(math.cos(angle), math.sin(angle))
end
function vector2.from_string(s, init)
local x, y, np = string.match(s, "^%s*%(%s*([^%s,]+)%s*[,%s]%s*([^%s,]+)%s*,?%s*%)()", init)
x = tonumber(x)
y = tonumber(y)
if not (x and y) then
return
end
return fast_new(x, y), np
end
function vector2.to_string(v)
return string.format("(%g, %g)", v.x, v.y)
end
metatable.__tostring = vector2.to_string
function vector2.equals(a, b)
return a.x == b.x and a.y == b.y
end
metatable.__eq = vector2.equals
-- unary operations
function vector2.length(v)
return math.sqrt(v.x * v.x + v.y * v.y)
end
function vector2.to_angle(v)
return math.atan2(v.y, v.x)
end
function vector2.normalize(v)
local len = vector2.length(v)
if len == 0 then
return fast_new(0, 0)
else
return vector2.divide(v, len)
end
end
function vector2.floor(v)
return vector2.apply(v, math.floor)
end
function vector2.round(v)
return vector2.apply(v, math.round)
end
function vector2.ceil(v)
return vector2.apply(v, math.ceil)
end
function vector2.sign(v, tolerance)
return vector2.apply(v, math.sign, tolerance)
end
function vector2.abs(v)
return vector2.apply(v, math.abs)
end
function vector2.apply(v, func, ...)
return fast_new(
func(v.x, ...),
func(v.y, ...)
)
end
function vector2.combine(a, b, func)
return fast_new(
func(a.x, b.x),
func(a.y, b.y)
)
end
function vector2.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y
return math.sqrt(x * x + y * y)
end
function vector2.direction(pos1, pos2)
return vector2.subtract(pos2, pos1):normalize()
end
function vector2.angle(a, b)
local dotp = vector2.dot(a, b)
local crossplen = math.abs(a.x * b.y - a.y * b.x)
return math.atan2(crossplen, dotp)
end
function vector2.dot(a, b)
return a.x * b.x + a.y * b.y
end
function vector2.rotate(v, angle)
local cosangle = math.cos(angle)
local sinangle = math.sin(angle)
return fast_new(
v.x * cosangle - v.y * sinangle,
v.x * sinangle + v.y * cosangle
)
end
function metatable.__unm(v)
return fast_new(-v.x, -v.y)
end
-- add, sub, mul, div operations
function vector2.add(a, b)
if type(b) == "table" then
return fast_new(
a.x + b.x,
a.y + b.y
)
else
return fast_new(
a.x + b,
a.y + b
)
end
end
function metatable.__add(a, b)
return fast_new(
a.x + b.x,
a.y + b.y
)
end
function vector2.subtract(a, b)
if type(b) == "table" then
return fast_new(
a.x - b.x,
a.y - b.y
)
else
return fast_new(
a.x - b,
a.y - b
)
end
end
function metatable.__sub(a, b)
return fast_new(
a.x - b.x,
a.y - b.y
)
end
function vector2.multiply(a, b)
return fast_new(
a.x * b,
a.y * b
)
end
function metatable.__mul(a, b)
if type(a) == "table" then
return fast_new(
a.x * b,
a.y * b
)
else
return fast_new(
a * b.x,
a * b.y
)
end
end
function vector2.divide(a, b)
return fast_new(
a.x / b,
a.y / b
)
end
-- vector÷vector makes no sense
metatable.__div = vector2.divide
-- misc stuff
function vector2.offset(v, x, y)
return fast_new(
v.x + x,
v.y + y
)
end
function vector2.sort(a, b)
return fast_new(math.min(a.x, b.x), math.min(a.y, b.y)),
fast_new(math.max(a.x, b.x), math.max(a.y, b.y))
end
function vector2.check(v)
return getmetatable(v) == metatable
end
function vector2.in_area(pos, min, max)
return (pos.x >= min.x) and (pos.x <= max.x) and
(pos.y >= min.y) and (pos.y <= max.y)
end
function vector2.random_direction()
-- Generate a random direction of unit length
local angle = math.random() * 2 * math.pi
return fast_new(math.cos(angle), math.sin(angle))
end
if rawget(_G, "core") and core.set_read_vector2 and core.set_push_vector2 then
local function read_vector2(v)
return v.x, v.y
end
core.set_read_vector2(read_vector2)
core.set_read_vector2 = nil
if rawget(_G, "jit") then
-- This is necessary to prevent trace aborts.
local function push_vector2(x, y)
return (fast_new(x, y))
end
core.set_push_vector2(push_vector2)
else
core.set_push_vector2(fast_new)
end
core.set_push_vector2 = nil
end
+1
View File
@@ -44,6 +44,7 @@ local asyncpath = scriptdir .. "async" .. DIR_DELIM
dofile(commonpath .. "math.lua")
dofile(commonpath .. "vector.lua")
dofile(commonpath .. "vector2.lua")
dofile(commonpath .. "strict.lua")
dofile(commonpath .. "serialize.lua")
dofile(commonpath .. "misc_helpers.lua")
+226 -114
View File
@@ -3995,6 +3995,152 @@ or [Wikipedia](https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Orienta
for a more detailed and pictorial explanation of these terms.
Vectors
=======
Luanti provides two vector classes for working with coordinates and mathematical operations:
* **Spatial Vectors** (`vector.*`) - 3-dimensional vectors for 3D positions, directions, and spatial operations
* **2D Vectors** (`vector2.*`) - 2-dimensional vectors for 2D positions, screen coordinates, and 2D operations
Both vector types share many common properties and operations, which are described in the following sections.
Common to all vector types
---------------------------
### Special properties
Vectors can be indexed with numbers and allow method and operator syntax.
All these forms of addressing a vector `v` are valid:
* For 3D vectors: `v[1]`, `v[3]`, `v.x`, `v[1] = 42`, `v.y = 13`
* For 2D vectors: `v[1]`, `v[2]`, `v.x`, `v[1] = 42`, `v.y = 13`
Note: Prefer letter over number indexing for performance and compatibility reasons.
Where `v` is a vector and `foo` stands for any function name, `v:foo(...)` does
the same as `vector.foo(v, ...)` (or `vector2.foo(v, ...)` for 2D vectors).
`tostring` is defined for vectors, see `vector.to_string` and `vector2.to_string`.
The metatable that is used for vectors can be accessed via `vector.metatable` or `vector2.metatable`.
Do not modify it!
All `vector.*` and `vector2.*` functions allow vectors (e.g., `{x = X, y = Y, z = Z}`) without metatables.
Returned vectors always have a metatable set.
Note: Vectors are *not* used for simple numeric arrays of the form `{num, num, num}` or `{num, num}`.
Use proper vector tables with named fields (`x`, `y`, `z`) instead.
### Operators
Operators can be used if all of the involved vectors have metatables:
* `v1 == v2`:
* Returns whether `v1` and `v2` are identical.
* `-v`:
* Returns the additive inverse of v.
* `v1 + v2`:
* Returns the sum of both vectors.
* Note: `+` cannot be used together with scalars.
* `v1 - v2`:
* Returns the difference of `v1` subtracted by `v2`.
* Note: `-` cannot be used together with scalars.
* `v * s` or `s * v`:
* Returns `v` scaled by `s`.
* `v / s`:
* Returns `v` scaled by `1 / s`.
### Common functions
The following functions are available for both `vector` and `vector2` types with the same signature and behavior.
Replace `vector` with `vector2` for 2D vectors (e.g., `vector2.add(v, x)`).
For the following functions,
`v`, `v1`, `v2` are vectors (either 3D or 2D depending on context),
`p1`, `p2` are position vectors,
`s` is a scalar (a number).
* `vector.copy(v)`:
* Returns a copy of the vector `v`.
* `vector.zero()`:
* Returns a new zero vector.
* For 3D: `(0, 0, 0)`. For 2D: `(0, 0)`.
* `vector.random_direction()`:
* Returns a new vector of length 1, pointing in a direction chosen uniformly at random.
* `vector.from_string(s[, init])`:
* Returns `v, np`, where `v` is a vector read from the given string `s` and
`np` is the next position in the string after the vector.
* Returns `nil` on failure.
* `s`: Has to begin with a substring of the form `"(x, y, z)"` (for 3D) or `"(x, y)"` (for 2D).
Additional spaces, omitting commas and adding an additional comma to the end is allowed.
* `init`: If given starts looking for the vector at this string index.
* `vector.to_string(v)`:
* Returns a human-readable string of the form `"(x, y, z)"` (for 3D) or `"(x, y)"` (for 2D).
* `tostring(v)` does the same.
* Note: This function loses precision. For exact precision, use `core.serialize()` instead.
* Note: Precision may increase in future versions.
* `vector.direction(p1, p2)`:
* Returns a vector of length 1 with direction `p1` to `p2`.
* If `p1` and `p2` are identical, returns a zero vector.
* `vector.distance(p1, p2)`:
* Returns zero or a positive number, the distance between `p1` and `p2`.
* `vector.length(v)`:
* Returns zero or a positive number, the length of vector `v`.
* `vector.normalize(v)`:
* Returns a vector of length 1 with direction of vector `v`.
* If `v` has zero length, returns a zero vector.
* `vector.floor(v)`:
* Returns a vector, each dimension rounded down.
* `vector.ceil(v)`:
* Returns a vector, each dimension rounded up.
* `vector.round(v)`:
* Returns a vector, each dimension rounded to nearest integer.
* At a multiple of 0.5, rounds away from zero.
* `vector.sign(v, tolerance)`:
* Returns a vector where `math.sign` was called for each component.
* See [Helper functions](#helper-functions) for details on `math.sign`.
* `vector.abs(v)`:
* Returns a vector with absolute values for each component.
* `vector.apply(v, func, ...)`:
* Returns a vector where the function `func` has been applied to each component.
* `...` are optional arguments passed to `func`.
* `vector.combine(v, w, func)`:
* Returns a vector where the function `func` has combined both components of `v` and `w`
for each component.
* `vector.equals(v1, v2)`:
* Returns a boolean, `true` if the vectors are identical.
* `vector.dot(v1, v2)`:
* Returns the dot product of `v1` and `v2`.
* `vector.check(v)`:
* Returns a boolean value indicating whether `v` is a real vector, e.g. created
by a `vector.*` or `vector2.*` function.
* Returns `false` for anything else, including tables like `{x=3, y=1, z=4}` or `{x=3, y=1}`.
* `vector.in_area(pos, min, max)`:
* Returns a boolean value indicating if `pos` is inside area formed by `min` and `max`.
* `min` and `max` are inclusive.
* If `min` is bigger than `max` on some axis, function always returns false.
* You can use `vector.sort` (or `vector2.sort` for 2D) if you have two vectors and don't know which are the minimum and the maximum.
For the following functions `x` can be either a vector or a number:
* `vector.add(v, x)`:
* Returns a vector.
* If `x` is a vector: Returns the sum of `v` and `x`.
* If `x` is a number: Adds `x` to each component of `v`.
* `vector.subtract(v, x)`:
* Returns a vector.
* If `x` is a vector: Returns the difference of `v` subtracted by `x`.
* If `x` is a number: Subtracts `x` from each component of `v`.
* `vector.multiply(v, s)`:
* Returns a scaled vector.
* For `vector` only, deprecated behavior: If `s` is a vector, returns the Schur product.
* `vector.divide(v, s)`:
* Returns a scaled vector.
* For `vector` only, deprecated behavior: If `s` is a vector, returns the Schur quotient.
Spatial Vectors
===============
@@ -4008,11 +4154,6 @@ Spatial vectors are used for various things, including, but not limited to:
* Euler angles (pitch/yaw/roll in radians) (Spatial vectors have no real semantic
meaning here. Therefore, most vector operations make no sense in this use case.)
Note that they are *not* used for:
* n-dimensional vectors where n is not 3 (ie. n=2)
* arrays of the form `{num, num, num}`
The API documentation may refer to spatial vectors, as produced by `vector.new`,
by any of the following notations:
@@ -4043,27 +4184,18 @@ stated otherwise. Mods should adapt this for convenience reasons.
Special properties of the class
-------------------------------
Vectors can be indexed with numbers and allow method and operator syntax.
For special properties common to all vector types (indexing, method syntax, operators, etc.),
see [Common to all vector types](#common-to-all-vector-types).
All these forms of addressing a vector `v` are valid:
`v[1]`, `v[3]`, `v.x`, `v[1] = 42`, `v.y = 13`
Note: Prefer letter over number indexing for performance and compatibility reasons.
Functions
---------
Where `v` is a vector and `foo` stands for any function name, `v:foo(...)` does
the same as `vector.foo(v, ...)`, apart from deprecated functionality.
For common functions available to both `vector` and `vector2`,
see [Common functions](#common-functions).
`tostring` is defined for vectors, see `vector.to_string`.
The following functions are specific to `vector` (3D vectors).
The metatable that is used for vectors can be accessed via `vector.metatable`.
Do not modify it!
All `vector.*` functions allow vectors `{x = X, y = Y, z = Z}` without metatables.
Returned vectors always have a metatable set.
Common functions and methods
----------------------------
For the following functions (and subchapters),
For the following functions,
`v`, `v1`, `v2` are vectors,
`p1`, `p2` are position vectors,
`s` is a scalar (a number),
@@ -4073,114 +4205,23 @@ vectors are written like this: `(x, y, z)`:
* Returns a new vector `(a, b, c)`.
* Deprecated: `vector.new()` does the same as `vector.zero()` and
`vector.new(v)` does the same as `vector.copy(v)`
* `vector.zero()`:
* Returns a new vector `(0, 0, 0)`.
* `vector.random_direction()`:
* Returns a new vector of length 1, pointing into a direction chosen uniformly at random.
* `vector.copy(v)`:
* Returns a copy of the vector `v`.
* `vector.from_string(s[, init])`:
* Returns `v, np`, where `v` is a vector read from the given string `s` and
`np` is the next position in the string after the vector.
* Returns `nil` on failure.
* `s`: Has to begin with a substring of the form `"(x, y, z)"`. Additional
spaces, leaving away commas and adding an additional comma to the end
is allowed.
* `init`: If given starts looking for the vector at this string index.
* `vector.to_string(v)`:
* Returns a string of the form `"(x, y, z)"`.
* `tostring(v)` does the same.
* `vector.direction(p1, p2)`:
* Returns a vector of length 1 with direction `p1` to `p2`.
* If `p1` and `p2` are identical, returns `(0, 0, 0)`.
* `vector.distance(p1, p2)`:
* Returns zero or a positive number, the distance between `p1` and `p2`.
* `vector.length(v)`:
* Returns zero or a positive number, the length of vector `v`.
* `vector.normalize(v)`:
* Returns a vector of length 1 with direction of vector `v`.
* If `v` has zero length, returns `(0, 0, 0)`.
* `vector.floor(v)`:
* Returns a vector, each dimension rounded down.
* `vector.ceil(v)`:
* Returns a vector, each dimension rounded up.
* `vector.round(v)`:
* Returns a vector, each dimension rounded to nearest integer.
* At a multiple of 0.5, rounds away from zero.
* `vector.sign(v, tolerance)`:
* Returns a vector where `math.sign` was called for each component.
* See [Helper functions](#helper-functions) for details.
* `vector.abs(v)`:
* Returns a vector with absolute values for each component.
* `vector.apply(v, func, ...)`:
* Returns a vector where the function `func` has been applied to each
component.
* `...` are optional arguments passed to `func`.
* `vector.combine(v, w, func)`:
* Returns a vector where the function `func` has combined both components of `v` and `w`
for each component
* `vector.equals(v1, v2)`:
* Returns a boolean, `true` if the vectors are identical.
* `vector.sort(v1, v2)`:
* Returns in order minp, maxp vectors of the cuboid defined by `v1`, `v2`.
* `vector.angle(v1, v2)`:
* Returns the angle between `v1` and `v2` in radians.
* `vector.dot(v1, v2)`:
* Returns the dot product of `v1` and `v2`.
* `vector.cross(v1, v2)`:
* Returns the cross product of `v1` and `v2`.
* `vector.offset(v, x, y, z)`:
* Returns the sum of the vectors `v` and `(x, y, z)`.
* `vector.check(v)`:
* Returns a boolean value indicating whether `v` is a real vector, eg. created
by a `vector.*` function.
* Returns `false` for anything else, including tables like `{x=3,y=1,z=4}`.
* `vector.in_area(pos, min, max)`:
* Returns a boolean value indicating if `pos` is inside area formed by `min` and `max`.
* `min` and `max` are inclusive.
* If `min` is bigger than `max` on some axis, function always returns false.
* You can use `vector.sort` if you have two vectors and don't know which are the minimum and the maximum.
* `vector.random_in_area(min, max)`:
* Returns a random integer position in area formed by `min` and `max`
* `min` and `max` are inclusive.
* You can use `vector.sort` if you have two vectors and don't know which are the minimum and the maximum.
For the following functions `x` can be either a vector or a number:
* `vector.add(v, x)`:
* Returns a vector.
* If `x` is a vector: Returns the sum of `v` and `x`.
* If `x` is a number: Adds `x` to each component of `v`.
* `vector.subtract(v, x)`:
* Returns a vector.
* If `x` is a vector: Returns the difference of `v` subtracted by `x`.
* If `x` is a number: Subtracts `x` from each component of `v`.
* `vector.multiply(v, s)`:
* Returns a scaled vector.
* Deprecated: If `s` is a vector: Returns the Schur product.
* `vector.divide(v, s)`:
* Returns a scaled vector.
* Deprecated: If `s` is a vector: Returns the Schur quotient.
Operators
---------
Operators can be used if all of the involved vectors have metatables:
* `v1 == v2`:
* Returns whether `v1` and `v2` are identical.
* `-v`:
* Returns the additive inverse of v.
* `v1 + v2`:
* Returns the sum of both vectors.
* Note: `+` cannot be used together with scalars.
* `v1 - v2`:
* Returns the difference of `v1` subtracted by `v2`.
* Note: `-` cannot be used together with scalars.
* `v * s` or `s * v`:
* Returns `v` scaled by `s`.
* `v / s`:
* Returns `v` scaled by `1 / s`.
For vector operators (`+`, `-`, `*`, `/`, `==`, unary `-`), see [Common to all vector types](#common-to-all-vector-types).
Rotation-related functions
--------------------------
@@ -4219,6 +4260,77 @@ For example:
2D Vectors
==========
Luanti stores 2-dimensional vectors in Lua as tables of 2 coordinates,
and has a class to represent them (`vector2.*`).
The API provides `vector2.new` to create vectors:
* `vector2.new(x, y)`
* `{x=num, y=num}` (Even here you are still supposed to use `vector2.new`.)
Compatibility notes
-------------------
Vectors should be created using `vector2.new(x, y)` to ensure they have the
proper metatable. This enables:
* Method call syntax (e.g., `v:length()` instead of `vector2.length(v)`)
* Operator overloading (e.g., `v1 + v2` instead of `vector2.add(v1, v2)`)
* Type checking with `vector2.check()`
Special properties of the class
-------------------------------
For special properties common to all vector types (indexing, method syntax, operators, etc.),
see [Common to all vector types](#common-to-all-vector-types).
Functions
---------
For common functions available to both `vector` and `vector2`,
see [Common functions](#common-functions).
The following functions are specific to `vector2` (2D vectors).
For the following functions,
`v`, `v1`, `v2` are vectors,
`p1`, `p2` are position vectors,
`s` is a scalar (a number),
vectors are written like this: `(x, y)`:
* `vector2.new(x, y)`:
* Returns a new vector `(x, y)`.
* `vector2.from_angle(angle)`:
* Returns a new unit vector from an angle.
* `angle` is the angle in radians from the positive x-axis (counterclockwise).
* Example: `vector2.from_angle(math.pi / 2)` returns a vector pointing up `(0, 1)`.
* `vector2.to_angle(v)`:
* Returns the angle of the vector in radians.
* `angle` is the angle from the positive x-axis (counterclockwise), in the range `(-pi, pi]`.
* The edge case of `(0, 0)` returns `0`.
* Example: `vector2.to_angle(vector2.new(0, 1))` returns `math.pi / 2`.
* `vector2.sort(v1, v2)`:
* Returns in order minp, maxp vectors of the rectangle defined by `v1`, `v2`.
* `vector2.angle(v1, v2)`:
* Returns the angle between `v1` and `v2` in radians.
* This is always a positive value (unsigned angle).
* `vector2.rotate(v, angle)`:
* Returns a new vector rotated counterclockwise by `angle` radians around the origin.
* The length of the vector is preserved.
* Example: `vector2.rotate(vector2.new(1, 0), math.pi / 2)` returns `(0, 1)`.
* `vector2.offset(v, x, y)`:
* Returns the sum of the vectors `v` and `(x, y)`.
Operators
---------
For vector operators (`+`, `-`, `*`, `/`, `==`, unary `-`), see [Common to all vector types](#common-to-all-vector-types).
Helper functions
================
+1
View File
@@ -22,6 +22,7 @@ read_globals = {
"dump", "dump2",
"fgettext", "fgettext_ne",
"vector",
"vector2",
"VoxelArea",
"VoxelManip",
"profiler",
+1
View File
@@ -201,6 +201,7 @@ dofile(modpath .. "/inventory.lua")
dofile(modpath .. "/load_time.lua")
dofile(modpath .. "/on_shutdown.lua")
dofile(modpath .. "/color.lua")
dofile(modpath .. "/vector2.lua")
--------------
+42
View File
@@ -0,0 +1,42 @@
--
-- Vector2 engine API push/read test
--
-- This test verifies that the engine correctly pushes and reads 2D vectors
-- to and from Lua. It uses the spritediv property on player object properties,
-- which is a 2D vector in the engine (v2s16).
-- The test ensures that metatables are correctly set by vector2.check().
--
local function test_vector2_push_read(player)
-- Get original properties to restore later
local old_props = player:get_properties()
-- Set a vector2 value via engine API (spritediv is a v2s16 in the engine)
local test_vector = vector2.new(5, 8)
player:set_properties({spritediv = test_vector})
-- Read back the value from engine
local props = player:get_properties()
local retrieved_vector = props.spritediv
-- Verify the engine correctly pushed a vector2 with proper metatable
assert(vector2.check(retrieved_vector), "Retrieved spritediv is not a valid vector2")
-- Verify the values are correct
assert(retrieved_vector.x == 5, "spritediv.x should be 5")
assert(retrieved_vector.y == 8, "spritediv.y should be 8")
-- Test with a table (should be converted by engine)
player:set_properties({spritediv = {x = 3, y = 7}})
props = player:get_properties()
retrieved_vector = props.spritediv
-- Verify the engine converted the table to a proper vector2
assert(vector2.check(retrieved_vector), "Retrieved spritediv from table is not a valid vector2")
assert(retrieved_vector.x == 3, "spritediv.x should be 3")
assert(retrieved_vector.y == 7, "spritediv.y should be 7")
-- Restore original properties
player:set_properties({spritediv = old_props.spritediv})
end
unittests.register("test_vector2_push_read", test_vector2_push_read, {player=true})
+32 -31
View File
@@ -71,6 +71,18 @@ static void read_v3_aux(lua_State *L, int index)
lua_call(L, 1, 3);
}
/**
* A helper which calls CUSTOM_RIDX_READ_VECTOR2 with the argument at the given index
*/
static void read_v2_aux(lua_State *L, int index)
{
CHECK_POS_TAB(index);
lua_pushvalue(L, index);
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_READ_VECTOR2);
lua_insert(L, -2);
lua_call(L, 1, 2);
}
// Retrieve an integer vector where all components are optional
template<class T>
static bool getv3intfield(lua_State *L, int index,
@@ -98,11 +110,10 @@ void push_v3f(lua_State *L, v3f p)
void push_v2f(lua_State *L, v2f p)
{
lua_createtable(L, 0, 2);
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_PUSH_VECTOR2);
lua_pushnumber(L, p.X);
lua_setfield(L, -2, "x");
lua_pushnumber(L, p.Y);
lua_setfield(L, -2, "y");
lua_call(L, 2, 1);
}
v2s16 read_v2s16(lua_State *L, int index)
@@ -117,20 +128,18 @@ void push_v2s16(lua_State *L, v2s16 p)
void push_v2s32(lua_State *L, v2s32 p)
{
lua_createtable(L, 0, 2);
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_PUSH_VECTOR2);
lua_pushinteger(L, p.X);
lua_setfield(L, -2, "x");
lua_pushinteger(L, p.Y);
lua_setfield(L, -2, "y");
lua_call(L, 2, 1);
}
void push_v2u32(lua_State *L, v2u32 p)
{
lua_createtable(L, 0, 2);
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_PUSH_VECTOR2);
lua_pushinteger(L, p.X);
lua_setfield(L, -2, "x");
lua_pushinteger(L, p.Y);
lua_setfield(L, -2, "y");
lua_call(L, 2, 1);
}
v2s32 read_v2s32(lua_State *L, int index)
@@ -140,34 +149,26 @@ v2s32 read_v2s32(lua_State *L, int index)
v2f read_v2f(lua_State *L, int index)
{
v2f p;
CHECK_POS_TAB(index);
lua_getfield(L, index, "x");
CHECK_POS_COORD2(-1, "x");
p.X = lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, index, "y");
read_v2_aux(L, index);
CHECK_POS_COORD2(-2, "x");
CHECK_POS_COORD2(-1, "y");
p.Y = lua_tonumber(L, -1);
lua_pop(L, 1);
return p;
float x = lua_tonumber(L, -2);
float y = lua_tonumber(L, -1);
lua_pop(L, 2);
return v2f(x, y);
}
v2f check_v2f(lua_State *L, int index)
{
v2f p;
CHECK_POS_TAB(index);
lua_getfield(L, index, "x");
CHECK_POS_COORD(-1, "x");
p.X = lua_tonumber(L, -1);
CHECK_FLOAT(p.X, "x");
lua_pop(L, 1);
lua_getfield(L, index, "y");
read_v2_aux(L, index);
CHECK_POS_COORD(-2, "x");
CHECK_POS_COORD(-1, "y");
p.Y = lua_tonumber(L, -1);
CHECK_FLOAT(p.Y, "y");
lua_pop(L, 1);
return p;
float x = lua_tonumber(L, -2);
float y = lua_tonumber(L, -1);
lua_pop(L, 2);
CHECK_FLOAT(x, "x");
CHECK_FLOAT(y, "y");
return v2f(x, y);
}
v3f read_v3f(lua_State *L, int index)
+2
View File
@@ -49,6 +49,8 @@ enum {
// trace them and optimize tables/string better than from the C API.
CUSTOM_RIDX_READ_VECTOR,
CUSTOM_RIDX_PUSH_VECTOR,
CUSTOM_RIDX_READ_VECTOR2,
CUSTOM_RIDX_PUSH_VECTOR2,
CUSTOM_RIDX_READ_NODE,
CUSTOM_RIDX_PUSH_NODE,
CUSTOM_RIDX_PUSH_MOVERESULT1,
+12
View File
@@ -127,6 +127,16 @@ ScriptApiBase::ScriptApiBase(ScriptingType type):
return 0;
});
lua_setfield(m_luastack, -2, "set_push_vector");
lua_pushcfunction(m_luastack, [](lua_State *L) -> int {
lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_READ_VECTOR2);
return 0;
});
lua_setfield(m_luastack, -2, "set_read_vector2");
lua_pushcfunction(m_luastack, [](lua_State *L) -> int {
lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_PUSH_VECTOR2);
return 0;
});
lua_setfield(m_luastack, -2, "set_push_vector2");
lua_pushcfunction(m_luastack, [](lua_State *L) -> int {
lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_READ_NODE);
return 0;
@@ -209,6 +219,8 @@ void ScriptApiBase::checkSetByBuiltin()
CHECK(CUSTOM_RIDX_READ_VECTOR, "read_vector");
CHECK(CUSTOM_RIDX_PUSH_VECTOR, "push_vector");
CHECK(CUSTOM_RIDX_READ_VECTOR2, "read_vector2");
CHECK(CUSTOM_RIDX_PUSH_VECTOR2, "push_vector2");
if (getType() == ScriptingType::Server ||
(getType() == ScriptingType::Async && m_gamedef) ||
+13
View File
@@ -103,6 +103,12 @@ void TestScriptApi::testVectorMetatable(MyScriptApi *script)
return lua_toboolean(L, -1);
};
const auto &call_vector2_check = [&] () -> bool {
lua_setglobal(L, "tmp");
run(L, "return vector2.check(tmp)", 1);
return lua_toboolean(L, -1);
};
push_v3s16(L, {1, 2, 3});
UASSERT(call_vector_check());
@@ -115,6 +121,13 @@ void TestScriptApi::testVectorMetatable(MyScriptApi *script)
push_v2f(L, {0, 0});
UASSERT(!call_vector_check());
// but they must have the vector2 metatable
push_v2s32(L, {0, 0});
UASSERT(call_vector2_check());
push_v2f(L, {0, 0});
UASSERT(call_vector2_check());
}
void TestScriptApi::testVectorRead(MyScriptApi *script)