From d3912a6486ed2806a6743b5f2203ffdfce3a8b38 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:31:20 +0100 Subject: [PATCH 01/36] Added Line struct and methods --- geometry.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/geometry.go b/geometry.go index 1e2922d..7e10a69 100644 --- a/geometry.go +++ b/geometry.go @@ -181,6 +181,173 @@ func Lerp(a, b Vec, t float64) Vec { return a.Scaled(1 - t).Add(b.Scaled(t)) } +// Line is a 2D line segment, between points `A` and `B`. +type Line struct { + A, B Vec +} + +// Bounds returns the lines bounding box. This is in the form of a normalized `Rect`. +func (l Line) Bounds() Rect { + return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm() +} + +// Center will return the point at center of the line; that is, the point equidistant from either end. +func (l Line) Center() Vec { + return l.A.Add(l.A.To(l.B).Scaled(0.5)) +} + +// Closest will return the point on the line which is closest to the `Vec` provided. +func (l Line) Closest(v Vec) Vec { + lenSquared := math.Pow(l.Len(), 2) + + if lenSquared == 0 { + return l.A + } + + t := math.Max(0, math.Min(1, l.A.Sub(v).Dot(l.B.Sub(v))/lenSquared)) + projection := l.A.Add(l.B.Sub(v).Scaled(t)) + return v.To(projection) +} + +// Contains returns whether the provided `Vec` lies on the line +func (l Line) Contains(v Vec) bool { + return l.Closest(v) == v +} + +// Formula will return the values that represent the line in the formula: y = mx + b +func (l Line) Formula() (m, b float64) { + m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X) + b = l.A.Y - (m * l.A.X) + + return m, b +} + +// Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, +// this function will return the zero-vector and `false`. +func (l Line) Intersect(k Line) (Vec, bool) { + // Check if the lines are parallel + lDir := l.A.To(l.B) + kDir := k.A.To(k.B) + if math.Abs(lDir.X) == math.Abs(kDir.X) && math.Abs(lDir.Y) == math.Abs(kDir.Y) { + return ZV, false + } + + // The lines intersect - but potentially not within the line segments. + // Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the + // segments + lm, lb := l.Formula() + km, kb := l.Formula() + + // Coordinates of intersect + x := (kb - lb) / (lm - km) + y := lm*x + lb + + if l.Contains(V(x, y)) && k.Contains(V(x, y)) { + // The intersect point is on both line segments, they intersect. + return V(x, y), true + } + + return ZV, false +} + +// IntersectCircle will return the shortest `Vec` such that the Line and Circle no longer intesect. If they do not +// intersect at all, this function will return a zero-vector. +func (l Line) IntersectCircle(c Circle) Vec { + // Get the point on the line closest to the center of the circle. + closest := l.Closest(c.Center) + cirToClosest := c.Center.To(closest) + + if cirToClosest.Len() >= c.Radius { + return ZV + } + + return cirToClosest.Scaled(cirToClosest.Len() - c.Radius) +} + +// IntersectRect will return the shortest `Vec` such that the Line and Rect no longer intesect. If they do not +// intersect at all, this function will return a zero-vector. +func (l Line) IntersectRect(r Rect) Vec { + // Check if either end of the line segment are within the rectangle + if r.Contains(l.A) || r.Contains(l.B) { + // Use the `Rect.Intersect` to get minimal return value + rIntersect := l.Bounds().Intersect(r) + if rIntersect.H() > rIntersect.W() { + // Go vertical + return V(0, rIntersect.H()) + } + return V(rIntersect.W(), 0) + } + + // Check if any of the rectangles' edges intersect with this line. + for _, edge := range r.Edges() { + if _, ok := l.Intersect(edge); ok { + // Get the closest points on the line to each corner, where: + // - the point is contained by the rectangle + // - the point is not the corner itself + corners := r.Vertices() + closest := ZV + closestCorner := corners[0] + for _, c := range corners { + cc := l.Closest(c) + if closest != ZV || (closest.Len() > cc.Len() && r.Contains(cc)) { + closest = cc + closestCorner = c + } + } + + return closest.To(closestCorner) + } + } + + // No intersect + return ZV +} + +// Len returns the length of the line segment. +func (l Line) Len() float64 { + return l.A.Sub(l.B).Len() +} + +// Moved will return a line moved by the delta `Vec` provided. +func (l Line) Moved(delta Vec) Line { + return Line{ + A: l.A.Add(delta), + B: l.B.Add(delta), + } +} + +// Rotated will rotate the line around the provided `Vec`. +func (l Line) Rotated(around Vec, angle float64) Line { + // Move the line so we can use `Vec.Rotated` + lineShifted := l.Moved(around.Scaled(-1)) + lineRotated := Line{ + A: lineShifted.A.Rotated(angle), + B: lineShifted.B.Rotated(angle), + } + + return lineRotated.Moved(around) +} + +// Scaled will return the line scaled around the center point. +func (l Line) Scaled(scale float64) Line { + return l.ScaledXY(l.Center(), scale) +} + +// ScaledXY will return the line scaled around the `Vec` provided. +func (l Line) ScaledXY(around Vec, scale float64) Line { + toA := around.To(l.A).Scaled(scale) + toB := around.To(l.B).Scaled(scale) + + return Line{ + A: around.Add(toA), + B: around.Add(toB), + } +} + +func (l Line) String() string { + return fmt.Sprintf("Line(%v, %v)", l.A, l.B) +} + // Rect is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two // points, Min and Max. // @@ -243,6 +410,18 @@ func (r Rect) Area() float64 { return r.W() * r.H() } +// Edges will return the four lines which make up the edges of the rectangle. +func (r Rect) Edges() [4]Line { + corners := r.Vertices() + + return [4]Line{ + {A: corners[0], B: corners[1]}, + {A: corners[1], B: corners[2]}, + {A: corners[2], B: corners[3]}, + {A: corners[3], B: corners[0]}, + } +} + // Center returns the position of the center of the Rect. func (r Rect) Center() Vec { return Lerp(r.Min, r.Max, 0.5) @@ -329,6 +508,22 @@ func (r Rect) IntersectCircle(c Circle) Vec { return c.IntersectRect(r).Scaled(-1) } +// IntersectLine will return the shortest `Vec` such that if the Rect is moved by the Vec returned, the Line and Rect no +// longer intersect. +func (r Rect) IntersectLine(l Line) Vec { + return l.IntersectRect(r).Scaled(-1) +} + +// Vertices returns a slice of the four corners which make up the rectangle. +func (r Rect) Vertices() [4]Vec { + return [4]Vec{ + r.Min, + r.Max, + V(r.Min.X, r.Max.Y), + V(r.Max.X, r.Min.Y), + } +} + // Circle is a 2D circle. It is defined by two properties: // - Center vector // - Radius float64 @@ -476,6 +671,12 @@ func (c Circle) Intersect(d Circle) Circle { } } +// IntersectLine will return the shortest `Vec` such that if the Rect is moved by the Vec returned, the Line and Rect no +// longer intersect. +func (c Circle) IntersectLine(l Line) Vec { + return l.IntersectCircle(c).Scaled(-1) +} + // IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle // and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only // the perimeters touch. From a5ea71811ea1670166b63c1ed74e415ef54eac8b Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:31:34 +0100 Subject: [PATCH 02/36] Added test templates --- geometry_test.go | 369 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) diff --git a/geometry_test.go b/geometry_test.go index dfa78cf..cd95795 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -690,3 +690,372 @@ func TestRect_IntersectCircle(t *testing.T) { }) } } + +func TestLine_Bounds(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + tests := []struct { + name string + fields fields + want pixel.Rect + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Bounds(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Bounds() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Center(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + tests := []struct { + name string + fields fields + want pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Center(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Center() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Closest(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + v pixel.Vec + } + tests := []struct { + name string + fields fields + args args + want pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Closest(tt.args.v); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Closest() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Contains(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + v pixel.Vec + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Contains(tt.args.v); got != tt.want { + t.Errorf("Line.Contains() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Formula(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + tests := []struct { + name string + fields fields + wantM float64 + wantB float64 + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + gotM, gotB := l.Formula() + if gotM != tt.wantM { + t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM) + } + if gotB != tt.wantB { + t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB) + } + }) + } +} + +func TestLine_Intersect(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + k pixel.Line + } + tests := []struct { + name string + fields fields + args args + want pixel.Vec + want1 bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + got, got1 := l.Intersect(tt.args.k) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Intersect() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Line.Intersect() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestLine_IntersectCircle(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + c pixel.Circle + } + tests := []struct { + name string + fields fields + args args + want pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.IntersectCircle(tt.args.c); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.IntersectCircle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_IntersectRect(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + r pixel.Rect + } + tests := []struct { + name string + fields fields + args args + want pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.IntersectRect(tt.args.r); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.IntersectRect() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Len(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + tests := []struct { + name string + fields fields + want float64 + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Len(); got != tt.want { + t.Errorf("Line.Len() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Rotated(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + around pixel.Vec + angle float64 + } + tests := []struct { + name string + fields fields + args args + want pixel.Line + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Rotated(tt.args.around, tt.args.angle); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Rotated() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_Scaled(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + scale float64 + } + tests := []struct { + name string + fields fields + args args + want pixel.Line + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.Scaled(tt.args.scale); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.Scaled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_ScaledXY(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + type args struct { + around pixel.Vec + scale float64 + } + tests := []struct { + name string + fields fields + args args + want pixel.Line + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.ScaledXY(tt.args.around, tt.args.scale); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Line.ScaledXY() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLine_String(t *testing.T) { + type fields struct { + A pixel.Vec + B pixel.Vec + } + tests := []struct { + name string + fields fields + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := pixel.Line{ + A: tt.fields.A, + B: tt.fields.B, + } + if got := l.String(); got != tt.want { + t.Errorf("Line.String() = %v, want %v", got, tt.want) + } + }) + } +} From 1eac5d8dc2671556f2d8d03c85c9425b284ea555 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:33:31 +0100 Subject: [PATCH 03/36] Added test templates for new rect tests --- geometry_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/geometry_test.go b/geometry_test.go index cd95795..8b13f5e 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -10,6 +10,31 @@ import ( "github.com/stretchr/testify/assert" ) +func TestRect_Edges(t *testing.T) { + type fields struct { + Min pixel.Vec + Max pixel.Vec + } + tests := []struct { + name string + fields fields + want [4]pixel.Line + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := pixel.Rect{ + Min: tt.fields.Min, + Max: tt.fields.Max, + } + if got := r.Edges(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Rect.Edges() = %v, want %v", got, tt.want) + } + }) + } +} + func TestRect_Resize(t *testing.T) { type rectTestTransform struct { name string @@ -80,6 +105,31 @@ func TestRect_Resize(t *testing.T) { } } +func TestRect_Vertices(t *testing.T) { + type fields struct { + Min pixel.Vec + Max pixel.Vec + } + tests := []struct { + name string + fields fields + want [4]pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := pixel.Rect{ + Min: tt.fields.Min, + Max: tt.fields.Max, + } + if got := r.Vertices(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Rect.Vertices() = %v, want %v", got, tt.want) + } + }) + } +} + func TestMatrix_Unproject(t *testing.T) { const delta = 1e-15 t.Run("for rotated matrix", func(t *testing.T) { From 128ec4d4c0b000fdabb925bc7bd09bb7b8bb95bd Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:37:23 +0100 Subject: [PATCH 04/36] Added function to create a Line --- geometry.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/geometry.go b/geometry.go index 7e10a69..a474ca7 100644 --- a/geometry.go +++ b/geometry.go @@ -186,6 +186,14 @@ type Line struct { A, B Vec } +// L creates and returns a new Line object. +func L(from, to Vec) Line { + return Line{ + A: from, + B: to, + } +} + // Bounds returns the lines bounding box. This is in the form of a normalized `Rect`. func (l Line) Bounds() Rect { return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm() From 9d07d69429a037ec3bcd4be97589156bc383414b Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:42:20 +0100 Subject: [PATCH 05/36] Fixed so corners are provided in anticlockwise pattern --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index a474ca7..053a016 100644 --- a/geometry.go +++ b/geometry.go @@ -526,8 +526,8 @@ func (r Rect) IntersectLine(l Line) Vec { func (r Rect) Vertices() [4]Vec { return [4]Vec{ r.Min, - r.Max, V(r.Min.X, r.Max.Y), + r.Max, V(r.Max.X, r.Min.Y), } } From 1a275b59295cc4b1c8e3c276777392643042fd07 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 15:44:14 +0100 Subject: [PATCH 06/36] Filled rect tests --- geometry_test.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 8b13f5e..ca2b286 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -20,7 +20,16 @@ func TestRect_Edges(t *testing.T) { fields fields want [4]pixel.Line }{ - // TODO: Add test cases. + { + name: "Get edges", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + want: [4]pixel.Line{ + pixel.L(pixel.V(0, 0), pixel.V(0, 10)), + pixel.L(pixel.V(0, 10), pixel.V(10, 10)), + pixel.L(pixel.V(10, 10), pixel.V(10, 0)), + pixel.L(pixel.V(10, 0), pixel.V(0, 0)), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -115,7 +124,16 @@ func TestRect_Vertices(t *testing.T) { fields fields want [4]pixel.Vec }{ - // TODO: Add test cases. + { + name: "Get corners", + fields: fields{Min: pixel.V(0, 0), Max: pixel.V(10, 10)}, + want: [4]pixel.Vec{ + pixel.V(0, 0), + pixel.V(0, 10), + pixel.V(10, 10), + pixel.V(10, 0), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From cca37c750e0ea5421e3ce6f3c544f5913fd9788d Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Mon, 1 Apr 2019 16:01:08 +0100 Subject: [PATCH 07/36] wip adding line tests --- geometry_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index ca2b286..32dac1e 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -769,7 +769,16 @@ func TestLine_Bounds(t *testing.T) { fields fields want pixel.Rect }{ - // TODO: Add test cases. + { + name: "Positive slope", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + want: pixel.R(0, 0, 10, 10), + }, + { + name: "Negative slope", + fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)}, + want: pixel.R(0, 0, 10, 10), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -794,7 +803,16 @@ func TestLine_Center(t *testing.T) { fields fields want pixel.Vec }{ - // TODO: Add test cases. + { + name: "Positive slope", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + want: pixel.V(5, 5), + }, + { + name: "Negative slope", + fields: fields{A: pixel.V(10, 10), B: pixel.V(0, 0)}, + want: pixel.V(5, 5), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -823,7 +841,24 @@ func TestLine_Closest(t *testing.T) { args args want pixel.Vec }{ - // TODO: Add test cases. + { + name: "Point on line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(5, 5)}, + want: pixel.V(5, 5), + }, + { + name: "Point on next to line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(0, 10)}, + want: pixel.V(5, 5), + }, + { + name: "Point on inline with line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(20, 20)}, + want: pixel.V(10, 10), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -852,7 +887,18 @@ func TestLine_Contains(t *testing.T) { args args want bool }{ - // TODO: Add test cases. + { + name: "Point on line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(5, 5)}, + want: true, + }, + { + name: "Point not on line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(0, 10)}, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -878,7 +924,24 @@ func TestLine_Formula(t *testing.T) { wantM float64 wantB float64 }{ - // TODO: Add test cases. + { + name: "Getting formula - 45 degs", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + wantM: 1, + wantB: 0, + }, + { + name: "Getting formula - 90 degs", + fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)}, + wantM: math.Inf(1), + wantB: math.NaN(), + }, + { + name: "Getting formula - 0 degs", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 0)}, + wantM: 0, + wantB: 0, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 2478da5d12f9235d2406e75891236fe60dadb336 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 08:49:45 +0100 Subject: [PATCH 08/36] wip adding line tests --- geometry_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/geometry_test.go b/geometry_test.go index 32dac1e..1493a5a 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -975,7 +975,27 @@ func TestLine_Intersect(t *testing.T) { want pixel.Vec want1 bool }{ - // TODO: Add test cases. + { + name: "Lines intersect", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{k: pixel.L(pixel.V(0, 10), pixel.V(10, 0))}, + want: pixel.V(5, 5), + want1: true, + }, + { + name: "Lines don't intersect", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{k: pixel.L(pixel.V(0, 10), pixel.V(1, 20))}, + want: pixel.ZV, + want1: false, + }, + { + name: "Lines parallel", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{k: pixel.L(pixel.V(0, 1), pixel.V(10, 11))}, + want: pixel.ZV, + want1: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 55b87ca5b12561bd1a30e31fc43b7b3944ba6f47 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 13:33:41 +0100 Subject: [PATCH 09/36] WIP line tests --- geometry_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 1493a5a..51c2ea9 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1028,7 +1028,18 @@ func TestLine_IntersectCircle(t *testing.T) { args args want pixel.Vec }{ - // TODO: Add test cases. + { + name: "Cirle intersects", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{c: pixel.C(pixel.V(5, 5), 1)}, + want: pixel.V(1, -1), + }, + { + name: "Cirle doesn't intersects", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{c: pixel.C(pixel.V(0, 5), 1)}, + want: pixel.ZV, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1057,7 +1068,36 @@ func TestLine_IntersectRect(t *testing.T) { args args want pixel.Vec }{ - // TODO: Add test cases. + { + name: "Line through rect vertically", + fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 10)}, + args: args{r: pixel.R(-1, 1, 5, 5)}, + want: pixel.V(-1, 0), + }, + { + name: "Line through rect horizontally", + fields: fields{A: pixel.V(-5, 0), B: pixel.V(5, 0)}, + args: args{r: pixel.R(-2, -5, 2, 1)}, + want: pixel.V(0, 1), + }, + { + name: "Line through rect diagonally bottom and left edges", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{r: pixel.R(0, 2, 3, 3)}, + want: pixel.V(1, -1), + }, + { + name: "Line through rect diagonally top and right edges", + fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)}, + args: args{r: pixel.R(5, 0, 8, 3)}, + want: pixel.V(-1, -1), + }, + { + name: "Line with not rect intersect", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{r: pixel.R(20, 20, 21, 21)}, + want: pixel.ZV, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1082,7 +1122,31 @@ func TestLine_Len(t *testing.T) { fields fields want float64 }{ - // TODO: Add test cases. + { + name: "End right-up of start", + fields: fields{A: pixel.V(0, 0), B: pixel.V(3, 4)}, + want: 5, + }, + { + name: "End left-up of start", + fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, 4)}, + want: 5, + }, + { + name: "End right-down of start", + fields: fields{A: pixel.V(0, 0), B: pixel.V(3, -4)}, + want: 5, + }, + { + name: "End left-down of start", + fields: fields{A: pixel.V(0, 0), B: pixel.V(-3, -4)}, + want: 5, + }, + { + name: "End same as start", + fields: fields{A: pixel.V(0, 0), B: pixel.V(0, 0)}, + want: 0, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1112,7 +1176,24 @@ func TestLine_Rotated(t *testing.T) { args args want pixel.Line }{ - // TODO: Add test cases. + { + name: "Rotating around line center", + fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + args: args{around: pixel.V(2, 2), angle: 2 * math.Pi}, + want: pixel.L(pixel.V(1, 1), pixel.V(3, 3)), + }, + { + name: "Rotating around x-y origin", + fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + args: args{around: pixel.V(0, 0), angle: 2 * math.Pi}, + want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)), + }, + { + name: "Rotating around line end", + fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + args: args{around: pixel.V(1, 1), angle: 2 * math.Pi}, + want: pixel.L(pixel.V(1, 1), pixel.V(-2, -2)), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1196,7 +1277,11 @@ func TestLine_String(t *testing.T) { fields fields want string }{ - // TODO: Add test cases. + { + name: "Getting string", + fields: fields{A: pixel.V(0, 0), B: pixel.V(1, 1)}, + want: "Line(Vec(0, 0), Vec(1, 1))", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From c7eac064993cd6b40c45c8c530d088614743faa2 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 13:46:50 +0100 Subject: [PATCH 10/36] WIP line tests --- geometry_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 51c2ea9..2b46b44 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1222,7 +1222,30 @@ func TestLine_Scaled(t *testing.T) { args args want pixel.Line }{ - // TODO: Add test cases. + { + name: "Scaling by 1", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{scale: 1}, + want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)), + }, + { + name: "Scaling by >1", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{scale: 2}, + want: pixel.L(pixel.V(-5, -5), pixel.V(15, 15)), + }, + { + name: "Scaling by <1", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{scale: 0.5}, + want: pixel.L(pixel.V(2.5, 2.5), pixel.V(7.5, 7.5)), + }, + { + name: "Scaling by -1", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{scale: -1}, + want: pixel.L(pixel.V(10, 10), pixel.V(0, 0)), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1252,7 +1275,30 @@ func TestLine_ScaledXY(t *testing.T) { args args want pixel.Line }{ - // TODO: Add test cases. + { + name: "Scaling by 1 around origin", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{around: pixel.ZV, scale: 1}, + want: pixel.L(pixel.V(0, 0), pixel.V(10, 10)), + }, + { + name: "Scaling by >1 around origin", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{around: pixel.ZV, scale: 2}, + want: pixel.L(pixel.V(0, 0), pixel.V(20, 20)), + }, + { + name: "Scaling by <1 around origin", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{around: pixel.ZV, scale: 0.5}, + want: pixel.L(pixel.V(0, 0), pixel.V(5, 5)), + }, + { + name: "Scaling by -1 around origin", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{around: pixel.ZV, scale: -1}, + want: pixel.L(pixel.V(0, 0), pixel.V(-10, -10)), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 237d77596ff9eb33b73cf71dc5b68a07aba87c10 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 13:48:43 +0100 Subject: [PATCH 11/36] WIP line tests --- geometry_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geometry_test.go b/geometry_test.go index 2b46b44..e2414b8 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -954,7 +954,9 @@ func TestLine_Formula(t *testing.T) { t.Errorf("Line.Formula() gotM = %v, want %v", gotM, tt.wantM) } if gotB != tt.wantB { - t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB) + if math.IsNaN(tt.wantB) && !math.IsNaN(gotB) { + t.Errorf("Line.Formula() gotB = %v, want %v", gotB, tt.wantB) + } } }) } From 04c3ef72a301b80d687e53f7e5c737394f520e5a Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 16:27:54 +0100 Subject: [PATCH 12/36] WIP line tests --- geometry.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/geometry.go b/geometry.go index 053a016..5126aaa 100644 --- a/geometry.go +++ b/geometry.go @@ -206,15 +206,28 @@ func (l Line) Center() Vec { // Closest will return the point on the line which is closest to the `Vec` provided. func (l Line) Closest(v Vec) Vec { - lenSquared := math.Pow(l.Len(), 2) + // Check if the point is within the lines' bounding box, if not, one of the endpoints will be the closest point + if !l.Bounds().Contains(v) { + // Not within bounding box + toStart := v.To(l.A) + toEnd := v.To(l.B) - if lenSquared == 0 { - return l.A + if toStart.Len() < toEnd.Len() { + return l.A + } + return l.B } - t := math.Max(0, math.Min(1, l.A.Sub(v).Dot(l.B.Sub(v))/lenSquared)) - projection := l.A.Add(l.B.Sub(v).Scaled(t)) - return v.To(projection) + // Closest point will be on a line, perpendicular to this line + m, b := l.Formula() + perpendicularM := -1 / m + perpendicularB := v.Y - (perpendicularM * v.X) + + // Coordinates of intersect (of infinite lines) + x := (perpendicularB - b) / (m - perpendicularM) + y := m*x - b + + return V(x, y) } // Contains returns whether the provided `Vec` lies on the line From 997f23dfb5b670e282d243b1133b67ad07d203af Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Tue, 2 Apr 2019 17:01:18 +0100 Subject: [PATCH 13/36] WIP line tests --- geometry.go | 17 +++++++++++++++-- geometry_test.go | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/geometry.go b/geometry.go index 5126aaa..d71cbd8 100644 --- a/geometry.go +++ b/geometry.go @@ -222,11 +222,18 @@ func (l Line) Closest(v Vec) Vec { m, b := l.Formula() perpendicularM := -1 / m perpendicularB := v.Y - (perpendicularM * v.X) + fmt.Println(m) + fmt.Println(b) + fmt.Println(perpendicularM) + fmt.Println(perpendicularB) // Coordinates of intersect (of infinite lines) x := (perpendicularB - b) / (m - perpendicularM) y := m*x - b + fmt.Println(x) + fmt.Println(y) + return V(x, y) } @@ -246,10 +253,13 @@ func (l Line) Formula() (m, b float64) { // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, // this function will return the zero-vector and `false`. func (l Line) Intersect(k Line) (Vec, bool) { + fmt.Printf("%v, %v\n", l, k) // Check if the lines are parallel lDir := l.A.To(l.B) kDir := k.A.To(k.B) - if math.Abs(lDir.X) == math.Abs(kDir.X) && math.Abs(lDir.Y) == math.Abs(kDir.Y) { + fmt.Printf("%v, %v\n", lDir, kDir) + if lDir.X == kDir.X && lDir.Y == kDir.Y { + fmt.Println("p") return ZV, false } @@ -257,11 +267,14 @@ func (l Line) Intersect(k Line) (Vec, bool) { // Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the // segments lm, lb := l.Formula() - km, kb := l.Formula() + km, kb := k.Formula() + fmt.Printf("%.2f, %.2f -- %.2f, %.2f\n", lm, lb, km, kb) // Coordinates of intersect x := (kb - lb) / (lm - km) y := lm*x + lb + fmt.Printf("(%.2f, %.2f)\n", x, y) + fmt.Printf("%t %t\n", l.Contains(V(x, y)), k.Contains(V(x, y))) if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. diff --git a/geometry_test.go b/geometry_test.go index e2414b8..ce31f9a 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -893,6 +893,12 @@ func TestLine_Contains(t *testing.T) { args: args{v: pixel.V(5, 5)}, want: true, }, + { + name: "Point on negative sloped line", + fields: fields{A: pixel.V(0, 10), B: pixel.V(10, 0)}, + args: args{v: pixel.V(5, 5)}, + want: true, + }, { name: "Point not on line", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, From 8cd352b7a32ea4b9d6dfd443d52a3b953e60b236 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 08:13:33 +0100 Subject: [PATCH 14/36] WIP line tests --- geometry.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/geometry.go b/geometry.go index d71cbd8..5854914 100644 --- a/geometry.go +++ b/geometry.go @@ -222,17 +222,10 @@ func (l Line) Closest(v Vec) Vec { m, b := l.Formula() perpendicularM := -1 / m perpendicularB := v.Y - (perpendicularM * v.X) - fmt.Println(m) - fmt.Println(b) - fmt.Println(perpendicularM) - fmt.Println(perpendicularB) // Coordinates of intersect (of infinite lines) x := (perpendicularB - b) / (m - perpendicularM) - y := m*x - b - - fmt.Println(x) - fmt.Println(y) + y := m*x + b return V(x, y) } @@ -253,11 +246,9 @@ func (l Line) Formula() (m, b float64) { // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, // this function will return the zero-vector and `false`. func (l Line) Intersect(k Line) (Vec, bool) { - fmt.Printf("%v, %v\n", l, k) // Check if the lines are parallel lDir := l.A.To(l.B) kDir := k.A.To(k.B) - fmt.Printf("%v, %v\n", lDir, kDir) if lDir.X == kDir.X && lDir.Y == kDir.Y { fmt.Println("p") return ZV, false @@ -268,13 +259,10 @@ func (l Line) Intersect(k Line) (Vec, bool) { // segments lm, lb := l.Formula() km, kb := k.Formula() - fmt.Printf("%.2f, %.2f -- %.2f, %.2f\n", lm, lb, km, kb) // Coordinates of intersect x := (kb - lb) / (lm - km) y := lm*x + lb - fmt.Printf("(%.2f, %.2f)\n", x, y) - fmt.Printf("%t %t\n", l.Contains(V(x, y)), k.Contains(V(x, y))) if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. From 11e2012ef5f1d8010b4c38e8ef36a34c90f7bbf2 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 08:23:34 +0100 Subject: [PATCH 15/36] WIP line tests --- geometry.go | 1 - geometry_test.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/geometry.go b/geometry.go index 5854914..9f2fe92 100644 --- a/geometry.go +++ b/geometry.go @@ -250,7 +250,6 @@ func (l Line) Intersect(k Line) (Vec, bool) { lDir := l.A.To(l.B) kDir := k.A.To(k.B) if lDir.X == kDir.X && lDir.Y == kDir.Y { - fmt.Println("p") return ZV, false } diff --git a/geometry_test.go b/geometry_test.go index ce31f9a..2690054 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1039,8 +1039,8 @@ func TestLine_IntersectCircle(t *testing.T) { { name: "Cirle intersects", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, - args: args{c: pixel.C(pixel.V(5, 5), 1)}, - want: pixel.V(1, -1), + args: args{c: pixel.C(pixel.V(6, 4), 2)}, + want: pixel.V(0.5857864376269049, -0.5857864376269049), }, { name: "Cirle doesn't intersects", From eb3d7e0787411875f3490fcbe1a580e8d2c02d57 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 11:09:14 +0100 Subject: [PATCH 16/36] wip line tests --- geometry.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++-- geometry_test.go | 74 +++++++++++++++++++++++++++++++--------------- 2 files changed, 125 insertions(+), 26 deletions(-) diff --git a/geometry.go b/geometry.go index 9f2fe92..d6f9709 100644 --- a/geometry.go +++ b/geometry.go @@ -206,8 +206,30 @@ func (l Line) Center() Vec { // Closest will return the point on the line which is closest to the `Vec` provided. func (l Line) Closest(v Vec) Vec { + // Closest point will be on a line, perpendicular to this line + m, b := l.Formula() + + // Account for horizontal lines + if m == 0 { + fmt.Println("h", l) + x := v.X + y := l.A.Y + fmt.Println(x, y) + return V(x, y) + } + + // Account for vertical lines + if math.IsInf(math.Abs(m), 1) { + fmt.Println("v", l) + x := l.A.X + y := v.Y + fmt.Println(x, y) + return V(x, y) + } + // Check if the point is within the lines' bounding box, if not, one of the endpoints will be the closest point if !l.Bounds().Contains(v) { + fmt.Println("out") // Not within bounding box toStart := v.To(l.A) toEnd := v.To(l.B) @@ -218,14 +240,14 @@ func (l Line) Closest(v Vec) Vec { return l.B } - // Closest point will be on a line, perpendicular to this line - m, b := l.Formula() perpendicularM := -1 / m perpendicularB := v.Y - (perpendicularM * v.X) + fmt.Println(m, b, perpendicularM, perpendicularB) // Coordinates of intersect (of infinite lines) x := (perpendicularB - b) / (m - perpendicularM) y := m*x + b + fmt.Println(x, y) return V(x, y) } @@ -237,6 +259,11 @@ func (l Line) Contains(v Vec) bool { // Formula will return the values that represent the line in the formula: y = mx + b func (l Line) Formula() (m, b float64) { + // Account for horizontal lines + if l.B.Y == l.A.Y { + return 0, l.A.Y + } + m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X) b = l.A.Y - (m * l.A.X) @@ -246,9 +273,11 @@ func (l Line) Formula() (m, b float64) { // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, // this function will return the zero-vector and `false`. func (l Line) Intersect(k Line) (Vec, bool) { + fmt.Println(l, k) // Check if the lines are parallel lDir := l.A.To(l.B) kDir := k.A.To(k.B) + fmt.Println(lDir, kDir) if lDir.X == kDir.X && lDir.Y == kDir.Y { return ZV, false } @@ -258,10 +287,47 @@ func (l Line) Intersect(k Line) (Vec, bool) { // segments lm, lb := l.Formula() km, kb := k.Formula() + fmt.Println(lm, lb, km, kb) + + // Account for vertical lines + if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) { + // Both vertical, therefore parallel + return ZV, false + } + + if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) { + // One line is vertical + intersectM := lm + intersectB := lb + verticalLine := k + + if math.IsInf(math.Abs(lm), 1) { + intersectM = km + intersectB = kb + verticalLine = l + } + + maxVerticalY := verticalLine.A.Y + minVerticalY := verticalLine.B.Y + if verticalLine.B.Y > maxVerticalY { + maxVerticalY = verticalLine.B.Y + minVerticalY = verticalLine.A.Y + } + + y := intersectM*l.A.X + intersectB + if y > maxVerticalY || y < minVerticalY { + // Point is not on the horizontal line + return ZV, false + } + + return V(l.A.X, y), true + } // Coordinates of intersect x := (kb - lb) / (lm - km) y := lm*x + lb + fmt.Println(x, y) + fmt.Println(l.Contains(V(x, y)), k.Contains(V(x, y))) if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. @@ -290,18 +356,22 @@ func (l Line) IntersectCircle(c Circle) Vec { func (l Line) IntersectRect(r Rect) Vec { // Check if either end of the line segment are within the rectangle if r.Contains(l.A) || r.Contains(l.B) { + fmt.Println("yes1") // Use the `Rect.Intersect` to get minimal return value rIntersect := l.Bounds().Intersect(r) if rIntersect.H() > rIntersect.W() { + fmt.Println("yes2") // Go vertical return V(0, rIntersect.H()) } return V(rIntersect.W(), 0) } + fmt.Println("No") // Check if any of the rectangles' edges intersect with this line. for _, edge := range r.Edges() { if _, ok := l.Intersect(edge); ok { + fmt.Println(edge) // Get the closest points on the line to each corner, where: // - the point is contained by the rectangle // - the point is not the corner itself @@ -316,6 +386,9 @@ func (l Line) IntersectRect(r Rect) Vec { } } + fmt.Println(closest) + fmt.Println(closestCorner) + return closest.To(closestCorner) } } diff --git a/geometry_test.go b/geometry_test.go index 2690054..dfcbf32 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -853,6 +853,18 @@ func TestLine_Closest(t *testing.T) { args: args{v: pixel.V(0, 10)}, want: pixel.V(5, 5), }, + { + name: "Point on next to vertical line", + fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)}, + args: args{v: pixel.V(6, 5)}, + want: pixel.V(5, 5), + }, + { + name: "Point on next to horizontal line", + fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)}, + args: args{v: pixel.V(5, 6)}, + want: pixel.V(5, 5), + }, { name: "Point on inline with line", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, @@ -990,6 +1002,20 @@ func TestLine_Intersect(t *testing.T) { want: pixel.V(5, 5), want1: true, }, + { + name: "Line intersect with vertical", + fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)}, + args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: pixel.V(5, 5), + want1: true, + }, + { + name: "Line intersect with horizontal", + fields: fields{A: pixel.V(0, 5), B: pixel.V(10, 5)}, + args: args{k: pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: pixel.V(5, 5), + want1: true, + }, { name: "Lines don't intersect", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, @@ -1082,30 +1108,30 @@ func TestLine_IntersectRect(t *testing.T) { args: args{r: pixel.R(-1, 1, 5, 5)}, want: pixel.V(-1, 0), }, - { - name: "Line through rect horizontally", - fields: fields{A: pixel.V(-5, 0), B: pixel.V(5, 0)}, - args: args{r: pixel.R(-2, -5, 2, 1)}, - want: pixel.V(0, 1), - }, - { - name: "Line through rect diagonally bottom and left edges", - fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, - args: args{r: pixel.R(0, 2, 3, 3)}, - want: pixel.V(1, -1), - }, - { - name: "Line through rect diagonally top and right edges", - fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)}, - args: args{r: pixel.R(5, 0, 8, 3)}, - want: pixel.V(-1, -1), - }, - { - name: "Line with not rect intersect", - fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, - args: args{r: pixel.R(20, 20, 21, 21)}, - want: pixel.ZV, - }, + // { + // name: "Line through rect horizontally", + // fields: fields{A: pixel.V(-5, 0), B: pixel.V(5, 0)}, + // args: args{r: pixel.R(-2, -5, 2, 1)}, + // want: pixel.V(0, 1), + // }, + // { + // name: "Line through rect diagonally bottom and left edges", + // fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + // args: args{r: pixel.R(0, 2, 3, 3)}, + // want: pixel.V(1, -1), + // }, + // { + // name: "Line through rect diagonally top and right edges", + // fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)}, + // args: args{r: pixel.R(5, 0, 8, 3)}, + // want: pixel.V(-1, -1), + // }, + // { + // name: "Line with not rect intersect", + // fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + // args: args{r: pixel.R(20, 20, 21, 21)}, + // want: pixel.ZV, + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From aa7ef59ecd79828d3b5e2c35fdcac758fceb00b4 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 11:47:36 +0100 Subject: [PATCH 17/36] wip line tests --- geometry.go | 46 +++++++++++++++-------------------------- geometry_test.go | 54 +++++++++++++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/geometry.go b/geometry.go index d6f9709..9872b74 100644 --- a/geometry.go +++ b/geometry.go @@ -211,25 +211,34 @@ func (l Line) Closest(v Vec) Vec { // Account for horizontal lines if m == 0 { - fmt.Println("h", l) x := v.X y := l.A.Y - fmt.Println(x, y) return V(x, y) } // Account for vertical lines if math.IsInf(math.Abs(m), 1) { - fmt.Println("v", l) x := l.A.X y := v.Y - fmt.Println(x, y) return V(x, y) } - // Check if the point is within the lines' bounding box, if not, one of the endpoints will be the closest point - if !l.Bounds().Contains(v) { - fmt.Println("out") + perpendicularM := -1 / m + perpendicularB := v.Y - (perpendicularM * v.X) + + // Coordinates of intersect (of infinite lines) + x := (perpendicularB - b) / (m - perpendicularM) + y := m*x + b + + // between is a helper function which determines whether 'x' is greater than min(a, b) and less than max(a, b) + between := func(a, b, x float64) bool { + min := math.Min(a, b) + max := math.Max(a, b) + return min < x && x < max + } + + // Check if the point lies between the x and y bounds of the segment + if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) { // Not within bounding box toStart := v.To(l.A) toEnd := v.To(l.B) @@ -240,15 +249,6 @@ func (l Line) Closest(v Vec) Vec { return l.B } - perpendicularM := -1 / m - perpendicularB := v.Y - (perpendicularM * v.X) - fmt.Println(m, b, perpendicularM, perpendicularB) - - // Coordinates of intersect (of infinite lines) - x := (perpendicularB - b) / (m - perpendicularM) - y := m*x + b - fmt.Println(x, y) - return V(x, y) } @@ -273,11 +273,9 @@ func (l Line) Formula() (m, b float64) { // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, // this function will return the zero-vector and `false`. func (l Line) Intersect(k Line) (Vec, bool) { - fmt.Println(l, k) // Check if the lines are parallel lDir := l.A.To(l.B) kDir := k.A.To(k.B) - fmt.Println(lDir, kDir) if lDir.X == kDir.X && lDir.Y == kDir.Y { return ZV, false } @@ -287,7 +285,6 @@ func (l Line) Intersect(k Line) (Vec, bool) { // segments lm, lb := l.Formula() km, kb := k.Formula() - fmt.Println(lm, lb, km, kb) // Account for vertical lines if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) { @@ -326,8 +323,6 @@ func (l Line) Intersect(k Line) (Vec, bool) { // Coordinates of intersect x := (kb - lb) / (lm - km) y := lm*x + lb - fmt.Println(x, y) - fmt.Println(l.Contains(V(x, y)), k.Contains(V(x, y))) if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. @@ -356,22 +351,18 @@ func (l Line) IntersectCircle(c Circle) Vec { func (l Line) IntersectRect(r Rect) Vec { // Check if either end of the line segment are within the rectangle if r.Contains(l.A) || r.Contains(l.B) { - fmt.Println("yes1") // Use the `Rect.Intersect` to get minimal return value rIntersect := l.Bounds().Intersect(r) if rIntersect.H() > rIntersect.W() { - fmt.Println("yes2") // Go vertical return V(0, rIntersect.H()) } return V(rIntersect.W(), 0) } - fmt.Println("No") // Check if any of the rectangles' edges intersect with this line. for _, edge := range r.Edges() { if _, ok := l.Intersect(edge); ok { - fmt.Println(edge) // Get the closest points on the line to each corner, where: // - the point is contained by the rectangle // - the point is not the corner itself @@ -380,15 +371,12 @@ func (l Line) IntersectRect(r Rect) Vec { closestCorner := corners[0] for _, c := range corners { cc := l.Closest(c) - if closest != ZV || (closest.Len() > cc.Len() && r.Contains(cc)) { + if closest == ZV || (closest.Len() > cc.Len() && r.Contains(cc)) { closest = cc closestCorner = c } } - fmt.Println(closest) - fmt.Println(closestCorner) - return closest.To(closestCorner) } } diff --git a/geometry_test.go b/geometry_test.go index dfcbf32..86f3a45 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -865,6 +865,12 @@ func TestLine_Closest(t *testing.T) { args: args{v: pixel.V(5, 6)}, want: pixel.V(5, 5), }, + { + name: "Point far from line", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{v: pixel.V(80, -70)}, + want: pixel.V(5, 5), + }, { name: "Point on inline with line", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, @@ -1108,30 +1114,30 @@ func TestLine_IntersectRect(t *testing.T) { args: args{r: pixel.R(-1, 1, 5, 5)}, want: pixel.V(-1, 0), }, - // { - // name: "Line through rect horizontally", - // fields: fields{A: pixel.V(-5, 0), B: pixel.V(5, 0)}, - // args: args{r: pixel.R(-2, -5, 2, 1)}, - // want: pixel.V(0, 1), - // }, - // { - // name: "Line through rect diagonally bottom and left edges", - // fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, - // args: args{r: pixel.R(0, 2, 3, 3)}, - // want: pixel.V(1, -1), - // }, - // { - // name: "Line through rect diagonally top and right edges", - // fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)}, - // args: args{r: pixel.R(5, 0, 8, 3)}, - // want: pixel.V(-1, -1), - // }, - // { - // name: "Line with not rect intersect", - // fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, - // args: args{r: pixel.R(20, 20, 21, 21)}, - // want: pixel.ZV, - // }, + { + name: "Line through rect horizontally", + fields: fields{A: pixel.V(0, 1), B: pixel.V(10, 1)}, + args: args{r: pixel.R(1, 0, 5, 5)}, + want: pixel.V(0, -1), + }, + { + name: "Line through rect diagonally bottom and left edges", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{r: pixel.R(0, 2, 3, 3)}, + want: pixel.V(-1, 1), + }, + { + name: "Line through rect diagonally top and right edges", + fields: fields{A: pixel.V(10, 0), B: pixel.V(0, 10)}, + args: args{r: pixel.R(5, 0, 8, 3)}, + want: pixel.V(-2.5, -2.5), + }, + { + name: "Line with not rect intersect", + fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, + args: args{r: pixel.R(20, 20, 21, 21)}, + want: pixel.ZV, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From e99ac56daa9ecf5446670c8ae31198268d6cfbef Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 11:52:42 +0100 Subject: [PATCH 18/36] wip line tests --- geometry.go | 6 ++++++ geometry_test.go | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/geometry.go b/geometry.go index 9872b74..41d2b4c 100644 --- a/geometry.go +++ b/geometry.go @@ -402,6 +402,12 @@ func (l Line) Moved(delta Vec) Line { func (l Line) Rotated(around Vec, angle float64) Line { // Move the line so we can use `Vec.Rotated` lineShifted := l.Moved(around.Scaled(-1)) + + fmt.Println(around.Scaled(-1)) + fmt.Println(lineShifted) + fmt.Println(lineShifted.A.Rotated(angle)) + fmt.Println(lineShifted.B.Rotated(angle)) + lineRotated := Line{ A: lineShifted.A.Rotated(angle), B: lineShifted.B.Rotated(angle), diff --git a/geometry_test.go b/geometry_test.go index 86f3a45..2a08e5c 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1216,24 +1216,24 @@ func TestLine_Rotated(t *testing.T) { args args want pixel.Line }{ - { - name: "Rotating around line center", - fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, - args: args{around: pixel.V(2, 2), angle: 2 * math.Pi}, - want: pixel.L(pixel.V(1, 1), pixel.V(3, 3)), - }, + // { + // name: "Rotating around line center", + // fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + // args: args{around: pixel.V(2, 2), angle: 2 * math.Pi}, + // want: pixel.L(pixel.V(1, 1), pixel.V(3, 3)), + // }, { name: "Rotating around x-y origin", fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, args: args{around: pixel.V(0, 0), angle: 2 * math.Pi}, want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)), }, - { - name: "Rotating around line end", - fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, - args: args{around: pixel.V(1, 1), angle: 2 * math.Pi}, - want: pixel.L(pixel.V(1, 1), pixel.V(-2, -2)), - }, + // { + // name: "Rotating around line end", + // fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + // args: args{around: pixel.V(1, 1), angle: 2 * math.Pi}, + // want: pixel.L(pixel.V(1, 1), pixel.V(-2, -2)), + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From faf6558294545215844fadaa3b4b68044513e904 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:03:07 +0100 Subject: [PATCH 19/36] wip line tests --- geometry_test.go | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 2a08e5c..ef00f33 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1216,24 +1216,24 @@ func TestLine_Rotated(t *testing.T) { args args want pixel.Line }{ - // { - // name: "Rotating around line center", - // fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, - // args: args{around: pixel.V(2, 2), angle: 2 * math.Pi}, - // want: pixel.L(pixel.V(1, 1), pixel.V(3, 3)), - // }, + { + name: "Rotating around line center", + fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + args: args{around: pixel.V(2, 2), angle: math.Pi}, + want: pixel.L(pixel.V(3, 3), pixel.V(1, 1)), + }, { name: "Rotating around x-y origin", fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, - args: args{around: pixel.V(0, 0), angle: 2 * math.Pi}, + args: args{around: pixel.V(0, 0), angle: math.Pi}, want: pixel.L(pixel.V(-1, -1), pixel.V(-3, -3)), }, - // { - // name: "Rotating around line end", - // fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, - // args: args{around: pixel.V(1, 1), angle: 2 * math.Pi}, - // want: pixel.L(pixel.V(1, 1), pixel.V(-2, -2)), - // }, + { + name: "Rotating around line end", + fields: fields{A: pixel.V(1, 1), B: pixel.V(3, 3)}, + args: args{around: pixel.V(1, 1), angle: math.Pi}, + want: pixel.L(pixel.V(1, 1), pixel.V(-1, -1)), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1241,7 +1241,13 @@ func TestLine_Rotated(t *testing.T) { A: tt.fields.A, B: tt.fields.B, } - if got := l.Rotated(tt.args.around, tt.args.angle); !reflect.DeepEqual(got, tt.want) { + // Have to round the results, due to floating-point in accuracies. Results are correct to approximately + // 10 decimal places. + got := l.Rotated(tt.args.around, tt.args.angle) + if math.Round(got.A.X) != tt.want.A.X || + math.Round(got.B.X) != tt.want.B.X || + math.Round(got.A.Y) != tt.want.A.Y || + math.Round(got.B.Y) != tt.want.B.Y { t.Errorf("Line.Rotated() = %v, want %v", got, tt.want) } }) From e5ff236d71ae06b01465645ee20d0420f927b124 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:03:58 +0100 Subject: [PATCH 20/36] Removed debug lines --- geometry.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/geometry.go b/geometry.go index 41d2b4c..4f4c1e4 100644 --- a/geometry.go +++ b/geometry.go @@ -403,11 +403,6 @@ func (l Line) Rotated(around Vec, angle float64) Line { // Move the line so we can use `Vec.Rotated` lineShifted := l.Moved(around.Scaled(-1)) - fmt.Println(around.Scaled(-1)) - fmt.Println(lineShifted) - fmt.Println(lineShifted.A.Rotated(angle)) - fmt.Println(lineShifted.B.Rotated(angle)) - lineRotated := Line{ A: lineShifted.A.Rotated(angle), B: lineShifted.B.Rotated(angle), From 98d5b9b417d585a2d0baf4a28c7b2dd3fe7b4b35 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:12:27 +0100 Subject: [PATCH 21/36] Adding vertical/horizontal edge cases --- geometry.go | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/geometry.go b/geometry.go index 4f4c1e4..73f4854 100644 --- a/geometry.go +++ b/geometry.go @@ -206,20 +206,48 @@ func (l Line) Center() Vec { // Closest will return the point on the line which is closest to the `Vec` provided. func (l Line) Closest(v Vec) Vec { - // Closest point will be on a line, perpendicular to this line + // between is a helper function which determines whether 'x' is greater than min(a, b) and less than max(a, b) + between := func(a, b, x float64) bool { + min := math.Min(a, b) + max := math.Max(a, b) + return min < x && x < max + } + + // Closest point will be on a line which perpendicular to this line. + // If and only if the infinite perpendicular line intersects the segment. m, b := l.Formula() // Account for horizontal lines if m == 0 { x := v.X y := l.A.Y - return V(x, y) + + // check if the X coordinate of v is on the line + if between(l.A.X, l.B.X, v.X) { + return V(x, y) + } + + // Otherwise get the closest endpoint + if l.A.To(v).Len() < l.B.To(v).Len() { + return l.A + } + return l.B } // Account for vertical lines if math.IsInf(math.Abs(m), 1) { x := l.A.X y := v.Y + + // check if the Y coordinate of v is on the line + if between(l.A.Y, l.B.Y, v.Y) { + return V(x, y) + } + + // Otherwise get the closest endpoint + if l.A.To(v).Len() < l.B.To(v).Len() { + return l.A + } return V(x, y) } @@ -230,13 +258,6 @@ func (l Line) Closest(v Vec) Vec { x := (perpendicularB - b) / (m - perpendicularM) y := m*x + b - // between is a helper function which determines whether 'x' is greater than min(a, b) and less than max(a, b) - between := func(a, b, x float64) bool { - min := math.Min(a, b) - max := math.Max(a, b) - return min < x && x < max - } - // Check if the point lies between the x and y bounds of the segment if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) { // Not within bounding box From a1d36f8c7e7635a9f6bbbd8bf224a9c9d17ebc2e Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:16:27 +0100 Subject: [PATCH 22/36] Clarified comment --- geometry.go | 1 + 1 file changed, 1 insertion(+) diff --git a/geometry.go b/geometry.go index 73f4854..da8fb7e 100644 --- a/geometry.go +++ b/geometry.go @@ -279,6 +279,7 @@ func (l Line) Contains(v Vec) bool { } // Formula will return the values that represent the line in the formula: y = mx + b +// This function will return `math.Inf+, math.Inf-` for a vertical line. func (l Line) Formula() (m, b float64) { // Account for horizontal lines if l.B.Y == l.A.Y { From 3592de858cea46b5c0cf234aeeed7a397556071a Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:19:30 +0100 Subject: [PATCH 23/36] Clarified comment --- geometry.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/geometry.go b/geometry.go index da8fb7e..e28cf2c 100644 --- a/geometry.go +++ b/geometry.go @@ -354,8 +354,8 @@ func (l Line) Intersect(k Line) (Vec, bool) { return ZV, false } -// IntersectCircle will return the shortest `Vec` such that the Line and Circle no longer intesect. If they do not -// intersect at all, this function will return a zero-vector. +// IntersectCircle will return the shortest `Vec` such that moving the Line by that Vec will cause the Line and Circle +// to no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectCircle(c Circle) Vec { // Get the point on the line closest to the center of the circle. closest := l.Closest(c.Center) @@ -368,8 +368,8 @@ func (l Line) IntersectCircle(c Circle) Vec { return cirToClosest.Scaled(cirToClosest.Len() - c.Radius) } -// IntersectRect will return the shortest `Vec` such that the Line and Rect no longer intesect. If they do not -// intersect at all, this function will return a zero-vector. +// IntersectRect will return the shortest `Vec` such that moving the Line by that Vec will cause the Line and Rect to +// no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectRect(r Rect) Vec { // Check if either end of the line segment are within the rectangle if r.Contains(l.A) || r.Contains(l.B) { From e6392a228dedb4cef08d0549cad11e37e9f637f1 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:22:21 +0100 Subject: [PATCH 24/36] Making len more sensible --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index e28cf2c..486eecd 100644 --- a/geometry.go +++ b/geometry.go @@ -409,7 +409,7 @@ func (l Line) IntersectRect(r Rect) Vec { // Len returns the length of the line segment. func (l Line) Len() float64 { - return l.A.Sub(l.B).Len() + return l.A.To(l.B).Len() } // Moved will return a line moved by the delta `Vec` provided. From 352785e1b8558579028e4728ad05af54f5690389 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 12:37:33 +0100 Subject: [PATCH 25/36] Supporting pre go1.10 --- geometry_test.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index ef00f33..488a5a3 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -1202,6 +1202,15 @@ func TestLine_Len(t *testing.T) { } func TestLine_Rotated(t *testing.T) { + // round returns the nearest integer, rounding ties away from zero. + // This is required because `math.Round` wasn't introduced until Go1.10 + round := func(x float64) float64 { + t := math.Trunc(x) + if math.Abs(x-t) >= 0.5 { + return t + math.Copysign(1, x) + } + return t + } type fields struct { A pixel.Vec B pixel.Vec @@ -1244,10 +1253,10 @@ func TestLine_Rotated(t *testing.T) { // Have to round the results, due to floating-point in accuracies. Results are correct to approximately // 10 decimal places. got := l.Rotated(tt.args.around, tt.args.angle) - if math.Round(got.A.X) != tt.want.A.X || - math.Round(got.B.X) != tt.want.B.X || - math.Round(got.A.Y) != tt.want.A.Y || - math.Round(got.B.Y) != tt.want.B.Y { + if round(got.A.X) != tt.want.A.X || + round(got.B.X) != tt.want.B.X || + round(got.A.Y) != tt.want.A.Y || + round(got.B.Y) != tt.want.B.Y { t.Errorf("Line.Rotated() = %v, want %v", got, tt.want) } }) From c3e69c4f3570570687e431d7547eefe8bb878d2d Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 15:26:38 +0100 Subject: [PATCH 26/36] Tidying up function comments --- geometry.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/geometry.go b/geometry.go index 486eecd..4b45afb 100644 --- a/geometry.go +++ b/geometry.go @@ -186,7 +186,7 @@ type Line struct { A, B Vec } -// L creates and returns a new Line object. +// L creates and returns a new Line. func L(from, to Vec) Line { return Line{ A: from, @@ -194,7 +194,7 @@ func L(from, to Vec) Line { } } -// Bounds returns the lines bounding box. This is in the form of a normalized `Rect`. +// Bounds returns the lines bounding box. This is in the form of a normalized Rect. func (l Line) Bounds() Rect { return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm() } @@ -204,9 +204,9 @@ func (l Line) Center() Vec { return l.A.Add(l.A.To(l.B).Scaled(0.5)) } -// Closest will return the point on the line which is closest to the `Vec` provided. +// Closest will return the point on the line which is closest to the Vec provided. func (l Line) Closest(v Vec) Vec { - // between is a helper function which determines whether 'x' is greater than min(a, b) and less than max(a, b) + // between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b) between := func(a, b, x float64) bool { min := math.Min(a, b) max := math.Max(a, b) @@ -273,13 +273,13 @@ func (l Line) Closest(v Vec) Vec { return V(x, y) } -// Contains returns whether the provided `Vec` lies on the line +// Contains returns whether the provided Vec lies on the line func (l Line) Contains(v Vec) bool { return l.Closest(v) == v } // Formula will return the values that represent the line in the formula: y = mx + b -// This function will return `math.Inf+, math.Inf-` for a vertical line. +// This function will return math.Inf+, math.Inf- for a vertical line. func (l Line) Formula() (m, b float64) { // Account for horizontal lines if l.B.Y == l.A.Y { @@ -293,7 +293,7 @@ func (l Line) Formula() (m, b float64) { } // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, -// this function will return the zero-vector and `false`. +// this function will return the zero-vector and false. func (l Line) Intersect(k Line) (Vec, bool) { // Check if the lines are parallel lDir := l.A.To(l.B) @@ -354,7 +354,7 @@ func (l Line) Intersect(k Line) (Vec, bool) { return ZV, false } -// IntersectCircle will return the shortest `Vec` such that moving the Line by that Vec will cause the Line and Circle +// IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle // to no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectCircle(c Circle) Vec { // Get the point on the line closest to the center of the circle. @@ -368,12 +368,12 @@ func (l Line) IntersectCircle(c Circle) Vec { return cirToClosest.Scaled(cirToClosest.Len() - c.Radius) } -// IntersectRect will return the shortest `Vec` such that moving the Line by that Vec will cause the Line and Rect to +// IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to // no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectRect(r Rect) Vec { // Check if either end of the line segment are within the rectangle if r.Contains(l.A) || r.Contains(l.B) { - // Use the `Rect.Intersect` to get minimal return value + // Use the Rect.Intersect to get minimal return value rIntersect := l.Bounds().Intersect(r) if rIntersect.H() > rIntersect.W() { // Go vertical @@ -412,7 +412,7 @@ func (l Line) Len() float64 { return l.A.To(l.B).Len() } -// Moved will return a line moved by the delta `Vec` provided. +// Moved will return a line moved by the delta Vec provided. func (l Line) Moved(delta Vec) Line { return Line{ A: l.A.Add(delta), @@ -420,7 +420,7 @@ func (l Line) Moved(delta Vec) Line { } } -// Rotated will rotate the line around the provided `Vec`. +// Rotated will rotate the line around the provided Vec. func (l Line) Rotated(around Vec, angle float64) Line { // Move the line so we can use `Vec.Rotated` lineShifted := l.Moved(around.Scaled(-1)) @@ -438,7 +438,7 @@ func (l Line) Scaled(scale float64) Line { return l.ScaledXY(l.Center(), scale) } -// ScaledXY will return the line scaled around the `Vec` provided. +// ScaledXY will return the line scaled around the Vec provided. func (l Line) ScaledXY(around Vec, scale float64) Line { toA := around.To(l.A).Scaled(scale) toB := around.To(l.B).Scaled(scale) @@ -613,7 +613,7 @@ func (r Rect) IntersectCircle(c Circle) Vec { return c.IntersectRect(r).Scaled(-1) } -// IntersectLine will return the shortest `Vec` such that if the Rect is moved by the Vec returned, the Line and Rect no +// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no // longer intersect. func (r Rect) IntersectLine(l Line) Vec { return l.IntersectRect(r).Scaled(-1) @@ -776,7 +776,7 @@ func (c Circle) Intersect(d Circle) Circle { } } -// IntersectLine will return the shortest `Vec` such that if the Rect is moved by the Vec returned, the Line and Rect no +// IntersectLine will return the shortest Vec such that if the Rect is moved by the Vec returned, the Line and Rect no // longer intersect. func (c Circle) IntersectLine(l Line) Vec { return l.IntersectCircle(c).Scaled(-1) From 83c62a031375c1597c4341884fcc674a011f7dab Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 16:21:57 +0100 Subject: [PATCH 27/36] fixing line intersect function --- geometry.go | 56 +++++++++++++++++++++------------ geometry_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 20 deletions(-) diff --git a/geometry.go b/geometry.go index 4b45afb..f2c0017 100644 --- a/geometry.go +++ b/geometry.go @@ -314,38 +314,26 @@ func (l Line) Intersect(k Line) (Vec, bool) { return ZV, false } + var x, y float64 + if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) { // One line is vertical intersectM := lm intersectB := lb - verticalLine := k if math.IsInf(math.Abs(lm), 1) { intersectM = km intersectB = kb - verticalLine = l } - maxVerticalY := verticalLine.A.Y - minVerticalY := verticalLine.B.Y - if verticalLine.B.Y > maxVerticalY { - maxVerticalY = verticalLine.B.Y - minVerticalY = verticalLine.A.Y - } - - y := intersectM*l.A.X + intersectB - if y > maxVerticalY || y < minVerticalY { - // Point is not on the horizontal line - return ZV, false - } - - return V(l.A.X, y), true + y = intersectM*l.A.X + intersectB + x = l.A.X + } else { + // Coordinates of intersect + x = (kb - lb) / (lm - km) + y = lm*x + lb } - // Coordinates of intersect - x := (kb - lb) / (lm - km) - y := lm*x + lb - if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. return V(x, y), true @@ -619,6 +607,28 @@ func (r Rect) IntersectLine(l Line) Vec { return l.IntersectRect(r).Scaled(-1) } +// IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or +// two points, depending on the location of the shapes. +func (r Rect) IntersectionPoints(l Line) []Vec { + // Use map keys to ensure unique points + pointMap := make(map[Vec]struct{}) + + for _, edge := range r.Edges() { + if intersect, ok := edge.Intersect(l); ok { + fmt.Println(edge) + fmt.Println(l) + fmt.Println(intersect) + pointMap[intersect] = struct{}{} + } + } + + points := make([]Vec, 0, len(pointMap)) + for point := range pointMap { + points = append(points, point) + } + return points +} + // Vertices returns a slice of the four corners which make up the rectangle. func (r Rect) Vertices() [4]Vec { return [4]Vec{ @@ -858,6 +868,12 @@ func (c Circle) IntersectRect(r Rect) Vec { } } +// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or +// two points, depending on the location of the shapes. +func (c Circle) IntersectionPoints(l Line) []Vec { + return []Vec{} +} + // 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 488a5a3..4f8f8df 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -609,6 +609,35 @@ func TestCircle_Intersect(t *testing.T) { } } +func TestCircle_IntersectPoints(t *testing.T) { + type fields struct { + Center pixel.Vec + Radius float64 + } + type args struct { + l pixel.Line + } + tests := []struct { + name string + fields fields + args args + want []pixel.Vec + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := pixel.Circle{ + Center: tt.fields.Center, + Radius: tt.fields.Radius, + } + if got := c.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Circle.IntersectPoints() = %v, want %v", got, tt.want) + } + }) + } +} + func TestRect_IntersectCircle(t *testing.T) { // closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them. // Effectively this compares two floats to a given decimal point. @@ -759,6 +788,52 @@ func TestRect_IntersectCircle(t *testing.T) { } } +func TestRect_IntersectionPoints(t *testing.T) { + type fields struct { + Min pixel.Vec + Max pixel.Vec + } + type args struct { + l pixel.Line + } + tests := []struct { + name string + fields fields + args args + want []pixel.Vec + }{ + { + name: "No intersection points", + fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, + args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, + want: []pixel.Vec{}, + }, + // { + // name: "One intersection point", + // fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, + // args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 2))}, + // want: []pixel.Vec{pixel.V(2, 1)}, + // }, + // { + // name: "Two intersection points", + // fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, + // args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))}, + // want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)}, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := pixel.Rect{ + Min: tt.fields.Min, + Max: tt.fields.Max, + } + if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want) + } + }) + } +} + func TestLine_Bounds(t *testing.T) { type fields struct { A pixel.Vec @@ -1008,6 +1083,13 @@ func TestLine_Intersect(t *testing.T) { want: pixel.V(5, 5), want1: true, }, + { + name: "Lines intersect 2", + fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)}, + args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, + want: pixel.ZV, + want1: false, + }, { name: "Line intersect with vertical", fields: fields{A: pixel.V(5, 0), B: pixel.V(5, 10)}, From 4795a92b41da2c1be64300226ac43228ca7d43ce Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Wed, 3 Apr 2019 16:58:30 +0100 Subject: [PATCH 28/36] fixed line intersect and added rect intersection points --- geometry.go | 13 ++++++------- geometry_test.go | 46 ++++++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/geometry.go b/geometry.go index f2c0017..4f1ae02 100644 --- a/geometry.go +++ b/geometry.go @@ -248,7 +248,7 @@ func (l Line) Closest(v Vec) Vec { if l.A.To(v).Len() < l.B.To(v).Len() { return l.A } - return V(x, y) + return l.B } perpendicularM := -1 / m @@ -320,14 +320,16 @@ func (l Line) Intersect(k Line) (Vec, bool) { // One line is vertical intersectM := lm intersectB := lb + verticalLine := k if math.IsInf(math.Abs(lm), 1) { intersectM = km intersectB = kb + verticalLine = l } - y = intersectM*l.A.X + intersectB - x = l.A.X + y = intersectM*verticalLine.A.X + intersectB + x = verticalLine.A.X } else { // Coordinates of intersect x = (kb - lb) / (lm - km) @@ -614,10 +616,7 @@ func (r Rect) IntersectionPoints(l Line) []Vec { pointMap := make(map[Vec]struct{}) for _, edge := range r.Edges() { - if intersect, ok := edge.Intersect(l); ok { - fmt.Println(edge) - fmt.Println(l) - fmt.Println(intersect) + if intersect, ok := l.Intersect(edge); ok { pointMap[intersect] = struct{}{} } } diff --git a/geometry_test.go b/geometry_test.go index 4f8f8df..a311159 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -808,18 +808,18 @@ func TestRect_IntersectionPoints(t *testing.T) { args: args{l: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, want: []pixel.Vec{}, }, - // { - // name: "One intersection point", - // fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, - // args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 2))}, - // want: []pixel.Vec{pixel.V(2, 1)}, - // }, - // { - // name: "Two intersection points", - // fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, - // args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))}, - // want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)}, - // }, + { + name: "One intersection point", + fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, + args: args{l: pixel.L(pixel.V(2, 0), pixel.V(2, 3))}, + want: []pixel.Vec{pixel.V(2, 1)}, + }, + { + name: "Two intersection points", + fields: fields{Min: pixel.V(1, 1), Max: pixel.V(5, 5)}, + args: args{l: pixel.L(pixel.V(0, 2), pixel.V(6, 2))}, + want: []pixel.Vec{pixel.V(1, 2), pixel.V(5, 2)}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1085,10 +1085,10 @@ func TestLine_Intersect(t *testing.T) { }, { name: "Lines intersect 2", - fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)}, - args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, - want: pixel.ZV, - want1: false, + fields: fields{A: pixel.V(5, 1), B: pixel.V(1, 1)}, + args: args{k: pixel.L(pixel.V(2, 0), pixel.V(2, 3))}, + want: pixel.V(2, 1), + want1: true, }, { name: "Line intersect with vertical", @@ -1111,6 +1111,20 @@ func TestLine_Intersect(t *testing.T) { want: pixel.ZV, want1: false, }, + { + name: "Lines don't intersect 2", + fields: fields{A: pixel.V(1, 1), B: pixel.V(1, 5)}, + args: args{k: pixel.L(pixel.V(-5, 0), pixel.V(-2, 2))}, + want: pixel.ZV, + want1: false, + }, + { + name: "Lines don't intersect 3", + fields: fields{A: pixel.V(2, 0), B: pixel.V(2, 3)}, + args: args{k: pixel.L(pixel.V(1, 5), pixel.V(5, 5))}, + want: pixel.ZV, + want1: false, + }, { name: "Lines parallel", fields: fields{A: pixel.V(0, 0), B: pixel.V(10, 10)}, From f3377bb16fc5d0be5429aa8f14e62ae0fb4e630d Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 11:53:48 +0100 Subject: [PATCH 29/36] Added circle intersection points --- geometry.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++- geometry_test.go | 82 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/geometry.go b/geometry.go index 4f1ae02..32b2467 100644 --- a/geometry.go +++ b/geometry.go @@ -707,6 +707,12 @@ func (c Circle) Contains(u Vec) bool { return c.Radius >= toCenter.Len() } +// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2 +// where r is the radius of the circle. +func (c Circle) Formula() (h, k float64) { + return c.Center.X, c.Center.Y +} + // maxCircle will return the larger circle based on the radius. func maxCircle(c, d Circle) Circle { if c.Radius < d.Radius { @@ -870,7 +876,90 @@ func (c Circle) IntersectRect(r Rect) Vec { // IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or // two points, depending on the location of the shapes. func (c Circle) IntersectionPoints(l Line) []Vec { - return []Vec{} + cContainsA := c.Contains(l.A) + cContainsB := c.Contains(l.B) + + // Special case for both endpoint being contained within the circle + if cContainsA && cContainsB { + return []Vec{} + } + + // Get closest point on the line to this circles' center + closestToCenter := l.Closest(c.Center) + + // If the distance to the closest point is greater than the radius, there are no points of intersection + if closestToCenter.To(c.Center).Len() > c.Radius { + return []Vec{} + } + + // If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the + // point at which it touches the circle. + if closestToCenter.To(c.Center).Len() == c.Radius { + return []Vec{closestToCenter} + } + + // Special case for endpoint being on the circles' center + if c.Center == l.A || c.Center == l.B { + otherEnd := l.B + if c.Center == l.B { + otherEnd = l.A + } + intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius)) + return []Vec{intersect} + } + + // This means the distance to the closest point is less than the radius, so there is at least one intersection, + // possibly two. + + // If one of the end points exists within the circle, there is only one intersection + if cContainsA || cContainsB { + containedPoint := l.A + otherEnd := l.B + if cContainsB { + containedPoint = l.B + otherEnd = l.A + } + + // Use trigonometry to get the length of the line between the contained point and the intersection point. + // The following is used to describe the triangle formed: + // - a is the side between contained point and circle center + // - b is the side between the center and the intersection point (radius) + // - c is the side between the contained point and the intersection point + // The captials of these letters are used as the angles opposite the respective sides. + // a and b are known + a := containedPoint.To(c.Center).Len() + b := c.Radius + // B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis) + B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle() + // Using the Sin rule we can get A + A := math.Asin((a * math.Sin(B)) / b) + // Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C + C := math.Pi - A + B + // If C is zero, the line segment is in-line with the center-intersect line. + var c float64 + if C == 0 { + c = b - a + } else { + // Using the Sine rule again, we can now get c + c = (a * math.Sin(C)) / math.Sin(A) + } + // Travelling from the contained point to the other end by length of a will provide the intersection point. + return []Vec{ + containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)), + } + } + + // Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations. + // The vector formed by going from the closest point to the center of the circle will be perpendicular to the line; + // this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse. + // Calculate the other triangles' sides' length. + a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2)) + + // Travelling in both directions from the closest point by length of a will provide the two intersection points. + first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a)) + second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a)) + + return []Vec{first, second} } // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such diff --git a/geometry_test.go b/geometry_test.go index a311159..2f214a7 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -10,6 +10,19 @@ import ( "github.com/stretchr/testify/assert" ) +// closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them. +// Effectively this compares two floats to a given decimal point. +// Example: +// closeEnough(100.125342432, 100.125, 2) == true +// closeEnough(math.Pi, 3.14, 2) == true +// closeEnough(0.1234, 0.1245, 3) == false +func closeEnough(got, expected float64, decimalAccuracy int) bool { + gotShifted := got * math.Pow10(decimalAccuracy) + expectedShifted := expected * math.Pow10(decimalAccuracy) + + return math.Trunc(gotShifted) == math.Trunc(expectedShifted) +} + func TestRect_Edges(t *testing.T) { type fields struct { Min pixel.Vec @@ -623,7 +636,54 @@ func TestCircle_IntersectPoints(t *testing.T) { args args want []pixel.Vec }{ - // TODO: Add test cases. + { + name: "Line intersects circle at two points", + fields: fields{Center: pixel.V(2, 2), Radius: 1}, + args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: []pixel.Vec{pixel.V(1.292, 1.292), pixel.V(2.707, 2.707)}, + }, + { + name: "Line intersects circle at one point", + fields: fields{Center: pixel.V(-0.5, -0.5), Radius: 1}, + args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: []pixel.Vec{pixel.V(0.207, 0.207)}, + }, + { + name: "Line endpoint is circle center", + fields: fields{Center: pixel.V(0, 0), Radius: 1}, + args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: []pixel.Vec{pixel.V(0.707, 0.707)}, + }, + { + name: "Both line endpoints within circle", + fields: fields{Center: pixel.V(0, 0), Radius: 1}, + args: args{pixel.L(pixel.V(0.2, 0.2), pixel.V(0.5, 0.5))}, + want: []pixel.Vec{}, + }, + { + name: "Line does not intersect circle", + fields: fields{Center: pixel.V(10, 0), Radius: 1}, + args: args{pixel.L(pixel.V(0, 0), pixel.V(10, 10))}, + want: []pixel.Vec{}, + }, + { + name: "Horizontal line intersects circle at two points", + fields: fields{Center: pixel.V(5, 5), Radius: 1}, + args: args{pixel.L(pixel.V(0, 5), pixel.V(10, 5))}, + want: []pixel.Vec{pixel.V(4, 5), pixel.V(6, 5)}, + }, + { + name: "Vertical line intersects circle at two points", + fields: fields{Center: pixel.V(5, 5), Radius: 1}, + args: args{pixel.L(pixel.V(5, 0), pixel.V(5, 10))}, + want: []pixel.Vec{pixel.V(5, 4), pixel.V(5, 6)}, + }, + { + name: "Left and down line intersects circle at two points", + fields: fields{Center: pixel.V(5, 5), Radius: 1}, + args: args{pixel.L(pixel.V(10, 10), pixel.V(0, 0))}, + want: []pixel.Vec{pixel.V(5.707, 5.707), pixel.V(4.292, 4.292)}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -631,27 +691,17 @@ func TestCircle_IntersectPoints(t *testing.T) { Center: tt.fields.Center, Radius: tt.fields.Radius, } - if got := c.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Circle.IntersectPoints() = %v, want %v", got, tt.want) + got := c.IntersectionPoints(tt.args.l) + for i, v := range got { + if !closeEnough(v.X, tt.want[i].X, 2) || !closeEnough(v.Y, tt.want[i].Y, 2) { + t.Errorf("Circle.IntersectPoints() = %v, want %v", v, tt.want[i]) + } } }) } } func TestRect_IntersectCircle(t *testing.T) { - // closeEnough will shift the decimal point by the accuracy required, truncates the results and compares them. - // Effectively this compares two floats to a given decimal point. - // Example: - // closeEnough(100.125342432, 100.125, 2) == true - // closeEnough(math.Pi, 3.14, 2) == true - // closeEnough(0.1234, 0.1245, 3) == false - closeEnough := func(got, expected float64, decimalAccuracy int) bool { - gotShifted := got * math.Pow10(decimalAccuracy) - expectedShifted := expected * math.Pow10(decimalAccuracy) - - return math.Trunc(gotShifted) == math.Trunc(expectedShifted) - } - type fields struct { Min pixel.Vec Max pixel.Vec From 966150a8568f2028348a67e7bc3447d9e1f6e8a4 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 15:13:24 +0100 Subject: [PATCH 30/36] Removing backticks --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 32b2467..5a60a1b 100644 --- a/geometry.go +++ b/geometry.go @@ -181,7 +181,7 @@ func Lerp(a, b Vec, t float64) Vec { return a.Scaled(1 - t).Add(b.Scaled(t)) } -// Line is a 2D line segment, between points `A` and `B`. +// Line is a 2D line segment, between points A and B. type Line struct { A, B Vec } From bcda85acd2a06bb4a23f59ae00d32ed23f5ba3c2 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 15:13:47 +0100 Subject: [PATCH 31/36] Adding fullstop at end of func comment --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index 5a60a1b..b332f25 100644 --- a/geometry.go +++ b/geometry.go @@ -273,7 +273,7 @@ func (l Line) Closest(v Vec) Vec { return V(x, y) } -// Contains returns whether the provided Vec lies on the line +// Contains returns whether the provided Vec lies on the line. func (l Line) Contains(v Vec) bool { return l.Closest(v) == v } From 364a7a84ae0ebd83a14ed38839821836bcbaaf31 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 15:31:34 +0100 Subject: [PATCH 32/36] Prevented test results order mattering --- geometry_test.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/geometry_test.go b/geometry_test.go index 2f214a7..ebabfa0 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -839,6 +839,16 @@ func TestRect_IntersectCircle(t *testing.T) { } func TestRect_IntersectionPoints(t *testing.T) { + in := func(v pixel.Vec, vs []pixel.Vec) bool { + for _, vec := range vs { + if vec == v { + return true + } + } + + return false + } + type fields struct { Min pixel.Vec Max pixel.Vec @@ -877,8 +887,14 @@ func TestRect_IntersectionPoints(t *testing.T) { Min: tt.fields.Min, Max: tt.fields.Max, } - if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want) + got := r.IntersectionPoints(tt.args.l) + if len(got) != len(tt.want) { + t.Errorf("Rect.IntersectPoints() has incorrect length. Expected %d, got %d", len(tt.want), len(got)) + } + for _, v := range got { + if !in(v, tt.want) { + t.Errorf("Rect.IntersectPoints(): got unexpected result = %v", v) + } } }) } From 29b1220ec33da3573eb0dc6492cafc417b2ebf39 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 15:48:27 +0100 Subject: [PATCH 33/36] Setting order of returned points on Rect --- geometry.go | 8 +++++++- geometry_test.go | 20 ++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/geometry.go b/geometry.go index b332f25..9b232bf 100644 --- a/geometry.go +++ b/geometry.go @@ -3,6 +3,7 @@ package pixel import ( "fmt" "math" + "sort" ) // Clamp returns x clamped to the interval [min, max]. @@ -610,7 +611,8 @@ func (r Rect) IntersectLine(l Line) Vec { } // IntersectionPoints returns all the points where the Rect intersects with the line provided. This can be zero, one or -// two points, depending on the location of the shapes. +// two points, depending on the location of the shapes. The points of intersection will be returned in order of +// closest-to-l.A to closest-to-l.B. func (r Rect) IntersectionPoints(l Line) []Vec { // Use map keys to ensure unique points pointMap := make(map[Vec]struct{}) @@ -625,6 +627,10 @@ func (r Rect) IntersectionPoints(l Line) []Vec { for point := range pointMap { points = append(points, point) } + + // Order the points + sort.Slice(points, func(i, j int) bool { return points[i].To(l.A).Len() < points[j].To(l.A).Len() }) + return points } diff --git a/geometry_test.go b/geometry_test.go index ebabfa0..2f214a7 100644 --- a/geometry_test.go +++ b/geometry_test.go @@ -839,16 +839,6 @@ func TestRect_IntersectCircle(t *testing.T) { } func TestRect_IntersectionPoints(t *testing.T) { - in := func(v pixel.Vec, vs []pixel.Vec) bool { - for _, vec := range vs { - if vec == v { - return true - } - } - - return false - } - type fields struct { Min pixel.Vec Max pixel.Vec @@ -887,14 +877,8 @@ func TestRect_IntersectionPoints(t *testing.T) { Min: tt.fields.Min, Max: tt.fields.Max, } - got := r.IntersectionPoints(tt.args.l) - if len(got) != len(tt.want) { - t.Errorf("Rect.IntersectPoints() has incorrect length. Expected %d, got %d", len(tt.want), len(got)) - } - for _, v := range got { - if !in(v, tt.want) { - t.Errorf("Rect.IntersectPoints(): got unexpected result = %v", v) - } + if got := r.IntersectionPoints(tt.args.l); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Rect.IntersectPoints() = %v, want %v", got, tt.want) } }) } From b8bb00a1617e47b904589582f1d27aaeb2fae715 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 15:49:40 +0100 Subject: [PATCH 34/36] Setting order of returned points on Circle --- geometry.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/geometry.go b/geometry.go index 9b232bf..83c0638 100644 --- a/geometry.go +++ b/geometry.go @@ -880,7 +880,8 @@ func (c Circle) IntersectRect(r Rect) Vec { } // IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or -// two points, depending on the location of the shapes. +// two points, depending on the location of the shapes. The points of intersection will be returned in order of +// closest-to-l.A to closest-to-l.B. func (c Circle) IntersectionPoints(l Line) []Vec { cContainsA := c.Contains(l.A) cContainsB := c.Contains(l.B) @@ -965,7 +966,10 @@ func (c Circle) IntersectionPoints(l Line) []Vec { first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a)) second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a)) - return []Vec{first, second} + points := []Vec{first, second} + sort.Slice(points, func(i, j int) bool { return points[i].To(l.A).Len() < points[j].To(l.A).Len() }) + + return points } // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such From 0092d6a577c62a196bf82c2faaf14e0018ced58c Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 16:09:25 +0100 Subject: [PATCH 35/36] implementing pre 1.8 sortslice solution --- geometry.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/geometry.go b/geometry.go index 83c0638..e6652cd 100644 --- a/geometry.go +++ b/geometry.go @@ -3,7 +3,6 @@ package pixel import ( "fmt" "math" - "sort" ) // Clamp returns x clamped to the interval [min, max]. @@ -629,7 +628,11 @@ func (r Rect) IntersectionPoints(l Line) []Vec { } // Order the points - sort.Slice(points, func(i, j int) bool { return points[i].To(l.A).Len() < points[j].To(l.A).Len() }) + if len(points) == 2 { + if points[1].To(l.A).Len() < points[0].To(l.A).Len() { + return []Vec{points[1], points[2]} + } + } return points } @@ -966,10 +969,10 @@ func (c Circle) IntersectionPoints(l Line) []Vec { first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a)) second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a)) - points := []Vec{first, second} - sort.Slice(points, func(i, j int) bool { return points[i].To(l.A).Len() < points[j].To(l.A).Len() }) - - return points + if first.To(l.A).Len() < second.To(l.A).Len() { + return []Vec{first, second} + } + return []Vec{second, first} } // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such From 9eb2834a10eb6a0460c4ef5fe442317f8a7f6211 Mon Sep 17 00:00:00 2001 From: Ben Cragg Date: Thu, 4 Apr 2019 16:20:21 +0100 Subject: [PATCH 36/36] fixed index issue --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index e6652cd..8ccab8d 100644 --- a/geometry.go +++ b/geometry.go @@ -630,7 +630,7 @@ func (r Rect) IntersectionPoints(l Line) []Vec { // Order the points if len(points) == 2 { if points[1].To(l.A).Len() < points[0].To(l.A).Len() { - return []Vec{points[1], points[2]} + return []Vec{points[1], points[0]} } }