diff --git a/worldeditadditions/lib/sculpt/apply.lua b/worldeditadditions/lib/sculpt/apply.lua index 6323cb2..d53577d 100644 --- a/worldeditadditions/lib/sculpt/apply.lua +++ b/worldeditadditions/lib/sculpt/apply.lua @@ -1,53 +1,65 @@ local wea = worldeditadditions local Vector3 = wea.Vector3 ---- Applies the given brush at the given x/z position to the given heightmap. --- Important: Where a Vector3 is mentioned in the parameter list, it reall MUST --- be a Vector3 instance. --- Also important: Remember that the position there is RELATIVE TO THE HEIGHTMAP'S origin (0, 0) and is on the X and Z axes! --- @param brush table The ZERO-indexed brush to apply. Values should be normalised to be between 0 and 1. --- @param brush_size Vector3 The size of the brush on the x/y axes. --- @pram height number The multiplier to apply to each brush pixel value just before applying it. Negative values are allowed - this will cause a subtraction operation instead of an addition. --- @param position Vector3 The position RELATIVE TO THE HEIGHTMAP on the x/z coordinates to centre the brush application on. --- @param heightmap table The heightmap to apply the brush to. See worldeditadditions.make_heightmap for how to obtain one of these. --- @param heightmap_size Vector3 The size of the aforementioned heightmap. See worldeditadditions.make_heightmap for more information. --- @returns true,number,number|false,string If the operation was not successful, then false followed by an error message as a string is returned. If it was successful however, 3 values are returned: true, then the number of nodes added, then the number of nodes removed. -local function apply(brush, brush_size, height, position, heightmap, heightmap_size) - -- Convert brush_size to match the scheme used in the heightmap - brush_size = brush_size:clone() - brush_size.z = brush_size.y - brush_size.y = 0 +--- Applies the given brush with the given height and size to the given position. +-- @param pos1 Vector3 The position at which to apply the brush. +-- @param brush_name string The name of the brush to apply. +-- @param height number The height of the brush application. +-- @param brush_size Vector3 The size of the brush application. Values are interpreted on the X/Y coordinates, and NOT X/Z! +-- @returns bool, string|{ added: number, removed: number } A bool indicating whether the operation was successful or not, followed by either an error message as a string (if it was not successful) or a table of statistics (if it was successful). +local function apply(pos1, brush_name, height, brush_size) + -- 1: Get & validate brush + local success, brush, brush_size_actual = wea.sculpt.make_brush(brush_name, brush_size) + if not success then return success, brush end - local brush_radius = (brush_size/2):ceil() - 1 - local pos_start = (position - brush_radius) - :clamp(Vector3.new(0, 0, 0), heightmap_size) - local pos_end = (pos_start + brush_size) - :clamp(Vector3.new(0, 0, 0), heightmap_size) + local brush_size_terrain = Vector3.new( + brush_size_actual.x, + 0, + brush_size_actual.y + ) + local brush_size_radius = (brush_size_terrain / 2):floor() - local added = 0 - local removed = 0 + local pos1_compute = pos1 - brush_size_radius + local pos2_compute = pos1 + brush_size_radius + Vector3.new(0, height, 0) - -- Iterate over the heightmap and apply the brush - -- Note that we do not iterate over the brush, because we don't know if the - -- brush actually fits inside the region.... O.o - for z = pos_end, pos_start, -1 do - for x = pos_end, pos_start, -1 do - local hi = z*heightmap_size.x + x - local pos_brush = Vector3.new(x, 0, z) - pos_start - local bi = pos_brush.z*brush_size.x + pos_brush.x + + -- 2: Fetch the nodes in the specified area, extract heightmap + local manip, area = worldedit.manip_helpers.init(pos1_compute, pos2_compute) + local data = manip:get_data() + + local heightmap, heightmap_size = wea.make_heightmap( + pos1_compute, pos2_compute, + manip, area, + data + ) + local heightmap_orig = wea.table.shallowcopy(heightmap) + + for z = pos2_compute.z, pos1_compute.z, -1 do + for x = pos2_compute.x, pos1_compute.x, -1 do + local next_index = 1 -- We use table.insert() in make_weighted + local placed_node = false - local adjustment = math.floor(brush[bi]*height) - if adjustment > 0 then - added = added + adjustment - elseif adjustment < 0 then - removed = removed + math.abs(adjustment) - end + local hi = (z-pos1_compute.z)*heightmap_size.x + (x-pos1_compute.x) - heightmap[hi] = heightmap[hi] + adjustment + local offset = brush[hi] * height + if height > 0 then offset = math.floor(offset) + else offset = math.ceil(offset) end + heightmap[hi] = heightmap[hi] + offset end end - return true, added, removed + -- 3: Save back to disk & return + local success2, stats = wea.apply_heightmap_changes( + pos1_compute, pos2_compute, + area, data, + heightmap_orig, heightmap, + heightmap_size + ) + if not success2 then return success2, stats end + + worldedit.manip_helpers.finish(manip, data) + + return true, stats end diff --git a/worldeditadditions/lib/sculpt/apply_heightmap.lua b/worldeditadditions/lib/sculpt/apply_heightmap.lua new file mode 100644 index 0000000..1ad9621 --- /dev/null +++ b/worldeditadditions/lib/sculpt/apply_heightmap.lua @@ -0,0 +1,54 @@ +local wea = worldeditadditions +local Vector3 = wea.Vector3 + +--- Applies the given brush at the given x/z position to the given heightmap. +-- Important: Where a Vector3 is mentioned in the parameter list, it reall MUST +-- be a Vector3 instance. +-- Also important: Remember that the position there is RELATIVE TO THE HEIGHTMAP'S origin (0, 0) and is on the X and Z axes! +-- @param brush table The ZERO-indexed brush to apply. Values should be normalised to be between 0 and 1. +-- @param brush_size Vector3 The size of the brush on the x/y axes. +-- @pram height number The multiplier to apply to each brush pixel value just before applying it. Negative values are allowed - this will cause a subtraction operation instead of an addition. +-- @param position Vector3 The position RELATIVE TO THE HEIGHTMAP on the x/z coordinates to centre the brush application on. +-- @param heightmap table The heightmap to apply the brush to. See worldeditadditions.make_heightmap for how to obtain one of these. +-- @param heightmap_size Vector3 The size of the aforementioned heightmap. See worldeditadditions.make_heightmap for more information. +-- @returns true,number,number|false,string If the operation was not successful, then false followed by an error message as a string is returned. If it was successful however, 3 values are returned: true, then the number of nodes added, then the number of nodes removed. +local function apply_heightmap(brush, brush_size, height, position, heightmap, heightmap_size) + -- Convert brush_size to match the scheme used in the heightmap + brush_size = brush_size:clone() + brush_size.z = brush_size.y + brush_size.y = 0 + + local brush_radius = (brush_size/2):ceil() - 1 + local pos_start = (position - brush_radius) + :clamp(Vector3.new(0, 0, 0), heightmap_size) + local pos_end = (pos_start + brush_size) + :clamp(Vector3.new(0, 0, 0), heightmap_size) + + local added = 0 + local removed = 0 + + -- Iterate over the heightmap and apply the brush + -- Note that we do not iterate over the brush, because we don't know if the + -- brush actually fits inside the region.... O.o + for z = pos_end, pos_start, -1 do + for x = pos_end, pos_start, -1 do + local hi = z*heightmap_size.x + x + local pos_brush = Vector3.new(x, 0, z) - pos_start + local bi = pos_brush.z*brush_size.x + pos_brush.x + + local adjustment = math.floor(brush[bi]*height) + if adjustment > 0 then + added = added + adjustment + elseif adjustment < 0 then + removed = removed + math.abs(adjustment) + end + + heightmap[hi] = heightmap[hi] + adjustment + end + end + + return true, added, removed +end + + +return apply_heightmap diff --git a/worldeditadditions/lib/sculpt/init.lua b/worldeditadditions/lib/sculpt/init.lua index 6dd75cd..fedb6e7 100644 --- a/worldeditadditions/lib/sculpt/init.lua +++ b/worldeditadditions/lib/sculpt/init.lua @@ -10,6 +10,7 @@ local sculpt = { make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua"), preview_brush = dofile(wea.modpath.."/lib/sculpt/preview_brush.lua"), read_brush_static = dofile(wea.modpath.."/lib/sculpt/read_brush_static.lua"), + apply_heightmap = dofile(wea.modpath.."/lib/sculpt/apply_heightmap.lua"), apply = dofile(wea.modpath.."/lib/sculpt/apply.lua") } @@ -17,6 +18,6 @@ return sculpt -- TODO: Automatically find & register all text file based brushes in the brushes directory --- TODO: Implement automatic scaling of static brushes to the correct size. We have ..scale already, but we probably need to implement a proper 2d canvas scaling algorithm. Some options to consider: linear < [bi]cubic < nohalo/lohalo +-- TODO: Implement automatic scaling of static brushes to the correct size. We have scale already, but we probably need to implement a proper 2d canvas scaling algorithm. Some options to consider: linear < [bi]cubic < nohalo/lohalo -- Note that we do NOT automatically find & register computed brushes because that's an easy way to execute arbitrary Lua code & cause a security issue unless handled very carefully diff --git a/worldeditadditions_commands/commands/sculpt.lua b/worldeditadditions_commands/commands/sculpt.lua new file mode 100644 index 0000000..1d01097 --- /dev/null +++ b/worldeditadditions_commands/commands/sculpt.lua @@ -0,0 +1,77 @@ +local we_c = worldeditadditions_commands +local wea = worldeditadditions + +-- ███████ ██████ ██ ██ ██ ██████ ████████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ██ ██ ██ ██████ ██ +-- ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██████ ██████ ███████ ██ ██ +worldedit.register_command("sculpt", { + params = "[ [ []]]", + description = "Applies a sculpting brush to the terrain with a given height. See //sculptlist to list all available brushes. Note that while the brush size is configurable, the actual brush size you end up with may be slightly different to that which you request due to brush size restrictions.", + privs = { worldedit = true }, + require_pos = 1, + parse = function(params_text) + if not params_text or params_text == "" then + params_text = "default" + end + + local parts = wea.split_shell(params_text) + + local brush_name = "default" + local height = 5 + local brush_size = 10 + + if #parts >= 1 then + brush_name = table.remove(parts, 1) + if not wea.sculpt.brushes[brush_name] then + return false, "A brush with the name '"..brush_name.."' doesn't exist. Try using //sculptlist to list all available brushes." + end + end + if #parts >= 1 then + height = tonumber(table.remove(parts, 1)) + if not height then + return false, "Invalid height value (must be an integer - negative values lower terrain instead of raising it)" + end + end + if #parts >= 1 then + brush_size = tonumber(table.remove(parts, 1)) + if not brush_size or brush_size < 1 then + return false, "Invalid brush size. Brush sizes must be a positive integer." + end + end + + return true, brush_name, math.floor(height), math.floor(brush_size) + end, + nodes_needed = function(name, brush_name, height, brush_size) + local success, brush, size_actual = wea.sculpt.make_brush(brush_name, brush_size) + if not success then return 0 end + + -- This solution allows for brushes with negative values + -- it also allows for brushes that 'break the rules' and have values + -- that exceed the -1 to 1 range + local brush_min = wea.min(brush) + local brush_max = wea.max(brush) + local range_nodes = (brush_max * height) - (brush_min * height) + print("//sculpt range_nodes", range_nodes) + + return size_actual.x * size_actual.y * range_nodes + end, + func = function(name, brush_name, height, brush_size) + local start_time = wea.get_ms_time() + local pos1, pos2 = wea.Vector3.sort( + worldedit.pos1[name], + worldedit.pos2[name] + ) + local success, stats = wea.sculpt.apply( + pos1, + brush_name, height, brush_size + ) + if not success then return success, stats.added end + + local time_taken = wea.get_ms_time() - start_time + + minetest.log("action", name .. " used //sculpt at "..pos1..", adding " .. stats.added.." nodes and removing "..stats.removed.." nodes in "..time_taken.."s") + return true, stats.added.." nodes added and "..stats.removed.." removed in "..wea.format.human_time(time_taken) + end +})