diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index f52424c..378f610 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -57,6 +57,7 @@ dofile(wea.modpath.."/lib/spiral_circle.lua") dofile(wea.modpath.."/lib/conv/conv.lua") dofile(wea.modpath.."/lib/erode/erode.lua") dofile(wea.modpath.."/lib/noise/init.lua") +dofile(wea.modpath.."/lib/sculpt/init.lua") dofile(wea.modpath.."/lib/copy.lua") dofile(wea.modpath.."/lib/move.lua") diff --git a/worldeditadditions/lib/sculpt/apply.lua b/worldeditadditions/lib/sculpt/apply.lua new file mode 100644 index 0000000..6323cb2 --- /dev/null +++ b/worldeditadditions/lib/sculpt/apply.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(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 diff --git a/worldeditadditions/lib/sculpt/brushes/__smooth.lua b/worldeditadditions/lib/sculpt/brushes/__smooth.lua new file mode 100644 index 0000000..e8647a5 --- /dev/null +++ b/worldeditadditions/lib/sculpt/brushes/__smooth.lua @@ -0,0 +1,13 @@ + +--- Returns a smooth gaussian brush. +-- @param size Vector3 The target size of the brush. Note that the actual size fo the brush will be different, as the gaussian function has some limitations. +-- @param sigma=2 number The 'smoothness' of the brush. Higher values are more smooth. +return function(size, sigma) + local size = math.min(size.x, size.y) + if size % 2 == 0 then size = size - 1 end + if size < 1 then + return false, "Error: Invalid brush size." + end + local success, gaussian = worldeditadditions.conv.kernel_gaussian(size, sigma) + return success, gaussian, { x = size, y = size } +end diff --git a/worldeditadditions/lib/sculpt/brushes/default.lua b/worldeditadditions/lib/sculpt/brushes/default.lua new file mode 100644 index 0000000..eadf4c6 --- /dev/null +++ b/worldeditadditions/lib/sculpt/brushes/default.lua @@ -0,0 +1,8 @@ +local wea = worldeditadditions + +local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua") + +return function(size) + local success, brush, size_actual = __smooth(size, 2) + return success, brush, size_actual +end diff --git a/worldeditadditions/lib/sculpt/brushes/default_hard.lua b/worldeditadditions/lib/sculpt/brushes/default_hard.lua new file mode 100644 index 0000000..c4fd325 --- /dev/null +++ b/worldeditadditions/lib/sculpt/brushes/default_hard.lua @@ -0,0 +1,8 @@ +local wea = worldeditadditions + +local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua") + +return function(size) + local success, brush, size_actual = __smooth(size, 1) + return success, brush, size_actual +end diff --git a/worldeditadditions/lib/sculpt/brushes/default_soft.lua b/worldeditadditions/lib/sculpt/brushes/default_soft.lua new file mode 100644 index 0000000..228e5ec --- /dev/null +++ b/worldeditadditions/lib/sculpt/brushes/default_soft.lua @@ -0,0 +1,8 @@ +local wea = worldeditadditions + +local __smooth = dofile(wea.modpath.."/lib/sculpt/brushes/__smooth.lua") + +return function(size) + local success, brush, size_actual = __smooth(size, 5) + return success, brush, size_actual +end diff --git a/worldeditadditions/lib/sculpt/brushes/square.lua b/worldeditadditions/lib/sculpt/brushes/square.lua new file mode 100644 index 0000000..cd78045 --- /dev/null +++ b/worldeditadditions/lib/sculpt/brushes/square.lua @@ -0,0 +1,11 @@ + +--- Returns a simple square brush with 100% weight for every pixel. +return function(size) + local result = {} + for y=0, size.y do + for x=0, size.x do + result[y*size.x + x] = 1 + end + end + return true, result, size +end diff --git a/worldeditadditions/lib/sculpt/init.lua b/worldeditadditions/lib/sculpt/init.lua new file mode 100644 index 0000000..723ca3a --- /dev/null +++ b/worldeditadditions/lib/sculpt/init.lua @@ -0,0 +1,20 @@ +local wea = worldeditadditions + +local sculpt = { + brushes = { + default_hard = dofile(wea.modpath.."/lib/sculpt/brushes/default_hard.lua"), + default = dofile(wea.modpath.."/lib/sculpt/brushes/default.lua"), + default_soft = dofile(wea.modpath.."/lib/sculpt/brushes/default_soft.lua"), + square = dofile(wea.modpath.."/lib/sculpt/brushes/square.lua") + }, + 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 = dofile(wea.modpath.."/lib/sculpt/apply.lua") +} + +-- 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 + +-- 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/lib/sculpt/make_brush.lua b/worldeditadditions/lib/sculpt/make_brush.lua new file mode 100644 index 0000000..5dc6d1b --- /dev/null +++ b/worldeditadditions/lib/sculpt/make_brush.lua @@ -0,0 +1,24 @@ +local wea = worldeditadditions + +--- Makes a sculpting brush that is as close to a target size as possible. +-- @param brush_name string The name of the brush to create. +-- @param target_size Vector3 The target size of the brush to create. +-- @returns true,table,Vector3|false,string If the operation was successful, true followed by the brush in a 1D ZERO-indexed table followed by the actual size of the brush as a Vector3 (x & y components used only). If the operation was not successful, false and an error message string is returned instead. +local function make_brush(brush_name, target_size) + if not wea.sculpt.brushes[brush_name] then return false, "Error: That brush does not exist. Try using //sculptbrushes to list all available sculpting brushes." end + + local brush_def = wea.sculpt.brushes[brush_name] + + local success, brush, size_actual + if type(brush_def) == "function" then + success, brush, size_actual = brush_def(target_size) + if not success then return success, brush end + else + brush = brush_def.brush + size_actual = brush_def.size + end + + return true, brush, size_actual +end + +return make_brush diff --git a/worldeditadditions/lib/sculpt/preview_brush.lua b/worldeditadditions/lib/sculpt/preview_brush.lua new file mode 100644 index 0000000..dd8f05d --- /dev/null +++ b/worldeditadditions/lib/sculpt/preview_brush.lua @@ -0,0 +1,46 @@ +local wea = worldeditadditions +local Vector3 = wea.Vector3 + +local make_brush = dofile(wea.modpath.."/lib/sculpt/make_brush.lua") + +--- Generates a textual preview of a given brush. +-- @param brush_name string The name of the brush to create a preview for. +-- @param target_size Vector3 The target size of the brush to create. Default: (10, 10, 0). +-- @returns bool,string If the operation was successful, true followed by a preview of the brush as a string. If the operation was not successful, false and an error message string is returned instead. +local function preview_brush(brush_name, target_size) + if not target_size then target_size = Vector3.new(10, 10, 0) end + local success, brush, brush_size = make_brush(brush_name, target_size) + + -- Values to map brush pixel values to. + -- Brush pixel values are first multiplied by 10 before comparing to these numbers + local values = {} + values["@"] = 9.5 + values["#"] = 8 + values["="] = 6 + values[":"] = 5 + values["-"] = 4 + values["."] = 1 + values[" "] = 0 + + local result = {} + for z = target_size.z, 0, -1 do + local row = {} + for x = target_size.x, 0, -1 do + local i = z*brush_size.x + x + local pixel = " " + local threshold_cur = -1 + for value,threshold in pairs(values) do + if brush[i] > threshold and threshold_cur < threshold then + pixel = value + threshold_cur = threshold + end + end + table.insert(row, pixel) + end + table.insert(result, table.concat(row)) + end + + return true, table.concat(result) +end + +return preview_brush diff --git a/worldeditadditions/lib/sculpt/read_brush_static.lua b/worldeditadditions/lib/sculpt/read_brush_static.lua new file mode 100644 index 0000000..df87f8c --- /dev/null +++ b/worldeditadditions/lib/sculpt/read_brush_static.lua @@ -0,0 +1,11 @@ + +return function(filepath) + local brush_size = { x = 0, y = 0 } + local brush = { } + + -- TODO: Import brush here + + return false, "Error: Not implemented yet" + + -- return true, brush, brush_size +end diff --git a/worldeditadditions_commands/commands/extra/sculptlist.lua b/worldeditadditions_commands/commands/extra/sculptlist.lua new file mode 100644 index 0000000..9317e99 --- /dev/null +++ b/worldeditadditions_commands/commands/extra/sculptlist.lua @@ -0,0 +1,54 @@ +local wea = worldeditadditions + +-- ███████ ██████ ██ ██ ██ ██████ ████████ ██ ██ ███████ ████████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ██ ██ ██ ██████ ██ ██ ██ ███████ ██ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██████ ██████ ███████ ██ ██ ███████ ██ ███████ ██ +minetest.register_chatcommand("/sculptlist", { + params = "[preview]", + description = "Lists all the currently registered sculpting brushes and their associated metadata. If the keyword preview is specified as an argument, a preview of each brush is also printed.", + privs = { worldedit = true }, + func = function(name, params_text) + if name == nil then return end + if not params_text then params_text = "" end + params_text = wea.trim(params_text) + + local msg = {} + + table.insert(msg, "Currently registered sculpting brushes:\n") + + if params_text == "preview" then + for brush_name, brush_def in pairs(wea.sculpt.brushes) do + local preview = wea.sculpt.preview_brush(brush_name) + + local brush_size = "dynamic" + if type(brush_size) ~= "function" then + brush_size = brush_def.size + end + + table.insert(msg, brush_name.." ["..brush_size.."]:\n") + table.insert(msg, preview.."\n\n") + end + else + local display = { { "Name", "Native Size" } } + for brush_name, brush_def in pairs(wea.sculpt.brushes) do + local brush_size = "dynamic" + if type(brush_size) ~= "function" then + brush_size = brush_def.size + end + + table.insert(display, { + brush_name, + brush_size + }) + end + -- Sort by brush name + table.sort(display, function(a, b) return a[1] < b[1] end) + + table.insert(msg, worldeditadditions.format.make_ascii_table(display)) + end + + worldedit.player_notify(name, table.concat(msg)) + end +}) diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index 033d3a3..b6b4244 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -55,8 +55,9 @@ dofile(we_c.modpath.."/commands/wireframe/init.lua") dofile(we_c.modpath.."/commands/extra/saplingaliases.lua") dofile(we_c.modpath.."/commands/extra/basename.lua") +dofile(we_c.modpath.."/commands/extra/sculptlist.lua") --- Don't registry the //bonemeal command if the bonemeal mod isn't present +-- Don't register the //bonemeal command if the bonemeal mod isn't present if minetest.global_exists("bonemeal") then dofile(we_c.modpath.."/commands/bonemeal.lua") dofile(we_c.modpath.."/commands/forest.lua") @@ -94,6 +95,7 @@ worldedit.alias_command("mfacing", "mface") -- These are commented out for now, as they could be potentially dangerous to stability -- Thorough testing is required of our replacement commands before these are uncommented -- TODO: Depend on worldeditadditions_core before uncommenting this +-- BUG: //move+ seems to be leaving stuff behind for some strange reason --@sbrl 2021-12-26 -- worldeditadditions_core.alias_override("copy", "copy+") -- worldeditadditions_core.alias_override("move", "move+") -- MAY have issues where it doesn't overwrite the old region properly, but haven't been able to reliably reproduce this -- worldeditadditions_core.alias_override("replace", "replacemix")