From 14d321e0edd7f8e804b6f9f7f4b079d170f596e7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 22 Jul 2026 18:17:55 -0700 Subject: [PATCH] Make the 'Angle Between' node report the signed angle and fix its zero-vector output --- node-graph/nodes/math/src/lib.rs | 40 +++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 443aea8e00..9b378b8e0e 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1473,15 +1473,28 @@ fn cross_product( Item::from_parts(value.perp_dot(*other_value.element()), attributes) } -/// Calculates the angle swept between two vectors. +/// Calculates the angle swept between two vec2s. /// -/// The value is always positive and ranges from 0° (both vectors point the same direction) to 180° (both vectors point opposite directions). +/// The angle ranges from -180° to 180° (or -π to π radians) and its sign gives the sweep direction from the "Direction From" input to the "Direction To" input: positive for clockwise, negative for counterclockwise, as drawn in the viewport and matching the direction convention of the Transform node's rotation. #[node_macro::node(category("Math: Vec2"))] -fn angle_between(_: impl Ctx, vector_a: Item, vector_b: Item, radians: Item) -> Item { - let (vector_a, attributes) = vector_a.into_parts(); +fn angle_between( + _: impl Ctx, + /// The direction the angle is measured from. + direction_from: Item, + /// The direction the angle is measured to. + #[default(1., 0.)] + direction_to: Item, + /// Whether the resulting angle should be given in radians instead of degrees. + radians: Item, +) -> Item { + let (direction_from, attributes) = direction_from.into_parts(); + let direction_to = *direction_to.element(); + + if direction_from == DVec2::ZERO || direction_to == DVec2::ZERO { + return Item::from_parts(0., attributes); + } - let dot_product = vector_a.normalize_or_zero().dot(vector_b.element().normalize_or_zero()); - let angle = dot_product.acos(); + let angle = direction_from.angle_to(direction_to); let result = if *radians.element() { angle } else { angle.to_degrees() }; Item::from_parts(result, attributes) } @@ -1653,6 +1666,21 @@ mod test { assert!(lerp_between(5., -0., 1.).is_sign_negative()); } + #[test] + pub fn angle_between_signed() { + let right = DVec2::new(1., 0.); + let down = DVec2::new(0., 1.); + let angle = |a, b, radians| angle_between((), Item::new_from_element(a), Item::new_from_element(b), Item::new_from_element(radians)).into_element(); + assert_eq!(angle(right, down, false), 90.); + assert_eq!(angle(down, right, false), -90.); + } + + #[test] + pub fn angle_between_zero_vector() { + let (zero, right) = (Item::new_from_element(DVec2::ZERO), Item::new_from_element(DVec2::new(1., 0.))); + assert_eq!(angle_between((), zero, right, Item::new_from_element(false)).into_element(), 0.); + } + #[test] pub fn clamp_vec2_within_swapped_bounds() { let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y));