diff --git a/init.lua b/init.lua index d1b9c31..ca62147 100644 --- a/init.lua +++ b/init.lua @@ -11,36 +11,352 @@ wesh = { } wesh.models_path = wesh.mod_path .. "/models/" - -minetest.register_privilege("wesh_capture", { - description = "Can use wesh canvases to capture new meshes", - give_to_singleplayer = true, -}) - -minetest.register_privilege("wesh_place", { - description = "Can place nodes created from wesh captures", - give_to_singleplayer = true, -}) - -minetest.register_privilege("wesh_delete", { - description = "Can delete captured meshes", - give_to_singleplayer = true, -}) - -minetest.register_privilege("wesh_import", { - description = "Can import matrix files", - give_to_singleplayer = true, -}) - -minetest.register_privilege("wesh_vacuum", { - description = "Can disintegrate all blocks in the canvas space", - give_to_singleplayer = true, -}) - local smartfs = dofile(wesh.mod_path .. "/smartfs.lua") - local storage = dofile(wesh.mod_path .. "/storage.lua") +-- ======================================================================== +-- initialization functions +-- ======================================================================== + +function wesh.init() + wesh.temp_path = minetest.get_worldpath() .. "/mod_storage/" .. wesh.temp_foldername .. "/" + wesh.gen_prefix = "mesh_" + + if not minetest.mkdir(wesh.temp_path) then + error("[wesh] Unable to create folder " .. wesh.temp_path) + end + + wesh.init_colors() + wesh.init_geometry() + wesh.init_privileges() + wesh.init_transforms() + wesh.init_variants() + wesh.init_vertex_textures() + wesh.register_canvas_nodes() + + wesh.delete_marked_objs() + wesh.move_temp_files() + wesh.load_mod_meshes() +end + +function wesh.init_colors() + wesh.colors = { + "violet", + "white", + "yellow", + "air", + "magenta", + "orange", + "pink", + "red", + "dark_green", + "dark_grey", + "green", + "grey", + "black", + "blue", + "brown", + "cyan", + } + + -- The following loop populates the color_vertices table with data like this... + -- + -- wesh.color_vertices = { + -- violet = { 1, 2, 3, 4 }, + -- white = { 5, 6, 7, 8 }, + -- + -- ...and so forth, in a boring sequence. + -- + -- Such indices will refer to the vt sequence generated by _init_vertex_textures() + -- The same loop will also fill the nodename_to_color table with default fallbacks for wool + + wesh.nodename_to_color = {} + wesh.color_vertices = {} + for i, color in ipairs(wesh.colors) do + local t = {} + local j = (i - 1) * 4 + 1 + for k = j, j + 3 do + table.insert(t, k) + end + wesh.color_vertices[color] = t + if color ~= "air" then + wesh.nodename_to_color["wool:" .. color] = color + end + end + + local colors_filename = "nodecolors.conf" + local default_colors_filename = "default." .. colors_filename + local full_colors_filename = wesh.mod_path .. "/" .. colors_filename + local full_default_colors_filename = wesh.mod_path .. "/" .. default_colors_filename + + local file = io.open(full_colors_filename, "rb") + if not file then + minetest.debug("[wesh] Copying " .. default_colors_filename .. " to " .. colors_filename) + local success, err = wesh.copy_file(full_default_colors_filename, full_colors_filename) + if not success then + minetest.debug("[wesh] " .. err) + return + end + file = io.open(full_colors_filename, "rb") + if not file then + minetest.debug("[wesh] Unable to load " .. colors_filename .. " file from mod folder") + return + end + end + + -- The following loop will fill the nodename_to_color table with custom values + local content = file:read("*all") + local lines = content:gsub("(\r\n)+", "\r"):gsub("\r+", "\n"):split("\n") + for _, line in ipairs(lines) do + local parts = line:gsub("%s+", ""):split("=") + if #parts == 2 then + wesh.nodename_to_color[parts[1]] = parts[2] + end + end + file:close() + +end + +function wesh.init_geometry() + + -- helper table to build the six faces + wesh.cube_vertices = { + { x = 1, y = -1, z = -1 }, -- 1 + { x = -1, y = -1, z = -1 }, -- 2 + { x = -1, y = -1, z = 1 }, -- 3 + { x = 1, y = -1, z = 1 }, -- 4 + { x = 1, y = 1, z = -1 }, -- 5 + { x = 1, y = 1, z = 1 }, -- 6 + { x = -1, y = 1, z = 1 }, -- 7 + { x = -1, y = 1, z = -1 }, -- 8 + } + + -- vertices refer to the above cube_vertices table + wesh.face_construction = { + bottom = { vertices = { 4, 3, 2, 1 }, normal = 1 }, + top = { vertices = { 8, 7, 6, 5 }, normal = 2 }, + back = { vertices = { 2, 8, 5, 1 }, normal = 3 }, + front = { vertices = { 4, 6, 7, 3 }, normal = 4 }, + left = { vertices = { 6, 4, 1, 5 }, normal = 5 }, + right = { vertices = { 3, 7, 8, 2 }, normal = 6 }, + } + + wesh.face_normals = { + {x = 0, y = -1, z = 0 }, + {x = 0, y = 1, z = 0 }, + {x = 0, y = 0, z = -1 }, + {x = 0, y = 0, z = 1 }, + {x = -1, y = 0, z = 0 }, + {x = 1, y = 0, z = 0 }, + } +end + +function wesh.init_privileges() + + minetest.register_privilege("wesh_capture", { + description = "Can use wesh canvases to capture new meshes", + give_to_singleplayer = true, + }) + + minetest.register_privilege("wesh_place", { + description = "Can place nodes created from wesh captures", + give_to_singleplayer = true, + }) + + minetest.register_privilege("wesh_delete", { + description = "Can delete captured meshes", + give_to_singleplayer = true, + }) + + minetest.register_privilege("wesh_import", { + description = "Can import matrix files", + give_to_singleplayer = true, + }) + + minetest.register_privilege("wesh_vacuum", { + description = "Can disintegrate all blocks in the canvas space", + give_to_singleplayer = true, + }) + +end + +function wesh.init_transforms() + local rot = {} + local dir = {} + + -- no rotation + rot[0] = {{ 1, 0, 0}, + { 0, 1, 0}, + { 0, 0, 1}} + -- 90 degrees clockwise + rot[1] = {{ 0, 0, 1}, + { 0, 1, 0}, + { -1, 0, 0}} + -- 180 degrees + rot[2] = {{ -1, 0, 0}, + { 0, 1, 0}, + { 0, 0, -1}} + -- 270 degrees clockwise + rot[3] = {{ 0, 0, -1}, + { 0, 1, 0}, + { 1, 0, 0}} + + -- directions + -- Y+ + dir[0] = {{ 1, 0, 0}, + { 0, 1, 0}, + { 0, 0, 1}} + -- Z+ + dir[1] = {{ 1, 0, 0}, + { 0, 0, -1}, + { 0, 1, 0}} + -- Z- + dir[2] = {{ 1, 0, 0}, + { 0, 0, 1}, + { 0, -1, 0}} + -- X+ + dir[3] = {{ 0, 1, 0}, + { -1, 0, 0}, + { 0, 0, 1}} + -- X- + dir[4] = {{ 0, -1, 0}, + { 1, 0, 0}, + { 0, 0, 1}} + -- Y- + dir[5] = {{ -1, 0, 0}, + { 0, -1, 0}, + { 0, 0, 1}} + + wesh.facedir_transform = {} + + for facedir = 0, 23 do + local direction = math.floor(facedir / 4) + local rotation = facedir % 4 + wesh.facedir_transform[facedir] = wesh.matrix_multiply(dir[direction], rot[rotation]) + end +end + +function wesh.init_variants() + local variants_filename = "nodevariants.lua" + local default_variants_filename = "default." .. variants_filename + local full_variants_filename = wesh.mod_path .. "/" .. variants_filename + local full_default_variants_filename = wesh.mod_path .. "/" .. default_variants_filename + + local file = io.open(full_variants_filename, "rb") + if not file then + minetest.debug("[wesh] Copying " .. default_variants_filename .. " to " .. variants_filename) + local success, err = wesh.copy_file(full_default_variants_filename, full_variants_filename) + if not success then + minetest.debug("[wesh] " .. err) + return + end + file = io.open(full_variants_filename, "rb") + if not file then + minetest.debug("[wesh] Unable to load " .. variants_filename .. " file from mod folder") + return + end + end + + local custom_variants = minetest.deserialize(file:read("*all")) + wesh.variants = { + plain = "plain-16.png", + } + + -- ensure there is at least one valid variant in the custom variants + if custom_variants and type(custom_variants) == "table" then + for name, texture in pairs(custom_variants) do + if name and type(name) == "string" and texture and type(texture) == "string" then + wesh.variants = custom_variants + break + end + end + end + file:close() +end + +function wesh.init_vertex_textures() + -- creates a 4x4 grid of UV mappings, each with a margin of one pixel + -- will be used by the .OBJ file generator + local vt = {} + local space = wesh.vt_size / 4 + local tile = space - 2 + local offset = tile / 2 + local start = offset + 1 + local stop = start + 3 * space + local mult = 1 / wesh.vt_size + for y = start, stop, space do + for x = start, stop, space do + table.insert(vt, "vt " .. ((x + offset) * mult) .. " " .. ((y + offset) * mult) .. "\n") -- top right + table.insert(vt, "vt " .. ((x + offset) * mult) .. " " .. ((y - offset) * mult) .. "\n") -- bottom right + table.insert(vt, "vt " .. ((x - offset) * mult) .. " " .. ((y - offset) * mult) .. "\n") -- bottom left + table.insert(vt, "vt " .. ((x - offset) * mult) .. " " .. ((y + offset) * mult) .. "\n") -- top left + end + end + wesh.vertex_textures = table.concat(vt) +end + +function wesh.register_canvas_nodes() + + local function register_canvas(index, size, inner) + minetest.register_craft({ + output = "wesh:canvas" .. size, + recipe = { + {"group:wool", "group:wool", "group:wool"}, + {"group:wool", inner, "group:wool"}, + {"group:wool", "group:wool", "group:wool"}, + } + }) + minetest.register_node("wesh:canvas" .. size, { + drawtype = "mesh", + mesh = "zzz_canvas" .. size .. ".obj", + inventory_image = "canvas_inventory.png^[verticalframe:6:" .. (index-1) .. ".png", + tiles = { "canvas.png" }, + paramtype2 = "facedir", + on_rightclick = wesh.canvas_interaction, + description = "Woolen Mesh Canvas - Size " .. size, + walkable = true, + groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 3 }, + }) + end + + local canvas_sizes = { + {"02", "default:steel_ingot"}, + {"04", "default:copper_ingot"}, + {"08", "default:tin_ingot"}, + {"16", "default:bronze_ingot"}, + {"32", "default:gold_ingot"}, + {"64", "default:diamond"}, + } + + wesh.valid_canvas_sizes = {} + + for index, canvas_data in pairs(canvas_sizes) do + local size = canvas_data[1] + local inner = canvas_data[2] + wesh.valid_canvas_sizes[tonumber(size)] = true + register_canvas(index, size, inner) + end + + minetest.register_alias("wesh:canvas", "wesh:canvas16") +end + +function wesh.reset_geometry(canv_size) + wesh.matrix = {} + wesh.vertices = {} + wesh.vertices_indices = {} + wesh.faces = {} + local function reset(p) + if not wesh.matrix[p.x] then wesh.matrix[p.x] = {} end + if not wesh.matrix[p.x][p.y] then wesh.matrix[p.x][p.y] = {} end + wesh.matrix[p.x][p.y][p.z] = "air" + end + wesh.traverse_matrix(reset, canv_size) +end + +-- ======================================================================== +-- forms handling +-- ======================================================================== + wesh.forms.capture = smartfs.create("wesh.forms.capture", function(state) state:size(7, 7) @@ -213,22 +529,23 @@ wesh.forms.giveme_meshes = smartfs.create("wesh.forms.giveme_meshes", function(s stored_variants:addItem(wesh.create_nodename(obj_filename, variant)) end end - stored_variants:onDoubleClick(wesh.give_mesh_callback) local give_button = state:button(0.5, 7.2, 3, 1, "give", "Giveme selected") - give_button:onClick(wesh.give_mesh_callback) local done_button = state:button(4, 7.2, 2, 1, "done", "Done") done_button:setClose(true) -end) + + local function give_mesh_callback(_, state) + local nodename = state:get("stored_variants"):getSelectedItem() + if not nodename then return end + local player_inv = minetest.get_player_by_name(state.player):get_inventory() + player_inv:add_item("main", {name = nodename, count = 1}) + wesh.notify(state.player, nodename .. " added to inventory") + end + stored_variants:onDoubleClick(give_mesh_callback) + give_button:onClick(give_mesh_callback) -function wesh.give_mesh_callback(_, state) - local nodename = state:get("stored_variants"):getSelectedItem() - if not nodename then return end - local player_inv = minetest.get_player_by_name(state.player):get_inventory() - player_inv:add_item("main", {name = nodename, count = 1}) - wesh.notify(state.player, nodename .. " added to inventory") -end +end) wesh.forms.import_matrix = smartfs.create("wesh.forms.import_matrix", function(state) state:size(8, 8) @@ -286,341 +603,6 @@ wesh.forms.vacuum_canvas = smartfs.create("wesh.forms.vacuum_canvas", function(s cancel_button:setClose(true) end) - --- ======================================================================== --- initialization functions --- ======================================================================== - -function wesh._init() - wesh.temp_path = minetest.get_worldpath() .. "/mod_storage/" .. wesh.temp_foldername .. "/" - wesh.gen_prefix = "mesh_" - - if not minetest.mkdir(wesh.temp_path) then - error("[wesh] Unable to create folder " .. wesh.temp_path) - end - wesh._init_vertex_textures() - wesh._init_colors() - wesh._init_geometry() - wesh._init_variants() - wesh._init_transforms() - wesh._delete_marked_objs() - wesh._move_temp_files() - wesh._load_mod_meshes() - wesh._register_canvas_nodes() -end - -function wesh._register_canvas_nodes() - - local function register_canvas(index, size, inner) - minetest.register_craft({ - output = "wesh:canvas" .. size, - recipe = { - {"group:wool", "group:wool", "group:wool"}, - {"group:wool", inner, "group:wool"}, - {"group:wool", "group:wool", "group:wool"}, - } - }) - minetest.register_node("wesh:canvas" .. size, { - drawtype = "mesh", - mesh = "zzz_canvas" .. size .. ".obj", - inventory_image = "canvas_inventory.png^[verticalframe:6:" .. (index-1) .. ".png", - tiles = { "canvas.png" }, - paramtype2 = "facedir", - on_rightclick = wesh.canvas_interaction, - description = "Woolen Mesh Canvas - Size " .. size, - walkable = true, - groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 3 }, - }) - end - - local canvas_sizes = { - {"02", "default:steel_ingot"}, - {"04", "default:copper_ingot"}, - {"08", "default:tin_ingot"}, - {"16", "default:bronze_ingot"}, - {"32", "default:gold_ingot"}, - {"64", "default:diamond"}, - } - - wesh.valid_canvas_sizes = {} - - for index, canvas_data in pairs(canvas_sizes) do - local size = canvas_data[1] - local inner = canvas_data[2] - wesh.valid_canvas_sizes[tonumber(size)] = true - register_canvas(index, size, inner) - end - - minetest.register_alias("wesh:canvas", "wesh:canvas16") -end - -function wesh._init_vertex_textures() - -- creates a 4x4 grid of UV mappings, each with a margin of one pixel - -- will be used by the .OBJ file generator - local vt = {} - local space = wesh.vt_size / 4 - local tile = space - 2 - local offset = tile / 2 - local start = offset + 1 - local stop = start + 3 * space - local mult = 1 / wesh.vt_size - for y = start, stop, space do - for x = start, stop, space do - table.insert(vt, "vt " .. ((x + offset) * mult) .. " " .. ((y + offset) * mult) .. "\n") -- top right - table.insert(vt, "vt " .. ((x + offset) * mult) .. " " .. ((y - offset) * mult) .. "\n") -- bottom right - table.insert(vt, "vt " .. ((x - offset) * mult) .. " " .. ((y - offset) * mult) .. "\n") -- bottom left - table.insert(vt, "vt " .. ((x - offset) * mult) .. " " .. ((y + offset) * mult) .. "\n") -- top left - end - end - wesh.vertex_textures = table.concat(vt) -end - -function wesh._init_colors() - wesh.colors = { - "violet", - "white", - "yellow", - "air", - "magenta", - "orange", - "pink", - "red", - "dark_green", - "dark_grey", - "green", - "grey", - "black", - "blue", - "brown", - "cyan", - } - - -- The following loop populates the color_vertices table with data like this... - -- - -- wesh.color_vertices = { - -- violet = { 1, 2, 3, 4 }, - -- white = { 5, 6, 7, 8 }, - -- - -- ...and so forth, in a boring sequence. - -- - -- Such indices will refer to the vt sequence generated by _init_vertex_textures() - -- The same loop will also fill the nodename_to_color table with default fallbacks for wool - - wesh.nodename_to_color = {} - wesh.color_vertices = {} - for i, color in ipairs(wesh.colors) do - local t = {} - local j = (i - 1) * 4 + 1 - for k = j, j + 3 do - table.insert(t, k) - end - wesh.color_vertices[color] = t - if color ~= "air" then - wesh.nodename_to_color["wool:" .. color] = color - end - end - - local colors_filename = "nodecolors.conf" - local default_colors_filename = "default." .. colors_filename - local full_colors_filename = wesh.mod_path .. "/" .. colors_filename - local full_default_colors_filename = wesh.mod_path .. "/" .. default_colors_filename - - local file = io.open(full_colors_filename, "rb") - if not file then - minetest.debug("[wesh] Copying " .. default_colors_filename .. " to " .. colors_filename) - local success, err = wesh.copy_file(full_default_colors_filename, full_colors_filename) - if not success then - minetest.debug("[wesh] " .. err) - return - end - file = io.open(full_colors_filename, "rb") - if not file then - minetest.debug("[wesh] Unable to load " .. colors_filename .. " file from mod folder") - return - end - end - - -- The following loop will fill the nodename_to_color table with custom values - local content = file:read("*all") - local lines = content:gsub("(\r\n)+", "\r"):gsub("\r+", "\n"):split("\n") - for _, line in ipairs(lines) do - local parts = line:gsub("%s+", ""):split("=") - if #parts == 2 then - wesh.nodename_to_color[parts[1]] = parts[2] - end - end - file:close() - -end - -function wesh._init_geometry() - - -- helper table to build the six faces - wesh.cube_vertices = { - { x = 1, y = -1, z = -1 }, -- 1 - { x = -1, y = -1, z = -1 }, -- 2 - { x = -1, y = -1, z = 1 }, -- 3 - { x = 1, y = -1, z = 1 }, -- 4 - { x = 1, y = 1, z = -1 }, -- 5 - { x = 1, y = 1, z = 1 }, -- 6 - { x = -1, y = 1, z = 1 }, -- 7 - { x = -1, y = 1, z = -1 }, -- 8 - } - - -- vertices refer to the above cube_vertices table - wesh.face_construction = { - bottom = { vertices = { 4, 3, 2, 1 }, normal = 1 }, - top = { vertices = { 8, 7, 6, 5 }, normal = 2 }, - back = { vertices = { 2, 8, 5, 1 }, normal = 3 }, - front = { vertices = { 4, 6, 7, 3 }, normal = 4 }, - left = { vertices = { 6, 4, 1, 5 }, normal = 5 }, - right = { vertices = { 3, 7, 8, 2 }, normal = 6 }, - } - - wesh.face_normals = { - {x = 0, y = -1, z = 0 }, - {x = 0, y = 1, z = 0 }, - {x = 0, y = 0, z = -1 }, - {x = 0, y = 0, z = 1 }, - {x = -1, y = 0, z = 0 }, - {x = 1, y = 0, z = 0 }, - } -end - -function wesh._init_transforms() - local rot = {} - local dir = {} - - -- no rotation - rot[0] = {{ 1, 0, 0}, - { 0, 1, 0}, - { 0, 0, 1}} - -- 90 degrees clockwise - rot[1] = {{ 0, 0, 1}, - { 0, 1, 0}, - { -1, 0, 0}} - -- 180 degrees - rot[2] = {{ -1, 0, 0}, - { 0, 1, 0}, - { 0, 0, -1}} - -- 270 degrees clockwise - rot[3] = {{ 0, 0, -1}, - { 0, 1, 0}, - { 1, 0, 0}} - - -- directions - -- Y+ - dir[0] = {{ 1, 0, 0}, - { 0, 1, 0}, - { 0, 0, 1}} - -- Z+ - dir[1] = {{ 1, 0, 0}, - { 0, 0, -1}, - { 0, 1, 0}} - -- Z- - dir[2] = {{ 1, 0, 0}, - { 0, 0, 1}, - { 0, -1, 0}} - -- X+ - dir[3] = {{ 0, 1, 0}, - { -1, 0, 0}, - { 0, 0, 1}} - -- X- - dir[4] = {{ 0, -1, 0}, - { 1, 0, 0}, - { 0, 0, 1}} - -- Y- - dir[5] = {{ -1, 0, 0}, - { 0, -1, 0}, - { 0, 0, 1}} - - wesh.facedir_transform = {} - - for facedir = 0, 23 do - local direction = math.floor(facedir / 4) - local rotation = facedir % 4 - wesh.facedir_transform[facedir] = wesh.matrix_multiply(dir[direction], rot[rotation]) - end -end - -function wesh.apply_transform(pos, transform) - return { - x = pos.x * transform[1][1] + pos.y * transform[1][2] + pos.z * transform[1][3], - y = pos.x * transform[2][1] + pos.y * transform[2][2] + pos.z * transform[2][3], - z = pos.x * transform[3][1] + pos.y * transform[3][2] + pos.z * transform[3][3], - } -end - -function wesh.matrix_multiply(a, b) - local res = {} - for row = 1, #a do - res[row] = {} - for col = 1, #b[1] do - local num = a[row][1] * b[1][col] - for i = 2, #a[1] do - num = num + a[row][i] * b[i][col] - end - res[row][col] = num - end - end - return res -end - -function wesh.transform(facedir, pos) - return wesh.apply_transform(pos, wesh.facedir_transform[facedir]) -end - -function wesh._reset_geometry(canv_size) - wesh.matrix = {} - wesh.vertices = {} - wesh.vertices_indices = {} - wesh.faces = {} - local function reset(p) - if not wesh.matrix[p.x] then wesh.matrix[p.x] = {} end - if not wesh.matrix[p.x][p.y] then wesh.matrix[p.x][p.y] = {} end - wesh.matrix[p.x][p.y][p.z] = "air" - end - wesh.traverse_matrix(reset, canv_size) -end - -function wesh._init_variants() - local variants_filename = "nodevariants.lua" - local default_variants_filename = "default." .. variants_filename - local full_variants_filename = wesh.mod_path .. "/" .. variants_filename - local full_default_variants_filename = wesh.mod_path .. "/" .. default_variants_filename - - local file = io.open(full_variants_filename, "rb") - if not file then - minetest.debug("[wesh] Copying " .. default_variants_filename .. " to " .. variants_filename) - local success, err = wesh.copy_file(full_default_variants_filename, full_variants_filename) - if not success then - minetest.debug("[wesh] " .. err) - return - end - file = io.open(full_variants_filename, "rb") - if not file then - minetest.debug("[wesh] Unable to load " .. variants_filename .. " file from mod folder") - return - end - end - - local custom_variants = minetest.deserialize(file:read("*all")) - wesh.variants = { - plain = "plain-16.png", - } - - -- ensure there is at least one valid variant in the custom variants - if custom_variants and type(custom_variants) == "table" then - for name, texture in pairs(custom_variants) do - if name and type(name) == "string" and texture and type(texture) == "string" then - wesh.variants = custom_variants - break - end - end - end - file:close() -end - -- ======================================================================== -- core functions -- ======================================================================== @@ -694,7 +676,7 @@ function wesh.save_new_mesh(canvas, playername, description) end -- empty all helper variables - wesh._reset_geometry(canvas.size) + wesh.reset_geometry(canvas.size) canvas.voxel_count = 0 @@ -727,9 +709,296 @@ function wesh.save_new_mesh(canvas, playername, description) end -- ======================================================================== --- matrix import helpers +-- file generation -- ======================================================================== +function wesh.create_nodename(obj_filename, variant) + return "wesh:" .. obj_filename:gsub("[^%w]+", "_"):gsub("_obj", "") .. "_" .. variant +end + +function wesh.prepare_data_file(description, canvas) + local boxes = {} + wesh.merge_collision_boxes(canvas) + for _, box in ipairs(canvas.boxes) do + table.insert(boxes, wesh.box_to_collision_box(box, canvas.size)) + end + + local data = { + description = description, + variants = canvas.chosen_variants, + collision_box = { + type = "fixed", + fixed = boxes, + } + } + return wesh.serialize(data, 2) +end + +function wesh.save_mesh_to_file(obj_filename, meshdata, description, playername, canvas) + + -- save .obj file + local full_filename = wesh.temp_path .. "/" .. obj_filename + local file, errmsg = io.open(full_filename, "wb") + if not file then + wesh.notify(playername, "Unable to write to file '" .. obj_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false + end + file:write(meshdata) + file:close() + + -- save .dat file + local data_filename = obj_filename .. ".dat" + local full_data_filename = wesh.temp_path .. "/" .. data_filename + local file, errmsg = io.open(full_data_filename, "wb") + if not file then + wesh.notify(playername, "Unable to write to file '" .. data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false + end + file:write(wesh.prepare_data_file(description, canvas)) + file:close() + + if canvas.generate_matrix then + -- save .matrix.dat file + local matrix_data_filename = obj_filename .. ".matrix.dat" + local full_matrix_data_filename = wesh.temp_path .. "/" .. matrix_data_filename + local file, errmsg = io.open(full_matrix_data_filename, "wb") + if not file then + wesh.notify(playername, "Unable to write to file '" .. matrix_data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false + end + file:write(minetest.serialize(wesh.matrix)) + file:close() + end + + wesh.notify(playername, "Mesh saved to '" .. obj_filename .. "' in '" .. wesh.temp_path .. "'") + wesh.notify(playername, "Reload the world to move newly created mesh to the mod folder") + wesh.notify(playername, "Mesh stats: " .. canvas.voxel_count .. " voxels, " .. #wesh.vertices .. " vertices, " .. #wesh.faces .. " faces") + return true +end + +-- ======================================================================== +-- file listing / filtering +-- ======================================================================== + +function wesh.filter_non_obj(filelist) + local list = {} + for _, filename in pairs(filelist) do + if wesh.is_valid_obj_filename(filename) then + table.insert(list, filename) + end + end + return list +end + +function wesh.filter_non_matrix(filelist) + local list = {} + for _, filename in pairs(filelist) do + if wesh.is_valid_matrix_filename(filename) then + table.insert(list, filename) + end + end + return list +end + +function wesh.get_all_files() + local all = wesh.get_temp_files() + for _, entry in pairs(wesh.get_stored_files()) do + table.insert(all, entry) + end + return all +end + +function wesh.get_all_obj_files() + local stored_obj_files = wesh.filter_non_obj(wesh.get_stored_files()) + local temp_obj_files = wesh.filter_non_obj(wesh.get_temp_files()) + local marked_objs = wesh.retrieve_marked_objs() + local result = {} + + for _, obj_filename in pairs(stored_obj_files) do + table.insert(result, { + filename = obj_filename, + type = marked_objs[obj_filename] and "pending deletion" or "stored", + }) + end + + for _, obj_filename in pairs(temp_obj_files) do + table.insert(result, { + filename = obj_filename, + type = "temporary", + }) + end + + return result +end + +function wesh.get_stored_files() + return minetest.get_dir_list(wesh.models_path, false) +end + +function wesh.get_temp_files() + return minetest.get_dir_list(wesh.temp_path, false) +end + +function wesh.is_valid_obj_filename(obj_filename) + return obj_filename:match("^" .. wesh.gen_prefix .. ".-%.obj$") +end + +function wesh.is_valid_matrix_filename(matrix_filename) + return matrix_filename:match("^" .. wesh.gen_prefix .. ".-%.obj%.matrix%.dat$") +end + +-- ======================================================================== +-- file movement / copy +-- ======================================================================== + +function wesh.copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then + return false, "copy_file() unable to open source for reading" + end + local src_data = src_file:read("*all") + src_file:close() + + local dest_file = io.open(dest, "wb") + if not dest_file then + return false, "copy_file() unable to open dest for writing" + end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +function wesh.move_temp_files() + local meshes = wesh.get_temp_files() + for _, filename in ipairs(meshes) do + os.rename(wesh.temp_path .. "/" .. filename, wesh.models_path .. filename) + end +end + +-- ======================================================================== +-- file deletion +-- ======================================================================== + +function wesh.delete_marked_objs() + for obj_filename, _ in pairs(wesh.retrieve_marked_objs()) do + wesh.delete_obj_fileset(wesh.models_path .. obj_filename) + end + storage:set_string("marked_objs", "") +end + +function wesh.delete_obj_fileset(full_obj_filename) + os.remove(full_obj_filename) + os.remove(full_obj_filename .. ".dat") + os.remove(full_obj_filename .. ".matrix.dat") +end + +function wesh.delete_temp_obj(obj_filename) + local full_obj_filename = wesh.temp_path .. "/" .. obj_filename + wesh.delete_obj_fileset(full_obj_filename) +end + +-- ======================================================================== +-- file marking +-- ======================================================================== + +function wesh.retrieve_marked_objs() + local marked_objs = minetest.deserialize(storage:get_string("marked_objs")) + return type(marked_objs) == "table" and marked_objs or {} +end + +function wesh.store_marked_objs(marked_objs) + storage:set_string("marked_objs", minetest.serialize(marked_objs)) +end + +function wesh.mark_obj_for_deletion(obj_filename) + local marked_objs = wesh.retrieve_marked_objs() + marked_objs[obj_filename] = 1 + wesh.store_marked_objs(marked_objs) +end + +function wesh.unmark_obj_for_deletion(obj_filename) + local marked_objs = wesh.retrieve_marked_objs() + marked_objs[obj_filename] = nil + wesh.store_marked_objs(marked_objs) +end + +-- ======================================================================== +-- file loading +-- ======================================================================== + +function wesh.load_mod_meshes() + local meshes = wesh.get_stored_files() + for _, filename in ipairs(meshes) do + if wesh.is_valid_obj_filename(filename) then + wesh.load_mesh(filename) + end + end +end + +function wesh.load_mesh(obj_filename) + local data = wesh.get_obj_filedata(obj_filename) + + local description = data.description or "Custom Woolen Mesh" + local variants = data.variants or { plain = "plain-16.png" } + + for variant, tile in pairs(variants) do + local props = { + drawtype = "mesh", + mesh = obj_filename, + paramtype = "light", + sunlight_propagates = true, + paramtype2 = "facedir", + description = description .. " (" .. variant .. ")", + tiles = { tile }, + walkable = true, + groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 3 }, + } + for prop, value in pairs(data) do + if prop ~= "variants" and prop ~= "description" then + props[prop] = value + end + end + if props.collision_box and not props.selection_box then + props.selection_box = props.collision_box + end + props.on_place = function(itemstack, placer, pointed_thing) + local playername = placer:get_player_name() + if not minetest.get_player_privs(playername).wesh_place then + wesh.notify(playername, "Insufficient privileges to place mesh nodes") + return + end + minetest.item_place(itemstack, placer, pointed_thing) + end + local nodename = wesh.create_nodename(obj_filename, variant) + minetest.register_node(nodename, props) + end +end + +function wesh.get_obj_filedata(obj_filename) + local full_data_filename = wesh.models_path .. obj_filename .. ".dat" + + local file = io.open(full_data_filename, "rb") + + local data = {} + if file then + data = minetest.deserialize(file:read("*all")) + data = type(data) == "table" and data or {} + file:close() + end + return data +end + +-- ======================================================================== +-- matrix import +-- ======================================================================== + +function wesh.get_content_id(nodename) + if not wesh.content_ids[nodename] then + wesh.content_ids[nodename] = minetest.get_content_id(nodename) + end + return wesh.content_ids[nodename] +end + function wesh.import_matrix(full_matrix_filename, playername) if not full_matrix_filename then return end local file = io.open(full_matrix_filename, "rb") @@ -792,13 +1061,6 @@ function wesh.import_matrix(full_matrix_filename, playername) return true end -function wesh.get_content_id(nodename) - if not wesh.content_ids[nodename] then - wesh.content_ids[nodename] = minetest.get_content_id(nodename) - end - return wesh.content_ids[nodename] -end - function wesh.vacuum_canvas(playername) local canvas = wesh.player_canvas[playername] @@ -833,249 +1095,6 @@ function wesh.vacuum_canvas(playername) end --- ======================================================================== --- mesh management helpers --- ======================================================================== - -function wesh.save_mesh_to_file(obj_filename, meshdata, description, playername, canvas) - - -- save .obj file - local full_filename = wesh.temp_path .. "/" .. obj_filename - local file, errmsg = io.open(full_filename, "wb") - if not file then - wesh.notify(playername, "Unable to write to file '" .. obj_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return false - end - file:write(meshdata) - file:close() - - -- save .dat file - local data_filename = obj_filename .. ".dat" - local full_data_filename = wesh.temp_path .. "/" .. data_filename - local file, errmsg = io.open(full_data_filename, "wb") - if not file then - wesh.notify(playername, "Unable to write to file '" .. data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return false - end - file:write(wesh.prepare_data_file(description, canvas)) - file:close() - - if canvas.generate_matrix then - -- save .matrix.dat file - local matrix_data_filename = obj_filename .. ".matrix.dat" - local full_matrix_data_filename = wesh.temp_path .. "/" .. matrix_data_filename - local file, errmsg = io.open(full_matrix_data_filename, "wb") - if not file then - wesh.notify(playername, "Unable to write to file '" .. matrix_data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return false - end - file:write(minetest.serialize(wesh.matrix)) - file:close() - end - - wesh.notify(playername, "Mesh saved to '" .. obj_filename .. "' in '" .. wesh.temp_path .. "'") - wesh.notify(playername, "Reload the world to move newly created mesh to the mod folder") - wesh.notify(playername, "Mesh stats: " .. canvas.voxel_count .. " voxels, " .. #wesh.vertices .. " vertices, " .. #wesh.faces .. " faces") - return true -end - -function wesh.prepare_data_file(description, canvas) - local boxes = {} - wesh.merge_collision_boxes(canvas) - for _, box in ipairs(canvas.boxes) do - table.insert(boxes, wesh.box_to_collision_box(box, canvas.size)) - end - - local data = { - description = description, - variants = canvas.chosen_variants, - collision_box = { - type = "fixed", - fixed = boxes, - } - } - return wesh.serialize(data, 2) -end - -function wesh.get_temp_files() - return minetest.get_dir_list(wesh.temp_path, false) -end - -function wesh.get_stored_files() - return minetest.get_dir_list(wesh.models_path, false) -end - -function wesh.get_all_files() - local all = wesh.get_temp_files() - for _, entry in pairs(wesh.get_stored_files()) do - table.insert(all, entry) - end - return all -end - -function wesh.filter_non_obj(filelist) - local list = {} - for _, filename in pairs(filelist) do - if wesh.is_valid_obj_filename(filename) then - table.insert(list, filename) - end - end - return list -end - -function wesh.filter_non_matrix(filelist) - local list = {} - for _, filename in pairs(filelist) do - if wesh.is_valid_matrix_filename(filename) then - table.insert(list, filename) - end - end - return list -end - -function wesh.retrieve_marked_objs() - local marked_objs = minetest.deserialize(storage:get_string("marked_objs")) - return type(marked_objs) == "table" and marked_objs or {} -end - -function wesh.store_marked_objs(marked_objs) - storage:set_string("marked_objs", minetest.serialize(marked_objs)) -end - -function wesh.mark_obj_for_deletion(obj_filename) - local marked_objs = wesh.retrieve_marked_objs() - marked_objs[obj_filename] = 1 - wesh.store_marked_objs(marked_objs) -end - -function wesh.unmark_obj_for_deletion(obj_filename) - local marked_objs = wesh.retrieve_marked_objs() - marked_objs[obj_filename] = nil - wesh.store_marked_objs(marked_objs) -end - -function wesh.delete_temp_obj(obj_filename) - local full_obj_filename = wesh.temp_path .. "/" .. obj_filename - wesh._delete_obj_fileset(full_obj_filename) -end - -function wesh._delete_obj_fileset(full_obj_filename) - os.remove(full_obj_filename) - os.remove(full_obj_filename .. ".dat") - os.remove(full_obj_filename .. ".matrix.dat") -end - -function wesh._delete_marked_objs() - for obj_filename, _ in pairs(wesh.retrieve_marked_objs()) do - wesh._delete_obj_fileset(wesh.models_path .. obj_filename) - end - storage:set_string("marked_objs", "") -end - -function wesh._move_temp_files() - local meshes = wesh.get_temp_files() - for _, filename in ipairs(meshes) do - os.rename(wesh.temp_path .. "/" .. filename, wesh.models_path .. filename) - end -end - -function wesh.is_valid_obj_filename(obj_filename) - return obj_filename:match("^" .. wesh.gen_prefix .. ".-%.obj$") -end - -function wesh.is_valid_matrix_filename(matrix_filename) - return matrix_filename:match("^" .. wesh.gen_prefix .. ".-%.obj%.matrix%.dat$") -end - -function wesh.create_nodename(obj_filename, variant) - return "wesh:" .. obj_filename:gsub("[^%w]+", "_"):gsub("_obj", "") .. "_" .. variant -end - -function wesh.get_all_obj_files() - local stored_obj_files = wesh.filter_non_obj(wesh.get_stored_files()) - local temp_obj_files = wesh.filter_non_obj(wesh.get_temp_files()) - local marked_objs = wesh.retrieve_marked_objs() - local result = {} - - for _, obj_filename in pairs(stored_obj_files) do - table.insert(result, { - filename = obj_filename, - type = marked_objs[obj_filename] and "pending deletion" or "stored", - }) - end - - for _, obj_filename in pairs(temp_obj_files) do - table.insert(result, { - filename = obj_filename, - type = "temporary", - }) - end - - return result -end - -function wesh.get_obj_filedata(obj_filename) - local full_data_filename = wesh.models_path .. obj_filename .. ".dat" - - local file = io.open(full_data_filename, "rb") - - local data = {} - if file then - data = minetest.deserialize(file:read("*all")) - data = type(data) == "table" and data or {} - file:close() - end - return data -end - -function wesh._load_mod_meshes() - local meshes = wesh.get_stored_files() - for _, filename in ipairs(meshes) do - if wesh.is_valid_obj_filename(filename) then - wesh._load_mesh(filename) - end - end -end - -function wesh._load_mesh(obj_filename) - local data = wesh.get_obj_filedata(obj_filename) - - local description = data.description or "Custom Woolen Mesh" - local variants = data.variants or { plain = "plain-16.png" } - - for variant, tile in pairs(variants) do - local props = { - drawtype = "mesh", - mesh = obj_filename, - paramtype = "light", - sunlight_propagates = true, - paramtype2 = "facedir", - description = description .. " (" .. variant .. ")", - tiles = { tile }, - walkable = true, - groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 3 }, - } - for prop, value in pairs(data) do - if prop ~= "variants" and prop ~= "description" then - props[prop] = value - end - end - if props.collision_box and not props.selection_box then - props.selection_box = props.collision_box - end - props.on_place = function(itemstack, placer, pointed_thing) - local playername = placer:get_player_name() - if not minetest.get_player_privs(playername).wesh_place then - wesh.notify(playername, "Insufficient privileges to place mesh nodes") - return - end - minetest.item_place(itemstack, placer, pointed_thing) - end - local nodename = wesh.create_nodename(obj_filename, variant) - minetest.register_node(nodename, props) - end -end - -- ======================================================================== -- collision box computers -- ======================================================================== @@ -1228,15 +1247,19 @@ function wesh.update_secondary_collision_box(rel_pos, box) end end - -- ======================================================================== --- mesh generation helpers +-- mesh generation -- ======================================================================== function wesh.construct_face(rel_pos, canvas, texture_vertices, facename, vertices, normal_index) + local function out_of_bounds(pos) + return pos.x < 1 or pos.x > canvas.size + or pos.y < 1 or pos.y > canvas.size + or pos.z < 1 or pos.z > canvas.size + end local normal = wesh.face_normals[normal_index] local hider_pos = vector.add(rel_pos, normal) - if not wesh.out_of_bounds(hider_pos, canvas.size) and wesh.get_voxel_color(hider_pos) ~= "air" then return end + if not out_of_bounds(hider_pos) and wesh.get_voxel_color(hider_pos) ~= "air" then return end local face_line = { "f " } for i, vertex in ipairs(vertices) do local index = wesh.get_vertex_index(rel_pos, canvas.size, vertex) @@ -1359,6 +1382,14 @@ end -- generic helpers -- ======================================================================== +function wesh.apply_transform(pos, transform) + return { + x = pos.x * transform[1][1] + pos.y * transform[1][2] + pos.z * transform[1][3], + y = pos.x * transform[2][1] + pos.y * transform[2][2] + pos.z * transform[2][3], + z = pos.x * transform[3][1] + pos.y * transform[3][2] + pos.z * transform[3][3], + } +end + function wesh.axis_min(pos1, pos2) local result = {} for axis, value in pairs(pos1) do @@ -1381,21 +1412,19 @@ function wesh.check_plain(text) return text:gsub("[^%w]+", "_"):lower() end -function wesh.copy_file(source, dest) - local src_file = io.open(source, "rb") - if not src_file then - return false, "copy_file() unable to open source for reading" +function wesh.matrix_multiply(a, b) + local res = {} + for row = 1, #a do + res[row] = {} + for col = 1, #b[1] do + local num = a[row][1] * b[1][col] + for i = 2, #a[1] do + num = num + a[row][i] * b[i][col] + end + res[row][col] = num + end end - local src_data = src_file:read("*all") - src_file:close() - - local dest_file = io.open(dest, "wb") - if not dest_file then - return false, "copy_file() unable to open dest for writing" - end - dest_file:write(src_data) - dest_file:close() - return true, "files copied successfully" + return res end function wesh.merge_tables(t1, t2) @@ -1408,12 +1437,6 @@ function wesh.notify(playername, message) minetest.chat_send_player(playername, "[wesh] " .. message) end -function wesh.out_of_bounds(pos, canv_size) - return pos.x < 1 or pos.x > canv_size - or pos.y < 1 or pos.y > canv_size - or pos.z < 1 or pos.z > canv_size -end - function wesh.serialize(object, max_wrapping) local function helper(obj, max_depth, depth, seen) if not depth then @@ -1480,6 +1503,10 @@ function wesh.serialize(object, max_wrapping) return "return " .. helper(object, max_wrapping) end +function wesh.transform(facedir, pos) + return wesh.apply_transform(pos, wesh.facedir_transform[facedir]) +end + function wesh.traverse_matrix(callback, boundary, ...) if type(boundary) == "table" then for x = boundary.min.x, boundary.max.x do @@ -1500,4 +1527,4 @@ function wesh.traverse_matrix(callback, boundary, ...) end end -wesh._init() +wesh.init()