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/.gitignore
CHANGED
data/README.markdown
CHANGED
@@ -23,9 +23,11 @@ Primitives
|
|
23
23
|
- Line
|
24
24
|
- Edge
|
25
25
|
- Circle
|
26
|
-
- Rectangle
|
27
|
-
- Polygon
|
26
|
+
- Rectangle, Square
|
27
|
+
- Path, [Polyline](http://en.wikipedia.org/wiki/Polyline), [Polygon](http://en.wikipedia.org/wiki/Polygon)
|
28
28
|
- Transformation
|
29
|
+
- [Triangle](http://en.wikipedia.org/wiki/Triangle)
|
30
|
+
- [Obround](http://en.wiktionary.org/wiki/obround)
|
29
31
|
|
30
32
|
Examples
|
31
33
|
--------
|
data/Rakefile
CHANGED
@@ -20,11 +20,3 @@ end
|
|
20
20
|
task :trim_whitespace do
|
21
21
|
system(%Q[git status --short | awk '{if ($1 != "D" && $1 != "R") print $2}' | grep -e '.*\.rb$' | xargs sed -i '' -e 's/[ \t]*$//g;'])
|
22
22
|
end
|
23
|
-
|
24
|
-
task :docs do
|
25
|
-
`yard`
|
26
|
-
end
|
27
|
-
|
28
|
-
task :uninstall do
|
29
|
-
`gem uninstall geometry`
|
30
|
-
end
|
data/geometry.gemspec
CHANGED
data/lib/geometry.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
require_relative 'geometry/arc'
|
2
2
|
require_relative 'geometry/circle'
|
3
3
|
require_relative 'geometry/line'
|
4
|
+
require_relative 'geometry/path'
|
4
5
|
require_relative 'geometry/point'
|
5
6
|
require_relative 'geometry/point_zero'
|
6
7
|
require_relative 'geometry/polygon'
|
8
|
+
require_relative 'geometry/polyline'
|
7
9
|
require_relative 'geometry/rectangle'
|
8
10
|
require_relative 'geometry/rotation'
|
9
11
|
require_relative 'geometry/size'
|
10
12
|
require_relative 'geometry/size_zero'
|
11
13
|
require_relative 'geometry/square'
|
12
14
|
require_relative 'geometry/transformation'
|
15
|
+
require_relative 'geometry/triangle'
|
13
16
|
require_relative 'geometry/vector'
|
14
17
|
|
15
18
|
module Geometry
|
data/lib/geometry/arc.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require_relative 'point'
|
2
2
|
|
3
|
+
require_relative 'cluster_factory'
|
4
|
+
require_relative 'point'
|
5
|
+
|
3
6
|
module Geometry
|
4
7
|
|
5
8
|
=begin rdoc
|
@@ -11,10 +14,31 @@ An {Arc} with its center at [1,1] and a radius of 2 that starts at the X-axis an
|
|
11
14
|
=end
|
12
15
|
|
13
16
|
class Arc
|
17
|
+
include ClusterFactory
|
18
|
+
|
14
19
|
attr_reader :center
|
15
20
|
attr_reader :radius
|
16
21
|
attr_reader :start_angle, :end_angle
|
17
22
|
|
23
|
+
# @overload new(center_point, start_point, end_point)
|
24
|
+
# Create a new {Arc} given center, start and end {Point}s
|
25
|
+
# @param [Point] center_point The {Point} at the center
|
26
|
+
# @param [Point] start_point The {Arc} starts at the start {Point}
|
27
|
+
# @param [Point] end_point The {Point} where it all ends
|
28
|
+
# @overload initialize(center, radius, start_angle, end_angle)
|
29
|
+
# Create a new {Arc} given a center, a radius and start and end angles
|
30
|
+
# @param [Point] center The {Point} at the center of it all
|
31
|
+
# @param [Numeric] radius Radius
|
32
|
+
# @param [Numeric] start_angle Starting angle
|
33
|
+
# @param [Numeric] end_angle Ending angle
|
34
|
+
def self.new(*args)
|
35
|
+
if 4 == args.size
|
36
|
+
original_new(*args)
|
37
|
+
elsif 3 == args.size
|
38
|
+
ThreePointArc.new(*args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
18
42
|
# Construct a new {Arc}
|
19
43
|
# @overload initialize(center, radius, start_angle, end_angle)
|
20
44
|
# @param [Point] center The {Point} at the center of it all
|
@@ -27,5 +51,40 @@ An {Arc} with its center at [1,1] and a radius of 2 that starts at the X-axis an
|
|
27
51
|
@start_angle = start_angle
|
28
52
|
@end_angle = end_angle
|
29
53
|
end
|
54
|
+
|
55
|
+
# @return [Point] The starting point of the {Arc}
|
56
|
+
def first
|
57
|
+
@center + @radius * Vector[Math.cos(@start_angle), Math.sin(@start_angle)]
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Point] The end point of the {Arc}
|
61
|
+
def last
|
62
|
+
@center + @radius * Vector[Math.cos(@end_angle), Math.sin(@end_angle)]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ThreePointArc < Arc
|
67
|
+
attr_reader :center
|
68
|
+
attr_reader :start, :end
|
69
|
+
|
70
|
+
# Contruct a new {Arc} given center, start and end {Point}s
|
71
|
+
# Always assumes that the {Arc} is counter-clockwise. Reverse the order
|
72
|
+
# of the start and end points to get an {Arc} that goes around the other way.
|
73
|
+
# @overload initialize(center_point, start_point, end_point)
|
74
|
+
# @param [Point] center_point The {Point} at the center
|
75
|
+
# @param [Point] start_point The {Arc} starts at the start {Point}
|
76
|
+
# @param [Point] end_point The {Point} where it all ends
|
77
|
+
def initialize(center_point, start_point, end_point)
|
78
|
+
@center, @start, @end = [center_point, start_point, end_point].map {|p| Point[p]}
|
79
|
+
raise ArgumentError unless [@center, @start, @end].all? {|p| p.is_a?(Point)}
|
80
|
+
end
|
81
|
+
|
82
|
+
# The starting point of the {Arc}
|
83
|
+
# @return [Point]
|
84
|
+
alias :first :start
|
85
|
+
|
86
|
+
# The end point of the {Arc}
|
87
|
+
# @return [Point]
|
88
|
+
alias :last :end
|
30
89
|
end
|
31
90
|
end
|
data/lib/geometry/circle.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require_relative 'cluster_factory'
|
1
2
|
require_relative 'point'
|
2
3
|
|
3
4
|
module Geometry
|
@@ -6,23 +7,88 @@ module Geometry
|
|
6
7
|
Circles come in all shapes and sizes, but they're usually round.
|
7
8
|
|
8
9
|
== Usage
|
9
|
-
circle = Geometry::Circle.new [1,
|
10
|
+
circle = Geometry::Circle.new [1,2], 3
|
11
|
+
circle = Geometry::Circle.new [1,2], :radius => 3
|
12
|
+
circle = Geometry::Circle.new [1,2], :diameter => 6
|
13
|
+
circle = Geometry::Circle.new :center => [1,2], :diameter => 6
|
10
14
|
=end
|
11
15
|
|
12
16
|
class Circle
|
13
|
-
|
17
|
+
include ClusterFactory
|
18
|
+
|
19
|
+
# @return [Point] The {Circle}'s center point
|
14
20
|
attr_reader :center
|
15
21
|
|
16
|
-
# @return [Number] The Circle's radius
|
22
|
+
# @return [Number] The {Circle}'s radius
|
17
23
|
attr_reader :radius
|
18
24
|
|
25
|
+
# @overload new(circle, radius)
|
26
|
+
# Construct a {Circle} using a centerpoint and radius
|
27
|
+
# @param [Point] center The center point of the {Circle}
|
28
|
+
# @param [Number] radius The radius of the {Circle}
|
29
|
+
# @overload new(options)
|
30
|
+
# Construct a circle using named center and radius parameters
|
31
|
+
# @option options [Point] :center
|
32
|
+
# @option options [Number] :radius
|
33
|
+
# @overload new(options)
|
34
|
+
# Construct a circle using named center and diameter parameters
|
35
|
+
# @option options [Point] :center
|
36
|
+
# @option options [Number] :diameter
|
37
|
+
def self.new(*args, &block)
|
38
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
39
|
+
options = options.reduce({}, :merge)
|
40
|
+
center, radius = args[0..1]
|
41
|
+
|
42
|
+
center = Point[center || options[:center]]
|
43
|
+
raise ArgumentError, "Circle.new requires a center" unless center
|
44
|
+
|
45
|
+
radius ||= options[:radius]
|
46
|
+
|
47
|
+
if radius
|
48
|
+
self.allocate.tap {|circle| circle.send :initialize, center, radius, &block }
|
49
|
+
elsif options.has_key?(:diameter)
|
50
|
+
CenterDiameterCircle.new center, options[:diameter], &block
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Circle.new requires a radius or a diameter"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
19
56
|
# Construct a new {Circle} from a centerpoint and radius
|
20
|
-
# @param [Point] center The center point of the Circle
|
21
|
-
# @param [Number] radius The radius of the Circle
|
22
|
-
# @return [Circle] A new Circle object
|
57
|
+
# @param [Point] center The center point of the {Circle}
|
58
|
+
# @param [Number] radius The radius of the {Circle}
|
59
|
+
# @return [Circle] A new {Circle} object
|
23
60
|
def initialize(center, radius)
|
24
61
|
@center = Point[center]
|
25
62
|
@radius = radius
|
26
63
|
end
|
64
|
+
|
65
|
+
# @!group Accessors
|
66
|
+
# @!attribute [r] diameter
|
67
|
+
# @return [Numeric] The diameter of the {Circle}
|
68
|
+
def diameter
|
69
|
+
@radius*2
|
70
|
+
end
|
71
|
+
# @!endgroup
|
72
|
+
end
|
73
|
+
|
74
|
+
class CenterDiameterCircle < Circle
|
75
|
+
# @return [Number] The {Circle}'s diameter
|
76
|
+
attr_reader :diameter
|
77
|
+
|
78
|
+
# Construct a new {Circle} from a centerpoint and a diameter
|
79
|
+
# @param [Point] center The center point of the {Circle}
|
80
|
+
# @param [Number] diameter The radius of the {Circle}
|
81
|
+
# @return [Circle] A new {Circle} object
|
82
|
+
def initialize(center, diameter)
|
83
|
+
@center = Point[center]
|
84
|
+
@diameter = diameter
|
85
|
+
end
|
86
|
+
|
87
|
+
# @!group Accessors
|
88
|
+
# @return The {Circle}'s radius
|
89
|
+
def radius
|
90
|
+
@diameter/2
|
91
|
+
end
|
92
|
+
# @!endgroup
|
27
93
|
end
|
28
94
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Include this module in the base class of a class cluster to handle swizzling
|
2
|
+
# of ::new
|
3
|
+
module ClusterFactory
|
4
|
+
def self.included(parent)
|
5
|
+
class << parent
|
6
|
+
alias :original_new :new
|
7
|
+
|
8
|
+
def inherited(subclass)
|
9
|
+
class << subclass
|
10
|
+
alias :new :original_new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/geometry/edge.rb
CHANGED
@@ -50,6 +50,67 @@ An edge. It's a line segment between 2 points. Generally part of a {Polygon}.
|
|
50
50
|
end
|
51
51
|
alias :to_s :inspect
|
52
52
|
|
53
|
+
# @return [Bool] Returns true if the passed {Edge} is parallel to the receiver
|
54
|
+
def parallel?(edge)
|
55
|
+
v1, v2 = self.direction, edge.direction
|
56
|
+
winding = v1[0]*v2[1] - v1[1]*v2[0]
|
57
|
+
if 0 == winding # collinear?
|
58
|
+
if v1 == v2
|
59
|
+
1 # same direction
|
60
|
+
else
|
61
|
+
-1 # opposite direction
|
62
|
+
end
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [Edge] other The other {Edge} to check
|
69
|
+
# @return [Bool] Returns true if the receiver and the passed {Edge} share an endpoint
|
70
|
+
def connected?(other)
|
71
|
+
(@first == other.last) || (@last == other.first) || (@first == other.first) || (@last == other.last)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Vector] A unit {Vector} pointing from first to last
|
75
|
+
def direction
|
76
|
+
self.vector.normalize
|
77
|
+
end
|
78
|
+
|
79
|
+
# Find the intersection of two {Edge}s (http://bloggingmath.wordpress.com/2009/05/29/line-segment-intersection/)
|
80
|
+
# @return [Point] The intersection of the two {Edge}s, nil if they don't intersect, true if they're collinear and overlapping, and false if they're collinear and non-overlapping
|
81
|
+
def intersection(other)
|
82
|
+
p0, p1 = self.first, self.last
|
83
|
+
p2, p3 = other.first, other.last
|
84
|
+
v1, v2 = self.vector, other.vector
|
85
|
+
|
86
|
+
denominator = v1[0] * v2[1] - v2[0] * v1[1] # v1 x v2
|
87
|
+
p = p0 - p2
|
88
|
+
if denominator == 0 # collinear, so check for overlap
|
89
|
+
if 0 == (-v1[1] * p.x + v1[0] * p.y) # collinear?
|
90
|
+
# The edges are collinear, but do they overlap?
|
91
|
+
# Project them onto the x and y axes to find out
|
92
|
+
left1, right1 = [self.first[0], self.last[0]].sort
|
93
|
+
bottom1, top1 = [self.first[1], self.last[1]].sort
|
94
|
+
left2, right2 = [other.first[0], other.last[0]].sort
|
95
|
+
bottom2, top2 = [other.first[1], other.last[1]].sort
|
96
|
+
|
97
|
+
!((left2 > right1) || (right2 < left1) || (top2 < bottom1) || (bottom2 > top1))
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
else
|
102
|
+
s = (-v1[1] * p.x + v1[0] * p.y) / denominator # v1 x (p0 - p2) / denominator
|
103
|
+
t = ( v2[0] * p.y - v2[1] * p.x) / denominator # v2 x (p0 - p2) / denominator
|
104
|
+
|
105
|
+
p0 + v1 * t if ((0..1) === s) && ((0..1) === t)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Vector] A {Vector} pointing from first to last
|
110
|
+
def vector
|
111
|
+
Vector[*((last-first).to_a)]
|
112
|
+
end
|
113
|
+
|
53
114
|
def to_a
|
54
115
|
[@first, @last]
|
55
116
|
end
|
@@ -0,0 +1,55 @@
|
|
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
|
+
@elements.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 @elements.push Edge.new(previous, n)
|
28
|
+
when Arc, Edge then @elements.push Edge.new(previous.last, n) unless previous.last == n
|
29
|
+
end
|
30
|
+
@elements.last
|
31
|
+
when Edge
|
32
|
+
case previous
|
33
|
+
when Point then @elements.push Edge.new(previous, n.first)
|
34
|
+
when Arc, Edge then @elements.push Edge.new(previous.last, n.first) unless previous.last == n.first
|
35
|
+
end
|
36
|
+
@elements.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
|
+
@elements.push Edge.new(previous, n.first)
|
44
|
+
end
|
45
|
+
when Arc, Edge
|
46
|
+
@elements.push Edge.new(previous.last, n.first) unless previous.last == n.first
|
47
|
+
end
|
48
|
+
@elements.push(n).last
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Unsupported argument type: #{n}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/geometry/point.rb
CHANGED
@@ -40,10 +40,21 @@ geometry class (x, y, z).
|
|
40
40
|
|
41
41
|
# Allow comparison with an Array, otherwise do the normal thing
|
42
42
|
def eql?(other)
|
43
|
+
return @elements.eql? other if other.is_a?(Array)
|
44
|
+
super other
|
45
|
+
end
|
46
|
+
|
47
|
+
# Allow comparison with an Array, otherwise do the normal thing
|
48
|
+
def ==(other)
|
43
49
|
return @elements == other if other.is_a?(Array)
|
44
50
|
super other
|
45
51
|
end
|
46
|
-
|
52
|
+
|
53
|
+
# Combined comparison operator
|
54
|
+
# @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point
|
55
|
+
def <=>(other)
|
56
|
+
Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact]
|
57
|
+
end
|
47
58
|
|
48
59
|
def coerce(other)
|
49
60
|
case other
|
@@ -61,35 +72,35 @@ geometry class (x, y, z).
|
|
61
72
|
'Point' + @elements.to_s
|
62
73
|
end
|
63
74
|
|
64
|
-
#
|
75
|
+
# @group Accessors
|
65
76
|
# @param [Integer] i Index into the {Point}'s elements
|
66
77
|
# @return [Numeric] Element i (starting at 0)
|
67
78
|
def [](i)
|
68
79
|
@elements[i]
|
69
80
|
end
|
70
81
|
|
71
|
-
#
|
82
|
+
# @attribute [r] x
|
72
83
|
# @return [Numeric] X-component
|
73
84
|
def x
|
74
85
|
@elements[0]
|
75
86
|
end
|
76
87
|
|
77
|
-
#
|
88
|
+
# @attribute [r] y
|
78
89
|
# @return [Numeric] Y-component
|
79
90
|
def y
|
80
91
|
@elements[1]
|
81
92
|
end
|
82
93
|
|
83
|
-
#
|
94
|
+
# @attribute [r] z
|
84
95
|
# @return [Numeric] Z-component
|
85
96
|
def z
|
86
97
|
@elements[2]
|
87
98
|
end
|
88
|
-
#
|
99
|
+
# @endgroup
|
89
100
|
|
90
|
-
#
|
101
|
+
# @group Arithmetic
|
91
102
|
|
92
|
-
#
|
103
|
+
# @group Unary operators
|
93
104
|
def +@
|
94
105
|
self
|
95
106
|
end
|
@@ -97,7 +108,7 @@ geometry class (x, y, z).
|
|
97
108
|
def -@
|
98
109
|
Point[@elements.map {|e| -e }]
|
99
110
|
end
|
100
|
-
#
|
111
|
+
# @endgroup
|
101
112
|
|
102
113
|
def +(other)
|
103
114
|
case other
|
@@ -127,7 +138,7 @@ geometry class (x, y, z).
|
|
127
138
|
end
|
128
139
|
end
|
129
140
|
|
130
|
-
#
|
141
|
+
# @endgroup
|
131
142
|
|
132
143
|
end
|
133
144
|
end
|