ruby-geometry 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/comparison_with_precision.rb +11 -0
- data/lib/geometry.rb +16 -0
- data/lib/line.rb +81 -0
- data/lib/point.rb +11 -0
- data/lib/polygon.rb +13 -0
- data/lib/segment.rb +105 -0
- data/lib/vector.rb +50 -0
- data/test/geometry/distance_test.rb +14 -0
- data/test/line/angle_to_test.rb +25 -0
- data/test/line/horizontal_test.rb +18 -0
- data/test/line/initialize_test.rb +20 -0
- data/test/line/intersect_x_test.rb +45 -0
- data/test/line/parallel_to_test.rb +45 -0
- data/test/line/slope_test.rb +53 -0
- data/test/line/vertical_test.rb +18 -0
- data/test/line/x_intercept_test.rb +44 -0
- data/test/line/y_intercept_test.rb +44 -0
- data/test/point/equals_test.rb +15 -0
- data/test/point/initialize_test.rb +20 -0
- data/test/polygon/edges_test.rb +56 -0
- data/test/segment/bounds_test.rb +26 -0
- data/test/segment/contains_point_test.rb +61 -0
- data/test/segment/initialize_test.rb +20 -0
- data/test/segment/intersection_point_with_test.rb +54 -0
- data/test/segment/intersects_with_test.rb +98 -0
- data/test/segment/length_test.rb +15 -0
- data/test/segment/overlaps_test.rb +46 -0
- data/test/segment/parallel_to_test.rb +27 -0
- data/test/segment/to_vector_test.rb +19 -0
- data/test/vector/arithmetics_test.rb +22 -0
- data/test/vector/collinear_with_test.rb +27 -0
- data/test/vector/cross_product_test.rb +22 -0
- data/test/vector/equals_test.rb +15 -0
- data/test/vector/modulus_test.rb +15 -0
- data/test/vector/scalar_product_test.rb +18 -0
- metadata +125 -0
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
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
|