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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +63 -1
- data/ext/larb/color.c +449 -0
- data/ext/larb/color.h +35 -0
- data/ext/larb/extconf.rb +11 -0
- data/ext/larb/larb.c +27 -0
- data/ext/larb/larb.h +31 -0
- data/ext/larb/mat2.c +303 -0
- data/ext/larb/mat2.h +30 -0
- data/ext/larb/mat2d.c +397 -0
- data/ext/larb/mat2d.h +35 -0
- data/ext/larb/mat3.c +455 -0
- data/ext/larb/mat3.h +33 -0
- data/ext/larb/mat4.c +651 -0
- data/ext/larb/mat4.h +31 -0
- data/ext/larb/matrix_utils.h +97 -0
- data/ext/larb/quat.c +567 -0
- data/ext/larb/quat.h +39 -0
- data/ext/larb/quat2.c +511 -0
- data/ext/larb/quat2.h +39 -0
- data/ext/larb/vec2.c +365 -0
- data/ext/larb/vec2.h +43 -0
- data/ext/larb/vec3.c +538 -0
- data/ext/larb/vec3.h +52 -0
- data/ext/larb/vec4.c +361 -0
- data/ext/larb/vec4.h +38 -0
- data/lib/larb/version.rb +5 -0
- data/lib/larb.rb +2 -14
- data/test/larb/color_test.rb +299 -0
- data/test/larb/mat2_test.rb +144 -0
- data/test/larb/mat2d_test.rb +197 -0
- data/test/larb/mat3_test.rb +147 -0
- data/test/larb/mat4_test.rb +347 -0
- data/test/larb/matrix_test.rb +135 -0
- data/test/larb/native_value_test.rb +157 -0
- data/test/larb/quat2_test.rb +254 -0
- data/test/larb/quat_test.rb +284 -0
- data/test/larb/vec2_test.rb +274 -0
- data/test/larb/vec3_test.rb +373 -0
- data/test/larb/vec4_test.rb +196 -0
- data/test/test_helper.rb +4 -0
- metadata +60 -18
- data/Rakefile +0 -11
- data/lib/larb/color.rb +0 -148
- data/lib/larb/mat2.rb +0 -119
- data/lib/larb/mat2d.rb +0 -180
- data/lib/larb/mat3.rb +0 -238
- data/lib/larb/mat4.rb +0 -329
- data/lib/larb/quat.rb +0 -238
- data/lib/larb/quat2.rb +0 -193
- data/lib/larb/vec2.rb +0 -150
- data/lib/larb/vec3.rb +0 -218
- 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
|