larb 0.1.0 → 1.0.1

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/README.md +63 -1
  4. data/ext/larb/color.c +449 -0
  5. data/ext/larb/color.h +35 -0
  6. data/ext/larb/extconf.rb +11 -0
  7. data/ext/larb/larb.c +27 -0
  8. data/ext/larb/larb.h +31 -0
  9. data/ext/larb/mat2.c +303 -0
  10. data/ext/larb/mat2.h +30 -0
  11. data/ext/larb/mat2d.c +397 -0
  12. data/ext/larb/mat2d.h +35 -0
  13. data/ext/larb/mat3.c +455 -0
  14. data/ext/larb/mat3.h +33 -0
  15. data/ext/larb/mat4.c +651 -0
  16. data/ext/larb/mat4.h +31 -0
  17. data/ext/larb/matrix_utils.h +97 -0
  18. data/ext/larb/quat.c +567 -0
  19. data/ext/larb/quat.h +39 -0
  20. data/ext/larb/quat2.c +511 -0
  21. data/ext/larb/quat2.h +39 -0
  22. data/ext/larb/vec2.c +365 -0
  23. data/ext/larb/vec2.h +43 -0
  24. data/ext/larb/vec3.c +538 -0
  25. data/ext/larb/vec3.h +52 -0
  26. data/ext/larb/vec4.c +361 -0
  27. data/ext/larb/vec4.h +38 -0
  28. data/lib/larb/version.rb +5 -0
  29. data/lib/larb.rb +2 -14
  30. data/test/larb/color_test.rb +299 -0
  31. data/test/larb/mat2_test.rb +144 -0
  32. data/test/larb/mat2d_test.rb +197 -0
  33. data/test/larb/mat3_test.rb +147 -0
  34. data/test/larb/mat4_test.rb +347 -0
  35. data/test/larb/matrix_test.rb +135 -0
  36. data/test/larb/native_value_test.rb +157 -0
  37. data/test/larb/quat2_test.rb +254 -0
  38. data/test/larb/quat_test.rb +284 -0
  39. data/test/larb/vec2_test.rb +274 -0
  40. data/test/larb/vec3_test.rb +373 -0
  41. data/test/larb/vec4_test.rb +196 -0
  42. data/test/test_helper.rb +4 -0
  43. metadata +60 -18
  44. data/Rakefile +0 -11
  45. data/lib/larb/color.rb +0 -148
  46. data/lib/larb/mat2.rb +0 -119
  47. data/lib/larb/mat2d.rb +0 -180
  48. data/lib/larb/mat3.rb +0 -238
  49. data/lib/larb/mat4.rb +0 -329
  50. data/lib/larb/quat.rb +0 -238
  51. data/lib/larb/quat2.rb +0 -193
  52. data/lib/larb/vec2.rb +0 -150
  53. data/lib/larb/vec3.rb +0 -218
  54. data/lib/larb/vec4.rb +0 -125
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class Mat3Test < Test::Unit::TestCase
6
+ def test_new_identity_by_default
7
+ m = Larb::Mat3.new
8
+ assert_equal 1.0, m[0]
9
+ assert_equal 1.0, m[4]
10
+ assert_equal 1.0, m[8]
11
+ end
12
+
13
+ def test_new_with_data
14
+ data = Array.new(9) { |i| i.to_f }
15
+ m = Larb::Mat3.new(data)
16
+ assert_equal data, m.data
17
+ end
18
+
19
+ def test_identity
20
+ m = Larb::Mat3.identity
21
+ assert_equal 1.0, m[0]
22
+ assert_equal 1.0, m[4]
23
+ assert_equal 1.0, m[8]
24
+ end
25
+
26
+ def test_zero
27
+ m = Larb::Mat3.zero
28
+ 9.times { |i| assert_equal 0.0, m[i] }
29
+ end
30
+
31
+ def test_from_mat4
32
+ m4 = Larb::Mat4.identity
33
+ m3 = Larb::Mat3.from_mat4(m4)
34
+ assert_equal 1.0, m3[0]
35
+ assert_equal 1.0, m3[4]
36
+ assert_equal 1.0, m3[8]
37
+ end
38
+
39
+ def test_from_quaternion
40
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 0, 1), Math::PI / 2)
41
+ m = Larb::Mat3.from_quaternion(q)
42
+ assert_in_delta 0.0, m[0], 1e-10
43
+ assert_in_delta 1.0, m[1], 1e-10
44
+ end
45
+
46
+ def test_translation
47
+ m = Larb::Mat3.translation(5, 10)
48
+ assert_equal 5.0, m[6]
49
+ assert_equal 10.0, m[7]
50
+ end
51
+
52
+ def test_rotation
53
+ m = Larb::Mat3.rotation(Math::PI / 2)
54
+ assert_in_delta 0.0, m[0], 1e-10
55
+ assert_in_delta 1.0, m[1], 1e-10
56
+ end
57
+
58
+ def test_scaling
59
+ m = Larb::Mat3.scaling(2, 3)
60
+ assert_equal 2.0, m[0]
61
+ assert_equal 3.0, m[4]
62
+ end
63
+
64
+ def test_multiply_mat3
65
+ m1 = Larb::Mat3.identity
66
+ m2 = Larb::Mat3.translation(1, 2)
67
+ result = m1 * m2
68
+ assert_equal 1.0, result[6]
69
+ assert_equal 2.0, result[7]
70
+ end
71
+
72
+ def test_multiply_vec3
73
+ m = Larb::Mat3.scaling(2, 3)
74
+ v = Larb::Vec3.new(1, 1, 1)
75
+ result = m * v
76
+ assert_equal 2.0, result.x
77
+ assert_equal 3.0, result.y
78
+ end
79
+
80
+ def test_determinant
81
+ m = Larb::Mat3.identity
82
+ assert_equal 1.0, m.determinant
83
+ end
84
+
85
+ def test_determinant_scaling
86
+ m = Larb::Mat3.scaling(2, 3)
87
+ assert_equal 6.0, m.determinant
88
+ end
89
+
90
+ def test_inverse
91
+ m = Larb::Mat3.scaling(2, 4)
92
+ inv = m.inverse
93
+ result = m * inv
94
+ assert result.near?(Larb::Mat3.identity)
95
+ end
96
+
97
+ def test_inverse_singular
98
+ m = Larb::Mat3.zero
99
+ assert_raise_message("Matrix is not invertible") { m.inverse }
100
+ end
101
+
102
+ def test_transpose
103
+ data = Array.new(9) { |i| i.to_f }
104
+ m = Larb::Mat3.new(data)
105
+ result = m.transpose
106
+ assert_equal 3.0, result[1]
107
+ assert_equal 1.0, result[3]
108
+ end
109
+
110
+ def test_translate
111
+ m = Larb::Mat3.identity
112
+ result = m.translate(5, 10)
113
+ assert_equal 5.0, result[6]
114
+ assert_equal 10.0, result[7]
115
+ end
116
+
117
+ def test_rotate
118
+ m = Larb::Mat3.identity
119
+ result = m.rotate(Math::PI / 2)
120
+ assert_in_delta 0.0, result[0], 1e-10
121
+ end
122
+
123
+ def test_scale
124
+ m = Larb::Mat3.identity
125
+ result = m.scale(2, 3)
126
+ assert_equal 2.0, result[0]
127
+ assert_equal 3.0, result[4]
128
+ end
129
+
130
+ def test_equality
131
+ m1 = Larb::Mat3.identity
132
+ m2 = Larb::Mat3.identity
133
+ assert_equal m1, m2
134
+ end
135
+
136
+ def test_near
137
+ m1 = Larb::Mat3.identity
138
+ m2 = Larb::Mat3.identity
139
+ m2[0] = 1.0000001
140
+ assert m1.near?(m2)
141
+ end
142
+
143
+ def test_inspect
144
+ m = Larb::Mat3.identity
145
+ assert_match(/Mat3/, m.inspect)
146
+ end
147
+ end
@@ -0,0 +1,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class Mat4Test < Test::Unit::TestCase
6
+ def test_new_identity_by_default
7
+ m = Larb::Mat4.new
8
+ assert_equal 1.0, m[0]
9
+ assert_equal 1.0, m[5]
10
+ assert_equal 1.0, m[10]
11
+ assert_equal 1.0, m[15]
12
+ assert_equal 0.0, m[1]
13
+ assert_equal 0.0, m[4]
14
+ end
15
+
16
+ def test_new_with_data
17
+ data = Array.new(16) { |i| i.to_f }
18
+ m = Larb::Mat4.new(data)
19
+ assert_equal data, m.data
20
+ end
21
+
22
+ def test_identity
23
+ m = Larb::Mat4.identity
24
+ assert_equal 1.0, m[0]
25
+ assert_equal 1.0, m[5]
26
+ assert_equal 1.0, m[10]
27
+ assert_equal 1.0, m[15]
28
+ end
29
+
30
+ def test_zero
31
+ m = Larb::Mat4.zero
32
+ 16.times { |i| assert_equal 0.0, m[i] }
33
+ end
34
+
35
+ def test_translation
36
+ m = Larb::Mat4.translation(1, 2, 3)
37
+ assert_equal 1.0, m[12]
38
+ assert_equal 2.0, m[13]
39
+ assert_equal 3.0, m[14]
40
+ assert_equal 1.0, m[0]
41
+ assert_equal 1.0, m[5]
42
+ assert_equal 1.0, m[10]
43
+ assert_equal 1.0, m[15]
44
+ end
45
+
46
+ def test_scaling
47
+ m = Larb::Mat4.scaling(2, 3, 4)
48
+ assert_equal 2.0, m[0]
49
+ assert_equal 3.0, m[5]
50
+ assert_equal 4.0, m[10]
51
+ assert_equal 1.0, m[15]
52
+ end
53
+
54
+ def test_rotation_x
55
+ m = Larb::Mat4.rotation_x(Math::PI / 2)
56
+ assert_equal 1.0, m[0]
57
+ assert_in_delta 0.0, m[5], 1e-10
58
+ assert_in_delta 1.0, m[6], 1e-10
59
+ assert_in_delta(-1.0, m[9], 1e-10)
60
+ assert_in_delta 0.0, m[10], 1e-10
61
+ end
62
+
63
+ def test_rotation_y
64
+ m = Larb::Mat4.rotation_y(Math::PI / 2)
65
+ assert_in_delta 0.0, m[0], 1e-10
66
+ assert_in_delta(-1.0, m[2], 1e-10)
67
+ assert_equal 1.0, m[5]
68
+ assert_in_delta 1.0, m[8], 1e-10
69
+ assert_in_delta 0.0, m[10], 1e-10
70
+ end
71
+
72
+ def test_rotation_z
73
+ m = Larb::Mat4.rotation_z(Math::PI / 2)
74
+ assert_in_delta 0.0, m[0], 1e-10
75
+ assert_in_delta 1.0, m[1], 1e-10
76
+ assert_in_delta(-1.0, m[4], 1e-10)
77
+ assert_in_delta 0.0, m[5], 1e-10
78
+ assert_equal 1.0, m[10]
79
+ end
80
+
81
+ def test_rotation
82
+ m = Larb::Mat4.rotation(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
83
+ assert_in_delta 0.0, m[0], 1e-10
84
+ assert_in_delta 1.0, m[8], 1e-10
85
+ end
86
+
87
+ def test_look_at
88
+ eye = Larb::Vec3.new(0, 0, 5)
89
+ target = Larb::Vec3.new(0, 0, 0)
90
+ up = Larb::Vec3.new(0, 1, 0)
91
+ m = Larb::Mat4.look_at(eye, target, up)
92
+ assert_instance_of Larb::Mat4, m
93
+ assert_in_delta(-5.0, m[14], 1e-10)
94
+ end
95
+
96
+ def test_perspective
97
+ m = Larb::Mat4.perspective(Math::PI / 4, 16.0 / 9.0, 0.1, 100.0)
98
+ assert_instance_of Larb::Mat4, m
99
+ assert_equal(-1.0, m[11])
100
+ end
101
+
102
+ def test_orthographic
103
+ m = Larb::Mat4.orthographic(-1, 1, -1, 1, 0.1, 100)
104
+ assert_instance_of Larb::Mat4, m
105
+ assert_equal 1.0, m[0]
106
+ assert_equal 1.0, m[5]
107
+ end
108
+
109
+ def test_index_access
110
+ m = Larb::Mat4.identity
111
+ assert_equal 1.0, m[0]
112
+ assert_equal 0.0, m[1]
113
+ end
114
+
115
+ def test_index_assignment
116
+ m = Larb::Mat4.identity
117
+ m[0] = 5.0
118
+ assert_equal 5.0, m[0]
119
+ end
120
+
121
+ def test_multiply_mat4
122
+ m1 = Larb::Mat4.translation(1, 0, 0)
123
+ m2 = Larb::Mat4.translation(0, 1, 0)
124
+ result = m1 * m2
125
+ assert_equal 1.0, result[12]
126
+ assert_equal 1.0, result[13]
127
+ end
128
+
129
+ def test_multiply_identity
130
+ m = Larb::Mat4.translation(1, 2, 3)
131
+ result = Larb::Mat4.identity * m
132
+ assert_equal m.data, result.data
133
+ end
134
+
135
+ def test_multiply_vec4
136
+ m = Larb::Mat4.translation(1, 2, 3)
137
+ v = Larb::Vec4.new(0, 0, 0, 1)
138
+ result = m * v
139
+ assert_equal 1.0, result.x
140
+ assert_equal 2.0, result.y
141
+ assert_equal 3.0, result.z
142
+ assert_equal 1.0, result.w
143
+ end
144
+
145
+ def test_multiply_vec3
146
+ m = Larb::Mat4.translation(1, 2, 3)
147
+ v = Larb::Vec3.new(0, 0, 0)
148
+ result = m * v
149
+ assert_equal 1.0, result.x
150
+ assert_equal 2.0, result.y
151
+ assert_equal 3.0, result.z
152
+ end
153
+
154
+ def test_transpose
155
+ data = Array.new(16) { |i| i.to_f }
156
+ m = Larb::Mat4.new(data)
157
+ result = m.transpose
158
+ assert_equal 4.0, result[1]
159
+ assert_equal 1.0, result[4]
160
+ assert_equal 8.0, result[2]
161
+ assert_equal 2.0, result[8]
162
+ end
163
+
164
+ def test_inverse
165
+ m = Larb::Mat4.translation(1, 2, 3)
166
+ inv = m.inverse
167
+ result = m * inv
168
+ assert_in_delta 1.0, result[0], 1e-10
169
+ assert_in_delta 1.0, result[5], 1e-10
170
+ assert_in_delta 1.0, result[10], 1e-10
171
+ assert_in_delta 1.0, result[15], 1e-10
172
+ assert_in_delta 0.0, result[12], 1e-10
173
+ assert_in_delta 0.0, result[13], 1e-10
174
+ assert_in_delta 0.0, result[14], 1e-10
175
+ end
176
+
177
+ def test_inverse_singular
178
+ m = Larb::Mat4.zero
179
+ assert_raise_message("Matrix is not invertible") { m.inverse }
180
+ end
181
+
182
+ def test_to_a
183
+ m = Larb::Mat4.identity
184
+ arr = m.to_a
185
+ assert_equal m.data, arr
186
+ arr[0] = 999
187
+ assert_equal 1.0, m.data[0]
188
+ end
189
+
190
+ def test_determinant
191
+ m = Larb::Mat4.identity
192
+ assert_equal 1.0, m.determinant
193
+ end
194
+
195
+ def test_determinant_zero
196
+ m = Larb::Mat4.zero
197
+ assert_equal 0.0, m.determinant
198
+ end
199
+
200
+ def test_determinant_scaling
201
+ m = Larb::Mat4.scaling(2, 3, 4)
202
+ assert_equal 24.0, m.determinant
203
+ end
204
+
205
+ def test_add
206
+ m1 = Larb::Mat4.identity
207
+ m2 = Larb::Mat4.identity
208
+ result = m1 + m2
209
+ assert_equal 2.0, result[0]
210
+ assert_equal 2.0, result[5]
211
+ end
212
+
213
+ def test_subtract
214
+ m1 = Larb::Mat4.identity
215
+ m2 = Larb::Mat4.identity
216
+ result = m1 - m2
217
+ assert_equal 0.0, result[0]
218
+ assert_equal 0.0, result[5]
219
+ end
220
+
221
+ def test_equality
222
+ m1 = Larb::Mat4.identity
223
+ m2 = Larb::Mat4.identity
224
+ assert_equal m1, m2
225
+ end
226
+
227
+ def test_inequality
228
+ m1 = Larb::Mat4.identity
229
+ m2 = Larb::Mat4.translation(1, 0, 0)
230
+ assert_not_equal m1, m2
231
+ end
232
+
233
+ def test_near
234
+ m1 = Larb::Mat4.identity
235
+ m2 = Larb::Mat4.identity
236
+ m2[0] = 1.0000001
237
+ assert m1.near?(m2)
238
+ end
239
+
240
+ def test_extract_translation
241
+ m = Larb::Mat4.translation(1, 2, 3)
242
+ result = m.extract_translation
243
+ assert_equal Larb::Vec3.new(1, 2, 3), result
244
+ end
245
+
246
+ def test_extract_scale
247
+ m = Larb::Mat4.scaling(2, 3, 4)
248
+ result = m.extract_scale
249
+ assert_in_delta 2.0, result.x, 1e-10
250
+ assert_in_delta 3.0, result.y, 1e-10
251
+ assert_in_delta 4.0, result.z, 1e-10
252
+ end
253
+
254
+ def test_frustum
255
+ m = Larb::Mat4.frustum(-1, 1, -1, 1, 0.1, 100)
256
+ assert_instance_of Larb::Mat4, m
257
+ end
258
+
259
+ def test_from_quaternion
260
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
261
+ m = Larb::Mat4.from_quaternion(q)
262
+ assert_instance_of Larb::Mat4, m
263
+ assert_in_delta 0.0, m[0], 1e-10
264
+ end
265
+
266
+ def test_inspect
267
+ m = Larb::Mat4.identity
268
+ assert_match(/Mat4/, m.inspect)
269
+ end
270
+
271
+ def test_trs_applies_scale_rotation_then_translation
272
+ matrix = Larb::Mat4.trs(
273
+ Larb::Vec3.new(1, 2, 3),
274
+ Larb::Quat.from_axis_angle(Larb::Vec3.back, Math::PI / 2),
275
+ Larb::Vec3.new(2, 3, 4)
276
+ )
277
+ assert_finite_close [1, 2, 3], matrix.extract_translation.to_a
278
+ assert_finite_close [-2, 4, 7, 1], (matrix * Larb::Vec4.new(1, 1, 1, 1)).to_a
279
+ end
280
+
281
+ def test_decomposition_round_trips_signed_scales
282
+ rotations = [Larb::Quat.identity] + [Larb::Vec3.right, Larb::Vec3.up, Larb::Vec3.forward].map do |axis|
283
+ Larb::Quat.from_axis_angle(axis, Math::PI)
284
+ end
285
+ rotations << Larb::Quat.from_axis_angle(Larb::Vec3.new(1, 2, 3), 1.2)
286
+ [-1, 1].repeated_permutation(3) do |signs|
287
+ rotations.each do |rotation|
288
+ scale = Larb::Vec3.new(signs[0] * 2, signs[1] * 3, signs[2] * 4)
289
+ matrix = Larb::Mat4.trs(Larb::Vec3.new(1, 2, 3), rotation, scale)
290
+ extracted_rotation = matrix.extract_rotation
291
+ extracted_scale = matrix.extract_scale
292
+ assert extracted_rotation.to_a.all?(&:finite?)
293
+ assert_in_delta 1.0, extracted_rotation.length, 1e-12
294
+ assert_equal signs.inject(:*) < 0, extracted_scale.x < 0
295
+ rebuilt = Larb::Mat4.trs(matrix.extract_translation, extracted_rotation, extracted_scale)
296
+ assert_finite_close matrix.to_a, rebuilt.to_a
297
+ end
298
+ end
299
+ end
300
+
301
+ def test_decomposition_handles_extreme_scales
302
+ [1e-200, 1e200].each do |magnitude|
303
+ matrix = Larb::Mat4.scaling(-magnitude, magnitude, magnitude)
304
+ scale = matrix.extract_scale
305
+ assert_finite_close [-1, 1, 1], scale.to_a.map { |value| value / magnitude }
306
+ assert_finite_close [0, 0, 0, 1], matrix.extract_rotation.to_a
307
+ end
308
+ end
309
+
310
+ def test_decomposition_rejects_unsupported_matrices
311
+ shear = Larb::Mat4.identity
312
+ shear[4] = 0.5
313
+ nonfinite = Larb::Mat4.identity
314
+ nonfinite[0] = Float::NAN
315
+ matrices = [shear, nonfinite, Larb::Mat4.scaling(0, 1, 1),
316
+ Larb::Mat4.perspective(Math::PI / 4, 1, 0.1, 100)]
317
+ matrices.each do |matrix|
318
+ assert_raise(ArgumentError) { matrix.extract_rotation }
319
+ assert_raise(ArgumentError) { matrix.extract_scale }
320
+ end
321
+ end
322
+
323
+ def test_look_at_rejects_degenerate_directions
324
+ origin = Larb::Vec3.zero
325
+ assert_raise(ArgumentError) { Larb::Mat4.look_at(origin, origin, Larb::Vec3.up) }
326
+ assert_raise(ArgumentError) { Larb::Mat4.look_at(origin, Larb::Vec3.forward, origin) }
327
+ assert_raise(ArgumentError) { Larb::Mat4.look_at(origin, Larb::Vec3.forward, Larb::Vec3.forward) }
328
+ assert_raise(ArgumentError) do
329
+ Larb::Mat4.look_at(origin, Larb::Vec3.new(Float::NAN, 0, 1), Larb::Vec3.up)
330
+ end
331
+ end
332
+
333
+ def test_look_at_normalizes_extreme_directions
334
+ [1e-200, 1e200].each do |magnitude|
335
+ matrix = Larb::Mat4.look_at(Larb::Vec3.zero, Larb::Vec3.new(0, 0, -magnitude),
336
+ Larb::Vec3.new(0, magnitude, 0))
337
+ assert_finite_close Larb::Mat4.identity.to_a, matrix.to_a
338
+ end
339
+ end
340
+
341
+ private
342
+
343
+ def assert_finite_close(expected, actual)
344
+ assert actual.all?(&:finite?), actual.inspect
345
+ expected.zip(actual).each { |a, b| assert_in_delta a, b, 1e-9 }
346
+ end
347
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class MatrixTest < Test::Unit::TestCase
6
+ MATRIX_CLASSES = [Larb::Mat2, Larb::Mat2d, Larb::Mat3, Larb::Mat4].freeze
7
+
8
+ def test_array_sizes_and_component_types
9
+ MATRIX_CLASSES.each do |klass|
10
+ size = klass.identity.to_a.size
11
+ [[], Array.new(size - 1, 0), Array.new(size + 1, 0)].each do |values|
12
+ assert_raise(ArgumentError) { klass.new(values) }
13
+ end
14
+ [nil, "1", Object.new].each do |value|
15
+ assert_raise(TypeError) { klass.new(value) }
16
+ values = klass.identity.to_a
17
+ values[-1] = value
18
+ assert_raise(TypeError) { klass.new(values) }
19
+ end
20
+ end
21
+ end
22
+
23
+ def test_numeric_scalar_multiplication_and_invalid_operands
24
+ MATRIX_CLASSES.each do |klass|
25
+ matrix = klass.identity
26
+ assert_equal matrix.to_a.map { |value| value * 2 }, (matrix * 2).to_a
27
+ [nil, "2", Object.new, []].each do |other|
28
+ assert_raise(TypeError) { matrix * other }
29
+ end
30
+ end
31
+ end
32
+
33
+ def test_near_rejects_nonfinite_components_and_invalid_epsilon
34
+ MATRIX_CLASSES.each do |klass|
35
+ matrix = klass.identity
36
+ [Float::NAN, Float::INFINITY, -Float::INFINITY].each do |value|
37
+ other = klass.identity
38
+ other[0] = value
39
+ assert_false matrix.near?(other)
40
+ assert_false other.near?(other)
41
+ end
42
+ [0, -1, Float::NAN, Float::INFINITY, -Float::INFINITY].each do |epsilon|
43
+ assert_raise(ArgumentError) { matrix.near?(matrix, epsilon) }
44
+ end
45
+ assert_raise(TypeError) { matrix.near?(matrix, nil) }
46
+ end
47
+ end
48
+
49
+ def test_inverse_does_not_depend_on_absolute_scale
50
+ MATRIX_CLASSES.each do |klass|
51
+ [1e-200, 1e-6, 1e200].each do |magnitude|
52
+ matrix = klass.identity * magnitude
53
+ inverse = matrix.inverse
54
+ assert inverse.to_a.all?(&:finite?)
55
+ assert_finite_close klass.identity.to_a, (matrix * inverse).to_a
56
+ assert_finite_close klass.identity.to_a, (inverse * matrix).to_a
57
+ end
58
+ end
59
+ end
60
+
61
+ def test_inverse_uses_pivot_swaps_and_handles_different_axis_scales
62
+ matrices = [
63
+ Larb::Mat2.new([0, 2, 3, 4]),
64
+ Larb::Mat2d.new([0, 2, 3, 4, 5, 6]),
65
+ Larb::Mat3.new([0, 1, 0, 1, 2, 0, 0, 0, 3]),
66
+ Larb::Mat4.new([0, 1, 0, 1, 1, 2, 1, 0, 2, 0, 3, 1, 1, 0, 0, 4]),
67
+ Larb::Mat2.scaling(1e-200, 1e200),
68
+ Larb::Mat2d.scaling(1e-200, 1e200),
69
+ Larb::Mat3.scaling(1e-200, 1e200),
70
+ Larb::Mat4.scaling(1e-200, 1e200, 1),
71
+ Larb::Mat2d.translation(1e200, 0),
72
+ Larb::Mat4.translation(1e200, 0, 0)
73
+ ]
74
+ matrices.each do |matrix|
75
+ inverse = matrix.inverse
76
+ assert inverse.to_a.all?(&:finite?)
77
+ assert_finite_close matrix.class.identity.to_a, (matrix * inverse).to_a
78
+ assert_finite_close matrix.class.identity.to_a, (inverse * matrix).to_a
79
+ end
80
+ end
81
+
82
+ def test_inverse_rejects_rank_deficient_and_nonfinite_matrices
83
+ MATRIX_CLASSES.each do |klass|
84
+ assert_raise(RuntimeError) { klass.new(Array.new(klass.identity.to_a.size, 1)).inverse }
85
+ [Float::NAN, Float::INFINITY].each do |value|
86
+ matrix = klass.identity
87
+ matrix[0] = value
88
+ assert_raise(RuntimeError) { matrix.inverse }
89
+ end
90
+ end
91
+ assert_raise(RuntimeError) { Larb::Mat3.new([1, 2, 3, 4, 5, 6, 5, 7, 9]).inverse }
92
+ assert_raise(RuntimeError) do
93
+ Larb::Mat4.new([1, 2, 3, 4, 5, 6, 7, 8, 6, 8, 10, 12, 0, 1, 0, 1]).inverse
94
+ end
95
+ end
96
+
97
+ def test_inverse_preserves_small_components_in_rows_and_columns
98
+ cases = [
99
+ [[1e200, 1e-200, 1e200, -1e-200], [5e-201, 5e-201, 5e199, -5e199]],
100
+ [[1e200, 1e200, 1e-200, -1e-200], [5e-201, 5e199, 5e-201, -5e199]]
101
+ ]
102
+ MATRIX_CLASSES.each do |klass|
103
+ size = klass == Larb::Mat2d ? 2 : Math.sqrt(klass.identity.to_a.size).to_i
104
+ indices = [0, 1, size, size + 1]
105
+ cases.each do |values, expected|
106
+ matrix = klass.identity
107
+ indices.zip(values).each { |index, value| matrix[index] = value }
108
+ inverse = matrix.inverse.to_a
109
+ assert inverse.all?(&:finite?)
110
+ indices.zip(expected).each do |index, value|
111
+ assert_in_delta 1.0, inverse[index] / value, 1e-12
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def test_frobenius_norm_handles_extreme_magnitudes
118
+ [Larb::Mat2, Larb::Mat2d, Larb::Mat3].each do |klass|
119
+ [1e-200, 1e200].each do |magnitude|
120
+ values = klass.zero.to_a
121
+ values[0] = magnitude
122
+ norm = klass.new(values).frobenius_norm
123
+ assert norm.finite?
124
+ assert_in_delta 1.0, norm / magnitude, 1e-12
125
+ end
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def assert_finite_close(expected, actual)
132
+ assert actual.all?(&:finite?), actual.inspect
133
+ expected.zip(actual).each { |a, b| assert_in_delta a, b, 1e-9 }
134
+ end
135
+ end