ruby3mf 0.2.4 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e46e2c266255e1ff3e37964c3cce6306f9361ced
4
- data.tar.gz: fcb133940bece74939393383af9c4f0df4234ba2
3
+ metadata.gz: e6ed7b5e499cd264c483fb07b8b64b2e87d66468
4
+ data.tar.gz: 7e4d586f7171056fc27a30295fb778c278f0cda1
5
5
  SHA512:
6
- metadata.gz: d1569f55c29cc7b3617db2eea3bc529081dc6408a4191f38c781dc25620156ed4c5492a87a6ff3ca166af7dd2740ba7089cb29063286a3bb7c81e3f8d6477a0c
7
- data.tar.gz: c31e67696eebe5f88a65f3cce3ebc591d6792432039259dac4bba7f856479c5f1ee5b6b9ffe689e758397006a8127d6c2f5f7ed35324debd68abf000e5211453
6
+ metadata.gz: 65fea6f8723fd942bb26876ccc2fdb74877a0c441264979e1ee1d88f2416085a701b023553a40d5a2cc5ce51a5ff1ae62aae6f6c5968920ddf94a99938892640
7
+ data.tar.gz: 5666f43cf880c92b2a2a583375c6eb4a64598e75b1ea3d4e182beddcaec4f07841027241bcf08ca194bc9a27d208b6b03eb2c66010b58da199df2d7ae338737d
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  /spec/ruby3mf-testfiles/
12
12
  /profile-run/
13
13
  .DS_Store
14
+ suite_test.txt
data/bin/suite_test.sh CHANGED
@@ -6,7 +6,9 @@ GOOD_FILES=../3mf-test-suite/Positive/${MATCH}*.3mf
6
6
  BAD_FILES=../3mf-test-suite/Negative/${MATCH}*.3mf
7
7
 
8
8
  echo "Test Suite: filter: ${MATCH}"
9
- printf "\n\nTest Suite: filter: ${MATCH} - $(date)\n" >> ${OUTFILE}
9
+ printf "\n\n==============================================================================="
10
+ printf "\n\n Test Suite: filter: ${MATCH} - $(date)\n" >> ${OUTFILE}
11
+ printf "\n\n==============================================================================="
10
12
 
11
13
  echo "Positive Files -------------"
12
14
  printf "\nPositive Files -------------\n" >> ${OUTFILE}
@@ -35,5 +37,5 @@ for filename in ${BAD_FILES}; do
35
37
  done
36
38
 
37
39
  echo "All Done."
38
- printf "Completed -- $(date)\n\n" >> ${OUTFILE}
40
+ printf "\nCompleted -- $(date)\n\n" >> ${OUTFILE}
39
41
 
@@ -71,7 +71,7 @@ Items within this schema follow a simple naming convention of appending a prefix
71
71
  <xs:sequence>
72
72
  <xs:element ref="vertices"/>
73
73
  <xs:element ref="triangles"/>
74
- <xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="30000"/>
74
+ <xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
75
75
  </xs:sequence>
76
76
  </xs:complexType>
77
77
  <xs:complexType name="CT_Vertices">
@@ -87,7 +87,7 @@ Items within this schema follow a simple naming convention of appending a prefix
87
87
  </xs:complexType>
88
88
  <xs:complexType name="CT_Triangles">
89
89
  <xs:sequence>
90
- <xs:element ref="triangle" minOccurs="1" maxOccurs="30000"/>
90
+ <xs:element ref="triangle" minOccurs="1" maxOccurs="unbounded"/>
91
91
  </xs:sequence>
92
92
  </xs:complexType>
93
93
  <xs:complexType name="CT_Triangle">
@@ -62,10 +62,10 @@ class ContentTypes
62
62
  end
63
63
  end
64
64
  required_content_types.each do |req_type|
65
- l.error "[Content_Types].xml is missing required ContentType \"#{req_type}\"", page: 10 unless found_types.values.include? req_type
65
+ l.error :invalid_content_type, mt: req_type unless found_types.values.include? req_type
66
66
  end
67
67
  rescue Nokogiri::XML::SyntaxError => e
