aurora-geometry 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/aurora-geometry.gemspec +23 -0
- data/lib/geometry.rb +22 -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.rb +171 -0
- data/lib/geometry/transformation/composition.rb +39 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/test/geometry.rb +5 -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.rb +169 -0
- data/test/geometry/transformation/composition.rb +49 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- metadata +115 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'geometry/arc'
|
2
|
+
require 'geometry/edge'
|
3
|
+
|
4
|
+
module Geometry
|
5
|
+
=begin
|
6
|
+
An object representing a set of connected elements, each of which could be an
|
7
|
+
{Edge} or an {Arc}. Unlike a {Polygon}, a {Path} is not guaranteed to be closed.
|
8
|
+
=end
|
9
|
+
class Path
|
10
|
+
attr_reader :elements
|
11
|
+
|
12
|
+
# Construct a new Path from {Point}s, {Edge}s, and {Arc}s
|
13
|
+
# Successive {Point}s will be converted to {Edge}s.
|
14
|
+
def initialize(*args)
|
15
|
+
args.map! {|a| (a.is_a?(Array) or a.is_a?(Vector)) ? Point[a] : a}
|
16
|
+
args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) or a.is_a?(Arc) }
|
17
|
+
|
18
|
+
@elements = []
|
19
|
+
|
20
|
+
first = args.shift
|
21
|
+
push first if first.is_a?(Edge) or first.is_a?(Arc)
|
22
|
+
|
23
|
+
args.reduce(first) do |previous, n|
|
24
|
+
case n
|
25
|
+
when Point
|
26
|
+
case previous
|
27
|
+
when Point then push Edge.new(previous, n)
|
28
|
+
when Arc, Edge then push Edge.new(previous.last, n) unless previous.last == n
|
29
|
+
end
|
30
|
+
last
|
31
|
+
when Edge
|
32
|
+
case previous
|
33
|
+
when Point then push Edge.new(previous, n.first)
|
34
|
+
when Arc, Edge then push Edge.new(previous.last, n.first) unless previous.last == n.first
|
35
|
+
end
|
36
|
+
push(n).last
|
37
|
+
when Arc
|
38
|
+
case previous
|
39
|
+
when Point
|
40
|
+
if previous == n.first
|
41
|
+
raise ArgumentError, "Duplicated point before an Arc"
|
42
|
+
else
|
43
|
+
push Edge.new(previous, n.first)
|
44
|
+
end
|
45
|
+
when Arc, Edge
|
46
|
+
push Edge.new(previous.last, n.first) unless previous.last == n.first
|
47
|
+
end
|
48
|
+
push(n).last
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Unsupported argument type: #{n}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Geometry] The last element in the {Path}
|
56
|
+
def last
|
57
|
+
@elements.last
|
58
|
+
end
|
59
|
+
|
60
|
+
# Append a new geometry element to the {Path}
|
61
|
+
# @return [Path]
|
62
|
+
def push(arg)
|
63
|
+
@elements.push arg
|
64
|
+
self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
require_relative 'point_zero'
|
4
|
+
|
5
|
+
module Geometry
|
6
|
+
DimensionMismatch = Class.new(StandardError)
|
7
|
+
OperationNotDefined = Class.new(StandardError)
|
8
|
+
|
9
|
+
=begin rdoc
|
10
|
+
An object repesenting a Point in N-dimensional space
|
11
|
+
|
12
|
+
Supports all of the familiar Vector methods and adds convenience
|
13
|
+
accessors for those variables you learned to hate in your high school
|
14
|
+
geometry class (x, y, z).
|
15
|
+
|
16
|
+
== Usage
|
17
|
+
|
18
|
+
=== Constructor
|
19
|
+
point = Geometry::Point[x,y]
|
20
|
+
=end
|
21
|
+
class Point < Vector
|
22
|
+
attr_reader :x, :y, :z
|
23
|
+
|
24
|
+
# Allow vector-style initialization, but override to support copy-init
|
25
|
+
# from Vector or another Point
|
26
|
+
#
|
27
|
+
# @overload [](x,y,z,...)
|
28
|
+
# @overload [](Array)
|
29
|
+
# @overload [](Point)
|
30
|
+
# @overload [](Vector)
|
31
|
+
def self.[](*array)
|
32
|
+
return array[0] if array[0].is_a?(Point) or array[0].is_a?(PointZero)
|
33
|
+
array = array[0] if array[0].is_a?(Array)
|
34
|
+
array = array[0].to_a if array[0].is_a?(Vector)
|
35
|
+
super *array
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given.
|
39
|
+
# @param size [Number] the size of the new {Point} full of zeros
|
40
|
+
# @return [PointZero] A new {PointZero} instance
|
41
|
+
def self.zero(size=nil)
|
42
|
+
size ? Point[Array.new(size, 0)] : PointZero.new
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return a copy of the {Point}
|
46
|
+
def clone
|
47
|
+
Point[@elements.clone]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Allow comparison with an Array, otherwise do the normal thing
|
51
|
+
def eql?(other)
|
52
|
+
if other.is_a?(Array)
|
53
|
+
@elements.eql? other
|
54
|
+
elsif other.is_a?(PointZero)
|
55
|
+
@elements.all? {|e| e.eql? 0 }
|
56
|
+
else
|
57
|
+
super other
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Allow comparison with an Array, otherwise do the normal thing
|
62
|
+
def ==(other)
|
63
|
+
if other.is_a?(Array)
|
64
|
+
@elements.eql? other
|
65
|
+
elsif other.is_a?(PointZero)
|
66
|
+
@elements.all? {|e| e.eql? 0 }
|
67
|
+
else
|
68
|
+
super other
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Combined comparison operator
|
73
|
+
# @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point
|
74
|
+
def <=>(other)
|
75
|
+
Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact]
|
76
|
+
end
|
77
|
+
|
78
|
+
def coerce(other)
|
79
|
+
case other
|
80
|
+
when Array then [Point[*other], self]
|
81
|
+
when Numeric then [Point[Array.new(self.size, other)], self]
|
82
|
+
when Vector then [Point[*other], self]
|
83
|
+
else
|
84
|
+
raise TypeError, "#{self.class} can't be coerced into #{other.class}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def inspect
|
89
|
+
'Point' + @elements.inspect
|
90
|
+
end
|
91
|
+
def to_s
|
92
|
+
'Point' + @elements.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# @group Accessors
|
96
|
+
# @param [Integer] i Index into the {Point}'s elements
|
97
|
+
# @return [Numeric] Element i (starting at 0)
|
98
|
+
def [](i)
|
99
|
+
@elements[i]
|
100
|
+
end
|
101
|
+
|
102
|
+
# @attribute [r] x
|
103
|
+
# @return [Numeric] X-component
|
104
|
+
def x
|
105
|
+
@elements[0]
|
106
|
+
end
|
107
|
+
|
108
|
+
# @attribute [r] y
|
109
|
+
# @return [Numeric] Y-component
|
110
|
+
def y
|
111
|
+
@elements[1]
|
112
|
+
end
|
113
|
+
|
114
|
+
# @attribute [r] z
|
115
|
+
# @return [Numeric] Z-component
|
116
|
+
def z
|
117
|
+
@elements[2]
|
118
|
+
end
|
119
|
+
# @endgroup
|
120
|
+
|
121
|
+
# @group Arithmetic
|
122
|
+
|
123
|
+
# @group Unary operators
|
124
|
+
def +@
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
def -@
|
129
|
+
Point[@elements.map {|e| -e }]
|
130
|
+
end
|
131
|
+
# @endgroup
|
132
|
+
|
133
|
+
def +(other)
|
134
|
+
case other
|
135
|
+
when Numeric
|
136
|
+
Point[@elements.map {|e| e + other}]
|
137
|
+
when PointZero, NilClass
|
138
|
+
self.dup
|
139
|
+
else
|
140
|
+
raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
|
141
|
+
raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size
|
142
|
+
Point[Array.new(size) {|i| @elements[i] + other[i] }]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def -(other)
|
147
|
+
case other
|
148
|
+
when Numeric
|
149
|
+
Point[@elements.map {|e| e - other}]
|
150
|
+
when PointZero, NilClass
|
151
|
+
self.dup
|
152
|
+
else
|
153
|
+
raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
|
154
|
+
raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size
|
155
|
+
Point[Array.new(size) {|i| @elements[i] - other[i] }]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @endgroup
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
@@ -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
|