ruby3mf 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,135 +1,135 @@
1
- require 'singleton'
2
- require 'yaml'
3
-
4
- # Example usage:
5
-
6
- # Log3mf.context "box.3mf" do |l|
7
- # --do some stuff here
8
-
9
- # l.context "[Content-Types].xml" do |l|
10
- # -- try to parse file. if fail...
11
- # l.log(:fatal_error, "couldn't parse XML") <<<--- THIS WILL GENERATE FATAL ERROR EXCEPTION
12
- # end
13
-
14
- # l.context "examing Relations" do |l|
15
- # l.log(:error, "a non-fatal error")
16
- # l.log(:warning, "a warning")
17
- # l.log(:info, "it is warm today")
18
- # end
19
- # end
20
- #
21
- # Log3mf.to_json
22
-
23
-
24
- class Log3mf
25
- include Singleton
26
- include Interpolation
27
-
28
- LOG_LEVELS = [:fatal_error, :error, :warning, :info, :debug]
29
-
30
- SPEC_LINKS = {
31
- core: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf',
32
- material: 'http://3mf.io/wp-content/uploads/2015/04/3MFmaterialsSpec_1.0.1.pdf',
33
- production: 'http://3mf.io/wp-content/uploads/2016/07/3MFproductionSpec.pdf',
34
- slice: 'http://3mf.io/wp-content/uploads/2016/07/3MFsliceSpec.pdf',
35
- #opc: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-376,%20Fourth%20Edition,%20Part%202%20-%20Open%20Packaging%20Conventions.zip'
36
- opc: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf'
37
- }.freeze
38
-
39
- # Allows us to throw FatalErrors if we ever get errors of severity :fatal_error
40
- class FatalError < RuntimeError
41
- end
42
-
43
- def initialize
44
- @log_list = []
45
- @context_stack = []
46
- @ledger = []
47
- errormap_path = File.join(File.dirname(__FILE__), "errors.yml")
48
- @errormap = YAML.load_file(errormap_path)
49
- end
50
-
51
- def reset_log
52
- @log_list = []
53
- @context_stack = []
54
- end
55
-
56
- def self.reset_log
57
- Log3mf.instance.reset_log
58
- end
59
-
60
- def context (context_description, &block)
61
- @context_stack.push(context_description)
62
- retval = block.call(Log3mf.instance)
63
- @context_stack.pop
64
- retval
65
- end
66
-
67
- def self.context(context_description, &block)
68
- Log3mf.instance.context(context_description, &block)
69
- end
70
-
71
- def method_missing(name, *args, &block)
72
- if LOG_LEVELS.include? name.to_sym
73
- if [:fatal_error, :error, :debug].include? name.to_sym
74
- linenumber = caller_locations[0].to_s.split('/')[-1].split(':')[-2].to_s
75
- filename = caller_locations[0].to_s.split('/')[-1].split(':')[0].to_s
76
- options = {linenumber: linenumber, filename: filename}
77
- # Mike: do not call error or fatal_error without an entry in errors.yml
78
- raise "{fatal_}error called WITHOUT using error symbol from: #{filename}:#{linenumber}" if ( !(args[0].is_a? Symbol) && (name.to_sym != :debug) )
79
-
80
- puts "***** Log3mf.#{name} called from #{filename}:#{linenumber} *****" if $DEBUG
81
-
82
- options = options.merge(args[1]) if args[1]
83
- log(name.to_sym, args[0], options)
84
- else
85
- log(name.to_sym, *args)
86
- end
87
- else
88
- super
89
- end
90
- end
91
-
92
- def log(severity, message, options = {})
93
- error = @errormap.fetch(message.to_s) { {"msg" => message.to_s, "page" => nil} }
94
- options[:page] = error["page"] unless options[:page]
95
- options[:spec] = error["spec"] unless options[:spec]
96
- entry = {id: message,
97
- context: "#{@context_stack.join("/")}",
98
- severity: severity,
99
- message: interpolate(error["msg"], options)}
100
- entry[:spec_ref] = spec_link(options[:spec], options[:page]) if (options && options[:page])
101
- entry[:caller] = "#{options[:filename]}:#{options[:linenumber]}" if (options && options[:filename] && options[:linenumber])
102
- @log_list << entry
103
- raise FatalError if severity == :fatal_error
104
- end
105
-
106
- def count_entries(*levels)
107
- entries(*levels).count
108
- end
109
-
110
- def self.count_entries(*l)
111
- Log3mf.instance.count_entries(*l)
112
- end
113
-
114
- def entries(*levels)
115
- return @log_list if levels.size == 0
116
- @log_list.select { |i| levels.include? i[:severity] }
117
- end
118
-
119
- def self.entries(*l)
120
- Log3mf.instance.entries(*l)
121
- end
122
-
123
- def spec_link(spec, page)
124
- spec = :core unless spec
125
- "#{SPEC_LINKS[spec]}#page=#{page}"
126
- end
127
-
128
- def to_json
129
- @log_list.to_json
130
- end
131
-
132
- def self.to_json
133
- Log3mf.instance.to_json
134
- end
135
- end
1
+ require 'singleton'
2
+ require 'yaml'
3
+
4
+ # Example usage:
5
+
6
+ # Log3mf.context "box.3mf" do |l|
7
+ # --do some stuff here
8
+
9
+ # l.context "[Content-Types].xml" do |l|
10
+ # -- try to parse file. if fail...
11
+ # l.log(:fatal_error, "couldn't parse XML") <<<--- THIS WILL GENERATE FATAL ERROR EXCEPTION
12
+ # end
13
+
14
+ # l.context "examing Relations" do |l|
15
+ # l.log(:error, "a non-fatal error")
16
+ # l.log(:warning, "a warning")
17
+ # l.log(:info, "it is warm today")
18
+ # end
19
+ # end
20
+ #
21
+ # Log3mf.to_json
22
+
23
+
24
+ class Log3mf
25
+ include Singleton
26
+ include Interpolation
27
+
28
+ LOG_LEVELS = [:fatal_error, :error, :warning, :info, :debug]
29
+
30
+ SPEC_LINKS = {
31
+ core: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf',
32
+ material: 'http://3mf.io/wp-content/uploads/2015/04/3MFmaterialsSpec_1.0.1.pdf',
33
+ production: 'http://3mf.io/wp-content/uploads/2016/07/3MFproductionSpec.pdf',
34
+ slice: 'http://3mf.io/wp-content/uploads/2016/07/3MFsliceSpec.pdf',
35
+ #opc: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-376,%20Fourth%20Edition,%20Part%202%20-%20Open%20Packaging%20Conventions.zip'
36
+ opc: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf'
37
+ }.freeze
38
+
39
+ # Allows us to throw FatalErrors if we ever get errors of severity :fatal_error
40
+ class FatalError < RuntimeError
41
+ end
42
+
43
+ def initialize
44
+ @log_list = []
45
+ @context_stack = []
46
+ @ledger = []
47
+ errormap_path = File.join(File.dirname(__FILE__), "errors.yml")
48
+ @errormap = YAML.load_file(errormap_path)
49
+ end
50
+
51
+ def reset_log
52
+ @log_list = []
53
+ @context_stack = []
54
+ end
55
+
56
+ def self.reset_log
57
+ Log3mf.instance.reset_log
58
+ end
59
+
60
+ def context (context_description, &block)
61
+ @context_stack.push(context_description)
62
+ retval = block.call(Log3mf.instance)
63
+ @context_stack.pop
64
+ retval
65
+ end
66
+
67
+ def self.context(context_description, &block)
68
+ Log3mf.instance.context(context_description, &block)
69
+ end
70
+
71
+ def method_missing(name, *args, &block)
72
+ if LOG_LEVELS.include? name.to_sym
73
+ if [:fatal_error, :error, :debug].include? name.to_sym
74
+ linenumber = caller_locations[0].to_s.split('/')[-1].split(':')[-2].to_s
75
+ filename = caller_locations[0].to_s.split('/')[-1].split(':')[0].to_s
76
+ options = {linenumber: linenumber, filename: filename}
77
+ # Mike: do not call error or fatal_error without an entry in errors.yml
78
+ raise "{fatal_}error called WITHOUT using error symbol from: #{filename}:#{linenumber}" if ( !(args[0].is_a? Symbol) && (name.to_sym != :debug) )
79
+
80
+ puts "***** Log3mf.#{name} called from #{filename}:#{linenumber} *****" if $DEBUG
81
+
82
+ options = options.merge(args[1]) if args[1]
83
+ log(name.to_sym, args[0], options)
84
+ else
85
+ log(name.to_sym, *args)
86
+ end
87
+ else
88
+ super
89
+ end
90
+ end
91
+
92
+ def log(severity, message, options = {})
93
+ error = @errormap.fetch(message.to_s) { {"msg" => message.to_s, "page" => nil} }
94
+ options[:page] = error["page"] unless options[:page]
95
+ options[:spec] = error["spec"] unless options[:spec]
96
+ entry = {id: message,
97
+ context: "#{@context_stack.join("/")}",
98
+ severity: severity,
99
+ message: interpolate(error["msg"], options)}
100
+ entry[:spec_ref] = spec_link(options[:spec], options[:page]) if (options && options[:page])
101
+ entry[:caller] = "#{options[:filename]}:#{options[:linenumber]}" if (options && options[:filename] && options[:linenumber])
102
+ @log_list << entry
103
+ raise FatalError if severity == :fatal_error
104
+ end
105
+
106
+ def count_entries(*levels)
107
+ entries(*levels).count
108
+ end
109
+
110
+ def self.count_entries(*l)
111
+ Log3mf.instance.count_entries(*l)
112
+ end
113
+
114
+ def entries(*levels)
115
+ return @log_list if levels.size == 0
116
+ @log_list.select { |i| levels.include? i[:severity] }
117
+ end
118
+
119
+ def self.entries(*l)
120
+ Log3mf.instance.entries(*l)
121
+ end
122
+
123
+ def spec_link(spec, page)
124
+ spec = :core unless spec
125
+ "#{SPEC_LINKS[spec]}#page=#{page}"
126
+ end
127
+
128
+ def to_json
129
+ @log_list.to_json
130
+ end
131
+
132
+ def self.to_json
133
+ Log3mf.instance.to_json
134
+ end
135
+ end
@@ -1,80 +1,80 @@
1
- class MeshAnalyzer
2
-
3
- def self.validate_object(object, includes_material)
4
- Log3mf.context "verifying object" do |l|
5
- children = object.children.map { |child| child.name }
6
- have_override = object.attributes["pid"] or object.attributes["pindex"]
7
- l.error :object_with_components_and_pid if have_override && children.include?("components")
8
- end
9
-
10
- Log3mf.context "validating geometry" do |l|
11
- list = EdgeList.new
12
-
13
- # if a triangle has a pid, then the object needs a pid
14
- has_triangle_pid = false
15
-
16
- meshs = object.css('mesh')
17
- meshs.each do |mesh|
18
-
19
- num_vertices = mesh.css("vertex").count
20
- triangles = mesh.css("triangle")
21
- l.error :not_enough_triangles if triangles.count < 4
22
-
23
- if triangles
24
- triangles.each do |triangle|
25
-
26
- v1 = triangle.attributes["v1"].to_s.to_i
27
- v2 = triangle.attributes["v2"].to_s.to_i
28
- v3 = triangle.attributes["v3"].to_s.to_i
29
-
30
- l.error :invalid_vertex_index if [v1, v2, v3].select{|vertex| vertex >= num_vertices}.count > 0
31
-
32
- unless includes_material
33
- l.context "validating property overrides" do |l|
34
- property_overrides = []
35
- property_overrides << triangle.attributes['p1'].to_s.to_i if triangle.attributes['p1']
36
- property_overrides << triangle.attributes['p2'].to_s.to_i if triangle.attributes['p2']
37
- property_overrides << triangle.attributes['p3'].to_s.to_i if triangle.attributes['p3']
38
-
39
- property_overrides.reject! { |prop| prop.nil? }
40
- l.error :has_base_materials_gradient unless property_overrides.uniq.size <= 1
41
- end
42
- end
43
-
44
- if v1 == v2 || v2 == v3 || v3 == v1
45
- l.error :non_distinct_indices
46
- end
47
-
48
- list.add_edge(v1, v2)
49
- list.add_edge(v2, v3)
50
- list.add_edge(v3, v1)
51
- unless has_triangle_pid
52
- has_triangle_pid = triangle.attributes["pid"] != nil
53
- end
54
- end
55
-
56
- if has_triangle_pid && !(object.attributes["pindex"] && object.attributes["pid"])
57
- l.error :missing_object_pid
58
- end
59
-
60
- result = list.verify_edges
61
- if result == :bad_orientation
62
- l.error :resource_3dmodel_orientation
63
- elsif result == :hole
64
- l.error :resource_3dmodel_hole
65
- elsif result == :nonmanifold
66
- l.error :resource_3dmodel_nonmanifold
67
- end
68
-
69
- end
70
- end
71
- end
72
- end
73
-
74
- def self.validate(model_doc, includes_material)
75
- model_doc.css('model/resources/object').select { |object| ['model', 'solidsupport', ''].include?(object.attributes['type'].to_s) }.each do |object|
76
- validate_object(object, includes_material)
77
- end
78
- end
79
-
80
- end
1
+ class MeshAnalyzer
2
+
3
+ def self.validate_object(object, includes_material)
4
+ Log3mf.context "verifying object" do |l|
5
+ children = object.children.map { |child| child.name }
6
+ have_override = object.attributes["pid"] or object.attributes["pindex"]
7
+ l.error :object_with_components_and_pid if have_override && children.include?("components")
8
+ end
9
+
10
+ Log3mf.context "validating geometry" do |l|
11
+ list = EdgeList.new
12
+
13
+ # if a triangle has a pid, then the object needs a pid
14
+ has_triangle_pid = false
15
+
16
+ meshs = object.css('mesh')
17
+ meshs.each do |mesh|
18
+
19
+ num_vertices = mesh.css("vertex").count
20
+ triangles = mesh.css("triangle")
21
+ l.error :not_enough_triangles if triangles.count < 4
22
+
23
+ if triangles
24
+ triangles.each do |triangle|
25
+
26
+ v1 = triangle.attributes["v1"].to_s.to_i
27
+ v2 = triangle.attributes["v2"].to_s.to_i
28
+ v3 = triangle.attributes["v3"].to_s.to_i
29
+
30
+ l.error :invalid_vertex_index if [v1, v2, v3].select{|vertex| vertex >= num_vertices}.count > 0
31
+
32
+ unless includes_material
33
+ l.context "validating property overrides" do |l|
34
+ property_overrides = []
35
+ property_overrides << triangle.attributes['p1'].to_s.to_i if triangle.attributes['p1']
36
+ property_overrides << triangle.attributes['p2'].to_s.to_i if triangle.attributes['p2']
37
+ property_overrides << triangle.attributes['p3'].to_s.to_i if triangle.attributes['p3']
38
+
39
+ property_overrides.reject! { |prop| prop.nil? }
40
+ l.error :has_base_materials_gradient unless property_overrides.uniq.size <= 1
41
+ end
42
+ end
43
+
44
+ if v1 == v2 || v2 == v3 || v3 == v1
45
+ l.error :non_distinct_indices
46
+ end
47
+
48
+ list.add_edge(v1, v2)
49
+ list.add_edge(v2, v3)
50
+ list.add_edge(v3, v1)
51
+ unless has_triangle_pid
52
+ has_triangle_pid = triangle.attributes["pid"] != nil
53
+ end
54
+ end
55
+
56
+ if has_triangle_pid && !(object.attributes["pindex"] && object.attributes["pid"])
57
+ l.error :missing_object_pid
58
+ end
59
+
60
+ result = list.verify_edges
61
+ if result == :bad_orientation
62
+ l.error :resource_3dmodel_orientation
63
+ elsif result == :hole
64
+ l.error :resource_3dmodel_hole
65
+ elsif result == :nonmanifold
66
+ l.error :resource_3dmodel_nonmanifold
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def self.validate(model_doc, includes_material)
75
+ model_doc.css('model/resources/object').select { |object| ['model', 'solidsupport', ''].include?(object.attributes['type'].to_s) }.each do |object|
76
+ validate_object(object, includes_material)
77
+ end
78
+ end
79
+
80
+ end
@@ -1,219 +1,219 @@
1
- class MeshNormalAnalyzer
2
-
3
- def initialize(mesh)
4
- @vertices = []
5
- @intersections = []
6
-
7
- vertices_node = mesh.css("vertices")
8
- vertices_node.children.each do |vertex_node|
9
- if vertex_node.attributes.count > 0
10
- x = vertex_node.attributes['x'].to_s.to_f
11
- y = vertex_node.attributes['y'].to_s.to_f
12
- z = vertex_node.attributes['z'].to_s.to_f
13
- @vertices << [x, y, z]
14
- end
15
- end
16
-
17
- @triangles = []
18
- triangles_node = mesh.css("triangles")
19
- triangles_node.children.each do |triangle_node|
20
- if triangle_node.attributes.count > 0
21
- v1 = triangle_node.attributes['v1'].to_s.to_i
22
- v2 = triangle_node.attributes['v2'].to_s.to_i
23
- v3 = triangle_node.attributes['v3'].to_s.to_i
24
- @triangles << [v1, v2, v3]
25
- end
26
- end
27
- end
28
-
29
- def found_inward_triangle
30
- # Trace a ray toward the center of the vertex points. This will hopefully
31
- # maximize our chances of hitting the object's trianges on the first try.
32
- center = point_cloud_center(@vertices)
33
-
34
- @point = [0.0, 0.0, 0.0]
35
- @direction = vector_to(@point, center)
36
-
37
- # Make sure that we have a reasonably sized direction.
38
- # Might end up with a zero length vector if the center is also
39
- # at the origin.
40
- if magnitude(@direction) < 0.1
41
- @direction = [0.57, 0.57, 0.57]
42
- end
43
-
44
- # make the direction a unit vector just to make the
45
- # debug info easier to understand
46
- @direction = normalize(@direction)
47
-
48
- attempts = 0
49
- begin
50
- # Get all of the intersections from the ray and put them in order of distance.
51
- # The triangle we hit that's farthest from the start of the ray should always be
52
- # a triangle that points away from us (otherwise we would hit a triangle even
53
- # further away, assuming the mesh is closed).
54
- #
55
- # One special case is when the set of triangles we hit at that distance is greater
56
- # than one. In that case we might have hit a "corner" of the model and so we don't
57
- # know which of the two (or more) points away from us. In that case, cast a random
58
- # ray from the center of the object and try again.
59
-
60
- @triangles.each do |tri|
61
- v1 = @vertices[tri[0]]
62
- v2 = @vertices[tri[1]]
63
- v3 = @vertices[tri[2]]
64
-
65
- process_triangle(@point, @direction, [v1, v2, v3])
66
- end
67
-
68
- if @intersections.count > 0
69
- # Sort the intersections so we can find the hits that are furthest away.
70
- @intersections.sort! {|left, right| left[0] <=> right[0]}
71
-
72
- max_distance = @intersections.last[0]
73
- furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
74
-
75
- # Print out the hits
76
- # furthest_hits.each {|hit| puts hit[1].to_s}
77
-
78
- found_good_hit = furthest_hits.count == 1
79
- end
80
-
81
- if found_good_hit
82
- outside_triangle = furthest_hits.last[2]
83
- else
84
- @intersections = []
85
- attempts = attempts + 1
86
-
87
- target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
88
- @point = center
89
- @direction = normalize(vector_to(@point, target))
90
- end
91
- end until found_good_hit || attempts >= 10
92
-
93
- # return true if we hit a triangle with an inward pointing normal
94
- # (according to counter-clockwise normal orientation)
95
- found_good_hit && !compare_normals(outside_triangle, @direction)
96
- end
97
-
98
- def compare_normals(triangle, hit_direction)
99
- oriented_normal = cross_product(
100
- vector_to(triangle[0], triangle[1]),
101
- vector_to(triangle[1], triangle[2]))
102
-
103
- angle = angle_between(oriented_normal, hit_direction)
104
-
105
- angle < Math::PI / 2.0
106
- end
107
-
108
- def process_triangle(point, direction, triangle)
109
- found_intersection, t = intersect(point, direction, triangle)
110
-
111
- if t > 0
112
- intersection = []
113
- intersection[0] = point[0] + t * direction[0]
114
- intersection[1] = point[1] + t * direction[1]
115
- intersection[2] = point[2] + t * direction[2]
116
-
117
- @intersections << [t, intersection, triangle]
118
- end
119
- end
120
-
121
- def intersect(point, direction, triangle)
122
- v0 = triangle[0]
123
- v1 = triangle[1]
124
- v2 = triangle[2]
125
-
126
- return [false, 0] if v0.nil? || v1.nil? || v2.nil?
127
-
128
- e1 = vector_to(v0, v1)
129
- e2 = vector_to(v0, v2)
130
-
131
- h = cross_product(direction, e2)
132
- a = dot_product(e1, h)
133
-
134
- if a.abs < 0.00001
135
- return false, 0
136
- end
137
-
138
- f = 1.0/a
139
- s = vector_to(v0, point)
140
- u = f * dot_product(s, h)
141
-
142
- if u < 0.0 || u > 1.0
143
- return false, 0
144
- end
145
-
146
- q = cross_product(s, e1)
147
- v = f * dot_product(direction, q)
148
-
149
- if v < 0.0 || u + v > 1.0
150
- return false, 0
151
- end
152
-
153
- t = f * dot_product(e2, q)
154
- [t > 0, t]
155
- end
156
- end
157
-
158
- # Various utility functions
159
-
160
- def cross_product(a, b)
161
- result = [0, 0, 0]
162
- result[0] = a[1]*b[2] - a[2]*b[1]
163
- result[1] = a[2]*b[0] - a[0]*b[2]
164
- result[2] = a[0]*b[1] - a[1]*b[0]
165
-
166
- result
167
- end
168
-
169
- def dot_product(a, b)
170
- a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
171
- end
172
-
173
- def vector_to(a, b)
174
- [b[0] - a[0], b[1] - a[1], b[2] - a[2]]
175
- end
176
-
177
- def magnitude(a)
178
- Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
179
- end
180
-
181
- def equal(a, b)
182
- (a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
183
- end
184
-
185
- def angle_between(a, b)
186
- cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
187
- Math.acos(cos_theta)
188
- end
189
-
190
- def normalize(a)
191
- length = magnitude(a)
192
- [a[0]/length, a[1]/length, a[2]/length]
193
- end
194
-
195
- def point_cloud_center(vertices)
196
- if vertices.count < 1
197
- return [0, 0, 0]
198
- end
199
-
200
- vertex = vertices[0]
201
- min_x = max_x = vertex[0]
202
- min_y = max_y = vertex[1]
203
- min_z = max_z = vertex[2]
204
-
205
- vertices.each do |vertex|
206
- x = vertex[0]
207
- y = vertex[1]
208
- z = vertex[2]
209
-
210
- min_x = x if x < min_x
211
- max_x = x if x > max_x
212
- min_y = y if y < min_y
213
- max_y = y if y > max_y
214
- min_z = z if z < min_z
215
- max_z = z if z > max_z
216
- end
217
-
218
- [(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
1
+ class MeshNormalAnalyzer
2
+
3
+ def initialize(mesh)
4
+ @vertices = []
5
+ @intersections = []
6
+
7
+ vertices_node = mesh.css("vertices")
8
+ vertices_node.children.each do |vertex_node|
9
+ if vertex_node.attributes.count > 0
10
+ x = vertex_node.attributes['x'].to_s.to_f
11
+ y = vertex_node.attributes['y'].to_s.to_f
12
+ z = vertex_node.attributes['z'].to_s.to_f
13
+ @vertices << [x, y, z]
14
+ end
15
+ end
16
+
17
+ @triangles = []
18
+ triangles_node = mesh.css("triangles")
19
+ triangles_node.children.each do |triangle_node|
20
+ if triangle_node.attributes.count > 0
21
+ v1 = triangle_node.attributes['v1'].to_s.to_i
22
+ v2 = triangle_node.attributes['v2'].to_s.to_i
23
+ v3 = triangle_node.attributes['v3'].to_s.to_i
24
+ @triangles << [v1, v2, v3]
25
+ end
26
+ end
27
+ end
28
+
29
+ def found_inward_triangle
30
+ # Trace a ray toward the center of the vertex points. This will hopefully
31
+ # maximize our chances of hitting the object's trianges on the first try.
32
+ center = point_cloud_center(@vertices)
33
+
34
+ @point = [0.0, 0.0, 0.0]
35
+ @direction = vector_to(@point, center)
36
+
37
+ # Make sure that we have a reasonably sized direction.
38
+ # Might end up with a zero length vector if the center is also
39
+ # at the origin.
40
+ if magnitude(@direction) < 0.1
41
+ @direction = [0.57, 0.57, 0.57]
42
+ end
43
+
44
+ # make the direction a unit vector just to make the
45
+ # debug info easier to understand
46
+ @direction = normalize(@direction)
47
+
48
+ attempts = 0
49
+ begin
50
+ # Get all of the intersections from the ray and put them in order of distance.
51
+ # The triangle we hit that's farthest from the start of the ray should always be
52
+ # a triangle that points away from us (otherwise we would hit a triangle even
53
+ # further away, assuming the mesh is closed).
54
+ #
55
+ # One special case is when the set of triangles we hit at that distance is greater
56
+ # than one. In that case we might have hit a "corner" of the model and so we don't
57
+ # know which of the two (or more) points away from us. In that case, cast a random
58
+ # ray from the center of the object and try again.
59
+
60
+ @triangles.each do |tri|
61
+ v1 = @vertices[tri[0]]
62
+ v2 = @vertices[tri[1]]
63
+ v3 = @vertices[tri[2]]
64
+
65
+ process_triangle(@point, @direction, [v1, v2, v3])
66
+ end
67
+
68
+ if @intersections.count > 0
69
+ # Sort the intersections so we can find the hits that are furthest away.
70
+ @intersections.sort! {|left, right| left[0] <=> right[0]}
71
+
72
+ max_distance = @intersections.last[0]
73
+ furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
74
+
75
+ # Print out the hits
76
+ # furthest_hits.each {|hit| puts hit[1].to_s}
77
+
78
+ found_good_hit = furthest_hits.count == 1
79
+ end
80
+
81
+ if found_good_hit
82
+ outside_triangle = furthest_hits.last[2]
83
+ else
84
+ @intersections = []
85
+ attempts = attempts + 1
86
+
87
+ target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
88
+ @point = center
89
+ @direction = normalize(vector_to(@point, target))
90
+ end
91
+ end until found_good_hit || attempts >= 10
92
+
93
+ # return true if we hit a triangle with an inward pointing normal
94
+ # (according to counter-clockwise normal orientation)
95
+ found_good_hit && !compare_normals(outside_triangle, @direction)
96
+ end
97
+
98
+ def compare_normals(triangle, hit_direction)
99
+ oriented_normal = cross_product(
100
+ vector_to(triangle[0], triangle[1]),
101
+ vector_to(triangle[1], triangle[2]))
102
+
103
+ angle = angle_between(oriented_normal, hit_direction)
104
+
105
+ angle < Math::PI / 2.0
106
+ end
107
+
108
+ def process_triangle(point, direction, triangle)
109
+ found_intersection, t = intersect(point, direction, triangle)
110
+
111
+ if t > 0
112
+ intersection = []
113
+ intersection[0] = point[0] + t * direction[0]
114
+ intersection[1] = point[1] + t * direction[1]
115
+ intersection[2] = point[2] + t * direction[2]
116
+
117
+ @intersections << [t, intersection, triangle]
118
+ end
119
+ end
120
+
121
+ def intersect(point, direction, triangle)
122
+ v0 = triangle[0]
123
+ v1 = triangle[1]
124
+ v2 = triangle[2]
125
+
126
+ return [false, 0] if v0.nil? || v1.nil? || v2.nil?
127
+
128
+ e1 = vector_to(v0, v1)
129
+ e2 = vector_to(v0, v2)
130
+
131
+ h = cross_product(direction, e2)
132
+ a = dot_product(e1, h)
133
+
134
+ if a.abs < 0.00001
135
+ return false, 0
136
+ end
137
+
138
+ f = 1.0/a
139
+ s = vector_to(v0, point)
140
+ u = f * dot_product(s, h)
141
+
142
+ if u < 0.0 || u > 1.0
143
+ return false, 0
144
+ end
145
+
146
+ q = cross_product(s, e1)
147
+ v = f * dot_product(direction, q)
148
+
149
+ if v < 0.0 || u + v > 1.0
150
+ return false, 0
151
+ end
152
+
153
+ t = f * dot_product(e2, q)
154
+ [t > 0, t]
155
+ end
156
+ end
157
+
158
+ # Various utility functions
159
+
160
+ def cross_product(a, b)
161
+ result = [0, 0, 0]
162
+ result[0] = a[1]*b[2] - a[2]*b[1]
163
+ result[1] = a[2]*b[0] - a[0]*b[2]
164
+ result[2] = a[0]*b[1] - a[1]*b[0]
165
+
166
+ result
167
+ end
168
+
169
+ def dot_product(a, b)
170
+ a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
171
+ end
172
+
173
+ def vector_to(a, b)
174
+ [b[0] - a[0], b[1] - a[1], b[2] - a[2]]
175
+ end
176
+
177
+ def magnitude(a)
178
+ Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
179
+ end
180
+
181
+ def equal(a, b)
182
+ (a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
183
+ end
184
+
185
+ def angle_between(a, b)
186
+ cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
187
+ Math.acos(cos_theta)
188
+ end
189
+
190
+ def normalize(a)
191
+ length = magnitude(a)
192
+ [a[0]/length, a[1]/length, a[2]/length]
193
+ end
194
+
195
+ def point_cloud_center(vertices)
196
+ if vertices.count < 1
197
+ return [0, 0, 0]
198
+ end
199
+
200
+ vertex = vertices[0]
201
+ min_x = max_x = vertex[0]
202
+ min_y = max_y = vertex[1]
203
+ min_z = max_z = vertex[2]
204
+
205
+ vertices.each do |vertex|
206
+ x = vertex[0]
207
+ y = vertex[1]
208
+ z = vertex[2]
209
+
210
+ min_x = x if x < min_x
211
+ max_x = x if x > max_x
212
+ min_y = y if y < min_y
213
+ max_y = y if y > max_y
214
+ min_z = z if z < min_z
215
+ max_z = z if z > max_z
216
+ end
217
+
218
+ [(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
219
219
  end