diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c07f942..369cf09 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -5,6 +5,7 @@ on: permissions: contents: read + pull-requests: write jobs: Format: @@ -81,4 +82,9 @@ jobs: - name: Test run: | - go test -v ./... \ No newline at end of file + go test -coverprofile=cover.out ./tests/... -coverpkg=./pkg/...,.,./internal/... + + - name: Coverage Report + uses: Jannik-Hm/go-test-coverage-report@v1.1 + with: + coverprofile: cover.out \ No newline at end of file diff --git a/pkg/MeshTypes/mesh.go b/pkg/MeshTypes/mesh.go index 7c8df1c..9b48026 100644 --- a/pkg/MeshTypes/mesh.go +++ b/pkg/MeshTypes/mesh.go @@ -1,5 +1,9 @@ package MeshTypes +import ( + "fmt" +) + type Mesh struct { Triangles []*Triangle } @@ -31,8 +35,13 @@ func (obj *Mesh) RotateAndTranslate(translationMatrix Matrix) { } func (obj *Mesh) calculateBoundingBox() Vector { - min := Vector{} - max := Vector{} + // init with first triangle to prohibit 0 values being min or max + if len(obj.Triangles) == 0 || obj.Triangles[0] == nil || obj.Triangles[0].V0 == nil { + return Vector{} + } + min := obj.Triangles[0].V0.Position + max := obj.Triangles[0].V0.Position + for _, triangle := range obj.Triangles { min = triangle.V0.Position.Min(&min) max = triangle.V0.Position.Max(&max) @@ -50,8 +59,11 @@ func (obj *Mesh) calculateBoundingBox() Vector { } } -func (obj *Mesh) ScaleToDimensions(desiredSize *Vector) { +func (obj *Mesh) ScaleToDimensions(desiredSize *Vector) error { actual := obj.calculateBoundingBox() + if actual.X == 0 && actual.Y == 0 && actual.Z == 0 { + return fmt.Errorf("invalid Mesh with 0 dimension") + } scaling := desiredSize.Div(actual) scaledVectors := make(map[*Vertex]struct{}) for _, triangle := range obj.Triangles { @@ -68,4 +80,5 @@ func (obj *Mesh) ScaleToDimensions(desiredSize *Vector) { scaledVectors[triangle.V2] = struct{}{} } } + return nil } diff --git a/pkg/file_handlers/3ds.go b/pkg/file_handlers/3ds.go index 2301000..3e3bc48 100644 --- a/pkg/file_handlers/3ds.go +++ b/pkg/file_handlers/3ds.go @@ -91,7 +91,10 @@ func Load3DS(fileData *[]byte, desiredSize *MeshTypes.Vector) (*MeshTypes.Mesh, mesh := &MeshTypes.Mesh{Triangles: triangles} if desiredSize != nil { - mesh.ScaleToDimensions(desiredSize) + err := mesh.ScaleToDimensions(desiredSize) + if err != nil { + return nil, err + } } return mesh, nil diff --git a/tests/MeshTypes/matrix_test.go b/tests/MeshTypes/matrix_test.go new file mode 100644 index 0000000..3a3362b --- /dev/null +++ b/tests/MeshTypes/matrix_test.go @@ -0,0 +1,67 @@ +package MeshTypes_Test + +import ( + "reflect" + "testing" + + "github.com/Patch2PDF/GDTF-Mesh-Reader/pkg/MeshTypes" +) + +func TestIdentityMatrix(t *testing.T) { + want := MeshTypes.Matrix{ + X00: 1, X01: 0, X02: 0, X03: 0, + X10: 0, X11: 1, X12: 0, X13: 0, + X20: 0, X21: 0, X22: 1, X23: 0, + X30: 0, X31: 0, X32: 0, X33: 1, + } + if !reflect.DeepEqual(MeshTypes.IdentityMatrix(), want) { + t.Errorf(`IdentityMatrix() Output does not match`) + } +} + +func TestMatrixMul(t *testing.T) { + a := MeshTypes.Matrix{ + X00: 1, X01: 2, X02: 3, X03: 4, + X10: 5, X11: 6, X12: 7, X13: 8, + X20: 9, X21: 10, X22: 11, X23: 12, + X30: 13, X31: 14, X32: 15, X33: 16, + } + b := MeshTypes.Matrix{ + X00: 17, X01: 18, X02: 19, X03: 20, + X10: 21, X11: 22, X12: 23, X13: 24, + X20: 25, X21: 26, X22: 27, X23: 28, + X30: 29, X31: 30, X32: 31, X33: 32, + } + want := MeshTypes.Matrix{ + X00: 250, X01: 260, X02: 270, X03: 280, + X10: 618, X11: 644, X12: 670, X13: 696, + X20: 986, X21: 1028, X22: 1070, X23: 1112, + X30: 1354, X31: 1412, X32: 1470, X33: 1528, + } + if !reflect.DeepEqual(a.Mul(b), want) { + t.Errorf(`Matrix Multiplication Output does not match`) + } +} + +func TestMulPosition(t *testing.T) { + a := MeshTypes.Matrix{ + X00: 1, X01: 2, X02: 3, X03: 4, + X10: 5, X11: 6, X12: 7, X13: 8, + X20: 9, X21: 10, X22: 11, X23: 12, + X30: 13, X31: 14, X32: 15, X33: 16, + } + b := MeshTypes.Vector{ + X: 17, + Y: 18, + Z: 19, + } + want := MeshTypes.Vector{ + X: 114, + Y: 334, + Z: 554, + } + + if !reflect.DeepEqual(a.MulPosition(b), want) { + t.Errorf(`Matrix Vector Multiplication Output does not match`) + } +} diff --git a/tests/MeshTypes/mesh_test.go b/tests/MeshTypes/mesh_test.go new file mode 100644 index 0000000..fda14bd --- /dev/null +++ b/tests/MeshTypes/mesh_test.go @@ -0,0 +1,210 @@ +package MeshTypes_Test + +import ( + "math/rand" + "reflect" + "testing" + + "github.com/Patch2PDF/GDTF-Mesh-Reader/pkg/MeshTypes" +) + +func RandomTriangle() *MeshTypes.Triangle { + return &MeshTypes.Triangle{ + V0: RandomVertex(), + V1: RandomVertex(), + V2: RandomVertex(), + } +} + +func RandomVertex() *MeshTypes.Vertex { + normal := RandomVector() + return &MeshTypes.Vertex{ + Position: RandomVector(), + Normal: &normal, + } +} + +func RandomVector() MeshTypes.Vector { + return MeshTypes.Vector{ + X: rand.Float64(), + Y: rand.Float64(), + Z: rand.Float64(), + } +} + +func TestAddTriangle(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{RandomTriangle()}, + } + b := RandomTriangle() + copy := a.Copy() + copy.AddTriangle(b) + if !reflect.DeepEqual(copy, MeshTypes.Mesh{Triangles: []*MeshTypes.Triangle{a.Triangles[0], b}}) { + t.Errorf("Mesh AddTriangle() Output does not match expected") + } +} + +func TestMeshCopy(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{RandomTriangle()}, + } + copy := a.Copy() + if !(reflect.DeepEqual(a, copy) && &a != ©) { + t.Errorf("Mesh Copy() Output does not match expected") + } +} + +func TestAddMesh(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{RandomTriangle()}, + } + b := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{RandomTriangle()}, + } + copy := a.Copy() + result := copy.Add(&b) + if !reflect.DeepEqual(*result, MeshTypes.Mesh{Triangles: []*MeshTypes.Triangle{a.Triangles[0], b.Triangles[0]}}) { + t.Errorf("Mesh Add() Output does not match expected") + } +} + +func TestRotateAndTranslate(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{RandomTriangle()}, + } + translationMatrix := MeshTypes.Matrix{ + X00: 1, X01: -1, X02: 5, X03: -20, + X10: 1, X11: 2, X12: -3, X13: 10, + X20: 1, X21: 5, X22: 3, X23: -5, + X30: 1, X31: 6, X32: -6, X33: 4, + } + result := a.Copy() + result.RotateAndTranslate(translationMatrix) + want := a.Copy() + for _, triangle := range want.Triangles { + triangle.V0.Position = translationMatrix.MulPosition(triangle.V0.Position) // safe to use as func is tested in another place + triangle.V1.Position = translationMatrix.MulPosition(triangle.V1.Position) + triangle.V2.Position = translationMatrix.MulPosition(triangle.V2.Position) + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Mesh RotateAndTranslate() Output does not match expected") + } +} + +func TestScaleToDimensions(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{ + { + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: -1, Y: 1, Z: -1}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 1, Y: -3, Z: 1}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 1, Y: 1, Z: 0}}, + }, + }, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + result := a.Copy() + result.ScaleToDimensions(&desiredSize) + want := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{ + { + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: -2, Y: 1, Z: -4}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 2, Y: -3, Z: 4}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 2, Y: 1, Z: 0}}, + }, + }, + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Mesh ScaleToDimensions() Output does not match expected") + } +} + +func TestScaleToDimensionsEmptyMesh(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{}, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + err := a.ScaleToDimensions(&desiredSize) + if err == nil { + t.Errorf("Mesh ScaleToDimensions() did not return error on faulty mesh") + } +} + +func TestScaleToDimensionsNullTriangle(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{nil}, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + err := a.ScaleToDimensions(&desiredSize) + if err == nil { + t.Errorf("Mesh ScaleToDimensions() did not return error on faulty mesh") + } +} + +func TestScaleToDimensionsNullVertex(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{ + { + V0: nil, + V1: nil, + V2: nil, + }, + }, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + err := a.ScaleToDimensions(&desiredSize) + if err == nil { + t.Errorf("Mesh ScaleToDimensions() did not return error on faulty mesh") + } +} + +func TestScaleToDimensions0Size(t *testing.T) { + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{ + { + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + }, + }, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + err := a.ScaleToDimensions(&desiredSize) + if err == nil { + t.Errorf("Mesh ScaleToDimensions() did not return error on faulty mesh") + } +} + +func TestScaleToDimensionsSameVertex(t *testing.T) { + triangle := MeshTypes.Triangle{ + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 0, Y: 0, Z: 0}}, + } + a := MeshTypes.Mesh{ + Triangles: []*MeshTypes.Triangle{ + &triangle, + { + V0: triangle.V0, + V1: triangle.V1, + V2: triangle.V2, + }, + }, + } + desiredSize := MeshTypes.Vector{ + X: 4, Y: 4, Z: 8, + } + err := a.ScaleToDimensions(&desiredSize) + if err == nil { + t.Errorf("Mesh ScaleToDimensions() did not return error on faulty mesh") + } +} diff --git a/tests/MeshTypes/triangle_test.go b/tests/MeshTypes/triangle_test.go new file mode 100644 index 0000000..4791777 --- /dev/null +++ b/tests/MeshTypes/triangle_test.go @@ -0,0 +1,33 @@ +package MeshTypes_Test + +import ( + "reflect" + "testing" + + "github.com/Patch2PDF/GDTF-Mesh-Reader/pkg/MeshTypes" +) + +func TestTriangleNormal(t *testing.T) { + a := MeshTypes.Triangle{ + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 1, Y: 4, Z: 3}, Normal: &MeshTypes.Vector{X: 4, Y: 5, Z: 6}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 4, Y: 8, Z: 10}, Normal: &MeshTypes.Vector{X: 10, Y: 11, Z: 12}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 13, Y: 14, Z: 15}, Normal: &MeshTypes.Vector{X: 16, Y: 17, Z: 18}}, + } + want := MeshTypes.Vector{X: -0.39436910666014113, Y: 0.8604416872584897, Z: -0.3226656327219336} + result := a.Normal() + if !reflect.DeepEqual(result, want) { + t.Error("Triangle Normal() returned value does not match expected value") + } +} + +func TestTriangleCopy(t *testing.T) { + a := MeshTypes.Triangle{ + V0: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 1, Y: 2, Z: 3}, Normal: &MeshTypes.Vector{X: 4, Y: 5, Z: 6}}, + V1: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 7, Y: 8, Z: 9}, Normal: &MeshTypes.Vector{X: 10, Y: 11, Z: 12}}, + V2: &MeshTypes.Vertex{Position: MeshTypes.Vector{X: 13, Y: 14, Z: 15}, Normal: &MeshTypes.Vector{X: 16, Y: 17, Z: 18}}, + } + copy := a.Copy() + if !(reflect.DeepEqual(copy, a) && a.V0 != copy.V0 && a.V1 != copy.V1 && a.V2 != copy.V2) { + t.Error("Triangle Copy() returned value does not match expected value") + } +} diff --git a/tests/MeshTypes/vector_test.go b/tests/MeshTypes/vector_test.go new file mode 100644 index 0000000..cc74691 --- /dev/null +++ b/tests/MeshTypes/vector_test.go @@ -0,0 +1,149 @@ +package MeshTypes_Test + +import ( + "reflect" + "testing" + + "github.com/Patch2PDF/GDTF-Mesh-Reader/pkg/MeshTypes" +) + +func TestVectorAdd(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 2, Z: 3, + } + b := MeshTypes.Vector{ + X: 4, Y: 5, Z: 6, + } + want := MeshTypes.Vector{ + X: 5, Y: 7, Z: 9, + } + if !reflect.DeepEqual(a.Add(b), want) { + t.Error("Vector Add() returned value does not match expected value") + } +} + +func TestVectorSub(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 2, Z: 3, + } + b := MeshTypes.Vector{ + X: 4, Y: 6, Z: 9, + } + want := MeshTypes.Vector{ + X: 3, Y: 4, Z: 6, + } + if !reflect.DeepEqual(b.Sub(a), want) { + t.Error("Vector Sub() returned value does not match expected value") + } +} + +func TestVectorCross(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 2, Z: 4, + } + b := MeshTypes.Vector{ + X: 5, Y: 6, Z: 7, + } + want := MeshTypes.Vector{ + X: -10, Y: 13, Z: -4, + } + result := a.Cross(b) + if !reflect.DeepEqual(result, want) { + t.Error("Vector Cross() returned value does not match expected value") + } +} + +func TestNormalize(t *testing.T) { + a := MeshTypes.Vector{ + X: 3, Y: 4, Z: 12, + } + want := MeshTypes.Vector{ + X: 3.0 / 13, Y: 4.0 / 13, Z: 12.0 / 13, + } + if !reflect.DeepEqual(a.Normalize(), want) { + t.Error("Vector Normalize() returned value does not match expected value") + } +} + +func TestToVertex(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 2, Z: 3, + } + b := MeshTypes.Vector{ + X: 4, Y: 5, Z: 6, + } + wantFirst := MeshTypes.Vertex{ + Position: a, + Normal: nil, + } + if !reflect.DeepEqual(*a.ToVertex(nil), wantFirst) { + t.Error("Vector To Vertex returned value with nil Normal does not match expected value") + } + wantSecond := MeshTypes.Vertex{ + Position: a, + Normal: &b, + } + if !reflect.DeepEqual(*a.ToVertex(&b), wantSecond) || &b != wantSecond.Normal { + t.Error("Vector To Vertex returned value with Normal does not match expected value") + } +} + +func TestMin(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 4, Z: 3, + } + b := MeshTypes.Vector{ + X: 4, Y: 2, Z: 6, + } + want := MeshTypes.Vector{ + X: 1, Y: 2, Z: 3, + } + if !reflect.DeepEqual(a.Min(&b), want) { + t.Error("Vector Min() returned value does not match expected value") + } +} + +func TestMax(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 4, Z: 3, + } + b := MeshTypes.Vector{ + X: 3, Y: 2, Z: 6, + } + want := MeshTypes.Vector{ + X: 3, Y: 4, Z: 6, + } + if !reflect.DeepEqual(a.Max(&b), want) { + t.Error("Vector Max() returned value does not match expected value") + } +} + +func TestMult(t *testing.T) { + a := MeshTypes.Vector{ + X: 1, Y: 4, Z: 3, + } + b := MeshTypes.Vector{ + X: 3, Y: 2, Z: 6, + } + want := MeshTypes.Vector{ + X: 3, Y: 8, Z: 18, + } + if !reflect.DeepEqual(a.Mult(b), want) { + t.Error("Vector Mul() returned value does not match expected value") + } +} + +func TestDiv(t *testing.T) { + a := MeshTypes.Vector{ + X: 6, Y: 8, Z: 24, + } + b := MeshTypes.Vector{ + X: 3, Y: 2, Z: 4, + } + want := MeshTypes.Vector{ + X: 2, Y: 4, Z: 6, + } + if !reflect.DeepEqual(a.Div(b), want) { + t.Error("Vector Div() returned value does not match expected value") + } +} diff --git a/tests/MeshTypes/vertex_test.go b/tests/MeshTypes/vertex_test.go new file mode 100644 index 0000000..856cee4 --- /dev/null +++ b/tests/MeshTypes/vertex_test.go @@ -0,0 +1,36 @@ +package MeshTypes_Test + +import ( + "reflect" + "testing" + + "github.com/Patch2PDF/GDTF-Mesh-Reader/pkg/MeshTypes" +) + +func TestVertexCopy(t *testing.T) { + a := MeshTypes.Vertex{ + Position: MeshTypes.Vector{ + X: 1, + Y: 2, + Z: 3, + }, + Normal: &MeshTypes.Vector{ + X: 4, + Y: 5, + Z: 6, + }, + } + copy := a.Copy() + if &a.Position == ©.Position { + t.Errorf("Vertex Copy func returns same Position Vector") + } + if a.Normal == copy.Normal { + t.Errorf("Vertex Copy func returns same Normal Vector") + } + if !reflect.DeepEqual(a.Position, copy.Position) { + t.Errorf("Vertex Copy func does not return same Position Vector Values") + } + if !reflect.DeepEqual(a.Normal, copy.Normal) { + t.Errorf("Vertex Copy func does not return same Normal Vector Values") + } +}