Add bag, line, and polygon tools

This commit is contained in:
MrRar 2024-02-19 20:02:24 -06:00
parent 403a24e860
commit 725d199191
16 changed files with 848 additions and 104 deletions

View File

@ -28,6 +28,8 @@ This mod was inspired by the Fill Start and Fill End blocks in Manic Digger.
| Circle | edit:circle | ![](textures/edit_circle.png) | | Circle | edit:circle | ![](textures/edit_circle.png) |
| Mirror | edit:mirror | ![](textures/edit_mirror.png) | | Mirror | edit:mirror | ![](textures/edit_mirror.png) |
| Screwdriver | edit:screwdriver | ![](textures/edit_screwdriver.png) | | Screwdriver | edit:screwdriver | ![](textures/edit_screwdriver.png) |
| Polygon | edit:polygon | ![](textures/edit_polygon.png) |
| Bag | edit:bag | ![](textures/edit_bag.png) |
## Dependencies ## Dependencies
@ -39,9 +41,7 @@ None
### Copy Tool ### Copy Tool
![figure.png](figure.png) When the copy tool is placed at opposite corners of an area, the area is copied. When the copy tool is placed for the first time, a marker entity is placed. To cancel the copy operation, punch the marker. When a copy tool is placed a second time, the selected area is copied and the markers are removed.
When the copy tool is placed at opposite corners of an area, they select the area as show in the figure. The copy tool uses the location under the placed position. When the copy tool is placed for the first time, a marker entity is placed. To cancel the copy operation, punch the entity marker. When a copy tool is placed a second time, the selected area is copied and the entity marker is removed.
### Paste Tool ### Paste Tool
@ -51,14 +51,14 @@ The paste tool is used for pasting the area copied by the copy tool or a schemat
### Fill Tool ### Fill Tool
The fill tool is used to fill a 3D area with a certain item. Start by placing the fill tool two times at opposite corners of the desired area. The selected area includes the positions of the fill markers themselves as shown in the figure. The fill tool is used to fill a 3D area with a certain item. Start by placing the fill tool two times at opposite corners of the desired area.
Once a second fill marker is placed, a dialog appears listing all items in the players inventory. A search field is also available to search all items. Clicking an item will cause it to be used used for filling the selected area. Clicking on a blank inventory slot will cause the selected area to be filled with air. To cancel the fill, press the "X". Once a second fill marker is placed, a dialog appears listing all items in the players inventory. A search field is also available to search all items. Clicking an item will cause it to be used used for filling the selected area. Clicking on a blank inventory slot will cause the selected area to be filled with air. To cancel the fill, press the "X".
### Replace Tool ### Replace Tool
The replace tool is used to replace certain nodes in a 3D area with a selected item. Start by placing the replace tool two times at opposite corners of the desired area. The selected area includes the positions of the replace markers themselves as shown in the figure. The replace tool is used to replace certain nodes in a 3D area with a selected item. Start by placing the replace tool two times at opposite corners of the desired area.
Once a second replace marker is placed, a dialog appears listing all node types in the selected area. Check the nodes that should be replaced and then press the "OK" button to proceed with the next step. Next a dialog will pop up showing all the items in the players inventory. A search field is also available to search all items. Clicking an item will cause it to be used used to replace the nodes that were checked earlier. Clicking on a blank inventory slot will cause the checked nodes to be replaced with air. To cancel the replace, press the "X". Once a second replace marker is placed, a dialog appears listing all node types in the selected area. Check the nodes that should be replaced and then press the "OK" button to proceed with the next step. Next a dialog will pop up showing all the items in the players inventory. A search field is also available to search all items. Clicking an item will cause it to be used used to replace the nodes that were checked earlier. Clicking on a blank inventory slot will cause the checked nodes to be replaced with air. To cancel the replace, press the "X".
@ -100,6 +100,16 @@ This tool is used to mirror the placement or digging of nodes. Place the tool to
This tool is used for rotating nodes that support rotation. Right clicking a node with the screwdriver rotates the node around the X or Z axis depending on the player's position. Left clicking a node with the screwdriver rotates the node clockwise around the Y axis. Param2 types `wallmounted`, `facedir`, and `degrotate` are supported. The node is rotated 90 degrees for all param2 types except `degrotate` where the node is rotated by either 1.5 or 15 degrees. If the aux1 key (E) is held while rotating a `degrotate` node, the rotation angle will be increased by 4x. This tool is used for rotating nodes that support rotation. Right clicking a node with the screwdriver rotates the node around the X or Z axis depending on the player's position. Left clicking a node with the screwdriver rotates the node clockwise around the Y axis. Param2 types `wallmounted`, `facedir`, and `degrotate` are supported. The node is rotated 90 degrees for all param2 types except `degrotate` where the node is rotated by either 1.5 or 15 degrees. If the aux1 key (E) is held while rotating a `degrotate` node, the rotation angle will be increased by 4x.
### Polygon Tool
This tool is used to create non-concave polygons in 3D space. Place the polygon tool to create markers. Each marker will create a triangle between itself, the last marker placed, and the first marker placed. The first marker placed will be green. To finish the polygon, place a marker on top of the green marker. After doing so, a dialog will appear to select a node or item to fill the polygon.
### Bag
The bag tool is used to place random items from a list of items. Dig (left click) with the bag to open the bag's inventory. Any item from the player inventory can be moved into the bag. The bag has 16 item slots. When placing the bag an item from the bag is randomly chosen to be placed. If a stack of several items is present in the bag, the item will be more likely to be placed than a single item. The probability of being placed is proportional to the item's count divided by the total count of items in the bag. For example, the probability of getting wood would be 75% for a bag with 3 wood and 1 dirt. Bags can be combined with other edit tools, for example, to fill an area with random kinds of dirt.
## Settings ## Settings
### edit_paste_preview_max_entities ### edit_paste_preview_max_entities
@ -114,10 +124,12 @@ The maximum volume of any edit operation. Increase to allow larger operations.
### edit_fast_node_fill_threshold ### edit_fast_node_fill_threshold
When the fill operation has a larger volume then the specified number, fast node fill will be used. When the fill operation has a larger volume then the specified number, fast node fill will be used. To disable fast node placement, set the threshold to be equal to the max operation volume. To disable slow node placement, set the threshold to 0. With fast node placement, callbacks are not called so some nodes might be broken.
To disable fast node placement, set the threshold to be equal to the max operation volume.
To disable slow node placement, set the threshold to 0.
With fast node placement, callbacks are not called so some nodes might be broken. ### edit_polygon_preview_wire_frame_threshold
If one side of the polygon preview is greater than this setting, a wire frame is used instead of the full preview. The full preview fills the entire polygon with preview entites. If the polygon gets big, the full preview will quickly crash a server. The wire frame preview outlines the triangle componants of the polygon. This setting only affects the preview. The polygon is always completely filled regardless of this setting.
## Privileges ## Privileges