68
- l.error "[Content_Types].xml file is not valid XML. #{e}", page: 15
68
+ l.error :content_types_invalid_xml, e: "#{e}"
69
69
  end
70
70
  end
71
71
  return new(found_types, found_overrides)
@@ -60,6 +60,7 @@ class Document
60
60
  Log3mf.context 'zip' do |l|
61
61
  begin
62
62
  Zip.warn_invalid_date = false
63
+ Zip.unicode_names = true
63
64
 
64
65
  # check for the general purpose flag set - if so, warn that 3mf may not work on some systems
65
66
  File.open(input_file, "r") do |file|
@@ -77,13 +78,7 @@ class Document
77
78
  zip_file.each do |part|
78
79
  l.context "part names /#{part.name}" do |l|
79
80
  unless part.name.end_with? '[Content_Types].xml'
80
- begin
81
- u = URI part.name
82
- rescue ArgumentError, URI::InvalidURIError
83
- l.fatal_error "This NEVER Happens! mdw 12Jan2017"
84
- l.error :err_uri_bad
85
- next
86
- end
81
+ next unless u = parse_uri(l, part.name)
87
82
 
88
83
  u.path.split('/').each do |segment|
89
84
  l.error :err_uri_hidden_file if (segment.start_with? '.') && !(segment.end_with? '.rels')
@@ -98,7 +93,7 @@ class Document
98
93
  if content_type_match
99
94
  m.types = ContentTypes.parse(content_type_match)
100
95
  else
101
- l.fatal_error 'Missing required file: [Content_Types].xml', page: 4
96
+ l.fatal_error :missing_content_types
102
97
  end
103
98
  end
104
99
 
@@ -109,6 +104,17 @@ class Document
109
104
  zip_file.glob('**/*.rels').each do |rel|
110
105
  m.relationships[rel.name] = Relationships.parse(rel)
111
106
  end
107
+
108
+ root_rels = m.relationships['_rels/.rels']
109
+ unless root_rels.nil?
110
+ start_part_rel = root_rels.select { |rel| rel[:type] == Document::MODEL_TYPE }.first
111
+ if start_part_rel
112
+ start_part_target = start_part_rel[:target]
113
+ start_part_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == Document::MODEL_TYPE && rel[:target] == start_part_target }
114
+ l.error :invalid_startpart_target, :target => start_part_target if start_part_types.size > 1
115
+ end
116
+ end
117
+
112
118
  end
113
119
 
114
120
  l.context "print tickets" do |l|
@@ -120,22 +126,23 @@ class Document
120
126
  m.relationships.each do |file_name, rels|
121
127
  rels.each do |rel|
122
128
  l.context rel[:target] do |l|
123
-
124
- begin
125
- u = URI rel[:target]
126
- rescue URI::InvalidURIError
127
- l.error :err_uri_bad
128
- next
129
- end
129
+ next unless u = parse_uri(l, rel[:target])
130
130
 
131
131
  l.error :err_uri_relative_path unless u.to_s.start_with? '/'
132
132
 
