Replace rightclick interaction

- Replace on_rightclick of registered node
- Replace on_place of empty hand item to
  handle sneak-rightclicking to invert rotation
- Move registry related things to registry.lua
- Fix off-by-one error in nodecore_filtered_lookup
- Detect corner interactions, not just edges
- Corner interactions rotate by 120°
- Add rotate_node function to mimic NodeCore spin
main
copygirl 1 year ago
parent b379937511
commit dcf12fcdf4
  1. 9
      README.md
  2. 105
      init.lua
  3. 2
      mod.conf
  4. 73
      registry.lua
  5. 66
      rotate.lua
  6. 49
      utility.lua

@ -5,10 +5,11 @@ This mod follows in the footsteps of the useful [Extended Placement] mod for
as optics and doors easily, without having to cycle through all possible as optics and doors easily, without having to cycle through all possible
rotations every time. rotations every time.
Rotating a node can be done in two ways, either by clicking the center or the Rotating a node can be done in three ways:
edge of a face. By interacting with the center, the node rotates around that - Click near the center of a face, and the node rotates 90° around it.
face. When interacting with the edge, the node is "pushed" that way, rotating - Click near an edge, and the node will be rotated 90° away from you.
away from you. Sneaking inverts the direction of the rotation. - Clikc near a corner, and the node will rotate 120° around it.
Sneaking inverts the direction of the rotation.
At this time, nodes that can be rotated are hardcoded into the mod, since At this time, nodes that can be rotated are hardcoded into the mod, since
their valid orientations can't be easily extracted. A function mimicking their valid orientations can't be easily extracted. A function mimicking

