├── codelingo.yaml
├── go.mod
├── .gitignore
├── go.sum
├── mat2
├── mat2_safe.go
├── mat2_unsafe.go
├── mat2_test.go
└── mat2.go
├── float64
├── mat2
│ ├── mat2_safe.go
│ ├── mat2_unsafe.go
│ ├── mat2_test.go
│ └── mat2.go
├── mat3
│ ├── mat3_unsafe.go
│ ├── mat3_safe.go
│ └── mat3_test.go
├── mat4
│ ├── mat4_unsafe.go
│ ├── mat4_safe.go
│ └── mat4_test.go
├── vec2
│ ├── config.go
│ ├── rect.go
│ └── vec2_test.go
├── vec3
│ ├── config.go
│ ├── box.go
│ ├── vec3_test.go
│ └── vec3.go
├── quaternion
│ ├── config.go
│ ├── quaternion_test.go
│ └── quaternion.go
├── generic
│ └── generic.go
├── vec4
│ └── vec4_test.go
├── bezier2
│ ├── bezier2_test.go
│ └── bezier2.go
├── qbezier2
│ ├── qbezier2_test.go
│ └── qbezier2.go
├── hermit2
│ ├── hermit2.go
│ └── hermit2_test.go
└── hermit3
│ ├── hermit3.go
│ └── hermit3_test.go
├── mat3
├── mat3_unsafe.go
├── mat3_safe.go
└── mat3_test.go
├── mat4
├── mat4_unsafe.go
├── mat4_safe.go
└── mat4_test.go
├── vec2
├── config.go
└── rect.go
├── vec3
├── config.go
├── box.go
├── vec3_test.go
└── vec3.go
├── quaternion
├── config.go
└── quaternion.go
├── .github
└── workflows
│ ├── gosec.yml
│ └── codeql-analysis.yml
├── generic
└── generic.go
├── vec4
└── vec4_test.go
├── LICENSE
├── bezier2
├── bezier2_test.go
└── bezier2.go
├── qbezier2
├── qbezier2_test.go
└── qbezier2.go
├── doc.go
├── hermit2
├── hermit2.go
└── hermit2_test.go
└── hermit3
├── hermit3.go
└── hermit3_test.go
/codelingo.yaml:
--------------------------------------------------------------------------------
1 | tenets:
2 | - import: codelingo/effective-go/comment-first-word-as-subject
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ungerik/go3d
2 |
3 | go 1.23
4 |
5 | require github.com/chewxy/math32 v1.11.1
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.6
2 | *.8
3 | *.o
4 | *.so
5 | *.out
6 | *.go~
7 | *.cgo?.*
8 | _cgo_*
9 | _obj
10 | _test
11 |
12 | .DS_Store
13 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8=
2 | github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
3 |
--------------------------------------------------------------------------------
/mat2/mat2_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat2
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[4]float32 {
8 | return &[...]float32{
9 | mat[0][0], mat[0][1],
10 | mat[1][0], mat[1][1],
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/float64/mat2/mat2_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat2
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[4]float64 {
8 | return &[...]float64{
9 | mat[0][0], mat[0][1],
10 | mat[1][0], mat[1][1],
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/mat2/mat2_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat2
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[4]float32 {
10 | return (*[4]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/mat3/mat3_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat3
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[9]float32 {
10 | return (*[9]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/mat4/mat4_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat4
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[16]float32 {
10 | return (*[16]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/float64/mat2/mat2_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat2
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[4]float64 {
10 | return (*[4]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/float64/mat3/mat3_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat3
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[9]float64 {
10 | return (*[9]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/float64/mat4/mat4_unsafe.go:
--------------------------------------------------------------------------------
1 | //go:build !netgo
2 |
3 | package mat4
4 |
5 | import "unsafe"
6 |
7 | // Array returns the elements of the matrix as array pointer.
8 | // The data may be a copy depending on the platform implementation.
9 | func (mat *T) Array() *[16]float64 {
10 | return (*[16]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK
11 | }
12 |
--------------------------------------------------------------------------------
/vec2/config.go:
--------------------------------------------------------------------------------
1 | package vec2
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize() and other methods to determine if a vector is effectively
5 | // zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-8 for float32 precision.
7 | var Epsilon float32 = 1e-8
8 |
--------------------------------------------------------------------------------
/float64/vec2/config.go:
--------------------------------------------------------------------------------
1 | package vec2
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize() and other methods to determine if a vector is effectively
5 | // zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-14 for float64 precision.
7 | var Epsilon float64 = 1e-14
8 |
--------------------------------------------------------------------------------
/vec3/config.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize(), Normal(), and other methods to determine if a vector is
5 | // effectively zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-8 for float32 precision.
7 | var Epsilon float32 = 1e-8
8 |
--------------------------------------------------------------------------------
/float64/vec3/config.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize(), Normal(), and other methods to determine if a vector is
5 | // effectively zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-14 for float64 precision.
7 | var Epsilon float64 = 1e-14
8 |
--------------------------------------------------------------------------------
/quaternion/config.go:
--------------------------------------------------------------------------------
1 | package quaternion
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize() and Normalized() methods to determine if a quaternion is
5 | // effectively zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-8 for float32 precision.
7 | var Epsilon float32 = 1e-8
8 |
--------------------------------------------------------------------------------
/float64/quaternion/config.go:
--------------------------------------------------------------------------------
1 | package quaternion
2 |
3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons.
4 | // It is used by Normalize() and Normalized() methods to determine if a quaternion is
5 | // effectively zero or already normalized. Can be adjusted for specific use cases.
6 | // Default: 1e-14 for float64 precision.
7 | var Epsilon float64 = 1e-14
8 |
--------------------------------------------------------------------------------
/mat3/mat3_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat3
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[9]float32 {
8 | return &[...]float32{
9 | mat[0][0], mat[0][1], mat[0][2],
10 | mat[1][0], mat[1][1], mat[1][2],
11 | mat[2][0], mat[2][1], mat[2][2],
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/float64/mat3/mat3_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat3
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[9]float64 {
8 | return &[...]float64{
9 | mat[0][0], mat[0][1], mat[0][2],
10 | mat[1][0], mat[1][1], mat[1][2],
11 | mat[2][0], mat[2][1], mat[2][2],
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/workflows/gosec.yml:
--------------------------------------------------------------------------------
1 | name: Run Gosec
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 | jobs:
10 | tests:
11 | runs-on: ubuntu-latest
12 | env:
13 | GO111MODULE: on
14 | steps:
15 | - name: Checkout Source
16 | uses: actions/checkout@v2
17 | - name: Run Gosec Security Scanner
18 | uses: securego/gosec@master
19 | with:
20 | args: ./...
21 |
--------------------------------------------------------------------------------
/mat4/mat4_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat4
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[16]float32 {
8 | return &[...]float32{
9 | mat[0][0], mat[0][1], mat[0][2], mat[0][3],
10 | mat[1][0], mat[1][1], mat[1][2], mat[1][3],
11 | mat[2][0], mat[2][1], mat[2][2], mat[2][3],
12 | mat[3][0], mat[3][1], mat[3][2], mat[3][3],
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/float64/mat4/mat4_safe.go:
--------------------------------------------------------------------------------
1 | //go:build netgo
2 |
3 | package mat4
4 |
5 | // Array returns the elements of the matrix as array pointer.
6 | // The data may be a copy depending on the platform implementation.
7 | func (mat *T) Array() *[16]float64 {
8 | return &[...]float64{
9 | mat[0][0], mat[0][1], mat[0][2], mat[0][3],
10 | mat[1][0], mat[1][1], mat[1][2], mat[1][3],
11 | mat[2][0], mat[2][1], mat[2][2], mat[2][3],
12 | mat[3][0], mat[3][1], mat[3][2], mat[3][3],
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/generic/generic.go:
--------------------------------------------------------------------------------
1 | // Package generic contains an interface T that
2 | // that all float32 vector and matrix types implement.
3 | package generic
4 |
5 | // T is an interface that all float32 vector and matrix types implement.
6 | type T interface {
7 |
8 | // Cols returns the number of columns of the vector or matrix.
9 | Cols() int
10 |
11 | // Rows returns the number of rows of the vector or matrix.
12 | Rows() int
13 |
14 | // Size returns the number elements of the vector or matrix.
15 | Size() int
16 |
17 | // Slice returns the elements of the vector or matrix as slice.
18 | Slice() []float32
19 |
20 | // Get returns one element of the vector or matrix.
21 | Get(row, col int) float32
22 |
23 | // IsZero checks if all elements of the vector or matrix are zero.
24 | IsZero() bool
25 | }
26 |
--------------------------------------------------------------------------------
/float64/generic/generic.go:
--------------------------------------------------------------------------------
1 | // Package generic contains an interface T that
2 | // that all float64 vector and matrix types implement.
3 | package generic
4 |
5 | // T is an interface that all float64 vector and matrix types implement.
6 | type T interface {
7 |
8 | // Cols returns the number of columns of the vector or matrix.
9 | Cols() int
10 |
11 | // Rows returns the number of rows of the vector or matrix.
12 | Rows() int
13 |
14 | // Size returns the number elements of the vector or matrix.
15 | Size() int
16 |
17 | // Slice returns the elements of the vector or matrix as slice.
18 | Slice() []float64
19 |
20 | // Get returns one element of the vector or matrix.
21 | Get(row, col int) float64
22 |
23 | // IsZero checks if all elements of the vector or matrix are zero.
24 | IsZero() bool
25 | }
26 |
--------------------------------------------------------------------------------
/vec4/vec4_test.go:
--------------------------------------------------------------------------------
1 | package vec4
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestIsZeroEps(t *testing.T) {
8 | tests := []struct {
9 | name string
10 | vec T
11 | epsilon float32
12 | want bool
13 | }{
14 | {"exact zero", T{0, 0, 0, 0}, 0.0001, true},
15 | {"within epsilon", T{0.00001, -0.00001, 0.00001, -0.00001}, 0.0001, true},
16 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001, 0.0001}, 0.0001, true},
17 | {"outside epsilon", T{0.001, 0, 0, 0}, 0.0001, false},
18 | {"one component outside", T{0.00001, 0.001, 0.00001, 0.00001}, 0.0001, false},
19 | {"negative outside epsilon", T{-0.001, 0, 0, 0}, 0.0001, false},
20 | {"large values", T{1.0, 2.0, 3.0, 4.0}, 0.0001, false},
21 | }
22 |
23 | for _, tt := range tests {
24 | t.Run(tt.name, func(t *testing.T) {
25 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want {
26 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon)
27 | }
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/float64/vec4/vec4_test.go:
--------------------------------------------------------------------------------
1 | package vec4
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestIsZeroEps(t *testing.T) {
8 | tests := []struct {
9 | name string
10 | vec T
11 | epsilon float64
12 | want bool
13 | }{
14 | {"exact zero", T{0, 0, 0, 0}, 0.0001, true},
15 | {"within epsilon", T{0.00001, -0.00001, 0.00001, -0.00001}, 0.0001, true},
16 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001, 0.0001}, 0.0001, true},
17 | {"outside epsilon", T{0.001, 0, 0, 0}, 0.0001, false},
18 | {"one component outside", T{0.00001, 0.001, 0.00001, 0.00001}, 0.0001, false},
19 | {"negative outside epsilon", T{-0.001, 0, 0, 0}, 0.0001, false},
20 | {"large values", T{1.0, 2.0, 3.0, 4.0}, 0.0001, false},
21 | }
22 |
23 | for _, tt := range tests {
24 | t.Run(tt.name, func(t *testing.T) {
25 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want {
26 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon)
27 | }
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2012-2025 Erik Unger
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/vec2/rect.go:
--------------------------------------------------------------------------------
1 | package vec2
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Rect is a coordinate system aligned rectangle defined by a Min and Max vector.
8 | type Rect struct {
9 | Min T
10 | Max T
11 | }
12 |
13 | // ParseRect parses a Rect from a string. See also String()
14 | func ParseRect(s string) (r Rect, err error) {
15 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Max[0], &r.Max[1])
16 | return r, err
17 | }
18 |
19 | // String formats Rect as string. See also ParseRect().
20 | func (rect *Rect) String() string {
21 | return rect.Min.String() + " " + rect.Max.String()
22 | }
23 |
24 | // ContainsPoint returns if a point is contained within the rectangle.
25 | func (rect *Rect) ContainsPoint(p *T) bool {
26 | return p[0] >= rect.Min[0] && p[0] <= rect.Max[0] &&
27 | p[1] >= rect.Min[1] && p[1] <= rect.Max[1]
28 | }
29 |
30 | func (rect *Rect) Contains(other *Rect) bool {
31 | return other.Min[0] >= rect.Min[0] &&
32 | other.Max[0] <= rect.Max[0] &&
33 | other.Min[1] >= rect.Min[1] &&
34 | other.Max[1] <= rect.Max[1]
35 | }
36 |
37 | func (rect *Rect) Intersects(other *Rect) bool {
38 | return other.Max[0] >= rect.Min[0] &&
39 | other.Min[0] <= rect.Max[0] &&
40 | other.Max[1] >= rect.Min[0] &&
41 | other.Min[1] <= rect.Max[1]
42 | }
43 |
44 | // func Intersect(a, b *Rect) Rect {
45 | // panic("not implemented")
46 | // }
47 |
48 | // func Join(a, b *Rect) Rect {
49 | // panic("not implemented")
50 | // }
51 |
--------------------------------------------------------------------------------
/bezier2/bezier2_test.go:
--------------------------------------------------------------------------------
1 | package bezier2
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ungerik/go3d/vec2"
7 | )
8 |
9 | func TestPoint(t *testing.T) {
10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}}
11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want {
12 | t.Errorf("cubic bezier point at t=0 failed, got %v, want %v", got, want)
13 | }
14 | if got, want := b.Point(1), (vec2.T{3, 0}); got != want {
15 | t.Errorf("cubic bezier point at t=1 failed, got %v, want %v", got, want)
16 | }
17 | if got, want := b.Point(0.5), (vec2.T{1.5, 0.75}); got != want {
18 | t.Errorf("cubic bezier point at t=0.5 failed, got %v, want %v", got, want)
19 | }
20 | if got, want := b.Point(0.25), (vec2.T{0.75, 0.5625}); got != want {
21 | t.Errorf("cubic bezier point at t=0.25 failed, got %v, want %v", got, want)
22 | }
23 | if got, want := b.Point(0.75), (vec2.T{2.25, 0.5625}); got != want {
24 | t.Errorf("cubic bezier point at t=0.75 failed, got %v, want %v", got, want)
25 | }
26 | }
27 |
28 | func TestTangent(t *testing.T) {
29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}}
30 | if got, want := b.Tangent(0), (vec2.T{3, 3}); got != want {
31 | t.Errorf("cubic bezier tangent at t=0 failed, got %v, want %v", got, want)
32 | }
33 | if got, want := b.Tangent(1), (vec2.T{3, -3}); got != want {
34 | t.Errorf("cubic bezier tangent at t=1 failed, got %v, want %v", got, want)
35 | }
36 | if got, want := b.Tangent(0.5), (vec2.T{3, 0}); got != want {
37 | t.Errorf("cubic bezier tangent at t=0.5 failed, got %v, want %v", got, want)
38 | }
39 | if got, want := b.Tangent(0.25), (vec2.T{3, 1.5}); got != want {
40 | t.Errorf("cubic bezier tangent at t=0.25 failed, got %v, want %v", got, want)
41 | }
42 | if got, want := b.Tangent(0.75), (vec2.T{3, -1.5}); got != want {
43 | t.Errorf("cubic bezier tangent at t=0.75 failed, got %v, want %v", got, want)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/qbezier2/qbezier2_test.go:
--------------------------------------------------------------------------------
1 | package qbezier2
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ungerik/go3d/vec2"
7 | )
8 |
9 | func TestPoint(t *testing.T) {
10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}}
11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want {
12 | t.Errorf("quadratic bezier point at t=0 failed, got %v, want %v", got, want)
13 | }
14 | if got, want := b.Point(1), (vec2.T{2, 0}); got != want {
15 | t.Errorf("quadratic bezier point at t=1 failed, got %v, want %v", got, want)
16 | }
17 | if got, want := b.Point(0.5), (vec2.T{1.0, 0.5}); got != want {
18 | t.Errorf("quadratic bezier point at t=0.5 failed, got %v, want %v", got, want)
19 | }
20 | if got, want := b.Point(0.25), (vec2.T{0.5, 0.375}); got != want {
21 | t.Errorf("quadratic bezier point at t=0.25 failed, got %v, want %v", got, want)
22 | }
23 | if got, want := b.Point(0.75), (vec2.T{1.5, 0.375}); got != want {
24 | t.Errorf("quadratic bezier point at t=0.75 failed, got %v, want %v", got, want)
25 | }
26 | }
27 |
28 | func TestTangent(t *testing.T) {
29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}}
30 | if got, want := b.Tangent(0), (vec2.T{2, 2}); got != want {
31 | t.Errorf("quadratic bezier tangent at t=0 failed, got %v, want %v", got, want)
32 | }
33 | if got, want := b.Tangent(1), (vec2.T{2, -2}); got != want {
34 | t.Errorf("quadratic bezier tangent at t=1 failed, got %v, want %v", got, want)
35 | }
36 | if got, want := b.Tangent(0.5), (vec2.T{2, 0}); got != want {
37 | t.Errorf("quadratic bezier tangent at t=0.5 failed, got %v, want %v", got, want)
38 | }
39 | if got, want := b.Tangent(0.25), (vec2.T{2, 1}); got != want {
40 | t.Errorf("quadratic bezier tangent at t=0.25 failed, got %v, want %v", got, want)
41 | }
42 | if got, want := b.Tangent(0.75), (vec2.T{2, -1}); got != want {
43 | t.Errorf("quadratic bezier tangent at t=0.75 failed, got %v, want %v", got, want)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/float64/bezier2/bezier2_test.go:
--------------------------------------------------------------------------------
1 | package bezier2
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ungerik/go3d/float64/vec2"
7 | )
8 |
9 | func TestPoint(t *testing.T) {
10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}}
11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want {
12 | t.Errorf("cubic bezier point at t=0 failed, got %v, want %v", got, want)
13 | }
14 | if got, want := b.Point(1), (vec2.T{3, 0}); got != want {
15 | t.Errorf("cubic bezier point at t=1 failed, got %v, want %v", got, want)
16 | }
17 | if got, want := b.Point(0.5), (vec2.T{1.5, 0.75}); got != want {
18 | t.Errorf("cubic bezier point at t=0.5 failed, got %v, want %v", got, want)
19 | }
20 | if got, want := b.Point(0.25), (vec2.T{0.75, 0.5625}); got != want {
21 | t.Errorf("cubic bezier point at t=0.25 failed, got %v, want %v", got, want)
22 | }
23 | if got, want := b.Point(0.75), (vec2.T{2.25, 0.5625}); got != want {
24 | t.Errorf("cubic bezier point at t=0.75 failed, got %v, want %v", got, want)
25 | }
26 | }
27 |
28 | func TestTangent(t *testing.T) {
29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}}
30 | if got, want := b.Tangent(0), (vec2.T{3, 3}); got != want {
31 | t.Errorf("cubic bezier tangent at t=0 failed, got %v, want %v", got, want)
32 | }
33 | if got, want := b.Tangent(1), (vec2.T{3, -3}); got != want {
34 | t.Errorf("cubic bezier tangent at t=1 failed, got %v, want %v", got, want)
35 | }
36 | if got, want := b.Tangent(0.5), (vec2.T{3, 0}); got != want {
37 | t.Errorf("cubic bezier tangent at t=0.5 failed, got %v, want %v", got, want)
38 | }
39 | if got, want := b.Tangent(0.25), (vec2.T{3, 1.5}); got != want {
40 | t.Errorf("cubic bezier tangent at t=0.25 failed, got %v, want %v", got, want)
41 | }
42 | if got, want := b.Tangent(0.75), (vec2.T{3, -1.5}); got != want {
43 | t.Errorf("cubic bezier tangent at t=0.75 failed, got %v, want %v", got, want)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/float64/qbezier2/qbezier2_test.go:
--------------------------------------------------------------------------------
1 | package qbezier2
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ungerik/go3d/float64/vec2"
7 | )
8 |
9 | func TestPoint(t *testing.T) {
10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}}
11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want {
12 | t.Errorf("quadratic bezier point at t=0 failed, got %v, want %v", got, want)
13 | }
14 | if got, want := b.Point(1), (vec2.T{2, 0}); got != want {
15 | t.Errorf("quadratic bezier point at t=1 failed, got %v, want %v", got, want)
16 | }
17 | if got, want := b.Point(0.5), (vec2.T{1.0, 0.5}); got != want {
18 | t.Errorf("quadratic bezier point at t=0.5 failed, got %v, want %v", got, want)
19 | }
20 | if got, want := b.Point(0.25), (vec2.T{0.5, 0.375}); got != want {
21 | t.Errorf("quadratic bezier point at t=0.25 failed, got %v, want %v", got, want)
22 | }
23 | if got, want := b.Point(0.75), (vec2.T{1.5, 0.375}); got != want {
24 | t.Errorf("quadratic bezier point at t=0.75 failed, got %v, want %v", got, want)
25 | }
26 | }
27 |
28 | func TestTangent(t *testing.T) {
29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}}
30 | if got, want := b.Tangent(0), (vec2.T{2, 2}); got != want {
31 | t.Errorf("quadratic bezier tangent at t=0 failed, got %v, want %v", got, want)
32 | }
33 | if got, want := b.Tangent(1), (vec2.T{2, -2}); got != want {
34 | t.Errorf("quadratic bezier tangent at t=1 failed, got %v, want %v", got, want)
35 | }
36 | if got, want := b.Tangent(0.5), (vec2.T{2, 0}); got != want {
37 | t.Errorf("quadratic bezier tangent at t=0.5 failed, got %v, want %v", got, want)
38 | }
39 | if got, want := b.Tangent(0.25), (vec2.T{2, 1}); got != want {
40 | t.Errorf("quadratic bezier tangent at t=0.25 failed, got %v, want %v", got, want)
41 | }
42 | if got, want := b.Tangent(0.75), (vec2.T{2, -1}); got != want {
43 | t.Errorf("quadratic bezier tangent at t=0.75 failed, got %v, want %v", got, want)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/float64/vec2/rect.go:
--------------------------------------------------------------------------------
1 | package vec2
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Rect is a coordinate system aligned rectangle defined by a Min and Max vector.
8 | type Rect struct {
9 | Min T
10 | Max T
11 | }
12 |
13 | // NewRect creates a Rect from two points.
14 | func NewRect(a, b *T) (rect Rect) {
15 | rect.Min = Min(a, b)
16 | rect.Max = Max(a, b)
17 | return rect
18 | }
19 |
20 | // ParseRect parses a Rect from a string. See also String()
21 | func ParseRect(s string) (r Rect, err error) {
22 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Max[0], &r.Max[1])
23 | return r, err
24 | }
25 |
26 | // String formats Rect as string. See also ParseRect().
27 | func (rect *Rect) String() string {
28 | return rect.Min.String() + " " + rect.Max.String()
29 | }
30 |
31 | // ContainsPoint returns if a point is contained within the rectangle.
32 | func (rect *Rect) ContainsPoint(p *T) bool {
33 | return p[0] >= rect.Min[0] && p[0] <= rect.Max[0] &&
34 | p[1] >= rect.Min[1] && p[1] <= rect.Max[1]
35 | }
36 |
37 | // Contains returns if other Rect is contained within the rectangle.
38 | func (rect *Rect) Contains(other *Rect) bool {
39 | return rect.Min[0] <= other.Min[0] &&
40 | rect.Min[1] <= other.Min[1] &&
41 | rect.Max[0] >= other.Max[0] &&
42 | rect.Max[1] >= other.Max[1]
43 | }
44 |
45 | // Area calculates the area of the rectangle.
46 | func (rect *Rect) Area() float64 {
47 | return (rect.Max[0] - rect.Min[0]) * (rect.Max[1] - rect.Min[1])
48 | }
49 |
50 | // func (rect *Rect) Intersects(other *Rect) bool {
51 | // panic("not implemented")
52 | // }
53 |
54 | // func Intersect(a, b *Rect) Rect {
55 | // panic("not implemented")
56 | // }
57 |
58 | // Join enlarges this rectangle to contain also the given rectangle.
59 | func (rect *Rect) Join(other *Rect) {
60 | rect.Min = Min(&rect.Min, &other.Min)
61 | rect.Max = Max(&rect.Max, &other.Max)
62 | }
63 |
64 | // Joined returns the minimal rectangle containing both a and b.
65 | func Joined(a, b *Rect) (rect Rect) {
66 | rect.Min = Min(&a.Min, &b.Min)
67 | rect.Max = Max(&a.Max, &b.Max)
68 | return rect
69 | }
70 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package go3d is a performance oriented vector and matrix math package for 2D and 3D graphics.
3 |
4 | Every type has its own sub-package and is named T. So vec3.T is the 3D vector type.
5 | For every vector and matrix type there is a String() method and a Parse() function.
6 | Besides methods of T there are also functions in the packages, like vec3.Dot(a, b).
7 |
8 | Packages under the float64 directory are using float64 values instead of float32.
9 |
10 | Matrices are organized as arrays of columns which is also the way OpenGL expects matrices.
11 | DirectX expects "arrays of rows" matrices, use the Transpose() to convert.
12 |
13 | Methods that don't return a specific value, return a pointer to the struct to allow method call chaining.
14 |
15 | Example:
16 |
17 | a := vec3.Zero
18 | b := vec3.UnitX
19 | a.Add(&b).Scale(5)
20 |
21 | Method names in the past tense return a copy of the struct instead of a pointer to it.
22 |
23 | Example:
24 |
25 | a := vec3.UnitX
26 | b := a.Scaled(5) // a still equals vec3.UnitX
27 |
28 | Documentation: http://godoc.org/github.com/ungerik/go3d
29 | */
30 | package go3d
31 |
32 | // Import all sub-packages for build
33 | import (
34 | _ "github.com/ungerik/go3d/float64/bezier2"
35 | _ "github.com/ungerik/go3d/float64/generic"
36 | _ "github.com/ungerik/go3d/float64/hermit2"
37 | _ "github.com/ungerik/go3d/float64/hermit3"
38 | _ "github.com/ungerik/go3d/float64/mat2"
39 | _ "github.com/ungerik/go3d/float64/mat3"
40 | _ "github.com/ungerik/go3d/float64/mat4"
41 | _ "github.com/ungerik/go3d/float64/qbezier2"
42 | _ "github.com/ungerik/go3d/float64/quaternion"
43 | _ "github.com/ungerik/go3d/float64/vec2"
44 | _ "github.com/ungerik/go3d/float64/vec3"
45 | _ "github.com/ungerik/go3d/float64/vec4"
46 |
47 | _ "github.com/ungerik/go3d/generic"
48 | _ "github.com/ungerik/go3d/hermit2"
49 | _ "github.com/ungerik/go3d/hermit3"
50 | _ "github.com/ungerik/go3d/mat2"
51 | _ "github.com/ungerik/go3d/mat3"
52 | _ "github.com/ungerik/go3d/mat4"
53 | _ "github.com/ungerik/go3d/quaternion"
54 | _ "github.com/ungerik/go3d/vec2"
55 | _ "github.com/ungerik/go3d/vec3"
56 | _ "github.com/ungerik/go3d/vec4"
57 | )
58 |
--------------------------------------------------------------------------------
/vec3/box.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Box is a coordinate system aligned 3D box defined by a Min and Max vector.
8 | type Box struct {
9 | Min T
10 | Max T
11 | }
12 |
13 | var (
14 | // MaxBox holds a box that contains the entire R3 space that can be represented as vec3
15 | MaxBox = Box{MinVal, MaxVal}
16 | )
17 |
18 | // ParseBox parses a Box from a string. See also String()
19 | func ParseBox(s string) (r Box, err error) {
20 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Min[2], &r.Max[0], &r.Max[1], &r.Max[2])
21 | return r, err
22 | }
23 |
24 | // String formats Box as string. See also ParseBox().
25 | func (box *Box) String() string {
26 | return box.Min.String() + " " + box.Max.String()
27 | }
28 |
29 | // ContainsPoint returns if a point is contained within the box.
30 | func (box *Box) ContainsPoint(p *T) bool {
31 | return p[0] >= box.Min[0] && p[0] <= box.Max[0] &&
32 | p[1] >= box.Min[1] && p[1] <= box.Max[1] &&
33 | p[2] >= box.Min[2] && p[2] <= box.Max[2]
34 | }
35 |
36 | func (box *Box) Center() T {
37 | c := Add(&box.Min, &box.Max)
38 | c.Scale(0.5)
39 | return c
40 | }
41 |
42 | func (box *Box) Diagonal() T {
43 | return Sub(&box.Max, &box.Min)
44 | }
45 |
46 | // Intersects returns true if this and the given box intersect.
47 | // For an explanation of the algorithm, see
48 | // http://rbrundritt.wordpress.com/2009/10/03/determining-if-two-bounding-boxes-overlap/
49 | func (box *Box) Intersects(other *Box) bool {
50 | d1 := box.Diagonal()
51 | d2 := other.Diagonal()
52 | sizes := Add(&d1, &d2)
53 | c1 := box.Center()
54 | c2 := other.Center()
55 | distCenters2 := Sub(&c1, &c2)
56 | distCenters2.Scale(2)
57 | distCenters2.Abs()
58 | return distCenters2[0] <= sizes[0] && distCenters2[1] <= sizes[1] && distCenters2[2] <= sizes[2]
59 | }
60 |
61 | // Join enlarges this box to contain also the given box.
62 | func (box *Box) Join(other *Box) {
63 | box.Min = Min(&box.Min, &other.Min)
64 | box.Max = Max(&box.Max, &other.Max)
65 | }
66 |
67 | // Joined returns the minimal box containing both a and b.
68 | func Joined(a, b *Box) Box {
69 | var joined Box
70 | joined.Min = Min(&a.Min, &b.Min)
71 | joined.Max = Max(&a.Max, &b.Max)
72 | return joined
73 | }
74 |
--------------------------------------------------------------------------------
/float64/vec3/box.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Box is a coordinate system aligned 3D box defined by a Min and Max vector.
8 | type Box struct {
9 | Min T
10 | Max T
11 | }
12 |
13 | var (
14 | // MaxBox holds a box that contains the entire R3 space that can be represented as vec3
15 | MaxBox = Box{MinVal, MaxVal}
16 | )
17 |
18 | // ParseBox parses a Box from a string. See also String()
19 | func ParseBox(s string) (r Box, err error) {
20 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Min[2], &r.Max[0], &r.Max[1], &r.Max[2])
21 | return r, err
22 | }
23 |
24 | // String formats Box as string. See also ParseBox().
25 | func (box *Box) String() string {
26 | return box.Min.String() + " " + box.Max.String()
27 | }
28 |
29 | // ContainsPoint returns if a point is contained within the box.
30 | func (box *Box) ContainsPoint(p *T) bool {
31 | return p[0] >= box.Min[0] && p[0] <= box.Max[0] &&
32 | p[1] >= box.Min[1] && p[1] <= box.Max[1] &&
33 | p[2] >= box.Min[2] && p[2] <= box.Max[2]
34 | }
35 |
36 | func (box *Box) Center() T {
37 | c := Add(&box.Min, &box.Max)
38 | c.Scale(0.5)
39 | return c
40 | }
41 |
42 | func (box *Box) Diagonal() T {
43 | return Sub(&box.Max, &box.Min)
44 | }
45 |
46 | // Intersects returns true if this and the given box intersect.
47 | // For an explanation of the algorithm, see
48 | // http://rbrundritt.wordpress.com/2009/10/03/determining-if-two-bounding-boxes-overlap/
49 | func (box *Box) Intersects(other *Box) bool {
50 | d1 := box.Diagonal()
51 | d2 := other.Diagonal()
52 | sizes := Add(&d1, &d2)
53 | c1 := box.Center()
54 | c2 := other.Center()
55 | distCenters2 := Sub(&c1, &c2)
56 | distCenters2.Scale(2)
57 | distCenters2.Abs()
58 | return distCenters2[0] <= sizes[0] && distCenters2[1] <= sizes[1] && distCenters2[2] <= sizes[2]
59 | }
60 |
61 | // Join enlarges this box to contain also the given box.
62 | func (box *Box) Join(other *Box) {
63 | box.Min = Min(&box.Min, &other.Min)
64 | box.Max = Max(&box.Max, &other.Max)
65 | }
66 |
67 | // Joined returns the minimal box containing both a and b.
68 | func Joined(a, b *Box) Box {
69 | var joined Box
70 | joined.Min = Min(&a.Min, &b.Min)
71 | joined.Max = Max(&a.Max, &b.Max)
72 | return joined
73 | }
74 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '34 8 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/float64/qbezier2/qbezier2.go:
--------------------------------------------------------------------------------
1 | // Package qbezier2 contains functions for 2D quadratic Bezier splines.
2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve
3 | package qbezier2
4 |
5 | import (
6 | "fmt"
7 | "math"
8 |
9 | "github.com/ungerik/go3d/float64/vec2"
10 | )
11 |
12 | // T holds the data to define a quadratic bezier spline.
13 | type T struct {
14 | P0, P1, P2 vec2.T
15 | }
16 |
17 | // Parse parses T from a string. See also String()
18 | func Parse(s string) (r T, err error) {
19 | _, err = fmt.Sscan(s,
20 | &r.P0[0], &r.P0[1],
21 | &r.P1[0], &r.P1[1],
22 | &r.P2[0], &r.P2[1],
23 | )
24 | return r, err
25 | }
26 |
27 | // String formats T as string. See also Parse().
28 | func (bez *T) String() string {
29 | return fmt.Sprintf("%s %s %s",
30 | bez.P0.String(), bez.P1.String(), bez.P2.String(),
31 | )
32 | }
33 |
34 | // Point returns a point on a quadratic bezier spline at t (0,1).
35 | func (bez *T) Point(t float64) vec2.T {
36 | return Point(&bez.P0, &bez.P1, &bez.P2, t)
37 | }
38 |
39 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1).
40 | func (bez *T) Tangent(t float64) vec2.T {
41 | return Tangent(&bez.P0, &bez.P1, &bez.P2, t)
42 | }
43 |
44 | // Length returns the length of a quadratic bezier spline from A.Point to t (0,1).
45 | func (bez *T) Length(t float64) float64 {
46 | return Length(&bez.P0, &bez.P1, &bez.P2, t)
47 | }
48 |
49 | // Point returns a point on a quadratic bezier spline at t (0,1).
50 | func Point(p0, p1, p2 *vec2.T, t float64) vec2.T {
51 | t1 := 1.0 - t
52 |
53 | f := t1 * t1
54 | result := p0.Scaled(f)
55 |
56 | f = 2.0 * t1 * t
57 | p1f := p1.Scaled(f)
58 | result.Add(&p1f)
59 |
60 | f = t * t
61 | p2f := p2.Scaled(f)
62 | result.Add(&p2f)
63 |
64 | return result
65 | }
66 |
67 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1).
68 | func Tangent(p0, p1, p2 *vec2.T, t float64) vec2.T {
69 | t1 := 1.0 - t
70 |
71 | f := 2.0 * t1
72 | p1f := vec2.Sub(p1, p0)
73 | result := p1f.Scaled(f)
74 |
75 | f = 2.0 * t
76 | p2f := vec2.Sub(p2, p1)
77 | p2f.Scale(f)
78 | result.Add(&p2f)
79 |
80 | if result[0] == 0 && result[1] == 0 {
81 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, t=%v\n", p0, p1, p2, t)
82 | panic("zero tangent of qbezier2")
83 | }
84 |
85 | return result
86 | }
87 |
88 | // Length returns the length of a quadratic bezier spline from p0 to t (0,1).
89 | //
90 | // Note that although this calculation is accurate at t=0, 0.5, and 1 due
91 | // to the nature of quadratic curves, it is an approximation for other values of t.
92 | func Length(p0, p1, p2 *vec2.T, t float64) float64 {
93 | ax := p0[0] - 2*p1[0] + p2[0]
94 | ay := p0[1] - 2*p1[1] + p2[1]
95 | bx := 2*p1[0] - 2*p0[0]
96 | by := 2*p1[1] - 2*p0[1]
97 |
98 | a := 4 * (ax*ax + ay*ay)
99 | b := 4 * (ax*bx + ay*by)
100 | c := bx*bx + by*by
101 |
102 | abc := 2 * math.Sqrt(a+b+c)
103 | a2 := math.Sqrt(a)
104 | a32 := 2 * a * a2
105 | c2 := 2 * math.Sqrt(c)
106 | ba := b / a2
107 |
108 | return t * (a32*abc + a2*b*(abc-c2) + (4*c*a-b*b)*math.Log((2*a2+ba+abc)/(ba+c2))) / (4 * a32)
109 | }
110 |
--------------------------------------------------------------------------------
/qbezier2/qbezier2.go:
--------------------------------------------------------------------------------
1 | // Package qbezier2 contains a float32 type T and functions for 2D quadratic Bezier splines.
2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve
3 | package qbezier2
4 |
5 | import (
6 | "fmt"
7 |
8 | math "github.com/chewxy/math32"
9 | "github.com/ungerik/go3d/vec2"
10 | )
11 |
12 | // T holds the data to define a quadratic bezier spline.
13 | type T struct {
14 | P0, P1, P2 vec2.T
15 | }
16 |
17 | // Parse parses T from a string. See also String()
18 | func Parse(s string) (r T, err error) {
19 | _, err = fmt.Sscan(s,
20 | &r.P0[0], &r.P0[1],
21 | &r.P1[0], &r.P1[1],
22 | &r.P2[0], &r.P2[1],
23 | )
24 | return r, err
25 | }
26 |
27 | // String formats T as string. See also Parse().
28 | func (bez *T) String() string {
29 | return fmt.Sprintf("%s %s %s",
30 | bez.P0.String(), bez.P1.String(), bez.P2.String(),
31 | )
32 | }
33 |
34 | // Point returns a point on a quadratic bezier spline at t (0,1).
35 | func (bez *T) Point(t float32) vec2.T {
36 | return Point(&bez.P0, &bez.P1, &bez.P2, t)
37 | }
38 |
39 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1).
40 | func (bez *T) Tangent(t float32) vec2.T {
41 | return Tangent(&bez.P0, &bez.P1, &bez.P2, t)
42 | }
43 |
44 | // Length returns the length of a quadratic bezier spline from P0 to t (0,1).
45 | func (bez *T) Length(t float32) float32 {
46 | return Length(&bez.P0, &bez.P1, &bez.P2, t)
47 | }
48 |
49 | // Point returns a point on a quadratic bezier spline at t (0,1).
50 | func Point(p0, p1, p2 *vec2.T, t float32) vec2.T {
51 | t1 := 1.0 - t
52 |
53 | f := t1 * t1
54 | result := p0.Scaled(f)
55 |
56 | f = 2.0 * t1 * t
57 | p1f := p1.Scaled(f)
58 | result.Add(&p1f)
59 |
60 | f = t * t
61 | p2f := p2.Scaled(f)
62 | result.Add(&p2f)
63 |
64 | return result
65 | }
66 |
67 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1).
68 | func Tangent(p0, p1, p2 *vec2.T, t float32) vec2.T {
69 | t1 := 1.0 - t
70 |
71 | f := 2.0 * t1
72 | p1f := vec2.Sub(p1, p0)
73 | result := p1f.Scaled(f)
74 |
75 | f = 2.0 * t
76 | p2f := vec2.Sub(p2, p1)
77 | p2f.Scale(f)
78 | result.Add(&p2f)
79 |
80 | if result[0] == 0 && result[1] == 0 {
81 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, t=%v\n", p0, p1, p2, t)
82 | panic("zero tangent of qbezier2")
83 | }
84 |
85 | return result
86 | }
87 |
88 | // Length returns the length of a quadratic bezier spline from p0 to t (0,1).
89 | //
90 | // Note that although this calculation is accurate at t=0, 0.5, and 1 due
91 | // to the nature of quadratic curves, it is an approximation for other values of t.
92 | func Length(p0, p1, p2 *vec2.T, t float32) float32 {
93 | ax := p0[0] - 2*p1[0] + p2[0]
94 | ay := p0[1] - 2*p1[1] + p2[1]
95 | bx := 2*p1[0] - 2*p0[0]
96 | by := 2*p1[1] - 2*p0[1]
97 |
98 | a := 4 * (ax*ax + ay*ay)
99 | b := 4 * (ax*bx + ay*by)
100 | c := bx*bx + by*by
101 |
102 | abc := 2 * math.Sqrt(a+b+c)
103 | a2 := math.Sqrt(a)
104 | a32 := 2 * a * a2
105 | c2 := 2 * math.Sqrt(c)
106 | ba := b / a2
107 |
108 | return t * (a32*abc + a2*b*(abc-c2) + (4*c*a-b*b)*math.Log((2*a2+ba+abc)/(ba+c2))) / (4 * a32)
109 | }
110 |
--------------------------------------------------------------------------------
/bezier2/bezier2.go:
--------------------------------------------------------------------------------
1 | // Package bezier2 contains a float32 type T and functions for 2D cubic Bezier splines.
2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve
3 | package bezier2
4 |
5 | import (
6 | "fmt"
7 |
8 | "github.com/ungerik/go3d/vec2"
9 | )
10 |
11 | // T holds the data to define a cubic bezier spline.
12 | type T struct {
13 | P0, P1, P2, P3 vec2.T
14 | }
15 |
16 | // Parse parses T from a string. See also String()
17 | func Parse(s string) (r T, err error) {
18 | _, err = fmt.Sscan(s,
19 | &r.P0[0], &r.P0[1],
20 | &r.P1[0], &r.P1[1],
21 | &r.P2[0], &r.P2[1],
22 | &r.P3[0], &r.P3[1],
23 | )
24 | return r, err
25 | }
26 |
27 | // String formats T as string. See also Parse().
28 | func (bez *T) String() string {
29 | return fmt.Sprintf("%s %s %s %s",
30 | bez.P0.String(), bez.P1.String(),
31 | bez.P2.String(), bez.P3.String(),
32 | )
33 | }
34 |
35 | // Point returns a point on a cubic bezier spline at t (0,1).
36 | func (bez *T) Point(t float32) vec2.T {
37 | return Point(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
38 | }
39 |
40 | // Tangent returns a tangent on a cubic bezier spline at t (0,1).
41 | func (bez *T) Tangent(t float32) vec2.T {
42 | return Tangent(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
43 | }
44 |
45 | // Length returns the length of a cubic bezier spline from P0 to t (0,1).
46 | func (bez *T) Length(t float32) float32 {
47 | return Length(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
48 | }
49 |
50 | // Point returns a point on a cubic bezier spline at t (0,1).
51 | func Point(p0, p1, p2, p3 *vec2.T, t float32) vec2.T {
52 | t1 := 1.0 - t
53 |
54 | f := t1 * t1 * t1
55 | result := p0.Scaled(f)
56 |
57 | f = 3.0 * t1 * t1 * t
58 | p1f := p1.Scaled(f)
59 | result.Add(&p1f)
60 |
61 | f = 3.0 * t1 * t * t
62 | p2f := p2.Scaled(f)
63 | result.Add(&p2f)
64 |
65 | f = t * t * t
66 | p3f := p3.Scaled(f)
67 | result.Add(&p3f)
68 |
69 | return result
70 | }
71 |
72 | // Tangent returns a tangent on a cubic bezier spline at t (0,1).
73 | func Tangent(p0, p1, p2, p3 *vec2.T, t float32) vec2.T {
74 | t1 := 1.0 - t
75 |
76 | f := 3.0 * t1 * t1
77 | p1f := vec2.Sub(p1, p0)
78 | result := p1f.Scaled(f)
79 |
80 | f = 6.0 * t1 * t
81 | p2f := vec2.Sub(p2, p1)
82 | p2f.Scale(f)
83 | result.Add(&p2f)
84 |
85 | f = 3.0 * t * t
86 | p3f := vec2.Sub(p3, p2)
87 | p3f.Scale(f)
88 | result.Add(&p3f)
89 |
90 | if result[0] == 0 && result[1] == 0 {
91 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, p3=%v, t=%v\n", p0, p1, p2, p3, t)
92 | panic("zero tangent of bezier2")
93 | }
94 |
95 | return result
96 | }
97 |
98 | // Length returns the length of a cubic bezier spline from p0 to t (0,1).
99 | func Length(p0, p1, p2, p3 *vec2.T, t float32) float32 {
100 | sqrT := t * t
101 | t1 := sqrT * 0.5
102 | t2 := sqrT * t * 1.0 / 3.0
103 | t3 := sqrT*sqrT + 1.0/4.0
104 |
105 | f := 2*t3 - 3*t2 + t
106 | result := p0.Scaled(f)
107 |
108 | f = t3 - 2*t2 + t1
109 | tAf := p1.Scaled(f)
110 | result.Add(&tAf)
111 |
112 | f = t3 - t2
113 | tBf := p3.Scaled(f)
114 | result.Add(&tBf)
115 |
116 | f = -2*t3 + 3*t2
117 | pBf := p2.Scaled(f)
118 | result.Add(&pBf)
119 |
120 | return result.Length()
121 | }
122 |
--------------------------------------------------------------------------------
/float64/bezier2/bezier2.go:
--------------------------------------------------------------------------------
1 | // Package bezier2 contains functions for 2D cubic Bezier splines.
2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve
3 | package bezier2
4 |
5 | import (
6 | "fmt"
7 |
8 | "github.com/ungerik/go3d/float64/vec2"
9 | )
10 |
11 | // T holds the data to define a cubic bezier spline.
12 | type T struct {
13 | P0, P1, P2, P3 vec2.T
14 | }
15 |
16 | // Parse parses T from a string. See also String()
17 | func Parse(s string) (r T, err error) {
18 | _, err = fmt.Sscan(s,
19 | &r.P0[0], &r.P0[1],
20 | &r.P1[0], &r.P1[1],
21 | &r.P2[0], &r.P2[1],
22 | &r.P3[0], &r.P3[1],
23 | )
24 | return r, err
25 | }
26 |
27 | // String formats T as string. See also Parse().
28 | func (bez *T) String() string {
29 | return fmt.Sprintf("%s %s %s %s",
30 | bez.P0.String(), bez.P1.String(),
31 | bez.P2.String(), bez.P3.String(),
32 | )
33 | }
34 |
35 | // Point returns a point on a cubic bezier spline at t (0,1).
36 | func (bez *T) Point(t float64) vec2.T {
37 | return Point(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
38 | }
39 |
40 | // Tangent returns a tangent on a cubic bezier spline at t (0,1).
41 | func (bez *T) Tangent(t float64) vec2.T {
42 | return Tangent(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
43 | }
44 |
45 | // Length returns the length of a cubic bezier spline from A.Point to t (0,1).
46 | func (bez *T) Length(t float64) float64 {
47 | return Length(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t)
48 | }
49 |
50 | // Point returns a point on a cubic bezier spline at t (0,1).
51 | func Point(p0, p1, p2, p3 *vec2.T, t float64) vec2.T {
52 | t1 := 1.0 - t
53 |
54 | f := t1 * t1 * t1
55 | result := p0.Scaled(f)
56 |
57 | f = 3.0 * t1 * t1 * t
58 | p1f := p1.Scaled(f)
59 | result.Add(&p1f)
60 |
61 | f = 3.0 * t1 * t * t
62 | p2f := p2.Scaled(f)
63 | result.Add(&p2f)
64 |
65 | f = t * t * t
66 | p3f := p3.Scaled(f)
67 | result.Add(&p3f)
68 |
69 | return result
70 | }
71 |
72 | // Tangent returns a tangent on a cubic bezier spline at t (0,1).
73 | func Tangent(p0, p1, p2, p3 *vec2.T, t float64) vec2.T {
74 | t1 := 1.0 - t
75 |
76 | f := 3.0 * t1 * t1
77 | p1f := vec2.Sub(p1, p0)
78 | result := p1f.Scaled(f)
79 |
80 | f = 6.0 * t1 * t
81 | p2f := vec2.Sub(p2, p1)
82 | p2f.Scale(f)
83 | result.Add(&p2f)
84 |
85 | f = 3.0 * t * t
86 | p3f := vec2.Sub(p3, p2)
87 | p3f.Scale(f)
88 | result.Add(&p3f)
89 |
90 | if result[0] == 0 && result[1] == 0 {
91 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, p3=%v, t=%v\n", p0, p1, p2, p3, t)
92 | panic("zero tangent of bezier2")
93 | }
94 |
95 | return result
96 | }
97 |
98 | // Length returns the length of a cubic bezier spline from p0 to t (0,1).
99 | func Length(p0, p1, p2, p3 *vec2.T, t float64) float64 {
100 | sqrT := t * t
101 | t1 := sqrT * 0.5
102 | t2 := sqrT * t * 1.0 / 3.0
103 | t3 := sqrT*sqrT + 1.0/4.0
104 |
105 | f := 2*t3 - 3*t2 + t
106 | result := p0.Scaled(f)
107 |
108 | f = t3 - 2*t2 + t1
109 | tAf := p1.Scaled(f)
110 | result.Add(&tAf)
111 |
112 | f = t3 - t2
113 | tBf := p3.Scaled(f)
114 | result.Add(&tBf)
115 |
116 | f = -2*t3 + 3*t2
117 | pBf := p2.Scaled(f)
118 | result.Add(&pBf)
119 |
120 | return result.Length()
121 | }
122 |
--------------------------------------------------------------------------------
/float64/hermit2/hermit2.go:
--------------------------------------------------------------------------------
1 | // Package hermit2 contains functions for 2D cubic hermit splines.
2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline
3 | package hermit2
4 |
5 | import (
6 | "fmt"
7 | "github.com/ungerik/go3d/float64/vec2"
8 | )
9 |
10 | // PointTangent contains a point and a tangent at that point.
11 | // This is a helper sub-struct for T.
12 | type PointTangent struct {
13 | Point vec2.T
14 | Tangent vec2.T
15 | }
16 |
17 | // T holds the data to define a hermit spline.
18 | type T struct {
19 | A PointTangent
20 | B PointTangent
21 | }
22 |
23 | // Parse parses T from a string. See also String()
24 | func Parse(s string) (r T, err error) {
25 | _, err = fmt.Sscan(s,
26 | &r.A.Point[0], &r.A.Point[1],
27 | &r.A.Tangent[0], &r.A.Tangent[1],
28 | &r.B.Point[0], &r.B.Point[1],
29 | &r.B.Tangent[0], &r.B.Tangent[1],
30 | )
31 | return r, err
32 | }
33 |
34 | // String formats T as string. See also Parse().
35 | func (herm *T) String() string {
36 | return fmt.Sprintf("%s %s %s %s",
37 | herm.A.Point.String(), herm.A.Tangent.String(),
38 | herm.B.Point.String(), herm.B.Tangent.String(),
39 | )
40 | }
41 |
42 | // Point returns a point on a hermit spline at t (0,1).
43 | func (herm *T) Point(t float64) vec2.T {
44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
45 | }
46 |
47 | // Tangent returns a tangent on a hermit spline at t (0,1).
48 | func (herm *T) Tangent(t float64) vec2.T {
49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
50 | }
51 |
52 | // Length returns the length of a hermit spline from A.Point to t (0,1).
53 | func (herm *T) Length(t float64) float64 {
54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
55 | }
56 |
57 | // Point returns a point on a hermit spline at t (0,1).
58 | func Point(pointA, tangentA, pointB, tangentB *vec2.T, t float64) vec2.T {
59 | t2 := t * t
60 | t3 := t2 * t
61 |
62 | f := 2*t3 - 3*t2 + 1
63 | result := pointA.Scaled(f)
64 |
65 | f = t3 - 2*t2 + t
66 | tAf := tangentA.Scaled(f)
67 | result.Add(&tAf)
68 |
69 | f = t3 - t2
70 | tBf := tangentB.Scaled(f)
71 | result.Add(&tBf)
72 |
73 | f = -2*t3 + 3*t2
74 | pAf := pointB.Scaled(f)
75 | result.Add(&pAf)
76 |
77 | return result
78 | }
79 |
80 | // Tangent returns a tangent on a hermit spline at t (0,1).
81 | func Tangent(pointA, tangentA, pointB, tangentB *vec2.T, t float64) vec2.T {
82 | t2 := t * t
83 | t3 := t2 * t
84 |
85 | f := 2*t3 - 3*t2
86 | result := pointA.Scaled(f)
87 |
88 | f = t3 - 2*t2 + 1
89 | tAf := tangentA.Scaled(f)
90 | result.Add(&tAf)
91 |
92 | f = t3 - t2
93 | tBf := tangentB.Scaled(f)
94 | result.Add(&tBf)
95 |
96 | f = -2*t3 + 3*t2
97 | pAf := pointB.Scaled(f)
98 | result.Add(&pAf)
99 |
100 | return result
101 | }
102 |
103 | // Length returns the length of a hermit spline from pointA to t (0,1).
104 | func Length(pointA, tangentA, pointB, tangentB *vec2.T, t float64) float64 {
105 | sqrT := t * t
106 | t1 := sqrT * 0.5
107 | t2 := sqrT * t * 1.0 / 3.0
108 | t3 := sqrT*sqrT + 1.0/4.0
109 |
110 | f := 2*t3 - 3*t2 + t
111 | result := pointA.Scaled(f)
112 |
113 | f = t3 - 2*t2 + t1
114 | tAf := tangentA.Scaled(f)
115 | result.Add(&tAf)
116 |
117 | f = t3 - t2
118 | tBf := tangentB.Scaled(f)
119 | result.Add(&tBf)
120 |
121 | f = -2*t3 + 3*t2
122 | pBf := pointB.Scaled(f)
123 | result.Add(&pBf)
124 |
125 | return result.Length()
126 | }
127 |
--------------------------------------------------------------------------------
/hermit2/hermit2.go:
--------------------------------------------------------------------------------
1 | // Package hermit2 contains a float32 type T and functions for 2D cubic hermit splines.
2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline
3 | package hermit2
4 |
5 | import (
6 | "fmt"
7 | "github.com/ungerik/go3d/vec2"
8 | )
9 |
10 | // PointTangent contains a point and a tangent at that point.
11 | // This is a helper sub-struct for T.
12 | type PointTangent struct {
13 | Point vec2.T
14 | Tangent vec2.T
15 | }
16 |
17 | // T holds the data to define a hermit spline.
18 | type T struct {
19 | A PointTangent
20 | B PointTangent
21 | }
22 |
23 | // Parse parses T from a string. See also String()
24 | func Parse(s string) (r T, err error) {
25 | _, err = fmt.Sscan(s,
26 | &r.A.Point[0], &r.A.Point[1],
27 | &r.A.Tangent[0], &r.A.Tangent[1],
28 | &r.B.Point[0], &r.B.Point[1],
29 | &r.B.Tangent[0], &r.B.Tangent[1],
30 | )
31 | return r, err
32 | }
33 |
34 | // String formats T as string. See also Parse().
35 | func (herm *T) String() string {
36 | return fmt.Sprintf("%s %s %s %s",
37 | herm.A.Point.String(), herm.A.Tangent.String(),
38 | herm.B.Point.String(), herm.B.Tangent.String(),
39 | )
40 | }
41 |
42 | // Point returns a point on a hermit spline at t (0,1).
43 | func (herm *T) Point(t float32) vec2.T {
44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
45 | }
46 |
47 | // Tangent returns a tangent on a hermit spline at t (0,1).
48 | func (herm *T) Tangent(t float32) vec2.T {
49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
50 | }
51 |
52 | // Length returns the length of a hermit spline from A.Point to t (0,1).
53 | func (herm *T) Length(t float32) float32 {
54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
55 | }
56 |
57 | // Point returns a point on a hermit spline at t (0,1).
58 | func Point(pointA, tangentA, pointB, tangentB *vec2.T, t float32) vec2.T {
59 | t2 := t * t
60 | t3 := t2 * t
61 |
62 | f := 2*t3 - 3*t2 + 1
63 | result := pointA.Scaled(f)
64 |
65 | f = t3 - 2*t2 + t
66 | tAf := tangentA.Scaled(f)
67 | result.Add(&tAf)
68 |
69 | f = t3 - t2
70 | tBf := tangentB.Scaled(f)
71 | result.Add(&tBf)
72 |
73 | f = -2*t3 + 3*t2
74 | pAf := pointB.Scaled(f)
75 | result.Add(&pAf)
76 |
77 | return result
78 | }
79 |
80 | // Tangent returns a tangent on a hermit spline at t (0,1).
81 | func Tangent(pointA, tangentA, pointB, tangentB *vec2.T, t float32) vec2.T {
82 | t2 := t * t
83 | t3 := t2 * t
84 |
85 | f := 2*t3 - 3*t2
86 | result := pointA.Scaled(f)
87 |
88 | f = t3 - 2*t2 + 1
89 | tAf := tangentA.Scaled(f)
90 | result.Add(&tAf)
91 |
92 | f = t3 - t2
93 | tBf := tangentB.Scaled(f)
94 | result.Add(&tBf)
95 |
96 | f = -2*t3 + 3*t2
97 | pAf := pointB.Scaled(f)
98 | result.Add(&pAf)
99 |
100 | return result
101 | }
102 |
103 | // Length returns the length of a hermit spline from pointA to t (0,1).
104 | func Length(pointA, tangentA, pointB, tangentB *vec2.T, t float32) float32 {
105 | sqrT := t * t
106 | t1 := sqrT * 0.5
107 | t2 := sqrT * t * 1.0 / 3.0
108 | t3 := sqrT*sqrT + 1.0/4.0
109 |
110 | f := 2*t3 - 3*t2 + t
111 | result := pointA.Scaled(f)
112 |
113 | f = t3 - 2*t2 + t1
114 | tAf := tangentA.Scaled(f)
115 | result.Add(&tAf)
116 |
117 | f = t3 - t2
118 | tBf := tangentB.Scaled(f)
119 | result.Add(&tBf)
120 |
121 | f = -2*t3 + 3*t2
122 | pBf := pointB.Scaled(f)
123 | result.Add(&pBf)
124 |
125 | return result.Length()
126 | }
127 |
--------------------------------------------------------------------------------
/float64/hermit3/hermit3.go:
--------------------------------------------------------------------------------
1 | // Package hermit3 contains functions for 3D cubic hermit splines.
2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline
3 | package hermit
4 |
5 | import (
6 | "fmt"
7 | "github.com/ungerik/go3d/float64/vec3"
8 | )
9 |
10 | // PointTangent contains a point and a tangent at that point.
11 | // This is a helper sub-struct for T.
12 | type PointTangent struct {
13 | Point vec3.T
14 | Tangent vec3.T
15 | }
16 |
17 | // T holds the data to define a hermit spline.
18 | type T struct {
19 | A PointTangent
20 | B PointTangent
21 | }
22 |
23 | // Parse parses T from a string. See also String()
24 | func Parse(s string) (r T, err error) {
25 | _, err = fmt.Sscan(s,
26 | &r.A.Point[0], &r.A.Point[1], &r.A.Point[2],
27 | &r.A.Tangent[0], &r.A.Tangent[1], &r.A.Tangent[2],
28 | &r.B.Point[0], &r.B.Point[1], &r.B.Point[2],
29 | &r.B.Tangent[0], &r.B.Tangent[1], &r.B.Tangent[2],
30 | )
31 | return r, err
32 | }
33 |
34 | // String formats T as string. See also Parse().
35 | func (herm *T) String() string {
36 | return fmt.Sprintf("%s %s %s %s",
37 | herm.A.Point.String(), herm.A.Tangent.String(),
38 | herm.B.Point.String(), herm.B.Tangent.String(),
39 | )
40 | }
41 |
42 | // Point returns a point on a hermit spline at t (0,1).
43 | func (herm *T) Point(t float64) vec3.T {
44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
45 | }
46 |
47 | // Tangent returns a tangent on a hermit spline at t (0,1).
48 | func (herm *T) Tangent(t float64) vec3.T {
49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
50 | }
51 |
52 | // Length returns the length of a hermit spline from A.Point to t (0,1).
53 | func (herm *T) Length(t float64) float64 {
54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
55 | }
56 |
57 | // Point returns a point on a hermit spline at t (0,1).
58 | func Point(pointA, tangentA, pointB, tangentB *vec3.T, t float64) vec3.T {
59 | t2 := t * t
60 | t3 := t2 * t
61 |
62 | f := 2*t3 - 3*t2 + 1
63 | result := pointA.Scaled(f)
64 |
65 | f = t3 - 2*t2 + t
66 | tAf := tangentA.Scaled(f)
67 | result.Add(&tAf)
68 |
69 | f = t3 - t2
70 | tBf := tangentB.Scaled(f)
71 | result.Add(&tBf)
72 |
73 | f = -2*t3 + 3*t2
74 | pAf := pointB.Scaled(f)
75 | result.Add(&pAf)
76 |
77 | return result
78 | }
79 |
80 | // Tangent returns a tangent on a hermit spline at t (0,1).
81 | func Tangent(pointA, tangentA, pointB, tangentB *vec3.T, t float64) vec3.T {
82 | t2 := t * t
83 | t3 := t2 * t
84 |
85 | f := 2*t3 - 3*t2
86 | result := pointA.Scaled(f)
87 |
88 | f = t3 - 2*t2 + 1
89 | tAf := tangentA.Scaled(f)
90 | result.Add(&tAf)
91 |
92 | f = t3 - t2
93 | tBf := tangentB.Scaled(f)
94 | result.Add(&tBf)
95 |
96 | f = -2*t3 + 3*t2
97 | pAf := pointB.Scaled(f)
98 | result.Add(&pAf)
99 |
100 | return result
101 | }
102 |
103 | // Length returns the length of a hermit spline from pointA to t (0,1).
104 | func Length(pointA, tangentA, pointB, tangentB *vec3.T, t float64) float64 {
105 | sqrT := t * t
106 | t1 := sqrT * 0.5
107 | t2 := sqrT * t * 1.0 / 3.0
108 | t3 := sqrT*sqrT + 1.0/4.0
109 |
110 | f := 2*t3 - 3*t2 + t
111 | result := pointA.Scaled(f)
112 |
113 | f = t3 - 2*t2 + t1
114 | tAf := tangentA.Scaled(f)
115 | result.Add(&tAf)
116 |
117 | f = t3 - t2
118 | tBf := tangentB.Scaled(f)
119 | result.Add(&tBf)
120 |
121 | f = -2*t3 + 3*t2
122 | pBf := pointB.Scaled(f)
123 | result.Add(&pBf)
124 |
125 | return result.Length()
126 | }
127 |
--------------------------------------------------------------------------------
/hermit3/hermit3.go:
--------------------------------------------------------------------------------
1 | // Package hermit3 contains a float32 type T and functions for 3D cubic hermit splines.
2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline
3 | package hermit
4 |
5 | import (
6 | "fmt"
7 | "github.com/ungerik/go3d/vec3"
8 | )
9 |
10 | // PointTangent contains a point and a tangent at that point.
11 | // This is a helper sub-struct for T.
12 | type PointTangent struct {
13 | Point vec3.T
14 | Tangent vec3.T
15 | }
16 |
17 | // T holds the data to define a hermit spline.
18 | type T struct {
19 | A PointTangent
20 | B PointTangent
21 | }
22 |
23 | // Parse parses T from a string. See also String()
24 | func Parse(s string) (r T, err error) {
25 | _, err = fmt.Sscan(s,
26 | &r.A.Point[0], &r.A.Point[1], &r.A.Point[2],
27 | &r.A.Tangent[0], &r.A.Tangent[1], &r.A.Tangent[2],
28 | &r.B.Point[0], &r.B.Point[1], &r.B.Point[2],
29 | &r.B.Tangent[0], &r.B.Tangent[1], &r.B.Tangent[2],
30 | )
31 | return r, err
32 | }
33 |
34 | // String formats T as string. See also Parse().
35 | func (herm *T) String() string {
36 | return fmt.Sprintf("%s %s %s %s",
37 | herm.A.Point.String(), herm.A.Tangent.String(),
38 | herm.B.Point.String(), herm.B.Tangent.String(),
39 | )
40 | }
41 |
42 | // Point returns a point on a hermit spline at t (0,1).
43 | func (herm *T) Point(t float32) vec3.T {
44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
45 | }
46 |
47 | // Tangent returns a tangent on a hermit spline at t (0,1).
48 | func (herm *T) Tangent(t float32) vec3.T {
49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
50 | }
51 |
52 | // Length returns the length of a hermit spline from A.Point to t (0,1).
53 | func (herm *T) Length(t float32) float32 {
54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t)
55 | }
56 |
57 | // Point returns a point on a hermit spline at t (0,1).
58 | func Point(pointA, tangentA, pointB, tangentB *vec3.T, t float32) vec3.T {
59 | t2 := t * t
60 | t3 := t2 * t
61 |
62 | f := 2*t3 - 3*t2 + 1
63 | result := pointA.Scaled(f)
64 |
65 | f = t3 - 2*t2 + t
66 | tAf := tangentA.Scaled(f)
67 | result.Add(&tAf)
68 |
69 | f = t3 - t2
70 | tBf := tangentB.Scaled(f)
71 | result.Add(&tBf)
72 |
73 | f = -2*t3 + 3*t2
74 | pAf := pointB.Scaled(f)
75 | result.Add(&pAf)
76 |
77 | return result
78 | }
79 |
80 | // Tangent returns a tangent on a hermit spline at t (0,1).
81 | func Tangent(pointA, tangentA, pointB, tangentB *vec3.T, t float32) vec3.T {
82 | t2 := t * t
83 | t3 := t2 * t
84 |
85 | f := 2*t3 - 3*t2
86 | result := pointA.Scaled(f)
87 |
88 | f = t3 - 2*t2 + 1
89 | tAf := tangentA.Scaled(f)
90 | result.Add(&tAf)
91 |
92 | f = t3 - t2
93 | tBf := tangentB.Scaled(f)
94 | result.Add(&tBf)
95 |
96 | f = -2*t3 + 3*t2
97 | pAf := pointB.Scaled(f)
98 | result.Add(&pAf)
99 |
100 | return result
101 | }
102 |
103 | // Length returns the length of a hermit spline from pointA to t (0,1).
104 | func Length(pointA, tangentA, pointB, tangentB *vec3.T, t float32) float32 {
105 | sqrT := t * t
106 | t1 := sqrT * 0.5
107 | t2 := sqrT * t * 1.0 / 3.0
108 | t3 := sqrT*sqrT + 1.0/4.0
109 |
110 | f := 2*t3 - 3*t2 + t
111 | result := pointA.Scaled(f)
112 |
113 | f = t3 - 2*t2 + t1
114 | tAf := tangentA.Scaled(f)
115 | result.Add(&tAf)
116 |
117 | f = t3 - t2
118 | tBf := tangentB.Scaled(f)
119 | result.Add(&tBf)
120 |
121 | f = -2*t3 + 3*t2
122 | pBf := pointB.Scaled(f)
123 | result.Add(&pBf)
124 |
125 | return result.Length()
126 | }
127 |
--------------------------------------------------------------------------------
/mat2/mat2_test.go:
--------------------------------------------------------------------------------
1 | package mat2
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/vec2"
8 | )
9 |
10 | const EPSILON = 0.0001
11 |
12 | // Some matrices used in multiple tests.
13 | var (
14 | invertableMatrix1 = T{vec2.T{4, -2}, vec2.T{8, -3}}
15 | invertedMatrix1 = T{vec2.T{-3.0 / 4.0, 1.0 / 2.0}, vec2.T{-2, 1}}
16 | nonInvertableMatrix1 = T{vec2.T{1, 1}, vec2.T{1, 1}}
17 | nonInvertableMatrix2 = T{vec2.T{2, 0}, vec2.T{1, 0}}
18 |
19 | row123Changed, _ = Parse("3 1 2 5")
20 | )
21 |
22 | func TestT_Transposed(t *testing.T) {
23 | matrix := T{
24 | vec2.T{1, 2},
25 | vec2.T{3, 4},
26 | }
27 | expectedMatrix := T{
28 | vec2.T{1, 3},
29 | vec2.T{2, 4},
30 | }
31 |
32 | transposedMatrix := matrix.Transposed()
33 |
34 | if transposedMatrix != expectedMatrix {
35 | t.Errorf("matrix trasnposed wrong: %v --> %v", matrix, transposedMatrix)
36 | }
37 | }
38 |
39 | func TestT_Transpose(t *testing.T) {
40 | matrix := T{
41 | vec2.T{10, 20},
42 | vec2.T{30, 40},
43 | }
44 |
45 | expectedMatrix := T{
46 | vec2.T{10, 30},
47 | vec2.T{20, 40},
48 | }
49 |
50 | transposedMatrix := matrix
51 | transposedMatrix.Transpose()
52 |
53 | if transposedMatrix != expectedMatrix {
54 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
55 | }
56 | }
57 |
58 | func TestDeterminant_2(t *testing.T) {
59 | detTwo := Ident
60 | detTwo[0][0] = 2
61 | if det := detTwo.Determinant(); det != (2*1 - 0*0) {
62 | t.Errorf("Wrong determinant: %f", det)
63 | }
64 | }
65 |
66 | func TestDeterminant_3(t *testing.T) {
67 | scale2 := Ident.Scaled(2)
68 | if det := scale2.Determinant(); det != (2*2 - 0*0) {
69 | t.Errorf("Wrong determinant: %f", det)
70 | }
71 | }
72 |
73 | func TestDeterminant_4(t *testing.T) {
74 | row1changed, _ := Parse("3 0 2 2")
75 | if det := row1changed.Determinant(); det != (3*2 - 0*2) {
76 | t.Errorf("Wrong determinant: %f", det)
77 | }
78 | }
79 |
80 | func TestDeterminant_5(t *testing.T) {
81 | row12changed, _ := Parse("3 1 2 5")
82 | if det := row12changed.Determinant(); det != (3*5 - 1*2) {
83 | t.Errorf("Wrong determinant: %f", det)
84 | }
85 | }
86 |
87 | func TestDeterminant_7(t *testing.T) {
88 | randomMatrix, err := Parse("0.43685 0.81673 0.16600 0.40608")
89 | randomMatrix.Transpose()
90 | if err != nil {
91 | t.Errorf("Could not parse random matrix: %v", err)
92 | }
93 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.0418189) {
94 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det)
95 | }
96 | }
97 |
98 | func PracticallyEquals(value1 float32, value2 float32) bool {
99 | return math.Abs(float64(value1-value2)) > EPSILON
100 | }
101 |
102 | func TestDeterminant_6(t *testing.T) {
103 | row123changed := row123Changed
104 | if det := row123changed.Determinant(); det != (3*5 - 2*1) {
105 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det)
106 | }
107 | }
108 |
109 | func TestDeterminant_1(t *testing.T) {
110 | detId := Ident.Determinant()
111 | if detId != 1 {
112 | t.Errorf("Wrong determinant for identity matrix: %f", detId)
113 | }
114 | }
115 |
116 | func TestInvert_ok(t *testing.T) {
117 | inv := invertableMatrix1
118 | _, err := inv.Invert()
119 |
120 | if err != nil {
121 | t.Error("Inverse not computed correctly", err)
122 | }
123 |
124 | invExpected := invertedMatrix1
125 | if inv != invExpected {
126 | t.Errorf("Inverse not computed correctly: %#v", inv)
127 | }
128 | }
129 |
130 | func TestInvert_nok_1(t *testing.T) {
131 | inv := nonInvertableMatrix1
132 | _, err := inv.Inverted()
133 | if err == nil {
134 | t.Error("Inverse should not be possible", err)
135 | }
136 | }
137 |
138 | func TestInvert_nok_2(t *testing.T) {
139 | inv := nonInvertableMatrix2
140 | _, err := inv.Inverted()
141 | if err == nil {
142 | t.Error("Inverse should not be possible", err)
143 | }
144 | }
145 |
146 | func TestIsZeroEps(t *testing.T) {
147 | tests := []struct {
148 | name string
149 | mat T
150 | epsilon float32
151 | want bool
152 | }{
153 | {"exact zero", Zero, 0.0001, true},
154 | {"within epsilon", T{vec2.T{0.00001, -0.00001}, vec2.T{0.00001, -0.00001}}, 0.0001, true},
155 | {"at epsilon boundary", T{vec2.T{0.0001, 0.0001}, vec2.T{0.0001, 0.0001}}, 0.0001, true},
156 | {"outside epsilon", T{vec2.T{0.001, 0}, vec2.T{0, 0}}, 0.0001, false},
157 | {"one element outside", T{vec2.T{0.00001, 0.00001}, vec2.T{0.001, 0.00001}}, 0.0001, false},
158 | {"negative outside epsilon", T{vec2.T{-0.001, 0}, vec2.T{0, 0}}, 0.0001, false},
159 | {"identity matrix", Ident, 0.0001, false},
160 | }
161 |
162 | for _, tt := range tests {
163 | t.Run(tt.name, func(t *testing.T) {
164 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
165 | t.Errorf("IsZeroEps() = %v, want %v for mat %v with epsilon %v", got, tt.want, tt.mat, tt.epsilon)
166 | }
167 | })
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/float64/mat2/mat2_test.go:
--------------------------------------------------------------------------------
1 | package mat2
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/float64/vec2"
8 | )
9 |
10 | const EPSILON = 0.0001
11 |
12 | // Some matrices used in multiple tests.
13 | var (
14 | invertableMatrix1 = T{vec2.T{4, -2}, vec2.T{8, -3}}
15 | invertedMatrix1 = T{vec2.T{-3.0 / 4.0, 1.0 / 2.0}, vec2.T{-2, 1}}
16 | nonInvertableMatrix1 = T{vec2.T{1, 1}, vec2.T{1, 1}}
17 | nonInvertableMatrix2 = T{vec2.T{2, 0}, vec2.T{1, 0}}
18 |
19 | row123Changed, _ = Parse("3 1 2 5")
20 | )
21 |
22 | func TestT_Transposed(t *testing.T) {
23 | matrix := T{
24 | vec2.T{1, 2},
25 | vec2.T{3, 4},
26 | }
27 | expectedMatrix := T{
28 | vec2.T{1, 3},
29 | vec2.T{2, 4},
30 | }
31 |
32 | transposedMatrix := matrix.Transposed()
33 |
34 | if transposedMatrix != expectedMatrix {
35 | t.Errorf("matrix trasnposed wrong: %v --> %v", matrix, transposedMatrix)
36 | }
37 | }
38 |
39 | func TestT_Transpose(t *testing.T) {
40 | matrix := T{
41 | vec2.T{10, 20},
42 | vec2.T{30, 40},
43 | }
44 |
45 | expectedMatrix := T{
46 | vec2.T{10, 30},
47 | vec2.T{20, 40},
48 | }
49 |
50 | transposedMatrix := matrix
51 | transposedMatrix.Transpose()
52 |
53 | if transposedMatrix != expectedMatrix {
54 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
55 | }
56 | }
57 |
58 | func TestDeterminant_2(t *testing.T) {
59 | detTwo := Ident
60 | detTwo[0][0] = 2
61 | if det := detTwo.Determinant(); det != (2*1 - 0*0) {
62 | t.Errorf("Wrong determinant: %f", det)
63 | }
64 | }
65 |
66 | func TestDeterminant_3(t *testing.T) {
67 | scale2 := Ident.Scaled(2)
68 | if det := scale2.Determinant(); det != (2*2 - 0*0) {
69 | t.Errorf("Wrong determinant: %f", det)
70 | }
71 | }
72 |
73 | func TestDeterminant_4(t *testing.T) {
74 | row1changed, _ := Parse("3 0 2 2")
75 | if det := row1changed.Determinant(); det != (3*2 - 0*2) {
76 | t.Errorf("Wrong determinant: %f", det)
77 | }
78 | }
79 |
80 | func TestDeterminant_5(t *testing.T) {
81 | row12changed, _ := Parse("3 1 2 5")
82 | if det := row12changed.Determinant(); det != (3*5 - 1*2) {
83 | t.Errorf("Wrong determinant: %f", det)
84 | }
85 | }
86 |
87 | func TestDeterminant_7(t *testing.T) {
88 | randomMatrix, err := Parse("0.43685 0.81673 0.16600 0.40608")
89 | randomMatrix.Transpose()
90 | if err != nil {
91 | t.Errorf("Could not parse random matrix: %v", err)
92 | }
93 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.0418189) {
94 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det)
95 | }
96 | }
97 |
98 | func PracticallyEquals(value1 float64, value2 float64) bool {
99 | return math.Abs(value1-value2) > EPSILON
100 | }
101 |
102 | func TestDeterminant_6(t *testing.T) {
103 | row123changed := row123Changed
104 | if det := row123changed.Determinant(); det != (3*5 - 2*1) {
105 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det)
106 | }
107 | }
108 |
109 | func TestDeterminant_1(t *testing.T) {
110 | detId := Ident.Determinant()
111 | if detId != 1 {
112 | t.Errorf("Wrong determinant for identity matrix: %f", detId)
113 | }
114 | }
115 |
116 | func TestInvert_ok(t *testing.T) {
117 | inv := invertableMatrix1
118 | _, err := inv.Invert()
119 |
120 | if err != nil {
121 | t.Error("Inverse not computed correctly", err)
122 | }
123 |
124 | invExpected := invertedMatrix1
125 | if inv != invExpected {
126 | t.Errorf("Inverse not computed correctly: %#v", inv)
127 | }
128 | }
129 |
130 | func TestInvert_nok_1(t *testing.T) {
131 | inv := nonInvertableMatrix1
132 | _, err := inv.Inverted()
133 | if err == nil {
134 | t.Error("Inverse should not be possible", err)
135 | }
136 | }
137 |
138 | func TestInvert_nok_2(t *testing.T) {
139 | inv := nonInvertableMatrix2
140 | _, err := inv.Inverted()
141 | if err == nil {
142 | t.Error("Inverse should not be possible", err)
143 | }
144 | }
145 |
146 | func TestIsZeroEps(t *testing.T) {
147 | tests := []struct {
148 | name string
149 | mat T
150 | epsilon float64
151 | want bool
152 | }{
153 | {"exact zero", Zero, 0.0001, true},
154 | {"within epsilon", T{vec2.T{0.00001, -0.00001}, vec2.T{0.00001, -0.00001}}, 0.0001, true},
155 | {"at epsilon boundary", T{vec2.T{0.0001, 0.0001}, vec2.T{0.0001, 0.0001}}, 0.0001, true},
156 | {"outside epsilon", T{vec2.T{0.001, 0}, vec2.T{0, 0}}, 0.0001, false},
157 | {"one element outside", T{vec2.T{0.00001, 0.00001}, vec2.T{0.001, 0.00001}}, 0.0001, false},
158 | {"negative outside epsilon", T{vec2.T{-0.001, 0}, vec2.T{0, 0}}, 0.0001, false},
159 | {"identity matrix", Ident, 0.0001, false},
160 | }
161 |
162 | for _, tt := range tests {
163 | t.Run(tt.name, func(t *testing.T) {
164 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
165 | t.Errorf("IsZeroEps() = %v, want %v for mat %v with epsilon %v", got, tt.want, tt.mat, tt.epsilon)
166 | }
167 | })
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/float64/quaternion/quaternion_test.go:
--------------------------------------------------------------------------------
1 | package quaternion
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "testing"
7 |
8 | "github.com/ungerik/go3d/float64/vec3"
9 | )
10 |
11 | // RotateVec3 rotates v by the rotation represented by the quaternion.
12 | func rotateAndNormalizeVec3(quat *T, v *vec3.T) {
13 | qv := T{v[0], v[1], v[2], 0}
14 | inv := quat.Inverted()
15 | q := Mul3(quat, &qv, &inv)
16 | v[0] = q[0]
17 | v[1] = q[1]
18 | v[2] = q[2]
19 | }
20 |
21 | func TestQuaternionRotateVec3(t *testing.T) {
22 | eulerAngles := []vec3.T{
23 | {90, 20, 21},
24 | {-90, 0, 0},
25 | {28, 1043, -38},
26 | }
27 | vecs := []vec3.T{
28 | {2, 3, 4},
29 | {1, 3, -2},
30 | {-6, 2, 9},
31 | }
32 | for _, vec := range vecs {
33 | for _, eulerAngle := range eulerAngles {
34 | func() {
35 | q := FromEulerAngles(eulerAngle[1]*math.Pi/180.0, eulerAngle[0]*math.Pi/180.0, eulerAngle[2]*math.Pi/180.0)
36 | vec_r1 := vec
37 | vec_r2 := vec
38 | magSqr := vec_r1.LengthSqr()
39 | rotateAndNormalizeVec3(&q, &vec_r2)
40 | q.RotateVec3(&vec_r1)
41 | vecd := q.RotatedVec3(&vec)
42 | magSqr2 := vec_r1.LengthSqr()
43 |
44 | if !vecd.PracticallyEquals(&vec_r1, 0.000000000000001) {
45 | t.Logf("test case %v rotates %v failed - vector rotation: %+v, %+v\n", eulerAngle, vec, vecd, vec_r1)
46 | t.Fail()
47 | }
48 |
49 | angle := vec3.Angle(&vec_r1, &vec_r2)
50 | length := math.Abs(magSqr - magSqr2)
51 |
52 | if angle > 0.0000001 {
53 | t.Logf("test case %v rotates %v failed - angle difference to large\n", eulerAngle, vec)
54 | t.Logf("vectors: %+v, %+v\n", vec_r1, vec_r2)
55 | t.Logf("angle: %v\n", angle)
56 | t.Fail()
57 | }
58 |
59 | if length > 0.000000000001 {
60 | t.Logf("test case %v rotates %v failed - squared length difference to large\n", eulerAngle, vec)
61 | t.Logf("vectors: %+v %+v\n", vec_r1, vec_r2)
62 | t.Logf("squared lengths: %v, %v\n", magSqr, magSqr2)
63 | t.Fail()
64 | }
65 | }()
66 | }
67 | }
68 | }
69 |
70 | func TestToEulerAngles(t *testing.T) {
71 | specialValues := []float64{-5, -math.Pi, -2, -math.Pi / 2, 0, math.Pi / 2, 2.4, math.Pi, 3.9}
72 | for _, x := range specialValues {
73 | for _, y := range specialValues {
74 | for _, z := range specialValues {
75 | quat1 := FromEulerAngles(y, x, z)
76 | ry, rx, rz := quat1.ToEulerAngles()
77 | quat2 := FromEulerAngles(ry, rx, rz)
78 | // quat must be equivalent
79 | const e64 = 1e-14
80 | cond1 := math.Abs(quat1[0]-quat2[0]) < e64 && math.Abs(quat1[1]-quat2[1]) < e64 && math.Abs(quat1[2]-quat2[2]) < e64 && math.Abs(quat1[3]-quat2[3]) < e64
81 | cond2 := math.Abs(quat1[0]+quat2[0]) < e64 && math.Abs(quat1[1]+quat2[1]) < e64 && math.Abs(quat1[2]+quat2[2]) < e64 && math.Abs(quat1[3]+quat2[3]) < e64
82 | if !cond1 && !cond2 {
83 | fmt.Printf("test case %v, %v, %v failed\n", x, y, z)
84 | fmt.Printf("result is %v, %v, %v\n", rx, ry, rz)
85 | fmt.Printf("quat1 is %v\n", quat1)
86 | fmt.Printf("quat2 is %v\n", quat2)
87 | t.Fail()
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 | func TestNormalizeEdgeCases(t *testing.T) {
95 | tests := []struct {
96 | name string
97 | quat T
98 | checkNorm bool
99 | }{
100 | {"zero quaternion", T{0, 0, 0, 0}, false},
101 | {"tiny quaternion (below epsilon)", T{1e-16, 1e-16, 1e-16, 1e-16}, false},
102 | {"already normalized", T{1, 0, 0, 0}, true},
103 | {"nearly normalized positive deviation", T{1.00000000001, 0, 0, 0}, true},
104 | {"nearly normalized negative deviation", T{0.99999999999, 0, 0, 0}, true},
105 | {"nearly normalized mixed", T{0.5, 0.5, 0.5, 0.5}, true}, // sqrt(4*0.25) = 1
106 | {"needs normalization", T{2, 0, 0, 0}, true},
107 | {"needs normalization mixed", T{1, 1, 1, 1}, true},
108 | }
109 |
110 | for _, tt := range tests {
111 | t.Run(tt.name, func(t *testing.T) {
112 | original := tt.quat
113 | originalNorm := original.Norm()
114 | result := tt.quat.Normalize()
115 |
116 | if result != &tt.quat {
117 | t.Errorf("Normalize() should return pointer to quat")
118 | }
119 |
120 | if tt.checkNorm {
121 | norm := tt.quat.Norm()
122 | // For unit quaternions, norm (squared magnitude) should be 1
123 | if math.Abs(norm-1.0) > 0.00001 {
124 | t.Errorf("After Normalize(), Norm() = %v, want 1.0 (original norm=%v)", norm, originalNorm)
125 | }
126 | }
127 | })
128 | }
129 | }
130 |
131 | func TestNormalizedEdgeCases(t *testing.T) {
132 | tests := []struct {
133 | name string
134 | quat T
135 | checkNorm bool
136 | }{
137 | {"zero quaternion", T{0, 0, 0, 0}, false},
138 | {"tiny quaternion", T{1e-16, 1e-16, 1e-16, 1e-16}, false},
139 | {"already normalized", T{1, 0, 0, 0}, true},
140 | {"needs normalization", T{2, 0, 0, 0}, true},
141 | {"needs normalization mixed", T{1, 1, 1, 1}, true},
142 | }
143 |
144 | for _, tt := range tests {
145 | t.Run(tt.name, func(t *testing.T) {
146 | original := tt.quat
147 | result := tt.quat.Normalized()
148 |
149 | if tt.quat != original {
150 | t.Errorf("Normalized() modified original quaternion")
151 | }
152 |
153 | if tt.checkNorm {
154 | norm := result.Norm()
155 | if math.Abs(norm-1.0) > 0.00001 {
156 | t.Errorf("Normalized().Norm() = %v, want 1.0", norm)
157 | }
158 | }
159 | })
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/float64/mat4/mat4_test.go:
--------------------------------------------------------------------------------
1 | package mat4
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/float64/vec3"
8 | "github.com/ungerik/go3d/float64/vec4"
9 | )
10 |
11 | const EPSILON = 0.0000001
12 |
13 | func TestIsZeroEps(t *testing.T) {
14 | tests := []struct {
15 | name string
16 | mat T
17 | epsilon float64
18 | want bool
19 | }{
20 | {"exact zero", Zero, 0.0001, true},
21 | {"within epsilon", T{vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}, vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}}, 0.0001, true},
22 | {"at epsilon boundary", T{vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}}, 0.0001, true},
23 | {"outside epsilon", T{vec4.T{0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false},
24 | {"one element outside", T{vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}}, 0.0001, false},
25 | {"negative outside epsilon", T{vec4.T{-0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false},
26 | {"identity matrix", Ident, 0.0001, false},
27 | }
28 |
29 | for _, tt := range tests {
30 | t.Run(tt.name, func(t *testing.T) {
31 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
32 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon)
33 | }
34 | })
35 | }
36 | }
37 |
38 | func TestScale(t *testing.T) {
39 | m := Ident
40 | m.Scale(2.5)
41 |
42 | expected := Ident
43 | expected[0][0] = 2.5
44 | expected[1][1] = 2.5
45 | expected[2][2] = 2.5
46 |
47 | if m != expected {
48 | t.Errorf("Scale() failed: got %v, want %v", m, expected)
49 | }
50 | }
51 |
52 | func TestScaled(t *testing.T) {
53 | m := Ident
54 | result := m.Scaled(2.5)
55 |
56 | // Original should be unchanged
57 | if m != Ident {
58 | t.Errorf("Scaled() modified original matrix")
59 | }
60 |
61 | expected := Ident
62 | expected[0][0] = 2.5
63 | expected[1][1] = 2.5
64 | expected[2][2] = 2.5
65 |
66 | if result != expected {
67 | t.Errorf("Scaled() failed: got %v, want %v", result, expected)
68 | }
69 | }
70 |
71 | func TestTrace(t *testing.T) {
72 | m := Ident
73 | if trace := m.Trace(); trace != 4.0 {
74 | t.Errorf("Trace() of identity should be 4.0, got %f", trace)
75 | }
76 |
77 | m[0][0] = 2
78 | m[1][1] = 3
79 | m[2][2] = 5
80 | m[3][3] = 7
81 | if trace := m.Trace(); trace != 17.0 {
82 | t.Errorf("Trace() should be 17.0, got %f", trace)
83 | }
84 | }
85 |
86 | func TestTrace3(t *testing.T) {
87 | m := Ident
88 | if trace := m.Trace3(); trace != 3.0 {
89 | t.Errorf("Trace3() of identity should be 3.0, got %f", trace)
90 | }
91 |
92 | m[0][0] = 2
93 | m[1][1] = 3
94 | m[2][2] = 5
95 | m[3][3] = 7 // Should not be included in Trace3
96 | if trace := m.Trace3(); trace != 10.0 {
97 | t.Errorf("Trace3() should be 10.0, got %f", trace)
98 | }
99 | }
100 |
101 | func TestAssignMul(t *testing.T) {
102 | m1, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 3 5 7 1")
103 | m2, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 2 4 8 1")
104 | var result T
105 | result.AssignMul(&m1, &m2)
106 | expected, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 5 9 15 1")
107 |
108 | if result != expected {
109 | t.Errorf("AssignMul() incorrect: got %v, want %v", result, expected)
110 | }
111 | }
112 |
113 | func TestMulVec4vsTransformVec4(t *testing.T) {
114 | m, _ := Parse("2 0 0 0 0 3 0 0 0 0 4 0 1 2 3 1")
115 | v := vec4.T{1, 2, 3, 1}
116 |
117 | // Method 1: MulVec4
118 | result1 := m.MulVec4(&v)
119 |
120 | // Method 2: TransformVec4
121 | result2 := v
122 | m.TransformVec4(&result2)
123 |
124 | if math.Abs(result1[0]-result2[0]) > EPSILON ||
125 | math.Abs(result1[1]-result2[1]) > EPSILON ||
126 | math.Abs(result1[2]-result2[2]) > EPSILON ||
127 | math.Abs(result1[3]-result2[3]) > EPSILON {
128 | t.Errorf("MulVec4 and TransformVec4 differ: %v vs %v", result1, result2)
129 | }
130 | }
131 |
132 | func TestMulVec3vsTransformVec3(t *testing.T) {
133 | m, _ := Parse("2 0 0 0 0 3 0 0 0 0 4 0 1 2 3 1")
134 | v := vec3.T{1, 2, 3}
135 |
136 | // Method 1: MulVec3
137 | result1 := m.MulVec3(&v)
138 |
139 | // Method 2: TransformVec3
140 | result2 := v
141 | m.TransformVec3(&result2)
142 |
143 | if math.Abs(result1[0]-result2[0]) > EPSILON ||
144 | math.Abs(result1[1]-result2[1]) > EPSILON ||
145 | math.Abs(result1[2]-result2[2]) > EPSILON {
146 | t.Errorf("MulVec3 and TransformVec3 differ: %v vs %v", result1, result2)
147 | }
148 | }
149 |
150 | func TestTranspose(t *testing.T) {
151 | m, _ := Parse("1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16")
152 | original := m
153 | m.Transpose()
154 |
155 | for i := 0; i < 4; i++ {
156 | for j := 0; j < 4; j++ {
157 | if m[i][j] != original[j][i] {
158 | t.Errorf("Transpose failed at [%d][%d]: got %f, want %f", i, j, m[i][j], original[j][i])
159 | }
160 | }
161 | }
162 | }
163 |
164 | func TestParseAndString(t *testing.T) {
165 | original := "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16"
166 | m, err := Parse(original)
167 | if err != nil {
168 | t.Errorf("Parse() failed: %v", err)
169 | }
170 |
171 | // Parse and stringify should preserve values
172 | m2, err := Parse(m.String())
173 | if err != nil {
174 | t.Errorf("Parse() of String() failed: %v", err)
175 | }
176 |
177 | for i := 0; i < 4; i++ {
178 | for j := 0; j < 4; j++ {
179 | if math.Abs(m[i][j]-m2[i][j]) > EPSILON {
180 | t.Errorf("Parse/String roundtrip failed at [%d][%d]", i, j)
181 | }
182 | }
183 | }
184 | }
185 |
186 | func TestIdentity(t *testing.T) {
187 | // Identity matrix should have 1s on diagonal, 0s elsewhere
188 | for i := 0; i < 4; i++ {
189 | for j := 0; j < 4; j++ {
190 | expected := 0.0
191 | if i == j {
192 | expected = 1.0
193 | }
194 | if Ident[i][j] != expected {
195 | t.Errorf("Identity matrix incorrect at [%d][%d]: got %f, want %f", i, j, Ident[i][j], expected)
196 | }
197 | }
198 | }
199 | }
200 |
201 | func TestZero(t *testing.T) {
202 | // Zero matrix should have all 0s
203 | for i := 0; i < 4; i++ {
204 | for j := 0; j < 4; j++ {
205 | if Zero[i][j] != 0 {
206 | t.Errorf("Zero matrix incorrect at [%d][%d]: got %f, want 0", i, j, Zero[i][j])
207 | }
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/float64/hermit2/hermit2_test.go:
--------------------------------------------------------------------------------
1 | package hermit2
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/float64/vec2"
8 | )
9 |
10 | const EPSILON = 0.0000001
11 |
12 | func TestParseAndString(t *testing.T) {
13 | original := T{
14 | A: PointTangent{
15 | Point: vec2.T{1.0, 2.0},
16 | Tangent: vec2.T{0.5, 0.5},
17 | },
18 | B: PointTangent{
19 | Point: vec2.T{3.0, 4.0},
20 | Tangent: vec2.T{-0.5, 0.5},
21 | },
22 | }
23 |
24 | // Convert to string and parse back
25 | str := original.String()
26 | parsed, err := Parse(str)
27 | if err != nil {
28 | t.Errorf("Parse() failed: %v", err)
29 | }
30 |
31 | // Check all components
32 | if math.Abs(parsed.A.Point[0]-original.A.Point[0]) > EPSILON ||
33 | math.Abs(parsed.A.Point[1]-original.A.Point[1]) > EPSILON ||
34 | math.Abs(parsed.A.Tangent[0]-original.A.Tangent[0]) > EPSILON ||
35 | math.Abs(parsed.A.Tangent[1]-original.A.Tangent[1]) > EPSILON ||
36 | math.Abs(parsed.B.Point[0]-original.B.Point[0]) > EPSILON ||
37 | math.Abs(parsed.B.Point[1]-original.B.Point[1]) > EPSILON ||
38 | math.Abs(parsed.B.Tangent[0]-original.B.Tangent[0]) > EPSILON ||
39 | math.Abs(parsed.B.Tangent[1]-original.B.Tangent[1]) > EPSILON {
40 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original)
41 | }
42 | }
43 |
44 | func TestPointEndpoints(t *testing.T) {
45 | // Create a simple hermit spline from (0,0) to (1,1) with tangents pointing in the same direction
46 | herm := T{
47 | A: PointTangent{
48 | Point: vec2.T{0.0, 0.0},
49 | Tangent: vec2.T{1.0, 1.0},
50 | },
51 | B: PointTangent{
52 | Point: vec2.T{1.0, 1.0},
53 | Tangent: vec2.T{1.0, 1.0},
54 | },
55 | }
56 |
57 | // At t=0, should return pointA
58 | p0 := herm.Point(0.0)
59 | if math.Abs(p0[0]-herm.A.Point[0]) > EPSILON ||
60 | math.Abs(p0[1]-herm.A.Point[1]) > EPSILON {
61 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point)
62 | }
63 |
64 | // At t=1, should return pointB
65 | p1 := herm.Point(1.0)
66 | if math.Abs(p1[0]-herm.B.Point[0]) > EPSILON ||
67 | math.Abs(p1[1]-herm.B.Point[1]) > EPSILON {
68 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point)
69 | }
70 | }
71 |
72 | func TestPointMidpoint(t *testing.T) {
73 | // Horizontal line from (0,0) to (2,0) with horizontal tangents
74 | herm := T{
75 | A: PointTangent{
76 | Point: vec2.T{0.0, 0.0},
77 | Tangent: vec2.T{1.0, 0.0},
78 | },
79 | B: PointTangent{
80 | Point: vec2.T{2.0, 0.0},
81 | Tangent: vec2.T{1.0, 0.0},
82 | },
83 | }
84 |
85 | // At t=0.5, should be approximately at (1, 0) for a straight line
86 | p := herm.Point(0.5)
87 |
88 | // The y-coordinate should be exactly 0 for horizontal tangents
89 | if math.Abs(p[1]) > EPSILON {
90 | t.Errorf("Point(0.5) y-coordinate should be 0: got %v", p[1])
91 | }
92 |
93 | // The x-coordinate should be approximately 1.0
94 | if math.Abs(p[0]-1.0) > EPSILON {
95 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0])
96 | }
97 | }
98 |
99 | func TestPointFunctionMatchesMethod(t *testing.T) {
100 | herm := T{
101 | A: PointTangent{
102 | Point: vec2.T{0.0, 0.0},
103 | Tangent: vec2.T{1.0, 0.5},
104 | },
105 | B: PointTangent{
106 | Point: vec2.T{2.0, 1.0},
107 | Tangent: vec2.T{1.0, -0.5},
108 | },
109 | }
110 |
111 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
112 | for _, tVal := range tests {
113 | // Method call
114 | p1 := herm.Point(tVal)
115 |
116 | // Function call
117 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
118 |
119 | if math.Abs(p1[0]-p2[0]) > EPSILON ||
120 | math.Abs(p1[1]-p2[1]) > EPSILON {
121 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2)
122 | }
123 | }
124 | }
125 |
126 | func TestTangentAtZero(t *testing.T) {
127 | herm := T{
128 | A: PointTangent{
129 | Point: vec2.T{0.0, 0.0},
130 | Tangent: vec2.T{1.0, 0.5},
131 | },
132 | B: PointTangent{
133 | Point: vec2.T{2.0, 1.0},
134 | Tangent: vec2.T{0.5, 1.0},
135 | },
136 | }
137 |
138 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0)
139 | tan0 := herm.Tangent(0.0)
140 | expectedTan0 := herm.A.Tangent
141 | if math.Abs(tan0[0]-expectedTan0[0]) > EPSILON ||
142 | math.Abs(tan0[1]-expectedTan0[1]) > EPSILON {
143 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0)
144 | }
145 | }
146 |
147 | func TestTangentFunctionMatchesMethod(t *testing.T) {
148 | herm := T{
149 | A: PointTangent{
150 | Point: vec2.T{0.0, 0.0},
151 | Tangent: vec2.T{1.0, 0.5},
152 | },
153 | B: PointTangent{
154 | Point: vec2.T{2.0, 1.0},
155 | Tangent: vec2.T{0.5, 1.0},
156 | },
157 | }
158 |
159 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
160 | for _, tVal := range tests {
161 | // Method call
162 | tan1 := herm.Tangent(tVal)
163 |
164 | // Function call
165 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
166 |
167 | if math.Abs(tan1[0]-tan2[0]) > EPSILON ||
168 | math.Abs(tan1[1]-tan2[1]) > EPSILON {
169 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2)
170 | }
171 | }
172 | }
173 |
174 | func TestLengthValues(t *testing.T) {
175 | herm := T{
176 | A: PointTangent{
177 | Point: vec2.T{0.0, 0.0},
178 | Tangent: vec2.T{1.0, 0.0},
179 | },
180 | B: PointTangent{
181 | Point: vec2.T{2.0, 0.0},
182 | Tangent: vec2.T{1.0, 0.0},
183 | },
184 | }
185 |
186 | // Test that Length returns a valid positive number
187 | length0 := herm.Length(0.0)
188 | if math.IsNaN(length0) || math.IsInf(length0, 0) {
189 | t.Errorf("Length(0) should be a valid number: got %f", length0)
190 | }
191 |
192 | length1 := herm.Length(1.0)
193 | if math.IsNaN(length1) || math.IsInf(length1, 0) {
194 | t.Errorf("Length(1) should be a valid number: got %f", length1)
195 | }
196 | }
197 |
198 | func TestLengthFunctionMatchesMethod(t *testing.T) {
199 | herm := T{
200 | A: PointTangent{
201 | Point: vec2.T{0.0, 0.0},
202 | Tangent: vec2.T{1.0, 0.5},
203 | },
204 | B: PointTangent{
205 | Point: vec2.T{2.0, 1.0},
206 | Tangent: vec2.T{0.5, 1.0},
207 | },
208 | }
209 |
210 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
211 | for _, tVal := range tests {
212 | // Method call
213 | len1 := herm.Length(tVal)
214 |
215 | // Function call
216 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
217 |
218 | if math.Abs(len1-len2) > EPSILON {
219 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2)
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/float64/mat2/mat2.go:
--------------------------------------------------------------------------------
1 | // Package mat2 contains a 2x2 float64 matrix type T and functions.
2 | package mat2
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | "math"
8 |
9 | "github.com/ungerik/go3d/float64/generic"
10 | "github.com/ungerik/go3d/float64/vec2"
11 | )
12 |
13 | var (
14 | // Zero holds a zero matrix.
15 | Zero = T{
16 | vec2.T{0, 0},
17 | vec2.T{0, 0},
18 | }
19 |
20 | // Ident holds an ident matrix.
21 | Ident = T{
22 | vec2.T{1, 0},
23 | vec2.T{0, 1},
24 | }
25 | )
26 |
27 | // T represents a 2x2 matrix.
28 | type T [2]vec2.T
29 |
30 | // From copies a T from a generic.T implementation.
31 | func From(other generic.T) T {
32 | r := Ident
33 | cols := other.Cols()
34 | rows := other.Rows()
35 | if (cols == 3 && rows == 3) || (cols == 4 && rows == 4) {
36 | cols = 2
37 | rows = 2
38 | } else if !(cols == 2 && rows == 2) {
39 | panic("Unsupported type")
40 | }
41 | for col := 0; col < cols; col++ {
42 | for row := 0; row < rows; row++ {
43 | r[col][row] = other.Get(col, row)
44 | }
45 | }
46 | return r
47 | }
48 |
49 | // Parse parses T from a string. See also String()
50 | func Parse(s string) (r T, err error) {
51 | _, err = fmt.Sscan(s,
52 | &r[0][0], &r[0][1],
53 | &r[1][0], &r[1][1],
54 | )
55 | return r, err
56 | }
57 |
58 | // String formats T as string. See also Parse().
59 | func (mat *T) String() string {
60 | return fmt.Sprintf("%s %s", mat[0].String(), mat[1].String())
61 | }
62 |
63 | // Rows returns the number of rows of the matrix.
64 | func (mat *T) Rows() int {
65 | return 2
66 | }
67 |
68 | // Cols returns the number of columns of the matrix.
69 | func (mat *T) Cols() int {
70 | return 2
71 | }
72 |
73 | // Size returns the number elements of the matrix.
74 | func (mat *T) Size() int {
75 | return 4
76 | }
77 |
78 | // Slice returns the elements of the matrix as slice.
79 | // The data may be a copy depending on the platform implementation.
80 | func (mat *T) Slice() []float64 {
81 | return mat.Array()[:]
82 | }
83 |
84 | // Get returns one element of the matrix.
85 | // Matrices are defined by (two) column vectors.
86 | //
87 | // Note that this function use the opposite reference order of rows and columns to the mathematical matrix indexing.
88 | //
89 | // A value in this matrix is referenced by
where both row and column is in the range [0..1].
90 | // This notation and range reflects the underlying representation.
91 | //
92 | // A value in a matrix A is mathematically referenced by A
93 | // where both row and column is in the range [1..2].
94 | // (It is really the lower case 'a' followed by but this documentation syntax is somewhat limited.)
95 | //
96 | // matrixA.Get(0, 1) == matrixA[0][1] ( == A21 in mathematical indexing)
97 | func (mat *T) Get(col, row int) float64 {
98 | return mat[col][row]
99 | }
100 |
101 | // IsZero checks if all elements of the matrix are exactly zero.
102 | // Uses exact equality comparison, which may not be suitable for floating-point math results.
103 | // For tolerance-based comparison, use IsZeroEps instead.
104 | func (mat *T) IsZero() bool {
105 | return *mat == Zero
106 | }
107 |
108 | // IsZeroEps checks if all elements of the matrix are zero within the given epsilon tolerance.
109 | // This is the recommended method for comparing floating-point matrices that result from calculations.
110 | // For exact zero comparison, use IsZero instead.
111 | func (mat *T) IsZeroEps(epsilon float64) bool {
112 | return math.Abs(mat[0][0]) <= epsilon && math.Abs(mat[0][1]) <= epsilon &&
113 | math.Abs(mat[1][0]) <= epsilon && math.Abs(mat[1][1]) <= epsilon
114 | }
115 |
116 | // Scale multiplies the diagonal scale elements by f returns mat.
117 | func (mat *T) Scale(f float64) *T {
118 | mat[0][0] *= f
119 | mat[1][1] *= f
120 | return mat
121 | }
122 |
123 | // Scaled returns a copy of the matrix with the diagonal scale elements multiplied by f.
124 | func (mat *T) Scaled(f float64) T {
125 | r := *mat
126 | return *r.Scale(f)
127 | }
128 |
129 | // Scaling returns the scaling diagonal of the matrix.
130 | func (mat *T) Scaling() vec2.T {
131 | return vec2.T{mat[0][0], mat[1][1]}
132 | }
133 |
134 | // SetScaling sets the scaling diagonal of the matrix.
135 | func (mat *T) SetScaling(s *vec2.T) *T {
136 | mat[0][0] = s[0]
137 | mat[1][1] = s[1]
138 | return mat
139 | }
140 |
141 | // Trace returns the trace value for the matrix.
142 | func (mat *T) Trace() float64 {
143 | return mat[0][0] + mat[1][1]
144 | }
145 |
146 | // AssignMul multiplies a and b and assigns the result to mat.
147 | func (mat *T) AssignMul(a, b *T) *T {
148 | mat[0] = a.MulVec2(&b[0])
149 | mat[1] = a.MulVec2(&b[1])
150 | return mat
151 | }
152 |
153 | // MulVec2 multiplies vec with mat.
154 | func (mat *T) MulVec2(vec *vec2.T) vec2.T {
155 | return vec2.T{
156 | mat[0][0]*vec[0] + mat[1][0]*vec[1],
157 | mat[0][1]*vec[1] + mat[1][1]*vec[1],
158 | }
159 | }
160 |
161 | // TransformVec2 multiplies v with mat and saves the result in v.
162 | func (mat *T) TransformVec2(v *vec2.T) {
163 | // Use intermediate variables to not alter further computations.
164 | x := mat[0][0]*v[0] + mat[1][0]*v[1]
165 | v[1] = mat[0][1]*v[0] + mat[1][1]*v[1]
166 | v[0] = x
167 | }
168 |
169 | func (mat *T) Determinant() float64 {
170 | return mat[0][0]*mat[1][1] - mat[1][0]*mat[0][1]
171 | }
172 |
173 | // PracticallyEquals compares two matrices if they are equal with each other within a delta tolerance.
174 | func (mat *T) PracticallyEquals(matrix *T, allowedDelta float64) bool {
175 | return mat[0].PracticallyEquals(&matrix[0], allowedDelta) &&
176 | mat[1].PracticallyEquals(&matrix[1], allowedDelta)
177 | }
178 |
179 | // Transpose transposes the matrix.
180 | func (mat *T) Transpose() *T {
181 | mat[0][1], mat[1][0] = mat[1][0], mat[0][1]
182 | return mat
183 | }
184 |
185 | // Transposed returns a transposed copy the matrix.
186 | func (mat *T) Transposed() T {
187 | result := *mat
188 | result.Transpose()
189 | return result
190 | }
191 |
192 | // Invert inverts the given matrix. Destructive operation.
193 | // Does not check if matrix is singular and may lead to strange results!
194 | func (mat *T) Invert() (*T, error) {
195 | determinant := mat.Determinant()
196 | if determinant == 0 {
197 | return &Zero, errors.New("can not create inverted matrix as determinant is 0")
198 | }
199 |
200 | invDet := 1.0 / determinant
201 |
202 | mat[0][0], mat[1][1] = invDet*mat[1][1], invDet*mat[0][0]
203 | mat[0][1] = -invDet * mat[0][1]
204 | mat[1][0] = -invDet * mat[1][0]
205 |
206 | return mat, nil
207 | }
208 |
209 | // Inverted inverts a copy of the given matrix.
210 | // Does not check if matrix is singular and may lead to strange results!
211 | func (mat *T) Inverted() (T, error) {
212 | result := *mat
213 | _, err := result.Invert()
214 | return result, err
215 | }
216 |
--------------------------------------------------------------------------------
/mat2/mat2.go:
--------------------------------------------------------------------------------
1 | // Package mat2 contains a 2x2 float32 matrix type T and functions.
2 | package mat2
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 |
8 | math "github.com/chewxy/math32"
9 | "github.com/ungerik/go3d/generic"
10 | "github.com/ungerik/go3d/vec2"
11 | )
12 |
13 | var (
14 | // Zero holds a zero matrix.
15 | Zero = T{
16 | vec2.T{0, 0},
17 | vec2.T{0, 0},
18 | }
19 |
20 | // Ident holds an ident matrix.
21 | Ident = T{
22 | vec2.T{1, 0},
23 | vec2.T{0, 1},
24 | }
25 | )
26 |
27 | // T represents a 2x2 matrix.
28 | type T [2]vec2.T
29 |
30 | // From copies a T from a generic.T implementation.
31 | func From(other generic.T) T {
32 | r := Ident
33 | cols := other.Cols()
34 | rows := other.Rows()
35 | if (cols == 3 && rows == 3) || (cols == 4 && rows == 4) {
36 | cols = 2
37 | rows = 2
38 | } else if !(cols == 2 && rows == 2) {
39 | panic("Unsupported type")
40 | }
41 | for col := 0; col < cols; col++ {
42 | for row := 0; row < rows; row++ {
43 | r[col][row] = other.Get(col, row)
44 | }
45 | }
46 | return r
47 | }
48 |
49 | // Parse parses T from a string. See also String()
50 | func Parse(s string) (r T, err error) {
51 | _, err = fmt.Sscan(s,
52 | &r[0][0], &r[0][1],
53 | &r[1][0], &r[1][1],
54 | )
55 | return r, err
56 | }
57 |
58 | // String formats T as string. See also Parse().
59 | func (mat *T) String() string {
60 | return fmt.Sprintf("%s %s", mat[0].String(), mat[1].String())
61 | }
62 |
63 | // Rows returns the number of rows of the matrix.
64 | func (mat *T) Rows() int {
65 | return 2
66 | }
67 |
68 | // Cols returns the number of columns of the matrix.
69 | func (mat *T) Cols() int {
70 | return 2
71 | }
72 |
73 | // Size returns the number elements of the matrix.
74 | func (mat *T) Size() int {
75 | return 4
76 | }
77 |
78 | // Slice returns the elements of the matrix as slice.
79 | // The data may be a copy depending on the platform implementation.
80 | func (mat *T) Slice() []float32 {
81 | return mat.Array()[:]
82 | }
83 |
84 | // Get returns one element of the matrix.
85 | // Matrices are defined by (two) column vectors.
86 | //
87 | // Note that this function use the opposite reference order of rows and columns to the mathematical matrix indexing.
88 | //
89 | // A value in this matrix is referenced by where both row and column is in the range [0..1].
90 | // This notation and range reflects the underlying representation.
91 | //
92 | // A value in a matrix A is mathematically referenced by A
93 | // where both row and column is in the range [1..2].
94 | // (It is really the lower case 'a' followed by but this documentation syntax is somewhat limited.)
95 | //
96 | // matrixA.Get(0, 1) == matrixA[0][1] ( == A21 in mathematical indexing)
97 | func (mat *T) Get(col, row int) float32 {
98 | return mat[col][row]
99 | }
100 |
101 | // IsZero checks if all elements of the matrix are exactly zero.
102 | // Uses exact equality comparison, which may not be suitable for floating-point math results.
103 | // For tolerance-based comparison, use IsZeroEps instead.
104 | func (mat *T) IsZero() bool {
105 | return *mat == Zero
106 | }
107 |
108 | // IsZeroEps checks if all elements of the matrix are zero within the given epsilon tolerance.
109 | // This is the recommended method for comparing floating-point matrices that result from calculations.
110 | // For exact zero comparison, use IsZero instead.
111 | func (mat *T) IsZeroEps(epsilon float32) bool {
112 | return math.Abs(mat[0][0]) <= epsilon && math.Abs(mat[0][1]) <= epsilon &&
113 | math.Abs(mat[1][0]) <= epsilon && math.Abs(mat[1][1]) <= epsilon
114 | }
115 |
116 | // Scale multiplies the diagonal scale elements by f returns mat.
117 | func (mat *T) Scale(f float32) *T {
118 | mat[0][0] *= f
119 | mat[1][1] *= f
120 | return mat
121 | }
122 |
123 | // Scaled returns a copy of the matrix with the diagonal scale elements multiplied by f.
124 | func (mat *T) Scaled(f float32) T {
125 | r := *mat
126 | return *r.Scale(f)
127 | }
128 |
129 | // Scaling returns the scaling diagonal of the matrix.
130 | func (mat *T) Scaling() vec2.T {
131 | return vec2.T{mat[0][0], mat[1][1]}
132 | }
133 |
134 | // SetScaling sets the scaling diagonal of the matrix.
135 | func (mat *T) SetScaling(s *vec2.T) *T {
136 | mat[0][0] = s[0]
137 | mat[1][1] = s[1]
138 | return mat
139 | }
140 |
141 | // Trace returns the trace value for the matrix.
142 | func (mat *T) Trace() float32 {
143 | return mat[0][0] + mat[1][1]
144 | }
145 |
146 | // AssignMul multiplies a and b and assigns the result to mat.
147 | func (mat *T) AssignMul(a, b *T) *T {
148 | mat[0] = a.MulVec2(&b[0])
149 | mat[1] = a.MulVec2(&b[1])
150 | return mat
151 | }
152 |
153 | // MulVec2 multiplies vec with mat.
154 | func (mat *T) MulVec2(vec *vec2.T) vec2.T {
155 | return vec2.T{
156 | mat[0][0]*vec[0] + mat[1][0]*vec[1],
157 | mat[0][1]*vec[1] + mat[1][1]*vec[1],
158 | }
159 | }
160 |
161 | // TransformVec2 multiplies v with mat and saves the result in v.
162 | func (mat *T) TransformVec2(v *vec2.T) {
163 | // Use intermediate variables to not alter further computations.
164 | x := mat[0][0]*v[0] + mat[1][0]*v[1]
165 | v[1] = mat[0][1]*v[0] + mat[1][1]*v[1]
166 | v[0] = x
167 | }
168 |
169 | func (mat *T) Determinant() float32 {
170 | return mat[0][0]*mat[1][1] - mat[1][0]*mat[0][1]
171 | }
172 |
173 | // PracticallyEquals compares two matrices if they are equal with each other within a delta tolerance.
174 | func (mat *T) PracticallyEquals(matrix *T, allowedDelta float32) bool {
175 | return mat[0].PracticallyEquals(&matrix[0], allowedDelta) &&
176 | mat[1].PracticallyEquals(&matrix[1], allowedDelta)
177 | }
178 |
179 | // Transpose transposes the matrix.
180 | func (mat *T) Transpose() *T {
181 | mat[0][1], mat[1][0] = mat[1][0], mat[0][1]
182 | return mat
183 | }
184 |
185 | // Transposed returns a transposed copy the matrix.
186 | func (mat *T) Transposed() T {
187 | result := *mat
188 | result.Transpose()
189 | return result
190 | }
191 |
192 | // Invert inverts the given matrix. Destructive operation.
193 | // Does not check if matrix is singular and may lead to strange results!
194 | func (mat *T) Invert() (*T, error) {
195 | determinant := mat.Determinant()
196 | if determinant == 0 {
197 | return &Zero, errors.New("can not create inverted matrix as determinant is 0")
198 | }
199 |
200 | invDet := 1.0 / determinant
201 |
202 | mat[0][0], mat[1][1] = invDet*mat[1][1], invDet*mat[0][0]
203 | mat[0][1] = -invDet * mat[0][1]
204 | mat[1][0] = -invDet * mat[1][0]
205 |
206 | return mat, nil
207 | }
208 |
209 | // Inverted inverts a copy of the given matrix.
210 | // Does not check if matrix is singular and may lead to strange results!
211 | func (mat *T) Inverted() (T, error) {
212 | result := *mat
213 | _, err := result.Invert()
214 | return result, err
215 | }
216 |
--------------------------------------------------------------------------------
/hermit2/hermit2_test.go:
--------------------------------------------------------------------------------
1 | package hermit2
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/vec2"
8 | )
9 |
10 | const EPSILON = 0.0001
11 |
12 | func TestParseAndString(t *testing.T) {
13 | original := T{
14 | A: PointTangent{
15 | Point: vec2.T{1.0, 2.0},
16 | Tangent: vec2.T{0.5, 0.5},
17 | },
18 | B: PointTangent{
19 | Point: vec2.T{3.0, 4.0},
20 | Tangent: vec2.T{-0.5, 0.5},
21 | },
22 | }
23 |
24 | // Convert to string and parse back
25 | str := original.String()
26 | parsed, err := Parse(str)
27 | if err != nil {
28 | t.Errorf("Parse() failed: %v", err)
29 | }
30 |
31 | // Check all components
32 | if math.Abs(float64(parsed.A.Point[0]-original.A.Point[0])) > EPSILON ||
33 | math.Abs(float64(parsed.A.Point[1]-original.A.Point[1])) > EPSILON ||
34 | math.Abs(float64(parsed.A.Tangent[0]-original.A.Tangent[0])) > EPSILON ||
35 | math.Abs(float64(parsed.A.Tangent[1]-original.A.Tangent[1])) > EPSILON ||
36 | math.Abs(float64(parsed.B.Point[0]-original.B.Point[0])) > EPSILON ||
37 | math.Abs(float64(parsed.B.Point[1]-original.B.Point[1])) > EPSILON ||
38 | math.Abs(float64(parsed.B.Tangent[0]-original.B.Tangent[0])) > EPSILON ||
39 | math.Abs(float64(parsed.B.Tangent[1]-original.B.Tangent[1])) > EPSILON {
40 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original)
41 | }
42 | }
43 |
44 | func TestPointEndpoints(t *testing.T) {
45 | // Create a simple hermit spline from (0,0) to (1,1) with tangents pointing in the same direction
46 | herm := T{
47 | A: PointTangent{
48 | Point: vec2.T{0.0, 0.0},
49 | Tangent: vec2.T{1.0, 1.0},
50 | },
51 | B: PointTangent{
52 | Point: vec2.T{1.0, 1.0},
53 | Tangent: vec2.T{1.0, 1.0},
54 | },
55 | }
56 |
57 | // At t=0, should return pointA
58 | p0 := herm.Point(0.0)
59 | if math.Abs(float64(p0[0]-herm.A.Point[0])) > EPSILON ||
60 | math.Abs(float64(p0[1]-herm.A.Point[1])) > EPSILON {
61 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point)
62 | }
63 |
64 | // At t=1, should return pointB
65 | p1 := herm.Point(1.0)
66 | if math.Abs(float64(p1[0]-herm.B.Point[0])) > EPSILON ||
67 | math.Abs(float64(p1[1]-herm.B.Point[1])) > EPSILON {
68 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point)
69 | }
70 | }
71 |
72 | func TestPointMidpoint(t *testing.T) {
73 | // Horizontal line from (0,0) to (2,0) with horizontal tangents
74 | herm := T{
75 | A: PointTangent{
76 | Point: vec2.T{0.0, 0.0},
77 | Tangent: vec2.T{1.0, 0.0},
78 | },
79 | B: PointTangent{
80 | Point: vec2.T{2.0, 0.0},
81 | Tangent: vec2.T{1.0, 0.0},
82 | },
83 | }
84 |
85 | // At t=0.5, should be approximately at (1, 0) for a straight line
86 | p := herm.Point(0.5)
87 |
88 | // The y-coordinate should be exactly 0 for horizontal tangents
89 | if math.Abs(float64(p[1])) > EPSILON {
90 | t.Errorf("Point(0.5) y-coordinate should be 0: got %v", p[1])
91 | }
92 |
93 | // The x-coordinate should be approximately 1.0
94 | if math.Abs(float64(p[0]-1.0)) > EPSILON {
95 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0])
96 | }
97 | }
98 |
99 | func TestPointFunctionMatchesMethod(t *testing.T) {
100 | herm := T{
101 | A: PointTangent{
102 | Point: vec2.T{0.0, 0.0},
103 | Tangent: vec2.T{1.0, 0.5},
104 | },
105 | B: PointTangent{
106 | Point: vec2.T{2.0, 1.0},
107 | Tangent: vec2.T{1.0, -0.5},
108 | },
109 | }
110 |
111 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
112 | for _, tVal := range tests {
113 | // Method call
114 | p1 := herm.Point(tVal)
115 |
116 | // Function call
117 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
118 |
119 | if math.Abs(float64(p1[0]-p2[0])) > EPSILON ||
120 | math.Abs(float64(p1[1]-p2[1])) > EPSILON {
121 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2)
122 | }
123 | }
124 | }
125 |
126 | func TestTangentAtZero(t *testing.T) {
127 | herm := T{
128 | A: PointTangent{
129 | Point: vec2.T{0.0, 0.0},
130 | Tangent: vec2.T{1.0, 0.5},
131 | },
132 | B: PointTangent{
133 | Point: vec2.T{2.0, 1.0},
134 | Tangent: vec2.T{0.5, 1.0},
135 | },
136 | }
137 |
138 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0)
139 | tan0 := herm.Tangent(0.0)
140 | expectedTan0 := herm.A.Tangent
141 | if math.Abs(float64(tan0[0]-expectedTan0[0])) > EPSILON ||
142 | math.Abs(float64(tan0[1]-expectedTan0[1])) > EPSILON {
143 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0)
144 | }
145 | }
146 |
147 | func TestTangentFunctionMatchesMethod(t *testing.T) {
148 | herm := T{
149 | A: PointTangent{
150 | Point: vec2.T{0.0, 0.0},
151 | Tangent: vec2.T{1.0, 0.5},
152 | },
153 | B: PointTangent{
154 | Point: vec2.T{2.0, 1.0},
155 | Tangent: vec2.T{0.5, 1.0},
156 | },
157 | }
158 |
159 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
160 | for _, tVal := range tests {
161 | // Method call
162 | tan1 := herm.Tangent(tVal)
163 |
164 | // Function call
165 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
166 |
167 | if math.Abs(float64(tan1[0]-tan2[0])) > EPSILON ||
168 | math.Abs(float64(tan1[1]-tan2[1])) > EPSILON {
169 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2)
170 | }
171 | }
172 | }
173 |
174 | func TestLengthValues(t *testing.T) {
175 | herm := T{
176 | A: PointTangent{
177 | Point: vec2.T{0.0, 0.0},
178 | Tangent: vec2.T{1.0, 0.0},
179 | },
180 | B: PointTangent{
181 | Point: vec2.T{2.0, 0.0},
182 | Tangent: vec2.T{1.0, 0.0},
183 | },
184 | }
185 |
186 | // Test that Length returns a valid positive number
187 | length0 := herm.Length(0.0)
188 | if math.IsNaN(float64(length0)) || math.IsInf(float64(length0), 0) {
189 | t.Errorf("Length(0) should be a valid number: got %f", length0)
190 | }
191 |
192 | length1 := herm.Length(1.0)
193 | if math.IsNaN(float64(length1)) || math.IsInf(float64(length1), 0) {
194 | t.Errorf("Length(1) should be a valid number: got %f", length1)
195 | }
196 | }
197 |
198 | func TestLengthFunctionMatchesMethod(t *testing.T) {
199 | herm := T{
200 | A: PointTangent{
201 | Point: vec2.T{0.0, 0.0},
202 | Tangent: vec2.T{1.0, 0.5},
203 | },
204 | B: PointTangent{
205 | Point: vec2.T{2.0, 1.0},
206 | Tangent: vec2.T{0.5, 1.0},
207 | },
208 | }
209 |
210 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
211 | for _, tVal := range tests {
212 | // Method call
213 | len1 := herm.Length(tVal)
214 |
215 | // Function call
216 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
217 |
218 | if math.Abs(float64(len1-len2)) > EPSILON {
219 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2)
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/float64/hermit3/hermit3_test.go:
--------------------------------------------------------------------------------
1 | package hermit
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/float64/vec3"
8 | )
9 |
10 | const EPSILON = 0.0000001
11 |
12 | func TestParseAndString(t *testing.T) {
13 | original := T{
14 | A: PointTangent{
15 | Point: vec3.T{1.0, 2.0, 3.0},
16 | Tangent: vec3.T{0.5, 0.5, 0.5},
17 | },
18 | B: PointTangent{
19 | Point: vec3.T{4.0, 5.0, 6.0},
20 | Tangent: vec3.T{-0.5, 0.5, -0.5},
21 | },
22 | }
23 |
24 | // Convert to string and parse back
25 | str := original.String()
26 | parsed, err := Parse(str)
27 | if err != nil {
28 | t.Errorf("Parse() failed: %v", err)
29 | }
30 |
31 | // Check all components
32 | if math.Abs(parsed.A.Point[0]-original.A.Point[0]) > EPSILON ||
33 | math.Abs(parsed.A.Point[1]-original.A.Point[1]) > EPSILON ||
34 | math.Abs(parsed.A.Point[2]-original.A.Point[2]) > EPSILON ||
35 | math.Abs(parsed.A.Tangent[0]-original.A.Tangent[0]) > EPSILON ||
36 | math.Abs(parsed.A.Tangent[1]-original.A.Tangent[1]) > EPSILON ||
37 | math.Abs(parsed.A.Tangent[2]-original.A.Tangent[2]) > EPSILON ||
38 | math.Abs(parsed.B.Point[0]-original.B.Point[0]) > EPSILON ||
39 | math.Abs(parsed.B.Point[1]-original.B.Point[1]) > EPSILON ||
40 | math.Abs(parsed.B.Point[2]-original.B.Point[2]) > EPSILON ||
41 | math.Abs(parsed.B.Tangent[0]-original.B.Tangent[0]) > EPSILON ||
42 | math.Abs(parsed.B.Tangent[1]-original.B.Tangent[1]) > EPSILON ||
43 | math.Abs(parsed.B.Tangent[2]-original.B.Tangent[2]) > EPSILON {
44 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original)
45 | }
46 | }
47 |
48 | func TestPointEndpoints(t *testing.T) {
49 | // Create a simple hermit spline from (0,0,0) to (1,1,1) with tangents pointing in the same direction
50 | herm := T{
51 | A: PointTangent{
52 | Point: vec3.T{0.0, 0.0, 0.0},
53 | Tangent: vec3.T{1.0, 1.0, 1.0},
54 | },
55 | B: PointTangent{
56 | Point: vec3.T{1.0, 1.0, 1.0},
57 | Tangent: vec3.T{1.0, 1.0, 1.0},
58 | },
59 | }
60 |
61 | // At t=0, should return pointA
62 | p0 := herm.Point(0.0)
63 | if math.Abs(p0[0]-herm.A.Point[0]) > EPSILON ||
64 | math.Abs(p0[1]-herm.A.Point[1]) > EPSILON ||
65 | math.Abs(p0[2]-herm.A.Point[2]) > EPSILON {
66 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point)
67 | }
68 |
69 | // At t=1, should return pointB
70 | p1 := herm.Point(1.0)
71 | if math.Abs(p1[0]-herm.B.Point[0]) > EPSILON ||
72 | math.Abs(p1[1]-herm.B.Point[1]) > EPSILON ||
73 | math.Abs(p1[2]-herm.B.Point[2]) > EPSILON {
74 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point)
75 | }
76 | }
77 |
78 | func TestPointMidpoint(t *testing.T) {
79 | // Line along X-axis from (0,0,0) to (2,0,0) with X-aligned tangents
80 | herm := T{
81 | A: PointTangent{
82 | Point: vec3.T{0.0, 0.0, 0.0},
83 | Tangent: vec3.T{1.0, 0.0, 0.0},
84 | },
85 | B: PointTangent{
86 | Point: vec3.T{2.0, 0.0, 0.0},
87 | Tangent: vec3.T{1.0, 0.0, 0.0},
88 | },
89 | }
90 |
91 | // At t=0.5, should be approximately at (1, 0, 0) for a straight line
92 | p := herm.Point(0.5)
93 |
94 | // The y and z coordinates should be exactly 0
95 | if math.Abs(p[1]) > EPSILON || math.Abs(p[2]) > EPSILON {
96 | t.Errorf("Point(0.5) y,z coordinates should be 0: got %v", p)
97 | }
98 |
99 | // The x-coordinate should be approximately 1.0
100 | if math.Abs(p[0]-1.0) > EPSILON {
101 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0])
102 | }
103 | }
104 |
105 | func TestPointFunctionMatchesMethod(t *testing.T) {
106 | herm := T{
107 | A: PointTangent{
108 | Point: vec3.T{0.0, 0.0, 0.0},
109 | Tangent: vec3.T{1.0, 0.5, 0.2},
110 | },
111 | B: PointTangent{
112 | Point: vec3.T{2.0, 1.0, 0.5},
113 | Tangent: vec3.T{1.0, -0.5, 0.3},
114 | },
115 | }
116 |
117 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
118 | for _, tVal := range tests {
119 | // Method call
120 | p1 := herm.Point(tVal)
121 |
122 | // Function call
123 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
124 |
125 | if math.Abs(p1[0]-p2[0]) > EPSILON ||
126 | math.Abs(p1[1]-p2[1]) > EPSILON ||
127 | math.Abs(p1[2]-p2[2]) > EPSILON {
128 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2)
129 | }
130 | }
131 | }
132 |
133 | func TestTangentAtZero(t *testing.T) {
134 | herm := T{
135 | A: PointTangent{
136 | Point: vec3.T{0.0, 0.0, 0.0},
137 | Tangent: vec3.T{1.0, 0.5, 0.2},
138 | },
139 | B: PointTangent{
140 | Point: vec3.T{2.0, 1.0, 0.5},
141 | Tangent: vec3.T{0.5, 1.0, 0.3},
142 | },
143 | }
144 |
145 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0)
146 | tan0 := herm.Tangent(0.0)
147 | expectedTan0 := herm.A.Tangent
148 | if math.Abs(tan0[0]-expectedTan0[0]) > EPSILON ||
149 | math.Abs(tan0[1]-expectedTan0[1]) > EPSILON ||
150 | math.Abs(tan0[2]-expectedTan0[2]) > EPSILON {
151 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0)
152 | }
153 | }
154 |
155 | func TestTangentFunctionMatchesMethod(t *testing.T) {
156 | herm := T{
157 | A: PointTangent{
158 | Point: vec3.T{0.0, 0.0, 0.0},
159 | Tangent: vec3.T{1.0, 0.5, 0.2},
160 | },
161 | B: PointTangent{
162 | Point: vec3.T{2.0, 1.0, 0.5},
163 | Tangent: vec3.T{0.5, 1.0, 0.3},
164 | },
165 | }
166 |
167 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
168 | for _, tVal := range tests {
169 | // Method call
170 | tan1 := herm.Tangent(tVal)
171 |
172 | // Function call
173 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
174 |
175 | if math.Abs(tan1[0]-tan2[0]) > EPSILON ||
176 | math.Abs(tan1[1]-tan2[1]) > EPSILON ||
177 | math.Abs(tan1[2]-tan2[2]) > EPSILON {
178 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2)
179 | }
180 | }
181 | }
182 |
183 | func TestLengthValues(t *testing.T) {
184 | herm := T{
185 | A: PointTangent{
186 | Point: vec3.T{0.0, 0.0, 0.0},
187 | Tangent: vec3.T{1.0, 0.0, 0.0},
188 | },
189 | B: PointTangent{
190 | Point: vec3.T{2.0, 0.0, 0.0},
191 | Tangent: vec3.T{1.0, 0.0, 0.0},
192 | },
193 | }
194 |
195 | // Test that Length returns a valid positive number
196 | length0 := herm.Length(0.0)
197 | if math.IsNaN(length0) || math.IsInf(length0, 0) {
198 | t.Errorf("Length(0) should be a valid number: got %f", length0)
199 | }
200 |
201 | length1 := herm.Length(1.0)
202 | if math.IsNaN(length1) || math.IsInf(length1, 0) {
203 | t.Errorf("Length(1) should be a valid number: got %f", length1)
204 | }
205 | }
206 |
207 | func TestLengthFunctionMatchesMethod(t *testing.T) {
208 | herm := T{
209 | A: PointTangent{
210 | Point: vec3.T{0.0, 0.0, 0.0},
211 | Tangent: vec3.T{1.0, 0.5, 0.2},
212 | },
213 | B: PointTangent{
214 | Point: vec3.T{2.0, 1.0, 0.5},
215 | Tangent: vec3.T{0.5, 1.0, 0.3},
216 | },
217 | }
218 |
219 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0}
220 | for _, tVal := range tests {
221 | // Method call
222 | len1 := herm.Length(tVal)
223 |
224 | // Function call
225 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
226 |
227 | if math.Abs(len1-len2) > EPSILON {
228 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2)
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/hermit3/hermit3_test.go:
--------------------------------------------------------------------------------
1 | package hermit
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/vec3"
8 | )
9 |
10 | const EPSILON = 0.0001
11 |
12 | func TestParseAndString(t *testing.T) {
13 | original := T{
14 | A: PointTangent{
15 | Point: vec3.T{1.0, 2.0, 3.0},
16 | Tangent: vec3.T{0.5, 0.5, 0.5},
17 | },
18 | B: PointTangent{
19 | Point: vec3.T{4.0, 5.0, 6.0},
20 | Tangent: vec3.T{-0.5, 0.5, -0.5},
21 | },
22 | }
23 |
24 | // Convert to string and parse back
25 | str := original.String()
26 | parsed, err := Parse(str)
27 | if err != nil {
28 | t.Errorf("Parse() failed: %v", err)
29 | }
30 |
31 | // Check all components
32 | if math.Abs(float64(parsed.A.Point[0]-original.A.Point[0])) > EPSILON ||
33 | math.Abs(float64(parsed.A.Point[1]-original.A.Point[1])) > EPSILON ||
34 | math.Abs(float64(parsed.A.Point[2]-original.A.Point[2])) > EPSILON ||
35 | math.Abs(float64(parsed.A.Tangent[0]-original.A.Tangent[0])) > EPSILON ||
36 | math.Abs(float64(parsed.A.Tangent[1]-original.A.Tangent[1])) > EPSILON ||
37 | math.Abs(float64(parsed.A.Tangent[2]-original.A.Tangent[2])) > EPSILON ||
38 | math.Abs(float64(parsed.B.Point[0]-original.B.Point[0])) > EPSILON ||
39 | math.Abs(float64(parsed.B.Point[1]-original.B.Point[1])) > EPSILON ||
40 | math.Abs(float64(parsed.B.Point[2]-original.B.Point[2])) > EPSILON ||
41 | math.Abs(float64(parsed.B.Tangent[0]-original.B.Tangent[0])) > EPSILON ||
42 | math.Abs(float64(parsed.B.Tangent[1]-original.B.Tangent[1])) > EPSILON ||
43 | math.Abs(float64(parsed.B.Tangent[2]-original.B.Tangent[2])) > EPSILON {
44 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original)
45 | }
46 | }
47 |
48 | func TestPointEndpoints(t *testing.T) {
49 | // Create a simple hermit spline from (0,0,0) to (1,1,1) with tangents pointing in the same direction
50 | herm := T{
51 | A: PointTangent{
52 | Point: vec3.T{0.0, 0.0, 0.0},
53 | Tangent: vec3.T{1.0, 1.0, 1.0},
54 | },
55 | B: PointTangent{
56 | Point: vec3.T{1.0, 1.0, 1.0},
57 | Tangent: vec3.T{1.0, 1.0, 1.0},
58 | },
59 | }
60 |
61 | // At t=0, should return pointA
62 | p0 := herm.Point(0.0)
63 | if math.Abs(float64(p0[0]-herm.A.Point[0])) > EPSILON ||
64 | math.Abs(float64(p0[1]-herm.A.Point[1])) > EPSILON ||
65 | math.Abs(float64(p0[2]-herm.A.Point[2])) > EPSILON {
66 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point)
67 | }
68 |
69 | // At t=1, should return pointB
70 | p1 := herm.Point(1.0)
71 | if math.Abs(float64(p1[0]-herm.B.Point[0])) > EPSILON ||
72 | math.Abs(float64(p1[1]-herm.B.Point[1])) > EPSILON ||
73 | math.Abs(float64(p1[2]-herm.B.Point[2])) > EPSILON {
74 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point)
75 | }
76 | }
77 |
78 | func TestPointMidpoint(t *testing.T) {
79 | // Line along X-axis from (0,0,0) to (2,0,0) with X-aligned tangents
80 | herm := T{
81 | A: PointTangent{
82 | Point: vec3.T{0.0, 0.0, 0.0},
83 | Tangent: vec3.T{1.0, 0.0, 0.0},
84 | },
85 | B: PointTangent{
86 | Point: vec3.T{2.0, 0.0, 0.0},
87 | Tangent: vec3.T{1.0, 0.0, 0.0},
88 | },
89 | }
90 |
91 | // At t=0.5, should be approximately at (1, 0, 0) for a straight line
92 | p := herm.Point(0.5)
93 |
94 | // The y and z coordinates should be exactly 0
95 | if math.Abs(float64(p[1])) > EPSILON || math.Abs(float64(p[2])) > EPSILON {
96 | t.Errorf("Point(0.5) y,z coordinates should be 0: got %v", p)
97 | }
98 |
99 | // The x-coordinate should be approximately 1.0
100 | if math.Abs(float64(p[0]-1.0)) > EPSILON {
101 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0])
102 | }
103 | }
104 |
105 | func TestPointFunctionMatchesMethod(t *testing.T) {
106 | herm := T{
107 | A: PointTangent{
108 | Point: vec3.T{0.0, 0.0, 0.0},
109 | Tangent: vec3.T{1.0, 0.5, 0.2},
110 | },
111 | B: PointTangent{
112 | Point: vec3.T{2.0, 1.0, 0.5},
113 | Tangent: vec3.T{1.0, -0.5, 0.3},
114 | },
115 | }
116 |
117 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
118 | for _, tVal := range tests {
119 | // Method call
120 | p1 := herm.Point(tVal)
121 |
122 | // Function call
123 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
124 |
125 | if math.Abs(float64(p1[0]-p2[0])) > EPSILON ||
126 | math.Abs(float64(p1[1]-p2[1])) > EPSILON ||
127 | math.Abs(float64(p1[2]-p2[2])) > EPSILON {
128 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2)
129 | }
130 | }
131 | }
132 |
133 | func TestTangentAtZero(t *testing.T) {
134 | herm := T{
135 | A: PointTangent{
136 | Point: vec3.T{0.0, 0.0, 0.0},
137 | Tangent: vec3.T{1.0, 0.5, 0.2},
138 | },
139 | B: PointTangent{
140 | Point: vec3.T{2.0, 1.0, 0.5},
141 | Tangent: vec3.T{0.5, 1.0, 0.3},
142 | },
143 | }
144 |
145 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0)
146 | tan0 := herm.Tangent(0.0)
147 | expectedTan0 := herm.A.Tangent
148 | if math.Abs(float64(tan0[0]-expectedTan0[0])) > EPSILON ||
149 | math.Abs(float64(tan0[1]-expectedTan0[1])) > EPSILON ||
150 | math.Abs(float64(tan0[2]-expectedTan0[2])) > EPSILON {
151 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0)
152 | }
153 | }
154 |
155 | func TestTangentFunctionMatchesMethod(t *testing.T) {
156 | herm := T{
157 | A: PointTangent{
158 | Point: vec3.T{0.0, 0.0, 0.0},
159 | Tangent: vec3.T{1.0, 0.5, 0.2},
160 | },
161 | B: PointTangent{
162 | Point: vec3.T{2.0, 1.0, 0.5},
163 | Tangent: vec3.T{0.5, 1.0, 0.3},
164 | },
165 | }
166 |
167 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
168 | for _, tVal := range tests {
169 | // Method call
170 | tan1 := herm.Tangent(tVal)
171 |
172 | // Function call
173 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
174 |
175 | if math.Abs(float64(tan1[0]-tan2[0])) > EPSILON ||
176 | math.Abs(float64(tan1[1]-tan2[1])) > EPSILON ||
177 | math.Abs(float64(tan1[2]-tan2[2])) > EPSILON {
178 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2)
179 | }
180 | }
181 | }
182 |
183 | func TestLengthValues(t *testing.T) {
184 | herm := T{
185 | A: PointTangent{
186 | Point: vec3.T{0.0, 0.0, 0.0},
187 | Tangent: vec3.T{1.0, 0.0, 0.0},
188 | },
189 | B: PointTangent{
190 | Point: vec3.T{2.0, 0.0, 0.0},
191 | Tangent: vec3.T{1.0, 0.0, 0.0},
192 | },
193 | }
194 |
195 | // Test that Length returns a valid positive number
196 | length0 := herm.Length(0.0)
197 | if math.IsNaN(float64(length0)) || math.IsInf(float64(length0), 0) {
198 | t.Errorf("Length(0) should be a valid number: got %f", length0)
199 | }
200 |
201 | length1 := herm.Length(1.0)
202 | if math.IsNaN(float64(length1)) || math.IsInf(float64(length1), 0) {
203 | t.Errorf("Length(1) should be a valid number: got %f", length1)
204 | }
205 | }
206 |
207 | func TestLengthFunctionMatchesMethod(t *testing.T) {
208 | herm := T{
209 | A: PointTangent{
210 | Point: vec3.T{0.0, 0.0, 0.0},
211 | Tangent: vec3.T{1.0, 0.5, 0.2},
212 | },
213 | B: PointTangent{
214 | Point: vec3.T{2.0, 1.0, 0.5},
215 | Tangent: vec3.T{0.5, 1.0, 0.3},
216 | },
217 | }
218 |
219 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0}
220 | for _, tVal := range tests {
221 | // Method call
222 | len1 := herm.Length(tVal)
223 |
224 | // Function call
225 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal)
226 |
227 | if math.Abs(float64(len1-len2)) > EPSILON {
228 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2)
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/float64/vec3/vec3_test.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | import (
4 | "math"
5 | "testing"
6 | )
7 |
8 | func TestBoxIntersection(t *testing.T) {
9 | bb1 := Box{T{0, 0, 0}, T{1, 1, 1}}
10 | bb2 := Box{T{1, 1, 1}, T{2, 2, 2}}
11 | if !bb1.Intersects(&bb2) {
12 | t.Fail()
13 | }
14 |
15 | bb3 := Box{T{1, 2, 1}, T{2, 3, 2}}
16 | if bb1.Intersects(&bb3) {
17 | t.Fail()
18 | }
19 | }
20 |
21 | func TestAdd(t *testing.T) {
22 | v1 := T{1, 2, 3}
23 | v2 := T{9, 8, 7}
24 |
25 | expectedV1 := T{10, 10, 10}
26 | expectedV2 := T{9, 8, 7}
27 | expectedV3 := T{10, 10, 10}
28 |
29 | v3 := v1.Add(&v2)
30 |
31 | if v1 != expectedV1 {
32 | t.Fail()
33 | }
34 | if v2 != expectedV2 {
35 | t.Fail()
36 | }
37 | if *v3 != expectedV3 {
38 | t.Fail()
39 | }
40 | }
41 |
42 | func TestAdded(t *testing.T) {
43 | v1 := T{1, 2, 3}
44 | v2 := T{9, 8, 7}
45 |
46 | expectedV1 := T{1, 2, 3}
47 | expectedV2 := T{9, 8, 7}
48 | expectedV3 := T{10, 10, 10}
49 |
50 | v3 := v1.Added(&v2)
51 |
52 | if v1 != expectedV1 {
53 | t.Fail()
54 | }
55 | if v2 != expectedV2 {
56 | t.Fail()
57 | }
58 | if v3 != expectedV3 {
59 | t.Fail()
60 | }
61 | }
62 |
63 | func TestSub(t *testing.T) {
64 | v1 := T{1, 2, 3}
65 | v2 := T{9, 8, 7}
66 |
67 | expectedV1 := T{-8, -6, -4}
68 | expectedV2 := T{9, 8, 7}
69 | expectedV3 := T{-8, -6, -4}
70 |
71 | v3 := v1.Sub(&v2)
72 |
73 | if v1 != expectedV1 {
74 | t.Fail()
75 | }
76 | if v2 != expectedV2 {
77 | t.Fail()
78 | }
79 | if *v3 != expectedV3 {
80 | t.Fail()
81 | }
82 | }
83 |
84 | func TestSubed(t *testing.T) {
85 | v1 := T{1, 2, 3}
86 | v2 := T{9, 8, 7}
87 |
88 | expectedV1 := T{1, 2, 3}
89 | expectedV2 := T{9, 8, 7}
90 | expectedV3 := T{-8, -6, -4}
91 |
92 | v3 := v1.Subed(&v2)
93 |
94 | if v1 != expectedV1 {
95 | t.Fail()
96 | }
97 | if v2 != expectedV2 {
98 | t.Fail()
99 | }
100 | if v3 != expectedV3 {
101 | t.Fail()
102 | }
103 | }
104 |
105 | func TestMul(t *testing.T) {
106 | v1 := T{1, 2, 3}
107 | v2 := T{9, 8, 7}
108 |
109 | expectedV1 := T{9, 16, 21}
110 | expectedV2 := T{9, 8, 7}
111 | expectedV3 := T{9, 16, 21}
112 |
113 | v3 := v1.Mul(&v2)
114 |
115 | if v1 != expectedV1 {
116 | t.Fail()
117 | }
118 | if v2 != expectedV2 {
119 | t.Fail()
120 | }
121 | if *v3 != expectedV3 {
122 | t.Fail()
123 | }
124 | }
125 |
126 | func TestMuled(t *testing.T) {
127 | v1 := T{1, 2, 3}
128 | v2 := T{9, 8, 7}
129 |
130 | expectedV1 := T{1, 2, 3}
131 | expectedV2 := T{9, 8, 7}
132 | expectedV3 := T{9, 16, 21}
133 |
134 | v3 := v1.Muled(&v2)
135 |
136 | if v1 != expectedV1 {
137 | t.Fail()
138 | }
139 | if v2 != expectedV2 {
140 | t.Fail()
141 | }
142 | if v3 != expectedV3 {
143 | t.Fail()
144 | }
145 | }
146 |
147 | func TestAngle(t *testing.T) {
148 | radFor45deg := math.Pi / 4.0
149 | testSetups := []struct {
150 | a, b T
151 | expectedAngle float64
152 | name string
153 | }{
154 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"},
155 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"},
156 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"},
157 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"},
158 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"},
159 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"},
160 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"},
161 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"},
162 | }
163 |
164 | for _, testSetup := range testSetups {
165 | t.Run(testSetup.name, func(t *testing.T) {
166 | angle := Angle(&testSetup.a, &testSetup.b)
167 |
168 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) {
169 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name)
170 | }
171 | })
172 | }
173 | }
174 |
175 | func TestCosine(t *testing.T) {
176 | radFor45deg := math.Pi / 4.0
177 | testSetups := []struct {
178 | a, b T
179 | expectedCosine float64
180 | name string
181 | }{
182 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
183 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"},
184 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
185 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"},
186 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
187 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"},
188 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"},
189 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"},
190 | }
191 |
192 | for _, testSetup := range testSetups {
193 | t.Run(testSetup.name, func(t *testing.T) {
194 | cos := Cosine(&testSetup.a, &testSetup.b)
195 |
196 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) {
197 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name)
198 | }
199 | })
200 | }
201 | }
202 |
203 | func TestSinus(t *testing.T) {
204 | radFor45deg := math.Pi / 4.0
205 | testSetups := []struct {
206 | a, b T
207 | expectedSine float64
208 | name string
209 | }{
210 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
211 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"},
212 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
213 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"},
214 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
215 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"},
216 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"},
217 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"},
218 | }
219 |
220 | for _, testSetup := range testSetups {
221 | t.Run(testSetup.name, func(t *testing.T) {
222 | sin := Sinus(&testSetup.a, &testSetup.b)
223 |
224 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) {
225 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name)
226 | }
227 | })
228 | }
229 | }
230 |
231 | func TestIsZeroEps(t *testing.T) {
232 | tests := []struct {
233 | name string
234 | vec T
235 | epsilon float64
236 | want bool
237 | }{
238 | {"exact zero", T{0, 0, 0}, 0.0001, true},
239 | {"within epsilon", T{0.00001, -0.00001, 0.00001}, 0.0001, true},
240 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001}, 0.0001, true},
241 | {"outside epsilon", T{0.001, 0, 0}, 0.0001, false},
242 | {"one component outside", T{0.00001, 0.001, 0.00001}, 0.0001, false},
243 | {"negative outside epsilon", T{-0.001, 0, 0}, 0.0001, false},
244 | {"large values", T{1.0, 2.0, 3.0}, 0.0001, false},
245 | }
246 |
247 | for _, tt := range tests {
248 | t.Run(tt.name, func(t *testing.T) {
249 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want {
250 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon)
251 | }
252 | })
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/vec3/vec3_test.go:
--------------------------------------------------------------------------------
1 | package vec3
2 |
3 | import (
4 | math "github.com/chewxy/math32"
5 |
6 | "testing"
7 | )
8 |
9 | func TestBoxIntersection(t *testing.T) {
10 | bb1 := Box{T{0, 0, 0}, T{1, 1, 1}}
11 | bb2 := Box{T{1, 1, 1}, T{2, 2, 2}}
12 | if !bb1.Intersects(&bb2) {
13 | t.Fail()
14 | }
15 |
16 | bb3 := Box{T{1, 2, 1}, T{2, 3, 2}}
17 | if bb1.Intersects(&bb3) {
18 | t.Fail()
19 | }
20 | }
21 |
22 | func TestAdd(t *testing.T) {
23 | v1 := T{1, 2, 3}
24 | v2 := T{9, 8, 7}
25 |
26 | expectedV1 := T{10, 10, 10}
27 | expectedV2 := T{9, 8, 7}
28 | expectedV3 := T{10, 10, 10}
29 |
30 | v3 := v1.Add(&v2)
31 |
32 | if v1 != expectedV1 {
33 | t.Fail()
34 | }
35 | if v2 != expectedV2 {
36 | t.Fail()
37 | }
38 | if *v3 != expectedV3 {
39 | t.Fail()
40 | }
41 | }
42 |
43 | func TestAdded(t *testing.T) {
44 | v1 := T{1, 2, 3}
45 | v2 := T{9, 8, 7}
46 |
47 | expectedV1 := T{1, 2, 3}
48 | expectedV2 := T{9, 8, 7}
49 | expectedV3 := T{10, 10, 10}
50 |
51 | v3 := v1.Added(&v2)
52 |
53 | if v1 != expectedV1 {
54 | t.Fail()
55 | }
56 | if v2 != expectedV2 {
57 | t.Fail()
58 | }
59 | if v3 != expectedV3 {
60 | t.Fail()
61 | }
62 | }
63 |
64 | func TestSub(t *testing.T) {
65 | v1 := T{1, 2, 3}
66 | v2 := T{9, 8, 7}
67 |
68 | expectedV1 := T{-8, -6, -4}
69 | expectedV2 := T{9, 8, 7}
70 | expectedV3 := T{-8, -6, -4}
71 |
72 | v3 := v1.Sub(&v2)
73 |
74 | if v1 != expectedV1 {
75 | t.Fail()
76 | }
77 | if v2 != expectedV2 {
78 | t.Fail()
79 | }
80 | if *v3 != expectedV3 {
81 | t.Fail()
82 | }
83 | }
84 |
85 | func TestSubed(t *testing.T) {
86 | v1 := T{1, 2, 3}
87 | v2 := T{9, 8, 7}
88 |
89 | expectedV1 := T{1, 2, 3}
90 | expectedV2 := T{9, 8, 7}
91 | expectedV3 := T{-8, -6, -4}
92 |
93 | v3 := v1.Subed(&v2)
94 |
95 | if v1 != expectedV1 {
96 | t.Fail()
97 | }
98 | if v2 != expectedV2 {
99 | t.Fail()
100 | }
101 | if v3 != expectedV3 {
102 | t.Fail()
103 | }
104 | }
105 |
106 | func TestMul(t *testing.T) {
107 | v1 := T{1, 2, 3}
108 | v2 := T{9, 8, 7}
109 |
110 | expectedV1 := T{9, 16, 21}
111 | expectedV2 := T{9, 8, 7}
112 | expectedV3 := T{9, 16, 21}
113 |
114 | v3 := v1.Mul(&v2)
115 |
116 | if v1 != expectedV1 {
117 | t.Fail()
118 | }
119 | if v2 != expectedV2 {
120 | t.Fail()
121 | }
122 | if *v3 != expectedV3 {
123 | t.Fail()
124 | }
125 | }
126 |
127 | func TestMuled(t *testing.T) {
128 | v1 := T{1, 2, 3}
129 | v2 := T{9, 8, 7}
130 |
131 | expectedV1 := T{1, 2, 3}
132 | expectedV2 := T{9, 8, 7}
133 | expectedV3 := T{9, 16, 21}
134 |
135 | v3 := v1.Muled(&v2)
136 |
137 | if v1 != expectedV1 {
138 | t.Fail()
139 | }
140 | if v2 != expectedV2 {
141 | t.Fail()
142 | }
143 | if v3 != expectedV3 {
144 | t.Fail()
145 | }
146 | }
147 |
148 | func TestAngle(t *testing.T) {
149 | radFor45deg := float32(math.Pi / 4.0)
150 | testSetups := []struct {
151 | a, b T
152 | expectedAngle float32
153 | name string
154 | }{
155 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"},
156 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"},
157 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"},
158 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"},
159 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"},
160 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"},
161 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"},
162 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"},
163 | }
164 |
165 | for _, testSetup := range testSetups {
166 | t.Run(testSetup.name, func(t *testing.T) {
167 | angle := Angle(&testSetup.a, &testSetup.b)
168 |
169 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) {
170 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name)
171 | }
172 | })
173 | }
174 | }
175 |
176 | func TestCosine(t *testing.T) {
177 | radFor45deg := float32(math.Pi / 4.0)
178 | testSetups := []struct {
179 | a, b T
180 | expectedCosine float32
181 | name string
182 | }{
183 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
184 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"},
185 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
186 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"},
187 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
188 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"},
189 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"},
190 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"},
191 | }
192 |
193 | for _, testSetup := range testSetups {
194 | t.Run(testSetup.name, func(t *testing.T) {
195 | cos := Cosine(&testSetup.a, &testSetup.b)
196 |
197 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.000001) {
198 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name)
199 | }
200 | })
201 | }
202 | }
203 |
204 | func TestSinus(t *testing.T) {
205 | radFor45deg := float32(math.Pi / 4.0)
206 | testSetups := []struct {
207 | a, b T
208 | expectedSine float32
209 | name string
210 | }{
211 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
212 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"},
213 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
214 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"},
215 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
216 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"},
217 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"},
218 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"},
219 | }
220 |
221 | for _, testSetup := range testSetups {
222 | t.Run(testSetup.name, func(t *testing.T) {
223 | sin := Sinus(&testSetup.a, &testSetup.b)
224 |
225 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.000001) {
226 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name)
227 | }
228 | })
229 | }
230 | }
231 |
232 | func TestIsZeroEps(t *testing.T) {
233 | tests := []struct {
234 | name string
235 | vec T
236 | epsilon float32
237 | want bool
238 | }{
239 | {"exact zero", T{0, 0, 0}, 0.0001, true},
240 | {"within epsilon", T{0.00001, -0.00001, 0.00001}, 0.0001, true},
241 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001}, 0.0001, true},
242 | {"outside epsilon", T{0.001, 0, 0}, 0.0001, false},
243 | {"one component outside", T{0.00001, 0.001, 0.00001}, 0.0001, false},
244 | {"negative outside epsilon", T{-0.001, 0, 0}, 0.0001, false},
245 | {"large values", T{1.0, 2.0, 3.0}, 0.0001, false},
246 | }
247 |
248 | for _, tt := range tests {
249 | t.Run(tt.name, func(t *testing.T) {
250 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want {
251 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon)
252 | }
253 | })
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/float64/mat3/mat3_test.go:
--------------------------------------------------------------------------------
1 | package mat3
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/float64/mat2"
8 | "github.com/ungerik/go3d/float64/vec2"
9 | "github.com/ungerik/go3d/float64/vec3"
10 | )
11 |
12 | const EPSILON = 0.0001
13 |
14 | // Some matrices used in multiple tests.
15 | var (
16 | invertableMatrix1 = T{vec3.T{4, -2, 3}, vec3.T{8, -3, 5}, vec3.T{7, -2, 4}}
17 | invertedMatrix1 = T{vec3.T{-2, 2, -1}, vec3.T{3, -5, 4}, vec3.T{5, -6, 4}}
18 | nonInvertableMatrix1 = T{vec3.T{1, 1, 1}, vec3.T{1, 1, 1}, vec3.T{1, 1, 1}}
19 | nonInvertableMatrix2 = T{vec3.T{1, 1, 1}, vec3.T{1, 0, 1}, vec3.T{1, 1, 1}}
20 |
21 | testMatrix1 = T{
22 | vec3.T{0.38016528, -0.0661157, -0.008264462},
23 | vec3.T{-0.19834709, 0.33884296, -0.08264463},
24 | vec3.T{0.11570247, -0.28099173, 0.21487603},
25 | }
26 |
27 | testMatrix2 = T{
28 | vec3.T{23, -4, -0.5},
29 | vec3.T{-12, 20.5, -5},
30 | vec3.T{7, -17, 13},
31 | }
32 |
33 | row123Changed, _ = Parse("3 1 0.5 2 5 2 1 6 7")
34 | )
35 |
36 | func Test_ColsAndRows(t *testing.T) {
37 | A := testMatrix2
38 |
39 | a11 := A.Get(0, 0)
40 | a21 := A.Get(0, 1)
41 | a31 := A.Get(0, 2)
42 |
43 | a12 := A.Get(1, 0)
44 | a22 := A.Get(1, 1)
45 | a32 := A.Get(1, 2)
46 |
47 | a13 := A.Get(2, 0)
48 | a23 := A.Get(2, 1)
49 | a33 := A.Get(2, 2)
50 |
51 | correctReference := a11 == 23 && a21 == -4 && a31 == -0.5 &&
52 | a12 == -12 && a22 == 20.5 && a32 == -5 &&
53 | a13 == 7 && a23 == -17 && a33 == 13
54 |
55 | if !correctReference {
56 | t.Errorf("matrix ill referenced")
57 | }
58 | }
59 |
60 | func TestT_Transposed(t *testing.T) {
61 | matrix := T{
62 | vec3.T{1, 2, 3},
63 | vec3.T{4, 5, 6},
64 | vec3.T{7, 8, 9},
65 | }
66 | expectedMatrix := T{
67 | vec3.T{1, 4, 7},
68 | vec3.T{2, 5, 8},
69 | vec3.T{3, 6, 9},
70 | }
71 |
72 | transposedMatrix := matrix.Transposed()
73 |
74 | if transposedMatrix != expectedMatrix {
75 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
76 | }
77 | }
78 |
79 | func TestT_Transpose(t *testing.T) {
80 | matrix := T{
81 | vec3.T{10, 20, 30},
82 | vec3.T{40, 50, 60},
83 | vec3.T{70, 80, 90},
84 | }
85 |
86 | expectedMatrix := T{
87 | vec3.T{10, 40, 70},
88 | vec3.T{20, 50, 80},
89 | vec3.T{30, 60, 90},
90 | }
91 |
92 | transposedMatrix := matrix
93 | transposedMatrix.Transpose()
94 |
95 | if transposedMatrix != expectedMatrix {
96 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
97 | }
98 | }
99 |
100 | func TestDeterminant_2(t *testing.T) {
101 | detTwo := Ident
102 | detTwo[0][0] = 2
103 | if det := detTwo.Determinant(); det != 2 {
104 | t.Errorf("Wrong determinant: %f", det)
105 | }
106 | }
107 |
108 | func TestDeterminant_3(t *testing.T) {
109 | scale2 := Ident.Scaled(2)
110 | if det := scale2.Determinant(); det != 2*2*2*1 {
111 | t.Errorf("Wrong determinant: %f", det)
112 | }
113 | }
114 |
115 | func TestDeterminant_4(t *testing.T) {
116 | row1changed, _ := Parse("3 0 0 2 2 0 1 0 2")
117 | if det := row1changed.Determinant(); det != 12 {
118 | t.Errorf("Wrong determinant: %f", det)
119 | }
120 | }
121 |
122 | func TestDeterminant_5(t *testing.T) {
123 | row12changed, _ := Parse("3 1 0 2 5 0 1 6 2")
124 | if det := row12changed.Determinant(); det != 26 {
125 | t.Errorf("Wrong determinant: %f", det)
126 | }
127 | }
128 |
129 | func TestDeterminant_7(t *testing.T) {
130 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.16600 0.40608 0.53479 0.37328 0.36436 0.56356")
131 | randomMatrix.Transpose()
132 | if err != nil {
133 | t.Errorf("Could not parse random matrix: %v", err)
134 | }
135 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.043437) {
136 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det)
137 | }
138 | }
139 |
140 | func PracticallyEquals(value1 float64, value2 float64) bool {
141 | return math.Abs(value1-value2) > EPSILON
142 | }
143 |
144 | func TestDeterminant_6(t *testing.T) {
145 | row123changed := row123Changed
146 | if det := row123changed.Determinant(); det != 60.500 {
147 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det)
148 | }
149 | }
150 |
151 | func TestDeterminant_1(t *testing.T) {
152 | detId := Ident.Determinant()
153 | if detId != 1 {
154 | t.Errorf("Wrong determinant for identity matrix: %f", detId)
155 | }
156 | }
157 |
158 | func TestMaskedBlock(t *testing.T) {
159 | m := row123Changed
160 | blockedExpected := mat2.T{vec2.T{5, 2}, vec2.T{6, 7}}
161 | if blocked := m.maskedBlock(0, 0); *blocked != blockedExpected {
162 | t.Errorf("Did not block 0,0 correctly: %#v", blocked)
163 | }
164 | }
165 |
166 | func TestAdjugate(t *testing.T) {
167 | adj := row123Changed
168 |
169 | // Computed in octave:
170 | adjExpected := T{
171 | vec3.T{23, -4, -0.5},
172 | vec3.T{-12, 20.5, -5},
173 | vec3.T{7, -17, 13},
174 | }
175 |
176 | adj.Adjugate()
177 |
178 | if adj != adjExpected {
179 | t.Errorf("Adjugate not computed correctly: %#v", adj)
180 | }
181 | }
182 |
183 | func TestAdjugated(t *testing.T) {
184 | sqrt2 := math.Sqrt(2)
185 | A := T{
186 | vec3.T{1, 0, -1},
187 | vec3.T{0, sqrt2, 0},
188 | vec3.T{1, 0, 1},
189 | }
190 |
191 | expectedAdjugated := T{
192 | vec3.T{1.4142135623730951, -0, 1.4142135623730951},
193 | vec3.T{-0, 2, -0},
194 | vec3.T{-1.4142135623730951, -0, 1.4142135623730951},
195 | }
196 |
197 | adjA := A.Adjugated()
198 |
199 | if adjA != expectedAdjugated {
200 | t.Errorf("Adjugate not computed correctly: %v", adjA)
201 | }
202 | }
203 |
204 | func TestInvert_ok(t *testing.T) {
205 | inv := invertableMatrix1
206 | _, err := inv.Invert()
207 |
208 | if err != nil {
209 | t.Error("Inverse not computed correctly", err)
210 | }
211 |
212 | invExpected := invertedMatrix1
213 | if inv != invExpected {
214 | t.Errorf("Inverse not computed correctly: %#v", inv)
215 | }
216 | }
217 |
218 | func TestInvert_ok2(t *testing.T) {
219 | sqrt2 := math.Sqrt(2)
220 | A := T{
221 | vec3.T{1, 0, -1},
222 | vec3.T{0, sqrt2, 0},
223 | vec3.T{1, 0, 1},
224 | }
225 |
226 | expectedInverted := T{
227 | vec3.T{0.5, 0, 0.5},
228 | vec3.T{0, 0.7071067811865475, 0},
229 | vec3.T{-0.5, 0, 0.5},
230 | }
231 |
232 | invA, err := A.Inverted()
233 | if err != nil {
234 | t.Error("Inverse not computed correctly", err)
235 | }
236 |
237 | if invA != expectedInverted {
238 | t.Errorf("Inverse not computed correctly: %v", A)
239 | }
240 | }
241 |
242 | func TestInvert_nok_1(t *testing.T) {
243 | inv := nonInvertableMatrix1
244 | _, err := inv.Inverted()
245 | if err == nil {
246 | t.Error("Inverse should not be possible", err)
247 | }
248 | }
249 |
250 | func TestInvert_nok_2(t *testing.T) {
251 | inv := nonInvertableMatrix2
252 | _, err := inv.Inverted()
253 | if err == nil {
254 | t.Error("Inverse should not be possible", err)
255 | }
256 | }
257 |
258 | func BenchmarkAssignMul(b *testing.B) {
259 | m1 := testMatrix1
260 | m2 := testMatrix2
261 | var mMult T
262 | b.ResetTimer()
263 | for i := 0; i < b.N; i++ {
264 | mMult.AssignMul(&m1, &m2)
265 | }
266 | }
267 |
268 | func TestIsZeroEps(t *testing.T) {
269 | tests := []struct {
270 | name string
271 | mat T
272 | epsilon float64
273 | want bool
274 | }{
275 | {"exact zero", Zero, 0.0001, true},
276 | {"within epsilon", T{vec3.T{0.00001, -0.00001, 0.00001}, vec3.T{-0.00001, 0.00001, -0.00001}, vec3.T{0.00001, -0.00001, 0.00001}}, 0.0001, true},
277 | {"at epsilon boundary", T{vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}}, 0.0001, true},
278 | {"outside epsilon", T{vec3.T{0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false},
279 | {"one element outside", T{vec3.T{0.00001, 0.00001, 0.00001}, vec3.T{0.001, 0.00001, 0.00001}, vec3.T{0.00001, 0.00001, 0.00001}}, 0.0001, false},
280 | {"negative outside epsilon", T{vec3.T{-0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false},
281 | {"identity matrix", Ident, 0.0001, false},
282 | }
283 |
284 | for _, tt := range tests {
285 | t.Run(tt.name, func(t *testing.T) {
286 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
287 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon)
288 | }
289 | })
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/mat3/mat3_test.go:
--------------------------------------------------------------------------------
1 | package mat3
2 |
3 | import (
4 | "math"
5 | "testing"
6 |
7 | "github.com/ungerik/go3d/mat2"
8 | "github.com/ungerik/go3d/vec2"
9 | "github.com/ungerik/go3d/vec3"
10 | )
11 |
12 | const EPSILON = 0.0001
13 |
14 | // Some matrices used in multiple tests.
15 | var (
16 | invertableMatrix1 = T{vec3.T{4, -2, 3}, vec3.T{8, -3, 5}, vec3.T{7, -2, 4}}
17 | invertedMatrix1 = T{vec3.T{-2, 2, -1}, vec3.T{3, -5, 4}, vec3.T{5, -6, 4}}
18 | nonInvertableMatrix1 = T{vec3.T{1, 1, 1}, vec3.T{1, 1, 1}, vec3.T{1, 1, 1}}
19 | nonInvertableMatrix2 = T{vec3.T{1, 1, 1}, vec3.T{1, 0, 1}, vec3.T{1, 1, 1}}
20 |
21 | testMatrix1 = T{
22 | vec3.T{0.38016528, -0.0661157, -0.008264462},
23 | vec3.T{-0.19834709, 0.33884296, -0.08264463},
24 | vec3.T{0.11570247, -0.28099173, 0.21487603},
25 | }
26 |
27 | testMatrix2 = T{
28 | vec3.T{23, -4, -0.5},
29 | vec3.T{-12, 20.5, -5},
30 | vec3.T{7, -17, 13},
31 | }
32 |
33 | row123Changed, _ = Parse("3 1 0.5 2 5 2 1 6 7")
34 | )
35 |
36 | func Test_ColsAndRows(t *testing.T) {
37 | A := testMatrix2
38 |
39 | a11 := A.Get(0, 0)
40 | a21 := A.Get(0, 1)
41 | a31 := A.Get(0, 2)
42 |
43 | a12 := A.Get(1, 0)
44 | a22 := A.Get(1, 1)
45 | a32 := A.Get(1, 2)
46 |
47 | a13 := A.Get(2, 0)
48 | a23 := A.Get(2, 1)
49 | a33 := A.Get(2, 2)
50 |
51 | correctReference := a11 == 23 && a21 == -4 && a31 == -0.5 &&
52 | a12 == -12 && a22 == 20.5 && a32 == -5 &&
53 | a13 == 7 && a23 == -17 && a33 == 13
54 |
55 | if !correctReference {
56 | t.Errorf("matrix ill referenced")
57 | }
58 | }
59 |
60 | func TestT_Transposed(t *testing.T) {
61 | matrix := T{
62 | vec3.T{1, 2, 3},
63 | vec3.T{4, 5, 6},
64 | vec3.T{7, 8, 9},
65 | }
66 | expectedMatrix := T{
67 | vec3.T{1, 4, 7},
68 | vec3.T{2, 5, 8},
69 | vec3.T{3, 6, 9},
70 | }
71 |
72 | transposedMatrix := matrix.Transposed()
73 |
74 | if transposedMatrix != expectedMatrix {
75 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
76 | }
77 | }
78 |
79 | func TestT_Transpose(t *testing.T) {
80 | matrix := T{
81 | vec3.T{10, 20, 30},
82 | vec3.T{40, 50, 60},
83 | vec3.T{70, 80, 90},
84 | }
85 |
86 | expectedMatrix := T{
87 | vec3.T{10, 40, 70},
88 | vec3.T{20, 50, 80},
89 | vec3.T{30, 60, 90},
90 | }
91 |
92 | transposedMatrix := matrix
93 | transposedMatrix.Transpose()
94 |
95 | if transposedMatrix != expectedMatrix {
96 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix)
97 | }
98 | }
99 |
100 | func TestDeterminant_2(t *testing.T) {
101 | detTwo := Ident
102 | detTwo[0][0] = 2
103 | if det := detTwo.Determinant(); det != 2 {
104 | t.Errorf("Wrong determinant: %f", det)
105 | }
106 | }
107 |
108 | func TestDeterminant_3(t *testing.T) {
109 | scale2 := Ident.Scaled(2)
110 | if det := scale2.Determinant(); det != 2*2*2*1 {
111 | t.Errorf("Wrong determinant: %f", det)
112 | }
113 | }
114 |
115 | func TestDeterminant_4(t *testing.T) {
116 | row1changed, _ := Parse("3 0 0 2 2 0 1 0 2")
117 | if det := row1changed.Determinant(); det != 12 {
118 | t.Errorf("Wrong determinant: %f", det)
119 | }
120 | }
121 |
122 | func TestDeterminant_5(t *testing.T) {
123 | row12changed, _ := Parse("3 1 0 2 5 0 1 6 2")
124 | if det := row12changed.Determinant(); det != 26 {
125 | t.Errorf("Wrong determinant: %f", det)
126 | }
127 | }
128 |
129 | func TestDeterminant_7(t *testing.T) {
130 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.16600 0.40608 0.53479 0.37328 0.36436 0.56356")
131 | randomMatrix.Transpose()
132 | if err != nil {
133 | t.Errorf("Could not parse random matrix: %v", err)
134 | }
135 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.043437) {
136 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det)
137 | }
138 | }
139 |
140 | func PracticallyEquals(value1 float32, value2 float32) bool {
141 | return math.Abs(float64(value1-value2)) > EPSILON
142 | }
143 |
144 | func TestDeterminant_6(t *testing.T) {
145 | row123changed := row123Changed
146 | if det := row123changed.Determinant(); det != 60.500 {
147 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det)
148 | }
149 | }
150 |
151 | func TestDeterminant_1(t *testing.T) {
152 | detId := Ident.Determinant()
153 | if detId != 1 {
154 | t.Errorf("Wrong determinant for identity matrix: %f", detId)
155 | }
156 | }
157 |
158 | func TestMaskedBlock(t *testing.T) {
159 | m := row123Changed
160 | blockedExpected := mat2.T{vec2.T{5, 2}, vec2.T{6, 7}}
161 | if blocked := m.maskedBlock(0, 0); *blocked != blockedExpected {
162 | t.Errorf("Did not block 0,0 correctly: %#v", blocked)
163 | }
164 | }
165 |
166 | func TestAdjugate(t *testing.T) {
167 | adj := row123Changed
168 |
169 | // Computed in octave:
170 | adjExpected := T{
171 | vec3.T{23, -4, -0.5},
172 | vec3.T{-12, 20.5, -5},
173 | vec3.T{7, -17, 13},
174 | }
175 |
176 | adj.Adjugate()
177 |
178 | if adj != adjExpected {
179 | t.Errorf("Adjugate not computed correctly: %#v", adj)
180 | }
181 | }
182 |
183 | func TestAdjugated(t *testing.T) {
184 | sqrt2 := float32(math.Sqrt(2))
185 | A := T{
186 | vec3.T{1, 0, -1},
187 | vec3.T{0, sqrt2, 0},
188 | vec3.T{1, 0, 1},
189 | }
190 |
191 | expectedAdjugated := T{
192 | vec3.T{1.4142135623730951, -0, 1.4142135623730951},
193 | vec3.T{-0, 2, -0},
194 | vec3.T{-1.4142135623730951, -0, 1.4142135623730951},
195 | }
196 |
197 | adjA := A.Adjugated()
198 |
199 | if adjA != expectedAdjugated {
200 | t.Errorf("Adjugate not computed correctly: %v", adjA)
201 | }
202 | }
203 |
204 | func TestInvert_ok(t *testing.T) {
205 | inv := invertableMatrix1
206 | _, err := inv.Invert()
207 |
208 | if err != nil {
209 | t.Error("Inverse not computed correctly", err)
210 | }
211 |
212 | invExpected := invertedMatrix1
213 | if inv != invExpected {
214 | t.Errorf("Inverse not computed correctly: %#v", inv)
215 | }
216 | }
217 |
218 | func TestInvert_ok2(t *testing.T) {
219 | sqrt2 := float32(math.Sqrt(2))
220 | A := T{
221 | vec3.T{1, 0, -1},
222 | vec3.T{0, sqrt2, 0},
223 | vec3.T{1, 0, 1},
224 | }
225 |
226 | expectedInverted := T{
227 | vec3.T{0.5, 0, 0.5},
228 | vec3.T{0, 0.7071067811865475, 0},
229 | vec3.T{-0.5, 0, 0.5},
230 | }
231 |
232 | invA, err := A.Inverted()
233 | if err != nil {
234 | t.Error("Inverse not computed correctly", err)
235 | }
236 |
237 | if !invA.PracticallyEquals(&expectedInverted, EPSILON) {
238 | t.Errorf("Inverse not computed correctly: %v", A)
239 | }
240 | }
241 |
242 | func TestInvert_nok_1(t *testing.T) {
243 | inv := nonInvertableMatrix1
244 | _, err := inv.Inverted()
245 | if err == nil {
246 | t.Error("Inverse should not be possible", err)
247 | }
248 | }
249 |
250 | func TestInvert_nok_2(t *testing.T) {
251 | inv := nonInvertableMatrix2
252 | _, err := inv.Inverted()
253 | if err == nil {
254 | t.Error("Inverse should not be possible", err)
255 | }
256 | }
257 |
258 | func BenchmarkAssignMul(b *testing.B) {
259 | m1 := testMatrix1
260 | m2 := testMatrix2
261 | var mMult T
262 | b.ResetTimer()
263 | for i := 0; i < b.N; i++ {
264 | mMult.AssignMul(&m1, &m2)
265 | }
266 | }
267 |
268 | func TestIsZeroEps(t *testing.T) {
269 | tests := []struct {
270 | name string
271 | mat T
272 | epsilon float32
273 | want bool
274 | }{
275 | {"exact zero", Zero, 0.0001, true},
276 | {"within epsilon", T{vec3.T{0.00001, -0.00001, 0.00001}, vec3.T{-0.00001, 0.00001, -0.00001}, vec3.T{0.00001, -0.00001, 0.00001}}, 0.0001, true},
277 | {"at epsilon boundary", T{vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}}, 0.0001, true},
278 | {"outside epsilon", T{vec3.T{0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false},
279 | {"one element outside", T{vec3.T{0.00001, 0.00001, 0.00001}, vec3.T{0.001, 0.00001, 0.00001}, vec3.T{0.00001, 0.00001, 0.00001}}, 0.0001, false},
280 | {"negative outside epsilon", T{vec3.T{-0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false},
281 | {"identity matrix", Ident, 0.0001, false},
282 | }
283 |
284 | for _, tt := range tests {
285 | t.Run(tt.name, func(t *testing.T) {
286 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
287 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon)
288 | }
289 | })
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/mat4/mat4_test.go:
--------------------------------------------------------------------------------
1 | package mat4
2 |
3 | import (
4 | "testing"
5 |
6 | math "github.com/chewxy/math32"
7 | "github.com/ungerik/go3d/mat3"
8 | "github.com/ungerik/go3d/vec3"
9 | "github.com/ungerik/go3d/vec4"
10 | )
11 |
12 | const EPSILON = 0.0001
13 |
14 | // Some matrices used in multiple tests.
15 | var (
16 | TEST_MATRIX1 = T{
17 | vec4.T{0.38016528, -0.0661157, -0.008264462, -0},
18 | vec4.T{-0.19834709, 0.33884296, -0.08264463, 0},
19 | vec4.T{0.11570247, -0.28099173, 0.21487603, -0},
20 | vec4.T{18.958677, -33.471073, 8.066115, 0.99999994},
21 | }
22 |
23 | TEST_MATRIX2 = T{
24 | vec4.T{23, -4, -0.5, -0},
25 | vec4.T{-12, 20.5, -5, 0},
26 | vec4.T{7, -17, 13, -0},
27 | vec4.T{1147, -2025, 488, 60.5},
28 | }
29 |
30 | ROW_123_CHANGED, _ = Parse("3 1 0.5 0 2 5 2 0 1 6 7 0 2 100 1 1")
31 | )
32 |
33 | func TestDeterminant(t *testing.T) {
34 | detId := Ident.Determinant()
35 | if detId != 1 {
36 | t.Errorf("Wrong determinant for identity matrix: %f", detId)
37 | }
38 |
39 | detTwo := Ident
40 | detTwo[0][0] = 2
41 | if det := detTwo.Determinant(); det != 2 {
42 | t.Errorf("Wrong determinant: %f", det)
43 | }
44 |
45 | scale2 := Ident.Scale(2)
46 | if det := scale2.Determinant(); det != 2*2*2*1 {
47 | t.Errorf("Wrong determinant: %f", det)
48 | }
49 |
50 | row1changed, _ := Parse("3 0 0 0 2 2 0 0 1 0 2 0 2 0 0 1")
51 | if det := row1changed.Determinant(); det != 12 {
52 | t.Errorf("Wrong determinant: %f", det)
53 | }
54 |
55 | row12changed, _ := Parse("3 1 0 0 2 5 0 0 1 6 2 0 2 100 0 1")
56 | if det := row12changed.Determinant(); det != 26 {
57 | t.Errorf("Wrong determinant: %f", det)
58 | }
59 |
60 | row123changed := ROW_123_CHANGED
61 | if det := row123changed.Determinant3x3(); det != 60.500 {
62 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det)
63 | }
64 | if det := row123changed.Determinant(); det != 60.500 {
65 | t.Errorf("Wrong determinant: %f", det)
66 | }
67 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.23421 0.16600 0.40608 0.53479 0.43210 0.37328 0.36436 0.56356 0.66830 0.32475 0.14294 0.42137 0.98046")
68 | randomMatrix.Transpose() //transpose for easy comparability with octave output
69 | if err != nil {
70 | t.Errorf("Could not parse random matrix: %v", err)
71 | }
72 | if det := randomMatrix.Determinant3x3(); math.Abs(det-0.043437) > EPSILON {
73 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det)
74 | }
75 |
76 | if det := randomMatrix.Determinant(); math.Abs(det-0.012208) > EPSILON {
77 | t.Errorf("Wrong determinant for random matrix: %f", det)
78 | }
79 | }
80 |
81 | func TestMaskedBlock(t *testing.T) {
82 | m := ROW_123_CHANGED
83 | blocked_expected := mat3.T{vec3.T{5, 2, 0}, vec3.T{6, 7, 0}, vec3.T{100, 1, 1}}
84 | if blocked := m.maskedBlock(0, 0); *blocked != blocked_expected {
85 | t.Errorf("Did not block 0,0 correctly: %#v", blocked)
86 | }
87 | }
88 |
89 | func TestAdjugate(t *testing.T) {
90 | adj := ROW_123_CHANGED
91 | adj.Adjugate()
92 | // Computed in octave:
93 | adj_expected := T{vec4.T{23, -4, -0.5, -0}, vec4.T{-12, 20.5, -5, 0}, vec4.T{7, -17, 13, -0}, vec4.T{1147, -2025, 488, 60.5}}
94 | if adj != adj_expected {
95 | t.Errorf("Adjugate not computed correctly: %#v", adj)
96 | }
97 | }
98 |
99 | func TestInvert(t *testing.T) {
100 | inv := ROW_123_CHANGED
101 | inv.Invert()
102 | // Computed in octave:
103 | inv_expected := T{vec4.T{0.38016528, -0.0661157, -0.008264462, -0}, vec4.T{-0.19834709, 0.33884296, -0.08264463, 0}, vec4.T{0.11570247, -0.28099173, 0.21487603, -0}, vec4.T{18.958677, -33.471073, 8.066115, 0.99999994}}
104 | if inv != inv_expected {
105 | t.Errorf("Inverse not computed correctly: %#v", inv)
106 | }
107 | }
108 |
109 | func TestMultSimpleMatrices(t *testing.T) {
110 | m1 := T{vec4.T{1, 0, 0, 2},
111 | vec4.T{0, 1, 2, 0},
112 | vec4.T{0, 2, 1, 0},
113 | vec4.T{2, 0, 0, 1}}
114 | m2 := m1
115 | var mMult T
116 | mMult.AssignMul(&m1, &m2)
117 | t.Log(&m1)
118 | t.Log(&m2)
119 | m1.MultMatrix(&m2)
120 | if m1 != mMult {
121 | t.Errorf("Multiplication of matrices above failed, expected: \n%v \ngotten: \n%v", &mMult, &m1)
122 | }
123 | }
124 |
125 | func TestMultMatrixVsAssignMul(t *testing.T) {
126 | m1 := TEST_MATRIX1
127 | m2 := TEST_MATRIX2
128 | var mMult T
129 | mMult.AssignMul(&m1, &m2)
130 | t.Log(&m1)
131 | t.Log(&m2)
132 | m1.MultMatrix(&m2)
133 | if m1 != mMult {
134 | t.Errorf("Multiplication of matrices above failed, expected: \n%v \ngotten: \n%v", &mMult, &m1)
135 | }
136 | }
137 |
138 | func BenchmarkAssignMul(b *testing.B) {
139 | m1 := TEST_MATRIX1
140 | m2 := TEST_MATRIX2
141 | var mMult T
142 | b.ResetTimer()
143 | for i := 0; i < b.N; i++ {
144 | mMult.AssignMul(&m1, &m2)
145 | }
146 | }
147 |
148 | func BenchmarkMultMatrix(b *testing.B) {
149 | m1 := TEST_MATRIX1
150 | m2 := TEST_MATRIX2
151 | b.ResetTimer()
152 | for i := 0; i < b.N; i++ {
153 | m1.MultMatrix(&m2)
154 | }
155 | }
156 |
157 | func TestMulVec4vsTransformVec4(t *testing.T) {
158 | m1 := TEST_MATRIX1
159 | v := vec4.T{1, 1.5, 2, 2.5}
160 | v_1 := m1.MulVec4(&v)
161 | v_2 := m1.MulVec4(&v_1)
162 |
163 | m1.TransformVec4(&v)
164 | m1.TransformVec4(&v)
165 |
166 | if v_2 != v {
167 | t.Error(v_2, v)
168 | }
169 |
170 | }
171 |
172 | func BenchmarkMulVec4(b *testing.B) {
173 | m1 := TEST_MATRIX1
174 | b.ResetTimer()
175 | for i := 0; i < b.N; i++ {
176 | v := vec4.T{1, 1.5, 2, 2.5}
177 | v_1 := m1.MulVec4(&v)
178 | m1.MulVec4(&v_1)
179 | }
180 | }
181 |
182 | func BenchmarkTransformVec4(b *testing.B) {
183 | m1 := TEST_MATRIX1
184 | b.ResetTimer()
185 | for i := 0; i < b.N; i++ {
186 | v := vec4.T{1, 1.5, 2, 2.5}
187 | m1.TransformVec4(&v)
188 | m1.TransformVec4(&v)
189 | }
190 | }
191 |
192 | func (mat *T) TransformVec4_PassByValue(v vec4.T) (r vec4.T) {
193 | // Use intermediate variables to not alter further computations.
194 | x := mat[0][0]*v[0] + mat[1][0]*v[1] + mat[2][0]*v[2] + mat[3][0]*v[3]
195 | y := mat[0][1]*v[0] + mat[1][1]*v[1] + mat[2][1]*v[2] + mat[3][1]*v[3]
196 | z := mat[0][2]*v[0] + mat[1][2]*v[1] + mat[2][2]*v[2] + mat[3][2]*v[3]
197 | r[3] = mat[0][3]*v[0] + mat[1][3]*v[1] + mat[2][3]*v[2] + mat[3][3]*v[3]
198 | r[0] = x
199 | r[1] = y
200 | r[2] = z
201 | return r
202 | }
203 |
204 | func Vec4Add_PassByValue(a, b vec4.T) vec4.T {
205 | if a[3] == b[3] {
206 | return vec4.T{a[0] + b[0], a[1] + b[1], a[2] + b[2], 1}
207 | } else {
208 | a3 := a.Vec3DividedByW()
209 | b3 := b.Vec3DividedByW()
210 | return vec4.T{a3[0] + b3[0], a3[1] + b3[1], a3[2] + b3[2], 1}
211 | }
212 | }
213 |
214 | func BenchmarkMulAddVec4_PassByPointer(b *testing.B) {
215 | m1 := TEST_MATRIX1
216 | m2 := TEST_MATRIX2
217 | var v1 vec4.T
218 | b.ResetTimer()
219 | for i := 0; i < b.N; i++ {
220 | v := vec4.T{1, 1.5, 2, 2.5}
221 | m1.TransformVec4(&v)
222 | m2.TransformVec4(&v)
223 | v = vec4.Add(&v, &v1)
224 | v = vec4.Add(&v, &v1)
225 | }
226 | }
227 |
228 | // Demonstrate that
229 | func BenchmarkMulAddVec4_PassByValue(b *testing.B) {
230 | m1 := TEST_MATRIX1
231 | m2 := TEST_MATRIX2
232 | var v1 vec4.T
233 | b.ResetTimer()
234 | for i := 0; i < b.N; i++ {
235 | v := vec4.T{1, 1.5, 2, 2.5}
236 | m1.TransformVec4_PassByValue(v)
237 | m2.TransformVec4_PassByValue(v)
238 | v = Vec4Add_PassByValue(v, v1)
239 | v = Vec4Add_PassByValue(v, v1)
240 | }
241 | }
242 |
243 | func TestIsZeroEps(t *testing.T) {
244 | tests := []struct {
245 | name string
246 | mat T
247 | epsilon float32
248 | want bool
249 | }{
250 | {"exact zero", Zero, 0.0001, true},
251 | {"within epsilon", T{vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}, vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}}, 0.0001, true},
252 | {"at epsilon boundary", T{vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}}, 0.0001, true},
253 | {"outside epsilon", T{vec4.T{0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false},
254 | {"one element outside", T{vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}}, 0.0001, false},
255 | {"negative outside epsilon", T{vec4.T{-0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false},
256 | {"identity matrix", Ident, 0.0001, false},
257 | }
258 |
259 | for _, tt := range tests {
260 | t.Run(tt.name, func(t *testing.T) {
261 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want {
262 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon)
263 | }
264 | })
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/quaternion/quaternion.go:
--------------------------------------------------------------------------------
1 | // Package quaternion contains a float32 unit-quaternion type T and functions.
2 | package quaternion
3 |
4 | import (
5 | "fmt"
6 |
7 | math "github.com/chewxy/math32"
8 | "github.com/ungerik/go3d/vec3"
9 | "github.com/ungerik/go3d/vec4"
10 | )
11 |
12 | var (
13 | // Zero holds a zero quaternion.
14 | Zero = T{}
15 |
16 | // Ident holds an ident quaternion.
17 | Ident = T{0, 0, 0, 1}
18 | )
19 |
20 | // T represents an orientation/rotation as a unit quaternion.
21 | // See http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
22 | type T [4]float32
23 |
24 | // FromAxisAngle returns a quaternion representing a rotation around and axis.
25 | func FromAxisAngle(axis *vec3.T, angle float32) T {
26 | angle *= 0.5
27 | sin := math.Sin(angle)
28 | q := T{axis[0] * sin, axis[1] * sin, axis[2] * sin, math.Cos(angle)}
29 | return q.Normalized()
30 | }
31 |
32 | // FromXAxisAngle returns a quaternion representing a rotation around the x axis.
33 | func FromXAxisAngle(angle float32) T {
34 | angle *= 0.5
35 | return T{math.Sin(angle), 0, 0, math.Cos(angle)}
36 | }
37 |
38 | // FromYAxisAngle returns a quaternion representing a rotation around the y axis.
39 | func FromYAxisAngle(angle float32) T {
40 | angle *= 0.5
41 | return T{0, math.Sin(angle), 0, math.Cos(angle)}
42 | }
43 |
44 | // FromZAxisAngle returns a quaternion representing a rotation around the z axis.
45 | func FromZAxisAngle(angle float32) T {
46 | angle *= 0.5
47 | return T{0, 0, math.Sin(angle), math.Cos(angle)}
48 | }
49 |
50 | // FromEulerAngles returns a quaternion representing Euler angle(in radian) rotations.
51 | func FromEulerAngles(yHead, xPitch, zRoll float32) T {
52 | qy := FromYAxisAngle(yHead)
53 | qx := FromXAxisAngle(xPitch)
54 | qz := FromZAxisAngle(zRoll)
55 | return Mul3(&qy, &qx, &qz)
56 | }
57 |
58 | // ToEulerAngles returns the euler angle(in radian) rotations of the quaternion.
59 | func (quat *T) ToEulerAngles() (yHead, xPitch, zRoll float32) {
60 | z := quat.RotatedVec3(&vec3.T{0, 0, 1})
61 | yHead = math.Atan2(z[0], z[2])
62 | xPitch = -math.Atan2(z[1], math.Sqrt(z[0]*z[0]+z[2]*z[2]))
63 |
64 | quatNew := FromEulerAngles(yHead, xPitch, 0)
65 |
66 | x2 := quatNew.RotatedVec3(&vec3.T{1, 0, 0})
67 | x := quat.RotatedVec3(&vec3.T{1, 0, 0})
68 | r := vec3.Cross(&x, &x2)
69 | sin := vec3.Dot(&r, &z)
70 | cos := vec3.Dot(&x, &x2)
71 | zRoll = -math.Atan2(sin, cos)
72 | return
73 | }
74 |
75 | // FromVec4 converts a vec4.T into a quaternion.
76 | func FromVec4(v *vec4.T) T {
77 | return T(*v)
78 | }
79 |
80 | // Vec4 converts the quaternion into a vec4.T.
81 | func (quat *T) Vec4() vec4.T {
82 | return vec4.T(*quat)
83 | }
84 |
85 | // Parse parses T from a string. See also String()
86 | func Parse(s string) (r T, err error) {
87 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2], &r[3])
88 | return r, err
89 | }
90 |
91 | // String formats T as string. See also Parse().
92 | func (quat *T) String() string {
93 | return fmt.Sprint(quat[0], quat[1], quat[2], quat[3])
94 | }
95 |
96 | // AxisAngle extracts the rotation in form of an axis and a rotation angle.
97 | func (quat *T) AxisAngle() (axis vec3.T, angle float32) {
98 | cos := quat[3]
99 | sin := math.Sqrt(1 - cos*cos)
100 | angle = math.Acos(cos)
101 |
102 | var ooSin float32
103 | if math.Abs(sin) < 0.0005 {
104 | ooSin = 1
105 | } else {
106 | ooSin = 1 / sin
107 | }
108 | axis[0] = quat[0] * ooSin
109 | axis[1] = quat[1] * ooSin
110 | axis[2] = quat[2] * ooSin
111 |
112 | return axis, angle
113 | }
114 |
115 | // Norm returns the norm value of the quaternion.
116 | func (quat *T) Norm() float32 {
117 | return quat[0]*quat[0] + quat[1]*quat[1] + quat[2]*quat[2] + quat[3]*quat[3]
118 | }
119 |
120 | // Normalize normalizes to a unit quaternation.
121 | // Uses the package Epsilon variable for numerical stability:
122 | // - Quaternions with norm < Epsilon are considered zero and left unchanged
123 | // - Quaternions with norm within Epsilon of 1.0 are considered already normalized
124 | func (quat *T) Normalize() *T {
125 | norm := quat.Norm()
126 | if norm < Epsilon {
127 | // Quaternion is effectively zero
128 | return quat
129 | }
130 | if math.Abs(norm-1) < Epsilon {
131 | // Quaternion is already normalized
132 | return quat
133 | }
134 | ool := 1 / math.Sqrt(norm)
135 | quat[0] *= ool
136 | quat[1] *= ool
137 | quat[2] *= ool
138 | quat[3] *= ool
139 | return quat
140 | }
141 |
142 | // Normalized returns a copy normalized to a unit quaternation.
143 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details.
144 | func (quat *T) Normalized() T {
145 | norm := quat.Norm()
146 | if norm < Epsilon {
147 | // Quaternion is effectively zero
148 | return *quat
149 | }
150 | if math.Abs(norm-1) < Epsilon {
151 | // Quaternion is already normalized
152 | return *quat
153 | }
154 | ool := 1 / math.Sqrt(norm)
155 | return T{
156 | quat[0] * ool,
157 | quat[1] * ool,
158 | quat[2] * ool,
159 | quat[3] * ool,
160 | }
161 | }
162 |
163 | // Negate negates the quaternion.
164 | func (quat *T) Negate() *T {
165 | quat[0] = -quat[0]
166 | quat[1] = -quat[1]
167 | quat[2] = -quat[2]
168 | quat[3] = -quat[3]
169 | return quat
170 | }
171 |
172 | // Negated returns a negated copy of the quaternion.
173 | func (quat *T) Negated() T {
174 | return T{-quat[0], -quat[1], -quat[2], -quat[3]}
175 | }
176 |
177 | // Invert inverts the quaternion.
178 | func (quat *T) Invert() *T {
179 | quat[0] = -quat[0]
180 | quat[1] = -quat[1]
181 | quat[2] = -quat[2]
182 | return quat
183 | }
184 |
185 | // Inverted returns an inverted copy of the quaternion.
186 | func (quat *T) Inverted() T {
187 | return T{-quat[0], -quat[1], -quat[2], quat[3]}
188 | }
189 |
190 | // SetShortestRotation negates the quaternion if it does not represent the shortest rotation from quat to the orientation of other.
191 | // (there are two directions to rotate from the orientation of quat to the orientation of other)
192 | // See IsShortestRotation()
193 | func (quat *T) SetShortestRotation(other *T) *T {
194 | if !IsShortestRotation(quat, other) {
195 | quat.Negate()
196 | }
197 | return quat
198 | }
199 |
200 | // IsShortestRotation returns if the rotation from a to b is the shortest possible rotation.
201 | // (there are two directions to rotate from the orientation of quat to the orientation of other)
202 | // See T.SetShortestRotation
203 | func IsShortestRotation(a, b *T) bool {
204 | return Dot(a, b) >= 0
205 | }
206 |
207 | // IsUnitQuat returns if the quaternion is within tolerance of the unit quaternion.
208 | func (quat *T) IsUnitQuat(tolerance float32) bool {
209 | norm := quat.Norm()
210 | return norm >= (1.0-tolerance) && norm <= (1.0+tolerance)
211 | }
212 |
213 | // RotateVec3 rotates v by the rotation represented by the quaternion.
214 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
215 | func (quat *T) RotateVec3(v *vec3.T) {
216 | u := vec3.T{quat[0], quat[1], quat[2]}
217 | s := quat[3]
218 | vt1 := u.Scaled(2 * vec3.Dot(&u, v))
219 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u))
220 | vt3 := vec3.Cross(&u, v)
221 | vt3 = vt3.Scaled(2 * s)
222 | v[0] = vt1[0] + vt2[0] + vt3[0]
223 | v[1] = vt1[1] + vt2[1] + vt3[1]
224 | v[2] = vt1[2] + vt2[2] + vt3[2]
225 | }
226 |
227 | // RotatedVec3 returns a rotated copy of v.
228 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
229 | func (quat *T) RotatedVec3(v *vec3.T) vec3.T {
230 | u := vec3.T{quat[0], quat[1], quat[2]}
231 | s := quat[3]
232 | vt1 := u.Scaled(2 * vec3.Dot(&u, v))
233 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u))
234 | vt3 := vec3.Cross(&u, v)
235 | vt3 = vt3.Scaled(2 * s)
236 | return vec3.T{vt1[0] + vt2[0] + vt3[0], vt1[1] + vt2[1] + vt3[1], vt1[2] + vt2[2] + vt3[2]}
237 | }
238 |
239 | // Dot returns the dot product of two quaternions.
240 | func Dot(a, b *T) float32 {
241 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]
242 | }
243 |
244 | // MulRaw multiplies two quaternions without normalizing the result.
245 | // Use this when you want to chain multiple quaternion operations and normalize once at the end:
246 | //
247 | // q := quaternion.MulRaw(&a, &b)
248 | // q = quaternion.MulRaw(&q, &c)
249 | // q.Normalize() // Normalize once at the end
250 | //
251 | // For most use cases, use Mul() instead which automatically normalizes.
252 | func MulRaw(a, b *T) T {
253 | return T{
254 | a[3]*b[0] + a[0]*b[3] + a[1]*b[2] - a[2]*b[1],
255 | a[3]*b[1] + a[1]*b[3] + a[2]*b[0] - a[0]*b[2],
256 | a[3]*b[2] + a[2]*b[3] + a[0]*b[1] - a[1]*b[0],
257 | a[3]*b[3] - a[0]*b[0] - a[1]*b[1] - a[2]*b[2],
258 | }
259 | }
260 |
261 | // Mul multiplies two quaternions and normalizes the result.
262 | // The result is guaranteed to be a unit quaternion suitable for rotations.
263 | // For chaining multiple operations, consider using MulRaw() and normalizing once at the end.
264 | func Mul(a, b *T) T {
265 | q := MulRaw(a, b)
266 | return q.Normalized()
267 | }
268 |
269 | // Mul3 multiplies three quaternions.
270 | func Mul3(a, b, c *T) T {
271 | q := Mul(a, b)
272 | return Mul(&q, c)
273 | }
274 |
275 | // Mul4 multiplies four quaternions.
276 | func Mul4(a, b, c, d *T) T {
277 | q := Mul(a, b)
278 | q = Mul(&q, c)
279 | return Mul(&q, d)
280 | }
281 |
282 | // Slerp returns the spherical linear interpolation quaternion between a and b at t (0,1).
283 | // See http://en.wikipedia.org/wiki/Slerp
284 | func Slerp(a, b *T, t float32) T {
285 | dot := a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]
286 |
287 | // If quaternions are very close, use linear interpolation to avoid division by zero
288 | const threshold = 0.9995
289 | if math.Abs(dot) > threshold {
290 | // Linear interpolation for close quaternions
291 | q := T{
292 | a[0]*(1-t) + b[0]*t,
293 | a[1]*(1-t) + b[1]*t,
294 | a[2]*(1-t) + b[2]*t,
295 | a[3]*(1-t) + b[3]*t,
296 | }
297 | return q.Normalized()
298 | }
299 |
300 | d := math.Acos(dot)
301 | sinD := math.Sin(d)
302 | ooSinD := 1 / sinD
303 |
304 | t1 := math.Sin(d*(1-t)) * ooSinD
305 | t2 := math.Sin(d*t) * ooSinD
306 |
307 | q := T{
308 | a[0]*t1 + b[0]*t2,
309 | a[1]*t1 + b[1]*t2,
310 | a[2]*t1 + b[2]*t2,
311 | a[3]*t1 + b[3]*t2,
312 | }
313 |
314 | return q.Normalized()
315 | }
316 |
317 | // Vec3Diff returns the rotation quaternion between two vectors.
318 | func Vec3Diff(a, b *vec3.T) T {
319 | dot := vec3.Dot(a, b)
320 |
321 | // Handle opposite vectors (dot product near -1)
322 | if dot < -0.999999 {
323 | // Find an orthogonal axis
324 | axis := vec3.UnitX
325 | test := vec3.Cross(a, &axis)
326 | if test.LengthSqr() < 0.01 {
327 | // a is parallel to UnitX, use UnitY instead
328 | axis = vec3.UnitY
329 | test = vec3.Cross(a, &axis)
330 | }
331 | test.Normalize()
332 | // 180 degree rotation around the orthogonal axis
333 | return T{test[0], test[1], test[2], 0}
334 | }
335 |
336 | cr := vec3.Cross(a, b)
337 | sr := math.Sqrt(2 * (1 + dot))
338 | oosr := 1 / sr
339 |
340 | q := T{cr[0] * oosr, cr[1] * oosr, cr[2] * oosr, sr * 0.5}
341 | return q.Normalized()
342 | }
343 |
--------------------------------------------------------------------------------
/float64/quaternion/quaternion.go:
--------------------------------------------------------------------------------
1 | // Package quaternion contains a float64 unit-quaternion type T and functions.
2 | package quaternion
3 |
4 | import (
5 | "fmt"
6 | "math"
7 |
8 | "github.com/ungerik/go3d/float64/vec3"
9 | "github.com/ungerik/go3d/float64/vec4"
10 | )
11 |
12 | var (
13 | // Zero holds a zero quaternion.
14 | Zero = T{}
15 |
16 | // Ident holds an ident quaternion.
17 | Ident = T{0, 0, 0, 1}
18 | )
19 |
20 | // T represents an orientation/rotation as a unit quaternion.
21 | // See http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
22 | type T [4]float64
23 |
24 | // FromAxisAngle returns a quaternion representing a rotation around and axis.
25 | func FromAxisAngle(axis *vec3.T, angle float64) T {
26 | angle *= 0.5
27 | sin := math.Sin(angle)
28 | q := T{axis[0] * sin, axis[1] * sin, axis[2] * sin, math.Cos(angle)}
29 | return q.Normalized()
30 | }
31 |
32 | // FromXAxisAngle returns a quaternion representing a rotation around the x axis.
33 | func FromXAxisAngle(angle float64) T {
34 | angle *= 0.5
35 | return T{math.Sin(angle), 0, 0, math.Cos(angle)}
36 | }
37 |
38 | // FromYAxisAngle returns a quaternion representing a rotation around the y axis.
39 | func FromYAxisAngle(angle float64) T {
40 | angle *= 0.5
41 | return T{0, math.Sin(angle), 0, math.Cos(angle)}
42 | }
43 |
44 | // FromZAxisAngle returns a quaternion representing a rotation around the z axis.
45 | func FromZAxisAngle(angle float64) T {
46 | angle *= 0.5
47 | return T{0, 0, math.Sin(angle), math.Cos(angle)}
48 | }
49 |
50 | // FromEulerAngles returns a quaternion representing Euler angle(in radian) rotations.
51 | func FromEulerAngles(yHead, xPitch, zRoll float64) T {
52 | qy := FromYAxisAngle(yHead)
53 | qx := FromXAxisAngle(xPitch)
54 | qz := FromZAxisAngle(zRoll)
55 | return Mul3(&qy, &qx, &qz)
56 | }
57 |
58 | // ToEulerAngles returns the euler angle(in radian) rotations of the quaternion.
59 | func (quat *T) ToEulerAngles() (yHead, xPitch, zRoll float64) {
60 | z := quat.RotatedVec3(&vec3.T{0, 0, 1})
61 | yHead = math.Atan2(z[0], z[2])
62 | xPitch = -math.Atan2(z[1], math.Sqrt(z[0]*z[0]+z[2]*z[2]))
63 |
64 | quatNew := FromEulerAngles(yHead, xPitch, 0)
65 |
66 | x2 := quatNew.RotatedVec3(&vec3.T{1, 0, 0})
67 | x := quat.RotatedVec3(&vec3.T{1, 0, 0})
68 | r := vec3.Cross(&x, &x2)
69 | sin := vec3.Dot(&r, &z)
70 | cos := vec3.Dot(&x, &x2)
71 | zRoll = -math.Atan2(sin, cos)
72 | return
73 | }
74 |
75 | // FromVec4 converts a vec4.T into a quaternion.
76 | func FromVec4(v *vec4.T) T {
77 | return T(*v)
78 | }
79 |
80 | // Vec4 converts the quaternion into a vec4.T.
81 | func (quat *T) Vec4() vec4.T {
82 | return vec4.T(*quat)
83 | }
84 |
85 | // Parse parses T from a string. See also String()
86 | func Parse(s string) (r T, err error) {
87 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2], &r[3])
88 | return r, err
89 | }
90 |
91 | // String formats T as string. See also Parse().
92 | func (quat *T) String() string {
93 | return fmt.Sprint(quat[0], quat[1], quat[2], quat[3])
94 | }
95 |
96 | // AxisAngle extracts the rotation from a normalized quaternion in the form of an axis and a rotation angle.
97 | func (quat *T) AxisAngle() (axis vec3.T, angle float64) {
98 | cos := quat[3]
99 | sin := math.Sqrt(1 - cos*cos)
100 | angle = math.Acos(cos) * 2
101 |
102 | var ooSin float64
103 | if math.Abs(sin) < 0.0005 {
104 | ooSin = 1
105 | } else {
106 | ooSin = 1 / sin
107 | }
108 | axis[0] = quat[0] * ooSin
109 | axis[1] = quat[1] * ooSin
110 | axis[2] = quat[2] * ooSin
111 |
112 | return axis, angle
113 | }
114 |
115 | // Norm returns the norm value of the quaternion.
116 | func (quat *T) Norm() float64 {
117 | return quat[0]*quat[0] + quat[1]*quat[1] + quat[2]*quat[2] + quat[3]*quat[3]
118 | }
119 |
120 | // Normalize normalizes to a unit quaternation.
121 | // Uses the package Epsilon variable for numerical stability:
122 | // - Quaternions with norm < Epsilon are considered zero and left unchanged
123 | // - Quaternions with norm within Epsilon of 1.0 are considered already normalized
124 | func (quat *T) Normalize() *T {
125 | norm := quat.Norm()
126 | if norm < Epsilon {
127 | // Quaternion is effectively zero
128 | return quat
129 | }
130 | if math.Abs(norm-1) < Epsilon {
131 | // Quaternion is already normalized
132 | return quat
133 | }
134 | ool := 1 / math.Sqrt(norm)
135 | quat[0] *= ool
136 | quat[1] *= ool
137 | quat[2] *= ool
138 | quat[3] *= ool
139 | return quat
140 | }
141 |
142 | // Normalized returns a copy normalized to a unit quaternation.
143 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details.
144 | func (quat *T) Normalized() T {
145 | norm := quat.Norm()
146 | if norm < Epsilon {
147 | // Quaternion is effectively zero
148 | return *quat
149 | }
150 | if math.Abs(norm-1) < Epsilon {
151 | // Quaternion is already normalized
152 | return *quat
153 | }
154 | ool := 1 / math.Sqrt(norm)
155 | return T{
156 | quat[0] * ool,
157 | quat[1] * ool,
158 | quat[2] * ool,
159 | quat[3] * ool,
160 | }
161 | }
162 |
163 | // Negate negates the quaternion.
164 | func (quat *T) Negate() *T {
165 | quat[0] = -quat[0]
166 | quat[1] = -quat[1]
167 | quat[2] = -quat[2]
168 | quat[3] = -quat[3]
169 | return quat
170 | }
171 |
172 | // Negated returns a negated copy of the quaternion.
173 | func (quat *T) Negated() T {
174 | return T{-quat[0], -quat[1], -quat[2], -quat[3]}
175 | }
176 |
177 | // Invert inverts the quaternion.
178 | func (quat *T) Invert() *T {
179 | quat[0] = -quat[0]
180 | quat[1] = -quat[1]
181 | quat[2] = -quat[2]
182 | return quat
183 | }
184 |
185 | // Inverted returns an inverted copy of the quaternion.
186 | func (quat *T) Inverted() T {
187 | return T{-quat[0], -quat[1], -quat[2], quat[3]}
188 | }
189 |
190 | // SetShortestRotation negates the quaternion if it does not represent the shortest rotation from quat to the orientation of other.
191 | // (there are two directions to rotate from the orientation of quat to the orientation of other)
192 | // See IsShortestRotation()
193 | func (quat *T) SetShortestRotation(other *T) *T {
194 | if !IsShortestRotation(quat, other) {
195 | quat.Negate()
196 | }
197 | return quat
198 | }
199 |
200 | // IsShortestRotation returns if the rotation from a to b is the shortest possible rotation.
201 | // (there are two directions to rotate from the orientation of quat to the orientation of other)
202 | // See T.SetShortestRotation
203 | func IsShortestRotation(a, b *T) bool {
204 | return Dot(a, b) >= 0
205 | }
206 |
207 | // IsUnitQuat returns if the quaternion is within tolerance of the unit quaternion.
208 | func (quat *T) IsUnitQuat(tolerance float64) bool {
209 | norm := quat.Norm()
210 | return norm >= (1.0-tolerance) && norm <= (1.0+tolerance)
211 | }
212 |
213 | // RotateVec3 rotates v by the rotation represented by the quaternion.
214 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
215 | func (quat *T) RotateVec3(v *vec3.T) {
216 | u := vec3.T{quat[0], quat[1], quat[2]}
217 | s := quat[3]
218 | vt1 := u.Scaled(2 * vec3.Dot(&u, v))
219 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u))
220 | vt3 := vec3.Cross(&u, v)
221 | vt3 = vt3.Scaled(2 * s)
222 | v[0] = vt1[0] + vt2[0] + vt3[0]
223 | v[1] = vt1[1] + vt2[1] + vt3[1]
224 | v[2] = vt1[2] + vt2[2] + vt3[2]
225 | }
226 |
227 | // RotatedVec3 returns a rotated copy of v.
228 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
229 | func (quat *T) RotatedVec3(v *vec3.T) vec3.T {
230 | u := vec3.T{quat[0], quat[1], quat[2]}
231 | s := quat[3]
232 | vt1 := u.Scaled(2 * vec3.Dot(&u, v))
233 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u))
234 | vt3 := vec3.Cross(&u, v)
235 | vt3 = vt3.Scaled(2 * s)
236 | return vec3.T{vt1[0] + vt2[0] + vt3[0], vt1[1] + vt2[1] + vt3[1], vt1[2] + vt2[2] + vt3[2]}
237 | }
238 |
239 | // Dot returns the dot product of two quaternions.
240 | func Dot(a, b *T) float64 {
241 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]
242 | }
243 |
244 | // MulRaw multiplies two quaternions without normalizing the result.
245 | // Use this when you want to chain multiple quaternion operations and normalize once at the end:
246 | //
247 | // q := quaternion.MulRaw(&a, &b)
248 | // q = quaternion.MulRaw(&q, &c)
249 | // q.Normalize() // Normalize once at the end
250 | //
251 | // For most use cases, use Mul() instead which automatically normalizes.
252 | func MulRaw(a, b *T) T {
253 | return T{
254 | a[3]*b[0] + a[0]*b[3] + a[1]*b[2] - a[2]*b[1],
255 | a[3]*b[1] + a[1]*b[3] + a[2]*b[0] - a[0]*b[2],
256 | a[3]*b[2] + a[2]*b[3] + a[0]*b[1] - a[1]*b[0],
257 | a[3]*b[3] - a[0]*b[0] - a[1]*b[1] - a[2]*b[2],
258 | }
259 | }
260 |
261 | // Mul multiplies two quaternions and normalizes the result.
262 | // The result is guaranteed to be a unit quaternion suitable for rotations.
263 | // For chaining multiple operations, consider using MulRaw() and normalizing once at the end.
264 | func Mul(a, b *T) T {
265 | q := MulRaw(a, b)
266 | return q.Normalized()
267 | }
268 |
269 | // Mul3 multiplies three quaternions.
270 | func Mul3(a, b, c *T) T {
271 | q := Mul(a, b)
272 | return Mul(&q, c)
273 | }
274 |
275 | // Mul4 multiplies four quaternions.
276 | func Mul4(a, b, c, d *T) T {
277 | q := Mul(a, b)
278 | q = Mul(&q, c)
279 | return Mul(&q, d)
280 | }
281 |
282 | // Slerp returns the spherical linear interpolation quaternion between a and b at t (0,1).
283 | // See http://en.wikipedia.org/wiki/Slerp
284 | func Slerp(a, b *T, t float64) T {
285 | dot := a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]
286 |
287 | // If quaternions are very close, use linear interpolation to avoid division by zero
288 | const threshold = 0.9995
289 | if math.Abs(dot) > threshold {
290 | // Linear interpolation for close quaternions
291 | q := T{
292 | a[0]*(1-t) + b[0]*t,
293 | a[1]*(1-t) + b[1]*t,
294 | a[2]*(1-t) + b[2]*t,
295 | a[3]*(1-t) + b[3]*t,
296 | }
297 | return q.Normalized()
298 | }
299 |
300 | d := math.Acos(dot)
301 | sinD := math.Sin(d)
302 | ooSinD := 1 / sinD
303 |
304 | t1 := math.Sin(d*(1-t)) * ooSinD
305 | t2 := math.Sin(d*t) * ooSinD
306 |
307 | q := T{
308 | a[0]*t1 + b[0]*t2,
309 | a[1]*t1 + b[1]*t2,
310 | a[2]*t1 + b[2]*t2,
311 | a[3]*t1 + b[3]*t2,
312 | }
313 |
314 | return q.Normalized()
315 | }
316 |
317 | // Vec3Diff returns the rotation quaternion between two vectors.
318 | func Vec3Diff(a, b *vec3.T) T {
319 | dot := vec3.Dot(a, b)
320 |
321 | // Handle opposite vectors (dot product near -1)
322 | if dot < -0.999999 {
323 | // Find an orthogonal axis
324 | axis := vec3.UnitX
325 | test := vec3.Cross(a, &axis)
326 | if test.LengthSqr() < 0.01 {
327 | // a is parallel to UnitX, use UnitY instead
328 | axis = vec3.UnitY
329 | test = vec3.Cross(a, &axis)
330 | }
331 | test.Normalize()
332 | // 180 degree rotation around the orthogonal axis
333 | return T{test[0], test[1], test[2], 0}
334 | }
335 |
336 | cr := vec3.Cross(a, b)
337 | sr := math.Sqrt(2 * (1 + dot))
338 | oosr := 1 / sr
339 |
340 | q := T{cr[0] * oosr, cr[1] * oosr, cr[2] * oosr, sr * 0.5}
341 | return q.Normalized()
342 | }
343 |
--------------------------------------------------------------------------------
/float64/vec3/vec3.go:
--------------------------------------------------------------------------------
1 | // Package vec3 contains a 3D float64 vector type T and functions.
2 | package vec3
3 |
4 | import (
5 | "fmt"
6 | "math"
7 |
8 | "github.com/ungerik/go3d/float64/generic"
9 | )
10 |
11 | var (
12 | // Zero holds a zero vector.
13 | Zero = T{}
14 |
15 | // UnitX holds a vector with X set to one.
16 | UnitX = T{1, 0, 0}
17 | // UnitY holds a vector with Y set to one.
18 | UnitY = T{0, 1, 0}
19 | // UnitZ holds a vector with Z set to one.
20 | UnitZ = T{0, 0, 1}
21 | // UnitXYZ holds a vector with X, Y, Z set to one.
22 | UnitXYZ = T{1, 1, 1}
23 |
24 | // Red holds the color red.
25 | Red = T{1, 0, 0}
26 | // Green holds the color green.
27 | Green = T{0, 1, 0}
28 | // Blue holds the color blue.
29 | Blue = T{0, 0, 1}
30 | // Black holds the color black.
31 | Black = T{0, 0, 0}
32 | // White holds the color white.
33 | White = T{1, 1, 1}
34 |
35 | // MinVal holds a vector with the smallest possible component values.
36 | MinVal = T{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64}
37 | // MaxVal holds a vector with the highest possible component values.
38 | MaxVal = T{+math.MaxFloat64, +math.MaxFloat64, +math.MaxFloat64}
39 | )
40 |
41 | // T represents a 3D vector.
42 | type T [3]float64
43 |
44 | // From copies a T from a generic.T implementation.
45 | func From(other generic.T) T {
46 | switch other.Size() {
47 | case 2:
48 | return T{other.Get(0, 0), other.Get(0, 1), 0}
49 | case 3, 4:
50 | return T{other.Get(0, 0), other.Get(0, 1), other.Get(0, 2)}
51 | default:
52 | panic("Unsupported type")
53 | }
54 | }
55 |
56 | // Parse parses T from a string. See also String()
57 | func Parse(s string) (r T, err error) {
58 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2])
59 | return r, err
60 | }
61 |
62 | // String formats T as string. See also Parse().
63 | func (vec *T) String() string {
64 | return fmt.Sprint(vec[0], vec[1], vec[2])
65 | }
66 |
67 | // Rows returns the number of rows of the vector.
68 | func (vec *T) Rows() int {
69 | return 3
70 | }
71 |
72 | // Cols returns the number of columns of the vector.
73 | func (vec *T) Cols() int {
74 | return 1
75 | }
76 |
77 | // Size returns the number elements of the vector.
78 | func (vec *T) Size() int {
79 | return 3
80 | }
81 |
82 | // Slice returns the elements of the vector as slice.
83 | func (vec *T) Slice() []float64 {
84 | return vec[:]
85 | }
86 |
87 | // Get returns one element of the vector.
88 | func (vec *T) Get(col, row int) float64 {
89 | return vec[row]
90 | }
91 |
92 | // IsZero checks if all elements of the vector are exactly zero.
93 | // Uses exact equality comparison, which may not be suitable for floating-point math results.
94 | // For tolerance-based comparison, use IsZeroEps instead.
95 | func (vec *T) IsZero() bool {
96 | return vec[0] == 0 && vec[1] == 0 && vec[2] == 0
97 | }
98 |
99 | // IsZeroEps checks if all elements of the vector are zero within the given epsilon tolerance.
100 | // This is the recommended method for comparing floating-point vectors that result from calculations.
101 | // For exact zero comparison, use IsZero instead.
102 | func (vec *T) IsZeroEps(epsilon float64) bool {
103 | return math.Abs(vec[0]) <= epsilon && math.Abs(vec[1]) <= epsilon && math.Abs(vec[2]) <= epsilon
104 | }
105 |
106 | // Length returns the length of the vector.
107 | // See also LengthSqr and Normalize.
108 | func (vec *T) Length() float64 {
109 | return math.Sqrt(vec.LengthSqr())
110 | }
111 |
112 | // LengthSqr returns the squared length of the vector.
113 | // See also Length and Normalize.
114 | func (vec *T) LengthSqr() float64 {
115 | return vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2]
116 | }
117 |
118 | // Scale multiplies all element of the vector by f and returns vec.
119 | func (vec *T) Scale(f float64) *T {
120 | vec[0] *= f
121 | vec[1] *= f
122 | vec[2] *= f
123 | return vec
124 | }
125 |
126 | // Scaled returns a copy of vec with all elements multiplies by f.
127 | func (vec *T) Scaled(f float64) T {
128 | return T{vec[0] * f, vec[1] * f, vec[2] * f}
129 | }
130 |
131 | // PracticallyEquals compares two vectors if they are equal with each other within a delta tolerance.
132 | func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float64) bool {
133 | return (math.Abs(vec[0]-compareVector[0]) <= allowedDelta) &&
134 | (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) &&
135 | (math.Abs(vec[2]-compareVector[2]) <= allowedDelta)
136 | }
137 |
138 | // PracticallyEquals compares two values if they are equal with each other within a delta tolerance.
139 | func PracticallyEquals(v1, v2, allowedDelta float64) bool {
140 | return math.Abs(v1-v2) <= allowedDelta
141 | }
142 |
143 | // Invert inverts the vector.
144 | func (vec *T) Invert() *T {
145 | vec[0] = -vec[0]
146 | vec[1] = -vec[1]
147 | vec[2] = -vec[2]
148 | return vec
149 | }
150 |
151 | // Inverted returns an inverted copy of the vector.
152 | func (vec *T) Inverted() T {
153 | return T{-vec[0], -vec[1], -vec[2]}
154 | }
155 |
156 | // Abs sets every component of the vector to its absolute value.
157 | func (vec *T) Abs() *T {
158 | vec[0] = math.Abs(vec[0])
159 | vec[1] = math.Abs(vec[1])
160 | vec[2] = math.Abs(vec[2])
161 | return vec
162 | }
163 |
164 | // Absed returns a copy of the vector containing the absolute values.
165 | func (vec *T) Absed() T {
166 | return T{math.Abs(vec[0]), math.Abs(vec[1]), math.Abs(vec[2])}
167 | }
168 |
169 | // Normalize normalizes the vector to unit length.
170 | // Uses the package Epsilon variable for numerical stability:
171 | // - Vectors with squared length < Epsilon are considered zero and left unchanged
172 | // - Vectors with squared length within Epsilon of 1.0 are considered already normalized
173 | func (vec *T) Normalize() *T {
174 | sl := vec.LengthSqr()
175 | if sl < Epsilon {
176 | // Vector is effectively zero
177 | return vec
178 | }
179 | if math.Abs(sl-1) < Epsilon {
180 | // Vector is already normalized
181 | return vec
182 | }
183 | return vec.Scale(1 / math.Sqrt(sl))
184 | }
185 |
186 | // Normalized returns a unit length normalized copy of the vector.
187 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details.
188 | func (vec *T) Normalized() T {
189 | v := *vec
190 | v.Normalize()
191 | return v
192 | }
193 |
194 | // Normal returns an orthogonal vector.
195 | // Uses the package Epsilon variable when checking if the cross product is zero,
196 | // which provides numerical stability when the input vector is parallel to UnitZ.
197 | // Falls back to UnitX when the cross product is effectively zero.
198 | func (vec *T) Normal() T {
199 | n := Cross(vec, &UnitZ)
200 | if n.IsZeroEps(Epsilon) {
201 | return UnitX
202 | }
203 | return n.Normalized()
204 | }
205 |
206 | // Add adds another vector to vec.
207 | func (vec *T) Add(v *T) *T {
208 | vec[0] += v[0]
209 | vec[1] += v[1]
210 | vec[2] += v[2]
211 | return vec
212 | }
213 |
214 | // Added adds another vector to vec and returns a copy of the result
215 | func (vec *T) Added(v *T) T {
216 | return T{vec[0] + v[0], vec[1] + v[1], vec[2] + v[2]}
217 | }
218 |
219 | // Sub subtracts another vector from vec.
220 | func (vec *T) Sub(v *T) *T {
221 | vec[0] -= v[0]
222 | vec[1] -= v[1]
223 | vec[2] -= v[2]
224 | return vec
225 | }
226 |
227 | // Subed subtracts another vector from vec and returns a copy of the result
228 | func (vec *T) Subed(v *T) T {
229 | return T{vec[0] - v[0], vec[1] - v[1], vec[2] - v[2]}
230 | }
231 |
232 | // Mul multiplies the components of the vector with the respective components of v.
233 | func (vec *T) Mul(v *T) *T {
234 | vec[0] *= v[0]
235 | vec[1] *= v[1]
236 | vec[2] *= v[2]
237 | return vec
238 | }
239 |
240 | // Muled multiplies the components of the vector with the respective components of v and returns a copy of the result
241 | func (vec *T) Muled(v *T) T {
242 | return T{vec[0] * v[0], vec[1] * v[1], vec[2] * v[2]}
243 | }
244 |
245 | // SquareDistance the distance between two vectors squared (= distance*distance)
246 | func SquareDistance(a, b *T) float64 {
247 | d := Sub(a, b)
248 | return d.LengthSqr()
249 | }
250 |
251 | // Distance the distance between two vectors
252 | func Distance(a, b *T) float64 {
253 | d := Sub(a, b)
254 | return d.Length()
255 | }
256 |
257 | // Add adds the composants of the two vectors and returns a new vector with the sum of the two vectors.
258 | func Add(a, b *T) T {
259 | return T{a[0] + b[0], a[1] + b[1], a[2] + b[2]}
260 | }
261 |
262 | // Sub returns the difference of two vectors.
263 | func Sub(a, b *T) T {
264 | return T{a[0] - b[0], a[1] - b[1], a[2] - b[2]}
265 | }
266 |
267 | // Mul returns the component wise product of two vectors.
268 | func Mul(a, b *T) T {
269 | return T{a[0] * b[0], a[1] * b[1], a[2] * b[2]}
270 | }
271 |
272 | // Dot returns the dot product of two vectors.
273 | func Dot(a, b *T) float64 {
274 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
275 | }
276 |
277 | // Cross returns the cross product of two vectors.
278 | func Cross(a, b *T) T {
279 | return T{
280 | a[1]*b[2] - a[2]*b[1],
281 | a[2]*b[0] - a[0]*b[2],
282 | a[0]*b[1] - a[1]*b[0],
283 | }
284 | }
285 |
286 | // Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b.
287 | // The returned sine value is in the range 0.0 ≤ value ≤ 1.0.
288 | // The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive.
289 | func Sinus(a, b *T) float64 {
290 | lenSqrProduct := a.LengthSqr() * b.LengthSqr()
291 | if lenSqrProduct < 1e-20 {
292 | return 0.0
293 | }
294 |
295 | cross := Cross(a, b)
296 | v := cross.Length() / math.Sqrt(lenSqrProduct)
297 |
298 | if v > 1.0 {
299 | return 1.0
300 | } else if v < 0.0 {
301 | return 0.0
302 | }
303 | return v
304 | }
305 |
306 | // Cosine returns the cosine value of the angle between the two vectors.
307 | // The returned cosine value is in the range -1.0 ≤ value ≤ 1.0.
308 | func Cosine(a, b *T) float64 {
309 | lenSqrProduct := a.LengthSqr() * b.LengthSqr()
310 | if lenSqrProduct < 1e-20 {
311 | return 0.0
312 | }
313 |
314 | v := Dot(a, b) / math.Sqrt(lenSqrProduct)
315 |
316 | if v > 1.0 {
317 | return 1.0
318 | } else if v < -1.0 {
319 | return -1.0
320 | }
321 | return v
322 | }
323 |
324 | // Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b.
325 | // The returned value is in the range 0 ≤ angle ≤ Pi radians.
326 | func Angle(a, b *T) float64 {
327 | return math.Acos(Cosine(a, b))
328 | }
329 |
330 | // Min returns the component wise minimum of two vectors.
331 | func Min(a, b *T) T {
332 | min := *a
333 | if b[0] < min[0] {
334 | min[0] = b[0]
335 | }
336 | if b[1] < min[1] {
337 | min[1] = b[1]
338 | }
339 | if b[2] < min[2] {
340 | min[2] = b[2]
341 | }
342 | return min
343 | }
344 |
345 | // Max returns the component wise maximum of two vectors.
346 | func Max(a, b *T) T {
347 | max := *a
348 | if b[0] > max[0] {
349 | max[0] = b[0]
350 | }
351 | if b[1] > max[1] {
352 | max[1] = b[1]
353 | }
354 | if b[2] > max[2] {
355 | max[2] = b[2]
356 | }
357 | return max
358 | }
359 |
360 | // Interpolate interpolates between a and b at t (0,1).
361 | func Interpolate(a, b *T, t float64) T {
362 | t1 := 1 - t
363 | return T{
364 | a[0]*t1 + b[0]*t,
365 | a[1]*t1 + b[1]*t,
366 | a[2]*t1 + b[2]*t,
367 | }
368 | }
369 |
370 | // Clamp clamps the vector's components to be in the range of min to max.
371 | func (vec *T) Clamp(min, max *T) *T {
372 | for i := range vec {
373 | if vec[i] < min[i] {
374 | vec[i] = min[i]
375 | } else if vec[i] > max[i] {
376 | vec[i] = max[i]
377 | }
378 | }
379 | return vec
380 | }
381 |
382 | // Clamped returns a copy of the vector with the components clamped to be in the range of min to max.
383 | func (vec *T) Clamped(min, max *T) T {
384 | result := *vec
385 | result.Clamp(min, max)
386 | return result
387 | }
388 |
389 | // Clamp01 clamps the vector's components to be in the range of 0 to 1.
390 | func (vec *T) Clamp01() *T {
391 | return vec.Clamp(&Zero, &UnitXYZ)
392 | }
393 |
394 | // Clamped01 returns a copy of the vector with the components clamped to be in the range of 0 to 1.
395 | func (vec *T) Clamped01() T {
396 | result := *vec
397 | result.Clamp01()
398 | return result
399 | }
400 |
--------------------------------------------------------------------------------
/vec3/vec3.go:
--------------------------------------------------------------------------------
1 | // Package vec3 contains a 3D float32 vector type T and functions.
2 | package vec3
3 |
4 | import (
5 | "fmt"
6 |
7 | math "github.com/chewxy/math32"
8 | "github.com/ungerik/go3d/generic"
9 | )
10 |
11 | var (
12 | // Zero holds a zero vector.
13 | Zero = T{}
14 |
15 | // UnitX holds a vector with X set to one.
16 | UnitX = T{1, 0, 0}
17 | // UnitY holds a vector with Y set to one.
18 | UnitY = T{0, 1, 0}
19 | // UnitZ holds a vector with Z set to one.
20 | UnitZ = T{0, 0, 1}
21 | // UnitXYZ holds a vector with X, Y, Z set to one.
22 | UnitXYZ = T{1, 1, 1}
23 |
24 | // Red holds the color red.
25 | Red = T{1, 0, 0}
26 | // Green holds the color green.
27 | Green = T{0, 1, 0}
28 | // Blue holds the color blue.
29 | Blue = T{0, 0, 1}
30 | // Black holds the color black.
31 | Black = T{0, 0, 0}
32 | // White holds the color white.
33 | White = T{1, 1, 1}
34 |
35 | // MinVal holds a vector with the smallest possible component values.
36 | MinVal = T{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32}
37 | // MaxVal holds a vector with the highest possible component values.
38 | MaxVal = T{+math.MaxFloat32, +math.MaxFloat32, +math.MaxFloat32}
39 | )
40 |
41 | // T represents a 3D vector.
42 | type T [3]float32
43 |
44 | // From copies a T from a generic.T implementation.
45 | func From(other generic.T) T {
46 | switch other.Size() {
47 | case 2:
48 | return T{other.Get(0, 0), other.Get(0, 1), 0}
49 | case 3, 4:
50 | return T{other.Get(0, 0), other.Get(0, 1), other.Get(0, 2)}
51 | default:
52 | panic("Unsupported type")
53 | }
54 | }
55 |
56 | // Parse parses T from a string. See also String()
57 | func Parse(s string) (r T, err error) {
58 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2])
59 | return r, err
60 | }
61 |
62 | // String formats T as string. See also Parse().
63 | func (vec *T) String() string {
64 | return fmt.Sprint(vec[0], vec[1], vec[2])
65 | }
66 |
67 | // Rows returns the number of rows of the vector.
68 | func (vec *T) Rows() int {
69 | return 3
70 | }
71 |
72 | // Cols returns the number of columns of the vector.
73 | func (vec *T) Cols() int {
74 | return 1
75 | }
76 |
77 | // Size returns the number elements of the vector.
78 | func (vec *T) Size() int {
79 | return 3
80 | }
81 |
82 | // Slice returns the elements of the vector as slice.
83 | func (vec *T) Slice() []float32 {
84 | return vec[:]
85 | }
86 |
87 | // Get returns one element of the vector.
88 | func (vec *T) Get(col, row int) float32 {
89 | return vec[row]
90 | }
91 |
92 | // IsZero checks if all elements of the vector are exactly zero.
93 | // Uses exact equality comparison, which may not be suitable for floating-point math results.
94 | // For tolerance-based comparison, use IsZeroEps instead.
95 | func (vec *T) IsZero() bool {
96 | return vec[0] == 0 && vec[1] == 0 && vec[2] == 0
97 | }
98 |
99 | // IsZeroEps checks if all elements of the vector are zero within the given epsilon tolerance.
100 | // This is the recommended method for comparing floating-point vectors that result from calculations.
101 | // For exact zero comparison, use IsZero instead.
102 | func (vec *T) IsZeroEps(epsilon float32) bool {
103 | return math.Abs(vec[0]) <= epsilon && math.Abs(vec[1]) <= epsilon && math.Abs(vec[2]) <= epsilon
104 | }
105 |
106 | // Length returns the length of the vector.
107 | // See also LengthSqr and Normalize.
108 | func (vec *T) Length() float32 {
109 | return float32(math.Sqrt(vec.LengthSqr()))
110 | }
111 |
112 | // LengthSqr returns the squared length of the vector.
113 | // See also Length and Normalize.
114 | func (vec *T) LengthSqr() float32 {
115 | return vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2]
116 | }
117 |
118 | // Scale multiplies all element of the vector by f and returns vec.
119 | func (vec *T) Scale(f float32) *T {
120 | vec[0] *= f
121 | vec[1] *= f
122 | vec[2] *= f
123 | return vec
124 | }
125 |
126 | // Scaled returns a copy of vec with all elements multiplies by f.
127 | func (vec *T) Scaled(f float32) T {
128 | return T{vec[0] * f, vec[1] * f, vec[2] * f}
129 | }
130 |
131 | // PracticallyEquals compares two vectors if they are equal with each other within a delta tolerance.
132 | func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float32) bool {
133 | return (math.Abs(vec[0]-compareVector[0]) <= allowedDelta) &&
134 | (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) &&
135 | (math.Abs(vec[2]-compareVector[2]) <= allowedDelta)
136 | }
137 |
138 | // PracticallyEquals compares two values if they are equal with each other within a delta tolerance.
139 | func PracticallyEquals(v1, v2, allowedDelta float32) bool {
140 | return math.Abs(v1-v2) <= allowedDelta
141 | }
142 |
143 | // Invert inverts the vector.
144 | func (vec *T) Invert() *T {
145 | vec[0] = -vec[0]
146 | vec[1] = -vec[1]
147 | vec[2] = -vec[2]
148 | return vec
149 | }
150 |
151 | // Inverted returns an inverted copy of the vector.
152 | func (vec *T) Inverted() T {
153 | return T{-vec[0], -vec[1], -vec[2]}
154 | }
155 |
156 | // Abs sets every component of the vector to its absolute value.
157 | func (vec *T) Abs() *T {
158 | vec[0] = math.Abs(vec[0])
159 | vec[1] = math.Abs(vec[1])
160 | vec[2] = math.Abs(vec[2])
161 | return vec
162 | }
163 |
164 | // Absed returns a copy of the vector containing the absolute values.
165 | func (vec *T) Absed() T {
166 | return T{math.Abs(vec[0]), math.Abs(vec[1]), math.Abs(vec[2])}
167 | }
168 |
169 | // Normalize normalizes the vector to unit length.
170 | // Uses the package Epsilon variable for numerical stability:
171 | // - Vectors with squared length < Epsilon are considered zero and left unchanged
172 | // - Vectors with squared length within Epsilon of 1.0 are considered already normalized
173 | func (vec *T) Normalize() *T {
174 | sl := vec.LengthSqr()
175 | if sl < Epsilon {
176 | // Vector is effectively zero
177 | return vec
178 | }
179 | if math.Abs(sl-1) < Epsilon {
180 | // Vector is already normalized
181 | return vec
182 | }
183 | return vec.Scale(1 / math.Sqrt(sl))
184 | }
185 |
186 | // Normalized returns a unit length normalized copy of the vector.
187 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details.
188 | func (vec *T) Normalized() T {
189 | v := *vec
190 | v.Normalize()
191 | return v
192 | }
193 |
194 | // Normal returns an orthogonal vector.
195 | // Uses the package Epsilon variable when checking if the cross product is zero,
196 | // which provides numerical stability when the input vector is parallel to UnitZ.
197 | // Falls back to UnitX when the cross product is effectively zero.
198 | func (vec *T) Normal() T {
199 | n := Cross(vec, &UnitZ)
200 | if n.IsZeroEps(Epsilon) {
201 | return UnitX
202 | }
203 | return n.Normalized()
204 | }
205 |
206 | // Add adds another vector to vec.
207 | func (vec *T) Add(v *T) *T {
208 | vec[0] += v[0]
209 | vec[1] += v[1]
210 | vec[2] += v[2]
211 | return vec
212 | }
213 |
214 | // Added adds another vector to vec and returns a copy of the result
215 | func (vec *T) Added(v *T) T {
216 | return T{vec[0] + v[0], vec[1] + v[1], vec[2] + v[2]}
217 | }
218 |
219 | // Sub subtracts another vector from vec.
220 | func (vec *T) Sub(v *T) *T {
221 | vec[0] -= v[0]
222 | vec[1] -= v[1]
223 | vec[2] -= v[2]
224 | return vec
225 | }
226 |
227 | // Subed subtracts another vector from vec and returns a copy of the result
228 | func (vec *T) Subed(v *T) T {
229 | return T{vec[0] - v[0], vec[1] - v[1], vec[2] - v[2]}
230 | }
231 |
232 | // Mul multiplies the components of the vector with the respective components of v.
233 | func (vec *T) Mul(v *T) *T {
234 | vec[0] *= v[0]
235 | vec[1] *= v[1]
236 | vec[2] *= v[2]
237 | return vec
238 | }
239 |
240 | // Muled multiplies the components of the vector with the respective components of v and returns a copy of the result
241 | func (vec *T) Muled(v *T) T {
242 | return T{vec[0] * v[0], vec[1] * v[1], vec[2] * v[2]}
243 | }
244 |
245 | // SquareDistance the distance between two vectors squared (= distance*distance)
246 | func SquareDistance(a, b *T) float32 {
247 | d := Sub(a, b)
248 | return d.LengthSqr()
249 | }
250 |
251 | // Distance the distance between two vectors
252 | func Distance(a, b *T) float32 {
253 | d := Sub(a, b)
254 | return d.Length()
255 | }
256 |
257 | // Add adds the composants of the two vectors and returns a new vector with the sum of the two vectors.
258 | func Add(a, b *T) T {
259 | return T{a[0] + b[0], a[1] + b[1], a[2] + b[2]}
260 | }
261 |
262 | // Sub returns the difference of two vectors.
263 | func Sub(a, b *T) T {
264 | return T{a[0] - b[0], a[1] - b[1], a[2] - b[2]}
265 | }
266 |
267 | // Mul returns the component wise product of two vectors.
268 | func Mul(a, b *T) T {
269 | return T{a[0] * b[0], a[1] * b[1], a[2] * b[2]}
270 | }
271 |
272 | // Dot returns the dot product of two vectors.
273 | func Dot(a, b *T) float32 {
274 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
275 | }
276 |
277 | // Cross returns the cross product of two vectors.
278 | func Cross(a, b *T) T {
279 | return T{
280 | a[1]*b[2] - a[2]*b[1],
281 | a[2]*b[0] - a[0]*b[2],
282 | a[0]*b[1] - a[1]*b[0],
283 | }
284 | }
285 |
286 | // Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b.
287 | // The returned sine value is in the range 0.0 ≤ value ≤ 1.0.
288 | // The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive.
289 | func Sinus(a, b *T) float32 {
290 | lenSqrProduct := a.LengthSqr() * b.LengthSqr()
291 | if lenSqrProduct < 1e-10 {
292 | return 0.0
293 | }
294 |
295 | cross := Cross(a, b)
296 | v := cross.Length() / math.Sqrt(lenSqrProduct)
297 |
298 | if v > 1.0 {
299 | return 1.0
300 | } else if v < 0.0 {
301 | return 0.0
302 | }
303 | return v
304 | }
305 |
306 | // Cosine returns the cosine value of the angle between the two vectors.
307 | // The returned cosine value is in the range -1.0 ≤ value ≤ 1.0.
308 | func Cosine(a, b *T) float32 {
309 | lenSqrProduct := a.LengthSqr() * b.LengthSqr()
310 | if lenSqrProduct < 1e-10 {
311 | return 0.0
312 | }
313 |
314 | v := Dot(a, b) / math.Sqrt(lenSqrProduct)
315 |
316 | if v > 1.0 {
317 | return 1.0
318 | } else if v < -1.0 {
319 | return -1.0
320 | }
321 | return v
322 | }
323 |
324 | // Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b.
325 | // The returned value is in the range 0 ≤ angle ≤ Pi radians.
326 | func Angle(a, b *T) float32 {
327 | return math.Acos(Cosine(a, b))
328 | }
329 |
330 | // Min returns the component wise minimum of two vectors.
331 | func Min(a, b *T) T {
332 | min := *a
333 | if b[0] < min[0] {
334 | min[0] = b[0]
335 | }
336 | if b[1] < min[1] {
337 | min[1] = b[1]
338 | }
339 | if b[2] < min[2] {
340 | min[2] = b[2]
341 | }
342 | return min
343 | }
344 |
345 | // Max returns the component wise maximum of two vectors.
346 | func Max(a, b *T) T {
347 | max := *a
348 | if b[0] > max[0] {
349 | max[0] = b[0]
350 | }
351 | if b[1] > max[1] {
352 | max[1] = b[1]
353 | }
354 | if b[2] > max[2] {
355 | max[2] = b[2]
356 | }
357 | return max
358 | }
359 |
360 | // Interpolate interpolates between a and b at t (0,1).
361 | func Interpolate(a, b *T, t float32) T {
362 | t1 := 1 - t
363 | return T{
364 | a[0]*t1 + b[0]*t,
365 | a[1]*t1 + b[1]*t,
366 | a[2]*t1 + b[2]*t,
367 | }
368 | }
369 |
370 | // Clamp clamps the vector's components to be in the range of min to max.
371 | func (vec *T) Clamp(min, max *T) *T {
372 | for i := range vec {
373 | if vec[i] < min[i] {
374 | vec[i] = min[i]
375 | } else if vec[i] > max[i] {
376 | vec[i] = max[i]
377 | }
378 | }
379 | return vec
380 | }
381 |
382 | // Clamped returns a copy of the vector with the components clamped to be in the range of min to max.
383 | func (vec *T) Clamped(min, max *T) T {
384 | result := *vec
385 | result.Clamp(min, max)
386 | return result
387 | }
388 |
389 | // Clamp01 clamps the vector's components to be in the range of 0 to 1.
390 | func (vec *T) Clamp01() *T {
391 | return vec.Clamp(&Zero, &UnitXYZ)
392 | }
393 |
394 | // Clamped01 returns a copy of the vector with the components clamped to be in the range of 0 to 1.
395 | func (vec *T) Clamped01() T {
396 | result := *vec
397 | result.Clamp01()
398 | return result
399 | }
400 |
--------------------------------------------------------------------------------
/float64/vec2/vec2_test.go:
--------------------------------------------------------------------------------
1 | package vec2
2 |
3 | import (
4 | "math"
5 | "strconv"
6 | "testing"
7 | )
8 |
9 | func TestAbs(t *testing.T) {
10 | v1 := T{1.5, -2.6}
11 |
12 | expectedV1 := T{1.5, 2.6}
13 | expectedV2 := T{1.5, 2.6}
14 |
15 | v2 := v1.Abs()
16 |
17 | if v1 != expectedV1 {
18 | t.Fail()
19 | }
20 | if *v2 != expectedV2 {
21 | t.Fail()
22 | }
23 | }
24 |
25 | func TestAbsed(t *testing.T) {
26 | v1 := T{1.5, -2.6}
27 |
28 | expectedV1 := T{1.5, -2.6}
29 | expectedV2 := T{1.5, 2.6}
30 |
31 | v2 := v1.Absed()
32 |
33 | if v1 != expectedV1 {
34 | t.Fail()
35 | }
36 | if v2 != expectedV2 {
37 | t.Fail()
38 | }
39 | }
40 |
41 | func TestNormal(t *testing.T) {
42 | v1 := T{1.0, 1.0}
43 |
44 | v1Length := math.Sqrt(1*1 + 1*1)
45 | expectedV1n := T{1 / v1Length, -1 / v1Length}
46 |
47 | v1n := v1.Normal()
48 |
49 | if v1n != expectedV1n {
50 | t.Fail()
51 | }
52 | }
53 |
54 | func TestNormal2(t *testing.T) {
55 | v1 := T{4, 6}
56 |
57 | v1Length := math.Sqrt(4*4 + 6*6)
58 | expectedV1n := T{6.0 / v1Length, -4 / v1Length}
59 |
60 | v1n := v1.Normal()
61 |
62 | if v1n != expectedV1n {
63 | t.Fail()
64 | }
65 | }
66 |
67 | func TestNormalCCW(t *testing.T) {
68 | v1 := T{1.0, 1.0}
69 |
70 | v1Length := math.Sqrt(1*1 + 1*1)
71 | expectedV1n := T{-1 / v1Length, 1 / v1Length}
72 |
73 | v1n := v1.NormalCCW()
74 |
75 | if v1n != expectedV1n {
76 | t.Fail()
77 | }
78 | }
79 |
80 | func TestNormalCCW2(t *testing.T) {
81 | v1 := T{4, 6}
82 |
83 | v1Length := math.Sqrt(4*4 + 6*6)
84 | expectedV1n := T{-6.0 / v1Length, 4 / v1Length}
85 |
86 | v1n := v1.NormalCCW()
87 |
88 | if v1n != expectedV1n {
89 | t.Fail()
90 | }
91 | }
92 |
93 | func TestAdd(t *testing.T) {
94 | v1 := T{1, 2}
95 | v2 := T{9, 8}
96 |
97 | expectedV1 := T{10, 10}
98 | expectedV2 := T{9, 8}
99 | expectedV3 := T{10, 10}
100 |
101 | v3 := v1.Add(&v2)
102 |
103 | if v1 != expectedV1 {
104 | t.Fail()
105 | }
106 | if v2 != expectedV2 {
107 | t.Fail()
108 | }
109 | if *v3 != expectedV3 {
110 | t.Fail()
111 | }
112 | }
113 |
114 | func TestAdded(t *testing.T) {
115 | v1 := T{1, 2}
116 | v2 := T{9, 8}
117 |
118 | expectedV1 := T{1, 2}
119 | expectedV2 := T{9, 8}
120 | expectedV3 := T{10, 10}
121 |
122 | v3 := v1.Added(&v2)
123 |
124 | if v1 != expectedV1 {
125 | t.Fail()
126 | }
127 | if v2 != expectedV2 {
128 | t.Fail()
129 | }
130 | if v3 != expectedV3 {
131 | t.Fail()
132 | }
133 | }
134 |
135 | func TestSub(t *testing.T) {
136 | v1 := T{1, 2}
137 | v2 := T{9, 8}
138 |
139 | expectedV1 := T{-8, -6}
140 | expectedV2 := T{9, 8}
141 | expectedV3 := T{-8, -6}
142 |
143 | v3 := v1.Sub(&v2)
144 |
145 | if v1 != expectedV1 {
146 | t.Fail()
147 | }
148 | if v2 != expectedV2 {
149 | t.Fail()
150 | }
151 | if *v3 != expectedV3 {
152 | t.Fail()
153 | }
154 | }
155 |
156 | func TestSubed(t *testing.T) {
157 | v1 := T{1, 2}
158 | v2 := T{9, 8}
159 |
160 | expectedV1 := T{1, 2}
161 | expectedV2 := T{9, 8}
162 | expectedV3 := T{-8, -6}
163 |
164 | v3 := v1.Subed(&v2)
165 |
166 | if v1 != expectedV1 {
167 | t.Fail()
168 | }
169 | if v2 != expectedV2 {
170 | t.Fail()
171 | }
172 | if v3 != expectedV3 {
173 | t.Fail()
174 | }
175 | }
176 |
177 | func TestMul(t *testing.T) {
178 | v1 := T{1, 2}
179 | v2 := T{9, 8}
180 |
181 | expectedV1 := T{9, 16}
182 | expectedV2 := T{9, 8}
183 | expectedV3 := T{9, 16}
184 |
185 | v3 := v1.Mul(&v2)
186 |
187 | if v1 != expectedV1 {
188 | t.Fail()
189 | }
190 | if v2 != expectedV2 {
191 | t.Fail()
192 | }
193 | if *v3 != expectedV3 {
194 | t.Fail()
195 | }
196 | }
197 |
198 | func TestMuled(t *testing.T) {
199 | v1 := T{1, 2}
200 | v2 := T{9, 8}
201 |
202 | expectedV1 := T{1, 2}
203 | expectedV2 := T{9, 8}
204 | expectedV3 := T{9, 16}
205 |
206 | v3 := v1.Muled(&v2)
207 |
208 | if v1 != expectedV1 {
209 | t.Fail()
210 | }
211 | if v2 != expectedV2 {
212 | t.Fail()
213 | }
214 | if v3 != expectedV3 {
215 | t.Fail()
216 | }
217 | }
218 |
219 | func TestAngle(t *testing.T) {
220 | radFor45deg := math.Pi / 4.0
221 | testSetups := []struct {
222 | a, b T
223 | expectedAngle float64
224 | name string
225 | }{
226 | {a: T{1, 0}, b: T{1, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"},
227 | {a: T{1, 0}, b: T{1, 1}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"},
228 | {a: T{1, 0}, b: T{0, 1}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"},
229 | {a: T{1, 0}, b: T{-1, 1}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"},
230 | {a: T{1, 0}, b: T{-1, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"},
231 | {a: T{1, 0}, b: T{-1, -1}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"},
232 | {a: T{1, 0}, b: T{0, -1}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"},
233 | {a: T{1, 0}, b: T{1, -1}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"},
234 | }
235 |
236 | for _, testSetup := range testSetups {
237 | t.Run(testSetup.name, func(t *testing.T) {
238 | angle := Angle(&testSetup.a, &testSetup.b)
239 |
240 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) {
241 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name)
242 | }
243 | })
244 | }
245 | }
246 |
247 | func TestCosine(t *testing.T) {
248 | radFor45deg := math.Pi / 4.0
249 | testSetups := []struct {
250 | a, b T
251 | expectedCosine float64
252 | name string
253 | }{
254 | {a: T{1, 0}, b: T{1, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
255 | {a: T{1, 0}, b: T{1, 1}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"},
256 | {a: T{1, 0}, b: T{0, 1}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
257 | {a: T{1, 0}, b: T{-1, 1}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"},
258 | {a: T{1, 0}, b: T{-1, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
259 | {a: T{1, 0}, b: T{-1, -1}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"},
260 | {a: T{1, 0}, b: T{0, -1}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"},
261 | {a: T{1, 0}, b: T{1, -1}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"},
262 | }
263 |
264 | for _, testSetup := range testSetups {
265 | t.Run(testSetup.name, func(t *testing.T) {
266 | cos := Cosine(&testSetup.a, &testSetup.b)
267 |
268 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) {
269 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name)
270 | }
271 | })
272 | }
273 | }
274 |
275 | func TestSinus(t *testing.T) {
276 | radFor45deg := math.Pi / 4.0
277 | testSetups := []struct {
278 | a, b T
279 | expectedSine float64
280 | name string
281 | }{
282 | {a: T{1, 0}, b: T{1, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"},
283 | {a: T{1, 0}, b: T{1, 1}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"},
284 | {a: T{1, 0}, b: T{0, 1}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"},
285 | {a: T{1, 0}, b: T{-1, 1}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"},
286 | {a: T{1, 0}, b: T{-1, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"},
287 | {a: T{1, 0}, b: T{-1, -1}, expectedSine: math.Sin(5 * radFor45deg), name: "225 degree angle"},
288 | {a: T{1, 0}, b: T{0, -1}, expectedSine: math.Sin(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"},
289 | {a: T{1, 0}, b: T{1, -1}, expectedSine: math.Sin(7 * radFor45deg), name: "315 degree angle"},
290 | }
291 |
292 | for _, testSetup := range testSetups {
293 | t.Run(testSetup.name, func(t *testing.T) {
294 | sin := Sinus(&testSetup.a, &testSetup.b)
295 |
296 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) {
297 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name)
298 | }
299 | })
300 | }
301 | }
302 |
303 | func TestLeftRightWinding(t *testing.T) {
304 | a := T{1.0, 0.0}
305 |
306 | for angle := 0; angle <= 360; angle += 15 {
307 | rad := (math.Pi / 180.0) * float64(angle)
308 |
309 | bx := clampDecimals(math.Cos(rad), 4)
310 | by := clampDecimals(math.Sin(rad), 4)
311 | b := T{bx, by}
312 |
313 | t.Run("left winding angle "+strconv.Itoa(angle), func(t *testing.T) {
314 | lw := IsLeftWinding(&a, &b)
315 | rw := IsRightWinding(&a, &b)
316 |
317 | if angle%180 == 0 {
318 | // No winding at 0, 180 and 360 degrees
319 | if lw || rw {
320 | t.Errorf("Neither left or right winding should be true on angle %d. Left winding=%t, right winding=%t", angle, lw, rw)
321 | }
322 | } else if angle < 180 {
323 | // Left winding at 0 < angle < 180
324 | if !lw || rw {
325 | t.Errorf("Left winding should be true (not right winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw)
326 | }
327 | } else if angle > 180 {
328 | // Right winding at 180 < angle < 360
329 | if lw || !rw {
330 | t.Errorf("Right winding should be true (not left winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw)
331 | }
332 | }
333 | })
334 | }
335 | }
336 |
337 | func clampDecimals(decimalValue float64, amountDecimals float64) float64 {
338 | factor := math.Pow(10, amountDecimals)
339 | return math.Round(decimalValue*factor) / factor
340 | }
341 |
342 | func TestIsZeroEps(t *testing.T) {
343 | tests := []struct {
344 | name string
345 | vec T
346 | epsilon float64
347 | want bool
348 | }{
349 | {"exact zero", T{0, 0}, 0.0001, true},
350 | {"within epsilon", T{0.00001, -0.00001}, 0.0001, true},
351 | {"at epsilon boundary", T{0.0001, 0.0001}, 0.0001, true},
352 | {"outside epsilon", T{0.001, 0}, 0.0001, false},
353 | {"one component outside", T{0.00001, 0.001}, 0.0001, false},
354 | {"negative outside epsilon", T{-0.001, 0}, 0.0001, false},
355 | {"large values", T{1.0, 2.0}, 0.0001, false},
356 | }
357 |
358 | for _, tt := range tests {
359 | t.Run(tt.name, func(t *testing.T) {
360 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want {
361 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon)
362 | }
363 | })
364 | }
365 | }
366 |
367 | func TestNormalizeEdgeCases(t *testing.T) {
368 | tests := []struct {
369 | name string
370 | vec T
371 | checkLength bool
372 | }{
373 | {"zero vector", T{0, 0}, false},
374 | {"tiny vector (below epsilon)", T{1e-16, 1e-16}, false},
375 | {"already normalized", T{1, 0}, true},
376 | {"nearly normalized positive deviation", T{1.00000000001, 0}, true},
377 | {"nearly normalized negative deviation", T{0.99999999999, 0}, true},
378 | {"nearly normalized mixed", T{0.7071067811865476, 0.7071067811865475}, true}, // ~sqrt(2)/2 components
379 | {"needs normalization", T{3, 4}, true},
380 | {"negative components", T{-3, -4}, true},
381 | }
382 |
383 | for _, tt := range tests {
384 | t.Run(tt.name, func(t *testing.T) {
385 | original := tt.vec
386 | originalLengthSqr := original.LengthSqr()
387 | result := tt.vec.Normalize()
388 |
389 | if result != &tt.vec {
390 | t.Errorf("Normalize() should return pointer to vec")
391 | }
392 |
393 | if tt.checkLength {
394 | length := tt.vec.Length()
395 | if !PracticallyEquals(length, 1.0, 0.00001) {
396 | t.Errorf("After Normalize(), Length() = %v, want 1.0 (original lengthSqr=%v)", length, originalLengthSqr)
397 | }
398 | }
399 | })
400 | }
401 | }
402 |
403 | func TestNormalizedEdgeCases(t *testing.T) {
404 | tests := []struct {
405 | name string
406 | vec T
407 | checkLength bool
408 | }{
409 | {"zero vector", T{0, 0}, false},
410 | {"tiny vector", T{1e-16, 1e-16}, false},
411 | {"already normalized", T{1, 0}, true},
412 | {"needs normalization", T{3, 4}, true},
413 | }
414 |
415 | for _, tt := range tests {
416 | t.Run(tt.name, func(t *testing.T) {
417 | original := tt.vec
418 | result := tt.vec.Normalized()
419 |
420 | if tt.vec != original {
421 | t.Errorf("Normalized() modified original vector")
422 | }
423 |
424 | if tt.checkLength {
425 | length := result.Length()
426 | if !PracticallyEquals(length, 1.0, 0.0001) {
427 | t.Errorf("Normalized().Length() = %v, want 1.0", length)
428 | }
429 | }
430 | })
431 | }
432 | }
433 |
--------------------------------------------------------------------------------