diff --git a/geometry.go b/geometry.go index 1e2922d..8ccab8d 100644 --- a/geometry.go +++ b/geometry.go @@ -181,6 +181,268 @@ 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) == 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() + closest := ZV + closestCorner := corners[0] + for _, c := range corners { + cc := l.Closest(c) + if closest == ZV || (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) +} + // Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two // points, Min and Max. // @@ -243,6 +505,18 @@ 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]}, + } +} + // Center returns the position of the center of the Rect. func (r Rect) Center() Vec { return Lerp(r.Min, r.Max, 0.5) @@ -329,6 +603,50 @@ 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), + } +} + // Circle is a 2D circle. It is defined by two properties: // - Center vector // - Radius float64 @@ -398,6 +716,12 @@ func (c Circle) Contains(u Vec) bool { 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 { @@ -476,6 +800,12 @@ func (c Circle) Intersect(d Circle) Circle { } } +// 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 (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. @@ -552,6 +882,99 @@ func (c Circle) IntersectRect(r Rect) Vec { } } +// 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} +} + // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such // as movement, scaling and rotations. // diff --git a/geometry_test.go b/geometry_test.go index dfa78cf..2f214a7 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -10,6 +10,53 @@ import ( "github.com/stretchr/testify/assert" ) +// 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) +} + +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_Resize(t *testing.T) { type rectTestTransform struct { name string @@ -80,6 +127,40 @@ func TestRect_Resize(t *testing.T) { } } +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 TestMatrix_Unproject(t *testing.T) { const delta = 1e-15 t.Run("for rotated matrix", func(t *testing.T) { @@ -541,20 +622,86 @@ func TestCircle_Intersect(t *testing.T) { } } -func TestRect_IntersectCircle(t *testing.T) { - // 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 - closeEnough := func(got, expected float64, decimalAccuracy int) bool { - gotShifted := got * math.Pow10(decimalAccuracy) - expectedShifted := expected * math.Pow10(decimalAccuracy) - - return math.Trunc(gotShifted) == math.Trunc(expectedShifted) +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]) + } + } + }) + } +} +func TestRect_IntersectCircle(t *testing.T) { type fields struct { Min pixel.Vec Max pixel.Vec @@ -690,3 +837,708 @@ func TestRect_IntersectCircle(t *testing.T) { }) } } + +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 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), + }, + } + 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, + }, + } + 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, + }, + } + 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) + } + }) + } +}