133
133
  target = rel[:target].gsub(/^\//, "")
134
134
  l.error :err_uri_empty_segment if target.end_with? '/' or target.include? '//'
135
135
  l.error :err_uri_relative_path if target.include? '/../'
136
- relationship_file = zip_file.glob(target).first
137
- rel_type = rel[:type]
138
136
 
137
+ # necessary since it has been observed that rubyzip treats all zip entry names
138
+ # as ASCII-8BIT regardless if they really contain unicode chars. Without forcing
139
+ # the encoding we are unable to find the target within the zip if the target contains
140
+ # unicode chars.
141
+ zip_target = target
142
+ zip_target.force_encoding('ASCII-8BIT')
143
+ relationship_file = zip_file.glob(zip_target).first
144
+
145
+ rel_type = rel[:type]
139
146
  if relationship_file
140
147
  relationship_type = RELATIONSHIP_TYPES[rel_type]
141
148
  if relationship_type.nil?
@@ -161,7 +168,7 @@ class Document
161
168
  end
162
169
  end
163
170
  else
164
- l.error "Relationship Target file #{rel[:target]} not found", page: 11
171
+ l.error :rel_file_not_found, mf: "#{rel[:target]}"
165
172
  end
166
173
  end
167
174
  end
@@ -173,7 +180,7 @@ class Document
173
180
 
174
181
  return m
175
182
  rescue Zip::Error
176
- l.fatal_error 'File provided is not a valid ZIP archive', page: 9
183
+ l.fatal_error :not_a_zip
177
184
  end
178
185
  end
179
186
  rescue Log3mf::FatalError
@@ -214,4 +221,22 @@ class Document
214
221
  end
215
222
  end
216
223
 
224
+ private
225
+
226
+ def self.parse_uri(logger, uri)
227
+ begin
228
+ reserved_chars = /[\[\]]/
229
+
230
+ if uri =~ reserved_chars
231
+ logger.error :err_uri_bad
232
+ return nil
233
+ end
234
+
235
+ u = Addressable::URI.parse uri
236
+ rescue ArgumentError, Addressable::URI::InvalidURIError
237
+ logger.error :err_uri_bad
238
+ end
239
+ u
240
+ end
241
+
217
242
  end
@@ -1,41 +1,11 @@
1
- resource_contentype_invalid:
2
- msg: "Relationship target %{rt} resource has invalid contenttype %{bt}"
3
- page: 10
4
- err_uri_empty_segment:
5
- msg: 'No segment of a 3MF part name path may be empty'
6
- page: 13
7
- err_uri_hidden_file:
8
- msg: "Other than /_rels/.rels, no segment of a 3MF part name may start with the '.' character"
9
- page: 13
10
- err_uri_bad:
11
- msg: 'Path names must be valid Open Package Convention URIs or IRIs'
12
- page: 13
13
- err_uri_relative_path:
14
- msg: 'Part names must not include relative paths'
15
- page: 13
16
- model_resource_not_in_rels:
17
- msg: "Missing required resource: %{mr}. Resource referenced in model, but not in .rels relationship file"
18
- page: 10
19
- resource_3dmodel_orientation:
20
- msg: "Bad triangle orientation"
21
- page: 27
22
- resource_3dmodel_hole:
23
- msg: "Hole in model"
24
- page: 27
25
- resource_3dmodel_nonmanifold:
26
- msg: "Non-manifold edge in 3dmodel"
1
+ build_with_other_item:
2
+ msg: "build item with object of type other"
27
3
  page: 27
28
- 3d_model_invalid_xml:
29
- msg: "Model file invalid XML. Exception Start tag expected, '<' not found"
30
- page:
31
- 3d_payload_files:
32
- msg: "Relationship Target file /3D/Texture/texture2.texture not found"
33
- page: 11
34
4
  content_types_invalid_xml:
35
- msg: "[Content_Types].xml file is not valid XML. Start tag expected, '<' not found"
5
+ msg: "[Content_Types].xml file is not valid XML. %{e}"
36
6
  page: 15
37
7
  dot_rels_file_has_invalid_xml:
38
- msg: "Relationships (.rel) file is not a valid XML file: Start tag expected, '<' not found"
8
+ msg: "Relationships (.rel) file is not a valid XML file: %{e}"
39
9
  page: 4
40
10
  dot_rels_file_missing_relationships_element:
41
11
  msg: ".rels XML must have &lt;Relationships&gt; root element"
@@ -43,133 +13,176 @@ dot_rels_file_missing_relationships_element:
43
13
  dot_rels_file_no_relationship_element:
44
14
  msg: "No relationship elements found"
45
15
  page: 4
16
+ duplicate_content_extension_types:
17
+ msg: "Only one ContentType definition is allowed per extension"
18
+ page: 8
19
+ duplicate_content_override_types:
20
+ msg: "Only one override is allowed per part"
21
+ page: 8
22
+ empty_override_part_name:
23
+ msg: "Overrides can't have empty partname"
24
+ page: 8
25
+ err_uri_bad:
26
+ msg: 'Path names must be valid Open Package Convention URIs or IRIs'
27
+ page: 13
28
+ err_uri_empty_segment:
29
+ msg: 'No segment of a 3MF part name path may be empty'
30
+ page: 13
31
+ err_uri_hidden_file:
32
+ msg: "Other than /_rels/.rels, no segment of a 3MF part name may start with the '.' character"
33
+ page: 13
34
+ err_uri_relative_path:
35
+ msg: 'Part names must not include relative paths'
36
+ page: 13
37
+ has_base_materials_gradient:
38
+ msg: "Base materials form a gradient on one or more triangles. Interpolation of materials is not supported in the core spec."
39
+ page: 31
40
+ has_commas_for_floats:
41
+ msg: "numbers not formatted for the en-US locale"
42
+ page: 15
43
+ has_improper_base_color:
44
+ msg: "An sRGB color MUST be specified with a value of a 6 or 8 digit hexadecimal number"
45
+ page: 35
46
+ has_xml_space_attribute:
47
+ msg: "xml:space attribute is not allowed"
48
+ page: 16
46
49
  invalid_content_type:
47
- msg: "[Content_Types].xml is missing required ContentType \"application/vnd.openxmlformats-package.relationships+xml\""
48
- page: 10
49
- invalid_startpart_type:
50
- msg: "rels/.rels Relationship file is missing a required StartPart relationship to the primary 3D payload"
50
+ msg: "[Content_Types].xml is missing required ContentType \"%{mt}\""
51
51
  page: 10
52
+ invalid_image_content_type:
53
+ msg: "Invalid content type for %{extension}"
54
+ page: 22
55
+ invalid_metadata_name:
56
+ msg: "Metadata names must be prefixed with a valid namespace"
57
+ page: 21
58
+ invalid_metadata_under_defaultns:
59
+ msg: "Metadata without a namespace name must only contain allowed name values"
60
+ page: 21
61
+ invalid_model_unit:
62
+ msg: "Invalid unit value of '%{unit}' specified in model file."
63
+ page: 17
52
64
  invalid_startpart_target:
53
65
  msg: "Invalid StartPart target '%{target}'. The 3MF Document StartPart relationship MUST point to the 3D Model part that identifies the root of the 3D payload."
54
66
  page: 10
55
- unsupported_relationship_type:
56
- msg: "Validation of relationship type '%{type}' is not supported by this tool. The targeted part '%{target}' will not be validated."
57
- page: 10
58
- invalid_thumbnail_file:
59
- msg: "thumbnail file must be valid image file"
67
+ invalid_startpart_type:
68
+ msg: "rels/.rels Relationship file is missing a required StartPart relationship to the primary 3D payload"
60
69
  page: 10
70
+ invalid_texture_file_type:
71
+ msg: "Expected a png or jpeg texture but the texture was of type %{type}"
72
+ spec: :material
73
+ page: 16
61
74
  invalid_thumbnail_colorspace:
62
75
  msg: "CMYK JPEG images must not be used for the thumbnail"
63
76
  page: 36
77
+ invalid_thumbnail_file:
78
+ msg: "thumbnail file must be valid image file"
79
+ page: 10
64
80
  invalid_thumbnail_file_type:
65
81
  msg: "Expected a png or jpeg thumbnail but the thumbnail was of type %{type}"
66
82
  page: 36
67
- invalid_texture_file_type:
68
- msg: "Expected a png or jpeg texture but the texture was of type image/gif"
69
- page: 16
70
- missing_content_types:
71
- msg: "Missing required file: [Content_Types].xml"
72
- page: 4
83
+ invalid_vertex_index:
84
+ msg: "Triangle with a invalid vertex index"
85
+ page: 26
86
+ invalid_xml_core:
87
+ msg: "XML file doesn't pass validation with the XSD file"
88
+ page: 15
89
+ inward_facing_normal:
90
+ msg: "All trianges must be oriented with normals pointing away from interior"
91
+ page: 31
92
+ metadata_elements_with_same_name:
93
+ msg: "metadata elements must not share the same name"
94
+ page: 22
73
95
  missing_content_type:
74
96
  msg: "Unable to find an associated content type for part '%{part}' in [Content_Types].xml"
75
97
  page: 10
98
+ missing_content_types:
99
+ msg: "Missing required file: [Content_Types].xml"
100
+ page: 4
76
101
  missing_dot_rels_file:
77
102
  msg: "Missing required file _rels/.rels"
78
103
  page: 4
104
+ missing_extension_namespace_uri:
105
+ msg: "Required extension '%{ns}' MUST refer to namespace with URI"
106
+ page: 14
107
+ missing_model_children:
108
+ msg: "Model element must include resources and build as child elements"
109
+ page: 20
110
+ missing_object_pid:
111
+ msg: "Object with triangle color override missing object level pid"
112
+ page: 26
113
+ missing_object_reference:
114
+ msg: "3D objects not referenced by an item element"
115
+ page: 23
79
116
  missing_rels_folder:
80
117
  msg: "Missing required file _rels/.rels"
81
118
  page: 4
119
+ model_invalid_xml:
120
+ msg: "Model file invalid XML. Exception %{e}"
121
+ page:
122
+ model_resource_not_in_rels:
123
+ msg: "Missing required resource: %{mr}. Resource referenced in model, but not in .rels relationship file"
124
+ page: 10
125
+ multiple_print_tickets:
126
+ msg: "Only one print ticket allowed for any given model"
127
+ page: 13
128
+ multiple_relationships:
129
+ msg: "There MUST NOT be more than one relationship of a given relationship type from one part to a second part"
130
+ page: 10
131
+ non_distinct_indices:
132
+ msg: "The indices v1, v2 and v3 MUST be distinct."
133
+ page: 31
82
134
  non_unique_rel_id:
83
135
  msg: "The ID value '%{id}' appears more than once in '%{file}'. Within a rels file, all ID's must be unique"
84
136
  page: 8 #TODO:change this to page 23 of the OPC spec once logging supports linking to that spec
85
137
  spec: opc
86
- multiple_relationships:
87
- msg: "There MUST NOT be more than one relationship of a given relationship type from one part to a second part"
88
- page: 10
89
138
  not_a_zip:
90
139
  msg: "File provided is not a valid ZIP archive"
91
140
  page: 9
92
- zero_size_texture:
93
- msg: "Texture file must be valid image file"
94
- page: 16
95
- has_xml_space_attribute:
96
- msg: "xml:space attribute is not allowed"
97
- page: 16
98
- invalid_model_unit:
99
- msg: "Invalid unit value of '%{unit}' specified in model file."
100
- page: 17
101
- wrong_encoding:
102
- msg: "XML content must be UTF8 encoded"
103
- page: 15
104
- missing_object_pid:
105
- msg: "Object with triangle color override missing object level pid"
106
- page: 26
107
- missing_model_children:
108
- msg: "Model element must include resources and build as child elements"
109
- page: 20
141
+ not_enough_triangles:
142
+ msg: "Mesh has fewer than four triangles"
143
+ page: 30
110
144
  object_with_components_and_pid:
111
145
  msg: "object with components and pid or pindex"
112
146
  page: 26
113
- build_with_other_item:
114
- msg: "build item with object of type other"
147
+ rel_file_not_found:
148
+ msg: "Relationship Target file %{mf} not found"
149
+ page: 11
150
+ resource_3dmodel_hole:
151
+ msg: "Hole in model"
152
+ page: 27
153
+ resource_3dmodel_nonmanifold:
154
+ msg: "Non-manifold edge in 3dmodel"
155
+ page: 27
156
+ resource_3dmodel_orientation:
157
+ msg: "Bad triangle orientation"
115
158
  page: 27
159
+ resource_contentype_invalid:
160
+ msg: "Relationship target %{rt} resource has invalid contenttype %{bt}"
161
+ page: 10
116
162
  resource_id_collision:
117
163
  msg: "resources must be unique within the model"
118
164
  page: 22
119
165
  resource_pid_missing:
120
- msg: "A model resource referenced a property group id (pid) that is not present. Missing pid is: %{pid}"
166
+ msg: "A model resource referenced a property group id (pid) that is not present. Missing pid is: %{pid}"
121
167
  page: 20
122
- metadata_elements_with_same_name:
123
- msg: "metadata elements must not share the same name"
124
- page: 22
125
- multiple_print_tickets:
126
- msg: "Only one print ticket allowed for any given model"
127
- page: 13
168
+ schema_error:
169
+ msg: "Schema error found: %{e}"
170
+ thumbnail_image_type_mismatch:
171
+ msg: "Image not of declared type"
172
+ page: 36
128
173
  unknown_required_extension:
129
174
  msg: "Required extension not supported: %{ext}"
130
175
  page: 14
131
- missing_extension_namespace_uri:
132
- msg: "Required extension '%{ns}' MUST refer to namespace with URI"
133
- page: 14
134
- invalid_metadata_under_defaultns:
135
- msg: "Metadata without a namespace name must only contain allowed name values"
136
- page: 21
137
- invalid_metadata_name:
138
- msg: "Metadata names must be prefixed with a valid namespace"
139
- page: 21
140
- has_commas_for_floats:
141
- msg: "numbers not formatted for the en-US locale"
142
- page: 15
143
- invalid_xml_core:
144
- msg: "XML file doesn't pass validation with the XSD file"
176
+ unsupported_relationship_type:
177
+ msg: "Validation of relationship type '%{type}' is not supported by this tool. The targeted part '%{target}' will not be validated."
178
+ page: 10
179
+ wrong_encoding:
180
+ msg: "XML content must be UTF8 encoded"
145
181
  page: 15
146
- missing_object_reference:
147
- msg: "3D objects not referenced by an item element"
148
- page: 23
149
- non_distinct_indices:
150
- msg: "The indices v1, v2 and v3 MUST be distinct."
151
- page: 31
152
- has_improper_base_color:
153
- msg: "An sRGB color MUST be specified with a value of a 6 or 8 digit hexadecimal number"
154
- page: 35
155
- duplicate_content_extension_types:
156
- msg: "Only one ContentType definition is allowed per extension"
157
- page: 8
158
- invalid_image_content_type:
159
- msg: "Invalid content type for %{extension}"
160
- page: 22
161
- duplicate_content_override_types:
162
- msg: "Only one override is allowed per part"
163
- page: 8
164
- empty_override_part_name:
165
- msg: "Overrides can't have empty partname"
166
- page: 8
167
- not_enough_triangles:
168
- msg: "Mesh has fewer than four triangles"
169
- page: 30
170
- has_base_materials_gradient:
171
- msg: "Base materials form a gradient on one or more triangles. Interpolation of materials is not supported in the core spec."
172
- page: 31
173
- thumbnail_image_type_mismatch:
174
- msg: "Image not of declared type"
175
- page: 36
182
+ zero_size_texture:
183
+ msg: "Texture file must be valid image file"
184
+ spec: :material
185
+ page: 16
186
+ contains_xsi_namespace:
187
+ msg: "XML content must not use the xsi namespace as it is not defined in the XSD schema"
188
+ page: 15
@@ -70,8 +70,21 @@ class Log3mf
70
70
 
71
71
  def method_missing(name, *args, &block)
72
72
  if LOG_LEVELS.include? name.to_sym
73
- log(name.to_sym, *args)
74
- else+
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
75
88
  super
76
89
  end
77
90
  end
@@ -80,11 +93,12 @@ class Log3mf
80
93
  error = @errormap.fetch(message.to_s) { {"msg" => message.to_s, "page" => nil} }
81
94
  options[:page] = error["page"] unless options[:page]
82
95
  options[:spec] = error["spec"] unless options[:spec]
83
- entry = { id: message,
84
- context: "#{@context_stack.join("/")}",
85
- severity: severity,
86
- message: interpolate(error["msg"], options) }
96
+ entry = {id: message,
97
+ context: "#{@context_stack.join("/")}",
98
+ severity: severity,
99
+ message: interpolate(error["msg"], options)}
87
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])
88
102
  @log_list << entry
