geometry 3 → 4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +10 -1
- data/Rakefile +11 -0
- data/geometry.gemspec +1 -1
- data/lib/geometry.rb +8 -1
- data/lib/geometry/arc.rb +31 -0
- data/lib/geometry/circle.rb +1 -1
- data/lib/geometry/edge.rb +1 -1
- data/lib/geometry/line.rb +2 -2
- data/lib/geometry/point.rb +67 -7
- data/lib/geometry/point_zero.rb +69 -0
- data/lib/geometry/polygon.rb +1 -0
- data/lib/geometry/rectangle.rb +38 -10
- data/lib/geometry/rotation.rb +46 -0
- data/lib/geometry/size_zero.rb +69 -0
- data/lib/geometry/square.rb +103 -0
- data/lib/geometry/transformation.rb +106 -0
- data/lib/geometry/vector.rb +14 -0
- data/test/{test_geometry.rb → geometry.rb} +6 -5
- data/test/geometry/arc.rb +13 -0
- data/test/geometry/circle.rb +6 -6
- data/test/geometry/edge.rb +15 -15
- data/test/{test_line.rb → geometry/line.rb} +18 -18
- data/test/geometry/point.rb +135 -46
- data/test/geometry/point_zero.rb +151 -0
- data/test/geometry/polygon.rb +10 -7
- data/test/geometry/rectangle.rb +60 -55
- data/test/geometry/rotation.rb +35 -0
- data/test/geometry/size.rb +46 -46
- data/test/geometry/size_zero.rb +151 -0
- data/test/geometry/square.rb +56 -0
- data/test/geometry/transformation.rb +90 -0
- data/test/geometry/vector.rb +17 -0
- metadata +28 -11
- data/test/helper.rb +0 -2
- data/test/test_unit_extensions.rb +0 -23
@@ -0,0 +1,46 @@
|
|
1
|
+
module Geometry
|
2
|
+
=begin
|
3
|
+
A generalized representation of a rotation transformation.
|
4
|
+
=end
|
5
|
+
class Rotation
|
6
|
+
# !@attribute [r] dimensions
|
7
|
+
# @return [Integer]
|
8
|
+
attr_reader :dimensions
|
9
|
+
attr_reader :x, :y, :z
|
10
|
+
|
11
|
+
# @overload initialize(options={})
|
12
|
+
# @option options [Integer] :dimensions Dimensionality of the rotation
|
13
|
+
# @option options [Vector] :x X-axis
|
14
|
+
# @option options [Vector] :y Y-axis
|
15
|
+
# @option options [Vector] :z Z-axis
|
16
|
+
def initialize(*args)
|
17
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
18
|
+
options = options.reduce({}, :merge)
|
19
|
+
|
20
|
+
@dimensions = options[:dimensions] || nil
|
21
|
+
|
22
|
+
axis_options = [options[:x], options[:y], options[:z]]
|
23
|
+
all_axes_options = [options[:x], options[:y], options[:z]].select {|a| a}
|
24
|
+
if all_axes_options.count != 0
|
25
|
+
@x = options[:x] || nil
|
26
|
+
@y = options[:y] || nil
|
27
|
+
@z = options[:z] || nil
|
28
|
+
|
29
|
+
raise ArgumentError, "All axis options must be Vectors" unless all_axes_options.all? {|a| a.is_a?(Vector) or a.is_a?(Array) }
|
30
|
+
|
31
|
+
raise ArgumentError, "All provided axes must be the same size" unless all_axes_options.all? {|a| a.size == all_axes_options.first.size}
|
32
|
+
|
33
|
+
@dimensions ||= all_axes_options.first.size
|
34
|
+
|
35
|
+
raise ArgumentError, "Dimensionality mismatch" unless all_axes_options.first.size <= @dimensions
|
36
|
+
if all_axes_options.first.size < @dimensions
|
37
|
+
@x, @y, @z = [@x, @y, @z].map {|a| (a && (a.size < @dimensions)) ? Array.new(@dimensions) {|i| a[i] || 0 } : a }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def identity?
|
43
|
+
(!@x && !@y && !@z) || ([@x, @y, @z].select {|a| a}.all? {|a| a.respond_to?(:magnitude) ? (1 == a.magnitude) : (1 == a.size)})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
|
3
|
+
module Geometry
|
4
|
+
=begin rdoc
|
5
|
+
An object repesenting a zero {Size} in N-dimensional space
|
6
|
+
|
7
|
+
A {SizeZero} object is a {Size} that will always compare equal to zero and unequal to
|
8
|
+
everything else, regardless of dimensionality.
|
9
|
+
=end
|
10
|
+
class SizeZero
|
11
|
+
def eql?(other)
|
12
|
+
if other.respond_to? :all?
|
13
|
+
other.all? {|e| e.eql? 0}
|
14
|
+
else
|
15
|
+
other == 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
alias == eql?
|
19
|
+
|
20
|
+
def coerce(other)
|
21
|
+
if other.is_a? Numeric
|
22
|
+
[other, 0]
|
23
|
+
elsif other.is_a? Array
|
24
|
+
[other, Array.new(other.size,0)]
|
25
|
+
elsif other.is_a? Vector
|
26
|
+
[other, Vector[*Array.new(other.size,0)]]
|
27
|
+
else
|
28
|
+
[Size[other], Size[Array.new(other.size,0)]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# !@group Arithmetic
|
33
|
+
|
34
|
+
# !@group Unary operators
|
35
|
+
def +@
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def -@
|
40
|
+
self
|
41
|
+
end
|
42
|
+
# !@endgroup
|
43
|
+
|
44
|
+
def +(other)
|
45
|
+
other
|
46
|
+
end
|
47
|
+
|
48
|
+
def -(other)
|
49
|
+
if other.respond_to? :-@
|
50
|
+
-other
|
51
|
+
elsif other.respond_to? :map
|
52
|
+
other.map {|a| -a }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def *(other)
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def /(other)
|
61
|
+
raise OperationNotDefined unless other.is_a? Numeric
|
62
|
+
raise ZeroDivisionError if 0 == other
|
63
|
+
self
|
64
|
+
end
|
65
|
+
# !@endgroup
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,103 @@
|
|
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 [1,2], [2,3] # Using two corners
|
12
|
+
square = Square.new [3,4], 5 # Using an origin point and a size
|
13
|
+
square = Square.new 6 # Using a size and an origin of [0,0]
|
14
|
+
=end
|
15
|
+
class Square
|
16
|
+
attr_reader :origin
|
17
|
+
|
18
|
+
# Creates a {Square} given two {Point}s
|
19
|
+
# @param [Point] point0 A corner (ie. bottom-left)
|
20
|
+
# @param [Point] point1 The other corner (ie. top-right)
|
21
|
+
def initialize(point0, point1)
|
22
|
+
point0, point1 = Point[point0], Point[point1]
|
23
|
+
raise(ArgumentError, "Point sizes must match (#{point0.size} != #{point1.size}") unless point0.size == point1.size
|
24
|
+
|
25
|
+
# Reorder the points to get lower-left and upper-right
|
26
|
+
minx, maxx = [point0.x, point1.x].minmax
|
27
|
+
miny, maxy = [point0.y, point1.y].minmax
|
28
|
+
@points = [Point[minx, miny], Point[maxx, maxy]]
|
29
|
+
|
30
|
+
raise(NotSquareError) if height != width
|
31
|
+
end
|
32
|
+
|
33
|
+
# !@group Accessors
|
34
|
+
# !@attribute [r] origin
|
35
|
+
# @return [Point] The lower left corner
|
36
|
+
def origin
|
37
|
+
@points.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def height
|
41
|
+
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
42
|
+
max.y - min.y
|
43
|
+
end
|
44
|
+
|
45
|
+
def width
|
46
|
+
min, max = @points.minmax {|a,b| a.x <=> b.x}
|
47
|
+
max.x - min.x
|
48
|
+
end
|
49
|
+
# !@endgroup
|
50
|
+
end
|
51
|
+
|
52
|
+
# A {Square} created with a center point and a size
|
53
|
+
class CenteredSquare < Square
|
54
|
+
# !@attribute [r] center
|
55
|
+
# @return [Point] The center of the {Square}
|
56
|
+
attr_reader :center
|
57
|
+
|
58
|
+
# @param [Point] center The center point
|
59
|
+
# @param [Numeric] size The length of each side
|
60
|
+
def initialize(center, size)
|
61
|
+
@center = Point[center]
|
62
|
+
@size = size
|
63
|
+
end
|
64
|
+
|
65
|
+
# !@group Accessors
|
66
|
+
# !@attribute [r] origin
|
67
|
+
# @return [Point] The lower left corner
|
68
|
+
def origin
|
69
|
+
Point[@center.x - size/2, @center.y - size/2]
|
70
|
+
end
|
71
|
+
|
72
|
+
# !@attribute [r] points
|
73
|
+
# @return [Array<Point>] The {Square}'s four points (clockwise)
|
74
|
+
def points
|
75
|
+
half_size = @size/2
|
76
|
+
minx = @center.x - half_size
|
77
|
+
maxx = @center.x + half_size
|
78
|
+
miny = @center.y - half_size
|
79
|
+
maxy = @center.y + half_size
|
80
|
+
|
81
|
+
[Point[minx,miny], Point[minx,maxy], Point[maxx, maxy], Point[maxx, miny]]
|
82
|
+
end
|
83
|
+
|
84
|
+
def height
|
85
|
+
@size
|
86
|
+
end
|
87
|
+
|
88
|
+
def width
|
89
|
+
@size
|
90
|
+
end
|
91
|
+
# !@endgroup
|
92
|
+
end
|
93
|
+
|
94
|
+
# A {Square} created with an origin point and a size
|
95
|
+
class SizedSquare < Square
|
96
|
+
# @param [Point] origin The origin point (bottom-left corner)
|
97
|
+
# @param [Numeric] size The length of each side
|
98
|
+
def initialize(origin, size)
|
99
|
+
@origin = Point[origin]
|
100
|
+
@size = size
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'geometry/point'
|
2
|
+
require 'geometry/rotation'
|
3
|
+
|
4
|
+
module Geometry
|
5
|
+
=begin
|
6
|
+
{Transformation} represents a relationship between two coordinate frames
|
7
|
+
|
8
|
+
To create a pure translation relationship:
|
9
|
+
|
10
|
+
translate = Geometry::Transformation.new(:translate => Point[4, 2])
|
11
|
+
|
12
|
+
To create a transformation with an origin and an X-axis aligned with the parent
|
13
|
+
coordinate system's Y-axis (the Y and Z axes will be chosen arbitrarily):
|
14
|
+
|
15
|
+
translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0])
|
16
|
+
|
17
|
+
To create a transformation with an origin, an X-axis aligned with the parent
|
18
|
+
coordinate system's Y-axis, and a Y-axis aligned with the parent coordinate
|
19
|
+
system's X-axis:
|
20
|
+
|
21
|
+
translate = Geometry::Transformation.new(:origin => [4, 2], :x => [0,1,0], :y => [1,0,0])
|
22
|
+
=end
|
23
|
+
class Transformation
|
24
|
+
attr_reader :dimensions
|
25
|
+
attr_reader :rotation
|
26
|
+
attr_reader :scale
|
27
|
+
attr_reader :translation
|
28
|
+
|
29
|
+
attr_reader :x_axis, :y_axis, :z_axis
|
30
|
+
|
31
|
+
# @overload new(translate, rotate, scale)
|
32
|
+
# @param [Point] translate Linear displacement
|
33
|
+
# @param [Rotation] rotate Rotation
|
34
|
+
# @param [Vector] scale Scaling
|
35
|
+
# @overload new(options)
|
36
|
+
# @param [Hash] options
|
37
|
+
# @option options [Integer] :dimensions Dimensionality of the transformation
|
38
|
+
# @option options [Point] :origin Same as :translate
|
39
|
+
# @option options [Point] :move Same as :translate
|
40
|
+
# @option options [Point] :translate Linear displacement
|
41
|
+
# @option options [Rotation] :rotate Rotation
|
42
|
+
# @option options [Vector] :scale Scaling
|
43
|
+
# @option options [Vector] :x X-axis
|
44
|
+
# @option options [Vector] :y Y-axis
|
45
|
+
# @option options [Vector] :z Z-axis
|
46
|
+
def initialize(*args)
|
47
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
48
|
+
translate, rotate, scale = args
|
49
|
+
options = options.reduce({}, :merge)
|
50
|
+
|
51
|
+
@dimensions = options[:dimensions] || nil
|
52
|
+
|
53
|
+
@rotation = options[:rotate] || rotate || Geometry::Rotation.new(options)
|
54
|
+
@scale = options[:scale] || scale
|
55
|
+
|
56
|
+
case options.count {|k,v| [:move, :origin, :translate].include? k }
|
57
|
+
when 0
|
58
|
+
@translation = translate
|
59
|
+
when 1
|
60
|
+
@translation = (options[:translate] ||= options.delete(:move) || options.delete(:origin))
|
61
|
+
else
|
62
|
+
raise ArgumentError, "Too many translation parameters in #{options}"
|
63
|
+
end
|
64
|
+
|
65
|
+
@translation = Point[*@translation]
|
66
|
+
if @translation
|
67
|
+
@translation = nil if @translation.all? {|v| v == 0}
|
68
|
+
raise ArgumentError, ":translate must be a Point or a Vector" if @translation and not @translation.is_a?(Vector)
|
69
|
+
end
|
70
|
+
|
71
|
+
if @dimensions
|
72
|
+
biggest = [@translation, @scale].select {|a| a}.map {|a| a.size}.max
|
73
|
+
|
74
|
+
if biggest and (biggest != 0) and (((biggest != @dimensions)) or (@rotation and (@rotation.dimensions != biggest)))
|
75
|
+
raise ArgumentError, "Dimensionality mismatch"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns true if the {Transformation} is the identity transformation
|
81
|
+
def identity?
|
82
|
+
@rotation.identity? && !(@scale || @translation)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Compose the current {Transformation} with another one
|
86
|
+
def +(other)
|
87
|
+
if other.is_a?(Array) or other.is_a?(Vector)
|
88
|
+
if @translation
|
89
|
+
Transformation.new(@translation+other, @rotation, @scale)
|
90
|
+
else
|
91
|
+
Transformation.new(other, @rotation, @scale)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def -(other)
|
97
|
+
if other.is_a?(Array) or other.is_a?(Vector)
|
98
|
+
if @translation
|
99
|
+
Transformation.new(@translation-other, @rotation, @scale)
|
100
|
+
else
|
101
|
+
Transformation.new(other.map {|e| -e}, @rotation, @scale)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry'
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
describe Geometry do
|
5
|
+
it "create a Point object" do
|
6
6
|
point = Geometry.point(2,1)
|
7
7
|
assert_kind_of(Geometry::Point, point)
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
|
+
it "create a Line object" do
|
10
11
|
line = Geometry.line([0,0], [10,10])
|
11
12
|
assert_kind_of(Geometry::Line, line)
|
12
13
|
assert_kind_of(Geometry::TwoPointLine, line)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/arc'
|
3
|
+
|
4
|
+
describe Geometry::Arc do
|
5
|
+
it "must create an Arc object from a Point and a radius" do
|
6
|
+
arc = Geometry::Arc.new [1,2], 3, 0, 90
|
7
|
+
arc.must_be_kind_of Geometry::Arc
|
8
|
+
arc.center.must_equal Point[1,2]
|
9
|
+
arc.radius.must_equal 3
|
10
|
+
arc.start_angle.must_equal 0
|
11
|
+
arc.end_angle.must_equal 90
|
12
|
+
end
|
13
|
+
end
|
data/test/geometry/circle.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/circle'
|
3
3
|
|
4
4
|
def Circle(*args)
|
5
5
|
Geometry::Circle.new(*args)
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
must
|
8
|
+
describe Geometry::Circle do
|
9
|
+
it "must create a Circle object from a Point and a radius" do
|
10
10
|
circle = Circle [1,2], 3
|
11
11
|
assert_kind_of(Geometry::Circle, circle)
|
12
12
|
end
|
13
13
|
|
14
|
-
must
|
14
|
+
it "must have a center point accessor" do
|
15
15
|
circle = Circle [1,2], 3
|
16
16
|
assert_equal(circle.center, [1,2])
|
17
17
|
end
|
18
18
|
|
19
|
-
must
|
19
|
+
it "must have a radius accessor" do
|
20
20
|
circle = Circle [1,2], 3
|
21
21
|
assert_equal(3, circle.radius)
|
22
22
|
end
|
data/test/geometry/edge.rb
CHANGED
@@ -1,40 +1,40 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/edge'
|
3
3
|
|
4
4
|
def Edge(*args)
|
5
5
|
Geometry::Edge.new(*args)
|
6
6
|
end
|
7
7
|
|
8
|
-
|
8
|
+
describe Geometry::Edge do
|
9
9
|
Edge = Geometry::Edge
|
10
10
|
|
11
|
-
must
|
12
|
-
edge =
|
11
|
+
it "must create an Edge object" do
|
12
|
+
edge = Edge.new([0,0], [1,0])
|
13
13
|
assert_kind_of(Geometry::Edge, edge)
|
14
14
|
assert_equal(Geometry::Point[0,0], edge.first)
|
15
15
|
assert_equal(Geometry::Point[1,0], edge.last)
|
16
16
|
end
|
17
|
-
must
|
18
|
-
edge =
|
19
|
-
assert_kind_of(
|
17
|
+
it "must create swap endpoints in place" do
|
18
|
+
edge = Edge.new([0,0], [1,0])
|
19
|
+
assert_kind_of(Edge, edge)
|
20
20
|
edge.reverse!
|
21
21
|
assert_equal(Geometry::Point[1,0], edge.first)
|
22
22
|
assert_equal(Geometry::Point[0,0], edge.last)
|
23
23
|
end
|
24
|
-
must
|
25
|
-
edge1 =
|
26
|
-
edge2 =
|
27
|
-
edge3 =
|
24
|
+
it "must handle equality" do
|
25
|
+
edge1 = Edge.new([1,0], [0,1])
|
26
|
+
edge2 = Edge.new([1,0], [0,1])
|
27
|
+
edge3 = Edge.new([1,1], [5,5])
|
28
28
|
assert_equal(edge1, edge2)
|
29
|
-
|
29
|
+
edge1.wont_equal edge3
|
30
30
|
end
|
31
31
|
|
32
|
-
must
|
32
|
+
it "must return the height of the edge" do
|
33
33
|
edge = Edge([0,0], [1,1])
|
34
34
|
assert_equal(1, edge.height)
|
35
35
|
end
|
36
36
|
|
37
|
-
must
|
37
|
+
it "must return the width of the edge" do
|
38
38
|
edge = Edge([0,0], [1,1])
|
39
39
|
assert_equal(1, edge.width)
|
40
40
|
end
|