From d649052255c10c15754c3a3707f2edf996d2468d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 9 Nov 2024 23:23:25 -0800 Subject: [PATCH] Upgrade and document the math operation nodes --- .../portfolio/portfolio_message_handler.rs | 15 ++ frontend/src/components/views/Graph.svelte | 4 +- libraries/bezier-rs/src/subpath/core.rs | 6 + node-graph/gcore/src/ops.rs | 183 ++++++++++++------ node-graph/gcore/src/vector/vector_nodes.rs | 2 +- 5 files changed, 148 insertions(+), 62 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index c9443e23b6..bf29fba1c8 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -559,6 +559,21 @@ impl MessageHandler> for PortfolioMes .set_input(&InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::F64(1.), false), &[]); } + // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default + // Also upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option + if (reference == "Sine" || reference == "Cosine" || reference == "Tangent" || reference == "Modulo") && inputs_count == 1 { + let node_definition = resolve_document_node_type(reference).unwrap(); + let document_node = node_definition.default_node_template().document_node; + document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone()); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), &[]); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), &[]); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(reference != "Modulo"), false), &[]); + } + // Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 if reference == "Merge" || reference == "Artboard" { let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap(); diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 30c49b35f6..fec5097af8 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -476,7 +476,7 @@ style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--layer-area-width={layerAreaWidth} style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0} - title={description + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} + title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} data-node={node.id} bind:this={nodeElements[nodeIndex]} > @@ -616,7 +616,7 @@ style:--clip-path-id={`url(#${clipPathId})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} - title={description + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} + title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} data-node={node.id} bind:this={nodeElements[nodeIndex]} > diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 00a5c9a635..323853d9c9 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -366,11 +366,15 @@ impl Subpath { } } +/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.) pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec { let len_points = points.len(); if len_points == 0 { return Vec::new(); } + if len_points == 1 { + return vec![points[0]]; + } // Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html). // Because the `a` coefficients are all 1, they need not be stored. @@ -418,6 +422,8 @@ pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec { d } +/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.) +/// If called with fewer than 3 points, this function will return an empty result. pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec { let len_points = points.len(); if len_points < 3 { diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 766a6a7340..117053616d 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -1,4 +1,3 @@ -use crate::raster::adjustments::RedGreenBlue; use crate::raster::BlendMode; use crate::raster::ImageFrame; use crate::registry::types::Percentage; @@ -14,7 +13,7 @@ use rand::{Rng, SeedableRng}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::float::Float; -// Add +/// The addition operation (+) calculates the sum of two numbers. #[node_macro::node(category("Math: Arithmetic"))] fn add, T>( _: (), @@ -24,7 +23,7 @@ fn add, T>( augend + addend } -// Subtract +/// The subtraction operation (-) calculates the difference between two numbers. #[node_macro::node(category("Math: Arithmetic"))] fn subtract, T>( _: (), @@ -34,7 +33,7 @@ fn subtract, T>( minuend - subtrahend } -// Multiply +/// The multiplication operation (×) calculates the product of two numbers. #[node_macro::node(category("Math: Arithmetic"))] fn multiply, T>( _: (), @@ -46,7 +45,7 @@ fn multiply, T>( multiplier * multiplicand } -// Divide +/// The division operation (÷) calculates the quotient of two numbers. #[node_macro::node(category("Math: Arithmetic"))] fn divide, T>( _: (), @@ -58,19 +57,24 @@ fn divide, T>( numerator / denominator } -// Modulo +/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled. #[node_macro::node(category("Math: Arithmetic"))] -fn modulo, T>( +fn modulo>>, T: Copy>( _: (), #[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U, #[default(2.)] #[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)] modulus: T, + always_positive: bool, ) -> >::Output { - numerator % modulus + if always_positive { + (numerator % modulus + modulus) % modulus + } else { + numerator % modulus + } } -// Exponent +/// The exponent operation (^) calculates the result of raising a number to a power. #[node_macro::node(category("Math: Arithmetic"))] fn exponent, T>( _: (), @@ -82,7 +86,7 @@ fn exponent, T>( base.pow(power) } -// Root +/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n. #[node_macro::node(category("Math: Arithmetic"))] fn root( _: (), @@ -102,7 +106,7 @@ fn root( } } -// Logarithm +/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e". #[node_macro::node(category("Math: Arithmetic"))] fn logarithm( _: (), @@ -122,25 +126,84 @@ fn logarithm( } } -// Sine +/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length. +#[node_macro::node(category("Math: Trig"))] +fn sine(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U { + if radians { + theta.sin() + } else { + theta.to_radians().sin() + } +} + +/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length. +#[node_macro::node(category("Math: Trig"))] +fn cosine(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U { + if radians { + theta.cos() + } else { + theta.to_radians().cos() + } +} + +/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length. #[node_macro::node(category("Math: Trig"))] -fn sine(_: (), #[implementations(f64, f32)] theta: U) -> U { - theta.sin() +fn tangent(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U { + if radians { + theta.tan() + } else { + theta.to_radians().tan() + } +} + +/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value. +#[node_macro::node(category("Math: Trig"))] +fn sine_inverse(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U { + if radians { + value.asin() + } else { + value.asin().to_degrees() + } } -// Cosine +/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value. #[node_macro::node(category("Math: Trig"))] -fn cosine(_: (), #[implementations(f64, f32)] theta: U) -> U { - theta.cos() +fn cosine_inverse(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U { + if radians { + value.acos() + } else { + value.acos().to_degrees() + } } -// Tangent +/// The inverse tangent trigonometric function (atan) calculates the angle whose tangent is the specified value. #[node_macro::node(category("Math: Trig"))] -fn tangent(_: (), #[implementations(f64, f32)] theta: U) -> U { - theta.tan() +fn tangent_inverse(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U { + if radians { + value.atan() + } else { + value.atan().to_degrees() + } } -// Random +/// The inverse tangent trigonometric function (atan2) calculates the angle whose tangent is the ratio of the two specified values. +#[node_macro::node(name("Tangent Inverse 2-Argument"), category("Math: Trig"))] +fn tangent_inverse_2_argument( + _: (), + #[implementations(f64, f32)] y: U, + #[expose] + #[implementations(f64, f32)] + x: U, + radians: bool, +) -> U { + if radians { + y.atan2(x) + } else { + y.atan2(x).to_degrees() + } +} + +/// 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( _: (), @@ -160,45 +223,45 @@ fn random( result * (max - min) + min } -// To u32 +/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented. #[node_macro::node(name("To u32"), category("Math: Numeric"))] fn to_u32(_: (), #[implementations(f64, f32)] value: U) -> u32 { let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap()); value.to_u32().unwrap() } -// To u64 +/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented. #[node_macro::node(name("To u64"), category("Math: Numeric"))] fn to_u64(_: (), #[implementations(f64, f32)] value: U) -> u64 { let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap()); value.to_u64().unwrap() } -// Round +/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero. #[node_macro::node(category("Math: Numeric"))] fn round(_: (), #[implementations(f64, f32)] value: U) -> U { value.round() } -// Floor +/// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole. #[node_macro::node(category("Math: Numeric"))] fn floor(_: (), #[implementations(f64, f32)] value: U) -> U { value.floor() } -// Ceiling +/// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole. #[node_macro::node(category("Math: Numeric"))] fn ceiling(_: (), #[implementations(f64, f32)] value: U) -> U { value.ceil() } -// Absolute Value +/// The absolute value function (abs) removes the negative sign from an input value, if present. #[node_macro::node(category("Math: Numeric"))] fn absolute_value(_: (), #[implementations(f64, f32)] value: U) -> U { value.abs() } -// Min +/// The minimum function (min) picks the smaller of two numbers. #[node_macro::node(category("Math: Numeric"))] fn min(_: (), #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T { match value < other_value { @@ -207,7 +270,7 @@ fn min(_: (), #[implementations(f64, &f64, f32, &f32, } } -// Max +/// The maximum function (max) picks the larger of two numbers. #[node_macro::node(category("Math: Numeric"))] fn max(_: (), #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T { match value > other_value { @@ -216,7 +279,7 @@ fn max(_: (), #[implementations(f64, &f64, f32, &f32, } } -// Clamp +/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed. #[node_macro::node(category("Math: Numeric"))] fn clamp( _: (), @@ -224,6 +287,7 @@ fn clamp( #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T, ) -> T { + let (min, max) = if min < max { (min, max) } else { (max, min) }; if value < min { min } else if value > max { @@ -233,7 +297,7 @@ fn clamp( } } -// Equals +/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not. #[node_macro::node(category("Math: Logic"))] fn equals, T>( _: (), @@ -246,112 +310,113 @@ fn equals, T>( other_value == value } -// Logical Or +/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are. +#[node_macro::node(category("Math: Logic"))] +fn not_equals, T>( + _: (), + #[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T, + #[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] + #[min(100.)] + #[max(200.)] + other_value: U, +) -> bool { + other_value != value +} + +/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false. #[node_macro::node(category("Math: Logic"))] fn logical_or(_: (), value: bool, other_value: bool) -> bool { value || other_value } -// Logical And +/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false. #[node_macro::node(category("Math: Logic"))] fn logical_and(_: (), value: bool, other_value: bool) -> bool { value && other_value } -// Logical Xor -#[node_macro::node(category("Math: Logic"))] -fn logical_xor(_: (), value: bool, other_value: bool) -> bool { - value ^ other_value -} - -// Logical Not +/// The logical not operation (!) reverses true and false value of the input. #[node_macro::node(category("Math: Logic"))] fn logical_not(_: (), input: bool) -> bool { !input } -// Bool Value +/// Constructs a bool value which may be set to true or false. #[node_macro::node(category("Value"))] fn bool_value(_: (), _primary: (), #[name("Bool")] bool_value: bool) -> bool { bool_value } -// Number Value +/// Constructs a number value which may be set to any real number. #[node_macro::node(category("Value"))] fn number_value(_: (), _primary: (), number: f64) -> f64 { number } -// Percentage Value +/// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider. #[node_macro::node(category("Value"))] fn percentage_value(_: (), _primary: (), percentage: Percentage) -> f64 { percentage } -// Vector2 Value +/// Constructs a two-dimensional vector value which may be set to any XY coordinate. #[node_macro::node(category("Value"))] fn vector2_value(_: (), _primary: (), x: f64, y: f64) -> DVec2 { DVec2::new(x, y) } -// Color Value +/// Constructs a color value which may be set to any color, or no color. #[node_macro::node(category("Value"))] fn color_value(_: (), _primary: (), #[default(Color::BLACK)] color: Option) -> Option { color } -// Gradient Value +/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors. #[node_macro::node(category("Value"))] fn gradient_value(_: (), _primary: (), gradient: GradientStops) -> GradientStops { gradient } -// Color Channel Value -#[node_macro::node(category("Value"))] -fn color_channel_value(_: (), _primary: (), color_channel: RedGreenBlue) -> RedGreenBlue { - color_channel -} - -// Blend Mode Value +/// Constructs a blend mode choice value which may be set to any of the available blend modes in order to tell another node which blending operation to use. #[node_macro::node(category("Value"))] fn blend_mode_value(_: (), _primary: (), blend_mode: BlendMode) -> BlendMode { blend_mode } -// Size Of +/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes. #[cfg(feature = "std")] #[node_macro::node(category("Debug"))] fn size_of(_: (), ty: crate::Type) -> Option { ty.size() } -// Some +/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option. #[node_macro::node(category("Debug"))] fn some(_: (), #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option { Some(input) } -// Unwrap +/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None. #[node_macro::node(category("Debug"))] fn unwrap(_: (), #[implementations(Option, Option, Option, Option, Option, Option)] input: Option) -> T { input.unwrap_or_default() } -// Clone +/// Meant for debugging purposes, not general use. Clones the input value. #[node_macro::node(category("Debug"))] fn clone<'i, T: Clone + 'i>(_: (), #[implementations(&ImageFrame)] value: &'i T) -> T { value.clone() } -// Identity // TODO: Rename to "Passthrough" -/// The identity function returns the input argument unchanged. +/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. #[node_macro::node(skip_impl)] fn identity<'i, T: 'i>(value: T) -> T { value } // Type +// TODO: Document this #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct TypeNode Node<'a, I>, I, O>(pub N, pub PhantomData<(I, O)>); impl<'i, N, I: 'i, O: 'i> Node<'i, I> for TypeNode diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index a502ddf680..bfe3fb2028 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -859,7 +859,7 @@ async fn splines_from_points( let mut segment_domain = SegmentDomain::default(); for subpath in vector_data.stroke_bezier_paths() { let positions = subpath.manipulator_groups().iter().map(|group| group.anchor).collect::>(); - let closed = subpath.closed(); + let closed = subpath.closed() && positions.len() > 2; // Compute control point handles for Bezier spline. let first_handles = if closed {