@ -1,54 +1,54 @@
local ipairs, minetest, vector, nodecore, include local minetest, vector, nodecore, include
= ipairs, minetest, vector, nodecore, include = minetest, vector, nodecore, include
local math_round = math.round
local hud = include("hud") local hud = include("hud")
local rotate = include("rotate") local rotate = include("rotate")
local utility = include("utility") local utility = include("utility")
local registry = include("registry")
-- TODO: Add crosshair indicators for rotating around edges. -- TODO: Add crosshair indicators for rotating around edges.
-- TODO: Replace `on_rightclick` of intended nodes?
-- TODO: Allow right-clicking while holding sneak with an empty hand to invert rotation.
-- TODO: Add some more comments. -- TODO: Add some more comments.
-- TODO: Add particles to preview rotation?
-- Distance at which we want to rotate by "pushing" on an edge. -- 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 EDGE_DISTANCE = 1.5 / 16 -- texels (at 16² texture resolution)
-- Contains a per-player state that holds information, created by a raycast -- Contains a per-player state that holds information, created by a raycast
-- done in `playerstep`, about the node this player might rotate. This is used -- done in `playerstep`, about the node this player might rotate. This is used
-- to decide what to do when a rightclick occurs and to update the their HUD. -- to update the their HUD and to decide what to do when a rightclick occurs.
local rotating_state = {} local rotating_state = {}
-- Register rotatable nodes.
do
local function nodecore_filtered_lookup(eq_func)
local lookup = {}
for i = 0, 23 do
local facedir = nodecore.facedirs[i]
for j = 1, #lookup do
local other = nodecore.facedirs[lookup[j]]
if eq_func(facedir, other) then lookup[i] = j; break end
end
lookup[i] = lookup[i] or i
end
return lookup
end
local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup( local function handle_rotatable_rightclick(pos, node, clicker)
function(a, b) return vector.equals(a.f, b.f) end) local state = rotating_state[clicker:get_player_name()]
local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup( if not state then return false end -- Not looking at anything rotatable.
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do -- Make sure we're still the same node that we raycasted in `playerstep`.
rotate.register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end if not vector.equals(pos, state.pos) then return true end
for _, prism_state in ipairs({ "", "_on", "_gated" }) do if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return true end
rotate.register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end -- Only continue if the node can actually be sucessfully rotated.
if not state.success then return true end
rotate.register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP) rotate.rotate_node(pos, node, state.facedir)
rotate.register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP) return true
end
registry.custom_on_rightclick = handle_rotatable_rightclick
-- Override `nc_scaling`'s default empty hand right-click behavior so we can use it
-- to rotate things when holding sneak, which doesn't trigger a node's `on_rightclick`.
local default_on_place = minetest.registered_items[""].on_place
minetest.registered_items[""].on_place = function(item_stack, placer, pointed_thing, ...)
-- Player must sneak in order for this to run. Non-sneak is handled by node's `on_rightclick`.
if pointed_thing.type == "node" and minetest.is_player(placer) and placer:get_player_control().sneak then
local pos = pointed_thing.under
local node = minetest.get_node(pos)
if handle_rotatable_rightclick(pos, node, placer) then return end
end
default_on_place(item_stack, placer, pointed_thing, ...)
end end
-- Calculates the state for the specified `player` and player `data`. -- Calculates the state for the specified `player` and player `data`.
-- This state contains information about how the player would rotate a block. -- This state contains information about how the player would rotate a block.
local function calculate_rotating_state(player, data) local function calculate_rotating_state(player, data)
@ -59,26 +59,27 @@ local function calculate_rotating_state(player, data)
state.pos = pointed_thing.under state.pos = pointed_thing.under
state.node = minetest.get_node(state.pos) state.node = minetest.get_node(state.pos)
if not rotate.is_rotatable(state.node) then return end if not registry.is_rotatable(state.node) then return end
if vector.equals(pointed_thing.above, pointed_thing.under) then return end if vector.equals(pointed_thing.above, pointed_thing.under) then return end
state.face = pointed_thing.above - pointed_thing.under state.face = pointed_thing.above - pointed_thing.under
local edge, distance = utility.find_closest_edge(state.node, pointed_thing) local degrees = 90
-- FIXME: Something in `find_closest_edge` is causing an unexpected edge to be returned, hence the additional check. local r = utility.rotation_vector_from_lookat(state.node, pointed_thing, EDGE_DISTANCE)
if edge and distance <= EDGE_DISTANCE and state.face:multiply(edge):length() ~= 0 then state.axis = r
state.axis = state.face:cross(edge):normalize()
state.mode = "edge" local num = math_round(r.x * r.x + r.y * r.y + r.z * r.z) -- Squared length.
else if num == 1 then state.mode = "face"
state.axis = state.face elseif num == 2 then state.mode = "edge"; state.axis = state.face:cross(r)
state.mode = "face" elseif num == 3 then state.mode = "mirror"; degrees = 120
end end
-- Sneaking causes the direction of the rotation to be inverted.
state.invert = player:get_player_control().sneak state.invert = player:get_player_control().sneak
if state.invert then degrees = -degrees end
local degrees = state.invert and 90 or -90 state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, -degrees)
state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, degrees) state.facedir = registry.fix_rotatable_facedir(state.node, state.facedir)
state.facedir = rotate.fix_rotatable_facedir(state.node, state.facedir)
state.success = state.facedir and state.facedir ~= state.node.param2 state.success = state.facedir and state.facedir ~= state.node.param2
return state return state
@ -97,19 +98,3 @@ minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name() local name = player:get_player_name()
rotating_state[name] = nil rotating_state[name] = nil
end) 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
-- 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
-- Only continue if the node can actually be sucessfully rotated.
if not state.success then return end
minetest.swap_node(pos, nodecore.underride({ param2 = state.facedir }, node))
end)

@ -2,4 +2,4 @@ name = nc_extended_rotating
title = NodeCore Extended Rotating title = NodeCore Extended Rotating
author = copygirl author = copygirl
description = Rotate optics and doors from NodeCore with ease. description = Rotate optics and doors from NodeCore with ease.
depends = nc_api_all, nc_optics, nc_doors depends = nc_api_all, nc_scaling, nc_optics, nc_doors

