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.
data/ext/larb/vec4.c CHANGED
@@ -20,11 +20,6 @@ static const rb_data_type_t vec4_type = {
20
20
 
21
21
  static VALUE cVec4 = Qnil;
22
22
 
23
- static double value_to_double(VALUE value) {
24
- VALUE coerced = rb_funcall(value, rb_intern("to_f"), 0);
25
- return NUM2DBL(coerced);
26
- }
27
-
28
23
  static Vec4Data *vec4_get(VALUE obj) {
29
24
  Vec4Data *data = NULL;
30
25
  TypedData_Get_Struct(obj, Vec4Data, &vec4_type, data);
@@ -51,18 +46,20 @@ VALUE vec4_alloc(VALUE klass) {
51
46
  }
52
47
 
53
48
  VALUE vec4_initialize(int argc, VALUE *argv, VALUE self) {
49
+ rb_check_frozen(self);
54
50
  VALUE vx = Qnil;
55
51
  VALUE vy = Qnil;
56
52
  VALUE vz = Qnil;
57
53
  VALUE vw = Qnil;
58
- Vec4Data *data = vec4_get(self);
59
-
60
54
  rb_scan_args(argc, argv, "04", &vx, &vy, &vz, &vw);
61
- data->x = NIL_P(vx) ? 0.0 : value_to_double(vx);
62
- data->y = NIL_P(vy) ? 0.0 : value_to_double(vy);
63
- data->z = NIL_P(vz) ? 0.0 : value_to_double(vz);
64
- data->w = NIL_P(vw) ? 1.0 : value_to_double(vw);
65
-
55
+ Vec4Data value = {
56
+ argc > 0 ? NUM2DBL(vx) : 0.0,
57
+ argc > 1 ? NUM2DBL(vy) : 0.0,
58
+ argc > 2 ? NUM2DBL(vz) : 0.0,
59
+ argc > 3 ? NUM2DBL(vw) : 1.0
60
+ };
61
+ rb_check_frozen(self);
62
+ *vec4_get(self) = value;
66
63
  return self;
67
64
  }
68
65
 
@@ -73,8 +70,8 @@ static VALUE vec4_class_bracket(int argc, VALUE *argv, VALUE klass) {
73
70
  VALUE vw = Qnil;
74
71
 
75
72
  rb_scan_args(argc, argv, "31", &vx, &vy, &vz, &vw);
76
- return vec4_build(klass, value_to_double(vx), value_to_double(vy),
77
- value_to_double(vz), NIL_P(vw) ? 1.0 : value_to_double(vw));
73
+ return vec4_build(klass, NUM2DBL(vx), NUM2DBL(vy),
74
+ NUM2DBL(vz), argc < 4 ? 1.0 : NUM2DBL(vw));
78
75
  }
79
76
 
80
77
  static VALUE vec4_class_zero(VALUE klass) {
@@ -91,8 +88,11 @@ static VALUE vec4_get_x(VALUE self) {
91
88
  }
92
89
 
93
90
  static VALUE vec4_set_x(VALUE self, VALUE value) {
91
+ rb_check_frozen(self);
94
92
  Vec4Data *data = vec4_get(self);
95
- data->x = value_to_double(value);
93
+ double component = NUM2DBL(value);
94
+ rb_check_frozen(self);
95
+ data->x = component;
96
96
  return value;
97
97
  }
98
98
 
@@ -102,8 +102,11 @@ static VALUE vec4_get_y(VALUE self) {
102
102
  }
103
103
 
104
104
  static VALUE vec4_set_y(VALUE self, VALUE value) {
105
+ rb_check_frozen(self);
105
106
  Vec4Data *data = vec4_get(self);
106
- data->y = value_to_double(value);
107
+ double component = NUM2DBL(value);
108
+ rb_check_frozen(self);
109
+ data->y = component;
107
110
  return value;
108
111
  }
109
112
 
@@ -113,8 +116,11 @@ static VALUE vec4_get_z(VALUE self) {
113
116
  }
114
117
 
115
118
  static VALUE vec4_set_z(VALUE self, VALUE value) {
119
+ rb_check_frozen(self);
116
120
  Vec4Data *data = vec4_get(self);
117
- data->z = value_to_double(value);
121
+ double component = NUM2DBL(value);
122
+ rb_check_frozen(self);
123
+ data->z = component;
118
124
  return value;
119
125
  }
120
126
 
@@ -124,8 +130,11 @@ static VALUE vec4_get_w(VALUE self) {
124
130
  }
125
131
 
126
132
  static VALUE vec4_set_w(VALUE self, VALUE value) {
133
+ rb_check_frozen(self);
127
134
  Vec4Data *data = vec4_get(self);
128
- data->w = value_to_double(value);
135
+ double component = NUM2DBL(value);
136
+ rb_check_frozen(self);
137
+ data->w = component;
129
138
  return value;
130
139
  }
131
140
 
@@ -145,14 +154,14 @@ VALUE vec4_sub(VALUE self, VALUE other) {
145
154
 
146
155
  VALUE vec4_mul(VALUE self, VALUE scalar) {
147
156
  Vec4Data *a = vec4_get(self);
148
- double s = value_to_double(scalar);
157
+ double s = NUM2DBL(scalar);
149
158
  return vec4_build(rb_obj_class(self), a->x * s, a->y * s, a->z * s,
150
159
  a->w * s);
151
160
  }
152
161
 
153
162
  VALUE vec4_div(VALUE self, VALUE scalar) {
154
163
  Vec4Data *a = vec4_get(self);
155
- double s = value_to_double(scalar);
164
+ double s = NUM2DBL(scalar);
156
165
  return vec4_build(rb_obj_class(self), a->x / s, a->y / s, a->z / s,
157
166
  a->w / s);
158
167
  }
@@ -170,7 +179,7 @@ VALUE vec4_dot(VALUE self, VALUE other) {
170
179
 
171
180
  VALUE vec4_length(VALUE self) {
172
181
  Vec4Data *a = vec4_get(self);
173
- return DBL2NUM(sqrt(a->x * a->x + a->y * a->y + a->z * a->z + a->w * a->w));
182
+ return DBL2NUM(hypot(hypot(hypot(a->x, a->y), a->z), a->w));
174
183
  }
175
184
 
176
185
  VALUE vec4_length_squared(VALUE self) {
@@ -180,18 +189,20 @@ VALUE vec4_length_squared(VALUE self) {
180
189
 
181
190
  VALUE vec4_normalize(VALUE self) {
182
191
  Vec4Data *a = vec4_get(self);
183
- double len = sqrt(a->x * a->x + a->y * a->y + a->z * a->z + a->w * a->w);
184
- return vec4_build(rb_obj_class(self), a->x / len, a->y / len, a->z / len,
185
- a->w / len);
192
+ double values[] = {a->x, a->y, a->z, a->w};
193
+ larb_normalize(values, 4);
194
+ return vec4_build(rb_obj_class(self), values[0], values[1], values[2], values[3]);
186
195
  }
187
196
 
188
197
  VALUE vec4_normalize_bang(VALUE self) {
198
+ rb_check_frozen(self);
189
199
  Vec4Data *a = vec4_get(self);
190
- double len = sqrt(a->x * a->x + a->y * a->y + a->z * a->z + a->w * a->w);
191
- a->x /= len;
192
- a->y /= len;
193
- a->z /= len;
194
- a->w /= len;
200
+ double values[] = {a->x, a->y, a->z, a->w};
201
+ larb_normalize(values, 4);
202
+ a->x = values[0];
203
+ a->y = values[1];
204
+ a->z = values[2];
205
+ a->w = values[3];
195
206
  return self;
196
207
  }
197
208
 
@@ -199,9 +210,8 @@ VALUE vec4_perspective_divide(VALUE self) {
199
210
  Vec4Data *a = vec4_get(self);
200
211
  VALUE vec3_class = rb_const_get(mLarb, rb_intern("Vec3"));
201
212
 
202
- if (a->w == 0.0 || a->w == 1.0) {
203
- return rb_funcall(vec3_class, rb_intern("new"), 3, DBL2NUM(a->x),
204
- DBL2NUM(a->y), DBL2NUM(a->z));
213
+ if (!isfinite(a->w) || a->w == 0.0) {
214
+ rb_raise(rb_eArgError, "w must be finite and nonzero for perspective division");
205
215
  }
206
216
 
207
217
  return rb_funcall(vec3_class, rb_intern("new"), 3, DBL2NUM(a->x / a->w),
@@ -259,7 +269,10 @@ VALUE vec4_near(int argc, VALUE *argv, VALUE self) {
259
269
  rb_scan_args(argc, argv, "11", &other, &epsilon);
260
270
  Vec4Data *a = vec4_get(self);
261
271
  Vec4Data *b = vec4_get(other);
262
- double eps = NIL_P(epsilon) ? 1e-6 : value_to_double(epsilon);
272
+ double eps = argc < 2 ? 1e-6 : NUM2DBL(epsilon);
273
+ if (!isfinite(eps) || eps <= 0.0) {
274
+ rb_raise(rb_eArgError, "epsilon must be finite and positive");
275
+ }
263
276
 
264
277
  if (fabs(a->x - b->x) < eps && fabs(a->y - b->y) < eps &&
265
278
  fabs(a->z - b->z) < eps && fabs(a->w - b->w) < eps) {
@@ -271,7 +284,7 @@ VALUE vec4_near(int argc, VALUE *argv, VALUE self) {
271
284
  VALUE vec4_lerp(VALUE self, VALUE other, VALUE t) {
272
285
  Vec4Data *a = vec4_get(self);
273
286
  Vec4Data *b = vec4_get(other);
274
- double s = value_to_double(t);
287
+ double s = NUM2DBL(t);
275
288
  return vec4_build(rb_obj_class(self), a->x + (b->x - a->x) * s,
276
289
  a->y + (b->y - a->y) * s,
277
290
  a->z + (b->z - a->z) * s,
@@ -296,11 +309,19 @@ VALUE vec4_inspect(VALUE self) {
296
309
  return str;
297
310
  }
298
311
 
312
+ static VALUE vec4_initialize_copy(VALUE self, VALUE other) {
313
+ if (self == other) return self;
314
+ rb_obj_init_copy(self, other);
315
+ *vec4_get(self) = *vec4_get(other);
316
+ return self;
317
+ }
318
+
299
319
  void Init_vec4(VALUE module) {
300
320
  cVec4 = rb_define_class_under(module, "Vec4", rb_cObject);
301
321
 
302
322
  rb_define_alloc_func(cVec4, vec4_alloc);
303
323
  rb_define_method(cVec4, "initialize", vec4_initialize, -1);
324
+ rb_define_method(cVec4, "initialize_copy", vec4_initialize_copy, 1);
304
325
 
305
326
  rb_define_singleton_method(cVec4, "[]", vec4_class_bracket, -1);
306
327
  rb_define_singleton_method(cVec4, "zero", vec4_class_zero, 0);
data/lib/larb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Larb
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
@@ -3,6 +3,27 @@
3
3
  require_relative "../test_helper"
4
4
 
5
5
  class ColorTest < Test::Unit::TestCase
6
+ def test_bytes_clamp_before_integer_conversion
7
+ [1e8, 1e300, Float::INFINITY].each do |large|
8
+ assert_equal [255, 0, 128, 255], Larb::Color.new(large, -large, 0.5, 1).to_bytes
9
+ assert_equal "#ff0080ff", Larb::Color.new(large, -large, 0.5, 1).to_hex
10
+ end
11
+ 4.times do |i|
12
+ values = [0, 0, 0, 1]
13
+ values[i] = Float::NAN
14
+ assert_raise(ArgumentError) { Larb::Color.new(*values).to_bytes }
15
+ end
16
+ end
17
+
18
+ def test_from_hex_validates_the_entire_string
19
+ ["#gg0000", "#ff0000zz", "#ff0000ffjunk", "f#f0000", "", "#fff",
20
+ "#ffff", " ff0000", "#ff0000\n", "ff0000\0", "#ff0000"].each do |hex|
21
+ assert_raise(ArgumentError, hex.inspect) { Larb::Color.from_hex(hex) }
22
+ end
23
+ assert_raise(TypeError) { Larb::Color.from_hex(nil) }
24
+ assert_equal [255, 128, 0, 170], Larb::Color.from_hex("#FF8000aA").to_bytes
25
+ end
26
+
6
27
  def test_new_with_default_values
7
28
  c = Larb::Color.new
8
29
  assert_equal 0.0, c.r
@@ -169,4 +169,29 @@ class Mat2dTest < Test::Unit::TestCase
169
169
  m = Larb::Mat2d.identity
170
170
  assert_match(/Mat2d/, m.inspect)
171
171
  end
172
+
173
+ def test_decomposition_round_trips_signed_scales
174
+ [-1, 1].repeated_permutation(2) do |signs|
175
+ [0, Math::PI / 2, 1.2].each do |angle|
176
+ matrix = Larb::Mat2d.from_rotation_translation_scale(
177
+ angle, Larb::Vec2.new(1, 2), Larb::Vec2.new(signs[0] * 2, signs[1] * 3)
178
+ )
179
+ scale = matrix.extract_scale
180
+ assert_equal signs.inject(:*) < 0, scale.x < 0
181
+ rebuilt = Larb::Mat2d.from_rotation_translation_scale(
182
+ matrix.extract_rotation, matrix.extract_translation, scale
183
+ )
184
+ assert rebuilt.to_a.all?(&:finite?)
185
+ matrix.to_a.zip(rebuilt.to_a).each { |a, b| assert_in_delta a, b, 1e-9 }
186
+ end
187
+ end
188
+ end
189
+
190
+ def test_decomposition_rejects_shear_zero_scale_and_nonfinite_values
191
+ [Larb::Mat2d.new([1, 0, 0.5, 1, 0, 0]), Larb::Mat2d.scaling(0, 1),
192
+ Larb::Mat2d.scaling(Float::NAN, 1)].each do |matrix|
193
+ assert_raise(ArgumentError) { matrix.extract_scale }
194
+ assert_raise(ArgumentError) { matrix.extract_rotation }
195
+ end
196
+ end
172
197
  end
@@ -267,4 +267,81 @@ class Mat4Test < Test::Unit::TestCase
267
267
  m = Larb::Mat4.identity
268
268
  assert_match(/Mat4/, m.inspect)
269
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
270
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
@@ -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