geometry 4 → 5

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.
@@ -29,9 +29,9 @@ everything else, regardless of size.
29
29
  end
30
30
  end
31
31
 
32
- # !@group Arithmetic
32
+ # @group Arithmetic
33
33
 
34
- # !@group Unary operators
34
+ # @group Unary operators
35
35
  def +@
36
36
  self
37
37
  end
@@ -39,7 +39,7 @@ everything else, regardless of size.
39
39
  def -@
40
40
  self
41
41
  end
42
- # !@endgroup
42
+ # @endgroup
43
43
 
44
44
  def +(other)
45
45
  other
@@ -62,7 +62,7 @@ everything else, regardless of size.
62
62
  raise ZeroDivisionError if 0 == other
63
63
  self
64
64
  end
65
- # !@endgroup
65
+ # @endgroup
66
66
 
67
67
  end
68
68
  end
@@ -1,9 +1,10 @@
1
1
  require_relative 'edge'
2
+ require_relative 'polyline'
2
3
 
3
4
  module Geometry
4
5
 
5
6
  =begin rdoc
6
- An object representing a closed set of vertices and edges.
7
+ A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve.
7
8
 
8
9
  {http://en.wikipedia.org/wiki/Polygon}
9
10
 
@@ -11,9 +12,7 @@ An object representing a closed set of vertices and edges.
11
12
 
12
13
  =end
13
14
 
14
- class Polygon
15
- attr_reader :edges, :vertices
16
- alias :points :vertices
15
+ class Polygon < Polyline
17
16
 
18
17
  # Construct a new Polygon from Points and/or Edges
19
18
  # The constructor will try to convert all of its arguments into Points and
@@ -21,63 +20,163 @@ An object representing a closed set of vertices and edges.
21
20
  # Edges that share a common vertex will be added to the new Polygon. If
22
21
  # there's a gap between Edges it will be automatically filled with a new
23
22
  # Edge. The resulting Polygon will then be closed if it isn't already.
24
- # @overload new(Array, Array, ...)
23
+ # @overload initialize(Edge, Edge, ...)
25
24
  # @return [Polygon]
26
- # @overload new(Edge, Edge, ...)
27
- # @return [Polygon]
28
- # @overload new(Point, Point, ...)
29
- # @return [Polygon]
30
- # @overload new(Vector, Vector, ...)
25
+ # @overload initialize(Point, Point, ...)
31
26
  # @return [Polygon]
32
27
  def initialize(*args)
33
- args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
34
- raise(ArgumentError,'Unknown argument type') unless args.all? {|a| a.is_a?(Point) || a.is_a?(Edge) }
35
-
36
- @edges = [];
37
- @vertices = [];
38
-
39
- first = args.shift
40
- if first.is_a?(Point)
41
- @vertices.push first
42
- elsif first.is_a?(Edge)
43
- @edges.push first
44
- @vertices.push *(first.to_a)
28
+ super
29
+
30
+ # Close the polygon if needed
31
+ @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
32
+ end
33
+
34
+ # @group Convex Hull
35
+
36
+ # Returns the convex hull of the {Polygon}
37
+ # @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex
38
+ def convex
39
+ wrap
40
+ end
41
+
42
+ # Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm}
43
+ # This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March}
44
+ # @return [Polygon]
45
+ def wrap
46
+ # Start with a Point that's guaranteed to be on the hull
47
+ leftmost_point = vertices.min_by {|v| v.x}
48
+ current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y}
49
+
50
+ current_angle = 0.0
51
+ hull_points = [current_point]
52
+ while true
53
+ min_angle = 4.0
54
+ min_point = nil
55
+ vertices.each do |v1|
56
+ next if current_point.equal? v1
57
+ angle = pseudo_angle_for_edge(current_point, v1)
58
+ min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle)
59
+ end
60
+ current_angle = min_angle
61
+ current_point = min_point
62
+ break if current_point == hull_points.first
63
+ hull_points << min_point
64
+ end
65
+ Polygon.new *hull_points
66
+ end
67
+
68
+ # @endgroup
69
+
70
+ # Outset the receiver by the specified distance
71
+ # @param [Number] distance The distance to offset by
72
+ # @return [Polygon] A new {Polygon} outset by the given distance
73
+ def outset(distance)
74
+ bisectors = offset_bisectors(distance)
75
+ offsets = (bisectors.each_cons(2).to_a << [bisectors.last, bisectors.first])
76
+
77
+ # Create the offset edges and then wrap them in Hashes so the edges
78
+ # can be altered while walking the array
79
+ active_edges = edges.zip(offsets).map do |e,offset|
80
+ offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
81
+
82
+ # Skip zero-length edges
83
+ {:edge => (offset.first == offset.last) ? nil : offset}
45
84
  end
