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.
@@ -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