添加utf8display库

This commit is contained in:
2025-12-08 16:29:32 +08:00
parent 87ea387834
commit 0f95039951
2 changed files with 586 additions and 0 deletions

109
utf8display/README.md Normal file
View File

@@ -0,0 +1,109 @@
```lua
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 -- print 函数是否自动滚动
}
```
### 修改配置
```lua
utf8display.setConfig("fontUrl", "your_font_url_here")
```
## 主要函数
### `utf8display.initFont()`
主动初始化字体,预先加载字体资源。
### `utf8display.write(str)`
- 不自动换行的字符串输出
- 遇到换行符 `\n` 会替换为空格
- 如果内容超出屏幕范围,光标回到起始位置
- 返回: `{success, info}`
### `utf8display.print(str)`
- 自动换行的字符串输出
- 遵循传统 print 行为,自动换行并滚动
- 遇到换行符 `\n` 会正常换行
- 光标始终在输出内容的下一行开头
- 返回: `{success, info}`
### `utf8display.blit(str, textColorStr, backgroundColorStr)`
- 带颜色的字符串输出
- 遇到换行符 `\n` 会替换为空格
- `textColorStr``backgroundColorStr` 为颜色字符串
- 如果内容超出屏幕范围,光标回到起始位置
- 返回: `{success, info}`
### `utf8display.getFontInfo()`
获取当前字体信息,包括高度、是否已加载等。
### `utf8display.isInitialized()`
检查字体是否已初始化。
## 使用示例
### 基本用法
#### 本地加载
```lua
local utf8display = require("utf8display")
-- 写入文本(不自动换行)
utf8display.write("Hello 世界!")
-- 写入文本(自动换行)
utf8display.print("Hello 世界!")
-- 带颜色输出
utf8display.blit("Hello 世界!", "ffffffff", "00000000")
```
#### 或网络加载
```lua
```
### 预加载字体
```lua
local utf8display = require("utf8display")
-- 主动初始化字体
utf8display.initFont()
```
### 修改配置
```lua
local utf8display = require("utf8display")
-- 设置本地字体路径
utf8display.setConfig("fontPath", "/path/to/local/font.lua")
```
## 返回值说明
所有显示函数都返回一个包含以下信息的表:
- `success`: 操作是否成功
- `textColor`: 文本颜色write/print
- `backgroundColor`: 背景颜色write/print
- `startX`, `startY`: 起始光标位置
- `endX`, `endY`: 结束光标位置
- `charCount`: 显示的字符数
- `fontHeight`: 字体高度
- `overflowX`: 水平方向是否溢出write/blit
- `overflowY`: 垂直方向是否溢出write/blit
## 注意事项
1. 字体文件包含预定义的字符映射,支持中文字符
2. write 和 blit 函数中的换行符 `\n` 会被替换为空格
3. print 函数会正常处理换行符并自动换行
4. 当内容超出屏幕范围时write 和 blit 会将光标移回起始位置
5. 支持在屏幕边界处正常显示字符,超出部分不会渲染
6. 字体高度固定,所有字符都按此高度计算行间距
## 如何制作Font
- 字体文件返回一个table键值为utf8编码值为和对应字体的bitmap。
- bitmap为一个包含等长string的tablestring中的char属于computer craft定义的2*3像素点阵如需使用右下角像素将char的码值减128表示反转backgroundColor 和 textColor
- 单个字体文件中可以有不同尺寸的bitmap且**需要**有'H'(ascII:72)的bitmap表示该文件中最大bitmap高度
- 会以FontFamily出现的最大bitmap高度为基准最终输出下对齐的文本
- FontFamily中**需要**'-'(ascII:45)的bitmap以供自动换行时可能的切断单词使用

477
utf8display/utf8display.lua Normal file
View 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