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 ef403d33d9..a5f3ba0178 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 @@ -480,7 +480,7 @@ fn document_node_definitions() -> HashMap bool { + let provided = Self::type_name(ty); + + interpreted_executor::node_registry::NODE_REGISTRY + .iter() + .filter(|(identifier, _)| identifier.as_str().starts_with("input_adapter<")) + .flat_map(|(_, implementations)| implementations.keys()) + .any(|node_io| node_io.inputs.first().is_some_and(|from| Self::type_name(from) == provided) && self.satisfies(&node_io.return_value)) + } + + /// Check if a default value of this type is valid for the constraint. + #[must_use] + fn accepts_default_value(&self, ty: &Type) -> bool { + // Empty types are used when the input must come from the graph so they can be skipped + ty.nested_type() == &concrete!(()) || self.satisfies(ty) || self.satisfies_through_input_adapter(ty) + } + /// 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: &NodeTemplate, name: &str, input_index: usize) -> Self { @@ -1589,9 +1609,7 @@ impl InputTypeConstraint { 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) { + if !constraint.accepts_default_value(&input_ty) { warn!("The default value for input index {index} node {name} is {input_ty}, but does not satisfy {constraint:?}"); } } @@ -1687,6 +1705,27 @@ mod test { editor.eval_graph().await.expect("the Origins to Polyline chain should type-resolve and evaluate"); } + + // Guards the unconnected Path input, whose default must match the rank of the Morph connector it feeds + #[tokio::test] + async fn blend_resolves_and_evaluates_with_default_inputs() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.draw_rect(0., 0., 10., 10.).await; + + let layer = editor.active_document().metadata().all_layers().next().expect("drawing a rectangle should create a layer"); + let node_id = NodeId::new(); + let node_template = resolve_network_node_type("Blend").expect("the Blend definition should exist").default_node_template(); + editor + .handle_message(NodeGraphMessage::InsertNode { + node_id, + node_template: Box::new(node_template), + }) + .await; + editor.handle_message(NodeGraphMessage::MoveNodeToChainStart { node_id, parent: layer }).await; + + editor.eval_graph().await.expect("the Blend network should type-resolve and evaluate"); + } } #[cfg(test)] @@ -1707,6 +1746,25 @@ mod test_type_constraints { } } + #[test] + fn every_definition_default_value_satisfies_its_constraint() { + let mut violations = Vec::new(); + for definition in super::DOCUMENT_NODE_TYPES.values() { + let name = &definition.node_template.display_name; + let constraints = InputTypeConstraint::constraints_for_all_inputs(&definition.node_template, name); + + for (index, (constraint, input)) in constraints.iter().zip(&definition.node_template.inputs).enumerate() { + if let Some(value) = input.as_value() + && !constraint.accepts_default_value(&value.ty()) + { + violations.push(format!("{name} input {index}: {} does not satisfy {constraint:?}", value.ty())); + } + } + } + + assert!(violations.is_empty(), "Default values rejected by their input constraints:\n{}", violations.join("\n")); + } + #[test] fn passthrough() { let node_type = resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).expect("passthrough node"); diff --git a/frontend/wrapper/src/lib.rs b/frontend/wrapper/src/lib.rs index c27805ed94..0785d8efab 100644 --- a/frontend/wrapper/src/lib.rs +++ b/frontend/wrapper/src/lib.rs @@ -173,6 +173,12 @@ pub struct WasmLog; impl log::Log for WasmLog { #[inline] fn enabled(&self, metadata: &log::Metadata) -> bool { + // Dependencies that log routine rendering details at the debug level are capped so they don't flood the console + let crate_name = metadata.target().split("::").next().unwrap_or_default(); + if crate_name.starts_with("vello") { + return metadata.level() <= log::Level::Info; + } + metadata.level() <= log::max_level() } diff --git a/node-graph/graph-craft/src/application_io/resource/opfs.rs b/node-graph/graph-craft/src/application_io/resource/opfs.rs index 261a172b64..5a0e109998 100644 --- a/node-graph/graph-craft/src/application_io/resource/opfs.rs +++ b/node-graph/graph-craft/src/application_io/resource/opfs.rs @@ -174,7 +174,8 @@ async fn request_persistence() { match storage.persist() { Ok(promise) => match JsFuture::from(promise).await { Ok(value) if value.as_bool() == Some(true) => {} - Ok(_) => log::warn!("OPFS persistence was not granted; browser may evict resources under storage pressure"), + // Browsers deny this by default unless the site is bookmarked, installed, or highly engaged, so it isn't worth a warning + Ok(_) => log::trace!("OPFS persistence was not granted; browser may evict resources under storage pressure"), Err(error) => log::warn!("OPFS persist() rejected: {error:?}"), }, Err(error) => log::warn!("OPFS persist() threw: {error:?}"), diff --git a/node-graph/nodes/brush/src/lib.rs b/node-graph/nodes/brush/src/lib.rs index 219fd9d701..8958222aa0 100644 --- a/node-graph/nodes/brush/src/lib.rs +++ b/node-graph/nodes/brush/src/lib.rs @@ -7,6 +7,7 @@ pub mod basic_brush; pub use brush_types::*; +// Fallbacks for stroke items carrying no such attribute, mirroring the `Brush Strokes` defaults below (which the node macro requires as literals) pub(crate) const DEFAULT_DIAMETER: f64 = 40.; pub(crate) const DEFAULT_HARDNESS: f64 = 0.; pub(crate) const DEFAULT_FLOW: f64 = 100.; @@ -17,9 +18,9 @@ fn brush_strokes( _: impl Ctx, strokes: List, color: List, - #[default(DEFAULT_DIAMETER)] diameter: Item, - #[default(DEFAULT_HARDNESS)] hardness: Item, - #[default(DEFAULT_FLOW)] flow: Item, + #[default(40.)] diameter: Item, + #[default(0.)] hardness: Item, + #[default(100.)] flow: Item, ) -> List { let (diameter, hardness, flow) = (diameter.into_element(), hardness.into_element(), flow.into_element()); List::new_from_item(