From 799c0122f0bf6cb275a7d6bdac27fe59d888c1f0 Mon Sep 17 00:00:00 2001 From: NianGao Dev Date: Sat, 15 Aug 2026 22:49:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=AD=97=E4=BD=93=E5=9B=BE?= =?UTF-8?q?=E9=9B=86=E6=9E=84=E5=BB=BA=EF=BC=88=E5=8E=9F=E7=89=88=208x8=20?= =?UTF-8?q?=E4=BD=8D=E5=9B=BE=20provider=20=E2=86=92=20=E5=AD=97=E5=BD=A2?= =?UTF-8?q?=E8=A1=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/font/font.go | 114 +++++++++++++++++++++++++++++++++++++ internal/font/font_test.go | 37 ++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 internal/font/font.go create mode 100644 internal/font/font_test.go diff --git a/internal/font/font.go b/internal/font/font.go new file mode 100644 index 00000000..e57c6a04 --- /dev/null +++ b/internal/font/font.go @@ -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 +} diff --git a/internal/font/font_test.go b/internal/font/font_test.go new file mode 100644 index 00000000..1a171f78 --- /dev/null +++ b/internal/font/font_test.go @@ -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()) + } +}