feat(ui): 字体图集构建(原版 8x8 位图 provider → 字形表)

This commit is contained in:
NianGao Dev
2026-08-15 22:49:27 +08:00
parent 637a4730ad
commit 799c0122f0
2 changed files with 151 additions and 0 deletions

114
internal/font/font.go Normal file
View File

@@ -0,0 +1,114 @@
// Package font 字体图集:解析素材包字体 provider,构建字形像素区域表(UI还原.md §4)。
//
// 原版字体为 8×8 位图(如 font/ascii.png,128×128 = 16×16 格),
// 本包负责把「provider 位图 + chars 索引」映射为每个 rune 的像素矩形与度量。
package font
import (
"fmt"
"image"
"image/png"
"os"
"strings"
"mc/internal/assets"
)
// Glyph 一个字形的像素区域与度量。
type Glyph struct {
X, Y int // 位图内像素位置(左上角)
W, H int // 像素尺寸(8×8,含下伸行高 9)
Width int // 渲染步进宽度(8)
Height int // 行高(provider.height,缺省 8)
Ascent int // 基线偏移(provider.ascent,缺省 8)
}
// Atlas 字体图集:位图图像 + 字形表。
type Atlas struct {
Image image.Image
Glyphs map[rune]Glyph
}
// Missing 缺失字形(用于未覆盖字符的回退显示)。
func (a *Atlas) Missing() Glyph { return Glyph{X: 0, Y: 0, W: 8, H: 8, Width: 8, Height: 8, Ascent: 8} }
// Build 从资源管理器加载字体(name 如 "default"),reference provider 已由 assets 展开。
// 每个 bitmap provider 对应一张 8px 网格位图:列数 = 图宽 / 8。
func Build(m *assets.Manager, name string) (*Atlas, error) {
f, err := m.LoadFont(name)
if err != nil {
return nil, fmt.Errorf("font.Build: %w", err)
}
at := &Atlas{Glyphs: make(map[rune]Glyph)}
imgCache := make(map[string]image.Image)
for _, p := range f.Providers {
if p.Type != "bitmap" || p.File == "" {
continue
}
img, ok := imgCache[p.File]
if !ok {
// 规范化纹理键:minecraft:font/ascii.png → font/ascii(素材包纹理键格式)
key := strings.TrimPrefix(p.File, "minecraft:")
key = strings.TrimSuffix(key, ".png")
path, found := m.ResolveTexture(key)
if !found {
continue // 该包没有此位图:跳过
}
img, err = loadPNG(path)
if err != nil {
return nil, fmt.Errorf("font.Build %s: %w", p.File, err)
}
imgCache[p.File] = img
}
cols := img.Bounds().Dx() / 8
if cols <= 0 {
cols = 16
}
height, ascent := p.Height, p.Ascent
if height <= 0 {
height = 8
}
if ascent <= 0 {
ascent = height
}
idx := 0
for _, chars := range p.Chars {
for _, r := range chars {
g := Glyph{
X: (idx % cols) * 8,
Y: (idx / cols) * 8,
W: 8,
H: height,
Width: 8,
Height: height,
Ascent: ascent,
}
if _, dup := at.Glyphs[r]; !dup {
at.Glyphs[r] = g
}
idx++
}
}
}
if at.Image == nil && len(imgCache) > 0 {
for _, img := range imgCache { // 取第一张作为主位图(测试/调试用)
at.Image = img
break
}
}
return at, nil
}
// loadPNG 解码 PNG。
func loadPNG(path string) (image.Image, error) {
fp, err := os.Open(path)
if err != nil {
return nil, err
}
defer fp.Close()
img, err := png.Decode(fp)
if err != nil {
return nil, err
}
return img, nil
}

View File

@@ -0,0 +1,37 @@
package font
import (
"path/filepath"
"testing"
"mc/internal/assets"
)
// TestBuild 验证从真实素材包构建字体图集(UI还原.md §4)。
func TestBuild(t *testing.T) {
m := assets.New()
packDir := filepath.Join("..", "..", "resourcepacks")
if err := m.Load(filepath.Join("..", "..", "assets"), packDir); err != nil {
t.Fatalf("加载内容包失败: %v", err)
}
at, err := Build(m, "default")
if err != nil {
t.Fatalf("构建字体失败: %v", err)
}
for _, ch := range []rune{'A', 'a', '0', '!'} {
g, ok := at.Glyphs[ch]
if !ok {
t.Fatalf("缺少字形 %q", ch)
}
if g.W != 8 {
t.Fatalf("字形 %q 宽度期望 8,实际 %d", ch, g.W)
}
}
// 主位图应为 128×128(16×16 网格)
if at.Image == nil {
t.Fatal("主位图为空")
}
if b := at.Image.Bounds(); b.Dx() != 128 || b.Dy() != 128 {
t.Fatalf("ascii 位图期望 128×128,实际 %dx%d", b.Dx(), b.Dy())
}
}