geometry-in-ruby 0.0.1
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/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/geometry-in-ruby.gemspec +23 -0
- data/lib/geometry/arc.rb +94 -0
- data/lib/geometry/circle.rb +122 -0
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +140 -0
- data/lib/geometry/line.rb +154 -0
- data/lib/geometry/obround.rb +238 -0
- data/lib/geometry/path.rb +67 -0
- data/lib/geometry/point.rb +163 -0
- data/lib/geometry/point_zero.rb +107 -0
- data/lib/geometry/polygon.rb +368 -0
- data/lib/geometry/polyline.rb +318 -0
- data/lib/geometry/rectangle.rb +378 -0
- data/lib/geometry/regular_polygon.rb +136 -0
- data/lib/geometry/rotation.rb +190 -0
- data/lib/geometry/size.rb +75 -0
- data/lib/geometry/size_zero.rb +70 -0
- data/lib/geometry/square.rb +113 -0
- data/lib/geometry/text.rb +24 -0
- data/lib/geometry/transformation/composition.rb +39 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/lib/geometry.rb +22 -0
- data/test/geometry/arc.rb +25 -0
- data/test/geometry/circle.rb +112 -0
- data/test/geometry/edge.rb +132 -0
- data/test/geometry/line.rb +132 -0
- data/test/geometry/obround.rb +25 -0
- data/test/geometry/path.rb +66 -0
- data/test/geometry/point.rb +258 -0
- data/test/geometry/point_zero.rb +177 -0
- data/test/geometry/polygon.rb +214 -0
- data/test/geometry/polyline.rb +266 -0
- data/test/geometry/rectangle.rb +154 -0
- data/test/geometry/regular_polygon.rb +120 -0
- data/test/geometry/rotation.rb +108 -0
- data/test/geometry/size.rb +97 -0
- data/test/geometry/size_zero.rb +153 -0
- data/test/geometry/square.rb +66 -0
- data/test/geometry/transformation/composition.rb +49 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- data/test/geometry.rb +5 -0
- metadata +117 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/polygon'
|
3
|
+
|
4
|
+
describe Geometry::Polygon do
|
5
|
+
Polygon = Geometry::Polygon
|
6
|
+
|
7
|
+
let(:cw_unit_square) { Polygon.new [0,0], [0,1], [1,1], [1,0] }
|
8
|
+
let(:unit_square) { Polygon.new [0,0], [1,0], [1,1], [0,1] }
|
9
|
+
let(:simple_concave) { Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2] }
|
10
|
+
subject { unit_square }
|
11
|
+
|
12
|
+
it "must create a Polygon object with no arguments" do
|
13
|
+
polygon = Geometry::Polygon.new
|
14
|
+
assert_kind_of(Geometry::Polygon, polygon)
|
15
|
+
assert_equal(0, polygon.edges.size)
|
16
|
+
assert_equal(0, polygon.vertices.size)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "must create a Polygon object from array arguments" do
|
20
|
+
polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1])
|
21
|
+
assert_kind_of(Geometry::Polygon, polygon)
|
22
|
+
assert_equal(4, polygon.edges.size)
|
23
|
+
assert_equal(4, polygon.vertices.size)
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "when creating a Polygon from an array of Points" do
|
27
|
+
it "must ignore repeated Points" do
|
28
|
+
polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [1,1], [0,1])
|
29
|
+
polygon.must_be_kind_of Geometry::Polygon
|
30
|
+
polygon.edges.size.must_equal 4
|
31
|
+
polygon.vertices.size.must_equal 4
|
32
|
+
polygon.must_equal Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1])
|
33
|
+
end
|
34
|
+
|
35
|
+
it "must collapse collinear Edges" do
|
36
|
+
polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0.5,1], [0,1])
|
37
|
+
polygon.must_equal Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1])
|
38
|
+
end
|
39
|
+
|
40
|
+
it "must collapse backtracking Edges" do
|
41
|
+
polygon = Geometry::Polygon.new [0,0], [2,0], [2,2], [1,2], [1,1], [1,2], [0,2]
|
42
|
+
polygon.must_equal Geometry::Polygon.new([0,0], [2,0], [2,2], [0,2])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it "must compare identical polygons as equal" do
|
47
|
+
(unit_square.eql? unit_square).must_equal true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "must create closed polygons" do
|
51
|
+
subject.closed?.must_equal true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "must handle already closed polygons" do
|
55
|
+
polygon = Geometry::Polygon.new([0,0], [1,0], [1,1], [0,1], [0,0])
|
56
|
+
assert_kind_of(Geometry::Polygon, polygon)
|
57
|
+
assert_equal(4, polygon.edges.size)
|
58
|
+
assert_equal(4, polygon.vertices.size)
|
59
|
+
assert_equal(polygon.edges.first.first, polygon.edges.last.last)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "must return itself on close" do
|
63
|
+
closed = subject.close
|
64
|
+
closed.closed?.must_equal true
|
65
|
+
closed.must_equal subject
|
66
|
+
closed.must_be_same_as subject
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "orientation" do
|
70
|
+
it "must return true for clockwise" do
|
71
|
+
Polygon.new([0,0], [0,1], [1,1], [1,0]).clockwise?.must_equal true
|
72
|
+
Polygon.new([1,1], [1,3], [3,3], [3,1]).clockwise?.must_equal true
|
73
|
+
end
|
74
|
+
|
75
|
+
it "must return false for counterclockwise" do
|
76
|
+
Polygon.new([0,0], [1,0], [1,1], [0,1]).clockwise?.must_equal false
|
77
|
+
Polygon.new([1,1], [3,1], [3,3], [1,3]).clockwise?.must_equal false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "must gift wrap a square polygon" do
|
82
|
+
polygon = Polygon.new [0,0], [1,0], [1,1], [0,1]
|
83
|
+
convex_hull = polygon.wrap
|
84
|
+
convex_hull.must_be_kind_of Geometry::Polygon
|
85
|
+
convex_hull.edges.size.must_equal 4
|
86
|
+
convex_hull.vertices.must_equal [[0,0], [0,1], [1,1], [1,0]].map {|a| Point[*a]}
|
87
|
+
end
|
88
|
+
|
89
|
+
it "must gift wrap another square polygon" do
|
90
|
+
polygon = Polygon.new [0,1], [0,0], [1,0], [1,1]
|
91
|
+
convex_hull = polygon.wrap
|
92
|
+
convex_hull.must_be_kind_of Geometry::Polygon
|
93
|
+
convex_hull.edges.size.must_equal 4
|
94
|
+
convex_hull.vertices.must_equal [[0,0], [0,1], [1,1], [1,0]].map {|a| Point[*a]}
|
95
|
+
end
|
96
|
+
|
97
|
+
it "must gift wrap a concave polygon" do
|
98
|
+
polygon = Polygon.new [0,0], [1,-1], [2,0], [1,1], [2,2], [0,1]
|
99
|
+
convex_hull = polygon.wrap
|
100
|
+
convex_hull.must_be_kind_of Geometry::Polygon
|
101
|
+
convex_hull.edges.size.must_equal 5
|
102
|
+
convex_hull.vertices.must_equal [Point[0, 0], Point[0, 1], Point[2, 2], Point[2, 0], Point[1, -1]]
|
103
|
+
end
|
104
|
+
|
105
|
+
it "must gift wrap a polygon" do
|
106
|
+
polygon = Polygon.new [0,0], [1,-1], [2,0], [2,1], [0,1]
|
107
|
+
convex_hull = polygon.wrap
|
108
|
+
convex_hull.must_be_kind_of Geometry::Polygon
|
109
|
+
convex_hull.edges.size.must_equal 5
|
110
|
+
convex_hull.vertices.must_equal [[0,0], [0,1], [2,1], [2,0], [1,-1]].map {|a| Point[*a]}
|
111
|
+
end
|
112
|
+
|
113
|
+
it "must generate spokes" do
|
114
|
+
unit_square.spokes.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1]]
|
115
|
+
cw_unit_square.spokes.must_equal [Vector[-1,-1], Vector[-1,1], Vector[1,1], Vector[1,-1]]
|
116
|
+
simple_concave.spokes.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1], Vector[-1,1], Vector[1,1], Vector[1,1], Vector[-1,1]]
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "spaceship" do
|
120
|
+
it "with a Point" do
|
121
|
+
(unit_square <=> Point[2,0]).must_equal -1
|
122
|
+
(unit_square <=> Point[1,0]).must_equal 0
|
123
|
+
(unit_square <=> Point[0.5,0.5]).must_equal 1
|
124
|
+
end
|
125
|
+
|
126
|
+
it "with a Point that lies on a horizontal edge" do
|
127
|
+
(unit_square <=> Point[0.5,0]).must_equal 0
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "when outsetting" do
|
132
|
+
it "must outset a unit square" do
|
133
|
+
outset_polygon = unit_square.outset(1)
|
134
|
+
expected_polygon = Polygon.new [-1.0,-1.0], [2.0,-1.0], [2.0,2.0], [-1.0,2.0]
|
135
|
+
outset_polygon.must_equal expected_polygon
|
136
|
+
end
|
137
|
+
|
138
|
+
it "must outset a simple concave polygon" do
|
139
|
+
concave_polygon = Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2]
|
140
|
+
outset_polygon = concave_polygon.outset(1)
|
141
|
+
outset_polygon.must_equal Polygon.new [-1,-1], [5,-1], [5,3], [-1,3]
|
142
|
+
end
|
143
|
+
|
144
|
+
it "must outset a concave polygon" do
|
145
|
+
concave_polygon = Polygon.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2]
|
146
|
+
outset_polygon = concave_polygon.outset(2)
|
147
|
+
outset_polygon.must_equal Polygon.new [-2,-2], [6,-2], [6,4], [-2,4]
|
148
|
+
end
|
149
|
+
|
150
|
+
it "must outset an asymetric concave polygon" do
|
151
|
+
concave_polygon = Polygon.new [0,0], [4,0], [4,3], [3,3], [3,1], [1,1], [1,2], [0,2]
|
152
|
+
outset_polygon = concave_polygon.outset(2)
|
153
|
+
outset_polygon.must_equal Polygon.new [-2,-2], [6,-2], [6,5], [1,5], [1,4], [-2,4]
|
154
|
+
end
|
155
|
+
|
156
|
+
it "must outset a concave polygon with multiply-intersecting edges" do
|
157
|
+
concave_polygon = Polygon.new [0,0], [5,0], [5,2], [4,2], [4,1], [3,1], [3,2], [2,2], [2,1], [1,1], [1,2], [0,2]
|
158
|
+
outset_polygon = concave_polygon.outset(1)
|
159
|
+
outset_polygon.must_equal Polygon.new [-1,-1], [6,-1], [6,3], [-1,3]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "must outset a concave polygon where the first outset edge intersects with the last outset edge" do
|
163
|
+
polygon = Polygon.new [0,0], [0,1], [2,1], [2,2], [-1,2], [-1,-1], [2,-1], [2,0]
|
164
|
+
polygon.edges.count.must_equal 8
|
165
|
+
polygon.outset(1).must_equal Polygon.new [3, 0], [3, 3], [-2, 3], [-2, -2], [3, -2]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Naturally, this test is very sensitive to the input coordinate values. This is a painfully contrived example that
|
169
|
+
# checks for sensitivity to edges that are very close to horizontal, but not quite.
|
170
|
+
# When the test fails, the first point of the offset polygon is at [0,-1]
|
171
|
+
it "must not be sensitive to floating point rounding errors" do
|
172
|
+
polygon = Polygon.new [0, 0], [0, -2], [10, -2], [10, 10], [-100, 10], [-100, -22], [-69, -22], [-69, 3.552713678800501e-15], [0,0]
|
173
|
+
outset = polygon.outset(1)
|
174
|
+
outset.edges.first.first.must_equal Geometry::Point[-1,-1]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "set operations" do
|
179
|
+
describe "union" do
|
180
|
+
it "must union two adjacent squares" do
|
181
|
+
polygonA = Polygon.new [0,0], [1,0], [1,1], [0,1]
|
182
|
+
polygonB = Polygon.new [1,0], [2,0], [2,1], [1,1]
|
183
|
+
(polygonA.union polygonB).must_equal Polygon.new [0,0], [2,0], [2,1], [0,1]
|
184
|
+
(polygonA + polygonB).must_equal Polygon.new [0,0], [2,0], [2,1], [0,1]
|
185
|
+
end
|
186
|
+
|
187
|
+
it "must union two overlapping squares" do
|
188
|
+
polygonA = Polygon.new [0,0], [2,0], [2,2], [0,2]
|
189
|
+
polygonB = Polygon.new [1,1], [3,1], [3,3], [1,3]
|
190
|
+
expected_polygon = Polygon.new [0,0], [2,0], [2,1], [3,1], [3,3], [1,3], [1,2], [0,2]
|
191
|
+
union = polygonA.union polygonB
|
192
|
+
union.must_be_kind_of Polygon
|
193
|
+
union.must_equal expected_polygon
|
194
|
+
end
|
195
|
+
|
196
|
+
it "must union two overlapping clockwise squares" do
|
197
|
+
polygonA = Polygon.new [0,0], [0,2], [2,2], [2,0]
|
198
|
+
polygonB = Polygon.new [1,1], [1,3], [3,3], [3,1]
|
199
|
+
expected_polygon = Polygon.new [0, 0], [0, 2], [1, 2], [1, 3], [3, 3], [3, 1], [2, 1], [2, 0]
|
200
|
+
union = polygonA.union polygonB
|
201
|
+
union.must_be_kind_of Polygon
|
202
|
+
union.must_equal expected_polygon
|
203
|
+
end
|
204
|
+
|
205
|
+
it "must union two overlapping squares with collinear edges" do
|
206
|
+
polygonA = Polygon.new [0,0], [2,0], [2,2], [0,2]
|
207
|
+
polygonB = Polygon.new [1,0], [3,0], [3,2], [1,2]
|
208
|
+
union = polygonA + polygonB
|
209
|
+
union.must_be_kind_of Polygon
|
210
|
+
union.must_equal Polygon.new [0,0], [3,0], [3,2], [0,2]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/polyline'
|
3
|
+
|
4
|
+
describe Geometry::Polyline do
|
5
|
+
Polyline = Geometry::Polyline
|
6
|
+
|
7
|
+
let(:closed_unit_square) { Polyline.new [0,0], [1,0], [1,1], [0,1], [0,0] }
|
8
|
+
let(:unit_square) { Polyline.new [0,0], [1,0], [1,1], [0,1] }
|
9
|
+
let(:reverse_unit_square) { Polyline.new [0,1], [1,1], [1,0], [0,0] }
|
10
|
+
|
11
|
+
it "must create a Polyline object with no arguments" do
|
12
|
+
polyline = Geometry::Polyline.new
|
13
|
+
polyline.must_be_kind_of Polyline
|
14
|
+
polyline.edges.count.must_equal 0
|
15
|
+
polyline.vertices.count.must_equal 0
|
16
|
+
end
|
17
|
+
|
18
|
+
it "must create a Polyline object from array arguments" do
|
19
|
+
polyline = Geometry::Polyline.new([0,0], [1,0], [1,1], [0,1])
|
20
|
+
polyline.must_be_kind_of Polyline
|
21
|
+
polyline.edges.count.must_equal 3
|
22
|
+
polyline.vertices.count.must_equal 4
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "when the Polyline is closed" do
|
26
|
+
let(:closed_concave_polyline) { Polyline.new [-2,0], [0,0], [0,-2], [2,-2], [2,2], [-2,2], [-2,0] }
|
27
|
+
subject { closed_concave_polyline }
|
28
|
+
|
29
|
+
it "must be closed" do
|
30
|
+
closed_unit_square.closed?.must_equal true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "must clone and close" do
|
34
|
+
closed = subject.close
|
35
|
+
closed.closed?.must_equal true
|
36
|
+
closed.must_equal subject
|
37
|
+
closed.wont_be_same_as subject
|
38
|
+
end
|
39
|
+
|
40
|
+
it "must be able to close itself" do
|
41
|
+
subject.close!
|
42
|
+
subject.closed?.must_equal true
|
43
|
+
subject.must_equal subject
|
44
|
+
end
|
45
|
+
|
46
|
+
it "must clone and reverse" do
|
47
|
+
vertices = subject.vertices
|
48
|
+
vertices.push vertices.shift
|
49
|
+
reversed = subject.reverse
|
50
|
+
reversed.vertices.must_equal vertices.reverse
|
51
|
+
reversed.wont_be_same_as subject
|
52
|
+
reversed.closed?.must_equal true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "must reverse itself" do
|
56
|
+
original = subject.vertices.dup
|
57
|
+
subject.reverse!
|
58
|
+
subject.vertices.to_a.must_equal original.reverse
|
59
|
+
subject.closed?.must_equal true
|
60
|
+
end
|
61
|
+
|
62
|
+
it "must generate bisectors" do
|
63
|
+
closed_unit_square.bisectors.must_equal [Vector[1, 1], Vector[-1, 1], Vector[-1, -1], Vector[1, -1]]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "must generate bisectors with an inside corner" do
|
67
|
+
closed_concave_polyline.bisectors.must_equal [Vector[1,1], Vector[-1,-1], Vector[1,1], Vector[-1,1], Vector[-1,-1], Vector[1,-1]]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "must generate left bisectors" do
|
71
|
+
closed_unit_square.left_bisectors.must_equal [Vector[1, 1], Vector[-1, 1], Vector[-1, -1], Vector[1, -1]]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "must generate left bisectors with an inside corner" do
|
75
|
+
closed_concave_polyline.left_bisectors.must_equal [Vector[1,1], Vector[1,1], Vector[1,1], Vector[-1,1], Vector[-1,-1], Vector[1,-1]]
|
76
|
+
end
|
77
|
+
|
78
|
+
it "must generate right bisectors" do
|
79
|
+
closed_unit_square.right_bisectors.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1]]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "must generate right bisectors with an inside corner" do
|
83
|
+
closed_concave_polyline.right_bisectors.must_equal [Vector[-1,-1], Vector[-1,-1], Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1]]
|
84
|
+
end
|
85
|
+
|
86
|
+
it "must generate spokes" do
|
87
|
+
closed_unit_square.spokes.must_equal [Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[-1,1]]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "must rightset a closed concave polyline where the first outset edge intersects with the last outset edge" do
|
91
|
+
skip
|
92
|
+
polyline = Polyline.new [0,0], [0,1], [2,1], [2,2], [-1,2], [-1,-1], [2,-1], [2,0], [0,0]
|
93
|
+
polyline.offset(-1).must_equal Polyline.new [1, 0], [3, 0], [3, 3], [-2, 3], [-2, -2], [3, -2]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "when the Polyline is open" do
|
98
|
+
let(:concave_polyline) { Polyline.new [-2,0], [0,0], [0,-2], [2,-2], [2,2], [-2,2] }
|
99
|
+
subject { concave_polyline }
|
100
|
+
|
101
|
+
it "must not be closed" do
|
102
|
+
unit_square.closed?.must_equal false
|
103
|
+
end
|
104
|
+
|
105
|
+
it "must clone and close" do
|
106
|
+
closed = subject.close
|
107
|
+
closed.closed?.must_equal true
|
108
|
+
closed.must_equal subject
|
109
|
+
closed.wont_be_same_as subject
|
110
|
+
end
|
111
|
+
|
112
|
+
it "must be able to close it" do
|
113
|
+
closed = subject.close!
|
114
|
+
closed.closed?.must_equal true
|
115
|
+
closed.must_equal subject
|
116
|
+
closed.must_be_same_as subject
|
117
|
+
end
|
118
|
+
|
119
|
+
it "must clone and reverse" do
|
120
|
+
reversed = subject.reverse
|
121
|
+
reversed.vertices.must_equal subject.vertices.reverse
|
122
|
+
reversed.wont_be_same_as subject
|
123
|
+
reversed.closed?.wont_equal true
|
124
|
+
end
|
125
|
+
|
126
|
+
it "must reverse itself" do
|
127
|
+
original = subject.vertices.dup
|
128
|
+
subject.reverse!
|
129
|
+
subject.vertices.to_a.must_equal original.reverse
|
130
|
+
subject.closed?.wont_equal true
|
131
|
+
end
|
132
|
+
|
133
|
+
it "must generate bisectors" do
|
134
|
+
unit_square.bisectors.must_equal [Vector[0, 1], Vector[-1, 1], Vector[-1, -1], Vector[0, -1]]
|
135
|
+
end
|
136
|
+
|
137
|
+
it "must generate bisectors with an inside corner" do
|
138
|
+
concave_polyline.bisectors.must_equal [Vector[0,1], Vector[-1,-1], Vector[1,1], Vector[-1,1], Vector[-1,-1], Vector[0,-1]]
|
139
|
+
end
|
140
|
+
|
141
|
+
it "must generate left bisectors" do
|
142
|
+
unit_square.left_bisectors.must_equal [Vector[0, 1], Vector[-1, 1], Vector[-1, -1], Vector[0, -1]]
|
143
|
+
reverse_unit_square.left_bisectors.must_equal [Vector[0, 1], Vector[1, 1], Vector[1, -1], Vector[0, -1]]
|
144
|
+
end
|
145
|
+
|
146
|
+
it "must generate right bisectors" do
|
147
|
+
unit_square.right_bisectors.must_equal [Vector[0,-1], Vector[1,-1], Vector[1,1], Vector[0,1]]
|
148
|
+
end
|
149
|
+
|
150
|
+
it "must generate right bisectors with an inside corner" do
|
151
|
+
concave_polyline.right_bisectors.must_equal [Vector[0,-1], Vector[-1,-1], Vector[-1,-1], Vector[1,-1], Vector[1,1], Vector[0,1]]
|
152
|
+
end
|
153
|
+
|
154
|
+
it "must generate left bisectors with an inside corner" do
|
155
|
+
concave_polyline.left_bisectors.must_equal [Vector[0,1], Vector[1,1], Vector[1,1], Vector[-1,1], Vector[-1,-1], Vector[0,-1]]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "must generate spokes" do
|
159
|
+
unit_square.spokes.must_equal [Vector[0,-1], Vector[1,-1], Vector[1,1], Vector[0,1]]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "when checking for closedness" do
|
164
|
+
it "must be closed when it is closed" do
|
165
|
+
closed_unit_square.closed?.must_equal true
|
166
|
+
end
|
167
|
+
|
168
|
+
it "must not be closed when it is not closed" do
|
169
|
+
unit_square.closed?.must_equal false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "when creating a Polyline from an array of Points" do
|
174
|
+
it "must ignore repeated Points" do
|
175
|
+
polyline = Geometry::Polyline.new([0,0], [1,0], [1,1], [1,1], [0,1])
|
176
|
+
polyline.must_be_kind_of Geometry::Polyline
|
177
|
+
polyline.must_equal Geometry::Polyline.new([0,0], [1,0], [1,1], [0,1])
|
178
|
+
end
|
179
|
+
|
180
|
+
it "must collapse collinear Edges" do
|
181
|
+
polyline = Geometry::Polyline.new([0,0], [1,0], [1,1], [0.5,1], [0,1])
|
182
|
+
polyline.must_equal Geometry::Polyline.new([0,0], [1,0], [1,1], [0,1])
|
183
|
+
end
|
184
|
+
|
185
|
+
it "must collapse backtracking Edges" do
|
186
|
+
polyline = Geometry::Polyline.new [0,0], [2,0], [2,2], [1,2], [1,1], [1,2], [0,2]
|
187
|
+
polyline.must_equal Geometry::Polyline.new([0,0], [2,0], [2,2], [0,2])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it "must compare identical polylines as equal" do
|
192
|
+
(unit_square.eql? unit_square).must_equal true
|
193
|
+
end
|
194
|
+
|
195
|
+
it "must rightset a closed concave polyline where the first outset edge intersects with the last outset edge" do
|
196
|
+
skip
|
197
|
+
polyline = Polyline.new [0,0], [0,1], [2,1], [2,2], [-1,2], [-1,-1], [2,-1], [2,0], [0,0]
|
198
|
+
polyline.offset(-1).must_equal Polyline.new [1, 0], [3, 0], [3, 3], [-2, 3], [-2, -2], [3, -2]
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "when offsetting" do
|
202
|
+
describe "with a positive offset" do
|
203
|
+
it "must leftset a unit square" do
|
204
|
+
expected_polyline = Polyline.new [0,0.1], [0.9,0.1], [0.9,0.9], [0,0.9]
|
205
|
+
unit_square.offset(0.1).must_equal expected_polyline
|
206
|
+
end
|
207
|
+
|
208
|
+
it "must leftset a simple concave polyline" do
|
209
|
+
concave_polyline = Polyline.new [0,0], [4,0], [4,4], [3,4], [3,3], [1,3], [1,4], [0,4]
|
210
|
+
offset_polyline = concave_polyline.offset(1)
|
211
|
+
offset_polyline.must_equal Polyline.new [0,1], [3,1], [3,2], [0,2], [0,3]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
describe "with a negative offset" do
|
216
|
+
it "must rightset a unit square" do
|
217
|
+
expected_polyline = Polyline.new [0,-1.0], [2.0,-1.0], [2.0,2.0], [0,2.0]
|
218
|
+
unit_square.offset(-1).must_equal expected_polyline
|
219
|
+
end
|
220
|
+
|
221
|
+
it "must rightset a simple concave polyline" do
|
222
|
+
concave_polyline = Polyline.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2]
|
223
|
+
offset_polyline = concave_polyline.offset(-1)
|
224
|
+
offset_polyline.must_equal Polyline.new [0,-1], [5,-1], [5,3], [0,3]
|
225
|
+
end
|
226
|
+
|
227
|
+
it "must rightset a concave polyline" do
|
228
|
+
concave_polyline = Polyline.new [0,0], [4,0], [4,2], [3,2], [3,1], [1,1], [1,2], [0,2]
|
229
|
+
offset_polyline = concave_polyline.offset(-2)
|
230
|
+
offset_polyline.must_equal Polyline.new [0,-2], [6,-2], [6,4], [0,4]
|
231
|
+
end
|
232
|
+
|
233
|
+
it "must rightset an asymetric concave polyline" do
|
234
|
+
concave_polyline = Polyline.new [0,0], [4,0], [4,3], [3,3], [3,1], [1,1], [1,2], [0,2]
|
235
|
+
offset_polyline = concave_polyline.offset(-2)
|
236
|
+
offset_polyline.must_equal Polyline.new [0,-2], [6,-2], [6,5], [1,5], [1,4], [0,4]
|
237
|
+
end
|
238
|
+
|
239
|
+
it "must rightset a concave polyline with multiply-intersecting edges" do
|
240
|
+
concave_polyline = Polyline.new [0,0], [5,0], [5,2], [4,2], [4,1], [3,1], [3,2], [2,2], [2,1], [1,1], [1,2], [0,2]
|
241
|
+
offset_polyline = concave_polyline.offset(-2)
|
242
|
+
offset_polyline.must_equal Polyline.new [0,-2], [7,-2], [7,4], [0,4]
|
243
|
+
end
|
244
|
+
|
245
|
+
it "must rightset a closed concave polyline with multiply-intersecting edges" do
|
246
|
+
concave_polyline = Polyline.new [0,0], [5,0], [5,2], [4,2], [4,1], [3,1], [3,2], [2,2], [2,1], [1,1], [1,2], [0,2], [0,0]
|
247
|
+
offset_polyline = concave_polyline.offset(-2)
|
248
|
+
offset_polyline.must_equal Polyline.new [-2,-2], [7,-2], [7,4], [-2,4]
|
249
|
+
end
|
250
|
+
|
251
|
+
it "must rightset a concave polyline where the first outset edge intersects with the last outset edge" do
|
252
|
+
polyline = Polyline.new [0,0], [0,1], [2,1], [2,2], [-1,2], [-1,-1], [2,-1], [2,0]
|
253
|
+
polyline.offset(-1).must_equal Polyline.new [1, 0], [3, 0], [3, 3], [-2, 3], [-2, -2], [3, -2]
|
254
|
+
end
|
255
|
+
|
256
|
+
# Naturally, this test is very sensitive to the input coordinate values. This is a painfully contrived example that
|
257
|
+
# checks for sensitivity to edges that are very close to horizontal, but not quite.
|
258
|
+
# When the test fails, the first point of the offset polyline is at [0,-1]
|
259
|
+
it "must not be sensitive to floating point rounding errors" do
|
260
|
+
polyline = Polyline.new [0, 0], [0, -2], [10, -2], [10, 10], [-100, 10], [-100, -22], [-69, -22], [-69, 3.552713678800501e-15], [0,0]
|
261
|
+
outset = polyline.offset(-1)
|
262
|
+
outset.edges.first.first.must_equal Geometry::Point[-1,-1]
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/rectangle'
|
3
|
+
|
4
|
+
def Rectangle(*args)
|
5
|
+
Geometry::Rectangle.new(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Geometry::Rectangle do
|
9
|
+
Rectangle = Geometry::Rectangle
|
10
|
+
|
11
|
+
describe "when initializing" do
|
12
|
+
it "must accept two corners as Arrays" do
|
13
|
+
rectangle = Rectangle.new [1,2], [2,3]
|
14
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
15
|
+
rectangle.height.must_equal 1
|
16
|
+
rectangle.width.must_equal 1
|
17
|
+
rectangle.origin.must_equal Point[1,2]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "must accept two named corners as Arrays" do
|
21
|
+
rectangle = Rectangle.new from:[1,2], to:[2,3]
|
22
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
23
|
+
rectangle.height.must_equal 1
|
24
|
+
rectangle.width.must_equal 1
|
25
|
+
rectangle.origin.must_equal Point[1,2]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "must accept named center point and size arguments" do
|
29
|
+
rectangle = Rectangle.new center:[1,2], size:[3,4]
|
30
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
31
|
+
rectangle.height.must_equal 4
|
32
|
+
rectangle.width.must_equal 3
|
33
|
+
rectangle.center.must_equal Point[1,2]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "must reject a named center argument with no size argument" do
|
37
|
+
-> { Rectangle.new center:[1,2] }.must_raise ArgumentError
|
38
|
+
end
|
39
|
+
|
40
|
+
it "must accept named origin point and size arguments" do
|
41
|
+
rectangle = Rectangle.new origin:[1,2], size:[3,4]
|
42
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
43
|
+
rectangle.height.must_equal 4
|
44
|
+
rectangle.width.must_equal 3
|
45
|
+
rectangle.origin.must_equal Point[1,2]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "must reject a named origin argument with no size argument" do
|
49
|
+
-> { Rectangle.new origin:[1,2] }.must_raise ArgumentError
|
50
|
+
end
|
51
|
+
|
52
|
+
it "must accept a sole named size argument that is an Array" do
|
53
|
+
rectangle = Rectangle.new size:[1,2]
|
54
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
55
|
+
rectangle.origin.must_equal Point[0,0]
|
56
|
+
rectangle.height.must_equal 2
|
57
|
+
rectangle.width.must_equal 1
|
58
|
+
end
|
59
|
+
|
60
|
+
it "must accept a sole named size argument that is a Size" do
|
61
|
+
rectangle = Rectangle.new size:Size[1,2]
|
62
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
63
|
+
rectangle.origin.must_equal Point[0,0]
|
64
|
+
rectangle.height.must_equal 2
|
65
|
+
rectangle.width.must_equal 1
|
66
|
+
end
|
67
|
+
|
68
|
+
it "must accept named width and height arguments" do
|
69
|
+
rectangle = Rectangle.new width:1, height:3
|
70
|
+
rectangle.must_be_kind_of Geometry::Rectangle
|
71
|
+
rectangle.height.must_equal 3
|
72
|
+
rectangle.width.must_equal 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it "must reject width or height by themselves" do
|
76
|
+
-> { Rectangle.new height:1 }.must_raise ArgumentError
|
77
|
+
-> { Rectangle.new width:1 }.must_raise ArgumentError
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "comparison" do
|
82
|
+
it "must compare equal" do
|
83
|
+
rectangle = Rectangle [1,2], [3,4]
|
84
|
+
rectangle.must_equal Rectangle([1,2], [3, 4])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "inset" do
|
89
|
+
subject { Rectangle.new [0,0], [10,10] }
|
90
|
+
|
91
|
+
it "must inset equally" do
|
92
|
+
subject.inset(1).must_equal Rectangle.new [1,1], [9,9]
|
93
|
+
end
|
94
|
+
|
95
|
+
it "must inset vertically and horizontally" do
|
96
|
+
subject.inset(1,2).must_equal Rectangle.new [1,2], [9,8]
|
97
|
+
subject.inset(x:1, y:2).must_equal Rectangle.new [1,2], [9,8]
|
98
|
+
end
|
99
|
+
|
100
|
+
it "must inset from individual sides" do
|
101
|
+
subject.inset(1,2,3,4).must_equal Rectangle.new [2,3], [6,9]
|
102
|
+
subject.inset(top:1, left:2, bottom:3, right:4).must_equal Rectangle.new [2,3], [6,9]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "properties" do
|
107
|
+
subject { Rectangle.new [1,2], [3,4] }
|
108
|
+
let(:rectangle) { Rectangle [1,2], [3,4] }
|
109
|
+
|
110
|
+
it "have a center point property" do
|
111
|
+
rectangle.center.must_equal Point[2,3]
|
112
|
+
end
|
113
|
+
|
114
|
+
it "have a width property" do
|
115
|
+
assert_equal(2, rectangle.width)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "have a height property" do
|
119
|
+
assert_equal(2, rectangle.height)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "have an origin property" do
|
123
|
+
assert_equal(Point[1,2], rectangle.origin)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "have an edges property that returns 4 edges" do
|
127
|
+
edges = rectangle.edges
|
128
|
+
assert_equal(4, edges.size)
|
129
|
+
edges.each {|edge| assert_kind_of(Geometry::Edge, edge)}
|
130
|
+
end
|
131
|
+
|
132
|
+
it "have a points property that returns 4 points" do
|
133
|
+
points = rectangle.points
|
134
|
+
assert_equal(4, points.size)
|
135
|
+
points.each {|point| assert_kind_of(Geometry::Point, point)}
|
136
|
+
end
|
137
|
+
|
138
|
+
it "must have a bounds property that returns a Rectangle" do
|
139
|
+
subject.bounds.must_equal Rectangle.new([1,2], [3,4])
|
140
|
+
end
|
141
|
+
|
142
|
+
it "must have a minmax property that returns the corners of the bounding rectangle" do
|
143
|
+
subject.minmax.must_equal [Point[1,2], Point[3,4]]
|
144
|
+
end
|
145
|
+
|
146
|
+
it "must have a max property that returns the upper right corner of the bounding rectangle" do
|
147
|
+
subject.max.must_equal Point[3,4]
|
148
|
+
end
|
149
|
+
|
150
|
+
it "must have a min property that returns the lower left corner of the bounding rectangle" do
|
151
|
+
subject.min.must_equal Point[1,2]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|