@ -0,0 +1,73 @@
local minetest, nodecore
= minetest, nodecore
local registry = {}
local registered_rotatables = {}
-- Registered rotatable nodes will call this function when their `on_rightclick`
-- is triggered instead of their default implementation. Can be overwritten.
registry.custom_on_rightclick = function(pos, node, clicker, item_stack, pointed_thing) end
local function register_rotatable(name, facedir_lookup)
local def = minetest.registered_nodes[name]
if not def then error("Unknown node '" .. name .. "'") end
if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end
def.on_rightclick = function(...) registry.custom_on_rightclick(...) end
registered_rotatables[name] = { lookup = facedir_lookup }
end
registry.register_rotatable = register_rotatable
-- Returns whether the specified `node` is rotatable by this mod.
local function is_rotatable(node)
local name = node and node.name or ""
return not not registered_rotatables[name]
end
registry.is_rotatable = is_rotatable
-- Fixes facedir for the specified `node` when rotated to `facedir`.
-- * Returns `nil` if the node can't rotate this way.
-- * Returns `facedir` when the node can rotate this way.
-- * Returns another facedir when the node should rotate another equivalent way.
local function fix_rotatable_facedir(node, facedir)
local name = node and node.name or ""
local entry = registered_rotatables[name]
if not entry then return nil end
if not entry.lookup then return facedir end
return entry.lookup[facedir]
end
registry.fix_rotatable_facedir = fix_rotatable_facedir
--------------------------------------------
-- Register rotatable nodes from NodeCore --
--------------------------------------------
local function nodecore_filtered_lookup(eq_func)
local lookup = {}
for i = 0, 23 do
local facedir = nodecore.facedirs[i]
for j = 0, #lookup - 1 do
local other = nodecore.facedirs[lookup[j]]
if eq_func(facedir, other) then lookup[i] = j; break end
end
lookup[i] = lookup[i] or i
end
return lookup
end
local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.f) end)
local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do
register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end
for _, prism_state in ipairs({ "", "_on", "_gated" }) do
register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end
register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP)
register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP)
return registry

