Merge pull request #287 from cebarks/geometry-refactor
Break geometry.go & geometry_test.go into multiple smaller files
This commit is contained in:
commit
4964768d4e
|
@ -0,0 +1,334 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Circle is a 2D circle. It is defined by two properties:
|
||||
// - Center vector
|
||||
// - Radius float64
|
||||
type Circle struct {
|
||||
Center Vec
|
||||
Radius float64
|
||||
}
|
||||
|
||||
// C returns a new Circle with the given radius and center coordinates.
|
||||
//
|
||||
// Note that a negative radius is valid.
|
||||
func C(center Vec, radius float64) Circle {
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: radius,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Circle.
|
||||
//
|
||||
// c := pixel.C(10.1234, pixel.ZV)
|
||||
// c.String() // returns "Circle(10.12, Vec(0, 0))"
|
||||
// fmt.Println(c) // Circle(10.12, Vec(0, 0))
|
||||
func (c Circle) String() string {
|
||||
return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius)
|
||||
}
|
||||
|
||||
// Norm returns the Circle in normalized form - this sets the radius to its absolute value.
|
||||
//
|
||||
// c := pixel.C(-10, pixel.ZV)
|
||||
// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10}
|
||||
func (c Circle) Norm() Circle {
|
||||
return Circle{
|
||||
Center: c.Center,
|
||||
Radius: math.Abs(c.Radius),
|
||||
}
|
||||
}
|
||||
|
||||
// Area returns the area of the Circle.
|
||||
func (c Circle) Area() float64 {
|
||||
return math.Pi * math.Pow(c.Radius, 2)
|
||||
}
|
||||
|
||||
// Moved returns the Circle moved by the given vector delta.
|
||||
func (c Circle) Moved(delta Vec) Circle {
|
||||
return Circle{
|
||||
Center: c.Center.Add(delta),
|
||||
Radius: c.Radius,
|
||||
}
|
||||
}
|
||||
|
||||
// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor.
|
||||
//
|
||||
// c := pixel.C(pixel.ZV, 10)
|
||||
// c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5}
|
||||
// c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35}
|
||||
func (c Circle) Resized(radiusDelta float64) Circle {
|
||||
return Circle{
|
||||
Center: c.Center,
|
||||
Radius: c.Radius + radiusDelta,
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks whether a vector `u` is contained within this Circle (including it's perimeter).
|
||||
func (c Circle) Contains(u Vec) bool {
|
||||
toCenter := c.Center.To(u)
|
||||
return c.Radius >= toCenter.Len()
|
||||
}
|
||||
|
||||
// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2
|
||||
// where r is the radius of the circle.
|
||||
func (c Circle) Formula() (h, k float64) {
|
||||
return c.Center.X, c.Center.Y
|
||||
}
|
||||
|
||||
// maxCircle will return the larger circle based on the radius.
|
||||
func maxCircle(c, d Circle) Circle {
|
||||
if c.Radius < d.Radius {
|
||||
return d
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// minCircle will return the smaller circle based on the radius.
|
||||
func minCircle(c, d Circle) Circle {
|
||||
if c.Radius < d.Radius {
|
||||
return c
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Union returns the minimal Circle which covers both `c` and `d`.
|
||||
func (c Circle) Union(d Circle) Circle {
|
||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
||||
smallerC := minCircle(c.Norm(), d.Norm())
|
||||
|
||||
// Get distance between centers
|
||||
dist := c.Center.To(d.Center).Len()
|
||||
|
||||
// If the bigger Circle encompasses the smaller one, we have the result
|
||||
if dist+smallerC.Radius <= biggerC.Radius {
|
||||
return biggerC
|
||||
}
|
||||
|
||||
// Calculate radius for encompassing Circle
|
||||
r := (dist + biggerC.Radius + smallerC.Radius) / 2
|
||||
|
||||
// Calculate center for encompassing Circle
|
||||
theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist)
|
||||
center := Lerp(smallerC.Center, biggerC.Center, theta)
|
||||
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: r,
|
||||
}
|
||||
}
|
||||
|
||||
// Intersect returns the maximal Circle which is covered by both `c` and `d`.
|
||||
//
|
||||
// If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's
|
||||
// centers.
|
||||
func (c Circle) Intersect(d Circle) Circle {
|
||||
// Check if one of the circles encompasses the other; if so, return that one
|
||||
biggerC := maxCircle(c.Norm(), d.Norm())
|
||||
smallerC := minCircle(c.Norm(), d.Norm())
|
||||
|
||||
if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius {
|
||||
return biggerC
|
||||
}
|
||||
|
||||
// Calculate the midpoint between the two radii
|
||||
// Distance between centers
|
||||
dist := c.Center.To(d.Center).Len()
|
||||
// Difference between radii
|
||||
diff := dist - (c.Radius + d.Radius)
|
||||
// Distance from c.Center to the weighted midpoint
|
||||
distToMidpoint := c.Radius + 0.5*diff
|
||||
// Weighted midpoint
|
||||
center := Lerp(c.Center, d.Center, distToMidpoint/dist)
|
||||
|
||||
// No need to calculate radius if the circles do not overlap
|
||||
if c.Center.To(d.Center).Len() >= c.Radius+d.Radius {
|
||||
return C(center, 0)
|
||||
}
|
||||
|
||||
radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius)
|
||||
|
||||
return Circle{
|
||||
Center: center,
|
||||
Radius: math.Abs(radius),
|
||||
}
|
||||
}
|
||||
|
||||
// IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no
|
||||
// longer intersect.
|
||||
func (c Circle) IntersectLine(l Line) Vec {
|
||||
return l.IntersectCircle(c).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle
|
||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
||||
// the perimeters touch.
|
||||
//
|
||||
// This function will return a non-zero vector if:
|
||||
// - The Rect contains the Circle, partially or fully
|
||||
// - The Circle contains the Rect, partially of fully
|
||||
func (c Circle) IntersectRect(r Rect) Vec {
|
||||
// Checks if the c.Center is not in the diagonal quadrants of the rectangle
|
||||
if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) {
|
||||
// 'grow' the Rect by c.Radius in each orthagonal
|
||||
grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))}
|
||||
if !grown.Contains(c.Center) {
|
||||
// c.Center not close enough to overlap, return zero-vector
|
||||
return ZV
|
||||
}
|
||||
|
||||
// Get minimum distance to travel out of Rect
|
||||
rToC := r.Center().To(c.Center)
|
||||
h := c.Radius - math.Abs(rToC.X) + (r.W() / 2)
|
||||
v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2)
|
||||
|
||||
if rToC.X < 0 {
|
||||
h = -h
|
||||
}
|
||||
if rToC.Y < 0 {
|
||||
v = -v
|
||||
}
|
||||
|
||||
// No intersect
|
||||
if h == 0 && v == 0 {
|
||||
return ZV
|
||||
}
|
||||
|
||||
if math.Abs(h) > math.Abs(v) {
|
||||
// Vertical distance shorter
|
||||
return V(0, v)
|
||||
}
|
||||
return V(h, 0)
|
||||
} else {
|
||||
// The center is in the diagonal quadrants
|
||||
|
||||
// Helper points to make code below easy to read.
|
||||
rectTopLeft := V(r.Min.X, r.Max.Y)
|
||||
rectBottomRight := V(r.Max.X, r.Min.Y)
|
||||
|
||||
// Check for overlap.
|
||||
if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) {
|
||||
// No overlap.
|
||||
return ZV
|
||||
}
|
||||
|
||||
var centerToCorner Vec
|
||||
if c.Center.To(r.Min).Len() <= c.Radius {
|
||||
// Closest to bottom-left
|
||||
centerToCorner = c.Center.To(r.Min)
|
||||
}
|
||||
if c.Center.To(r.Max).Len() <= c.Radius {
|
||||
// Closest to top-right
|
||||
centerToCorner = c.Center.To(r.Max)
|
||||
}
|
||||
if c.Center.To(rectTopLeft).Len() <= c.Radius {
|
||||
// Closest to top-left
|
||||
centerToCorner = c.Center.To(rectTopLeft)
|
||||
}
|
||||
if c.Center.To(rectBottomRight).Len() <= c.Radius {
|
||||
// Closest to bottom-right
|
||||
centerToCorner = c.Center.To(rectBottomRight)
|
||||
}
|
||||
|
||||
cornerToCircumferenceLen := c.Radius - centerToCorner.Len()
|
||||
|
||||
return centerToCorner.Unit().Scaled(cornerToCircumferenceLen)
|
||||
}
|
||||
}
|
||||
|
||||
// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or
|
||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
||||
// closest-to-l.A to closest-to-l.B.
|
||||
func (c Circle) IntersectionPoints(l Line) []Vec {
|
||||
cContainsA := c.Contains(l.A)
|
||||
cContainsB := c.Contains(l.B)
|
||||
|
||||
// Special case for both endpoint being contained within the circle
|
||||
if cContainsA && cContainsB {
|
||||
return []Vec{}
|
||||
}
|
||||
|
||||
// Get closest point on the line to this circles' center
|
||||
closestToCenter := l.Closest(c.Center)
|
||||
|
||||
// If the distance to the closest point is greater than the radius, there are no points of intersection
|
||||
if closestToCenter.To(c.Center).Len() > c.Radius {
|
||||
return []Vec{}
|
||||
}
|
||||
|
||||
// If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the
|
||||
// point at which it touches the circle.
|
||||
if closestToCenter.To(c.Center).Len() == c.Radius {
|
||||
return []Vec{closestToCenter}
|
||||
}
|
||||
|
||||
// Special case for endpoint being on the circles' center
|
||||
if c.Center == l.A || c.Center == l.B {
|
||||
otherEnd := l.B
|
||||
if c.Center == l.B {
|
||||
otherEnd = l.A
|
||||
}
|
||||
intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius))
|
||||
return []Vec{intersect}
|
||||
}
|
||||
|
||||
// This means the distance to the closest point is less than the radius, so there is at least one intersection,
|
||||
// possibly two.
|
||||
|
||||
// If one of the end points exists within the circle, there is only one intersection
|
||||
if cContainsA || cContainsB {
|
||||
containedPoint := l.A
|
||||
otherEnd := l.B
|
||||
if cContainsB {
|
||||
containedPoint = l.B
|
||||
otherEnd = l.A
|
||||
}
|
||||
|
||||
// Use trigonometry to get the length of the line between the contained point and the intersection point.
|
||||
// The following is used to describe the triangle formed:
|
||||
// - a is the side between contained point and circle center
|
||||
// - b is the side between the center and the intersection point (radius)
|
||||
// - c is the side between the contained point and the intersection point
|
||||
// The captials of these letters are used as the angles opposite the respective sides.
|
||||
// a and b are known
|
||||
a := containedPoint.To(c.Center).Len()
|
||||
b := c.Radius
|
||||
// B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis)
|
||||
B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle()
|
||||
// Using the Sin rule we can get A
|
||||
A := math.Asin((a * math.Sin(B)) / b)
|
||||
// Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C
|
||||
C := math.Pi - A + B
|
||||
// If C is zero, the line segment is in-line with the center-intersect line.
|
||||
var c float64
|
||||
if C == 0 {
|
||||
c = b - a
|
||||
} else {
|
||||
// Using the Sine rule again, we can now get c
|
||||
c = (a * math.Sin(C)) / math.Sin(A)
|
||||
}
|
||||
// Travelling from the contained point to the other end by length of a will provide the intersection point.
|
||||
return []Vec{
|
||||
containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)),
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations.
|
||||
// The vector formed by going from the closest point to the center of the circle will be perpendicular to the line;
|
||||
// this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse.
|
||||
// Calculate the other triangles' sides' length.
|
||||
a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2))
|
||||
|
||||
// Travelling in both directions from the closest point by length of a will provide the two intersection points.
|
||||
first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a))
|
||||
second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a))
|
||||
|
||||
if first.To(l.A).Len() < second.To(l.A).Len() {
|
||||
return []Vec{first, second}
|
||||
}
|
||||
return []Vec{second, first}
|
||||
}
|
|
@ -0,0 +1,466 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestC(t *testing.T) {
|
||||
type args struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "C(): positive radius",
|
||||
args: args{radius: 10, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: 10, Center: pixel.ZV},
|
||||
},
|
||||
{
|
||||
name: "C(): zero radius",
|
||||
args: args{radius: 0, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: 0, Center: pixel.ZV},
|
||||
},
|
||||
{
|
||||
name: "C(): negative radius",
|
||||
args: args{radius: -5, center: pixel.ZV},
|
||||
want: pixel.Circle{Radius: -5, Center: pixel.ZV},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("C() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_String(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Circle.String(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 10.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 0.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), -5.00)",
|
||||
},
|
||||
{
|
||||
name: "Circle.String(): irrational radius",
|
||||
fields: fields{radius: math.Pi, center: pixel.ZV},
|
||||
want: "Circle(Vec(0, 0), 3.14)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.String(); got != tt.want {
|
||||
t.Errorf("Circle.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Norm(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Norm(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Norm(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 0),
|
||||
},
|
||||
{
|
||||
name: "Circle.Norm(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: pixel.C(pixel.ZV, 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Area(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "Circle.Area(): positive radius",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
want: 100 * math.Pi,
|
||||
},
|
||||
{
|
||||
name: "Circle.Area(): zero radius",
|
||||
fields: fields{radius: 0, center: pixel.ZV},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Circle.Area(): negative radius",
|
||||
fields: fields{radius: -5, center: pixel.ZV},
|
||||
want: 25 * math.Pi,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Area(); got != tt.want {
|
||||
t.Errorf("Circle.Area() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Moved(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
delta pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Moved(): positive movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.V(10, 20)},
|
||||
want: pixel.C(pixel.V(10, 20), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Moved(): zero movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.ZV},
|
||||
want: pixel.C(pixel.V(0, 0), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Moved(): negative movement",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{delta: pixel.V(-5, -10)},
|
||||
want: pixel.C(pixel.V(-5, -10), 10),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Moved(tt.args.delta); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Moved() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Resized(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
radiusDelta float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Resized(): positive delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: 5},
|
||||
want: pixel.C(pixel.V(0, 0), 15),
|
||||
},
|
||||
{
|
||||
name: "Circle.Resized(): zero delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: 0},
|
||||
want: pixel.C(pixel.V(0, 0), 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Resized(): negative delta",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{radiusDelta: -5},
|
||||
want: pixel.C(pixel.V(0, 0), 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Resized(tt.args.radiusDelta); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Resized() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Contains(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
u pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Circle.Contains(): point on cicles' center",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point offcenter",
|
||||
fields: fields{radius: 10, center: pixel.V(5, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point on circumference",
|
||||
fields: fields{radius: 10, center: pixel.V(10, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Circle.Contains(): point outside circle",
|
||||
fields: fields{radius: 10, center: pixel.V(15, 0)},
|
||||
args: args{u: pixel.ZV},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Contains(tt.args.u); got != tt.want {
|
||||
t.Errorf("Circle.Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Union(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
d pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Union(): overlapping circles",
|
||||
fields: fields{radius: 5, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.ZV, 5)},
|
||||
want: pixel.C(pixel.ZV, 5),
|
||||
},
|
||||
{
|
||||
name: "Circle.Union(): separate circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(0, 2), 1)},
|
||||
want: pixel.C(pixel.V(0, 1), 2),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(tt.fields.center, tt.fields.radius)
|
||||
if got := c.Union(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Union() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_Intersect(t *testing.T) {
|
||||
type fields struct {
|
||||
radius float64
|
||||
center pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
d pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Circle
|
||||
}{
|
||||
{
|
||||
name: "Circle.Intersect(): intersecting circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(1, 0), 1)},
|
||||
want: pixel.C(pixel.V(0.5, 0), 1),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): non-intersecting circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
||||
want: pixel.C(pixel.V(1.5, 1.5), 0),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): first circle encompassing second",
|
||||
fields: fields{radius: 10, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.V(3, 3), 1)},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): second circle encompassing first",
|
||||
fields: fields{radius: 1, center: pixel.V(-1, -4)},
|
||||
args: args{d: pixel.C(pixel.ZV, 10)},
|
||||
want: pixel.C(pixel.ZV, 10),
|
||||
},
|
||||
{
|
||||
name: "Circle.Intersect(): matching circles",
|
||||
fields: fields{radius: 1, center: pixel.ZV},
|
||||
args: args{d: pixel.C(pixel.ZV, 1)},
|
||||
want: pixel.C(pixel.ZV, 1),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.C(
|
||||
tt.fields.center,
|
||||
tt.fields.radius,
|
||||
)
|
||||
if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircle_IntersectPoints(t *testing.T) {
|
||||
type fields struct {
|
||||
Center pixel.Vec
|
||||
Radius float64
|
||||
}
|
||||
type args struct {
|
||||
l pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(2, 2), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)},
|
||||
},
|
||||
{
|
||||
name: "Line intersects circle at one point",
|
||||
fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(0.207, 0.207)},
|
||||
},
|
||||
{
|
||||
name: "Line endpoint is circle center",
|
||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{pixel.V(0.707, 0.707)},
|
||||
},
|
||||
{
|
||||
name: "Both line endpoints within circle",
|
||||
fields: fields{Center: pixel.V(0, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "Line does not intersect circle",
|
||||
fields: fields{Center: pixel.V(10, 0), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "Horizontal line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))},
|
||||
want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)},
|
||||
},
|
||||
{
|
||||
name: "Vertical line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))},
|
||||
want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)},
|
||||
},
|
||||
{
|
||||
name: "Left and down line intersects circle at two points",
|
||||
fields: fields{Center: pixel.V(5, 5), Radius: 1},
|
||||
args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))},
|
||||
want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := pixel.Circle{
|
||||
Center: tt.fields.Center,
|
||||
Radius: tt.fields.Radius,
|
||||
}
|
||||
got := c.IntersectionPoints(tt.args.l)
|
||||
for i, v := range got {
|
||||
if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) {
|
||||
t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
1169
geometry.go
1169
geometry.go
File diff suppressed because it is too large
Load Diff
1645
geometry_test.go
1645
geometry_test.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,699 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestLine_Bounds(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Rect
|
||||
}{
|
||||
{
|
||||
name: "Positive slope",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
want: pixel.R(0, 0, 10, 10),
|
||||
},
|
||||
{
|
||||
name: "Negative slope",
|
||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
||||
want: pixel.R(0, 0, 10, 10),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Bounds(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Bounds() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Center(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Positive slope",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Negative slope",
|
||||
fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Center(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Center() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Closest(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
v pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Point on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(0, 10)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to vertical line",
|
||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
||||
args: args{v: pixel.V(6, 5)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on next to horizontal line",
|
||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
||||
args: args{v: pixel.V(5, 6)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point far from line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(80, -70)},
|
||||
want: pixel.V(5, 5),
|
||||
},
|
||||
{
|
||||
name: "Point on inline with line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(20, 20)},
|
||||
want: pixel.V(10, 10),
|
||||
},
|
||||
{
|
||||
name: "Vertical line",
|
||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
||||
args: args{v: pixel.V(-1, 0)},
|
||||
want: pixel.V(0, 0),
|
||||
},
|
||||
{
|
||||
name: "Horizontal line",
|
||||
fields: fields{A: pixel.V(-10, 0), B: pixel.V(10, 0)},
|
||||
args: args{v: pixel.V(0, -1)},
|
||||
want: pixel.V(0, 0),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Closest(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Closest() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Contains(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
v pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Point on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Point on negative sloped line",
|
||||
fields: fields{A: pixel.V(0, 10), B: pixel.V(10, 0)},
|
||||
args: args{v: pixel.V(5, 5)},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Point not on line",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{v: pixel.V(0, 10)},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Contains(tt.args.v); got != tt.want {
|
||||
t.Errorf("Line.Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Formula(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantM float64
|
||||
wantB float64
|
||||
}{
|
||||
{
|
||||
name: "Getting formula - 45 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
wantM: 1,
|
||||
wantB: 0,
|
||||
},
|
||||
{
|
||||
name: "Getting formula - 90 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
||||
wantM: math.Inf(1),
|
||||
wantB: math.NaN(),
|
||||
},
|
||||
{
|
||||
name: "Getting formula - 0 degs",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 0)},
|
||||
wantM: 0,
|
||||
wantB: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
gotM, gotB := l.Formula()
|
||||
if gotM != tt.wantM {
|
||||
t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM)
|
||||
}
|
||||
if gotB != tt.wantB {
|
||||
if math.IsNaN(tt.wantB) && !math.IsNaN(gotB) {
|
||||
t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Intersect(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
k pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
want1 bool
|
||||
}{
|
||||
{
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(10, 0))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines intersect 2",
|
||||
fields: fields{A: pixel.V(5, 1), B: pixel.V(1, 1)},
|
||||
args: args{k: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
||||
want: pixel.V(2, 1),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Line intersect with vertical",
|
||||
fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Line intersect with horizontal",
|
||||
fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)},
|
||||
args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))},
|
||||
want: pixel.V(5, 5),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 10), pixel.V(1, 20))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect 2",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)},
|
||||
args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines don't intersect 3",
|
||||
fields: fields{A: pixel.V(2, 0), B: pixel.V(2, 3)},
|
||||
args: args{k: pixel.L(pixel.V(1, 5), pixel.V(5, 5))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "Lines parallel",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{k: pixel.L(pixel.V(0, 1), pixel.V(10, 11))},
|
||||
want: pixel.ZV,
|
||||
want1: false,
|
||||
}, {
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255))},
|
||||
want: pixel.V(849.1666666666666, 255),
|
||||
want1: true,
|
||||
},
|
||||
{
|
||||
name: "Lines intersect",
|
||||
fields: fields{A: pixel.V(600, 600), B: pixel.V(925, 150)},
|
||||
args: args{k: pixel.L(pixel.V(740, 255), pixel.V(925, 255.0001))},
|
||||
want: pixel.V(849.1666240490657, 255.000059008986),
|
||||
want1: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
got, got1 := l.Intersect(tt.args.k)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Intersect() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("Line.Intersect() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_IntersectCircle(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
c pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Cirle intersects",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(6, 4), 2)},
|
||||
want: pixel.V(0.5857864376269049, -0.5857864376269049),
|
||||
},
|
||||
{
|
||||
name: "Cirle doesn't intersects",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.IntersectCircle(tt.args.c); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.IntersectCircle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_IntersectRect(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
r pixel.Rect
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Line through rect vertically",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(-1, 1, 5, 5)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
{
|
||||
name: "Line through rect horizontally",
|
||||
fields: fields{A: pixel.V(0, 1), B: pixel.V(10, 1)},
|
||||
args: args{r: pixel.R(1, 0, 5, 5)},
|
||||
want: pixel.V(0, -1),
|
||||
},
|
||||
{
|
||||
name: "Line through rect diagonally bottom and left edges",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{r: pixel.R(0, 2, 3, 3)},
|
||||
want: pixel.V(-1, 1),
|
||||
},
|
||||
{
|
||||
name: "Line through rect diagonally top and right edges",
|
||||
fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(5, 0, 8, 3)},
|
||||
want: pixel.V(-2.5, -2.5),
|
||||
},
|
||||
{
|
||||
name: "Line with not rect intersect",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{r: pixel.R(20, 20, 21, 21)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Line intersects at 0,0",
|
||||
fields: fields{A: pixel.V(0, -10), B: pixel.V(0, 10)},
|
||||
args: args{r: pixel.R(-1, 0, 2, 2)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.IntersectRect(tt.args.r); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.IntersectRect() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Len(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "End right-up of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, 4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End left-up of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, 4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End right-down of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(3, -4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End left-down of start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, -4)},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "End same as start",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 0)},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Len(); got != tt.want {
|
||||
t.Errorf("Line.Len() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Rotated(t *testing.T) {
|
||||
// round returns the nearest integer, rounding ties away from zero.
|
||||
// This is required because `math.Round` wasn't introduced until Go1.10
|
||||
round := func(x float64) float64 {
|
||||
t := math.Trunc(x)
|
||||
if math.Abs(x-t) >= 0.5 {
|
||||
return t + math.Copysign(1, x)
|
||||
}
|
||||
return t
|
||||
}
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
around pixel.Vec
|
||||
angle float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Rotating around line center",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(2, 2), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(3, 3), pixel.V(1, 1)),
|
||||
},
|
||||
{
|
||||
name: "Rotating around x-y origin",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(0, 0), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)),
|
||||
},
|
||||
{
|
||||
name: "Rotating around line end",
|
||||
fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)},
|
||||
args: args{around: pixel.V(1, 1), angle: math.Pi},
|
||||
want: pixel.L(pixel.V(1, 1), pixel.V(-1, -1)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
// Have to round the results, due to floating-point in accuracies. Results are correct to approximately
|
||||
// 10 decimal places.
|
||||
got := l.Rotated(tt.args.around, tt.args.angle)
|
||||
if round(got.A.X) != tt.want.A.X ||
|
||||
round(got.B.X) != tt.want.B.X ||
|
||||
round(got.A.Y) != tt.want.A.Y ||
|
||||
round(got.B.Y) != tt.want.B.Y {
|
||||
t.Errorf("Line.Rotated() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_Scaled(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
scale float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Scaling by 1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by >1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 2},
|
||||
want: pixel.L(pixel.V(-5, -5), pixel.V(15, 15)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by <1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: 0.5},
|
||||
want: pixel.L(pixel.V(2.5, 2.5), pixel.V(7.5, 7.5)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by -1",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{scale: -1},
|
||||
want: pixel.L(pixel.V(10, 10), pixel.V(0, 0)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.Scaled(tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.Scaled() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_ScaledXY(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
around pixel.Vec
|
||||
scale float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Scaling by 1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by >1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 2},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(20, 20)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by <1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: 0.5},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(5, 5)),
|
||||
},
|
||||
{
|
||||
name: "Scaling by -1 around origin",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)},
|
||||
args: args{around: pixel.ZV, scale: -1},
|
||||
want: pixel.L(pixel.V(0, 0), pixel.V(-10, -10)),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.ScaledXY(tt.args.around, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Line.ScaledXY() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_String(t *testing.T) {
|
||||
type fields struct {
|
||||
A pixel.Vec
|
||||
B pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Getting string",
|
||||
fields: fields{A: pixel.V(0, 0), B: pixel.V(1, 1)},
|
||||
want: "Line(Vec(0, 0), Vec(1, 1))",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := pixel.Line{
|
||||
A: tt.fields.A,
|
||||
B: tt.fields.B,
|
||||
}
|
||||
if got := l.String(); got != tt.want {
|
||||
t.Errorf("Line.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package pixel
|
||||
|
||||
// Clamp returns x clamped to the interval [min, max].
|
||||
//
|
||||
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is
|
||||
// returned.
|
||||
func Clamp(x, min, max float64) float64 {
|
||||
if x < min {
|
||||
return min
|
||||
}
|
||||
if x > max {
|
||||
return max
|
||||
}
|
||||
return x
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
// closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them.
|
||||
// Effectively this compares two floats to a given decimal point.
|
||||
// Example:
|
||||
// closeEnough(100.125342432, 100.125, 2) == true
|
||||
// closeEnough(math.Pi, 3.14, 2) == true
|
||||
// closeEnough(0.1234, 0.1245, 3) == false
|
||||
func closeEnough(got, expected float64, decimalAccuracy int) bool {
|
||||
gotShifted := got * math.Pow10(decimalAccuracy)
|
||||
expectedShifted := expected * math.Pow10(decimalAccuracy)
|
||||
|
||||
return math.Trunc(gotShifted) == math.Trunc(expectedShifted)
|
||||
}
|
||||
|
||||
type clampTest struct {
|
||||
number float64
|
||||
min float64
|
||||
max float64
|
||||
expected float64
|
||||
}
|
||||
|
||||
func TestClamp(t *testing.T) {
|
||||
tests := []clampTest{
|
||||
{number: 1, min: 0, max: 5, expected: 1},
|
||||
{number: 2, min: 0, max: 5, expected: 2},
|
||||
{number: 8, min: 0, max: 5, expected: 5},
|
||||
{number: -5, min: 0, max: 5, expected: 0},
|
||||
{number: -5, min: -4, max: 5, expected: -4},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := pixel.Clamp(tc.number, tc.min, tc.max)
|
||||
if result != tc.expected {
|
||||
t.Error(fmt.Sprintf("Clamping %v with min %v and max %v should have given %v, but gave %v", tc.number, tc.min, tc.max, tc.expected, result))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such
|
||||
// as movement, scaling and rotations.
|
||||
//
|
||||
// Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
|
||||
// example:
|
||||
//
|
||||
// pixel.IM.Moved(pixel.V(100, 200)).Rotated(pixel.ZV, math.Pi/2)
|
||||
//
|
||||
// This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
|
||||
// vertically and then rotates everything by 90 degrees around the origin.
|
||||
//
|
||||
// Layout is:
|
||||
// [0] [2] [4]
|
||||
// [1] [3] [5]
|
||||
// 0 0 1 (implicit row)
|
||||
type Matrix [6]float64
|
||||
|
||||
// IM stands for identity matrix. Does nothing, no transformation.
|
||||
var IM = Matrix{1, 0, 0, 1, 0, 0}
|
||||
|
||||
// String returns a string representation of the Matrix.
|
||||
//
|
||||
// m := pixel.IM
|
||||
// fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
|
||||
func (m Matrix) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Matrix(%v %v %v | %v %v %v)",
|
||||
m[0], m[2], m[4],
|
||||
m[1], m[3], m[5],
|
||||
)
|
||||
}
|
||||
|
||||
// Moved moves everything by the delta vector.
|
||||
func (m Matrix) Moved(delta Vec) Matrix {
|
||||
m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// ScaledXY scales everything around a given point by the scale factor in each axis respectively.
|
||||
func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
|
||||
m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Scaled scales everything around a given point by the scale factor.
|
||||
func (m Matrix) Scaled(around Vec, scale float64) Matrix {
|
||||
return m.ScaledXY(around, V(scale, scale))
|
||||
}
|
||||
|
||||
// Rotated rotates everything around a given point by the given angle in radians.
|
||||
func (m Matrix) Rotated(around Vec, angle float64) Matrix {
|
||||
sint, cost := math.Sincos(angle)
|
||||
m[4], m[5] = m[4]-around.X, m[5]-around.Y
|
||||
m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
|
||||
m[4], m[5] = m[4]+around.X, m[5]+around.Y
|
||||
return m
|
||||
}
|
||||
|
||||
// Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
|
||||
// after the transformations of this Matrix.
|
||||
func (m Matrix) Chained(next Matrix) Matrix {
|
||||
return Matrix{
|
||||
next[0]*m[0] + next[2]*m[1],
|
||||
next[1]*m[0] + next[3]*m[1],
|
||||
next[0]*m[2] + next[2]*m[3],
|
||||
next[1]*m[2] + next[3]*m[3],
|
||||
next[0]*m[4] + next[2]*m[5] + next[4],
|
||||
next[1]*m[4] + next[3]*m[5] + next[5],
|
||||
}
|
||||
}
|
||||
|
||||
// Project applies all transformations added to the Matrix to a vector u and returns the result.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Project(u Vec) Vec {
|
||||
return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]}
|
||||
}
|
||||
|
||||
// Unproject does the inverse operation to Project.
|
||||
//
|
||||
// Time complexity is O(1).
|
||||
func (m Matrix) Unproject(u Vec) Vec {
|
||||
det := m[0]*m[3] - m[2]*m[1]
|
||||
return Vec{
|
||||
(m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det,
|
||||
(-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det,
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkMatrix(b *testing.B) {
|
||||
|
@ -61,3 +64,86 @@ func BenchmarkMatrix(b *testing.B) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatrix_Unproject(t *testing.T) {
|
||||
const delta = 1e-15
|
||||
t.Run("for rotated matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Rotated(pixel.ZV, math.Pi/2)
|
||||
unprojected := matrix.Unproject(pixel.V(0, 1))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
||||
})
|
||||
t.Run("for moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Moved(pixel.V(1, 2))
|
||||
unprojected := matrix.Unproject(pixel.V(2, 5))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 3, delta)
|
||||
})
|
||||
t.Run("for scaled matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Scaled(pixel.ZV, 2)
|
||||
unprojected := matrix.Unproject(pixel.V(2, 4))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
||||
})
|
||||
t.Run("for scaled, rotated and moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Scaled(pixel.ZV, 2).
|
||||
Rotated(pixel.ZV, math.Pi/2).
|
||||
Moved(pixel.V(2, 2))
|
||||
unprojected := matrix.Unproject(pixel.V(-2, 6))
|
||||
assert.InDelta(t, unprojected.X, 2, delta)
|
||||
assert.InDelta(t, unprojected.Y, 2, delta)
|
||||
})
|
||||
t.Run("for rotated and moved matrix", func(t *testing.T) {
|
||||
matrix := pixel.IM.
|
||||
Rotated(pixel.ZV, math.Pi/2).
|
||||
Moved(pixel.V(1, 1))
|
||||
unprojected := matrix.Unproject(pixel.V(1, 2))
|
||||
assert.InDelta(t, unprojected.X, 1, delta)
|
||||
assert.InDelta(t, unprojected.Y, 0, delta)
|
||||
})
|
||||
t.Run("for projected vertices using all kinds of matrices", func(t *testing.T) {
|
||||
namedMatrices := map[string]pixel.Matrix{
|
||||
"IM": pixel.IM,
|
||||
"Scaled": pixel.IM.Scaled(pixel.ZV, 0.5),
|
||||
"Scaled x 2": pixel.IM.Scaled(pixel.ZV, 2),
|
||||
"Rotated": pixel.IM.Rotated(pixel.ZV, math.Pi/4),
|
||||
"Moved": pixel.IM.Moved(pixel.V(0.5, 1)),
|
||||
"Moved 2": pixel.IM.Moved(pixel.V(-1, -0.5)),
|
||||
"Scaled and Rotated": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4),
|
||||
"Scaled, Rotated and Moved": pixel.IM.Scaled(pixel.ZV, 0.5).Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
||||
"Rotated and Moved": pixel.IM.Rotated(pixel.ZV, math.Pi/4).Moved(pixel.V(1, 2)),
|
||||
}
|
||||
vertices := [...]pixel.Vec{
|
||||
pixel.V(0, 0),
|
||||
pixel.V(5, 0),
|
||||
pixel.V(5, 10),
|
||||
pixel.V(0, 10),
|
||||
pixel.V(-5, 10),
|
||||
pixel.V(-5, 0),
|
||||
pixel.V(-5, -10),
|
||||
pixel.V(0, -10),
|
||||
pixel.V(5, -10),
|
||||
}
|
||||
for matrixName, matrix := range namedMatrices {
|
||||
for _, vertex := range vertices {
|
||||
testCase := fmt.Sprintf("for matrix %s and vertex %v", matrixName, vertex)
|
||||
t.Run(testCase, func(t *testing.T) {
|
||||
projected := matrix.Project(vertex)
|
||||
unprojected := matrix.Unproject(projected)
|
||||
assert.InDelta(t, vertex.X, unprojected.X, delta)
|
||||
assert.InDelta(t, vertex.Y, unprojected.Y, delta)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("for singular matrix", func(t *testing.T) {
|
||||
matrix := pixel.Matrix{0, 0, 0, 0, 0, 0}
|
||||
unprojected := matrix.Unproject(pixel.ZV)
|
||||
assert.True(t, math.IsNaN(unprojected.X))
|
||||
assert.True(t, math.IsNaN(unprojected.Y))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -54,8 +54,9 @@ func TestSprite_Draw(t *testing.T) {
|
|||
sprite := pixel.NewSprite(pic, pic.Bounds())
|
||||
|
||||
cfg := pixelgl.WindowConfig{
|
||||
Title: "testing",
|
||||
Bounds: pixel.R(0, 0, 150, 150),
|
||||
Title: "testing",
|
||||
Bounds: pixel.R(0, 0, 150, 150),
|
||||
Invisible: true,
|
||||
}
|
||||
|
||||
win, err := pixelgl.NewWindow(cfg)
|
||||
|
|
|
@ -0,0 +1,284 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two
|
||||
// points, Min and Max.
|
||||
//
|
||||
// The invariant should hold, that Max's components are greater or equal than Min's components
|
||||
// respectively.
|
||||
type Rect struct {
|
||||
Min, Max Vec
|
||||
}
|
||||
|
||||
// ZR is a zero rectangle.
|
||||
var ZR = Rect{Min: ZV, Max: ZV}
|
||||
|
||||
// R returns a new Rect with given the Min and Max coordinates.
|
||||
//
|
||||
// Note that the returned rectangle is not automatically normalized.
|
||||
func R(minX, minY, maxX, maxY float64) Rect {
|
||||
return Rect{
|
||||
Min: Vec{minX, minY},
|
||||
Max: Vec{maxX, maxY},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Rect.
|
||||
//
|
||||
// r := pixel.R(100, 50, 200, 300)
|
||||
// r.String() // returns "Rect(100, 50, 200, 300)"
|
||||
// fmt.Println(r) // Rect(100, 50, 200, 300)
|
||||
func (r Rect) String() string {
|
||||
return fmt.Sprintf("Rect(%v, %v, %v, %v)", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
||||
}
|
||||
|
||||
// Norm returns the Rect in normal form, such that Max is component-wise greater or equal than Min.
|
||||
func (r Rect) Norm() Rect {
|
||||
return Rect{
|
||||
Min: Vec{
|
||||
math.Min(r.Min.X, r.Max.X),
|
||||
math.Min(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
Max: Vec{
|
||||
math.Max(r.Min.X, r.Max.X),
|
||||
math.Max(r.Min.Y, r.Max.Y),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// W returns the width of the Rect.
|
||||
func (r Rect) W() float64 {
|
||||
return r.Max.X - r.Min.X
|
||||
}
|
||||
|
||||
// H returns the height of the Rect.
|
||||
func (r Rect) H() float64 {
|
||||
return r.Max.Y - r.Min.Y
|
||||
}
|
||||
|
||||
// Size returns the vector of width and height of the Rect.
|
||||
func (r Rect) Size() Vec {
|
||||
return V(r.W(), r.H())
|
||||
}
|
||||
|
||||
// Area returns the area of r. If r is not normalized, area may be negative.
|
||||
func (r Rect) Area() float64 {
|
||||
return r.W() * r.H()
|
||||
}
|
||||
|
||||
// Edges will return the four lines which make up the edges of the rectangle.
|
||||
func (r Rect) Edges() [4]Line {
|
||||
corners := r.Vertices()
|
||||
|
||||
return [4]Line{
|
||||
{A: corners[0], B: corners[1]},
|
||||
{A: corners[1], B: corners[2]},
|
||||
{A: corners[2], B: corners[3]},
|
||||
{A: corners[3], B: corners[0]},
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor is a vector used to define anchors, such as `Center`, `Top`, `TopRight`, etc.
|
||||
type Anchor Vec
|
||||
|
||||
var (
|
||||
Center = Anchor{0.5, 0.5}
|
||||
Top = Anchor{0.5, 0}
|
||||
TopRight = Anchor{0, 0}
|
||||
Right = Anchor{0, 0.5}
|
||||
BottomRight = Anchor{0, 1}
|
||||
Bottom = Anchor{0.5, 1}
|
||||
BottomLeft = Anchor{1, 1}
|
||||
Left = Anchor{1, 0.5}
|
||||
TopLeft = Anchor{1, 0}
|
||||
)
|
||||
|
||||
var anchorStrings map[Anchor]string = map[Anchor]string{
|
||||
Center: "center",
|
||||
Top: "top",
|
||||
TopRight: "top-right",
|
||||
Right: "right",
|
||||
BottomRight: "bottom-right",
|
||||
Bottom: "bottom",
|
||||
BottomLeft: "bottom-left",
|
||||
Left: "left",
|
||||
TopLeft: "top-left",
|
||||
}
|
||||
|
||||
// String returns the string representation of an anchor.
|
||||
func (anchor Anchor) String() string {
|
||||
return anchorStrings[anchor]
|
||||
}
|
||||
|
||||
var oppositeAnchors map[Anchor]Anchor = map[Anchor]Anchor{
|
||||
Center: Center,
|
||||
Top: Bottom,
|
||||
Bottom: Top,
|
||||
Right: Left,
|
||||
Left: Right,
|
||||
TopRight: BottomLeft,
|
||||
BottomLeft: TopRight,
|
||||
BottomRight: TopLeft,
|
||||
TopLeft: BottomRight,
|
||||
}
|
||||
|
||||
// Opposite returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).
|
||||
func (anchor Anchor) Opposite() Anchor {
|
||||
return oppositeAnchors[anchor]
|
||||
}
|
||||
|
||||
// AnchorPos returns the relative position of the given anchor.
|
||||
func (r Rect) AnchorPos(anchor Anchor) Vec {
|
||||
return r.Size().ScaledXY(V(0, 0).Sub(Vec(anchor)))
|
||||
}
|
||||
|
||||
// AlignedTo returns the rect moved by the given anchor.
|
||||
func (rect Rect) AlignedTo(anchor Anchor) Rect {
|
||||
return rect.Moved(rect.AnchorPos(anchor))
|
||||
}
|
||||
|
||||
// Center returns the position of the center of the Rect.
|
||||
// `rect.Center()` is equivalent to `rect.Anchor(pixel.Anchor.Center)`
|
||||
func (r Rect) Center() Vec {
|
||||
return Lerp(r.Min, r.Max, 0.5)
|
||||
}
|
||||
|
||||
// Moved returns the Rect moved (both Min and Max) by the given vector delta.
|
||||
func (r Rect) Moved(delta Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min.Add(delta),
|
||||
Max: r.Max.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
// Resized returns the Rect resized to the given size while keeping the position of the given
|
||||
// anchor.
|
||||
//
|
||||
// r.Resized(r.Min, size) // resizes while keeping the position of the lower-left corner
|
||||
// r.Resized(r.Max, size) // same with the top-right corner
|
||||
// r.Resized(r.Center(), size) // resizes around the center
|
||||
//
|
||||
// This function does not make sense for resizing a rectangle of zero area and will panic. Use
|
||||
// ResizedMin in the case of zero area.
|
||||
func (r Rect) Resized(anchor, size Vec) Rect {
|
||||
if r.W()*r.H() == 0 {
|
||||
panic(fmt.Errorf("(%T).Resize: zero area", r))
|
||||
}
|
||||
fraction := Vec{size.X / r.W(), size.Y / r.H()}
|
||||
return Rect{
|
||||
Min: anchor.Add(r.Min.Sub(anchor).ScaledXY(fraction)),
|
||||
Max: anchor.Add(r.Max.Sub(anchor).ScaledXY(fraction)),
|
||||
}
|
||||
}
|
||||
|
||||
// ResizedMin returns the Rect resized to the given size while keeping the position of the Rect's
|
||||
// Min.
|
||||
//
|
||||
// Sizes of zero area are safe here.
|
||||
func (r Rect) ResizedMin(size Vec) Rect {
|
||||
return Rect{
|
||||
Min: r.Min,
|
||||
Max: r.Min.Add(size),
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks whether a vector u is contained within this Rect (including it's borders).
|
||||
func (r Rect) Contains(u Vec) bool {
|
||||
return r.Min.X <= u.X && u.X <= r.Max.X && r.Min.Y <= u.Y && u.Y <= r.Max.Y
|
||||
}
|
||||
|
||||
// Union returns the minimal Rect which covers both r and s. Rects r and s must be normalized.
|
||||
func (r Rect) Union(s Rect) Rect {
|
||||
return R(
|
||||
math.Min(r.Min.X, s.Min.X),
|
||||
math.Min(r.Min.Y, s.Min.Y),
|
||||
math.Max(r.Max.X, s.Max.X),
|
||||
math.Max(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// Intersect returns the maximal Rect which is covered by both r and s. Rects r and s must be normalized.
|
||||
//
|
||||
// If r and s don't overlap, this function returns a zero-rectangle.
|
||||
func (r Rect) Intersect(s Rect) Rect {
|
||||
t := R(
|
||||
math.Max(r.Min.X, s.Min.X),
|
||||
math.Max(r.Min.Y, s.Min.Y),
|
||||
math.Min(r.Max.X, s.Max.X),
|
||||
math.Min(r.Max.Y, s.Max.Y),
|
||||
)
|
||||
if t.Min.X >= t.Max.X || t.Min.Y >= t.Max.Y {
|
||||
return ZR
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Intersects returns whether or not the given Rect intersects at any point with this Rect.
|
||||
//
|
||||
// This function is overall about 5x faster than Intersect, so it is better
|
||||
// to use if you have no need for the returned Rect from Intersect.
|
||||
func (r Rect) Intersects(s Rect) bool {
|
||||
return !(s.Max.X < r.Min.X ||
|
||||
s.Min.X > r.Max.X ||
|
||||
s.Max.Y < r.Min.Y ||
|
||||
s.Min.Y > r.Max.Y)
|
||||
}
|
||||
|
||||
// IntersectCircle returns a minimal required Vector, such that moving the rect by that vector would stop the Circle
|
||||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only
|
||||
// the perimeters touch.
|
||||
//
|
||||
// This function will return a non-zero vector if:
|
||||
// - The Rect contains the Circle, partially or fully
|
||||
// - The Circle contains the Rect, partially of fully
|
||||
func (r Rect) IntersectCircle(c Circle) Vec {
|
||||
return c.IntersectRect(r).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no
|
||||
// longer intersect.
|
||||
func (r Rect) IntersectLine(l Line) Vec {
|
||||
return l.IntersectRect(r).Scaled(-1)
|
||||
}
|
||||
|
||||
// IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or
|
||||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of
|
||||
// closest-to-l.A to closest-to-l.B.
|
||||
func (r Rect) IntersectionPoints(l Line) []Vec {
|
||||
// Use map keys to ensure unique points
|
||||
pointMap := make(map[Vec]struct{})
|
||||
|
||||
for _, edge := range r.Edges() {
|
||||
if intersect, ok := l.Intersect(edge); ok {
|
||||
pointMap[intersect] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
points := make([]Vec, 0, len(pointMap))
|
||||
for point := range pointMap {
|
||||
points = append(points, point)
|
||||
}
|
||||
|
||||
// Order the points
|
||||
if len(points) == 2 {
|
||||
if points[1].To(l.A).Len() < points[0].To(l.A).Len() {
|
||||
return []Vec{points[1], points[0]}
|
||||
}
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
// Vertices returns a slice of the four corners which make up the rectangle.
|
||||
func (r Rect) Vertices() [4]Vec {
|
||||
return [4]Vec{
|
||||
r.Min,
|
||||
V(r.Min.X, r.Max.Y),
|
||||
r.Max,
|
||||
V(r.Max.X, r.Min.Y),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
func TestRect_Resize(t *testing.T) {
|
||||
type rectTestTransform struct {
|
||||
name string
|
||||
f func(pixel.Rect) pixel.Rect
|
||||
}
|
||||
|
||||
// rectangles
|
||||
squareAroundOrigin := pixel.R(-10, -10, 10, 10)
|
||||
squareAround2020 := pixel.R(10, 10, 30, 30)
|
||||
rectangleAroundOrigin := pixel.R(-20, -10, 20, 10)
|
||||
rectangleAround2020 := pixel.R(0, 10, 40, 30)
|
||||
|
||||
// resize transformations
|
||||
resizeByHalfAroundCenter := rectTestTransform{"by half around center", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Center(), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMin := rectTestTransform{"by half around Min", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Min, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMax := rectTestTransform{"by half around Max", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(rect.Max, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundMiddleOfLeftSide := rectTestTransform{"by half around middle of left side", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.V(rect.Min.X, rect.Center().Y), rect.Size().Scaled(0.5))
|
||||
}}
|
||||
resizeByHalfAroundOrigin := rectTestTransform{"by half around the origin", func(rect pixel.Rect) pixel.Rect {
|
||||
return rect.Resized(pixel.ZV, rect.Size().Scaled(0.5))
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
input pixel.Rect
|
||||
transform rectTestTransform
|
||||
answer pixel.Rect
|
||||
}{
|
||||
{squareAroundOrigin, resizeByHalfAroundCenter, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundCenter, pixel.R(15, 15, 25, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundCenter, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundCenter, pixel.R(10, 15, 30, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMin, pixel.R(-10, -10, 0, 0)},
|
||||
{squareAround2020, resizeByHalfAroundMin, pixel.R(10, 10, 20, 20)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMin, pixel.R(-20, -10, 0, 0)},
|
||||
{rectangleAround2020, resizeByHalfAroundMin, pixel.R(0, 10, 20, 20)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 10, 10)},
|
||||
{squareAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 30, 30)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMax, pixel.R(0, 0, 20, 10)},
|
||||
{rectangleAround2020, resizeByHalfAroundMax, pixel.R(20, 20, 40, 30)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-10, -5, 0, 5)},
|
||||
{squareAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(10, 15, 20, 25)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundMiddleOfLeftSide, pixel.R(-20, -5, 0, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundMiddleOfLeftSide, pixel.R(0, 15, 20, 25)},
|
||||
|
||||
{squareAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-5, -5, 5, 5)},
|
||||
{squareAround2020, resizeByHalfAroundOrigin, pixel.R(5, 5, 15, 15)},
|
||||
{rectangleAroundOrigin, resizeByHalfAroundOrigin, pixel.R(-10, -5, 10, 5)},
|
||||
{rectangleAround2020, resizeByHalfAroundOrigin, pixel.R(0, 5, 20, 15)},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("Resize %v %s", testCase.input, testCase.transform.name), func(t *testing.T) {
|
||||
testResult := testCase.transform.f(testCase.input)
|
||||
if testResult != testCase.answer {
|
||||
t.Errorf("Got: %v, wanted: %v\n", testResult, testCase.answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_Edges(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want [4]pixel.Line
|
||||
}{
|
||||
{
|
||||
name: "Get edges",
|
||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
||||
want: [4]pixel.Line{
|
||||
pixel.L(pixel.V(0, 0), pixel.V(0, 10)),
|
||||
pixel.L(pixel.V(0, 10), pixel.V(10, 10)),
|
||||
pixel.L(pixel.V(10, 10), pixel.V(10, 0)),
|
||||
pixel.L(pixel.V(10, 0), pixel.V(0, 0)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.Edges(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.Edges() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_Vertices(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want [4]pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Get corners",
|
||||
fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)},
|
||||
want: [4]pixel.Vec{
|
||||
pixel.V(0, 0),
|
||||
pixel.V(0, 10),
|
||||
pixel.V(10, 10),
|
||||
pixel.V(10, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_IntersectCircle(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
c pixel.Circle
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "Rect.IntersectCircle(): no overlap",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(50, 50), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle contains rect",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 5), 10)},
|
||||
want: pixel.V(-15, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): rect contains circle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 5), 1)},
|
||||
want: pixel.V(-6, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom-left corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-0.5, -0.5), 1)},
|
||||
want: pixel.V(-0.2, -0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top-left corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-0.5, 10.5), 1)},
|
||||
want: pixel.V(-0.2, 0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom-right corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10.5, -0.5), 1)},
|
||||
want: pixel.V(0.2, -0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top-right corner",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10.5, 10.5), 1)},
|
||||
want: pixel.V(0.2, 0.2),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps two corners",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 6)},
|
||||
want: pixel.V(6, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps left edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(0, 5), 1)},
|
||||
want: pixel.V(1, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps bottom edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 0), 1)},
|
||||
want: pixel.V(0, 1),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps right edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(10, 5), 1)},
|
||||
want: pixel.V(-1, 0),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle overlaps top edge",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 10), 1)},
|
||||
want: pixel.V(0, -1),
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): edge is tangent of left side",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): edge is tangent of top side",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, -1), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle above rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, 12), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle below rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(5, -2), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle left of rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(-1, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
{
|
||||
name: "Rect.IntersectCircle(): circle right of rectangle",
|
||||
fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)},
|
||||
args: args{c: pixel.C(pixel.V(11, 5), 1)},
|
||||
want: pixel.ZV,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
got := r.IntersectCircle(tt.args.c)
|
||||
if !closeEnough(got.X, tt.want.X, 2) || !closeEnough(got.Y, tt.want.Y, 2) {
|
||||
t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRect_IntersectionPoints(t *testing.T) {
|
||||
type fields struct {
|
||||
Min pixel.Vec
|
||||
Max pixel.Vec
|
||||
}
|
||||
type args struct {
|
||||
l pixel.Line
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []pixel.Vec
|
||||
}{
|
||||
{
|
||||
name: "No intersection points",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))},
|
||||
want: []pixel.Vec{},
|
||||
},
|
||||
{
|
||||
name: "One intersection point",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))},
|
||||
want: []pixel.Vec{pixel.V(2, 1)},
|
||||
},
|
||||
{
|
||||
name: "Two intersection points",
|
||||
fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)},
|
||||
args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))},
|
||||
want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := pixel.Rect{
|
||||
Min: tt.fields.Min,
|
||||
Max: tt.fields.Max,
|
||||
}
|
||||
if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRect_Intersect(b *testing.B) {
|
||||
root := pixel.R(10, 10, 50, 50)
|
||||
inter := pixel.R(11, 11, 15, 15)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if root.Intersect(inter) != pixel.ZR {
|
||||
// do a thing
|
||||
}
|
||||
|
||||
// do a thing
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRect_IsIntersect(b *testing.B) {
|
||||
root := pixel.R(10, 10, 50, 50)
|
||||
inter := pixel.R(11, 11, 15, 15)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if root.Intersects(inter) {
|
||||
// do a thing
|
||||
}
|
||||
|
||||
// do a thing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,457 @@
|
|||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Vec is a 2D vector type with X and Y coordinates.
|
||||
//
|
||||
// Create vectors with the V constructor:
|
||||
//
|
||||
// u := pixel.V(1, 2)
|
||||
// v := pixel.V(8, -3)
|
||||
//
|
||||
// Use various methods to manipulate them:
|
||||
//
|
||||
// w := u.Add(v)
|
||||
// fmt.Println(w) // Vec(9, -1)
|
||||
// fmt.Println(u.Sub(v)) // Vec(-7, 5)
|
||||
// u = pixel.V(2, 3)
|
||||
// v = pixel.V(8, 1)
|
||||
// if u.X < 0 {
|
||||
// fmt.Println("this won't happen")
|
||||
// }
|
||||
// x := u.Unit().Dot(v.Unit())
|
||||
type Vec struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// ZV is a zero vector.
|
||||
var ZV = Vec{0, 0}
|
||||
|
||||
// V returns a new 2D vector with the given coordinates.
|
||||
func V(x, y float64) Vec {
|
||||
return Vec{x, y}
|
||||
}
|
||||
|
||||
// nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the
|
||||
// result is correct to 7 significant digits.
|
||||
func nearlyEqual(a, b float64) bool {
|
||||
epsilon := 0.000001
|
||||
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
diff := math.Abs(a - b)
|
||||
|
||||
if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
|
||||
return diff < (epsilon * math.SmallestNonzeroFloat64)
|
||||
}
|
||||
|
||||
absA := math.Abs(a)
|
||||
absB := math.Abs(b)
|
||||
|
||||
return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon
|
||||
}
|
||||
|
||||
// Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result
|
||||
// is correct to 7 significant digits.
|
||||
func (u Vec) Eq(v Vec) bool {
|
||||
return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y)
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the given angle.
|
||||
func Unit(angle float64) Vec {
|
||||
return Vec{1, 0}.Rotated(angle)
|
||||
}
|
||||
|
||||
// String returns the string representation of the vector u.
|
||||
//
|
||||
// u := pixel.V(4.5, -1.3)
|
||||
// u.String() // returns "Vec(4.5, -1.3)"
|
||||
// fmt.Println(u) // Vec(4.5, -1.3)
|
||||
func (u Vec) String() string {
|
||||
return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y)
|
||||
}
|
||||
|
||||
// XY returns the components of the vector in two return values.
|
||||
func (u Vec) XY() (x, y float64) {
|
||||
return u.X, u.Y
|
||||
}
|
||||
|
||||
// Add returns the sum of vectors u and v.
|
||||
func (u Vec) Add(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X + v.X,
|
||||
u.Y + v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Sub returns the difference betweeen vectors u and v.
|
||||
func (u Vec) Sub(v Vec) Vec {
|
||||
return Vec{
|
||||
u.X - v.X,
|
||||
u.Y - v.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Floor converts x and y to their integer equivalents.
|
||||
func (u Vec) Floor() Vec {
|
||||
return Vec{
|
||||
math.Floor(u.X),
|
||||
math.Floor(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// To returns the vector from u to v. Equivalent to v.Sub(u).
|
||||
func (u Vec) To(v Vec) Vec {
|
||||
return Vec{
|
||||
v.X - u.X,
|
||||
v.Y - u.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Scaled returns the vector u multiplied by c.
|
||||
func (u Vec) Scaled(c float64) Vec {
|
||||
return Vec{u.X * c, u.Y * c}
|
||||
}
|
||||
|
||||
// ScaledXY returns the vector u multiplied by the vector v component-wise.
|
||||
func (u Vec) ScaledXY(v Vec) Vec {
|
||||
return Vec{u.X * v.X, u.Y * v.Y}
|
||||
}
|
||||
|
||||
// Len returns the length of the vector u.
|
||||
func (u Vec) Len() float64 {
|
||||
return math.Hypot(u.X, u.Y)
|
||||
}
|
||||
|
||||
// Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi].
|
||||
func (u Vec) Angle() float64 {
|
||||
return math.Atan2(u.Y, u.X)
|
||||
}
|
||||
|
||||
// Unit returns a vector of length 1 facing the direction of u (has the same angle).
|
||||
func (u Vec) Unit() Vec {
|
||||
if u.X == 0 && u.Y == 0 {
|
||||
return Vec{1, 0}
|
||||
}
|
||||
return u.Scaled(1 / u.Len())
|
||||
}
|
||||
|
||||
// Rotated returns the vector u rotated by the given angle in radians.
|
||||
func (u Vec) Rotated(angle float64) Vec {
|
||||
sin, cos := math.Sincos(angle)
|
||||
return Vec{
|
||||
u.X*cos - u.Y*sin,
|
||||
u.X*sin + u.Y*cos,
|
||||
}
|
||||
}
|
||||
|
||||
// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster.
|
||||
func (u Vec) Normal() Vec {
|
||||
return Vec{-u.Y, u.X}
|
||||
}
|
||||
|
||||
// Dot returns the dot product of vectors u and v.
|
||||
func (u Vec) Dot(v Vec) float64 {
|
||||
return u.X*v.X + u.Y*v.Y
|
||||
}
|
||||
|
||||
// Cross return the cross product of vectors u and v.
|
||||
func (u Vec) Cross(v Vec) float64 {
|
||||
return u.X*v.Y - v.X*u.Y
|
||||
}
|
||||
|
||||
// Project returns a projection (or component) of vector u in the direction of vector v.
|
||||
//
|
||||
// Behaviour is undefined if v is a zero vector.
|
||||
func (u Vec) Project(v Vec) Vec {
|
||||
len := u.Dot(v) / v.Len()
|
||||
return v.Unit().Scaled(len)
|
||||
}
|
||||
|
||||
// Map applies the function f to both x and y components of the vector u and returns the modified
|
||||
// vector.
|
||||
//
|
||||
// u := pixel.V(10.5, -1.5)
|
||||
// v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored
|
||||
func (u Vec) Map(f func(float64) float64) Vec {
|
||||
return Vec{
|
||||
f(u.X),
|
||||
f(u.Y),
|
||||
}
|
||||
}
|
||||
|
||||
// Lerp returns a linear interpolation between vectors a and b.
|
||||
//
|
||||
// This function basically returns a point along the line between a and b and t chooses which one.
|
||||
// If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will
|
||||
// return the appropriate point between a and b and so on.
|
||||
func Lerp(a, b Vec, t float64) Vec {
|
||||
return a.Scaled(1 - t).Add(b.Scaled(t))
|
||||
}
|
||||
|
||||
// Line is a 2D line segment, between points A and B.
|
||||
type Line struct {
|
||||
A, B Vec
|
||||
}
|
||||
|
||||
// L creates and returns a new Line.
|
||||
func L(from, to Vec) Line {
|
||||
return Line{
|
||||
A: from,
|
||||
B: to,
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds returns the lines bounding box. This is in the form of a normalized Rect.
|
||||
func (l Line) Bounds() Rect {
|
||||
return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm()
|
||||
}
|
||||
|
||||
// Center will return the point at center of the line; that is, the point equidistant from either end.
|
||||
func (l Line) Center() Vec {
|
||||
return l.A.Add(l.A.To(l.B).Scaled(0.5))
|
||||
}
|
||||
|
||||
// Closest will return the point on the line which is closest to the Vec provided.
|
||||
func (l Line) Closest(v Vec) Vec {
|
||||
// between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b)
|
||||
between := func(a, b, x float64) bool {
|
||||
min := math.Min(a, b)
|
||||
max := math.Max(a, b)
|
||||
return min < x && x < max
|
||||
}
|
||||
|
||||
// Closest point will be on a line which perpendicular to this line.
|
||||
// If and only if the infinite perpendicular line intersects the segment.
|
||||
m, b := l.Formula()
|
||||
|
||||
// Account for horizontal lines
|
||||
if m == 0 {
|
||||
x := v.X
|
||||
y := l.A.Y
|
||||
|
||||
// check if the X coordinate of v is on the line
|
||||
if between(l.A.X, l.B.X, v.X) {
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Otherwise get the closest endpoint
|
||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
// Account for vertical lines
|
||||
if math.IsInf(math.Abs(m), 1) {
|
||||
x := l.A.X
|
||||
y := v.Y
|
||||
|
||||
// check if the Y coordinate of v is on the line
|
||||
if between(l.A.Y, l.B.Y, v.Y) {
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Otherwise get the closest endpoint
|
||||
if l.A.To(v).Len() < l.B.To(v).Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
perpendicularM := -1 / m
|
||||
perpendicularB := v.Y - (perpendicularM * v.X)
|
||||
|
||||
// Coordinates of intersect (of infinite lines)
|
||||
x := (perpendicularB - b) / (m - perpendicularM)
|
||||
y := m*x + b
|
||||
|
||||
// Check if the point lies between the x and y bounds of the segment
|
||||
if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) {
|
||||
// Not within bounding box
|
||||
toStart := v.To(l.A)
|
||||
toEnd := v.To(l.B)
|
||||
|
||||
if toStart.Len() < toEnd.Len() {
|
||||
return l.A
|
||||
}
|
||||
return l.B
|
||||
}
|
||||
|
||||
return V(x, y)
|
||||
}
|
||||
|
||||
// Contains returns whether the provided Vec lies on the line.
|
||||
func (l Line) Contains(v Vec) bool {
|
||||
return l.Closest(v).Eq(v)
|
||||
}
|
||||
|
||||
// Formula will return the values that represent the line in the formula: y = mx + b
|
||||
// This function will return math.Inf+, math.Inf- for a vertical line.
|
||||
func (l Line) Formula() (m, b float64) {
|
||||
// Account for horizontal lines
|
||||
if l.B.Y == l.A.Y {
|
||||
return 0, l.A.Y
|
||||
}
|
||||
|
||||
m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X)
|
||||
b = l.A.Y - (m * l.A.X)
|
||||
|
||||
return m, b
|
||||
}
|
||||
|
||||
// Intersect will return the point of intersection for the two line segments. If the line segments do not intersect,
|
||||
// this function will return the zero-vector and false.
|
||||
func (l Line) Intersect(k Line) (Vec, bool) {
|
||||
// Check if the lines are parallel
|
||||
lDir := l.A.To(l.B)
|
||||
kDir := k.A.To(k.B)
|
||||
if lDir.X == kDir.X && lDir.Y == kDir.Y {
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
// The lines intersect - but potentially not within the line segments.
|
||||
// Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the
|
||||
// segments
|
||||
lm, lb := l.Formula()
|
||||
km, kb := k.Formula()
|
||||
|
||||
// Account for vertical lines
|
||||
if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) {
|
||||
// Both vertical, therefore parallel
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
var x, y float64
|
||||
|
||||
if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) {
|
||||
// One line is vertical
|
||||
intersectM := lm
|
||||
intersectB := lb
|
||||
verticalLine := k
|
||||
|
||||
if math.IsInf(math.Abs(lm), 1) {
|
||||
intersectM = km
|
||||
intersectB = kb
|
||||
verticalLine = l
|
||||
}
|
||||
|
||||
y = intersectM*verticalLine.A.X + intersectB
|
||||
x = verticalLine.A.X
|
||||
} else {
|
||||
// Coordinates of intersect
|
||||
x = (kb - lb) / (lm - km)
|
||||
y = lm*x + lb
|
||||
}
|
||||
|
||||
if l.Contains(V(x, y)) && k.Contains(V(x, y)) {
|
||||
// The intersect point is on both line segments, they intersect.
|
||||
return V(x, y), true
|
||||
}
|
||||
|
||||
return ZV, false
|
||||
}
|
||||
|
||||
// IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle
|
||||
// to no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
||||
func (l Line) IntersectCircle(c Circle) Vec {
|
||||
// Get the point on the line closest to the center of the circle.
|
||||
closest := l.Closest(c.Center)
|
||||
cirToClosest := c.Center.To(closest)
|
||||
|
||||
if cirToClosest.Len() >= c.Radius {
|
||||
return ZV
|
||||
}
|
||||
|
||||
return cirToClosest.Scaled(cirToClosest.Len() - c.Radius)
|
||||
}
|
||||
|
||||
// IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to
|
||||
// no longer intesect. If they do not intersect at all, this function will return a zero-vector.
|
||||
func (l Line) IntersectRect(r Rect) Vec {
|
||||
// Check if either end of the line segment are within the rectangle
|
||||
if r.Contains(l.A) || r.Contains(l.B) {
|
||||
// Use the Rect.Intersect to get minimal return value
|
||||
rIntersect := l.Bounds().Intersect(r)
|
||||
if rIntersect.H() > rIntersect.W() {
|
||||
// Go vertical
|
||||
return V(0, rIntersect.H())
|
||||
}
|
||||
return V(rIntersect.W(), 0)
|
||||
}
|
||||
|
||||
// Check if any of the rectangles' edges intersect with this line.
|
||||
for _, edge := range r.Edges() {
|
||||
if _, ok := l.Intersect(edge); ok {
|
||||
// Get the closest points on the line to each corner, where:
|
||||
// - the point is contained by the rectangle
|
||||
// - the point is not the corner itself
|
||||
corners := r.Vertices()
|
||||
var closest *Vec
|
||||
closestCorner := corners[0]
|
||||
for _, c := range corners {
|
||||
cc := l.Closest(c)
|
||||
if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) {
|
||||
closest = &cc
|
||||
closestCorner = c
|
||||
}
|
||||
}
|
||||
|
||||
return closest.To(closestCorner)
|
||||
}
|
||||
}
|
||||
|
||||
// No intersect
|
||||
return ZV
|
||||
}
|
||||
|
||||
// Len returns the length of the line segment.
|
||||
func (l Line) Len() float64 {
|
||||
return l.A.To(l.B).Len()
|
||||
}
|
||||
|
||||
// Moved will return a line moved by the delta Vec provided.
|
||||
func (l Line) Moved(delta Vec) Line {
|
||||
return Line{
|
||||
A: l.A.Add(delta),
|
||||
B: l.B.Add(delta),
|
||||
}
|
||||
}
|
||||
|
||||
// Rotated will rotate the line around the provided Vec.
|
||||
func (l Line) Rotated(around Vec, angle float64) Line {
|
||||
// Move the line so we can use `Vec.Rotated`
|
||||
lineShifted := l.Moved(around.Scaled(-1))
|
||||
|
||||
lineRotated := Line{
|
||||
A: lineShifted.A.Rotated(angle),
|
||||
B: lineShifted.B.Rotated(angle),
|
||||
}
|
||||
|
||||
return lineRotated.Moved(around)
|
||||
}
|
||||
|
||||
// Scaled will return the line scaled around the center point.
|
||||
func (l Line) Scaled(scale float64) Line {
|
||||
return l.ScaledXY(l.Center(), scale)
|
||||
}
|
||||
|
||||
// ScaledXY will return the line scaled around the Vec provided.
|
||||
func (l Line) ScaledXY(around Vec, scale float64) Line {
|
||||
toA := around.To(l.A).Scaled(scale)
|
||||
toB := around.To(l.B).Scaled(scale)
|
||||
|
||||
return Line{
|
||||
A: around.Add(toA),
|
||||
B: around.Add(toB),
|
||||
}
|
||||
}
|
||||
|
||||
func (l Line) String() string {
|
||||
return fmt.Sprintf("Line(%v, %v)", l.A, l.B)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package pixel_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/faiface/pixel"
|
||||
)
|
||||
|
||||
type floorTest struct {
|
||||
input pixel.Vec
|
||||
expected pixel.Vec
|
||||
}
|
||||
|
||||
func TestFloor(t *testing.T) {
|
||||
tests := []floorTest{
|
||||
{input: pixel.V(4.50, 6.70), expected: pixel.V(4, 6)},
|
||||
{input: pixel.V(9.0, 6.70), expected: pixel.V(9, 6)},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := tc.input.Floor()
|
||||
if result != tc.expected {
|
||||
t.Error(fmt.Sprintf("Expected %v but got %v", tc.expected, result))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue