diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d32cf81..ca6fff3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,7 @@ name: main on: [ push ] jobs: - build: + tests: name: Tests runs-on: ubuntu-latest diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..3f356b0 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +comment: + layout: "condensed_header, condensed_files, condensed_footer" + hide_project_coverage: true diff --git a/geometry_line.go b/geometry_line.go index ae3d07e..0f17c61 100644 --- a/geometry_line.go +++ b/geometry_line.go @@ -20,13 +20,46 @@ func (ln Line) Length() float64 { return ln.From.Distance(ln.To) } +// Vector returns the vector of the Line. +// https://en.wikipedia.org/wiki/Euclidean_vector +func (ln Line) Vector() Point { + return ln.To.Sub(ln.From) +} + +// Slope returns the slope value of the Line. +// https://en.wikipedia.org/wiki/Slope +func (ln Line) Slope() float64 { + return (ln.To.Y - ln.From.Y) / (ln.To.X - ln.From.X) +} + +// CrossProduct returns the cross product of the Line. +// https://en.wikipedia.org/wiki/Cross_product +func (ln Line) CrossProduct() float64 { + return ln.From.X*ln.To.Y - ln.From.Y*ln.To.X +} + +// IsHorizontal tests if the Line is horizontal. +func (ln Line) IsHorizontal() bool { + return ln.From.Y == ln.To.Y +} + +// IsVertical tests if the Line is vertical. +func (ln Line) IsVertical() bool { + return ln.From.X == ln.To.X +} + +// IsMoving tests if the Line has magnitude. +func (ln Line) IsMoving() bool { + return !ln.From.IsSame(ln.To) +} + // Segment returns the Line segment with the given length. func (ln Line) Segment(length float64) Line { if length == 0 { return Line{ln.From, ln.From} } - v := ln.Direction() + v := ln.Vector() vl := ln.Length() ux := v.X / vl uy := v.Y / vl @@ -40,31 +73,6 @@ func (ln Line) Segment(length float64) Line { } } -// IsMoving tests if the Line represents a moving object. -func (ln Line) IsMoving() bool { - return !ln.From.IsSame(ln.To) -} - -// Direction returns the direction of the Line. -func (ln Line) Direction() Point { - return ln.To.Sub(ln.From) -} - -// IsHorizontal tests if the Line is horizontal. -func (ln Line) IsHorizontal() bool { - return ln.From.Y == ln.To.Y -} - -// IsVertical tests if the Line is vertical. -func (ln Line) IsVertical() bool { - return ln.From.X == ln.To.X -} - -// Slope returns the slope value of the Line. -func (ln Line) Slope() float64 { - return (ln.To.Y - ln.From.Y) / (ln.To.X - ln.From.X) -} - // IsPointOnLine tests if the Point is on the Line. func (ln Line) IsPointOnLine(p Point) bool { if ln.IsHorizontal() { @@ -92,75 +100,24 @@ func (ln Line) IsPointOnSegment(p Point) bool { // LinesIntersection returns the crossing Point of two Lines. func (ln Line) LinesIntersection(tl Line) (Point, bool) { - // parallel - if (ln.IsHorizontal() && tl.IsHorizontal()) || (ln.IsVertical() && tl.IsVertical()) { - return Point{}, false - } - - // vertical / horizontal - if ln.IsVertical() && tl.IsHorizontal() { - return tl.LinesIntersection(ln) - } - if ln.IsHorizontal() && tl.IsVertical() { - return NewPoint(tl.From.X, ln.From.Y), true - } - - // diagonal and vertical - if tl.IsVertical() { - return tl.LinesIntersection(ln) - } - if ln.IsVertical() { - x := ln.From.X - y := tl.Slope()*x + tl.From.Y - return NewPoint(x, y), true - } - - // diagonal - slope1 := ln.Slope() - slope2 := tl.Slope() - // parallel - if slope1 == slope2 { - return Point{}, false - } - - yIntercept1 := ln.From.Y - slope1*ln.From.X - yIntercept2 := tl.From.Y - slope2*tl.From.X - - x := (yIntercept2 - yIntercept1) / (slope1 - slope2) - y := slope1*x + yIntercept1 - - return NewPoint(x, y), true + return linesIntersection(ln, tl, false, false) } // SegmentsIntersection returns the crossing Point of two Line segments. func (ln Line) SegmentsIntersection(tl Line) (Point, bool) { - p, ok := ln.LinesIntersection(tl) - if !ok { - return Point{}, false - } - if !ln.Rect().IsContainsPoint(p) || !tl.Rect().IsContainsPoint(p) { - return Point{}, false - } - return p, true + return linesIntersection(ln, tl, true, true) } // LineSegmentIntersection returns the crossing Point of the Line and the Line segment. func (ln Line) LineSegmentIntersection(tl Line) (Point, bool) { - p, ok := ln.LinesIntersection(tl) - if !ok { - return Point{}, false - } - if !tl.Rect().IsContainsPoint(p) { - return Point{}, false - } - return p, true + return linesIntersection(ln, tl, false, true) } // Rotate returns the Line rotated by the given angle. func (ln Line) Rotate(angle float64) Line { radians := angle * math.Pi / 180 - v := ln.Direction() + v := ln.Vector() rx := v.X*math.Cos(radians) + v.Y*math.Sin(radians) ry := v.X*-math.Sin(radians) + v.Y*math.Cos(radians) @@ -169,18 +126,17 @@ func (ln Line) Rotate(angle float64) Line { // IsCollision tests whether a moving object collides with another moving object within a given radius. func (ln Line) IsCollision(tl Line, radius float64) bool { - tv := tl.Direction() - lv := ln.Direction() + tv := tl.Vector() + lv := ln.Vector() dx := tl.From.Sub(ln.From) - vx2 := tv.X - lv.X - vy2 := tv.Y - lv.Y + vx := tv.Sub(lv) - a := vx2*vx2 + vy2*vy2 + a := vx.X*vx.X + vx.Y*vx.Y if a <= 0 { return false } - b := 2 * (dx.X*vx2 + dx.Y*vy2) + b := 2 * (dx.X*vx.X + dx.Y*vx.Y) c := dx.X*dx.X + dx.Y*dx.Y - radius*radius d := b*b - 4*a*c if d < 0 { diff --git a/geometry_line_test.go b/geometry_line_test.go index 6e155cf..476a6bb 100644 --- a/geometry_line_test.go +++ b/geometry_line_test.go @@ -8,35 +8,101 @@ import ( ) func TestLine_IsSame(t *testing.T) { + var ln Line + ln = Line{Point{0, 0}, Point{300, 400}} + assert.True(t, ln.IsSame(Line{Point{0, 0}, Point{300, 400}})) + ln = Line{Point{0, 0}, Point{300, 400}} + assert.False(t, ln.IsSame(Line{Point{300, 400}, Point{0, 0}})) +} + +func TestLine_Length(t *testing.T) { + line := NewLine(Point{0, 0}, Point{300, 400}) + assert.EqualValues(t, 500, line.Length()) +} + +func TestLine_Vector(t *testing.T) { + var ln Line + ln = Line{Point{200, 250}, Point{300, 300}} + assert.Equal(t, Point{100, 50}, ln.Vector()) + ln = Line{Point{300, 300}, Point{200, 250}} + assert.Equal(t, Point{-100, -50}, ln.Vector()) +} + +func TestLine_Slope(t *testing.T) { tests := []struct { name string - a, b Line - want bool + line Line + want float64 }{ { - name: `true`, - a: Line{Point{0, 0}, Point{300, 400}}, - b: Line{Point{0, 0}, Point{300, 400}}, - want: true, + name: `horizontal left to right`, + line: Line{Point{0, 0}, Point{300, 0}}, + want: 0, }, { - name: `false`, - a: Line{Point{0, 0}, Point{300, 400}}, - b: Line{Point{300, 400}, Point{0, 0}}, - want: false, + name: `horizontal right to left`, + line: Line{Point{300, 0}, Point{0, 0}}, + want: 0, + }, + { + name: `vertical bottom to top`, + line: Line{Point{0, 0}, Point{0, 300}}, + want: math.Inf(1), + }, + { + name: `vertical top to bottom`, + line: Line{Point{0, 300}, Point{0, 0}}, + want: math.Inf(-1), + }, + { + name: `diagonal ascending`, + line: Line{Point{0, 0}, Point{300, 300}}, + want: 1, + }, + { + name: `diagonal descending`, + line: Line{Point{0, 300}, Point{300, 0}}, + want: -1, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.a.IsSame(tc.b)) + assert.Equal(t, tc.want, tc.line.Slope()) }) } } -func TestLine_Length(t *testing.T) { - line := NewLine(Point{0, 0}, Point{300, 400}) - assert.EqualValues(t, 500, line.Length()) +func TestLine_CrossProduct(t *testing.T) { + var ln Line + ln = Line{Point{200, 200}, Point{300, 300}} + assert.EqualValues(t, 0, ln.CrossProduct()) + ln = Line{Point{100, 200}, Point{300, 400}} + assert.EqualValues(t, -20000, ln.CrossProduct()) +} + +func TestLine_IsHorizontal(t *testing.T) { + var ln Line + ln = Line{Point{0, 0}, Point{300, 0}} + assert.True(t, ln.IsHorizontal()) + ln = Line{Point{0, 0}, Point{0, 300}} + assert.False(t, ln.IsHorizontal()) +} + +func TestLine_IsVertical(t *testing.T) { + var ln Line + ln = Line{Point{0, 0}, Point{0, 300}} + assert.True(t, ln.IsVertical()) + ln = Line{Point{0, 0}, Point{300, 0}} + assert.False(t, ln.IsVertical()) +} + +func TestLine_IsMoving(t *testing.T) { + var ln Line + ln = Line{Point{0, 0}, Point{300, 400}} + assert.True(t, ln.IsMoving()) + ln = Line{Point{300, 400}, Point{300, 400}} + assert.False(t, ln.IsMoving()) } func TestLine_Segment(t *testing.T) { @@ -103,151 +169,6 @@ func TestLine_Segment(t *testing.T) { } } -func TestLine_IsMoving(t *testing.T) { - tests := []struct { - name string - line Line - want bool - }{ - { - name: `true`, - line: Line{Point{0, 0}, Point{300, 400}}, - want: true, - }, - { - name: `false`, - line: Line{Point{0, 0}, Point{0, 0}}, - want: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.IsMoving()) - }) - } -} - -func TestLine_Direction(t *testing.T) { - tests := []struct { - name string - line Line - want Point - }{ - { - name: `positive`, - line: Line{Point{100, 100}, Point{200, 200}}, - want: Point{100, 100}, - }, - { - name: `negative`, - line: Line{Point{200, 200}, Point{100, 100}}, - want: Point{-100, -100}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.Direction()) - }) - } -} - -func TestLine_IsHorizontal(t *testing.T) { - tests := []struct { - name string - line Line - want bool - }{ - { - name: `true`, - line: Line{Point{0, 0}, Point{300, 0}}, - want: true, - }, - { - name: `false`, - line: Line{Point{0, 0}, Point{300, 400}}, - want: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.IsHorizontal()) - }) - } -} - -func TestLine_IsVertical(t *testing.T) { - tests := []struct { - name string - line Line - want bool - }{ - { - name: `true`, - line: Line{Point{0, 0}, Point{0, 300}}, - want: true, - }, - { - name: `false`, - line: Line{Point{0, 0}, Point{300, 400}}, - want: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.IsVertical()) - }) - } -} - -func TestLine_Slope(t *testing.T) { - tests := []struct { - name string - line Line - want float64 - }{ - { - name: `horizontal left to right`, - line: Line{Point{0, 0}, Point{300, 0}}, - want: 0, - }, - { - name: `horizontal right to left`, - line: Line{Point{300, 0}, Point{0, 0}}, - want: 0, - }, - { - name: `vertical bottom to top`, - line: Line{Point{0, 0}, Point{0, 300}}, - want: math.Inf(1), - }, - { - name: `vertical top to bottom`, - line: Line{Point{0, 300}, Point{0, 0}}, - want: math.Inf(-1), - }, - { - name: `diagonal ascending`, - line: Line{Point{0, 0}, Point{300, 300}}, - want: 1, - }, - { - name: `diagonal descending`, - line: Line{Point{0, 300}, Point{300, 0}}, - want: -1, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.Slope()) - }) - } -} - func TestLine_IsPointOnLine(t *testing.T) { tests := []struct { name string @@ -346,7 +267,7 @@ func TestLine_IsPointOnSegment(t *testing.T) { } } -func TestLine_LinesIntersectionPoint(t *testing.T) { +func TestLine_LinesIntersection(t *testing.T) { tests := []struct { name string a, b Line @@ -434,7 +355,7 @@ func TestLine_LinesIntersectionPoint(t *testing.T) { } } -func TestLine_SegmentsIntersectionPoint(t *testing.T) { +func TestLine_SegmentsIntersection(t *testing.T) { tests := []struct { name string a, b Line @@ -527,7 +448,7 @@ func TestLine_LineSegmentIntersection(t *testing.T) { } } -func TestLine_RotateLine(t *testing.T) { +func TestLine_Rotate(t *testing.T) { tests := []struct { name string line Line diff --git a/geometry_point.go b/geometry_point.go index 1a5faf5..cb23476 100644 --- a/geometry_point.go +++ b/geometry_point.go @@ -96,12 +96,12 @@ func (p Point) DistanceChebyshev(t Point) float64 { // DistanceToXBound returns the distance between the Point and the field bound of specified width. func (p Point) DistanceToXBound(bound float64) float64 { - return DistanceToBound(p.X, bound) + return distanceToBound(p.X, bound) } // DistanceToYBound returns the distance between the Point and the field bound of specified height. func (p Point) DistanceToYBound(bound float64) float64 { - return DistanceToBound(p.Y, bound) + return distanceToBound(p.Y, bound) } // NeighborsCross returns the neighbors of the Point in the cross shape. diff --git a/geometry_utils.go b/geometry_utils.go index b3df828..a8baf70 100644 --- a/geometry_utils.go +++ b/geometry_utils.go @@ -4,7 +4,38 @@ import ( "math" ) -// DistanceToBound returns the distance between x and the field 0 to bound. -func DistanceToBound(x, bound float64) float64 { +// distanceToBound returns the distance between x and the expected bound. +func distanceToBound(x, bound float64) float64 { return math.Min(x, bound-x) } + +// linesIntersection returns the intersection point of two Lines. +// Arguments s1 and s2 are true if the Line threaten as segment. +// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection +func linesIntersection(a, b Line, s1, s2 bool) (Point, bool) { + av := a.Vector() + bv := b.Vector() + + vcp := av.X*bv.Y - av.Y*bv.X + if vcp == 0 { + return Point{}, false + } + + sv := b.From.Sub(a.From) + cpa := sv.X*av.Y - sv.Y*av.X + cpb := sv.X*bv.Y - sv.Y*bv.X + t := cpb / vcp + u := cpa / vcp + + if s1 && (t < 0 || t > 1) { + return Point{}, false + } + if s2 && (u < 0 || u > 1) { + return Point{}, false + } + + return Point{ + X: a.From.X + t*av.X, + Y: a.From.Y + t*av.Y, + }, true +} diff --git a/geometry_utils_test.go b/geometry_utils_test.go index 82aad9d..0f32a4e 100644 --- a/geometry_utils_test.go +++ b/geometry_utils_test.go @@ -2,46 +2,12 @@ package main import ( "testing" - - "github.com/stretchr/testify/assert" ) -func TestDistanceToBound(t *testing.T) { - tests := []struct { - name string - x float64 - bound float64 - want float64 - }{ - { - name: `left`, - x: 400, - bound: 1000, - want: 400, - }, - { - name: `right`, - x: 600, - bound: 1000, - want: 400, - }, - { - name: `middle`, - x: 500, - bound: 1000, - want: 500, - }, - { - name: `outside`, - x: 1400, - bound: 1000, - want: -400, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, DistanceToBound(tc.x, tc.bound)) - }) +func BenchmarkLine_LinesIntersection(b *testing.B) { + al := Line{Point{0, 0}, Point{300, 400}} + bl := Line{Point{0, 400}, Point{300, 0}} + for i := 0; i < b.N; i++ { + linesIntersection(al, bl, true, true) } }