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/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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
a->
|
|
193
|
-
a->
|
|
194
|
-
a->
|
|
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
|
|
203
|
-
|
|
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 =
|
|
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 =
|
|
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
data/test/larb/color_test.rb
CHANGED
|
@@ -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
|
data/test/larb/mat2d_test.rb
CHANGED
|
@@ -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
|
data/test/larb/mat4_test.rb
CHANGED
|
@@ -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
|