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