Skip to content

Commit 196a4c6

Browse files
committed
refactor: rotation API changes; testing; refactoring
1 parent 816898e commit 196a4c6

File tree

17 files changed

+712
-865
lines changed

17 files changed

+712
-865
lines changed

md2/polygon.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
math "math"
1212
"github.com/soypat/geometry/internal"
13+
ms1 "github.com/soypat/geometry/md1"
1314
)
1415

1516
type cpAtIdxErr struct {
@@ -208,22 +209,24 @@ func appendArcWithCenter(dst []Vec, start, center Vec, arcAngle float64, facets
208209

209210
func arcCenterFrom2points(p1, p2 Vec, r float64) (Vec, float64, error) {
210211
rabs := math.Abs(r)
212+
semiArcTol := rabs * 1e-3
211213
V12 := Sub(p2, p1)
212214
chordCenter := Add(p1, Scale(0.5, V12))
213215
chordLen := Norm(V12) // Chord length.
214216
maxChordLen := 2 * rabs // Make sure the perimeter of a arc with infinite radius is less than the chord.
217+
218+
// Theta is the opening angle from the center of the arc circle
219+
// to the two chord points.
220+
// Due to chord definition theta/2 is the angle formed
221+
// by the chord and the tangent to the chord point.
222+
sinTheta := chordLen / maxChordLen
215223
if chordLen == 0 {
216224
return Vec{}, 0, errArcCPEqualToPrev
217-
} else if maxChordLen-chordLen < -internal.Smallfloat64 {
225+
} else if maxChordLen-chordLen <= -semiArcTol {
218226
return Vec{}, 0, errBadArc
219-
} else if math.Abs(chordLen/maxChordLen-1) < internal.Smallfloat64 {
227+
} else if ms1.EqualWithinAbs(sinTheta, 1, semiArcTol) {
220228
return Scale(0.5, Add(p1, p2)), math.Copysign(math.Pi/2, r), nil
221229
}
222-
// Theta is the opening angle from the center of the arc circle
223-
// to the two chord points.
224-
// Due to chord definition theta/2 is the angle formed
225-
// by the chord and the tangent to the chord point.
226-
sinTheta := chordLen / (2 * rabs)
227230
chordThetaDiv2 := math.Asin(sinTheta)
228231
diffTo90 := chordThetaDiv2 - math.Pi/2
229232
if math.Abs(diffTo90) < internal.Smallfloat64/10 {

md2/polygon_test.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,28 +141,30 @@ func TestPolygon_IsClockwise(t *testing.T) {
141141
func TestArc_invalidArc(t *testing.T) {
142142
var cases = []struct {
143143
start, end, center Vec
144+
isValid bool
144145
}{
145-
// 180 degree:
146-
// {start: Vec{X: 165.36844, Y: 125.63215}, end: Vec{X: 165.59346, Y: 125.85769}, center: Vec{X: 165.48108, Y: 125.74479}},
147-
//
148-
{start: Vec{X: 168.19885, Y: 129.9802}, end: Vec{X: 167.97331, Y: 129.75517}, center: Vec{X: 168.08597, Y: 129.86781}},
149-
{start: Vec{X: 135.1107, Y: 116.67478}, end: Vec{X: 135.1107, Y: 116.67478}, center: Vec{X: 135.10947, Y: 116.673546}},
150-
{start: Vec{X: -1.05, Y: 149.07}, end: Vec{X: -1.12, Y: 148.7}, center: Vec{X: -1.0500132, Y: 149}},
146+
147+
0: {start: Vec{X: 135.1107, Y: 116.67478}, end: Vec{X: 135.1107, Y: 116.67478}, center: Vec{X: 135.10947, Y: 116.673546}},
148+
1: {start: Vec{X: -1.05, Y: 149.07}, end: Vec{X: -1.12, Y: 148.7}, center: Vec{X: -1.0500132, Y: 149}},
149+
// Close to 180 Degree arcs:
150+
2: {isValid: true, start: Vec{X: -40.52799, Y: -25.628536}, end: Vec{X: -40.88199, Y: -25.628536}, center: Vec{X: -40.704987, Y: -25.628536}},
151+
3: {isValid: true, start: Vec{X: 165.36844, Y: 125.63215}, end: Vec{X: 165.59346, Y: 125.85769}, center: Vec{X: 165.48108, Y: 125.74479}},
152+
4: {isValid: true, start: Vec{X: 168.19885, Y: 129.9802}, end: Vec{X: 167.97331, Y: 129.75517}, center: Vec{X: 168.08597, Y: 129.86781}},
151153
}
152154
var poly PolygonBuilder
153-
for _, test := range cases {
155+
for i, test := range cases {
154156
start, end, center := test.start, test.end, test.center
155157
radius1 := Norm(Sub(start, center))
156158
radius2 := Norm(Sub(end, center))
157159
if math.Abs(radius1-radius2)/radius1 > 0.001 {
158-
t.Log("start/end not equidistant from center:", radius1, radius2)
160+
t.Log("start/end not equidistant from center:", i, radius1, radius2)
159161
}
160162
poly.Reset()
161163
poly.Add(start)
162164
poly.Add(end).Arc(radius1, 3)
163165
vecs, err := poly.AppendVecs(nil)
164-
if err == nil {
165-
t.Error("expected invalid input")
166+
if err == nil && !test.isValid {
167+
t.Error("expected invalid input", i)
166168
for _, v := range vecs {
167169
if v != v {
168170
t.Error("effectively got NaNs")
@@ -173,6 +175,15 @@ func TestArc_invalidArc(t *testing.T) {
173175
t.Errorf("bad radius got=%f want=%f", gotRadius, radius1)
174176
}
175177
}
178+
} else if test.isValid {
179+
if err != nil {
180+
t.Error("error for valid input", i)
181+
}
182+
for _, v := range vecs {
183+
if v != v {
184+
t.Error("NaN output for valid input")
185+
}
186+
}
176187
}
177188
}
178189
}

md3/linalg.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (a Mat3) SVD() (U, S, V Mat3) {
2828
_, qVr := ATA.jacobiEigenanalysis()
2929

3030
// Compute B = A * V
31-
V = RotatingMat3(qVr)
31+
V = qVr.RotationMat3()
3232
b := MulMat3(a, V)
3333

3434
// Sort singular values and adjust V

md3/mat3.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,6 @@ func EqualMat3(a, b Mat3, tolerance float64) bool {
6969
ms1.EqualWithinAbs(a.x22, b.x22, tolerance)
7070
}
7171

72-
// MulPosition multiplies a V2 position with a rotate/translate matrix.
73-
func (a Mat3) mulPosition(x, y float64) (float64, float64) {
74-
return a.x00*x + a.x01*y + a.x02,
75-
a.x10*x + a.x11*y + a.x12
76-
}
77-
7872
// MulMat3 multiplies two 3x3 matrices.
7973
func MulMat3(a, b Mat3) Mat3 {
8074
m := Mat3{}
@@ -258,27 +252,6 @@ func (m Mat3) AsMat4() Mat4 {
258252
}
259253
}
260254

261-
// RotatingMat3 returns a 3×3 rotation matrix corresponding to the receiver. It
262-
// may be used to perform rotations on a 3-vector or to apply the rotation
263-
// to a 3×n matrix of column vectors. If the receiver is not a unit
264-
// quaternion, the returned matrix will not be a pure rotation.
265-
func RotatingMat3(rotationUnit Quat) Mat3 {
266-
w, i, j, k := rotationUnit.W, rotationUnit.I, rotationUnit.J, rotationUnit.K
267-
ii := 2 * i * i
268-
jj := 2 * j * j
269-
kk := 2 * k * k
270-
wi := 2 * w * i
271-
wj := 2 * w * j
272-
wk := 2 * w * k
273-
ij := 2 * i * j
274-
jk := 2 * j * k
275-
ki := 2 * k * i
276-
return mat3(
277-
1-(jj+kk), ij-wk, ki+wj,
278-
ij+wk, 1-(ii+kk), jk-wi,
279-
ki-wj, jk+wi, 1-(ii+jj))
280-
}
281-
282255
// Hessian returns the Hessian matrix of the vector field f at point p.
283256
// step is the step with which the second derivative is calculated.
284257
func Hessian(p Vec, step float64, f func(Vec) float64) Mat3 {

md3/mat4.go

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,6 @@ func ScalingMat4(v Vec) Mat4 {
6464
0, 0, 0, 1}
6565
}
6666

67-
// RotatingMat4 returns an orthographic 4x4 rotation matrix (right hand rule).
68-
func RotatingMat4(angleRadians float64, axis Vec) Mat4 {
69-
axis = Unit(axis)
70-
s, c := math.Sincos(angleRadians)
71-
m := 1 - c
72-
return Mat4{
73-
m*axis.X*axis.X + c, m*axis.X*axis.Y - axis.Z*s, m*axis.Z*axis.X + axis.Y*s, 0,
74-
m*axis.X*axis.Y + axis.Z*s, m*axis.Y*axis.Y + c, m*axis.Y*axis.Z - axis.X*s, 0,
75-
m*axis.Z*axis.X - axis.Y*s, m*axis.Y*axis.Z + axis.X*s, m*axis.Z*axis.Z + c, 0,
76-
0, 0, 0, 1,
77-
}
78-
}
79-
8067
// MulMat4 multiplies two 4x4 matrices and returns the result.
8168
func MulMat4(a, b Mat4) Mat4 {
8269
m := Mat4{}
@@ -222,46 +209,6 @@ func (m Mat4) Array() (rowmajor [16]float64) {
222209
return rowmajor
223210
}
224211

225-
// RotatingBetweenVecsMat4 returns the rotation matrix that transforms "start" onto the same direction as "dest".
226-
func RotatingBetweenVecsMat4(start, dest Vec) Mat4 {
227-
// is either vector == 0?
228-
const epsilon = 1e-12
229-
if EqualElem(start, Vec{}, epsilon) || EqualElem(dest, Vec{}, epsilon) {
230-
return IdentityMat4()
231-
}
232-
// normalize both vectors
233-
start = Unit(start)
234-
dest = Unit(dest)
235-
// are the vectors the same?
236-
if EqualElem(start, dest, epsilon) {
237-
return IdentityMat4()
238-
}
239-
240-
// are the vectors opposite (180 degrees apart)?
241-
if EqualElem(Scale(-1, start), dest, epsilon) {
242-
return Mat4{
243-
-1, 0, 0, 0,
244-
0, -1, 0, 0,
245-
0, 0, -1, 0,
246-
0, 0, 0, 1,
247-
}
248-
}
249-
// general case
250-
// See: https://math.stackexchange.com/questions/180418/calculate-rotation-matrix-to-align-vector-a-to-vector-b-in-3d
251-
v := Cross(start, dest)
252-
vx := Skew(v)
253-
k := 1. / (1. + Dot(start, dest))
254-
255-
vx2 := MulMat3(vx, vx)
256-
vx2 = ScaleMat3(vx2, k)
257-
258-
// Calculate sum of matrices.
259-
vx = AddMat3(vx, IdentityMat3())
260-
vx = AddMat3(vx, vx2)
261-
262-
return vx.AsMat4()
263-
}
264-
265212
// EqualMat4 tests the equality of 4x4 matrices.
266213
func EqualMat4(a, b Mat4, tolerance float64) bool {
267214
return ms1.EqualWithinAbs(a.x00, b.x00, tolerance) &&

md3/md3_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,71 @@ package md3
77
import (
88
"fmt"
99
"math"
10+
"math/rand"
1011
"testing"
12+
13+
"github.com/soypat/geometry/internal"
1114
)
1215

1316
func TestRotation(t *testing.T) {
1417
const tol = 1e-7
1518
v := Vec{X: 1}
16-
y90 := RotatingMat4(math.Pi/2, Vec{Y: 1})
19+
y90 := RotationMat4(math.Pi/2, Vec{Y: 1})
1720
got := y90.MulPosition(v)
1821
want := Vec{Z: -1}
1922
if !EqualElem(got, want, tol) {
2023
t.Errorf("want %v, got %v", want, got)
2124
}
2225
}
2326

27+
func TestRotationConversions(t *testing.T) {
28+
const tol = internal.Smallfloat64 / 10
29+
rotations := []Quat{
30+
Rotation(1, Vec{X: 1}),
31+
Rotation(1, Vec{X: 1, Y: 1}),
32+
Rotation(2, Vec{X: 1, Y: 1, Z: 1}),
33+
Rotation(math.Pi, Vec{X: 1}),
34+
Rotation(math.Pi, Vec{X: 1, Y: 1}),
35+
Rotation(math.Pi, Vec{X: 1, Y: 1, Z: 1}),
36+
Rotation(2*math.Pi, Vec{X: 1}),
37+
Rotation(0, Vec{X: 1}),
38+
}
39+
rng := newRNG(1)
40+
for _, rot := range rotations {
41+
angle, axis := rot.Rotation()
42+
gotrot := Rotation(angle, axis)
43+
if !EqualQuat(rot, gotrot, tol) {
44+
t.Errorf("want %v, got %v", rot, gotrot)
45+
}
46+
if !rot.EqualOrientation(gotrot, tol) {
47+
t.Errorf("not equal orientations %v, got %v", rot, gotrot)
48+
}
49+
m3 := rot.RotationMat3()
50+
for i := 0; i < 20; i++ {
51+
v := rng.Vec()
52+
vgot := MulMatVec(m3, v)
53+
vwant := rot.Rotate(v)
54+
if !EqualElem(vgot, vwant, tol) {
55+
t.Errorf("want %v, got %v", vwant, vgot)
56+
}
57+
}
58+
}
59+
}
60+
61+
func TestRotationBetweenVecs(t *testing.T) {
62+
const tol = internal.Smallfloat64 / 10
63+
rng := newRNG(1)
64+
for i := 0; i < 80; i++ {
65+
start := rng.Vec()
66+
dest := rng.Vec()
67+
rot := RotationBetweenVecs(start, dest)
68+
gotDest := rot.Rotate(start)
69+
if !EqualElem(Unit(gotDest), Unit(dest), tol) { // test direction, so use Unit.
70+
t.Errorf("want %v, got %v", dest, gotDest)
71+
}
72+
}
73+
}
74+
2475
func TestSVD(t *testing.T) {
2576
const tol = 1e-6
2677
a := mat3(-0.558253, -0.0461681, -0.505735, -0.411397, 0.0365854, 0.199707, 0.285389, -0.313789, 0.200189)
@@ -59,3 +110,27 @@ func printMat(a Mat3) {
59110
fmt.Printf("%f %f %f \n", a.x10, a.x11, a.x12)
60111
fmt.Printf("%f %f %f \n", a.x20, a.x21, a.x22)
61112
}
113+
114+
func newRNG(src int) *rngGen {
115+
return &rngGen{rng: *rand.New(rand.NewSource(int64(src)))}
116+
}
117+
118+
type rngGen struct {
119+
rng rand.Rand
120+
}
121+
122+
func (rng *rngGen) Vec() Vec {
123+
return Vec{X: rng.Float(), Y: rng.Float(), Z: rng.Float()}
124+
}
125+
126+
func (rng *rngGen) Float() float64 {
127+
return float64(rng.rng.Float64())
128+
}
129+
130+
func (rng *rngGen) FloatRange(start, end float64) float64 {
131+
return float64(rng.rng.Float64())*(end-start) + start
132+
}
133+
134+
func (rng *rngGen) VecRange(start, end float64) Vec {
135+
return Vec{X: rng.FloatRange(start, end), Y: rng.FloatRange(start, end), Z: rng.FloatRange(start, end)}
136+
}

0 commit comments

Comments
 (0)