mirror of
https://github.com/gonum/gonum.git
synced 2025-10-07 16:11:03 +08:00
spatial/r3: add Triangle
This commit is contained in:
203
spatial/r3/triangle_test.go
Normal file
203
spatial/r3/triangle_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright ©2022 The Gonum Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package r3
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
func TestTriangleDegenerate(t *testing.T) {
|
||||
const (
|
||||
// tol is how much closer the problematic
|
||||
// vertex is placed to avoid floating point error
|
||||
// for degeneracy calculation.
|
||||
tol = 1e-12
|
||||
// This is the argument to Degenerate and represents
|
||||
// the minimum permissible distance between the triangle
|
||||
// longest edge and the opposite vertex.
|
||||
spatialTol = 1e-2
|
||||
)
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 200; i++ {
|
||||
// Generate a random line for the longest triangle side.
|
||||
ln := line{randomVec(rnd), randomVec(rnd)}
|
||||
lineDir := Sub(ln[1], ln[0])
|
||||
|
||||
perpendicular := Unit(Cross(lineDir, randomVec(rnd)))
|
||||
// generate 3 permutations of needle triangles for
|
||||
// each vertex. A needle triangle has two vertices
|
||||
// very close to eachother an its third vertex far away.
|
||||
var needle Triangle
|
||||
for j := 0; j < 3; j++ {
|
||||
needle[j] = ln[0]
|
||||
needle[(j+1)%3] = ln[1]
|
||||
needle[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, perpendicular))
|
||||
if !needle.IsDegenerate(spatialTol) {
|
||||
t.Error("needle triangle not degenerate")
|
||||
}
|
||||
}
|
||||
|
||||
midpoint := ln.vecOnLine(0.5)
|
||||
// cap triangles are characterized by having two sides
|
||||
// of similar lengths and whose sum is approximately equal
|
||||
// to the remaining longest side.
|
||||
var cap Triangle
|
||||
for j := 0; j < 3; j++ {
|
||||
cap[j] = ln[0]
|
||||
cap[(j+1)%3] = ln[1]
|
||||
cap[(j+2)%3] = Add(midpoint, Scale((1-tol)*spatialTol, perpendicular))
|
||||
if !cap.IsDegenerate(spatialTol) {
|
||||
t.Error("cap triangle not degenerate")
|
||||
}
|
||||
}
|
||||
|
||||
var degenerate Triangle
|
||||
for j := 0; j < 3; j++ {
|
||||
degenerate[j] = ln[0]
|
||||
degenerate[(j+1)%3] = ln[1]
|
||||
// vertex perpendicular to some random point on longest side.
|
||||
degenerate[(j+2)%3] = Add(ln.vecOnLine(rnd.Float64()), Scale((1-tol)*spatialTol, perpendicular))
|
||||
if !degenerate.IsDegenerate(spatialTol) {
|
||||
t.Error("random degenerate triangle not degenerate")
|
||||
}
|
||||
// vertex about longest side 0 vertex
|
||||
degenerate[(j+2)%3] = Add(ln[0], Scale((1-tol)*spatialTol, Unit(randomVec(rnd))))
|
||||
if !degenerate.IsDegenerate(spatialTol) {
|
||||
t.Error("needle-like degenerate triangle not degenerate")
|
||||
}
|
||||
// vertex about longest side 1 vertex
|
||||
degenerate[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, Unit(randomVec(rnd))))
|
||||
if !degenerate.IsDegenerate(spatialTol) {
|
||||
t.Error("needle-like degenerate triangle not degenerate")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriangleCentroid(t *testing.T) {
|
||||
const tol = 1e-12
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
tri := randomTriangle(rnd)
|
||||
got := tri.Centroid()
|
||||
want := Vec{
|
||||
X: (tri[0].X + tri[1].X + tri[2].X) / 3,
|
||||
Y: (tri[0].Y + tri[1].Y + tri[2].Y) / 3,
|
||||
Z: (tri[0].Z + tri[1].Z + tri[2].Z) / 3,
|
||||
}
|
||||
if !vecApproxEqual(got, want, tol) {
|
||||
t.Fatalf("got %.6g, want %.6g", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriangleNormal(t *testing.T) {
|
||||
const tol = 1e-12
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
tri := randomTriangle(rnd)
|
||||
got := tri.Normal()
|
||||
expect := goldenNormal(tri)
|
||||
if !vecApproxEqual(got, expect, tol) {
|
||||
t.Fatalf("got %.6g, want %.6g", got, expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriangleArea(t *testing.T) {
|
||||
const tol = 1e-16
|
||||
for _, test := range []struct {
|
||||
T Triangle
|
||||
Expect float64
|
||||
}{
|
||||
{
|
||||
T: Triangle{
|
||||
{0, 0, 0},
|
||||
{1, 0, 0},
|
||||
{0, 1, 0},
|
||||
},
|
||||
Expect: 0.5,
|
||||
},
|
||||
{
|
||||
T: Triangle{
|
||||
{1, 0, 0},
|
||||
{0, 1, 0},
|
||||
{0, 0, 0},
|
||||
},
|
||||
Expect: 0.5,
|
||||
},
|
||||
{
|
||||
T: Triangle{
|
||||
{20, 0, 0},
|
||||
{0, 0, 20},
|
||||
{0, 0, 0},
|
||||
},
|
||||
Expect: 20 * 20 / 2,
|
||||
},
|
||||
} {
|
||||
got := test.T.Area()
|
||||
if math.Abs(got-test.Expect) > tol {
|
||||
t.Errorf("got area %g, expected %g", got, test.Expect)
|
||||
}
|
||||
if test.T.IsDegenerate(tol) {
|
||||
t.Error("well-formed triangle is degenerate")
|
||||
}
|
||||
}
|
||||
const tol2 = 1e-12
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100; i++ {
|
||||
tri := randomTriangle(rnd)
|
||||
got := tri.Area()
|
||||
want := Norm(Cross(Sub(tri[1], tri[0]), Sub(tri[2], tri[0]))) / 2
|
||||
if math.Abs(got-want) > tol2 {
|
||||
t.Errorf("got area %g not match half norm of cross product %g", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriangleOrderedLengths(t *testing.T) {
|
||||
rnd := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 200; i++ {
|
||||
tri := randomTriangle(rnd)
|
||||
s1, s2, s3 := tri.sides()
|
||||
l1 := Norm(s1)
|
||||
l2 := Norm(s2)
|
||||
l3 := Norm(s3)
|
||||
a, b, c := tri.orderedLengths()
|
||||
if a != l1 && a != l2 && a != l3 {
|
||||
t.Error("shortest ordered length not a side of the triangle")
|
||||
}
|
||||
if b != l1 && b != l2 && b != l3 {
|
||||
t.Error("middle ordered length not a side of the triangle")
|
||||
}
|
||||
if c != l1 && c != l2 && c != l3 {
|
||||
t.Error("longest ordered length not a side of the triangle")
|
||||
}
|
||||
if a > b || a > c {
|
||||
t.Error("ordered short side not shortest side")
|
||||
}
|
||||
if c < b {
|
||||
t.Error("ordered long side not longest side")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// taken from soypat/sdf library where it has been thoroughly tested empirically.
|
||||
func goldenNormal(t Triangle) Vec {
|
||||
e1 := Sub(t[1], t[0])
|
||||
e2 := Sub(t[2], t[0])
|
||||
return Cross(e1, e2)
|
||||
}
|
||||
|
||||
func randomTriangle(rnd *rand.Rand) Triangle {
|
||||
return Triangle{
|
||||
randomVec(rnd),
|
||||
randomVec(rnd),
|
||||
randomVec(rnd),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user