geometry-in-ruby 0.0.1
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/.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
|