ruby3mf 0.2.5 → 0.2.6

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