ruby3mf 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +13 -13
- data/.rspec +2 -2
- data/bin/cli.rb +12 -12
- data/bin/console +14 -14
- data/bin/folder_test.sh +13 -13
- data/bin/suite_test.sh +41 -41
- data/lib/ruby3mf/3MFcoreSpec_1.1.xsd.template +188 -188
- data/lib/ruby3mf/content_types.rb +79 -77
- data/lib/ruby3mf/document.rb +242 -242
- data/lib/ruby3mf/edge_list.rb +60 -60
- data/lib/ruby3mf/errors.yml +191 -188
- data/lib/ruby3mf/log3mf.rb +135 -135
- data/lib/ruby3mf/mesh_analyzer.rb +80 -80
- data/lib/ruby3mf/mesh_normal_analyzer.rb +218 -218
- data/lib/ruby3mf/model3mf.rb +120 -117
- data/lib/ruby3mf/relationships.rb +50 -50
- data/lib/ruby3mf/schema_files.rb +26 -26
- data/lib/ruby3mf/texture3mf.rb +29 -29
- data/lib/ruby3mf/thumbnail3mf.rb +21 -21
- data/lib/ruby3mf/version.rb +3 -3
- data/lib/ruby3mf/xml.xsd +286 -286
- data/lib/ruby3mf/xml_val.rb +68 -68
- data/lib/ruby3mf.rb +26 -26
- data/ruby3mf.gemspec +32 -32
- metadata +3 -4
- data/suite.011917.out +0 -237
@@ -1,219 +1,219 @@
|
|
1
|
-
class MeshNormalAnalyzer
|
2
|
-
|
3
|
-
def initialize(mesh)
|
4
|
-
@vertices = []
|
5
|
-
@intersections = []
|
6
|
-
|
7
|
-
vertices_node = mesh.css("vertices")
|
8
|
-
vertices_node.children.each do |vertex_node|
|
9
|
-
if vertex_node.attributes.count > 0
|
10
|
-
x = vertex_node.attributes['x'].to_s.to_f
|
11
|
-
y = vertex_node.attributes['y'].to_s.to_f
|
12
|
-
z = vertex_node.attributes['z'].to_s.to_f
|
13
|
-
@vertices << [x, y, z]
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
@triangles = []
|
18
|
-
triangles_node = mesh.css("triangles")
|
19
|
-
triangles_node.children.each do |triangle_node|
|
20
|
-
if triangle_node.attributes.count > 0
|
21
|
-
v1 = triangle_node.attributes['v1'].to_s.to_i
|
22
|
-
v2 = triangle_node.attributes['v2'].to_s.to_i
|
23
|
-
v3 = triangle_node.attributes['v3'].to_s.to_i
|
24
|
-
@triangles << [v1, v2, v3]
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def found_inward_triangle
|
30
|
-
# Trace a ray toward the center of the vertex points. This will hopefully
|
31
|
-
# maximize our chances of hitting the object's trianges on the first try.
|
32
|
-
center = point_cloud_center(@vertices)
|
33
|
-
|
34
|
-
@point = [0.0, 0.0, 0.0]
|
35
|
-
@direction = vector_to(@point, center)
|
36
|
-
|
37
|
-
# Make sure that we have a reasonably sized direction.
|
38
|
-
# Might end up with a zero length vector if the center is also
|
39
|
-
# at the origin.
|
40
|
-
if magnitude(@direction) < 0.1
|
41
|
-
@direction = [0.57, 0.57, 0.57]
|
42
|
-
end
|
43
|
-
|
44
|
-
# make the direction a unit vector just to make the
|
45
|
-
# debug info easier to understand
|
46
|
-
@direction = normalize(@direction)
|
47
|
-
|
48
|
-
attempts = 0
|
49
|
-
begin
|
50
|
-
# Get all of the intersections from the ray and put them in order of distance.
|
51
|
-
# The triangle we hit that's farthest from the start of the ray should always be
|
52
|
-
# a triangle that points away from us (otherwise we would hit a triangle even
|
53
|
-
# further away, assuming the mesh is closed).
|
54
|
-
#
|
55
|
-
# One special case is when the set of triangles we hit at that distance is greater
|
56
|
-
# than one. In that case we might have hit a "corner" of the model and so we don't
|
57
|
-
# know which of the two (or more) points away from us. In that case, cast a random
|
58
|
-
# ray from the center of the object and try again.
|
59
|
-
|
60
|
-
@triangles.each do |tri|
|
61
|
-
v1 = @vertices[tri[0]]
|
62
|
-
v2 = @vertices[tri[1]]
|
63
|
-
v3 = @vertices[tri[2]]
|
64
|
-
|
65
|
-
process_triangle(@point, @direction, [v1, v2, v3])
|
66
|
-
end
|
67
|
-
|
68
|
-
if @intersections.count > 0
|
69
|
-
# Sort the intersections so we can find the hits that are furthest away.
|
70
|
-
@intersections.sort! {|left, right| left[0] <=> right[0]}
|
71
|
-
|
72
|
-
max_distance = @intersections.last[0]
|
73
|
-
furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
|
74
|
-
|
75
|
-
# Print out the hits
|
76
|
-
# furthest_hits.each {|hit| puts hit[1].to_s}
|
77
|
-
|
78
|
-
found_good_hit = furthest_hits.count == 1
|
79
|
-
end
|
80
|
-
|
81
|
-
if found_good_hit
|
82
|
-
outside_triangle = furthest_hits.last[2]
|
83
|
-
else
|
84
|
-
@intersections = []
|
85
|
-
attempts = attempts + 1
|
86
|
-
|
87
|
-
target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
|
88
|
-
@point = center
|
89
|
-
@direction = normalize(vector_to(@point, target))
|
90
|
-
end
|
91
|
-
end until found_good_hit || attempts >= 10
|
92
|
-
|
93
|
-
# return true if we hit a triangle with an inward pointing normal
|
94
|
-
# (according to counter-clockwise normal orientation)
|
95
|
-
found_good_hit && !compare_normals(outside_triangle, @direction)
|
96
|
-
end
|
97
|
-
|
98
|
-
def compare_normals(triangle, hit_direction)
|
99
|
-
oriented_normal = cross_product(
|
100
|
-
vector_to(triangle[0], triangle[1]),
|
101
|
-
vector_to(triangle[1], triangle[2]))
|
102
|
-
|
103
|
-
angle = angle_between(oriented_normal, hit_direction)
|
104
|
-
|
105
|
-
angle < Math::PI / 2.0
|
106
|
-
end
|
107
|
-
|
108
|
-
def process_triangle(point, direction, triangle)
|
109
|
-
found_intersection, t = intersect(point, direction, triangle)
|
110
|
-
|
111
|
-
if t > 0
|
112
|
-
intersection = []
|
113
|
-
intersection[0] = point[0] + t * direction[0]
|
114
|
-
intersection[1] = point[1] + t * direction[1]
|
115
|
-
intersection[2] = point[2] + t * direction[2]
|
116
|
-
|
117
|
-
@intersections << [t, intersection, triangle]
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def intersect(point, direction, triangle)
|
122
|
-
v0 = triangle[0]
|
123
|
-
v1 = triangle[1]
|
124
|
-
v2 = triangle[2]
|
125
|
-
|
126
|
-
return [false, 0] if v0.nil? || v1.nil? || v2.nil?
|
127
|
-
|
128
|
-
e1 = vector_to(v0, v1)
|
129
|
-
e2 = vector_to(v0, v2)
|
130
|
-
|
131
|
-
h = cross_product(direction, e2)
|
132
|
-
a = dot_product(e1, h)
|
133
|
-
|
134
|
-
if a.abs < 0.00001
|
135
|
-
return false, 0
|
136
|
-
end
|
137
|
-
|
138
|
-
f = 1.0/a
|
139
|
-
s = vector_to(v0, point)
|
140
|
-
u = f * dot_product(s, h)
|
141
|
-
|
142
|
-
if u < 0.0 || u > 1.0
|
143
|
-
return false, 0
|
144
|
-
end
|
145
|
-
|
146
|
-
q = cross_product(s, e1)
|
147
|
-
v = f * dot_product(direction, q)
|
148
|
-
|
149
|
-
if v < 0.0 || u + v > 1.0
|
150
|
-
return false, 0
|
151
|
-
end
|
152
|
-
|
153
|
-
t = f * dot_product(e2, q)
|
154
|
-
[t > 0, t]
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# Various utility functions
|
159
|
-
|
160
|
-
def cross_product(a, b)
|
161
|
-
result = [0, 0, 0]
|
162
|
-
result[0] = a[1]*b[2] - a[2]*b[1]
|
163
|
-
result[1] = a[2]*b[0] - a[0]*b[2]
|
164
|
-
result[2] = a[0]*b[1] - a[1]*b[0]
|
165
|
-
|
166
|
-
result
|
167
|
-
end
|
168
|
-
|
169
|
-
def dot_product(a, b)
|
170
|
-
a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
171
|
-
end
|
172
|
-
|
173
|
-
def vector_to(a, b)
|
174
|
-
[b[0] - a[0], b[1] - a[1], b[2] - a[2]]
|
175
|
-
end
|
176
|
-
|
177
|
-
def magnitude(a)
|
178
|
-
Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
|
179
|
-
end
|
180
|
-
|
181
|
-
def equal(a, b)
|
182
|
-
(a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
|
183
|
-
end
|
184
|
-
|
185
|
-
def angle_between(a, b)
|
186
|
-
cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
|
187
|
-
Math.acos(cos_theta)
|
188
|
-
end
|
189
|
-
|
190
|
-
def normalize(a)
|
191
|
-
length = magnitude(a)
|
192
|
-
[a[0]/length, a[1]/length, a[2]/length]
|
193
|
-
end
|
194
|
-
|
195
|
-
def point_cloud_center(vertices)
|
196
|
-
if vertices.count < 1
|
197
|
-
return [0, 0, 0]
|
198
|
-
end
|
199
|
-
|
200
|
-
vertex = vertices[0]
|
201
|
-
min_x = max_x = vertex[0]
|
202
|
-
min_y = max_y = vertex[1]
|
203
|
-
min_z = max_z = vertex[2]
|
204
|
-
|
205
|
-
vertices.each do |vertex|
|
206
|
-
x = vertex[0]
|
207
|
-
y = vertex[1]
|
208
|
-
z = vertex[2]
|
209
|
-
|
210
|
-
min_x = x if x < min_x
|
211
|
-
max_x = x if x > max_x
|
212
|
-
min_y = y if y < min_y
|
213
|
-
max_y = y if y > max_y
|
214
|
-
min_z = z if z < min_z
|
215
|
-
max_z = z if z > max_z
|
216
|
-
end
|
217
|
-
|
218
|
-
[(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
|
1
|
+
class MeshNormalAnalyzer
|
2
|
+
|
3
|
+
def initialize(mesh)
|
4
|
+
@vertices = []
|
5
|
+
@intersections = []
|
6
|
+
|
7
|
+
vertices_node = mesh.css("vertices")
|
8
|
+
vertices_node.children.each do |vertex_node|
|
9
|
+
if vertex_node.attributes.count > 0
|
10
|
+
x = vertex_node.attributes['x'].to_s.to_f
|
11
|
+
y = vertex_node.attributes['y'].to_s.to_f
|
12
|
+
z = vertex_node.attributes['z'].to_s.to_f
|
13
|
+
@vertices << [x, y, z]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@triangles = []
|
18
|
+
triangles_node = mesh.css("triangles")
|
19
|
+
triangles_node.children.each do |triangle_node|
|
20
|
+
if triangle_node.attributes.count > 0
|
21
|
+
v1 = triangle_node.attributes['v1'].to_s.to_i
|
22
|
+
v2 = triangle_node.attributes['v2'].to_s.to_i
|
23
|
+
v3 = triangle_node.attributes['v3'].to_s.to_i
|
24
|
+
@triangles << [v1, v2, v3]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def found_inward_triangle
|
30
|
+
# Trace a ray toward the center of the vertex points. This will hopefully
|
31
|
+
# maximize our chances of hitting the object's trianges on the first try.
|
32
|
+
center = point_cloud_center(@vertices)
|
33
|
+
|
34
|
+
@point = [0.0, 0.0, 0.0]
|
35
|
+
@direction = vector_to(@point, center)
|
36
|
+
|
37
|
+
# Make sure that we have a reasonably sized direction.
|
38
|
+
# Might end up with a zero length vector if the center is also
|
39
|
+
# at the origin.
|
40
|
+
if magnitude(@direction) < 0.1
|
41
|
+
@direction = [0.57, 0.57, 0.57]
|
42
|
+
end
|
43
|
+
|
44
|
+
# make the direction a unit vector just to make the
|
45
|
+
# debug info easier to understand
|
46
|
+
@direction = normalize(@direction)
|
47
|
+
|
48
|
+
attempts = 0
|
49
|
+
begin
|
50
|
+
# Get all of the intersections from the ray and put them in order of distance.
|
51
|
+
# The triangle we hit that's farthest from the start of the ray should always be
|
52
|
+
# a triangle that points away from us (otherwise we would hit a triangle even
|
53
|
+
# further away, assuming the mesh is closed).
|
54
|
+
#
|
55
|
+
# One special case is when the set of triangles we hit at that distance is greater
|
56
|
+
# than one. In that case we might have hit a "corner" of the model and so we don't
|
57
|
+
# know which of the two (or more) points away from us. In that case, cast a random
|
58
|
+
# ray from the center of the object and try again.
|
59
|
+
|
60
|
+
@triangles.each do |tri|
|
61
|
+
v1 = @vertices[tri[0]]
|
62
|
+
v2 = @vertices[tri[1]]
|
63
|
+
v3 = @vertices[tri[2]]
|
64
|
+
|
65
|
+
process_triangle(@point, @direction, [v1, v2, v3])
|
66
|
+
end
|
67
|
+
|
68
|
+
if @intersections.count > 0
|
69
|
+
# Sort the intersections so we can find the hits that are furthest away.
|
70
|
+
@intersections.sort! {|left, right| left[0] <=> right[0]}
|
71
|
+
|
72
|
+
max_distance = @intersections.last[0]
|
73
|
+
furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
|
74
|
+
|
75
|
+
# Print out the hits
|
76
|
+
# furthest_hits.each {|hit| puts hit[1].to_s}
|
77
|
+
|
78
|
+
found_good_hit = furthest_hits.count == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
if found_good_hit
|
82
|
+
outside_triangle = furthest_hits.last[2]
|
83
|
+
else
|
84
|
+
@intersections = []
|
85
|
+
attempts = attempts + 1
|
86
|
+
|
87
|
+
target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
|
88
|
+
@point = center
|
89
|
+
@direction = normalize(vector_to(@point, target))
|
90
|
+
end
|
91
|
+
end until found_good_hit || attempts >= 10
|
92
|
+
|
93
|
+
# return true if we hit a triangle with an inward pointing normal
|
94
|
+
# (according to counter-clockwise normal orientation)
|
95
|
+
found_good_hit && !compare_normals(outside_triangle, @direction)
|
96
|
+
end
|
97
|
+
|
98
|
+
def compare_normals(triangle, hit_direction)
|
99
|
+
oriented_normal = cross_product(
|
100
|
+
vector_to(triangle[0], triangle[1]),
|
101
|
+
vector_to(triangle[1], triangle[2]))
|
102
|
+
|
103
|
+
angle = angle_between(oriented_normal, hit_direction)
|
104
|
+
|
105
|
+
angle < Math::PI / 2.0
|
106
|
+
end
|
107
|
+
|
108
|
+
def process_triangle(point, direction, triangle)
|
109
|
+
found_intersection, t = intersect(point, direction, triangle)
|
110
|
+
|
111
|
+
if t > 0
|
112
|
+
intersection = []
|
113
|
+
intersection[0] = point[0] + t * direction[0]
|
114
|
+
intersection[1] = point[1] + t * direction[1]
|
115
|
+
intersection[2] = point[2] + t * direction[2]
|
116
|
+
|
117
|
+
@intersections << [t, intersection, triangle]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def intersect(point, direction, triangle)
|
122
|
+
v0 = triangle[0]
|
123
|
+
v1 = triangle[1]
|
124
|
+
v2 = triangle[2]
|
125
|
+
|
126
|
+
return [false, 0] if v0.nil? || v1.nil? || v2.nil?
|
127
|
+
|
128
|
+
e1 = vector_to(v0, v1)
|
129
|
+
e2 = vector_to(v0, v2)
|
130
|
+
|
131
|
+
h = cross_product(direction, e2)
|
132
|
+
a = dot_product(e1, h)
|
133
|
+
|
134
|
+
if a.abs < 0.00001
|
135
|
+
return false, 0
|
136
|
+
end
|
137
|
+
|
138
|
+
f = 1.0/a
|
139
|
+
s = vector_to(v0, point)
|
140
|
+
u = f * dot_product(s, h)
|
141
|
+
|
142
|
+
if u < 0.0 || u > 1.0
|
143
|
+
return false, 0
|
144
|
+
end
|
145
|
+
|
146
|
+
q = cross_product(s, e1)
|
147
|
+
v = f * dot_product(direction, q)
|
148
|
+
|
149
|
+
if v < 0.0 || u + v > 1.0
|
150
|
+
return false, 0
|
151
|
+
end
|
152
|
+
|
153
|
+
t = f * dot_product(e2, q)
|
154
|
+
[t > 0, t]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Various utility functions
|
159
|
+
|
160
|
+
def cross_product(a, b)
|
161
|
+
result = [0, 0, 0]
|
162
|
+
result[0] = a[1]*b[2] - a[2]*b[1]
|
163
|
+
result[1] = a[2]*b[0] - a[0]*b[2]
|
164
|
+
result[2] = a[0]*b[1] - a[1]*b[0]
|
165
|
+
|
166
|
+
result
|
167
|
+
end
|
168
|
+
|
169
|
+
def dot_product(a, b)
|
170
|
+
a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
171
|
+
end
|
172
|
+
|
173
|
+
def vector_to(a, b)
|
174
|
+
[b[0] - a[0], b[1] - a[1], b[2] - a[2]]
|
175
|
+
end
|
176
|
+
|
177
|
+
def magnitude(a)
|
178
|
+
Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
|
179
|
+
end
|
180
|
+
|
181
|
+
def equal(a, b)
|
182
|
+
(a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
|
183
|
+
end
|
184
|
+
|
185
|
+
def angle_between(a, b)
|
186
|
+
cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
|
187
|
+
Math.acos(cos_theta)
|
188
|
+
end
|
189
|
+
|
190
|
+
def normalize(a)
|
191
|
+
length = magnitude(a)
|
192
|
+
[a[0]/length, a[1]/length, a[2]/length]
|
193
|
+
end
|
194
|
+
|
195
|
+
def point_cloud_center(vertices)
|
196
|
+
if vertices.count < 1
|
197
|
+
return [0, 0, 0]
|
198
|
+
end
|
199
|
+
|
200
|
+
vertex = vertices[0]
|
201
|
+
min_x = max_x = vertex[0]
|
202
|
+
min_y = max_y = vertex[1]
|
203
|
+
min_z = max_z = vertex[2]
|
204
|
+
|
205
|
+
vertices.each do |vertex|
|
206
|
+
x = vertex[0]
|
207
|
+
y = vertex[1]
|
208
|
+
z = vertex[2]
|
209
|
+
|
210
|
+
min_x = x if x < min_x
|
211
|
+
max_x = x if x > max_x
|
212
|
+
min_y = y if y < min_y
|
213
|
+
max_y = y if y > max_y
|
214
|
+
min_z = z if z < min_z
|
215
|
+
max_z = z if z > max_z
|
216
|
+
end
|
217
|
+
|
218
|
+
[(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
|
219
219
|
end
|