115 lines
2.9 KiB
Go
115 lines
2.9 KiB
Go
// 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
|
||
}
|