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