-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Frontend type constraints #4334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ mod document_node_derive; | |
|
|
||
| use super::node_properties::choice::enum_choice; | ||
| use super::node_properties::{self, ParameterWidgetsInfo}; | ||
| use super::utility_types::FrontendNodeType; | ||
| use super::utility_types::{FrontendNodeType, FrontendTypeConstraintForInput}; | ||
| use crate::messages::layout::utility_types::widget_prelude::*; | ||
| use crate::messages::portfolio::document::utility_types::network_interface::{ | ||
| DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, | ||
|
|
@@ -2090,27 +2090,134 @@ pub fn resolve_document_node_type(identifier: &DefinitionIdentifier) -> Option<& | |
| DOCUMENT_NODE_TYPES.get(identifier) | ||
| } | ||
|
|
||
| impl FrontendTypeConstraintForInput { | ||
| #[must_use] | ||
| fn type_name(ty: &Type) -> String { | ||
| ty.nested_type().to_string() | ||
| } | ||
|
|
||
| /// Find types that satisfy both constraints | ||
| #[must_use] | ||
| fn intersection(self, other: Self) -> Self { | ||
| match (self, other) { | ||
| (Self::Limited(a), Self::Limited(b)) => Self::Limited(a.intersection(&b).cloned().collect()), | ||
| (Self::Limited(a), Self::All) | (Self::All, Self::Limited(a)) => Self::Limited(a), | ||
| (Self::All, Self::All) => Self::All, | ||
| } | ||
| } | ||
|
|
||
| /// Construct a constraint that no types satisfy | ||
| #[must_use] | ||
| fn empty() -> Self { | ||
| Self::Limited(Default::default()) | ||
| } | ||
|
|
||
| /// Construct a constraint satisfied by the named types only | ||
| #[must_use] | ||
| #[cfg(test)] | ||
| fn new(accept: &[Type]) -> Self { | ||
| Self::Limited(accept.iter().map(Self::type_name).collect()) | ||
| } | ||
|
|
||
| /// Check if type satisfies the constraint | ||
| #[must_use] | ||
| fn satisfies(&self, ty: &Type) -> bool { | ||
| match self { | ||
| Self::Limited(types) => types.contains(&Self::type_name(ty)), | ||
| Self::All => true, | ||
| } | ||
| } | ||
|
|
||
| /// Add a new type that the constraint satisfied. Returns true if the type is not previously added. | ||
| fn insert(&mut self, ty: &Type) -> bool { | ||
| match self { | ||
| Self::Limited(types) => types.insert(Self::type_name(ty)), | ||
| Self::All => false, | ||
| } | ||
| } | ||
|
|
||
| /// Compute the type constraint for one input. Note that this cannot use the infrastructure in the node network interface as the node is not placed in a network. | ||
| #[must_use] | ||
| fn compute_constraint_for_input(template_document_node: &DocumentNode, name: &str, input_index: usize) -> Self { | ||
| // Add input type from node implementation | ||
| match &template_document_node.implementation { | ||
| // TODO: This does not consider the constrains by nodes not directly connected to the import | ||
| DocumentNodeImplementation::Network(nested_network) => { | ||
| // Find all inputs connected to the relevant import | ||
| fn valid_types_from_node(child_node: &DocumentNode, name: &str, input_index: usize) -> impl Iterator<Item = FrontendTypeConstraintForInput> { | ||
| let all_inputs = child_node.inputs.iter().enumerate(); | ||
| let input_for_import = all_inputs.filter(move |(_, child_input)| matches!(child_input, NodeInput::Import { import_index, .. } if *import_index == input_index)); | ||
| input_for_import.map(move |(index, _)| FrontendTypeConstraintForInput::compute_constraint_for_input(child_node, name, index)) | ||
| } | ||
| let all_type_constraints = nested_network.nodes.values().flat_map(|node| valid_types_from_node(node, name, input_index)); | ||
| // The type fed to the network must satisfy all protonodes it is connected to | ||
| let intersection_of_constraints = all_type_constraints.reduce(|a, b| a.intersection(b)); | ||
| // If no constraints, then all types are accepted | ||
| intersection_of_constraints.unwrap_or(Self::All) | ||
| } | ||
| DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { | ||
| // The passthrough node has no implementations but accepts all types (it is filtered from the compiled network) | ||
| if proto_node_identifier == &graphene_std::ops::passthrough::IDENTIFIER { | ||
| return Self::All; | ||
| } | ||
|
|
||
| let Some(implementations) = interpreted_executor::node_registry::NODE_REGISTRY.get(proto_node_identifier) else { | ||
| warn!("No implementations found for protonode {proto_node_identifier} in {name}"); | ||
| return Self::All; | ||
| }; | ||
|
|
||
| // Find the union of all the possible types from the dynamic executor implementations | ||
| let mut result_accepted = Self::empty(); | ||
| for node_io in implementations.keys() { | ||
| if let Some(input_type) = node_io.inputs.get(input_index) { | ||
| result_accepted.insert(input_type); | ||
| } | ||
| } | ||
| result_accepted | ||
| } | ||
| DocumentNodeImplementation::Extract => { | ||
| warn!("Input types for extract node {name} not supported"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Self::All | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Compute the type constraints for each input, validating that the current input satisfies the value. | ||
| #[must_use] | ||
| fn constraints_for_all_inputs(template_document_node: &DocumentNode, name: &str) -> Vec<Self> { | ||
| let input_indices = 0..template_document_node.inputs.len(); | ||
| let all_input_constraints: Vec<_> = input_indices.map(|input_index| Self::compute_constraint_for_input(template_document_node, name, input_index)).collect(); | ||
|
|
||
| // Validate that the current inputs are valid | ||
| for (index, (constraint, input)) in all_input_constraints.iter().zip(&template_document_node.inputs).enumerate() { | ||
| if let Some(value) = input.as_value() { | ||
| let input_ty = value.ty(); | ||
|
|
||
| // Empty types are used when the input must come from the graph so they can be skipped. | ||
| if input_ty.nested_type() != &concrete!(()) && !constraint.satisfies(&input_ty) { | ||
| warn!("The default value for input index {index} node {name} is {input_ty}, but does not satisfy {constraint:?}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| all_input_constraints | ||
| } | ||
| } | ||
|
|
||
| pub fn collect_node_types() -> Vec<FrontendNodeType> { | ||
| DOCUMENT_NODE_TYPES | ||
| .iter() | ||
| .filter(|(_, definition)| !definition.category.is_empty()) | ||
| .map(|(identifier, definition)| { | ||
| let input_types = definition | ||
| .node_template | ||
| .document_node | ||
| .inputs | ||
| .iter() | ||
| .map(|node_input| node_input.as_value().map(|node_value| node_value.ty().nested_type().to_string()).unwrap_or_default()) | ||
| .collect::<Vec<String>>(); | ||
| let mut name = definition.node_template.persistent_node_metadata.display_name.clone(); | ||
| if name.is_empty() { | ||
| name = identifier.implementation_name_from_identifier() | ||
| } | ||
| FrontendNodeType { | ||
| identifier: identifier.serialized(), | ||
| input_types: FrontendTypeConstraintForInput::constraints_for_all_inputs(&definition.node_template.document_node, &name), | ||
| name, | ||
| category: definition.category.to_string(), | ||
| input_types, | ||
| } | ||
| }) | ||
| .collect() | ||
|
|
@@ -2207,3 +2314,63 @@ impl DocumentNodeDefinition { | |
| self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some)) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test_frontend_type_constraints { | ||
| use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type; | ||
| use crate::messages::portfolio::document::node_graph::utility_types::FrontendTypeConstraintForInput; | ||
| use crate::test_utils::test_prelude::*; | ||
| use graph_craft::NodeNetwork; | ||
| use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; | ||
| use graphene_std::{concrete, generic}; | ||
|
|
||
| #[test] | ||
| fn passthrough() { | ||
| let node_type = resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).expect("passthrough node"); | ||
| let constraint = FrontendTypeConstraintForInput::constraints_for_all_inputs(&node_type.node_template.document_node, "name"); | ||
| assert_eq!(constraint, vec![FrontendTypeConstraintForInput::All]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn tangent_inverse() { | ||
| let node_type = resolve_proto_node_type(graphene_std::math_nodes::tangent_inverse::IDENTIFIER).expect("tangent inverse node"); | ||
| let constraint = FrontendTypeConstraintForInput::constraints_for_all_inputs(&node_type.node_template.document_node, "name"); | ||
| assert_eq!( | ||
| constraint, | ||
| vec![ | ||
| FrontendTypeConstraintForInput::new(&[concrete!(glam::DVec2), concrete!(f32), concrete!(f64)]), | ||
| FrontendTypeConstraintForInput::new(&[concrete!(bool)]) | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn fill() { | ||
| let node_type = resolve_proto_node_type(graphene_std::graphic_nodes::index_elements::IDENTIFIER).expect("Index Elements node"); | ||
| let constraint = FrontendTypeConstraintForInput::compute_constraint_for_input(&node_type.node_template.document_node, "name", 0); | ||
| assert!(constraint.satisfies(&concrete!(graphene_std::list::List<String>))); | ||
| assert!(constraint.satisfies(&concrete!(graphene_std::list::List<f64>))); | ||
| assert!(!constraint.satisfies(&concrete!(f64))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn network() { | ||
| let atan_definition = resolve_proto_node_type(graphene_std::math_nodes::tangent_inverse::IDENTIFIER).expect("tangent inverse node"); | ||
| let min_definition = resolve_proto_node_type(graphene_std::math_nodes::min::IDENTIFIER).expect("tangent inverse node"); | ||
| let atan_node = atan_definition.node_template_input_override([Some(NodeInput::import(generic!(X), 0))]).document_node; | ||
| let min_node = min_definition.node_template_input_override([Some(NodeInput::import(generic!(X), 0))]).document_node; | ||
| let inner_network = NodeNetwork { | ||
| exports: vec![NodeInput::node(NodeId(10), 0)], | ||
| nodes: [(NodeId(10), atan_node), (NodeId(11), min_node)].into_iter().collect(), | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| let node = DocumentNode { | ||
| inputs: vec![NodeInput::import(generic!(X), 0)], | ||
| implementation: DocumentNodeImplementation::Network(inner_network), | ||
| ..Default::default() | ||
| }; | ||
| let constraint = FrontendTypeConstraintForInput::compute_constraint_for_input(&node, "name", 0); | ||
| assert_eq!(constraint, FrontendTypeConstraintForInput::new(&[concrete!(f32), concrete!(f64)]),); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,7 +46,7 @@ | |
| let matchesRemainingTerms = true; | ||
|
|
||
| if (isTypeSearch && typeSearchTerm) { | ||
| matchesTypeSearch = node.inputTypes?.some((inputType) => inputType.toLowerCase().includes(typeSearchTerm)) || false; | ||
| matchesTypeSearch = node.inputTypes?.some((constraint) => constraint === "All" || constraint.Limited.some((inputType) => inputType.toLowerCase().includes(typeSearchTerm))) || false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent potential runtime |
||
| } | ||
|
|
||
| if (remainingSearchTerms.length > 0) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
warn!macro is used directly without being imported, which will likely cause a compilation error since the rest of the file consistently useslog::warn!orlog::error!. Uselog::warn!instead.