aurora-geometry 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE +21 -0
  5. data/README.markdown +105 -0
  6. data/Rakefile +24 -0
  7. data/aurora-geometry.gemspec +23 -0
  8. data/lib/geometry.rb +22 -0
  9. data/lib/geometry/arc.rb +94 -0
  10. data/lib/geometry/circle.rb +122 -0
  11. data/lib/geometry/cluster_factory.rb +15 -0
  12. data/lib/geometry/edge.rb +140 -0
  13. data/lib/geometry/line.rb +154 -0
  14. data/lib/geometry/obround.rb +238 -0
  15. data/lib/geometry/path.rb +67 -0
  16. data/lib/geometry/point.rb +163 -0
  17. data/lib/geometry/point_zero.rb +107 -0
  18. data/lib/geometry/polygon.rb +368 -0
  19. data/lib/geometry/polyline.rb +318 -0
  20. data/lib/geometry/rectangle.rb +378 -0
  21. data/lib/geometry/regular_polygon.rb +136 -0
  22. data/lib/geometry/rotation.rb +190 -0
  23. data/lib/geometry/size.rb +75 -0
  24. data/lib/geometry/size_zero.rb +70 -0
  25. data/lib/geometry/square.rb +113 -0
  26. data/lib/geometry/text.rb +24 -0
  27. data/lib/geometry/transformation.rb +171 -0
  28. data/lib/geometry/transformation/composition.rb +39 -0
  29. data/lib/geometry/triangle.rb +78 -0
  30. data/lib/geometry/vector.rb +34 -0
  31. data/test/geometry.rb +5 -0
  32. data/test/geometry/arc.rb +25 -0
  33. data/test/geometry/circle.rb +112 -0
  34. data/test/geometry/edge.rb +132 -0
  35. data/test/geometry/line.rb +132 -0
  36. data/test/geometry/obround.rb +25 -0
  37. data/test/geometry/path.rb +66 -0
  38. data/test/geometry/point.rb +258 -0
  39. data/test/geometry/point_zero.rb +177 -0
  40. data/test/geometry/polygon.rb +214 -0
  41. data/test/geometry/polyline.rb +266 -0
  42. data/test/geometry/rectangle.rb +154 -0
  43. data/test/geometry/regular_polygon.rb +120 -0
  44. data/test/geometry/rotation.rb +108 -0
  45. data/test/geometry/size.rb +97 -0
  46. data/test/geometry/size_zero.rb +153 -0
  47. data/test/geometry/square.rb +66 -0
  48. data/test/geometry/transformation.rb +169 -0
  49. data/test/geometry/transformation/composition.rb +49 -0
  50. data/test/geometry/triangle.rb +32 -0
  51. data/test/geometry/vector.rb +41 -0
  52. metadata +115 -0
