41 Commits

Author SHA1 Message Date
f400a91e90 Remove functions which are deprecated and which I do not recommend to use
Removing deprecated functionality can help to clean up the code a bit.

I remove the following deprecated functions and values:
* vector.zero
* vector.plane()
* vector.pos_to_string()
* vector.quickadd()
* vector.scalar()
* vector.get_data_from_pos()
* vector.set_data_to_pos()
* vector.set_data_to_pos_optional()
* vector.remove_data_from_pos()
* vector.get_data_pos_table()

Furthermore, I remove the unused, experimental vector_meta.lua file
and make luacheck linting more strict so that it can show accidental
uses of deprecated functions in the code of this mod if there are any.

This change can break compatility with old mods which use vector_extras.
2024-08-27 17:38:30 +02:00
63edf837d7 depends.txt -> mod.conf 2021-10-10 12:57:57 +02:00
ed6b514057 Make vector.zero callable
This is needed to keep compatibility with new minetest builtin code
2021-09-05 19:04:02 +02:00
f8c12047d5 Add .luacheckrc and some code cleanup 2021-03-01 20:37:59 +01:00
fdbcc2e425 A small comment 2021-03-01 19:26:24 +01:00
8c7aaf6c0b Fix a bug in deprecated vector.plane 2020-08-25 11:03:17 +02:00
e826bbd9b9 Replace vector.scalar with vector.dot 2020-08-25 11:03:17 +02:00
6f2bc919db vector.triangle: (try to) avoid holes in neighbouring triangles 2020-04-18 14:16:35 +02:00
bc08421e20 Add documentation 2020-04-07 20:49:10 +02:00
8ddb3879fb Update README 2020-03-27 18:41:15 +01:00
275ec4af3b Remove unused vector.triangle code and try to make it a bit faster 2020-03-27 18:30:19 +01:00
f7dbb1a884 Add (unoptimized) vector.triangle 2020-03-27 18:17:25 +01:00
d1a3c95286 Remove the vector.cross and deprecate vector.scalar; those functions are in minetest now 2020-02-17 13:04:53 +01:00
9c6e53dd0a Add another untested fill algorithm and update LICENSE 2019-12-01 11:31:11 +01:00
dc368f7a7e Add untested efficient flood fill algorithm
http://www.adammil.net/blog/v126_A_More_Efficient_Flood_Fill.html
2019-12-01 11:00:53 +01:00
8d78f3cb9b Fix wrong file name in dofile
Excuse me for inconveniences, I hope nobody used content from vector_meta.lua.
2017-12-25 20:52:09 +01:00
02df8d2d46 Add new functions and remove deprecated/inefficient 2017-11-26 11:22:24 +01:00
fdb043d709 Thicken parabola 2017-06-19 17:52:01 +02:00
c221f0eec3 Fix parabola calculation mistakes, parabola works 2017-06-18 13:48:03 +02:00
2fce6dbdc9 test code 2017-06-11 09:32:04 +02:00
ecce896497 Add throw_parabola, doesnt work yet 2017-06-11 08:58:13 +02:00
472deb6c04 rename the sort function, fixes #2
M  init.lua
2017-01-25 09:43:00 +01:00
86d267e8f1 add vector.serialize
it works ~4.5 times as fast as dump
dump works ~1.3 times as fast as minetest.serialize if a vector is passed
2016-05-27 17:20:57 +02:00
83ff39b66a add get max coord and rayIter and improve fine line 2016-04-23 11:09:10 +02:00
934714fb48 🐈 2016-02-18 18:46:22 +01:00
5877a2c8bb 🐈 2016-02-06 12:04:27 +01:00
52adb555cb 🐈 2016-01-31 12:17:38 +01:00
70e08a15a2 returning the table isnt necessary, vector.sort usage changed a bit 2015-12-19 23:20:10 +01:00
177c679621 🐈 2015-08-31 11:29:17 +02:00
07d9584e32 add table modificating functions 2015-08-30 15:51:56 +02:00
8700859ea5 🐈 2015-08-27 21:23:11 +02:00
f85a54a070 🐈 2015-08-27 20:44:45 +02:00
bb71910c2c Add collision function to vector 2015-08-27 11:18:17 +02:00
95a528221c 🐈 2015-05-29 12:09:25 +02:00
55fd22890b 🐈 2015-05-12 21:29:05 +02:00
1db996a040 some changes 2015-04-25 09:54:07 +02:00
02291fcb65 edit code 2015-04-25 08:35:23 +02:00
c75042050a the triangle function works now 2015-04-25 08:28:18 +02:00
3ee60ac7a3 automatic update 2015-04-16 20:42:21 +02:00
622e644cfd automatic update 2015-03-07 16:02:28 +01:00
052a995875 automatic update 2014-12-28 22:01:45 +01:00
9 changed files with 943 additions and 238 deletions

11
.luacheckrc Normal file
View File

@ -0,0 +1,11 @@
read_globals = {
-- Defined by Minetest
"minetest", "PseudoRandom", "VoxelArea", "string", "dump", "math",
vector = {
fields = {
"add", "cross", "direction", "distance", "dot", "multiply",
"new", "normalize", "round", "subtract",
"from_number", "get_max_coord", "twoline", "threeline", "rayIter"
}
}
}

1
LICENSE.txt Normal file
View File

@ -0,0 +1 @@
CC0, except for code copied from e.g. minetest's builtin

View File

@ -1,2 +1,6 @@
TODO:
— add things to this list
* maybe make the explosion table function return a perlin explosion table
* Figure out and implement 3D scanline search
* Add vector.hollowsphere, less positions than WorldEdit hollowsphere
* Add unit tests
* Use %a string format for vector.serialize so that it is reversible

