Files
ComputerCraft-Utf8/utf8display/utf8display.lua
2025-12-08 16:29:32 +08:00

477 lines
14 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- UTF-8 Display Library for ComputerCraft
-- 文件名: utf8display.lua
local utf8display = {}
-- 配置参数
utf8display.config = {
fontUrl = "https://git.liulikeji.cn/xingluo/ComputerCraft-Utf8/raw/branch/main/fonts/fusion-pixel-8px-proportional-zh_hans.lua",
fontPath = nil,
cacheFont = true,
autoScroll = true
}
-- 内部状态
local state = {
font = nil,
fontHeight = 8,
loadedFonts = {}
}
-- 字体管理模块
local fontManager = {}
function fontManager.loadRemoteFont(url)
if state.loadedFonts[url] then
return state.loadedFonts[url]
end
local response = http.get(url)
if not response then
return nil, "无法连接到字体服务器: " .. url
end
if response.getResponseCode() ~= 200 then
return nil, "字体服务器返回错误: " .. response.getResponseCode()
end
local content = response.readAll()
response.close()
local sandbox = {}
local chunk, err = load(content, "=remoteFont", "t", sandbox)
if not chunk then
return nil, "加载字体失败: " .. err
end
local success, result = pcall(chunk)
if not success then
return nil, "执行字体脚本失败: " .. result
end
local fontData = sandbox.font or sandbox[1] or result
if utf8display.config.cacheFont then
state.loadedFonts[url] = fontData
end
return fontData
end
function fontManager.loadLocalFont(path)
if state.loadedFonts[path] then
return state.loadedFonts[path]
end
if not fs.exists(path) then
return nil, "字体文件不存在: " .. path
end
local file = fs.open(path, "r")
local content = file.readAll()
file.close()
local sandbox = {}
local chunk, err = load(content, "=localFont", "t", sandbox)
if not chunk then
return nil, "加载本地字体失败: " .. err
end
local success, result = pcall(chunk)
if not success then
return nil, "执行本地字体脚本失败: " .. result
end
local fontData = sandbox.font or sandbox[1] or result
if utf8display.config.cacheFont then
state.loadedFonts[path] = fontData
end
return fontData
end
function fontManager.getFont()
if not state.font then
local success, err = utf8display.loadFont()
if not success then
error("字体加载失败: " .. err)
end
end
return state.font
end
function fontManager.getFontHeight()
local font = fontManager.getFont()
if font and font[32] then
return #font[32]
end
return state.fontHeight
end
-- 渲染引擎模块
local renderer = {}
function renderer.displayChar(charMap, x, y, textColor, backgroundColor)
if not charMap or #charMap == 0 then
return
end
-- 保存当前终端颜色和光标位置
local originalTextColor = term.getTextColor()
local originalBackgroundColor = term.getBackgroundColor()
local originalCursorX, originalCursorY = term.getCursorPos()
-- 如果没有提供自定义颜色,则使用当前终端颜色
local useTextColor = textColor or originalTextColor
local useBackgroundColor = backgroundColor or originalBackgroundColor
-- 渲染字符(仅在可视区域内)
local width, height = term.getSize()
for row = 1, #charMap do
local drawY = y + row - 1
if drawY > height then break end -- 超出底部,不再绘制
term.setCursorPos(x, drawY)
local line = charMap[row]
for col = 1, #line do
local byte = string.byte(line, col)
if byte < 128 then
term.setTextColor(useBackgroundColor)
term.setBackgroundColor(useTextColor)
term.write(string.char(byte + 128))
else
term.setTextColor(useTextColor)
term.setBackgroundColor(useBackgroundColor)
term.write(string.char(byte))
end
end
end
-- 恢复原始终端颜色和光标位置
term.setTextColor(originalTextColor)
term.setBackgroundColor(originalBackgroundColor)
term.setCursorPos(originalCursorX, originalCursorY)
end
function renderer.utf8Decode(str)
local i = 1
return function()
if i > #str then return end
local b1 = string.byte(str, i)
i = i + 1
if b1 < 0x80 then
return b1
elseif b1 >= 0xC0 and b1 < 0xE0 then
local b2 = string.byte(str, i) or 0
i = i + 1
return (b1 - 0xC0) * 64 + (b2 - 0x80)
elseif b1 >= 0xE0 and b1 < 0xF0 then
local b2 = string.byte(str, i) or 0
i = i + 1
local b3 = string.byte(str, i) or 0
i = i + 1
return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80)
else
return 32
end
end
end
-- 公共API函数
function utf8display.setConfig(key, value)
if utf8display.config[key] ~= nil then
utf8display.config[key] = value
return true
end
return false, "配置项不存在"
end
function utf8display.getConfig(key)
if utf8display.config[key] ~= nil then
return utf8display.config[key]
end
return nil, "配置项不存在"
end
function utf8display.loadFont()
if utf8display.config.fontPath then
state.font, err = fontManager.loadLocalFont(utf8display.config.fontPath)
if not state.font then
return false, err
end
else
state.font, err = fontManager.loadRemoteFont(utf8display.config.fontUrl)
if not state.font then
return false, err
end
end
state.fontHeight = fontManager.getFontHeight()
return true
end
-- 主动初始化字体函数
function utf8display.initFont()
return utf8display.loadFont()
end
local function clampCursorPos(x, y, width, height)
return math.min(math.max(x, 1), width), math.min(math.max(y, 1), height)
end
function utf8display.write(raw_str)
str = tostring(raw_str)
-- 将换行符替换为空格
str = string.gsub(str, "\n", " ")
local font = fontManager.getFont()
local fontHeight = fontManager.getFontHeight()
local textColor = term.getTextColor()
local backgroundColor = term.getBackgroundColor()
local startX, startY = term.getCursorPos()
local width, height = term.getSize()
local cursorX, cursorY = startX, startY
local charCount = 0
local contentFitsHorizontally = true
local contentFitsVertically = true
for code in renderer.utf8Decode(str) do
if code == 10 then -- 换行符 (理论上不会再有,因为我们已经替换了)
cursorX = 1
cursorY = cursorY + fontHeight
else
local charMap = font[code] or font[32]
local charWidth = #charMap[1]
-- 不自动换行:即使超出横向宽度也继续写(但不渲染超出部分)
if cursorX + charWidth - 1 > width then
contentFitsHorizontally = false
end
-- 判断是否还能在竖直方向绘制(至少第一行能画)
if cursorY <= height then
renderer.displayChar(charMap, cursorX, cursorY, textColor, backgroundColor)
charCount = charCount + 1
else
contentFitsVertically = false
-- 不 break继续推进光标逻辑用于正确设置最终光标位置
end
cursorX = cursorX + charWidth
end
end
-- 确定最终光标位置
local finalX, finalY
if not contentFitsHorizontally or not contentFitsVertically then
-- 若任一方向溢出,光标回到起始位置
finalX, finalY = startX, startY
else
-- 否则放在正常结束位置
finalX, finalY = cursorX, cursorY
end
-- 安全设置光标(避免超出屏幕导致卡住)
finalX, finalY = clampCursorPos(finalX, finalY, width, height)
term.setCursorPos(finalX, finalY)
return true, {
textColor = textColor,
backgroundColor = backgroundColor,
startX = startX,
startY = startY,
endX = finalX,
endY = finalY,
charCount = charCount,
fontHeight = fontHeight,
overflowX = not contentFitsHorizontally,
overflowY = not contentFitsVertically
}
end
function utf8display.print(raw_str)
str = tostring(raw_str)
local font = fontManager.getFont()
local fontHeight = fontManager.getFontHeight()
local textColor = term.getTextColor()
local backgroundColor = term.getBackgroundColor()
local startX, startY = term.getCursorPos()
local width, height = term.getSize()
-- 预计算所需总高度(用于滚动)
local tempCursorX, tempCursorY = startX, startY
for code in renderer.utf8Decode(str) do
if code == 10 then
tempCursorX = 1
tempCursorY = tempCursorY + fontHeight
else
local charMap = font[code] or font[32]
local charWidth = #charMap[1]
if tempCursorX + charWidth - 1 > width then
tempCursorX = 1
tempCursorY = tempCursorY + fontHeight
end
tempCursorX = tempCursorX + charWidth
end
end
local totalContentHeight = tempCursorY + fontHeight - startY
local availableHeight = height - startY + 1
local scrollHeight = math.max(0, totalContentHeight - availableHeight)
if scrollHeight > 0 and utf8display.config.autoScroll then
term.scroll(scrollHeight)
startY = startY - scrollHeight
end
-- 实际渲染
local cursorX, cursorY = startX, startY
local charCount = 0
for code in renderer.utf8Decode(str) do
if code == 10 then
cursorX = 1
cursorY = cursorY + fontHeight
else
local charMap = font[code] or font[32]
local charWidth = #charMap[1]
if cursorX + charWidth - 1 > width then
cursorX = 1
cursorY = cursorY + fontHeight
end
if cursorY <= height then
renderer.displayChar(charMap, cursorX, cursorY, textColor, backgroundColor)
charCount = charCount + 1
end
cursorX = cursorX + charWidth
end
end
-- print 总是把光标放到下一行开头(符合常规行为)
local finalY = cursorY + fontHeight
local finalX = 1
finalX, finalY = clampCursorPos(finalX, finalY, width, height)
term.setCursorPos(finalX, finalY)
return true, {
textColor = textColor,
backgroundColor = backgroundColor,
startX = startX,
startY = startY,
endX = finalX,
endY = finalY,
charCount = charCount,
fontHeight = fontHeight
}
end
function utf8display.blit(raw_str, textColorStr, backgroundColorStr)
text = tostring(raw_str)
-- 将换行符替换为空格
text = string.gsub(text, "\n", " ")
local font = fontManager.getFont()
local fontHeight = fontManager.getFontHeight()
local startX, startY = term.getCursorPos()
local width, height = term.getSize()
local cursorX, cursorY = startX, startY
local charCount = 0
local contentFitsHorizontally = true
local contentFitsVertically = true
-- 解析颜色字符串
local textColors = {}
local backgroundColors = {}
for i = 1, #textColorStr do
textColors[i] = colors.fromBlit(string.sub(textColorStr, i, i))
end
for i = 1, #backgroundColorStr do
backgroundColors[i] = colors.fromBlit(string.sub(backgroundColorStr, i, i))
end
local index = 1
for code in renderer.utf8Decode(text) do
if code == 10 then -- 换行符 (理论上不会再有,因为我们已经替换了)
cursorX = 1
cursorY = cursorY + fontHeight
else
local charMap = font[code] or font[32]
local charWidth = #charMap[1]
if cursorX + charWidth - 1 > width then
contentFitsHorizontally = false
end
if cursorY <= height then
local useTextColor = textColors[index] or term.getTextColor()
local useBackgroundColor = backgroundColors[index] or term.getBackgroundColor()
renderer.displayChar(charMap, cursorX, cursorY, useTextColor, useBackgroundColor)
charCount = charCount + 1
else
contentFitsVertically = false
end
cursorX = cursorX + charWidth
index = index + 1
end
end
local finalX, finalY
if not contentFitsHorizontally or not contentFitsVertically then
finalX, finalY = startX, startY
else
finalX, finalY = cursorX, cursorY
end
finalX, finalY = clampCursorPos(finalX, finalY, width, height)
term.setCursorPos(finalX, finalY)
return true, {
startX = startX,
startY = startY,
endX = finalX,
endY = finalY,
charCount = charCount,
fontHeight = fontHeight,
overflowX = not contentFitsHorizontally,
overflowY = not contentFitsVertically
}
end
-- 工具函数
function utf8display.getFontInfo()
local font = fontManager.getFont()
local fontHeight = fontManager.getFontHeight()
return {
height = fontHeight,
loaded = font ~= nil,
source = utf8display.config.fontPath or utf8display.config.fontUrl,
cached = state.loadedFonts[utf8display.config.fontPath or utf8display.config.fontUrl] ~= nil
}
end
function utf8display.isInitialized()
return state.font ~= nil
end
-- 自动初始化(第一次使用时)
setmetatable(utf8display, {
__index = function(self, key)
if key == "write" or key == "print" or key == "blit" then
if not utf8display.isInitialized() then
local success, err = utf8display.loadFont()
if not success then
error("自动初始化失败: " .. err)
end
end
return rawget(self, key)
end
return rawget(self, key)
end
})
return utf8display