diff --git a/worldeditadditions/lib/rotate.lua b/worldeditadditions/lib/rotate.lua index 3b45e6d..af0c877 100644 --- a/worldeditadditions/lib/rotate.lua +++ b/worldeditadditions/lib/rotate.lua @@ -9,12 +9,15 @@ local Vector3 = weac.Vector3 --- Compiles a list of rotations into something we can iteratively pass to Vector3.rotate3d. -- TODO Learn Quaternions. --- @param rotlist table<{axis: string, rad: number}> The list of rotations. Rotations will be processed in order. Each rotation is a table with a SINGLE axis as a string (x, y, z, -x, -y, or -z; the axis parameter), and an amount in radians to rotate by (the rad parameter. +-- @param rotlist table<{axis: string|Vector3, rad: number}> The list of rotations. Rotations will be processed in order. Each rotation is a table with a SINGLE axis as a string (x, y, z, -x, -y, or -z; the axis parameter) or a Vector3 (only a SINGLE AXIS set to anything other than 0, and ONLY with a value of 1 or -1), and an amount in radians to rotate by (the rad parameter. -- @returns Vector3[] The list of the compiled rotations, in a form that Vector3.rotate3d understands. local function __compile_rotlist(rotlist) return weac.table.map(rotlist, function(rot) --- 1: Construct a Vector3 to represent which axis we want to rotate on local rotval = Vector3.new(0, 0, 0) + -- Assume that if it's a table, it's a Vector3 instance + if type(rot) == "table" then rotval = rot:clone() end + if rot.axis:find("x", 1, true) then rotval.x = 1 elseif rot.axis:find("y", 1, true) then rotval.y = 1 elseif rot.axis:find("z", 1, true) then rotval.z = 1 end @@ -33,7 +36,7 @@ end -- @param pos1 Vector3 Position 1 of the defined region to rotate. -- @param pos2 Vector3 Position 2 of the defined region to rotate. -- @param origin Vector3 The coordinates of the origin point around which we should rotate the region defined by pos1..pos2. --- @param rotlist table<{axis: string, rad: number}> The list of rotations. Rotations will be processed in order. Each rotation is a table with a SINGLE axis as a string (x, y, z, -x, -y, or -z; the axis parameter), and an amount in radians to rotate by (the rad parameter. +-- @param rotlist table<{axis: string, rad: number}> The list of rotations. Rotations will be processed in order. Each rotation is a table with a SINGLE axis as a string (x, y, z, -x, -y, or -z; the axis parameter), and an amount in radians to rotate by (the rad parameter). -- @returns bool,string|table<{changed: number}> A success boolean (true == success; false == failure), followed by either an error message as a string if success == false or a table of statistics if success == true. -- -- Currently the only parameter in the statistics table is changed, which is a number representing the number of nodes that were rotated. diff --git a/worldeditadditions_commands/commands/rotate.lua b/worldeditadditions_commands/commands/rotate.lua new file mode 100644 index 0000000..23b4653 --- /dev/null +++ b/worldeditadditions_commands/commands/rotate.lua @@ -0,0 +1,95 @@ +local wea_c = worldeditadditions_core +local Vector3 = wea_c.Vector3 + + +-- ██████ ██████ ████████ █████ ████████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██████ ██ ██ ██ ███████ ██ █████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ██████ ██ ██ ██ ██ ███████ + +worldeditadditions_core.register_command("rotate", { + params = " [ ...] [origin|o []]", + description = "Rotates the defined region arounnd the given axis by the given number of degrees. Angles are NOT limited to 90-degree increments. When multiple axes and angles are specified, these transformations are applied in order. If o [] is specified, then the specific position number (default: 3) is considered a custom rotation origin instead of the centre of the region. CAUTION: Rotating large areas of memory by 45° can be memory intensive!", + privs = { worldedit = true }, + require_pos = 2, + parse = function (params_text) + if not params_text then params_text = "" end + local parts = wea_c.split_shell(params_text) + + local mode_store + local mode = "AXIS" + local success, axis_next, angle + + local origin = "__AUTO__" + local rotlist = {} + + for i,part in ipairs(parts) do + if part == "origin" or part == "o" then + mode_store = mode + mode = "ORIGIN" + elseif mode == "ORIGIN" then + origin = tonumber(part) + if not origin or origin < 1 then + return false, "Error: Expected positive integer as the WorldEditAdditions position that should be considered the origin point for rotation operation." + end + origin = math.floor(origin) + + mode = mode_store + mode_store = nil + elseif mode == "AXIS" then + axis_next = wea_c.parse.axis_rotation.axis_name(part) + if not success then return success, axis_next end + mode = "ANGLE" + elseif mode == "ANGLE" then + angle = part + + -- 1: Determine if radians; strip suffix + local pos_rad = part:find("ra?d?$") + if pos_rad then + angle = angle:sub(1, pos_rad-1) + end + + -- 2: Parse as number + angle = tonumber(angle) + if not angle then + return false, "Error: Expected numerical angle value, but found '"..tostring(part).."'." + end + + -- 3: Convert degrees → radians + if not pos_rad then + -- We have degrees! Convert em to radians 'cauuse mathematics + angle = math.rad(angle) + end + + -- 4: Add to rotlist + table.insert(rotlist, { + axis = axis_next, + rad = angle + }) + + -- 5: Change mode and continue + mode = "AXIS" + else + return false, "Error: Unknown parsing mode "..tostring(mode)..". This is a bug." + end + end + + return true, origin, rotlist + end, + nodes_needed = function(name, times) + -- TODO: .......this is a good question, actually. + end, + func = function(name, origin, rotlist) + local start_time = wea_c.get_ms_time() + + -- TODO: Do rotation operation here. + + local time_taken = wea_c.get_ms_time() - start_time + + + -- TODO: Update logging below. This will obviously crash due to unknown variables right now. + minetest.log("action", name .. " used //rotate at "..pos1.." - "..pos2.." with origin "..origin..", replacing "..changed.." nodes in "..time_taken.."s") + return true, changed.." nodes replaced in "..wea_c.format.human_time(time_taken) + end +}) diff --git a/worldeditadditions_core/utils/parse/axes_rotation.lua b/worldeditadditions_core/utils/parse/axes_rotation.lua new file mode 100644 index 0000000..ede628f --- /dev/null +++ b/worldeditadditions_core/utils/parse/axes_rotation.lua @@ -0,0 +1,81 @@ +local weac = worldeditadditions_core +local Vector3 = weac.Vector3 + +-- From worldedit_hub_helper. +-- Written by me, @sbrl and just lifted :-) +local function calc_rotation_text(rotation) + if rotation <= math.pi / 4 or rotation >= (7 * math.pi) / 4 then + return Vector3.new(0, 0, 1) -- +z + elseif rotation > math.pi / 4 and rotation <= (3 * math.pi) / 4 then + return Vector3.new(-1, 0, 0) -- -x + elseif rotation > (3 * math.pi) / 4 and rotation <= (5 * math.pi) / 4 then + return Vector3.new(0, 0, -1) -- -z + else + return Vector3.new(1, 0, 0) -- +x + end +end + +function rot_axis_left(axis) + if axis.x == 1 or axis.x == -1 then + axis.x, axis.z = 0, axis.x + elseif axis.z == 1 or axis.z == -1 then + axis.x, axis.z = -axis.z, 0 + end + return axis -- CAUDION, this function mutates! +end + +--- Parses an axis name into a string. +-- Currently handles: +-- - x/y/z -x/-y/-z +-- - front → downwards pitch +-- - back → upwards pitch +-- - left → rotate around Z to left +-- - right → rotate around Z to right +-- - pitch, yaw, roll - clockwise is considered positive. +-- @param str string The axis to parse, as a single (pre-trimmed) string. +-- @param [player=nil] Player Optional. A Minetest Player object representing the player to make any relative keywords like front-back/pitch/roll/etc relative to in parsing. Defautls to nil, which disables parsing of relatiive terms. Any object passed just needs to support player:get_look_horizontal(): https://github.com/minetest/minetest/blob/master/doc/lua_api.md#player-only-no-op-for-other-objects +-- @returns bool,Vector3|string A success bool (false=failure) followed by either an error message (if success=false) or otherwise the axis name, parsed into a Vector3 instance. +local function parse_rotation_axis_name(str, player) + local vector = Vector3.new(0, 0, 0) + + -- At most ONE of these cases is true at a time. + -- Even if it were possible to handle more than one (which it isn't without quaternions which aren't used in the backend), we would have an undefined ordering problem with the application of such rotations. Rotations ≠ translations in this case! + if str:find("x", 1, true) then rotval.x = 1 + elseif str:find("y", 1, true) then rotval.y = 1 + elseif str:find("z", 1, true) then rotval.z = 1 + elseif str:find("left", 1, true) then rotval.z = -1 + elseif str:find("right", 1, true) + or str:find("yaw") then rotval.z = 1 + elseif type(player) ~= "nil" then + local player_rotation_h = calc_rotation_text(player:get_look_horizontal()) + + -- FRONT BACK + + if str:find("front", 1, true) + or str:find("f$") + or str:find("pitch") then + vector = rot_axis_left(player_rotation_h) + elseif str:find("back", 1, true) + or str:find("b$") then + vector = rot_axis_left(player_rotation_h) * -1 + elseif str:find("roll") then + vector = player_rotation_h + else + return false, "Error: Could not understand rotational axis keyword '"..str.."'." + end + else + return false, "Error: Could not understand rotational axis keyword '" .. str .. "'." + end + + --- Handle negatives + if str:sub(1, 1) == "-" then + vector = vector * -1 + end + + return true, vector +end + + +return { + axis_name = parse_rotation_axis_name +} \ No newline at end of file diff --git a/worldeditadditions_core/utils/parse/init.lua b/worldeditadditions_core/utils/parse/init.lua index a36e958..9a314c3 100644 --- a/worldeditadditions_core/utils/parse/init.lua +++ b/worldeditadditions_core/utils/parse/init.lua @@ -21,6 +21,8 @@ wea_c.parse = { axes = axes.parse_axes, axis_name = axes.parse_axis_name, + axes_rotation = dofile(wea_c.modpath .. "/utils/parse/axes_rotation.lua"), + seed = dofile(wea_c.modpath.."/utils/parse/seed.lua"), chance = dofile(wea_c.modpath.."/utils/parse/chance.lua"), map = dofile(wea_c.modpath.."/utils/parse/map.lua"),