From d102169151d5522ba9dc176531dad6839fff2091 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 28 Jan 2019 09:00:24 +0000 Subject: [PATCH 01/22] Added Circle geometry and tests --- geometry.go | 119 ++++++++++++++ geometry_test.go | 398 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) diff --git a/geometry.go b/geometry.go index 5f16ba4..591a794 100644 --- a/geometry.go +++ b/geometry.go @@ -310,6 +310,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 e1c1a6f..98a4472 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -2,6 +2,8 @@ package pixel_test import ( "fmt" + "math" + "reflect" "testing" "github.com/faiface/pixel" @@ -77,3 +79,399 @@ func TestResizeRect(t *testing.T) { }) } } + +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) + } + }) + } +} From 0d5ba92fbe8562a6dca5487cf194451eaa20349c Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 09:33:20 +0000 Subject: [PATCH 02/22] fixed circle.Intersect --- geometry.go | 51 +++++++++++++++++++++----- geometry_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/geometry.go b/geometry.go index 591a794..46cfb7c 100644 --- a/geometry.go +++ b/geometry.go @@ -337,7 +337,7 @@ 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. +// 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{10, pixel.Vec{0, 0}} @@ -366,7 +366,7 @@ func (c Circle) Moved(delta Vec) Circle { } } -// Resized returns the Circle resized by the given delta. +// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor. // // c := pixel.C(10, pixel.ZV) // c.Resized(-5) // returns pixel.Circle{5, pixel.Vec{0, 0}} @@ -384,14 +384,26 @@ func (c Circle) Contains(u Vec) bool { return c.Radius >= toCenter.Len() } +// 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 := c - smallerC := d - if c.Radius < d.Radius { - biggerC = d - smallerC = c - } + biggerC := MaxCircle(c, d) + smallerC := MinCircle(c, d) // Get distance between centers dist := c.Center.To(d.Center).Len() @@ -419,7 +431,28 @@ func (c Circle) Union(d Circle) Circle { // 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) + // Check if one of the circles encompasses the other; if so, return that one + biggerC := MaxCircle(c, d) + smallerC := MinCircle(c, d) + + 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(0, center) + } radius := math.Min(0, c.Center.To(d.Center).Len()-(c.Radius+d.Radius)) diff --git a/geometry_test.go b/geometry_test.go index 98a4472..361ca35 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -462,6 +462,24 @@ func TestCircle_Intersect(t *testing.T) { args: args{d: pixel.C(1, pixel.V(3, 3))}, want: pixel.C(0, pixel.V(1.5, 1.5)), }, + { + name: "Circle.Intersect(): first circle encompassing second", + fields: fields{radius: 10, center: pixel.V(0, 0)}, + args: args{d: pixel.C(1, pixel.V(3, 3))}, + want: pixel.C(10, pixel.V(0, 0)), + }, + { + name: "Circle.Intersect(): second circle encompassing first", + fields: fields{radius: 1, center: pixel.V(-1, -4)}, + args: args{d: pixel.C(10, pixel.V(0, 0))}, + want: pixel.C(10, pixel.V(0, 0)), + }, + { + name: "Circle.Intersect(): matching circles", + fields: fields{radius: 1, center: pixel.V(0, 0)}, + args: args{d: pixel.C(1, pixel.V(0, 0))}, + want: pixel.C(1, pixel.V(0, 0)), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -475,3 +493,79 @@ func TestCircle_Intersect(t *testing.T) { }) } } + +func TestMaxCircle(t *testing.T) { + bigCircle := pixel.C(10, pixel.ZV) + smallCircle := pixel.C(1, pixel.ZV) + + type args struct { + c pixel.Circle + d pixel.Circle + } + tests := []struct { + name string + args args + want pixel.Circle + }{ + { + name: "MaxCircle(): first bigger", + args: args{c: bigCircle, d: smallCircle}, + want: bigCircle, + }, + { + name: "MaxCircle(): first smaller", + args: args{c: smallCircle, d: bigCircle}, + want: bigCircle, + }, + { + name: "MaxCircle(): both same size", + args: args{c: smallCircle, d: smallCircle}, + want: smallCircle, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pixel.MaxCircle(tt.args.c, tt.args.d); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MaxCircle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMinCircle(t *testing.T) { + bigCircle := pixel.C(10, pixel.ZV) + smallCircle := pixel.C(1, pixel.ZV) + + type args struct { + c pixel.Circle + d pixel.Circle + } + tests := []struct { + name string + args args + want pixel.Circle + }{ + { + name: "MinCircle(): first bigger", + args: args{c: bigCircle, d: smallCircle}, + want: smallCircle, + }, + { + name: "MinCircle(): first smaller", + args: args{c: smallCircle, d: bigCircle}, + want: smallCircle, + }, + { + name: "MinCircle(): both same size", + args: args{c: smallCircle, d: smallCircle}, + want: smallCircle, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pixel.MinCircle(tt.args.c, tt.args.d); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MinCircle() = %v, want %v", got, tt.want) + } + }) + } +} From a56f7fa42296b14f68b17bf6281e8ec5f0b26dd4 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 09:38:24 +0000 Subject: [PATCH 03/22] not exporting circle size comparisons --- geometry.go | 16 +++++----- geometry_test.go | 76 ------------------------------------------------ 2 files changed, 8 insertions(+), 84 deletions(-) diff --git a/geometry.go b/geometry.go index 46cfb7c..02084d2 100644 --- a/geometry.go +++ b/geometry.go @@ -384,16 +384,16 @@ func (c Circle) Contains(u Vec) bool { return c.Radius >= toCenter.Len() } -// MaxCircle will return the larger circle based on the radius. -func MaxCircle(c, d Circle) Circle { +// 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 { +// minCircle will return the smaller circle based on the radius. +func minCircle(c, d Circle) Circle { if c.Radius < d.Radius { return c } @@ -402,8 +402,8 @@ func MinCircle(c, d Circle) Circle { // Union returns the minimal Circle which covers both `c` and `d`. func (c Circle) Union(d Circle) Circle { - biggerC := MaxCircle(c, d) - smallerC := MinCircle(c, d) + biggerC := maxCircle(c, d) + smallerC := minCircle(c, d) // Get distance between centers dist := c.Center.To(d.Center).Len() @@ -432,8 +432,8 @@ func (c Circle) Union(d Circle) Circle { // 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, d) - smallerC := MinCircle(c, d) + biggerC := maxCircle(c, d) + smallerC := minCircle(c, d) if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius { return biggerC diff --git a/geometry_test.go b/geometry_test.go index 361ca35..fc6e3fb 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -493,79 +493,3 @@ func TestCircle_Intersect(t *testing.T) { }) } } - -func TestMaxCircle(t *testing.T) { - bigCircle := pixel.C(10, pixel.ZV) - smallCircle := pixel.C(1, pixel.ZV) - - type args struct { - c pixel.Circle - d pixel.Circle - } - tests := []struct { - name string - args args - want pixel.Circle - }{ - { - name: "MaxCircle(): first bigger", - args: args{c: bigCircle, d: smallCircle}, - want: bigCircle, - }, - { - name: "MaxCircle(): first smaller", - args: args{c: smallCircle, d: bigCircle}, - want: bigCircle, - }, - { - name: "MaxCircle(): both same size", - args: args{c: smallCircle, d: smallCircle}, - want: smallCircle, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := pixel.MaxCircle(tt.args.c, tt.args.d); !reflect.DeepEqual(got, tt.want) { - t.Errorf("MaxCircle() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMinCircle(t *testing.T) { - bigCircle := pixel.C(10, pixel.ZV) - smallCircle := pixel.C(1, pixel.ZV) - - type args struct { - c pixel.Circle - d pixel.Circle - } - tests := []struct { - name string - args args - want pixel.Circle - }{ - { - name: "MinCircle(): first bigger", - args: args{c: bigCircle, d: smallCircle}, - want: smallCircle, - }, - { - name: "MinCircle(): first smaller", - args: args{c: smallCircle, d: bigCircle}, - want: smallCircle, - }, - { - name: "MinCircle(): both same size", - args: args{c: smallCircle, d: smallCircle}, - want: smallCircle, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := pixel.MinCircle(tt.args.c, tt.args.d); !reflect.DeepEqual(got, tt.want) { - t.Errorf("MinCircle() = %v, want %v", got, tt.want) - } - }) - } -} From 9b8d4c7461021338fe2ebedb11b9b1e095f0defe Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 09:39:44 +0000 Subject: [PATCH 04/22] moved rect test struct --- geometry_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index fc6e3fb..0638837 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -9,12 +9,11 @@ import ( "github.com/faiface/pixel" ) -type rectTestTransform struct { - name string - f func(pixel.Rect) pixel.Rect -} - func TestResizeRect(t *testing.T) { + type rectTestTransform struct { + name string + f func(pixel.Rect) pixel.Rect + } // rectangles squareAroundOrigin := pixel.R(-10, -10, 10, 10) From fdc068e855bad330093cba1b3f9fb5391654adc5 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 09:40:44 +0000 Subject: [PATCH 05/22] renamed test function to match convention --- geometry_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry_test.go b/geometry_test.go index 0638837..2192c26 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -9,7 +9,7 @@ import ( "github.com/faiface/pixel" ) -func TestResizeRect(t *testing.T) { +func TestRect_Resize(t *testing.T) { type rectTestTransform struct { name string f func(pixel.Rect) pixel.Rect From 9a32601c6b02375da44867816545cd3f28991a0b Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:20:21 +0000 Subject: [PATCH 06/22] added rect-circle intersection functions --- geometry.go | 32 ++++++++++++++++++++++ geometry_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/geometry.go b/geometry.go index 02084d2..98fd8c1 100644 --- a/geometry.go +++ b/geometry.go @@ -310,6 +310,16 @@ func (r Rect) Intersect(s Rect) Rect { return t } +// IntersectsCircle returns whether the Circle and the Rect intersect. +// +// 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) +} + // Circle is a 2D circle. It is defined by two properties: // - Radius float64 // - Center vector @@ -462,6 +472,28 @@ func (c Circle) Intersect(d Circle) Circle { } } +// IntersectsRect returns whether the Circle and the Rect intersect. +// +// 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 { + // Checks if the c.Center is not in the diagonal quadrants of the rectangle + var grownR Rect + 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 diagonal + grownR = Rect{ + Min: r.Min.Sub(V(c.Radius, c.Radius)), + Max: r.Max.Add(V(c.Radius, c.Radius)), + } + + return grownR.Contains(c.Center) + } + // The center is in the diagonal quadrants + return c.Center.To(r.Min).Len() <= c.Radius || c.Center.To(r.Max).Len() <= c.Radius +} + // 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 2192c26..37f4229 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -492,3 +492,73 @@ func TestCircle_Intersect(t *testing.T) { }) } } + +func TestRect_IntersectsCircle(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 bool + }{ + { + name: "Rect.IntersectsCircle(): no overlap", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(50, 50))}, + want: false, + }, + { + name: "Rect.IntersectsCircle(): circle contains rect", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(10, pixel.V(5, 5))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): rect contains circle", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(5, 5))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): circle overlaps one corner", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(0, 0))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): circle overlaps two corner", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(11, pixel.V(0, 5))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): circle overlaps one edge", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(0, 5))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): edge is tangent", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(-1, 5))}, + want: true, + }, + } + 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.IntersectsCircle(tt.args.c); got != tt.want { + t.Errorf("Rect.IntersectsCircle() = %v, want %v", got, tt.want) + } + }) + } +} From 8efed1de5cfa511b0e055bd7208ab1e646f64893 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:23:55 +0000 Subject: [PATCH 07/22] Removed Diameter function --- geometry.go | 7 +------ geometry_test.go | 36 ------------------------------------ 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/geometry.go b/geometry.go index 98fd8c1..7e17924 100644 --- a/geometry.go +++ b/geometry.go @@ -358,14 +358,9 @@ func (c Circle) Norm() Circle { } } -// 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() + return math.Pi * c.Radius * 2 } // Moved returns the Circle moved by the given vector delta. diff --git a/geometry_test.go b/geometry_test.go index 37f4229..3bebbbe 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -191,42 +191,6 @@ func TestCircle_Norm(t *testing.T) { } } -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 From 56cd321ab883bed9867ff0acd9849a47cfbb4689 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:33:12 +0000 Subject: [PATCH 08/22] normalising before getting bigger/smaller --- geometry.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/geometry.go b/geometry.go index 7e17924..fd9b3c3 100644 --- a/geometry.go +++ b/geometry.go @@ -407,8 +407,8 @@ func minCircle(c, d Circle) Circle { // Union returns the minimal Circle which covers both `c` and `d`. func (c Circle) Union(d Circle) Circle { - biggerC := maxCircle(c, d) - smallerC := minCircle(c, d) + biggerC := maxCircle(c.Norm(), d.Norm()) + smallerC := minCircle(c.Norm(), d.Norm()) // Get distance between centers dist := c.Center.To(d.Center).Len() @@ -437,8 +437,8 @@ func (c Circle) Union(d Circle) Circle { // 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, d) - smallerC := minCircle(c, d) + 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 From d0ceea684902c10ff3d19f880e8cc93072f7a92f Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:34:59 +0000 Subject: [PATCH 09/22] remove local var --- geometry.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/geometry.go b/geometry.go index fd9b3c3..9a4265c 100644 --- a/geometry.go +++ b/geometry.go @@ -475,15 +475,12 @@ func (c Circle) Intersect(d Circle) Circle { // - An edge of the Rect is a tangent to the Circle func (c Circle) IntersectsRect(r Rect) bool { // Checks if the c.Center is not in the diagonal quadrants of the rectangle - var grownR Rect 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 diagonal - grownR = Rect{ + return Rect{ Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius)), - } - - return grownR.Contains(c.Center) + }.Contains(c.Center) } // The center is in the diagonal quadrants return c.Center.To(r.Min).Len() <= c.Radius || c.Center.To(r.Max).Len() <= c.Radius From cff8697c97a47adbd11b1b082fa192a2d6010ed7 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:41:10 +0000 Subject: [PATCH 10/22] Made test clearer --- geometry_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 3bebbbe..eb1d541 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -496,9 +496,9 @@ func TestRect_IntersectsCircle(t *testing.T) { want: true, }, { - name: "Rect.IntersectsCircle(): circle overlaps two corner", + name: "Rect.IntersectsCircle(): circle overlaps two corners", fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, - args: args{c: pixel.C(11, pixel.V(0, 5))}, + args: args{c: pixel.C(6, pixel.V(0, 5))}, want: true, }, { From 4b6cf201f7e6c5ea99852c52eaf3fa1591fcdabb Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:45:00 +0000 Subject: [PATCH 11/22] using Lerp --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 9a4265c..72485b9 100644 --- a/geometry.go +++ b/geometry.go @@ -423,7 +423,7 @@ func (c Circle) Union(d Circle) Circle { // Calculate center for encompassing Circle theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist) - center := smallerC.Center.Scaled(1 - theta).Add(biggerC.Center.Scaled(theta)) + center := Lerp(smallerC.Center, biggerC.Center, theta) return Circle{ Radius: r, From 0cddb56114b9792d676c4ca46ed23be3c08210d4 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 11:47:49 +0000 Subject: [PATCH 12/22] corrected comment --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 72485b9..599c368 100644 --- a/geometry.go +++ b/geometry.go @@ -476,7 +476,7 @@ func (c Circle) Intersect(d Circle) Circle { func (c Circle) IntersectsRect(r Rect) bool { // 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 diagonal + // '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)), From 4eba5e37ae5eb17dfacb6a9ce95a223f0d011a09 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 12:18:35 +0000 Subject: [PATCH 13/22] made test param generation more consistant --- geometry_test.go | 96 ++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index eb1d541..3738b86 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -91,18 +91,18 @@ func TestC(t *testing.T) { }{ { name: "C(): positive radius", - args: args{radius: 10, center: pixel.V(0, 0)}, - want: pixel.Circle{Radius: 10, Center: pixel.V(0, 0)}, + 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.V(0, 0)}, - want: pixel.Circle{Radius: 0, Center: pixel.V(0, 0)}, + 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.V(0, 0)}, - want: pixel.Circle{Radius: -5, Center: pixel.V(0, 0)}, + args: args{radius: -5, center: pixel.ZV}, + want: pixel.Circle{Radius: -5, Center: pixel.ZV}, }, } for _, tt := range tests { @@ -126,22 +126,22 @@ func TestCircle_String(t *testing.T) { }{ { name: "Circle.String(): positive radius", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, want: "Circle(10.00, Vec(0, 0))", }, { name: "Circle.String(): zero radius", - fields: fields{radius: 0, center: pixel.V(0, 0)}, + fields: fields{radius: 0, center: pixel.ZV}, want: "Circle(0.00, Vec(0, 0))", }, { name: "Circle.String(): negative radius", - fields: fields{radius: -5, center: pixel.V(0, 0)}, + fields: fields{radius: -5, center: pixel.ZV}, want: "Circle(-5.00, Vec(0, 0))", }, { name: "Circle.String(): irrational radius", - fields: fields{radius: math.Pi, center: pixel.V(0, 0)}, + fields: fields{radius: math.Pi, center: pixel.ZV}, want: "Circle(3.14, Vec(0, 0))", }, } @@ -167,18 +167,18 @@ func TestCircle_Norm(t *testing.T) { }{ { 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}}, + fields: fields{radius: 10, center: pixel.ZV}, + want: pixel.C(10, pixel.ZV), }, { 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}}, + fields: fields{radius: 0, center: pixel.ZV}, + want: pixel.C(0, pixel.ZV), }, { 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}}, + fields: fields{radius: -5, center: pixel.ZV}, + want: pixel.C(5, pixel.ZV), }, } for _, tt := range tests { @@ -203,17 +203,17 @@ func TestCircle_Area(t *testing.T) { }{ { name: "Circle.Area(): positive radius", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, want: 20 * math.Pi, }, { name: "Circle.Area(): zero radius", - fields: fields{radius: 0, center: pixel.V(0, 0)}, + fields: fields{radius: 0, center: pixel.ZV}, want: 0, }, { name: "Circle.Area(): negative radius", - fields: fields{radius: -5, center: pixel.V(0, 0)}, + fields: fields{radius: -5, center: pixel.ZV}, want: -10 * math.Pi, }, } @@ -243,21 +243,21 @@ func TestCircle_Moved(t *testing.T) { }{ { name: "Circle.Moved(): positive movement", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{delta: pixel.V(10, 20)}, - want: pixel.Circle{Radius: 10, Center: pixel.Vec{X: 10, Y: 20}}, + want: pixel.C(10, pixel.V(10, 20)), }, { name: "Circle.Moved(): zero movement", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{delta: pixel.ZV}, - want: pixel.Circle{Radius: 10, Center: pixel.Vec{X: 0, Y: 0}}, + want: pixel.C(10, pixel.V(0, 0)), }, { name: "Circle.Moved(): negative movement", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{delta: pixel.V(-5, -10)}, - want: pixel.Circle{Radius: 10, Center: pixel.Vec{X: -5, Y: -10}}, + want: pixel.C(10, pixel.V(-5, -10)), }, } for _, tt := range tests { @@ -286,21 +286,21 @@ func TestCircle_Resized(t *testing.T) { }{ { name: "Circle.Resized(): positive delta", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{radiusDelta: 5}, - want: pixel.Circle{Radius: 15, Center: pixel.Vec{X: 0, Y: 0}}, + want: pixel.C(15, pixel.V(0, 0)), }, { name: "Circle.Resized(): zero delta", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{radiusDelta: 0}, - want: pixel.Circle{Radius: 10, Center: pixel.Vec{X: 0, Y: 0}}, + want: pixel.C(10, pixel.V(0, 0)), }, { name: "Circle.Resized(): negative delta", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{radiusDelta: -5}, - want: pixel.Circle{Radius: 5, Center: pixel.Vec{X: 0, Y: 0}}, + want: pixel.C(5, pixel.V(0, 0)), }, } for _, tt := range tests { @@ -415,33 +415,33 @@ func TestCircle_Intersect(t *testing.T) { }{ { name: "Circle.Intersect(): intersecting circles", - fields: fields{radius: 1, center: pixel.V(0, 0)}, + fields: fields{radius: 1, center: pixel.ZV}, 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)}, + fields: fields{radius: 1, center: pixel.ZV}, args: args{d: pixel.C(1, pixel.V(3, 3))}, want: pixel.C(0, pixel.V(1.5, 1.5)), }, { name: "Circle.Intersect(): first circle encompassing second", - fields: fields{radius: 10, center: pixel.V(0, 0)}, + fields: fields{radius: 10, center: pixel.ZV}, args: args{d: pixel.C(1, pixel.V(3, 3))}, - want: pixel.C(10, pixel.V(0, 0)), + want: pixel.C(10, pixel.ZV), }, { name: "Circle.Intersect(): second circle encompassing first", fields: fields{radius: 1, center: pixel.V(-1, -4)}, - args: args{d: pixel.C(10, pixel.V(0, 0))}, - want: pixel.C(10, pixel.V(0, 0)), + args: args{d: pixel.C(10, pixel.ZV)}, + want: pixel.C(10, pixel.ZV), }, { name: "Circle.Intersect(): matching circles", - fields: fields{radius: 1, center: pixel.V(0, 0)}, - args: args{d: pixel.C(1, pixel.V(0, 0))}, - want: pixel.C(1, pixel.V(0, 0)), + fields: fields{radius: 1, center: pixel.ZV}, + args: args{d: pixel.C(1, pixel.ZV)}, + want: pixel.C(1, pixel.ZV), }, } for _, tt := range tests { @@ -473,43 +473,43 @@ func TestRect_IntersectsCircle(t *testing.T) { }{ { name: "Rect.IntersectsCircle(): no overlap", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(1, pixel.V(50, 50))}, want: false, }, { name: "Rect.IntersectsCircle(): circle contains rect", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(10, pixel.V(5, 5))}, want: true, }, { name: "Rect.IntersectsCircle(): rect contains circle", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(1, pixel.V(5, 5))}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps one corner", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(0, 0))}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.ZV)}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps two corners", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(6, pixel.V(0, 5))}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps one edge", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(1, pixel.V(0, 5))}, want: true, }, { name: "Rect.IntersectsCircle(): edge is tangent", - fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(1, pixel.V(-1, 5))}, want: true, }, From c70d7575ce9e0b3b7bedf955823c2717c2e7326c Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 12:27:38 +0000 Subject: [PATCH 14/22] removed unneeded Min call --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 599c368..70404bc 100644 --- a/geometry.go +++ b/geometry.go @@ -459,7 +459,7 @@ func (c Circle) Intersect(d Circle) Circle { return C(0, center) } - radius := math.Min(0, c.Center.To(d.Center).Len()-(c.Radius+d.Radius)) + radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius) return Circle{ Radius: math.Abs(radius), From 2cce50832d2200d47aa046479ee531ce3406cff0 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 29 Jan 2019 12:31:59 +0000 Subject: [PATCH 15/22] fixed intersect function --- geometry.go | 3 ++- geometry_test.go | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/geometry.go b/geometry.go index 70404bc..ab7573e 100644 --- a/geometry.go +++ b/geometry.go @@ -483,7 +483,8 @@ func (c Circle) IntersectsRect(r Rect) bool { }.Contains(c.Center) } // The center is in the diagonal quadrants - return c.Center.To(r.Min).Len() <= c.Radius || c.Center.To(r.Max).Len() <= c.Radius + 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 } // 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 3738b86..0ea1f1c 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -490,9 +490,15 @@ func TestRect_IntersectsCircle(t *testing.T) { want: true, }, { - name: "Rect.IntersectsCircle(): circle overlaps one corner", + name: "Rect.IntersectsCircle(): circle overlaps bottom-left corner", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.ZV)}, + args: args{c: pixel.C(1, pixel.V(-.5, -.5))}, + want: true, + }, + { + name: "Rect.IntersectsCircle(): circle overlaps top-left corner", + fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, + args: args{c: pixel.C(1, pixel.V(-.5, 10.5))}, want: true, }, { From ecd686769ccf90eba1100fcca6d467b24878a843 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 30 Jan 2019 08:37:27 +0000 Subject: [PATCH 16/22] swapped radius and center order --- geometry.go | 30 ++++++++--------- geometry_test.go | 88 ++++++++++++++++++++++++------------------------ 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/geometry.go b/geometry.go index ab7573e..17c574b 100644 --- a/geometry.go +++ b/geometry.go @@ -321,20 +321,20 @@ func (r Rect) IntersectsCircle(c Circle) bool { } // Circle is a 2D circle. It is defined by two properties: -// - Radius float64 // - Center vector +// - Radius float64 type Circle struct { - Radius float64 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(radius float64, center Vec) Circle { +func C(center Vec, radius float64) Circle { return Circle{ - Radius: radius, Center: center, + Radius: radius, } } @@ -344,17 +344,17 @@ func C(radius float64, center Vec) Circle { // 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) + 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{10, pixel.Vec{0, 0}} +// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10} func (c Circle) Norm() Circle { return Circle{ - Radius: math.Abs(c.Radius), Center: c.Center, + Radius: math.Abs(c.Radius), } } @@ -366,20 +366,20 @@ func (c Circle) Area() float64 { // 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), + Radius: c.Radius, } } // Resized returns the Circle resized by the given delta. The Circles center is use as the anchor. // -// 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}} +// 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{ - Radius: c.Radius + radiusDelta, Center: c.Center, + Radius: c.Radius + radiusDelta, } } @@ -426,8 +426,8 @@ func (c Circle) Union(d Circle) Circle { center := Lerp(smallerC.Center, biggerC.Center, theta) return Circle{ - Radius: r, Center: center, + Radius: r, } } @@ -456,14 +456,14 @@ func (c Circle) Intersect(d Circle) Circle { // No need to calculate radius if the circles do not overlap if c.Center.To(d.Center).Len() >= c.Radius+d.Radius { - return C(0, center) + return C(center, 0) } radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius) return Circle{ - Radius: math.Abs(radius), Center: center, + Radius: math.Abs(radius), } } diff --git a/geometry_test.go b/geometry_test.go index 0ea1f1c..eec854d 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -107,7 +107,7 @@ func TestC(t *testing.T) { } 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) { + if got := pixel.C(tt.args.center, tt.args.radius); !reflect.DeepEqual(got, tt.want) { t.Errorf("C() = %v, want %v", got, tt.want) } }) @@ -127,27 +127,27 @@ func TestCircle_String(t *testing.T) { { name: "Circle.String(): positive radius", fields: fields{radius: 10, center: pixel.ZV}, - want: "Circle(10.00, Vec(0, 0))", + want: "Circle(Vec(0, 0), 10.00)", }, { name: "Circle.String(): zero radius", fields: fields{radius: 0, center: pixel.ZV}, - want: "Circle(0.00, Vec(0, 0))", + want: "Circle(Vec(0, 0), 0.00)", }, { name: "Circle.String(): negative radius", fields: fields{radius: -5, center: pixel.ZV}, - want: "Circle(-5.00, Vec(0, 0))", + want: "Circle(Vec(0, 0), -5.00)", }, { name: "Circle.String(): irrational radius", fields: fields{radius: math.Pi, center: pixel.ZV}, - want: "Circle(3.14, Vec(0, 0))", + want: "Circle(Vec(0, 0), 3.14)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := pixel.C(tt.fields.radius, tt.fields.center) + 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) } @@ -168,22 +168,22 @@ func TestCircle_Norm(t *testing.T) { { name: "Circle.Norm(): positive radius", fields: fields{radius: 10, center: pixel.ZV}, - want: pixel.C(10, pixel.ZV), + want: pixel.C(pixel.ZV, 10), }, { name: "Circle.Norm(): zero radius", fields: fields{radius: 0, center: pixel.ZV}, - want: pixel.C(0, pixel.ZV), + want: pixel.C(pixel.ZV, 0), }, { name: "Circle.Norm(): negative radius", fields: fields{radius: -5, center: pixel.ZV}, - want: pixel.C(5, 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.radius, tt.fields.center) + 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) } @@ -219,7 +219,7 @@ func TestCircle_Area(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := pixel.C(tt.fields.radius, tt.fields.center) + 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) } @@ -245,24 +245,24 @@ func TestCircle_Moved(t *testing.T) { name: "Circle.Moved(): positive movement", fields: fields{radius: 10, center: pixel.ZV}, args: args{delta: pixel.V(10, 20)}, - want: pixel.C(10, 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(10, pixel.V(0, 0)), + 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(10, 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.radius, tt.fields.center) + 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) } @@ -288,24 +288,24 @@ func TestCircle_Resized(t *testing.T) { name: "Circle.Resized(): positive delta", fields: fields{radius: 10, center: pixel.ZV}, args: args{radiusDelta: 5}, - want: pixel.C(15, pixel.V(0, 0)), + 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(10, pixel.V(0, 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(5, pixel.V(0, 0)), + 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.radius, tt.fields.center) + 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) } @@ -354,7 +354,7 @@ func TestCircle_Contains(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := pixel.C(tt.fields.radius, tt.fields.center) + 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) } @@ -379,19 +379,19 @@ func TestCircle_Union(t *testing.T) { { 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), + 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(1, pixel.V(0, 2))}, - want: pixel.C(2, pixel.V(0, 1)), + 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.radius, tt.fields.center) + 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) } @@ -416,39 +416,39 @@ func TestCircle_Intersect(t *testing.T) { { name: "Circle.Intersect(): intersecting circles", fields: fields{radius: 1, center: pixel.ZV}, - args: args{d: pixel.C(1, pixel.V(1, 0))}, - want: pixel.C(1, pixel.V(0.5, 0)), + 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(1, pixel.V(3, 3))}, - want: pixel.C(0, pixel.V(1.5, 1.5)), + 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(1, pixel.V(3, 3))}, - want: pixel.C(10, 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(10, pixel.ZV)}, - want: pixel.C(10, pixel.ZV), + 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(1, pixel.ZV)}, - want: pixel.C(1, 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.radius, 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) @@ -474,49 +474,49 @@ func TestRect_IntersectsCircle(t *testing.T) { { name: "Rect.IntersectsCircle(): no overlap", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(50, 50))}, + args: args{c: pixel.C(pixel.V(50, 50), 1)}, want: false, }, { name: "Rect.IntersectsCircle(): circle contains rect", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(10, pixel.V(5, 5))}, + args: args{c: pixel.C(pixel.V(5, 5), 10)}, want: true, }, { name: "Rect.IntersectsCircle(): rect contains circle", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(5, 5))}, + args: args{c: pixel.C(pixel.V(5, 5), 1)}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps bottom-left corner", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(-.5, -.5))}, + args: args{c: pixel.C(pixel.V(-.5, -.5), 1)}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps top-left corner", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(-.5, 10.5))}, + args: args{c: pixel.C(pixel.V(-.5, 10.5), 1)}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps two corners", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(6, pixel.V(0, 5))}, + args: args{c: pixel.C(pixel.V(0, 5), 6)}, want: true, }, { name: "Rect.IntersectsCircle(): circle overlaps one edge", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(0, 5))}, + args: args{c: pixel.C(pixel.V(0, 5), 1)}, want: true, }, { name: "Rect.IntersectsCircle(): edge is tangent", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, - args: args{c: pixel.C(1, pixel.V(-1, 5))}, + args: args{c: pixel.C(pixel.V(-1, 5), 1)}, want: true, }, } From ba4f417559e58d44f9eaa09079c8228d0bf6d01e Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 30 Jan 2019 08:38:51 +0000 Subject: [PATCH 17/22] corrected area formula --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 17c574b..7353f1a 100644 --- a/geometry.go +++ b/geometry.go @@ -360,7 +360,7 @@ func (c Circle) Norm() Circle { // Area returns the area of the Circle. func (c Circle) Area() float64 { - return math.Pi * c.Radius * 2 + return math.Pi * math.Pow(c.Radius, 2) } // Moved returns the Circle moved by the given vector delta. From 8650692efa621bc831694fa37a2589d33901ae21 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 31 Jan 2019 08:22:59 +0000 Subject: [PATCH 18/22] wip --- geometry.go | 84 ++++++++++++++++++++++++++++++++++++++++-------- geometry_test.go | 54 ++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 26 deletions(-) diff --git a/geometry.go b/geometry.go index 7353f1a..5937361 100644 --- a/geometry.go +++ b/geometry.go @@ -310,14 +310,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: @@ -467,24 +468,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 eec854d..2bdc284 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -469,55 +469,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 { From e8a3621140c0a1eb3280a6a1af34dc9551e859e5 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 14 Feb 2019 14:12:05 +0000 Subject: [PATCH 19/22] Made naming consistent --- geometry.go | 10 +++++----- geometry_test.go | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/geometry.go b/geometry.go index 5937361..eb87ede 100644 --- a/geometry.go +++ b/geometry.go @@ -310,15 +310,15 @@ func (r Rect) Intersect(s Rect) Rect { return t } -// IntersectsCircle returns a minimal required Vector, such that moving the circle by that vector would stop the Circle +// IntersectCircle 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 -func (r Rect) IntersectsCircle(c Circle) Vec { - return c.IntersectsRect(r).Scaled(-1) +func (r Rect) IntersectCircle(c Circle) Vec { + return c.IntersectRect(r).Scaled(-1) } // Circle is a 2D circle. It is defined by two properties: @@ -468,14 +468,14 @@ func (c Circle) Intersect(d Circle) Circle { } } -// IntersectsRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle +// 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 true if: // - The Rect contains the Circle, partially or fully // - The Circle contains the Rect, partially of fully -func (c Circle) IntersectsRect(r Rect) Vec { +func (c Circle) IntersectRect(r Rect) Vec { // h and v will hold the minimum horizontal and vertical distances (respectively) to avoid overlapping var h, v float64 diff --git a/geometry_test.go b/geometry_test.go index 2bdc284..e0cd305 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -457,7 +457,7 @@ func TestCircle_Intersect(t *testing.T) { } } -func TestRect_IntersectsCircle(t *testing.T) { +func TestRect_IntersectCircle(t *testing.T) { type fields struct { Min pixel.Vec Max pixel.Vec @@ -472,79 +472,79 @@ func TestRect_IntersectsCircle(t *testing.T) { want pixel.Vec }{ { - name: "Rect.IntersectsCircle(): no overlap", + 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.IntersectsCircle(): circle contains rect", + 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.IntersectsCircle(): rect contains circle", + 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.IntersectsCircle(): circle overlaps bottom-left corner", + 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, 0), 1)}, want: pixel.V(1, 0), }, { - name: "Rect.IntersectsCircle(): circle overlaps top-left corner", + 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, 10), 1)}, want: pixel.V(1, 0), }, { - name: "Rect.IntersectsCircle(): circle overlaps bottom-right corner", + 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, 0), 1)}, want: pixel.V(-1, 0), }, { - name: "Rect.IntersectsCircle(): circle overlaps top-right corner", + 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, 10), 1)}, want: pixel.V(-1, 0), }, { - name: "Rect.IntersectsCircle(): circle overlaps two corners", + 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.IntersectsCircle(): circle overlaps left edge", + 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.IntersectsCircle(): circle overlaps bottom edge", + 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.IntersectsCircle(): circle overlaps right edge", + 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.IntersectsCircle(): circle overlaps top edge", + 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.IntersectsCircle(): edge is tangent", + name: "Rect.IntersectCircle(): edge is tangent", fields: fields{Min: pixel.ZV, Max: pixel.V(10, 10)}, args: args{c: pixel.C(pixel.V(-1, 5), 1)}, want: pixel.ZV, @@ -556,8 +556,8 @@ func TestRect_IntersectsCircle(t *testing.T) { Min: tt.fields.Min, Max: tt.fields.Max, } - if got := r.IntersectsCircle(tt.args.c); got != tt.want { - t.Errorf("Rect.IntersectsCircle() = %v, want %v", got, tt.want) + if got := r.IntersectCircle(tt.args.c); got != tt.want { + t.Errorf("Rect.IntersectCircle() = %v, want %v", got, tt.want) } }) } From 91c16c34dad41b55854a6c4ca9e7d29ad29ec078 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 14 Feb 2019 14:12:54 +0000 Subject: [PATCH 20/22] fixed area tests --- geometry_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index e0cd305..914c944 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -204,7 +204,7 @@ func TestCircle_Area(t *testing.T) { { name: "Circle.Area(): positive radius", fields: fields{radius: 10, center: pixel.ZV}, - want: 20 * math.Pi, + want: 100 * math.Pi, }, { name: "Circle.Area(): zero radius", @@ -214,7 +214,7 @@ func TestCircle_Area(t *testing.T) { { name: "Circle.Area(): negative radius", fields: fields{radius: -5, center: pixel.ZV}, - want: -10 * math.Pi, + want: 25 * math.Pi, }, } for _, tt := range tests { From 3b63b7eff9c2ff1597dcfde11764ec476c6ee655 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 14 Feb 2019 14:15:26 +0000 Subject: [PATCH 21/22] corrected function preambles --- geometry.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geometry.go b/geometry.go index eb87ede..c7d8571 100644 --- a/geometry.go +++ b/geometry.go @@ -314,7 +314,7 @@ func (r Rect) Intersect(s Rect) Rect { // 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: +// 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 { @@ -472,7 +472,7 @@ func (c Circle) Intersect(d Circle) 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: +// 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 { From d4530ca9feb371bdfc7f5b0fb34c0747732f3bed Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 14 Feb 2019 14:18:14 +0000 Subject: [PATCH 22/22] More intersection tests --- geometry_test.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/geometry_test.go b/geometry_test.go index 914c944..2a4a660 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -544,11 +544,41 @@ func TestRect_IntersectCircle(t *testing.T) { want: pixel.V(0, -1), }, { - name: "Rect.IntersectCircle(): edge is tangent", + 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) {