ruby3mf 0.2.5 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +13 -13
- data/.rspec +2 -2
- data/bin/batch.rb +3 -1
- data/bin/cli.rb +12 -12
- data/bin/console +14 -14
- data/bin/folder_test.sh +13 -13
- data/bin/suite_test.sh +41 -41
- data/lib/ruby3mf.rb +26 -26
- data/lib/ruby3mf/3MFcoreSpec_1.1.xsd.template +188 -188
- data/lib/ruby3mf/content_types.rb +77 -73
- data/lib/ruby3mf/document.rb +242 -242
- data/lib/ruby3mf/edge_list.rb +60 -60
- data/lib/ruby3mf/errors.yml +188 -188
- data/lib/ruby3mf/log3mf.rb +135 -135
- data/lib/ruby3mf/mesh_analyzer.rb +80 -80
- data/lib/ruby3mf/mesh_normal_analyzer.rb +218 -218
- data/lib/ruby3mf/model3mf.rb +117 -118
- data/lib/ruby3mf/relationships.rb +50 -50
- data/lib/ruby3mf/schema_files.rb +26 -26
- data/lib/ruby3mf/texture3mf.rb +29 -29
- data/lib/ruby3mf/thumbnail3mf.rb +21 -21
- data/lib/ruby3mf/version.rb +3 -3
- data/lib/ruby3mf/xml.xsd +286 -286
- data/lib/ruby3mf/xml_val.rb +68 -74
- data/ruby3mf.gemspec +32 -32
- data/suite.011917.out +236 -236
- metadata +3 -9
- data/foo/2D/ffffa2c3-ba74-4bea-a4d0-167a4211134d.model +0 -18747
- data/foo/3D/3dmodel.model +0 -40
- data/foo/3D/_rels/3dmodel.model.rels +0 -4
- data/foo/Thumbnails/ffffa6c3-ba74-4bea-a4d0-167a4211134d.model +0 -0
- data/foo/[Content_Types].xml +0 -7
- data/foo/_rels/.rels +0 -5
data/lib/ruby3mf/log3mf.rb
CHANGED
@@ -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
|