@ -7,8 +7,6 @@ local minetest, vector
local rotate = {} local rotate = {}
local rotatable_registry = {}
-- Axis lookup table based on how Minetest's `facedir` operates. -- Axis lookup table based on how Minetest's `facedir` operates.
local AXIS_LOOKUP = { local AXIS_LOOKUP = {
vector.new( 0, 1, 0), -- +Y vector.new( 0, 1, 0), -- +Y
@ -52,50 +50,44 @@ for up_index, up in ipairs(AXIS_LOOKUP) do
end end
end end
local function register_rotatable(name, facedir_lookup) -- Rotates (or "spins") the specified `node` to face `facedir`,
local def = minetest.registered_nodes[name] -- emulating the behavior of NodeCore's spin functionality.
if not def then error("Unknown node '" .. name .. "'") end local function rotate_node(pos, node, facedir)
if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end local def = minetest.registered_nodes[node.name]
rotatable_registry[name] = { lookup = facedir_lookup } if (not def) or def.paramtype2 ~= "facedir" then error("Node's paramtype2 must be 'facedir'") end
end minetest.swap_node(pos, nodecore.underride({ param2 = facedir }, node))
rotate.register_rotatable = register_rotatable nodecore.node_sound(pos, "place")
if def.on_spin then def.on_spin(pos, node) end
-- Returns whether the specified `node` is rotatable by this mod.
local function is_rotatable(node)
local name = node and node.name or ""
return not not rotatable_registry[name]
end
rotate.is_rotatable = is_rotatable
-- Fixes facedir for the specified `node` when rotated to `facedir`.
-- * Returns `nil` if the node can't rotate this way.
-- * Returns `facedir` when the node can rotate this way.
-- * Returns another facedir when the node should rotate another equivalent way.
local function fix_rotatable_facedir(node, facedir)
local name = node and node.name or ""
local entry = rotatable_registry[name]
if not entry then return nil end
if not entry.lookup then return facedir end
return entry.lookup[facedir]
end end
rotate.fix_rotatable_facedir = fix_rotatable_facedir rotate.rotate_node = rotate_node
-- Returns the specified `facedir` rotated around `axis` by `degrees` counter-clickwise.
local function rotate_facedir(facedir, 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 " .. tostring(axis) .. " is not an axis vector") end
-- Returns the `up` and `back` vectors that make up the specified `facedir`.
local function vectors_from_facedir(facedir)
local up = AXIS_LOOKUP[1 + math_floor(facedir / 4)] local up = AXIS_LOOKUP[1 + math_floor(facedir / 4)]
local back = minetest.facedir_to_dir(facedir) local back = minetest.facedir_to_dir(facedir)
return up, back
end
rotate.vectors_from_facedir = vectors_from_facedir
up = up :rotate_around_axis(axis, math_rad(degrees)):round() -- Returns a `facedir` constructed from the specified `up` and `back` vectors.
back = back:rotate_around_axis(axis, math_rad(degrees)):round() local function facedir_from_vectors(up, back)
local up_index = axis_vector_to_index(up) local up_index = axis_vector_to_index(up)
local back_index = axis_vector_to_index(back) local back_index = axis_vector_to_index(back)
return FACEDIR_LOOKUP[up_index][back_index] return FACEDIR_LOOKUP[up_index][back_index]
end end
rotate.facedir_from_vectors = facedir_from_vectors
-- Returns the specified `facedir` rotated around `vec` by `degrees` counter-clickwise.
local function rotate_facedir(facedir, vec, degrees)
-- NOTE: Removed the axis requirement so we can use this to rotate around corners.
-- However, since we `round` the result vectors this might ignore errors.
-- if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end
-- if axis_vector_to_index(vec) == nil then error("axis is not an axis vector") end
local up, back = vectors_from_facedir(facedir)
up = up :rotate_around_axis(vec, math_rad(degrees)):round()
back = back:rotate_around_axis(vec, math_rad(degrees)):round()
return facedir_from_vectors(up, back)
end
rotate.rotate_facedir = rotate_facedir rotate.rotate_facedir = rotate_facedir
-- Rotates the specified default orientation `{ min, max }` box to -- Rotates the specified default orientation `{ min, max }` box to

@ -29,39 +29,22 @@ local function get_node_active_selection_box(node)
return box return box
end end
-- Finds the closest edge of the node the player is looking at as -- Calculates the rotation vector from where a player is looking at on this node.
-- a vector pointing away from the center, and the distance to it. -- * If looking near an edge, the vector points in the direction of that edge.
local function find_closest_edge(node, pointed_thing) -- * If looking near a corner, the vector similarly points in that direction.
-- For this math to work, we assume that selection box is centered. -- * Otherwise, vector simply points away from the face.
local max = get_node_active_selection_box(node).max local function rotation_vector_from_lookat(node, pointed_thing, edge_distance)
-- Point relative to the collision box we're pointing at. local box = get_node_active_selection_box(node)
local point = pointed_thing.intersection_point - pointed_thing.under local center = (box.min + box.max) / 2
-- Find the edge we're closest to. -- Calculate bounds, single vector describing the size of the selection box.
local vec = vector.zero() local bounds = box.max - center -- This extends both into positive and negative.
if math_abs(point.y / point.x) > max.x / max.y then -- Calculate intersection point relative to these bounds.
vec.y = math_sign(point.y) local point = pointed_thing.intersection_point - pointed_thing.under - center
if math_abs(point.z / point.x) > max.x / max.z
then vec.z = math_sign(point.z) return point:combine(bounds, function(p, b)
else vec.x = math_sign(point.x) return b - math_abs(p) <= edge_distance and math_sign(p) or 0 end)
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 end
utility.find_closest_edge = find_closest_edge utility.rotation_vector_from_lookat = rotation_vector_from_lookat
return utility return utility

Loading…
Cancel
Save