Implement slope constraint for //layers, but it isn't tested yet

This commit is contained in:
Starbeamrainbowlabs 2021-08-04 01:41:51 +01:00
parent 902d5ddc8b
commit db830c6633
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
5 changed files with 105 additions and 33 deletions

View file

@ -9,6 +9,7 @@ Note to self: See the bottom of this file for the release template text.
- Add `//airapply` for applying commands only to air nodes in the defined region - Add `//airapply` for applying commands only to air nodes in the defined region
- Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues - Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues
- Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting - Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting
- Add optional slope constraint to `//layers` (inspired by [WorldPainter](https://worldpainter.net/))
## v1.12: The selection tools update (26th June 2021) ## v1.12: The selection tools update (26th June 2021)

View file

@ -1,8 +1,21 @@
--- Overlap command. Places a specified node on top of each column. -- ██ █████ ██ ██ ███████ ██████ ███████
-- @module worldeditadditions.layers -- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ███████ ████ █████ ██████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ███████ ██ ██ ███████
function worldeditadditions.layers(pos1, pos2, node_weights) local wea = worldeditadditions
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
--- Replaces the non-air nodes in each column with a list of nodes from top to bottom.
-- @param pos1 Vector Position 1 of the region to operate on
-- @param pos2 Vector Position 2 of the region to operate on
-- @param node_weights string[]
-- @param min_slope number?
-- @param max_slope number?
function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slope)
pos1, pos2 = wea.vector3.sort(pos1, pos2)
if not min_slope then min_slope = math.rad(0) end
if not max_slope then max_slope = math.rad(180) end
-- pos2 will always have the highest co-ordinates now -- pos2 will always have the highest co-ordinates now
-- Fetch the nodes in the specified area -- Fetch the nodes in the specified area
@ -11,38 +24,53 @@ function worldeditadditions.layers(pos1, pos2, node_weights)
local node_id_ignore = minetest.get_content_id("ignore") local node_id_ignore = minetest.get_content_id("ignore")
local node_ids, node_ids_count = worldeditadditions.unwind_node_list(node_weights) local node_ids, node_ids_count = wea.unwind_node_list(node_weights)
-- minetest.log("action", "pos1: " .. worldeditadditions.vector.tostring(pos1)) local heightmap, heightmap_size = wea.make_heightmap(
-- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) pos1, pos2,
manip, area, data
)
local slopemap = wea.calculate_slopes(heightmap, heightmap_size)
--luacheck:ignore 311
heightmap = nil -- Just in case Lua wants to garbage collect it
-- minetest.log("action", "pos1: " .. wea.vector.tostring(pos1))
-- minetest.log("action", "pos2: " .. wea.vector.tostring(pos2))
-- for i,v in ipairs(node_ids) do -- for i,v in ipairs(node_ids) do
-- print("[layer] i", i, "node id", v) -- print("[layer] i", i, "node id", v)
-- end -- end
-- z y x is the preferred loop order, but that isn't really possible here -- z y x is the preferred loop order, but that isn't really possible here
local changes = { replaced = 0, skipped_columns = 0 } local changes = { replaced = 0, skipped_columns = 0, skipped_columns_slope = 0 }
for z = pos2.z, pos1.z, -1 do for z = pos2.z, pos1.z, -1 do
for x = pos2.x, pos1.x, -1 do for x = pos2.x, pos1.x, -1 do
local next_index = 1 -- We use table.insert() in make_weighted local next_index = 1 -- We use table.insert() in make_weighted
local placed_node = false local placed_node = false
for y = pos2.y, pos1.y, -1 do local hi = z*heightmap_size.x + x
local i = area:index(x, y, z)
-- Again, Lua 5.1 doesn't have a continue statement :-/
local is_air = worldeditadditions.is_airlike(data[i]) if slopemap[hi] >= min_slope and slopemap[hi] <= max_slope then
local is_ignore = data[i] == node_id_ignore for y = pos2.y, pos1.y, -1 do
local i = area:index(x, y, z)
if not is_air and not is_ignore then
-- It's not an airlike node or something else odd
data[i] = node_ids[next_index]
next_index = next_index + 1
changes.replaced = changes.replaced + 1
-- If we're done replacing nodes in this column, move to the next one local is_air = wea.is_airlike(data[i])
if next_index > #node_ids then local is_ignore = data[i] == node_id_ignore
break
if not is_air and not is_ignore then
-- It's not an airlike node or something else odd
data[i] = node_ids[next_index]
next_index = next_index + 1
changes.replaced = changes.replaced + 1
-- If we're done replacing nodes in this column, move to the next one
if next_index > #node_ids then
break
end
end end
end end
else
changes.skipped_columns_slope = changes.skipped_columns_slope + 1
end end
if not placed_node then if not placed_node then

View file

