ruby-geometry 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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