89
103
  raise FatalError if severity == :fatal_error
90
104
  end
@@ -1,14 +1,3 @@
1
- def find_child(node, child_name)
2
- node.children.each do |child|
3
- if child.name == child_name
4
- return child
5
- end
6
- end
7
-
8
- nil
9
- end
10
-
11
-
12
1
  class MeshAnalyzer
13
2
 
14
3
  def self.validate_object(object, includes_material)
@@ -27,6 +16,7 @@ class MeshAnalyzer
27
16
  meshs = object.css('mesh')
28
17
  meshs.each do |mesh|
29
18
 
19
+ num_vertices = mesh.css("vertex").count
30
20
  triangles = mesh.css("triangle")
31
21
  l.error :not_enough_triangles if triangles.count < 4
32
22
 
@@ -37,6 +27,8 @@ class MeshAnalyzer
37
27
  v2 = triangle.attributes["v2"].to_s.to_i
38
28
  v3 = triangle.attributes["v3"].to_s.to_i
39
29
 
30
+ l.error :invalid_vertex_index if [v1, v2, v3].select{|vertex| vertex >= num_vertices}.count > 0
31
+
40
32
  unless includes_material
41
33
  l.context "validating property overrides" do |l|
42
34
  property_overrides = []
@@ -0,0 +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]
219
+ end
@@ -19,7 +19,7 @@ class Model3mf
19
19
  begin
