From f3377bb16fc5d0be5429aa8f14e62ae0fb4e630d Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 11:53:48 +0100 Subject: [PATCH] Added circle intersection points --- geometry.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++- geometry_test.go | 82 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/geometry.go b/geometry.go index 4f1ae02..32b2467 100644 --- a/geometry.go +++ b/geometry.go @@ -707,6 +707,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 { @@ -870,7 +876,90 @@ 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. func (c Circle) IntersectionPoints(l Line) []Vec { - return []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)) + + return []Vec{first, second} } // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such diff --git a/geometry_test.go b/geometry_test.go index a311159..2f214a7 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -10,6 +10,19 @@ 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 @@ -623,7 +636,54 @@ func TestCircle_IntersectPoints(t *testing.T) { args args want []pixel.Vec }{ - // TODO: Add test cases. + { + 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) { @@ -631,27 +691,17 @@ func TestCircle_IntersectPoints(t *testing.T) { Center: tt.fields.Center, Radius: tt.fields.Radius, } - if got := c.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Circle.IntersectPoints() = %v, want %v", got, tt.want) + 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) { - // 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) - } - type fields struct { Min pixel.Vec Max pixel.Vec