116
adammil_flood_fill.lua Normal file
View File

@ -0,0 +1,116 @@
-- http://www.adammil.net/blog/v126_A_More_Efficient_Flood_Fill.html
local can_go
local marked_places
local function calc_2d_index(x, y)
return (y + 32768) * 65536 + x + 32768
end
local function mark(x, y)
marked_places[calc_2d_index(x, y)] = true
end
local _fill
local function fill(x, y)
if can_go(x, y) then
_fill(x, y)
end
end
local corefill
function _fill(x, y)
while true do
local ox = x
local oy = y
while can_go(x, y-1) do
y = y-1
end
while can_go(x-1, y) do
x = x-1
end
if x == ox
and y == oy then
break
end
end
corefill(x, y)
end
function corefill(x, y)
local lastcnt = 0
repeat
local cnt = 0
local sx = x
if lastcnt ~= 0
and not can_go(y, x) then
-- go right to find the x start
repeat
lastcnt = lastcnt-1
if lastcnt == 0 then
return
end
x = x+1
until can_go(x, y)
sx = x
else
-- go left if possible, and mark and _fill above
while can_go(x-1, y) do
x = x-1
mark(x, y)
if can_go(x, y-1) then
_fill(x, y-1)
end
cnt = cnt+1
lastcnt = lastcnt+1
end
end
-- go right if possible, and mark
while can_go(sx, y) do
mark(sx, y)
cnt = cnt+1
sx = sx+1
end
if cnt < lastcnt then
local e = x + lastcnt
sx = sx+1
while sx < e do
if can_go(sx, y) then
corefill(sx, y)
end
sx = sx+1
end
elseif cnt > lastcnt then
local ux = x + lastcnt + 1
while ux < sx do
if can_go(ux, y-1) then
_fill(ux, y-1)
end
ux = ux+1
end
end
lastcnt = cnt
y = y+1
until lastcnt == 0
end
local function apply_fill(go_test, x0, y0, allow_revisit)
if allow_revisit then
can_go = go_test
else
local visited = {}
can_go = function(x, y)
local vi = calc_2d_index(x, y)
if visited[vi] then
return false
end
visited[vi] = true
return go_test(x, y)
end
end
marked_places = {}
fill(x0, y0)
return marked_places
end
return apply_fill

136
doc.md Normal file
View File

@ -0,0 +1,136 @@
# Vector helpers added by this mod
## Helpers which return many positions for a shape, e.g. a line
### Line functions
These may be deprecated since raycasting has been added to minetest.
See e.g. `minetest.line_of_sight`.
* `vector.line([pos, dir[, range][, alt]])`: returns a table of vectors
* `dir` is either a direction (when range is a number) or
the start position (when range is the end position).
* If alt is true, an old path calculation is used.
* `vector.twoline(x, y)`: can return e.g. `{{0,0}, {0,1}}`
* This is a lower-level function than `vector.line`; it can be used for
a 2D line.
* `vector.threeline(x, y, z)`: can return e.g. `{{0,0,0}, {0,1,0}}`
* Similar to `vector.twoline`; this one is for the 3D case.
* The parameters should be integers.
* `vector.rayIter(pos, dir)`: returns an iterator for a for loop
* `pos` can have non-integer values
* `vector.fine_line([pos, dir[, range], scale])`: returns a table of vectors
* Like `vector.line` but allows non-integer positions
* It uses `vector.rayIter`.
### Flood Fill
* `vector.search_2d(go_test, x0, y0, allow_revisit, give_map)`: returns e.g.
`{{0,0}, {0,1}}`
* This function uses a Flood Fill algorithm, so it can be used to detect
positions connected to each other in 2D.
* `go_test(x, y)` should be a function which returns true iff the algorithm
can "fill" at the position `(x, y)`.
* `(x0, y0)` defines the start position.
* If `allow_revisit` is false (the default), the function
invokes `go_test` only once at every potential position.
* If `give_map` is true (default is false), the function returns the
marked table, whose indices are 2D vector indices, instead of a list of
2D positions.
* `vector.search_3d(can_go, startpos, apply_move, moves)`: returns FIXME
* FIXME
### Other Shapes
* `vector.explosion_table(r)`: returns e.g. `{{pos1}, {pos2, true}}`
* The returned list of positions and boolean represents a sphere;
if the boolean is true, the position is on the outer side of the sphere.
* It might be used for explosion calculations; but `vector.explosion_perlin`
should make more realistic holes.
* `vector.explosion_perlin(rmin, rmax[, nparams])`: returns e.g.
`{{pos1}, {pos2, true}}`
* This function is similar to `vector.explosion_table`; the positions
do not represent a sphere but a more complex hole which is calculated
with the help of perlin noise.
* `rmin` and `rmax` represent the minimum and maximum radius,
and `nparams` (which has a default value) are parameters for the perlin
noise.
* `vector.circle(r)`: returns a table of vectors
* The returned positions represent a circle of radius `r` along the x and z
directions; the y coordinates are all zero.
* `vector.ring(r)`: returns a table of vectors
* This function is similar to `vector.circle`; the positions are all
touching each other (i.e. they are connected on whole surfaces and not
only infinitely thin edges), so it is called `ring` instead of `circle`
* `r` can be a non-integer number.
* `vector.throw_parabola(pos, vel, gravity, point_count, time)`
* FIXME: should return positions along a parabola so that moving objects
collisions can be calculated
* `vector.triangle(pos1, pos2, pos3)`: returns a table of positions, a number
and a table with barycentric coordinates
* This function calculates integer positions for a triangle defined by
`pos1`, `pos2` and `pos3`, so it can be used to place polygons in
minetest.
* The returned number is the number of positions.
* The barycentric coordinates are specified in a table with three elements;
the first one corresponds to `pos1`, etc.
## Helpers for various vector calculations
* `vector.sort_positions(ps[, preferred_coords])`
* Sorts a table of vectors `ps` along the coordinates specified in the
table `preferred_coords` in-place.
* If `preferred_coords` is omitted, it sorts along z, y and x in this order,
where z has the highest priority.
* `vector.maxnorm(v)`: returns the Tschebyshew norm of `v`
* `vector.sumnorm(v)`: returns the Manhattan norm of `v`
* `vector.pnorm(v, p)`: returns the `p` norm of `v`
* `vector.inside(pos, minp, maxp)`: returns a boolean
* Returns true iff `pos` is within the closed AABB defined by `minp`
and `maxp`.
* `vector.minmax(pos1, pos2)`: returns two vectors
* This does the same as `worldedit.sort_pos`.
* The components of the second returned vector are all bigger or equal to
those of the first one.
* `vector.move(pos1, pos2, length)`: returns a vector
* Go from `pos1` `length` metres to `pos2` and then round to the nearest
integer position.
* Made for rubenwardy
* `vector.from_number(i)`: returns `{x=i, y=i, z=i}`
* `vector.chunkcorner(pos)`: returns a vector
* Returns the mapblock position of the mapblock which contains
the integer position `pos`
* `vector.point_distance_minmax(p1, p2)`: returns two numbers
* Returns the minimum and maximum of the absolute component-wise distances
* `vector.collision(p1, p2)` FIXME
* `vector.update_minp_maxp(minp, maxp, pos)`
* Can change `minp` and `maxp` so that `pos` is within the AABB defined by
`minp` and `maxp`
* `vector.unpack(v)`: returns three numbers
* Returns `v.z, v.y, v.x`
* `vector.get_max_coord(v)`: returns a string
* Returns `"x"`, `"y"` or `"z"`, depending on which component has the
biggest value
* `vector.get_max_coords(v)`: returns three strings
* Similar to `vector.get_max_coord`; it returns the coordinates in the order
of their component values
* Example: `vector.get_max_coords{x=1, y=5, z=3}` returns `"y", "z", "x"`
* `vector.serialize(v)`: returns a string
* In comparison to `minetest.serialize`, this function uses a more compact
string for the serialization.
## Minetest-specific helper functions
* `vector.straightdelay([length, vel[, acc]])`: returns a number
* Returns the time an object takes to move `length` if it has velocity `vel`
and acceleration `acc`
* `vector.sun_dir([time])`: returns a vector or nil
* Returns the vector which points to the sun
* If `time` is omitted, it uses the current time.
* This function does not yet support the moon;
at night it simply returns `nil`.

