New nodes: 'Sign', 'Distance', 'Cross Product', and 'Lerp' (#4361)

This commit is contained in:
Keavon Chambers
2026-07-22 22:03:50 -07:00
committed by GitHub
parent 2f65a3aee2
commit 515a5265fe
4 changed files with 225 additions and 39 deletions

View File

@@ -1170,7 +1170,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
DocumentNodeDefinition {
identifier: "Split Vec2",
category: "Math: Vector",
category: "Math: Vec2",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {

View File

@@ -6,7 +6,7 @@ use glam::{DVec2, IVec2, UVec2};
/// Obtains the X or Y component of a vec2.
///
/// The inverse of this node is **Combine Vec2**, which composes a vec2 from its X and Y components.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
#[node_macro::node(name("Extract XY"), category("Math: Vec2"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: Item<T>, axis: Item<XY>) -> Item<f64> {
let vector = vector.into_element();
let axis = axis.into_element();

View File

@@ -84,7 +84,7 @@ fn math<T: num_traits::float::Float>(
Item::from_parts(result, attributes)
}
/// The addition operation (`+`) calculates the sum of two scalar numbers or vectors.
/// The addition operation (`+`) calculates the sum of two scalar numbers or vec2s.
#[node_macro::node(category("Math: Arithmetic"))]
fn add<A: Add<B>, B>(
_: impl Ctx,
@@ -100,7 +100,7 @@ fn add<A: Add<B>, B>(
Item::from_parts(augend + addend.into_element(), attributes)
}
/// The subtraction operation (`-`) calculates the difference between two scalar numbers or vectors.
/// The subtraction operation (`-`) calculates the difference between two scalar numbers or vec2s.
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<A: Sub<B>, B>(
_: impl Ctx,
@@ -116,7 +116,7 @@ fn subtract<A: Sub<B>, B>(
Item::from_parts(minuend - subtrahend.into_element(), attributes)
}
/// The multiplication operation (`×`) calculates the product of two scalar numbers, vectors, or transforms.
/// The multiplication operation (`×`) calculates the product of two scalar numbers, vec2s, or transforms.
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<A: Mul<B>, B>(
_: impl Ctx,
@@ -174,7 +174,7 @@ impl SafeDivide<DVec2> for f64 {
}
}
/// The division operation (`÷`) calculates the quotient of two scalar numbers or vectors.
/// The division operation (`÷`) calculates the quotient of two scalar numbers or vec2s.
///
/// Produces 0 for any division by 0. With vec2 inputs, this applies separately to the X and Y components.
#[node_macro::node(category("Math: Arithmetic"))]
@@ -227,7 +227,7 @@ fn reciprocal<T: Componentwise>(
Item::from_parts(value.componentwise(|value| if value == 0. { 0. } else { 1. / value }), attributes)
}
/// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors.
/// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vec2s.
///
/// The sign of the result shares the sign of the numerator unless *Always Positive* is enabled.
#[node_macro::node(category("Math: Arithmetic"))]
@@ -639,6 +639,59 @@ fn remap<U: num_traits::float::Float>(
Item::from_parts(result, attributes)
}
trait Lerp {
fn lerp(self, end: Self, factor: f64) -> Self;
}
impl Lerp for f64 {
fn lerp(self, end: Self, factor: f64) -> Self {
self * (1. - factor) + end * factor
}
}
impl Lerp for f32 {
fn lerp(self, end: Self, factor: f64) -> Self {
(self as f64 * (1. - factor) + end as f64 * factor) as f32
}
}
impl Lerp for DVec2 {
fn lerp(self, end: Self, factor: f64) -> Self {
self * (1. - factor) + end * factor
}
}
/// Linearly interpolates between the start and end values, where a factor of 0 gives the start value, 1 gives the end value, and 0.5 gives their midpoint.
///
/// With vec2 inputs, this traces the straight line path between the two points.
#[node_macro::node(category("Math: Numeric"))]
fn lerp<T: Lerp>(
_: impl Ctx,
/// The value produced when the factor is 0.
#[implementations(f64, f32, DVec2)]
start: Item<T>,
/// The value produced when the factor is 1.
#[default(1.)]
#[implementations(f64, f32, DVec2)]
end: Item<T>,
/// The mix between the start (at 0) and end (at 1) values.
#[default(0.5)]
factor: Item<f64>,
/// Whether to constrain the factor within 0 to 1, preventing extrapolation beyond the start and end values.
#[default(true)]
clamped: Item<bool>,
) -> Item<T> {
let (start, attributes) = start.into_parts();
let factor = if *clamped.element() { factor.element().clamp(0., 1.) } else { *factor.element() };
// Exact endpoint factors pass the endpoint through untouched, since the unused operand would otherwise contaminate the weighted sum (NaN or infinity times 0 is NaN)
let result = if factor == 0. {
start
} else if factor == 1. {
end.into_element()
} else {
start.lerp(end.into_element(), factor)
};
Item::from_parts(result, attributes)
}
/// The random function (`rand`) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn random(
@@ -769,6 +822,30 @@ fn absolute_value<T: AbsoluteValue>(
Item::from_parts(value.abs(), attributes)
}
/// The sign function (`sign`) reports whether an input value is positive (1), negative (-1), or zero (0).
///
/// With a vec2 input, this applies separately to the X and Y components.
#[node_macro::node(category("Math: Numeric"))]
fn sign<T: Componentwise>(
_: impl Ctx,
/// The number whose sign is checked.
#[implementations(f64, f32, DVec2)]
value: Item<T>,
) -> Item<T> {
let (value, attributes) = value.into_parts();
let result = value.componentwise(|value| {
if value > 0. {
1.
} else if value < 0. {
-1.
} else {
0.
}
});
Item::from_parts(result, attributes)
}
pub trait MinMax<Rhs = Self> {
type Output;
fn minimum(self, other: Rhs) -> Self::Output;
@@ -1234,7 +1311,7 @@ fn percentage_value(_: impl Ctx, _primary: (), percentage: Item<Percentage>) ->
percentage
}
/// Constructs a two-dimensional vector value which may be set to any XY pair.
/// Constructs a vec2 value, a two-dimensional quantity which may be set to any XY pair.
#[node_macro::node(category("Value"), name("Vec2 Value"))]
fn vec2_value(_: impl Ctx, _primary: (), #[name("Vec2")] vec2: Item<DVec2>) -> Item<DVec2> {
vec2
@@ -1337,7 +1414,7 @@ fn footprint_value(_: impl Ctx, _primary: (), transform: Item<DAffine2>, #[defau
/// Composes a vec2 from its X and Y components.
///
/// The inverse of this node is **Split Vec2**, which decomposes a vec2 back into its X and Y components.
#[node_macro::node(category("Math: Vector"), name("Combine Vec2"))]
#[node_macro::node(category("Math: Vec2"), name("Combine Vec2"))]
fn combine_vec2(
_: impl Ctx,
_primary: (),
@@ -1355,34 +1432,51 @@ fn combine_vec2(
///
/// Calculated as `‖a‖‖b‖cos(θ)`, it represents the product of their lengths (`‖a‖‖b‖`) scaled by the alignment of their directions (`cos(θ)`).
/// The output ranges from the positive to negative product of their lengths based on when they are pointing in the same or opposite directions.
/// If any vector has zero length, the output is 0.
#[node_macro::node(category("Math: Vector"))]
/// If either vec2 has zero length, the output is 0.
#[node_macro::node(category("Math: Vec2"))]
fn dot_product(
_: impl Ctx,
/// An operand of the dot product operation.
vector_a: Item<DVec2>,
value: Item<DVec2>,
/// The other operand of the dot product operation.
#[default(1., 0.)]
vector_b: Item<DVec2>,
/// Whether to normalize both input vectors so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment.
other_value: Item<DVec2>,
/// Whether to normalize both input vec2s so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment.
normalize: Item<bool>,
) -> Item<f64> {
let (vector_a, attributes) = vector_a.into_parts();
let vector_b = *vector_b.element();
let (value, attributes) = value.into_parts();
let other_value = *other_value.element();
let result = if *normalize.element() {
vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero())
value.normalize_or_zero().dot(other_value.normalize_or_zero())
} else {
vector_a.dot(vector_b)
value.dot(other_value)
};
Item::from_parts(result, attributes)
}
/// The cross product operation (`×`) calculates the signed area of the parallelogram formed by a vec2 pair.
///
/// The sign gives the rotation direction from the first vec2 to the second: positive for clockwise, negative for counterclockwise, and 0 when both are parallel, as drawn in the viewport.
#[node_macro::node(category("Math: Vec2"))]
fn cross_product(
_: impl Ctx,
/// The vec2 on the left-hand side of the cross product operation.
value: Item<DVec2>,
/// The vec2 on the right-hand side of the cross product operation.
#[default(1., 0.)]
other_value: Item<DVec2>,
) -> Item<f64> {
let (value, attributes) = value.into_parts();
Item::from_parts(value.perp_dot(*other_value.element()), attributes)
}
/// Calculates the angle swept between two vectors.
///
/// The value is always positive and ranges from 0° (both vectors point the same direction) to 180° (both vectors point opposite directions).
#[node_macro::node(category("Math: Vector"))]
#[node_macro::node(category("Math: Vec2"))]
fn angle_between(_: impl Ctx, vector_a: Item<DVec2>, vector_b: Item<DVec2>, radians: Item<bool>) -> Item<f64> {
let (vector_a, attributes) = vector_a.into_parts();
@@ -1406,46 +1500,60 @@ impl ToPosition for DAffine2 {
}
}
/// Calculates the angle needed for a rightward-facing object placed at the observer position to turn so it points toward the target position.
#[node_macro::node(category("Math: Vector"))]
/// Calculates the angle needed for a rightward-facing object placed at the "Position From" point to turn so it points toward the "Position To" point.
#[node_macro::node(category("Math: Vec2"))]
fn angle_to<T: ToPosition, U: ToPosition>(
_: impl Ctx,
/// The position from which the angle is measured.
#[implementations(DVec2, DAffine2, DVec2, DAffine2)]
observer: Item<T>,
position_from: Item<T>,
/// The position toward which the angle is measured.
#[expose]
#[implementations(DVec2, DVec2, DAffine2, DAffine2)]
target: Item<U>,
position_to: Item<U>,
/// Whether the resulting angle should be given in radians instead of degrees.
radians: Item<bool>,
) -> Item<f64> {
let (observer, attributes) = observer.into_parts();
let (position_from, attributes) = position_from.into_parts();
let from = observer.to_position();
let to = target.into_element().to_position();
let from = position_from.to_position();
let to = position_to.into_element().to_position();
let delta = to - from;
let angle = delta.y.atan2(delta.x);
let result = if *radians.element() { angle } else { angle.to_degrees() };
Item::from_parts(result, attributes)
}
/// The magnitude operator (`‖x‖`) calculates the length of a vec2, which is the distance from the base to the tip of the arrow represented by the vector.
#[node_macro::node(category("Math: Vector"))]
fn magnitude(_: impl Ctx, vector: Item<DVec2>) -> Item<f64> {
let (vector, attributes) = vector.into_parts();
/// The magnitude operator (`‖x‖`) calculates the length of a vec2, which is the distance from the base to the tip of the arrow it represents.
#[node_macro::node(category("Math: Vec2"))]
fn magnitude(_: impl Ctx, vec2: Item<DVec2>) -> Item<f64> {
let (vec2, attributes) = vec2.into_parts();
Item::from_parts(vector.length(), attributes)
Item::from_parts(vec2.length(), attributes)
}
/// Scales the input vector to unit length while preserving its direction. This is equivalent to dividing the input vector by its own magnitude.
///
/// Returns 0 when the input vector has zero length.
#[node_macro::node(category("Math: Vector"))]
fn normalize(_: impl Ctx, vector: Item<DVec2>) -> Item<DVec2> {
let (vector, attributes) = vector.into_parts();
/// Measures the distance between two points, which is the length of the straight line segment connecting them.
#[node_macro::node(category("Math: Vec2"))]
fn distance(
_: impl Ctx,
/// The point the distance is measured from.
position_from: Item<DVec2>,
/// The point the distance is measured to.
position_to: Item<DVec2>,
) -> Item<f64> {
let (position_from, attributes) = position_from.into_parts();
Item::from_parts(vector.normalize_or_zero(), attributes)
Item::from_parts(position_from.distance(*position_to.element()), attributes)
}
/// Scales the input vec2 to unit length while preserving its direction. This is equivalent to dividing the input vec2 by its own magnitude.
///
/// Returns 0 when the input vec2 has zero length.
#[node_macro::node(category("Math: Vec2"))]
fn normalize(_: impl Ctx, vec2: Item<DVec2>) -> Item<DVec2> {
let (vec2, attributes) = vec2.into_parts();
Item::from_parts(vec2.normalize_or_zero(), attributes)
}
#[cfg(test)]
@@ -1467,6 +1575,84 @@ mod test {
assert_eq!(magnitude((), vector).into_element(), 5.);
}
#[test]
pub fn distance_function() {
let (position_from, position_to) = (Item::new_from_element(DVec2::new(1., 2.)), Item::new_from_element(DVec2::new(4., 6.)));
assert_eq!(distance((), position_from, position_to).into_element(), 5.);
}
#[test]
pub fn cross_product_sign() {
let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y));
assert_eq!(cross_product((), vec2(1., 0.), vec2(0., 1.)).into_element(), 1.);
assert_eq!(cross_product((), vec2(0., 1.), vec2(1., 0.)).into_element(), -1.);
assert_eq!(cross_product((), vec2(2., 2.), vec2(1., 1.)).into_element(), 0.);
}
#[test]
pub fn sign_of_negative_zero_is_positive_zero() {
let result = sign((), Item::new_from_element(-0.0_f64)).into_element();
assert_eq!(result, 0.);
assert!(result.is_sign_positive());
}
#[test]
pub fn sign_componentwise() {
assert_eq!(sign((), Item::new_from_element(DVec2::new(-5., 3.))).into_element(), DVec2::new(-1., 1.));
}
#[test]
pub fn lerp_endpoints_are_exact() {
let lerp_between = |factor, clamped| {
lerp(
(),
Item::new_from_element(3.),
Item::new_from_element(7.),
Item::new_from_element(factor),
Item::new_from_element(clamped),
)
.into_element()
};
assert_eq!(lerp_between(0., true), 3.);
assert_eq!(lerp_between(1., true), 7.);
assert_eq!(lerp_between(0.5, true), 5.);
}
#[test]
pub fn lerp_clamped_and_extrapolated() {
let lerp_between = |factor, clamped| {
lerp(
(),
Item::new_from_element(0.),
Item::new_from_element(10.),
Item::new_from_element(factor),
Item::new_from_element(clamped),
)
.into_element()
};
assert_eq!(lerp_between(2., true), 10.);
assert_eq!(lerp_between(2., false), 20.);
}
#[test]
pub fn lerp_endpoint_factors_pass_endpoints_through() {
let lerp_between = |start: f64, end: f64, factor| {
lerp(
(),
Item::new_from_element(start),
Item::new_from_element(end),
Item::new_from_element(factor),
Item::new_from_element(true),
)
.into_element()
};
assert_eq!(lerp_between(3., f64::INFINITY, 0.), 3.);
assert_eq!(lerp_between(f64::NAN, 7., 1.), 7.);
assert_eq!(lerp_between(3., f64::INFINITY, 1.), f64::INFINITY);
assert!(lerp_between(-0., 7., 0.).is_sign_negative());
assert!(lerp_between(5., -0., 1.).is_sign_negative());
}
#[test]
pub fn clamp_vec2_within_swapped_bounds() {
let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y));

View File

@@ -27,7 +27,7 @@ pub fn category_description(category: &str) -> &str {
"Math: Numeric" => "Nodes in this category perform discontinuous numeric operations such as rounding, clamping, mapping, and randomization.",
"Math: Transform" => "Nodes in this category perform transformations on graphical elements and calculations involving transformation matrices.",
"Math: Trig" => "Nodes in this category perform trigonometric operations such as sine, cosine, tangent, and their inverses.",
"Math: Vector" => "Nodes in this category perform operations involving `vec2` values (points or arrows in 2D space) such as the dot product, normalization, and distance calculations.",
"Math: Vec2" => "Nodes in this category perform operations involving `vec2` values (points or arrows in 2D space) such as the dot product, normalization, and distance calculations.",
"Raster: Adjustment" => "Nodes in this category perform per-pixel color adjustments on raster graphics, such as brightness and contrast modifications.",
"Raster: Channels" => "Nodes in this category enable channel-specific manipulation of the RGB and alpha channels of raster graphics.",
"Raster: Filter" => "Nodes in this category apply filtering effects to raster graphics such as blurs and sharpening.",