@@ -0,0 +1,113 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+ NotSquareError = Class.new(ArgumentError)
5
+
6
+ =begin
7
+ The {Square} class cluster is like the {Rectangle} class cluster, but not longer in one direction.
8
+
9
+ == Constructors
10
+
11
+ square = Square.new from:[1,2], to:[2,3] # Using two corners
12
+ square = Square.new origin:[3,4], size:5 # Using an origin point and a size
13
+ =end
14
+ class Square
15
+ attr_reader :origin
16
+
17
+ # Creates a {Square} given two {Point}s
18
+ # @option options [Point] :from A corner (ie. bottom-left)
19
+ # @option options [Point] :to The other corner (ie. top-right)
20
+ # @option options [Point] :origin The lower left corner
21
+ # @option options [Number] :size Bigness
22
+ def initialize(options={})
23
+ origin = options[:from] || options[:origin]
24
+ origin = origin ? Point[origin] : PointZero.new
25
+
26
+ if options.has_key? :to
27
+ point1 = options[:to]
28
+ elsif options.has_key? :size
29
+ point1 = origin + options[:size]
30
+ end
31
+
32
+ point1 = Point[point1]
33
+ raise(ArgumentError, "Point sizes must match (#{origin.size} != #{point1.size})") unless origin.is_a?(PointZero) || (origin.size == point1.size)
34
+
35
+ # Reorder the points to get lower-left and upper-right
36
+ minx, maxx = [origin.x, point1.x].minmax
37
+ miny, maxy = [origin.y, point1.y].minmax
38
+ @points = [Point[minx, miny], Point[maxx, maxy]]
39
+
40
+ raise(NotSquareError) if height != width
41
+ end
42
+
43
+ # !@group Accessors
44
+ # @attribute [r] origin
45
+ # @return [Point] The lower left corner
46
+ def origin
47
+ @points.first
48
+ end
49
+
50
+ def height
51
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
52
+ max.y - min.y
53
+ end
54
+
55
+ def width
56
+ min, max = @points.minmax {|a,b| a.x <=> b.x}
57
+ max.x - min.x
58
+ end
59
+ # @endgroup
60
+ end
61
+
62
+ # A {Square} created with a center point and a size
63
+ class CenteredSquare < Square
64
+ # @attribute [r] center
65
+ # @return [Point] The center of the {Square}
66
+ attr_reader :center
67
+
68
+ # @param [Point] center The center point
69
+ # @param [Numeric] size The length of each side
70
+ def initialize(center, size)
71
+ @center = Point[center]
72
+ @size = size
73
+ end
74
+
75
+ # @group Accessors
76
+ # @attribute [r] origin
77
+ # @return [Point] The lower left corner
78
+ def origin
79
+ Point[@center.x - size/2, @center.y - size/2]
80
+ end
81
+
82
+ # @attribute [r] points
83
+ # @return [Array<Point>] The {Square}'s four points (counterclockwise)
84
+ def points
85
+ half_size = @size/2
86
+ minx = @center.x - half_size
87
+ maxx = @center.x + half_size
88
+ miny = @center.y - half_size
89
+ maxy = @center.y + half_size
90
+
91
+ [Point[minx,miny], Point[maxx, miny], Point[maxx, maxy], Point[minx,maxy]]
92
+ end
93
+
94
+ def height
95
+ @size
96
+ end
97
+
98
+ def width
99
+ @size
100
+ end
101
+ # @endgroup
102
+ end
103
+
104
+ # A {Square} created with an origin point and a size
105
+ class SizedSquare < Square
106
+ # @param [Point] origin The origin point (bottom-left corner)
107
+ # @param [Numeric] size The length of each side
108
+ def initialize(origin, size)
109
+ @origin = Point[origin]
110
+ @size = size
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+
5
+ class Text
6
+
7
+ # @return [Point] The point located in the top left corner of {Text}'s
8
+ # bounding box
9
+ attr_reader :position
10
+
11
+ # @return [String] The {Text}'s textual content
12
+ attr_reader :content
13
+
14
+ def initialize(position, content)
15
+ @position = Point[position]
16
+ @content = content
17
+ end
18
+
19
+ def eql?(other)
20
+ self.content == other.content
21
+ end
22
+ alias :== :eql?
23
+ end
24
+ end
@@ -0,0 +1,171 @@
1
+ require 'geometry/point'
2
+ require 'geometry/rotation'
3
+
4
+ require_relative 'transformation/composition'
5
+
6
+ module Geometry
7
+ =begin
8
+ {Transformation} represents a relationship between two coordinate frames.
9
+
10
+ To create a pure translation relationship:
11
+
12
+ translate = Geometry::Transformation.new(:translate => Point[4, 2])
13
+
14
+ To create a transformation with an origin and an X-axis aligned with the parent
15
+ coordinate system's Y-axis (the Y and Z axes will be chosen arbitrarily):
16
+
17
+ translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0])
18
+
19
+ To create a transformation with an origin, an X-axis aligned with the parent
20
+ coordinate system's Y-axis, and a Y-axis aligned with the parent coordinate
21
+ system's X-axis:
22
+
23
+ translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0], :y => [1,0,0])
24
+ =end
25
+ class Transformation
26
+ attr_reader :dimensions
27
+ attr_reader :rotation
28
+ attr_reader :scale
29
+ attr_reader :translation
30
+
31
+ attr_reader :x_axis, :y_axis, :z_axis
32
+
33
+ # @overload new(translate, rotate, scale)
34
+ # @param [Point] translate Linear displacement
35
+ # @param [Rotation] rotate Rotation
36
+ # @param [Vector] scale Scaling
37
+ # @overload new(options)
38
+ # @param [Hash] options
39
+ # @option options [Integer] :dimensions Dimensionality of the transformation
40
+ # @option options [Point] :origin Same as :translate
41
+ # @option options [Point] :move Same as :translate
42
+ # @option options [Point] :translate Linear displacement
43
+ # @option options [Angle] :angle Rotation angle (assumes planar geometry)
44
+ # @option options [Rotation] :rotate Rotation
45
+ # @option options [Vector] :scale Scaling
46
+ # @option options [Vector] :x X-axis
47
+ # @option options [Vector] :y Y-axis
48
+ # @option options [Vector] :z Z-axis
49
+ def initialize(*args)
50
+ options, args = args.partition {|a| a.is_a? Hash}
51
+ translate, rotate, scale = args
52
+ options = options.reduce({}, :merge)
53
+
54
+ @dimensions = options[:dimensions] || nil
55
+
56
+ rotation_options = options.select {|key, value| [:angle, :x, :y, :z].include? key }
57
+ @rotation = options[:rotate] || rotate || ((rotation_options.size > 0) ? Geometry::Rotation.new(rotation_options) : nil)
58
+ @scale = options[:scale] || scale
59
+
60
+ case options.count {|k,v| [:move, :origin, :translate].include? k }
61
+ when 0
62
+ @translation = translate
63
+ when 1
64
+ @translation = (options[:translate] ||= options.delete(:move) || options.delete(:origin))
65
+ else
66
+ raise ArgumentError, "Too many translation parameters in #{options}"
67
+ end
68
+
69
+ raise ArgumentError, "Bad translation" if @translation.is_a? Hash
70
+ @translation = Point[*@translation]
71
+ if @translation
72
+ @translation = nil if @translation.all? {|v| v == 0}
73
+ raise ArgumentError, ":translate must be a Point or a Vector" if @translation and not @translation.is_a?(Vector)
74
+ end
75
+
76
+ if @dimensions
77
+ biggest = [@translation, @scale].select {|a| a}.map {|a| a.size}.max
78
+
79
+ if biggest and (biggest != 0) and (((biggest != @dimensions)) or (@rotation and (@rotation.dimensions != biggest)))
80
+ raise ArgumentError, "Dimensionality mismatch"
81
+ end
82
+ end
83
+ end
84
+
85
+ def initialize_clone(source)
86
+ super
87
+ @rotation = @rotation.clone if @rotation
88
+ @scale = @scale.clone if @scale
89
+ @translation = @translation.clone if @translation
90
+ end
91
+
92
+ # !@attribute [r] has_rotation?
93
+ # @return [Bool] true if the transformation has any rotation components
94
+ def has_rotation?
95
+ !!@rotation
96
+ end
97
+
98
+ # Returns true if the {Transformation} is the identity transformation
99
+ def identity?
100
+ !(@rotation || @scale || @translation)
101
+ end
102
+
103
+ def eql?(other)
104
+ return false unless other
105
+ return true if !self.dimensions && !other.dimensions && !self.rotation && !other.rotation && !self.translation && !other.translation && !self.scale && !other.scale
106
+ return false if !(self.dimensions && other.dimensions) && !(self.rotation && other.rotation) && !(self.translation && other.translation) && !(self.scale && other.scale)
107
+
108
+ ((self.dimensions && other.dimensions && self.dimensions.eql?(other.dimensions)) || !(self.dimensions && other.dimensions)) &&
109
+ ((self.rotation && other.rotation && self.rotation.eql?(other.rotation)) || !(self.rotation && other.rotation)) &&
110
+ ((self.scale && other.scale && self.scale.eql?(other.scale)) || !(self.scale && other.rotation)) &&
111
+ ((self.translation && other.translation && self.translation.eql?(other.translation)) || !(self.scale && other.rotation))
112
+ end
113
+ alias :== :eql?
114
+
115
+ # Compose the current {Transformation} with another one
116
+ def +(other)
117
+ return self.clone unless other
118
+
119
+ case other
120
+ when Array, Vector
121
+ if @translation
122
+ Transformation.new(@translation+other, @rotation, @scale)
123
+ else
124
+ Transformation.new(other, @rotation, @scale)
125
+ end
126
+ when Composition
127
+ Composition.new(self, *other.transformations)
128
+ when Transformation
129
+ if @rotation || other.rotation
130
+ Composition.new(self, other)
131
+ else
132
+ translation = @translation ? (@translation + other.translation) : other.translation
133
+ Transformation.new(translation, @rotation, @scale)
134
+ end
135
+ end
136
+ end
137
+
138
+ def -(other)
139
+ return self.clone unless other
140
+
141
+ case other
142
+ when Array, Vector
143
+ if @translation
144
+ Transformation.new(@translation-other, @rotation, @scale)
145
+ else
146
+ Transformation.new(other.map {|e| -e}, @rotation, @scale)
147
+ end
148
+ when Transformation
149
+ if @rotation
150
+ rotation = other.rotation ? (@rotation - other.rotation) : @rotation
151
+ elsif other.rotation
152
+ rotation = -other.rotation
153
+ else
154
+ rotation = nil
155
+ end
156
+
157
+ translation = @translation ? (@translation - other.translation) : -other.translation
158
+
159
+ Transformation.new(translation, rotation, @scale)
160
+ end
161
+ end
162
+
163
+ # Transform and return a new {Point}. Rotation is applied before translation.
164
+ # @param [Point] point the {Point} to transform into the parent coordinate frame
165
+ # @return [Point] The transformed {Point}
166
+ def transform(point)
167
+ point = @rotation.transform(point) if @rotation
168
+ @translation ? (@translation + point) : point
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,39 @@
1
+ module Geometry
2
+ class Transformation
3
+ class Composition
4
+ attr_reader :transformations
5
+
6
+ def initialize(*args)
7
+ raise TypeError unless args.all? {|a| a.is_a? Transformation }
8
+ @transformations = *args
9
+ end
10
+
11
+ def +(other)
12
+ case other
13
+ when Transformation
14
+ Composition.new(*transformations, other)
15
+ when Composition
16
+ Composition.new(*transformations, *other.transformations)
17
+ end
18
+ end
19
+
20
+ # @group Accessors
21
+ # !@attribute [r] has_rotation?
22
+ # @return [Bool] true if the transformation has any rotation components
23
+ def has_rotation?
24
+ transformations.any? {|t| t.is_a?(Rotation) || t.has_rotation? }
25
+ end
26
+
27
+ # !@attribute [r] size
28
+ # @return [Number] the number of composed {Transformation}s
29
+ def size
30
+ transformations.size
31
+ end
32
+ # @endgroup
33
+
34
+ def transform(point)
35
+ transformations.reverse.reduce(point) {|point, transformation| transformation.transform(point) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'point'
3
+
4
+ module Geometry
5
+ =begin rdoc
6
+ A {http://en.wikipedia.org/wiki/Triangle Triangle} is not a square.
7
+
8
+ == Usage
9
+ A right {Triangle} with its right angle at the origin and sides of length 1
10
+ triangle = Geometry::Triangle.new [0,0], [1,0], [0,1]
11
+
12
+ An isoscoles right {Triangle} created with an origin and leg length
13
+ triangle = Geometry::Triangle.new [0,0], 1
14
+ =end
15
+
16
+ # @abstract Factory class that instantiates the appropriate subclasses
17
+ class Triangle
18
+
19
+ include ClusterFactory
20
+
21
+ # @overload new(point0, point1, point2)
22
+ # Contruct a {ScaleneTriangle} using three {Point}s
23
+ # @param [Point] point0 The first vertex of the {Triangle}
24
+ # @param [Point] point1 Another vertex of the {Triangle}
25
+ # @param [Point] point2 The final vertex of the {Triangle}
26
+ # @overload new(point, length)
27
+ # Construct a {RightTriangle} using a {Point} and the lengths of the sides
28
+ # @param [Point] point The location of the vertex at {Triangle}'s right angle
29
+ # @param [Number] base The length of the {Triangle}'s base leg
30
+ # @param [Number] height The length of the {Triangle}'s vertical leg
31
+ def self.new(*args)
32
+ if args.size == 3
33
+ ScaleneTriangle.new *args
34
+ elsif args.size == 2
35
+ RightTriangle.new args[0], args[1], args[1]
36
+ end
37
+ end
38
+ end
39
+
40
+ # {http://en.wikipedia.org/wiki/Equilateral_triangle Equilateral Triangle}
41
+ class EquilateralTriangle < Triangle
42
+ def self.new(*args)
43
+ original_new(*args)
44
+ end
45
+ end
46
+
47
+ class IsoscelesTriangle < Triangle
48
+ def self.new(*args)
49
+ original_new(*args)
50
+ end
51
+ end
52
+
53
+ # {http://en.wikipedia.org/wiki/Right_triangle Right Triangle}
54
+ class RightTriangle < Triangle
55
+ attr_reader :origin, :base, :height
56
+
57
+ # Construct a Right Triangle given a {Point} and the leg lengths
58
+ def initialize(origin, base, height)
59
+ @origin = Point[origin]
60
+ @base, @height = base, height
61
+ end
62
+
63
+ # An array of points corresponding to the vertices of the {Triangle} (clockwise)
64
+ # @return [Array<Point>] Vertices
65
+ def points
66
+ [@origin, @origin + Point[0,@height], @origin + Point[@base,0]]
67
+ end
68
+ end
69
+
70
+ class ScaleneTriangle < Triangle
71
+ attr_reader :points
72
+
73
+ # Construct a scalene {Triangle}
74
+ def initialize(point0, point1, point2)
75
+ @points = [point0, point1, point2].map {|p| Point[p] }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,34 @@
1
+ require 'matrix'
2
+
3
+ # Monkeypatch Vector to overcome some deficiencies
4
+ class Vector
5
+ X = Vector[1,0,0]
6
+ Y = Vector[0,1,0]
7
+ Z = Vector[0,0,1]
8
+
9
+ # @group Unary operators
10
+ def +@
11
+ self
12
+ end
13
+
14
+ def -@
15
+ Vector[*(@elements.map {|e| -e })]
16
+ end
17
+ # @endgroup
18
+
19
+ # Cross-product of two {Vector}s
20
+ # @return [Vector]
21
+ def cross(other)
22
+ Vector.Raise ErrDimensionMismatch unless @elements.size == other.size
23
+
24
+ case @elements.size
25
+ when 0 then raise ArgumentError, "Can't multply zero-length Vectors"
26
+ when 1 then @elements.first * other.first
27
+ when 2 then @elements.first * other[1] - @elements.last * other.first
28
+ when 3 then Vector[ @elements[1]*other[2] - @elements[2]*other[1],
29
+ @elements[2]*other[0] - @elements[0]*other[2],
30
+ @elements[0]*other[1] - @elements[1]*other[0]]
31
+ end
32
+ end
33
+ alias ** cross
34
+ end