ruby3mf 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/bin/suite_test.sh +4 -2
- data/lib/ruby3mf/3MFcoreSpec_1.1.xsd.template +2 -2
- data/lib/ruby3mf/content_types.rb +2 -2
- data/lib/ruby3mf/document.rb +44 -19
- data/lib/ruby3mf/errors.yml +136 -123
- data/lib/ruby3mf/log3mf.rb +20 -6
- data/lib/ruby3mf/mesh_analyzer.rb +3 -11
- data/lib/ruby3mf/mesh_normal_analyzer.rb +219 -0
- data/lib/ruby3mf/model3mf.rb +13 -16
- data/lib/ruby3mf/relationships.rb +3 -3
- data/lib/ruby3mf/texture3mf.rb +2 -2
- data/lib/ruby3mf/version.rb +1 -1
- data/lib/ruby3mf/xml_val.rb +6 -1
- data/lib/ruby3mf.rb +2 -0
- data/ruby3mf.gemspec +1 -0
- metadata +17 -3
- data/PO_102_03.3mf +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6ed7b5e499cd264c483fb07b8b64b2e87d66468
|
4
|
+
data.tar.gz: 7e4d586f7171056fc27a30295fb778c278f0cda1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65fea6f8723fd942bb26876ccc2fdb74877a0c441264979e1ee1d88f2416085a701b023553a40d5a2cc5ce51a5ff1ae62aae6f6c5968920ddf94a99938892640
|
7
|
+
data.tar.gz: 5666f43cf880c92b2a2a583375c6eb4a64598e75b1ea3d4e182beddcaec4f07841027241bcf08ca194bc9a27d208b6b03eb2c66010b58da199df2d7ae338737d
|
data/.gitignore
CHANGED
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\
|
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 "
|
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="
|
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="
|
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
|
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
|
68
|
+
l.error :content_types_invalid_xml, e: "#{e}"
|
69
69
|
end
|
70
70
|
end
|
71
71
|
return new(found_types, found_overrides)
|
data/lib/ruby3mf/document.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
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
|
data/lib/ruby3mf/errors.yml
CHANGED
@@ -1,41 +1,11 @@
|
|
1
|
-
|
2
|
-
msg: "
|
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.
|
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:
|
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 <Relationships> 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 \"
|
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
|
-
|
56
|
-
msg: "
|
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
|
-
|
68
|
-
msg: "
|
69
|
-
page:
|
70
|
-
|
71
|
-
msg: "
|
72
|
-
page:
|
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
|
-
|
93
|
-
msg: "
|
94
|
-
page:
|
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
|
-
|
114
|
-
msg: "
|
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:
|
166
|
+
msg: "A model resource referenced a property group id (pid) that is not present. Missing pid is: %{pid}"
|
121
167
|
page: 20
|
122
|
-
|
123
|
-
msg: "
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
132
|
-
msg: "
|
133
|
-
page:
|
134
|
-
|
135
|
-
msg: "
|
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
|
-
|
147
|
-
msg: "
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
data/lib/ruby3mf/log3mf.rb
CHANGED
@@ -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
|
-
|
74
|
-
|
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 = {
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
data/lib/ruby3mf/model3mf.rb
CHANGED
@@ -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
|
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 =
|
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
|
38
|
+
l.error :dot_rels_file_no_relationship_element
|
39
39
|
end
|
40
40
|
else
|
41
|
-
l.error
|
41
|
+
l.error :dot_rels_file_missing_relationships_element
|
42
42
|
end
|
43
43
|
|
44
44
|
rescue Nokogiri::XML::SyntaxError => e
|
45
|
-
l.error
|
45
|
+
l.error :dot_rels_file_has_invalid_xml, e: "#{e.message}"
|
46
46
|
end
|
47
47
|
end
|
48
48
|
relationships
|
data/lib/ruby3mf/texture3mf.rb
CHANGED
@@ -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
|
14
|
+
l.fatal_error :zero_size_texture unless img_type
|
15
15
|
l.debug "texture is of type: #{img_type}"
|
16
|
-
l.error
|
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
|
data/lib/ruby3mf/version.rb
CHANGED
data/lib/ruby3mf/xml_val.rb
CHANGED
@@ -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
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
|
+
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-
|
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
|