Skip to content

Conversation

@gw-dev101
Copy link

addresses #25811 by replacing some of the formulas with the correct ones
(the old one only worked when both vectors had the same magnitude)

i think there is still some more work to be done on the module before it is satifactory

… functions

added documentation for some methods 
and fixed project and abs 

vec probably a lot more work
Refactor project function to handle zero vector case and calculate projection properly
Copy link
Member

@spytheman spytheman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests for the changed .project methods, to prevent future regressions.

@spytheman
Copy link
Member

You have to also run v fmt -w vlib/math (or mention the specific changed files), in order to make sure that everything is kept formatted properly.
Then commit the result and push.

@gw-dev101 gw-dev101 requested a review from spytheman November 23, 2025 12:24
@tankf33der
Copy link
Contributor

Lets wait for my test(s).

@tankf33der
Copy link
Contributor

My research.

@spytheman Yes, the current implementation is incorrect. I've checked everything and I'm not approving the merge. I have several sources to prove that his implementation is not correct.

@gw-dev101 Thanks for working on this. I appreciate it. You got confused in the details and parameters, fix the code, comments, tests and come back with new commit(s).

@gw-dev101
Copy link
Author

That's the main issue with trying to make a pr at 2am
I was awake enough to notice the old one was definitely broken
But not enough to make a clean and working fix
Hopefully my second pr will go better
My bad and sorry for wasting your time

@JalonSolov
Copy link
Contributor

Not a waste of time - it brought an issue up that not many were even thinking about.

You also don't need to make a second PR - just push modifications to this one.

@Linklancien
Copy link
Contributor

Is anyone working on this issue currently ?

I can try to fix some check the weren't successful if nobody is on it already

@Linklancien
Copy link
Contributor

Linklancien commented Nov 24, 2025

I do not understand why, but it seems to be a issue
May be because of the generics ?

import math.vec

fn main() {
	v1 := vec.vec2(3.0, 4.0) // magnitude 5 vector
	v2 := vec.vec2(5.0, 6.0) // magnitude ~7.81 vector
	proj := v1.project(v2)
	solution := vec.vec2(4.68, 6.24)
	denom := v1.dot(v1)
	
	scale := v2.dot(v1) / denom
	proj2 := vec.Vec2[f64]{v1.x * scale, v1.y * scale}
	println('x: $proj.x $proj2.x $solution.x')
	println('y: $proj.y $proj2.y $solution.y')
	println('Vec: $proj $proj2 $solution')
}

Returns

x: proj: 5.0 proj2: 4.68 solution: 4.68
y: proj: 6.0 proj2: 6.24 solution: 6.24
Vec: proj: vec.Vec2[f64]{
    x: 5.0
    y: 6.0
} proj2: vec.Vec2[f64]{
    x: 4.68
    y: 6.24
} solution: vec.Vec2[f64]{
    x: 4.68
    y: 6.24
}

when using

pub fn (v Vec2[T]) project(u Vec2[T]) Vec2[T] {
	denom := T(v.dot(v))
	if denom <= vec_epsilon {
		return Vec2[T]{T(0.0), T(0.0)}
	}
	scale := u.dot(v) / denom
	return Vec2[T]{v.x * scale, v.y * scale}
}

While it should have been the same result with the function and with the equivalent code used in the main

@Linklancien
Copy link
Contributor

The generic may not be involved, by defining a function directly in the file:

fn project_intern[T](v vec.Vec2[T], u vec.Vec2[T]) vec.Vec2[T] {
	denom := v.dot(v)
	if denom <= vec.vec_epsilon {
		return vec.Vec2[T]{T(0.0), T(0.0)}
	}
	scale := u.dot(v) / denom
	return vec.Vec2[T]{v.x * scale, v.y * scale}
}

It works as entended:

proj3 := project_intern(v1, v2)

returns

proj3: vec.Vec2[f64]{
    x: 4.68
    y: 6.24
}

@tankf33der
Copy link
Contributor

@Linklancien

@gw-dev101 pushed wrong commits and we are waiting for him.

@Linklancien
Copy link
Contributor

The generic may not be involved, by defining a function directly in the file:

fn project_intern[T](v vec.Vec2[T], u vec.Vec2[T]) vec.Vec2[T] {
	denom := v.dot(v)
	if denom <= vec.vec_epsilon {
		return vec.Vec2[T]{T(0.0), T(0.0)}
	}
	scale := u.dot(v) / denom
	return vec.Vec2[T]{v.x * scale, v.y * scale}
}

It works as entended:

proj3 := project_intern(v1, v2)

returns

proj3: vec.Vec2[f64]{
    x: 4.68
    y: 6.24
}

It isn't an issue, I was juste using the wrong compilation of V, so it didn't uses the version of math.vec I was working on

@tankf33der
Copy link
Contributor

There is a fix for vec3 only. Rest must have the same.
This patch passed 1M loop against cglm library + zero vectors too.

This is a candidate for comparison.

commit 28a0a03663fa359da954816d0affbaa3fc0163b8 (HEAD -> vec_project)
Author: Mike <[email protected]>
Date:   Tue Nov 25 20:39:48 2025 +0200

    math.vec: fix

