From 029cb62972c96d3da4f1c7ef52ae808d8fd116d7 Mon Sep 17 00:00:00 2001 From: danhale-git <36298392+danhale-git@users.noreply.github.com> Date: Thu, 9 Jul 2020 13:30:55 +0100 Subject: [PATCH] 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. --- d2common/d2interface/vector.go | 45 -- d2common/d2math/d2vector/position.go | 72 ++- d2common/d2math/d2vector/position_test.go | 38 +- d2common/d2math/d2vector/vector.go | 622 +++++++++------------- d2common/d2math/d2vector/vector_test.go | 472 +++++++++++++++- d2common/d2math/doc.go | 2 +- d2common/d2math/math.go | 67 +++ d2common/d2math/math_test.go | 113 ++++ 8 files changed, 960 insertions(+), 471 deletions(-) delete mode 100644 d2common/d2interface/vector.go create mode 100644 d2common/d2math/math.go create mode 100644 d2common/d2math/math_test.go diff --git a/d2common/d2interface/vector.go b/d2common/d2interface/vector.go deleted file mode 100644 index 8eb965f6..00000000 --- a/d2common/d2interface/vector.go +++ /dev/null @@ -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 -} diff --git a/d2common/d2math/d2vector/position.go b/d2common/d2math/d2vector/position.go index ce3d6fde..abcbaaa6 100644 --- a/d2common/d2math/d2vector/position.go +++ b/d2common/d2math/d2vector/position.go @@ -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()) } diff --git a/d2common/d2math/d2vector/position_test.go b/d2common/d2math/d2vector/position_test.go index 3e55de2f..085d7c4f 100644 --- a/d2common/d2math/d2vector/position_test.go +++ b/d2common/d2math/d2vector/position_test.go @@ -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) } diff --git a/d2common/d2math/d2vector/vector.go b/d2common/d2math/d2vector/vector.go index 42efd8da..6a57ef91 100644 --- a/d2common/d2math/d2vector/vector.go +++ b/d2common/d2math/d2vector/vector.go @@ -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) } diff --git a/d2common/d2math/d2vector/vector_test.go b/d2common/d2math/d2vector/vector_test.go index f370dd6d..e8ad7dae 100644 --- a/d2common/d2math/d2vector/vector_test.go +++ b/d2common/d2math/d2vector/vector_test.go @@ -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(÷) + + 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) } diff --git a/d2common/d2math/doc.go b/d2common/d2math/doc.go index 88473ed0..528283e1 100644 --- a/d2common/d2math/doc.go +++ b/d2common/d2math/doc.go @@ -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 diff --git a/d2common/d2math/math.go b/d2common/d2math/math.go new file mode 100644 index 00000000..ef788502 --- /dev/null +++ b/d2common/d2math/math.go @@ -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) +} diff --git a/d2common/d2math/math_test.go b/d2common/d2math/math_test.go new file mode 100644 index 00000000..f16e67be --- /dev/null +++ b/d2common/d2math/math_test.go @@ -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) + } +}