@ -91,7 +91,7 @@ end
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). -- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap().
-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] -- @param heightmap_size int[] The size of the heightmap in the form [ z, x ]
-- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians. -- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians.
function worldeditadditions.calculate_slope(heightmap, heightmap_size) function worldeditadditions.calculate_slopes(heightmap, heightmap_size)
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size)
local slopes = { } local slopes = { }

View file

@ -256,7 +256,8 @@ end
--- Sorts the components of the given vectors. --- Sorts the components of the given vectors.
-- pos1 will contain the minimum values, and pos2 the maximum values. -- pos1 will contain the minimum values, and pos2 the maximum values.
-- Returns 2 new vectors. -- Returns 2 new vectors.
-- Note that the vectors provided do not *have* to be instances of Vector3. -- 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. -- It is only required that they have the keys x, y, and z.
-- Vector3 instances are always returned. -- Vector3 instances are always returned.
-- This enables convenient ingesting of positions from outside. -- This enables convenient ingesting of positions from outside.

View file

@ -1,11 +1,34 @@
local function parse_slope_range(text)
if string.match(text, "%.%.") then
-- It's in the form a..b
local parts = worldeditadditions.split(text, "..", true)
if not parts then return nil end
if #parts ~= 2 then return false, "Error: Exactly 2 numbers may be separated by a double dot '..' (e.g. 10..45)" end
local min_slope = tonumber(parts[1])
local max_slope = tonumber(parts[2])
if not min_slope then return false, "Error: Failed to parse the specified min_slope '"..tostring(min_slope).."' value as a number." end
if not max_slope then return false, "Error: Failed to parse the specified max_slope '"..tostring(max_slope).."' value as a number." end
-- math.rad converts degrees to radians
return true, math.rad(min_slope), math.rad(max_slope)
else
-- It's a single value
local max_slope = tonumber(text)
if not max_slope then return nil end
return true, 0, math.rad(max_slope)
end
end
-- ██████ ██ ██ ███████ ██████ ██ █████ ██ ██ -- ██████ ██ ██ ███████ ██████ ██ █████ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ █████ ██████ ██ ███████ ████ -- ██ ██ ██ ██ █████ ██████ ██ ███████ ████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ████ ███████ ██ ██ ███████ ██ ██ ██ -- ██████ ████ ███████ ██ ██ ███████ ██ ██ ██
worldedit.register_command("layers", { worldedit.register_command("layers", {
params = "[<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...", params = "[<max_slope|min_slope..max_slope>] [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...",
description = "Replaces the topmost non-airlike nodes with layers of the given nodes from top to bottom. Like WorldEdit for MC's //naturalize command. Default: dirt_with_grass dirt 3", description = "Replaces the topmost non-airlike nodes with layers of the given nodes from top to bottom. Like WorldEdit for MC's //naturalize command. Optionally takes a maximum or minimum and maximum slope value. If a column's slope value falls outside the defined range, then it's skipped. Default: dirt_with_grass dirt 3",
privs = { worldedit = true }, privs = { worldedit = true },
require_pos = 2, require_pos = 2,
parse = function(params_text) parse = function(params_text)
@ -13,21 +36,40 @@ worldedit.register_command("layers", {
params_text = "dirt_with_grass dirt 3" params_text = "dirt_with_grass dirt 3"
end end
local success, node_list = worldeditadditions.parse.weighted_nodes( local parts = worldeditadditions.split_shell(params_text)
worldeditadditions.split_shell(params_text), local success, min_slope, max_slope
if #parts > 0 then
success, min_slope, max_slope = parse_slope_range(parts[1])
if success then
table.remove(parts, 1) -- Automatically shifts other values down
end
end
if not min_slope then min_slope = 0 end
if not max_slope then max_slope = 180 end
local node_list
success, node_list = worldeditadditions.parse.weighted_nodes(
parts,
true true
) )
return success, node_list return success, node_list, min_slope, max_slope
end, end,
nodes_needed = function(name) nodes_needed = function(name)
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end, end,
func = function(name, node_list) func = function(name, node_list, min_slope, max_slope)
local start_time = worldeditadditions.get_ms_time() local start_time = worldeditadditions.get_ms_time()
local changes = worldeditadditions.layers(worldedit.pos1[name], worldedit.pos2[name], node_list) local changes = worldeditadditions.layers(
worldedit.pos1[name], worldedit.pos2[name],
node_list,
min_slope, max_slope
)
local time_taken = worldeditadditions.get_ms_time() - start_time local time_taken = worldeditadditions.get_ms_time() - start_time
minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns in " .. time_taken .. "s") minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns ("..changes.skipped_columns_slope.." due to slope constraints) in " .. time_taken .. "s")
return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.format.human_time(time_taken) return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped ("..changes.skipped_columns_slope.." due to slope constraints) in " .. worldeditadditions.format.human_time(time_taken)
end end
}) })