Minetest-WorldEditAdditions/worldeditadditions/floodfill.lua

133 lines
4.4 KiB
Lua

--- Flood-fill command for complex lakes etc.
-- @module worldeditadditions.floodfill
-------------------------------------------------------------------------------
--- A Queue implementation, taken & adapted from https://www.lua.org/pil/11.4.html
-- @submodule worldeditadditions.utils.queue
local Queue = {}
function Queue.new()
return { first = 0, last = -1 }
end
function Queue.enqueue(queue, value)
local new_last = queue.last + 1
queue.last = new_last
queue[new_last] = value
end
function Queue.contains(queue, value)
for i=queue.first,queue.last do
if queue[i] == value then
return true
end
end
return false
end
function Queue.is_empty(queue)
return queue.first > queue.last
end
function Queue.dequeue(queue)
local first = queue.first
if Queue.is_empty(queue) then
error("Error: The queue is empty!")
end
local value = queue[first]
queue[first] = nil -- Help the garbage collector out
queue.first = first + 1
return value
end
-------------------------------------------------------------------------------
-- These really should be in a utilities file, but Lua is being stupid and preventing access to it (and minetest is also being stupid, as we can't modularise our code the way you ought to be able to - or at least the documentation on dofile() is so poor I've no idea at this point)
local function vector2string(v)
return "(" .. v.x ..", " .. v.y ..", " .. v.z ..")"
end
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
function worldedit.floodfill(start_pos, radius, replace_node)
-- Calculate the area we want to modify
local pos1 = vector.add(start_pos, { x = radius, y = 0, z = radius })
local pos2 = vector.subtract(start_pos, { x = radius, y = radius, z = radius })
pos1, pos2 = worldedit.sort_pos(pos1, pos2) -- Just in case
minetest.log("action", "radius: " .. radius)
minetest.log("action", "pos1: " .. vector2string(pos1))
minetest.log("action", "pos2: " .. vector2string(pos2))
-- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data()
-- Setup for the floodfill operation itself
local start_pos_index = area:index(start_pos.x, start_pos.y, start_pos.z);
local search_id = data[start_pos_index]
local replace_id = minetest.get_content_id(replace_node)
minetest.log("action", "ids: " .. search_id .. " -> " .. replace_id)
local count = 0
local remaining_nodes = Queue.new()
Queue.enqueue(remaining_nodes, start_pos_index)
-- Do the floodfill
while Queue.is_empty(remaining_nodes) == false do
local cur = Queue.dequeue(remaining_nodes)
-- TODO: Check distance from start_pos
-- Replace this node
data[cur] = replace_id
count = count + 1
-- Check all the nearby nodes
-- We don't need to go upwards here, since we're filling in lake-style
local xplus = cur + 1 -- +X
if data[xplus] == search_id and not Queue.contains(remaining_nodes, xplus) then
-- minetest.log("action", "[floodfill] [+X] index " .. xplus .. " is a " .. data[xplus] .. ", searching for a " .. search_id)
Queue.enqueue(remaining_nodes, xplus)
end
local xminus = cur - 1 -- -X
if data[xminus] == search_id and not Queue.contains(remaining_nodes, xminus) then
-- minetest.log("action", "[floodfill] [-X] index " .. xminus .. " is a " .. data[xminus] .. ", searching for a " .. search_id)
Queue.enqueue(remaining_nodes, xminus)
end
local zplus = cur + area.zstride -- +Z
if data[zplus] == search_id and not Queue.contains(remaining_nodes, zplus) then
-- minetest.log("action", "[floodfill] [+Z] index " .. zplus .. " is a " .. data[zplus] .. ", searching for a " .. search_id)
Queue.enqueue(remaining_nodes, zplus)
end
local zminus = cur - area.zstride -- -Z
if data[zminus] == search_id and not Queue.contains(remaining_nodes, zminus) then
-- minetest.log("action", "[floodfill] [-Z] index " .. zminus .. " is a " .. data[zminus] .. ", searching for a " .. search_id)
Queue.enqueue(remaining_nodes, zminus)
end
local yminus = cur - area.ystride -- -Y
if data[yminus] == search_id and not Queue.contains(remaining_nodes, yminus) then
-- minetest.log("action", "[floodfill] [-Y] index " .. yminus .. " is a " .. data[yminus] .. ", searching for a " .. search_id)
Queue.enqueue(remaining_nodes, yminus)
end
count = count + 1
end
-- Save the modified nodes back to disk & return
worldedit.manip_helpers.finish(manip, data)
return count
end
return floodfill