gonum.org/v1/gonum@v0.14.0/spatial/r3/triangle_test.go (about) 1 // Copyright ©2022 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package r3 6 7 import ( 8 "math" 9 "testing" 10 11 "golang.org/x/exp/rand" 12 ) 13 14 func TestTriangleDegenerate(t *testing.T) { 15 const ( 16 // tol is how much closer the problematic 17 // vertex is placed to avoid floating point error 18 // for degeneracy calculation. 19 tol = 1e-12 20 // This is the argument to Degenerate and represents 21 // the minimum permissible distance between the triangle 22 // longest edge and the opposite vertex. 23 spatialTol = 1e-2 24 ) 25 rnd := rand.New(rand.NewSource(1)) 26 for i := 0; i < 200; i++ { 27 // Generate a random line for the longest triangle side. 28 ln := line{randomVec(rnd), randomVec(rnd)} 29 lineDir := Sub(ln[1], ln[0]) 30 31 perpendicular := Unit(Cross(lineDir, randomVec(rnd))) 32 // generate 3 permutations of needle triangles for 33 // each vertex. A needle triangle has two vertices 34 // very close to eachother an its third vertex far away. 35 var needle Triangle 36 for j := 0; j < 3; j++ { 37 needle[j] = ln[0] 38 needle[(j+1)%3] = ln[1] 39 needle[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, perpendicular)) 40 if !needle.IsDegenerate(spatialTol) { 41 t.Error("needle triangle not degenerate") 42 } 43 } 44 45 midpoint := ln.vecOnLine(0.5) 46 // cap triangles are characterized by having two sides 47 // of similar lengths and whose sum is approximately equal 48 // to the remaining longest side. 49 var cap Triangle 50 for j := 0; j < 3; j++ { 51 cap[j] = ln[0] 52 cap[(j+1)%3] = ln[1] 53 cap[(j+2)%3] = Add(midpoint, Scale((1-tol)*spatialTol, perpendicular)) 54 if !cap.IsDegenerate(spatialTol) { 55 t.Error("cap triangle not degenerate") 56 } 57 } 58 59 var degenerate Triangle 60 for j := 0; j < 3; j++ { 61 degenerate[j] = ln[0] 62 degenerate[(j+1)%3] = ln[1] 63 // vertex perpendicular to some random point on longest side. 64 degenerate[(j+2)%3] = Add(ln.vecOnLine(rnd.Float64()), Scale((1-tol)*spatialTol, perpendicular)) 65 if !degenerate.IsDegenerate(spatialTol) { 66 t.Error("random degenerate triangle not degenerate") 67 } 68 // vertex about longest side 0 vertex 69 degenerate[(j+2)%3] = Add(ln[0], Scale((1-tol)*spatialTol, Unit(randomVec(rnd)))) 70 if !degenerate.IsDegenerate(spatialTol) { 71 t.Error("needle-like degenerate triangle not degenerate") 72 } 73 // vertex about longest side 1 vertex 74 degenerate[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, Unit(randomVec(rnd)))) 75 if !degenerate.IsDegenerate(spatialTol) { 76 t.Error("needle-like degenerate triangle not degenerate") 77 } 78 } 79 } 80 } 81 82 func TestTriangleCentroid(t *testing.T) { 83 const tol = 1e-12 84 rnd := rand.New(rand.NewSource(1)) 85 for i := 0; i < 100; i++ { 86 tri := randomTriangle(rnd) 87 got := tri.Centroid() 88 want := Vec{ 89 X: (tri[0].X + tri[1].X + tri[2].X) / 3, 90 Y: (tri[0].Y + tri[1].Y + tri[2].Y) / 3, 91 Z: (tri[0].Z + tri[1].Z + tri[2].Z) / 3, 92 } 93 if !vecApproxEqual(got, want, tol) { 94 t.Fatalf("got %.6g, want %.6g", got, want) 95 } 96 } 97 } 98 99 func TestTriangleNormal(t *testing.T) { 100 const tol = 1e-12 101 rnd := rand.New(rand.NewSource(1)) 102 for i := 0; i < 100; i++ { 103 tri := randomTriangle(rnd) 104 got := tri.Normal() 105 expect := goldenNormal(tri) 106 if !vecApproxEqual(got, expect, tol) { 107 t.Fatalf("got %.6g, want %.6g", got, expect) 108 } 109 } 110 } 111 112 func TestTriangleArea(t *testing.T) { 113 const tol = 1e-16 114 for _, test := range []struct { 115 T Triangle 116 Expect float64 117 }{ 118 { 119 T: Triangle{ 120 {0, 0, 0}, 121 {1, 0, 0}, 122 {0, 1, 0}, 123 }, 124 Expect: 0.5, 125 }, 126 { 127 T: Triangle{ 128 {1, 0, 0}, 129 {0, 1, 0}, 130 {0, 0, 0}, 131 }, 132 Expect: 0.5, 133 }, 134 { 135 T: Triangle{ 136 {20, 0, 0}, 137 {0, 0, 20}, 138 {0, 0, 0}, 139 }, 140 Expect: 20 * 20 / 2, 141 }, 142 } { 143 got := test.T.Area() 144 if math.Abs(got-test.Expect) > tol { 145 t.Errorf("got area %g, expected %g", got, test.Expect) 146 } 147 if test.T.IsDegenerate(tol) { 148 t.Error("well-formed triangle is degenerate") 149 } 150 } 151 const tol2 = 1e-12 152 rnd := rand.New(rand.NewSource(1)) 153 for i := 0; i < 100; i++ { 154 tri := randomTriangle(rnd) 155 got := tri.Area() 156 want := Norm(Cross(Sub(tri[1], tri[0]), Sub(tri[2], tri[0]))) / 2 157 if math.Abs(got-want) > tol2 { 158 t.Errorf("got area %g not match half norm of cross product %g", got, want) 159 } 160 } 161 } 162 163 func TestTriangleOrderedLengths(t *testing.T) { 164 rnd := rand.New(rand.NewSource(1)) 165 for i := 0; i < 200; i++ { 166 tri := randomTriangle(rnd) 167 s1, s2, s3 := tri.sides() 168 l1 := Norm(s1) 169 l2 := Norm(s2) 170 l3 := Norm(s3) 171 a, b, c := tri.orderedLengths() 172 if a != l1 && a != l2 && a != l3 { 173 t.Error("shortest ordered length not a side of the triangle") 174 } 175 if b != l1 && b != l2 && b != l3 { 176 t.Error("middle ordered length not a side of the triangle") 177 } 178 if c != l1 && c != l2 && c != l3 { 179 t.Error("longest ordered length not a side of the triangle") 180 } 181 if a > b || a > c { 182 t.Error("ordered short side not shortest side") 183 } 184 if c < b { 185 t.Error("ordered long side not longest side") 186 } 187 } 188 } 189 190 // taken from soypat/sdf library where it has been thoroughly tested empirically. 191 func goldenNormal(t Triangle) Vec { 192 e1 := Sub(t[1], t[0]) 193 e2 := Sub(t[2], t[0]) 194 return Cross(e1, e2) 195 } 196 197 func randomTriangle(rnd *rand.Rand) Triangle { 198 return Triangle{ 199 randomVec(rnd), 200 randomVec(rnd), 201 randomVec(rnd), 202 } 203 }