添加utf8display库
This commit is contained in:
477
utf8display/utf8display.lua
Normal file
477
utf8display/utf8display.lua
Normal file
@@ -0,0 +1,477 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user