Repository: qmuntal/gltf
Pull request #5 (2019-03-25 10:38:51)

add extensions to specific packages
- Implement mechanism to define custom extensions
- Create new package with specular extension


Patch:
diff --git a/doc.go b/doc.go
index 743fdd9..23255a8 100644
--- a/doc.go
+++ b/doc.go
@@ -1,6 +1,5 @@
+// Package gltf implements a glTF 2.0 file decoder, encoder and validator.
 /*
-	Package gltf implements a glTF 2.0 file decoder, encoder and validator.
-
-	The glTF 2.0 specification is at https://github.com/KhronosGroup/glTF/tree/master/specification/2.0/.
+The glTF 2.0 specification is at https://github.com/KhronosGroup/glTF/tree/master/specification/2.0/.
 */
 package gltf
diff --git a/extensions.go b/extensions.go
deleted file mode 100644
index 4b01ec4..0000000
--- a/extensions.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package gltf
-
-import "encoding/json"
-
-const (
-	ExtPBRSpecularGlossiness = "KHR_materials_pbrSpecularGlossiness"
-)
-
-// Extension is map where the keys are the extension identifiers and the values are the extensions payloads.
-// If a key matches with one of the supported extensions the value will be marshalled as a pointer to the extension struct.
-// If a key does not match with any of the supported extensions the value will be a json.RawMessage so its decoding can be delayed.
-type Extensions map[string]interface{}
-
-type envelope map[string]json.RawMessage
-
-// UnmarshalJSON unmarshal the extensions with the supported extensions initialized.
-func (ext *Extensions) UnmarshalJSON(data []byte) error {
-	if len(*ext) == 0 {
-		*ext = make(Extensions)
-	}
-	var raw envelope
-	err := json.Unmarshal(data, &raw)
-	if err == nil {
-		for key, value := range raw {
-			switch key {
-			case ExtPBRSpecularGlossiness:
-				n := &PBRSpecularGlossiness{}
-				if err := json.Unmarshal(value, n); err != nil {
-					return err
-				}
-				(*ext)[ExtPBRSpecularGlossiness] = n
-			default:
-				(*ext)[key] = value
-			}
-		}
-	}
-
-	return err
-}
-
-type PBRSpecularGlossiness struct {
-	DiffuseFactor             [4]float64   `json:"diffuseFactor" validate:"dive,gte=0,lte=1"`
-	DiffuseTexture            *TextureInfo `json:"diffuseTexture,omitempty"`
-	SpecularFactor            [3]float64   `json:"specularFactor" validate:"dive,gte=0,lte=1"`
-	GlossinessFactor          float64      `json:"glossinessFactor" validate:"gte=0,lte=1"`
-	SpecularGlossinessTexture *TextureInfo `json:"specularGlossinessTexture,omitempty"`
-}
-
-// PBRSpecularGlossiness returns a default PBRSpecularGlossiness.
-func NewPBRSpecularGlossiness() *PBRSpecularGlossiness {
-	return &PBRSpecularGlossiness{DiffuseFactor: [4]float64{1, 1, 1, 1}, SpecularFactor: [3]float64{1, 1, 1}, GlossinessFactor: 1}
-}
-
-// UnmarshalJSON unmarshal the pbr with the correct default values.
-func (p *PBRSpecularGlossiness) UnmarshalJSON(data []byte) error {
-	type alias PBRSpecularGlossiness
-	tmp := alias(*NewPBRSpecularGlossiness())
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*p = PBRSpecularGlossiness(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the pbr with the correct default values.
-func (p *PBRSpecularGlossiness) MarshalJSON() ([]byte, error) {
-	type alias PBRSpecularGlossiness
-	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(p)})
-	if err == nil {
-		if p.GlossinessFactor == 1 {
-			out = removeProperty([]byte(`"glossinessFactor":1`), out)
-		}
-		if p.DiffuseFactor == [4]float64{1, 1, 1, 1} {
-			out = removeProperty([]byte(`"diffuseFactor":[1,1,1,1]`), out)
-		}
-		if p.SpecularFactor == [3]float64{1, 1, 1} {
-			out = removeProperty([]byte(`"specularFactor":[1,1,1]`), out)
-		}
-		out = sanitizeJSON(out)
-	}
-	return out, err
-}
diff --git a/specular/specular.go b/specular/specular.go
new file mode 100644
index 0000000..7eb8374
--- /dev/null
+++ b/specular/specular.go
@@ -0,0 +1,71 @@
+package specular
+
+import (
+	"bytes"
+	"encoding/json"
+
+	"github.com/qmuntal/gltf"
+)
+
+const (
+	// ExtPBRSpecularGlossiness defines the PBRSpecularGlossiness unique key.
+	ExtPBRSpecularGlossiness = "KHR_materials_pbrSpecularGlossiness"
+)
+
+// New returns a new specular.PBRSpecularGlossiness.
+func New() json.Unmarshaler {
+	return new(PBRSpecularGlossiness)
+}
+
+func init() {
+	gltf.RegisterExtension(ExtPBRSpecularGlossiness, New)
+}
+
+// PBRSpecularGlossiness defines a specular-glossiness material model.
+type PBRSpecularGlossiness struct {
+	DiffuseFactor             *gltf.RGBA        `json:"diffuseFactor,omitempty"`
+	DiffuseTexture            *gltf.TextureInfo `json:"diffuseTexture,omitempty"`
+	SpecularFactor            *gltf.RGB         `json:"specularFactor,omitempty"`
+	GlossinessFactor          *float64          `json:"glossinessFactor,omitempty" validate:"omitempty,gte=0,lte=1"`
+	SpecularGlossinessTexture *gltf.TextureInfo `json:"specularGlossinessTexture,omitempty"`
+}
+
+// UnmarshalJSON unmarshal the pbr with the correct default values.
+func (p *PBRSpecularGlossiness) UnmarshalJSON(data []byte) error {
+	type alias PBRSpecularGlossiness
+	tmp := alias(PBRSpecularGlossiness{DiffuseFactor: gltf.NewRGBA(), SpecularFactor: gltf.NewRGB(), GlossinessFactor: gltf.Float64(1)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*p = PBRSpecularGlossiness(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the pbr with the correct default values.
+func (p *PBRSpecularGlossiness) MarshalJSON() ([]byte, error) {
+	type alias PBRSpecularGlossiness
+	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(p)})
+	if err == nil {
+		if p.GlossinessFactor != nil && *p.GlossinessFactor == 1 {
+			out = removeProperty([]byte(`"glossinessFactor":1`), out)
+		}
+		if p.DiffuseFactor != nil && *p.DiffuseFactor == *gltf.NewRGBA() {
+			out = removeProperty([]byte(`"diffuseFactor":[1,1,1,1]`), out)
+		}
+		if p.SpecularFactor != nil && *p.SpecularFactor == *gltf.NewRGB() {
+			out = removeProperty([]byte(`"specularFactor":[1,1,1]`), out)
+		}
+		out = sanitizeJSON(out)
+	}
+	return out, err
+}
+
+func removeProperty(str []byte, b []byte) []byte {
+	b = bytes.Replace(b, str, []byte(""), 1)
+	return bytes.Replace(b, []byte(`,,`), []byte(","), 1)
+}
+
+func sanitizeJSON(b []byte) []byte {
+	b = bytes.Replace(b, []byte(`{,`), []byte("{"), 1)
+	return bytes.Replace(b, []byte(`,}`), []byte("}"), 1)
+}
diff --git a/struct.go b/struct.go
index d4df437..eeb8456 100644
--- a/struct.go
+++ b/struct.go
@@ -193,12 +193,11 @@ func (n *Node) TranslationOrDefault() [3]float64 {
 // UnmarshalJSON unmarshal the node with the correct default values.
 func (n *Node) UnmarshalJSON(data []byte) error {
 	type alias Node
-	def := Node{
+	tmp := alias(Node{
 		Matrix:   DefaultMatrix,
 		Rotation: DefaultRotation,
 		Scale:    DefaultScale,
-	}
-	tmp := alias(def)
+	})
 	err := json.Unmarshal(data, &tmp)
 	if err == nil {
 		*n = Node(tmp)
@@ -455,7 +454,7 @@ func (o *OcclusionTexture) MarshalJSON() ([]byte, error) {
 	return json.Marshal(&struct{ *alias }{alias: (*alias)(o)})
 }
 
-// The RGBA components of the base color of the material.
+// The RGBA components of a color.
 // Each element must be greater than or equal to 0 and less than or equal to 1.
 type RGBA struct {
 	R, G, B, A float64 `validate:"gte=0,lte=1"`
@@ -481,6 +480,32 @@ func (c *RGBA) MarshalJSON() ([]byte, error) {
 	return json.Marshal([4]float64{c.R, c.G, c.B, c.A})
 }
 
+// The RGB components of a color.
+// Each element must be greater than or equal to 0 and less than or equal to 1.
+type RGB struct {
+	R, G, B float64 `validate:"gte=0,lte=1"`
+}
+
+// NewRGB returns a default RGB color.
+func NewRGB() *RGB {
+	return &RGB{1, 1, 1}
+}
+
+// UnmarshalJSON unmarshal the color with the correct default values.
+func (c *RGB) UnmarshalJSON(data []byte) error {
+	tmp := [3]float64{1, 1, 1}
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		c.R, c.G, c.B = tmp[0], tmp[1], tmp[2]
+	}
+	return err
+}
+
+// MarshalJSON marshal the color with the correct default values.
+func (c *RGB) MarshalJSON() ([]byte, error) {
+	return json.Marshal([3]float64{c.R, c.G, c.B})
+}
+
 // PBRMetallicRoughness defines a set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology.
 type PBRMetallicRoughness struct {
 	Extensions               Extensions   `json:"extensions,omitempty"`
@@ -641,6 +666,48 @@ type ChannelTarget struct {
 	Path       TRSProperty `json:"path" validate:"lte=4"`
 }
 
+// Extensions is map where the keys are the extension identifiers and the values are the extensions payloads.
+// If a key matches with one of the supported extensions the value will be marshalled as a pointer to the extension struct.
+// If a key does not match with any of the supported extensions the value will be a json.RawMessage so its decoding can be delayed.
+type Extensions map[string]interface{}
+
+type envelope map[string]json.RawMessage
+
+var extensions = make(map[string]func() json.Unmarshaler)
+
+// RegisterExtension registers a function that returns a new extension of the given
+// byte array. This is intended to be called from the init function in
+// packages that implement extensions.
+func RegisterExtension(key string, f func() json.Unmarshaler) {
+	extensions[key] = f
+}
+
+// UnmarshalJSON unmarshal the extensions with the supported extensions initialized.
+func (ext *Extensions) UnmarshalJSON(data []byte) error {
+	if len(*ext) == 0 {
+		*ext = make(Extensions)
+	}
+	var raw envelope
+	err := json.Unmarshal(data, &raw)
+	if err == nil {
+		for key, value := range raw {
+			if extFactory, ok := extensions[key]; ok {
+				n := extFactory()
+				err := json.Unmarshal(value, n)
+				if err != nil {
+					(*ext)[key] = value
+				} else {
+					(*ext)[key] = n
+				}
+			} else {
+				(*ext)[key] = value
+			}
+		}
+	}
+
+	return err
+}
+
 func removeProperty(str []byte, b []byte) []byte {
 	b = bytes.Replace(b, str, []byte(""), 1)
 	return bytes.Replace(b, []byte(`,,`), []byte(","), 1)


Test patch:
diff --git a/extensions_test.go b/extensions_test.go
deleted file mode 100644
index bd52c0e..0000000
--- a/extensions_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package gltf
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestPBRSpecularGlossiness_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		p       *PBRSpecularGlossiness
-		args    args
-		want    *PBRSpecularGlossiness
-		wantErr bool
-	}{
-		{"default", new(PBRSpecularGlossiness), args{[]byte("{}")}, &PBRSpecularGlossiness{DiffuseFactor: [4]float64{1, 1, 1, 1}, SpecularFactor: [3]float64{1, 1, 1}, GlossinessFactor: 1}, false},
-		{"nodefault", new(PBRSpecularGlossiness), args{[]byte(`{"diffuseFactor": [0.1,0.2,0.3,0.4],"specularFactor":[0.5,0.6,0.7],"glossinessFactor":0.5}`)}, &PBRSpecularGlossiness{
-			DiffuseFactor: [4]float64{0.1, 0.2, 0.3, 0.4}, SpecularFactor: [3]float64{0.5, 0.6, 0.7}, GlossinessFactor: 0.5,
-		}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("PBRSpecularGlossiness.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.p, tt.want) {
-				t.Errorf("PBRSpecularGlossiness.UnmarshalJSON() = %v, want %v", tt.p, tt.want)
-			}
-		})
-	}
-}
-
-func TestPBRSpecularGlossiness_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		p       *PBRSpecularGlossiness
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &PBRSpecularGlossiness{GlossinessFactor: 1, DiffuseFactor: [4]float64{1, 1, 1, 1}, SpecularFactor: [3]float64{1, 1, 1}}, []byte(`{}`), false},
-		{"empty", &PBRSpecularGlossiness{GlossinessFactor: 0, DiffuseFactor: [4]float64{0, 0, 0, 0}, SpecularFactor: [3]float64{0, 0, 0}}, []byte(`{"diffuseFactor":[0,0,0,0],"specularFactor":[0,0,0],"glossinessFactor":0}`), false},
-		{"nodefault", &PBRSpecularGlossiness{GlossinessFactor: 0.5, DiffuseFactor: [4]float64{1, 0.5, 1, 1}, SpecularFactor: [3]float64{1, 1, 0.5}}, []byte(`{"diffuseFactor":[1,0.5,1,1],"specularFactor":[1,1,0.5],"glossinessFactor":0.5}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.p.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("PBRSpecularGlossiness.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("PBRSpecularGlossiness.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestExtensions_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    *Extensions
-		wantErr bool
-	}{
-		{"specularGlossiness", args{[]byte(`{"KHR_materials_pbrSpecularGlossiness": {"diffuseFactor":[1,0.5,1,1],"specularFactor":[1,1,0.5],"glossinessFactor":0.5}}`)}, &Extensions{
-			ExtPBRSpecularGlossiness: &PBRSpecularGlossiness{GlossinessFactor: 0.5, DiffuseFactor: [4]float64{1, 0.5, 1, 1}, SpecularFactor: [3]float64{1, 1, 0.5}},
-		}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			ext := Extensions{}
-			if err := ext.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("Extensions.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(&ext, tt.want) {
-				t.Errorf("PBRSpecularGlossiness.MarshalJSON() = %v, want %v", &ext, tt.want)
-			}
-		})
-	}
-}
diff --git a/specular/specular_test.go b/specular/specular_test.go
new file mode 100644
index 0000000..1dd0a12
--- /dev/null
+++ b/specular/specular_test.go
@@ -0,0 +1,79 @@
+package specular
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+
+	"github.com/qmuntal/gltf"
+)
+
+func TestPBRSpecularGlossiness_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		p       *PBRSpecularGlossiness
+		args    args
+		want    *PBRSpecularGlossiness
+		wantErr bool
+	}{
+		{"default", new(PBRSpecularGlossiness), args{[]byte("{}")}, &PBRSpecularGlossiness{DiffuseFactor: gltf.NewRGBA(), SpecularFactor: gltf.NewRGB(), GlossinessFactor: gltf.Float64(1)}, false},
+		{"nodefault", new(PBRSpecularGlossiness), args{[]byte(`{"diffuseFactor": [0.1,0.2,0.3,0.4],"specularFactor":[0.5,0.6,0.7],"glossinessFactor":0.5}`)}, &PBRSpecularGlossiness{
+			DiffuseFactor: &gltf.RGBA{R: 0.1, G: 0.2, B: 0.3, A: 0.4}, SpecularFactor: &gltf.RGB{R: 0.5, G: 0.6, B: 0.7}, GlossinessFactor: gltf.Float64(0.5),
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("PBRSpecularGlossiness.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.p, tt.want) {
+				t.Errorf("PBRSpecularGlossiness.UnmarshalJSON() = %v, want %v", tt.p, tt.want)
+			}
+		})
+	}
+}
+
+func TestPBRSpecularGlossiness_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		p       *PBRSpecularGlossiness
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &PBRSpecularGlossiness{GlossinessFactor: gltf.Float64(1), DiffuseFactor: gltf.NewRGBA(), SpecularFactor: gltf.NewRGB()}, []byte(`{}`), false},
+		{"empty", &PBRSpecularGlossiness{}, []byte(`{}`), false},
+		{"nodefault", &PBRSpecularGlossiness{GlossinessFactor: gltf.Float64(0.5), DiffuseFactor: &gltf.RGBA{R: 1, G: 0.5, B: 1, A: 1}, SpecularFactor: &gltf.RGB{R: 1, G: 1, B: 0.5}}, []byte(`{"diffuseFactor":[1,0.5,1,1],"specularFactor":[1,1,0.5],"glossinessFactor":0.5}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.p.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("PBRSpecularGlossiness.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("PBRSpecularGlossiness.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestNew(t *testing.T) {
+	tests := []struct {
+		name string
+		want json.Unmarshaler
+	}{
+		{"base", new(PBRSpecularGlossiness)},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := New(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("New() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/struct_test.go b/struct_test.go
index 533b86f..168a403 100644
--- a/struct_test.go
+++ b/struct_test.go
@@ -1,6 +1,7 @@
 package gltf
 
 import (
+	"encoding/json"
 	"reflect"
 	"testing"
 )
@@ -627,3 +628,53 @@ func TestPBRMetallicRoughness_RoughnessFactorOrDefault(t *testing.T) {
 		})
 	}
 }
+
+type fakeExt struct {
+	A int `json:"a"`
+}
+
+func (f *fakeExt) UnmarshalJSON(data []byte) error {
+	type alias fakeExt
+	tmp := alias(fakeExt{})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*f = fakeExt(tmp)
+	}
+	return err
+}
+
+func TestExtensions_UnmarshalJSON(t *testing.T) {
+	RegisterExtension("fake_ext", func() json.Unmarshaler { return new(fakeExt) })
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *Extensions
+		wantErr bool
+	}{
+		{"fake_ext", args{[]byte(`{"fake_ext": {"a":2}}`)}, &Extensions{
+			"fake_ext": &fakeExt{A: 2},
+		}, false},
+		{"err", args{[]byte(`{"fake_ext": {{"a":2}}`)}, &Extensions{}, true},
+		{"errext", args{[]byte(`{"fake_ext": {"a":"incorrect"}}`)}, &Extensions{
+			"fake_ext": json.RawMessage([]byte(`{"a":"incorrect"}`)),
+		}, false},
+		{"noregistered", args{[]byte(`{"fake_ext_1": {"a":"incorrect"}}`)}, &Extensions{
+			"fake_ext_1": json.RawMessage([]byte(`{"a":"incorrect"}`)),
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ext := Extensions{}
+			if err := ext.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Extensions.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(&ext, tt.want) {
+				t.Errorf("PBRSpecularGlossiness.MarshalJSON() = %v, want %v", &ext, tt.want)
+			}
+		})
+	}
+}