20
20
  model_doc = XmlVal.validate_parse(zip_entry, SchemaFiles::SchemaTemplate)
21
21
  rescue Nokogiri::XML::SyntaxError => e
22
- l.fatal_error "Model file invalid XML. Exception #{e}"
22
+ l.fatal_error :model_invalid_xml, e: e
23
23
  end
24
24
 
25
25
  l.context "verifying requiredextensions" do |l|
@@ -62,24 +62,10 @@ class Model3mf
62
62
  }
63
63
  end
64
64
 
65
- l.context "verifying StartPart relationship points to the root 3D Model" do |l|
66
- #Find the root 3D model which is pointed to by the start part
67
- root_rels = document.relationships['_rels/.rels']
68
- unless root_rels.nil?
69
- start_part_rel = root_rels.select { |rel| rel[:type] == Document::MODEL_TYPE }.first
70
- unless start_part_rel.nil? || start_part_rel[:target] != '/' + zip_entry.name
71
- #Verify that the model is a valid root 3D model by checking if it has at least one object
72
- l.fatal_error :invalid_startpart_target, :target => start_part_rel[:target] if model_doc.css("//model//resources//object").size == 0
73
- end
74
- else
75
- l.fatal_error :missing_dot_rels_file
76
- end
77
- end
78
-
79
65
  end
