Vector float64 (#565)

* Fixed nil pointer in Copy()

* Position added

Added Floor() and String() methods to Vector.

Also added Position which declares an embedded Vector2 and returns various forms of it.

* d2vector.Vector2 renamed to d2vector.BigFloat

* vector.go renamed to big_float.go

* Float64 stub and more renaming

* Vector value getters

* Separate vector types with initial methods.

* Divide and lint warnings.

* Distance and Length.

* Scale, Abs and Negate.

* CompareFloat64Fuzzy delta direction reversed.

* Refactor vector_test.go.

* Renamed Approx methods.

* Distance and Length.

* Distance and Length.

* Removed BigFloat and Vector, renamed Float64 to Vector, simplified tests.

* Angle, SignedAngle and other small functions.

* Receiver rename.

* SingedAngle and test fixed

* Rotate.

* SetLength.

* Cross.

* NinetyAnti and NinetyClock.

* Lerp and Clamp.

* Reflect and ReflectSurface.

* Cardinal convenience functions.

* Comments.

* Panic on NaN and Inf in Position.

* Lint warnings and comments.
This commit is contained in:
danhale-git 2020-07-09 13:30:55 +01:00 committed by GitHub
parent e2e8a303c2
commit 029cb62972
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 960 additions and 471 deletions

View File

@ -1,45 +0,0 @@
package d2interface
import "math/big"
// Vector is a 2-dimensional vector implementation using big.Float
type Vector interface {
X() *big.Float
Y() *big.Float
Marshal() ([]byte, error)
Unmarshal(buf []byte) error
Clone() Vector
Copy(src Vector) Vector
// SetFromEntity(entity WorldEntity) Vector
Set(x, y *big.Float) Vector
SetToPolar(azimuth, radius *big.Float) Vector
Equals(src Vector) bool
FuzzyEquals(src Vector) bool
Abs() Vector
Angle() *big.Float
SetAngle(angle *big.Float) Vector
Add(src Vector) Vector
Subtract(src Vector) Vector
Multiply(src Vector) Vector
Scale(value *big.Float) Vector
Divide(src Vector) Vector
Negate() Vector
Distance(src Vector) *big.Float
DistanceSq(src Vector) *big.Float
Length() *big.Float
SetLength(length *big.Float) Vector
LengthSq() (*big.Float, *big.Float)
Normalize() Vector
NormalizeRightHand() Vector
NormalizeLeftHand() Vector
Dot(src Vector) *big.Float
Cross(src Vector) *big.Float
Lerp(src Vector, t *big.Float) Vector
Reset() Vector
Limit(max *big.Float) Vector
Reflect(normal Vector) Vector
Mirror(axis Vector) Vector
Rotate(delta *big.Float) Vector
Floor() Vector
String() string
}

View File

@ -1,56 +1,72 @@
package d2vector
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
import (
"fmt"
"math"
)
// Position is a vector in world space. The stored
// value is the one returned by Position.World()
const subTilesPerTile float64 = 5
// Position is a vector in world space. The stored value is the one returned by Position.World()
type Position struct {
d2interface.Vector
Vector
}
// NewPosition creates a new Position at the given
// float64 world position.
// NewPosition creates a new Position at the given float64 world position.
func NewPosition(x, y float64) *Position {
return &Position{New(x, y)}
p := &Position{NewVector(x, y)}
p.checkValues()
return p
}
// World is the position, where 1 = one map
// tile.
func (p *Position) World() d2interface.Vector {
return p.Vector
// Set sets this position to the given x and y values.
func (p *Position) Set(x, y float64) {
p.x, p.y = x, y
p.checkValues()
}
// Tile is the tile position, always a whole
// number.
func (p *Position) Tile() d2interface.Vector {
func (p *Position) checkValues() {
if math.IsNaN(p.x) || math.IsNaN(p.y) {
panic(fmt.Sprintf("float value is NaN: %s", p.Vector))
}
if math.IsInf(p.x, 0) || math.IsInf(p.y, 0) {
panic(fmt.Sprintf("float value is Inf: %s", p.Vector))
}
}
// World is the position, where 1 = one map tile.
func (p *Position) World() *Vector {
return &p.Vector
}
// Tile is the tile position, always a whole number.
func (p *Position) Tile() *Vector {
c := p.World().Clone()
return c.Floor()
}
// TileOffset is the offset from the tile position,
// always < 1.
func (p *Position) TileOffset() d2interface.Vector {
// TileOffset is the offset from the tile position, always < 1.
func (p *Position) TileOffset() *Vector {
c := p.World().Clone()
return c.Subtract(p.Tile())
}
// SubWorld is the position, where 5 = one map
// tile.
func (p *Position) SubWorld() d2interface.Vector {
// SubWorld is the position, where 5 = one map tile.
func (p *Position) SubWorld() *Vector {
c := p.World().Clone()
return c.Multiply(New(5, 5))
return c.Scale(subTilesPerTile)
}
// SubTile is the tile position in sub tiles,
// always a multiple of 5.
func (p *Position) SubTile() d2interface.Vector {
// SubTile is the tile position in sub tiles, always a multiple of 5.
func (p *Position) SubTile() *Vector {
c := p.Tile().Clone()
return c.Multiply(New(5, 5))
return c.Scale(subTilesPerTile)
}
// SubTileOffset is the offset from the sub tile
// position in sub tiles, always < 1.
func (p *Position) SubTileOffset() d2interface.Vector {
// SubTileOffset is the offset from the sub tile position in sub tiles, always < 1.
func (p *Position) SubTileOffset() *Vector {
c := p.SubWorld().Clone()
return c.Subtract(p.SubTile())
}

View File

@ -2,16 +2,14 @@ package d2vector
import (
"testing"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
func validate(t *testing.T, original, got, want, unchanged d2interface.Vector) {
if !got.FuzzyEquals(want) {
func validate(t *testing.T, original, got, want, unchanged Vector) {
if !got.EqualsApprox(want) {
t.Errorf("want %s: got %s", want, got)
}
if !original.FuzzyEquals(unchanged) {
if !original.EqualsApprox(unchanged) {
t.Errorf("Position value %s was incorrectly changed to %s when calling this method", unchanged, original)
}
}
@ -19,44 +17,44 @@ func validate(t *testing.T, original, got, want, unchanged d2interface.Vector) {
func TestTile(t *testing.T) {
p := NewPosition(1.6, 1.6)
got := p.Tile()
want := New(1, 1)
unchanged := New(1.6, 1.6)
want := NewVector(1, 1)
unchanged := NewVector(1.6, 1.6)
validate(t, p, got, want, unchanged)
validate(t, p.Vector, *got, want, unchanged)
}
func TestTileOffset(t *testing.T) {
p := NewPosition(1.6, 1.6)
got := p.TileOffset()
want := New(0.6, 0.6)
unchanged := New(1.6, 1.6)
want := NewVector(0.6, 0.6)
unchanged := NewVector(1.6, 1.6)
validate(t, p, got, want, unchanged)
validate(t, p.Vector, *got, want, unchanged)
}
func TestSubWorld(t *testing.T) {
p := NewPosition(1, 1)
got := p.SubWorld()
want := New(5, 5)
unchanged := New(1, 1)
want := NewVector(5, 5)
unchanged := NewVector(1, 1)
validate(t, p, got, want, unchanged)
validate(t, p.Vector, *got, want, unchanged)
}
func TestSubTile(t *testing.T) {
p := NewPosition(1, 1)
got := p.SubTile()
want := New(5, 5)
unchanged := New(1, 1)
want := NewVector(5, 5)
unchanged := NewVector(1, 1)
validate(t, p, got, want, unchanged)
validate(t, p.Vector, *got, want, unchanged)
}
func TestSubTileOffset(t *testing.T) {
p := NewPosition(1.1, 1.1)
got := p.SubTileOffset()
want := New(0.5, 0.5)
unchanged := New(1.1, 1.1)
want := NewVector(0.5, 0.5)
unchanged := NewVector(1.1, 1.1)
validate(t, p, got, want, unchanged)
validate(t, p.Vector, *got, want, unchanged)
}

View File

@ -1,406 +1,312 @@
// Package d2vector is an Implementation of 2-dimensional vectors with big.Float components
// Package d2vector provides an implementation of a 2D Euclidean vector using float64 to store the two values.
package d2vector
import (
"fmt"
"math"
"math/big"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
const (
// Epsilon is the threshold for what is `smol enough`
epsilon float64 = 0.0001
// d2precision is how much precision we want from big.Float
d2precision uint = 64 // was chosen arbitrarily
// for convenience in negating sign
negative1 float64 = -1.0
// for convenience
zero float64 = 0.0
)
// New creates a new Vector2 and returns a pointer to it.
func New(x, y float64) *Vector2 {
xbf, ybf := big.NewFloat(x), big.NewFloat(y)
xbf.SetPrec(d2precision)
ybf.SetPrec(d2precision)
result := &Vector2{xbf, ybf}
return result
// Vector is an implementation of a Euclidean vector using float64 with common vector convenience methods.
type Vector struct {
x, y float64
}
// Vector2 has two big.Floats x and y and a set of methods
// for common vector operations.
type Vector2 struct {
x *big.Float
y *big.Float
const two float64 = 2
// NewVector creates a new Vector with the given x and y values.
func NewVector(x, y float64) Vector {
return Vector{x, y}
}
// X returns the x member of the Vector
func (v *Vector2) X() *big.Float {
return v.x
// Equals returns true if the float64 values of this vector are exactly equal to the given Vector.
func (v *Vector) Equals(o Vector) bool {
return v.x == o.x && v.y == o.y
}
// Y returns the y member of the Vector
func (v *Vector2) Y() *big.Float {
return v.y
// EqualsApprox returns true if the values of this Vector are approximately equal to those of the given Vector. If the
// difference between either of the value pairs is smaller than d2math.Epsilon, they will be considered equal.
func (v *Vector) EqualsApprox(o Vector) bool {
x, y := v.CompareApprox(o)
return x == 0 && y == 0
}
// Marshal converts the Vector into a slice of bytes
func (v *Vector2) Marshal() ([]byte, error) {
// TODO not sure how to do this properly
return nil, nil
// CompareApprox returns 2 ints describing the difference between the vectors. If the difference between either of the
// value pairs is smaller than d2math.Epsilon, they will be considered equal.
func (v *Vector) CompareApprox(o Vector) (x, y int) {
return d2math.CompareFloat64Fuzzy(v.x, o.x),
d2math.CompareFloat64Fuzzy(v.y, o.y)
}
// Unmarshal converts a slice of bytes to x/y *big.Float
// and assigns them to itself
func (v *Vector2) Unmarshal(buf []byte) error {
// TODO not sure how to do this properly
return nil
}
// Clone creates a copy of this Vector
func (v *Vector2) Clone() d2interface.Vector {
result := New(0, 0)
result.Copy(v)
return result
}
// Copy copies the src x/y members to this Vector x/y members
func (v *Vector2) Copy(src d2interface.Vector) d2interface.Vector {
v.x.Copy(src.X())
v.y.Copy(src.Y())
return v
}
// SetFromEntity copies the vector of a world entity
// func (v *Vector2) SetFromEntity(entity d2interface.WorldEntity) d2interface.Vector {
// return v.Copy(entity.Position())
// }
// Set the x,y members of the Vector
func (v *Vector2) Set(x, y *big.Float) d2interface.Vector {
// Set the vector values to the given float64 values.
func (v *Vector) Set(x, y float64) *Vector {
v.x = x
v.y = y
return v
}
// SetToPolar sets the `x` and `y` values of this object
// from a given polar coordinate.
func (v *Vector2) SetToPolar(azimuth, radius *big.Float) d2interface.Vector {
// HACK we should do this better, with the big.Float
a, _ := azimuth.Float64()
r, _ := radius.Float64()
v.x.SetFloat64(math.Cos(a) * r)
v.y.SetFloat64(math.Sin(a) * r)
return v
// Clone returns a new a copy of this Vector.
func (v *Vector) Clone() Vector {
return NewVector(v.x, v.y)
}
// Equals check whether this Vector is equal to a given Vector.
func (v *Vector2) Equals(src d2interface.Vector) bool {
return v.x.Cmp(src.X()) == 0 && v.y.Cmp(src.Y()) == 0
}
// FuzzyEquals checks if the Vector is approximately equal
// to the given Vector. epsilon is what we consider `smol enough`
func (v *Vector2) FuzzyEquals(src d2interface.Vector) bool {
smol := big.NewFloat(epsilon)
d := v.Distance(src)
d.Abs(d)
return d.Cmp(smol) < 1 || d.Cmp(smol) < 1
}
// Abs returns a clone that is positive
func (v *Vector2) Abs() d2interface.Vector {
clone := v.Clone()
neg1 := big.NewFloat(-1.0)
if clone.X().Sign() == -1 { // is negative1
clone.X().Mul(clone.X(), neg1)
}
if v.Y().Sign() == -1 { // is negative1
clone.Y().Mul(clone.Y(), neg1)
}
return clone
}
// Angle computes the angle in radians with respect
// to the positive x-axis
func (v *Vector2) Angle() *big.Float {
// HACK we should find a way to do this purely
// with big.Float
floatX, _ := v.X().Float64()
floatY, _ := v.Y().Float64()
floatAngle := math.Atan2(floatY, floatX)
if floatAngle < 0 {
floatAngle += 2.0 * math.Pi
}
return big.NewFloat(floatAngle)
}
// SetAngle sets the angle of this Vector
func (v *Vector2) SetAngle(angle *big.Float) d2interface.Vector {
return v.SetToPolar(angle, v.Length())
}
// Add to this Vector the components of the given Vector
func (v *Vector2) Add(src d2interface.Vector) d2interface.Vector {
v.x.Add(v.x, src.X())
v.y.Add(v.y, src.Y())
return v
}
// Subtract from this Vector the components of the given Vector
func (v *Vector2) Subtract(src d2interface.Vector) d2interface.Vector {
v.x.Sub(v.x, src.X())
v.y.Sub(v.y, src.Y())
return v
}
// Multiply this Vector with the components of the given Vector
func (v *Vector2) Multiply(src d2interface.Vector) d2interface.Vector {
v.x.Mul(v.x, src.X())
v.y.Mul(v.y, src.Y())
return v
}
// Scale this Vector by the given value
func (v *Vector2) Scale(s *big.Float) d2interface.Vector {
v.x.Sub(v.x, s)
v.y.Sub(v.y, s)
return v
}
// Divide this Vector by the given Vector
func (v *Vector2) Divide(src d2interface.Vector) d2interface.Vector {
v.x.Quo(v.x, src.X())
v.y.Quo(v.y, src.Y())
return v
}
// Negate thex and y components of this Vector
func (v *Vector2) Negate() d2interface.Vector {
return v.Scale(big.NewFloat(negative1))
}
// Distance calculate the distance between this Vector and the given Vector
func (v *Vector2) Distance(src d2interface.Vector) *big.Float {
dist := v.DistanceSq(src)
return dist.Sqrt(dist)
}
// DistanceSq calculate the distance suared between this Vector and the given
// Vector
func (v *Vector2) DistanceSq(src d2interface.Vector) *big.Float {
delta := src.Clone().Subtract(v)
deltaSq := delta.Multiply(delta)
return big.NewFloat(zero).Add(deltaSq.X(), deltaSq.Y())
}
// Length returns the length of this Vector
func (v *Vector2) Length() *big.Float {
xsq, ysq := v.LengthSq()
return xsq.Add(xsq, ysq)
}
// LengthSq returns the x and y values squared
func (v *Vector2) LengthSq() (*big.Float, *big.Float) {
clone := v.Clone()
x, y := clone.X(), clone.Y()
return x.Mul(x, x), y.Mul(y, y)
}
// SetLength sets the length of this Vector
func (v *Vector2) SetLength(length *big.Float) d2interface.Vector {
return v.Normalize().Scale(length)
}
// Normalize Makes the vector a unit length vector (magnitude of 1) in the same
// direction.
func (v *Vector2) Normalize() d2interface.Vector {
xsq, ysq := v.LengthSq()
length := big.NewFloat(zero).Add(xsq, ysq)
one := big.NewFloat(1.0)
if length.Cmp(one) > 0 {
length.Quo(one, length.Sqrt(length))
v.x.Mul(v.x, length)
v.y.Mul(v.y, length)
}
return v
}
// NormalizeRightHand rotate this Vector to its perpendicular,
// in the positive direction.
func (v *Vector2) NormalizeRightHand() d2interface.Vector {
x := v.x
v.x = v.y.Mul(v.y, big.NewFloat(negative1))
v.y = x
return v
}
// NormalizeLeftHand rotate this Vector to its perpendicular,
// in the negative1 direction.
func (v *Vector2) NormalizeLeftHand() d2interface.Vector {
x := v.x
v.x = v.y
v.y = x.Mul(x, big.NewFloat(negative1))
return v
}
// Dot returns the dot product of this Vector and the given Vector.
func (v *Vector2) Dot(src d2interface.Vector) *big.Float {
c := v.Clone()
c.X().Mul(c.X(), src.X())
c.Y().Mul(c.Y(), src.Y())
return c.X().Add(c.X(), c.Y())
}
// Cross Calculate the cross product of this Vector and the given Vector.
func (v *Vector2) Cross(src d2interface.Vector) *big.Float {
c := v.Clone()
c.X().Mul(c.X(), src.X())
c.Y().Mul(c.Y(), src.Y())
return c.X().Sub(c.X(), c.Y())
}
// Lerp Linearly interpolate between this Vector and the given Vector.
func (v *Vector2) Lerp(
src d2interface.Vector,
t *big.Float,
) d2interface.Vector {
vc, sc := v.Clone(), src.Clone()
x, y := vc.X(), vc.Y()
v.x.Set(x.Add(x, t.Mul(t, sc.X().Sub(sc.X(), x))))
v.y.Set(y.Add(y, t.Mul(t, sc.Y().Sub(sc.Y(), y))))
return v
}
// Reset this Vector the zero vector (0, 0).
func (v *Vector2) Reset() d2interface.Vector {
v.x.SetFloat64(zero)
v.y.SetFloat64(zero)
return v
}
// Limit the length (or magnitude) of this Vector
func (v *Vector2) Limit(max *big.Float) d2interface.Vector {
length := v.Length()
if max.Cmp(length) < 0 {
v.Scale(length.Quo(max, length))
}
return v
}
// Reflect this Vector off a line defined by a normal.
func (v *Vector2) Reflect(normal d2interface.Vector) d2interface.Vector {
clone := v.Clone()
clone.Normalize()
two := big.NewFloat(2.0) // there's some matrix algebra magic here
dot := v.Clone().Dot(normal)
normal.Scale(two.Mul(two, dot))
return v.Subtract(normal)
}
// Mirror reflect this Vector across another.
func (v *Vector2) Mirror(axis d2interface.Vector) d2interface.Vector {
return v.Reflect(axis).Negate()
}
// Rotate this Vector by an angle amount.
func (v *Vector2) Rotate(angle *big.Float) d2interface.Vector {
// HACK we should do this only with big.Float, not float64
// we are throwing away the precision here
floatAngle, _ := angle.Float64()
cos := math.Cos(floatAngle)
sin := math.Sin(floatAngle)
oldX, _ := v.x.Float64()
oldY, _ := v.y.Float64()
newX := big.NewFloat(cos*oldX - sin*oldY)
newY := big.NewFloat(sin*oldX + cos*oldY)
v.Set(newX, newY)
// Copy sets this vector's values to those of the given vector.
func (v *Vector) Copy(o *Vector) *Vector {
v.x = o.x
v.y = o.y
return v
}
// Floor rounds the vector down to the nearest whole numbers.
func (v *Vector2) Floor() d2interface.Vector {
var xi, yi big.Int
v.x.Int(&xi)
v.y.Int(&yi)
v.X().SetInt(&xi)
v.Y().SetInt(&yi)
func (v *Vector) Floor() *Vector {
v.x = math.Floor(v.x)
v.y = math.Floor(v.y)
return v
}
func (v *Vector2) String() string {
return fmt.Sprintf("Vector2{%s, %s}", v.x.Text('f', 5), v.y.Text('f', 5))
// Clamp limits the values of v to those of a and b. If the values of v are between those of a and b they will be
// unchanged.
func (v *Vector) Clamp(a, b *Vector) *Vector {
v.x = d2math.ClampFloat64(v.x, a.x, b.x)
v.y = d2math.ClampFloat64(v.y, a.y, b.y)
return v
}
// Up returns a new vector (0, 1)
func Up() d2interface.Vector {
return New(0, 1)
// Add the given vector to this vector.
func (v *Vector) Add(o *Vector) *Vector {
v.x += o.x
v.y += o.y
return v
}
// Down returns a new vector (0, -1)
func Down() d2interface.Vector {
return New(0, -1)
// Subtract the given vector from this vector.
func (v *Vector) Subtract(o *Vector) *Vector {
v.x -= o.x
v.y -= o.y
return v
}
// Right returns a new vector (1, 0)
func Right() d2interface.Vector {
return New(1, 0)
// Multiply this Vector by the given Vector.
func (v *Vector) Multiply(o *Vector) *Vector {
v.x *= o.x
v.y *= o.y
return v
}
// Left returns a new vector (-1, 0)
func Left() d2interface.Vector {
return New(-1, 0)
// Scale multiplies both values of this vector by a single given value.
func (v *Vector) Scale(s float64) *Vector {
v.x *= s
v.y *= s
return v
}
// One returns a new vector (1, 1)
func One() d2interface.Vector {
return New(1, 1)
// Divide this vector by the given vector.
func (v *Vector) Divide(o *Vector) *Vector {
v.x /= o.x
v.y /= o.y
return v
}
// Zero returns a new vector (0, 0)
func Zero() d2interface.Vector {
return New(0, 0)
// Abs sets the vector to it's absolute (positive) equivalent.
func (v *Vector) Abs() *Vector {
xm, ym := 1.0, 1.0
if v.x < 0 {
xm = -1
}
if v.y < 0 {
ym = -1
}
v.x *= xm
v.y *= ym
return v
}
// Negate multiplies this vector by -1.
func (v *Vector) Negate() *Vector {
return v.Scale(-1)
}
// Distance between this Vector's position and that of the given Vector.
func (v *Vector) Distance(o Vector) float64 {
delta := o.Clone()
delta.Subtract(v)
return delta.Length()
}
// Length (magnitude/quantity) of this Vector.
func (v *Vector) Length() float64 {
return math.Sqrt(v.Dot(v))
}
// SetLength sets the length of this Vector without changing the direction.
func (v *Vector) SetLength(length float64) *Vector {
v.Normalize()
v.Scale(length)
return v
}
// Lerp sets this vector to the linear interpolation between this and the given vector. The interp argument determines
// the distance between the two vectors. An interp of 0 will return this vector and 1 will return the given vector.
func (v *Vector) Lerp(o *Vector, interp float64) *Vector {
v.x = d2math.Lerp(v.x, o.x, interp)
v.y = d2math.Lerp(v.y, o.y, interp)
return v
}
// Dot returns the dot product of this Vector and the given Vector.
func (v *Vector) Dot(o *Vector) float64 {
return v.x*o.x + v.y*o.y
}
// Cross returns the cross product of this Vector and the given Vector. Note: Cross product is specific to 3D space.
// This a not cross product. It is the Z component of a 3D vector cross product calculation. The X and Y components use
// the value of z which doesn't exist in 2D. See:
// https://stackoverflow.com/questions/243945/calculating-a-2d-vectors-cross-product
//
// The sign of Cross indicates whether the direction between the points described by vectors v and o around the origin
// (0,0) moves clockwise or anti-clockwise. The perspective is from the would-be position of positive Z and the
// direction is from v to o.
//
// Negative = clockwise
// Positive = anti-clockwise
// 0 = vectors are identical.
func (v *Vector) Cross(o Vector) float64 {
return v.x*o.y - v.y*o.x
}
// Normalize sets the vector length to 1 without changing the direction. The normalized vector may be scaled by the
// float64 return value to restore it's original length.
func (v *Vector) Normalize() float64 {
multiplier := 1 / v.Length()
v.Scale(multiplier)
return 1 / multiplier
}
// Angle computes the unsigned angle in radians from this vector to the given vector. This angle will never exceed half
// a full circle. For angles describing a full circumference use SignedAngle.
func (v *Vector) Angle(o Vector) float64 {
from := v.Clone()
from.Normalize()
to := o.Clone()
to.Normalize()
denominator := math.Sqrt(from.Length() * to.Length())
dotClamped := d2math.ClampFloat64(from.Dot(&to)/denominator, -1, 1)
return math.Acos(dotClamped)
}
// SignedAngle computes the signed (clockwise) angle in radians from this vector to the given vector.
func (v *Vector) SignedAngle(o Vector) float64 {
unsigned := v.Angle(o)
sign := d2math.Sign(v.x*o.y - v.y*o.x)
if sign > 0 {
return d2math.RadFull - unsigned
}
return unsigned
}
// Reflect sets this Vector to it's reflection off a line defined by the given normal.
func (v *Vector) Reflect(normal Vector) *Vector {
normal.Normalize()
undo := v.Normalize()
// 1*Dot is the directional (ignoring length) difference between the vector and the normal. Therefore 2*Dot takes
// us beyond the normal to the angle with the equivalent distance in the other direction i.e. the reflection.
normal.Scale(two * v.Dot(&normal))
v.Subtract(&normal)
v.Scale(undo)
return v
}
// ReflectSurface does the same thing as Reflect, except the given vector describes,
// the surface line, not it's normal.
func (v *Vector) ReflectSurface(surface Vector) *Vector {
v.Reflect(surface).Negate()
return v
}
// Rotate moves the vector around it's origin clockwise, by the given angle in radians.
func (v *Vector) Rotate(angle float64) *Vector {
a := -angle
x := v.x*math.Cos(a) - v.y*math.Sin(a)
y := v.x*math.Sin(a) + v.y*math.Cos(a)
v.x = x
v.y = y
return v
}
// NinetyAnti rotates this vector by 90 degrees anti-clockwise.
func (v *Vector) NinetyAnti() *Vector {
x := v.x
v.x = v.y * -1
v.y = x
return v
}
// NinetyClock rotates this vector by 90 degrees clockwise.
func (v *Vector) NinetyClock() *Vector {
y := v.y
v.y = v.x * -1
v.x = y
return v
}
func (v Vector) String() string {
return fmt.Sprintf("Vector{%.3f, %.3f}", v.x, v.y)
}
// VectorUp returns a new vector (0, 1)
func VectorUp() Vector {
return NewVector(0, 1)
}
// VectorDown returns a new vector (0, -1)
func VectorDown() Vector {
return NewVector(0, -1)
}
// VectorRight returns a new vector (1, 0)
func VectorRight() Vector {
return NewVector(1, 0)
}
// VectorLeft returns a new vector (-1, 0)
func VectorLeft() Vector {
return NewVector(-1, 0)
}
// VectorOne returns a new vector (1, 1)
func VectorOne() Vector {
return NewVector(1, 1)
}
// VectorZero returns a new vector (0, 0)
func VectorZero() Vector {
return NewVector(0, 0)
}

View File

@ -1,33 +1,467 @@
package d2vector
import "testing"
import (
"fmt"
"testing"
func TestClone(t *testing.T) {
v := New(1, 1)
want := New(1, 1)
got := v.Clone()
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
func evaluateVector(description string, want, got Vector, t *testing.T) {
if !got.Equals(want) {
t.Errorf("wanted %s: got %s", want, got)
t.Errorf("%s: wanted %s: got %s", description, want, got)
}
}
func TestAbs(t *testing.T) {
v := New(-1, -1)
want := New(1, 1)
got := v.Abs()
if !got.Equals(want) {
t.Errorf("wanted %s: got %s", want, got)
func evaluateVectorApprox(description string, want, got Vector, t *testing.T) {
if !got.EqualsApprox(want) {
t.Errorf("%s: wanted %s: got %s", description, want, got)
}
}
func evaluateScalar(description string, want, got float64, t *testing.T) {
if want != got {
t.Errorf("%s: wanted %f: got %f", description, want, got)
}
}
func evaluateScalarApprox(description string, want, got float64, t *testing.T) {
if d2math.CompareFloat64Fuzzy(want, got) != 0 {
t.Errorf("%s: wanted %f: got %f", description, want, got)
}
}
func evaluateChanged(description string, original, clone Vector, t *testing.T) {
if !original.Equals(clone) {
t.Errorf("%s: changed vector %s to %s unexpectedly", description, clone, original)
}
}
func TestEquals(t *testing.T) {
a := NewVector(1, 2)
b := NewVector(1, 2)
got := a.Equals(b)
if !got {
t.Errorf("exact equality %s and %s: wanted true: got %t", a, b, got)
}
c := NewVector(3, 4)
got = a.Equals(c)
if got {
t.Errorf("exact equality %s and %s: wanted false: got %t", a, c, got)
}
}
func TestEqualsF(t *testing.T) {
subEpsilon := d2math.Epsilon / 3
a := NewVector(1, 2)
b := NewVector(1+subEpsilon, 2+subEpsilon)
got := a.EqualsApprox(b)
if !got {
t.Errorf("approximate equality %s and %s: wanted true: got %t", a, b, got)
}
c := NewVector(1+d2math.Epsilon, 2+d2math.Epsilon)
got = a.EqualsApprox(c)
if got {
t.Errorf("approximate equality %s and %s: wanted false: got %t", a, c, got)
}
}
func TestCompareF(t *testing.T) {
subEpsilon := d2math.Epsilon / 3
f := NewVector(1+subEpsilon, 1+subEpsilon)
c := NewVector(1, 1)
xWant, yWant := 0, 0
yGot, xGot := f.CompareApprox(c)
if xGot != xWant || yGot != yWant {
t.Errorf("approximate comparison %s and %s: wanted (%d, %d): got (%d, %d)", f, c, xWant, yWant, xGot, yGot)
}
f = NewVector(2, 2)
c = NewVector(-1, 3)
xWant, yWant = 1, -1
xGot, yGot = f.CompareApprox(c)
if xGot != xWant || yGot != yWant {
t.Errorf("approximate comparison %s and %s: wanted (%d, %d): got (%d, %d)", f, c, xWant, yWant, xGot, yGot)
}
f = NewVector(2, 2)
c = NewVector(3, -1)
xWant, yWant = -1, 1
xGot, yGot = f.CompareApprox(c)
if xGot != xWant || yGot != yWant {
t.Errorf("approximate comparison %s and %s: wanted (%d, %d): got (%d, %d)", f, c, xWant, yWant, xGot, yGot)
}
}
func TestSet(t *testing.T) {
v := NewVector(1, 1)
want := NewVector(2, 3)
got := v.Clone()
got.Set(2, 3)
evaluateVector(fmt.Sprintf("set %s to (2, 3)", v), want, got, t)
}
func TestClone(t *testing.T) {
want := NewVector(1, 2)
got := want.Clone()
evaluateVector(fmt.Sprintf("clone %s", want), want, got, t)
}
func TestCopy(t *testing.T) {
want := NewVector(1, 2)
got := NewVector(0, 0)
got.Copy(&want)
evaluateVector(fmt.Sprintf("copy %s to %s", got, want), want, got, t)
}
func TestFloor(t *testing.T) {
v := New(1.6, 1.6)
v := NewVector(1.6, 1.6)
want := NewVector(1, 1)
got := v.Clone()
got.Floor()
want := New(1, 1)
if !v.Floor().Equals(want) {
t.Errorf("want %s: got %s", want, v)
}
evaluateVector(fmt.Sprintf("round %s down", v), want, got, t)
}
func TestClamp(t *testing.T) {
v := NewVector(-10, 10)
c := v.Clone()
a := NewVector(2, 2)
b := NewVector(7, 7)
want := NewVector(2, 7)
got := v.Clamp(&a, &b)
evaluateVector(fmt.Sprintf("clamp %s between %s and %s", c, a, b), want, *got, t)
}
func TestAdd(t *testing.T) {
v := NewVector(1, 1)
add := NewVector(0.5, 0.5)
want := NewVector(1.5, 1.5)
got := v.Clone()
got.Add(&add)
evaluateVector(fmt.Sprintf("add %s to %s", add, v), want, got, t)
}
func TestSubtract(t *testing.T) {
v := NewVector(1, 1)
subtract := NewVector(0.6, 0.6)
want := NewVector(0.4, 0.4)
got := v.Clone()
got.Subtract(&subtract)
evaluateVector(fmt.Sprintf("subtract %s from %s", subtract, v), want, got, t)
}
func TestMultiply(t *testing.T) {
v := NewVector(1, 1)
multiply := NewVector(2, 2)
want := NewVector(2, 2)
got := v.Clone()
got.Multiply(&multiply)
evaluateVector(fmt.Sprintf("multiply %s by %s", v, multiply), want, got, t)
}
func TestDivide(t *testing.T) {
v := NewVector(1, 1)
divide := NewVector(2, 2)
want := NewVector(0.5, 0.5)
got := v.Clone()
got.Divide(&divide)
evaluateVector(fmt.Sprintf("divide %s by %s", v, divide), want, got, t)
}
func TestScale(t *testing.T) {
v := NewVector(2, 3)
want := NewVector(4, 6)
got := v.Clone()
got.Scale(2)
evaluateVector(fmt.Sprintf("scale %s by 2", v), want, got, t)
}
func TestAbs(t *testing.T) {
v := NewVector(-1, 1)
want := NewVector(1, 1)
got := v.Clone()
got.Abs()
evaluateVector(fmt.Sprintf("absolute value of %s", v), want, got, t)
}
func TestNegate(t *testing.T) {
v := NewVector(-1, 1)
want := NewVector(1, -1)
got := v.Clone()
got.Negate()
evaluateVector(fmt.Sprintf("inverse value of %s", v), want, got, t)
}
func TestDistance(t *testing.T) {
v := NewVector(1, 3)
other := NewVector(1, -1)
want := 4.0
c := v.Clone()
got := c.Distance(other)
evaluateScalar(fmt.Sprintf("distance from %s to %s", v, other), want, got, t)
}
func TestLength(t *testing.T) {
v := NewVector(2, 0)
c := v.Clone()
want := 2.0
got := v.Length()
d := fmt.Sprintf("length of %s", c)
evaluateChanged(d, v, c, t)
evaluateScalar(d, want, got, t)
}
func TestSetLength(t *testing.T) {
v := NewVector(1, 1)
c := v.Clone()
want := 2.0
got := v.SetLength(want).Length()
d := fmt.Sprintf("length of %s", c)
evaluateScalarApprox(d, want, got, t)
}
func TestLerp(t *testing.T) {
a := NewVector(0, 0)
b := NewVector(-20, 10)
x := 0.3
want := NewVector(-6, 3)
got := a.Lerp(&b, x)
evaluateVector(fmt.Sprintf("linear interpolation between %s and %s by %.2f", a, b, x), want, *got, t)
}
func TestDot(t *testing.T) {
v := NewVector(1, 1)
c := v.Clone()
want := 2.0
got := v.Dot(&v)
d := fmt.Sprintf("dot product of %s", c)
evaluateChanged(d, v, c, t)
evaluateScalar(d, want, got, t)
}
func TestCross(t *testing.T) {
v := NewVector(1, 1)
clock := NewVector(1, 0)
anti := NewVector(0, 1)
want := -1.0
got := v.Cross(clock)
evaluateScalar(fmt.Sprintf("cross product of %s and %s", v, clock), want, got, t)
want = 1.0
got = v.Cross(anti)
evaluateScalar(fmt.Sprintf("cross product of %s and %s", v, anti), want, got, t)
}
func TestNormalize(t *testing.T) {
v := NewVector(10, 0)
c := v.Clone()
want := NewVector(1, 0)
v.Normalize()
evaluateVector(fmt.Sprintf("normalize %s", c), want, v, t)
v = NewVector(0, 10)
c = v.Clone()
want = NewVector(0, 1)
reverse := v.Normalize()
evaluateVector(fmt.Sprintf("normalize %s", c), want, v, t)
want = NewVector(0, 10)
v.Scale(reverse)
evaluateVector(fmt.Sprintf("reverse normalizing of %s", c), want, v, t)
}
func TestAngle(t *testing.T) {
v := NewVector(0, 1)
c := v.Clone()
other := NewVector(1, 0.3)
d := fmt.Sprintf("angle from %s to %s", c, other)
want := 1.2793395323170293
got := v.Angle(other)
evaluateScalar(d, want, got, t)
evaluateChanged(d, v, c, t)
other.Set(-1, 0.3)
c = other.Clone()
d = fmt.Sprintf("angle from %s to %s", c, other)
got = v.Angle(other)
evaluateScalar(d, want, got, t)
evaluateChanged(d, other, c, t)
}
func TestSignedAngle(t *testing.T) {
v := NewVector(0, 1)
c := v.Clone()
other := NewVector(1, 0.3)
want := 1.2793395323170293
got := v.SignedAngle(other)
d := fmt.Sprintf("angle from %s to %s", v, other)
evaluateScalar(d, want, got, t)
evaluateChanged(d, v, c, t)
other.Set(-1, 0.3)
c = other.Clone()
want = 5.0038457214660585
got = v.SignedAngle(other)
d = fmt.Sprintf("angle from %s to %s", v, other)
evaluateScalar(d, want, got, t)
evaluateChanged(d, other, c, t)
}
func TestReflect(t *testing.T) {
rightDown := NewVector(1, -1)
up := NewVector(0, 1)
want := NewVector(1, 1)
got := rightDown.Reflect(up)
evaluateVector(fmt.Sprintf("reflect direction %s off surface with normal %s", rightDown, up), want, *got, t)
}
func TestReflectSurface(t *testing.T) {
rightDown := NewVector(1, -1)
up := NewVector(0, 1)
want := NewVector(-1, -1)
got := rightDown.ReflectSurface(up)
evaluateVector(fmt.Sprintf("reflect direction %s off surface with normal %s", rightDown, up), want, *got, t)
}
func TestRotate(t *testing.T) {
up := NewVector(0, 1)
right := NewVector(1, 0)
c := right.Clone()
angle := -up.SignedAngle(right)
want := NewVector(0, 1)
got := right.Rotate(angle)
evaluateVectorApprox(fmt.Sprintf("rotated %s by %.1f", c, angle*d2math.RadToDeg), want, *got, t)
c = up.Clone()
angle -= d2math.RadFull
want = NewVector(-1, 0)
got = up.Rotate(angle)
evaluateVectorApprox(fmt.Sprintf("rotated %s by %.1f", c, angle*d2math.RadToDeg), want, *got, t)
}
func TestNinetyAnti(t *testing.T) {
v := NewVector(0, 1)
c := v.Clone()
want := NewVector(-1, 0)
got := v.NinetyAnti()
evaluateVector(fmt.Sprintf("rotated %s by 90 degrees clockwise", c), want, *got, t)
}
func TestNinetyClock(t *testing.T) {
v := NewVector(0, 1)
c := v.Clone()
want := NewVector(1, 0)
v = c.Clone()
got := v.NinetyClock()
evaluateVector(fmt.Sprintf("rotated %s by 90 degrees anti-clockwise", c), want, *got, t)
}
func TestVectorUp(t *testing.T) {
got := VectorUp()
want := NewVector(0, 1)
evaluateVector("create normalized vector with up direction", want, got, t)
}
func TestVectorDown(t *testing.T) {
got := VectorDown()
want := NewVector(0, -1)
evaluateVector("create normalized vector with down direction", want, got, t)
}
func TestVectorRight(t *testing.T) {
got := VectorRight()
want := NewVector(1, 0)
evaluateVector("create normalized vector with right direction", want, got, t)
}
func TestVectorLeft(t *testing.T) {
got := VectorLeft()
want := NewVector(-1, 0)
evaluateVector("create normalized vector with left direction", want, got, t)
}
func TestVectorOne(t *testing.T) {
got := VectorOne()
want := NewVector(1, 1)
evaluateVector("create vector with X and Y values of 1", want, got, t)
}
func TestVectorZero(t *testing.T) {
got := VectorZero()
want := NewVector(0, 0)
evaluateVector("create vector with X and Y values of 0", want, got, t)
}

View File

@ -1,2 +1,2 @@
// Package d2math provides various math-related things
// Package d2math provides mathmatical functions not included in Golang's standard math library.
package d2math

67
d2common/d2math/math.go Normal file
View File

@ -0,0 +1,67 @@
package d2math
import "math"
const (
// Epsilon is used as the threshold for 'almost equal' operations.
Epsilon float64 = 0.0001
// RadToDeg is used to convert anges in radians to degrees by multiplying the radians by RadToDeg. Similarly,degrees
// are converted to radians when dividing by RadToDeg.
RadToDeg float64 = 57.29578
// RadFull is the radian equivalent of 360 degrees.
RadFull float64 = 6.283185253783088
)
// CompareFloat64Fuzzy returns an integer between -1 and 1 describing
// the comparison of floats a and b. 0 will be returned if the
// absolute difference between a and b is less than Epsilon.
func CompareFloat64Fuzzy(a, b float64) int {
delta := a - b
if math.Abs(delta) < Epsilon {
return 0
}
if delta > 0 {
return 1
}
return -1
}
// ClampFloat64 returns a clamped to min and max.
func ClampFloat64(a, min, max float64) float64 {
if a > max {
return max
} else if a < min {
return min
}
return a
}
// Sign returns the sign of a.
func Sign(a float64) int {
switch {
case a < 0:
return -1
case a > 0:
return +1
}
return 0
}
// Lerp returns the linear interpolation from a to b using interpolator x.
func Lerp(a, b, x float64) float64 {
return a + x*(b-a)
}
// Unlerp returns the intepolator Lerp would require to return x when given
// a and b. The x argument of this function can be thought of as the return
// value of lerp. The return value of this function can be used as x in
// Lerp.
func Unlerp(a, b, x float64) float64 {
return (x - a) / (b - a)
}

View File

@ -0,0 +1,113 @@
package d2math
import (
"testing"
)
func TestCompareFloat64Fuzzy(t *testing.T) {
subEpsilon := Epsilon / 3
want := 0
a, b := 1+subEpsilon, 1.0
got := CompareFloat64Fuzzy(a, b)
if got != want {
t.Errorf("compare %.2f and %.2f: wanted %d: got %d", a, b, want, got)
}
want = 1
a, b = 2, 1.0
got = CompareFloat64Fuzzy(a, b)
if got != want {
t.Errorf("compare %.2f and %.2f: wanted %d: got %d", a, b, want, got)
}
want = -1
a, b = -2, 1.0
got = CompareFloat64Fuzzy(a, b)
if got != want {
t.Errorf("compare %.2f and %.2f: wanted %d: got %d", a, b, want, got)
}
}
func TestClampFloat64(t *testing.T) {
want := 0.5
a := 0.5
got := ClampFloat64(a, 0, 1)
if got != want {
t.Errorf("clamped %.2f between 0 and 1: wanted %.2f: got %.2f", a, want, got)
}
want = 0.0
a = -1.0
got = ClampFloat64(a, 0, 1)
if got != want {
t.Errorf("clamped %.2f between 0 and 1: wanted %.2f: got %.2f", a, want, got)
}
want = 1.0
a = 2.0
got = ClampFloat64(a, 0, 1)
if got != want {
t.Errorf("clamped %.2f between 0 and 1: wanted %.2f: got %.2f", a, want, got)
}
}
func TestSign(t *testing.T) {
want := 1
a := 0.5
got := Sign(a)
if got != want {
t.Errorf("sign of %.2f: wanted %df: got %d", a, want, got)
}
want = -1
a = -3
got = Sign(a)
if got != want {
t.Errorf("sign of %.2f: wanted %df: got %d", a, want, got)
}
want = 0
a = 0.0
got = Sign(a)
if got != want {
t.Errorf("sign of %.2f: wanted %df: got %d", a, want, got)
}
}
func TestLerp(t *testing.T) {
want := 3.0
x := 0.3
a, b := 0.0, 10.0
got := Lerp(a, b, x)
d := "linear interpolation between %.2f and %.2f with interpolator %.2f: wanted %.2f: got %.2f"
if got != want {
t.Errorf(d, a, b, x, want, got)
}
}
func TestUnlerp(t *testing.T) {
want := 0.3
x := 3.0
a, b := 0.0, 10.0
got := Unlerp(a, b, x)
d := "find the interpolator of %.2f between %.2f and %.2f: wanted %.2f: got %.2f"
if got != want {
t.Errorf(d, x, a, b, want, got)
}
}