feat(ui): 字体图集构建(原版 8x8 位图 provider → 字形表)
This commit is contained in:
114
internal/font/font.go
Normal file
114
internal/font/font.go
Normal 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
|
||||
}
|
||||
37
internal/font/font_test.go
Normal file
37
internal/font/font_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user