local media_paths = epidermis.media_paths local send_notification = epidermis.send_notification minetest.register_craftitem("epidermis:spawner_paintable", { description = "Paintable Spawner", inventory_image = "epidermis_paintable_spawner.png", on_place = function(itemstack, user, pointed_thing) if not pointed_thing.above then return end local base_texture = epidermis.get_skin(user) if not media_paths[base_texture] then send_notification(user, "Invalid (combined?) texture! Defaulting to character.png.", "warning") base_texture = "character.png" end local object = minetest.add_entity( vector.divide(vector.add(pointed_thing.under, pointed_thing.above), 2), "epidermis:paintable", minetest.serialize{ owner = user:get_player_name(), base_texture = base_texture, mesh = assert(epidermis.get_model(user)) } ) if not object then send_notification(user, "Can't spawn paintable!", "error") return end local entity = object:get_luaentity() entity:_face(user) if epidermis.get_epidermis_path_from_texture(base_texture) then -- Force-copy the texture if it belongs to another epidermis, -- as the texture will be dropped when the player & the other epidermis stop using it entity:_write_texture(modlib.func.no_op) end itemstack:take_item() return itemstack end, }) local colorpicker_name = "epidermis:colorpicker" local colorpicker_thickness = minetest.registered_entities[colorpicker_name]._thickness minetest.register_craftitem("epidermis:spawner_colorpicker", { description = "HSV colorpicker spawner", inventory_image = "epidermis_palette.png", on_place = function(itemstack, user, pointed_thing) if pointed_thing.type ~= "node" then return end -- HACK assuming a node size of exactly one local face_pos = vector.divide(vector.add(pointed_thing.above, pointed_thing.under), 2) local direction = vector.direction(pointed_thing.under, pointed_thing.above) local object = minetest.add_entity( vector.add(face_pos, vector.multiply(direction, colorpicker_thickness/2)), colorpicker_name) if not object then send_notification(user, "Can't spawn colorpicker!", "error") return end local normal = vector.subtract(pointed_thing.above, pointed_thing.under) object:get_luaentity():_set_rotation(moblib.get_rotation(normal)) itemstack:take_item() return itemstack end, }) local function set_item_color(itemstack, colorspec) local colorstring = colorspec:to_string() itemstack:get_meta():set_string("color", colorstring) local foreground = "#FFFFFF" if (colorspec.r + colorspec.g + colorspec.b) > 3 * 127 then -- Bright background: Choose a dark foreground color foreground = "#000000" end itemstack:get_meta():set_string("description", minetest.get_background_escape_sequence(colorstring) .. minetest.colorize(foreground, itemstack:get_definition().description)) end local function get_entity(user, pointed_thing, allow_any) if not (user and user:is_player()) then return end if not pointed_thing or pointed_thing.type ~= "object" then return end local object = pointed_thing.ref local entity = object:get_luaentity() if not entity then return end if entity.name == "epidermis:paintable" then if not entity:_can_edit(user) then return end if object:get_animation().y > 1 then send_notification(user, "Playing animation!", "warning") return end return entity elseif allow_any then return entity end end local function get_eye_pos(player) local eye_pos = player:get_pos() eye_pos.y = eye_pos.y + player:get_properties().eye_height local first, third = player:get_eye_offset() if not vector.equals(first, third) then minetest.log("warning", "First & third person eye offsets don't match, assuming first person") end return vector.add(eye_pos, vector.divide(first, 10)) end local function get_intersection_infos(user, paintable) return paintable:_get_intersection_infos(get_eye_pos(user), user:get_look_dir()) end local function get_paintable_intersection(user, paintable) local intersection_infos = get_intersection_infos(user, paintable) for _, intersection_info in ipairs(intersection_infos) do if intersection_info.color.a > 0 then return intersection_info end end end local color_tools = {} local default_color = "#FFFFFF" local function on_secondary_use(itemstack, user, pointed_thing) local entity = get_entity(user, pointed_thing, true) if entity then if entity.name == "epidermis:colorpicker" then local color = entity:_get_color(user) if color then set_item_color(itemstack, color) return itemstack end return end if entity.name == "epidermis:paintable" then local intersection_info = get_paintable_intersection(user, entity) if intersection_info then set_item_color(itemstack, intersection_info.color) return itemstack end return end end local colorstring = itemstack:get_meta():get"color" or default_color local colorspec = assert(modlib.minetest.colorspec.from_string(colorstring)) epidermis.show_colorpicker_formspec(user, colorspec, function(color) if not color then return end local wstack = user:get_wielded_item() if not color_tools[wstack:get_name()] then return end set_item_color(wstack, color) user:set_wielded_item(wstack) end) end local function register_color_tool(name, def, on_paint) color_tools[name] = true def.color = default_color def.on_secondary_use = on_secondary_use def.on_place = on_secondary_use function def.on_use(itemstack, user, pointed_thing) local entity = get_entity(user, pointed_thing, true) if not entity then return end if entity.name == "epidermis:colorpicker" then -- The other params aren't used by the on_punch handler entity:on_punch(user) return end if entity.name ~= "epidermis:paintable" then return end local colorstring = itemstack:get_meta():get"color" or default_color local color = assert(modlib.minetest.colorspec.from_string(colorstring), colorstring) local intersection_info = get_paintable_intersection(user, entity) if intersection_info then return on_paint(entity, intersection_info.pixelcoord, color, user) end end epidermis.register_tool(name, def) end register_color_tool("epidermis:pen", { description = "Pen", inventory_image = "epidermis_pen_tip.png", inventory_overlay = "epidermis_pen_handle.png", }, function(entity, pixelcoord, color) entity:_set_color(entity:_get_pixel_index(unpack(pixelcoord)), color:to_number(), "undo") entity:_update_texture() end) -- TODO (?) allow holding these items using a globalstep epidermis.register_tool("epidermis:eraser", { description = "Eraser", inventory_image = "epidermis_eraser.png", on_secondary_use = function(_, user, pointed_thing) local paintable = get_entity(user, pointed_thing) if not paintable then return end local last_transparent_frontface local intersection_infos = get_intersection_infos(user, paintable) for _, intersection_info in ipairs(intersection_infos) do if intersection_info.color.a < 255 then last_transparent_frontface = intersection_info else break end end if last_transparent_frontface then local idx = paintable:_get_pixel_index(unpack(last_transparent_frontface.pixelcoord)) paintable:_set_color(idx, paintable:_get_color(idx) % 0x1000000 + 0xFF * 0x1000000, true) paintable:_update_texture() end end, on_use = function(_, user, pointed_thing) local paintable = get_entity(user, pointed_thing) if not paintable then return end local intersection_infos = get_intersection_infos(user, paintable) for _, intersection_info in ipairs(intersection_infos) do if intersection_info.color.a > 0 then local idx = paintable:_get_pixel_index(unpack(intersection_info.pixelcoord)) paintable:_set_color(idx, paintable:_get_color(idx) % 0x1000000, true) paintable:_update_texture() return end end end }) local function undo_redo_use_func(logname) return function(_, user, pointed_thing) local paintable = get_entity(user, pointed_thing) if not paintable then return end if not paintable:_reverse_last_log_action(logname) then send_notification(user, "Nothing to " .. logname .. "!", "warning") else paintable:_update_texture() end end end epidermis.register_tool("epidermis:undo_redo", { description = "Undo / Redo", inventory_image = "epidermis_undo_redo.png", on_secondary_use = undo_redo_use_func"redo", on_use = undo_redo_use_func"undo" }) register_color_tool("epidermis:filling_bucket", { description = "Filling Bucket", inventory_image = "epidermis_filling_paint.png", inventory_overlay = "epidermis_filling_bucket.png", }, function(entity, pixelcoord, color) local start_index = entity:_get_pixel_index(unpack(pixelcoord)) local replace_color = entity:_get_color(start_index) local to_fill = {[start_index] = replace_color} local additions local width, height = entity._.width, entity._.height local function fill(index) if to_fill[index] or not entity._paintable_pixels[index] then return end local actual_color = entity:_get_color(index) -- Doesn't need to handle transparent pixels, as those can't be pointed anyways if actual_color ~= replace_color then return end additions[index] = actual_color end repeat additions = {} for index in pairs(to_fill) do local x, y = entity:_get_xy(index) if x > 0 then fill(index - 1) end if x < width - 1 then fill(index + 1) end if y > 0 then fill(index - width) end if y < height - 1 then fill(index + width) end end modlib.table.add_all(to_fill, additions) until not next(additions) local color_argb = color:to_number() for index in pairs(to_fill) do entity:_set_color(index, color_argb) end entity:_log_actions("undo", to_fill) entity:_update_texture() end) -- Dragging tools (line & rectangle) local dragging = {} modlib.minetest.register_on_wielditem_change(function(player) local name = player:get_player_name() if dragging[name] then -- Clear preview dragging[name].entity:_update_texture() send_notification(player, "Dragging stopped (wielded item changed)", "warning") dragging[name] = nil end end) minetest.register_globalstep(function() for player in modlib.minetest.connected_players() do local name = player:get_player_name() local LMB = player:get_player_control().LMB if dragging[name] then local wielded_item = player:get_wielded_item() local def = wielded_item:get_definition() local range = def.range or 4 local eye_pos = get_eye_pos(player) local raycast = minetest.raycast(eye_pos, vector.add(eye_pos, vector.multiply(player:get_look_dir(), range)), true, def.liquids_pointable) local pointed_thing = raycast() if pointed_thing.type == "object" and pointed_thing.ref:is_player() and pointed_thing.ref:get_player_name() == name then -- Skip player pointed_thing = raycast(pointed_thing) end local entity = pointed_thing and get_entity(player, pointed_thing) if not (entity and entity == dragging[name].entity) then send_notification(player, "Dragging stopped (pointed thing changed)", "warning") -- Clear preview dragging[name].entity:_update_texture() dragging[name] = nil else local intersection_info = get_paintable_intersection(player, entity) if intersection_info then if LMB then -- still dragging if dragging[name].preview then dragging[name].preview(intersection_info.pixelcoord) else local action_preview, color = dragging[name].pixels(intersection_info.pixelcoord) for k in pairs(action_preview) do action_preview[k] = color end entity:_update_texture(action_preview) end else -- dragging stopped, finish the action local actions, color = dragging[name].pixels(intersection_info.pixelcoord) for idx in pairs(actions) do entity:_set_color(idx, color) end entity:_log_actions("undo", actions) entity:_update_texture() dragging[name] = nil end end end end end end) register_color_tool("epidermis:rectangle", { description = "Rectangle", inventory_image = "epidermis_rectangle_background.png", inventory_overlay = "epidermis_rectangle_border.png", }, function(entity, pixelcoord_start, color, user) local color_argb = color:to_number() dragging[user:get_player_name()] = { entity = entity, -- Texture modifier based preview as up to width * height pixels might be needed preview = function(pixelcoord_end) local min = modlib.vector.combine(pixelcoord_start, pixelcoord_end, math.min) local max = modlib.vector.combine(pixelcoord_start, pixelcoord_end, math.max) local dim = max - min local preview = "^[combine:" .. entity._.width .. "x" .. entity._.height .. ":" .. min[1] .."," .. min[2] .. "=epxw.png\\^[multiply\\:" .. color:to_string() .. "\\^[resize\\:" .. (dim[1] + 1) .. "x" .. (dim[2] + 1) entity:_update_texture(preview) end, pixels = function(pixelcoord_end) local actions = {} local min = modlib.vector.combine(pixelcoord_start, pixelcoord_end, math.min) local max = modlib.vector.combine(pixelcoord_start, pixelcoord_end, math.max) for x = min[1], max[1] do for y = min[2], max[2] do local idx = entity:_get_pixel_index(x, y) actions[idx] = entity:_get_color(idx) end end return actions, color_argb end } end) register_color_tool("epidermis:line", { description = "Line", inventory_image = "epidermis_line_background.png", inventory_overlay = "epidermis_line_border.png", }, function(entity, pixelcoord_start, color, user) local color_argb = color:to_number() dragging[user:get_player_name()] = { entity = entity, -- A pixel preview is sufficient here as the line may at most have max(width, height) pixels pixels = function(pixelcoord_end) -- This might be copied & swapped. We don't want this to affect the upvalue, so we localize it. local pixelcoord_start_copy = pixelcoord_start -- Uses Bresenham's line algorithm local diff = modlib.vector.subtract(pixelcoord_end, pixelcoord_start_copy) if diff:norm() == 0 then -- Early return: We would divide by zero when obtaining the slope otherwise local idx = entity:_get_pixel_index(unpack(pixelcoord_start_copy)) return {[idx] = entity:_get_color(idx)}, color_argb end local swapped if math.abs(diff[2]) > math.abs(diff[1]) then swapped = true pixelcoord_start_copy = {pixelcoord_start_copy[2], pixelcoord_start_copy[1]} pixelcoord_end = {pixelcoord_end[2], pixelcoord_end[1]} end local actions = {} local min = pixelcoord_start_copy local max = pixelcoord_end if min[1] > max[1] then min, max = max, min end local slope = (max[2] - min[2]) / (max[1] - min[1]) for x = min[1], max[1] do local y = math.floor(0.5 + slope * (x - min[1])) + min[2] if swapped then x, y = y, x end local idx = entity:_get_pixel_index(x, y) actions[idx] = entity:_get_color(idx) end return actions, color_argb end } end)