diff --git a/hud.lua b/hud.lua index b854acc..200cff8 100644 --- a/hud.lua +++ b/hud.lua @@ -1,44 +1,37 @@ -local ipairs - = ipairs - -local minetest, nodecore - = minetest, nodecore - -local rotate = include("rotate") +local nodecore + = nodecore local LABEL_ROTATION_HINT = "rotation hint" -local TEX_ROTATE_CLOCKWISE = "nc_extended_rotating_hud_rotate_clockwise.png" -local TEX_ROTATE_COUNTER_CLOCKWISE = "nc_extended_rotating_hud_rotate_counter_clockwise.png" - -local function do_player_rotating_checks(player, data) - local pt = data.raycast() - local node = pt and pt.type == "node" and minetest.get_node(pt.under) - - if node and rotate.is_rotatable(node) then - local is_sneaking = player:get_player_control().sneak - local texture = is_sneaking and TEX_ROTATE_COUNTER_CLOCKWISE or TEX_ROTATE_CLOCKWISE +local TEX_ROTATE_FACE = "nc_extended_rotating_hud_rotate_face.png" +local TEX_ROTATE_EDGE = "nc_extended_rotating_hud_rotate_edge.png" + +local hud = {} + +local function crosshair_hud_element(texture) + return { + label = LABEL_ROTATION_HINT, + hud_elem_type = "image", + text = texture, + position = { x = 0.5, y = 0.5 }, + offset = { x = 0, y = 0 }, + alignment = { x = 0, y = 0 }, + scale = { x = 1, y = 1 }, + quick = true + } +end - nodecore.hud_set(player, { - label = LABEL_ROTATION_HINT, - hud_elem_type = "image", - text = texture .. "^[opacity:" .. 192, - position = { x = 0.5, y = 0.5 }, - offset = { x = 0, y = 0 }, - alignment = { x = 0, y = 0 }, - scale = { x = 1, y = 1 }, - quick = true - }) +local function update_player_hud(player, state) + local mode = state and state.mode + if mode == "face" then + local texture = TEX_ROTATE_FACE + if state.invert then texture = texture .. "^[transformFX" end + texture = texture .. "^[opacity:" .. 192 + nodecore.hud_set(player, crosshair_hud_element(texture)) else - nodecore.hud_set(player, { - label = LABEL_ROTATION_HINT, - ttl = 0 - }) + nodecore.hud_set(player, { label = LABEL_ROTATION_HINT, ttl = 0 }) end end +hud.update_player_hud = update_player_hud -nodecore.register_playerstep({ - label = "crosshair", - priority = -101, - action = do_player_rotating_checks, -}) +return hud diff --git a/init.lua b/init.lua index 99e69b4..68dfe8a 100755 --- a/init.lua +++ b/init.lua @@ -1,21 +1,66 @@ -local minetest, vector, include - = minetest, vector, include +local minetest, vector, nodecore, include + = minetest, vector, nodecore, include -local rotate = include("rotate") -local hud = include("hud") +local hud = include("hud") +local rotate = include("rotate") +local utility = include("utility") -minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) - local def = minetest.registered_nodes[node.name] - if def.paramtype2 ~= "facedir" then return end +-- TODO: Add crosshair indicators for rotating around edges. +-- TODO: Register only the nodes that should be rotatable. +-- TODO: Replace `on_rightclick` of intended nodes. +-- TODO: Add some more comments. + +-- Distance at which we want to rotate by "pushing" on an edge. +local EDGE_DISTANCE = 1.5 / 16 -- 1.5 texels (at 16² texture resolution) + +local rotating_state = {} + +nodecore.register_playerstep({ + label = "extended rotating update", + action = function(player, player_data) + local name = player:get_player_name() + local state = {}; rotating_state[name] = state + + local pointed_thing = player_data.raycast() + if (not pointed_thing) or pointed_thing.type ~= "node" then return nil end + + state.pos = pointed_thing.under + state.node = minetest.get_node(state.pos) + if not rotate.is_rotatable(state.node) then return end + + if vector.equals(pointed_thing.above, pointed_thing.under) then return end + state.face = pointed_thing.above - pointed_thing.under - -- Vector that points away from the punched face. - if (not pointed_thing.above) or (not pointed_thing.under) then return end - local axis = vector.subtract(pointed_thing.above, pointed_thing.under) + local edge, distance = utility.find_closest_edge(state.node, pointed_thing) + if edge and distance > EDGE_DISTANCE then + state.axis = state.face + state.mode = "face" + else + state.axis = state.face:cross(edge):normalize():round() + state.mode = "edge" + end + + state.invert = player:get_player_control().sneak + hud.update_player_hud(player, state) + end, +}) + +minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + rotating_state[name] = nil +end) + + +minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) + if not minetest.is_player(puncher) then return end + local name = puncher:get_player_name() + local state = rotating_state[name] + if not state then return end - -- Rotate clockwise by default. - local degrees = -90 - -- If player is sneaking, reverse the rotation. - if minetest.is_player(puncher) and puncher:get_player_control().sneak then degrees = -degrees end + -- Make sure we're still the same node that we raycasted in `playerstep`. + if not vector.equals(pos, state.pos) then return end + if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return end - rotate.rotate_node(pos, node, axis, degrees) + local degrees = state.invert and 90 or -90 + rotate.rotate_node(state.pos, state.node, state.axis, degrees) end) diff --git a/rotate.lua b/rotate.lua index caf193c..5951be6 100644 --- a/rotate.lua +++ b/rotate.lua @@ -1,6 +1,7 @@ -local math, ipairs - = math, ipairs - +local ipairs, unpack + = ipairs, unpack +local math_min, math_max, math_floor, math_rad + = math.min, math.max, math.floor, math.rad local minetest, vector = minetest, vector @@ -16,6 +17,16 @@ local AXIS_LOOKUP = { vector.new( 0, -1, 0), -- -Y } +-- Lookup table to find out how to rotate for each `facedir` axis. +local AXIS_ROTATION = { + nil, -- No rotation. + { vector.new(1, 0, 0), 90 }, + { vector.new(1, 0, 0), -90 }, + { vector.new(0, 0, 1), -90 }, + { vector.new(0, 0, 1), 90 }, + { vector.new(0, 0, 1), 180 }, +} + -- Takes an axis vector and returns its index in the `AXIS_LOOKUP` table. -- Returns `nil` for any vector that is not a valid axis vector. local function axis_vector_to_index(vec) @@ -40,24 +51,49 @@ for up_index, up in ipairs(AXIS_LOOKUP) do end -- Returns whether the specified `node` is rotatable by this mod. --- TODO: Register such that only certain nodes may be rotated. local function is_rotatable(node) - local def = minetest.registered_nodes[node.name] - return def.paramtype2 == "facedir" + local name = node and node.name + local def = minetest.registered_nodes[name] + return def and def.paramtype2 == "facedir" end rotate.is_rotatable = is_rotatable +local function rotate_box_by_facedir(box, facedir) + if facedir == 0 then return end + + local axis_index = math_floor(facedir / 4) + if axis_index ~= 0 then + local axis, degrees = unpack(AXIS_ROTATION[1 + axis_index]) + box.min = box.min:rotate_around_axis(axis, math_rad(degrees)) + box.max = box.max:rotate_around_axis(axis, math_rad(degrees)) + end + + local axis_rot = facedir % 4 + if axis_rot ~= 0 then + local axis = AXIS_LOOKUP[1 + axis_index] + local degrees = axis_rot * 90 + box.min = box.min:rotate_around_axis(axis, math_rad(degrees)) + box.max = box.max:rotate_around_axis(axis, math_rad(degrees)) + end + + -- Recalculate the proper minimum and maximum bounds, since we just rotated these vectors. + box.min.x, box.max.x = math_min(box.min.x, box.max.x), math_max(box.min.x, box.max.x) + box.min.y, box.max.y = math_min(box.min.y, box.max.y), math_max(box.min.y, box.max.y) + box.min.z, box.max.z = math_min(box.min.z, box.max.z), math_max(box.min.z, box.max.z) +end +rotate.rotate_box_by_facedir = rotate_box_by_facedir + -- Rotates `node` at the specified `pos` around `axis` by `degrees` counter-clockwise. local function rotate_node(pos, node, axis, degrees) if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end if axis_vector_to_index(axis) == nil then error("axis must be an axis vector") end if not is_rotatable(node) then error("node is not rotatable") end - local up = AXIS_LOOKUP[1 + math.floor(node.param2 / 4)] + local up = AXIS_LOOKUP[1 + math_floor(node.param2 / 4)] local back = minetest.facedir_to_dir(node.param2) - up = up :rotate_around_axis(axis, math.rad(degrees)):round() - back = back:rotate_around_axis(axis, math.rad(degrees)):round() + up = up :rotate_around_axis(axis, math_rad(degrees)):round() + back = back:rotate_around_axis(axis, math_rad(degrees)):round() local up_index = axis_vector_to_index(up) local back_index = axis_vector_to_index(back) diff --git a/textures/nc_extended_rotating_hud_rotate_counter_clockwise.png b/textures/nc_extended_rotating_hud_rotate_edge.png similarity index 67% rename from textures/nc_extended_rotating_hud_rotate_counter_clockwise.png rename to textures/nc_extended_rotating_hud_rotate_edge.png index 49aaf05..69c2cf5 100644 Binary files a/textures/nc_extended_rotating_hud_rotate_counter_clockwise.png and b/textures/nc_extended_rotating_hud_rotate_edge.png differ diff --git a/textures/nc_extended_rotating_hud_rotate_clockwise.png b/textures/nc_extended_rotating_hud_rotate_face.png similarity index 100% rename from textures/nc_extended_rotating_hud_rotate_clockwise.png rename to textures/nc_extended_rotating_hud_rotate_face.png diff --git a/utility.lua b/utility.lua index 81647b6..8695f10 100644 --- a/utility.lua +++ b/utility.lua @@ -1,16 +1,67 @@ -local ipairs, tostring - = ipairs, tostring -local minetest - = minetest +local math_abs, math_sign + = math.abs, math.sign +local minetest, vector, include + = minetest, vector, include + +local rotate = include("rotate") local utility = {} -local function debug_tell(...) - local text = "" - for _, v in ipairs({...}) do text = text .. tostring(v) end - minetest.chat_send_all(text) +-- Default selection boxes for a node that doesn't have one them explicitly. +local DEFAULT_SELECTION_BOX = { min = vector.new(-0.5, -0.5, -0.5), + max = vector.new( 0.5, 0.5, 0.5) } + +-- Gets the active `selection_box` for the specified `node` which +-- has a `paramtype2` of `facedir`, based on its `param2` value. +local function get_node_active_selection_box(node) + local def = minetest.registered_nodes[node.name] + local box = def.selection_box and def.selection_box.fixed + -- No need to rotate the default selection box. + if not box then return DEFAULT_SELECTION_BOX end + -- If node definition specifies multiple selection boxes, just pick the first (for now). + if type(box[1]) == "table" then box = box[1] end + -- Transform the `{ x1, y1, z1, x2, y2, z2 }` box to a `{ min, max }` one. + box = { min = vector.new(box[1], box[2], box[3]), + max = vector.new(box[4], box[5], box[6]) } + -- Rotate the box to face `facedir`. + rotate.rotate_box_by_facedir(box, node.param2) + return box +end + +-- Finds the closest edge of the node the player is looking at as +-- a vector pointing away from the center, and the distance to it. +local function find_closest_edge(node, pointed_thing) + -- For this math to work, we assume that selection box is centered. + local max = get_node_active_selection_box(node).max + -- Point relative to the collision box we're pointing at. + local point = pointed_thing.intersection_point - pointed_thing.under + + -- Find the edge we're closest to. + local vec = vector.zero() + if math_abs(point.y / point.x) > max.x / max.y then + vec.y = math_sign(point.y) + if math_abs(point.z / point.x) > max.x / max.z + then vec.z = math_sign(point.z) + else vec.x = math_sign(point.x) + end + else + vec.x = math_sign(point.x) + if math_abs(point.z / point.y) > max.y / max.z + then vec.z = math_sign(point.z) + else vec.y = math_sign(point.y) + end + end + + local d = 1 + local v = max - point:multiply(vec):apply(math_abs) + local EPSILON = tonumber("1.19e-07") + if math_abs(v.x) > EPSILON and v.x < d then d = v.x end + if math_abs(v.y) > EPSILON and v.y < d then d = v.y end + if math_abs(v.z) > EPSILON and v.z < d then d = v.z end + + return vec, d end -utility.debug_tell = debug_tell +utility.find_closest_edge = find_closest_edge return utility