geometry 3 → 4
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.
- 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
|