118
bag.lua Normal file
View File

@ -0,0 +1,118 @@
local function get_item_list(itemstack)
local meta = itemstack:get_meta()
local str = meta:get("edit_bag")
local item_list = {}
if str then item_list = minetest.deserialize(str) end
for i, str in pairs(item_list) do
item_list[i] = ItemStack(str)
end
return item_list
end
local function put_item_list(itemstack, item_list)
local meta = itemstack:get_meta()
local str_list = {}
local description = ""
local description_len = 0
for i, item in pairs(item_list) do
str_list[i] = item:to_string()
if str_list[i] ~= "" then
if description_len < 3 then
description = description ..
"\n" .. item:get_count() ..
" " .. item:get_short_description()
description_len = description_len + 1
elseif description_len == 3 then
description = description .. "\n..."
description_len = description_len + 1
end
end
end
description = minetest.registered_items["edit:bag"].short_description ..
minetest.colorize("yellow", description)
local str = minetest.serialize(str_list)
meta:set_string("edit_bag", str)
meta:set_string("description", description)
end
local function on_inv_change(player)
local bag = player:get_wielded_item()
if bag:get_name() ~= "edit:bag" then return end
if not bag then return end
local name = player:get_player_name()
local inv_ref = minetest.get_inventory({type = "detached", name = "edit_bag_" .. name})
put_item_list(bag, inv_ref:get_list("main"))
player:set_wielded_item(bag)
end
minetest.register_on_joinplayer(function(player)
local inv_ref = minetest.create_detached_inventory("edit_bag_" .. player:get_player_name(), {
on_move = function(inv, from_list, from_index, to_list, to_index, count, player) on_inv_change(player) end,
on_put = function(inv, listname, index, stack, player) on_inv_change(player) end,
on_take = function(inv, listname, index, stack, player) on_inv_change(player) end,
})
inv_ref:set_size("main", 16)
end)
minetest.register_on_leaveplayer(function(player)
minetest.remove_detached_inventory("edit_bag_" .. player:get_player_name())
end)
local function on_place(itemstack, player, pointed_thing)
if pointed_thing.type ~= "node" then return end
local item_list = get_item_list(itemstack)
local total_count = 0
for i, item_stack in ipairs(item_list) do
total_count = total_count + item_stack:get_count()
end
local selected_index = math.round((total_count - 1) * math.random()) + 1
local selected_item
local current_index = 0
for i, item_stack in ipairs(item_list) do
local count = item_stack:get_count()
if count > 0 then
current_index = current_index + count
if current_index >= selected_index then
selected_item = item_stack
break
end
end
end
if selected_item then
local pos = edit.pointed_thing_to_pos(pointed_thing)
edit.place_item_like_player(player, {name = selected_item}, pos)
end
end
local function on_use(itemstack, user, pointed_thing)
local meta = itemstack:get_meta()
local str = meta:get("edit_bag")
local name = user:get_player_name()
local item_list = {}
if str then item_list = minetest.deserialize(str) end
for i, str in pairs(item_list) do
item_list[i] = ItemStack(str)
end
local inv_ref = minetest.get_inventory({type = "detached", name = "edit_bag_" .. name})
inv_ref:set_list("main", item_list)
local formspec = "formspec_version[4]size[10.2,10]" ..
"label[0.2,0.9;Bag contents:]" ..
"button_exit[9,0.2;1,1;quit;X]" ..
"list[detached:edit_bag_" .. name .. ";main;0.2,1.4;8,2;]" ..
"label[0.2,4.5;Inventory:]" ..
"list[current_player;main;0.2,5;8,4;]"
minetest.show_formspec(name, "edit:bag", formspec)
end
minetest.register_tool("edit:bag", {
description = "Edit Bag",
short_description = "Edit Bag",
tiles = {"edit_bag.png"},
inventory_image = "edit_bag.png",
range = 10,
on_place = on_place,
on_secondary_use = on_place,
on_use = on_use,
})

View File

@ -47,7 +47,7 @@ local function place_circle(player, pos, node)
offset1.y * factor.y, offset1.y * factor.y,
offset1.z * factor.z ) offset1.z * factor.z )
local pos1 = vector.add(center, offset1) local pos1 = vector.add(center, offset1)
edit.place_node_like_player(player, node, pos1) edit.place_item_like_player(player, node, pos1)
local offset2 = vector.new( local offset2 = vector.new(
offset1.z, offset1.z,
@ -55,7 +55,7 @@ local function place_circle(player, pos, node)
offset1.x offset1.x
) )
local pos2 = vector.add(center, offset2) local pos2 = vector.add(center, offset2)
edit.place_node_like_player(player, node, pos2) edit.place_item_like_player(player, node, pos2)
end end
end end

View File

