From 5072f34b916197895fb476c65c79a047c6908149 Mon Sep 17 00:00:00 2001
From: Ben Cragg <bcvery1@gmail.com>
Date: Mon, 28 Jan 2019 09:00:24 +0000
Subject: [PATCH] Added Circle geometry and tests

---
 geometry.go      | 119 ++++++++++++++
 geometry_test.go | 402 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 518 insertions(+), 3 deletions(-)

diff --git a/geometry.go b/geometry.go
index 4680872..82ff17b 100644
--- a/geometry.go
+++ b/geometry.go
@@ -318,6 +318,125 @@ func (r Rect) Intersect(s Rect) Rect {
 	return t
 }
 
+// Circle is a 2D circle. It is defined by two properties:
+//  - Radius float64
+//  - Center vector
+type Circle struct {
+	Radius float64
+	Center Vec
+}
+
+// C returns a new Circle with the given radius and center coordinates.
+//
+// Note that a negative radius is valid.
+func C(radius float64, center Vec) Circle {
+	return Circle{
+		Radius: radius,
+		Center: center,
+	}
+}
+
+// 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(%.2f, %s)", c.Radius, c.Center)
+}
+
+// Norm returns the Circle in normalized form - this is that the radius is set to an absolute version.
+//
+// c := pixel.C(-10, pixel.ZV)
+// c.Norm() // returns pixel.Circle{10, pixel.Vec{0, 0}}
+func (c Circle) Norm() Circle {
+	return Circle{
+		Radius: math.Abs(c.Radius),
+		Center: c.Center,
+	}
+}
+
+// Diameter returns the diameter of the Circle.
+func (c Circle) Diameter() float64 {
+	return c.Radius * 2
+}
+
+// Area returns the area of the Circle.
+func (c Circle) Area() float64 {
+	return math.Pi * c.Diameter()
+}
+
+// Moved returns the Circle moved by the given vector delta.
+func (c Circle) Moved(delta Vec) Circle {
+	return Circle{
+		Radius: c.Radius,
+		Center: c.Center.Add(delta),
+	}
+}
+
+// Resized returns the Circle resized by the given delta.
+//
+// c := pixel.C(10, pixel.ZV)
+// c.Resized(-5) // returns pixel.Circle{5, pixel.Vec{0, 0}}
+// c.Resized(25) // returns pixel.Circle{35, pixel.Vec{0, 0}}
+func (c Circle) Resized(radiusDelta float64) Circle {
+	return Circle{
+		Radius: c.Radius + radiusDelta,
+		Center: c.Center,
+	}
+}
+
+// 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()
+}
+
+// Union returns the minimal Circle which covers both `c` and `d`.
+func (c Circle) Union(d Circle) Circle {
+	biggerC := c
+	smallerC := d
+	if c.Radius < d.Radius {
+		biggerC = d
+		smallerC = c
+	}
+
+	// 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 := smallerC.Center.Scaled(1 - theta).Add(biggerC.Center.Scaled(theta))
+
+	return Circle{
+		Radius: r,
+		Center: center,
+	}
+}
+
+// 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 {
+	center := Lerp(c.Center, d.Center, 0.5)
+
+	radius := math.Min(0, c.Center.To(d.Center).Len()-(c.Radius+d.Radius))
+
+	return Circle{
+		Radius: math.Abs(radius),
+		Center: center,
+	}
+}
+
 // 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 d766b1d..7010620 100644
