Compare commits

..

1 Commits

Author SHA1 Message Date
c82f9a193c feat: update from AI 2025-08-06 16:52:37 +03:00
6 changed files with 376 additions and 141 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/qmuntal/gltf"
"github.com/qmuntal/gltf/modeler"
"git.influ.su/artmares/digglestool/internal/pkg/errors"
"git.influ.su/artmares/digglestool/internal/pkg/logger"
"git.influ.su/artmares/digglestool/internal/services/exporter"
"git.influ.su/artmares/digglestool/pkg/threedb"
@ -35,14 +36,14 @@ func main() {
f, err := os.Open(path.Join(inputPath, inputModel+".3db"))
if err != nil {
logger.Fatal(err)
errors.LogFatal(errors.Wrap(err, "failed to open input file"))
}
defer f.Close()
model := threedb.Model{}
err = threedb.NewDecoder(f).Decode(&model)
if err != nil {
logger.Error(err)
errors.LogError(errors.Wrap(err, "failed to decode model"))
return
}
//log.Printf("DB Version: %v\n", model.DBVersion)
@ -64,14 +65,14 @@ func main() {
logFile, err := os.OpenFile(fmt.Sprintf("./%s.json", inputModel), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logger.Error(err)
errors.LogError(errors.Wrap(err, "failed to create log file"))
return
}
defer logFile.Close()
enc := json.NewEncoder(logFile)
enc.SetIndent("", " ")
if err = enc.Encode(model); err != nil {
logger.Error(err)
errors.LogError(errors.Wrap(err, "failed to encode model to JSON"))
return
}
@ -81,7 +82,7 @@ func main() {
exporter.WithAnimation(),
)
if err = export.Export(outputPath, &model); err != nil {
logger.Error(err)
errors.LogError(errors.Wrap(err, "failed to export model"))
return
}
@ -91,47 +92,76 @@ func main() {
//}
}
// exportFn exports a 3DB model to GLTF format
func exportFn(model *threedb.Model, inputModel string, cache map[string]string) error {
//maxMeshIndex := 133
//meshIndex := randRange(0, len(model.Meshes)-1)
meshIndex := 0
//log.Println("Mesh Index:", meshIndex)
binaryCache := make(map[string][]byte)
//for meshIndex := 0; meshIndex < len(model.Meshes); meshIndex++ {
doc := gltf.NewDocument()
//doc.Materials = []*gltf.Material{{
// Name: "Default", AlphaMode: gltf.AlphaOpaque, AlphaCutoff: gltf.Float(0.5),
// PBRMetallicRoughness: &gltf.PBRMetallicRoughness{BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, MetallicFactor: gltf.Float(0.1), RoughnessFactor: gltf.Float(0.99)},
//}}
// Process materials and textures
binaryCache := make(map[string][]byte)
if err := processMaterials(doc, model, cache, binaryCache); err != nil {
return err
}
// Process mesh data
mesh := model.Meshes[meshIndex]
if err := processMesh(doc, model, mesh); err != nil {
return err
}
// Setup scene
setupScene(doc)
// Save the GLTF document
if err := gltf.Save(doc, path.Join(outputPath, fmt.Sprintf("%s-%d.gltf", inputModel, meshIndex))); err != nil {
return errors.Wrap(err, "failed to save GLTF document")
}
return nil
}
// processMaterials processes all materials in the model and adds them to the GLTF document
func processMaterials(doc *gltf.Document, model *threedb.Model, cache map[string]string, binaryCache map[string][]byte) error {
for _, material := range model.Materials {
data, ok := binaryCache[material.Name]
if !ok {
texturePath, ok := cache[material.Name]
if !ok {
logger.Warnf("Invalid texture cache %q", material.Name)
errors.LogWarn(errors.New("invalid texture cache: " + material.Name))
continue
return fmt.Errorf("invalid texture cache %q", material.Name)
}
data, err := os.ReadFile(texturePath)
var err error
data, err = os.ReadFile(texturePath)
if err != nil {
return err
return errors.Wrap(err, "failed to read texture file: "+texturePath)
}
binaryCache[material.Name] = data
}
mt := mimetype.Detect(data)
if mt == nil {
return fmt.Errorf("can't geting mimetype %q", material.Name)
}
imageIndex, err := modeler.WriteImage(doc, material.Name+".tga", mt.String(), bytes.NewBuffer(data))
if err != nil {
if err := addMaterialToDocument(doc, material, data); err != nil {
return err
}
}
return nil
}
// addMaterialToDocument adds a material and its texture to the GLTF document
func addMaterialToDocument(doc *gltf.Document, material threedb.Material, textureData []byte) error {
mt := mimetype.Detect(textureData)
if mt == nil {
return errors.New("failed to detect mimetype for texture: " + material.Name)
}
imageIndex, err := modeler.WriteImage(doc, material.Name+".tga", mt.String(), bytes.NewBuffer(textureData))
if err != nil {
return errors.Wrap(err, "failed to write image to GLTF document")
}
doc.Textures = append(doc.Textures, &gltf.Texture{
Name: material.Name,
//Sampler: gltf.Index(0),
Source: gltf.Index(imageIndex),
})
doc.Materials = append(doc.Materials, &gltf.Material{
Name: material.Name,
AlphaMode: gltf.AlphaOpaque,
@ -143,31 +173,62 @@ func exportFn(model *threedb.Model, inputModel string, cache map[string]string)
RoughnessFactor: gltf.Float(0.99),
},
})
}
mesh := model.Meshes[meshIndex]
return nil
}
// processMesh processes a mesh and adds it to the GLTF document
func processMesh(doc *gltf.Document, model *threedb.Model, mesh threedb.Mesh) error {
for _, meshLink := range mesh.Links {
triangles := model.Triangles[meshLink.Triangles]
points := model.Points[meshLink.Points]
textureCoordinates := model.TextureCoordinates[meshLink.TextureCoordinates]
material := model.Materials[meshLink.Material]
// Process vertices
vertices := processVertices(points)
verticesIndex := modeler.WriteAccessor(doc, gltf.TargetElementArrayBuffer, vertices)
// Process texture coordinates
textureCoords := processTextureCoordinates(textureCoordinates)
textureIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, textureCoords)
// Process triangles
trianglesIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, triangles)
// Add mesh to document
addMeshToDocument(doc, model, verticesIndex, textureIndex, trianglesIndex, material)
}
return nil
}
// processVertices processes the vertices of a mesh
func processVertices(points []threedb.Vector) [][3]float32 {
var vertices [][3]float32
var mins []float64
var maxs []float64
for _, point := range points {
vec := point.Transform(100)
vec = vec.Normalize()
vertices = append(vertices, vec)
mins = append(mins, float64(vec.Min()))
maxs = append(maxs, float64(vec.Max()))
}
verticesIndex := modeler.WriteAccessor(doc, gltf.TargetElementArrayBuffer, vertices)
return vertices
}
// processTextureCoordinates processes the texture coordinates of a mesh
func processTextureCoordinates(textureCoordinates []threedb.Coordinate) [][2]float32 {
var textureCoords [][2]float32
for _, txc := range textureCoordinates {
textureCoords = append(textureCoords, txc)
}
textureIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, textureCoords)
trianglesIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, triangles)
return textureCoords
}
// addMeshToDocument adds a mesh to the GLTF document
func addMeshToDocument(doc *gltf.Document, model *threedb.Model, verticesIndex, textureIndex, trianglesIndex int, material threedb.Material) {
materialIndex := len(doc.Materials)
doc.Meshes = append(doc.Meshes, &gltf.Mesh{
Name: fmt.Sprintf("%s-%d", model.Name, len(doc.Meshes)),
Primitives: []*gltf.Primitive{{
@ -180,22 +241,24 @@ func exportFn(model *threedb.Model, inputModel string, cache map[string]string)
}},
})
//imageIndex, err := modeler.WriteImage(doc)
doc.Materials = append(doc.Materials, &gltf.Material{
Name: material.Name,
AlphaMode: gltf.AlphaOpaque,
AlphaCutoff: gltf.Float(0.5),
PBRMetallicRoughness: &gltf.PBRMetallicRoughness{
//BaseColorTexture: &gltf.TextureInfo{Index: imageIndex},
BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1},
MetallicFactor: gltf.Float(0.1),
RoughnessFactor: gltf.Float(0.99),
},
})
doc.Nodes = append(doc.Nodes, &gltf.Node{
Mesh: gltf.Index(len(doc.Meshes) - 1),
})
}
}
// setupScene sets up the scene in the GLTF document
func setupScene(doc *gltf.Document) {
var nodes []int
for index := range doc.Nodes {
nodes = append(nodes, index)
@ -204,15 +267,6 @@ func exportFn(model *threedb.Model, inputModel string, cache map[string]string)
doc.Scenes = []*gltf.Scene{
{Name: "Root Scene", Nodes: nodes},
}
if err := gltf.Save(doc, path.Join(outputPath, fmt.Sprintf("%s-%d.gltf", inputModel, meshIndex))); err != nil {
return err
}
//if err := gltf.SaveBinary(doc, fmt.Sprintf("./%s.glb", inputModel)); err != nil {
// return err
//}
//}
return nil
}
func randRange(min, max int) int {

View File

@ -17,9 +17,9 @@
- [x] Добавить более подробные комментарии для объяснения сложных алгоритмов, особенно в декодере
- [ ] Удалить закомментированный код в decoder/main.go и других файлах
- [ ] Внедрить единые шаблоны обработки ошибок в кодовой базе
- [x] Внедрить единые шаблоны обработки ошибок в кодовой базе
- [ ] Добавить поддержку контекста для операций, которые могут занять много времени
- [ ] Рефакторинг функции exportFn в decoder/main.go для большей модульности
- [x] Рефакторинг функции exportFn в decoder/main.go для большей модульности
- [ ] Улучшить именование переменных для лучшей читаемости кода
- [ ] Добавить правильную валидацию для входных файлов и параметров
- [ ] Последовательно реализовать правильную очистку ресурсов с помощью операторов defer

View File

@ -0,0 +1,162 @@
// Package errors provides a unified error handling strategy for the application.
// It extends the standard Go errors package with additional functionality for
// error wrapping, error types, and error logging.
package errors
import (
"errors"
"fmt"
"runtime"
"strings"
"git.influ.su/artmares/digglestool/internal/pkg/logger"
)
// Standard errors that can be used throughout the application.
var (
ErrNotFound = errors.New("not found")
ErrInvalidInput = errors.New("invalid input")
ErrInternal = errors.New("internal error")
ErrNotImplemented = errors.New("not implemented")
)
// Error represents an error with additional context.
type Error struct {
// Original is the original error.
Original error
// Message is an additional message to provide context.
Message string
// File is the file where the error occurred.
File string
// Line is the line where the error occurred.
Line int
}
// Error returns the error message.
func (e *Error) Error() string {
if e.Original == nil {
return e.Message
}
if e.Message == "" {
return e.Original.Error()
}
return fmt.Sprintf("%s: %s", e.Message, e.Original.Error())
}
// Unwrap returns the original error.
func (e *Error) Unwrap() error {
return e.Original
}
// New creates a new error with the given message.
func New(message string) error {
return &Error{
Message: message,
}
}
// Wrap wraps an error with additional context.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
_, file, line, _ := runtime.Caller(1)
file = trimFilePath(file)
return &Error{
Original: err,
Message: message,
File: file,
Line: line,
}
}
// Wrapf wraps an error with a formatted message.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
_, file, line, _ := runtime.Caller(1)
file = trimFilePath(file)
return &Error{
Original: err,
Message: fmt.Sprintf(format, args...),
File: file,
Line: line,
}
}
// Is reports whether any error in err's chain matches target.
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true. Otherwise, it returns false.
func As(err error, target interface{}) bool {
return errors.As(err, target)
}
// LogError logs an error with the appropriate log level and returns it.
// This is useful for logging an error while still returning it up the call stack.
func LogError(err error) error {
if err == nil {
return nil
}
var e *Error
if errors.As(err, &e) {
logger.Errorf("[%s:%d] %s", e.File, e.Line, err.Error())
} else {
_, file, line, _ := runtime.Caller(1)
file = trimFilePath(file)
logger.Errorf("[%s:%d] %s", file, line, err.Error())
}
return err
}
// LogWarn logs an error as a warning and returns it.
func LogWarn(err error) error {
if err == nil {
return nil
}
var e *Error
if errors.As(err, &e) {
logger.Warnf("[%s:%d] %s", e.File, e.Line, err.Error())
} else {
_, file, line, _ := runtime.Caller(1)
file = trimFilePath(file)
logger.Warnf("[%s:%d] %s", file, line, err.Error())
}
return err
}
// LogFatal logs an error as fatal and exits the program.
func LogFatal(err error) {
if err == nil {
return
}
var e *Error
if errors.As(err, &e) {
logger.Fatalf("[%s:%d] %s", e.File, e.Line, err.Error())
} else {
_, file, line, _ := runtime.Caller(1)
file = trimFilePath(file)
logger.Fatalf("[%s:%d] %s", file, line, err.Error())
}
}
// trimFilePath trims the file path to make it more readable.
func trimFilePath(file string) string {
// Find the last occurrence of "git.influ.su/artmares/digglestool"
idx := strings.LastIndex(file, "git.influ.su\\artmares\\digglestool")
if idx >= 0 {
return file[idx+len("git.influ.su\\artmares\\digglestool"):]
}
return file
}

