larb 1.0.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 +9 -1
- data/README.md +49 -0
- data/ext/larb/color.c +85 -82
- data/ext/larb/extconf.rb +1 -1
- data/ext/larb/larb.h +23 -0
- data/ext/larb/mat2.c +50 -47
- data/ext/larb/mat2d.c +84 -67
- data/ext/larb/mat3.c +73 -87
- data/ext/larb/mat4.c +180 -200
- data/ext/larb/matrix_utils.h +97 -0
- data/ext/larb/quat.c +110 -66
- data/ext/larb/quat2.c +123 -85
- data/ext/larb/vec2.c +52 -29
- data/ext/larb/vec3.c +106 -71
- data/ext/larb/vec4.c +55 -34
- data/lib/larb/version.rb +1 -1
- data/test/larb/color_test.rb +21 -0
- data/test/larb/mat2d_test.rb +25 -0
- data/test/larb/mat4_test.rb +77 -0
- data/test/larb/matrix_test.rb +135 -0
- data/test/larb/native_value_test.rb +157 -0
- data/test/larb/quat2_test.rb +93 -0
- data/test/larb/quat_test.rb +60 -0
- data/test/larb/vec2_test.rb +23 -0
- data/test/larb/vec3_test.rb +63 -0
- data/test/larb/vec4_test.rb +8 -1
- metadata +8 -5
data/test/larb/quat2_test.rb
CHANGED
|
@@ -158,4 +158,97 @@ class Quat2Test < Test::Unit::TestCase
|
|
|
158
158
|
q = Larb::Quat2.identity
|
|
159
159
|
assert_match(/Quat2/, q.inspect)
|
|
160
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
|
|
161
254
|
end
|
data/test/larb/quat_test.rb
CHANGED
|
@@ -221,4 +221,64 @@ class QuatTest < Test::Unit::TestCase
|
|
|
221
221
|
q = Larb::Quat.new(1, 2, 3, 4)
|
|
222
222
|
assert_equal q.inspect, q.to_s
|
|
223
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
|
|
224
284
|
end
|
data/test/larb/vec2_test.rb
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
require_relative "../test_helper"
|
|
4
4
|
|
|
5
5
|
class Vec2Test < Test::Unit::TestCase
|
|
6
|
+
def test_index_assignment_bounds_and_negative_indices
|
|
7
|
+
v = Larb::Vec2.new(1, 2)
|
|
8
|
+
v[-2] = 3
|
|
9
|
+
v[-1] = 4
|
|
10
|
+
assert_equal [3.0, 4.0], v.to_a
|
|
11
|
+
[-3, 2, 100].each { |index| assert_raise(IndexError) { v[index] = 99 } }
|
|
12
|
+
assert_equal [3.0, 4.0], v.to_a
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_clamp_length_validates_limits_and_handles_extreme_magnitudes
|
|
16
|
+
[Larb::Vec2, Larb::Vec3].each do |klass|
|
|
17
|
+
v = klass.new(1e200, 0)
|
|
18
|
+
[-1, Float::NAN, Float::INFINITY].each do |limit|
|
|
19
|
+
assert_raise(ArgumentError) { v.clamp_length(limit) }
|
|
20
|
+
end
|
|
21
|
+
assert_equal klass.new(2, 0), v.clamp_length(2)
|
|
22
|
+
assert_equal klass.zero, v.clamp_length(0)
|
|
23
|
+
assert_equal klass.zero, klass.zero.clamp_length(0)
|
|
24
|
+
tiny = klass.new(1e-200, 0)
|
|
25
|
+
assert_equal klass.new(1e-201, 0), tiny.clamp_length(1e-201)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
6
29
|
def test_new_with_default_values
|
|
7
30
|
v = Larb::Vec2.new
|
|
8
31
|
assert_equal 0.0, v.x
|
data/test/larb/vec3_test.rb
CHANGED
|
@@ -3,6 +3,69 @@
|
|
|
3
3
|
require_relative "../test_helper"
|
|
4
4
|
|
|
5
5
|
class Vec3Test < Test::Unit::TestCase
|
|
6
|
+
def assert_finite_vector(expected, actual)
|
|
7
|
+
actual.to_a.zip(expected).each do |value, target|
|
|
8
|
+
assert_predicate value, :finite?
|
|
9
|
+
assert_in_delta target, value, 1e-10
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_slerp_equal_opposite_and_nonunit_endpoints
|
|
14
|
+
a = Larb::Vec3.new(2, 0, 0)
|
|
15
|
+
[0, 0.5, 1].each { |t| assert_finite_vector a.to_a, a.slerp(a, t) }
|
|
16
|
+
b = Larb::Vec3.new(0, 4, 0)
|
|
17
|
+
assert_equal a, a.slerp(b, 0)
|
|
18
|
+
assert_equal b, a.slerp(b, 1)
|
|
19
|
+
assert_finite_vector [3 / Math.sqrt(2), 3 / Math.sqrt(2), 0], a.slerp(b, 0.5)
|
|
20
|
+
same_direction = Larb::Vec3.new(4, 0, 0)
|
|
21
|
+
assert_finite_vector [3, 0, 0], a.slerp(same_direction, 0.5)
|
|
22
|
+
|
|
23
|
+
[Larb::Vec3.right, Larb::Vec3.up, Larb::Vec3.new(1, 2, 3).normalize].each do |v|
|
|
24
|
+
middle = v.slerp(-v, 0.5)
|
|
25
|
+
assert middle.to_a.all?(&:finite?)
|
|
26
|
+
assert_in_delta 1, middle.length, 1e-10
|
|
27
|
+
assert_in_delta 0, middle.dot(v), 1e-10
|
|
28
|
+
assert_equal(-v, v.slerp(-v, 1))
|
|
29
|
+
assert_equal middle, v.slerp(-v, 0.5)
|
|
30
|
+
end
|
|
31
|
+
close = Larb::Vec3.new(1, 1e-8, 0).normalize
|
|
32
|
+
assert_finite_vector [1, 5e-9, 0], Larb::Vec3.right.slerp(close, 0.5)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_direction_operations_reject_zero_vectors
|
|
36
|
+
v = Larb::Vec3.right
|
|
37
|
+
assert_raise(ArgumentError) { v.project(Larb::Vec3.zero) }
|
|
38
|
+
assert_raise(ArgumentError) { v.reject(Larb::Vec3.zero) }
|
|
39
|
+
assert_raise(ArgumentError) { v.angle_between(Larb::Vec3.zero) }
|
|
40
|
+
assert_raise(ArgumentError) { Larb::Vec3.zero.angle_between(v) }
|
|
41
|
+
assert_raise(ArgumentError) { v.slerp(Larb::Vec3.zero, 0.5) }
|
|
42
|
+
assert_raise(ArgumentError) { Larb::Vec3.zero.slerp(v, 0.5) }
|
|
43
|
+
assert_raise(ArgumentError) { v.slerp(v, Float::NAN) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_slerp_near_antipodes_preserves_interpolated_length
|
|
47
|
+
a = Larb::Vec3.new(1, 2, 3).normalize
|
|
48
|
+
tangent = a.cross(Larb::Vec3.right).normalize
|
|
49
|
+
[1e-8, 1e-10, 1e-11, 1e-13].each do |offset|
|
|
50
|
+
b = (-a + tangent * offset).normalize
|
|
51
|
+
[0.25, 0.5, 0.75].each do |t|
|
|
52
|
+
result = (a * 2).slerp(b * 4, t)
|
|
53
|
+
assert result.to_a.all?(&:finite?)
|
|
54
|
+
assert_in_delta 2 + 2 * t, result.length, 1e-12
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_projection_and_angle_handle_extreme_direction_magnitudes
|
|
60
|
+
[1e200, 1e-200].each do |magnitude|
|
|
61
|
+
v = Larb::Vec3.new(3, 4, 5)
|
|
62
|
+
direction = Larb::Vec3.new(magnitude, 0, 0)
|
|
63
|
+
assert_finite_vector [3, 0, 0], v.project(direction)
|
|
64
|
+
assert_finite_vector [0, 4, 5], v.reject(direction)
|
|
65
|
+
assert_in_delta Math::PI / 2, direction.angle_between(Larb::Vec3.up * magnitude), 1e-10
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
6
69
|
def test_new_with_default_values
|
|
7
70
|
v = Larb::Vec3.new
|
|
8
71
|
assert_equal 0.0, v.x
|
data/test/larb/vec4_test.rb
CHANGED
|
@@ -78,7 +78,14 @@ class Vec4Test < Test::Unit::TestCase
|
|
|
78
78
|
|
|
79
79
|
def test_perspective_divide_w_is_0
|
|
80
80
|
v = Larb::Vec4.new(1, 2, 3, 0)
|
|
81
|
-
|
|
81
|
+
assert_raise(ArgumentError) { v.perspective_divide }
|
|
82
|
+
assert_equal Larb::Vec3.new(1, 2, 3), v.xyz
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_perspective_divide_rejects_nonfinite_w
|
|
86
|
+
[Float::NAN, Float::INFINITY].each do |w|
|
|
87
|
+
assert_raise(ArgumentError) { Larb::Vec4.new(1, 2, 3, w).perspective_divide }
|
|
88
|
+
end
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
def test_xyz
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: larb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- ext/larb/mat3.h
|
|
49
49
|
- ext/larb/mat4.c
|
|
50
50
|
- ext/larb/mat4.h
|
|
51
|
+
- ext/larb/matrix_utils.h
|
|
51
52
|
- ext/larb/quat.c
|
|
52
53
|
- ext/larb/quat.h
|
|
53
54
|
- ext/larb/quat2.c
|
|
@@ -65,18 +66,20 @@ files:
|
|
|
65
66
|
- test/larb/mat2d_test.rb
|
|
66
67
|
- test/larb/mat3_test.rb
|
|
67
68
|
- test/larb/mat4_test.rb
|
|
69
|
+
- test/larb/matrix_test.rb
|
|
70
|
+
- test/larb/native_value_test.rb
|
|
68
71
|
- test/larb/quat2_test.rb
|
|
69
72
|
- test/larb/quat_test.rb
|
|
70
73
|
- test/larb/vec2_test.rb
|
|
71
74
|
- test/larb/vec3_test.rb
|
|
72
75
|
- test/larb/vec4_test.rb
|
|
73
76
|
- test/test_helper.rb
|
|
74
|
-
homepage: https://github.com/
|
|
77
|
+
homepage: https://github.com/rbgfx/larb
|
|
75
78
|
licenses:
|
|
76
79
|
- MIT
|
|
77
80
|
metadata:
|
|
78
|
-
source_code_uri: https://github.com/
|
|
79
|
-
changelog_uri: https://github.com/
|
|
81
|
+
source_code_uri: https://github.com/rbgfx/larb
|
|
82
|
+
changelog_uri: https://github.com/rbgfx/larb/blob/main/CHANGELOG.md
|
|
80
83
|
rdoc_options: []
|
|
81
84
|
require_paths:
|
|
82
85
|
- lib
|
|
@@ -91,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
94
|
- !ruby/object:Gem::Version
|
|
92
95
|
version: '0'
|
|
93
96
|
requirements: []
|
|
94
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.19
|
|
95
98
|
specification_version: 4
|
|
96
99
|
summary: Linear algebra library for 2D/3D graphics in Ruby
|
|
97
100
|
test_files: []
|