53
fill_3d.lua Normal file
View File

@ -0,0 +1,53 @@
-- Algorithm created by sofar and changed by others:
-- https://github.com/minetest/minetest/commit/d7908ee49480caaab63d05c8a53d93103579d7a9
local function search(go, p, apply_move, moves)
local num_moves = #moves
-- We make a stack, and manually maintain size for performance.
-- Stored in the stack, we will maintain tables with pos, and
-- last neighbor visited. This way, when we get back to each
-- node, we know which directions we have already walked, and
-- which direction is the next to walk.
local s = {}
local n = 0
-- The neighbor order we will visit from our table.
local v = 1
while true do
-- Push current pos onto the stack.
n = n + 1
s[n] = {p = p, v = v}
-- Select next node from neighbor list.
p = apply_move(p, moves[v])
-- Now we check out the node. If it is in need of an update,
-- it will let us know in the return value (true = updated).
if not go(p) then
-- If we don't need to "recurse" (walk) to it then pop
-- our previous pos off the stack and continue from there,
-- with the v value we were at when we last were at that
-- node
repeat
local pop = s[n]
p = pop.p
v = pop.v
s[n] = nil
n = n - 1
-- If there's nothing left on the stack, and no
-- more sides to walk to, we're done and can exit
if n == 0 and v == num_moves then
return
end
until v < num_moves
-- The next round walk the next neighbor in list.
v = v + 1
else
-- If we did need to walk the neighbor, then
-- start walking it from the walk order start (1),
-- and not the order we just pushed up the stack.
v = 1
end
end
end
return search

674
init.lua
View File

