Skip to content

Commit

Permalink
pgtf start
Browse files Browse the repository at this point in the history
  • Loading branch information
EliCDavis committed Apr 23, 2024
1 parent d2d83a1 commit f5bc5cb
Show file tree
Hide file tree
Showing 19 changed files with 866 additions and 315 deletions.
4 changes: 4 additions & 0 deletions examples/edit-gaussian-splats/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ type InfoNodeData struct {
}

func (in InfoNodeData) Process() (string, error) {
if in.Original == nil || in.Final == nil {
return "", nil
}

original := in.Original.Value().AttributeLength()
final := in.Final.Value().AttributeLength()

Expand Down
3 changes: 3 additions & 0 deletions formats/pgtf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Polyform Graph Transmission Format (PGTF)

Taking a million notes out of GLTF and making a JSON format that can reference binaries either internally or externally.
27 changes: 27 additions & 0 deletions formats/pgtf/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pgtf

import "io"

type format = typedFormat[any]

type buffer struct {
ByteLength int `json:"byteLength"` // The length of the buffer in bytes.
URI string `json:"uri,omitempty"` // The URI (or IRI) of the buffer. Relative paths are relative to the current glTF asset. Instead of referencing an external file, this field **MAY** contain a `data:`-URI.
}

type bufferView struct {
Buffer int `json:"buffer"` // The index of the buffer
ByteOffset int `json:"byteOffset,omitempty"` // The offset into the buffer in bytes.
ByteLength int `json:"byteLength"` // The length of the bufferView in bytes.
}

type typedFormat[T any] struct {
Buffers []buffer `json:"buffers,omitempty"`
BufferViews []bufferView `json:"bufferViews,omitempty"`
Data T `json:"data"`
}

type PgtfSerializable interface {
Deserialize(io.Reader) error
Serialize(io.Writer) error
}
97 changes: 97 additions & 0 deletions formats/pgtf/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package pgtf

import (
"bytes"
"encoding/base64"
"encoding/json"
"reflect"
)

func isStruct(v any) bool {
value := reflect.ValueOf(v)

for value.Kind() == reflect.Pointer {
value = value.Elem()
}

return value.Kind() == reflect.Struct
}

func buildBufferView(buf *bytes.Buffer, currentStructure map[string]any, v any, f *format) error {
value := reflect.ValueOf(v)

for value.Kind() == reflect.Pointer {
value = value.Elem()
}

if value.Kind() != reflect.Struct {
return nil
}

valueType := value.Type()
for i := 0; i < valueType.NumField(); i++ {
viewFieldValue := value.Field(i)
structField := valueType.Field(i)

if viewFieldValue.CanInterface() {

i := viewFieldValue.Interface()
perm, ok := i.(PgtfSerializable)
if !ok {
currentStructure[structField.Name] = i
continue
}

if perm == nil {
continue
}

start := buf.Len()
err := perm.Serialize(buf)
if err != nil {
return err
}

currentStructure["$"+structField.Name] = len(f.BufferViews)

f.BufferViews = append(f.BufferViews, bufferView{
Buffer: 0,
ByteOffset: start,
ByteLength: buf.Len() - start,
})

continue
}
}

return nil
}

func Marshal(v any) ([]byte, error) {
structure := &format{
Data: v,
Buffers: make([]buffer, 0),
BufferViews: make([]bufferView, 0),
}

buf := &bytes.Buffer{}

if isStruct(v) {
data := make(map[string]any)
err := buildBufferView(buf, data, v, structure)
if err != nil {
return nil, err
}
structure.Data = data
}

if len(structure.BufferViews) > 0 {
bufData := buf.Bytes()
structure.Buffers = append(structure.Buffers, buffer{
ByteLength: len(bufData),
URI: "data:application/octet-stream;base64," + base64.StdEncoding.EncodeToString(bufData),
})
}

return json.MarshalIndent(structure, "", "\t")
}
176 changes: 176 additions & 0 deletions formats/pgtf/marshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package pgtf_test

import (
"encoding/binary"
"io"
"testing"

"github.com/EliCDavis/polyform/formats/pgtf"
"github.com/stretchr/testify/assert"
)

type basicStruct struct {
A int
B bool
C string
}

type pgtfSerializableStruct struct {
data int32
}

func (pss *pgtfSerializableStruct) Deserialize(in io.Reader) (err error) {
data := make([]byte, 4)
_, err = io.ReadFull(in, data)
i := binary.LittleEndian.Uint32(data)
pss.data = int32(i)
return
}

func (pss pgtfSerializableStruct) Serialize(out io.Writer) (err error) {
bytes := make([]byte, 4)
binary.LittleEndian.PutUint32(bytes, uint32(pss.data))
_, err = out.Write(bytes)
return
}

type embeddedBinaryStruct struct {
Basic basicStruct
Serializable *pgtfSerializableStruct
}

type multipleEmbeddedBinaryStruct struct {
Basic basicStruct
SerializableA *pgtfSerializableStruct
SerializableB *pgtfSerializableStruct
}

// TESTING ====================================================================

type testCase interface {
Run(t *testing.T)
}

type typedTestCase[T any] struct {
input T
want string
}

func (tc typedTestCase[T]) Run(t *testing.T) {
out, err := pgtf.Marshal(tc.input)
assert.NoError(t, err)
assert.Equal(t, tc.want, string(out))

v, err := pgtf.Unmarshal[T](out)
assert.NoError(t, err)
assert.Equal(t, tc.input, v)
}

func TestMarshal(t *testing.T) {

tests := map[string]testCase{
"single bool: True": typedTestCase[bool]{
input: true,
want: `{
"data": true
}`,
},
"single bool: False": typedTestCase[bool]{
input: false,
want: `{
"data": false
}`,
},
"single int: 123": typedTestCase[int]{
input: 123,
want: `{
"data": 123
}`,
},
"single string: bababa": typedTestCase[string]{
input: "bababa",
want: `{
"data": "bababa"
}`,
},
"basic struct": typedTestCase[basicStruct]{
input: basicStruct{A: 123, B: true, C: "yee haw"},
want: `{
"data": {
"A": 123,
"B": true,
"C": "yee haw"
}
}`,
},
"embedded binary": typedTestCase[embeddedBinaryStruct]{
input: embeddedBinaryStruct{
Basic: basicStruct{A: 123, B: true, C: "yee haw"},
Serializable: &pgtfSerializableStruct{data: 12345},
},
want: `{
"buffers": [
{
"byteLength": 4,
"uri": "data:application/octet-stream;base64,OTAAAA=="
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 4
}
],
"data": {
"$Serializable": 0,
"Basic": {
"A": 123,
"B": true,
"C": "yee haw"
}
}
}`,
},
"multiple embedded binary": typedTestCase[multipleEmbeddedBinaryStruct]{
input: multipleEmbeddedBinaryStruct{
Basic: basicStruct{A: 123, B: true, C: "yee haw"},
SerializableA: &pgtfSerializableStruct{data: 12345},
SerializableB: &pgtfSerializableStruct{data: 67890},
},
want: `{
"buffers": [
{
"byteLength": 8,
"uri": "data:application/octet-stream;base64,OTAAADIJAQA="
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 4
},
{
"buffer": 0,
"byteOffset": 4,
"byteLength": 4
}
],
"data": {
"$SerializableA": 0,
"$SerializableB": 1,
"Basic": {
"A": 123,
"B": true,
"C": "yee haw"
}
}
}`,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
tc.Run(t)
})
}
}
Loading

0 comments on commit f5bc5cb

Please sign in to comment.