515 lines
12 KiB
Lua
515 lines
12 KiB
Lua
imaging = {}
|
|
|
|
local mod_path = minetest.get_modpath(minetest.get_current_modname());
|
|
local smartfs = dofile(mod_path .. "/lib/smartfs.lua")
|
|
local notify = dofile(mod_path .. "/notify.lua")
|
|
|
|
-- constants
|
|
|
|
local POS = {}
|
|
local NEG = {}
|
|
POS.Y = 0
|
|
POS.Z = 1
|
|
NEG.Z = 2
|
|
POS.X = 3
|
|
NEG.X = 4
|
|
NEG.Y = 5
|
|
|
|
-- ============================================================
|
|
-- helper variables
|
|
|
|
local rot_matrices = {}
|
|
local dir_matrices = {}
|
|
|
|
local facedir_memory = {}
|
|
|
|
-- ============================================================
|
|
-- init
|
|
|
|
local function init_transforms()
|
|
local rot = {}
|
|
local dir = {}
|
|
|
|
-- no rotation
|
|
rot[0] = matrix{{ 1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, 1}}
|
|
-- 90 degrees clockwise
|
|
rot[1] = matrix{{ 0, 0, 1},
|
|
{ 0, 1, 0},
|
|
{ -1, 0, 0}}
|
|
-- 180 degrees
|
|
rot[2] = matrix{{ -1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, -1}}
|
|
-- 270 degrees clockwise
|
|
rot[3] = matrix{{ 0, 0, -1},
|
|
{ 0, 1, 0},
|
|
{ 1, 0, 0}}
|
|
|
|
rot_matrices = rot
|
|
|
|
-- directions
|
|
-- Y+
|
|
dir[0] = matrix{{ 1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, 1}}
|
|
-- Z+
|
|
dir[1] = matrix{{ 1, 0, 0},
|
|
{ 0, 0, -1},
|
|
{ 0, 1, 0}}
|
|
-- Z-
|
|
dir[2] = matrix{{ 1, 0, 0},
|
|
{ 0, 0, 1},
|
|
{ 0, -1, 0}}
|
|
-- X+
|
|
dir[3] = matrix{{ 0, 1, 0},
|
|
{ -1, 0, 0},
|
|
{ 0, 0, 1}}
|
|
-- X-
|
|
dir[4] = matrix{{ 0, -1, 0},
|
|
{ 1, 0, 0},
|
|
{ 0, 0, 1}}
|
|
-- Y-
|
|
dir[5] = matrix{{ -1, 0, 0},
|
|
{ 0, -1, 0},
|
|
{ 0, 0, 1}}
|
|
|
|
dir_matrices = dir
|
|
|
|
imaging._facedir_transform = {}
|
|
imaging._matrix_to_facedir = {}
|
|
|
|
for facedir = 0, 23 do
|
|
local direction = math.floor(facedir / 4)
|
|
local rotation = facedir % 4
|
|
local transform = dir[direction] * rot[rotation]
|
|
imaging._facedir_transform[facedir] = transform
|
|
imaging._matrix_to_facedir[transform:tostring():gsub("%-0", "0")] = facedir
|
|
end
|
|
|
|
end
|
|
|
|
init_transforms()
|
|
|
|
-- ============================================================
|
|
-- helper functions
|
|
|
|
local function cross_product(a, b)
|
|
return vector.new(
|
|
a.y * b.z - a.z * b.y,
|
|
a.z * b.x - a.x * b.z,
|
|
a.x * b.y - a.y * b.x
|
|
)
|
|
end
|
|
|
|
local function extract_main_axis(dir)
|
|
local axes = { "x", "y", "z" }
|
|
local axis = 1
|
|
local max = 0
|
|
for i = 1, 3 do
|
|
local abs = math.abs(dir[axes[i]])
|
|
if abs > max then
|
|
axis = i
|
|
max = abs
|
|
end
|
|
end
|
|
return axes[axis]
|
|
end
|
|
|
|
local function sign(num)
|
|
return (num < 0) and -1 or 1
|
|
end
|
|
|
|
local function extract_unit_vectors(player, pointed_thing)
|
|
assert(pointed_thing.type == "node")
|
|
local abs_face_pos = minetest.pointed_thing_to_face_pos(player, pointed_thing)
|
|
local pos = pointed_thing.under
|
|
local f = vector.subtract(abs_face_pos, pos)
|
|
local facedir = 0
|
|
local primary = 0
|
|
|
|
local m1, m2
|
|
|
|
local unit_direction = vector.new()
|
|
local unit_rotation = vector.new()
|
|
local rotation = vector.new()
|
|
|
|
if math.abs(f.y) == 0.5 then
|
|
unit_direction.y = sign(f.y)
|
|
rotation.x = f.x
|
|
rotation.z = f.z
|
|
elseif math.abs(f.z) == 0.5 then
|
|
unit_direction.z = sign(f.z)
|
|
rotation.x = f.x
|
|
rotation.y = f.y
|
|
else
|
|
unit_direction.x = sign(f.x)
|
|
rotation.y = f.y
|
|
rotation.z = f.z
|
|
end
|
|
|
|
local main_axis = extract_main_axis(rotation)
|
|
|
|
unit_rotation[main_axis] = sign(rotation[main_axis])
|
|
|
|
return {
|
|
back = unit_direction,
|
|
wrap = unit_rotation,
|
|
thumb = cross_product(unit_direction, unit_rotation),
|
|
}
|
|
end
|
|
|
|
local function 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
|
|
|
|
local function get_facedir_transform(facedir)
|
|
return imaging._facedir_transform[facedir] or imaging._facedir_transform[0]
|
|
end
|
|
|
|
local function matrix_to_facedir(mtx)
|
|
local key = mtx:tostring():gsub("%-0", "0")
|
|
if not imaging._matrix_to_facedir[key] then
|
|
error("Unsupported matrix:\n" .. key)
|
|
end
|
|
return imaging._matrix_to_facedir[key]
|
|
end
|
|
|
|
local function vector_to_dir_index(vec)
|
|
local main_axis = extract_main_axis(vec)
|
|
if main_axis == "x" then return (vec.x > 0) and POS.X or NEG.X end
|
|
if main_axis == "z" then return (vec.z > 0) and POS.Z or NEG.Z end
|
|
return (vec.y > 0) and POS.Y or NEG.Y
|
|
end
|
|
|
|
-- ========================================================================
|
|
-- local helpers
|
|
|
|
local function 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
|
|
|
|
local function custom_or_default(modname, path, filename)
|
|
local default_filename = "default/" .. filename
|
|
local full_filename = path .. "/custom." .. filename
|
|
local full_default_filename = path .. "/" .. default_filename
|
|
|
|
os.rename(path .. "/" .. filename, full_filename)
|
|
|
|
local file = io.open(full_filename, "rb")
|
|
if not file then
|
|
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
|
|
local success, err = copy_file(full_default_filename, full_filename)
|
|
if not success then
|
|
minetest.debug("[" .. modname .. "] " .. err)
|
|
return false
|
|
end
|
|
file = io.open(full_filename, "rb")
|
|
if not file then
|
|
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
|
|
return false
|
|
end
|
|
end
|
|
file:close()
|
|
return full_filename
|
|
end
|
|
|
|
-- ============================================================
|
|
-- palette functions
|
|
|
|
local function textToGrid(text)
|
|
local parts = text:split(" ")
|
|
if #parts < 3 then
|
|
return false, "Invalid paste data"
|
|
end
|
|
local width = tonumber(parts[1])
|
|
local height = tonumber(parts[2])
|
|
if width < 1 or height < 1 then
|
|
return false, "Invalid sizes: " .. parts[1] .. " " .. parts[2]
|
|
end
|
|
local index = 2
|
|
local rows = {}
|
|
local x = 0
|
|
local y = 0
|
|
|
|
local function addPixel(paletteIndex)
|
|
if x >= width then
|
|
x = 0
|
|
y = y + 1
|
|
end
|
|
if y >= height then
|
|
return false, "Pixels exceed declared sizes"
|
|
end
|
|
if not rows[y] then
|
|
rows[y] = {}
|
|
end
|
|
rows[y][x] = paletteIndex
|
|
x = x + 1
|
|
return true
|
|
end
|
|
|
|
local function processCell(cell)
|
|
local includeEmpty = true
|
|
local parts = cell:split(":", includeEmpty)
|
|
if #parts ~= 2 then
|
|
return false, "Invalid cell format: " .. cell
|
|
end
|
|
local paletteIndex = parts[1] ~= "" and tonumber(parts[1]) or false
|
|
local count = parts[2] == "" and 1 or tonumber(parts[2])
|
|
if paletteIndex ~= false and (paletteIndex < 0 or paletteIndex > 255) then
|
|
return false, "Invalid cell index: " .. cell
|
|
end
|
|
if count == 0 then
|
|
return false, "Invalid cell count: " .. cell
|
|
end
|
|
for c = 1, count do
|
|
local success, err = addPixel(paletteIndex)
|
|
if not success then
|
|
return false, err
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
for index, cell in ipairs(parts) do
|
|
if index > 2 then
|
|
local success, err = processCell(cell)
|
|
if not success then
|
|
return false, err
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
width = width,
|
|
height = height,
|
|
rows = rows,
|
|
}
|
|
end
|
|
|
|
local function getPaletteNames()
|
|
local entries = minetest.get_dir_list(mod_path .. "/textures")
|
|
local names = {}
|
|
for _, entry in ipairs(entries) do
|
|
local name = entry:match("^palette%-(.+)%.png$")
|
|
if name then
|
|
names[name] = entry
|
|
end
|
|
end
|
|
return names
|
|
end
|
|
|
|
imaging.init = function()
|
|
imaging.palettes = getPaletteNames()
|
|
for name, palette in pairs(imaging.palettes) do
|
|
local def = {
|
|
description = "Imaging " .. name,
|
|
paramtype = "light",
|
|
paramtype2 = "color",
|
|
tiles = { "white.png" },
|
|
palette = palette,
|
|
groups = {cracky = 3, not_in_creative_inventory = 1},
|
|
}
|
|
minetest.register_node("imaging:palette_" .. name, def)
|
|
end
|
|
|
|
local node_box = {
|
|
type = "fixed",
|
|
fixed = {
|
|
{-0.5, -0.5, -0.15, 0.5, 0.5, 0.15},
|
|
},
|
|
}
|
|
|
|
minetest.register_node("imaging:canvas", {
|
|
drawtype = "nodebox",
|
|
description = "Imaging Canvas",
|
|
tiles = {
|
|
"black.png",
|
|
"black.png",
|
|
"black.png",
|
|
"black.png",
|
|
"back.png",
|
|
"front.png"
|
|
},
|
|
paramtype = "light",
|
|
paramtype2 = "facedir",
|
|
node_box = node_box,
|
|
groups = {cracky = 3 },
|
|
on_rightclick = imaging.on_rightclick,
|
|
})
|
|
|
|
local full_recipes_filename = custom_or_default("imaging", mod_path, "recipes.lua")
|
|
if not full_recipes_filename then return end
|
|
local recipes = dofile(full_recipes_filename);
|
|
|
|
if recipes["imaging:canvas"] then
|
|
minetest.register_craft({
|
|
output = "imaging:canvas",
|
|
recipe = recipes["imaging:canvas"]
|
|
})
|
|
end
|
|
end
|
|
|
|
local clicked_node = {}
|
|
local main_memory = {}
|
|
|
|
imaging.on_rightclick = function(clicked_pos, node, clicker)
|
|
local playername = clicker:get_player_name()
|
|
node.pos = clicked_pos
|
|
clicked_node[playername] = node
|
|
local state = imaging.forms.main:show(playername)
|
|
|
|
local memory = main_memory[playername]
|
|
|
|
if not memory then return end
|
|
|
|
if memory.text then state:get("paste"):setText(memory.text) end
|
|
if memory.palette then state:get("palettes"):setSelectedItem(memory.palette) end
|
|
state:get("replacer"):setValue(memory.replacer)
|
|
if memory.replacement then state:get("replacement"):setText(memory.replacement) end
|
|
if memory.bumpvalue then state:get("bumpvalue"):setText(memory.bumpvalue) end
|
|
|
|
end
|
|
|
|
imaging.generate = function(_, state)
|
|
local text = state:get("paste"):getText()
|
|
local palette = state:get("palettes"):getSelectedItem()
|
|
local replacer = state:get("replacer"):getValue()
|
|
local replacement = state:get("replacement"):getText()
|
|
local bumpvalue = tonumber(state:get("bumpvalue"):getText())
|
|
|
|
if replacer then
|
|
local def = minetest.registered_nodes[replacement]
|
|
if not def then
|
|
notify.err(state.player, "Invalid node entered: " .. replacement)
|
|
return
|
|
end
|
|
else
|
|
replacement = false
|
|
end
|
|
|
|
if type(bumpvalue) ~= "number" or bumpvalue < 0 then
|
|
bumpvalue = 0
|
|
end
|
|
|
|
if not imaging.palettes[palette] then
|
|
notify.err(state.player, "Invalid palette name " .. palette)
|
|
return
|
|
end
|
|
|
|
local grid, err = textToGrid(text)
|
|
if not grid then
|
|
notify.err(state.player, err)
|
|
return
|
|
end
|
|
|
|
main_memory[state.player] = {
|
|
palette = palette,
|
|
replacer = replacer,
|
|
replacement = replacement,
|
|
bumpvalue = bumpvalue,
|
|
text = text,
|
|
}
|
|
|
|
imaging.fillGrid(state.player, palette, grid, replacement, bumpvalue)
|
|
end
|
|
|
|
imaging.fillGrid = function(playername, palette, grid, replacement, bumpvalue)
|
|
|
|
local node = clicked_node[playername]
|
|
if not node or node.name ~= "imaging:canvas" then
|
|
notify.err(playername, "How did you end up here?")
|
|
return
|
|
end
|
|
|
|
local facedir = node.param2
|
|
local transform = get_facedir_transform(facedir)
|
|
|
|
local multi = bumpvalue / 255
|
|
|
|
function placeNode(x, y, paletteIndex)
|
|
local pos = {
|
|
x = math.floor(-grid.width / 2 + x + 0.5),
|
|
y = grid.height - y,
|
|
z = math.floor(-multi * paletteIndex + 0.5),
|
|
}
|
|
local newpos = vector.add(node.pos, apply_transform(pos, transform))
|
|
local newnode
|
|
|
|
if replacement then
|
|
newnode = { name = replacement }
|
|
else
|
|
newnode = {
|
|
name = "imaging:palette_" .. palette,
|
|
param2 = paletteIndex,
|
|
}
|
|
end
|
|
minetest.swap_node(newpos, newnode)
|
|
end
|
|
|
|
for y = 0, grid.height - 1 do
|
|
for x = 0, grid.width - 1 do
|
|
local paletteIndex = grid.rows[y][x]
|
|
if paletteIndex ~= false then
|
|
placeNode(x, y, paletteIndex)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
imaging.init()
|
|
|
|
imaging.forms = {}
|
|
|
|
imaging.forms.main = smartfs.create("imaging.forms.main", function(state)
|
|
state:size(7.5, 8)
|
|
|
|
local paste_area = state:field(0.5, 0.5, 6.95, 3.5, "paste", "Paste Imaging data here")
|
|
paste_area:isMultiline(true)
|
|
paste_area:setCloseOnEnter(false)
|
|
|
|
local palettes = state:dropdown(0.2, 3.7, 5.2, 0, "palettes", {})
|
|
for name, palette in pairs(imaging.palettes) do
|
|
palettes:addItem(name)
|
|
end
|
|
if imaging.palettes.vga then
|
|
palettes:setSelectedItem("vga")
|
|
else
|
|
palettes:setSelected(1)
|
|
end
|
|
|
|
local generate_button = state:button(5.2, 3.6, 2, 1, "generate", "Build")
|
|
generate_button:onClick(imaging.generate)
|
|
generate_button:setClose(true)
|
|
|
|
local replacer = state:checkbox(0.2, 4.5, "replacer", "Build as:")
|
|
local replacement = state:field(0.5, 4.6, 5, 4.5, "replacement", "'air' or 'modname:nodename'")
|
|
replacement:setText("air")
|
|
replacement:setCloseOnEnter(false)
|
|
|
|
local bumpvalue = state:field(0.5, 5.8, 5, 4.5, "bumpvalue", "Bump value (zero or positive)")
|
|
bumpvalue:setText("0")
|
|
|
|
local close_button = state:button(5.2, 7, 2, 1, "close", "Close")
|
|
close_button:setClose(true)
|
|
end)
|
|
|