geometry 4 → 5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,6 +4,3 @@
4
4
  Gemfile.lock
5
5
  pkg/*
6
6
  doc
7
-
8
- .DS_Store
9
- *.xcodeproj
@@ -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
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "geometry"
6
- s.version = '4'
6
+ s.version = '5'
7
7
  s.authors = ["Brandon Fosdick"]
8
8
  s.email = ["bfoz@bfoz.net"]
9
9
  s.homepage = "http://github.com/bfoz/geometry"
@@ -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
@@ -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
@@ -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,1], 2
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
- # @return [Point] The Circle's center point
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
@@ -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
@@ -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
- alias == eql?
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
- # !@group Accessors
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
- # !@attribute [r] x
82
+ # @attribute [r] x
72
83
  # @return [Numeric] X-component
73
84
  def x
74
85
  @elements[0]
75
86
  end
76
87
 
77
- # !@attribute [r] y
88
+ # @attribute [r] y
78
89
  # @return [Numeric] Y-component
79
90
  def y
80
91
  @elements[1]
81
92
  end
82
93
 
83
- # !@attribute [r] z
94
+ # @attribute [r] z
84
95
  # @return [Numeric] Z-component
85
96
  def z
86
97
  @elements[2]
87
98
  end
88
- # !@endgroup
99
+ # @endgroup
89
100
 
90
- # !@group Arithmetic
101
+ # @group Arithmetic
91
102
 
92
- # !@group Unary operators
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
- # !@endgroup
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
- # !@endgroup
141
+ # @endgroup
131
142
 
132
143
  end
133
144
  end