Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
Keavon marked this conversation as resolved.
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,
Expand Down
48 changes: 48 additions & 0 deletions node-graph/interpreted-executor/src/dynamic_executor/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>");

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<Item<f64>> = 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<String>");

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<Item<String>> = 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() {
Expand Down
5 changes: 3 additions & 2 deletions node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
node_types.extend(convert_adapter_wildcard!(from: u64, to: [f64, f32, u32, i32, i64, DVec2, String]));
node_types.extend(convert_adapter_wildcard!(from: i32, to: [f64, f32, u32, u64, i64, DVec2, String]));
node_types.extend(convert_adapter_wildcard!(from: i64, to: [f64, f32, u32, u64, i32, DVec2, String]));
// Bool, position, and transform wires may feed a ranked `String` connector by formatting each element as text
node_types.extend(convert_adapter_node!(from_element: bool, element: String));
// A bool embeds in number types as exactly 0 or 1 and formats as text as true or false, but deliberately has no `DVec2` row, which would silently turn a stray bool wire into (1., 1.)
node_types.extend(convert_adapter_wildcard!(from: bool, to: [f64, f32, u32, u64, i32, i64, String]));
// Position and transform wires may feed a ranked `String` connector by formatting each element as text
node_types.extend(convert_adapter_node!(from_element: DVec2, element: String));
node_types.extend(convert_adapter_node!(from_element: DAffine2, element: String));
// The sanctioned attribute value conversions: an Item wire's elements box per cell, while a List wire boxes whole as one value
Expand Down
15 changes: 15 additions & 0 deletions node-graph/libraries/core-types/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,18 @@ impl_convert!(i128);
impl_convert!(u128);
impl_convert!(isize);
impl_convert!(usize);

/// Implements the [`Convert`] trait from `bool` into each numeric type, embedding `false` and `true` as exactly 0 and 1.
/// The reverse direction is deliberately absent: a number only becomes a truth value through an explicit comparison.
macro_rules! impl_convert_from_bool {
($($to:ty),* $(,)?) => {
$(
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);
42 changes: 37 additions & 5 deletions node-graph/nodes/math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>) -> Item<u32> {
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<u64>) -> Item<u64> {
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<f64>) -> Item<f64> {
/// 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<f64>) -> Item<f64> {
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<bool>) -> Item<bool> {
value
}

Expand Down Expand Up @@ -1062,6 +1069,18 @@ fn all(_: impl Ctx, values: List<bool>) -> Item<bool> {
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<T: Default + std::cmp::PartialEq>(
_: impl Ctx,
/// The value compared against zero.
#[implementations(f64, f32, u32, u64, i32, i64, DVec2)]
value: Item<T>,
) -> Item<bool> {
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"))]
Expand Down Expand Up @@ -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.));
Expand Down
Loading