80
66
 
81
67
  l.context 'verifying resources' do |l|
82
- resources = find_child(model_doc.root, 'resources')
68
+ resources = model_doc.root.css("resources")
83
69
  if resources
84
70
  ids = resources.children.map { |child| child.attributes['id'].to_s if child.attributes['id'] }
85
71
  l.error :resource_id_collision if ids.uniq.size != ids.size
@@ -113,8 +99,19 @@ class Model3mf
113
99
  end
114
100
  end
115
101
  end
102
+
116
103
  includes_material = model_doc.namespaces.values.include?(MATERIAL_EXTENSION)
117
104
  MeshAnalyzer.validate(model_doc, includes_material)
105
+
106
+ l.context "verifying triangle normal" do |l|
107
+ model_doc.css('model/resources/object').select { |object| ['model', 'solidsupport', ''].include?(object.attributes['type'].to_s) }.each do |object|
108
+ meshes = object.css('mesh')
109
+ meshes.each do |mesh|
110
+ processor = MeshNormalAnalyzer.new(mesh)
111
+ l.error :inward_facing_normal if processor.found_inward_triangle
112
+ end
113
+ end
114
+ end
118
115
  end
119
116
  model_doc
120
117
  end
@@ -35,14 +35,14 @@ class Relationships
35
35
  end
