From b171eeba8432361a25cf6e23236eb04c198a5755 Mon Sep 17 00:00:00 2001 From: James Lindsay <78500760+0HyperCube@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:13:15 +0000 Subject: [PATCH] Add tests to the Ellipse, Artboard, and Fill tools (#2181) * Add ellipse tests * Add tests for fill tool and re-enable some other tests * Code review * Fix Rust crate advisory --------- Co-authored-by: Keavon Chambers --- Cargo.lock | 7 +- editor/src/application.rs | 7 + editor/src/dispatcher.rs | 182 ++++++----- editor/src/lib.rs | 1 + .../utility_types/network_interface.rs | 28 ++ .../portfolio/portfolio_message_handler.rs | 6 +- .../graph_modification_utils.rs | 9 + .../tool/tool_messages/artboard_tool.rs | 166 ++++++---- .../tool/tool_messages/ellipse_tool.rs | 131 ++++++++ .../messages/tool/tool_messages/fill_tool.rs | 58 ++++ editor/src/node_graph_executor.rs | 146 ++++++++- editor/src/test_utils.rs | 292 ++++++++++++++---- node-graph/gcore/src/lib.rs | 24 +- node-graph/gcore/src/memo.rs | 4 +- node-graph/gcore/src/ops.rs | 27 +- node-graph/gcore/src/raster/adjustments.rs | 2 +- node-graph/gcore/src/registry.rs | 6 +- node-graph/graph-craft/src/document/value.rs | 23 ++ node-graph/graph-craft/src/proto.rs | 12 +- .../src/dynamic_executor.rs | 4 +- .../interpreted-executor/src/node_registry.rs | 22 ++ node-graph/node-macro/src/codegen.rs | 176 ++++++++++- 22 files changed, 1097 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f365762..ea9e1040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3619,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -5565,15 +5565,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] diff --git a/editor/src/application.rs b/editor/src/application.rs index 19d4aad6..2af1a5a7 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -15,6 +15,13 @@ impl Editor { Self { dispatcher: Dispatcher::new() } } + #[cfg(test)] + pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) { + let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime(); + let dispatcher = Dispatcher::with_executor(executor); + (Self { dispatcher }, runtime) + } + pub fn handle_message>(&mut self, message: T) -> Vec { self.dispatcher.handle_message(message, true); diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 6a772ee4..fd0e6d22 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -26,6 +26,15 @@ pub struct DispatcherMessageHandlers { workspace_message_handler: WorkspaceMessageHandler, } +impl DispatcherMessageHandlers { + pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { + Self { + portfolio_message_handler: PortfolioMessageHandler::with_executor(executor), + ..Default::default() + } + } +} + /// For optimization, these are messages guaranteed to be redundant when repeated. /// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. /// In addition, these messages do not change any state in the backend (aside from caches). @@ -53,6 +62,13 @@ impl Dispatcher { Self::default() } + pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { + Self { + message_handlers: DispatcherMessageHandlers::with_executor(executor), + ..Default::default() + } + } + // If the deepest queues (higher index in queues list) are now empty (after being popped from) then remove them fn cleanup_queues(&mut self, leave_last: bool) { while self.message_queues.last().filter(|queue| queue.is_empty()).is_some() { @@ -328,60 +344,48 @@ impl Dispatcher { #[cfg(test)] mod test { - use crate::application::Editor; - use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; - use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; - use crate::messages::prelude::*; - use crate::test_utils::EditorTestUtils; - use graphene_core::raster::color::Color; - - fn init_logger() { - let _ = env_logger::builder().is_test(true).try_init(); - } + pub use crate::test_utils::test_prelude::*; /// Create an editor with three layers /// 1. A red rectangle /// 2. A blue shape /// 3. A green ellipse - fn create_editor_with_three_layers() -> Editor { - init_logger(); - let mut editor = Editor::create(); + async fn create_editor_with_three_layers() -> EditorTestUtils { + let mut editor = EditorTestUtils::create(); - editor.new_document(); + editor.new_document().await; - editor.select_primary_color(Color::RED); - editor.draw_rect(100., 200., 300., 400.); + editor.select_primary_color(Color::RED).await; + editor.draw_rect(100., 200., 300., 400.).await; - editor.select_primary_color(Color::BLUE); - editor.draw_polygon(10., 1200., 1300., 400.); + editor.select_primary_color(Color::BLUE).await; + editor.draw_polygon(10., 1200., 1300., 400.).await; - editor.select_primary_color(Color::GREEN); - editor.draw_ellipse(104., 1200., 1300., 400.); + editor.select_primary_color(Color::GREEN).await; + editor.draw_ellipse(104., 1200., 1300., 400.).await; editor } - // TODO: Fix text - #[ignore] - #[test] /// - create rect, shape and ellipse /// - copy /// - paste /// - assert that ellipse was copied - fn copy_paste_single_layer() { - let mut editor = create_editor_with_three_layers(); + #[tokio::test] + async fn copy_paste_single_layer() { + let mut editor = create_editor_with_three_layers().await; - let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); - editor.handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }); - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; + editor + .handle_message(PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::Internal, + parent: LayerNodeIdentifier::ROOT_PARENT, + insert_index: 0, + }) + .await; - let layers_before_copy = document_before_copy.metadata().all_layers().collect::>(); - let layers_after_copy = document_after_copy.metadata().all_layers().collect::>(); + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 4); @@ -392,33 +396,30 @@ mod test { } } - // TODO: Fix text - #[ignore] - #[test] #[cfg_attr(miri, ignore)] /// - create rect, shape and ellipse /// - select shape /// - copy /// - paste /// - assert that shape was copied - fn copy_paste_single_layer_from_middle() { - let mut editor = create_editor_with_three_layers(); + #[tokio::test] + async fn copy_paste_single_layer_from_middle() { + let mut editor = create_editor_with_three_layers().await; - let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); - let shape_id = document_before_copy.metadata().all_layers().nth(1).unwrap(); + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + let shape_id = editor.active_document().metadata().all_layers().nth(1).unwrap(); - editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] }); - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); - editor.handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }); + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] }).await; + editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; + editor + .handle_message(PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::Internal, + parent: LayerNodeIdentifier::ROOT_PARENT, + insert_index: 0, + }) + .await; - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); - - let layers_before_copy = document_before_copy.metadata().all_layers().collect::>(); - let layers_after_copy = document_after_copy.metadata().all_layers().collect::>(); + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 4); @@ -429,9 +430,6 @@ mod test { } } - // TODO: Fix text - #[ignore] - #[test] #[cfg_attr(miri, ignore)] /// - create rect, shape and ellipse /// - select ellipse and rect @@ -440,36 +438,40 @@ mod test { /// - create another rect /// - paste /// - paste - fn copy_paste_deleted_layers() { - let mut editor = create_editor_with_three_layers(); + #[tokio::test] + async fn copy_paste_deleted_layers() { + let mut editor = create_editor_with_three_layers().await; + assert_eq!(editor.active_document().metadata().all_layers().count(), 3); - let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); - let mut layers = document_before_copy.metadata().all_layers(); - let rect_id = layers.next().expect("rectangle"); - let shape_id = layers.next().expect("shape"); - let ellipse_id = layers.next().expect("ellipse"); + let layers_before_copy = editor.active_document().metadata().all_layers().collect::>(); + let rect_id = layers_before_copy[0]; + let shape_id = layers_before_copy[1]; + let ellipse_id = layers_before_copy[2]; - editor.handle_message(NodeGraphMessage::SelectedNodesSet { - nodes: vec![rect_id.to_node(), ellipse_id.to_node()], - }); - editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); - editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }); - editor.draw_rect(0., 800., 12., 200.); - editor.handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }); - editor.handle_message(PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::Internal, - parent: LayerNodeIdentifier::ROOT_PARENT, - insert_index: 0, - }); + editor + .handle_message(NodeGraphMessage::SelectedNodesSet { + nodes: vec![rect_id.to_node(), ellipse_id.to_node()], + }) + .await; + editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await; + editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }).await; + editor.draw_rect(0., 800., 12., 200.).await; + editor + .handle_message(PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::Internal, + parent: LayerNodeIdentifier::ROOT_PARENT, + insert_index: 0, + }) + .await; + editor + .handle_message(PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::Internal, + parent: LayerNodeIdentifier::ROOT_PARENT, + insert_index: 0, + }) + .await; - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone(); - - let layers_before_copy = document_before_copy.metadata().all_layers().collect::>(); - let layers_after_copy = document_after_copy.metadata().all_layers().collect::>(); + let layers_after_copy = editor.active_document().metadata().all_layers().collect::>(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 6); @@ -498,8 +500,7 @@ mod test { panic!() }; - init_logger(); - let mut editor = Editor::create(); + let mut editor = EditorTestUtils::create(); // UNCOMMENT THIS FOR RUNNING UNDER MIRI // @@ -523,20 +524,13 @@ mod test { "Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)", ); - let responses = editor.handle_message(PortfolioMessage::OpenDocumentFile { + let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile { document_name: document_name.into(), document_serialized_content, }); // Check if the graph renders - let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; - portfolio - .executor - .submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap(), glam::UVec2::ONE, true) - .expect("submit_node_graph_evaluation failed"); - crate::node_graph_executor::run_node_graph().await; - let mut messages = VecDeque::new(); - editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render"); + editor.eval_graph().await; for response in responses { // Check for the existence of the file format incompatibility warning dialog after opening the test file diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 0fe3b2a3..80d80b53 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -12,5 +12,6 @@ pub mod consts; pub mod dispatcher; pub mod messages; pub mod node_graph_executor; +#[cfg(test)] pub mod test_utils; pub mod utility_traits; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index fcdde4f1..a77cb5e0 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -5622,6 +5622,34 @@ impl NodeNetworkInterface { self.force_set_upstream_to_chain(node_id, network_path); } } + + pub fn iter_recursive(&self) -> NodesRecursiveIter<'_> { + NodesRecursiveIter { + stack: vec![&self.network], + current_slice: None, + } + } +} + +pub struct NodesRecursiveIter<'a> { + stack: Vec<&'a NodeNetwork>, + current_slice: Option>, +} + +impl<'a> Iterator for NodesRecursiveIter<'a> { + type Item = (NodeId, &'a DocumentNode); + fn next(&mut self) -> Option { + loop { + if let Some((id, node)) = self.current_slice.as_mut().and_then(|iter| iter.next()) { + if let DocumentNodeImplementation::Network(network) = &node.implementation { + self.stack.push(network); + } + return Some((*id, node)); + } + let network = self.stack.pop()?; + self.current_slice = Some(network.nodes.iter()); + } + } } #[derive(PartialEq)] diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 4c1e1cc0..b8a2cc7a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1160,7 +1160,11 @@ impl MessageHandler> for PortfolioMes } impl PortfolioMessageHandler { - pub async fn introspect_node(&self, node_path: &[NodeId]) -> Result, IntrospectError> { + pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { + Self { executor, ..Default::default() } + } + + pub async fn introspect_node(&self, node_path: &[NodeId]) -> Result, IntrospectError> { self.executor.introspect_node(node_path).await } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index f2637742..0eae1532 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -394,6 +394,15 @@ impl<'a> NodeGraphLayer<'a> { .find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string()))) } + /// Node id of a protonode if it exists in the layer's primary flow + pub fn upstream_node_id_from_protonode(&self, protonode_identifier: &'static str) -> Option { + self.horizontal_layer_flow().find(move |node_id| { + self.network_interface + .implementation(node_id, &[]) + .is_some_and(move |implementation| *implementation == graph_craft::document::DocumentNodeImplementation::proto(protonode_identifier)) + }) + } + /// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached. pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec> { self.horizontal_layer_flow() diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 7699470e..6828b758 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -3,18 +3,16 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::snapping; use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::snapping::SnapManager; -use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; use crate::messages::tool::common_functionality::transformation_cage::*; use graph_craft::document::NodeId; use graphene_core::renderer::Quad; -use glam::{IVec2, Vec2Swizzles}; - #[derive(Default)] pub struct ArtboardTool { fsm_state: ArtboardToolFsmState, @@ -112,7 +110,8 @@ struct ArtboardToolData { drag_current: DVec2, auto_panning: AutoPanning, snap_candidates: Vec, - dragging_current_artboard_location: IVec2, + dragging_current_artboard_location: glam::IVec2, + draw: Resize, } impl ArtboardToolData { @@ -256,14 +255,7 @@ impl Fsm for ArtboardToolFsmState { tool_data.get_snap_candidates(document, input); ArtboardToolFsmState::Dragging } else { - tool_data.get_snap_candidates(document, input); - - let point = SnapCandidatePoint::handle(to_document.transform_point2(input.mouse.position)); - - let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); - - tool_data.drag_start = snapped.snapped_point_document; - tool_data.drag_current = snapped.snapped_point_document; + tool_data.draw.start(document, input); ArtboardToolFsmState::Drawing }; @@ -324,46 +316,15 @@ impl Fsm for ArtboardToolFsmState { ArtboardToolFsmState::Dragging } (ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => { - let to_viewport = document.metadata().document_to_viewport; - let ignore = if let Some(layer) = tool_data.selected_artboard { vec![layer] } else { vec![] }; - let snap_data = SnapData::ignore(document, input, &ignore); - - let document_mouse = to_viewport.inverse().transform_point2(input.mouse.position); - - let config = SnapTypeConfiguration::default(); - let snapped = tool_data.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config); - let snapped_mouse_position = to_viewport.transform_point2(snapped.snapped_point_document); - - tool_data.snap_manager.update_indicator(snapped); - - let mut start = to_viewport.transform_point2(tool_data.drag_start); - let mut size = snapped_mouse_position - start; - - // Constrain axis - if input.keyboard.get(constrain_axis_or_aspect as usize) { - size = size.abs().max(size.abs().yx()) * size.signum(); - } - - // From center - if input.keyboard.get(center as usize) { - start -= size; - size *= 2.; - } - - let start = to_viewport.inverse().transform_point2(start); - let size = to_viewport.inverse().transform_vector2(size); - let end = start + size; - + let [start, end] = tool_data.draw.calculate_points_ignore_layer(document, input, center, constrain_axis_or_aspect); if let Some(artboard) = tool_data.selected_artboard { - if artboard == LayerNodeIdentifier::ROOT_PARENT { - log::error!("Selected artboard cannot be ROOT_PARENT"); - } else { - responses.add(GraphOperationMessage::ResizeArtboard { - layer: artboard, - location: start.min(end).round().as_ivec2(), - dimensions: (start.round() - end.round()).abs().as_ivec2(), - }); - } + assert_ne!(artboard, LayerNodeIdentifier::ROOT_PARENT, "Selected artboard cannot be ROOT_PARENT"); + + responses.add(GraphOperationMessage::ResizeArtboard { + layer: artboard, + location: start.min(end).round().as_ivec2(), + dimensions: (start.round() - end.round()).abs().as_ivec2(), + }); } else { let id = NodeId::new(); @@ -374,8 +335,8 @@ impl Fsm for ArtboardToolFsmState { artboard: graphene_core::Artboard { graphic_group: graphene_core::GraphicGroupTable::default(), label: String::from("Artboard"), - location: start.round().as_ivec2(), - dimensions: IVec2::splat(1), + location: start.min(end).round().as_ivec2(), + dimensions: (start.round() - end.round()).abs().as_ivec2(), background: graphene_core::Color::WHITE, clip: false, }, @@ -594,3 +555,102 @@ impl Fsm for ArtboardToolFsmState { } } } + +#[cfg(test)] +mod test_artboard { + pub use crate::test_utils::test_prelude::*; + + async fn get_artboards(editor: &mut EditorTestUtils) -> Vec { + let instrumented = editor.eval_graph().await; + instrumented.grab_all_input::(&editor.runtime).collect() + } + + #[tokio::test] + async fn artboard_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::empty()).await; + + let artboards = get_artboards(&mut editor).await; + + assert_eq!(artboards.len(), 1); + assert_eq!(artboards[0].location, IVec2::new(10, 0)); + assert_eq!(artboards[0].dimensions, IVec2::new(10, 11)); + } + + #[tokio::test] + async fn artboard_draw_square() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 10., 10., -10., 11., ModifierKeys::SHIFT).await; + + let artboards = get_artboards(&mut editor).await; + assert_eq!(artboards.len(), 1); + assert_eq!(artboards[0].location, IVec2::new(-10, 10)); + assert_eq!(artboards[0].dimensions, IVec2::new(20, 20)); + } + + #[tokio::test] + async fn artboard_draw_square_rotated() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + // 45 degree rotation of content clockwise + angle_radians: f64::consts::FRAC_PI_4, + }) + .await; + // Viewport coordinates + editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT).await; + + let artboards = get_artboards(&mut editor).await; + assert_eq!(artboards.len(), 1); + assert_eq!(artboards[0].location, IVec2::new(0, 0)); + let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.); + assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2()); + } + + #[tokio::test] + async fn artboard_draw_center_square_rotated() { + let mut editor = EditorTestUtils::create(); + + editor.new_document().await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + // 45 degree rotation of content clockwise + angle_radians: f64::consts::FRAC_PI_4, + }) + .await; + // Viewport coordinates + editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; + + let artboards = get_artboards(&mut editor).await; + assert_eq!(artboards.len(), 1); + assert_eq!(artboards[0].location, DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2()); + let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.); + assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2()); + } + + #[tokio::test] + async fn artboard_delete() { + let mut editor = EditorTestUtils::create(); + + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::default()).await; + editor.press(Key::Delete, ModifierKeys::default()).await; + + let artboards = get_artboards(&mut editor).await; + assert_eq!(artboards.len(), 0); + } + + #[tokio::test] + async fn artboard_cancel() { + let mut editor = EditorTestUtils::create(); + + editor.new_document().await; + + editor.drag_tool_cancel_rmb(ToolType::Artboard).await; + let artboards = get_artboards(&mut editor).await; + assert_eq!(artboards.len(), 0); + } +} diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index 84406b14..a91f7f72 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -315,3 +315,134 @@ impl Fsm for EllipseToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); } } + +#[cfg(test)] +mod test_ellipse { + pub use crate::test_utils::test_prelude::*; + use glam::DAffine2; + use graphene_core::vector::generator_nodes::ellipse; + + #[derive(Debug, PartialEq)] + struct ResolvedEllipse { + radius_x: f64, + radius_y: f64, + transform: DAffine2, + } + + async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec { + let instrumented = editor.eval_graph().await; + + let document = editor.active_document(); + let layers = document.metadata().all_layers(); + layers + .filter_map(|layer| { + let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface); + let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?; + Some(ResolvedEllipse { + radius_x: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), + radius_y: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), + transform: document.metadata().transform_to_document(layer), + }) + }) + .collect() + } + + #[tokio::test] + async fn ellipse_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + assert_eq!( + ellipse[0], + ResolvedEllipse { + radius_x: 4.5, + radius_y: 5., + transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center + } + ); + } + + #[tokio::test] + async fn ellipse_draw_circle() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await; + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + assert_eq!( + ellipse[0], + ResolvedEllipse { + radius_x: 10., + radius_y: 10., + transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center + } + ); + } + + #[tokio::test] + async fn ellipse_draw_square_rotated() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + // 45 degree rotation of content clockwise + angle_radians: f64::consts::FRAC_PI_4, + }) + .await; + editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + println!("{ellipse:?}"); + // TODO: re-enable after https://github.com/GraphiteEditor/Graphite/issues/2370 + // assert_eq!(ellipse[0].radius_x, 5.); + // assert_eq!(ellipse[0].radius_y, 5.); + + // assert!(ellipse[0] + // .transform + // .abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)); + + float_eq!(ellipse[0].radius_x, 11. / core::f64::consts::SQRT_2 / 2.); + float_eq!(ellipse[0].radius_y, 11. / core::f64::consts::SQRT_2 / 2.); + assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_translation(DVec2::splat(11. / core::f64::consts::SQRT_2 / 2.)), 0.001)); + } + + #[tokio::test] + async fn ellipse_draw_center_square_rotated() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + // 45 degree rotation of content clockwise + angle_radians: f64::consts::FRAC_PI_4, + }) + .await; + editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + // TODO: re-enable after https://github.com/GraphiteEditor/Graphite/issues/2370 + // assert_eq!(ellipse[0].radius_x, 10.); + // assert_eq!(ellipse[0].radius_y, 10.); + // assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001)); + float_eq!(ellipse[0].radius_x, 11. / core::f64::consts::SQRT_2); + float_eq!(ellipse[0].radius_y, 11. / core::f64::consts::SQRT_2); + assert!(ellipse[0].transform.abs_diff_eq(DAffine2::IDENTITY, 0.001)); + } + + #[tokio::test] + async fn ellipse_cancel() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool_cancel_rmb(ToolType::Ellipse).await; + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 0); + } +} diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 9600883d..ce87e181 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -124,3 +124,61 @@ impl Fsm for FillToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } + +#[cfg(test)] +mod test_fill { + pub use crate::test_utils::test_prelude::*; + use graphene_core::vector::fill; + use graphene_std::vector::style::Fill; + + async fn get_fills(editor: &mut EditorTestUtils) -> Vec { + let instrumented = editor.eval_graph().await; + + instrumented.grab_all_input::>(&editor.runtime).collect() + } + + #[tokio::test] + async fn ignore_artboard() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await; + editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; + assert!(get_fills(&mut editor,).await.is_empty()); + } + + #[tokio::test] + // TODO: fix https://github.com/GraphiteEditor/Graphite/issues/2270 + #[should_panic] + async fn ignore_raster() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await; + editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; + assert!(get_fills(&mut editor,).await.is_empty()); + } + + #[tokio::test] + async fn primary() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + editor.select_primary_color(Color::GREEN).await; + editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; + let fills = get_fills(&mut editor).await; + assert_eq!(fills.len(), 1); + assert_eq!(fills[0], Fill::Solid(Color::GREEN)); + } + + #[tokio::test] + async fn secondary() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + let color = Color::YELLOW; + editor.handle_message(ToolMessage::SelectSecondaryColor { color }).await; + editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; + let fills = get_fills(&mut editor).await; + assert_eq!(fills.len(), 1); + assert_eq!(fills[0], Fill::Solid(color)); + } +} diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index b0b84e4d..96d6a986 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -4,7 +4,7 @@ use crate::messages::prelude::*; use graph_craft::concrete; use graph_craft::document::value::{RenderOutput, TaggedValue}; -use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork}; +use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; @@ -346,7 +346,7 @@ impl NodeRuntime { } } -pub async fn introspect_node(path: &[NodeId]) -> Result, IntrospectError> { +pub async fn introspect_node(path: &[NodeId]) -> Result, IntrospectError> { let runtime = NODE_RUNTIME.lock(); if let Some(ref mut runtime) = runtime.as_ref() { return runtime.executor.introspect(path); @@ -396,6 +396,22 @@ impl Default for NodeGraphExecutor { } impl NodeGraphExecutor { + /// A local runtime is useful on threads since having global state causes flakes + #[cfg(test)] + pub(crate) fn new_with_local_runtime() -> (NodeRuntime, Self) { + let (request_sender, request_receiver) = std::sync::mpsc::channel(); + let (response_sender, response_receiver) = std::sync::mpsc::channel(); + let node_runtime = NodeRuntime::new(request_receiver, response_sender); + + let node_executor = Self { + futures: Default::default(), + sender: request_sender, + receiver: response_receiver, + node_graph_hash: 0, + }; + (node_runtime, node_executor) + } + /// Execute the network by flattening it and creating a borrow stack. fn queue_execution(&self, render_config: RenderConfig) -> u64 { let execution_id = generate_uuid(); @@ -405,7 +421,7 @@ impl NodeGraphExecutor { execution_id } - pub async fn introspect_node(&self, path: &[NodeId]) -> Result, IntrospectError> { + pub async fn introspect_node(&self, path: &[NodeId]) -> Result, IntrospectError> { introspect_node(path).await } @@ -439,9 +455,20 @@ impl NodeGraphExecutor { Some(extract_data(downcasted)) } - /// Evaluates a node graph, computing the entire graph - pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, ignore_hash: bool) -> Result<(), String> { - // Get the node graph layer + /// Updates the network to monitor all inputs. Useful for the testing. + #[cfg(test)] + pub(crate) fn update_node_graph_instrumented(&mut self, document: &mut DocumentMessageHandler) -> Result { + // We should always invalidate the cache. + self.node_graph_hash = generate_uuid(); + let mut network = document.network_interface.network(&[]).unwrap().clone(); + let instrumented = Instrumented::new(&mut network); + + self.sender.send(NodeRuntimeMessage::GraphUpdate(network)).map_err(|e| e.to_string())?; + Ok(instrumented) + } + + /// Update the cached network if necessary. + fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, ignore_hash: bool) -> Result<(), String> { let network_hash = document.network_interface.network(&[]).unwrap().current_hash(); if network_hash != self.node_graph_hash || ignore_hash { self.node_graph_hash = network_hash; @@ -449,7 +476,11 @@ impl NodeGraphExecutor { .send(NodeRuntimeMessage::GraphUpdate(document.network_interface.network(&[]).unwrap().clone())) .map_err(|e| e.to_string())?; } + Ok(()) + } + /// Adds an evaluate request for whatever current network is cached. + pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2) -> Result<(), String> { let render_config = RenderConfig { viewport: Footprint { transform: document.metadata().document_to_viewport, @@ -469,6 +500,13 @@ impl NodeGraphExecutor { let execution_id = self.queue_execution(render_config); self.futures.insert(execution_id, ExecutionContext { export_config: None }); + Ok(()) + } + + /// Evaluates a node graph, computing the entire graph + pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, ignore_hash: bool) -> Result<(), String> { + self.update_node_graph(document, ignore_hash)?; + self.submit_current_node_graph_evaluation(document, viewport_resolution)?; Ok(()) } @@ -675,3 +713,99 @@ impl NodeGraphExecutor { Ok(()) } } + +/// Stores all of the monitor nodes that have been attached to a graph +#[derive(Default)] +pub struct Instrumented { + protonodes_by_name: HashMap>>>, + protonodes_by_path: HashMap, Vec>>, +} + +impl Instrumented { + /// Adds montior nodes to the network + fn add(&mut self, network: &mut NodeNetwork, path: &mut Vec) { + // Required to do seperately to satiate the borrow checker. + let mut monitor_nodes = Vec::new(); + for (id, node) in network.nodes.iter_mut() { + // Recursively instrument + if let DocumentNodeImplementation::Network(nested) = &mut node.implementation { + path.push(*id); + self.add(nested, path); + path.pop(); + } + let mut monitor_node_ids = Vec::with_capacity(node.inputs.len()); + for input in &mut node.inputs { + let node_id = NodeId::new(); + let old_input = std::mem::replace(input, NodeInput::node(node_id, 0)); + monitor_nodes.push((old_input, node_id)); + path.push(node_id); + monitor_node_ids.push(path.clone()); + path.pop(); + } + if let DocumentNodeImplementation::ProtoNode(identifier) = &mut node.implementation { + path.push(*id); + self.protonodes_by_name.entry(identifier.name.to_string()).or_default().push(monitor_node_ids.clone()); + self.protonodes_by_path.insert(path.clone(), monitor_node_ids); + path.pop(); + } + } + for (input, monitor_id) in monitor_nodes { + let monitor_node = DocumentNode { + inputs: vec![input], + implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"), + manual_composition: Some(graph_craft::generic!(T)), + skip_deduplication: true, + ..Default::default() + }; + network.nodes.insert(monitor_id, monitor_node); + } + } + + /// Instrument a graph and return a new [Instrumented] state. + pub fn new(network: &mut NodeNetwork) -> Self { + let mut instrumented = Self::default(); + instrumented.add(network, &mut Vec::new()); + instrumented + } + + fn downcast(dynamic: Arc) -> Option + where + Input::Result: Send + Sync + Clone + 'static, + { + // This is quite inflexible since it only allows the footprint as inputs. + if let Some(x) = dynamic.downcast_ref::>() { + Some(x.output.clone()) + } else if let Some(x) = dynamic.downcast_ref::>() { + Some(x.output.clone()) + } else if let Some(x) = dynamic.downcast_ref::>() { + Some(x.output.clone()) + } else { + panic!("cannot downcast type for introspection"); + } + } + + /// Grab all of the values of the input every time it occurs in the graph. + pub fn grab_all_input<'a, Input: graphene_std::NodeInputDecleration + 'a>(&'a self, runtime: &'a NodeRuntime) -> impl Iterator + 'a + where + Input::Result: Send + Sync + Clone + 'static, + { + self.protonodes_by_name + .get(Input::identifier()) + .map_or([].as_slice(), |x| x.as_slice()) + .iter() + .filter_map(|inputs| inputs.get(Input::INDEX)) + .filter_map(|input_monitor_node| runtime.executor.introspect(input_monitor_node).ok()) + .filter_map(Instrumented::downcast::) + } + + pub fn grab_protonode_input(&self, path: &Vec, runtime: &NodeRuntime) -> Option + where + Input::Result: Send + Sync + Clone + 'static, + { + let input_monitor_node = self.protonodes_by_path.get(path)?.get(Input::INDEX)?; + + let dynamic = runtime.executor.introspect(input_monitor_node).ok()?; + + Self::downcast::(dynamic) + } +} diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 9a591753..545613da 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -4,36 +4,29 @@ use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition}; use crate::messages::portfolio::utility_types::Platform; use crate::messages::prelude::*; +use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; +use crate::node_graph_executor::Instrumented; +use crate::node_graph_executor::NodeRuntime; +use graph_craft::document::DocumentNode; use graphene_core::raster::color::Color; +use graphene_core::InputAccessor; + +use glam::DVec2; /// A set of utility functions to make the writing of editor test more declarative -pub trait EditorTestUtils { - fn create() -> Editor; - - fn new_document(&mut self); - - fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); - fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); - fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); - - /// Select given tool and drag it from (x1, y1) to (x2, y2) - fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64); - fn move_mouse(&mut self, x: f64, y: f64); - fn mousedown(&mut self, state: EditorMouseState); - fn mouseup(&mut self, state: EditorMouseState); - fn left_mousedown(&mut self, x: f64, y: f64); - fn input(&mut self, message: InputPreprocessorMessage); - fn select_tool(&mut self, typ: ToolType); - fn select_primary_color(&mut self, color: Color); +pub struct EditorTestUtils { + pub editor: Editor, + pub runtime: NodeRuntime, } -impl EditorTestUtils for Editor { - fn create() -> Editor { +impl EditorTestUtils { + pub fn create() -> Self { + let _ = env_logger::builder().is_test(true).try_init(); set_uuid_seed(0); - let mut editor = Editor::new(); + let (mut editor, runtime) = Editor::new_local_executor(); // We have to set this directly instead of using `GlobalsMessage::SetPlatform` because race conditions with multiple tests can cause that message handler to set it more than once, which is a failure. // It isn't sufficient to guard the message dispatch here with a check if the once_cell is empty, because that isn't atomic and the time between checking and handling the dispatch can let multiple through. @@ -41,73 +34,250 @@ impl EditorTestUtils for Editor { editor.handle_message(Message::Init); - editor + Self { editor, runtime } } - fn new_document(&mut self) { - self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") })); + pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future + 'a { + // An inner function is required since async functions in traits are a bit weird + async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Instrumented { + let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; + let exector = &mut portfolio.executor; + let document = portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap(); + + let instrumented = exector.update_node_graph_instrumented(document).expect("update_node_graph_instrumented failed"); + + let viewport_resolution = glam::UVec2::ONE; + exector + .submit_current_node_graph_evaluation(document, viewport_resolution) + .expect("submit_current_node_graph_evaluation failed"); + runtime.run().await; + + let mut messages = VecDeque::new(); + editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render"); + let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message)); + + for message in frontend_messages { + message.check_node_graph_error(); + } + + instrumented + } + + run(&mut self.editor, &mut self.runtime) } - fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { - self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2); + pub async fn handle_message(&mut self, message: impl Into) { + self.editor.handle_message(message); + + // Required to process any buffered messages + self.eval_graph().await; } - fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { - self.drag_tool(ToolType::Polygon, x1, y1, x2, y2); + pub async fn new_document(&mut self) { + self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") })) + .await; } - fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { - self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2); + pub async fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { + self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2, ModifierKeys::default()).await; } - fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64) { - self.select_tool(typ); - self.move_mouse(x1, y1); - self.left_mousedown(x1, y1); - self.move_mouse(x2, y2); - self.mouseup(EditorMouseState { - editor_position: (x2, y2).into(), - mouse_keys: MouseKeys::empty(), - scroll_delta: ScrollDelta::default(), - }); + pub async fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { + self.drag_tool(ToolType::Polygon, x1, y1, x2, y2, ModifierKeys::default()).await; } - fn move_mouse(&mut self, x: f64, y: f64) { + pub async fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { + self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2, ModifierKeys::default()).await; + } + + pub async fn click_tool(&mut self, typ: ToolType, button: MouseKeys, position: DVec2, modifier_keys: ModifierKeys) { + self.select_tool(typ).await; + + self.move_mouse(position.x, position.y, modifier_keys, MouseKeys::empty()).await; + + self.mousedown( + EditorMouseState { + editor_position: position, + mouse_keys: button, + ..Default::default() + }, + modifier_keys, + ) + .await; + + self.mouseup( + EditorMouseState { + editor_position: position, + ..Default::default() + }, + modifier_keys, + ) + .await; + } + + pub async fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64, modifier_keys: ModifierKeys) { + self.select_tool(typ).await; + + self.move_mouse(x1, y1, modifier_keys, MouseKeys::empty()).await; + + self.left_mousedown(x1, y1, modifier_keys).await; + + self.move_mouse(x2, y2, modifier_keys, MouseKeys::LEFT).await; + + self.mouseup( + EditorMouseState { + editor_position: (x2, y2).into(), + mouse_keys: MouseKeys::empty(), + scroll_delta: ScrollDelta::default(), + }, + modifier_keys, + ) + .await; + } + + pub async fn drag_tool_cancel_rmb(&mut self, typ: ToolType) { + self.select_tool(typ).await; + + self.move_mouse(50., 50., ModifierKeys::default(), MouseKeys::empty()).await; + + self.left_mousedown(50., 50., ModifierKeys::default()).await; + + self.move_mouse(100., 100., ModifierKeys::default(), MouseKeys::LEFT).await; + + self.mousedown( + EditorMouseState { + editor_position: (100., 100.).into(), + mouse_keys: MouseKeys::LEFT | MouseKeys::RIGHT, + scroll_delta: ScrollDelta::default(), + }, + ModifierKeys::default(), + ) + .await; + } + + pub fn active_document(&self) -> &DocumentMessageHandler { + self.editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap() + } + + pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler { + self.editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap() + } + + pub fn get_node<'a, T: InputAccessor<'a, DocumentNode>>(&'a self) -> impl Iterator + 'a { + self.active_document() + .network_interface + .iter_recursive() + .inspect(|node| println!("{:#?}", node.1.implementation)) + .filter_map(move |(_, document)| T::new_with_source(document)) + } + + pub async fn move_mouse(&mut self, x: f64, y: f64, modifier_keys: ModifierKeys, mouse_keys: MouseKeys) { let editor_mouse_state = EditorMouseState { editor_position: ViewportPosition::new(x, y), + mouse_keys, ..Default::default() }; - let modifier_keys = ModifierKeys::default(); - self.input(InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys }); + self.input(InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys }).await; } - fn mousedown(&mut self, editor_mouse_state: EditorMouseState) { - let modifier_keys = ModifierKeys::default(); - self.input(InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys }); + pub async fn mousedown(&mut self, editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys) { + self.input(InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys }).await; } - fn mouseup(&mut self, editor_mouse_state: EditorMouseState) { - let modifier_keys = ModifierKeys::default(); - self.handle_message(InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys }); + pub async fn mouseup(&mut self, editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys) { + self.handle_message(InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys }).await; } - fn left_mousedown(&mut self, x: f64, y: f64) { - self.mousedown(EditorMouseState { - editor_position: (x, y).into(), - mouse_keys: MouseKeys::LEFT, - scroll_delta: ScrollDelta::default(), - }); + pub async fn press(&mut self, key: Key, modifier_keys: ModifierKeys) { + let key_repeat = false; + + self.handle_message(InputPreprocessorMessage::KeyDown { key, modifier_keys, key_repeat }).await; + self.handle_message(InputPreprocessorMessage::KeyUp { key, modifier_keys, key_repeat }).await; } - fn input(&mut self, message: InputPreprocessorMessage) { - self.handle_message(Message::InputPreprocessor(message)); + pub async fn left_mousedown(&mut self, x: f64, y: f64, modifier_keys: ModifierKeys) { + self.mousedown( + EditorMouseState { + editor_position: (x, y).into(), + mouse_keys: MouseKeys::LEFT, + scroll_delta: ScrollDelta::default(), + }, + modifier_keys, + ) + .await; } - fn select_tool(&mut self, tool_type: ToolType) { - self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })); + pub async fn input(&mut self, message: InputPreprocessorMessage) { + self.handle_message(Message::InputPreprocessor(message)).await; } - fn select_primary_color(&mut self, color: Color) { - self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor { color })); + pub async fn select_tool(&mut self, tool_type: ToolType) { + self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await; + } + + pub async fn select_primary_color(&mut self, color: Color) { + self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor { color })).await; + } + + pub async fn create_raster_image(&mut self, image: graphene_core::raster::Image, mouse: Option<(f64, f64)>) { + self.handle_message(PortfolioMessage::PasteImage { + name: None, + image, + mouse, + parent_and_insert_index: None, + }) + .await; + } +} + +pub trait FrontendMessageTestUtils { + fn check_node_graph_error(&self); +} + +impl FrontendMessageTestUtils for FrontendMessage { + fn check_node_graph_error(&self) { + let FrontendMessage::UpdateNodeGraph { nodes, .. } = self else { return }; + + for node in nodes { + if let Some(error) = &node.errors { + panic!("error on {}: {}", node.display_name, error); + } + } + } +} + +#[cfg(test)] +pub mod test_prelude { + pub use super::FrontendMessageTestUtils; + pub use crate::application::Editor; + pub use crate::float_eq; + pub use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; + pub use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; + pub use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; + pub use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; + pub use crate::messages::prelude::*; + pub use crate::messages::tool::common_functionality::graph_modification_utils::{is_layer_fed_by_node_of_name, NodeGraphLayer}; + pub use crate::messages::tool::utility_types::ToolType; + pub use crate::node_graph_executor::NodeRuntime; + pub use crate::test_utils::EditorTestUtils; + pub use core::f64; + pub use glam::DVec2; + pub use glam::IVec2; + pub use graph_craft::document::DocumentNode; + pub use graphene_core::raster::{Color, Image}; + pub use graphene_core::{InputAccessor, InputAccessorSource}; + pub use graphene_std::{transform::Footprint, GraphicGroup}; + + #[macro_export] + macro_rules! float_eq { + ($left:expr, $right:expr $(,)?) => { + match (&$left, &$right) { + (left_val, right_val) => { + if (*left_val - *right_val).abs() > 1e-10 { + panic!("assertion `left == right` failed\n left: {}\n right: {}", *left_val, *right_val) + } + } + } + }; } } diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 883513f8..ff8c75d6 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -72,7 +72,7 @@ pub trait Node<'i, Input> { } /// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes. #[cfg(feature = "std")] - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { log::warn!("Node::serialize not implemented for {}", core::any::type_name::()); None } @@ -170,3 +170,25 @@ pub use crate::application_io::{SurfaceFrame, SurfaceId}; pub type WasmSurfaceHandle = application_io::SurfaceHandle; #[cfg(feature = "wasm")] pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame; + +pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + core::fmt::Debug { + fn get_input(&'a self, index: usize) -> Option<&'a T>; + fn set_input(&'a mut self, index: usize, value: T); +} + +pub trait InputAccessorSourceIdentifier { + fn has_identifier(&self, identifier: &str) -> bool; +} + +pub trait InputAccessor<'n, Source: 'n> +where + Self: Sized, +{ + fn new_with_source(source: &'n Source) -> Option; +} + +pub trait NodeInputDecleration { + const INDEX: usize; + fn identifier() -> &'static str; + type Result; +} diff --git a/node-graph/gcore/src/memo.rs b/node-graph/gcore/src/memo.rs index edebee9d..83193b70 100644 --- a/node-graph/gcore/src/memo.rs +++ b/node-graph/gcore/src/memo.rs @@ -134,9 +134,9 @@ where }) } - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { let io = self.io.lock().unwrap(); - (io).as_ref().map(|output| output.clone() as Arc) + (io).as_ref().map(|output| output.clone() as Arc) } } diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index a44f379e..36bdd007 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -506,7 +506,7 @@ where self.0.reset(); } - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { self.0.serialize() } } @@ -581,4 +581,29 @@ mod test { let fnn = FnNode::new(|(a, b)| (b, a)); assert_eq!(fnn.eval((1u32, 2u32)), (2, 1)); } + + #[test] + pub fn add_vectors() { + assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); + } + + #[test] + pub fn subtract_f64() { + assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); + } + + #[test] + pub fn divide_vectors() { + assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.); + } + + #[test] + pub fn modulo_positive() { + assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64); + } + + #[test] + pub fn modulo_negative() { + assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64); + } } diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 5021f8ae..dad1c93f 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1461,7 +1461,7 @@ const WINDOW_SIZE: usize = 1024; #[cfg(feature = "alloc")] #[node_macro::node(category(""))] -fn generate_curves(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode { +fn generate_curves(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode { use bezier_rs::{Bezier, TValue}; let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle]; diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index c28715ec..38bef7a0 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -184,7 +184,7 @@ where self.node.reset(); } - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { self.node.serialize() } } @@ -217,7 +217,7 @@ where } #[inline(always)] - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { self.node.serialize() } } @@ -273,7 +273,7 @@ where self.node.reset(); } - fn serialize(&self) -> Option> { + fn serialize(&self) -> Option> { self.node.serialize() } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 60b43f3a..0db4eb82 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -19,6 +19,8 @@ use std::marker::PhantomData; use std::str::FromStr; pub use std::sync::Arc; +pub struct TaggedValueTypeError; + /// Macro to generate the tagged value enum. macro_rules! tagged_value { ($ ($( #[$meta:meta] )* $identifier:ident ($ty:ty) ),* $(,)?) => { @@ -111,6 +113,26 @@ macro_rules! tagged_value { Self::from_type(input).unwrap_or(TaggedValue::None) } } + + $( + impl From<$ty> for TaggedValue { + fn from(value: $ty) -> Self { + Self::$identifier(value) + } + } + )* + + $( + impl<'a> TryFrom<&'a TaggedValue> for &'a $ty { + type Error = TaggedValueTypeError; + fn try_from(value: &'a TaggedValue) -> Result { + match value{ + TaggedValue::$identifier(value) => Ok(value), + _ => Err(TaggedValueTypeError), + } + } + } + )* }; } @@ -128,6 +150,7 @@ tagged_value! { #[cfg_attr(feature = "serde", serde(deserialize_with = "graphene_core::migrate_artboard_group"))] ArtboardGroup(graphene_core::ArtboardGroupTable), GraphicElement(graphene_core::GraphicElement), + Artboard(graphene_core::Artboard), String(String), U32(u32), U64(u64), diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 29e6e0c7..f9fdffbb 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -923,12 +923,12 @@ mod test { assert_eq!( ids, vec![ - NodeId(907133870432995942), - NodeId(13049623730817360317), - NodeId(2177355904460308500), - NodeId(17479234042764485524), - NodeId(10988236038173832469), - NodeId(11097818235165626738), + NodeId(10795919842314709924), + NodeId(5986931472261716476), + NodeId(1689970140162147057), + NodeId(17084072420335757359), + NodeId(17163508657634907814), + NodeId(3540151743833532788) ] ); } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index eb127631..89071b6b 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -95,7 +95,7 @@ impl DynamicExecutor { } /// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path. - pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { + pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { self.tree.introspect(node_path) } @@ -217,7 +217,7 @@ impl BorrowTree { } /// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path. - pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { + pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; node.serialize().ok_or(IntrospectError::NoData) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 5027a107..8564f36a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -103,6 +103,28 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_core::RasterFrame]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::instances::Instances]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => glam::IVec2]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => glam::DVec2]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::BooleanOperation]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Fill]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineCap]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineJoin]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Stroke]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Gradient]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientStops]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::Color]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box]), #[cfg(feature = "gpu")] ( ProtoNodeIdentifier::new("graphene_std::executor::MapGpuSingleImageNode"), diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 7bc6148c..81f82f8c 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -5,7 +5,10 @@ use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; use proc_macro_crate::FoundCrate; use quote::{format_ident, quote}; -use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, Ident, Token, WhereClause, WherePredicate}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::{parse_quote, Error, Ident, PatIdent, Token, WhereClause, WherePredicate}; static NODE_ID: AtomicU64 = AtomicU64::new(0); pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result { @@ -250,6 +253,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result syn::Result syn::Result TokenStream2 { + if parsed.attributes.skip_impl { + return quote! {}; + } + let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake)); + + let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics); + + let mut generated_input_accessor = Vec::new(); + for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() { + let mut ty = match parsed_input { + ParsedField::Regular { ty, .. } => ty, + ParsedField::Node { output_type, .. } => output_type, + } + .clone(); + + // We only want the necessary generics. + let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty); + // TODO: figure out a better name that doesn't conflict with so many types + let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal)); + let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter()); + + // Only create structs with phantom data where necessary. + generated_input_accessor.push(if phantom_data_declerations.is_empty() { + quote! { + pub struct #struct_name; + } + } else { + quote! { + pub struct #struct_name <#(#used),*>{ + #(#phantom_data_declerations,)* + } + } + }); + generated_input_accessor.push(quote! { + impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { + const INDEX: usize = #input_index; + fn identifier() -> &'static str { + protonode_identifier() + } + type Result = #ty; + } + }) + } + + quote! { + pub mod #inputs_module_name { + use super::*; + + pub fn protonode_identifier() -> &'static str { + // Storing the string in a once lock should reduce allocations (since we call this in a loop)? + static NODE_NAME: std::sync::OnceLock = std::sync::OnceLock::new(); + NODE_NAME.get_or_init(|| #identifier ) + } + #(#generated_input_accessor)* + } + } +} + +/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors. +fn generate_phantom_data<'a>(fn_generics: impl Iterator) -> (Vec, Vec) { + let mut phantom_data_declerations = Vec::new(); + let mut fn_generic_params = Vec::new(); + + for fn_generic_param in fn_generics { + let field_name = format_ident!("phantom_{}", phantom_data_declerations.len()); + + match fn_generic_param { + crate::GenericParam::Lifetime(lifetime_param) => { + let lifetime = &lifetime_param.lifetime; + + fn_generic_params.push(quote! {#lifetime}); + phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>}) + } + crate::GenericParam::Type(type_param) => { + let generic_name = &type_param.ident; + + fn_generic_params.push(quote! {#generic_name}); + phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>}); + } + _ => {} + } + } + (fn_generic_params, phantom_data_declerations) +} + fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], struct_name: &Ident, identifier: &TokenStream2) -> Result { if parsed.attributes.skip_impl { return Ok(quote!()); @@ -469,3 +563,83 @@ fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type { LifetimeReplacer(lifetime).visit_type_mut(&mut ty); ty } + +/// Get only the necessary generics. +struct FilterUsedGenerics { + all: Vec, + used: Vec, +} + +impl VisitMut for FilterUsedGenerics { + fn visit_lifetime_mut(&mut self, used_lifetime: &mut syn::Lifetime) { + for (generic, used) in self.all.iter().zip(self.used.iter_mut()) { + let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue }; + if used_lifetime == &lifetime_param.lifetime { + *used = true; + } + } + } + + fn visit_path_mut(&mut self, path: &mut syn::Path) { + for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() { + let crate::GenericParam::Type(type_param) = generic else { continue }; + if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident { + *used = true; + // Sometimes the generics conflict with the type name so we rename the generics. + path.segments[0].ident = format_ident!("G{index}"); + } + } + for mut el in Punctuated::pairs_mut(&mut path.segments) { + self.visit_path_segment_mut(el.value_mut()); + } + } +} + +impl FilterUsedGenerics { + fn new(fn_generics: &[crate::GenericParam]) -> (Vec, Self) { + let mut all_possible_generics = fn_generics.to_vec(); + // The 'n lifetime may also be needed; we must add it in + all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site())))); + + let modified = all_possible_generics + .iter() + .cloned() + .enumerate() + .map(|(index, mut generic)| { + let crate::GenericParam::Type(type_param) = &mut generic else { return generic }; + // Sometimes the generics conflict with the type name so we rename the generics. + type_param.ident = format_ident!("G{index}"); + generic + }) + .collect::>(); + + let generic_collector = Self { + used: vec![false; all_possible_generics.len()], + all: all_possible_generics, + }; + + (modified, generic_collector) + } + + fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator { + modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value) + } + + fn filter_unnecessary_generics(&mut self, modified: &mut Vec, ty: &mut Type) -> Vec { + self.used.fill(false); + + // Find out which generics are necessary to support the node input + self.visit_type_mut(ty); + + // Sometimes generics may reference other generics. This is a non-optimal way of dealing with that. + for _ in 0..=self.all.len() { + for (index, item) in modified.iter_mut().enumerate() { + if self.used[index] { + self.visit_generic_param_mut(item); + } + } + } + + self.used(&*modified).cloned().collect() + } +}