Implement //Smake (#61)

This commit is contained in:
Starbeamrainbowlabs 2021-06-26 02:19:55 +01:00 committed by GitHub
commit 9ad3fd97d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 305 additions and 33 deletions

View file

@ -7,13 +7,14 @@ Note to self: See the bottom of this file for the release template text.
- Add `//spush`, `//spop`, and `//sstack`
- Add `//srect` (_select rectangle_), `//scol` (_select column_), `//scube` (_select cube_) - thanks, @VorTechnix!
- Add `//scloud` (_select point cloud_), `//scentre` (_select centre node(s)_), `//srel` (_select relative_) - thanks, @VorTechnix!
- Add `//smake` (_selection make_) - thanks, @VorTechnix!
- Significantly refactored backend utility functions (more to come in future updates)
- `//bonemeal`: Try bonemealing everything that isn't an air block (#49)
- Add new universal chance parsing
- Any `<chance>` can now either be a 1-in-N number (e.g. `4`, `10`), or a percentage chance (e.g. `50%`, `10%`).
- Caveat: Percentages are converted to a 1-in-N chance, but additionally that number is rounded down in some places
- `//torus`, `//hollowtorus`: Add optional new axes
- `//torus`: Add optional hollow keyword - @VorTechnix
- `//torus`, `//ellipsoid`: Add optional hollow keyword - @VorTechnix
- `//multi`: Add curly brace syntax for nesting command calls ([more information](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#multi-command_a-command_b-command_c-))
- `//erode`: Add new `river` erosion algorithm for filling in potholes and removing pillars

View file

@ -109,13 +109,15 @@ Note that the *entire* cave you want filling must be selected, as `//fillcaves`
//fillcaves brick
```
## `//ellipsoid <rx> <ry> <rz> <node_name>`
## `//ellipsoid <rx> <ry> <rz> <node_name> [h[ollow]]`
Creates a solid ellipsoid at position 1 with the radius `(rx, ry, rz)`.
```
//ellipsoid 10 5 15 ice
//ellipsoid 3 5 10 dirt
//ellipsoid 20 10 40 air
//ellipsoid 14 5 8 steelblock h
//ellipsoid 7 4 7 papyrus hollow
```
## `//hollowellipsoid <rx> <ry> <rz> <node_name>`
@ -447,7 +449,7 @@ maxdiff | `float` | 0.4 | The maximum difference in height (between 0 and
count | `float` | 25000 | The number of snowballs to simulate.
noconv | any | n/a | When set to any value, disables to automatic 3x3 gaussian convolution.
Example invocations:
Usage examples:
```
//erode
@ -468,7 +470,7 @@ raise_sides | `string` | 4,3 | Comma separated list of numbers. Columns wit
doraise | `boolean` | true | Whether to raise columns in height. If false, then no columns will be raised in height even if they are eligible to be so according to `raise_sides`.
dolower | `boolean` | true | Whether to lower columns in height. If false, then no columns will be lowered in height even if they are eligible to be so according to `lower_sides`.
Example invocations:
Usage examples:
```
//erode river
@ -584,14 +586,14 @@ Short for _select point cloud_. Sets pos1 and pos2 to include the nodes you punc
```
## `//scentre`
Short for _select center_. Sets pos1 and pos2 to the centre point(s) of the current selection area. 1, 2, 4 or 8 nodes may be selected depending on what parts of the original selection are even in distance. Implementation by @VorTechnix.
Short for _select center_. Sets pos1 and pos2 to the centre point(s) of the current selection area. 1, 2, 4 or 8 nodes may be selected depending on what parts of the original selection are even in distance. Implementation thanks to @VorTechnix.
```
//scentre
```
## `//srel <axis1> <length1> [<axis2> <length2> [<axis3> <length3>]]`
Short for _select relative_. Sets the pos2 at set distances along 3 axes relative to pos1. If pos1 is not set it will default to the node directly under the player. The axis arguments accept `x, y, z` as well as `up, down, left, right, front, back`. Left, right, front and back are relative to player facing direction. Negative (`-`) can be applied to the axis, the length or both. Implementation by @VorTechnix.
Short for _select relative_. Sets the pos2 at set distances along 3 axes relative to pos1. If pos1 is not set it will default to the node directly under the player. The axis arguments accept `x, y, z` as well as `up, down, left, right, front, back`. Left, right, front and back are relative to player facing direction. Negative (`-`) can be applied to the axis, the length or both. Implementation thanks to @VorTechnix.
```
//srel front 5
@ -600,6 +602,55 @@ Short for _select relative_. Sets the pos2 at set distances along 3 axes relativ
//scube -z 12 -y -2 x -2
```
## `//smake <operation> <mode> [<target=xz> [<base>]]`
Short for _selection make_. Modifies existing selection by moving pos2. Allows you to make the selection an odd or even length on one or more axes or set two or more axes equal to each other or the longest, shortest or average of them. Implementation thanks to @VorTechnix.
Usage examples:
```
//smake odd shrink
//smake even avg xz
//smake equal grow xy
//smake equal average
//smake equal zy x
```
### `<operation>`: odd/even/equal/factor
|Value | Description |
| --- | --- |
odd: | round up or down, based on mode, all axes specified in `<target>` to the nearest odd length relative to pos1
even: | round up or down, based on mode, all axes specified in `<target>` to the nearest even length relative to pos1
equal: | set `<target>` axes length equal to the length of `<base>` axis if specified or to the length of the largest, smallest or average of the `<target>` axes based on mode.
### `<mode>:` grow/shrink/average
#### *If `<operation>` == odd or even:*
|Value | Description |
| --- | --- |
grow: | grow each axis specified in `<target>` to the nearest odd/even number to itself
shrink: | shrink each axis specified in `<target>` to the nearest odd/even number to itself
average/avg: | take the average of all axes specified in `<target>` and then for each specified axis grow or shrink it, depending on weather it is less than or greater than the average, to the nearest odd/even number to itself
#### *If `<operation>` == equal:* ^[1]
|Value | Description |
| --- | --- |
grow: | grow each axis specified in `<target>` to the length of the longest specified axis
shrink: | shrink each axis specified in `<target>` to the length of the shortest specified axis
average/avg: | set each axis specified in `<target>` to the average length of all the specified axes
### Additional arguments:
|Name | Description |
| --- | --- |
`<target>`: | Specify axes to perform operation on (default= xz)|
`<base>`: If `<operation>` == odd or even: | Does nothing
`<base>`: If `<operation>` == equal: | Overrides `<mode>`^[1] and sets all `<target>` axes equal to itself
^[1]: `<mode>` argument can be omitted and will not be parsed if present if `<base>` is specified
## `//sstack`
Displays the contents of your per-user selection stack. This stack can be pushed to and popped from rather like a stack of plates. See also `//spush` (for pushing to the selection stack) and `//spop` (for popping from the selection stack).

View file

@ -9,6 +9,7 @@ See also:
## Fix lighting
```
//multi //1 //2 //outset 50 //fixlight //y
```

View file

@ -28,7 +28,7 @@ _(Do you have a cool build that you used WorldEditAdditions to build? [Get in to
The detailed explanations have moved! Check them out [here](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md), or click the links below.
### Geometry
- [`//ellipsoid <rx> <ry> <rz> <node_name>`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#ellipsoid-rx-ry-rz-node_name)
- [`//ellipsoid <rx> <ry> <rz> <node_name> [h[ollow]]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#ellipsoid-rx-ry-rz-node_name-hollow)
- [`//hollowellipsoid <rx> <ry> <rz> <node_name>`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#hollowellipsoid-rx-ry-rz-node_name)
- [`//torus <major_radius> <minor_radius> <node_name> [<axes=xy> [h[ollow]]]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#torus-major_radius-minor_radius-node_name-axesxy-hollow)
- [`//hollowtorus <major_radius> <minor_radius> <node_name> [<axes=xy>]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#hollowtorus-major_radius-minor_radius-node_name-axesxy)

View file

@ -1,3 +1,9 @@
-- ██████ █████ ██████ ███████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ███████ ██████ ███████ █████
-- ██ ██ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ███████ ███████
worldeditadditions.parse = {}
dofile(worldeditadditions.modpath.."/utils/parse/chance.lua")

View file

@ -1,2 +1,3 @@
dofile(worldeditadditions.modpath.."/utils/strings/split.lua")
dofile(worldeditadditions.modpath.."/utils/strings/polyfill.lua")
dofile(worldeditadditions.modpath.."/utils/strings/tochars.lua")

View file

@ -0,0 +1,27 @@
--- Split into table of characters.
-- @param text string The string to iterate over
-- @param sort bool Sort characters
-- @param rem_dups bool Remove duplicate characters
-- @returns table A sequence table containing the substrings
function worldeditadditions.tochars(text,sort,rem_dups)
local t, set = {}, {}
if rem_dups then
text:gsub(".",function(c) set[c] = true end)
for k,v in pairs(set) do table.insert(t,k) end
else
text:gsub(".",function(c) table.insert(t,c) end)
end
if sort then table.sort(t) end
return t
end
--- Split into a set of characters.
-- @param text string The string to iterate over
-- @returns table A sequence set table containing the substrings
function worldeditadditions.tocharset(text)
local t = {}
text:gsub(".",function(c) t[c] = true end)
return t
end

View file

@ -9,12 +9,14 @@
-- extensive collection of functions :P
-- TODO: Refactor into its own worldeditadditions.tables namespace.
worldeditadditions.tables = {}
dofile(worldeditadditions.modpath.."/utils/tables/sets.lua")
dofile(worldeditadditions.modpath.."/utils/tables/shallowcopy.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_apply.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_unpack.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_get_last.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_tostring.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_map.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_filter.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_get_last.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_map.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_tostring.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_unique.lua")
dofile(worldeditadditions.modpath.."/utils/tables/table_unpack.lua")

View file

@ -0,0 +1,9 @@
--- Creates a table that stores data in keys.
-- @source https://riptutorial.com/lua/example/13407/search-for-an-item-in-a-list
-- @param list table The table of values to convert to keys.
-- @return table The table of (key,true) pairs.
function worldeditadditions.makeset (list)
local set = {}
for _, l in ipairs(list) do set[l] = true end
return set
end

View file

@ -5,7 +5,7 @@ function worldeditadditions.vector.tostring(v)
return "(" .. v.x ..", " .. v.y ..", " .. v.z ..")"
end
-- Calculates the length squared of the given vector.
--- Calculates the length squared of the given vector.
-- @param v Vector The vector to operate on
-- @return number The length of the given vector squared
function worldeditadditions.vector.lengthsquared(v)
@ -53,6 +53,17 @@ function worldeditadditions.vector.ceil(v)
if v.z then v.z = math.ceil(v.z) end
end
--- Sets the values in a vector to their absolute values.
-- Warning: This MUTATES the given vector!
-- @param v Vector The vector to operate on
function worldeditadditions.vector.abs(v)
if v.x then v.x = math.abs(v.x) end
-- Some vectors are 2d, but on the x / z axes
if v.y then v.y = math.abs(v.y) end
-- Some vectors are 2d
if v.z then v.z = math.abs(v.z) end
end
--- Determines if the target point is contained within the defined worldedit region.
-- @param pos1 Vector pos1 of the defined region.
-- @param pos2 Vector pos2 of the defined region.
@ -93,3 +104,19 @@ end
function worldeditadditions.vector.mean(pos1, pos2)
return vector.new((pos1.x + pos2.x)/2, (pos1.y + pos2.y)/2, (pos1.z + pos2.z)/2)
end
--- Returns a vector of the min values of 2 positions.
-- @param pos1 Vector pos1 of the defined region.
-- @param pos2 Vector pos2 of the defined region.
-- @return Vector Min values from input vectors.
function worldeditadditions.vector.min(pos1, pos2)
return vector.new(math.min(pos1.x + pos2.x), math.min(pos1.y + pos2.y), math.min(pos1.z + pos2.z))
end
--- Returns a vector of the max values of 2 positions.
-- @param pos1 Vector pos1 of the defined region.
-- @param pos2 Vector pos2 of the defined region.
-- @return Vector Max values from input vectors.
function worldeditadditions.vector.max(pos1, pos2)
return vector.new(math.max(pos1.x + pos2.x), math.max(pos1.y + pos2.y), math.max(pos1.z + pos2.z))
end

View file

@ -3,35 +3,35 @@
-- █████ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ███████ ███████ ██ ██ ███████ ██████ ██ ██████
local wea = worldeditadditions
local function parse_params_ellipsoid(params_text)
local found, _, radius_x, radius_y, radius_z, replace_node = params_text:find("([0-9]+)%s+([0-9]+)%s+([0-9]+)%s+([a-z:_\\-]+)")
local parts = wea.split(params_text, "%s+", false)
if found == nil then
return false, "Invalid syntax"
if #parts < 4 then
return false, "Error: Not enough arguments. Expected \"<rx> <ry> <rz> <replace_node> [h[ollow]]\"."
end
local radius = {
x = tonumber(radius_x),
y = tonumber(radius_y),
z = tonumber(radius_z)
}
local radius = minetest.string_to_pos(parts[1].." "..parts[2].." "..parts[3])
if not radius then
return false, "Error: 3 radii must be specified."
end
wea.vector.abs(radius)
replace_node = worldedit.normalize_nodename(replace_node)
local replace_node = worldedit.normalize_nodename(parts[4])
if not replace_node then
worldedit.player_notify(name, "Error: Invalid node name.")
return false
end
if not radius.x or not radius.y or not radius.z then
worldedit.player_notify(name, "Error: Invalid radius(es).")
return false
return false, "Error: Invalid replace_node specified."
end
return true, replace_node, radius
local hollow = false
if parts[5] == "hollow" or parts[5] == "h" then
hollow = true
end
return true, replace_node, radius, hollow
end
worldedit.register_command("ellipsoid", {
params = "<rx> <ry> <rz> <replace_node>",
params = "<rx> <ry> <rz> <replace_node> [h[ollow]]",
description = "Creates a 3D ellipsoid with a radius of (rx, ry, rz) at pos1, filled with <replace_node>.",
privs = { worldedit = true },
require_pos = 1,
@ -42,9 +42,9 @@ worldedit.register_command("ellipsoid", {
nodes_needed = function(name, target_node, radius)
return math.ceil(4/3 * math.pi * radius.x * radius.y * radius.z)
end,
func = function(name, target_node, radius)
func = function(name, target_node, radius, hollow)
local start_time = worldeditadditions.get_ms_time()
local replaced = worldeditadditions.ellipsoid(worldedit.pos1[name], radius, target_node, false)
local replaced = worldeditadditions.ellipsoid(worldedit.pos1[name], radius, target_node, hollow)
local time_taken = worldeditadditions.get_ms_time() - start_time
minetest.log("action", name .. " used //ellipsoid at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. replaced .. " nodes in " .. time_taken .. "s")

View file

@ -0,0 +1,146 @@
-- ███████ ███ ███ █████ ██ ██ ███████
-- ██ ████ ████ ██ ██ ██ ██ ██
-- ███████ ██ ████ ██ ███████ █████ █████
-- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ██ ██ ██ ███████
local wea = worldeditadditions
worldedit.register_command("smake", {
params = "<operation> <mode> [<target=xyz> [<base>]]",
description = "Make one or more axes of the current selection odd, even, or equal to another.",
privs = { worldedit = true },
require_pos = 2,
parse = function(params_text)
-- Split params_text, check for missing arguments and fill in empty spots
local parts = wea.split(params_text, "%s+", false)
if #parts < 2 then
return false, "Error: Not enough arguments. Expected \"<operation> <mode> [<target=xyz> [<base>]]\"."
else
for i=3,4 do if not parts[i] then parts[i] = false end end
end
-- Initialze local variables and sets
local oper, mode, targ, base = unpack(parts)
local operSet, modeSet = wea.makeset {"equal", "odd", "even"}, wea.makeset {"grow", "shrink", "avg"}
-- Main Logic
-- Check base if base is present and if so valid.
if base then
if base:match("[xyz]") then -- ensure correct base syntax
base = base:match("[xyz]")
else
return false, "Error: Invalid base \""..base.."\". Expected \"x\", \"y\" or \"z\"."
end
end
-- Resolve target then mode (in that order incase mode is target).
if not targ then -- If no target set to default (xz)
targ = "xz"
elseif targ:match("[xyz]+") then -- ensure correct target syntax
targ = table.concat(wea.tochars(targ:match("[xyz]+"),true,true))
else
return false, "Error: Invalid <target> \""..targ.."\". Expected \"x\" and or \"y\" and or \"z\"."
end
if mode == "average" then -- If mode is average set to avg
mode = "avg"
elseif mode:match("[xyz]+") then -- If target is actually base set vars to correct values.
base, targ, mode = targ:sub(1,1), table.concat(wea.tochars(mode:match("[xyz]+"),true,true)), false
elseif not modeSet[mode] and not base then -- If mode is invalid and base isn't present throw error
return false, "Error: Invalid <mode> \""..mode.."\". Expected \"grow\", \"shrink\", or \"average\"/\"avg\"."
end
if base then
if oper ~= "equal" then base = false -- If operation isn't equalize we don't need <base>
elseif targ:match(base) then -- Else check that base is not in target and return error if it is
return false, "Error: <base> ("..base..") cannot be included in <target> ("..targ..")."
end
end
-- Check if operator is valid
if not operSet[oper] then
return false, "Error: Invalid operator \""..oper.."\". Expected \"odd\", \"even\" or \"equal\"."
end
if false then -- Argument test
return false, "<operator>: " .. oper .. ", <mode>: " .. tostring(mode) .. ", <target>: " .. tostring(targ) .. ", <base>: " .. tostring(base)
end
return true, oper, mode, targ, base
end,
func = function(name, oper, mode, targ, base)
local p1, p2 = vector.new(worldedit.pos1[name]), vector.new(worldedit.pos2[name])
local eval = function() end -- Declare eval placeholder function to edit later
local delta = vector.subtract(p2,p1) -- local delta equation: Vd(a) = V2(a) - V1(a)
local _tl = #targ -- Get targ length as a variable incase mode is "average"/"avg"
local targ = wea.tocharset(targ) -- Break up targ string into set table
local _m = 0 -- _m is the container to hold the max, min or average (depending on the mode) of the axes in targ targ
-- set _m to the max, min or mean of the target axes depending on mode or base if it exists
if base then _m = delta[base]
elseif mode == "avg" then
for k,v in pairs(targ) do _m = _m + math.abs(delta[k]) end
_m = _m / _tl
elseif mode == "grow" then
for k,v in pairs(targ) do if math.abs(delta[k]) > _m then _m = math.abs(delta[k]) end end
else
-- Take output of next(targ), put it in a table, wrap the table in brackets to force evlauation so that
-- we can call the first element of that table to serve as the key for a call to delta.
_m = delta[({next(targ)})[1]]
for k,v in pairs(targ) do if math.abs(delta[k]) < _m then _m = math.abs(delta[k]) end end
end
if oper == "even" then
eval = function(int)
local tmp, abs, neg = int / 2, math.abs(int), int < 0
if math.floor(tmp) ~= tmp then
if mode == "avg" then
if int > _m then int = abs - 1
else int = abs + 1 end
elseif mode == "shrink" and abs > 0 then int = abs - 1
else int = abs + 1 end
end
if neg then int = int * -1 end
return int
end
elseif oper == "odd" then
eval = function(int)
local tmp, abs, neg = int / 2, math.abs(int), int < 0
if math.floor(tmp) == tmp then
if mode == "avg" then
if int > _m then int = abs - 1
else int = abs + 1 end
elseif mode == "shrink" and abs > 0 then int = abs - 1
else int = abs + 1 end
end
if neg then int = int * -1 end
return int
end
else -- Case: oper == "equal"
eval = function(int)
-- Bug: shrink sets pos2 to pos1
if int < 0 then return _m * -1
else return _m end
end
-- return false, "Case \"equal\" not handled."
end
--- Test:
if false then
local brk = ""
for k,v in pairs(targ) do
brk = brk..k..": "..delta[k]..", "
delta[k] = eval(delta[k])
brk = brk..k..": "..delta[k]..", "
end
return false, brk
end
-- //multi //fp set1 589 2 -82 //fp set2 615 2 -53
-- //smake even shrink
for k,v in pairs(targ) do delta[k] = eval(delta[k]) end
worldedit.pos2[name] = vector.add(p1,delta)
worldedit.mark_pos2(name)
return true, "position 2 set to " .. minetest.pos_to_string(p2)
end
})

View file

@ -90,7 +90,7 @@ worldedit.register_command("hollowtorus", {
local values = {parse_params_torus(params_text)}
return unpack(values)
end,
nodes_needed = function(name, target_node, major_radius, minor_radius, axes)
nodes_needed = function(name, target_node, major_radius, minor_radius)
return math.ceil(2 * math.pi*math.pi * major_radius * minor_radius*minor_radius)
end,
func = function(name, target_node, major_radius, minor_radius, axes)

View file

@ -46,6 +46,7 @@ dofile(we_c.modpath.."/commands/selectors/scentre.lua")
dofile(we_c.modpath.."/commands/selectors/scloud.lua")
dofile(we_c.modpath.."/commands/selectors/scol.lua")
dofile(we_c.modpath.."/commands/selectors/scube.lua")
dofile(we_c.modpath.."/commands/selectors/smake.lua")
dofile(we_c.modpath.."/commands/selectors/spop.lua")
dofile(we_c.modpath.."/commands/selectors/spush.lua")
dofile(we_c.modpath.."/commands/selectors/srect.lua")