Files
ngzz-mc/internal/font/font.go

120 lines
3.2 KiB
Go
Raw Normal View History

// 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。
// 主位图(Atlas.Image)确定性选择:优先含 "ascii" 的位图,否则取首个(供 UI 直接采样)。
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)
var primaryName string
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
if primaryName == "" {
primaryName = p.File // 首个位图作为回退主位图
}
if strings.Contains(p.File, "ascii") {
primaryName = p.File // ascii 优先(UI 主字体)
}
}
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 primaryName != "" {
at.Image = imgCache[primaryName] // 确定性主位图
}
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
}