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,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class NativeValueTest < Test::Unit::TestCase
6
+ TYPES = [Larb::Vec2, Larb::Vec3, Larb::Vec4, Larb::Mat2, Larb::Mat2d,
7
+ Larb::Mat3, Larb::Mat4, Larb::Quat, Larb::Quat2, Larb::Color].freeze
8
+ ARRAY_TYPES = [Larb::Mat2, Larb::Mat2d, Larb::Mat3, Larb::Mat4, Larb::Quat2].freeze
9
+
10
+ def arguments(klass, values)
11
+ ARRAY_TYPES.include?(klass) ? [values] : values
12
+ end
13
+
14
+ def sample(klass)
15
+ klass.new(*arguments(klass, (1..klass.new.to_a.length).to_a))
16
+ end
17
+
18
+ def setters(value)
19
+ %i[x= y= z= w= r= g= b= a=].select { |method| value.respond_to?(method) }
20
+ end
21
+
22
+ def coercion(&block)
23
+ Object.new.tap { |object| object.define_singleton_method(:to_f, &block) }
24
+ end
25
+
26
+ def test_copies_preserve_components_and_have_independent_storage
27
+ TYPES.each do |klass|
28
+ original = sample(klass)
29
+ [original.dup, original.clone, original.freeze.clone(freeze: false)].each do |copy|
30
+ assert_equal original.to_a, copy.to_a, klass.name
31
+ copy.send(:initialize)
32
+ assert_equal (1..original.to_a.length).to_a, original.to_a
33
+ end
34
+ assert_predicate original.clone, :frozen?
35
+ subclass = Class.new(klass)
36
+ assert_instance_of subclass, subclass.new.dup
37
+ assert_raise(TypeError) { klass.new.send(:initialize_copy, Object.new) }
38
+ end
39
+ end
40
+
41
+ def test_frozen_values_reject_every_mutator
42
+ TYPES.each do |klass|
43
+ value = sample(klass).freeze
44
+ before = value.to_a
45
+ setters(value).each { |method| assert_raise(FrozenError) { value.public_send(method, 99) } }
46
+ assert_raise(FrozenError) { value[0] = 99 } if value.respond_to?(:[]=)
47
+ assert_raise(FrozenError) { value.normalize! } if value.respond_to?(:normalize!)
48
+ assert_raise(FrozenError) { value.send(:initialize) }
49
+ assert_raise(FrozenError) { value.send(:initialize_copy, sample(klass)) }
50
+ assert_equal before, value.to_a
51
+ end
52
+ end
53
+
54
+ def test_setters_recheck_freeze_after_numeric_conversion
55
+ TYPES.each do |klass|
56
+ setters(sample(klass)).each do |method|
57
+ value = sample(klass)
58
+ before = value.to_a
59
+ number = coercion { value.freeze; 99.0 }
60
+ assert_raise(FrozenError) { value.public_send(method, number) }
61
+ assert_equal before, value.to_a
62
+ end
63
+ next unless sample(klass).respond_to?(:[]=)
64
+
65
+ value = sample(klass)
66
+ before = value.to_a
67
+ number = coercion { value.freeze; 99.0 }
68
+ assert_raise(FrozenError) { value[0] = number }
69
+ assert_equal before, value.to_a
70
+
71
+ value = sample(klass)
72
+ index = Object.new
73
+ index.define_singleton_method(:to_int) { value.freeze; 0 }
74
+ assert_raise(FrozenError) { value[index] = 99 }
75
+ assert_equal before, value.to_a
76
+ end
77
+ end
78
+
79
+ def test_reinitialization_is_atomic_when_conversion_freezes_or_fails
80
+ TYPES.each do |klass|
81
+ value = sample(klass)
82
+ before = value.to_a
83
+ input = Array.new(before.length, 99)
84
+ input[-1] = coercion { value.freeze; 99.0 }
85
+ assert_raise(FrozenError) { value.send(:initialize, *arguments(klass, input)) }
86
+ assert_equal before, value.to_a
87
+
88
+ value = sample(klass)
89
+ input[-1] = Object.new
90
+ assert_raise(TypeError) { value.send(:initialize, *arguments(klass, input)) }
91
+ assert_equal before, value.to_a
92
+ end
93
+ end
94
+
95
+ def test_numeric_inputs_reject_strings_and_nil
96
+ TYPES.each do |klass|
97
+ [nil, "not a number"].each do |invalid|
98
+ input = sample(klass).to_a
99
+ input[0] = invalid
100
+ assert_raise(TypeError) { klass.new(*arguments(klass, input)) }
101
+ assert_raise(TypeError) { sample(klass) * invalid }
102
+ setters(sample(klass)).each do |method|
103
+ assert_raise(TypeError) { sample(klass).public_send(method, invalid) }
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def test_array_constructors_require_exact_component_counts
110
+ ARRAY_TYPES.each do |klass|
111
+ count = klass.new.to_a.length
112
+ [0, count - 1, count + 1].each do |length|
113
+ assert_raise(ArgumentError) { klass.new(Array.new(length, 0)) }
114
+ end
115
+ assert_raise(TypeError) { klass.new(nil) }
116
+ end
117
+ end
118
+
119
+ def test_near_rejects_nan_components_and_invalid_tolerances
120
+ TYPES.each do |klass|
121
+ good = sample(klass)
122
+ values = good.to_a
123
+ values.each_index do |i|
124
+ bad_values = values.dup
125
+ bad_values[i] = Float::NAN
126
+ bad = klass.new(*arguments(klass, bad_values))
127
+ assert_false good.near?(bad), klass.name
128
+ assert_false bad.near?(good), klass.name
129
+ end
130
+ [0, -1, Float::NAN, Float::INFINITY].each do |epsilon|
131
+ assert_raise(ArgumentError) { good.near?(good, epsilon) }
132
+ end
133
+ assert_raise(TypeError) { good.near?(good, nil) }
134
+ end
135
+ end
136
+
137
+ def test_vector_lengths_and_normalization_across_magnitudes
138
+ [Larb::Vec2, Larb::Vec3, Larb::Vec4].each do |klass|
139
+ count = klass.zero.to_a.length
140
+ [1e200, 1e-200, Float::MAX, Float::MIN * Float::EPSILON].each do |magnitude|
141
+ value = klass.new(magnitude, *Array.new(count - 1, 0))
142
+ assert_equal magnitude, value.length
143
+ assert_equal magnitude, value.distance(klass.zero) if value.respond_to?(:distance)
144
+ assert_equal [1.0, *Array.new(count - 1, 0.0)], value.normalize.to_a
145
+ value.normalize!
146
+ assert_equal [1.0, *Array.new(count - 1, 0.0)], value.to_a
147
+ end
148
+ value = klass.new(*Array.new(count, Float::MAX)).normalize
149
+ value.to_a.each { |v| assert_in_delta 1.0 / Math.sqrt(count), v, 1e-15 }
150
+ [0.0, Float::INFINITY, Float::NAN].each do |invalid|
151
+ value = klass.new(invalid, *Array.new(count - 1, 0))
152
+ assert_raise(ArgumentError) { value.normalize }
153
+ assert_raise(ArgumentError) { value.normalize! }
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class Quat2Test < Test::Unit::TestCase
6
+ def test_new_identity_by_default
7
+ q = Larb::Quat2.new
8
+ assert_equal 1.0, q[3]
9
+ assert_equal 0.0, q[7]
10
+ end
11
+
12
+ def test_new_with_data
13
+ data = [0, 0, 0, 1, 0, 0, 0, 0]
14
+ q = Larb::Quat2.new(data)
15
+ assert_equal data, q.data
16
+ end
17
+
18
+ def test_identity
19
+ q = Larb::Quat2.identity
20
+ assert_equal Larb::Quat.identity, q.real
21
+ end
22
+
23
+ def test_from_rotation_translation
24
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
25
+ translation = Larb::Vec3.new(1, 2, 3)
26
+ q = Larb::Quat2.from_rotation_translation(rotation, translation)
27
+ assert q.rotation.near?(rotation)
28
+ assert q.translation.near?(translation)
29
+ end
30
+
31
+ def test_from_translation
32
+ translation = Larb::Vec3.new(5, 10, 15)
33
+ q = Larb::Quat2.from_translation(translation)
34
+ assert q.translation.near?(translation)
35
+ assert q.rotation.near?(Larb::Quat.identity)
36
+ end
37
+
38
+ def test_from_rotation
39
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
40
+ q = Larb::Quat2.from_rotation(rotation)
41
+ assert q.rotation.near?(rotation)
42
+ assert q.translation.near?(Larb::Vec3.zero)
43
+ end
44
+
45
+ def test_real
46
+ q = Larb::Quat2.identity
47
+ assert_instance_of Larb::Quat, q.real
48
+ end
49
+
50
+ def test_dual
51
+ q = Larb::Quat2.identity
52
+ assert_instance_of Larb::Quat, q.dual
53
+ end
54
+
55
+ def test_multiply_quat2
56
+ q1 = Larb::Quat2.from_translation(Larb::Vec3.new(1, 0, 0))
57
+ q2 = Larb::Quat2.from_translation(Larb::Vec3.new(0, 1, 0))
58
+ result = q1 * q2
59
+ assert result.translation.near?(Larb::Vec3.new(1, 1, 0))
60
+ end
61
+
62
+ def test_multiply_vec3
63
+ q = Larb::Quat2.from_translation(Larb::Vec3.new(10, 0, 0))
64
+ point = Larb::Vec3.new(0, 0, 0)
65
+ result = q * point
66
+ assert result.near?(Larb::Vec3.new(10, 0, 0))
67
+ end
68
+
69
+ def test_add
70
+ q1 = Larb::Quat2.identity
71
+ q2 = Larb::Quat2.identity
72
+ result = q1 + q2
73
+ assert_equal 2.0, result[3]
74
+ end
75
+
76
+ def test_subtract
77
+ q1 = Larb::Quat2.identity
78
+ q2 = Larb::Quat2.identity
79
+ result = q1 - q2
80
+ assert_equal 0.0, result[3]
81
+ end
82
+
83
+ def test_dot
84
+ q = Larb::Quat2.identity
85
+ assert_equal 1.0, q.dot(q)
86
+ end
87
+
88
+ def test_length
89
+ q = Larb::Quat2.identity
90
+ assert_equal 1.0, q.length
91
+ end
92
+
93
+ def test_normalize
94
+ q = Larb::Quat2.new([0, 0, 0, 2, 0, 0, 0, 0])
95
+ result = q.normalize
96
+ assert_in_delta 1.0, result.length, 1e-10
97
+ end
98
+
99
+ def test_conjugate
100
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
101
+ q = Larb::Quat2.from_rotation(rotation)
102
+ conj = q.conjugate
103
+ result = q * conj
104
+ assert result.rotation.near?(Larb::Quat.identity)
105
+ end
106
+
107
+ def test_inverse
108
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 4)
109
+ translation = Larb::Vec3.new(1, 2, 3)
110
+ q = Larb::Quat2.from_rotation_translation(rotation, translation)
111
+ inv = q.inverse
112
+ result = q * inv
113
+ assert result.rotation.near?(Larb::Quat.identity)
114
+ end
115
+
116
+ def test_translation
117
+ t = Larb::Vec3.new(5, 10, 15)
118
+ q = Larb::Quat2.from_translation(t)
119
+ assert q.translation.near?(t)
120
+ end
121
+
122
+ def test_rotation
123
+ r = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
124
+ q = Larb::Quat2.from_rotation(r)
125
+ assert q.rotation.near?(r)
126
+ end
127
+
128
+ def test_transform_point
129
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
130
+ translation = Larb::Vec3.new(0, 0, 5)
131
+ q = Larb::Quat2.from_rotation_translation(rotation, translation)
132
+ point = Larb::Vec3.new(1, 0, 0)
133
+ result = q.transform_point(point)
134
+ assert_in_delta 0.0, result.x, 1e-10
135
+ assert_in_delta 4.0, result.z, 1e-10
136
+ end
137
+
138
+ def test_lerp
139
+ q1 = Larb::Quat2.from_translation(Larb::Vec3.new(0, 0, 0))
140
+ q2 = Larb::Quat2.from_translation(Larb::Vec3.new(10, 0, 0))
141
+ result = q1.lerp(q2, 0.5)
142
+ assert_in_delta 5.0, result.translation.x, 1e-6
143
+ end
144
+
145
+ def test_equality
146
+ q1 = Larb::Quat2.identity
147
+ q2 = Larb::Quat2.identity
148
+ assert_equal q1, q2
149
+ end
150
+
151
+ def test_near
152
+ q1 = Larb::Quat2.identity
153
+ q2 = Larb::Quat2.new([0, 0, 0, 1.0000001, 0, 0, 0, 0])
154
+ assert q1.near?(q2)
155
+ end
156
+
157
+ def test_inspect
158
+ q = Larb::Quat2.identity
159
+ assert_match(/Quat2/, q.inspect)
160
+ end
161
+
162
+ def test_matrix_and_point_transform_agree
163
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.forward, Math::PI / 2)
164
+ q = Larb::Quat2.from_rotation_translation(rotation, Larb::Vec3.new(1, 2, 3))
165
+ [Larb::Vec3.zero, Larb::Vec3.new(4, -2, 7)].each do |point|
166
+ actual = (q.to_mat4 * Larb::Vec4.new(*point.to_a, 1)).xyz
167
+ assert_components q.transform_point(point).to_a, actual
168
+ end
169
+ assert_components [1, 2, 3], q.to_mat4.extract_translation
170
+ end
171
+
172
+ def test_lerp_preserves_antipodal_rigid_transforms
173
+ rotation = Larb::Quat.from_axis_angle(Larb::Vec3.up, 0.7)
174
+ q = Larb::Quat2.from_rotation_translation(rotation, Larb::Vec3.new(1, 2, 3))
175
+ [0, 0.5, 1].each do |t|
176
+ result = (q * 1e200).lerp(q * -1e-200, t)
177
+ assert_components q.to_a, result
178
+ assert_components q.transform_point(Larb::Vec3.right).to_a,
179
+ result.transform_point(Larb::Vec3.right)
180
+ end
181
+ end
182
+
183
+ def test_lerp_preserves_real_dual_orthogonality
184
+ a = Larb::Quat2.identity
185
+ b = Larb::Quat2.from_rotation_translation(Larb::Quat.new(0, 0, 1, 0), Larb::Vec3.new(0, 0, 2))
186
+ [0, 0.25, 0.5, 0.75, 1].each do |t|
187
+ q = a.lerp(b, t)
188
+ assert q.to_a.all?(&:finite?)
189
+ assert_in_delta 1.0, q.real.length, 1e-12
190
+ assert_in_delta 0.0, q.real.dot(q.dual), 1e-12
191
+ assert_components Larb::Quat2.identity.to_a, q * q.inverse
192
+ end
193
+ end
194
+
195
+ def test_normalization_removes_parallel_dual_component
196
+ q = Larb::Quat2.new([0, 0, 0, 2, 4, 6, 8, 10])
197
+ assert_components [0, 0, 0, 1, 2, 3, 4, 0], q.normalize
198
+ assert_same q, q.normalize!
199
+ assert_components [0, 0, 0, 1, 2, 3, 4, 0], q
200
+ assert_components Larb::Quat2.identity.to_a,
201
+ Larb::Quat2.new([0, 0, 0, 1e-200, 0, 0, 0, 1e200]).normalize
202
+ end
203
+
204
+ def test_normalization_and_inverse_across_extreme_magnitudes
205
+ q = Larb::Quat2.from_translation(Larb::Vec3.new(1, 2, 3))
206
+ [1e200, 1e-200].each do |magnitude|
207
+ scaled = q * magnitude
208
+ assert_equal magnitude, scaled.length
209
+ assert_components q.to_a, scaled.normalize
210
+ assert_components Larb::Quat2.identity.to_a, scaled * scaled.inverse
211
+ assert_same scaled, scaled.normalize!
212
+ assert_components q.to_a, scaled
213
+ end
214
+ assert_components [0.5, 0.5, 0.5, 0.5, 0, 0, 0, 0],
215
+ Larb::Quat2.new([Float::MAX] * 4 + [0] * 4).normalize
216
+ end
217
+
218
+ def test_normalization_preserves_subnormal_dual_components
219
+ magnitude = Float::MIN * Float::EPSILON
220
+ q = Larb::Quat2.new([magnitude] * 4 + [magnitude, -magnitude] * 2)
221
+ expected = [0.5] * 4 + [0.5, -0.5] * 2
222
+ assert_components expected, q.normalize
223
+ assert_same q, q.normalize!
224
+ assert_components expected, q
225
+ end
226
+
227
+ def test_inverse_supports_nonorthogonal_dual_quaternions
228
+ q = Larb::Quat2.new([2, 3, 4, 5, 6, 7, 8, 9])
229
+ assert_components Larb::Quat2.identity.to_a, q * q.inverse
230
+ assert_components Larb::Quat2.identity.to_a, q.inverse * q
231
+ end
232
+
233
+ def test_degenerate_dual_quaternions_are_rejected_atomically
234
+ [[0] * 8, [0, 0, 0, Float::INFINITY, 0, 0, 0, 0],
235
+ [0, 0, 0, 2, 3, Float::INFINITY, 0, 0]].each do |values|
236
+ q = Larb::Quat2.new(values)
237
+ %i[normalize normalize! inverse].each do |method|
238
+ assert_raise(ArgumentError) { q.public_send(method) }
239
+ assert_equal values, q.to_a
240
+ end
241
+ assert_raise(ArgumentError) { Larb::Quat2.identity.lerp(q, 0.5) }
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ def assert_components(expected, actual)
248
+ assert_equal expected.size, actual.to_a.size
249
+ expected.zip(actual.to_a).each do |wanted, value|
250
+ assert value.finite?, "expected finite component, got #{value.inspect}"
251
+ assert_in_delta wanted, value, 1e-10
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class QuatTest < Test::Unit::TestCase
6
+ def test_new_with_default_values
7
+ q = Larb::Quat.new
8
+ assert_equal 0.0, q.x
9
+ assert_equal 0.0, q.y
10
+ assert_equal 0.0, q.z
11
+ assert_equal 1.0, q.w
12
+ end
13
+
14
+ def test_new_with_given_values
15
+ q = Larb::Quat.new(1, 2, 3, 4)
16
+ assert_equal 1.0, q.x
17
+ assert_equal 2.0, q.y
18
+ assert_equal 3.0, q.z
19
+ assert_equal 4.0, q.w
20
+ end
21
+
22
+ def test_bracket_syntax
23
+ q = Larb::Quat[1, 2, 3, 4]
24
+ assert_equal 1.0, q.x
25
+ assert_equal 2.0, q.y
26
+ assert_equal 3.0, q.z
27
+ assert_equal 4.0, q.w
28
+ end
29
+
30
+ def test_identity
31
+ q = Larb::Quat.identity
32
+ assert_equal 0.0, q.x
33
+ assert_equal 0.0, q.y
34
+ assert_equal 0.0, q.z
35
+ assert_equal 1.0, q.w
36
+ end
37
+
38
+ def test_from_axis_angle
39
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
40
+ assert_in_delta 1.0, q.length, 1e-10
41
+ assert_in_delta Math.sin(Math::PI / 4), q.y, 1e-10
42
+ assert_in_delta Math.cos(Math::PI / 4), q.w, 1e-10
43
+ end
44
+
45
+ def test_from_euler
46
+ q = Larb::Quat.from_euler(0, Math::PI / 2, 0)
47
+ assert_in_delta 1.0, q.length, 1e-10
48
+ end
49
+
50
+ def test_multiply_quaternions
51
+ q1 = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 4)
52
+ q2 = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 4)
53
+ result = q1 * q2
54
+ expected = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
55
+ assert result.near?(expected)
56
+ end
57
+
58
+ def test_multiply_rotates_vector
59
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
60
+ v = Larb::Vec3.new(1, 0, 0)
61
+ result = q * v
62
+ assert_in_delta 0.0, result.x, 1e-10
63
+ assert_in_delta(-1.0, result.z, 1e-10)
64
+ end
65
+
66
+ def test_multiply_by_scalar
67
+ q = Larb::Quat.new(1, 2, 3, 4)
68
+ result = q * 2
69
+ assert_equal 2.0, result.x
70
+ assert_equal 4.0, result.y
71
+ assert_equal 6.0, result.z
72
+ assert_equal 8.0, result.w
73
+ end
74
+
75
+ def test_add
76
+ q1 = Larb::Quat.new(1, 2, 3, 4)
77
+ q2 = Larb::Quat.new(5, 6, 7, 8)
78
+ result = q1 + q2
79
+ assert_equal 6.0, result.x
80
+ assert_equal 8.0, result.y
81
+ assert_equal 10.0, result.z
82
+ assert_equal 12.0, result.w
83
+ end
84
+
85
+ def test_subtract
86
+ q1 = Larb::Quat.new(5, 6, 7, 8)
87
+ q2 = Larb::Quat.new(1, 2, 3, 4)
88
+ result = q1 - q2
89
+ assert_equal 4.0, result.x
90
+ assert_equal 4.0, result.y
91
+ assert_equal 4.0, result.z
92
+ assert_equal 4.0, result.w
93
+ end
94
+
95
+ def test_negate
96
+ q = Larb::Quat.new(1, 2, 3, 4)
97
+ result = -q
98
+ assert_equal(-1.0, result.x)
99
+ assert_equal(-2.0, result.y)
100
+ assert_equal(-3.0, result.z)
101
+ assert_equal(-4.0, result.w)
102
+ end
103
+
104
+ def test_dot
105
+ q1 = Larb::Quat.new(1, 2, 3, 4)
106
+ q2 = Larb::Quat.new(5, 6, 7, 8)
107
+ assert_equal 70.0, q1.dot(q2)
108
+ end
109
+
110
+ def test_length
111
+ q = Larb::Quat.new(1, 2, 2, 0)
112
+ assert_equal 3.0, q.length
113
+ end
114
+
115
+ def test_length_squared
116
+ q = Larb::Quat.new(1, 2, 2, 0)
117
+ assert_equal 9.0, q.length_squared
118
+ end
119
+
120
+ def test_normalize
121
+ q = Larb::Quat.new(1, 2, 2, 0)
122
+ result = q.normalize
123
+ assert_in_delta 1.0, result.length, 1e-10
124
+ end
125
+
126
+ def test_normalize!
127
+ q = Larb::Quat.new(1, 2, 2, 0)
128
+ q.normalize!
129
+ assert_in_delta 1.0, q.length, 1e-10
130
+ end
131
+
132
+ def test_conjugate
133
+ q = Larb::Quat.new(1, 2, 3, 4)
134
+ result = q.conjugate
135
+ assert_equal(-1.0, result.x)
136
+ assert_equal(-2.0, result.y)
137
+ assert_equal(-3.0, result.z)
138
+ assert_equal 4.0, result.w
139
+ end
140
+
141
+ def test_inverse
142
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 4)
143
+ inv = q.inverse
144
+ result = q * inv
145
+ assert result.near?(Larb::Quat.identity)
146
+ end
147
+
148
+ def test_lerp
149
+ q1 = Larb::Quat.identity
150
+ q2 = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
151
+ result = q1.lerp(q2, 0.5)
152
+ assert_in_delta 1.0, result.length, 1e-10
153
+ end
154
+
155
+ def test_slerp
156
+ q1 = Larb::Quat.identity
157
+ q2 = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
158
+ result = q1.slerp(q2, 0.5)
159
+ expected = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 4)
160
+ assert result.near?(expected, 1e-6)
161
+ end
162
+
163
+ def test_to_axis_angle
164
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
165
+ axis, angle = q.to_axis_angle
166
+ assert_in_delta Math::PI / 2, angle, 1e-10
167
+ assert_in_delta 1.0, axis.y, 1e-10
168
+ end
169
+
170
+ def test_to_euler
171
+ q = Larb::Quat.from_euler(0.1, 0.2, 0.3)
172
+ euler = q.to_euler
173
+ assert_in_delta 0.1, euler.x, 1e-6
174
+ assert_in_delta 0.2, euler.y, 1e-6
175
+ assert_in_delta 0.3, euler.z, 1e-6
176
+ end
177
+
178
+ def test_to_mat4
179
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.new(0, 1, 0), Math::PI / 2)
180
+ m = q.to_mat4
181
+ assert_instance_of Larb::Mat4, m
182
+ end
183
+
184
+ def test_to_a
185
+ q = Larb::Quat.new(1, 2, 3, 4)
186
+ assert_equal [1.0, 2.0, 3.0, 4.0], q.to_a
187
+ end
188
+
189
+ def test_index_access
190
+ q = Larb::Quat.new(1, 2, 3, 4)
191
+ assert_equal 1.0, q[0]
192
+ assert_equal 2.0, q[1]
193
+ assert_equal 3.0, q[2]
194
+ assert_equal 4.0, q[3]
195
+ end
196
+
197
+ def test_equality
198
+ q1 = Larb::Quat.new(1, 2, 3, 4)
199
+ q2 = Larb::Quat.new(1, 2, 3, 4)
200
+ assert_equal q1, q2
201
+ end
202
+
203
+ def test_inequality
204
+ q1 = Larb::Quat.new(1, 2, 3, 4)
205
+ q2 = Larb::Quat.new(1, 2, 3, 5)
206
+ assert_not_equal q1, q2
207
+ end
208
+
209
+ def test_near
210
+ q1 = Larb::Quat.new(1.0, 2.0, 3.0, 4.0)
211
+ q2 = Larb::Quat.new(1.0000001, 2.0000001, 3.0000001, 4.0000001)
212
+ assert q1.near?(q2)
213
+ end
214
+
215
+ def test_inspect
216
+ q = Larb::Quat.new(1, 2, 3, 4)
217
+ assert_equal "Quat[1.0, 2.0, 3.0, 4.0]", q.inspect
218
+ end
219
+
220
+ def test_to_s
221
+ q = Larb::Quat.new(1, 2, 3, 4)
222
+ assert_equal q.inspect, q.to_s
223
+ end
224
+
225
+ def test_interpolation_preserves_antipodal_rotations
226
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.up, 0.7)
227
+ %i[lerp slerp].each do |method|
228
+ [0, 0.5, 1].each do |t|
229
+ result = (q * 1e200).public_send(method, q * -1e-200, t)
230
+ assert_components q.to_a, result
231
+ assert_in_delta 1.0, result.length, 1e-12
232
+ end
233
+ assert_raise(ArgumentError) { q.public_send(method, Larb::Quat.new(0, 0, 0, 0), 0.5) }
234
+ assert_raise(ArgumentError) { q.public_send(method, q, Float::NAN) }
235
+ end
236
+ end
237
+
238
+ def test_normalization_and_inverse_across_extreme_magnitudes
239
+ [1e200, 1e-200, Float::MAX, Float::MIN].each do |magnitude|
240
+ q = Larb::Quat.new(magnitude, 0, 0, 0)
241
+ assert_equal magnitude, q.length
242
+ assert_components [1, 0, 0, 0], q.normalize
243
+ assert_components [0, 0, 0, 1], q * q.inverse
244
+ assert_same q, q.normalize!
245
+ assert_components [1, 0, 0, 0], q
246
+ end
247
+ assert_components [0.5, 0.5, 0.5, 0.5], Larb::Quat.new(*[Float::MAX] * 4).normalize
248
+ end
249
+
250
+ def test_degenerate_quaternions_cannot_be_normalized_or_inverted
251
+ [0, Float::INFINITY, Float::NAN].each do |value|
252
+ q = Larb::Quat.new(value, 0, 0, 0)
253
+ %i[normalize normalize! inverse to_axis_angle].each do |method|
254
+ assert_raise(ArgumentError) { q.public_send(method) }
255
+ end
256
+ assert_equal [0.0, 0.0, 0.0], q.to_a.drop(1)
257
+ value.is_a?(Float) && value.nan? ? assert(q.x.nan?) : assert_equal(value, q.x)
258
+ end
259
+ end
260
+
261
+ def test_small_axis_angles_round_trip
262
+ [1e-3, 1e-12, 1e-200].each do |angle|
263
+ q = Larb::Quat.from_axis_angle(Larb::Vec3.up, angle)
264
+ axis, actual_angle = q.to_axis_angle
265
+ assert_components [0, 1, 0], axis
266
+ assert actual_angle.finite?
267
+ assert_in_delta angle, actual_angle, angle * 1e-12
268
+ assert_components q.to_a, Larb::Quat.from_axis_angle(axis, actual_angle)
269
+ end
270
+ axis, angle = Larb::Quat.identity.to_axis_angle
271
+ assert_components [1, 0, 0], axis
272
+ assert_equal 0.0, angle
273
+ end
274
+
275
+ private
276
+
277
+ def assert_components(expected, actual)
278
+ assert_equal expected.size, actual.to_a.size
279
+ expected.zip(actual.to_a).each do |wanted, value|
280
+ assert value.finite?, "expected finite component, got #{value.inspect}"
281
+ assert_in_delta wanted, value, 1e-10
282
+ end
283
+ end
284
+ end