From 258a9c1cde20329262232570b3d653064fd2a55e Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Sun, 25 Sep 2022 01:18:40 +0100 Subject: [PATCH] implement initial //spline, but it isn't finished yet --- worldeditadditions/lib/spline.lua | 80 ++++++++++++++++ .../commands/spline.lua | 91 +++++++++++++++++++ worldeditadditions_commands/init.lua | 1 + worldeditadditions_core/init.lua | 5 +- worldeditadditions_core/utils/chaikin.lua | 43 +++++++++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 worldeditadditions/lib/spline.lua create mode 100644 worldeditadditions_commands/commands/spline.lua create mode 100644 worldeditadditions_core/utils/chaikin.lua diff --git a/worldeditadditions/lib/spline.lua b/worldeditadditions/lib/spline.lua new file mode 100644 index 0000000..da1307d --- /dev/null +++ b/worldeditadditions/lib/spline.lua @@ -0,0 +1,80 @@ +local wea_c = worldeditadditions_core +local Vector3 = wea_c.Vector3 + +-- ███████ ██████ ██ ██ ███ ██ ███████ +-- ██ ██ ██ ██ ██ ████ ██ ██ +-- ███████ ██████ ██ ██ ██ ██ ██ █████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ███████ ██ ██ ████ ███████ + +--- Creates a spline that follows the given list of points. +-- @param pos_list Vector3[] The list of points that define the path the spline should follow. Minimum: 2 +-- @param width_start number The width of the spline at the start. +-- @param width_end number The width of the spline at the end. If nil, then width_start is used. +-- @param steps number The number of smoothing passes to apply to the list of points. +-- @param target_node Vector3 The *normalised* name of the node to use to build the square spiral with. +-- @returns bool,number|string A success boolean value, followed by either the number of the nodes set or an error message string. +function worldeditadditions.spline(pos_list, width_start, width_end, steps, target_node) + if #pos_list < 2 then return false, "Error: At least 2 positions are required to make a spline." end + if not width_end then width_end = width_start end + + --- + -- 0: Find bounding box + --- + -- We can't use wea_c.pos.get_bounds 'cause that requires a player name + local pos1, pos2 = pos_list[1], pos_list[2] + for _, pos in pairs(pos_list) do + pos1, pos2 = Vector3.expand_region(pos, pos1, pos2) + end + local max_width = math.max(width_start, width_end) + pos1 = pos1 - max_width + pos2 = pos2 + max_width + + + --- + -- 1: Interpolate points & widths + --- + local pos_list_chaikin = wea_c.chaikin(pos_list, steps) + + local widths_lerped = {} + for i = 1, #pos_list_chaikin do + table.insert( + widths_lerped, + wea_c.lerp(width_start, width_end, (1/#pos_list_chaikin)*(i-1)) + ) + end + + --- + -- 2: Fetch VoxelManipulator, prepare for replacement + --- + local manip, area = worldedit.manip_helpers.init(pos1, pos2) + local data = manip:get_data() + + local node_id = minetest.get_content_id(target_node) + + local count = 0 -- The number of nodes replaced + + --- + -- 3: Replace nodes + --- + + for i = 1, #pos_list_chaikin do + local pos_next = pos_list_chaikin[i] + local width_next = widths_lerped[i] + + -- For now, just plot a point at each node + local index_node = area:index(pos_next.x, pos_next.y, pos_next.z) + data[index_node] = node_id + + count = count + 1 + end + + --- + -- 4: Save changes and return + --- + + -- Save the modified nodes back to disk & return + worldedit.manip_helpers.finish(manip, data) + + return true, count +end diff --git a/worldeditadditions_commands/commands/spline.lua b/worldeditadditions_commands/commands/spline.lua new file mode 100644 index 0000000..ad23291 --- /dev/null +++ b/worldeditadditions_commands/commands/spline.lua @@ -0,0 +1,91 @@ +local wea_c = worldeditadditions_core +local Vector3 = wea_c.Vector3 + +-- ███████ ██████ ██ ██ ███ ██ ███████ +-- ██ ██ ██ ██ ██ ████ ██ ██ +-- ███████ ██████ ██ ██ ██ ██ ██ █████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ███████ ██ ██ ████ ███████ + + +worldeditadditions_core.register_command("spline", { + params = " [ []]", + description = "Draws a spline through the defined points. NOTE: Uses the NEW worldeditadditions position system, not the existing WorldEdit one!", + privs = { worldedit = true }, + require_pos = 3, + parse = function(params_text) + local parts = wea_c.split_shell(params_text) + + local replace_node + local width_start + local width_end + local steps = 6 + + if #parts < 1 then + return false, "Error: The replace_node (e.g. dirt) was not specified." + end + if #parts < 2 then + return false, "Error: The starting width wasn't specified." + end + + replace_node = worldedit.normalize_nodename(parts[1]) + if not replace_node then + return false, "Error: Unknown node name '"..parts[1].."." + end + + width_start = tonumber(parts[2]) + if not width_start then + return false, "Error: width_start must be an integer greater than or equal to 1." + end + if width_start < 1 then + return false, "Error: width_start must be an integer greater than 0, but you passed '"..parts[2].."'." + end + + if #parts >= 3 then + width_end = tonumber(parts[3]) + if not width_end then + return false, "Error: width_end must be an integer greater than or equal to 1." + end + if width_end < 1 then + return false, "Error: width_end must be an integer greater than 0, but you passed '"..parts[3].."'." + end + else + width_end = width_start + end + + if #parts >= 4 then + steps = tonumber(parts[4]) + if not steps then + return false, "Error: steps must be an integer greater than or equal to 0." + end + if steps < 0 then + return false, "Error: steps must be an integer greater than or equal to 0, but you passed '"..parts[3].."'." + end + end + + return true, replace_node, width_start, width_end, steps + end, + nodes_needed = function(player_name, _replace_node, _width_start, _width_end, steps) + if steps > 10 then return "may result in a large number of interpolated points" end + -- //overlay only modifies up to 1 node per column in the selected region + local pos1, pos2 = wea_c.pos.get_bounds(player_name) + return Vector3.volume(pos1, pos2) + end, + func = function(player_name, replace_node, width_start, width_end, steps) + local start_time = wea_c.get_ms_time() + local pos_list = wea_c.pos.get_all(player_name) + + local success, nodes_replaced = worldeditadditions.spline( + pos_list, + width_start, + width_end, + replace_node + ) + if not success then return success, nodes_replaced end + local time_taken = wea_c.format.human_time(wea_c.get_ms_time() - start_time) + + + minetest.log("action", player_name.." used //spline with "..#pos_list.." initial points and "..steps.." steps, replacing "..nodes_replaced.." nodes in "..time_taken) + return true, nodes_replaced.." nodes replaced in "..time_taken + end +}) diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index 6a2278d..a6d25c5 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -36,6 +36,7 @@ dofile(wea_cmd.modpath.."/commands/dome.lua") dofile(wea_cmd.modpath.."/commands/metaball.lua") dofile(wea_cmd.modpath.."/commands/count.lua") dofile(wea_cmd.modpath.."/commands/sculpt.lua") +dofile(wea_cmd.modpath.."/commands/spline.lua") -- Meta Commands dofile(wea_cmd.modpath.."/commands/meta/init.lua") diff --git a/worldeditadditions_core/init.lua b/worldeditadditions_core/init.lua index f8f79da..0e68a14 100644 --- a/worldeditadditions_core/init.lua +++ b/worldeditadditions_core/init.lua @@ -41,6 +41,10 @@ wea_c.bit = dofile(wea_c.modpath.."/utils/bit.lua") wea_c.terrain = dofile(wea_c.modpath.."/utils/terrain/init.lua") +local chaikin = dofile(wea_c.modpath.."/utils/chaikin.lua") +wea_c.chaikin = chaikin.chaikin +wea_c.lerp = chaikin.linear_interpolate + dofile(wea_c.modpath.."/utils/strings/init.lua") dofile(wea_c.modpath.."/utils/format/init.lua") dofile(wea_c.modpath.."/utils/parse/init.lua") @@ -57,7 +61,6 @@ dofile(wea_c.modpath.."/utils/player.lua") -- Player info functions - wea_c.pos = dofile(modpath.."/core/pos.lua") -- AFTER EventEmitter wea_c.register_command = dofile(modpath.."/core/register_command.lua") wea_c.fetch_command_def = dofile(modpath.."/core/fetch_command_def.lua") diff --git a/worldeditadditions_core/utils/chaikin.lua b/worldeditadditions_core/utils/chaikin.lua new file mode 100644 index 0000000..23ca743 --- /dev/null +++ b/worldeditadditions_core/utils/chaikin.lua @@ -0,0 +1,43 @@ +local wea_c = worldeditadditions_core + +--- Interpolates between the 2 given points +-- @param a Vector3|number The starting point. +-- @param b Vector3|number The ending point. +-- @param time number The percentage between 0 and 1 to interpolate to. +-- @returns Vector3|number The interpolated point. +local function linear_interpolate(a, b, time) + return ((b - a) * time) + a +end + +--- Chaikin curve smoothing implementation. +-- This algorithm works by taking a list of points that define a segmented line, +-- and then interpolating between them to smooth the line. +-- @source Ported from https://git.starbeamrainbowlabs.com/sbrl-GitLab/Chaikin-Generator/src/branch/master/ChaikinGenerator/ChaikinCurve.cs +-- @param arr_pos Vector3[] A list of points to interpolate. +-- @param steps number The number of interpolatioon passes to do. +-- @returns Vector3[] A (longer) list of interpolated points. +local function chaikin(arr_pos, steps) + + local result = wea_c.table.shallowcopy(arr_pos) + for pass = 1, steps do + local pos_start = result[1] + local pos_end = result[#result] + + for i = 1,#result-1,2 do + result[i] = linear_interpolate(result[i], result[i+1], 0.25) + table.insert(result, i+1, linear_interpolate(result[i], result[i+2])) + end + -- table.remove(result, #result-1) -- In the original, but I don't know why + + -- Keep the starting & ending positions the same + result[1] = pos_start + result[#result] = pos_end + end + + return result +end + +return { + chaikin = chaikin, + linear_interpolate = linear_interpolate +} \ No newline at end of file