View File

@ -1,7 +1,6 @@
package exporter
import (
"errors"
"fmt"
_ "image/png"
"os"
@ -12,6 +11,7 @@ import (
_ "github.com/ftrvxmtrx/tga"
"github.com/qmuntal/gltf"
"git.influ.su/artmares/digglestool/internal/pkg/errors"
"git.influ.su/artmares/digglestool/internal/pkg/logger"
"git.influ.su/artmares/digglestool/pkg/threedb"
)
@ -57,30 +57,31 @@ func NewExporter(options ...Option) *Exporter {
func (e *Exporter) Export(basePath string, model *threedb.Model) error {
if model == nil {
return errors.New("model is nil")
return errors.ErrInvalidInput
}
meshesLen := len(model.Meshes)
if meshesLen == 0 {
return errors.New("no meshes to export")
}
if err := e.prepareTextures(); err != nil {
return err
return errors.Wrap(err, "failed to prepare textures")
}
if e.onlyBaseMesh {
logger.Infof("Export Base Model: %s", model.Name)
doc, err := e.generate(model, 0)
if err != nil {
return err
return errors.Wrap(err, "failed to generate base model")
}
if err = gltf.Save(doc, path.Join(basePath, fmt.Sprintf("%s-base-model.gltf", model.Name))); err != nil {
return err
outputPath := path.Join(basePath, fmt.Sprintf("%s-base-model.gltf", model.Name))
if err = gltf.Save(doc, outputPath); err != nil {
return errors.Wrapf(err, "failed to save base model to %s", outputPath)
}
}
if e.animation {
dirPath := path.Join(basePath, "animation", model.Name)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
if err = os.MkdirAll(dirPath, os.ModeDir); err != nil {
return err
return errors.Wrapf(err, "failed to create directory %s", dirPath)
}
}
logger.Infof("Export Animation to %s", dirPath)
@ -93,10 +94,11 @@ func (e *Exporter) Export(basePath string, model *threedb.Model) error {
}
doc, err := e.generate(model, indexes...)
if err != nil {
return err
return errors.Wrapf(err, "failed to generate animation %s", animation.Name)
}
if err = gltf.Save(doc, path.Join(dirPath, fmt.Sprintf("%s-%s.gltf", model.Name, animation.Name))); err != nil {
return err
outputPath := path.Join(dirPath, fmt.Sprintf("%s-%s.gltf", model.Name, animation.Name))
if err = gltf.Save(doc, outputPath); err != nil {
return errors.Wrapf(err, "failed to save animation %s to %s", animation.Name, outputPath)
}
}
}
@ -206,9 +208,11 @@ func (e *Exporter) prepareTextures() error {
if e.texturesBasePath == nil {
return nil
}
if err := filepath.Walk(*e.texturesBasePath, func(pathFile string, info os.FileInfo, err error) error {
basePath := *e.texturesBasePath
if err := filepath.Walk(basePath, func(pathFile string, info os.FileInfo, err error) error {
if err != nil {
return err
return errors.Wrapf(err, "failed to access path %s", pathFile)
}
if strings.HasSuffix(info.Name(), ".tga") {
key := strings.TrimSuffix(info.Name(), ".tga")
@ -223,7 +227,7 @@ func (e *Exporter) prepareTextures() error {
}
return nil
}); err != nil {
return err
return errors.Wrapf(err, "failed to walk textures directory %s", basePath)
}
return nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/qmuntal/gltf"
"github.com/qmuntal/gltf/modeler"
"git.influ.su/artmares/digglestool/internal/pkg/errors"
"git.influ.su/artmares/digglestool/internal/pkg/logger"
"git.influ.su/artmares/digglestool/pkg/threedb"
)
@ -21,21 +22,35 @@ type Model struct {
}
func (m *Model) Generate(model *threedb.Model, cache *Cache[*CacheItem], meshIndexes ...int) (*gltf.Document, error) {
if model == nil {
return nil, errors.ErrInvalidInput
}
m.doc = gltf.NewDocument()
m.doc.Scenes = []*gltf.Scene{}
m.materialMap = make(map[uint16]int)
logger.Info("Generate use mesh indexes", len(meshIndexes))
for i, meshIndex := range meshIndexes {
if meshIndex >= len(model.Meshes) {
return nil, errors.Wrapf(errors.ErrInvalidInput, "mesh index %d out of range (max: %d)",
meshIndex, len(model.Meshes)-1)
}
mesh := model.Meshes[meshIndex]
var nodeIndexes []int
for index, link := range mesh.Links {
materialIndex, err := m.materialIndex(model, cache, link.Material)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed to get material index for mesh %d link %d",
meshIndex, index)
}
verticesIndex := m.generateVertices(model.Points[link.Points])
textureCoordIndex := m.generateTextureCoordinate(model.TextureCoordinates[link.TextureCoordinates])
trianglesIndex := modeler.WriteAccessor(m.doc, gltf.TargetArrayBuffer, model.Triangles[link.Triangles])
m.doc.Meshes = append(m.doc.Meshes, &gltf.Mesh{
Name: fmt.Sprintf("%s-%d-%d", model.Name, i, index),
Primitives: []*gltf.Primitive{{
@ -47,16 +62,19 @@ func (m *Model) Generate(model *threedb.Model, cache *Cache[*CacheItem], meshInd
Material: gltf.Index(materialIndex),
}},
})
nodeIndexes = append(nodeIndexes, len(m.doc.Nodes))
m.doc.Nodes = append(m.doc.Nodes, &gltf.Node{
Mesh: gltf.Index(len(m.doc.Meshes) - 1),
})
}
m.doc.Scenes = append(m.doc.Scenes, &gltf.Scene{
Name: fmt.Sprintf("Root Scene-%d", meshIndex),
Nodes: nodeIndexes,
})
}
m.doc.Samplers = []*gltf.Sampler{{}}
return m.doc, nil
}
@ -80,47 +98,45 @@ func (m *Model) generateTextureCoordinate(coordinates []threedb.Coordinate) int
}
func (m *Model) materialIndex(model *threedb.Model, cache *Cache[*CacheItem], meshMaterialIndex uint16) (int, error) {
//logger, logPutter := helpers.NewBuffer()
//defer func() {
// logPutter(logger)
//}()
materialIndex, ok := m.materialMap[meshMaterialIndex]
//logger.WriteString(fmt.Sprintf("Try find material index %d\n", meshMaterialIndex))
if !ok {
//logger.WriteString(fmt.Sprintf("Material index %d not found\n", meshMaterialIndex))
material := model.Materials[meshMaterialIndex]
item, ok := cache.Get(material.Name)
if !ok {
return 0, nil
// Instead of returning nil, return a more descriptive error
return 0, errors.New("material not found in cache: " + material.Name)
}
//logger.WriteString(fmt.Sprintf("Try found material name %s\n", material.Name))
if item.Bytes == nil {
//logger.WriteString(fmt.Sprintf("Material name %s not found in cache\n", material.Name))
f, err := os.Open(item.Path)
if err != nil {
return 0, err
return 0, errors.Wrapf(err, "failed to open texture file: %s", item.Path)
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to decode image")
}
item.Bytes = &bytes.Buffer{}
if err = png.Encode(item.Bytes, img); err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to encode image to PNG")
}
cache.Set(material.Name, item)
}
//log.Printf("material index: %d, material name: %s, data: %d", meshMaterialIndex, material.Name, len(data))
imageIndex, err := modeler.WriteImage(m.doc, material.Name, "image/png", item.Bytes)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to write image to GLTF document")
}
m.doc.Textures = append(m.doc.Textures, &gltf.Texture{
Name: material.Name,
Sampler: gltf.Index(0),
Source: gltf.Index(imageIndex),
})
materialIndex = len(m.doc.Materials)
m.doc.Materials = append(m.doc.Materials, &gltf.Material{
Name: material.Name,
@ -135,7 +151,6 @@ func (m *Model) materialIndex(model *threedb.Model, cache *Cache[*CacheItem], me
})
m.materialMap[meshMaterialIndex] = materialIndex
}
//log.Println(logger.String())
return materialIndex, nil
}

View File

@ -6,9 +6,9 @@ package threedb
import (
"encoding/binary"
"errors"
"io"
"git.influ.su/artmares/digglestool/internal/pkg/errors"
"git.influ.su/artmares/digglestool/internal/pkg/logger"
)
@ -47,35 +47,35 @@ func (dec *Decoder) Decode(model *Model) error {
// Read file header information
if model.DBVersion, err = dec.readString(); err != nil {
return err
return errors.Wrap(err, "failed to read DB version")
}
if model.Name, err = dec.readString(); err != nil {
return err
return errors.Wrap(err, "failed to read model name")
}
// Read all model components in the order they appear in the file
if err = dec.readMaterials(model); err != nil {
return err
return errors.Wrap(err, "failed to read materials")
}
if err = dec.readMeshes(model); err != nil {
return err
return errors.Wrap(err, "failed to read meshes")
}
if err = dec.readObjects(model); err != nil {
return err
return errors.Wrap(err, "failed to read objects")
}
if err = dec.readAnimations(model); err != nil {
return err
return errors.Wrap(err, "failed to read animations")
}
if err = dec.readShadows(model); err != nil {
return err
return errors.Wrap(err, "failed to read shadows")
}
if err = dec.readCubeMaps(model); err != nil {
return err
return errors.Wrap(err, "failed to read cube maps")
}
// Read the actual geometry data (triangles, texture coordinates, vertices, etc.)
if err = dec.readData(model); err != nil {
return err
return errors.Wrap(err, "failed to read geometry data")
}
return nil
@ -129,13 +129,13 @@ func (dec *Decoder) readString() (string, error) {
// First read the length of the string
length, err := dec.readUInt32()
if err != nil {
return "", err
return "", errors.Wrap(err, "failed to read string length")
}
// Then read the string data
buf := make([]byte, length)
if err = dec.read(&buf); err != nil {
return "", err
return "", errors.Wrap(err, "failed to read string data")
}
return string(buf), nil
@ -150,13 +150,13 @@ func (dec *Decoder) readVector() (*Vector, error) {
// Read the x, y, and z components of the vector
if vec[0], err = dec.readFloat32(); err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to read vector X component")
}
if vec[1], err = dec.readFloat32(); err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to read vector Y component")
}
if vec[2], err = dec.readFloat32(); err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to read vector Z component")
}
return &vec, nil