aurora-geometry 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/aurora-geometry.gemspec +23 -0
- data/lib/geometry.rb +22 -0
- data/lib/geometry/arc.rb +94 -0
- data/lib/geometry/circle.rb +122 -0
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +140 -0
- data/lib/geometry/line.rb +154 -0
- data/lib/geometry/obround.rb +238 -0
- data/lib/geometry/path.rb +67 -0
- data/lib/geometry/point.rb +163 -0
- data/lib/geometry/point_zero.rb +107 -0
- data/lib/geometry/polygon.rb +368 -0
- data/lib/geometry/polyline.rb +318 -0
- data/lib/geometry/rectangle.rb +378 -0
- data/lib/geometry/regular_polygon.rb +136 -0
- data/lib/geometry/rotation.rb +190 -0
- data/lib/geometry/size.rb +75 -0
- data/lib/geometry/size_zero.rb +70 -0
- data/lib/geometry/square.rb +113 -0
- data/lib/geometry/text.rb +24 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/transformation/composition.rb +39 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/test/geometry.rb +5 -0
- data/test/geometry/arc.rb +25 -0
- data/test/geometry/circle.rb +112 -0
- data/test/geometry/edge.rb +132 -0
- data/test/geometry/line.rb +132 -0
- data/test/geometry/obround.rb +25 -0
- data/test/geometry/path.rb +66 -0
- data/test/geometry/point.rb +258 -0
- data/test/geometry/point_zero.rb +177 -0
- data/test/geometry/polygon.rb +214 -0
- data/test/geometry/polyline.rb +266 -0
- data/test/geometry/rectangle.rb +154 -0
- data/test/geometry/regular_polygon.rb +120 -0
- data/test/geometry/rotation.rb +108 -0
- data/test/geometry/size.rb +97 -0
- data/test/geometry/size_zero.rb +153 -0
- data/test/geometry/square.rb +66 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/transformation/composition.rb +49 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- 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
|