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.
- 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
|