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
parent 1190e82322
commit b171eeba84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1097 additions and 236 deletions

7
Cargo.lock generated
View File

@ -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",
]

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);

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

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;

View File

@ -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)]

View File

@ -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
}

View File

@ -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()

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

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)
}
}

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)
}
}
}
};
}
}

View File

@ -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<std::sync::Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
log::warn!("Node::serialize not implemented for {}", core::any::type_name::<Self>());
None
}
@ -170,3 +170,25 @@ pub use crate::application_io::{SurfaceFrame, SurfaceId};
pub type WasmSurfaceHandle = application_io::SurfaceHandle<web_sys::HtmlCanvasElement>;
#[cfg(feature = "wasm")]
pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
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<Self>;
}
pub trait NodeInputDecleration {
const INDEX: usize;
fn identifier() -> &'static str;
type Result;
}

View File

@ -134,9 +134,9 @@ where
})
}
fn serialize(&self) -> Option<Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<Arc<dyn core::any::Any + Send + Sync>> {
let io = self.io.lock().unwrap();
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any>)
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any + Send + Sync>)
}
}

View File

@ -506,7 +506,7 @@ where
self.0.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
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);
}
}

View File

@ -1461,7 +1461,7 @@ const WINDOW_SIZE: usize = 1024;
#[cfg(feature = "alloc")]
#[node_macro::node(category(""))]
fn generate_curves<C: Channel + super::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
use bezier_rs::{Bezier, TValue};
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];

View File

@ -184,7 +184,7 @@ where
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
self.node.serialize()
}
}
@ -217,7 +217,7 @@ where
}
#[inline(always)]
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
self.node.serialize()
}
}
@ -273,7 +273,7 @@ where
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
self.node.serialize()
}
}

View File

@ -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<Self, Self::Error> {
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),

View File

@ -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)
]
);
}

View File

@ -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<Arc<dyn std::any::Any>, IntrospectError> {
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, 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<Arc<dyn std::any::Any>, IntrospectError> {
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, 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)

View File

@ -103,6 +103,28 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
node_io
},
),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::RasterFrame]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::instances::Instances<Artboard>]),
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<f64>]),
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<graphene_core::Color>]),
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<graphene_core::uuid::NodeId>]),
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<graphene_core::vector::VectorModification>]),
#[cfg(feature = "gpu")]
(
ProtoNodeIdentifier::new("graphene_std::executor::MapGpuSingleImageNode"),

View File

@ -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<TokenStream2> {
@ -250,6 +253,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier);
Ok(quote! {
/// Underlying implementation for [#struct_name]
#[inline]
@ -265,6 +269,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
#[doc(inline)]
pub use #mod_name::#struct_name;
#[doc(hidden)]
#node_input_accessor
#[doc(hidden)]
mod #mod_name {
use super::*;
@ -326,6 +333,93 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
})
}
/// Generates strongly typed utilites to access inputs
fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::GenericParam], field_idents: &[&PatIdent], graphene_core: &TokenStream2, identifier: &TokenStream2) -> 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<String> = 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<Item = &'a crate::GenericParam>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
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<TokenStream2, syn::Error> {
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<crate::GenericParam>,
used: Vec<bool>,
}
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<crate::GenericParam>, 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::<Vec<_>>();
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<Item = &'a crate::GenericParam> {
modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value)
}
fn filter_unnecessary_generics(&mut self, modified: &mut Vec<syn::GenericParam>, ty: &mut Type) -> Vec<syn::GenericParam> {
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()
}
}