Repository: godotengine/godot (language: cpp)

Issue:
[BUG] sample_baked_with_rotation return wrong rotation when offset=0
### Godot version

v4.1.stable.official [970459615]

### System information

Godot v4.1.stable - Windows 10.0.19045 - Vulkan (Mobile) - integrated AMD Radeon(TM) Vega 8 Graphics  () - AMD Ryzen 5 3550H with Radeon Vega Mobile Gfx (8 Threads)

### Issue description

![image](https://github.com/godotengine/godot/assets/50233205/c180f8f9-5f7c-4ef2-9410-0ce3d1e0aad6)

As shown in the picture above, when the curve offset is 0, the rotation angle seems to be incorrect. I just get the rotation of the offset (contained in the transform) and then copy the sprite2d to this position.
The other offset are correct, And I've also looked at existing questions and there doesn't seem to be the exact same problem as mine.

code as below.
![image](https://github.com/godotengine/godot/assets/50233205/f317529f-a82b-4340-8fda-58adb211fea6)

Thanks for any help.


### Steps to reproduce

1, create a Node Path2D
2, invoke path2d's curve with sample_baked_with_rotation(0) # only offset=0 seemd not correct
3, display path in debug and run

### Minimal reproduction project

[Curve2DSampleTest.zip](https://github.com/godotengine/godot/files/12779488/Curve2DSampleTest.zip)



Discussion / hints:
It seems that there is also a problem with the calculation of point positions.
I just want the image to line up across the entire curve

![image](https://github.com/godotengine/godot/assets/50233205/6598a65e-8c94-434e-a020-b36f5534918f)
![image](https://github.com/godotengine/godot/assets/50233205/be28080c-da5c-4f47-9fc4-34ac81c60526)



Can reproduced in 4.2.dev6

The rotation at the start and end of the curve remains in the state when handle vector is 0.


[path-start-end.webm](https://github.com/godotengine/godot/assets/45038185/766b887e-a926-4724-b435-140f98b6490f)

Do you have time to test it and list it as a bug?
@Calinou 

Fix patch:
diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp
index de68737b8c93..ca9b247f2719 100644
--- a/scene/resources/curve.cpp
+++ b/scene/resources/curve.cpp
@@ -881,12 +881,22 @@ void Curve2D::_bake_segment2d_even_length(RBMap<real_t, Vector2> &r_bake, real_t
 
 Vector2 Curve2D::_calculate_tangent(const Vector2 &p_begin, const Vector2 &p_control_1, const Vector2 &p_control_2, const Vector2 &p_end, const real_t p_t) {
 	// Handle corner cases.
-	if (Math::is_zero_approx(p_t - 0.0f) && p_control_1.is_equal_approx(p_begin)) {
-		return (p_end - p_begin).normalized();
-	}
-
-	if (Math::is_zero_approx(p_t - 1.0f) && p_control_2.is_equal_approx(p_end)) {
-		return (p_end - p_begin).normalized();
+	if (Math::is_zero_approx(p_t - 0.0f)) {
+		if (p_control_1.is_equal_approx(p_begin)) {
+			if (p_control_1.is_equal_approx(p_control_2)) {
+				return (p_end - p_begin).normalized();
+			} else {
+				return (p_control_2 - p_begin).normalized();
+			}
+		}
+	} else if (Math::is_zero_approx(p_t - 1.0f)) {
+		if (p_control_2.is_equal_approx(p_end)) {
+			if (p_control_2.is_equal_approx(p_control_1)) {
+				return (p_end - p_begin).normalized();
+			} else {
+				return (p_end - p_control_1).normalized();
+			}
+		}
 	}
 
 	return p_begin.bezier_derivative(p_control_1, p_control_2, p_end, p_t).normalized();
@@ -1620,12 +1630,22 @@ void Curve3D::_bake_segment3d_even_length(RBMap<real_t, Vector3> &r_bake, real_t
 
 Vector3 Curve3D::_calculate_tangent(const Vector3 &p_begin, const Vector3 &p_control_1, const Vector3 &p_control_2, const Vector3 &p_end, const real_t p_t) {
 	// Handle corner cases.
-	if (Math::is_zero_approx(p_t - 0.0f) && p_control_1.is_equal_approx(p_begin)) {
-		return (p_end - p_begin).normalized();
-	}
-
-	if (Math::is_zero_approx(p_t - 1.0f) && p_control_2.is_equal_approx(p_end)) {
-		return (p_end - p_begin).normalized();
+	if (Math::is_zero_approx(p_t - 0.0f)) {
+		if (p_control_1.is_equal_approx(p_begin)) {
+			if (p_control_1.is_equal_approx(p_control_2)) {
+				return (p_end - p_begin).normalized();
+			} else {
+				return (p_control_2 - p_begin).normalized();
+			}
+		}
+	} else if (Math::is_zero_approx(p_t - 1.0f)) {
+		if (p_control_2.is_equal_approx(p_end)) {
+			if (p_control_2.is_equal_approx(p_control_1)) {
+				return (p_end - p_begin).normalized();
+			} else {
+				return (p_end - p_control_1).normalized();
+			}
+		}
 	}
 
 	return p_begin.bezier_derivative(p_control_1, p_control_2, p_end, p_t).normalized();


Test patch:
diff --git a/tests/scene/test_path_follow_3d.h b/tests/scene/test_path_follow_3d.h
index 367b60b43d45..8c39f9367b6b 100644
--- a/tests/scene/test_path_follow_3d.h
+++ b/tests/scene/test_path_follow_3d.h
@@ -45,7 +45,8 @@ bool is_equal_approx(const Vector3 &p_a, const Vector3 &p_b) {
 }
 
 TEST_CASE("[SceneTree][PathFollow3D] Sampling with progress ratio") {
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	curve->add_point(Vector3(100, 100, 0));
@@ -89,7 +90,8 @@ TEST_CASE("[SceneTree][PathFollow3D] Sampling with progress ratio") {
 }
 
 TEST_CASE("[SceneTree][PathFollow3D] Sampling with progress") {
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	curve->add_point(Vector3(100, 100, 0));
@@ -133,7 +135,8 @@ TEST_CASE("[SceneTree][PathFollow3D] Sampling with progress") {
 }
 
 TEST_CASE("[SceneTree][PathFollow3D] Removal of a point in curve") {
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	curve->add_point(Vector3(100, 100, 0));
@@ -157,7 +160,8 @@ TEST_CASE("[SceneTree][PathFollow3D] Removal of a point in curve") {
 }
 
 TEST_CASE("[SceneTree][PathFollow3D] Progress ratio out of range") {
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	Path3D *path = memnew(Path3D);
@@ -194,7 +198,8 @@ TEST_CASE("[SceneTree][PathFollow3D] Progress ratio out of range") {
 }
 
 TEST_CASE("[SceneTree][PathFollow3D] Progress out of range") {
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	Path3D *path = memnew(Path3D);
@@ -232,7 +237,8 @@ TEST_CASE("[SceneTree][PathFollow3D] Progress out of range") {
 
 TEST_CASE("[SceneTree][PathFollow3D] Calculate forward vector") {
 	const real_t dist_cube_100 = 100 * Math::sqrt(3.0);
-	Ref<Curve3D> curve = memnew(Curve3D);
+	Ref<Curve3D> curve;
+	curve.instantiate();
 	curve->add_point(Vector3(0, 0, 0));
 	curve->add_point(Vector3(100, 0, 0));
 	curve->add_point(Vector3(200, 100, -100));
@@ -283,4 +289,52 @@ TEST_CASE("[SceneTree][PathFollow3D] Calculate forward vector") {
 
 	memdelete(path);
 }
+
+TEST_CASE("[SceneTree][PathFollow3D] Calculate forward vector with degenerate curves") {
+	Ref<Curve3D> curve;
+	curve.instantiate();
+	curve->add_point(Vector3(0, 0, 1), Vector3(), Vector3(1, 0, 0));
+	curve->add_point(Vector3(1, 0, 0), Vector3(0, 0, 0), Vector3(0, 0, 0));
+	curve->add_point(Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(-1, 0, 0));
+	curve->add_point(Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(0, 0, 0));
+	curve->add_point(Vector3(0, 0, 1), Vector3(-1, 0, 0), Vector3());
+	Path3D *path = memnew(Path3D);
+	path->set_curve(curve);
+	PathFollow3D *path_follow_3d = memnew(PathFollow3D);
+	path->add_child(path_follow_3d);
+	SceneTree::get_singleton()->get_root()->add_child(path);
+
+	path_follow_3d->set_loop(false);
+	path_follow_3d->set_rotation_mode(PathFollow3D::RotationMode::ROTATION_ORIENTED);
+
+	path_follow_3d->set_progress_ratio(0.00);
+	CHECK(is_equal_approx(Vector3(-1, 0, 0), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.25);
+	CHECK(is_equal_approx(Vector3(0, 0, 1), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.50);
+	CHECK(is_equal_approx(Vector3(1, 0, 0), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.75);
+	CHECK(is_equal_approx(Vector3(0, 0, -1), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(1.00);
+	CHECK(is_equal_approx(Vector3(-1, 0, 0), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.125);
+	CHECK(is_equal_approx(Vector3(-0.688375, 0, 0.725355), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.375);
+	CHECK(is_equal_approx(Vector3(0.688375, 0, 0.725355), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.625);
+	CHECK(is_equal_approx(Vector3(0.688375, 0, -0.725355), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	path_follow_3d->set_progress_ratio(0.875);
+	CHECK(is_equal_approx(Vector3(-0.688375, 0, -0.725355), path_follow_3d->get_transform().get_basis().get_column(2)));
+
+	memdelete(path);
+}
+
 } // namespace TestPathFollow3D