geometry 4 → 5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -3
- data/README.markdown +4 -2
- data/Rakefile +0 -8
- data/geometry.gemspec +1 -1
- data/lib/geometry.rb +3 -0
- data/lib/geometry/arc.rb +59 -0
- data/lib/geometry/circle.rb +72 -6
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +61 -0
- data/lib/geometry/path.rb +55 -0
- data/lib/geometry/point.rb +21 -10
- data/lib/geometry/point_zero.rb +4 -4
- data/lib/geometry/polygon.rb +145 -46
- data/lib/geometry/polyline.rb +212 -0
- data/lib/geometry/rectangle.rb +3 -12
- data/lib/geometry/rotation.rb +29 -2
- data/lib/geometry/size_zero.rb +4 -4
- data/lib/geometry/square.rb +7 -7
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +18 -2
- data/test/geometry/arc.rb +9 -0
- data/test/geometry/circle.rb +49 -13
- data/test/geometry/edge.rb +53 -0
- data/test/geometry/path.rb +67 -0
- data/test/geometry/point.rb +27 -2
- data/test/geometry/polygon.rb +101 -0
- data/test/geometry/polyline.rb +91 -0
- data/test/geometry/rotation.rb +5 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +6 -0
- metadata +12 -3
data/lib/geometry/point_zero.rb
CHANGED
@@ -29,9 +29,9 @@ everything else, regardless of size.
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
32
|
+
# @group Arithmetic
|
33
33
|
|
34
|
-
#
|
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
|
-
#
|
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
|
-
#
|
65
|
+
# @endgroup
|
66
66
|
|
67
67
|
end
|
68
68
|
end
|
data/lib/geometry/polygon.rb
CHANGED
@@ -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
|
-
|
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
|
23
|
+
# @overload initialize(Edge, Edge, ...)
|
25
24
|
# @return [Polygon]
|
26
|
-
# @overload
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@edges
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|