36
36
  end
37
37
  else
38
- l.error "No relationship elements found", page: 4
38
+ l.error :dot_rels_file_no_relationship_element
39
39
  end
40
40
  else
41
- l.error ".rels XML must have &lt;Relationships&gt; root element", page: 4
41
+ l.error :dot_rels_file_missing_relationships_element
42
42
  end
43
43
 
44
44
  rescue Nokogiri::XML::SyntaxError => e
45
- l.error "Relationships (.rel) file is not a valid XML file: #{e.message}", page: 4
45
+ l.error :dot_rels_file_has_invalid_xml, e: "#{e.message}"
46
46
  end
47
47
  end
48
48
  relationships
@@ -11,9 +11,9 @@ class Texture3mf
11
11
  stream = relationship_file.get_input_stream
12
12
  img_type = MimeMagic.by_magic(stream)
13
13
  Log3mf.context "Texture3mf" do |l|
14
- l.fatal_error "Texture file must be valid image file", spec: :material, page: 16 unless img_type
14
+ l.fatal_error :zero_size_texture unless img_type
15
15
  l.debug "texture is of type: #{img_type}"
16
- l.error "Expected a png or jpeg texture but the texture was of type #{img_type}", spec: :material, page: 16 unless ['image/png', 'image/jpeg'].include? img_type.type
16
+ l.error(:invalid_texture_file_type, type: img_type) unless ['image/png', 'image/jpeg'].include? img_type.type
17
17
  end
