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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
- assert_equal Larb::Vec3.new(1, 2, 3), v.perspective_divide
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.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/ydah/larb
77
+ homepage: https://github.com/rbgfx/larb
75
78
  licenses:
76
79
  - MIT
77
80
  metadata:
78
- source_code_uri: https://github.com/ydah/larb
79
- changelog_uri: https://github.com/ydah/larb/blob/main/CHANGELOG.md
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.3
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: []