Fix snapping when drawing an artboard (#3312)

* Fix snapping when drawing an artboard

* Fix typo in test
This commit is contained in:
James Lindsay 2025-10-29 21:41:39 +00:00 committed by GitHub
parent e5f40a3316
commit 40c6c6160b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 35 deletions

View File

@ -9,7 +9,6 @@ use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping; use crate::messages::tool::common_functionality::snapping;
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::snapping::SnapData;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::transformation_cage::*;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Artboard; use graphene_std::Artboard;
@ -108,7 +107,6 @@ impl Default for ArtboardToolFsmState {
struct ArtboardToolData { struct ArtboardToolData {
bounding_box_manager: Option<BoundingBoxManager>, bounding_box_manager: Option<BoundingBoxManager>,
selected_artboard: Option<LayerNodeIdentifier>, selected_artboard: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
cursor: MouseCursorIcon, cursor: MouseCursorIcon,
drag_start: DVec2, drag_start: DVec2,
drag_current: DVec2, drag_current: DVec2,
@ -188,7 +186,7 @@ impl ArtboardToolData {
let center = from_center.then_some(bounds.center_of_transformation); let center = from_center.then_some(bounds.center_of_transformation);
let ignore = self.selected_artboard.map_or(Vec::new(), |layer| vec![layer]); let ignore = self.selected_artboard.map_or(Vec::new(), |layer| vec![layer]);
let snap = Some(SizeSnapData { let snap = Some(SizeSnapData {
manager: &mut self.snap_manager, manager: &mut self.draw.snap_manager,
points: &mut self.snap_candidates, points: &mut self.snap_candidates,
snap_data: SnapData::ignore(document, input, &ignore), snap_data: SnapData::ignore(document, input, &ignore),
}); });
@ -275,7 +273,7 @@ impl Fsm for ArtboardToolFsmState {
} }
} }
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.draw.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
@ -322,7 +320,7 @@ impl Fsm for ArtboardToolFsmState {
let snap_data = SnapData::ignore(document, input, &ignore); let snap_data = SnapData::ignore(document, input, &ignore);
let document_to_viewport = document.metadata().document_to_viewport; let document_to_viewport = document.metadata().document_to_viewport;
let [start, current] = [tool_data.drag_start, tool_data.drag_current].map(|point| document_to_viewport.transform_point2(point)); let [start, current] = [tool_data.drag_start, tool_data.drag_current].map(|point| document_to_viewport.transform_point2(point));
let mouse_delta = snap_drag(start, current, axis_align, Axis::None, snap_data, &mut tool_data.snap_manager, &tool_data.snap_candidates); let mouse_delta = snap_drag(start, current, axis_align, Axis::None, snap_data, &mut tool_data.draw.snap_manager, &tool_data.snap_candidates);
let size = bounds.bounds[1] - bounds.bounds[0]; let size = bounds.bounds[1] - bounds.bounds[0];
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_delta); let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_delta);
@ -354,6 +352,8 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Dragging ArtboardToolFsmState::Dragging
} }
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => { (ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
// The draw.calculate_points_ignore_layer uses this value to avoid snapping to itself.
tool_data.draw.layer = tool_data.selected_artboard;
let [start, end] = tool_data.draw.calculate_points_ignore_layer(document, input, center, constrain_axis_or_aspect, true); let [start, end] = tool_data.draw.calculate_points_ignore_layer(document, input, center, constrain_axis_or_aspect, true);
let viewport_to_document = document.metadata().document_to_viewport.inverse(); let viewport_to_document = document.metadata().document_to_viewport.inverse();
let [start, end] = [start, end].map(|point| viewport_to_document.transform_point2(point)); let [start, end] = [start, end].map(|point| viewport_to_document.transform_point2(point));
@ -400,11 +400,11 @@ impl Fsm for ArtboardToolFsmState {
.map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false, false, None)); .map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false, false, None));
if cursor == MouseCursorIcon::Default && !hovered { if cursor == MouseCursorIcon::Default && !hovered {
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); tool_data.draw.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
cursor = MouseCursorIcon::Crosshair; cursor = MouseCursorIcon::Crosshair;
} else { } else {
tool_data.snap_manager.cleanup(responses); tool_data.draw.cleanup(responses);
} }
if tool_data.cursor != cursor { if tool_data.cursor != cursor {
@ -445,7 +445,7 @@ impl Fsm for ArtboardToolFsmState {
(ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds | ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerUp) => { (ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds | ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerUp) => {
responses.add(DocumentMessage::EndTransaction); responses.add(DocumentMessage::EndTransaction);
tool_data.snap_manager.cleanup(responses); tool_data.draw.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear(); bounds.original_transforms.clear();
@ -553,7 +553,7 @@ impl Fsm for ArtboardToolFsmState {
(ArtboardToolFsmState::Dragging | ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::Abort) => { (ArtboardToolFsmState::Dragging | ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
tool_data.snap_manager.cleanup(responses); tool_data.draw.cleanup(responses);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
ArtboardToolFsmState::Ready { hovered } ArtboardToolFsmState::Ready { hovered }
@ -611,17 +611,53 @@ mod test_artboard {
.collect() .collect()
} }
#[derive(Debug, PartialEq)]
struct ArtboardLayoutDocument {
position: IVec2,
dimensions: IVec2,
}
impl ArtboardLayoutDocument {
pub fn new(position: impl Into<IVec2>, dimensions: impl Into<IVec2>) -> Self {
Self {
position: position.into(),
dimensions: dimensions.into(),
}
}
}
/// Check if all of the artboards exist in any ordering
async fn has_artboards(editor: &mut EditorTestUtils, mut expected: Vec<ArtboardLayoutDocument>) {
let artboards = get_artboards(editor)
.await
.iter()
.map(|row| ArtboardLayoutDocument::new(row.element.location, row.element.dimensions))
.collect::<Vec<_>>();
assert_eq!(artboards.len(), expected.len(), "incorrect len: actual {:?}, expected {:?}", artboards, expected);
for artboard in artboards {
let Some(index) = expected.iter().position(|expected| *expected == artboard) else {
panic!("found {:?} that did not match any expected artboards\nexpected {:?}", artboard, expected);
};
expected.remove(index);
}
}
#[tokio::test] #[tokio::test]
async fn artboard_draw_simple() { async fn artboard_draw_simple() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::empty()).await; editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::empty()).await;
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((10, 0), (10, 11))]).await;
}
let artboards = get_artboards(&mut editor).await; #[tokio::test]
async fn artboard_snapping() {
assert_eq!(artboards.len(), 1); let mut editor = EditorTestUtils::create();
assert_eq!(artboards.get(0).unwrap().element.location, IVec2::new(10, 0)); editor.new_document().await;
assert_eq!(artboards.get(0).unwrap().element.dimensions, IVec2::new(10, 11)); editor.set_viewport_size(DVec2::splat(-1000.), DVec2::splat(1000.)).await; // Necessary for doing snapping since snaps outside of the viewport are discarded
editor.drag_tool(ToolType::Artboard, 10., 10., 20., 20., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Artboard, 11., 50., 19., 60., ModifierKeys::empty()).await;
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((10, 10), (10, 10)), ArtboardLayoutDocument::new((10, 50), (10, 10))]).await;
} }
#[tokio::test] #[tokio::test]
@ -629,11 +665,7 @@ mod test_artboard {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 10., 10., -10., 11., ModifierKeys::SHIFT).await; editor.drag_tool(ToolType::Artboard, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((-10, 10), (20, 20))]).await;
let artboards = get_artboards(&mut editor).await;
assert_eq!(artboards.len(), 1);
assert_eq!(artboards.get(0).unwrap().element.location, IVec2::new(-10, 10));
assert_eq!(artboards.get(0).unwrap().element.dimensions, IVec2::new(20, 20));
} }
#[tokio::test] #[tokio::test]
@ -648,12 +680,9 @@ mod test_artboard {
.await; .await;
// Viewport coordinates // Viewport coordinates
editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT).await; 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.get(0).unwrap().element.location, IVec2::new(0, 0));
let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.); let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.);
assert_eq!(artboards.get(0).unwrap().element.dimensions, desired_size.round().as_ivec2());
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new(IVec2::new(0, 0), desired_size.round().as_ivec2())]).await;
} }
#[tokio::test] #[tokio::test]
@ -669,12 +698,9 @@ mod test_artboard {
.await; .await;
// Viewport coordinates // Viewport coordinates
editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await;
let desired_location = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2();
let artboards = get_artboards(&mut editor).await; let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.).as_ivec2();
assert_eq!(artboards.len(), 1); has_artboards(&mut editor, vec![ArtboardLayoutDocument::new(desired_location, desired_size)]).await;
assert_eq!(artboards.get(0).unwrap().element.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.get(0).unwrap().element.dimensions, desired_size.round().as_ivec2());
} }
#[tokio::test] #[tokio::test]
@ -685,8 +711,7 @@ mod test_artboard {
editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::default()).await; editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::default()).await;
editor.press(Key::Delete, ModifierKeys::default()).await; editor.press(Key::Delete, ModifierKeys::default()).await;
let artboards = get_artboards(&mut editor).await; has_artboards(&mut editor, vec![]).await;
assert_eq!(artboards.len(), 0);
} }
#[tokio::test] #[tokio::test]
@ -696,7 +721,27 @@ mod test_artboard {
editor.new_document().await; editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Artboard).await; editor.drag_tool_cancel_rmb(ToolType::Artboard).await;
let artboards = get_artboards(&mut editor).await; has_artboards(&mut editor, vec![]).await;
assert_eq!(artboards.len(), 0); }
#[tokio::test]
async fn artboard_move() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 10., 10., 20., 22., ModifierKeys::empty()).await; // Artboard to drag
editor.drag_tool(ToolType::Artboard, 15., 15., 65., 65., ModifierKeys::empty()).await; // Drag from the middle by (50,50)
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((60, 60), (10, 12))]).await;
}
#[tokio::test]
async fn artboard_move_snapping() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.set_viewport_size(DVec2::splat(-1000.), DVec2::splat(1000.)).await; // Necessary for doing snapping since snaps outside of the viewport are discarded
editor.drag_tool(ToolType::Artboard, 10., 10., 20., 22., ModifierKeys::empty()).await; // Artboard to drag
editor.drag_tool(ToolType::Artboard, 70., 0., 80., 100., ModifierKeys::empty()).await; // Artboard to snap to
editor.drag_tool(ToolType::Artboard, 15., 15., 15. + 49., 15., ModifierKeys::empty()).await; // Drag the artboard so it should snap to the edge
has_artboards(&mut editor, vec![ArtboardLayoutDocument::new((60, 10), (10, 12)), ArtboardLayoutDocument::new((70, 0), (10, 100))]).await;
} }
} }

View File

@ -1,6 +1,7 @@
use crate::application::Editor; use crate::application::Editor;
use crate::application::set_uuid_seed; use crate::application::set_uuid_seed;
use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition}; use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition};
use crate::messages::portfolio::utility_types::Platform; use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*; use crate::messages::prelude::*;
@ -32,7 +33,6 @@ impl EditorTestUtils {
let _ = GLOBAL_PLATFORM.set(Platform::Windows).is_ok(); let _ = GLOBAL_PLATFORM.set(Platform::Windows).is_ok();
editor.handle_message(PortfolioMessage::Init); editor.handle_message(PortfolioMessage::Init);
Self { editor, runtime } Self { editor, runtime }
} }
@ -293,6 +293,14 @@ impl EditorTestUtils {
) )
.await; .await;
} }
/// Necessary for doing snapping since snaps outside of the viewport are discarded
pub async fn set_viewport_size(&mut self, top_left: DVec2, bottom_right: DVec2) {
self.handle_message(InputPreprocessorMessage::BoundsOfViewports {
bounds_of_viewports: vec![ViewportBounds { top_left, bottom_right }],
})
.await;
}
} }
pub trait FrontendMessageTestUtils { pub trait FrontendMessageTestUtils {