18
18
  t
19
19
  end
@@ -1,3 +1,3 @@
1
1
  module Ruby3mf
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.5"
3
3
  end
@@ -17,6 +17,7 @@ class XmlVal
17
17
  l.error :dtd_not_allowed if dtd_exists?(file)
18
18
  l.error :has_commas_for_floats if bad_floating_numbers?(document)
19
19
  l.warning :missing_object_reference if objects_not_referenced?(document)
20
+ l.error :contains_xsi_namespace if contains_xsi_namespace?(document)
20
21
 
21
22
  if schema_filename
22
23
  Log3mf.context "validating core schema" do |l|
@@ -29,7 +30,7 @@ class XmlVal
29
30
  if error_involves_colorvalue?(error)
30
31
  l.error :has_improper_base_color
31
32
  else
32
- l.error error
33
+ l.error :schema_error, e: error
33
34
  end
34
35
  end
35
36
  end
@@ -66,4 +67,8 @@ class XmlVal
66
67
  def self.error_involves_colorvalue?(error)
67
68
  error.to_s.include?("ST_ColorValue") || error.to_s.include?("displaycolor")
68
69
  end
70
+
71
+ def self.contains_xsi_namespace?(document)
72
+ document.namespaces.has_value?('http://www.w3.org/2001/XMLSchema-instance')
73
+ end
69
74
  end
data/lib/ruby3mf.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "ruby3mf/texture3mf"
11
11
  require_relative "ruby3mf/xml_val"
12
12
  require_relative "ruby3mf/edge_list"
13
13
  require_relative "ruby3mf/mesh_analyzer"
14
+ require_relative "ruby3mf/mesh_normal_analyzer"
14
15
 
15
16
  require 'zip'
16
17
  require 'nokogiri'
@@ -18,6 +19,7 @@ require 'json'
18
19
  require 'mimemagic'
19
20
  require 'uri'
20
21
  require 'yaml'
22
+ require "addressable/uri"
21
23
 
22
24
  module Ruby3mf
23
25
  # Your code goes here...
data/ruby3mf.gemspec CHANGED
@@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency 'nokogiri', '~>1.6.8'
29
29
  spec.add_runtime_dependency 'mimemagic'
30
30
  spec.add_runtime_dependency 'mini_magick', '~> 4.6'
31
+ spec.add_runtime_dependency 'addressable', '~> 2.5'
31
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby3mf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Whitmarsh, Jeff Porter, and William Hertling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-24 00:00:00.000000000 Z
11
+ date: 2017-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '4.6'
125
+ - !ruby/object:Gem::Dependency
126
+ name: addressable
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.5'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.5'
125
139
  description: Read, write and validate 3MF files with Ruby easily.
126
140
  email:
127
141
  - mwhit@hp.com
@@ -139,7 +153,6 @@ files:
139
153
  - CODE_OF_CONDUCT.md
140
154
  - Gemfile
141
155
  - LICENSE.txt
142
- - PO_102_03.3mf
143
156
  - README.md
144
157
  - Rakefile
145
158
  - bin/batch.rb
@@ -163,6 +176,7 @@ files:
163
176
  - lib/ruby3mf/interpolation.rb
164
177
  - lib/ruby3mf/log3mf.rb
165
178
  - lib/ruby3mf/mesh_analyzer.rb
179
+ - lib/ruby3mf/mesh_normal_analyzer.rb
166
180
  - lib/ruby3mf/model3mf.rb
167
181
  - lib/ruby3mf/relationships.rb
168
182
  - lib/ruby3mf/schema_files.rb
data/PO_102_03.3mf DELETED
Binary file