diff --git a/utf8display/README.md b/utf8display/README.md new file mode 100644 index 0000000..2dcada0 --- /dev/null +++ b/utf8display/README.md @@ -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的table,string中的char属于computer craft定义的2*3像素点阵(如需使用右下角像素,将char的码值减128表示反转backgroundColor 和 textColor) +- 单个字体文件中可以有不同尺寸的bitmap,且**需要**有'H'(ascII:72)的bitmap表示该文件中最大bitmap高度 +- 会以FontFamily出现的最大bitmap高度为基准,最终输出下对齐的文本 +- FontFamily中**需要**'-'(ascII:45)的bitmap以供自动换行时可能的切断单词使用 diff --git a/utf8display/utf8display.lua b/utf8display/utf8display.lua new file mode 100644 index 0000000..93c369e --- /dev/null +++ b/utf8display/utf8display.lua @@ -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 \ No newline at end of file