geom 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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ coverage
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in geom.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'shoulda'
8
+ gem 'simplecov', :require => false
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/geom.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/geom/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Adrian Smith"]
6
+ gem.email = ["adrian.smith@ennova.com.au"]
7
+ gem.description = "A 3D geometry library that includes Point, Vector, Line, Plane, Coordinate System and Transformation objects"
8
+ gem.summary = "A 3D geometry library"
9
+ gem.homepage = "https://github.com/AdrianSmith/geom"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "geom"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Geom::VERSION
17
+ end
data/lib/geom/line.rb ADDED
@@ -0,0 +1,118 @@
1
+ module Geom
2
+ # Line defined by parametric equations X = X0 + XAt, Y = Y0 + YAt, ,Z = Z0 + ZAt
3
+ class Line
4
+ attr_accessor :x0, :xa, :y0, :ya, :z0, :za
5
+
6
+ def initialize(*args)
7
+
8
+ case args.size
9
+ when 2 # Start point and End point
10
+ start_point = args[0]
11
+ end_point = args[1]
12
+
13
+ line_direction = Vector.new(start_point, end_point)
14
+
15
+ @x0 = start_point.x
16
+ @y0 = start_point.y
17
+ @z0 = start_point.z
18
+ @xa = line_direction.x
19
+ @ya = line_direction.y
20
+ @za = line_direction.z
21
+
22
+ else # Coefficients of line equation
23
+ @x0, @xa, @y0, @ya, @z0, @za = args.flatten
24
+ end
25
+ end
26
+
27
+ def ==(line)
28
+ (@x0 - line.x0).abs < TOLERANCE &&
29
+ (@y0 - line.y0).abs < TOLERANCE &&
30
+ (@z0 - line.z0).abs < TOLERANCE &&
31
+ (@xa - line.xa).abs < TOLERANCE &&
32
+ (@ya - line.ya).abs < TOLERANCE &&
33
+ (@za - line.za).abs < TOLERANCE
34
+ end
35
+
36
+ alias_method :eql?, :==
37
+
38
+ def hash
39
+ (@x0.to_int ^ @y0.to_int ^ @z0.to_int ^ @xa.to_int ^ @ya.to_int ^ @za.to_int)
40
+ end
41
+
42
+ def direction
43
+ Vector.new(point_at_parameter(0), point_at_parameter(1))
44
+ end
45
+
46
+ def point_at_parameter(t)
47
+ Point.new((@x0 + @xa * t), (@y0 + @ya * t), (@z0 + @za * t))
48
+ end
49
+
50
+ def parameter_at_point(point)
51
+ if (@xa != 0)
52
+ ((point.x - @x0) / @xa)
53
+ elsif (@ya != 0)
54
+ ((point.y - @y0) / @ya)
55
+ else
56
+ ((point.z - @z0) / @za)
57
+ end
58
+ end
59
+
60
+ def closest_approach_parameter(line)
61
+ if self.direction.parallel?(line.direction)
62
+ raise ArgumentError.new("Lines are parallel")
63
+ else
64
+ s1 = Vector.new(@x0, @y0, @z0)
65
+ v1 = Vector.new(@xa, @ya, @za)
66
+ s2 = Vector.new(line.x0, line.y0, line.z0)
67
+ v2 = Vector.new(line.xa, line.ya, line.za)
68
+
69
+ i = 1 / ((v1.dot(v2) * v1.dot(v2)) - (v1.length * v1.length) * (v2.length * v2.length))
70
+ j11 = -(v2.length * v2.length)
71
+ j12 = v1.dot(v2)
72
+
73
+ vk = Vector.new(s1, s2)
74
+
75
+ k1 = vk.dot(v1)
76
+ k2 = vk.dot(v2)
77
+
78
+ i * (j11 * k1 + j12 * k2)
79
+ end
80
+ end
81
+
82
+ def intersection_with_line(line)
83
+ t1 = self.closest_approach_parameter(line)
84
+ t2 = line.closest_approach_parameter(self)
85
+
86
+ p1 = self.point_at_parameter(t1)
87
+ p2 = line.point_at_parameter(t2)
88
+
89
+ dist = p1.distance_to_point(p2)
90
+
91
+ if (dist < TOLERANCE)
92
+ p1
93
+ else
94
+ raise ArgumentError.new("Lines do not intersect")
95
+ end
96
+ end
97
+
98
+ def intersection_with_plane(plane)
99
+ var = [0,0]
100
+ var[0] = @x0 * plane.a + @y0 * plane.b + @z0 * plane.c
101
+ var[1] = @xa * plane.a + @ya * plane.b + @za * plane.c
102
+
103
+ raise ArgumentError.new("Line is parallel to the plane") if var[1] == 0
104
+
105
+ t = (plane.d - var[0]) / var[1]
106
+ Point.new((@x0 + @xa * t), (@y0 + @ya * t), (@z0 + @za * t))
107
+ end
108
+
109
+ def on_plane?(plane)
110
+ self.point_at_parameter(0).on_plane?(plane) && self.point_at_parameter(1).on_plane?(plane)
111
+ end
112
+
113
+ def to_s
114
+ "Line(%.3f,%.3f,%.3f,%.3f,%.3f,%.3f)" % [@x0, @xa, @y0, @ya, @z0, @za]
115
+ end
116
+
117
+ end
118
+ end
data/lib/geom/plane.rb ADDED
@@ -0,0 +1,71 @@
1
+ module Geom
2
+ # Plane defined by the equation:Ax + By + Cz = D
3
+ class Plane
4
+ attr_accessor :a, :b, :c, :d
5
+
6
+ def initialize(*args)
7
+
8
+ case args.size
9
+ when 2 # Point and normal vector
10
+ point = args[0]
11
+ normal = args[1]
12
+
13
+ @a = normal.x
14
+ @b = normal.y
15
+ @c = normal.z
16
+ @d = ((normal.x * point.x) + ((normal.y * point.y) + (normal.z * point.z)))
17
+
18
+ when 3 # Points on the plane
19
+ point_1 = args[0]
20
+ point_2 = args[1]
21
+ point_3 = args[2]
22
+
23
+ vector_12 = Vector.new(point_1, point_2)
24
+ vector_13 = Vector.new(point_1, point_3)
25
+ plane_normal = vector_12.cross(vector_13)
26
+
27
+ @a = plane_normal.x
28
+ @b = plane_normal.y
29
+ @c = plane_normal.z
30
+ @d = ((point_1.x * @a) + ((point_1.y * @b) + (point_1.z * @c)))
31
+
32
+ else # Coefficients of plane equation
33
+ @a, @b, @c, @d = args.flatten
34
+ end
35
+ self.normalize
36
+ end
37
+
38
+ def ==(plane)
39
+ (@a - plane.a).abs < TOLERANCE &&
40
+ (@b - plane.b).abs < TOLERANCE &&
41
+ (@c - plane.c).abs < TOLERANCE &&
42
+ (@d - plane.d).abs < TOLERANCE
43
+ end
44
+ alias_method :eql?, :==
45
+
46
+ def hash
47
+ (@a.to_int ^ @b.to_int ^ @c.to_int ^ @d.to_int)
48
+ end
49
+
50
+ def normal
51
+ Vector.new(@a, @b, @c).unitize
52
+ end
53
+
54
+ def to_s
55
+ "Plane(%.3f,%.3f,%.3f,%.3f)" % [@a, @b, @c, @d]
56
+ end
57
+
58
+ def normalize
59
+ if (@a == 0 && @b == 0 && @c == 0)
60
+ raise ArgumentError, "Plane definition error, points may be coincident or collinear"
61
+ else
62
+ norm_factor = 1.0 / Math.sqrt((@a * @a + @b * @b + @c * @c))
63
+ @a *= norm_factor
64
+ @b *= norm_factor
65
+ @c *= norm_factor
66
+ @d *= norm_factor
67
+ end
68
+ end
69
+
70
+ end
71
+ end
data/lib/geom/point.rb ADDED
@@ -0,0 +1,148 @@
1
+ module Geom
2
+ # Point defined by coordinates x, y, z
3
+ class Point
4
+ attr_accessor :x, :y, :z
5
+
6
+ def initialize(*args)
7
+ @x, @y, @z = args.flatten
8
+ end
9
+
10
+ def -(point)
11
+ Point.new(@x - point.x, @y - point.y, @z - point.z)
12
+ end
13
+
14
+ def +(point)
15
+ Point.new(@x + point.x, @y + point.y, @z + point.z)
16
+ end
17
+
18
+ def ==(point)
19
+ (@x - point.x).abs < TOLERANCE &&
20
+ (@y - point.y).abs < TOLERANCE &&
21
+ (@z - point.z).abs < TOLERANCE
22
+ end
23
+ alias_method :eql?, :==
24
+ alias_method :coincident?, :==
25
+
26
+ def hash
27
+ (@x.to_int ^ @y.to_int ^ @z.to_int)
28
+ end
29
+
30
+ def distance_to_point(point)
31
+ x_dist = @x - point.x
32
+ y_dist = @y - point.y
33
+ z_dist = @z - point.z
34
+ Math.sqrt(x_dist * x_dist + y_dist * y_dist + z_dist * z_dist)
35
+ end
36
+
37
+ def distance_to_plane(plane)
38
+ n = Vector.new(plane.a, plane.b, plane.c)
39
+ l = n.length
40
+ d = (plane.d / (l * l))
41
+ pnt = Point.new((plane.a * d), (plane.b * d), (plane.c * d))
42
+ vec = Vector.new(self, pnt)
43
+ vec.dot(n) / l
44
+ end
45
+
46
+ def translate(direction, distance=1)
47
+ transation_vector = direction.unitize.scale distance
48
+ Point.new(@x + transation_vector.x, @y + transation_vector.y, @z + transation_vector.z)
49
+ end
50
+
51
+ def transform(rectangular_coordinate_system)
52
+ m = Transformation.new(rectangular_coordinate_system).matrix
53
+ tx = m[0,0] * @x + m[0,1] * @y + m[0,2] * @z + m[0,3] * 1.0
54
+ ty = m[1,0] * @x + m[1,1] * @y + m[1,2] * @z + m[1,3] * 1.0
55
+ tz = m[2,0] * @x + m[2,1] * @y + m[2,2] * @z + m[2,3] * 1.0
56
+ Point.new(tx, ty, tz)
57
+ end
58
+
59
+ def project_onto_line(line)
60
+ l = line.xa * line.xa + line.ya * line.ya + line.za * line.za
61
+ m = 2 * (line.x0 - self.x) * line.xa + 2 * (line.y0 - self.y) * line.ya + 2 * (line.z0 - self.z) * line.za
62
+ t = -m / (2 * l)
63
+ Point.new(line.x0 + line.xa * t, line.y0 + line.ya * t, line.z0 + line.za * t)
64
+ end
65
+
66
+ def project_onto_line_along_vector(line, vector)
67
+ ref_line = Line.new(@x, vector.x, @y, vector.y, @z, vector.z)
68
+ line.intersection_with_line(ref_line)
69
+ end
70
+
71
+ def project_onto_plane(plane)
72
+ n = plane.normal
73
+ q = self.to_vector
74
+
75
+ r = (q.dot(n) - plane.d) / n.length
76
+
77
+ result = q - n.unitize.scale(r)
78
+ result.to_point
79
+ end
80
+
81
+ def project_onto_plane_along_vector(plane, vector)
82
+
83
+ l = Math.sqrt(plane.a * plane.a + plane.b * plane.b + plane.c * plane.c)
84
+ d = (plane.d / (l * l))
85
+ q = Point.new((plane.a * d), (plane.b * d), (plane.c * d))
86
+ v = Vector.new(q, self)
87
+
88
+ u = plane.normal
89
+ w = vector.scale((v.dot(u) / vector.dot(u)))
90
+ r = q.to_vector + (v - w)
91
+
92
+ r.to_point
93
+ end
94
+
95
+ def between?(first_point, second_point, include_ends=true)
96
+ line = Line.new(first_point, second_point)
97
+ projected_point = self.project_onto_line(line)
98
+
99
+ distance_12 = first_point.distance_to_point(second_point)
100
+ distance_T1 = first_point.distance_to_point(projected_point)
101
+ distance_T2 = second_point.distance_to_point(projected_point)
102
+
103
+ if include_ends
104
+ (distance_T1 < distance_12) && (distance_T2 < distance_12)
105
+ else
106
+ (distance_T1 <= distance_12) && (distance_T2 <= distance_12)
107
+ end
108
+ end
109
+
110
+ def on_plane?(plane)
111
+ projected_point = self.project_onto_plane(plane)
112
+ projected_point.distance_to_point(self).abs <= TOLERANCE
113
+ end
114
+
115
+ def on_line?(line)
116
+ projected_point = self.project_onto_line(line)
117
+ self.Distance(projected_point) <= Tolerance
118
+ end
119
+
120
+ def self.remove_coincident(points)
121
+ points.uniq
122
+ end
123
+
124
+ def self.average(points)
125
+ tx, ty, tz = 0, 0, 0
126
+ num = points.size.to_f
127
+ points.each do |point|
128
+ tx += point.x
129
+ ty += point.y
130
+ tz += point.z
131
+ end
132
+ Point.new(tx/num, ty/num, tz/num)
133
+ end
134
+
135
+ def to_vector
136
+ Vector.new(@x,@y,@z)
137
+ end
138
+
139
+ def to_s
140
+ "Point(%.3f,%.3f,%.3f)" % [@x, @y, @z]
141
+ end
142
+
143
+ def to_a
144
+ [@x, @y, @z]
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,77 @@
1
+ module Geom
2
+ class RectangularCoordinateSystem
3
+ attr_accessor :origin, :x_vector, :y_vector, :z_vector
4
+
5
+ def initialize
6
+ @origin = Point.new(0.0, 0.0, 0.0)
7
+ @x_vector = Vector.new(1.0, 0.0, 0.0)
8
+ @y_vector = Vector.new(0.0, 1.0, 0.0)
9
+ @z_vector = Vector.new(0.0, 0.0, 1.0)
10
+ end
11
+
12
+ def self.new_from_xvector_and_xyplane(origin, axis_vector, plane_vector)
13
+ rcs = self.new
14
+ rcs.origin = origin
15
+ rcs.x_vector = axis_vector.unitize
16
+ rcs.z_vector = plane_vector.unitize
17
+ rcs.y_vector = rcs.z_vector.cross(rcs.x_vector).unitize
18
+ rcs.z_vector = rcs.x_vector.cross(rcs.y_vector).unitize
19
+ rcs
20
+ end
21
+
22
+ def self.new_from_yvector_and_yzplane(origin, axis_vector, plane_vector)
23
+ rcs = self.new
24
+ rcs.origin = origin
25
+ rcs.y_vector = axis_vector.unitize
26
+ rcs.x_vector = plane_vector.unitize
27
+ rcs.z_vector = rcs.x_vector.cross(rcs.y_vector).unitize
28
+ rcs.x_vector = rcs.y_vector.cross(rcs.z_vector).unitize
29
+ rcs
30
+ end
31
+
32
+ def self.new_from_zvector_and_zxplane(origin, axis_vector, plane_vector)
33
+ rcs = self.new
34
+ rcs.origin = origin
35
+ rcs.z_vector = axis_vector.unitize
36
+ rcs.y_vector = plane_vector.unitize
37
+ rcs.x_vector = rcs.y_vector.cross(rcs.z_vector).unitize
38
+ rcs.y_vector = rcs.z_vector.cross(rcs.x_vector).unitize
39
+ rcs
40
+ end
41
+
42
+ def transformation_matrix
43
+ Matrix[
44
+ [@x_vector.x, @y_vector.x, @z_vector.x, @origin.x],
45
+ [@x_vector.y, @y_vector.y, @z_vector.y, @origin.y],
46
+ [@x_vector.z, @y_vector.z, @z_vector.z, @origin.z],
47
+ [0.0, 0.0, 0.0, 1.0]
48
+ ]
49
+ end
50
+
51
+ def to_s(verbose=false)
52
+ unless verbose
53
+ "RCS[#{@origin.to_s} X-#{@x_vector.to_s} Y-#{@y_vector.to_s} Z-#{@z_vector.to_s}]"
54
+ else
55
+ str = "Rectangular Coordinate System\n"
56
+ str += "Origin: #{@origin.to_s}\n"
57
+ str += "X-Vector: #{@x_vector.to_s}\n"
58
+ str += "Y-Vector: #{@y_vector.to_s}\n"
59
+ str += "Z-Vector: #{@z_vector.to_s}"
60
+ str
61
+ end
62
+ end
63
+
64
+ def == rcs
65
+ rcs.x_vector == @x_vector &&
66
+ rcs.y_vector == @y_vector &&
67
+ rcs.z_vector == @z_vector &&
68
+ rcs.origin == @origin
69
+ end
70
+
71
+ alias_method :eql?, :==
72
+
73
+ def hash
74
+ (@x_vector.hash ^ @y_vector.hash ^ @z_vector.hash ^ @origin.hash)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Geom
2
+ TOLERANCE = 10 ** -12
3
+ end
@@ -0,0 +1,93 @@
1
+ module Geom
2
+ # 3D transformation object that is composed of a 4x4 matrix representing the
3
+ # rotation and translation components
4
+ # rx1 ry1 rz1 tx
5
+ # rx2 ry2 rz2 ty
6
+ # rx3 ry3 rz3 tz
7
+ # 0 0 0 1
8
+ # Transformations are applied by using the * operator
9
+ # Available transformations include translation and rotation
10
+ class Transformation
11
+ attr_accessor :type, :matrix, :translation_vector
12
+
13
+ TRANSLATION = 1
14
+ ROTATION = 2
15
+ SCALING = 4
16
+
17
+ def initialize(coordinate_system)
18
+ @matrix = coordinate_system.transformation_matrix.inverse * 1.0 # ensure initialization as float not integer
19
+ @translation_vector = Vector.new(Point.new(0.0, 0.0, 0.0), coordinate_system.origin)
20
+ @type = 0 # initialize as integer not boolean
21
+
22
+ @type |= ROTATION if self.rotation_submatrix_orthogonal? && !rotation_submatrix_identity?
23
+ @type |= TRANSLATION unless self.translation_vector.zero?
24
+ @type |= SCALING unless self.scale_vector.unitity?
25
+
26
+ raise ArgumentError, "Transformation is non-linear" unless self.rotation_submatrix_orthogonal? or self.rotation_submatrix_diagonal
27
+ end
28
+
29
+ def type_description
30
+ names = ["Type #{@type}"]
31
+ names << 'Translation' if self.translation?
32
+ names << 'Rotation' if self.rotation?
33
+ names << 'Scaling' if self.scaling?
34
+ names.join(' ')
35
+ end
36
+
37
+ def rotation_submatrix
38
+ @matrix.minor(0..2,0..2)
39
+ end
40
+
41
+ def rotation_submatrix_orthogonal?
42
+ self.rotation_submatrix.transpose == self.rotation_submatrix.inverse
43
+ end
44
+
45
+ def rotation_submatrix_diagonal?
46
+ @matrix[1,0] == 0.0 && @matrix[2,0] == 0.0 && @matrix[2,1] == 0.0 &&
47
+ @matrix[0,1] == 0.0 && @matrix[0,2] == 0.0 && @matrix[1,2] == 0.0 &&
48
+ @matrix[0,0].abs > TOLERANCE && @matrix[1,1].abs > TOLERANCE && @matrix[2,2].abs > TOLERANCE
49
+ end
50
+
51
+ def identity?
52
+ @matrix == Matrix.identity(4) * 1.0
53
+ end
54
+
55
+ def rotation_submatrix_identity?
56
+ self.rotation_submatrix == Matrix.identity(3) * 1.0
57
+ end
58
+
59
+ def scaling?
60
+ SCALING & self.type > 0.0
61
+ end
62
+
63
+ def translation?
64
+ TRANSLATION & self.type > 0.0
65
+ end
66
+
67
+ def rotation?
68
+ ROTATION & self.type > 0.0
69
+ end
70
+
71
+ def scale_vector
72
+ Vector.new(
73
+ Math.sqrt(@matrix[0,0]*@matrix[0,0] + @matrix[0,1]*@matrix[0,1] + @matrix[0,2]*@matrix[0,2]),
74
+ Math.sqrt(@matrix[1,0]*@matrix[1,0] + @matrix[1,1]*@matrix[1,1] + @matrix[1,2]*@matrix[1,2]),
75
+ Math.sqrt(@matrix[2,0]*@matrix[2,0] + @matrix[2,1]*@matrix[2,1] + @matrix[2,2]*@matrix[2,2])
76
+ )
77
+ end
78
+
79
+ def to_s(verbose=false)
80
+ unless verbose
81
+ "Transformation #{@matrix.to_s}"
82
+ else
83
+ str = "Transformation\n"
84
+ str += " Vx | Vy | Vz | T\n"
85
+ str += sprintf("%9.2e | %9.2e | %9.2e | %9.2e\n", @matrix[0,0], @matrix[0,1], @matrix[0,2], @matrix[0,3])
86
+ str += sprintf("%9.2e | %9.2e | %9.2e | %9.2e\n", @matrix[1,0], @matrix[1,1], @matrix[1,2], @matrix[1,3])
87
+ str += sprintf("%9.2e | %9.2e | %9.2e | %9.2e\n", @matrix[2,0], @matrix[2,1], @matrix[2,2], @matrix[2,3])
88
+ str += sprintf("%9.2e | %9.2e | %9.2e | %9.2e", @matrix[3,0], @matrix[3,1], @matrix[3,2], @matrix[3,3])
89
+ str
90
+ end
91
+ end
92
+ end
93
+ end