46
85
 
47
- args.reduce(@vertices.last) do |previous,n|
48
- if n.is_a?(Point)
49
- push_edge Edge.new(previous, n)
50
- push_vertex n
51
- n
52
- elsif n.is_a?(Edge)
53
- if previous == n.first
54
- push_edge n
55
- push_vertex n.last
56
- elsif previous == n.last
57
- push_edge n.reverse!
58
- push_vertex n.last
86
+ # Walk the array and handle any intersections
87
+ active_edges.each_with_index do |e, i|
88
+ e1 = e[:edge]
89
+ next unless e1 # Ignore deleted edges
90
+
91
+ intersection, j = find_last_intersection(active_edges, i, e1)
92
+ if intersection
93
+ e2 = active_edges[j][:edge]
94
+ wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i))
95
+
96
+ if intersection.is_a? Point
97
+ if wrap_around_is_shortest
98
+ active_edges[i][:edge] = Edge.new(intersection, e1.last)
99
+ active_edges[j][:edge] = Edge.new(e2.first, intersection)
100
+ else
101
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
102
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
103
+ end
59
104
  else
60
- e = Edge.new(previous, n.first)
61
- push_edge e, n
62
- push_vertex *(e.to_a), *(n.to_a)
105
+ # Handle the collinear case
106
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
107
+ active_edges[j].delete(:edge)
63
108
  end
64
- n.last
109
+
110
+ # Delete everything between e1 and e2
111
+ if wrap_around_is_shortest # Choose the shortest path
112
+ for k in 0...i do
113
+ active_edges[k].delete(:edge)
114
+ end
115
+ for k in j...active_edges.count do
116
+ next if k==j # Exclude e2
117
+ active_edges[k].delete(:edge)
118
+ end
119
+ else
120
+ for k in i...j do
121
+ next if k==i # Exclude e1 and e2
122
+ active_edges[k].delete(:edge)
123
+ end
124
+ end
125
+
126
+ redo # Recheck the modified edges
65
127
  end
66
128
  end
129
+ Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
130
+ end
67
131
 
68
- # Close the polygon if needed
69
- @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
132
+ # Vertex bisectors suitable for offsetting
133
+ # @param [Number] length The distance to offset by
134
+ # @return [Array<Edge>] {Edge}s representing the bisectors
135
+ def offset_bisectors(length)
136
+ vectors = edges.map {|e| e.direction }
137
+ winding = 0
138
+ sums = vectors.unshift(vectors.last).each_cons(2).map do |v1,v2|
139
+ k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
140
+ winding += k
141
+ if v1 == v2 # collinear, same direction?
142
+ Vector[-v1[1], v1[0]]
143
+ elsif 0 == k # collinear, reverse direction
144
+ nil
145
+ else
146
+ by = (v2[1] - v1[1])/k
147
+ v = (0 == v1[1]) ? v2 : v1
148
+ Vector[(v[0]*by - 1)/v[1], by]
149
+ end
150
+ end
151
+
152
+ # Check the polygon's orientation. If clockwise, negate length as a hack for injecting a -1 into the final result
153
+ length = -length if winding >= 0
154
+ vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
70
155
  end
71
156
 
72
157
  private
73
158
 
