diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index d2b1f905c7..49b019fbed 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1221,7 +1221,7 @@ fn document_node_definitions() -> HashMap>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 { match axis { XY::X => vector.into().x, diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 3dbc4c7e3a..63170e02ea 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -77,7 +77,7 @@ fn math( } } -/// 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, B>( _: impl Ctx, @@ -91,7 +91,7 @@ fn add, B>( augend + addend } -/// 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, B>( _: impl Ctx, @@ -105,7 +105,7 @@ fn subtract, B>( minuend - subtrahend } -/// 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, B>( _: impl Ctx, @@ -161,7 +161,7 @@ impl SafeDivide 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"))] @@ -210,7 +210,7 @@ fn reciprocal( value.componentwise(|value| if value == 0. { 0. } else { 1. / value }) } -/// 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"))] @@ -587,6 +587,57 @@ fn remap( } } +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( + _: impl Ctx, + /// The value produced when the factor is 0. + #[implementations(f64, f32, DVec2)] + start: T, + /// The value produced when the factor is 1. + #[default(1.)] + #[implementations(f64, f32, DVec2)] + end: T, + /// The mix between the start (at 0) and end (at 1) values. + #[default(0.5)] + factor: f64, + /// Whether to constrain the factor within 0 to 1, preventing extrapolation beyond the start and end values. + #[default(true)] + clamped: bool, +) -> T { + let factor = if clamped { factor.clamp(0., 1.) } else { factor }; + + // 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) + if factor == 0. { + start + } else if factor == 1. { + end + } else { + start.lerp(end, factor) + } +} + /// 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( @@ -708,6 +759,27 @@ fn absolute_value( value.abs() } +/// 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( + _: impl Ctx, + /// The number whose sign is checked. + #[implementations(f64, f32, DVec2)] + value: T, +) -> T { + value.componentwise(|value| { + if value > 0. { + 1. + } else if value < 0. { + -1. + } else { + 0. + } + }) +} + pub trait MinMax { type Output; fn minimum(self, other: Rhs) -> Self::Output; @@ -1068,7 +1140,7 @@ fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 { 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: DVec2) -> DVec2 { vec2 @@ -1167,7 +1239,7 @@ fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100 /// 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: (), @@ -1185,29 +1257,44 @@ 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: DVec2, + value: DVec2, /// The other operand of the dot product operation. #[default(1., 0.)] - vector_b: DVec2, - /// Whether to normalize both input vectors so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment. + other_value: DVec2, + /// Whether to normalize both input vec2s so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment. normalize: bool, ) -> f64 { if normalize { - 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) } } -/// Calculates the angle swept between two vectors. +/// The cross product operation (`×`) calculates the signed area of the parallelogram formed by a vec2 pair. /// -/// 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"))] +/// 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: DVec2, + /// The vec2 on the right-hand side of the cross product operation. + #[default(1., 0.)] + other_value: DVec2, +) -> f64 { + value.perp_dot(other_value) +} + +/// Calculates the angle swept between two vec2s. +/// +/// The value is always positive and ranges from 0° (both vec2s point the same direction) to 180° (both vec2s point opposite directions). +#[node_macro::node(category("Math: Vec2"))] fn angle_between(_: impl Ctx, vector_a: DVec2, vector_b: DVec2, radians: bool) -> f64 { let dot_product = vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero()); let angle = dot_product.acos(); @@ -1228,39 +1315,51 @@ 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( _: impl Ctx, /// The position from which the angle is measured. #[implementations(DVec2, DAffine2, DVec2, DAffine2)] - observer: T, + position_from: T, /// The position toward which the angle is measured. #[expose] #[implementations(DVec2, DVec2, DAffine2, DAffine2)] - target: U, + position_to: U, /// Whether the resulting angle should be given in radians instead of degrees. radians: bool, ) -> f64 { - let from = observer.to_position(); - let to = target.to_position(); + let from = position_from.to_position(); + let to = position_to.to_position(); let delta = to - from; let angle = delta.y.atan2(delta.x); if radians { angle } else { angle.to_degrees() } } -/// 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: DVec2) -> f64 { - vector.length() +/// 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: DVec2) -> f64 { + vec2.length() } -/// Scales the input vector to unit length while preserving its direction. This is equivalent to dividing the input vector by its own magnitude. +/// 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: DVec2, + /// The point the distance is measured to. + position_to: DVec2, +) -> f64 { + position_from.distance(position_to) +} + +/// 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 vector has zero length. -#[node_macro::node(category("Math: Vector"))] -fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 { - vector.normalize_or_zero() +/// Returns 0 when the input vec2 has zero length. +#[node_macro::node(category("Math: Vec2"))] +fn normalize(_: impl Ctx, vec2: DVec2) -> DVec2 { + vec2.normalize_or_zero() } #[cfg(test)] @@ -1280,6 +1379,57 @@ mod test { assert_eq!(magnitude(&(), vector), 5.); } + #[test] + pub fn distance_function() { + let (position_from, position_to) = (DVec2::new(1., 2.), DVec2::new(4., 6.)); + assert_eq!(distance(&(), position_from, position_to), 5.); + } + + #[test] + pub fn cross_product_sign() { + let vec2 = |x, y| DVec2::new(x, y); + assert_eq!(cross_product(&(), vec2(1., 0.), vec2(0., 1.)), 1.); + assert_eq!(cross_product(&(), vec2(0., 1.), vec2(1., 0.)), -1.); + assert_eq!(cross_product(&(), vec2(2., 2.), vec2(1., 1.)), 0.); + } + + #[test] + pub fn sign_of_negative_zero_is_positive_zero() { + let result = sign(&(), -0.0_f64); + assert_eq!(result, 0.); + assert!(result.is_sign_positive()); + } + + #[test] + pub fn sign_componentwise() { + assert_eq!(sign(&(), DVec2::new(-5., 3.)), DVec2::new(-1., 1.)); + } + + #[test] + pub fn lerp_endpoints_are_exact() { + let lerp_between = |factor, clamped| lerp(&(), 3., 7., factor, clamped); + 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(&(), 0., 10., factor, clamped); + 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(&(), start, end, factor, true); + 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| DVec2::new(x, y); diff --git a/tools/node-docs/src/utility.rs b/tools/node-docs/src/utility.rs index 27fd1caaa6..b8734742c8 100644 --- a/tools/node-docs/src/utility.rs +++ b/tools/node-docs/src/utility.rs @@ -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.",