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,107 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
|
3
|
+
module Geometry
|
4
|
+
=begin rdoc
|
5
|
+
An object repesenting a {Point} at the origin in N-dimensional space
|
6
|
+
|
7
|
+
A {PointZero} object is a {Point} that will always compare equal to zero and unequal to
|
8
|
+
everything else, regardless of size. You can think of it as an application of the
|
9
|
+
{http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}.
|
10
|
+
=end
|
11
|
+
class PointZero
|
12
|
+
def eql?(other)
|
13
|
+
if other.respond_to? :all?
|
14
|
+
other.all? {|e| e.eql? 0}
|
15
|
+
else
|
16
|
+
other == 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias == eql?
|
20
|
+
|
21
|
+
def coerce(other)
|
22
|
+
if other.is_a? Numeric
|
23
|
+
[other, 0]
|
24
|
+
elsif other.is_a? Array
|
25
|
+
[other, Array.new(other.size,0)]
|
26
|
+
elsif other.is_a? Vector
|
27
|
+
[other, Vector[*Array.new(other.size,0)]]
|
28
|
+
else
|
29
|
+
[Point[other], Point[Array.new(other.size,0)]]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
|
34
|
+
def to_ary
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
# @group Accessors
|
39
|
+
# @param [Integer] i Index into the {Point}'s elements
|
40
|
+
# @return [Numeric] Element i (starting at 0)
|
41
|
+
def [](i)
|
42
|
+
0
|
43
|
+
end
|
44
|
+
|
45
|
+
# @attribute [r] x
|
46
|
+
# @return [Numeric] X-component
|
47
|
+
def x
|
48
|
+
0
|
49
|
+
end
|
50
|
+
|
51
|
+
# @attribute [r] y
|
52
|
+
# @return [Numeric] Y-component
|
53
|
+
def y
|
54
|
+
0
|
55
|
+
end
|
56
|
+
|
57
|
+
# @attribute [r] z
|
58
|
+
# @return [Numeric] Z-component
|
59
|
+
def z
|
60
|
+
0
|
61
|
+
end
|
62
|
+
# @endgroup
|
63
|
+
|
64
|
+
# @group Arithmetic
|
65
|
+
|
66
|
+
# @group Unary operators
|
67
|
+
def +@
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def -@
|
72
|
+
self
|
73
|
+
end
|
74
|
+
# @endgroup
|
75
|
+
|
76
|
+
def +(other)
|
77
|
+
case other
|
78
|
+
when Array, Numeric then other
|
79
|
+
else
|
80
|
+
Point[other]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def -(other)
|
85
|
+
if other.is_a? Size
|
86
|
+
-Point[other]
|
87
|
+
elsif other.respond_to? :-@
|
88
|
+
-other
|
89
|
+
elsif other.respond_to? :map
|
90
|
+
other.map {|a| -a }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def *(other)
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def /(other)
|
99
|
+
raise OperationNotDefined unless other.is_a? Numeric
|
100
|
+
raise ZeroDivisionError if 0 == other
|
101
|
+
self
|
102
|
+
end
|
103
|
+
# @endgroup
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
@@ -0,0 +1,368 @@
|
|
1
|
+
require_relative 'edge'
|
2
|
+
require_relative 'polyline'
|
3
|
+
|
4
|
+
module Geometry
|
5
|
+
|
6
|
+
=begin rdoc
|
7
|
+
A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve.
|
8
|
+
|
9
|
+
{http://en.wikipedia.org/wiki/Polygon}
|
10
|
+
|
11
|
+
The {Polygon} class is generally intended to represent {http://en.wikipedia.org/wiki/Simple_polygon Simple polygons},
|
12
|
+
but there's currently nothing that enforces simplicity.
|
13
|
+
|
14
|
+
== Usage
|
15
|
+
|
16
|
+
=end
|
17
|
+
|
18
|
+
class Polygon < Polyline
|
19
|
+
|
20
|
+
# Construct a new Polygon from Points and/or Edges
|
21
|
+
# The constructor will try to convert all of its arguments into Points and
|
22
|
+
# Edges. Then successive Points will be collpased into Edges. Successive
|
23
|
+
# Edges that share a common vertex will be added to the new Polygon. If
|
24
|
+
# there's a gap between Edges it will be automatically filled with a new
|
25
|
+
# Edge. The resulting Polygon will then be closed if it isn't already.
|
26
|
+
# @overload initialize(Edge, Edge, ...)
|
27
|
+
# @return [Polygon]
|
28
|
+
# @overload initialize(Point, Point, ...)
|
29
|
+
# @return [Polygon]
|
30
|
+
def initialize(*args)
|
31
|
+
super
|
32
|
+
close! # A Polygon is always closed
|
33
|
+
end
|
34
|
+
|
35
|
+
# This method returns the receiver because a {Polygon} is always closed
|
36
|
+
# @return [Polygon] the receiver
|
37
|
+
def close
|
38
|
+
close!
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check the orientation of the {Polygon}
|
42
|
+
# @return [Boolean] True if the {Polygon} is clockwise, otherwise false
|
43
|
+
def clockwise?
|
44
|
+
edges.map {|e| (e.last.x - e.first.x) * (e.last.y + e.first.y)}.reduce(:+) >= 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
|
48
|
+
def reverse
|
49
|
+
self.class.new *(self.vertices.reverse)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Reverse the receiver and return it
|
53
|
+
# @return [Polygon] the reversed receiver
|
54
|
+
def reverse!
|
55
|
+
super
|
56
|
+
|
57
|
+
# Simply reversing the vertex array causes the reversed polygon to
|
58
|
+
# start at what had been the last vertex, instead of starting at
|
59
|
+
# the same vertex and just going the other direction.
|
60
|
+
vertices.unshift vertices.pop
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# @group Boolean operators
|
66
|
+
|
67
|
+
# Test a {Point} for inclusion in the receiver using a simplified winding number algorithm
|
68
|
+
# @param [Point] point The {Point} to test
|
69
|
+
# @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge}
|
70
|
+
def <=>(point)
|
71
|
+
sum = edges.reduce(0) do |sum, e|
|
72
|
+
direction = e.last.y <=> e.first.y
|
73
|
+
# Ignore edges that don't cross the point's x coordinate
|
74
|
+
next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
|
75
|
+
|
76
|
+
if 0 == direction # Special case horizontal edges
|
77
|
+
return 0 if ((point.x <=> e.last.x) + (point.x <=> e.first.x)).abs <= 1
|
78
|
+
next sum # Doesn't intersect
|
79
|
+
else
|
80
|
+
is_left = e <=> point
|
81
|
+
return 0 if 0 == is_left
|
82
|
+
next sum unless is_left
|
83
|
+
sum += 0 <=> (direction + is_left)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
(0 == sum) ? -1 : 1
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
|
90
|
+
# This is a simplified implementation of the alogrithm outlined in the
|
91
|
+
# paper {http://gvu.gatech.edu/people/official/jarek/graphics/papers/04PolygonBooleansMargalit.pdf An algorithm for computing the union, intersection or difference of two polygons}.
|
92
|
+
# In particular, this method assumes the receiver and passed {Polygon}s are "island" type and that the desired output is "regular", as those terms are described in the paper.
|
93
|
+
# @param [Polygon] other The {Polygon} to union with the receiver
|
94
|
+
# @return [Polygon] The union of the receiver and the passed {Polygon}
|
95
|
+
def union(other)
|
96
|
+
# Table 1: Both polygons are islands and the operation is union, so both must have the same orientation
|
97
|
+
# Reverse the other polygon if the orientations are different
|
98
|
+
other = other.reverse if self.clockwise? != other.clockwise?
|
99
|
+
|
100
|
+
# Receiver's vertex ring
|
101
|
+
ringA = VertexRing.new
|
102
|
+
self.vertices.each {|v| ringA.push v, (other <=> v)}
|
103
|
+
|
104
|
+
# The other vertex ring
|
105
|
+
ringB = VertexRing.new
|
106
|
+
other.vertices.each {|v| ringB.push v, (self <=> v)}
|
107
|
+
|
108
|
+
# Find intersections
|
109
|
+
offsetA = 0
|
110
|
+
edgesB = other.edges.dup
|
111
|
+
self.edges.each_with_index do |a, indexA|
|
112
|
+
offsetB = 0
|
113
|
+
ringB.edges_with_index do |b, indexB|
|
114
|
+
intersection = a.intersection(b)
|
115
|
+
if intersection === true
|
116
|
+
if (a.first == b.first) and (a.last == b.last) # Equal edges
|
117
|
+
elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges
|
118
|
+
else
|
119
|
+
if a.vector.normalize == b.vector.normalize # Same direction?
|
120
|
+
offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first)
|
121
|
+
offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last)
|
122
|
+
else # Opposite direction
|
123
|
+
offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.last)
|
124
|
+
offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.first)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
elsif intersection.is_a?(Point)
|
128
|
+
offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, intersection)
|
129
|
+
offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, intersection)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Table 2: Both polygons are islands and the operation is union, so select outside from both polygons
|
135
|
+
edgeFragments = []
|
136
|
+
[[ringA, other], [ringB, self]].each do |ring, other_polygon|
|
137
|
+
ring.edges do |v1,v2|
|
138
|
+
if (v1[:type] == -1) or (v2[:type] == -1)
|
139
|
+
edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
|
140
|
+
elsif (v1[:type] == 0) and (v2[:type] == 0)
|
141
|
+
if (other_polygon <=> Point[(v1[:vertex] + v2[:vertex])/2]) <= 0
|
142
|
+
edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Delete any duplicated edges. Array#uniq doesn't do the right thing, so using inject instead.
|
149
|
+
edgeFragments = edgeFragments.inject([]) {|result,h| result << h unless result.include?(h); result}
|
150
|
+
|
151
|
+
# Delete any equal-and-opposite edges
|
152
|
+
edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
|
153
|
+
|
154
|
+
# Construct the output polygons
|
155
|
+
output = edgeFragments.reduce([Array.new]) do |output, fragment|
|
156
|
+
next output if fragment.empty?
|
157
|
+
polygon = output.last
|
158
|
+
polygon.push fragment[:first], fragment[:last] if polygon.empty?
|
159
|
+
while 1 do
|
160
|
+
adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
|
161
|
+
break unless adjacent_fragment
|
162
|
+
|
163
|
+
polygon.push adjacent_fragment[:first], adjacent_fragment[:last]
|
164
|
+
fragment = adjacent_fragment.dup
|
165
|
+
adjacent_fragment.clear
|
166
|
+
|
167
|
+
break if polygon.first == polygon.last # closed?
|
168
|
+
end
|
169
|
+
output << Array.new
|
170
|
+
end
|
171
|
+
|
172
|
+
# If everything worked properly there should be only one output Polygon
|
173
|
+
output.reject! {|a| a.empty?}
|
174
|
+
output = Polygon.new *(output[0])
|
175
|
+
|
176
|
+
# Table 4: Both input polygons are "island" type and the operation
|
177
|
+
# is union, so the output polygon's orientation should be the same
|
178
|
+
# as the input polygon's orientation
|
179
|
+
(self.clockwise? != output.clockwise?) ? output.reverse : output
|
180
|
+
end
|
181
|
+
alias :+ :union
|
182
|
+
|
183
|
+
# @endgroup
|
184
|
+
|
185
|
+
# @group Convex Hull
|
186
|
+
|
187
|
+
# Returns the convex hull of the {Polygon}
|
188
|
+
# @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex
|
189
|
+
def convex
|
190
|
+
wrap
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm}
|
194
|
+
# This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March}
|
195
|
+
# @return [Polygon]
|
196
|
+
def wrap
|
197
|
+
# Start with a Point that's guaranteed to be on the hull
|
198
|
+
leftmost_point = vertices.min_by {|v| v.x}
|
199
|
+
current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y}
|
200
|
+
|
201
|
+
current_angle = 0.0
|
202
|
+
hull_points = [current_point]
|
203
|
+
while true
|
204
|
+
min_angle = 4.0
|
205
|
+
min_point = nil
|
206
|
+
vertices.each do |v1|
|
207
|
+
next if current_point.equal? v1
|
208
|
+
angle = pseudo_angle_for_edge(current_point, v1)
|
209
|
+
min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle)
|
210
|
+
end
|
211
|
+
current_angle = min_angle
|
212
|
+
current_point = min_point
|
213
|
+
break if current_point == hull_points.first
|
214
|
+
hull_points << min_point
|
215
|
+
end
|
216
|
+
Polygon.new *hull_points
|
217
|
+
end
|
218
|
+
|
219
|
+
# @endgroup
|
220
|
+
|
221
|
+
# Outset the receiver by the specified distance
|
222
|
+
# @param [Number] distance The distance to offset by
|
223
|
+
# @return [Polygon] A new {Polygon} outset by the given distance
|
224
|
+
def outset(distance)
|
225
|
+
bisector_edges = outset_bisectors(distance)
|
226
|
+
bisector_pairs = bisector_edges.push(bisector_edges.first).each_cons(2)
|
227
|
+
|
228
|
+
# Create the offset edges and then wrap them in Hashes so the edges
|
229
|
+
# can be altered while walking the array
|
230
|
+
active_edges = edges.zip(bisector_pairs).map do |e,offset|
|
231
|
+
offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
|
232
|
+
|
233
|
+
# Skip zero-length edges
|
234
|
+
{:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
|
235
|
+
end
|
236
|
+
|
237
|
+
# Walk the array and handle any intersections
|
238
|
+
active_edges.each_with_index do |e, i|
|
239
|
+
e1 = e[:edge]
|
240
|
+
next unless e1 # Ignore deleted edges
|
241
|
+
|
242
|
+
intersection, j = find_last_intersection(active_edges, i, e1)
|
243
|
+
if intersection
|
244
|
+
e2 = active_edges[j][:edge]
|
245
|
+
wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i))
|
246
|
+
|
247
|
+
if intersection.is_a? Point
|
248
|
+
if wrap_around_is_shortest
|
249
|
+
active_edges[i][:edge] = Edge.new(intersection, e1.last)
|
250
|
+
active_edges[j][:edge] = Edge.new(e2.first, intersection)
|
251
|
+
else
|
252
|
+
active_edges[i][:edge] = Edge.new(e1.first, intersection)
|
253
|
+
active_edges[j][:edge] = Edge.new(intersection, e2.last)
|
254
|
+
end
|
255
|
+
else
|
256
|
+
# Handle the collinear case
|
257
|
+
active_edges[i][:edge] = Edge.new(e1.first, e2.last)
|
258
|
+
active_edges[j].delete(:edge)
|
259
|
+
wrap_around_is_shortest = false
|
260
|
+
end
|
261
|
+
|
262
|
+
# Delete everything between e1 and e2
|
263
|
+
if wrap_around_is_shortest # Choose the shortest path
|
264
|
+
for k in 0...i do
|
265
|
+
active_edges[k].delete(:edge)
|
266
|
+
end
|
267
|
+
for k in j...active_edges.count do
|
268
|
+
next if k==j # Exclude e2
|
269
|
+
active_edges[k].delete(:edge)
|
270
|
+
end
|
271
|
+
else
|
272
|
+
for k in i...j do
|
273
|
+
next if k==i # Exclude e1 and e2
|
274
|
+
active_edges[k].delete(:edge)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
redo # Recheck the modified edges
|
279
|
+
end
|
280
|
+
end
|
281
|
+
Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Vertex bisectors suitable for outsetting
|
285
|
+
# @param [Number] length The distance to offset by
|
286
|
+
# @return [Array<Edge>] {Edge}s representing the bisectors
|
287
|
+
def outset_bisectors(length)
|
288
|
+
vertices.zip(spokes).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
|
289
|
+
end
|
290
|
+
|
291
|
+
# Generate the unit-length spokes for each vertex
|
292
|
+
# @return [Array<Vector>] the unit {Vector}s representing the spoke of each vertex
|
293
|
+
def spokes
|
294
|
+
clockwise? ? left_bisectors : right_bisectors
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
# Return a number that increases with the slope of the {Edge}
|
300
|
+
# @return [Number] A number in the range [0,4)
|
301
|
+
def pseudo_angle_for_edge(point0, point1)
|
302
|
+
delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f]
|
303
|
+
if delta.x >= 0
|
304
|
+
if delta.y >= 0
|
305
|
+
quadrant_one_psuedo_angle(delta.x, delta.y)
|
306
|
+
else
|
307
|
+
1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x)
|
308
|
+
end
|
309
|
+
else
|
310
|
+
if delta.y >= 0
|
311
|
+
3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs)
|
312
|
+
else
|
313
|
+
2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def quadrant_one_psuedo_angle(dx, dy)
|
319
|
+
dx / (dx + dy)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
private
|
324
|
+
|
325
|
+
class VertexRing
|
326
|
+
attr_reader :vertices
|
327
|
+
|
328
|
+
def initialize
|
329
|
+
@vertices = []
|
330
|
+
end
|
331
|
+
|
332
|
+
# @param [Integer] index The index to insert the new {Point} before
|
333
|
+
# @param [Point] point The {Point} to insert
|
334
|
+
# @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
|
335
|
+
def insert(index, point, type)
|
336
|
+
if v = @vertices.find {|v| v[:vertex] == point }
|
337
|
+
v[:type] = type
|
338
|
+
false
|
339
|
+
else
|
340
|
+
@vertices.insert(index, {:vertex => point, :type => type})
|
341
|
+
true
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Insert a boundary vertex
|
346
|
+
# @param [Integer] index The index to insert the new {Point} before
|
347
|
+
# @param [Point] point The {Point} to insert
|
348
|
+
def insert_boundary(index, point)
|
349
|
+
self.insert(index, point, 0)
|
350
|
+
end
|
351
|
+
|
352
|
+
# @param [Point] point The {Point} to push
|
353
|
+
# @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
|
354
|
+
def push(point, type)
|
355
|
+
@vertices << {:vertex => point, :type => type}
|
356
|
+
end
|
357
|
+
|
358
|
+
# Enumerate the pairs of vertices corresponding to each edge
|
359
|
+
def edges
|
360
|
+
(@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield v1, v2}
|
361
|
+
end
|
362
|
+
|
363
|
+
def edges_with_index
|
364
|
+
index = 0
|
365
|
+
(@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield(Edge.new(v1[:vertex], v2[:vertex]), index); index += 1}
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|