From 5115aedcb735a319330bf4730fe562f1caac53b7 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 14 Oct 2023 13:25:20 +0200 Subject: [PATCH] Add rotating by "pushing" on edges - Move functionality to init.lua: - Player step event handling - Rotation state is stored in rotating_state - Calls hud.update_player_hud - Punch node event uses rotating_state - Add utility functions: - Rotate box by a specified facedir value - Find node's current (rotated) selection box - Find pointed-at node's closest edge --- hud.lua | 65 +++++++-------- init.lua | 75 ++++++++++++++---- rotate.lua | 54 ++++++++++--- ... nc_extended_rotating_hud_rotate_edge.png} | Bin 5002 -> 4976 bytes ... nc_extended_rotating_hud_rotate_face.png} | Bin utility.lua | 69 +++++++++++++--- 6 files changed, 194 insertions(+), 69 deletions(-) rename textures/{nc_extended_rotating_hud_rotate_counter_clockwise.png => nc_extended_rotating_hud_rotate_edge.png} (67%) rename textures/{nc_extended_rotating_hud_rotate_clockwise.png => nc_extended_rotating_hud_rotate_face.png} (100%) 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 49aaf05e7aff3a8ca6710db4462086045963b561..69c2cf5c04cbc337fa8ca72f764466de24e8aeb1 100644 GIT binary patch delta 1048 zcmeBD|DZOZnUQ&7i=mG>69aG_W!>Q7|;J zGO)BVFkxV1U|hfqBpJXMNHOp%JopyKYnjZ*C|YmpaX3Jv)jxRY(h%7<3Xdi%w2BlE zEnE|-;nW(Sz2#NHr%U!3mnLl4VZpVImAzhUHJ{$OT~<4|njY|XtnQw?sC!C9d{Bnq zG8TY4ahI)#`j1RcC$Fg_F#-Yq7-%I@yN`2petgvGnvhSX_Je@bT}v zCC-1^_x<^+`ryxE%r#1db0pJxE1E0PHm4QbQ}5d_X^qf);r`iI6h10n7oC3JLR?m? zBJIeTCC@vRk0fpEo~oCo8mv4|t#8#Vzn@OmQm1|vOJ5t5{?_g%_ue<$P>DzycCC%P5zhuh`L4ysvF)`&Psr)W+& z^3|V;R=M5jsAgc`x62IKJc+4;NhK}SG|eJ4IYl=mH8ok+Bqh;M*U~uIMAzKR%*fOr z(ZI~mByF-C>k1wta|=TgQ%gfrb0f>ipI8gIOhRB0Jvo<61&Ou4ijj$t`D7iAU{)g&Z3Cmp3)wT26V1)c&C*PC zO;ZdkfjUeqbS(`HQ+1OQEfW)ylZ=y1Efaw{yb)HJZf@kr<6wzeSG;cWX91&nJ}yqy zgGW^)fJw~V)5S5Q;?~=1d%c(g1y~=jLCi@&V1h@!%k>M7X5nH_No~68wb@k2YJH_>7XILLQVB*S!EtxmO81|aNOW`^ZZ}ixK19pd)G6Q?`+QL zyW+2J?l<4OnP>mr<2ei;+=||B-xsQR`M>Yy19#U4vp8ImK5wx3UVNB?X7qy=V}>2f w7xLReZYfqc7A2+S#J%$~o_(!c5Dq-wGFShTYG#zIuL9*?Pgg&ebxsLQ0IgV*f&c&j delta 1254 zcmeyM)}=n7nUQH?i=mGP0|Nsi5W{G9Pu~CqBRxX{J!2CF1|tI_V=F@gD??)iLnA8# zGb>|DplZei%wRnreIO7Uah?rmO7mn!M$vj}kHY~XZU2{b1x3lcQFt_Aq18=SSF3AU zA`6{Xh(?tf?0m^R%S1&}udgZl;KAcbR}UVI3}+8_6sT#~vCBepmq+JwwN^1hIg2~} z|L6TLc(=3R!YaeFue_ON<;}iX;&nzm>}X?ni@-u21x4f2GbS(Lsd@Z*-YboFkM>Nj zzdK(ru)bT2nMtW|lH@YqJ&rcJGo|Nz?_9h_Y4wrkM<%QBdi*(2di1gRxzy>b_l&ci zZTfs9FlpnR!(RNGyH*OG7dRf`yZoQiz2vE%#nRVaOPFiF;4!o`G&3+THZZd=F_`?BwUEm=1QyYg^Vn37xSQv&SR|V1 zCYu_h=$aUsTk2X`m|Ex>r>2^urC6ky7^WFc7U1-TYcw&~oXVNU!LskqUCqhY1&!)? zx!5!W%}@IxLu{Q2ExDIxD6m{{%vXSr01JHQb2iDAN-2Qs~_+AaGaht&o9McjFTo?&D8CMVOz=nIia}U+zxr=Wj;E z&y0%G7>*apAfbQihcfMyBJ0@=q?2o+A1Qdmu@~?*tn%k@WT;YXxgL2^)px=jwV%By zivt*=e(|bUtIVF{l=rD!?WXvoU{JRY_yr$itl^qq;<+hb=-l*OlP1S_*WQ`3CHdDK)yUJL2^UrBv!?Hv zIx*&{f0~N#WE+Fpc)73lEu0!=KYQMNXnQeZg=F#{5vLo>7mRYhZ)RaAIC|vmA=V#! z4^AGq)3QPHWp>wkwfO~UjC!*x_7^b9wF}IXWcS|3_G4=gy%OKp!oe=&nZ{5KNv$jVHGka#HScP$$yXQXc5ig#6 z#iPu3(cBbs*P^u#4xXRg`PzGy%yXX&6Zpig9zIsJQ2p=(zP$avc9zO@>GAH&zM)a_ zEnMq6Q)cnbu!;ai2B}FAp}P(o^k>{K^{2+M9kZj|d?(En4EJ5@q`hF`y7${u=k5q@ z6;!&z7~pSg`{v~liQBDR1=0tOOCY}Dx{5$als5J6)^>bP0l+XkK D2MP_1 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