diff --git a/geometry.go b/geometry.go index 175b268..95ccbf9 100644 --- a/geometry.go +++ b/geometry.go @@ -318,14 +318,15 @@ func (r Rect) Intersect(s Rect) Rect { return t } -// IntersectsCircle returns whether the Circle and the Rect intersect. +// IntersectsCircle 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 true if: // - The Rect contains the Circle, partially or fully // - The Circle contains the Rect, partially of fully -// - An edge of the Rect is a tangent to the Circle -func (r Rect) IntersectsCircle(c Circle) bool { - return c.IntersectsRect(r) +func (r Rect) IntersectsCircle(c Circle) Vec { + return c.IntersectsRect(r).Scaled(-1) } // Circle is a 2D circle. It is defined by two properties: @@ -475,24 +476,79 @@ func (c Circle) Intersect(d Circle) Circle { } } -// IntersectsRect returns whether the Circle and the Rect intersect. +// IntersectsRect 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 true if: // - The Rect contains the Circle, partially or fully // - The Circle contains the Rect, partially of fully -// - An edge of the Rect is a tangent to the Circle -func (c Circle) IntersectsRect(r Rect) bool { +func (c Circle) IntersectsRect(r Rect) Vec { + // h and v will hold the minimum horizontal and vertical distances (respectively) to avoid overlapping + var h, v float64 + // 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 - return Rect{ - Min: r.Min.Sub(V(c.Radius, c.Radius)), - Max: r.Max.Add(V(c.Radius, c.Radius)), - }.Contains(c.Center) + 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 + } + } else { + // The center is in the diagonal quadrants + if c.Center.To(r.Min).Len() <= c.Radius { + // Closest to bottom-left + cornerToCenter := r.Min.To(c.Center) + // Get the horizontal and vertical overlaps + h = c.Radius - math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.Y, 2)) + v = -1 * (c.Radius + math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.X, 2))) + } + if c.Center.To(r.Max).Len() <= c.Radius { + // Closest to top-right + cornerToCenter := r.Max.To(c.Center) + // Get the horizontal and vertical overlaps + h = c.Radius - math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.Y, 2)) + v = c.Radius - math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.X, 2)) + } + if c.Center.To(V(r.Min.X, r.Max.Y)).Len() <= c.Radius { + // Closest to top-left + cornerToCenter := V(r.Min.X, r.Max.Y).To(c.Center) + // Get the horizontal and vertical overlaps + h = -1 * (c.Radius + math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.Y, 2))) + v = c.Radius - math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.X, 2)) + } + if c.Center.To(V(r.Max.X, r.Min.Y)).Len() <= c.Radius { + // Closest to bottom-right + cornerToCenter := V(r.Max.X, r.Min.Y).To(c.Center) + // Get the horizontal and vertical overlaps + h = -1 * (c.Radius + math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.Y, 2))) + v = -1 * (c.Radius + math.Sqrt(math.Pow(c.Radius, 2)-math.Pow(cornerToCenter.X, 2))) + } } - // The center is in the diagonal quadrants - return c.Center.To(r.Min).Len() <= c.Radius || c.Center.To(r.Max).Len() <= c.Radius || - c.Center.To(V(r.Min.X, r.Max.Y)).Len() <= c.Radius || c.Center.To(V(r.Max.X, r.Min.Y)).Len() <= c.Radius + + // 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) } // 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 5425dff..da321b2 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -553,55 +553,85 @@ func TestRect_IntersectsCircle(t *testing.T) { name string fields fields args args - want bool + want pixel.Vec }{ { name: "Rect.IntersectsCircle(): no overlap", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(pixel.V(50, 50), 1)}, - want: false, + want: pixel.ZV, }, { name: "Rect.IntersectsCircle(): circle contains rect", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(pixel.V(5, 5), 10)}, - want: true, + want: pixel.V(-15, 0), }, { name: "Rect.IntersectsCircle(): rect contains circle", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(pixel.V(5, 5), 1)}, - want: true, + want: pixel.V(-6, 0), }, { name: "Rect.IntersectsCircle(): circle overlaps bottom-left corner", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(pixel.V(-.5, -.5), 1)}, - want: true, + args: args{c: pixel.C(pixel.V(0, 0), 1)}, + want: pixel.V(1, 0), }, { name: "Rect.IntersectsCircle(): circle overlaps top-left corner", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(pixel.V(-.5, 10.5), 1)}, - want: true, + args: args{c: pixel.C(pixel.V(0, 10), 1)}, + want: pixel.V(1, 0), + }, + { + name: "Rect.IntersectsCircle(): circle overlaps bottom-right corner", + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, + args: args{c: pixel.C(pixel.V(10, 0), 1)}, + want: pixel.V(-1, 0), + }, + { + name: "Rect.IntersectsCircle(): circle overlaps top-right corner", + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, + args: args{c: pixel.C(pixel.V(10, 10), 1)}, + want: pixel.V(-1, 0), }, { name: "Rect.IntersectsCircle(): 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: true, + want: pixel.V(6, 0), }, { - name: "Rect.IntersectsCircle(): circle overlaps one edge", + name: "Rect.IntersectsCircle(): 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: true, + want: pixel.V(1, 0), + }, + { + name: "Rect.IntersectsCircle(): 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.IntersectsCircle(): 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.IntersectsCircle(): 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.IntersectsCircle(): edge is tangent", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(pixel.V(-1, 5), 1)}, - want: true, + want: pixel.ZV, }, } for _, tt := range tests {