flash_math 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.rspec +4 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +187 -0
- data/Rakefile +6 -0
- data/flash_math.gemspec +27 -0
- data/lib/flash_math.rb +3 -0
- data/lib/flash_math/modules/geometry.rb +10 -0
- data/lib/flash_math/modules/geometry/geometric_bounding_box.rb +16 -0
- data/lib/flash_math/modules/geometry/geometric_distance.rb +20 -0
- data/lib/flash_math/modules/geometry/geometric_line.rb +87 -0
- data/lib/flash_math/modules/geometry/geometric_point.rb +19 -0
- data/lib/flash_math/modules/geometry/geometric_point_in_polygon.rb +63 -0
- data/lib/flash_math/modules/geometry/geometric_polygon.rb +36 -0
- data/lib/flash_math/modules/geometry/geometric_segment.rb +128 -0
- data/lib/flash_math/modules/geometry/geometric_vector.rb +46 -0
- data/lib/flash_math/modules/statistics.rb +1 -0
- data/lib/flash_math/modules/statistics/statistical_spread.rb +129 -0
- data/lib/flash_math/version.rb +3 -0
- data/spec/lib/geometry/geometric_bounding_box_spec.rb +67 -0
- data/spec/lib/geometry/geometric_distance_spec.rb +55 -0
- data/spec/lib/geometry/geometric_line_spec.rb +287 -0
- data/spec/lib/geometry/geometric_point_spec.rb +53 -0
- data/spec/lib/geometry/geometric_polygon_spec.rb +148 -0
- data/spec/lib/geometry/geometric_segment_spec.rb +231 -0
- data/spec/lib/geometry/geometric_vector_spec.rb +110 -0
- data/spec/lib/statistics/statistics_spread_spec.rb +219 -0
- data/spec/spec_helper.rb +4 -0
- metadata +168 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
class GeometricPoint < Struct.new(:x, :y)
|
2
|
+
|
3
|
+
def self.new_by_array(array)
|
4
|
+
self.new(array[0], array[1])
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_vector
|
8
|
+
GeometricVector.new(x, y)
|
9
|
+
end
|
10
|
+
|
11
|
+
def advance_by(vector)
|
12
|
+
GeometricPoint.new(x + vector.x, y + vector.y)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(another_point)
|
16
|
+
x === another_point.x && y === another_point.y
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class GeometricPointInPolygon < Struct.new(:point, :polygon)
|
2
|
+
extend Memoist
|
3
|
+
|
4
|
+
def inside?
|
5
|
+
point_location == :inside
|
6
|
+
end
|
7
|
+
|
8
|
+
def outside?
|
9
|
+
point_location == :outside
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_the_boundary?
|
13
|
+
point_location == :on_the_boundary
|
14
|
+
end
|
15
|
+
|
16
|
+
def point_location
|
17
|
+
return :outside unless bounding_box.contains?(point)
|
18
|
+
return :on_the_boundary if point_is_vertex? || point_on_edge?
|
19
|
+
|
20
|
+
intersection_count(choose_good_ray).odd? ? :inside : :outside
|
21
|
+
end
|
22
|
+
|
23
|
+
delegate :vertices, :edges, :bounding_box, to: :polygon
|
24
|
+
memoize :point_location, :edges, :bounding_box
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def choose_good_ray
|
29
|
+
ray = random_ray
|
30
|
+
while ! good_ray?(ray) do
|
31
|
+
ray = random_ray
|
32
|
+
end
|
33
|
+
ray
|
34
|
+
end
|
35
|
+
|
36
|
+
def good_ray?(ray)
|
37
|
+
edges.none? { |edge| edge.parallel_to?(ray) } && vertices.none? { |vertex| ray.contains_point?(vertex) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def intersection_count(ray)
|
41
|
+
edges.select { |edge| edge.intersects_with?(ray) }.size
|
42
|
+
end
|
43
|
+
|
44
|
+
def point_is_vertex?
|
45
|
+
vertices.any? { |vertex| vertex == point }
|
46
|
+
end
|
47
|
+
|
48
|
+
def point_on_edge?
|
49
|
+
edges.any? { |edge| edge.contains_point?(point) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def random_ray
|
53
|
+
random_direction = rand * (2 * Math::PI)
|
54
|
+
|
55
|
+
ray_endpoint = GeometricPoint.new(sufficient_ray_radius * Math.cos(random_direction), sufficient_ray_radius * Math.sin(random_direction))
|
56
|
+
GeometricSegment.new(point, ray_endpoint)
|
57
|
+
end
|
58
|
+
|
59
|
+
def sufficient_ray_radius
|
60
|
+
@sufficient_ray_radius ||= bounding_box.diagonal.length * 2
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class GeometricPolygon < Struct.new(:vertices)
|
2
|
+
|
3
|
+
def bounding_box
|
4
|
+
leftbottom = GeometricPoint.new(vertices.map(&:x).min, vertices.map(&:y).min)
|
5
|
+
righttop = GeometricPoint.new(vertices.map(&:x).max, vertices.map(&:y).max)
|
6
|
+
|
7
|
+
GeometricBoundingBox.new(leftbottom, righttop)
|
8
|
+
end
|
9
|
+
|
10
|
+
def area
|
11
|
+
sum = 0.0
|
12
|
+
(0..vertices.length-1).each do |i|
|
13
|
+
a = vertices[i-1]
|
14
|
+
b = vertices[i]
|
15
|
+
|
16
|
+
sum = sum + ((a.x * b.y) - (a.y * b.x))
|
17
|
+
end
|
18
|
+
(sum / 2).abs
|
19
|
+
end
|
20
|
+
|
21
|
+
def contains?(point)
|
22
|
+
point_in_polygon = GeometricPointInPolygon.new(point, self)
|
23
|
+
point_in_polygon.inside? || point_in_polygon.on_the_boundary?
|
24
|
+
end
|
25
|
+
|
26
|
+
def edges
|
27
|
+
edges = []
|
28
|
+
|
29
|
+
1.upto(vertices.length - 1) do |vertex_index|
|
30
|
+
edges << GeometricSegment.new(vertices[vertex_index - 1], vertices[vertex_index])
|
31
|
+
end
|
32
|
+
|
33
|
+
edges << GeometricSegment.new(vertices.last, vertices.first)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
class GeometricSegmentsDoNotIntersect < Exception; end
|
2
|
+
class GeometricSegmentsOverlap < Exception; end
|
3
|
+
|
4
|
+
class GeometricSegment < Struct.new(:point1, :point2)
|
5
|
+
|
6
|
+
def self.new_by_arrays(point1_coordinates, point2_coordinates)
|
7
|
+
self.new(GeometricPoint.new_by_array(point1_coordinates),
|
8
|
+
GeometricPoint.new_by_array(point2_coordinates))
|
9
|
+
end
|
10
|
+
|
11
|
+
def bottommost_endpoint
|
12
|
+
((point1.y <=> point2.y) == -1) ? point1 : point2
|
13
|
+
end
|
14
|
+
|
15
|
+
def leftmost_endpoint
|
16
|
+
((point1.x <=> point2.x) == -1) ? point1 : point2
|
17
|
+
end
|
18
|
+
|
19
|
+
def rightmost_endpoint
|
20
|
+
((point1.x <=> point2.x) == 1) ? point1 : point2
|
21
|
+
end
|
22
|
+
|
23
|
+
def topmost_endpoint
|
24
|
+
((point1.y <=> point2.y) == 1) ? point1 : point2
|
25
|
+
end
|
26
|
+
|
27
|
+
def contains_point?(point)
|
28
|
+
GeometricDistance.new(point1, point2).distance ===
|
29
|
+
GeometricDistance.new(point1, point).distance + GeometricDistance.new(point, point2).distance
|
30
|
+
end
|
31
|
+
|
32
|
+
def distance_to(point)
|
33
|
+
q = point.to_vector
|
34
|
+
p1 = point1.to_vector
|
35
|
+
p2 = point2.to_vector
|
36
|
+
|
37
|
+
u = p2 - p1
|
38
|
+
v = q - p1
|
39
|
+
|
40
|
+
a = u.scalar_product(v)
|
41
|
+
if a < 0
|
42
|
+
p = p1
|
43
|
+
else
|
44
|
+
b = u.scalar_product(u)
|
45
|
+
if a > b
|
46
|
+
p = p2
|
47
|
+
else
|
48
|
+
p = p1 + (a.to_f / b * u)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
return GeometricDistance.new(q, p).distance
|
53
|
+
end
|
54
|
+
|
55
|
+
def intersection_point_with(segment)
|
56
|
+
raise GeometricSegmentsDoNotIntersect unless intersects_with?(segment)
|
57
|
+
raise GeometricSegmentsOverlap if overlaps?(segment)
|
58
|
+
|
59
|
+
numerator = (segment.point1.y - point1.y) * (segment.point1.x - segment.point2.x) -
|
60
|
+
(segment.point1.y - segment.point2.y) * (segment.point1.x - point1.x);
|
61
|
+
denominator = (point2.y - point1.y) * (segment.point1.x - segment.point2.x) -
|
62
|
+
(segment.point1.y - segment.point2.y) * (point2.x - point1.x);
|
63
|
+
|
64
|
+
t = numerator.to_f / denominator;
|
65
|
+
|
66
|
+
x = point1.x + t * (point2.x - point1.x)
|
67
|
+
y = point1.y + t * (point2.y - point1.y)
|
68
|
+
|
69
|
+
GeometricPoint.new(x, y)
|
70
|
+
end
|
71
|
+
|
72
|
+
def intersects_with?(segment)
|
73
|
+
GeometricSegment.have_intersecting_bounds?(self, segment) &&
|
74
|
+
lies_on_line_intersecting?(segment) &&
|
75
|
+
segment.lies_on_line_intersecting?(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def length
|
79
|
+
GeometricDistance.new(point1, point2).distance
|
80
|
+
end
|
81
|
+
|
82
|
+
def lies_on_one_line_with?(segment)
|
83
|
+
GeometricSegment.new(point1, segment.point1).parallel_to?(self) &&
|
84
|
+
GeometricSegment.new(point1, segment.point2).parallel_to?(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def overlaps?(segment)
|
88
|
+
GeometricSegment.have_intersecting_bounds?(self, segment) &&
|
89
|
+
lies_on_one_line_with?(segment)
|
90
|
+
end
|
91
|
+
|
92
|
+
def parallel_to?(segment)
|
93
|
+
to_vector.collinear_with?(segment.to_vector)
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_vector
|
97
|
+
GeometricVector.new(point2.x - point1.x, point2.y - point1.y)
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def self.have_intersecting_bounds?(segment1, segment2)
|
103
|
+
intersects_on_x_axis =
|
104
|
+
(segment1.leftmost_endpoint.x < segment2.rightmost_endpoint.x ||
|
105
|
+
segment1.leftmost_endpoint.x == segment2.rightmost_endpoint.x) &&
|
106
|
+
(segment2.leftmost_endpoint.x < segment1.rightmost_endpoint.x ||
|
107
|
+
segment2.leftmost_endpoint.x == segment1.rightmost_endpoint.x)
|
108
|
+
|
109
|
+
intersects_on_y_axis =
|
110
|
+
(segment1.bottommost_endpoint.y < segment2.topmost_endpoint.y ||
|
111
|
+
segment1.bottommost_endpoint.y == segment2.topmost_endpoint.y) &&
|
112
|
+
(segment2.bottommost_endpoint.y < segment1.topmost_endpoint.y ||
|
113
|
+
segment2.bottommost_endpoint.y == segment1.topmost_endpoint.y)
|
114
|
+
|
115
|
+
intersects_on_x_axis && intersects_on_y_axis
|
116
|
+
end
|
117
|
+
|
118
|
+
def lies_on_line_intersecting?(segment)
|
119
|
+
vector_to_first_endpoint = GeometricSegment.new(self.point1, segment.point1).to_vector
|
120
|
+
vector_to_second_endpoint = GeometricSegment.new(self.point1, segment.point2).to_vector
|
121
|
+
|
122
|
+
#FIXME: '>=' and '<=' method of Fixnum and Float should be overriden too (take precision into account)
|
123
|
+
# there is a rare case, when this method is wrong due to precision
|
124
|
+
self.to_vector.cross_product(vector_to_first_endpoint) *
|
125
|
+
self.to_vector.cross_product(vector_to_second_endpoint) <= 0
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class GeometricVector < Struct.new(:x, :y)
|
2
|
+
|
3
|
+
def self.new_by_array(array)
|
4
|
+
self.new(array[0], array[1])
|
5
|
+
end
|
6
|
+
|
7
|
+
def ==(vector)
|
8
|
+
x === vector.x && y === vector.y
|
9
|
+
end
|
10
|
+
|
11
|
+
def +(vector)
|
12
|
+
GeometricVector.new(x + vector.x, y + vector.y)
|
13
|
+
end
|
14
|
+
|
15
|
+
def -(vector)
|
16
|
+
self + (-1) * vector
|
17
|
+
end
|
18
|
+
|
19
|
+
def *(scalar)
|
20
|
+
GeometricVector.new(x * scalar, y * scalar)
|
21
|
+
end
|
22
|
+
|
23
|
+
def coerce(scalar)
|
24
|
+
if scalar.is_a?(Numeric)
|
25
|
+
[self, scalar]
|
26
|
+
else
|
27
|
+
raise ArgumentError, "GeometricVector: cannot coerce #{scalar.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def cross_product(vector)
|
32
|
+
x * vector.y - y * vector.x
|
33
|
+
end
|
34
|
+
|
35
|
+
def scalar_product(vector)
|
36
|
+
x * vector.x + y * vector.y
|
37
|
+
end
|
38
|
+
|
39
|
+
def collinear_with?(vector)
|
40
|
+
cross_product(vector) === 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def modulus
|
44
|
+
Math.hypot(x ,y)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'flash_math/modules/statistics/statistical_spread'
|
@@ -0,0 +1,129 @@
|
|
1
|
+
class StatisticalSpread < Struct.new(:values)
|
2
|
+
|
3
|
+
def sum(identity=0, &block)
|
4
|
+
if block_given?
|
5
|
+
StatisticalSpread.new(values.map(&block)).sum(identity)
|
6
|
+
else
|
7
|
+
values.inject(:+) || identity
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def mean
|
12
|
+
return if values.length < 1
|
13
|
+
sum / values.length.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def median
|
17
|
+
return if values.length < 1
|
18
|
+
sorted = values.sort
|
19
|
+
|
20
|
+
if values.length % 2 == 0
|
21
|
+
(sorted[(values.length / 2) -1] + sorted[values.length / 2]) / 2.0
|
22
|
+
else
|
23
|
+
sorted[values.length / 2]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mode
|
28
|
+
return if values.length < 1
|
29
|
+
frequency_distribution = values.inject(Hash.new(0)) { |hash, value| hash[value] += 1; hash }
|
30
|
+
top_2 = frequency_distribution.sort { |a,b| b[1] <=> a[1] } .take(2)
|
31
|
+
if top_2.length == 1
|
32
|
+
top_2.first.first
|
33
|
+
elsif top_2.first.last == top_2.last.last
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
top_2.first.first
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def range
|
41
|
+
return if values.length < 1
|
42
|
+
sorted = values.sort
|
43
|
+
sorted.last - sorted.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def max
|
47
|
+
return if values.length < 1
|
48
|
+
values.sort.last
|
49
|
+
end
|
50
|
+
|
51
|
+
def min
|
52
|
+
return if values.length < 1
|
53
|
+
values.sort.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def percentile_from_value(value)
|
57
|
+
return if values.length < 1
|
58
|
+
(values.sort.index(value) / values.length.to_f * 100).ceil
|
59
|
+
end
|
60
|
+
|
61
|
+
def value_from_percentile(percentile)
|
62
|
+
return if values.length < 1
|
63
|
+
value_index = (percentile.to_f / 100 * values.length).ceil
|
64
|
+
values.sort[value_index]
|
65
|
+
end
|
66
|
+
|
67
|
+
def variance
|
68
|
+
return if values.length < 1
|
69
|
+
precalculated_mean = StatisticalSpread.new(values).mean
|
70
|
+
sum = values.inject(0) { |accumulator, value| accumulator + (value - precalculated_mean) ** 2 }
|
71
|
+
sum / (values.length.to_f - 1)
|
72
|
+
end
|
73
|
+
|
74
|
+
def population_variance
|
75
|
+
return if values.length < 1
|
76
|
+
precalculated_mean = StatisticalSpread.new(values).mean
|
77
|
+
sum = values.inject(0) { |accumulator, value| accumulator + (value - precalculated_mean) ** 2 }
|
78
|
+
sum / values.length.to_f
|
79
|
+
end
|
80
|
+
|
81
|
+
def standard_deviation
|
82
|
+
return if values.length < 2
|
83
|
+
Math.sqrt(StatisticalSpread.new(values).variance)
|
84
|
+
end
|
85
|
+
|
86
|
+
def relative_standard_deviation
|
87
|
+
return if values.length < 1
|
88
|
+
precalculated_mean = StatisticalSpread.new(values).mean
|
89
|
+
(StatisticalSpread.new(values).population_standard_deviation / precalculated_mean) * 100.0
|
90
|
+
end
|
91
|
+
|
92
|
+
def population_standard_deviation
|
93
|
+
return if values.length < 1
|
94
|
+
Math.sqrt(StatisticalSpread.new(values).population_variance)
|
95
|
+
end
|
96
|
+
|
97
|
+
def skewness
|
98
|
+
return if values.length == 0
|
99
|
+
return 0 if values.length == 1
|
100
|
+
StatisticalSpread.new(values).sum_cubed_deviation / ((values.length - 1) * StatisticalSpread.new(values).cubed_standard_deviation.to_f)
|
101
|
+
end
|
102
|
+
|
103
|
+
def kurtosis
|
104
|
+
return if values.length == 0
|
105
|
+
return 0 if values.length == 1
|
106
|
+
StatisticalSpread.new(values).sum_quarted_deviation / ((values.length - 1) * StatisticalSpread.new(values).quarted_standard_deviation.to_f)
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def sum_cubed_deviation
|
112
|
+
precalculated_mean = StatisticalSpread.new(values).mean
|
113
|
+
values.inject(0) { |sum, value| sum + (value - precalculated_mean) ** 3}
|
114
|
+
end
|
115
|
+
|
116
|
+
def cubed_standard_deviation
|
117
|
+
StatisticalSpread.new(values).standard_deviation ** 3
|
118
|
+
end
|
119
|
+
|
120
|
+
def sum_quarted_deviation
|
121
|
+
precalculated_mean = StatisticalSpread.new(values).mean
|
122
|
+
values.inject(0) { |sum, value| sum + (value - precalculated_mean) ** 4}
|
123
|
+
end
|
124
|
+
|
125
|
+
def quarted_standard_deviation
|
126
|
+
StatisticalSpread.new(values).standard_deviation ** 4
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GeometricBoundingBox do
|
4
|
+
|
5
|
+
describe '#initialize_by_points' do
|
6
|
+
point1 = GeometricPoint.new(1, 2)
|
7
|
+
point2 = GeometricPoint.new(3, 4)
|
8
|
+
bounding_box = GeometricBoundingBox.new(point1, point2)
|
9
|
+
|
10
|
+
it 'to be equal' do
|
11
|
+
expect(bounding_box.leftbottom).to eq(point1)
|
12
|
+
expect(bounding_box.righttop).to eq(point2)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'to be not equal' do
|
16
|
+
expect(bounding_box.leftbottom).not_to eq(point2)
|
17
|
+
expect(bounding_box.righttop).not_to eq(point1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#initialize_by_arrays' do
|
22
|
+
point1 = GeometricPoint.new(1, 2)
|
23
|
+
point2 = GeometricPoint.new(3, 4)
|
24
|
+
bounding_box = GeometricBoundingBox.new_by_arrays([1, 2], [3, 4])
|
25
|
+
|
26
|
+
it 'to be equal' do
|
27
|
+
expect(bounding_box.leftbottom).to eq(point1)
|
28
|
+
expect(bounding_box.righttop).to eq(point2)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'to be not equal' do
|
32
|
+
expect(bounding_box.leftbottom).not_to eq(point2)
|
33
|
+
expect(bounding_box.righttop).not_to eq(point1)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#contains' do
|
38
|
+
bounding_box = GeometricBoundingBox.new(GeometricPoint.new(-1, -1), GeometricPoint.new(1, 1))
|
39
|
+
|
40
|
+
it 'to be containing point' do
|
41
|
+
expect(bounding_box.contains?(GeometricPoint.new(0, 0))).to eq(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'to be not containing point' do
|
45
|
+
expect(bounding_box.contains?(GeometricPoint.new(2, 2))).to eq(false)
|
46
|
+
expect(bounding_box.contains?(GeometricPoint.new(-2, -2))).to eq(false)
|
47
|
+
expect(bounding_box.contains?(GeometricPoint.new(1, 2))).to eq(false)
|
48
|
+
expect(bounding_box.contains?(GeometricPoint.new(0, -1.1))).to eq(false)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'to be not containing point' do
|
52
|
+
expect(bounding_box.contains?(GeometricPoint.new(2, 2))).to eq(false)
|
53
|
+
expect(bounding_box.contains?(GeometricPoint.new(-2, -2))).to eq(false)
|
54
|
+
expect(bounding_box.contains?(GeometricPoint.new(1, 2))).to eq(false)
|
55
|
+
expect(bounding_box.contains?(GeometricPoint.new(0, -1.1))).to eq(false)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'to be containing point at edge' do
|
59
|
+
expect(bounding_box.contains?(GeometricPoint.new(1, 0))).to eq(true)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'to be containing point at vertex' do
|
63
|
+
expect(bounding_box.contains?(GeometricPoint.new(1, 1))).to eq(true)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|