@ -1,10 +1,9 @@
local load_time_start = os.clock()
local path = minetest.get_modpath"vector_extras"
function vector.pos_to_string(pos)
return "("..pos.x.."|"..pos.y.."|"..pos.z..")"
end
local funcs = {}
local r_corr = 0.25 --remove a bit more nodes (if shooting diagonal) to let it look like a hole (sth like antialiasing)
local r_corr = 0.25 --remove a bit more nodes (if shooting diagonal) to let it
-- look like a hole (sth like antialiasing)
-- this doesn't need to be calculated every time
local f_1 = 0.5-r_corr
@ -49,6 +48,7 @@ local function return_line(pos, dir, range) --range ~= length
local num = 1
local t_dir = get_used_dir(dir)
local dir_typ = t_dir[1]
local f_tab
if t_dir[3] == "+" then
f_tab = {0, range, 1}
else
@ -95,39 +95,54 @@ local function return_line(pos, dir, range) --range ~= length
return tab
end
local function table_contains2(t, v)
for i = #t, 1, -1 do
if t[i] == v then
return true
function funcs.rayIter(pos, dir)
-- make a table of possible movements
local step = {}
for i in pairs(pos) do
local v = math.sign(dir[i])
if v ~= 0 then
step[i] = v
end
end
return false
end
local function return_fine_line(pos, dir, range, scale)
local ps1 = return_line(vector.round(vector.multiply(pos, scale)), dir, range*scale)
local ps2 = {}
local ps2_num = 1
for _,p1 in ipairs(ps1) do
local p2 = vector.round(vector.divide(p1, scale))
if not table_contains2(ps2, p2) then
ps2[ps2_num] = p2
ps2_num = ps2_num+1
local p
return function()
if not p then
-- avoid skipping the first position
p = vector.round(pos)
return vector.new(p)
end
-- find the position which has the smallest distance to the line
local choose = {}
local choosefit = vector.new()
for i in pairs(step) do
choose[i] = vector.new(p)
choose[i][i] = choose[i][i] + step[i]
choosefit[i] = vector.dot(vector.normalize(vector.subtract(choose[i], pos)), dir)
end
p = choose[vector.get_max_coord(choosefit)]
return vector.new(p)
end
return ps2
end
function vector.fine_line(pos, dir, range, scale)
--assert_vector(pos)
function funcs.fine_line(pos, dir, range)
if not range then --dir = pos2
dir = vector.direction(pos, dir)
range = vector.distance(pos, dir)
dir, range = vector.direction(pos, dir), vector.distance(pos, dir)
end
return return_fine_line(pos, dir, range, scale)
local result,n = {},1
for p in vector.rayIter(pos, dir) do
if vector.distance(p, pos) > range then
break
end
result[n] = p
n = n+1
end
return result
end
function vector.line(pos, dir, range, alt)
function funcs.line(pos, dir, range, alt)
--assert_vector(pos)
if alt then
if not range then --dir = pos2
@ -135,9 +150,9 @@ function vector.line(pos, dir, range, alt)
end
return return_line(pos, dir, range)
end
if range then --dir = pos2
if range then
dir = vector.round(vector.multiply(dir, range))
else
else --dir = pos2
dir = vector.subtract(dir, pos)
end
local line,n = {},1
@ -149,7 +164,7 @@ function vector.line(pos, dir, range, alt)
end
local twolines = {}
function vector.twoline(x, y)
function funcs.twoline(x, y)
local pstr = x.." "..y
local line = twolines[pstr]
if line then
@ -183,14 +198,15 @@ function vector.twoline(x, y)
end
local threelines = {}
function vector.threeline(x, y, z)
function funcs.threeline(x, y, z)
local pstr = x.." "..y.." "..z
local line = threelines[pstr]
if line then
return line
end
if x ~= math.floor(x) then
print("[vector_extras] INFO: The position used for vector.threeline isn't round.")
minetest.log("error", "[vector_extras] INFO: The position used for " ..
"vector.threeline isn't round.")
end
local two_line = vector.twoline(x, y)
line = {}
@ -218,16 +234,46 @@ function vector.threeline(x, y, z)
return line
end
function vector.straightdelay(s, v, a)
function funcs.sort_positions(ps, preferred_coords)
preferred_coords = preferred_coords or {"z", "y", "x"}
local a,b,c = unpack(preferred_coords)
local function ps_sorting(p1, p2)
if p1[a] == p2[a] then
if p1[b] == p2[a] then
if p1[c] < p2[c] then
return true
end
elseif p1[b] < p2[b] then
return true
end
elseif p1[a] < p2[a] then
return true
end
end
table.sort(ps, ps_sorting)
end
-- Tschebyschew norm
function funcs.maxnorm(v)
return math.max(math.max(math.abs(v.x), math.abs(v.y)), math.abs(v.z))
end
function funcs.sumnorm(v)
return math.abs(v.x) + math.abs(v.y) + math.abs(v.z)
end
function funcs.pnorm(v, p)
return (math.abs(v.x)^p + math.abs(v.y)^p + math.abs(v.z)^p)^(1 / p)
end
function funcs.straightdelay(s, v, a)
if not a then
return s/v
end
return (math.sqrt(v*v+2*a*s)-v)/a
end
vector.zero = {x=0, y=0, z=0}
function vector.sun_dir(time)
function funcs.sun_dir(time)
if not time then
time = minetest.get_timeofday()
end
@ -240,8 +286,8 @@ function vector.sun_dir(time)
return {x=tmp, y=math.sqrt(1-tmp*tmp), z=0}
end
function vector.inside(pos, minp, maxp)
for _,i in ipairs({"x", "y", "z"}) do
function funcs.inside(pos, minp, maxp)
for _,i in pairs({"x", "y", "z"}) do
if pos[i] < minp[i]
or pos[i] > maxp[i] then
return false
@ -250,9 +296,9 @@ function vector.inside(pos, minp, maxp)
return true
end
function vector.minmax(p1, p2)
local p1 = vector.new(p1) --Are these 2 redefinitions necessary?
local p2 = vector.new(p2)
function funcs.minmax(pos1, pos2)
local p1 = vector.new(pos1)
local p2 = vector.new(pos2)
for _,i in ipairs({"x", "y", "z"}) do
if p1[i] > p2[i] then
p1[i], p2[i] = p2[i], p1[i]
@ -261,7 +307,7 @@ function vector.minmax(p1, p2)
return p1, p2
end
function vector.move(p1, p2, s)
function funcs.move(p1, p2, s)
return vector.round(
vector.add(
vector.multiply(
@ -276,21 +322,85 @@ function vector.move(p1, p2, s)
)
end
function funcs.from_number(i)
return {x=i, y=i, z=i}
end
local adammil_fill = dofile(path .. "/adammil_flood_fill.lua")
function funcs.search_2d(go_test, x0, y0, allow_revisit, give_map)
local marked_places = adammil_fill(go_test, x0, y0, allow_revisit)
if give_map then
return marked_places
end
local l = {}
for vi in pairs(marked_places) do
local x = (vi % 65536) - 32768
local y = (math.floor(x / 65536) % 65536) - 32768
l[#l+1] = {x, y}
end
return l
end
local fallings_search = dofile(path .. "/fill_3d.lua")
local moves_touch = {
{x = -1, y = 0, z = 0},
{x = 0, y = 0, z = 0}, -- FIXME should this be here?
{x = 1, y = 0, z = 0},
{x = 0, y = -1, z = 0},
{x = 0, y = 1, z = 0},
{x = 0, y = 0, z = -1},
{x = 0, y = 0, z = 1},
}
local moves_near = {}
for z = -1,1 do
for y = -1,1 do
for x = -1,1 do
moves_near[#moves_near+1] = {x = x, y = y, z = z}
end
end
end
function funcs.search_3d(can_go, startpos, apply_move, moves)
local visited = {}
local found = {}
local function on_visit(pos)
local vi = minetest.hash_node_position(pos)
if visited[vi] then
return false
end
visited[vi] = true
local valid_pos = can_go(pos)
if valid_pos then
found[#found+1] = pos
end
return valid_pos
end
if apply_move == "touch" then
apply_move = vector.add
moves = moves_touch
elseif apply_move == "near" then
apply_move = vector.add
moves = moves_near
end
fallings_search(on_visit, startpos, apply_move, moves)
end
local explosion_tables = {}
function vector.explosion_table(r)
function funcs.explosion_table(r)
local table = explosion_tables[r]
if table then
return table
end
local t1 = os.clock()
--~ local t1 = os.clock()
local tab, n = {}, 1
local tmp = r*r + r
for x=-r,r do
for y=-r,r do
for z=-r,r do
local rc = x*x+y*y+z*z
local rc = x*x+y*y+z*z
if rc <= tmp then
local np={x=x, y=y, z=z}
if math.floor(math.sqrt(rc) +0.5) > r-1 then
@ -304,12 +414,97 @@ function vector.explosion_table(r)
end
end
explosion_tables[r] = tab
print(string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
--~ minetest.log("info", string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
return tab
end
local default_nparams = {
offset = 0,
scale = 1,
seed = 1337,
octaves = 6,
persist = 0.6
}
function funcs.explosion_perlin(rmin, rmax, nparams)
local t1 = os.clock()
local r = math.ceil(rmax)
nparams = nparams or {}
for i,v in pairs(default_nparams) do
nparams[i] = nparams[i] or v
end
nparams.spread = nparams.spread or vector.from_number(r*5)
local pos = {x=math.random(-30000, 30000), y=math.random(-30000, 30000), z=math.random(-30000, 30000)}
local map = minetest.get_perlin_map(nparams, vector.from_number(r+r+1)
):get3dMap_flat(pos)
local id = 1
local bare_maxdist = rmax*rmax
local bare_mindist = rmin*rmin
local mindist = math.sqrt(bare_mindist)
local dist_diff = math.sqrt(bare_maxdist)-mindist
mindist = mindist/dist_diff
local pval_min, pval_max
local tab, n = {}, 1
for z=-r,r do
local bare_dist_z = z*z
for y=-r,r do
local bare_dist_yz = bare_dist_z + y*y
for x=-r,r do
local bare_dist = bare_dist_yz + x*x
local add = bare_dist < bare_mindist
local pval, distdiv
if not add
and bare_dist <= bare_maxdist then
distdiv = math.sqrt(bare_dist)/dist_diff-mindist
pval = math.abs(map[id]) -- strange perlin values…
if not pval_min then
pval_min = pval
pval_max = pval
else
pval_min = math.min(pval, pval_min)
pval_max = math.max(pval, pval_max)
end
add = true--distdiv < 1-math.abs(map[id])
end
if add then
tab[n] = {{x=x, y=y, z=z}, pval, distdiv}
n = n+1
end
id = id+1
end
end
end
-- change strange values
local pval_diff = pval_max - pval_min
pval_min = pval_min/pval_diff
for k,i in pairs(tab) do
if i[2] then
local new_pval = math.abs(i[2]/pval_diff - pval_min)
if i[3]+0.33 < new_pval then
tab[k] = {i[1]}
elseif i[3] < new_pval then
tab[k] = {i[1], true}
else
tab[k] = nil
end
end
end
minetest.log("info", string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
return tab
end
local circle_tables = {}
function vector.circle(r)
function funcs.circle(r)
local table = circle_tables[r]
if table then
return table
@ -327,12 +522,12 @@ function vector.circle(r)
end
end
circle_tables[r] = tab
print(string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
minetest.log("info", string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
return tab
end
local ring_tables = {}
function vector.ring(r)
function funcs.ring(r)
local table = ring_tables[r]
if table then
return table
@ -356,7 +551,8 @@ function vector.ring(r)
end
end
local tab2, n = {}, 1
local tab2 = {}
n = 1
for _,i in ipairs(tab) do
for _,j in ipairs({
{i.x, i.z},
@ -369,14 +565,384 @@ function vector.ring(r)
end
end
ring_tables[r] = tab2
print(string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
minetest.log("info", string.format("[vector_extras] table created after ca. %.2fs", os.clock() - t1))
return tab2
end
function vector.chunkcorner(pos)
local function get_parabola_points(pos, vel, gravity, waypoints, max_pointcount,
time)
local pointcount = 0
-- the height of the 45° angle point
local yswitch = -0.5 * (vel.x^2 + vel.z^2 - vel.y^2)
/ gravity + pos.y
-- the times of the 45° angle point
local vel_len = math.sqrt(vel.x^2 + vel.z^2)
local t_raise_end = (-vel_len + vel.y) / gravity
local t_fall_start = (vel_len + vel.y) / gravity
if t_fall_start > 0 then
-- the right 45° angle point wasn't passed yet
if t_raise_end > 0 then
-- put points from before the 45° angle
for y = math.ceil(pos.y), math.floor(yswitch +.5) do
local t = (vel.y -
math.sqrt(vel.y^2 + 2 * gravity * (pos.y - y))) / gravity
if t > time then
return
end
local p = {
x = math.floor(vel.x * t + pos.x +.5),
y = y,
z = math.floor(vel.z * t + pos.z +.5),
}
pointcount = pointcount+1
waypoints[pointcount] = {p, t}
if pointcount == max_pointcount then
return
end
end
end
-- smaller and bigger horizonzal pivot
local shp, bhp
if math.abs(vel.x) > math.abs(vel.z) then
shp = "z"
bhp = "x"
else
shp = "x"
bhp = "z"
end
-- put points between the 45° angles
local cstart, cdir
local cend = math.floor(vel[bhp] * t_fall_start + pos[bhp] +.5)
if vel[bhp] > 0 then
cstart = math.floor(math.max(pos[bhp],
vel[bhp] * t_raise_end + pos[bhp]) +.5)
cdir = 1
else
cstart = math.floor(math.min(pos[bhp],
vel[bhp] * t_raise_end + pos[bhp]) +.5)
cdir = -1
end
for i = cstart, cend, cdir do
local t = (i - pos[bhp]) / vel[bhp]
if t > time then
return
end
local p = {
[bhp] = i,
y = math.floor(-0.5 * gravity * t * t + vel.y * t + pos.y +.5),
[shp] = math.floor(vel[shp] * t + pos[shp] +.5),
}
pointcount = pointcount+1
waypoints[pointcount] = {p, t}
if pointcount == max_pointcount then
return
end
end
end
-- put points from after the 45° angle
local y = yswitch
if vel.y < 0
and pos.y < yswitch then
y = pos.y
end
y = math.floor(y +.5)
while pointcount < max_pointcount do
local t = (vel.y +
math.sqrt(vel.y^2 + 2 * gravity * (pos.y - y))) / gravity
if t > time then
return
end
local p = {
x = math.floor(vel.x * t + pos.x +.5),
y = y,
z = math.floor(vel.z * t + pos.z +.5),
}
pointcount = pointcount+1
waypoints[pointcount] = {p, t}
y = y-1
end
end
--[[
minetest.override_item("default:axe_wood", {
on_use = function(_, player)
local dir = player:get_look_dir()
local pos = player:getpos()
local grav = 0.03
local ps = vector.throw_parabola(pos, dir, grav, 80)
for i = 1,#ps do
minetest.set_node(ps[i], {name="default:stone"})
end
--~ for t = 0,50,3 do
--~ local p = {
--~ x = dir.x * t + pos.x,
--~ y = -0.5*grav*t*t + dir.y*t + pos.y,
--~ z = dir.z * t + pos.z
--~ }
--~ minetest.set_node(p, {name="default:sandstone"})
--~ end
end,
})--]]
function funcs.throw_parabola(pos, vel, gravity, point_count, time)
local waypoints = {}
get_parabola_points(pos, vel, gravity, waypoints, point_count,
time or math.huge)
local ps = {}
local ptscnt = #waypoints
local i = 1
while i < ptscnt do
local p,t = unpack(waypoints[i])
i = i+1
local p2,t2 = unpack(waypoints[i])
ps[#ps+1] = p
local dist = vector.distance(p, p2)
if dist < 1.1 then
if dist < 0.9 then
-- same position
i = i+1
end
-- touching
elseif dist < 1.7 then
-- common edge
-- get a list of possible positions between
local diff = vector.subtract(p2, p)
local possible_positions = {}
for c,v in pairs(diff) do
if v ~= 0 then
local pos_moved = vector.new(p)
pos_moved[c] = pos_moved[c] + v
possible_positions[#possible_positions+1] = pos_moved
end
end
-- test which one fits best
t = 0.5 * (t + t2)
local near_p = {
x = vel.x * t + pos.x,
y = -0.5 * gravity * t * t + vel.y * t + pos.y,
z = vel.z * t + pos.z,
}
local d = math.huge
for k = 1,2 do
local pos_moved = possible_positions[k]
local dist_current = vector.distance(pos_moved, near_p)
if dist_current < d then
p = pos_moved
d = dist_current
end
end
-- add it
ps[#ps+1] = p
elseif dist < 1.8 then
-- common vertex
for k = 1,2 do
-- get a list of possible positions between
local diff = vector.subtract(p2, p)
local possible_positions = {}
for c,v in pairs(diff) do
if v ~= 0 then
local pos_moved = vector.new(p)
pos_moved[c] = pos_moved[c] + v
possible_positions[#possible_positions+1] = pos_moved
end
end
-- test which one fits best
t = k / 3 * (t + t2)
local near_p = {
x = vel.x * t + pos.x,
y = -0.5 * gravity * t * t + vel.y * t + pos.y,
z = vel.z * t + pos.z,
}
local d = math.huge
assert(#possible_positions == 4-k, "how, number positions?")
for j = 1,4-k do
local pos_moved = possible_positions[j]
local dist_current = vector.distance(pos_moved, near_p)
if dist_current < d then
p = pos_moved
d = dist_current
end
end
-- add it
ps[#ps+1] = p
end
else
minetest.log("warning", "[vector_extras] A gap: " .. dist)
--~ error("A gap, it's a gap!: " .. dist)
end
end
if i == ptscnt then
ps[#ps+1] = waypoints[i]
end
return ps
end
function funcs.chunkcorner(pos)
return {x=pos.x-pos.x%16, y=pos.y-pos.y%16, z=pos.z-pos.z%16}
end
dofile(minetest.get_modpath("vector_extras").."/vector_meta.lua")
function funcs.point_distance_minmax(pos1, pos2)
local p1 = vector.new(pos1)
local p2 = vector.new(pos2)
local min, max, vmin, vmax, num
for _,i in ipairs({"x", "y", "z"}) do
num = math.abs(p1[i] - p2[i])
if not vmin or num < vmin then
vmin = num
min = i
end
if not vmax or num > vmax then
vmax = num
max = i
end
end
return min, max
end
print(string.format("[vector_extras] loaded after ca. %.2fs", os.clock() - load_time_start))
function funcs.collision(p1, p2)
local clear, node_pos = minetest.line_of_sight(p1, p2)
if clear then
return false
end
local collision_pos = {}
local _, max = funcs.point_distance_minmax(node_pos, p2)
if node_pos[max] > p2[max] then
collision_pos[max] = node_pos[max] - 0.5
else
collision_pos[max] = node_pos[max] + 0.5
end
local dmax = p2[max] - node_pos[max]
local dcmax = p2[max] - collision_pos[max]
local pt = dcmax / dmax
for _,i in ipairs({"x", "y", "z"}) do
collision_pos[i] = p2[i] - (p2[i] - node_pos[i]) * pt
end
return true, collision_pos, node_pos
end
function funcs.update_minp_maxp(minp, maxp, pos)
for _,i in pairs({"z", "y", "x"}) do
minp[i] = math.min(minp[i], pos[i])
maxp[i] = math.max(maxp[i], pos[i])
end
end
function funcs.unpack(pos)
return pos.z, pos.y, pos.x
end
function funcs.get_max_coord(vec)
if vec.x < vec.y then
if vec.y < vec.z then
return "z"
end
return "y"
end
if vec.x < vec.z then
return "z"
end
return "x"
end
function funcs.get_max_coords(pos)
if pos.x < pos.y then
if pos.y < pos.z then
return "z", "y", "x"
end
if pos.x < pos.z then
return "y", "z", "x"
end
return "y", "x", "z"
end
if pos.x < pos.z then
return "z", "x", "y"
end
if pos.y < pos.z then
return "x", "z", "y"
end
return "x", "y", "z"
end
function funcs.serialize(vec)
return "{x=" .. vec.x .. ",y=" .. vec.y .. ",z=" .. vec.z .. "}"
end
function funcs.triangle(pos1, pos2, pos3)
local normal = vector.cross(vector.subtract(pos2, pos1),
vector.subtract(pos3, pos1))
-- Find the biggest absolute component of the normal vector
local dir = vector.get_max_coord({
x = math.abs(normal.x),
y = math.abs(normal.y),
z = math.abs(normal.z),
})
-- Find the other directions for the for loops
local all_other_dirs = {
x = {"z", "y"},
y = {"z", "x"},
z = {"y", "x"},
}
local other_dirs = all_other_dirs[dir]
local odir1, odir2 = other_dirs[1], other_dirs[2]
local pos1_2d = {pos1[odir1], pos1[odir2]}
local pos2_2d = {pos2[odir1], pos2[odir2]}
local pos3_2d = {pos3[odir1], pos3[odir2]}
-- The boundaries of the 2D AABB along other_dirs
local p1 = {}
local p2 = {}
for i = 1,2 do
p1[i] = math.floor(math.min(pos1_2d[i], pos2_2d[i], pos3_2d[i]))
p2[i] = math.ceil(math.max(pos1_2d[i], pos2_2d[i], pos3_2d[i]))
end
-- https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage
local function edgefunc(vert1, vert2, pos)
return (pos[1] - vert1[1]) * (vert2[2] - vert1[2])
- (pos[2] - vert1[2]) * (vert2[1] - vert1[1])
end
-- eps is used to prevend holes in neighbouring triangles
-- It should be smaller than the smallest possible barycentric value
-- FIXME: I'm not sure if it really does what it should.
local eps = 0.5 / math.max(p2[1] - p1[1], p2[2] - p1[2])
local a_all_inv = 1.0 / edgefunc(pos1_2d, pos2_2d, pos3_2d)
local step_k3 = - (pos2_2d[1] - pos1_2d[1]) * a_all_inv
local step_k1 = - (pos3_2d[1] - pos2_2d[1]) * a_all_inv
-- Calculate the triangle points
local points = {}
local barycentric_coords = {}
local n = 0
-- It is possible to further optimize this
for v1 = p1[1], p2[1] do
local p = {v1, p1[2]}
local k3 = edgefunc(pos1_2d, pos2_2d, p) * a_all_inv
local k1 = edgefunc(pos2_2d, pos3_2d, p) * a_all_inv
for _ = p1[2], p2[2] do
local k2 = 1 - k1 - k3
if k1 >= -eps and k2 >= -eps and k3 >= -eps then
-- On triangle
local h = math.floor(k1 * pos1[dir] + k2 * pos2[dir] +
k3 * pos3[dir] + 0.5)
n = n+1
points[n] = {[odir1] = v1, [odir2] = p[2], [dir] = h}
barycentric_coords[n] = {k1, k2, k3}
end
p[2] = p[2]+1
k3 = k3 + step_k3
k1 = k1 + step_k1
end
end
return points, n, barycentric_coords
end
for name,func in pairs(funcs) do
if vector[name] then
minetest.log("error", "[vector_extras] vector."..name..
" already exists.")
else
-- luacheck: globals vector
vector[name] = func
end
end

1
mod.conf Normal file
View File

@ -0,0 +1 @@
name = vector_extras

View File

@ -1,183 +0,0 @@
vector.meta = vector.meta or {}
vector.meta.nodes = {}
vector.meta.nodes_file = {
load = function()
local nodesfile = io.open(minetest.get_worldpath()..'/vector_nodes.txt', "r")
if nodesfile then
local contents = nodesfile:read('*all')
io.close(nodesfile)
if contents ~= nil then
local lines = string.split(contents, "\n")
for _,entry in ipairs(lines) do
local name, px, py, pz, meta = unpack(string.split(entry, "°"))
vector.meta.set_node({x=px, y=py, z=pz}, name, meta)
end
end
end
end,
save = function() --WRITE CHANGES TO FILE
local output = ''
for x,ys in pairs(vector.meta.nodes) do
for y,zs in pairs(ys) do
for z,names in pairs(zs) do
for name,meta in pairs(names) do
output = name.."°"..x.."°"..y.."°"..z.."°"..dump(meta).."\n"
end
end
end
end
local f = io.open(minetest.get_worldpath()..'/vector_nodes.txt', "w")
f:write(output)
io.close(f)
end
}
local function table_empty(tab) --looks if it's an empty table
if next(tab) == nil then
return true
end
return false
end
function vector.meta.nodes_info() --returns an info string of the node table
local tmp = "[vector] "..dump(vector.meta.nodes).."\n[vector]:\n"
for x,a in pairs(vector.meta.nodes) do
for y,b in pairs(a) do
for z,c in pairs(b) do
for name,meta in pairs(c) do
tmp = tmp..">\t"..name.." "..x.." "..y.." "..z.." "..dump(meta).."\n"
end
end
end
end
return tmp
end
function vector.meta.clean_node_table() --replaces {} with nil
local again = true
while again do
again = false
for x,ys in pairs(vector.meta.nodes) do
if table_empty(ys) then
vector.meta.nodes[x] = nil
again = true
else
for y,zs in pairs(ys) do
if table_empty(zs) then
vector.meta.nodes[x][y] = nil
again = true
else
for z,names in pairs(zs) do
if table_empty(names) then
vector.meta.nodes[x][y][z] = nil
again = true
else
for name,meta in pairs(names) do
if table_empty(meta)
or meta == "" then
vector.meta.nodes[x][y][z][name] = nil
again = true
end
end
end
end
end
end
end
end
end
end
function vector.meta.complete_node_table(pos, name) --neccesary because tab[1] wouldn't work if tab is not a table
local tmp = vector.meta.nodes[pos.x]
if not tmp then
vector.meta.nodes[pos.x] = {}
end
local tmp = vector.meta.nodes[pos.x][pos.y]
if not tmp then
vector.meta.nodes[pos.x][pos.y] = {}
end
local tmp = vector.meta.nodes[pos.x][pos.y][pos.z]
if not tmp then
vector.meta.nodes[pos.x][pos.y][pos.z] = {}
end
local tmp = vector.meta.nodes[pos.x][pos.y][pos.z][name]
if not tmp then
vector.meta.nodes[pos.x][pos.y][pos.z][name] = {}
end
end
function vector.meta.get_node(pos, name)
if not pos then
return false
end
local tmp = vector.meta.nodes[pos.x]
if not tmp
or table_empty(tmp) then
return false
end
local tmp = vector.meta.nodes[pos.x][pos.y]
if not tmp
or table_empty(tmp) then
return false
end
local tmp = vector.meta.nodes[pos.x][pos.y][pos.z]
if not tmp
or table_empty(tmp) then
return false
end
-- if name isn't mentioned, just look if there's a node
if not name then
return true
end
local tmp = vector.meta.nodes[pos.x][pos.y][pos.z][name]
if not tmp
or table_empty(tmp) then
return false
end
return tmp
end
function vector.meta.remove_node(pos)
if not pos then
return false
end
if vector.meta.get_node(pos) then
vector.meta.nodes[pos.x][pos.y][pos.z] = nil
local xarr = vector.meta.nodes[pos.x]
if table_empty(xarr[pos.y]) then
vector.meta.nodes[pos.x][pos.y] = nil
end
if table_empty(xarr) then
vector.meta.nodes[pos.x] = nil
end
else
print("[vector_extras] Warning: The node at "..vector.pos_to_string(pos).." wasn't stored in vector.meta.nodes.")
end
end
function vector.meta.set_node(pos, name, meta)
if not (name or pos) then
return false
end
vector.meta.complete_node_table(pos, name)
meta = meta or true
vector.meta.nodes[pos.x][pos.y][pos.z][name] = meta
end
minetest.register_chatcommand('cleanvectormetatable',{
description = 'Tidy up it.',
params = "",
privs = {},
func = function(name)
vector.meta.clean_node_table()
local tmp = vector.meta.nodes_info()
minetest.chat_send_player(name, tmp)
print("[vector_extras] "..tmp)
end
})
vector.meta.nodes_file.load()