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 <keavon@keavon.com>
This commit is contained in:
James Lindsay
2025-03-07 02:13:15 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 1190e82322
commit b171eeba84
22 changed files with 1097 additions and 236 deletions
+7
View File
@@ -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<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
self.dispatcher.handle_message(message, true);
+88 -94
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
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
+1
View File
@@ -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;
@@ -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<std::collections::hash_map::Iter<'a, NodeId, DocumentNode>>,
}
impl<'a> Iterator for NodesRecursiveIter<'a> {
type Item = (NodeId, &'a DocumentNode);
fn next(&mut self) -> Option<Self::Item> {
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)]
@@ -1160,7 +1160,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
impl PortfolioMessageHandler {
pub async fn introspect_node(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, 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<Arc<dyn std::any::Any + Send + Sync>, IntrospectError> {
self.executor.introspect_node(node_path).await
}
@@ -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<NodeId> {
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<NodeInput>> {
self.horizontal_layer_flow()
@@ -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<SnapCandidatePoint>,
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<graphene_core::Artboard> {
let instrumented = editor.eval_graph().await;
instrumented.grab_all_input::<graphene_core::append_artboard::ArtboardInput>(&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);
}
}
@@ -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<ResolvedEllipse> {
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::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&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);
}
}
@@ -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<Fill> {
let instrumented = editor.eval_graph().await;
instrumented.grab_all_input::<fill::FillInput<Fill>>(&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));
}
}
+140 -6
View File
@@ -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<Arc<dyn std::any::Any>, IntrospectError> {
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, 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<Arc<dyn std::any::Any>, IntrospectError> {
pub async fn introspect_node(&self, path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, 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<Instrumented, String> {
// 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<String, Vec<Vec<Vec<NodeId>>>>,
protonodes_by_path: HashMap<Vec<NodeId>, Vec<Vec<NodeId>>>,
}
impl Instrumented {
/// Adds montior nodes to the network
fn add(&mut self, network: &mut NodeNetwork, path: &mut Vec<NodeId>) {
// 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<Input: graphene_std::NodeInputDecleration>(dynamic: Arc<dyn std::any::Any + Send + Sync>) -> Option<Input::Result>
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::<IORecord<(), Input::Result>>() {
Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Context, Input::Result>>() {
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<Item = Input::Result> + '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::<Input>)
}
pub fn grab_protonode_input<Input: graphene_std::NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
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::<Input>(dynamic)
}
}
+231 -61
View File
@@ -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<Output = Instrumented> + '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<Message>) {
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<Item = T> + '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<Color>, 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)
}
}
}
};
}
}