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