Skip to content

Commit 8938a40

Browse files
authored
math: fix vec2,3,4 project not using the right formulas (fix #25811) (#25813)
1 parent 64f1162 commit 8938a40

6 files changed

Lines changed: 221 additions & 13 deletions

File tree

‎vlib/math/vec/vec2.v‎

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,17 @@ pub fn (v Vec2[T]) magnitude_y() T {
221221
}
222222

223223
// dot returns the dot product of `v` and `u`.
224+
// The dot product is a scalar value that represents the magnitude of one vector
225+
// projected onto another vector.
226+
// It is calculated by multiplying the corresponding components of the vectors
227+
// and summing the results.
228+
// example:
229+
// ```v
230+
// v := vec2[f32](3, 4) //magnitude = 5
231+
// u := vec2[f32](5, 6) //magnitude = 7.81
232+
// dot := v.dot(u) // 3*5 + 4*6 = 15 + 24 = 39
233+
// (dot) // Output: 39
234+
// ```
224235
pub fn (v Vec2[T]) dot(u Vec2[T]) T {
225236
return (v.x * u.x) + (v.y * u.y)
226237
}
@@ -253,9 +264,22 @@ pub fn (v Vec2[T]) perpendicular(u Vec2[T]) Vec2[T] {
253264
}
254265

255266
// project returns the projected vector.
267+
// The projection of vector `u` onto vector `v` is the orthogonal projection
268+
// of `u` onto a straight line parallel to `v` that passes through the origin.
269+
// This is equivalent to the vector projection of `u` onto the unit vector in the direction of `v`.
270+
// and is given by the formula: proj_v(u) = (u · v / |v|^2) * v
271+
// where "·" denotes the dot product and |v| is the magnitude of vector `v`.
272+
// If `u` is a zero vector, the result will also be a zero vector.
273+
// example:
274+
// ```v
275+
// v := vec2[f32](3, 4)
276+
// u := vec2[f32](5, 6)
277+
// proj := v.project(u)
278+
// println(proj) // Output: vec2[f32](3.61, 4.81)
279+
// ```
256280
pub fn (v Vec2[T]) project(u Vec2[T]) Vec2[T] {
257-
percent := v.dot(u) / u.dot(v)
258-
return u.mul_scalar(percent)
281+
scale := u.dot(v) / v.dot(v)
282+
return v.mul_scalar(scale)
259283
}
260284

261285
// rotate_around_cw returns the vector `v` rotated *clockwise* `radians` around an origin vector `o` in Cartesian space.
@@ -351,6 +375,15 @@ pub fn (p1 Vec2[T]) angle_towards(p2 Vec2[T]) T {
351375
}
352376

353377
// angle returns the angle in radians of the vector.
378+
// example:
379+
// ```v
380+
// v := vec2[f32](3.0, 4.0)
381+
// a := v.angle()
382+
// assert a == 0.64 (approximate value in radians)
383+
// w := vec2[f32](0.0, 1.0)
384+
// b := w.angle()
385+
// assert b == 1.57 (approximate value in radians)
386+
// ```
354387
pub fn (v Vec2[T]) angle() T {
355388
$if T is f64 {
356389
return math.atan2(v.y, v.x)
@@ -361,12 +394,8 @@ pub fn (v Vec2[T]) angle() T {
361394

362395
// abs sets `x` and `y` field values to their absolute values.
363396
pub fn (mut v Vec2[T]) abs() {
364-
if v.x < 0 {
365-
v.x = math.abs(v.x)
366-
}
367-
if v.y < 0 {
368-
v.y = math.abs(v.y)
369-
}
397+
v.x = math.abs(v.x)
398+
v.y = math.abs(v.y)
370399
}
371400

372401
// clean returns a vector with all fields of this vector set to zero (0) if they fall within `tolerance`.
@@ -392,6 +421,14 @@ pub fn (mut v Vec2[T]) clean_tolerance[U](tolerance U) {
392421
}
393422

394423
// inv returns the inverse, or reciprocal, of the vector.
424+
// If a field is zero, its inverse is also set to zero to avoid division by zero.
425+
// the direction the vector points is generally not preserved, but
426+
// the magnitude of each field is inverted.
427+
// example:
428+
// ```v
429+
// v := vec2[f32](2.0, 4.0)
430+
// inv_v := v.inv() // inv_v == vec2[f32](0.5, 0.25)
431+
// ```
395432
pub fn (v Vec2[T]) inv() Vec2[T] {
396433
return Vec2[T]{
397434
x: if v.x != 0 { T(1) / v.x } else { 0 }
@@ -400,6 +437,13 @@ pub fn (v Vec2[T]) inv() Vec2[T] {
400437
}
401438

402439
// normalize normalizes the vector.
440+
// A normalized vector has the same direction as the original vector but a magnitude of 1.
441+
// If the vector has a magnitude of 0, a zero vector is returned since we cannot find the direction of a zero-length vector.
442+
// example:
443+
// ```v
444+
// v := vec2[f32](3.0, 4.0)//magnitude = 5.0
445+
// n := v.normalize() // n == vec2[f32](0.6, 0.8) // magnitude = 1.0
446+
// ```
403447
pub fn (v Vec2[T]) normalize() Vec2[T] {
404448
m := v.magnitude()
405449
if m == 0 {
@@ -412,6 +456,12 @@ pub fn (v Vec2[T]) normalize() Vec2[T] {
412456
}
413457

414458
// sum returns a sum of all the fields.
459+
// example:
460+
// ```v
461+
// v := vec2[f32](3.0, 4.0)
462+
// s := v.sum()
463+
// assert s == 7.0
464+
// ```
415465
pub fn (v Vec2[T]) sum() T {
416466
return v.x + v.y
417467
}

‎vlib/math/vec/vec2_test.v‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import math { close, radians, veryclose }
1+
import math { close, is_nan, radians, tolerance, veryclose }
22
import math.vec
33

44
fn test_vec2_int() {
@@ -228,3 +228,74 @@ fn test_vec2_rotate_around_ccw_2() {
228228
assert close(v.x, -1.0)
229229
assert close(v.y, -1.0)
230230
}
231+
232+
// Test for Vec2 projection
233+
//
234+
fn test_vec2_project_onto_basic() {
235+
u := vec.vec2(3.0, 4.0) // magnitude 5 vector
236+
v := vec.vec2(5.0, 6.0) // magnitude ~7.81 vector
237+
// hand-computed:
238+
// u·v = 5*3 + 6*4 = 39
239+
// |v|^2 = 3^2 + 4^2 = 25
240+
// scale = 39/25 = 1.56
241+
// proj = scale * v = (1.56*3, 1.56*4) = (4.68, 6.24)
242+
proj := u.project(v)
243+
assert tolerance(proj.x, 4.68, vec.vec_epsilon)
244+
assert tolerance(proj.y, 6.24, vec.vec_epsilon)
245+
}
246+
247+
// Test for Vec2 projection onto zero vector
248+
// project v into the null vector
249+
fn test_vec2_project_onto_zero() {
250+
u := vec.vec2(0.0, 0.0)
251+
v := vec.vec2(5.0, 6.0)
252+
proj := u.project(v)
253+
// must be nan
254+
assert is_nan(proj.x)
255+
assert is_nan(proj.y)
256+
}
257+
258+
// Test for Vec2 projection of zero vector
259+
// project a null vector
260+
fn test_vec2_project_zero_vector() {
261+
u := vec.vec2(3.0, 4.0)
262+
v := vec.vec2(0.0, 0.0)
263+
proj := u.project(v)
264+
assert proj.x == 0.0
265+
assert proj.y == 0.0
266+
}
267+
268+
// Test for Vec2 projection onto itself
269+
//
270+
fn test_vec2_project_onto_self() {
271+
u := vec.vec2(3.0, 4.0)
272+
proj := u.project(u)
273+
assert veryclose(proj.x, u.x)
274+
assert veryclose(proj.y, u.y)
275+
}
276+
277+
// Test for Vec2 projection onto orthogonal vector
278+
//
279+
fn test_vec2_project_onto_orthogonal() {
280+
u := vec.vec2(1.0, 0.0)
281+
v := vec.vec2(0.0, 1.0)
282+
proj := u.project(v)
283+
// more sensitive to floating point errors so i think close is better here
284+
assert close(proj.x, 0.0)
285+
assert close(proj.y, 0.0)
286+
}
287+
288+
// Test for Vec2 projection with negative components
289+
//
290+
fn test_vec2_project_negative_components() {
291+
u := vec.vec2(-3.0, 4.0)
292+
v := vec.vec2(5.0, -6.0)
293+
// hand-computed:
294+
// u·v = 5*-3 + -6*4 = -15 - 24 = -39
295+
// |v|^2 = -3^2 + 4^2 = 9 + 16 = 25
296+
// scale = -39/25 = -1.56
297+
// proj = scale * v = (-1.56*-3, -1.56*4) = (4.68, -6.24)
298+
proj := u.project(v)
299+
assert tolerance(proj.x, 4.68, vec.vec_epsilon)
300+
assert tolerance(proj.y, -6.24, vec.vec_epsilon)
301+
}

‎vlib/math/vec/vec3.v‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,18 @@ pub fn (v Vec3[T]) perpendicular(u Vec3[T]) Vec3[T] {
260260
}
261261

262262
// project returns the projected vector.
263+
// The projection of vector `u` onto vector `v` is the orthogonal projection
264+
// of `u` onto a straight line parallel to `v` that passes through the origin.
265+
// This is equivalent to the vector projection of `u` onto the unit vector in the direction of `v`.
266+
// and is given by the formula: proj_v(u) = (u · v / |v|^2) * v
267+
// where "·" denotes the dot product and |v| is the magnitude of vector `v`.
268+
// If `u` is a zero vector, the result will also be a zero vector.
269+
// example:
270+
// TODO: add examples
271+
// ```
263272
pub fn (v Vec3[T]) project(u Vec3[T]) Vec3[T] {
264-
percent := v.dot(u) / u.dot(v)
265-
return u.mul_scalar(percent)
273+
scale := T(u.dot(v) / v.dot(v))
274+
return v.mul_scalar(scale)
266275
}
267276

268277
// eq returns a bool indicating if the two vectors are equal.

‎vlib/math/vec/vec3_test.v‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math { veryclose }
12
import math.vec
23

34
fn test_vec3_int() {
@@ -88,3 +89,45 @@ fn test_vec3_f64_utils_2() {
8889
assert invv2.y == 0.5
8990
assert invv2.z == 0.25
9091
}
92+
93+
// sample tests for vec3 projection
94+
fn test_vec3_project_onto_basic() {
95+
u := vec.vec3(3.0, 4.0, 0.0) // magnitude 5 vector
96+
v := vec.vec3(5.0, 6.0, 0.0) // magnitude ~7.81 vector
97+
// hand-computed:
98+
// u·v = 5*3 + 6*4 + 0*0 = 39
99+
// |v|^2 = 3^2 + 4^2 +0^2 = 25
100+
// scale = 39/25 = 1.56
101+
// proj = scale * v = (1.56*3, 1.56*4, 1.56*0) = (4.68, 6.24, 0)
102+
proj := u.project(v)
103+
assert veryclose(proj.x, 4.68)
104+
assert veryclose(proj.y, 6.24)
105+
assert veryclose(proj.z, 0.0)
106+
}
107+
108+
// Test for Vec3 projection onto zero vector
109+
//
110+
fn test_vec3_project_onto_zero() {
111+
u := vec.vec3(3.0, 4.0, 0.0)
112+
v := vec.vec3(0.0, 0.0, 0.0)
113+
proj := u.project(v)
114+
assert proj.x == 0.0
115+
assert proj.y == 0.0
116+
assert proj.z == 0.0
117+
}
118+
119+
// Test for vec3 projection at an angle
120+
//
121+
fn test_vec3_project_onto_angle() {
122+
u := vec.vec3(1.0, 0.0, 0.0) // magnitude 1 vector
123+
v := vec.vec3(1.0, 1.0, 0.0) // magnitude sqrt(2) vector
124+
// hand-computed:
125+
// u·v = 1*1 + 0*1 + 0*0 = 1
126+
// |v|^2 = 1^2 + 0^2 +0^2 = 1
127+
// scale = 1/1 = 1
128+
// proj = scale * v = (1*1, 1*0, 1*0) = (1, 0, 0)
129+
proj := u.project(v)
130+
assert veryclose(proj.x, 1.0)
131+
assert veryclose(proj.y, 0.0)
132+
assert veryclose(proj.z, 0.0)
133+
}

‎vlib/math/vec/vec4.v‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,18 @@ pub fn (v Vec4[T]) perpendicular(u Vec4[T]) Vec4[T] {
276276
}
277277

278278
// project returns the projected vector.
279+
// The projection of vector `u` onto vector `v` is the orthogonal projection
280+
// of `u` onto a straight line parallel to `v` that passes through the origin.
281+
// This is equivalent to the vector projection of `u` onto the unit vector in the direction of `v`.
282+
// and is given by the formula: proj_v(u) = (u · v / |v|^2) * v
283+
// where "·" denotes the dot product and |v| is the magnitude of vector `v`.
284+
// If `u` is a zero vector, the result will also be a zero vector.
285+
// example:
286+
// TODO: add examples
287+
// ```
279288
pub fn (v Vec4[T]) project(u Vec4[T]) Vec4[T] {
280-
percent := v.dot(u) / u.dot(v)
281-
return u.mul_scalar(percent)
289+
scale := u.dot(v) / v.dot(v)
290+
return v.mul_scalar(scale)
282291
}
283292

284293
// eq returns a bool indicating if the two vectors are equal.

‎vlib/math/vec/vec4_test.v‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,29 @@ fn test_vec4_f64_utils_2() {
9797
assert invv2.z == 0.25
9898
assert invv2.w == 1.0
9999
}
100+
101+
// sample tests for vec4 projection
102+
fn test_vec4_project_onto_basic() {
103+
u := vec.vec4(3.0, 4.0, 0.0, 0.0) // magnitude 5 vector
104+
v := vec.vec4(5.0, 6.0, 0.0, 0.0) // magnitude ~7.81 vector
105+
// hand-computed:
106+
// u·v = 5*3 + 6*4 + 0*0 + 0*0 = 39
107+
// |v|^2 = 3^2 + 4^2 +0^2 +0^2 = 25
108+
proj := u.project(v)
109+
assert proj.x == 3.0
110+
assert proj.y == 4.0
111+
assert proj.z == 0.0
112+
assert proj.w == 0.0
113+
}
114+
115+
// Test for Vec4 projection onto zero vector
116+
//
117+
fn test_vec4_project_onto_zero() {
118+
u := vec.vec4(3.0, 4.0, 0.0, 0.0)
119+
v := vec.vec4(0.0, 0.0, 0.0, 0.0)
120+
proj := u.project(v)
121+
assert proj.x == 0.0
122+
assert proj.y == 0.0
123+
assert proj.z == 0.0
124+
assert proj.w == 0.0
125+
}

0 commit comments

Comments
 (0)