geometry-in-ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/geometry-in-ruby.gemspec +23 -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/composition.rb +39 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/lib/geometry.rb +22 -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/composition.rb +49 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- data/test/geometry.rb +5 -0
- metadata +117 -0
@@ -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,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
|
data/lib/geometry.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'geometry/arc'
|
2
|
+
require_relative 'geometry/circle'
|
3
|
+
require_relative 'geometry/line'
|
4
|
+
require_relative 'geometry/obround'
|
5
|
+
require_relative 'geometry/path'
|
6
|
+
require_relative 'geometry/point'
|
7
|
+
require_relative 'geometry/point_zero'
|
8
|
+
require_relative 'geometry/polygon'
|
9
|
+
require_relative 'geometry/polyline'
|
10
|
+
require_relative 'geometry/rectangle'
|
11
|
+
require_relative 'geometry/regular_polygon'
|
12
|
+
require_relative 'geometry/rotation'
|
13
|
+
require_relative 'geometry/size'
|
14
|
+
require_relative 'geometry/size_zero'
|
15
|
+
require_relative 'geometry/square'
|
16
|
+
require_relative 'geometry/transformation'
|
17
|
+
require_relative 'geometry/triangle'
|
18
|
+
require_relative 'geometry/vector'
|
19
|
+
require_relative 'geometry/text'
|
20
|
+
|
21
|
+
module Geometry
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/arc'
|
3
|
+
|
4
|
+
describe Geometry::Arc do
|
5
|
+
Arc = Geometry::Arc
|
6
|
+
|
7
|
+
describe "when constructed" do
|
8
|
+
it "must accept a center point, radius, start and end angles" do
|
9
|
+
arc = Geometry::Arc.new center:[1,2], radius:3, start:0, end:90
|
10
|
+
arc.must_be_kind_of Geometry::Arc
|
11
|
+
arc.center.must_equal Point[1,2]
|
12
|
+
arc.radius.must_equal 3
|
13
|
+
arc.start_angle.must_equal 0
|
14
|
+
arc.end_angle.must_equal 90
|
15
|
+
end
|
16
|
+
|
17
|
+
it "must create an Arc from center, start and end points" do
|
18
|
+
arc = Geometry::Arc.new center:[1,2], start:[3,4], end:[5,6]
|
19
|
+
arc.must_be_kind_of Geometry::Arc
|
20
|
+
arc.center.must_equal Point[1,2]
|
21
|
+
arc.first.must_equal Point[3,4]
|
22
|
+
arc.last.must_equal Point[5,6]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/circle'
|
3
|
+
|
4
|
+
describe Geometry::Circle do
|
5
|
+
Circle = Geometry::Circle
|
6
|
+
|
7
|
+
describe "when constructed with center and radius arguments" do
|
8
|
+
let(:circle) { Circle.new [1,2], 3 }
|
9
|
+
|
10
|
+
it "must create a Circle" do
|
11
|
+
circle.must_be_instance_of(Circle)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "must have a center point accessor" do
|
15
|
+
circle.center.must_equal Point[1,2]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "must have a radius accessor" do
|
19
|
+
circle.radius.must_equal 3
|
20
|
+
end
|
21
|
+
|
22
|
+
it "must compare equal" do
|
23
|
+
circle.must_equal Circle.new([1,2], 3)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "when constructed with named center and radius arguments" do
|
28
|
+
let(:circle) { Circle.new :center => [1,2], :radius => 3 }
|
29
|
+
|
30
|
+
it "must create a Circle" do
|
31
|
+
circle.must_be_instance_of(Circle)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "must have a center point accessor" do
|
35
|
+
circle.center.must_equal Point[1,2]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "must have a radius accessor" do
|
39
|
+
circle.radius.must_equal 3
|
40
|
+
end
|
41
|
+
|
42
|
+
it "must compare equal" do
|
43
|
+
(circle == Circle.new(:center => [1,2], :radius => 3)).must_equal true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "when constructed with named center and diameter arguments" do
|
48
|
+
let(:circle) { Circle.new center:[1,2], diameter:4 }
|
49
|
+
|
50
|
+
it "must be a CenterDiameterCircle" do
|
51
|
+
circle.must_be_instance_of(Geometry::CenterDiameterCircle)
|
52
|
+
circle.must_be_kind_of(Circle)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "must have a center" do
|
56
|
+
circle.center.must_equal Point[1,2]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "must have a diameter" do
|
60
|
+
circle.diameter.must_equal 4
|
61
|
+
end
|
62
|
+
|
63
|
+
it "must calculate the correct radius" do
|
64
|
+
circle.radius.must_equal 2
|
65
|
+
end
|
66
|
+
|
67
|
+
it "must compare equal" do
|
68
|
+
circle.must_equal Circle.new([1,2], :diameter => 4)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "when constructed with a diameter and no center" do
|
73
|
+
let(:circle) { Circle.new :diameter => 4 }
|
74
|
+
|
75
|
+
it "must be a CenterDiameterCircle" do
|
76
|
+
circle.must_be_instance_of(Geometry::CenterDiameterCircle)
|
77
|
+
circle.must_be_kind_of(Circle)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "must have a nil center" do
|
81
|
+
circle.center.must_be_kind_of Geometry::PointZero
|
82
|
+
end
|
83
|
+
|
84
|
+
it "must have a diameter" do
|
85
|
+
circle.diameter.must_equal 4
|
86
|
+
end
|
87
|
+
|
88
|
+
it "must calculate the correct radius" do
|
89
|
+
circle.radius.must_equal 2
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "properties" do
|
94
|
+
subject { Circle.new center:[1,2], :diameter => 4 }
|
95
|
+
|
96
|
+
it "must have a bounds property that returns a Rectangle" do
|
97
|
+
subject.bounds.must_equal Rectangle.new([-1,0], [3,4])
|
98
|
+
end
|
99
|
+
|
100
|
+
it "must have a minmax property that returns the corners of the bounding rectangle" do
|
101
|
+
subject.minmax.must_equal [Point[-1,0], Point[3,4]]
|
102
|
+
end
|
103
|
+
|
104
|
+
it "must have a max property that returns the upper right corner of the bounding rectangle" do
|
105
|
+
subject.max.must_equal Point[3,4]
|
106
|
+
end
|
107
|
+
|
108
|
+
it "must have a min property that returns the lower left corner of the bounding rectangle" do
|
109
|
+
subject.min.must_equal Point[-1,0]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'geometry/edge'
|
3
|
+
|
4
|
+
def Edge(*args)
|
5
|
+
Geometry::Edge.new(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Geometry::Edge do
|
9
|
+
Edge = Geometry::Edge
|
10
|
+
subject { Geometry::Edge.new [0,0], [1,1] }
|
11
|
+
|
12
|
+
it "must create an Edge object" do
|
13
|
+
edge = Edge.new([0,0], [1,0])
|
14
|
+
assert_kind_of(Geometry::Edge, edge)
|
15
|
+
assert_equal(Geometry::Point[0,0], edge.first)
|
16
|
+
assert_equal(Geometry::Point[1,0], edge.last)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "must handle equality" do
|
20
|
+
edge1 = Edge.new([1,0], [0,1])
|
21
|
+
edge2 = Edge.new([1,0], [0,1])
|
22
|
+
edge3 = Edge.new([1,1], [5,5])
|
23
|
+
assert_equal(edge1, edge2)
|
24
|
+
edge1.wont_equal edge3
|
25
|
+
end
|
26
|
+
|
27
|
+
it "must return the height of the edge" do
|
28
|
+
edge = Edge([0,0], [1,1])
|
29
|
+
assert_equal(1, edge.height)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "must return the width of the edge" do
|
33
|
+
edge = Edge([0,0], [1,1])
|
34
|
+
assert_equal(1, edge.width)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "must convert an Edge to a Vector" do
|
38
|
+
Edge.new([0,0], [1,0]).vector.must_equal Vector[1,0]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "must return the normalized direction of a vector" do
|
42
|
+
Edge.new([0,0], [1,0]).direction.must_equal Vector[1,0]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "must return true for parallel edges" do
|
46
|
+
Edge.new([0,0], [1,0]).parallel?(Edge.new([0,0], [1,0])).must_equal 1
|
47
|
+
Edge.new([0,0], [1,0]).parallel?(Edge.new([1,0], [2,0])).must_equal 1
|
48
|
+
Edge.new([0,0], [1,0]).parallel?(Edge.new([3,0], [4,0])).must_equal 1
|
49
|
+
Edge.new([0,0], [1,0]).parallel?(Edge.new([3,1], [4,1])).must_equal 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "must return false for non-parallel edges" do
|
53
|
+
Edge.new([0,0], [2,0]).parallel?(Edge.new([1,-1], [1,1])).must_equal false
|
54
|
+
end
|
55
|
+
|
56
|
+
it "must clone and reverse" do
|
57
|
+
reversed = subject.reverse
|
58
|
+
reversed.to_a.must_equal subject.to_a.reverse
|
59
|
+
reversed.wont_be_same_as subject
|
60
|
+
end
|
61
|
+
|
62
|
+
it "must reverse itself" do
|
63
|
+
original = subject.to_a
|
64
|
+
subject.reverse!
|
65
|
+
subject.to_a.must_equal original.reverse
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "spaceship" do
|
69
|
+
it "ascending with a Point" do
|
70
|
+
edge = Edge.new [0,0], [1,1]
|
71
|
+
(edge <=> Point[0,0]).must_equal 0
|
72
|
+
(edge <=> Point[1,0]).must_equal -1
|
73
|
+
(edge <=> Point[0,1]).must_equal 1
|
74
|
+
(edge <=> Point[2,2]).must_equal nil
|
75
|
+
end
|
76
|
+
|
77
|
+
it "descending with a Point" do
|
78
|
+
edge = Edge.new [1,1], [0,0]
|
79
|
+
(edge <=> Point[0,0]).must_equal 0
|
80
|
+
(edge <=> Point[1,0]).must_equal 1
|
81
|
+
(edge <=> Point[0,1]).must_equal -1
|
82
|
+
(edge <=> Point[2,2]).must_equal nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "when finding an intersection" do
|
87
|
+
it "must find the intersection of two end-intersecting Edges" do
|
88
|
+
intersection = Edge.new([0,0],[1,1]).intersection(Edge.new([0,1],[1,1]))
|
89
|
+
intersection.must_be_kind_of Geometry::Point
|
90
|
+
intersection.must_equal Geometry::Point[1,1]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "must find the intersection of two collinear end-intersecting Edges" do
|
94
|
+
intersection = Edge.new([2,2], [0,2]).intersection(Edge.new([3,2], [2,2]))
|
95
|
+
intersection.must_be_kind_of Geometry::Point
|
96
|
+
intersection.must_equal Geometry::Point[2,2]
|
97
|
+
|
98
|
+
intersection = Edge.new([0,2], [2,2]).intersection(Edge.new([2,2], [3,2]))
|
99
|
+
intersection.must_be_kind_of Geometry::Point
|
100
|
+
intersection.must_equal Geometry::Point[2,2]
|
101
|
+
end
|
102
|
+
|
103
|
+
it "must find the itersection of two crossed Edges" do
|
104
|
+
edge1 = Edge.new [0.0, 0], [2.0, 2.0]
|
105
|
+
edge2 = Edge.new [2.0, 0], [0.0, 2.0]
|
106
|
+
intersection = edge1.intersection edge2
|
107
|
+
intersection.must_be_kind_of Geometry::Point
|
108
|
+
intersection.must_equal Geometry::Point[1,1]
|
109
|
+
end
|
110
|
+
|
111
|
+
it "must return nil for two edges that do not intersect" do
|
112
|
+
Edge.new([0,0],[1,0]).intersection(Edge.new([0,1],[1,1])).must_equal nil
|
113
|
+
end
|
114
|
+
|
115
|
+
it "must return true for two collinear and overlapping edges" do
|
116
|
+
Edge.new([0,0],[2,0]).intersection(Edge.new([1,0],[3,0])).must_equal true
|
117
|
+
end
|
118
|
+
|
119
|
+
it "must return false for collinear but non-overlapping edges" do
|
120
|
+
Edge.new([0,0],[2,0]).intersection(Edge.new([3,0],[4,0])).must_equal false
|
121
|
+
Edge.new([0,0],[0,2]).intersection(Edge.new([0,3],[0,4])).must_equal false
|
122
|
+
end
|
123
|
+
|
124
|
+
it "must return nil for two parallel but not collinear edges" do
|
125
|
+
Edge.new([0,0],[2,0]).intersection(Edge.new([1,1],[3,1])).must_equal nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it "must return nil for two perpendicular but not interseting edges" do
|
129
|
+
Edge.new([0, 0], [2, 0]).intersection(Edge.new([3, 3], [3, 1])).must_equal nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|