diff --git a/vlib/math/vec/vec3.v b/vlib/math/vec/vec3.v
index 6f82d231e..5e72616f1 100644
--- a/vlib/math/vec/vec3.v
+++ b/vlib/math/vec/vec3.v
@@ -261,7 +261,7 @@ pub fn (v Vec3[T]) perpendicular(u Vec3[T]) Vec3[T] {
 
 // project returns the projected vector.
 pub fn (v Vec3[T]) project(u Vec3[T]) Vec3[T] {
-       percent := v.dot(u) / u.dot(v)
+       percent := v.dot(u) / u.dot(u)
        return u.mul_scalar(percent)
 }

@Linklancien
Copy link
Contributor

 // project returns the projected vector.
 pub fn (v Vec3[T]) project(u Vec3[T]) Vec3[T] {
-       percent := v.dot(u) / u.dot(v)
+       percent := v.dot(u) / u.dot(u)
        return u.mul_scalar(percent)
 }

This doesn't work for u : (0; 0; 0)
Does it ?

If you project any vector on the null vector, it must return a null vector or a nan vector ?

@tankf33der
Copy link
Contributor

 // project returns the projected vector.
 pub fn (v Vec3[T]) project(u Vec3[T]) Vec3[T] {
-       percent := v.dot(u) / u.dot(v)
+       percent := v.dot(u) / u.dot(u)
        return u.mul_scalar(percent)
 }

This doesn't work for u : (0; 0; 0) Does it ?

If you project any vector on the null vector, it must return a null vector or a nan vector ?

i see nan for my patch and cglm.

@Linklancien
Copy link
Contributor

@tankf33der

Si it is intended that
(1;0;0) Project on (0;0;0) returns
(nan;nan;nan)
And not
(0;0;0)

It feel strange because, if you project into a vector that doesn't existe, the line passing by this vectors also doesn't exist
And so it must return a (0;0;0) vector

I am sorry if I am not clear enought

@gw-dev101
Copy link
Author

that was my assumption too

@tankf33der
Copy link
Contributor

that was my assumption too

@Linklancien @gw-dev101 - cglm returns NaN and I'm convinced that we should follow this machinery. Do you have other libraries that we can compare with and check what they return?

@Linklancien
Copy link
Contributor

Linklancien commented Nov 25, 2025

that was my assumption too

@Linklancien @gw-dev101 - cglm returns NaN and I'm convinced that we should follow this machinery. Do you have other libraries that we can compare with and check what they return?

Makes sense, I will change accordigly

@tankf33der
Copy link
Contributor

@Linklancien @gw-dev101 python's numpy also returns NaN.

@gw-dev101
Copy link
Author

unreal engine one does return the zero vector
i didn't even manage to find the numpy one

@tankf33der
Copy link
Contributor

unreal engine one does return the zero vector i didn't even manage to find the numpy one

import numpy as np

v = np.array([1.0, 0.0, 0.0])
u = np.array([0.0, 0.0, 0.0])

projection = u * (np.dot(v, u) / np.dot(u, u))
print(projection)

which generates a warning and NaN.
Is it ok? :/

@Linklancien
Copy link
Contributor

unreal engine one does return the zero vector i didn't even manage to find the numpy one

import numpy as np

v = np.array([1.0, 0.0, 0.0])
u = np.array([0.0, 0.0, 0.0])

projection = u * (np.dot(v, u) / np.dot(u, u))
print(projection)

which generates a warning and NaN. Is it ok? :/

I am ok with returning a nan
I just do not know how to declare that a nan is expected for the test

fn test_vec2_project_zero_vector() {
	v1 := vec.vec2(0.0, 0.0)
	v2 := vec.vec2(5.0, 6.0)
	proj := v1.project(v2)
	println(proj)
	assert proj.x == nan
	assert proj.y == nan
}

When using this code it doesn't work, do you know a way ?

@tankf33der
Copy link
Contributor

unreal engine one does return the zero vector i didn't even manage to find the numpy one

import numpy as np

v = np.array([1.0, 0.0, 0.0])
u = np.array([0.0, 0.0, 0.0])

projection = u * (np.dot(v, u) / np.dot(u, u))
print(projection)

which generates a warning and NaN. Is it ok? :/

I am ok with returning a nan I just do not know how to declare that a nan is expected for the test

fn test_vec2_project_zero_vector() {
	v1 := vec.vec2(0.0, 0.0)
	v2 := vec.vec2(5.0, 6.0)
	proj := v1.project(v2)
	println(proj)
	assert proj.x == nan
	assert proj.y == nan
}

When using this code it doesn't work, do you know a way ?

i am using math.is_nan()

@gw-dev101
Copy link
Author

gw-dev101 commented Nov 25, 2025

NaNs are weird in that by definition they are
not equal to themselves or anything else

fn test_vec2_project_zero_vector() {
	v1 := vec.vec2(0.0, 0.0)
	v2 := vec.vec2(5.0, 6.0)
	proj := v1.project(v2)
	println(proj)
	assert proj.x != proj.x
	assert proj.y != proj.y
}

this should work

@Linklancien
Copy link
Contributor

this should work

This indeed work, maybe math.is_nan() is more explicit ?

@gw-dev101
Copy link
Author

gw-dev101 commented Nov 25, 2025

a quick check tell me that math.isnan() is doing exactly that so yeah
could it be worth making isnan() inline somewhere in the future ?

test_vec2_project_zero_vector
test_vec2_project_onto_zero
@JalonSolov
Copy link
Contributor

Remember: Marking something inline does not guarantee that it will be inlined - the C compiler is free to ignore that directive.

@gw-dev101
Copy link
Author

i sure hope marking something inline never make it not inline if the compiler was going to inline it to begin with

@JalonSolov
Copy link
Contributor

No, the compiler can completely ignore it. It will automatically inline things that are worthwhile to inline, and refuse to inline things that aren't.

Adding that marker just tells the compiler you would like for it to be inlined, but as I said, it can ignore it.

@medvednikov
Copy link
Member

We can probably fix it on the V side by inlining it ourselves :)

@gw-dev101
Copy link
Author

on the v side isnan(x) is basically

return x!=x

so it should inline pretty nicely

@Linklancien
Copy link
Contributor

All the test files must passed with this push, it least it does on my PC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants