ruby-geometry 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ class Float
2
+ def ===(other)
3
+ (self - other).abs <= Float::EPSILON * 2
4
+ end
5
+ end
6
+
7
+ class Fixnum
8
+ def ===(other)
9
+ self == other
10
+ end
11
+ end
data/lib/geometry.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'point'
2
+ require 'segment'
3
+ require 'vector'
4
+ require 'polygon'
5
+ require 'line'
6
+
7
+ module Geometry
8
+ include Math
9
+ extend Math
10
+
11
+ def distance(point1, point2)
12
+ hypot point1.x - point2.x, point1.y - point2.y
13
+ end
14
+
15
+ module_function :distance
16
+ end
data/lib/line.rb ADDED
@@ -0,0 +1,81 @@
1
+ module Geometry
2
+ class Line < Struct.new(:point1, :point2)
3
+ def self.new_by_arrays(point1_coordinates, point2_coordinates)
4
+ self.new(Point.new_by_array(point1_coordinates),
5
+ Point.new_by_array(point2_coordinates))
6
+ end
7
+
8
+ def slope
9
+ dy = Float(point2.y - point1.y)
10
+ dx = Float(point2.x - point1.x)
11
+
12
+ return 0.0 if dy == 0
13
+
14
+ dy / dx
15
+ end
16
+
17
+ def y_intercept
18
+ return nil if vertical?
19
+
20
+ # compute change in y between point1 and the origin
21
+ dy = point1.x * slope
22
+ point1.y - dy
23
+ end
24
+
25
+ def x_intercept
26
+ return nil if horizontal?
27
+
28
+ # compute change in x between point1 and the origin
29
+ dx = point1.y / slope
30
+ point1.x - dx
31
+ end
32
+
33
+ def parallel_to?(other)
34
+ # Special handling for when one slope is inf and the other is -inf:
35
+ return true if slope.infinite? and other.slope.infinite?
36
+
37
+ slope == other.slope
38
+ end
39
+
40
+ def vertical?
41
+ if slope.infinite?
42
+ return true
43
+ else
44
+ return false
45
+ end
46
+ end
47
+
48
+ def horizontal?
49
+ slope == 0
50
+ end
51
+
52
+ def intersect_x(other)
53
+ if vertical? and other.vertical?
54
+ if x_intercept == other.x_intercept
55
+ return x_intercept
56
+ else
57
+ return nil
58
+ end
59
+ end
60
+
61
+ return nil if horizontal? and other.horizontal?
62
+
63
+ return x_intercept if vertical?
64
+ return other.x_intercept if other.vertical?
65
+
66
+ d_intercept = other.y_intercept - y_intercept
67
+ d_slope = slope - other.slope
68
+
69
+ # if d_intercept and d_slope are both 0, the result is NaN, which indicates
70
+ # the lines are identical
71
+ d_intercept / d_slope
72
+ end
73
+
74
+ def angle_to(other)
75
+ # return absolute difference between angles to horizontal of self and other
76
+ sa = Math::atan(slope)
77
+ oa = Math::atan(other.slope)
78
+ (sa-oa).abs
79
+ end
80
+ end
81
+ end
data/lib/point.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Geometry
2
+ class Point < Struct.new(:x, :y)
3
+ def self.new_by_array(array)
4
+ self.new(array[0], array[1])
5
+ end
6
+
7
+ def ==(another_point)
8
+ x === another_point.x && y === another_point.y
9
+ end
10
+ end
11
+ end
data/lib/polygon.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Geometry
2
+ class Polygon < Struct.new(:vertices)
3
+ def edges
4
+ edges = []
5
+
6
+ 1.upto(vertices.length - 1) do |vertex_index|
7
+ edges << Segment.new(vertices[vertex_index - 1], vertices[vertex_index])
8
+ end
9
+
10
+ edges << Segment.new(vertices.last, vertices.first)
11
+ end
12
+ end
13
+ end
data/lib/segment.rb ADDED
@@ -0,0 +1,105 @@
1
+ module Geometry
2
+ class SegmentsDoNotIntersect < Exception; end
3
+ class SegmentsOverlap < Exception; end
4
+
5
+ class Segment < Struct.new(:point1, :point2)
6
+ def self.new_by_arrays(point1_coordinates, point2_coordinates)
7
+ self.new(Point.new_by_array(point1_coordinates),
8
+ Point.new_by_array(point2_coordinates))
9
+ end
10
+
11
+ def leftmost_endpoint
12
+ ((point1.x <=> point2.x) == -1) ? point1 : point2
13
+ end
14
+
15
+ def rightmost_endpoint
16
+ ((point1.x <=> point2.x) == 1) ? point1 : point2
17
+ end
18
+
19
+ def topmost_endpoint
20
+ ((point1.y <=> point2.y) == 1) ? point1 : point2
21
+ end
22
+
23
+ def bottommost_endpoint
24
+ ((point1.y <=> point2.y) == -1) ? point1 : point2
25
+ end
26
+
27
+ def contains_point?(point)
28
+ Geometry.distance(point1, point2) ===
29
+ Geometry.distance(point1, point) + Geometry.distance(point, point2)
30
+ end
31
+
32
+ def parallel_to?(segment)
33
+ to_vector.collinear_with?(segment.to_vector)
34
+ end
35
+
36
+ def lies_on_one_line_with?(segment)
37
+ Segment.new(point1, segment.point1).parallel_to?(self) &&
38
+ Segment.new(point1, segment.point2).parallel_to?(self)
39
+ end
40
+
41
+ def intersects_with?(segment)
42
+ Segment.have_intersecting_bounds?(self, segment) &&
43
+ lies_on_line_intersecting?(segment) &&
44
+ segment.lies_on_line_intersecting?(self)
45
+ end
46
+
47
+ def overlaps?(segment)
48
+ Segment.have_intersecting_bounds?(self, segment) &&
49
+ lies_on_one_line_with?(segment)
50
+ end
51
+
52
+ def intersection_point_with(segment)
53
+ raise SegmentsDoNotIntersect unless intersects_with?(segment)
54
+ raise SegmentsOverlap if overlaps?(segment)
55
+
56
+ numerator = (segment.point1.y - point1.y) * (segment.point1.x - segment.point2.x) -
57
+ (segment.point1.y - segment.point2.y) * (segment.point1.x - point1.x);
58
+ denominator = (point2.y - point1.y) * (segment.point1.x - segment.point2.x) -
59
+ (segment.point1.y - segment.point2.y) * (point2.x - point1.x);
60
+
61
+ t = numerator.to_f / denominator;
62
+
63
+ x = point1.x + t * (point2.x - point1.x)
64
+ y = point1.y + t * (point2.y - point1.y)
65
+
66
+ Point.new(x, y)
67
+ end
68
+
69
+ def length
70
+ Geometry.distance(point1, point2)
71
+ end
72
+
73
+ def to_vector
74
+ Vector.new(point2.x - point1.x, point2.y - point1.y)
75
+ end
76
+
77
+ protected
78
+
79
+ def self.have_intersecting_bounds?(segment1, segment2)
80
+ intersects_on_x_axis =
81
+ (segment1.leftmost_endpoint.x < segment2.rightmost_endpoint.x ||
82
+ segment1.leftmost_endpoint.x == segment2.rightmost_endpoint.x) &&
83
+ (segment2.leftmost_endpoint.x < segment1.rightmost_endpoint.x ||
84
+ segment2.leftmost_endpoint.x == segment1.rightmost_endpoint.x)
85
+
86
+ intersects_on_y_axis =
87
+ (segment1.bottommost_endpoint.y < segment2.topmost_endpoint.y ||
88
+ segment1.bottommost_endpoint.y == segment2.topmost_endpoint.y) &&
89
+ (segment2.bottommost_endpoint.y < segment1.topmost_endpoint.y ||
90
+ segment2.bottommost_endpoint.y == segment1.topmost_endpoint.y)
91
+
92
+ intersects_on_x_axis && intersects_on_y_axis
93
+ end
94
+
95
+ def lies_on_line_intersecting?(segment)
96
+ vector_to_first_endpoint = Segment.new(self.point1, segment.point1).to_vector
97
+ vector_to_second_endpoint = Segment.new(self.point1, segment.point2).to_vector
98
+
99
+ #FIXME: '>=' and '<=' method of Fixnum and Float should be overriden too (take precision into account)
100
+ # there is a rare case, when this method is wrong due to precision
101
+ self.to_vector.cross_product(vector_to_first_endpoint) *
102
+ self.to_vector.cross_product(vector_to_second_endpoint) <= 0
103
+ end
104
+ end
105
+ end
data/lib/vector.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Geometry
2
+ class Vector < Struct.new(:x, :y)
3
+ def ==(vector)
4
+ x === vector.x && y === vector.y
5
+ end
6
+
7
+ # Modulus of vector. Also known as length, size or norm
8
+ def modulus
9
+ Math.hypot(x ,y)
10
+ end
11
+
12
+ # z-coordinate of cross product (also known as vector product or outer product)
13
+ # It is positive if other vector should be turned counter-clockwise in order to superpose them.
14
+ # It is negetive if other vector should be turned clockwise in order to superpose them.
15
+ # It is zero when vectors are collinear.
16
+ # Remark: x- and y- coordinates of plane vectors cross product are always zero
17
+ def cross_product(vector)
18
+ x * vector.y - y * vector.x
19
+ end
20
+
21
+ # Scalar product, also known as inner product or dot product
22
+ def scalar_product(vector)
23
+ x * vector.x + y * vector.y
24
+ end
25
+
26
+ def collinear_with?(vector)
27
+ cross_product(vector) === 0
28
+ end
29
+
30
+ def +(vector)
31
+ Vector.new(x + vector.x, y + vector.y)
32
+ end
33
+
34
+ def -(vector)
35
+ self + (-1) * vector
36
+ end
37
+
38
+ def *(scalar)
39
+ Vector.new(x * scalar, y * scalar)
40
+ end
41
+
42
+ def coerce(scalar)
43
+ if scalar.is_a?(Numeric)
44
+ [self, scalar]
45
+ else
46
+ raise ArgumentError, "Vector: cannot coerce #{scalar.inspect}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class DistanceTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_trivial_cases
8
+ assert 1 === distance(Point.new(1, 1), Point.new(1, 2))
9
+
10
+ assert 1 === distance(Point.new(1, 1), Point.new(2, 1))
11
+
12
+ assert sqrt(2) === distance(Point.new(1, 1), Point.new(2, 2))
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class AngleToTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_angle_to_self
8
+ line = Line.new_by_arrays([0, 0], [1, 1])
9
+ assert_equal 0, line.angle_to(line)
10
+ end
11
+
12
+ def test_angle_to_perpendicular
13
+ line = Line.new_by_arrays([0, 0], [1, 1])
14
+ perp = Line.new_by_arrays([0, 0], [1, -1])
15
+ assert_equal Math::PI/2, line.angle_to(perp)
16
+ assert_equal Math::PI/2, perp.angle_to(line)
17
+ end
18
+
19
+ def test_angle_to_acute
20
+ line = Line.new_by_arrays([0, 0], [1, 1])
21
+ acute = Line.new_by_arrays([0, 0], [0, 1])
22
+ assert_equal Math::PI/4, line.angle_to(acute)
23
+ assert_equal Math::PI/4, acute.angle_to(line)
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class HorizontalTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_horizontal
8
+ x, y = 0, 0
9
+ l1 = Line.new_by_arrays([x, y], [x+1, y])
10
+ assert l1.horizontal?
11
+ end
12
+
13
+ def test_not_horizontal
14
+ x, y = 0, 0
15
+ l1 = Line.new_by_arrays([x, y], [x+1, y+1])
16
+ assert ! l1.horizontal?
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class InitializeTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_initialize_by_points
8
+ line = Line.new(Point.new(1, 2), Point.new(3, 4))
9
+
10
+ assert_equal Point.new(1, 2), line.point1
11
+ assert_equal Point.new(3, 4), line.point2
12
+ end
13
+
14
+ def test_initialize_by_points_coordinates_arrays
15
+ line = Line.new_by_arrays([1, 2], [3, 4])
16
+
17
+ assert_equal Point.new(1, 2), line.point1
18
+ assert_equal Point.new(3, 4), line.point2
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class IntersectXTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_vertical_non_overlapping
8
+ l1 = Line.new_by_arrays([0, 0], [0, 1])
9
+ l2 = Line.new_by_arrays([1, 0], [1, 1])
10
+ assert l1.intersect_x(l2).nil?
11
+ end
12
+
13
+ def test_vertical_overlapping
14
+ x = 0
15
+ l1 = Line.new_by_arrays([x, 0], [x, 1])
16
+ l2 = Line.new_by_arrays([x, 0], [x, 1])
17
+ assert_equal x, l1.intersect_x(l2)
18
+ assert_equal x, l2.intersect_x(l1)
19
+ end
20
+
21
+ def test_horizontal_non_overlapping
22
+ l1 = Line.new_by_arrays([0, 0], [1, 0])
23
+ l2 = Line.new_by_arrays([0, 1], [1, 1])
24
+ p l1.intersect_x(l2)
25
+ assert l1.intersect_x(l2).nil?
26
+ end
27
+
28
+ def test_horizontal_overlapping
29
+ y = 0
30
+ l1 = Line.new_by_arrays([0, y], [1, y])
31
+ l2 = Line.new_by_arrays([0, y], [1, y])
32
+ assert l1.intersect_x(l2).nil?
33
+ end
34
+
35
+ def test_perpendicular
36
+ [-1, 0, 1].each do |xo|
37
+ [-1, 0, 1].each do |yo|
38
+ l1 = Line.new_by_arrays([xo, yo], [xo+0, yo+1])
39
+ l2 = Line.new_by_arrays([xo, yo], [xo+1, yo+0])
40
+ assert_equal xo, l1.intersect_x(l2)
41
+ assert_equal xo, l2.intersect_x(l1)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ require 'test/unit'
2
+ require 'geometry'
3
+
4
+ class ParallelToTest < Test::Unit::TestCase
5
+ include Geometry
6
+
7
+ def test_identical
8
+ num_tests = 100
9
+ (1..num_tests).each do
10
+ point1 = [rand-0.5, rand-0.5]
11
+ point2 = [rand-0.5, rand-0.5]
12
+ line = Line.new_by_arrays(point1, point2)
13
+ assert line.parallel_to? line
14
+ end
15
+ end
16
+
17
+ def test_verticals
18
+ up = Line.new_by_arrays([0, 0], [0, 1])
19
+ down = Line.new_by_arrays([0, 0], [0, -1])
20
+ assert up.parallel_to? down
21
+ assert down.parallel_to? up
22
+ end
23
+
24
+ def test_horizontals
25
+ left = Line.new_by_arrays([0, 0], [-1, 0])
26
+ right = Line.new_by_arrays([0, 0], [1, 0])
27
+ assert left.parallel_to? right
28
+ assert right.parallel_to? left
29
+ end
30
+
31
+ def test_shifted
32
+ shift = 1
33
+ l1 = Line.new_by_arrays([0, 0], [1, 1])
34
+ l2 = Line.new_by_arrays([0+shift, 0], [1+shift, 1])
35
+ assert l1.parallel_to? l2
36
+ assert l2.parallel_to? l1
37
+ end
38
+
39
+ def test_not_parallel
40
+ l1 = Line.new_by_arrays([0, 0], [1, 1])
41
+ l2 = Line.new_by_arrays([0, 0], [1, -1])
42
+ assert ! l1.parallel_to?(l2)
43
+ assert ! l1.parallel_to?(l2)
44
+ end
45
+ end