--- a/geometry_test.go
+++ b/geometry_test.go
@@ -2,11 +2,12 @@ package pixel_test
 
 import (
 	"fmt"
-	"github.com/stretchr/testify/assert"
 	"math"
+	"reflect"
 	"testing"
 
 	"github.com/faiface/pixel"
+	"github.com/stretchr/testify/assert"
 )
 
 type rectTestTransform struct {
@@ -14,8 +15,7 @@ type rectTestTransform struct {
 	f    func(pixel.Rect) pixel.Rect
 }
 
-func TestResizeRect(t *testing.T) {
-
+func TestRect_Resize(t *testing.T) {
 	// rectangles
 	squareAroundOrigin := pixel.R(-10, -10, 10, 10)
 	squareAround2020 := pixel.R(10, 10, 30, 30)
@@ -162,3 +162,399 @@ func TestMatrix_Unproject(t *testing.T) {
 		assert.True(t, math.IsNaN(unprojected.Y))
 	})
 }
+
+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.V(0, 0)},
+			want: pixel.Circle{Radius: 10, Center: pixel.V(0, 0)},
+		},
+		{
+			name: "C(): zero radius",
+			args: args{radius: 0, center: pixel.V(0, 0)},
+			want: pixel.Circle{Radius: 0, Center: pixel.V(0, 0)},
+		},
+		{
+			name: "C(): negative radius",
+			args: args{radius: -5, center: pixel.V(0, 0)},
+			want: pixel.Circle{Radius: -5, Center: pixel.V(0, 0)},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := pixel.C(tt.args.radius, tt.args.center); !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.V(0, 0)},
+			want:   "Circle(10.00, Vec(0, 0))",
+		},
+		{
+			name:   "Circle.String(): zero radius",
+			fields: fields{radius: 0, center: pixel.V(0, 0)},
+			want:   "Circle(0.00, Vec(0, 0))",
+		},
+		{
+			name:   "Circle.String(): negative radius",
+			fields: fields{radius: -5, center: pixel.V(0, 0)},
+			want:   "Circle(-5.00, Vec(0, 0))",
+		},
+		{
+			name:   "Circle.String(): irrational radius",
+			fields: fields{radius: math.Pi, center: pixel.V(0, 0)},
+			want:   "Circle(3.14, Vec(0, 0))",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			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.V(0, 0)},
+			want:   pixel.Circle{Radius: 10, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+		{
+			name:   "Circle.Norm(): zero radius",
+			fields: fields{radius: 0, center: pixel.V(0, 0)},
+			want:   pixel.Circle{Radius: 0, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+		{
+			name:   "Circle.Norm(): negative radius",
+			fields: fields{radius: -5, center: pixel.V(0, 0)},
+			want:   pixel.Circle{Radius: 5, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			if got := c.Norm(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Circle.Norm() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestCircle_Diameter(t *testing.T) {
+	type fields struct {
+		radius float64
+		center pixel.Vec
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   float64
+	}{
+		{
+			name:   "Circle.Diameter(): positive radius",
+			fields: fields{radius: 10, center: pixel.V(0, 0)},
+			want:   20,
+		},
+		{
+			name:   "Circle.Diameter(): zero radius",
+			fields: fields{radius: 0, center: pixel.V(0, 0)},
+			want:   0,
+		},
+		{
+			name:   "Circle.Diameter(): negative radius",
+			fields: fields{radius: -5, center: pixel.V(0, 0)},
+			want:   -10,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			if got := c.Diameter(); got != tt.want {
+				t.Errorf("Circle.Diameter() = %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.V(0, 0)},
+			want:   20 * math.Pi,
+		},
+		{
+			name:   "Circle.Area(): zero radius",
+			fields: fields{radius: 0, center: pixel.V(0, 0)},
+			want:   0,
+		},
+		{
+			name:   "Circle.Area(): negative radius",
+			fields: fields{radius: -5, center: pixel.V(0, 0)},
+			want:   -10 * math.Pi,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			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.V(0, 0)},
+			args:   args{delta: pixel.V(10, 20)},
+			want:   pixel.Circle{Radius: 10, Center: pixel.Vec{X: 10, Y: 20}},
+		},
+		{
+			name:   "Circle.Moved(): zero movement",
+			fields: fields{radius: 10, center: pixel.V(0, 0)},
+			args:   args{delta: pixel.ZV},
+			want:   pixel.Circle{Radius: 10, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+		{
+			name:   "Circle.Moved(): negative movement",
+			fields: fields{radius: 10, center: pixel.V(0, 0)},
+			args:   args{delta: pixel.V(-5, -10)},
+			want:   pixel.Circle{Radius: 10, Center: pixel.Vec{X: -5, Y: -10}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			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.V(0, 0)},
+			args:   args{radiusDelta: 5},
+			want:   pixel.Circle{Radius: 15, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+		{
+			name:   "Circle.Resized(): zero delta",
+			fields: fields{radius: 10, center: pixel.V(0, 0)},
+			args:   args{radiusDelta: 0},
+			want:   pixel.Circle{Radius: 10, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+		{
+			name:   "Circle.Resized(): negative delta",
+			fields: fields{radius: 10, center: pixel.V(0, 0)},
+			args:   args{radiusDelta: -5},
+			want:   pixel.Circle{Radius: 5, Center: pixel.Vec{X: 0, Y: 0}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			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.radius, tt.fields.center)
+			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(5, pixel.ZV)},
+			want:   pixel.C(5, pixel.ZV),
+		},
+		{
+			name:   "Circle.Union(): separate circles",
+			fields: fields{radius: 1, center: pixel.ZV},
+			args:   args{d: pixel.C(1, pixel.V(0, 2))},
+			want:   pixel.C(2, pixel.V(0, 1)),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(tt.fields.radius, tt.fields.center)
+			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.V(0, 0)},
+			args:   args{d: pixel.C(1, pixel.V(1, 0))},
+			want:   pixel.C(1, pixel.V(0.5, 0)),
+		},
+		{
+			name:   "Circle.Intersect(): non-intersecting circles",
+			fields: fields{radius: 1, center: pixel.V(0, 0)},
+			args:   args{d: pixel.C(1, pixel.V(3, 3))},
+			want:   pixel.C(0, pixel.V(1.5, 1.5)),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := pixel.C(
+				tt.fields.radius,
+				tt.fields.center,
+			)
+			if got := c.Intersect(tt.args.d); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Circle.Intersect() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
-- 
GitLab