geometry 3 → 4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,14 @@
1
+ require 'matrix'
2
+
3
+ # Monkeypatch Vector to overcome some deficiencies
4
+ class Vector
5
+ # !@group Unary operators
6
+ def +@
7
+ self
8
+ end
9
+
10
+ def -@
11
+ Vector[*(@elements.map {|e| -e })]
12
+ end
13
+ # !@endgroup
14
+ end
@@ -1,12 +1,13 @@
1
- require_relative 'helper'
2
- require_relative '../lib/geometry'
1
+ require 'minitest/autorun'
2
+ require 'geometry'
3
3
 
4
- class GeometryTest < Test::Unit::TestCase
5
- must "create a Point object" do
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
- must "create a Line object" do
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
@@ -1,22 +1,22 @@
1
- require_relative '../helper'
2
- require_relative '../../lib/geometry/circle'
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
- class CircleTest < Test::Unit::TestCase
9
- must "create a Circle object from a Point and a radius" do
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 "have a center point accessor" do
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 "have a radius accessor" do
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
@@ -1,40 +1,40 @@
1
- require_relative '../helper'
2
- require_relative '../../lib/geometry/edge'
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
- class EdgeTest < Test::Unit::TestCase
8
+ describe Geometry::Edge do
9
9
  Edge = Geometry::Edge
10
10
 
11
- must "create an Edge object" do
12
- edge = Geometry::Edge.new([0,0], [1,0])
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 "create swap endpoints in place" do
18
- edge = Geometry::Edge.new([0,0], [1,0])
19
- assert_kind_of(Geometry::Edge, edge)
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 "handle equality" do
25
- edge1 = Geometry::Edge.new([1,0], [0,1])
26
- edge2 = Geometry::Edge.new([1,0], [0,1])
27
- edge3 = Geometry::Edge.new([1,1], [5,5])
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
- assert_not_equal(edge1, edge3)
29
+ edge1.wont_equal edge3
30
30
  end
31
31
 
32
- must "return the height of the edge" do
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 "return the width of the edge" do
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