aurora-geometry 0.0.2

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