Minetest-WorldEditAdditions/worldeditadditions/lib/spline.lua

122 lines
4.4 KiB
Lua

local wea_c = worldeditadditions_core
local Vector3 = wea_c.Vector3
---
-- @module worldeditadditions
-- ███████ ██████ ██ ██ ███ ██ ███████
-- ██ ██ ██ ██ ██ ████ ██ ██
-- ███████ ██████ ██ ██ ██ ██ ██ █████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ███████ ██ ██ ████ ███████
--- 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
---
local pos_prev = pos_list_chaikin[1]:floor()
local width_prev = widths_lerped[1]
for i = 2, #pos_list_chaikin do
local pos_next = pos_list_chaikin[i]:floor()
local width_next = widths_lerped[i]
-- For now, just plot a point at each node
data[area:indexp(pos_next)] = node_id
data[area:indexp(pos_prev)] = node_id
local subline_length = (pos_next - pos_prev):length()
print("DEBUG:spline DRAW pos", pos_next, "width", width_next, "length", subline_length)
if subline_length > 0 then
-- Iterate a box around the subline and draw it
local width_max = math.ceil(math.max(width_prev, width_next))
local subpos1, subpos2 = Vector3.sort(pos_next, pos_prev)
subpos1 = subpos1 - width_max
subpos2 = subpos2 + width_max
print("subpos1", subpos1, "subpos2", subpos2, "width_prev", width_prev, "width_next", width_next)
for z = subpos2.z, subpos1.z, -1 do
for y = subpos2.y, subpos1.y, -1 do
for x = subpos2.x, subpos1.x, -1 do
local here = Vector3.new(x, y, z)
local D = (pos_next - pos_prev):normalise()
local d = Vector3.dot(here - pos_prev, D)
local closest_on_line = pos_prev + (D * d)
local distance = (here - closest_on_line):length()
local distance_to_prev = (closest_on_line - pos_prev):length()
local percentage_along_line = distance_to_prev / subline_length
local width_here = wea_c.lerp(width_prev, width_next, percentage_along_line)
if distance < width_here then
-- print("POINT ", here, "width_here", width_here, "distance from line", distance)
data[area:indexp(here)] = node_id
-- May hit some nodes multiplle times, but there's nothing doing about that without a big performance (and/or memory) hit
count = count + 1
end
end
end
end
pos_prev = pos_next:clone()
width_prev = width_next
end
end
---
-- 4: Save changes and return
---
-- Save the modified nodes back to disk & return
worldedit.manip_helpers.finish(manip, data)
return true, count
end