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.
- 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
|