diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index a2d105c40e5..8980c17a760 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -416,8 +416,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ aliases: &["graphene_math_nodes::TangentInverseNode", "graphene_core::ops::TangentInverseNode"], }, NodeReplacement { - node: graphene_std::math_nodes::as_f_64::IDENTIFIER, - aliases: &["graphene_math_nodes::ToF64Node", "graphene_core::ops::ToF64Node", "math_nodes::ToF64Node"], + node: graphene_std::math_nodes::as_number::IDENTIFIER, + aliases: &["graphene_math_nodes::ToF64Node", "graphene_core::ops::ToF64Node", "math_nodes::ToF64Node", "math_nodes::AsF64Node"], }, NodeReplacement { node: graphene_std::math_nodes::as_u_32::IDENTIFIER, diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index cd5b96fa607..58b1feb0968 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -538,6 +538,54 @@ fn number_value_formats_through_the_string_input_adapter() { assert_eq!(result.map(|item| item.element().clone()), Some("42".to_string()), "The number should format as its text representation"); } +// A boolean wire feeding a number connector embeds as exactly 0 or 1 through the input adapter's `Convert` row +#[test] +fn bool_value_embeds_through_the_number_input_adapter() { + for (value, expected) in [(true, 1.), (false, 0.)] { + let bool_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(value).into()), vec![NodeId(0)]); + + let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]); + input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter"); + + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![(NodeId(0), bool_node), (NodeId(1), input_adapter_node)], + }; + let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); + typing_context.update(&network).expect("A bool wire should resolve the adapter's embedding conversion row"); + let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The embedding constructor should instantiate"); + + let context: Context = None; + let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + assert_eq!(result.map(|item| *item.element()), Some(expected), "{value} should embed as exactly {expected}"); + } +} + +// A boolean wire feeding a `String` connector formats as "true" or "false", not as its 0 or 1 number embedding +#[test] +fn bool_value_formats_through_the_string_input_adapter() { + for (value, expected) in [(true, "true"), (false, "false")] { + let bool_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(value).into()), vec![NodeId(0)]); + + let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]); + input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter"); + + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![(NodeId(0), bool_node), (NodeId(1), input_adapter_node)], + }; + let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); + typing_context.update(&network).expect("A bool wire should resolve the adapter's formatting conversion row"); + let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The formatting constructor should instantiate"); + + let context: Context = None; + let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + assert_eq!(result.map(|item| item.element().clone()), Some(expected.to_string()), "{value} should format as `{expected}`"); + } +} + // A `List` wire feeding a `ListDyn` connector erases its element type through the input adapter's `Into` row #[test] fn list_wire_erases_through_the_list_dyn_input_adapter() { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 0736d0a9167..de598fd89f8 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -551,8 +551,9 @@ fn node_registry() -> HashMap { + $( + impl Convert<$to, ()> for bool { + async fn convert(self, _: Footprint, _: ()) -> $to { + self as u8 as $to + } + } + )* + }; +} +impl_convert_from_bool!(f32, f64, i8, u8, u16, i16, i32, u32, i64, u64, i128, u128, isize, usize); diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 671ce806bf1..d82907a5d4f 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -713,23 +713,30 @@ fn random( } // TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions. -/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. +/// Converts a number to an integer of the type u32, which may be the required type for certain node inputs. #[node_macro::node(name("As u32"), category("Type Assertion"))] fn as_u32(_: impl Ctx, value: Item) -> Item { value } // TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions. -/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. +/// Converts a number to an integer of the type u64, which may be the required type for certain node inputs. #[node_macro::node(name("As u64"), category("Type Assertion"))] fn as_u64(_: impl Ctx, value: Item) -> Item { value } // TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions. -/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. -#[node_macro::node(name("As f64"), category("Type Assertion"))] -fn as_f64(_: impl Ctx, value: Item) -> Item { +/// Converts an integer or bool to the decimal number type, which may be the required type for certain node inputs. A bool becomes 0 (false) or 1 (true). +#[node_macro::node(category("Type Assertion"))] +fn as_number(_: impl Ctx, value: Item) -> Item { + value +} + +// TODO: Test that these are no longer needed in all circumstances, then remove them and add a migration to convert these into Passthrough nodes. Note: these act more as type annotations than as identity functions. +/// Passes a true or false value through as the type bool, which may be the required type for certain node inputs. +#[node_macro::node(category("Type Assertion"))] +fn as_bool(_: impl Ctx, value: Item) -> Item { value } @@ -1062,6 +1069,18 @@ fn all(_: impl Ctx, values: List) -> Item { Item::new_from_element(values.iter_element_values().all(|&value| value)) } +/// Outputs true if the value is anything other than zero. A vector counts as zero only when every component is zero. +#[node_macro::node(category("Math: Logic"))] +fn is_nonzero( + _: impl Ctx, + /// The value compared against zero. + #[implementations(f64, f32, u32, u64, i32, i64, DVec2)] + value: Item, +) -> Item { + let (value, attributes) = value.into_parts(); + Item::from_parts(value != T::default(), attributes) +} + /// The less-than operation (`<`) compares two values and returns true if the first value is less than the second, or false if it is not. /// If enabled with *Or Equal*, the less-than-or-equal operation (`<=`) is used instead. #[node_macro::node(category("Math: Logic"))] @@ -1827,6 +1846,19 @@ mod test { assert_eq!(result.into_element(), 0.); } + #[test] + fn test_is_nonzero() { + assert!(!is_nonzero((), Item::new_from_element(0.)).into_element()); + assert!(is_nonzero((), Item::new_from_element(0.5)).into_element()); + assert!(is_nonzero((), Item::new_from_element(-3_i64)).into_element()); + assert!(!is_nonzero((), Item::new_from_element(DVec2::ZERO)).into_element()); + assert!(is_nonzero((), Item::new_from_element(DVec2::new(0., 1.))).into_element()); + + // Negative zero is zero, while NaN, being unequal to zero, is nonzero + assert!(!is_nonzero((), Item::new_from_element(-0.)).into_element()); + assert!(is_nonzero((), Item::new_from_element(f64::NAN)).into_element()); + } + #[test] fn test_invalid_expression() { let result = math((), Item::new_from_element(0.), Item::new_from_element("invalid".to_string()), Item::new_from_element(0.));