74
- def push_edge(*e)
75
- @edges.push *e
76
- @edges.uniq!
159
+ # Return a number that increases with the slope of the {Edge}
160
+ # @return [Number] A number in the range [0,4)
161
+ def pseudo_angle_for_edge(point0, point1)
162
+ delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f]
163
+ if delta.x >= 0
164
+ if delta.y >= 0
165
+ quadrant_one_psuedo_angle(delta.x, delta.y)
166
+ else
167
+ 1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x)
168
+ end
169
+ else
170
+ if delta.y >= 0
171
+ 3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs)
172
+ else
173
+ 2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs)
174
+ end
175
+ end
77
176
  end
78
- def push_vertex(*v)
79
- @vertices.push *v
80
- @vertices.uniq!
177
+
178
+ def quadrant_one_psuedo_angle(dx, dy)
179
+ dx / (dx + dy)
81
180
  end
82
181
  end
83
182
  end
@@ -0,0 +1,212 @@
1
+ require_relative 'edge'
2
+
3
+ module Geometry
4
+
5
+ =begin rdoc
6
+ A {Polyline} is like a {Polygon} in that it only contains straight lines, but
7
+ also like a {Path} in that it isn't necessarily closed.
8
+
9
+ {http://en.wikipedia.org/wiki/Polyline}
10
+
11
+ == Usage
12
+
13
+ =end
14
+
15
+ class Polyline
16
+ attr_reader :edges, :vertices
17
+
18
+ # Construct a new Polyline from Points and/or Edges
19
+ # The constructor will try to convert all of its arguments into {Point}s and
20
+ # {Edge}s. Then successive {Point}s will be collpased into {Edge}s. Successive
21
+ # {Edge}s that share a common vertex will be added to the new {Polyline}. If
22
+ # there's a gap between {Edge}s it will be automatically filled with a new
23
+ # {Edge}.
24
+ # @overload initialize(Edge, Edge, ...)
25
+ # @return [Polyline]
26
+ # @overload initialize(Point, Point, ...)
27
+ # @return [Polyline]
28
+ def initialize(*args)
29
+ args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
30
+ args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) }
31
+
32
+ @edges = [];
33
+ @vertices = [];
34
+
35
+ first = args.shift
36
+ if first.is_a?(Point)
37
+ @vertices.push first
38
+ elsif first.is_a?(Edge)
39
+ @edges.push first
40
+ @vertices.push *(first.to_a)
41
+ end
42
+
43
+ args.reduce(@vertices.last) do |previous,n|
44
+ if n.is_a?(Point)
45
+ if n == previous # Ignore repeated Points
46
+ previous
47
+ else
48
+ if @edges.last
49
+ new_edge = Edge.new(previous, n)
50
+ if @edges.last.parallel?(new_edge)
51
+ @edges.pop # Remove the previous Edge
52
+ @vertices.pop(@edges.size ? 1 : 2) # Remove the now unused vertex, or vertices
53
+ if n == @edges.last.last
54
+ @edges.last.last
55
+ else
56
+ push_edge Edge.new(@edges.last.last, n)
57
+ push_vertex @edges.last.first
58
+ push_vertex n
59
+ n
60
+ end
61
+ else
62
+ push_edge Edge.new(previous, n)
63
+ push_vertex n
64
+ n
65
+ end
66
+ else
67
+ push_edge Edge.new(previous, n)
68
+ push_vertex n
69
+ n
70
+ end
71
+ end
72
+ elsif n.is_a?(Edge)
73
+ if previous == n.first
74
+ push_edge n
75
+ push_vertex n.last
76
+ elsif previous == n.last
77
+ push_edge n.reverse!
78
+ push_vertex n.last
79
+ else
80
+ e = Edge.new(previous, n.first)
81
+ push_edge e, n
82
+ push_vertex *(e.to_a), *(n.to_a)
83
+ end
84
+ n.last
85
+ end
86
+ end
87
+ end
88
+
89
+ # Check the equality of two {Polyline}s. Note that if two {Polyline}s have
90
+ # opposite winding, but are otherwise identical, they will be considered unequal.
91
+ # @return [Bool] true if both {Polyline}s have equal edges
92
+ def eql?(other)
93
+ @vertices.zip(other.vertices).all? {|a,b| a == b}
94
+ end
95
+ alias :== :eql?
96
+
97
+ # Offset the receiver by the specified distance. A positive distance
98
+ # will offset to the left, and a negative distance to the right.
99
+ # @param [Number] distance The distance to offset by
100
+ # @return [Polygon] A new {Polygon} outset by the given distance
101
+ def offset(distance)
102
+ bisectors = offset_bisectors(distance)
103
+ offsets = bisectors.each_cons(2).to_a
104
+
105
+ # Create the offset edges and then wrap them in Hashes so the edges
106
+ # can be altered while walking the array
107
+ active_edges = edges.zip(offsets).map do |e,offset|
108
+ offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
109
+
110
+ # Skip zero-length edges
111
+ {:edge => (offset.first == offset.last) ? nil : offset}
112
+ end
113
+
114
+ # Walk the array and handle any intersections
115
+ for i in 0..(active_edges.count-1) do
116
+ e1 = active_edges[i][:edge]
117
+ next unless e1 # Ignore deleted edges
118
+
119
+ intersection, j = find_last_intersection(active_edges, i, e1)
120
+ if intersection
121
+ e2 = active_edges[j][:edge]
122
+ if intersection.is_a? Point
123
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
124
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
125
+ else
126
+ # Handle the collinear case
127
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
128
+ active_edges[j].delete(:edge)
129
+ end
130
+
131
+ # Delete everything between e1 and e2
132
+ for k in i..j do
133
+ next if (k==i) or (k==j) # Exclude e1 and e2
134
+ active_edges[k].delete(:edge)
135
+ end
136
+
137
+ redo # Recheck the modified edges
138
+ end
139
+ end
140
+ Polyline.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
141
+ end
142
+ alias :leftset :offset
143
+
144
+ # Rightset the receiver by the specified distance
145
+ # @param [Number] distance The distance to offset by
146
+ # @return [Polygon] A new {Polygon} rightset by the given distance
147
+ def rightset(distance)
148
+ offset(-distance)
149
+ end
150
+
151
+ private
152
+
153
+ # @group Helpers for offset()
154
+
155
+ # Vertex bisectors suitable for offsetting
156
+ # @param [Number] length The distance to offset by
157
+ # @return [Array<Edge>] {Edge}s representing the bisectors
158
+ def offset_bisectors(length)
159
+ vectors = edges.map {|e| e.direction }
160
+ winding = 0
161
+ sums = vectors.unshift(vectors.first).push(vectors.last).each_cons(2).map do |v1,v2|
162
+ k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
163
+ winding += k
164
+ if v1 == v2 # collinear, same direction?
165
+ Vector[-v1[1], v1[0]]
166
+ elsif 0 == k # collinear, reverse direction
167
+ nil
168
+ else
169
+ by = (v2[1] - v1[1])/k
170
+ v = (0 == v1[1]) ? v2 : v1
171
+ Vector[(v[0]*by - 1)/v[1], by]
172
+ end
173
+ end
174
+
175
+ vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
176
+ end
177
+
178
+ # Find the next edge that intersects with e, starting at index i
179
+ def find_next_intersection(edges, i, e)
180
+ for j in i..(edges.count-1)
181
+ e2 = edges[j][:edge]
182
+ next if !e2 || e.connected?(e2)
183
+ intersection = e.intersection(e2)
184
+ return [intersection, j] if intersection
185
+ end
186
+ nil
187
+ end
188
+
189
+ # Find the last edge that intersects with e, starting at index i
190
+ def find_last_intersection(edges, i, e)
191
+ intersection, intersection_at = nil, nil
192
+ for j in i..(edges.count-1)
193
+ e2 = edges[j][:edge]
194
+ next if !e2 || e.connected?(e2)
195
+ _intersection = e.intersection(e2)
196
+ intersection, intersection_at = _intersection, j if _intersection
197
+ end
198
+ [intersection, intersection_at]
199
+ end
200
+ # @endgroup
201
+
202
+ def push_edge(*e)
203
+ @edges.push *e
204
+ @edges.uniq!
205
+ end
206
+
207
+ def push_vertex(*v)
208
+ @vertices.push *v
209
+ @vertices.uniq!
210
+ end
211
+ end
212
+ end