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