diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8839b..7979d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 + - Add optional slope constraint to `//layers` (inspired by [WorldPainter](https://worldpainter.net/)) ## v1.12: The selection tools update (26th June 2021) diff --git a/worldeditadditions/lib/layers.lua b/worldeditadditions/lib/layers.lua index 78572b4..e370df3 100644 --- a/worldeditadditions/lib/layers.lua +++ b/worldeditadditions/lib/layers.lua @@ -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) - pos1, pos2 = worldedit.sort_pos(pos1, pos2) +local wea = worldeditadditions + +--- 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 -- 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_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)) - -- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) + local heightmap, heightmap_size = wea.make_heightmap( + 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 -- print("[layer] i", i, "node id", v) -- end -- 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 x = pos2.x, pos1.x, -1 do local next_index = 1 -- We use table.insert() in make_weighted local placed_node = false - for y = pos2.y, pos1.y, -1 do - local i = area:index(x, y, z) - - local is_air = worldeditadditions.is_airlike(data[i]) - local is_ignore = data[i] == node_id_ignore - - 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 + local hi = z*heightmap_size.x + x + + -- Again, Lua 5.1 doesn't have a continue statement :-/ + if slopemap[hi] >= min_slope and slopemap[hi] <= max_slope then + for y = pos2.y, pos1.y, -1 do + local i = area:index(x, y, z) - -- If we're done replacing nodes in this column, move to the next one - if next_index > #node_ids then - break + local is_air = wea.is_airlike(data[i]) + local is_ignore = data[i] == node_id_ignore + + 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 + else + changes.skipped_columns_slope = changes.skipped_columns_slope + 1 end if not placed_node then diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 014c0b3..49aa9d1 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -91,7 +91,7 @@ end -- @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 ] -- @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 slopes = { } diff --git a/worldeditadditions/utils/vector3.lua b/worldeditadditions/utils/vector3.lua index 734aabb..6ec8811 100644 --- a/worldeditadditions/utils/vector3.lua +++ b/worldeditadditions/utils/vector3.lua @@ -256,7 +256,8 @@ end --- Sorts the components of the given vectors. -- pos1 will contain the minimum values, and pos2 the maximum values. -- Returns 2 new vectors. --- Note that 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. -- Vector3 instances are always returned. -- This enables convenient ingesting of positions from outside. diff --git a/worldeditadditions_commands/commands/layers.lua b/worldeditadditions_commands/commands/layers.lua index ae09158..9f7a204 100644 --- a/worldeditadditions_commands/commands/layers.lua +++ b/worldeditadditions_commands/commands/layers.lua @@ -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", { - params = "[ []] [ []] ...", - 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", + params = "[] [ []] [ []] ...", + 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 }, require_pos = 2, parse = function(params_text) @@ -13,21 +36,40 @@ worldedit.register_command("layers", { params_text = "dirt_with_grass dirt 3" end - local success, node_list = worldeditadditions.parse.weighted_nodes( - worldeditadditions.split_shell(params_text), + local parts = 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 ) - return success, node_list + return success, node_list, min_slope, max_slope end, nodes_needed = function(name) return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) end, - func = function(name, node_list) + func = function(name, node_list, min_slope, max_slope) 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 - 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") - return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.format.human_time(time_taken) + 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 ("..changes.skipped_columns_slope.." due to slope constraints) in " .. worldeditadditions.format.human_time(time_taken) end })