@ -46,7 +46,7 @@ minetest.register_tool("edit:copy",{
tiles = {"edit_copy.png"}, tiles = {"edit_copy.png"},
inventory_image = "edit_copy.png", inventory_image = "edit_copy.png",
range = 10, range = 10,
groups = {edit_place_preview = 1,}, groups = {edit_place_preview = 1, edit_box_select_preview = 1},
on_place = copy_on_place, on_place = copy_on_place,
on_secondary_use = copy_on_place, on_secondary_use = copy_on_place,
_edit_get_selection_points = function(player) _edit_get_selection_points = function(player)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,5 +1,5 @@
local function player_select_node_formspec(player) local function player_select_item_formspec(player)
edit.player_select_node(player, "Select item to use for fill", function(player, name) edit.player_select_item(player, "Select item to use for fill", function(player, item_str)
local d = edit.player_data[player] local d = edit.player_data[player]
if if
@ -12,7 +12,9 @@ local function player_select_node_formspec(player)
d.fill1.object:remove() d.fill1.object:remove()
if not name then return end local item = ItemStack(item_str)
local name = item:get_name()
if name == "" then return end
local def = minetest.registered_items[name] local def = minetest.registered_items[name]
@ -63,13 +65,13 @@ local function player_select_node_formspec(player)
voxel_manip:write_to_map(true) voxel_manip:write_to_map(true)
voxel_manip:update_liquids() voxel_manip:update_liquids()
else else
local node = {name = name, param2 = param2} local node = {name = item_str, param2 = param2}
-- Work top to bottom so we can remove falling nodes -- Work top to bottom so we can remove falling nodes
for x = _end.x, start.x, -1 do for x = _end.x, start.x, -1 do
for y = _end.y, start.y, -1 do for y = _end.y, start.y, -1 do
for z = _end.z, start.z, -1 do for z = _end.z, start.z, -1 do
local pos = vector.new(x, y, z) local pos = vector.new(x, y, z)
edit.place_node_like_player(player, node, pos) edit.place_item_like_player(player, node, pos)
end end
end end
end end
@ -100,7 +102,7 @@ local function fill_on_place(itemstack, player, pointed_thing)
return return
end end
player_select_node_formspec(player) player_select_item_formspec(player)
elseif pos then elseif pos then
player_data.fill1 = edit.add_marker("edit:fill", pos, player) player_data.fill1 = edit.add_marker("edit:fill", pos, player)
end end
@ -111,7 +113,7 @@ minetest.register_tool("edit:fill", {
tiles = {"edit_fill.png"}, tiles = {"edit_fill.png"},
inventory_image = "edit_fill.png", inventory_image = "edit_fill.png",
range = 10, range = 10,
groups = {edit_place_preview = 1,}, groups = {edit_place_preview = 1, edit_box_select_preview = 1},
on_place = fill_on_place, on_place = fill_on_place,
on_secondary_use = fill_on_place, on_secondary_use = fill_on_place,
_edit_get_selection_points = function(player) _edit_get_selection_points = function(player)

View File

@ -4,6 +4,7 @@ edit.player_data = {}
edit.paste_preview_max_entities = tonumber(minetest.settings:get("edit_paste_preview_max_entities") or 2000) edit.paste_preview_max_entities = tonumber(minetest.settings:get("edit_paste_preview_max_entities") or 2000)
edit.max_operation_volume = tonumber(minetest.settings:get("edit_max_operation_volume") or 20000) edit.max_operation_volume = tonumber(minetest.settings:get("edit_max_operation_volume") or 20000)
edit.fast_node_fill_threshold = tonumber(minetest.settings:get("edit_fast_node_fill_threshold") or 2000) edit.fast_node_fill_threshold = tonumber(minetest.settings:get("edit_fast_node_fill_threshold") or 2000)
edit.polygon_preview_wire_frame_threshold = tonumber(minetest.settings:get("edit_polygon_preview_wire_frame_threshold") or 40)
minetest.register_privilege("edit", { minetest.register_privilege("edit", {
description = "Allows usage of edit mod nodes", description = "Allows usage of edit mod nodes",
@ -128,10 +129,13 @@ minetest.register_on_placenode = function(func)
return old_register_on_placenode(func) return old_register_on_placenode(func)
end end
function edit.place_node_like_player(player, node, pos) function edit.place_item_like_player(player, item, pos)
local node = table.copy(item)
local itemstack = ItemStack(item.name)
node.name = itemstack:get_name()
local def = minetest.registered_items[node.name] local def = minetest.registered_items[node.name]
if not def then return end
local is_node = minetest.registered_nodes[node.name] ~= nil local is_node = minetest.registered_nodes[node.name] ~= nil
local itemstack = ItemStack(node.name)
local pointed_thing = { local pointed_thing = {
type = "node", type = "node",
above = pos, above = pos,
@ -173,9 +177,9 @@ function edit.add_marker(id, pos, player)
return luaentity return luaentity
end end
local function player_select_node_formspec(player) local function player_select_item_formspec(player)
local d = edit.player_data[player] local d = edit.player_data[player]
local search_value = d.player_select_node_search_value local search_value = d.player_select_item_search_value
local doing_search = #search_value > 0 local doing_search = #search_value > 0
local inv = minetest.get_inventory({type = "player", name = player:get_player_name()}) local inv = minetest.get_inventory({type = "player", name = player:get_player_name()})
local size = doing_search and 12 * 8 or inv:get_size("main") local size = doing_search and 12 * 8 or inv:get_size("main")
@ -223,7 +227,7 @@ local function player_select_node_formspec(player)
local formspec = "formspec_version[4]size[" .. formspec_width .. "," .. formspec_height .. "]" .. local formspec = "formspec_version[4]size[" .. formspec_width .. "," .. formspec_height .. "]" ..
"button[0,0;0,0;minetest_sucks;" .. math.random() .. "]" .. -- Force Minetest to show this formspec "button[0,0;0,0;minetest_sucks;" .. math.random() .. "]" .. -- Force Minetest to show this formspec
"label[0.5,0.7;" .. d.player_select_node_message .. "]" .. "label[0.5,0.7;" .. d.player_select_item_message .. "]" ..
"button_exit[" .. formspec_width - 1.2 .. ",0.2;1,1;quit;X]" .. "button_exit[" .. formspec_width - 1.2 .. ",0.2;1,1;quit;X]" ..
"field_close_on_enter[search_field;false]" .. "field_close_on_enter[search_field;false]" ..
"label[0.2,1.7;Search all items]" .. "label[0.2,1.7;Search all items]" ..
@ -237,7 +241,7 @@ local function player_select_node_formspec(player)
if doing_search then if doing_search then
name = search_results[i] name = search_results[i]
else else
name = inv:get_stack("main", i):get_name() name = inv:get_stack("main", i):to_string()
end end
if not name then break end if not name then break end
@ -255,28 +259,28 @@ local function player_select_node_formspec(player)
name .. ";" .. name .. ";" ..
name .. ";]" name .. ";]"
end end
edit.reliable_show_formspec(player, "edit:player_select_node", formspec) edit.reliable_show_formspec(player, "edit:player_select_item", formspec)
end end
minetest.register_on_player_receive_fields(function(player, formname, fields) minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "edit:player_select_node" then return false end if formname ~= "edit:player_select_item" then return false end
local d = edit.player_data[player] local d = edit.player_data[player]
for key, val in pairs(fields) do for key, val in pairs(fields) do
if key:find(":") or key == "air" then if key:find(":") or key == "air" then
if d.player_select_node_callback then if d.player_select_item_callback then
d.player_select_node_callback(player, key) d.player_select_item_callback(player, key)
d.player_select_node_callback = nil d.player_select_item_callback = nil
minetest.close_formspec(player:get_player_name(), "edit:player_select_node") minetest.close_formspec(player:get_player_name(), "edit:player_select_item")
end end
return true return true
end end
end end
if fields.quit then if fields.quit then
if d.player_select_node_callback then if d.player_select_item_callback then
d.player_select_node_callback(player, nil) d.player_select_item_callback(player, nil)
d.player_select_node_callback = nil d.player_select_item_callback = nil
end end
return true return true
elseif fields.cancel_search then elseif fields.cancel_search then
@ -285,24 +289,24 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if if
fields.search_field and fields.search_field and
fields.search_field ~= d.player_select_node_search_value fields.search_field ~= d.player_select_item_search_value
then then
d.player_select_node_search_value = fields.search_field d.player_select_item_search_value = fields.search_field
player_select_node_formspec(player) player_select_item_formspec(player)
return true return true
end end
return true return true
end) end)
function edit.player_select_node(player, message, callback) function edit.player_select_item(player, message, callback)
local d = edit.player_data[player] local d = edit.player_data[player]
if d.player_select_node_callback then if d.player_select_item_callback then
d.player_select_node_callback(player, nil) d.player_select_item_callback(player, nil)
end end
d.player_select_node_callback = callback d.player_select_item_callback = callback
d.player_select_node_search_value = d.player_select_node_search_value or "" d.player_select_item_search_value = d.player_select_item_search_value or ""
d.player_select_node_message = message d.player_select_item_message = message
player_select_node_formspec(player) player_select_item_formspec(player)
end end
edit.modpath = minetest.get_modpath("edit") edit.modpath = minetest.get_modpath("edit")
@ -318,3 +322,6 @@ dofile(edit.modpath .. "/circle.lua")
dofile(edit.modpath .. "/mirror.lua") dofile(edit.modpath .. "/mirror.lua")
dofile(edit.modpath .. "/screwdriver.lua") dofile(edit.modpath .. "/screwdriver.lua")
dofile(edit.modpath .. "/replace.lua") dofile(edit.modpath .. "/replace.lua")
dofile(edit.modpath .. "/polygon.lua")
dofile(edit.modpath .. "/line.lua")
dofile(edit.modpath .. "/bag.lua")

190
line.lua Normal file
View File

@ -0,0 +1,190 @@
-- https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/
-- This Site is affiliated under CCBY-SA https://www.geeksforgeeks.org/legal/copyright-information/
-- JS code for generating points on a 3-D line
-- using Bresenham's Algorithm
-- Converted from the original to Lua
function edit.calculate_line_points(p1, p2)
p1 = vector.copy(p1)
p2 = vector.copy(p2)
local output = {vector.copy(p1)}
local d = vector.apply(vector.subtract(p1, p2), math.abs)
local s = vector.new(
p2.x > p1.x and 1 or -1,
p2.y > p1.y and 1 or -1,
p2.z > p1.z and 1 or -1
)
-- Driving axis is X-axis
if d.x >= d.y and d.x >= d.z then
local n1 = 2 * d.y - d.x
local n2 = 2 * d.z - d.x
while p1.x ~= p2.x do
p1.x = p1.x + s.x
if n1 >= 0 then
p1.y = p1.y + s.y
n1 = n1 - 2 * d.x
end
if n2 >= 0 then
p1.z = p1.z + s.z
n2 = n2 - 2 * d.x
end
n1 = n1 + 2 * d.y
n2 = n2 + 2 * d.z
table.insert(output, vector.copy(p1))
end
-- Driving axis is Y-axis
elseif d.y >= d.x and d.y >= d.z then
local n1 = 2 * d.x - d.y
local n2 = 2 * d.z - d.y
while p1.y ~= p2.y do
p1.y = p1.y + s.y
if n1 >= 0 then
p1.x = p1.x + s.x
n1 = n1 - 2 * d.y
end
if n2 >= 0 then
p1.z = p1.z + s.z
n2 = n2 - 2 * d.y
end
n1 = n1 + 2 * d.x
n2 = n2 + 2 * d.z
table.insert(output, vector.copy(p1))
end
-- Driving axis is Z-axis
else
local n1 = 2 * d.y - d.z
local n2 = 2 * d.x - d.z
while p1.z ~= p2.z do
p1.z = p1.z + s.z
if n1 >= 0 then
p1.y = p1.y + s.y
n1 = n1 - 2 * d.z
end
if n2 >= 0 then
p1.x = p1.x + s.x
n2 = n2 - 2 * d.z
end
n1 = n1 + 2 * d.y
n2 = n2 + 2 * d.x
table.insert(output, vector.copy(p1))
end
end
return output
end
minetest.register_entity("edit:line", {
initial_properties = {
visual = "cube",
visual_size = { x = 1.1, y = 1.1 },
physical = false,
collide_with_objects = false,
static_save = false,
use_texture_alpha = true,
glow = -1,
backface_culling = false,
hp_max = 1,
textures = {
"edit_line.png",
"edit_line.png",
"edit_line.png",
"edit_line.png",
"edit_line.png",
"edit_line.png",
},
},
on_deactivate = function(self)
local player_data = edit.player_data[self._placer]
self.remove_called = true
if player_data then
local line1 = player_data.line1
if line1 and not line1.remove_called then
line1.object:remove()
end
player_data.line1 = nil
local line2 = player_data.line2
if line2 and not line2.remove_called then
line2.object:remove()
end
player_data.line2 = nil
player_data.old_pointed_pos = nil
end
end,
})
local function place_line(player, item_name)
local player_data = edit.player_data[player]
if not player_data.line1 then return end
if not item_name then
player_data.line1.object:remove()
return
end
local pos1 = player_data.line1._pos
local pos2 = player_data.line2._pos
local size = vector.add(vector.apply(vector.subtract(pos1, pos2), math.abs), vector.new(1, 1, 1))
local pos = vector.new(
math.min(pos1.x, pos2.x),
math.min(pos1.y, pos2.y),
math.min(pos1.z, pos2.z)
)
player_data.undo_schematic = edit.schematic_from_map(pos, size)
local line_points = edit.calculate_line_points(pos1, pos2)
local item = {name = item_name}
for i, pos in pairs(line_points) do
edit.place_item_like_player(player, item, pos)
end
player_data.line1.object:remove()
end
local function line_on_place(itemstack, player, pointed_thing)
if not edit.on_place_checks(player) then return end
if not pointed_thing.above then
pointed_thing = edit.get_pointed_thing_node(player)
end
local pos = edit.pointed_thing_to_pos(pointed_thing)
if not pos then return end
local player_data = edit.player_data[player]
if not player_data.line1 then
player_data.line1 = edit.add_marker("edit:line", pos, player)
if not player_data.line1 then return end
else
player_data.line2 = edit.add_marker("edit:line", pos, player)
if not player_data.line2 then return end
local diff = vector.subtract(player_data.line1._pos, pos)
local volume = vector.add(vector.apply(diff, math.abs), 1)
if volume.x * volume.y * volume.z > edit.max_operation_volume then
edit.display_size_error(player)
player_data.line1.object:remove()
return
end
edit.player_select_item(player, "Select item to fill the line", place_line)
end
edit.old_pointed_pos = nil
end
minetest.register_tool("edit:line", {
description = "Edit Line",
tiles = {"edit_line.png"},
inventory_image = "edit_line.png",
range = 10,
groups = {edit_place_preview = 1,},
on_place = line_on_place,
on_secondary_use = line_on_place,
_edit_get_selection_points = function(player)
local d = edit.player_data[player]
return d.line1 and d.line1._pos, d.line2 and d.line2._pos
end
})

View File

@ -21,10 +21,10 @@ local function do_mirror(player, pos, node)
if d.mirror_mode == "x" then if d.mirror_mode == "x" then
offset.x = -offset.x offset.x = -offset.x
edit.place_node_like_player(player, node, vector.add(center, offset)) edit.place_item_like_player(player, node, vector.add(center, offset))
elseif d.mirror_mode == "z" then elseif d.mirror_mode == "z" then
offset.z = -offset.z offset.z = -offset.z
edit.place_node_like_player(player, node, vector.add(center, offset)) edit.place_item_like_player(player, node, vector.add(center, offset))
elseif d.mirror_mode == "xz" then elseif d.mirror_mode == "xz" then
for i = 1, 4 do for i = 1, 4 do
local axis = "x" local axis = "x"
@ -32,7 +32,7 @@ local function do_mirror(player, pos, node)
axis = "z" axis = "z"
end end
offset[axis] = -offset[axis] offset[axis] = -offset[axis]
edit.place_node_like_player(player, node, vector.add(center, offset)) edit.place_item_like_player(player, node, vector.add(center, offset))
end end
elseif d.mirror_mode == "eighths" then elseif d.mirror_mode == "eighths" then
for i = 1, 8 do for i = 1, 8 do
@ -44,7 +44,7 @@ local function do_mirror(player, pos, node)
offset = vector.new(offset.z, offset.y, offset.x) offset = vector.new(offset.z, offset.y, offset.x)
end end
offset[axis] = -offset[axis] offset[axis] = -offset[axis]
edit.place_node_like_player(player, node, vector.add(center, offset)) edit.place_item_like_player(player, node, vector.add(center, offset))
end end
end end

226
polygon.lua Normal file
View File

@ -0,0 +1,226 @@
-- https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation
-- Converted from C++ to Lua
-- License: CC BY-SA https://creativecommons.org/licenses/by-sa/4.0/
local function ray_intersects_triangle(ray_origin, ray_vector, vertex_a, vertex_b, vertex_c)
local epsilon = 0.0000001
local edge1 = vector.subtract(vertex_b, vertex_a)
local edge2 = vector.subtract(vertex_c, vertex_a)
local ray_cross_e2 = vector.cross(ray_vector, edge2)
local det = vector.dot(edge1, ray_cross_e2)
if det > -epsilon and det < epsilon then
return -- This ray is parallel to this triangle.
end
local inv_det = 1.0 / det
local s = vector.subtract(ray_origin, vertex_a)
local u = inv_det * vector.dot(s, ray_cross_e2)
if u < 0 or u > 1 then return end
local s_cross_e1 = vector.cross(s, edge1)
local v = inv_det * vector.dot(ray_vector, s_cross_e1)
if v < 0 or u + v > 1 then return end
-- At this stage we can compute t to find out where the intersection point is on the line.
local t = inv_det * vector.dot(edge2, s_cross_e1)
if t > epsilon then -- ray intersection
return vector.add(ray_origin, vector.multiply(ray_vector, t))
else -- This means that there is a line intersection but not a ray intersection.
return
end
end
function edit.calculate_triangle_points(a, b, c)
local bounding_box_min = vector.copy(a)
local bounding_box_max = vector.copy(a)
for index, axis in pairs({"x", "y", "z"}) do
bounding_box_min[axis] = math.min(a[axis], b[axis], c[axis])
bounding_box_max[axis] = math.max(a[axis], b[axis], c[axis])
end
-- Calculate normal
local u = vector.subtract(b, a)
local v = vector.subtract(c, a)
local normal = vector.new(
u.y * v.z - u.z * v.y,
u.z * v.x - u.x * v.z,
u.x * v.y - u.y * v.x
)
local selected_axis = "y"
local longest_length = 0
for axis, length in pairs(normal) do
length = math.abs(length)
if length > longest_length then
longest_length = length
selected_axis = axis
end
end
-- Switch from local to global coordinate system.
-- Also works the same to convert local to global coordinate system.
local function swap_coord_sys(v)
v = vector.copy(v)
local old_selected = v[selected_axis]
v[selected_axis] = v.y
v.y = old_selected
return v
end
local bounding_box_min_local = swap_coord_sys(bounding_box_min)
local bounding_box_max_local = swap_coord_sys(bounding_box_max)
local a_local = swap_coord_sys(a)
local b_local = swap_coord_sys(b)
local c_local = swap_coord_sys(c)
local results = {}
for x = bounding_box_min_local.x, bounding_box_max_local.x do
for z = bounding_box_min_local.z, bounding_box_max_local.z do
local intersection = ray_intersects_triangle(vector.new(x, 30928, z), vector.new(0, -1, 0), a_local, b_local, c_local)
if intersection then
table.insert(results, vector.round(swap_coord_sys(intersection)))
end
end
end
return results
end
local function place_polygon(player, item_name)
local player_data = edit.player_data[player]
if not player_data then return end
if not item_name or #player_data.polygon_markers < 2 then
player_data.polygon_markers.object:remove()
return
end
local markers = player_data.polygon_markers
local inf = 1 / 0
local bounding_box_min = vector.new(inf, inf, inf)
local bounding_box_max = vector.new(-inf, -inf, -inf)
for index, axis in pairs({"x", "y", "z"}) do
for i, marker in ipairs(markers) do
bounding_box_min[axis] = math.min(bounding_box_min[axis], marker._pos[axis])
bounding_box_max[axis] = math.max(bounding_box_max[axis], marker._pos[axis])
end
end
local volume = vector.add(vector.subtract(bounding_box_max, bounding_box_min), vector.new(1, 1, 1))
if volume.x * volume.y * volume.z > edit.max_operation_volume then
edit.display_size_error(player)
player_data.polygon_markers.object:remove()
return
end
player_data.undo_schematic = edit.schematic_from_map(bounding_box_min, volume)
local points = {}
for i = 3, #markers do
table.insert_all(
points,
edit.calculate_triangle_points(
markers[i]._pos,
markers[i - 1]._pos,
markers[1]._pos
)
)
end
local item = {name = item_name}
edit.place_item_like_player(player, item, markers[1]._pos)
item.param2 = minetest.get_node(markers[1]._pos).param2
for i, pos in pairs(points) do
edit.place_item_like_player(player, item, pos)
end
player_data.polygon_markers.object:remove()
end
minetest.register_entity("edit:polygon", {
initial_properties = {
visual = "cube",
visual_size = { x = 1.1, y = 1.1 },
physical = false,
collide_with_objects = false,
static_save = false,
use_texture_alpha = true,
glow = -1,
backface_culling = false,
hp_max = 1,
textures = {
"edit_polygon.png",
"edit_polygon.png",
"edit_polygon.png",
"edit_polygon.png",
"edit_polygon.png",
"edit_polygon.png",
},
},
on_deactivate = function(self)
local player_data = edit.player_data[self._placer]
if player_data then
local index = table.indexof(player_data.polygon_markers, self)
table.remove(player_data.polygon_markers, index)
local marker = player_data.polygon_markers[1]
if index == 1 and marker then
local textures = marker.object:get_properties().textures
for i, texture in pairs(textures) do
textures[i] = texture .. "^[multiply:green"
end
marker.object:set_properties({textures = textures})
end
end
player_data.old_pointed_pos = nil
end,
})
local function polygon_on_place(itemstack, player, pointed_thing)
if not edit.on_place_checks(player) then return end
if not pointed_thing.above then
pointed_thing = edit.get_pointed_thing_node(player)
end
local pos = edit.pointed_thing_to_pos(pointed_thing)
if not pos then return end
local player_data = edit.player_data[player]
if not player_data.polygon_markers then
player_data.polygon_markers = {}
player_data.polygon_markers.object = player_data.polygon_markers
player_data.polygon_markers.object.remove = function(self)
for i, luaentity in ipairs(table.copy(self)) do
luaentity.object:remove()
end
end
end
if player_data.polygon_markers[1] and vector.equals(player_data.polygon_markers[1]._pos, pos) then
edit.player_select_item(player, "Select item to fill the polygon", place_polygon)
return
end
local marker = edit.add_marker("edit:polygon", pos, player)
if not marker then return end
table.insert(player_data.polygon_markers, marker)
if marker == player_data.polygon_markers[1] then
local textures = marker.object:get_properties().textures
for i, texture in pairs(textures) do
textures[i] = texture .. "^[multiply:green"
end
marker.object:set_properties({textures = textures})
end
end
minetest.register_tool("edit:polygon", {
description = "Edit Polygon",
tiles = {"edit_polygon.png"},
inventory_image = "edit_polygon.png",
range = 10,
groups = {edit_place_preview = 1,},
on_place = polygon_on_place,
on_secondary_use = polygon_on_place,
})

View File

@ -103,6 +103,135 @@ local function create_paste_preview(player)
edit.rotate_paste_preview(player) edit.rotate_paste_preview(player)
end end
minetest.register_entity("edit:polygon_preview", {
initial_properties = {
visual = "cube",
physical = false,
pointable = false,
collide_with_objects = false,
static_save = false,
use_texture_alpha = true,
glow = -1,
backface_culling = false,
visual_size = { x = 1.05, y = 1.05 },
textures = {
"edit_select_preview.png^[sheet:8x8:1,1",
"edit_select_preview.png^[sheet:8x8:1,1",
"edit_select_preview.png^[sheet:8x8:1,1",
"edit_select_preview.png^[sheet:8x8:1,1",
"edit_select_preview.png^[sheet:8x8:1,1",
"edit_select_preview.png^[sheet:8x8:1,1",
},
}
})
local function hide_polygon_preview(player)
local player_data = edit.player_data[player]
local previews = player_data.polygon_previews
for i, obj_ref in ipairs(previews) do
obj_ref:set_properties({is_visible = false})
end
if player_data.polygon_preview_hud then
player:hud_remove(player_data.polygon_preview_hud)
player_data.polygon_preview_hud = nil
end
player_data.polygon_preview_shown = false
end
local function show_polygon_preview(player, show_polygon_hud)
local player_data = edit.player_data[player]
if not player_data.polygon_previews then
player_data.polygon_previews = {}
player_data.polygon_previews.object = player_data.polygon_previews
player_data.polygon_previews.object.remove = function(self)
for i, luaentity in ipairs(table.copy(self)) do
luaentity:remove()
end
end
end
for i, obj_ref in ipairs(player_data.polygon_previews) do
obj_ref:set_properties({is_visible = true})
end
player_data.polygon_preview_shown = true
end
local function update_polygon_preview(player, marker_pos_list, show_polygon_hud)
local player_pos = player:get_pos()
local player_data = edit.player_data[player]
local show_full_preview = true
local bounding_box_min = vector.copy(marker_pos_list[1])
local bounding_box_max = vector.copy(marker_pos_list[1])
for index, axis in pairs({"x", "y", "z"}) do
for i, pos in ipairs(marker_pos_list) do
bounding_box_min[axis] = math.min(bounding_box_min[axis], pos[axis])
bounding_box_max[axis] = math.max(bounding_box_max[axis], pos[axis])
end
if
bounding_box_max[axis] - bounding_box_min[axis] + 1 >
edit.polygon_preview_wire_frame_threshold
then
show_full_preview = false
end
end
local pos_list = {}
local volume = vector.add(vector.subtract(bounding_box_max, bounding_box_min), vector.new(1, 1, 1))
if volume.x * volume.y * volume.z <= edit.max_operation_volume then
if #marker_pos_list == 2 or not show_full_preview then
table.insert_all(
pos_list,
edit.calculate_line_points(marker_pos_list[1], marker_pos_list[2])
)
end
for i = 3, #marker_pos_list do
if show_full_preview then
table.insert_all(
pos_list,
edit.calculate_triangle_points(
marker_pos_list[i],
marker_pos_list[i - 1],
marker_pos_list[1]
)
)
else
table.insert_all(
pos_list,
edit.calculate_line_points(marker_pos_list[i], marker_pos_list[i - 1])
)
table.insert_all(
pos_list,
edit.calculate_line_points(marker_pos_list[i], marker_pos_list[1])
)
end
end
end
local preview_objs = player_data.polygon_previews
if #preview_objs > #pos_list then
for i = #pos_list + 1, #preview_objs do
preview_objs[#pos_list + 1]:remove()
table.remove(preview_objs, #pos_list + 1)
end
elseif #preview_objs < #pos_list then
for i = #preview_objs + 1, #pos_list do
local obj_ref = minetest.add_entity(player_pos, "edit:polygon_preview")
table.insert(preview_objs, obj_ref)
end
end
for i, pos in pairs(pos_list) do
preview_objs[i]:set_pos(pos)
end
end
minetest.register_entity("edit:select_preview", { minetest.register_entity("edit:select_preview", {
initial_properties = { initial_properties = {
visual = "cube", visual = "cube",
@ -289,6 +418,36 @@ local function set_schematic_offset(player)
d.schematic_offset = offset d.schematic_offset = offset
end end
local function show_place_preview(player, pos, item)
local d = edit.player_data[player]
if not d.place_preview or not d.place_preview.object:get_pos() then
local obj_ref = minetest.add_entity(player:get_pos(), "edit:place_preview")
if not obj_ref then return end
d.place_preview = obj_ref:get_luaentity()
d.place_preview_shown = true
d.place_preview_item = nil
elseif not d.place_preview_shown then
d.place_preview.object:set_properties({ is_visible = true })
d.place_preview.object:set_detach()
d.place_preview_shown = true
end
if not vector.equals(d.place_preview.object:get_pos(), pos) then
d.place_preview.object:set_pos(pos)
end
if d.place_preview_item ~= item then
local tex = minetest.registered_items[item].tiles[1] ..
"^[opacity:150"
d.place_preview_item = item
d.place_preview.object:set_properties({
textures = { tex, tex, tex, tex, tex, tex }
})
end
end
minetest.register_globalstep(function(dtime) minetest.register_globalstep(function(dtime)
for _, player in pairs(minetest.get_connected_players()) do for _, player in pairs(minetest.get_connected_players()) do
local item = player:get_wielded_item():get_name() local item = player:get_wielded_item():get_name()
@ -314,70 +473,96 @@ minetest.register_globalstep(function(dtime)
elseif d.paste_preview_hud then hide_paste_preview(player) end elseif d.paste_preview_hud then hide_paste_preview(player) end
elseif d.paste_preview_hud then hide_paste_preview(player) end elseif d.paste_preview_hud then hide_paste_preview(player) end
-- Select preview -- Stuff for Place preview and box select preview
local node1_pos local marker1_pos
local node2_pos local marker2_pos
local pointed_pos local should_show_place_preview = minetest.get_item_group(item, "edit_place_preview") ~= 0
local tool_def = minetest.registered_items[item] or minetest.registered_items["air"] local should_use_box_select_preview = minetest.get_item_group(item, "edit_box_select_preview") ~= 0
if should_show_place_preview or should_use_box_select_preview then
local tool_def = minetest.registered_items[item]
if tool_def._edit_get_selection_points then if tool_def._edit_get_selection_points then
node1_pos, node2_pos = tool_def._edit_get_selection_points(player) marker1_pos, marker2_pos = tool_def._edit_get_selection_points(player)
end end
if not marker2_pos then
if not node2_pos or not node1_pos then
if tool_def._edit_get_pointed_pos then if tool_def._edit_get_pointed_pos then
pointed_pos = tool_def._edit_get_pointed_pos(player) marker2_pos = tool_def._edit_get_pointed_pos(player)
else else
local pointed_thing = edit.get_pointed_thing_node(player) local pointed_thing = edit.get_pointed_thing_node(player)
pointed_pos = edit.pointed_thing_to_pos(pointed_thing) marker2_pos = edit.pointed_thing_to_pos(pointed_thing)
end end
else should_show_place_preview = false end
end end
if minetest.get_item_group(item, "edit_place_preview") ~= 0 and not node2_pos and pointed_pos then -- Box select preview
if not d.place_preview or not d.place_preview.object:get_pos() then if should_use_box_select_preview and marker1_pos and marker2_pos then
local obj_ref = minetest.add_entity(player:get_pos(), "edit:place_preview") local diff = vector.subtract(marker1_pos, marker2_pos)
if not obj_ref then return end local size = vector.apply(diff, math.abs)
d.place_preview = obj_ref:get_luaentity() size = vector.add(size, vector.new(1, 1, 1))
d.place_preview_shown = true local size_too_big = size.x * size.y * size.z > edit.max_operation_volume
d.place_preview_item = nil if not size_too_big then
elseif not d.place_preview_shown then local preview_pos = vector.add(marker2_pos, vector.multiply(diff, 0.5))
d.place_preview.object:set_properties({ is_visible = true }) update_select_preview(player, preview_pos, size)
d.place_preview.object:set_detach() elseif d.select_preview_shown then hide_select_preview(player) end
d.place_preview_shown = true elseif d.select_preview_shown then hide_select_preview(player) end
end
if not vector.equals(d.place_preview.object:get_pos(), pointed_pos) then -- Place preview
d.place_preview.object:set_pos(pointed_pos) if should_show_place_preview and marker2_pos then
end show_place_preview(player, marker2_pos, item)
if d.place_preview_item ~= item then
local tex = minetest.registered_items[item].tiles[1] ..
"^[opacity:150"
d.place_preview_item = item
d.place_preview.object:set_properties({
textures = { tex, tex, tex, tex, tex, tex }
})
end
elseif d.place_preview_shown then elseif d.place_preview_shown then
d.place_preview.object:set_properties({ is_visible = false }) d.place_preview.object:set_properties({ is_visible = false })
d.place_preview.object:set_attach(player) d.place_preview.object:set_attach(player)
d.place_preview_shown = false d.place_preview_shown = false
end end
if not node2_pos then -- Polygon preview
node2_pos = pointed_pos if item == "edit:polygon" or item == "edit:line" then
if marker2_pos then
if not d.polygon_preview_shown then
show_polygon_preview(player)
end end
if node1_pos and node2_pos then if not d.old_pointed_pos then d.old_pointed_pos = vector.new(0.5, 0.5, 0.5) end
local diff = vector.subtract(node1_pos, node2_pos)
local size = vector.apply(diff, math.abs) if
size = vector.add(size, vector.new(1, 1, 1)) d.old_polygon_item ~= item or
local size_too_big = size.x * size.y * size.z > edit.max_operation_volume not vector.equals(d.old_pointed_pos, marker2_pos)
if not size_too_big then then
local preview_pos = vector.add(node2_pos, vector.multiply(diff, 0.5)) if item == "edit:polygon" then
update_select_preview(player, preview_pos, size) local markers = d.polygon_markers or {}
elseif d.select_preview_shown then hide_select_preview(player) end local marker_pos_list = {}
elseif d.select_preview_shown then hide_select_preview(player) end for i, marker in ipairs(markers) do
table.insert(marker_pos_list, marker._pos)
end
table.insert(marker_pos_list, marker2_pos)
update_polygon_preview(player, marker_pos_list)
else
local marker_pos_list = {}
if marker1_pos then table.insert(marker_pos_list, marker1_pos) end
table.insert(marker_pos_list, marker2_pos)
update_polygon_preview(player, marker_pos_list)
end
d.old_pointed_pos = marker2_pos
d.old_polygon_item = item
end
end
elseif d.polygon_preview_shown then
hide_polygon_preview(player)
end
if item == "edit:polygon" then
if not d.polygon_preview_hud then
d.polygon_preview_hud = player:hud_add({
hud_elem_type = "text",
text = "Finish the polygon by placing a marker on the green marker",
position = {x = 0.5, y = 0.8},
z_index = 100,
number = 0xffffff
})
end
elseif d.polygon_preview_hud then
player:hud_remove(d.polygon_preview_hud)
d.polygon_preview_hud = nil
end
end end
end) end)

View File

@ -102,7 +102,7 @@ minetest.register_tool("edit:replace", {
tiles = {"edit_replace.png"}, tiles = {"edit_replace.png"},
inventory_image = "edit_replace.png", inventory_image = "edit_replace.png",
range = 10, range = 10,
groups = {edit_place_preview = 1,}, groups = {edit_place_preview = 1, edit_box_select_preview = 1},
on_place = replace_on_place, on_place = replace_on_place,
on_secondary_use = replace_on_place, on_secondary_use = replace_on_place,
_edit_get_selection_points = function(player) _edit_get_selection_points = function(player)
@ -187,7 +187,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if not fields.continue then return true end if not fields.continue then return true end
edit.player_select_node(player, "Select item to replace nodes", function(player, name) edit.player_select_item(player, "Select item to replace nodes", function(player, name)
if if
not d.replace1 or not d.replace2 or not d.replace1 or not d.replace2 or
not d.replace_source_nodes or not d.replace_source_nodes or
@ -233,7 +233,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if is_node then if is_node then
minetest.swap_node(pos, node) minetest.swap_node(pos, node)
else else
edit.place_node_like_player(player, node, pos) edit.place_item_like_player(player, node, pos)
end end
end end
end end

View File

@ -10,3 +10,7 @@ edit_max_operation_volume (Max edit operation volume) int 20000
# To disable slow node placement, set the threshold to 0. # To disable slow node placement, set the threshold to 0.
# With fast node placement, callbacks are not called so some nodes might be broken. # With fast node placement, callbacks are not called so some nodes might be broken.
edit_fast_node_fill_threshold (Fast node fill threshold volume) int 2000 edit_fast_node_fill_threshold (Fast node fill threshold volume) int 2000
# If one side of the polygon preview is greater than this setting,
# a wire frame is used instead of the full preview.
edit_polygon_preview_wire_frame_threshold (Polygon preview wire frame threshold) int 40

BIN
textures/edit_bag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

BIN
textures/edit_line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

BIN
textures/edit_polygon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B