diff --git a/Cargo.lock b/Cargo.lock index 92b87cd829..22ad5e1e8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2264,6 +2264,7 @@ dependencies = [ "graphite-proc-macros", "interpreted-executor", "js-sys", + "kurbo", "log", "num_enum", "once_cell", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 78791a1a91..31e54a7a16 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -40,6 +40,7 @@ serde_json = { workspace = true } bezier-rs = { workspace = true } futures = { workspace = true } glam = { workspace = true } +kurbo = { workspace = true } derivative = { workspace = true } specta = { workspace = true } dyn-any = { workspace = true } diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 144ddd6757..19edd48afd 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -119,6 +119,13 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.; pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.; pub const DEFAULT_BRUSH_SIZE: f64 = 20.; +// GIZMOS +pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.; +pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; +pub const NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION: f64 = 1.2; +pub const NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH: f64 = 10.; +pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; + // SCROLLBARS pub const SCROLLBAR_SPACING: f64 = 0.1; pub const ASYMPTOTIC_EFFECT: f64 = 0.5; diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 024480f2d4..46854dc760 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -171,12 +171,40 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=GradientToolMessage::Abort), // - // RectangleToolMessage - entry!(KeyDown(MouseLeft); action_dispatch=RectangleToolMessage::DragStart), - entry!(KeyUp(MouseLeft); action_dispatch=RectangleToolMessage::DragStop), - entry!(KeyDown(MouseRight); action_dispatch=RectangleToolMessage::Abort), - entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort), - entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::PointerMove { center: Alt, lock_ratio: Shift }), + // ShapeToolMessage + entry!(KeyDown(MouseLeft); action_dispatch=ShapeToolMessage::DragStart), + entry!(KeyUp(MouseLeft); action_dispatch=ShapeToolMessage::DragStop), + entry!(KeyDown(MouseRight); action_dispatch=ShapeToolMessage::Abort), + entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort), + entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides), + entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides), + entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control, Shift])), + entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::IncreaseSides), + entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::DecreaseSides), // // ImaginateToolMessage // entry!(KeyDown(MouseLeft); action_dispatch=ImaginateToolMessage::DragStart), @@ -185,27 +213,6 @@ pub fn input_mappings() -> Mapping { // entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort), // entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }), // - // EllipseToolMessage - entry!(KeyDown(MouseLeft); action_dispatch=EllipseToolMessage::DragStart), - entry!(KeyUp(MouseLeft); action_dispatch=EllipseToolMessage::DragStop), - entry!(KeyDown(MouseRight); action_dispatch=EllipseToolMessage::Abort), - entry!(KeyDown(Escape); action_dispatch=EllipseToolMessage::Abort), - entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=EllipseToolMessage::PointerMove { center: Alt, lock_ratio: Shift }), - // - // PolygonToolMessage - entry!(KeyDown(MouseLeft); action_dispatch=PolygonToolMessage::DragStart), - entry!(KeyUp(MouseLeft); action_dispatch=PolygonToolMessage::DragStop), - entry!(KeyDown(MouseRight); action_dispatch=PolygonToolMessage::Abort), - entry!(KeyDown(Escape); action_dispatch=PolygonToolMessage::Abort), - entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PolygonToolMessage::PointerMove { center: Alt, lock_ratio: Shift }), - // - // LineToolMessage - entry!(KeyDown(MouseLeft); action_dispatch=LineToolMessage::DragStart), - entry!(KeyUp(MouseLeft); action_dispatch=LineToolMessage::DragStop), - entry!(KeyDown(MouseRight); action_dispatch=LineToolMessage::Abort), - entry!(KeyDown(Escape); action_dispatch=LineToolMessage::Abort), - entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }), - // // PathToolMessage entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), @@ -308,10 +315,10 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath), entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen), entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand), - entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine), - entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle), - entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse), - entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolPolygon), + entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolShapeLine), + entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolShapeRectangle), + entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolShapeEllipse), + entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape), entry!(KeyDown(KeyB); action_dispatch=ToolMessage::ActivateToolBrush), entry!(KeyDown(KeyX); modifiers=[Accel, Shift], action_dispatch=ToolMessage::ResetColors), entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 21906fac46..8e7b1c39cd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1705,6 +1705,21 @@ impl DocumentMessageHandler { self.click_list(ipp).last() } + pub fn click_based_on_position(&self, mouse_snapped_positon: DVec2) -> Option { + ClickXRayIter::new(&self.network_interface, XRayTarget::Point(mouse_snapped_positon)) + .filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) + .skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT) + .scan(true, |last_had_children, layer| { + if *last_had_children { + *last_had_children = layer.has_children(self.network_interface.document_metadata()); + Some(layer) + } else { + None + } + }) + .last() + } + /// Get the combined bounding box of the click targets of the selected visible layers in viewport space pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> { self.network_interface diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 8c03ac51fe..eb6900a53f 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -34,19 +34,16 @@ pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastE pub use crate::messages::message::{Message, MessageDiscriminant}; pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant}; -pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant}; // pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant}; -pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant}; -pub use crate::messages::tool::tool_messages::polygon_tool::{PolygonToolMessage, PolygonToolMessageDiscriminant}; -pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant}; +pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant}; diff --git a/editor/src/messages/tool/common_functionality/mod.rs b/editor/src/messages/tool/common_functionality/mod.rs index 9bc2236061..4a003d0254 100644 --- a/editor/src/messages/tool/common_functionality/mod.rs +++ b/editor/src/messages/tool/common_functionality/mod.rs @@ -6,6 +6,8 @@ pub mod measure; pub mod pivot; pub mod resize; pub mod shape_editor; +pub mod shape_gizmos; +pub mod shapes; pub mod snapping; pub mod transformation_cage; pub mod utility_functions; diff --git a/editor/src/messages/tool/common_functionality/resize.rs b/editor/src/messages/tool/common_functionality/resize.rs index f81b13fed9..fc8f1ee6da 100644 --- a/editor/src/messages/tool/common_functionality/resize.rs +++ b/editor/src/messages/tool/common_functionality/resize.rs @@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2, Vec2Swizzles}; #[derive(Clone, Debug, Default)] pub struct Resize { /// Stored as a document position so the start doesn't move if the canvas is panned. - drag_start: DVec2, + pub drag_start: DVec2, pub layer: Option, pub snap_manager: SnapManager, } diff --git a/editor/src/messages/tool/common_functionality/shape_gizmos/mod.rs b/editor/src/messages/tool/common_functionality/shape_gizmos/mod.rs new file mode 100644 index 0000000000..03951cd2d7 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shape_gizmos/mod.rs @@ -0,0 +1,2 @@ +pub mod number_of_points_handle; +pub mod point_radius_handle; diff --git a/editor/src/messages/tool/common_functionality/shape_gizmos/number_of_points_handle.rs b/editor/src/messages/tool/common_functionality/shape_gizmos/number_of_points_handle.rs new file mode 100644 index 0000000000..d698292ea4 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shape_gizmos/number_of_points_handle.rs @@ -0,0 +1,241 @@ +use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION, NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ + extract_polygon_parameters, extract_star_parameters, inside_polygon, inside_star, polygon_vertex_position, star_vertex_position, +}; +use crate::messages::tool::tool_messages::tool_prelude::Key; +use glam::{DAffine2, DVec2}; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; +use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum NumberOfPointsHandleState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct NumberOfPointsHandle { + pub layer: Option, + pub initial_points: u32, + pub handle_state: NumberOfPointsHandleState, +} + +impl NumberOfPointsHandle { + pub fn cleanup(&mut self) { + self.handle_state = NumberOfPointsHandleState::Inactive; + self.layer = None; + } + + pub fn update_state(&mut self, state: NumberOfPointsHandleState) { + self.handle_state = state; + } + + pub fn is_hovering(&self) -> bool { + self.handle_state == NumberOfPointsHandleState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.handle_state == NumberOfPointsHandleState::Dragging + } + + pub fn handle_actions( + &mut self, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + responses: &mut VecDeque, + ) { + if input.keyboard.key(Key::Control) { + return; + } + + match &self.handle_state { + NumberOfPointsHandleState::Inactive => { + let selected_nodes = document.network_interface.selected_nodes(); + let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| { + graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some() + }); + for layer in layers { + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2); + + if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD { + self.layer = Some(layer); + self.initial_points = n; + self.update_state(NumberOfPointsHandleState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius); + + if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD { + self.layer = Some(layer); + self.initial_points = n; + self.update_state(NumberOfPointsHandleState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + } + } + NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => { + let Some(layer) = self.layer else { return }; + + let Some((n, radius)) = extract_star_parameters(Some(layer), document) + .map(|(n, r1, r2)| (n, r1.max(r2))) + .or_else(|| extract_polygon_parameters(Some(layer), document)) + else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + if mouse_position.distance(center) > NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsHandleState::Hover) { + self.update_state(NumberOfPointsHandleState::Inactive); + self.layer = None; + self.draw_spokes(center, viewport, n, radius, overlay_context); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + } + } + + pub fn overlays( + &mut self, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if input.keyboard.key(Key::Control) { + return; + } + + match &self.handle_state { + NumberOfPointsHandleState::Inactive => { + let selected_nodes = document.network_interface.selected_nodes(); + let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| { + graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some() + }); + for layer in layers { + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let radius = radius1.max(radius2); + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) { + if closest_segment.layer() == layer { + return; + } + } + let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2); + + if inside_star(viewport, n, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD { + self.draw_spokes(center, viewport, n, radius, overlay_context); + return; + } + } + + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) { + if closest_segment.layer() == layer { + return; + } + } + let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius); + + if inside_polygon(viewport, n, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD { + self.draw_spokes(center, viewport, n, radius, overlay_context); + return; + } + } + } + } + NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => { + let Some(layer) = self.layer else { return }; + + let Some((n, radius)) = extract_star_parameters(Some(layer), document) + .map(|(n, r1, r2)| (n, r1.max(r2))) + .or_else(|| extract_polygon_parameters(Some(layer), document)) + else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + self.draw_spokes(center, viewport, n, radius, overlay_context); + } + } + } + + fn draw_spokes(&self, center: DVec2, viewport: DAffine2, n: u32, radius: f64, overlay_context: &mut OverlayContext) { + for i in 0..n { + let angle = ((i as f64) * TAU) / (n as f64); + + let point = viewport.transform_point2(DVec2 { + x: radius * angle.sin(), + y: -radius * angle.cos(), + }); + + let Some(direction) = (point - center).try_normalize() else { continue }; + + // If the user zooms out such that shape is very small hide the gizmo + if point.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + let end_point = direction * NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH; + if matches!(self.handle_state, NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging) { + overlay_context.line(center, end_point * NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION + center, None, None); + } else { + overlay_context.line(center, end_point + center, None, None); + } + } + } + + pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start); + let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum(); + let net_delta = (delta.length() / 25.).round() * sign; + + let Some(layer) = self.layer else { return }; + let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { + return; + }; + + let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } +} diff --git a/editor/src/messages/tool/common_functionality/shape_gizmos/point_radius_handle.rs b/editor/src/messages/tool/common_functionality/shape_gizmos/point_radius_handle.rs new file mode 100644 index 0000000000..4a72de0319 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shape_gizmos/point_radius_handle.rs @@ -0,0 +1,441 @@ +use crate::consts::{COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD, POINT_RADIUS_HANDLE_SNAP_THRESHOLD}; +use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, extract_star_parameters, polygon_vertex_position, star_vertex_position}; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; +use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum PointRadiusHandleState { + #[default] + Inactive, + Hover, + Dragging, + Snapped(usize), +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct PointRadiusHandle { + pub layer: Option, + point: u32, + radius_index: usize, + snap_radii: Vec, + initial_radius: f64, + handle_state: PointRadiusHandleState, +} + +impl PointRadiusHandle { + pub fn cleanup(&mut self) { + self.handle_state = PointRadiusHandleState::Inactive; + self.snap_radii.clear(); + self.layer = None; + } + + pub fn is_inactive(&self) -> bool { + self.handle_state == PointRadiusHandleState::Inactive + } + + pub fn hovered(&self) -> bool { + self.handle_state == PointRadiusHandleState::Hover + } + + pub fn update_state(&mut self, state: PointRadiusHandleState) { + self.handle_state = state; + } + + pub fn handle_actions(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) { + match &self.handle_state { + PointRadiusHandleState::Inactive => { + for layer in document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .filter(|layer| { + graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some() + }) { + // Draw the point handle gizmo for the star shape + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + + for i in 0..2 * n { + let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) }; + let point = star_vertex_position(viewport, i as i32, n, radius1, radius2); + let center = viewport.transform_point2(DVec2::ZERO); + + // If the user zooms out such that shape is very small hide the gizmo + if point.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + if point.distance(mouse_position) < 5. { + self.radius_index = radius_index; + self.layer = Some(layer); + self.point = i; + self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index); + self.initial_radius = radius; + self.update_state(PointRadiusHandleState::Hover); + + return; + } + } + } + + // Draw the point handle gizmo for the polygon shape + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + + for i in 0..n { + let point = polygon_vertex_position(viewport, i as i32, n, radius); + let center = viewport.transform_point2(DVec2::ZERO); + + // If the user zooms out so the shape is very small, hide the gizmo + if point.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + if point.distance(mouse_position) < 5. { + self.radius_index = 2; + self.layer = Some(layer); + self.point = i; + self.snap_radii.clear(); + self.initial_radius = radius; + self.update_state(PointRadiusHandleState::Hover); + + return; + } + } + } + } + } + + PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => { + let Some(layer) = self.layer else { return }; + + let viewport = document.metadata().transform_to_viewport(layer); + + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); + + if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. { + self.update_state(PointRadiusHandleState::Inactive); + self.layer = None; + return; + } + } + + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let point = polygon_vertex_position(viewport, self.point as i32, n, radius); + + if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. { + self.update_state(PointRadiusHandleState::Inactive); + self.layer = None; + } + } + } + PointRadiusHandleState::Snapped(_) => {} + } + } + + pub fn overlays(&mut self, other_gizmo_active: bool, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, mouse_position: DVec2, overlay_context: &mut OverlayContext) { + match &self.handle_state { + PointRadiusHandleState::Inactive => { + let selected_nodes = document.network_interface.selected_nodes(); + let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| { + graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some() + }); + for layer in layers { + if other_gizmo_active { + return; + } + // Draw the point handle gizmo for the star shape + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + + for i in 0..(2 * n) { + let point = star_vertex_position(viewport, i as i32, n, radius1, radius2); + let center = viewport.transform_point2(DVec2::ZERO); + let viewport_diagonal = input.viewport_bounds.size().length(); + + // If the user zooms out such that shape is very small hide the gizmo + if point.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + if point.distance(mouse_position) < 5. { + let Some(direction) = (point - center).try_normalize() else { continue }; + + overlay_context.manipulator_handle(point, true, None); + let angle = ((i as f64) * PI) / (n as f64); + overlay_context.line(center, center + direction * viewport_diagonal, None, None); + + draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context); + + return; + } + + overlay_context.manipulator_handle(point, false, None); + } + } + + // Draw the point handle gizmo for the Polygon shape + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let viewport = document.metadata().transform_to_viewport(layer); + + for i in 0..n { + let point = polygon_vertex_position(viewport, i as i32, n, radius); + let center = viewport.transform_point2(DVec2::ZERO); + let viewport_diagonal = input.viewport_bounds.size().length(); + + // If the user zooms out such that shape is very small hide the gizmo + if point.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + if point.distance(mouse_position) < 5. { + let Some(direction) = (point - center).try_normalize() else { continue }; + + overlay_context.manipulator_handle(point, true, None); + overlay_context.line(center, center + direction * viewport_diagonal, None, None); + + return; + } + + overlay_context.manipulator_handle(point, false, None); + } + } + } + } + + PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => { + let Some(layer) = self.layer else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + let viewport_diagonal = input.viewport_bounds.size().length(); + + if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { + let angle = ((self.point as f64) * PI) / (n as f64); + let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); + + let Some(direction) = (point - center).try_normalize() else { return }; + + // Draws the ray from the center to the dragging point extending till the viewport + overlay_context.manipulator_handle(point, true, None); + overlay_context.line(center, center + direction * viewport_diagonal, None, None); + + // Makes the tick marks for snapping + + // Only show the snapping ticks if the radius is positive + if (mouse_position - center).dot(direction) >= 0. { + draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context); + } + + return; + } + + if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { + let point = polygon_vertex_position(viewport, self.point as i32, n, radius); + + let Some(direction) = (point - center).try_normalize() else { return }; + + // Draws the ray from the center to the dragging point and extending until the viewport edge is reached + overlay_context.manipulator_handle(point, true, None); + overlay_context.line(center, center + direction * viewport_diagonal, None, None); + } + } + PointRadiusHandleState::Snapped(snapping_index) => { + let Some(layer) = self.layer else { return }; + let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + match snapping_index { + // Make a triangle with the previous two points + 0 => { + let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, n, radius1, radius2); + let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2); + let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); + + overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + + let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return }; + let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return }; + let Some(direction) = (center - outer_position).try_normalize() else { return }; + + let l1 = 0.2 * (before_outer_position - outer_position).length(); + let new_point = SQRT_2 * l1 * direction + outer_position; + + let before_outer_position = l1 * l1_direction + outer_position; + let point_position = l1 * l2_direction + outer_position; + + overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + } + 1 => { + let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2); + let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, n, radius1, radius2); + let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); + + overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + + let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return }; + let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return }; + let Some(direction) = (center - point_position).try_normalize() else { return }; + + let l1 = 0.2 * (before_outer_position - point_position).length(); + let new_point = SQRT_2 * l1 * direction + point_position; + + let before_outer_position = l1 * l1_direction + point_position; + let after_point_position = l1 * l2_direction + point_position; + + overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + } + i => { + // Use `self.point` as an absolute reference, as it matches the index of the star's vertices starting from 0 + if i % 2 != 0 { + // Flipped case + let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); + let target_index = (1 - (*i as i32)).abs() + (self.point as i32); + let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2); + + let mirrored_index = 2 * (self.point as i32) - target_index; + let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2); + + overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.)); + } else { + let outer_index = (self.point as i32) - 1; + let outer_position = star_vertex_position(viewport, outer_index, n, radius1, radius2); + + // The vertex which is colinear with the point we are dragging and its previous outer vertex + let target_index = (self.point as i32) + (*i as i32) - 1; + let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2); + + let mirrored_index = 2 * outer_index - target_index; + + let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2); + + overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.)); + } + } + } + } + } + } + + fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec { + let mut snap_radii = Vec::new(); + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else { + return snap_radii; + }; + + let other_index = if radius_index == 3 { 2 } else { 3 }; + let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else { + return snap_radii; + }; + let Some(&TaggedValue::U32(n)) = node_inputs[1].as_value() else { + return snap_radii; + }; + + // Inner radius for 90° + let b = FRAC_PI_4 * 3. - PI / (n as f64); + let angle = b.sin(); + let required_radius = (other_radius / angle) * FRAC_1_SQRT_2; + + snap_radii.push(required_radius); + + // Also add the case where the radius exceeds the other radius (the "flipped" case) + let flipped = other_radius * angle * SQRT_2; + snap_radii.push(flipped); + + for i in 1..n { + let n = n as f64; + let i = i as f64; + let denominator = 2. * ((PI * (i - 1.)) / n).cos() * ((PI * i) / n).sin(); + let numerator = ((2. * PI * i) / n).sin(); + let factor = numerator / denominator; + + if factor < 0. { + break; + } + + if other_radius * factor > 1e-6 { + snap_radii.push(other_radius * factor); + } + + snap_radii.push((other_radius * 1.) / factor); + } + + snap_radii + } + + fn check_snapping(&self, new_radius: f64, original_radius: f64) -> Option<(usize, f64)> { + self.snap_radii + .iter() + .enumerate() + .filter(|(_, rad)| (**rad - new_radius).abs() < POINT_RADIUS_HANDLE_SNAP_THRESHOLD) + .min_by(|(i_a, a), (i_b, b)| { + let dist_a = (**a - new_radius).abs(); + let dist_b = (**b - new_radius).abs(); + + // Check if either index is 0 or 1 and prioritize them + match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) { + (true, false) => std::cmp::Ordering::Less, // a is priority index, b is not + (false, true) => std::cmp::Ordering::Greater, // b is priority index, a is not + _ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal), // normal comparison + } + }) + .map(|(i, rad)| (i, *rad - original_radius)) + } + + pub fn update_inner_radius( + &mut self, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + responses: &mut VecDeque, + drag_start: DVec2, + ) { + let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { + return; + }; + + let transform = document.network_interface.document_metadata().transform_to_viewport(layer); + let center = transform.transform_point2(DVec2::ZERO); + let radius_index = self.radius_index; + + let original_radius = self.initial_radius; + + let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start); + let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center; + let projection = delta.project_onto(radius); + let sign = radius.dot(delta).signum(); + + let mut net_delta = projection.length() * sign; + let new_radius = original_radius + net_delta; + + self.update_state(PointRadiusHandleState::Dragging); + if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) { + net_delta = snapped_delta; + self.update_state(PointRadiusHandleState::Snapped(index)); + } + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, radius_index), + input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/ellipse_shape.rs b/editor/src/messages/tool/common_functionality/shapes/ellipse_shape.rs new file mode 100644 index 0000000000..fe97c22318 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/ellipse_shape.rs @@ -0,0 +1,181 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Ellipse; + +impl Ellipse { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) { + let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false), + }); + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_translation(start.midpoint(end)), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} + +#[cfg(test)] +mod test_ellipse { + pub use crate::test_utils::test_prelude::*; + use glam::DAffine2; + use graphene_std::vector::generator_nodes::ellipse; + + #[derive(Debug, PartialEq)] + struct ResolvedEllipse { + radius_x: f64, + radius_y: f64, + transform: DAffine2, + } + + async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec { + let instrumented = match editor.eval_graph().await { + Ok(instrumented) => instrumented, + Err(e) => panic!("Failed to evaluate graph: {e}"), + }; + + let document = editor.active_document(); + let layers = document.metadata().all_layers(); + layers + .filter_map(|layer| { + let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface); + let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?; + Some(ResolvedEllipse { + radius_x: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), + radius_y: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), + transform: document.metadata().transform_to_document(layer), + }) + }) + .collect() + } + + #[tokio::test] + async fn ellipse_draw_simple() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await; + + assert_eq!(editor.active_document().metadata().all_layers().count(), 1); + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + assert_eq!( + ellipse[0], + ResolvedEllipse { + radius_x: 4.5, + radius_y: 5., + transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center + } + ); + } + + #[tokio::test] + async fn ellipse_draw_circle() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await; + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + assert_eq!( + ellipse[0], + ResolvedEllipse { + radius_x: 10., + radius_y: 10., + transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center + } + ); + } + + #[tokio::test] + async fn ellipse_draw_square_rotated() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + // 45 degree rotation of content clockwise + angle_radians: f64::consts::FRAC_PI_4, + }) + .await; + editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 1); + println!("{ellipse:?}"); + 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) + ); + } + + #[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); + 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)); + } + + #[tokio::test] + async fn ellipse_cancel() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool_cancel_rmb(ToolType::Ellipse).await; + + let ellipse = get_ellipse(&mut editor).await; + assert_eq!(ellipse.len(), 0); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/line_shape.rs b/editor/src/messages/tool/common_functionality/shapes/line_shape.rs new file mode 100644 index 0000000000..985457c208 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/line_shape.rs @@ -0,0 +1,383 @@ +use super::shape_utility::ShapeToolModifierKey; +use crate::consts::{BOUNDS_SELECT_THRESHOLD, LINE_ROTATE_SNAP_ANGLE}; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration}; +use crate::messages::tool::tool_messages::shape_tool::ShapeToolData; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Clone, PartialEq, Debug, Default)] +pub enum LineEnd { + #[default] + Start, + End, +} + +#[derive(Clone, Debug, Default)] +pub struct LineToolData { + pub drag_start: DVec2, + pub drag_current: DVec2, + pub angle: f64, + pub weight: f64, + pub selected_layers_with_position: HashMap, + pub editing_layer: Option, + pub dragging_endpoint: Option, +} + +#[derive(Default)] +pub struct Line; + +impl Line { + pub fn create_node(document: &DocumentMessageHandler, drag_start: DVec2) -> NodeTemplate { + let node_type = resolve_document_node_type("Line").expect("Line node can't be found"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)), + Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)), + ]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, _, lock_angle, snap_angle] = modifier; + + shape_tool_data.line_data.drag_current = ipp.mouse.position; + + let keyboard = &ipp.keyboard; + let ignore = [layer]; + let snap_data = SnapData::ignore(document, ipp, &ignore); + let mut document_points = generate_line(shape_tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center)); + + if shape_tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) { + document_points.swap(0, 1); + } + + let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) { + shape_tool_data.line_data.selected_layers_with_position = document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .filter_map(|layer| { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?; + + let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else { + return None; + }; + + let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point)); + if !start.abs_diff_eq(end, f64::EPSILON * 1000.) { + overlay_context.line(viewport_start, viewport_end, None, None); + overlay_context.square(viewport_start, Some(6.), None, None); + overlay_context.square(viewport_end, Some(6.), None, None); + } + + Some((layer, [start, end])) + }) + .collect::>(); + } +} + +fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] { + let document_to_viewport = snap_data.document.metadata().document_to_viewport; + let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)]; + + let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X); + let mut line_length = (document_points[1] - document_points[0]).length(); + + if lock_angle { + angle = tool_data.line_data.angle; + } else if snap_angle { + let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); + angle = (angle / snap_resolution).round() * snap_resolution; + } + + tool_data.line_data.angle = angle; + + if lock_angle { + let angle_vec = DVec2::new(angle.cos(), angle.sin()); + line_length = (document_points[1] - document_points[0]).dot(angle_vec); + } + + document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin()); + + let constrained = snap_angle || lock_angle; + let snap = &mut tool_data.data.snap_manager; + + let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.data.drag_start]); + let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.data.drag_start]); + let config = SnapTypeConfiguration::default(); + + if constrained { + let constraint = SnapConstraint::Line { + origin: document_points[0], + direction: document_points[1] - document_points[0], + }; + if center { + let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); + let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config); + let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; + document_points[1] = document_points[0] * 2. - best.snapped_point_document; + document_points[0] = best.snapped_point_document; + snap.update_indicator(best); + } else { + let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); + document_points[1] = snapped.snapped_point_document; + snap.update_indicator(snapped); + } + } else if center { + let snapped = snap.free_snap(&snap_data, &near_point, config); + let snapped_far = snap.free_snap(&snap_data, &far_point, config); + let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; + document_points[1] = document_points[0] * 2. - best.snapped_point_document; + document_points[0] = best.snapped_point_document; + snap.update_indicator(best); + } else { + let snapped = snap.free_snap(&snap_data, &near_point, config); + document_points[1] = snapped.snapped_point_document; + snap.update_indicator(snapped); + } + + document_points +} + +pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool { + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line") else { + return false; + }; + + let (Some(&TaggedValue::DVec2(document_start)), Some(&TaggedValue::DVec2(document_end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else { + return false; + }; + + let transform = document.metadata().transform_to_viewport(layer); + let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD; + let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD; + let threshold_x = transform.inverse().transform_vector2(viewport_x).length(); + let threshold_y = transform.inverse().transform_vector2(viewport_y).length(); + + let drag_start = input.mouse.position; + let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(point)); + + let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x; + let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x; + + if start_click || end_click { + shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start }); + shape_tool_data.data.drag_start = if end_click { document_start } else { document_end }; + shape_tool_data.line_data.editing_layer = Some(layer); + return true; + } + false +} + +#[cfg(test)] +mod test_line_tool { + use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; + use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; + use crate::test_utils::test_prelude::*; + use glam::DAffine2; + use graph_craft::document::value::TaggedValue; + + async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> { + let document = editor.active_document(); + let network_interface = &document.network_interface; + let node_id = network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(network_interface) + .filter_map(|layer| { + let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?; + let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else { + return None; + }; + Some((start, end)) + }) + .next(); + node_id + } + + #[tokio::test] + async fn test_line_tool_basicdraw() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await; + if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { + match (start_input, end_input) { + (start_input, end_input) => { + assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)"); + assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)"); + } + } + } + } + + #[tokio::test] + async fn test_line_tool_with_transformed_viewport() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await; + editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await; + editor + .handle_message(NavigationMessage::CanvasTiltSet { + angle_radians: (30. as f64).to_radians(), + }) + .await; + editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await; + if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { + let document = editor.active_document(); + let document_to_viewport = document.metadata().document_to_viewport; + let viewport_to_document = document_to_viewport.inverse(); + + let expected_start = viewport_to_document.transform_point2(DVec2::ZERO); + let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.)); + + assert!( + (start_input - expected_start).length() < 1., + "Start point should match expected document coordinates. Got {:?}, expected {:?}", + start_input, + expected_start + ); + assert!( + (end_input - expected_end).length() < 1., + "End point should match expected document coordinates. Got {:?}, expected {:?}", + end_input, + expected_end + ); + } else { + panic!("Line was not created successfully with transformed viewport"); + } + } + + #[tokio::test] + async fn test_line_tool_ctrl_anglelock() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await; + if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { + match (start_input, end_input) { + (start_input, end_input) => { + let line_vec = end_input - start_input; + let original_angle = line_vec.angle_to(DVec2::X); + editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await; + if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await { + match (updated_start, updated_end) { + (updated_start, updated_end) => { + let updated_line_vec = updated_end - updated_start; + let updated_angle = updated_line_vec.angle_to(DVec2::X); + print!("{:?}", original_angle); + print!("{:?}", updated_angle); + assert!( + line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6, + "Line angle should be locked when Ctrl is kept pressed" + ); + assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed"); + } + } + } + } + } + } + } + + #[tokio::test] + async fn test_line_tool_alt() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await; + if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { + match (start_input, end_input) { + (start_input, end_input) => { + let expected_start = DVec2::new(0., 100.); + let expected_end = DVec2::new(200., 100.); + assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)"); + assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)"); + } + } + } + } + + #[tokio::test] + async fn test_line_tool_alt_shift_drag() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await; + if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { + match (start_input, end_input) { + (start_input, end_input) => { + let line_vec = end_input - start_input; + let angle_radians = line_vec.angle_to(DVec2::X); + let angle_degrees = angle_radians.to_degrees(); + let nearest_angle = (angle_degrees / 15.).round() * 15.; + + assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees"); + } + } + } + } + + #[tokio::test] + async fn test_line_tool_with_transformed_artboard() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await; + + let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard"); + + editor + .handle_message(GraphOperationMessage::TransformChange { + layer: artboard_id, + transform: DAffine2::from_angle(45_f64.to_radians()), + transform_in: TransformIn::Local, + skip_rerender: false, + }) + .await; + + editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await; + + let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard"); + // The line should still be diagonal with equal change in x and y + let line_vector = end_input - start_input; + // Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square) + let line_length = line_vector.length(); + assert!( + (line_length - 141.42).abs() < 1., // 100 * sqrt(2) ~= 141.42 + "Line length should be approximately 141.42 units. Got: {line_length}" + ); + assert!((line_vector.x - 100.).abs() < 1., "X-component of line vector should be approximately 100. Got: {}", line_vector.x); + assert!( + (line_vector.y.abs() - 100.).abs() < 1., + "Absolute Y-component of line vector should be approximately 100. Got: {}", + line_vector.y.abs() + ); + let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees(); + assert!((angle_degrees - (-45.)).abs() < 1., "Line angle should be close to -45 degrees. Got: {angle_degrees}"); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs new file mode 100644 index 0000000000..44f40b5982 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -0,0 +1,11 @@ +pub mod ellipse_shape; +pub mod line_shape; +pub mod polygon_shape; +pub mod rectangle_shape; +pub mod shape_utility; +pub mod star_shape; + +pub use super::shapes::ellipse_shape::Ellipse; +pub use super::shapes::line_shape::{Line, LineEnd}; +pub use super::shapes::rectangle_shape::Rectangle; +pub use crate::messages::tool::tool_messages::shape_tool::ShapeToolData; diff --git a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs new file mode 100644 index 0000000000..871cf2c0b2 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -0,0 +1,68 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::shape_utility::update_radius_sign; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Polygon; + +impl Polygon { + pub fn create_node(vertices: u32) -> NodeTemplate { + let node_type = resolve_document_node_type("Regular Polygon").expect("Regular Polygon can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::U32(vertices), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) { + // TODO: We need to determine how to allow the polygon node to make irregular shapes + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + + // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly + let mut scale = DVec2::ONE; + let radius; + if dimensions.x > dimensions.y { + scale.x = dimensions.x / dimensions.y; + radius = dimensions.y / 2.; + } else { + scale.y = dimensions.y / dimensions.x; + radius = dimensions.x / 2.; + } + + let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(radius), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/rectangle_shape.rs b/editor/src/messages/tool/common_functionality/shapes/rectangle_shape.rs new file mode 100644 index 0000000000..cbd6722960 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/rectangle_shape.rs @@ -0,0 +1,54 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Rectangle; + +impl Rectangle { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) { + let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false), + }); + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_translation(start.midpoint(end)), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs new file mode 100644 index 0000000000..772991be4b --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -0,0 +1,320 @@ +use super::ShapeToolData; +use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::{DocumentMessageHandler, NodeGraphMessage, Responses}; +use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager; +use crate::messages::tool::tool_messages::tool_prelude::Key; +use crate::messages::tool::utility_types::*; +use bezier_rs::Subpath; +use glam::{DAffine2, DMat2, DVec2}; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::renderer::ClickTargetType; +use graphene_std::vector::misc::dvec2_to_point; +use kurbo::{BezPath, PathEl, Shape}; +use std::collections::VecDeque; +use std::f64::consts::{PI, TAU}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ShapeType { + #[default] + Polygon = 0, + Star = 1, + Rectangle = 2, + Ellipse = 3, + Line = 4, +} + +impl ShapeType { + pub fn name(&self) -> String { + (match self { + Self::Polygon => "Polygon", + Self::Star => "Star", + Self::Rectangle => "Rectangle", + Self::Ellipse => "Ellipse", + Self::Line => "Line", + }) + .into() + } + + pub fn tooltip(&self) -> String { + (match self { + Self::Line => "Line Tool", + Self::Rectangle => "Rectangle Tool", + Self::Ellipse => "Ellipse Tool", + _ => "", + }) + .into() + } + + pub fn icon_name(&self) -> String { + (match self { + Self::Line => "VectorLineTool", + Self::Rectangle => "VectorRectangleTool", + Self::Ellipse => "VectorEllipseTool", + _ => "", + }) + .into() + } + + pub fn tool_type(&self) -> ToolType { + match self { + Self::Line => ToolType::Line, + Self::Rectangle => ToolType::Rectangle, + Self::Ellipse => ToolType::Ellipse, + _ => ToolType::Shape, + } + } +} + +/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side +pub type ShapeToolModifierKey = [Key; 4]; + +pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let sign_num = if end[1] > start[1] { 1. } else { -1. }; + let new_layer = NodeGraphLayer::new(layer, &document.network_interface); + + if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 { + let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(polygon_node_id, 2), + input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false), + }); + return; + } + + if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 { + let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(star_node_id, 2), + input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(star_node_id, 3), + input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false), + }); + } +} + +pub fn transform_cage_overlays(document: &DocumentMessageHandler, tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) { + let mut transform = document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) + .map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface)) + .unwrap_or_default(); + + // Check if the matrix is not invertible + let mut transform_tampered = false; + if transform.matrix2.determinant() == 0. { + transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this? + transform_tampered = true; + } + + let bounds = document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) + .filter_map(|layer| { + document + .metadata() + .bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer)) + }) + .reduce(graphene_std::renderer::Quad::combine_bounds); + + if let Some(bounds) = bounds { + let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default()); + + bounding_box_manager.bounds = bounds; + bounding_box_manager.transform = transform; + bounding_box_manager.transform_tampered = transform_tampered; + bounding_box_manager.render_overlays(overlay_context, true); + } else { + tool_data.bounding_box_manager.take(); + } +} + +pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + let transform = document.metadata().transform_to_viewport(layer); + + overlay_context.outline_vector(&vector_data, transform); + + for (_, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { + overlay_context.manipulator_anchor(transform.transform_point2(position), false, None); + } + } +} + +/// Extract the node input values of Star +pub fn extract_star_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> { + let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?; + + let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(outer)), Some(&TaggedValue::F64(inner))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value()) + else { + return None; + }; + + Some((n, outer, inner)) +} + +/// Extract the node input values of Polygon +pub fn extract_polygon_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(u32, f64)> { + let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?; + + let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(radius))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value()) else { + return None; + }; + + Some((n, radius)) +} + +/// Calculate the viewport position of as a star vertex given its index +pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 { + let angle = ((vertex_index as f64) * PI) / (n as f64); + let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 }; + + viewport.transform_point2(DVec2 { + x: radius * angle.sin(), + y: -radius * angle.cos(), + }) +} + +/// Calculate the viewport position of as a polygon vertex given its index +pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 { + let angle = ((vertex_index as f64) * TAU) / (n as f64); + + viewport.transform_point2(DVec2 { + x: radius * angle.sin(), + y: -radius * angle.cos(), + }) +} + +/// Outlines the geometric shape made by the Star node +pub fn star_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + let mut anchors = Vec::new(); + let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return }; + + let viewport = document.metadata().transform_to_viewport(layer); + for i in 0..2 * n { + let angle = ((i as f64) * PI) / (n as f64); + let radius = if i % 2 == 0 { radius1 } else { radius2 }; + + let point = DVec2 { + x: radius * angle.sin(), + y: -radius * angle.cos(), + }; + + anchors.push(point); + } + + let subpath = [ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))]; + overlay_context.outline(subpath.iter(), viewport, None); +} + +/// Outlines the geometric shape made by the Polygon node +pub fn polygon_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + let mut anchors = Vec::new(); + + let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + for i in 0..2 * n { + let angle = ((i as f64) * TAU) / (n as f64); + + let point = DVec2 { + x: radius * angle.sin(), + y: -radius * angle.cos(), + }; + + anchors.push(point); + } + + let subpath: Vec = vec![ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))]; + + overlay_context.outline(subpath.iter(), viewport, None); +} + +/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications +pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool { + let mut paths = Vec::new(); + + for i in 0..2 * n { + let new_point = dvec2_to_point(star_vertex_position(viewport, i as i32, n, radius1, radius2)); + + if i == 0 { + paths.push(PathEl::MoveTo(new_point)); + } else { + paths.push(PathEl::LineTo(new_point)); + } + } + + paths.push(PathEl::ClosePath); + + let bez_path = BezPath::from_vec(paths); + let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box()); + + if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y { + return false; + } + + let winding = shape.winding(dvec2_to_point(mouse_position)); + + // Non-zero fill rule + winding != 0 +} + +/// Check if the the cursor is inside the geometric polygon shape made by the Polygon node without any upstream node modifications +pub fn inside_polygon(viewport: DAffine2, n: u32, radius: f64, mouse_position: DVec2) -> bool { + let mut paths = Vec::new(); + + for i in 0..n { + let new_point = dvec2_to_point(polygon_vertex_position(viewport, i as i32, n, radius)); + + if i == 0 { + paths.push(PathEl::MoveTo(new_point)); + } else { + paths.push(PathEl::LineTo(new_point)); + } + } + + paths.push(PathEl::ClosePath); + + let bez_path = BezPath::from_vec(paths); + let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box()); + + if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y { + return false; + } + + let winding = shape.winding(dvec2_to_point(mouse_position)); + + // Non-zero fill rule + winding != 0 +} + +pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffine2, angle: f64, overlay_context: &mut OverlayContext) { + for &snapped_radius in snap_radii { + let Some(tick_direction) = direction.perp().try_normalize() else { + return; + }; + + let tick_position = viewport.transform_point2(DVec2 { + x: snapped_radius * angle.sin(), + y: -snapped_radius * angle.cos(), + }); + + overlay_context.line(tick_position, tick_position + tick_direction * 5., None, Some(2.)); + overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.)); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs new file mode 100644 index 0000000000..21069620d8 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs @@ -0,0 +1,78 @@ +use super::shape_utility::{ShapeToolModifierKey, update_radius_sign}; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::*; +use core::f64; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Star; + +impl Star { + pub fn create_node(vertices: u32) -> NodeTemplate { + let node_type = resolve_document_node_type("Star").expect("Star node can't be found"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::U32(vertices), false)), + Some(NodeInput::value(TaggedValue::F64(0.5), false)), + Some(NodeInput::value(TaggedValue::F64(0.25), false)), + ]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) { + // TODO: We need to determine how to allow the polygon node to make irregular shapes + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + + // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly + let mut scale = DVec2::ONE; + let radius: f64; + if dimensions.x > dimensions.y { + scale.x = dimensions.x / dimensions.y; + radius = dimensions.y / 2.; + } else { + scale.y = dimensions.y / dimensions.x; + radius = dimensions.x / 2.; + } + + let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(radius), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 3), + input: NodeInput::value(TaggedValue::F64(radius / 2.), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 7e334fefc5..63319b32a0 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -250,6 +250,10 @@ impl SnapManager { self.update_indicator(snapped); } + pub fn indicator_pos(&self) -> Option { + self.indicator.as_ref().map(|point| point.snapped_point_document) + } + fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, constrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint { let mut snapped_points = Vec::new(); let document = snap_data.document; diff --git a/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs index 486d2532d9..4d5645e539 100644 --- a/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs @@ -70,7 +70,7 @@ impl AlignmentSnapper { if let Some(quad) = target_point.quad.map(|q| q.0) { if quad[0] == quad[3] && quad[1] == quad[2] && quad[0] == target_point.document_point { let [p1, p2, ..] = quad; - let direction = (p2 - p1).normalize(); + let Some(direction) = (p2 - p1).try_normalize() else { return }; let normal = DVec2::new(-direction.y, direction.x); for endpoint in [p1, p2] { diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 9d8554d780..c6974d5009 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -1,9 +1,15 @@ +use super::snapping::{SnapCandidatePoint, SnapData, SnapManager}; +use super::transformation_cage::{BoundingBoxManager, SizeSnapData}; +use crate::consts::ROTATE_INCREMENT; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::get_text; +use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; +use crate::messages::tool::utility_types::ToolType; use bezier_rs::Bezier; -use glam::DVec2; +use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::text::{FontCache, load_face}; use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType}; @@ -198,6 +204,228 @@ pub fn is_visible_point( } } +pub fn resize_bounds( + document: &DocumentMessageHandler, + responses: &mut VecDeque, + bounds: &mut BoundingBoxManager, + dragging_layers: &mut Vec, + snap_manager: &mut SnapManager, + snap_candidates: &mut Vec, + input: &InputPreprocessorMessageHandler, + center: bool, + constrain: bool, + tool: ToolType, +) { + if let Some(movement) = &mut bounds.selected_edges { + let center = center.then_some(bounds.center_of_transformation); + let snap = Some(SizeSnapData { + manager: snap_manager, + points: snap_candidates, + snap_data: SnapData::ignore(document, input, &dragging_layers), + }); + let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); + let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); + + let pivot_transform = DAffine2::from_translation(pivot); + let transformation = pivot_transform * delta * pivot_transform.inverse(); + + dragging_layers.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.document_network().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of layers_dragging"); + false + } + }); + + let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &dragging_layers, responses, &document.network_interface, None, &tool, None); + selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); + } +} + +pub fn rotate_bounds( + document: &DocumentMessageHandler, + responses: &mut VecDeque, + bounds: &mut BoundingBoxManager, + dragging_layers: &mut Vec, + drag_start: DVec2, + mouse_position: DVec2, + snap_angle: bool, + tool: ToolType, +) { + let angle = { + let start_offset = drag_start - bounds.center_of_transformation; + let end_offset = mouse_position - bounds.center_of_transformation; + start_offset.angle_to(end_offset) + }; + + let snapped_angle = if snap_angle { + let snap_resolution = ROTATE_INCREMENT.to_radians(); + (angle / snap_resolution).round() * snap_resolution + } else { + angle + }; + + let delta = DAffine2::from_angle(snapped_angle); + + dragging_layers.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.document_network().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of replacement_selected_layers"); + false + } + }); + + let mut selected = Selected::new( + &mut bounds.original_transforms, + &mut bounds.center_of_transformation, + &dragging_layers, + responses, + &document.network_interface, + None, + &tool, + None, + ); + selected.update_transforms(delta, None, None); +} + +pub fn skew_bounds( + document: &DocumentMessageHandler, + responses: &mut VecDeque, + bounds: &mut BoundingBoxManager, + free_movement: bool, + layers: &mut Vec, + mouse_position: DVec2, + tool: ToolType, +) { + if let Some(movement) = &mut bounds.selected_edges { + let mut pivot = DVec2::ZERO; + + let transformation = movement.skew_transform(mouse_position, bounds.original_bound_transform, free_movement); + + layers.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.document_network().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of layers_dragging"); + false + } + }); + + let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &layers, responses, &document.network_interface, None, &tool, None); + selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); + } +} + +// TODO: Replace returned tuple (where at most 1 element is true at a time) with an enum. +/// Returns the tuple (resize, rotate, skew). +pub fn transforming_transform_cage( + document: &DocumentMessageHandler, + mut bounding_box_manager: &mut Option, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + layers_dragging: &mut Vec, +) -> (bool, bool, bool) { + let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| { + let edges = bounding_box.check_selected_edges(input.mouse.position); + + bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { + let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); + bounding_box.opposite_pivot = selected_edges.calculate_pivot(); + selected_edges + }); + + edges + }); + + let rotating_bounds = bounding_box_manager.as_ref().map(|bounding_box| bounding_box.check_rotate(input.mouse.position)).unwrap_or_default(); + + let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); + + let is_flat_layer = bounding_box_manager.as_ref().map(|bounding_box_manager| bounding_box_manager.transform_tampered).unwrap_or(true); + + if dragging_bounds.is_some() && !is_flat_layer { + responses.add(DocumentMessage::StartTransaction); + + *layers_dragging = selected; + + if let Some(bounds) = &mut bounding_box_manager { + bounds.original_bound_transform = bounds.transform; + + layers_dragging.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.document_network().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of layers_dragging"); + false + } + }); + + let mut selected = Selected::new( + &mut bounds.original_transforms, + &mut bounds.center_of_transformation, + &layers_dragging, + responses, + &document.network_interface, + None, + &ToolType::Select, + None, + ); + bounds.center_of_transformation = selected.mean_average_of_pivots(); + + // Check if we're hovering over a skew triangle + let edges = bounds.check_selected_edges(input.mouse.position); + if let Some(edges) = edges { + let closest_edge = bounds.get_closest_edge(edges, input.mouse.position); + if bounds.check_skew_handle(input.mouse.position, closest_edge) { + // No resize or rotate, just skew + return (false, false, true); + } + } + } + + // Just resize, no rotate or skew + return (true, false, false); + } + + if rotating_bounds { + responses.add(DocumentMessage::StartTransaction); + + if let Some(bounds) = &mut bounding_box_manager { + layers_dragging.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.document_network().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of layers_dragging"); + false + } + }); + + let mut selected = Selected::new( + &mut bounds.original_transforms, + &mut bounds.center_of_transformation, + &selected, + responses, + &document.network_interface, + None, + &ToolType::Select, + None, + ); + + bounds.center_of_transformation = selected.mean_average_of_pivots(); + } + + *layers_dragging = selected; + + // No resize or skew, just rotate + return (false, true, false); + } + + // No resize, rotate, or skew + return (false, false, false); +} + /// Calculates similarity metric between new bezier curve and two old beziers by using sampled points. #[allow(clippy::too_many_arguments)] pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 { diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index 2f90076954..6a5d893e38 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -32,13 +32,7 @@ pub enum ToolMessage { #[child] Spline(SplineToolMessage), #[child] - Line(LineToolMessage), - #[child] - Rectangle(RectangleToolMessage), - #[child] - Ellipse(EllipseToolMessage), - #[child] - Polygon(PolygonToolMessage), + Shape(ShapeToolMessage), #[child] Text(TextToolMessage), @@ -62,7 +56,6 @@ pub enum ToolMessage { ActivateToolArtboard, ActivateToolNavigate, ActivateToolEyedropper, - ActivateToolText, ActivateToolFill, ActivateToolGradient, @@ -70,10 +63,11 @@ pub enum ToolMessage { ActivateToolPen, ActivateToolFreehand, ActivateToolSpline, - ActivateToolLine, - ActivateToolRectangle, - ActivateToolEllipse, - ActivateToolPolygon, + ActivateToolShapeLine, + ActivateToolShapeRectangle, + ActivateToolShapeEllipse, + ActivateToolShape, + ActivateToolText, ActivateToolBrush, // ActivateToolImaginate, diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index f2818f4405..badf9ac5f3 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -1,4 +1,5 @@ use super::common_functionality::shape_editor::ShapeState; +use super::common_functionality::shapes::shape_utility::ShapeType::{self, Ellipse, Line, Rectangle}; use super::utility_types::{ToolActionHandlerData, ToolFsmState, tool_message_to_tool_type}; use crate::application::generate_uuid; use crate::messages::layout::utility_types::widget_prelude::*; @@ -58,21 +59,47 @@ impl MessageHandler> for ToolMessageHandler { ToolMessage::ActivateToolPen => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }), ToolMessage::ActivateToolFreehand => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }), ToolMessage::ActivateToolSpline => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }), - ToolMessage::ActivateToolLine => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }), - ToolMessage::ActivateToolRectangle => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }), - ToolMessage::ActivateToolEllipse => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }), - ToolMessage::ActivateToolPolygon => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Polygon }), - + ToolMessage::ActivateToolShape => { + if self.tool_state.tool_data.active_shape_type.is_some() { + self.tool_state.tool_data.active_shape_type = None; + self.tool_state.tool_data.active_tool_type = ToolType::Shape; + } + responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); + responses.add(ShapeToolMessage::SetShape(ShapeType::Polygon)); + responses.add(ShapeToolMessage::HideShapeTypeWidget(false)) + } ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }), + ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => { + let shape = match message { + ToolMessage::ActivateToolShapeLine => Line, + ToolMessage::ActivateToolShapeRectangle => Rectangle, + ToolMessage::ActivateToolShapeEllipse => Ellipse, + _ => unreachable!(), + }; + + self.tool_state.tool_data.active_shape_type = Some(shape.tool_type()); + responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }); + responses.add(ShapeToolMessage::HideShapeTypeWidget(true)); + responses.add(ShapeToolMessage::SetShape(shape)); + } // ToolMessage::ActivateToolImaginate => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }), ToolMessage::ActivateTool { tool_type } => { let tool_data = &mut self.tool_state.tool_data; - let old_tool = tool_data.active_tool_type; + let old_tool = tool_data.active_tool_type.get_tool(); + let tool_type = tool_type.get_tool(); + + responses.add(ToolMessage::RefreshToolOptions); + tool_data.send_layout(responses, LayoutTarget::ToolShelf); // Do nothing if switching to the same tool if self.tool_is_active && tool_type == old_tool { return; } + + if tool_type != ToolType::Shape { + tool_data.active_shape_type = None; + } + self.tool_is_active = true; // Send the old and new tools a transition to their FSM Abort states @@ -299,7 +326,6 @@ impl MessageHandler> for ToolMessageHandler { ActivateToolArtboard, ActivateToolNavigate, ActivateToolEyedropper, - ActivateToolText, ActivateToolFill, ActivateToolGradient, @@ -307,10 +333,11 @@ impl MessageHandler> for ToolMessageHandler { ActivateToolPen, ActivateToolFreehand, ActivateToolSpline, - ActivateToolLine, - ActivateToolRectangle, - ActivateToolEllipse, - ActivateToolPolygon, + ActivateToolShapeLine, + ActivateToolShapeRectangle, + ActivateToolShapeEllipse, + ActivateToolShape, + ActivateToolText, ActivateToolBrush, // ActivateToolImaginate, diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs deleted file mode 100644 index 0f64ef06e3..0000000000 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ /dev/null @@ -1,444 +0,0 @@ -use super::tool_prelude::*; -use crate::consts::DEFAULT_STROKE_WIDTH; -use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils; -use crate::messages::tool::common_functionality::resize::Resize; -use crate::messages::tool::common_functionality::snapping::SnapData; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Color; - -#[derive(Default)] -pub struct EllipseTool { - fsm_state: EllipseToolFsmState, - data: EllipseToolData, - options: EllipseToolOptions, -} - -pub struct EllipseToolOptions { - line_weight: f64, - fill: ToolColorOptions, - stroke: ToolColorOptions, -} - -impl Default for EllipseToolOptions { - fn default() -> Self { - Self { - line_weight: DEFAULT_STROKE_WIDTH, - fill: ToolColorOptions::new_secondary(), - stroke: ToolColorOptions::new_primary(), - } - } -} - -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum EllipseOptionsUpdate { - FillColor(Option), - FillColorType(ToolColorType), - LineWeight(f64), - StrokeColor(Option), - StrokeColorType(ToolColorType), - WorkingColors(Option, Option), -} - -#[impl_message(Message, ToolMessage, Ellipse)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum EllipseToolMessage { - // Standard messages - Overlays(OverlayContext), - Abort, - WorkingColorChanged, - - // Tool-specific messages - DragStart, - DragStop, - PointerMove { center: Key, lock_ratio: Key }, - PointerOutsideViewport { center: Key, lock_ratio: Key }, - UpdateOptions(EllipseOptionsUpdate), -} - -impl ToolMetadata for EllipseTool { - fn icon_name(&self) -> String { - "VectorEllipseTool".into() - } - fn tooltip(&self) -> String { - "Ellipse Tool".into() - } - fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { - ToolType::Ellipse - } -} - -fn create_weight_widget(line_weight: f64) -> WidgetHolder { - NumberInput::new(Some(line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder() -} - -impl LayoutHolder for EllipseTool { - fn layout(&self) -> Layout { - let mut widgets = self.options.fill.create_widgets( - "Fill", - true, - |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - ); - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.stroke.create_widgets( - "Stroke", - true, - |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - )); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} - -impl<'a> MessageHandler> for EllipseTool { - fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let ToolMessage::Ellipse(EllipseToolMessage::UpdateOptions(action)) = message else { - self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true); - return; - }; - match action { - EllipseOptionsUpdate::FillColor(color) => { - self.options.fill.custom_color = color; - self.options.fill.color_type = ToolColorType::Custom; - } - EllipseOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - EllipseOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - EllipseOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - EllipseOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - EllipseOptionsUpdate::WorkingColors(primary, secondary) => { - self.options.stroke.primary_working_color = primary; - self.options.stroke.secondary_working_color = secondary; - self.options.fill.primary_working_color = primary; - self.options.fill.secondary_working_color = secondary; - } - } - - self.send_layout(responses, LayoutTarget::ToolOptions); - } - - fn actions(&self) -> ActionList { - match self.fsm_state { - EllipseToolFsmState::Ready => actions!(EllipseToolMessageDiscriminant; - DragStart, - PointerMove, - ), - EllipseToolFsmState::Drawing => actions!(EllipseToolMessageDiscriminant; - DragStop, - Abort, - PointerMove, - ), - } - } -} - -impl ToolTransition for EllipseTool { - fn event_to_message_map(&self) -> EventToMessageMap { - EventToMessageMap { - overlay_provider: Some(|overlay_context| EllipseToolMessage::Overlays(overlay_context).into()), - tool_abort: Some(EllipseToolMessage::Abort.into()), - working_color_changed: Some(EllipseToolMessage::WorkingColorChanged.into()), - ..Default::default() - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum EllipseToolFsmState { - #[default] - Ready, - Drawing, -} - -#[derive(Clone, Debug, Default)] -struct EllipseToolData { - data: Resize, - auto_panning: AutoPanning, -} - -impl Fsm for EllipseToolFsmState { - type ToolData = EllipseToolData; - type ToolOptions = EllipseToolOptions; - - fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { - let ToolActionHandlerData { - document, global_tool_data, input, .. - } = tool_action_data; - - let shape_data = &mut tool_data.data; - - let ToolMessage::Ellipse(event) = event else { return self }; - match (self, event) { - (_, EllipseToolMessage::Overlays(mut overlay_context)) => { - shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); - self - } - (EllipseToolFsmState::Ready, EllipseToolMessage::DragStart) => { - shape_data.start(document, input); - responses.add(DocumentMessage::StartTransaction); - - // Create a new ellipse vector shape - let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist"); - let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]); - let nodes = vec![(NodeId(0), node)]; - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses); - responses.add(Message::StartBuffer); - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - shape_data.layer = Some(layer); - - EllipseToolFsmState::Drawing - } - (EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => { - if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) { - if let Some(layer) = shape_data.layer { - let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else { - return self; - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false), - }); - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_translation((start + end) / 2.), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - } - } - - // Auto-panning - let messages = [ - EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - EllipseToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - - self - } - (_, EllipseToolMessage::PointerMove { .. }) => { - shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); - responses.add(OverlaysMessage::Draw); - self - } - (EllipseToolFsmState::Drawing, EllipseToolMessage::PointerOutsideViewport { .. }) => { - // Auto-panning - let _ = tool_data.auto_panning.shift_viewport(input, responses); - - EllipseToolFsmState::Drawing - } - (state, EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }) => { - // Auto-panning - let messages = [ - EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - EllipseToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.stop(&messages, responses); - - state - } - (EllipseToolFsmState::Drawing, EllipseToolMessage::DragStop) => { - input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses); - shape_data.cleanup(responses); - - EllipseToolFsmState::Ready - } - (EllipseToolFsmState::Drawing, EllipseToolMessage::Abort) => { - responses.add(DocumentMessage::AbortTransaction); - shape_data.cleanup(responses); - - EllipseToolFsmState::Ready - } - (_, EllipseToolMessage::WorkingColorChanged) => { - responses.add(EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::WorkingColors( - Some(global_tool_data.primary_color), - Some(global_tool_data.secondary_color), - ))); - self - } - _ => self, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), - HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])]), - EllipseToolFsmState::Drawing => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), - ]), - }; - - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } -} - -#[cfg(test)] -mod test_ellipse { - pub use crate::test_utils::test_prelude::*; - use glam::DAffine2; - use graphene_std::vector::generator_nodes::ellipse; - - #[derive(Debug, PartialEq)] - struct ResolvedEllipse { - radius_x: f64, - radius_y: f64, - transform: DAffine2, - } - - async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec { - let instrumented = match editor.eval_graph().await { - Ok(instrumented) => instrumented, - Err(e) => panic!("Failed to evaluate graph: {e}"), - }; - - let document = editor.active_document(); - let layers = document.metadata().all_layers(); - layers - .filter_map(|layer| { - let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface); - let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?; - Some(ResolvedEllipse { - radius_x: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), - radius_y: instrumented.grab_protonode_input::(&vec![ellipse_node], &editor.runtime).unwrap(), - transform: document.metadata().transform_to_document(layer), - }) - }) - .collect() - } - - #[tokio::test] - async fn ellipse_draw_simple() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await; - - assert_eq!(editor.active_document().metadata().all_layers().count(), 1); - - let ellipse = get_ellipse(&mut editor).await; - assert_eq!(ellipse.len(), 1); - assert_eq!( - ellipse[0], - ResolvedEllipse { - radius_x: 4.5, - radius_y: 5., - transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center - } - ); - } - - #[tokio::test] - async fn ellipse_draw_circle() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await; - - let ellipse = get_ellipse(&mut editor).await; - assert_eq!(ellipse.len(), 1); - assert_eq!( - ellipse[0], - ResolvedEllipse { - radius_x: 10., - radius_y: 10., - transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center - } - ); - } - - #[tokio::test] - async fn ellipse_draw_square_rotated() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor - .handle_message(NavigationMessage::CanvasTiltSet { - // 45 degree rotation of content clockwise - angle_radians: f64::consts::FRAC_PI_4, - }) - .await; - editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates - - let ellipse = get_ellipse(&mut editor).await; - assert_eq!(ellipse.len(), 1); - println!("{ellipse:?}"); - 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) - ); - } - - #[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); - 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)); - } - - #[tokio::test] - async fn ellipse_cancel() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool_cancel_rmb(ToolType::Ellipse).await; - - let ellipse = get_ellipse(&mut editor).await; - assert_eq!(ellipse.len(), 0); - } -} diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 519db67d96..6265c64b1e 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -535,7 +535,8 @@ impl Fsm for GradientToolFsmState { mod test_gradient { use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState; use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta; - use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, utility_types::misc::GroupFolderType}; + use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; + use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; pub use crate::test_utils::test_prelude::*; use glam::DAffine2; use graphene_std::vector::fill; diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs deleted file mode 100644 index 0200f420d5..0000000000 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ /dev/null @@ -1,624 +0,0 @@ -use super::tool_prelude::*; -use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, LINE_ROTATE_SNAP_ANGLE}; -use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; -use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Color; - -#[derive(Default)] -pub struct LineTool { - fsm_state: LineToolFsmState, - tool_data: LineToolData, - options: LineOptions, -} - -pub struct LineOptions { - line_weight: f64, - stroke: ToolColorOptions, -} - -impl Default for LineOptions { - fn default() -> Self { - Self { - line_weight: DEFAULT_STROKE_WIDTH, - stroke: ToolColorOptions::new_primary(), - } - } -} - -#[impl_message(Message, ToolMessage, Line)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum LineToolMessage { - // Standard messages - Overlays(OverlayContext), - Abort, - WorkingColorChanged, - - // Tool-specific messages - DragStart, - DragStop, - PointerMove { center: Key, lock_angle: Key, snap_angle: Key }, - PointerOutsideViewport { center: Key, lock_angle: Key, snap_angle: Key }, - UpdateOptions(LineOptionsUpdate), -} - -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum LineOptionsUpdate { - LineWeight(f64), - StrokeColor(Option), - StrokeColorType(ToolColorType), - WorkingColors(Option, Option), -} - -impl ToolMetadata for LineTool { - fn icon_name(&self) -> String { - "VectorLineTool".into() - } - fn tooltip(&self) -> String { - "Line Tool".into() - } - fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { - ToolType::Line - } -} - -fn create_weight_widget(line_weight: f64) -> WidgetHolder { - NumberInput::new(Some(line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder() -} - -impl LayoutHolder for LineTool { - fn layout(&self) -> Layout { - let mut widgets = self.options.stroke.create_widgets( - "Stroke", - true, - |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - ); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} - -impl<'a> MessageHandler> for LineTool { - fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message else { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); - return; - }; - match action { - LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - LineOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - LineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - LineOptionsUpdate::WorkingColors(primary, secondary) => { - self.options.stroke.primary_working_color = primary; - self.options.stroke.secondary_working_color = secondary; - } - } - - self.send_layout(responses, LayoutTarget::ToolOptions); - } - - fn actions(&self) -> ActionList { - match self.fsm_state { - LineToolFsmState::Ready => actions!(LineToolMessageDiscriminant; DragStart, PointerMove), - LineToolFsmState::Drawing => actions!(LineToolMessageDiscriminant; DragStop, PointerMove, Abort), - } - } -} - -impl ToolTransition for LineTool { - fn event_to_message_map(&self) -> EventToMessageMap { - EventToMessageMap { - overlay_provider: Some(|overlay_context| LineToolMessage::Overlays(overlay_context).into()), - tool_abort: Some(LineToolMessage::Abort.into()), - working_color_changed: Some(LineToolMessage::WorkingColorChanged.into()), - ..Default::default() - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -enum LineToolFsmState { - #[default] - Ready, - Drawing, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum LineEnd { - Start, - End, -} - -#[derive(Clone, Debug, Default)] -struct LineToolData { - drag_begin: DVec2, - drag_start_shifted: DVec2, - drag_current_shifted: DVec2, - drag_start: DVec2, - drag_current: DVec2, - angle: f64, - weight: f64, - selected_layers_with_position: HashMap, - editing_layer: Option, - snap_manager: SnapManager, - auto_panning: AutoPanning, - dragging_endpoint: Option, -} - -impl Fsm for LineToolFsmState { - type ToolData = LineToolData; - type ToolOptions = LineOptions; - - fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { - let ToolActionHandlerData { - document, global_tool_data, input, .. - } = tool_action_data; - - let ToolMessage::Line(event) = event else { return self }; - match (self, event) { - (_, LineToolMessage::Overlays(mut overlay_context)) => { - tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); - - tool_data.selected_layers_with_position = document - .network_interface - .selected_nodes() - .selected_visible_and_unlocked_layers(&document.network_interface) - .filter_map(|layer| { - let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?; - - let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else { - return None; - }; - - let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point)); - if !start.abs_diff_eq(end, f64::EPSILON * 1000.) { - overlay_context.line(viewport_start, viewport_end, None, None); - overlay_context.square(viewport_start, Some(6.), None, None); - overlay_context.square(viewport_end, Some(6.), None, None); - } - - Some((layer, [start, end])) - }) - .collect::>(); - - self - } - (LineToolFsmState::Ready, LineToolMessage::DragStart) => { - let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().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_begin = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start); - - responses.add(DocumentMessage::StartTransaction); - - for (layer, [document_start, document_end]) in tool_data.selected_layers_with_position.iter() { - let transform = document.metadata().transform_to_viewport(*layer); - let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD; - let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD; - let threshold_x = transform.inverse().transform_vector2(viewport_x).length(); - let threshold_y = transform.inverse().transform_vector2(viewport_y).length(); - - let drag_start = input.mouse.position; - let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(*point)); - - let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x; - let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x; - - if start_click || end_click { - tool_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start }); - tool_data.drag_start = if end_click { *document_start } else { *document_end }; - tool_data.editing_layer = Some(*layer); - return LineToolFsmState::Drawing; - } - } - - let node_type = resolve_document_node_type("Line").expect("Line node does not exist"); - let node = node_type.node_template_input_override([ - None, - Some(NodeInput::value( - TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)), - false, - )), - Some(NodeInput::value( - TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)), - false, - )), - ]); - let nodes = vec![(NodeId(0), node)]; - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses); - responses.add(Message::StartBuffer); - - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - - tool_data.editing_layer = Some(layer); - tool_data.angle = 0.; - tool_data.weight = tool_options.line_weight; - - LineToolFsmState::Drawing - } - (LineToolFsmState::Drawing, LineToolMessage::PointerMove { center, snap_angle, lock_angle }) => { - let Some(layer) = tool_data.editing_layer else { return LineToolFsmState::Ready }; - - tool_data.drag_current_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(input.mouse.position); - tool_data.drag_current = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); - tool_data.drag_start_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(tool_data.drag_begin); - - let keyboard = &input.keyboard; - let ignore = vec![layer]; - let snap_data = SnapData::ignore(document, input, &ignore); - let mut document_points = generate_line(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center)); - - if tool_data.dragging_endpoint == Some(LineEnd::Start) { - document_points.swap(0, 1); - } - - let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else { - return LineToolFsmState::Ready; - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - - // Auto-panning - let messages = [ - LineToolMessage::PointerOutsideViewport { center, snap_angle, lock_angle }.into(), - LineToolMessage::PointerMove { center, snap_angle, lock_angle }.into(), - ]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - - LineToolFsmState::Drawing - } - (_, LineToolMessage::PointerMove { .. }) => { - tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); - responses.add(OverlaysMessage::Draw); - self - } - (LineToolFsmState::Drawing, LineToolMessage::PointerOutsideViewport { .. }) => { - // Auto-panning - let _ = tool_data.auto_panning.shift_viewport(input, responses); - - LineToolFsmState::Drawing - } - (state, LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }) => { - // Auto-panning - let messages = [ - LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }.into(), - LineToolMessage::PointerMove { center, lock_angle, snap_angle }.into(), - ]; - tool_data.auto_panning.stop(&messages, responses); - - state - } - (LineToolFsmState::Drawing, LineToolMessage::DragStop) => { - tool_data.snap_manager.cleanup(responses); - tool_data.editing_layer.take(); - input.mouse.finish_transaction(tool_data.drag_start, responses); - LineToolFsmState::Ready - } - (LineToolFsmState::Drawing, LineToolMessage::Abort) => { - tool_data.snap_manager.cleanup(responses); - tool_data.editing_layer.take(); - responses.add(DocumentMessage::AbortTransaction); - LineToolFsmState::Ready - } - (_, LineToolMessage::WorkingColorChanged) => { - responses.add(LineToolMessage::UpdateOptions(LineOptionsUpdate::WorkingColors( - Some(global_tool_data.primary_color), - Some(global_tool_data.secondary_color), - ))); - self - } - _ => self, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - LineToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), - HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), - ])]), - LineToolFsmState::Drawing => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![ - HintInfo::keys([Key::Shift], "15° Increments"), - HintInfo::keys([Key::Alt], "From Center"), - HintInfo::keys([Key::Control], "Lock Angle"), - ]), - ]), - }; - - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } -} - -fn generate_line(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] { - let shift = tool_data.drag_current_shifted - tool_data.drag_current; - let mut document_points = [tool_data.drag_start, tool_data.drag_current]; - - let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X); - let mut line_length = (document_points[1] - document_points[0]).length(); - - if lock_angle { - angle = tool_data.angle; - } else if snap_angle { - let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); - angle = (angle / snap_resolution).round() * snap_resolution; - } - - tool_data.angle = angle; - - let angle_vec = DVec2::from_angle(angle); - if lock_angle { - line_length = (document_points[1] - document_points[0]).dot(angle_vec); - } - - document_points[1] = document_points[0] + line_length * angle_vec; - - let constrained = snap_angle || lock_angle; - let snap = &mut tool_data.snap_manager; - - let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.drag_start]); - let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.drag_start]); - let mid_point = SnapCandidatePoint::handle_neighbors((tool_data.drag_start + document_points[1]) / 2., [tool_data.drag_start]); - let config = SnapTypeConfiguration::default(); - - if constrained { - let constraint = SnapConstraint::Line { - origin: document_points[0], - direction: document_points[1] - document_points[0], - }; - if center { - let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); - let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config); - let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; - document_points[1] = document_points[0] * 2. - best.snapped_point_document; - document_points[0] = best.snapped_point_document; - snap.update_indicator(best); - } else { - let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); - let snapped_mid = snap.constrained_snap(&snap_data, &mid_point, constraint, config); - let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) { - document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.; - snapped_mid - } else { - document_points[1] = snapped.snapped_point_document; - snapped.clone() - }; - snap.update_indicator(best); - } - } else if center { - let snapped = snap.free_snap(&snap_data, &near_point, config); - let snapped_far = snap.free_snap(&snap_data, &far_point, config); - let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; - document_points[1] = document_points[0] * 2. - best.snapped_point_document; - document_points[0] = best.snapped_point_document; - snap.update_indicator(best); - } else { - let snapped = snap.free_snap(&snap_data, &near_point, config); - let snapped_mid = snap.free_snap(&snap_data, &mid_point, config); - let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) { - document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.; - snapped_mid - } else { - document_points[1] = snapped.snapped_point_document; - snapped.clone() - }; - snap.update_indicator(best); - } - - // Snapping happens in other space, while document graph renders in another. - document_points.map(|vector| vector + shift) -} - -#[cfg(test)] -mod test_line_tool { - use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; - use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; - use crate::test_utils::test_prelude::*; - use glam::DAffine2; - use graph_craft::document::value::TaggedValue; - - async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> { - let document = editor.active_document(); - let network_interface = &document.network_interface; - let node_id = network_interface - .selected_nodes() - .selected_visible_and_unlocked_layers(network_interface) - .filter_map(|layer| { - let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?; - let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else { - return None; - }; - Some((start, end)) - }) - .next(); - node_id - } - - #[tokio::test] - async fn test_line_tool_basicdraw() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await; - if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { - match (start_input, end_input) { - (start_input, end_input) => { - assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)"); - assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)"); - } - } - } - } - - #[tokio::test] - async fn test_line_tool_with_transformed_viewport() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await; - editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await; - editor - .handle_message(NavigationMessage::CanvasTiltSet { - angle_radians: (30. as f64).to_radians(), - }) - .await; - editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await; - if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { - let document = editor.active_document(); - let document_to_viewport = document.metadata().document_to_viewport; - let viewport_to_document = document_to_viewport.inverse(); - - let expected_start = viewport_to_document.transform_point2(DVec2::ZERO); - let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.)); - - assert!( - (start_input - expected_start).length() < 1., - "Start point should match expected document coordinates. Got {:?}, expected {:?}", - start_input, - expected_start - ); - assert!( - (end_input - expected_end).length() < 1., - "End point should match expected document coordinates. Got {:?}, expected {:?}", - end_input, - expected_end - ); - } else { - panic!("Line was not created successfully with transformed viewport"); - } - } - - #[tokio::test] - async fn test_line_tool_ctrl_anglelock() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await; - if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { - match (start_input, end_input) { - (start_input, end_input) => { - let line_vec = end_input - start_input; - let original_angle = line_vec.angle_to(DVec2::X); - editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await; - if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await { - match (updated_start, updated_end) { - (updated_start, updated_end) => { - let updated_line_vec = updated_end - updated_start; - let updated_angle = updated_line_vec.angle_to(DVec2::X); - assert!((original_angle - updated_angle).abs() < 0.1, "Line angle should be locked when Ctrl is kept pressed"); - assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed"); - } - } - } - } - } - } - } - - #[tokio::test] - async fn test_line_tool_alt() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await; - if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { - match (start_input, end_input) { - (start_input, end_input) => { - let expected_start = DVec2::new(0., 100.); - let expected_end = DVec2::new(200., 100.); - assert!((start_input - expected_start).length() < 1., "start point should be near (0,100)"); - assert!((end_input - expected_end).length() < 1., "end point should be near (200,100)"); - } - } - } - } - - #[tokio::test] - async fn test_line_tool_alt_shift_drag() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await; - if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await { - match (start_input, end_input) { - (start_input, end_input) => { - let line_vec = end_input - start_input; - let angle_radians = line_vec.angle_to(DVec2::X); - let angle_degrees = angle_radians.to_degrees(); - let nearest_angle = (angle_degrees / 15.).round() * 15.; - - assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees"); - } - } - } - } - - #[tokio::test] - async fn test_line_tool_with_transformed_artboard() { - let mut editor = EditorTestUtils::create(); - editor.new_document().await; - editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await; - - let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard"); - - editor - .handle_message(GraphOperationMessage::TransformChange { - layer: artboard_id, - transform: DAffine2::from_angle(45.0_f64.to_radians()), - transform_in: TransformIn::Local, - skip_rerender: false, - }) - .await; - - editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await; - - let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard"); - // The line should still be diagonal with equal change in x and y - let line_vector = end_input - start_input; - // Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square) - let line_length = line_vector.length(); - assert!( - (line_length - 141.42).abs() < 1.0, // 100 * sqrt(2) ~= 141.42 - "Line length should be approximately 141.42 units. Got: {line_length}" - ); - assert!((line_vector.x - 100.0).abs() < 1.0, "X-component of line vector should be approximately 100. Got: {}", line_vector.x); - assert!( - (line_vector.y.abs() - 100.0).abs() < 1.0, - "Absolute Y-component of line vector should be approximately 100. Got: {}", - line_vector.y.abs() - ); - let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees(); - assert!((angle_degrees - (-45.0)).abs() < 1.0, "Line angle should be close to -45 degrees. Got: {angle_degrees}"); - } -} diff --git a/editor/src/messages/tool/tool_messages/mod.rs b/editor/src/messages/tool/tool_messages/mod.rs index 65ac871ab2..b11a20ec09 100644 --- a/editor/src/messages/tool/tool_messages/mod.rs +++ b/editor/src/messages/tool/tool_messages/mod.rs @@ -1,18 +1,15 @@ pub mod artboard_tool; pub mod brush_tool; -pub mod ellipse_tool; pub mod eyedropper_tool; pub mod fill_tool; pub mod freehand_tool; pub mod gradient_tool; // pub mod imaginate_tool; -pub mod line_tool; pub mod navigate_tool; pub mod path_tool; pub mod pen_tool; -pub mod polygon_tool; -pub mod rectangle_tool; pub mod select_tool; +pub mod shape_tool; pub mod spline_tool; pub mod text_tool; diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs deleted file mode 100644 index 5b5b617095..0000000000 --- a/editor/src/messages/tool/tool_messages/polygon_tool.rs +++ /dev/null @@ -1,436 +0,0 @@ -use super::tool_prelude::*; -use crate::consts::DEFAULT_STROKE_WIDTH; -use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; -use crate::messages::tool::common_functionality::resize::Resize; -use crate::messages::tool::common_functionality::snapping::SnapData; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Color; - -#[derive(Default)] -pub struct PolygonTool { - fsm_state: PolygonToolFsmState, - tool_data: PolygonToolData, - options: PolygonOptions, -} - -pub struct PolygonOptions { - line_weight: f64, - fill: ToolColorOptions, - stroke: ToolColorOptions, - vertices: u32, - polygon_type: PolygonType, -} - -impl Default for PolygonOptions { - fn default() -> Self { - Self { - vertices: 5, - line_weight: DEFAULT_STROKE_WIDTH, - fill: ToolColorOptions::new_secondary(), - stroke: ToolColorOptions::new_primary(), - polygon_type: PolygonType::Convex, - } - } -} - -#[impl_message(Message, ToolMessage, Polygon)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum PolygonToolMessage { - // Standard messages - Overlays(OverlayContext), - Abort, - WorkingColorChanged, - - // Tool-specific messages - DragStart, - DragStop, - PointerMove { center: Key, lock_ratio: Key }, - PointerOutsideViewport { center: Key, lock_ratio: Key }, - UpdateOptions(PolygonOptionsUpdate), -} - -#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum PolygonType { - Convex = 0, - Star = 1, -} - -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum PolygonOptionsUpdate { - FillColor(Option), - FillColorType(ToolColorType), - LineWeight(f64), - PolygonType(PolygonType), - StrokeColor(Option), - StrokeColorType(ToolColorType), - Vertices(u32), - WorkingColors(Option, Option), -} - -impl ToolMetadata for PolygonTool { - fn icon_name(&self) -> String { - "VectorPolygonTool".into() - } - fn tooltip(&self) -> String { - "Polygon Tool".into() - } - fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { - ToolType::Polygon - } -} - -fn create_sides_widget(vertices: u32) -> WidgetHolder { - NumberInput::new(Some(vertices as f64)) - .label("Sides") - .int() - .min(3.) - .max(1000.) - .mode(NumberInputMode::Increment) - .on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) - .widget_holder() -} - -fn create_star_option_widget(polygon_type: PolygonType) -> WidgetHolder { - let entries = vec![ - RadioEntryData::new("convex") - .label("Convex") - .on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Convex)).into()), - RadioEntryData::new("star") - .label("Star") - .on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Star)).into()), - ]; - RadioInput::new(entries).selected_index(Some(polygon_type as u32)).widget_holder() -} - -fn create_weight_widget(line_weight: f64) -> WidgetHolder { - NumberInput::new(Some(line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder() -} - -impl LayoutHolder for PolygonTool { - fn layout(&self) -> Layout { - let mut widgets = vec![ - create_star_option_widget(self.options.polygon_type), - Separator::new(SeparatorType::Related).widget_holder(), - create_sides_widget(self.options.vertices), - ]; - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.fill.create_widgets( - "Fill", - true, - |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - )); - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.stroke.create_widgets( - "Stroke", - true, - |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - )); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} -impl<'a> MessageHandler> for PolygonTool { - fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let ToolMessage::Polygon(PolygonToolMessage::UpdateOptions(action)) = message else { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); - return; - }; - match action { - PolygonOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices, - PolygonOptionsUpdate::PolygonType(polygon_type) => self.options.polygon_type = polygon_type, - PolygonOptionsUpdate::FillColor(color) => { - self.options.fill.custom_color = color; - self.options.fill.color_type = ToolColorType::Custom; - } - PolygonOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - PolygonOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - PolygonOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - PolygonOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - PolygonOptionsUpdate::WorkingColors(primary, secondary) => { - self.options.stroke.primary_working_color = primary; - self.options.stroke.secondary_working_color = secondary; - self.options.fill.primary_working_color = primary; - self.options.fill.secondary_working_color = secondary; - } - } - - self.send_layout(responses, LayoutTarget::ToolOptions); - } - - fn actions(&self) -> ActionList { - match self.fsm_state { - PolygonToolFsmState::Ready => actions!(PolygonToolMessageDiscriminant; - DragStart, - PointerMove, - ), - PolygonToolFsmState::Drawing => actions!(PolygonToolMessageDiscriminant; - DragStop, - Abort, - PointerMove, - ), - } - } -} - -impl ToolTransition for PolygonTool { - fn event_to_message_map(&self) -> EventToMessageMap { - EventToMessageMap { - overlay_provider: Some(|overlay_context| PolygonToolMessage::Overlays(overlay_context).into()), - tool_abort: Some(PolygonToolMessage::Abort.into()), - working_color_changed: Some(PolygonToolMessage::WorkingColorChanged.into()), - ..Default::default() - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum PolygonToolFsmState { - #[default] - Ready, - Drawing, -} - -#[derive(Clone, Debug, Default)] -struct PolygonToolData { - data: Resize, - auto_panning: AutoPanning, -} - -impl Fsm for PolygonToolFsmState { - type ToolData = PolygonToolData; - type ToolOptions = PolygonOptions; - - fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { - let ToolActionHandlerData { - document, global_tool_data, input, .. - } = tool_action_data; - - let polygon_data = &mut tool_data.data; - - let ToolMessage::Polygon(event) = event else { return self }; - match (self, event) { - (_, PolygonToolMessage::Overlays(mut overlay_context)) => { - polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); - self - } - (PolygonToolFsmState::Ready, PolygonToolMessage::DragStart) => { - polygon_data.start(document, input); - responses.add(DocumentMessage::StartTransaction); - - let node = match tool_options.polygon_type { - PolygonType::Convex => resolve_document_node_type("Regular Polygon") - .expect("Regular Polygon node does not exist") - .node_template_input_override([ - None, - Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)), - Some(NodeInput::value(TaggedValue::F64(0.5), false)), - ]), - PolygonType::Star => resolve_document_node_type("Star").expect("Star node does not exist").node_template_input_override([ - None, - Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)), - Some(NodeInput::value(TaggedValue::F64(0.5), false)), - Some(NodeInput::value(TaggedValue::F64(0.25), false)), - ]), - }; - - let nodes = vec![(NodeId(0), node)]; - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses); - responses.add(Message::StartBuffer); - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - polygon_data.layer = Some(layer); - - PolygonToolFsmState::Drawing - } - (PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => { - if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) { - if let Some(layer) = tool_data.data.layer { - // TODO: We need to determine how to allow the polygon node to make irregular shapes - update_radius_sign(end, start, layer, document, responses); - - let dimensions = (start - end).abs(); - let mut scale = DVec2::ONE; - let radius: f64; - - // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly - if dimensions.x > dimensions.y { - scale.x = dimensions.x / dimensions.y; - radius = dimensions.y / 2.; - } else { - scale.y = dimensions.y / dimensions.x; - radius = dimensions.x / 2.; - } - - match tool_options.polygon_type { - PolygonType::Convex => { - let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else { - return self; - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(radius), false), - }); - } - PolygonType::Star => { - let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else { - return self; - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(radius), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 3), - input: NodeInput::value(TaggedValue::F64(radius / 2.), false), - }); - } - } - - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - } - } - - // Auto-panning - let messages = [ - PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - PolygonToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - - self - } - (_, PolygonToolMessage::PointerMove { .. }) => { - polygon_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); - responses.add(OverlaysMessage::Draw); - self - } - (PolygonToolFsmState::Drawing, PolygonToolMessage::PointerOutsideViewport { .. }) => { - // Auto-panning - let _ = tool_data.auto_panning.shift_viewport(input, responses); - - PolygonToolFsmState::Drawing - } - (state, PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }) => { - // Auto-panning - let messages = [ - PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - PolygonToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.stop(&messages, responses); - - state - } - (PolygonToolFsmState::Drawing, PolygonToolMessage::DragStop) => { - input.mouse.finish_transaction(polygon_data.viewport_drag_start(document), responses); - polygon_data.cleanup(responses); - - PolygonToolFsmState::Ready - } - (PolygonToolFsmState::Drawing, PolygonToolMessage::Abort) => { - responses.add(DocumentMessage::AbortTransaction); - - polygon_data.cleanup(responses); - - PolygonToolFsmState::Ready - } - (_, PolygonToolMessage::WorkingColorChanged) => { - responses.add(PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::WorkingColors( - Some(global_tool_data.primary_color), - Some(global_tool_data.secondary_color), - ))); - self - } - _ => self, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - PolygonToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), - HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])]), - PolygonToolFsmState::Drawing => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), - ]), - }; - - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } -} - -/// In the case where the polygon/star is upside down and the number of sides is odd, we negate the radius instead of using a negative scale. -fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &mut DocumentMessageHandler, responses: &mut VecDeque) { - let sign_num = if end[1] > start[1] { 1. } else { -1. }; - let new_layer = NodeGraphLayer::new(layer, &document.network_interface); - - if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 { - let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(polygon_node_id, 2), - input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false), - }); - return; - } - - if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 { - let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(star_node_id, 2), - input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(star_node_id, 3), - input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false), - }); - } -} diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs deleted file mode 100644 index 3092e9ceef..0000000000 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ /dev/null @@ -1,323 +0,0 @@ -use super::tool_prelude::*; -use crate::consts::DEFAULT_STROKE_WIDTH; -use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils; -use crate::messages::tool::common_functionality::resize::Resize; -use crate::messages::tool::common_functionality::snapping::SnapData; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Color; - -#[derive(Default)] -pub struct RectangleTool { - fsm_state: RectangleToolFsmState, - tool_data: RectangleToolData, - options: RectangleToolOptions, -} - -pub struct RectangleToolOptions { - line_weight: f64, - fill: ToolColorOptions, - stroke: ToolColorOptions, -} - -impl Default for RectangleToolOptions { - fn default() -> Self { - Self { - line_weight: DEFAULT_STROKE_WIDTH, - fill: ToolColorOptions::new_secondary(), - stroke: ToolColorOptions::new_primary(), - } - } -} - -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum RectangleOptionsUpdate { - FillColor(Option), - FillColorType(ToolColorType), - LineWeight(f64), - StrokeColor(Option), - StrokeColorType(ToolColorType), - WorkingColors(Option, Option), -} - -#[impl_message(Message, ToolMessage, Rectangle)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum RectangleToolMessage { - // Standard messages - Overlays(OverlayContext), - Abort, - WorkingColorChanged, - - // Tool-specific messages - DragStart, - DragStop, - PointerMove { center: Key, lock_ratio: Key }, - PointerOutsideViewport { center: Key, lock_ratio: Key }, - UpdateOptions(RectangleOptionsUpdate), -} - -fn create_weight_widget(line_weight: f64) -> WidgetHolder { - NumberInput::new(Some(line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder() -} - -impl LayoutHolder for RectangleTool { - fn layout(&self) -> Layout { - let mut widgets = self.options.fill.create_widgets( - "Fill", - true, - |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - ); - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.stroke.create_widgets( - "Stroke", - true, - |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), - )); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} - -impl<'a> MessageHandler> for RectangleTool { - fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let ToolMessage::Rectangle(RectangleToolMessage::UpdateOptions(action)) = message else { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); - return; - }; - match action { - RectangleOptionsUpdate::FillColor(color) => { - self.options.fill.custom_color = color; - self.options.fill.color_type = ToolColorType::Custom; - } - RectangleOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - RectangleOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - RectangleOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - RectangleOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - RectangleOptionsUpdate::WorkingColors(primary, secondary) => { - self.options.stroke.primary_working_color = primary; - self.options.stroke.secondary_working_color = secondary; - self.options.fill.primary_working_color = primary; - self.options.fill.secondary_working_color = secondary; - } - } - - self.send_layout(responses, LayoutTarget::ToolOptions); - } - - fn actions(&self) -> ActionList { - match self.fsm_state { - RectangleToolFsmState::Ready => actions!(RectangleToolMessageDiscriminant; - DragStart, - PointerMove, - ), - RectangleToolFsmState::Drawing => actions!(RectangleToolMessageDiscriminant; - DragStop, - Abort, - PointerMove, - ), - } - } -} - -impl ToolMetadata for RectangleTool { - fn icon_name(&self) -> String { - "VectorRectangleTool".into() - } - fn tooltip(&self) -> String { - "Rectangle Tool".into() - } - fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { - ToolType::Rectangle - } -} - -impl ToolTransition for RectangleTool { - fn event_to_message_map(&self) -> EventToMessageMap { - EventToMessageMap { - overlay_provider: Some(|overlay_context| RectangleToolMessage::Overlays(overlay_context).into()), - tool_abort: Some(RectangleToolMessage::Abort.into()), - working_color_changed: Some(RectangleToolMessage::WorkingColorChanged.into()), - ..Default::default() - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum RectangleToolFsmState { - #[default] - Ready, - Drawing, -} - -#[derive(Clone, Debug, Default)] -struct RectangleToolData { - data: Resize, - auto_panning: AutoPanning, -} - -impl Fsm for RectangleToolFsmState { - type ToolData = RectangleToolData; - type ToolOptions = RectangleToolOptions; - - fn transition( - self, - event: ToolMessage, - tool_data: &mut Self::ToolData, - ToolActionHandlerData { - document, global_tool_data, input, .. - }: &mut ToolActionHandlerData, - tool_options: &Self::ToolOptions, - responses: &mut VecDeque, - ) -> Self { - let shape_data = &mut tool_data.data; - - let ToolMessage::Rectangle(event) = event else { return self }; - match (self, event) { - (_, RectangleToolMessage::Overlays(mut overlay_context)) => { - shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); - self - } - (RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => { - shape_data.start(document, input); - - responses.add(DocumentMessage::StartTransaction); - - let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node does not exist"); - let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))]); - let nodes = vec![(NodeId(0), node)]; - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses); - responses.add(Message::StartBuffer); - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - shape_data.layer = Some(layer); - - RectangleToolFsmState::Drawing - } - (RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => { - if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) { - if let Some(layer) = shape_data.layer { - let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else { - return self; - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false), - }); - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: DAffine2::from_translation((start + end) / 2.), - transform_in: TransformIn::Viewport, - skip_rerender: false, - }); - } - } - - // Auto-panning - let messages = [ - RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - RectangleToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - - self - } - (_, RectangleToolMessage::PointerMove { .. }) => { - shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); - responses.add(OverlaysMessage::Draw); - self - } - (RectangleToolFsmState::Drawing, RectangleToolMessage::PointerOutsideViewport { .. }) => { - // Auto-panning - let _ = tool_data.auto_panning.shift_viewport(input, responses); - - RectangleToolFsmState::Drawing - } - (state, RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }) => { - // Auto-panning - let messages = [ - RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }.into(), - RectangleToolMessage::PointerMove { center, lock_ratio }.into(), - ]; - tool_data.auto_panning.stop(&messages, responses); - - state - } - (RectangleToolFsmState::Drawing, RectangleToolMessage::DragStop) => { - input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses); - shape_data.cleanup(responses); - - RectangleToolFsmState::Ready - } - (RectangleToolFsmState::Drawing, RectangleToolMessage::Abort) => { - responses.add(DocumentMessage::AbortTransaction); - - shape_data.cleanup(responses); - - RectangleToolFsmState::Ready - } - (_, RectangleToolMessage::WorkingColorChanged) => { - responses.add(RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::WorkingColors( - Some(global_tool_data.primary_color), - Some(global_tool_data.secondary_color), - ))); - self - } - _ => self, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), - HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])]), - RectangleToolFsmState::Drawing => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), - ]), - }; - - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } -} diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3ce536cc50..d468c4dc75 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1,10 +1,7 @@ #![allow(clippy::too_many_arguments)] use super::tool_prelude::*; -use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, - SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE, -}; +use crate::consts::*; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -12,7 +9,6 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; -use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::preferences::SelectionMode; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose}; @@ -22,7 +18,7 @@ use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::transformation_cage::*; -use crate::messages::tool::common_functionality::utility_functions::text_bounding_box; +use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage}; use bezier_rs::Subpath; use glam::DMat2; use graph_craft::document::NodeId; @@ -861,35 +857,19 @@ impl Fsm for SelectToolFsmState { remove_from_selection, select_deepest, lasso_select, - skew, + .. }, ) => { tool_data.drag_start = input.mouse.position; tool_data.drag_current = input.mouse.position; tool_data.selection_mode = None; - let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| { - let edges = bounding_box.check_selected_edges(input.mouse.position); - - bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { - let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); - bounding_box.opposite_pivot = selected_edges.calculate_pivot(); - selected_edges - }); - - edges - }); - - let rotating_bounds = tool_data - .bounding_box_manager - .as_ref() - .map(|bounding_box| bounding_box.check_rotate(input.mouse.position)) - .unwrap_or_default(); - let mut selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); let intersection_list = document.click_list(input).collect::>(); let intersection = document.find_deepest(&intersection_list); + let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging); + // If the user is dragging the bounding box bounds, go into ResizingBounds mode. // If the user is dragging the rotate trigger, go into RotatingBounds mode. // If the user clicks on a layer that is in their current selection, go into the dragging mode. @@ -907,15 +887,10 @@ impl Fsm for SelectToolFsmState { let show_compass = bounds.is_some_and(|quad| quad.all_sides_at_least_width(COMPASS_ROSE_HOVER_RING_DIAMETER) && quad.contains(mouse_position)); let can_grab_compass_rose = compass_rose_state.can_grab() && (show_compass || bounds.is_none()); - let is_flat_layer = tool_data - .bounding_box_manager - .as_ref() - .map(|bounding_box_manager| bounding_box_manager.transform_tampered) - .unwrap_or(true); - let state = + let state = if is_over_pivot // Dragging the pivot - if is_over_pivot { + { responses.add(DocumentMessage::StartTransaction); // tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true); @@ -924,47 +899,12 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::DraggingPivot } // Dragging one (or two, forming a corner) of the transform cage bounding box edges - else if dragging_bounds.is_some() && !is_flat_layer { - responses.add(DocumentMessage::StartTransaction); - - tool_data.layers_dragging = selected; - - if let Some(bounds) = &mut tool_data.bounding_box_manager { - bounds.original_bound_transform = bounds.transform; - - tool_data.layers_dragging.retain(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - document.network_interface.document_network().nodes.contains_key(&layer.to_node()) - } else { - log::error!("ROOT_PARENT should not be part of layers_dragging"); - false - } - }); - - let mut selected = Selected::new( - &mut bounds.original_transforms, - &mut bounds.center_of_transformation, - &tool_data.layers_dragging, - responses, - &document.network_interface, - None, - &ToolType::Select, - None - ); - bounds.center_of_transformation = selected.mean_average_of_pivots(); - - // Check if we're hovering over a skew triangle - let edges = bounds.check_selected_edges(input.mouse.position); - if let Some(edges) = edges { - let closest_edge = bounds.get_closest_edge(edges, input.mouse.position); - if bounds.check_skew_handle(input.mouse.position, closest_edge) { - tool_data.get_snap_candidates(document, input); - return SelectToolFsmState::SkewingBounds { skew }; - } - } - } + else if resize { tool_data.get_snap_candidates(document, input); SelectToolFsmState::ResizingBounds + } else if skew { + tool_data.get_snap_candidates(document, input); + SelectToolFsmState::SkewingBounds { skew: Key::Control } } // Dragging the selected layers around to transform them else if can_grab_compass_rose || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) { @@ -983,37 +923,16 @@ impl Fsm for SelectToolFsmState { let axis_state = compass_rose_state.axis_type().filter(|_| can_grab_compass_rose); (axis_state.unwrap_or_default(), axis_state.is_some()) }; - SelectToolFsmState::Dragging { axis, using_compass, has_dragged: false, deepest: input.keyboard.key(select_deepest), remove: input.keyboard.key(extend_selection) } + SelectToolFsmState::Dragging { + axis, + using_compass, + has_dragged: false, + deepest: input.keyboard.key(select_deepest), + remove: input.keyboard.key(extend_selection), + } } // Dragging near the transform cage bounding box to rotate it - else if rotating_bounds { - responses.add(DocumentMessage::StartTransaction); - - if let Some(bounds) = &mut tool_data.bounding_box_manager { - tool_data.layers_dragging.retain(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - document.network_interface.document_network().nodes.contains_key(&layer.to_node()) - } else { - log::error!("ROOT_PARENT should not be part of layers_dragging"); - false - } - }); - let mut selected = Selected::new( - &mut bounds.original_transforms, - &mut bounds.center_of_transformation, - &selected, - responses, - &document.network_interface, - None, - &ToolType::Select, - None - ); - - bounds.center_of_transformation = selected.mean_average_of_pivots(); - } - - tool_data.layers_dragging = selected; - + else if rotate { SelectToolFsmState::RotatingBounds } // Dragging a selection box @@ -1036,7 +955,13 @@ impl Fsm for SelectToolFsmState { tool_data.get_snap_candidates(document, input); responses.add(DocumentMessage::StartTransaction); - SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false, has_dragged: false, deepest: input.keyboard.key(select_deepest), remove: input.keyboard.key(extend_selection) } + SelectToolFsmState::Dragging { + axis: Axis::None, + using_compass: false, + has_dragged: false, + deepest: input.keyboard.key(select_deepest), + remove: input.keyboard.key(extend_selection), + } } else { let selection_shape = if input.keyboard.key(lasso_select) { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; SelectToolFsmState::Drawing { selection_shape, has_drawn: false } @@ -1120,123 +1045,52 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { - if let Some(movement) = &mut bounds.selected_edges { - let (center, constrain) = (input.keyboard.key(modifier_keys.center), input.keyboard.key(modifier_keys.axis_align)); - - let center = center.then_some(bounds.center_of_transformation); - let snap = Some(SizeSnapData { - manager: &mut tool_data.snap_manager, - points: &mut tool_data.snap_candidates, - snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging), - }); - let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); - let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); - - let pivot_transform = DAffine2::from_translation(pivot); - let transformation = pivot_transform * delta * pivot_transform.inverse(); - - tool_data.layers_dragging.retain(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - document.network_interface.document_network().nodes.contains_key(&layer.to_node()) - } else { - log::error!("ROOT_PARENT should not be part of layers_dragging"); - false - } - }); - let selected = &tool_data.layers_dragging; - let mut selected = Selected::new( - &mut bounds.original_transforms, - &mut pivot, - selected, - responses, - &document.network_interface, - None, - &ToolType::Select, - None, - ); - - selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); - - // Auto-panning - let messages = [ - SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), - SelectToolMessage::PointerMove(modifier_keys).into(), - ]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - } + resize_bounds( + document, + responses, + bounds, + &mut tool_data.layers_dragging, + &mut tool_data.snap_manager, + &mut tool_data.snap_candidates, + input, + input.keyboard.key(modifier_keys.center), + input.keyboard.key(modifier_keys.axis_align), + ToolType::Select, + ); + let messages = [ + SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(), + SelectToolMessage::PointerMove(modifier_keys).into(), + ]; + tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); } SelectToolFsmState::ResizingBounds } (SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { - if let Some(movement) = &mut bounds.selected_edges { - let free_movement = input.keyboard.key(skew); - let transformation = movement.skew_transform(input.mouse.position, bounds.original_bound_transform, free_movement); - - tool_data.layers_dragging.retain(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - document.network_interface.document_network().nodes.contains_key(&layer.to_node()) - } else { - log::error!("ROOT_PARENT should not be part of layers_dragging"); - false - } - }); - let selected = &tool_data.layers_dragging; - let mut pivot = DVec2::ZERO; - let mut selected = Selected::new( - &mut bounds.original_transforms, - &mut pivot, - selected, - responses, - &document.network_interface, - None, - &ToolType::Select, - None, - ); - - selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None); - } + skew_bounds( + document, + responses, + bounds, + input.keyboard.key(skew), + &mut tool_data.layers_dragging, + input.mouse.position, + ToolType::Select, + ); } SelectToolFsmState::SkewingBounds { skew } } - (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { + (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(_)) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { - let angle = { - let start_offset = tool_data.drag_start - bounds.center_of_transformation; - let end_offset = input.mouse.position - bounds.center_of_transformation; - - start_offset.angle_to(end_offset) - }; - - let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) { - let snap_resolution = ROTATE_INCREMENT.to_radians(); - (angle / snap_resolution).round() * snap_resolution - } else { - angle - }; - - let delta = DAffine2::from_angle(snapped_angle); - - tool_data.layers_dragging.retain(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - document.network_interface.document_network().nodes.contains_key(&layer.to_node()) - } else { - log::error!("ROOT_PARENT should not be part of replacement_selected_layers"); - false - } - }); - let mut selected = Selected::new( - &mut bounds.original_transforms, - &mut bounds.center_of_transformation, - &tool_data.layers_dragging, + rotate_bounds( + document, responses, - &document.network_interface, - None, - &ToolType::Select, - None, + bounds, + &mut tool_data.layers_dragging, + tool_data.drag_start, + input.mouse.position, + input.keyboard.key(Key::Shift), + ToolType::Select, ); - - selected.update_transforms(delta, None, None); } SelectToolFsmState::RotatingBounds diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs new file mode 100644 index 0000000000..a6b79a1139 --- /dev/null +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -0,0 +1,966 @@ +use super::tool_prelude::*; +use crate::consts::{DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE}; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; +use crate::messages::tool::common_functionality::resize::Resize; +use crate::messages::tool::common_functionality::shape_gizmos::number_of_points_handle::{NumberOfPointsHandle, NumberOfPointsHandleState}; +use crate::messages::tool::common_functionality::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState}; +use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; +use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, polygon_outline, star_outline, transform_cage_overlays}; +use crate::messages::tool::common_functionality::shapes::star_shape::Star; +use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle}; +use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration}; +use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool}; +use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage}; +use graph_craft::document::value::TaggedValue; +use graph_craft::document::{NodeId, NodeInput}; +use graphene_std::Color; +use graphene_std::renderer::Quad; + +#[derive(Default)] +pub struct ShapeTool { + fsm_state: ShapeToolFsmState, + tool_data: ShapeToolData, + options: ShapeToolOptions, +} + +pub struct ShapeToolOptions { + line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, + vertices: u32, + shape_type: ShapeType, +} + +impl Default for ShapeToolOptions { + fn default() -> Self { + Self { + line_weight: DEFAULT_STROKE_WIDTH, + fill: ToolColorOptions::new_secondary(), + stroke: ToolColorOptions::new_primary(), + shape_type: ShapeType::Polygon, + vertices: 5, + } + } +} + +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ShapeOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), + LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), + Vertices(u32), + ShapeType(ShapeType), +} + +#[impl_message(Message, ToolMessage, Shape)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ShapeToolMessage { + // Standard messages + Overlays(OverlayContext), + Abort, + WorkingColorChanged, + + // Tool-specific messages + DragStart, + DragStop, + HideShapeTypeWidget(bool), + PointerMove(ShapeToolModifierKey), + PointerOutsideViewport(ShapeToolModifierKey), + UpdateOptions(ShapeOptionsUpdate), + SetShape(ShapeType), + + IncreaseSides, + DecreaseSides, + + NudgeSelectedLayers { delta_x: f64, delta_y: f64, resize: Key, resize_opposite_corner: Key }, +} + +fn create_sides_widget(vertices: u32) -> WidgetHolder { + NumberInput::new(Some(vertices as f64)) + .label("Sides") + .int() + .min(3.) + .max(1000.) + .mode(NumberInputMode::Increment) + .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) + .widget_holder() +} + +fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { + let entries = vec![vec![ + MenuListEntry::new("Polygon") + .label("Polygon") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Polygon)).into()), + MenuListEntry::new("Star") + .label("Star") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), + ]]; + DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() +} + +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .max((1_u64 << f64::MANTISSA_DIGITS) as f64) + .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + +impl LayoutHolder for ShapeTool { + fn layout(&self) -> Layout { + let mut widgets = vec![]; + + if !self.tool_data.hide_shape_option_widget { + widgets.push(create_shape_option_widget(self.options.shape_type)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + if self.options.shape_type == ShapeType::Polygon || self.options.shape_type == ShapeType::Star { + widgets.push(create_sides_widget(self.options.vertices)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } + } + + if self.options.shape_type != ShapeType::Line { + widgets.append(&mut self.options.fill.create_widgets( + "Fill", + true, + |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(), + |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), + |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid())).into(), + )); + + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(), + |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), + |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid())).into(), + )); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +impl<'a> MessageHandler> for ShapeTool { + fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { + let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else { + self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); + return; + }; + match action { + ShapeOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + ShapeOptionsUpdate::FillColorType(color_type) => { + self.options.fill.color_type = color_type; + } + ShapeOptionsUpdate::LineWeight(line_weight) => { + self.options.line_weight = line_weight; + } + ShapeOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + ShapeOptionsUpdate::StrokeColorType(color_type) => { + self.options.stroke.color_type = color_type; + } + ShapeOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } + ShapeOptionsUpdate::ShapeType(shape) => { + self.options.shape_type = shape; + self.tool_data.current_shape = shape; + } + ShapeOptionsUpdate::Vertices(vertices) => { + self.options.vertices = vertices; + } + } + + self.fsm_state.update_hints(responses); + self.send_layout(responses, LayoutTarget::ToolOptions); + } + + fn actions(&self) -> ActionList { + match self.fsm_state { + ShapeToolFsmState::Ready(_) => actions!(ShapeToolMessageDiscriminant; + DragStart, + PointerMove, + SetShape, + Abort, + HideShapeTypeWidget, + IncreaseSides, + DecreaseSides, + NudgeSelectedLayers, + ), + ShapeToolFsmState::Drawing(_) + | ShapeToolFsmState::ResizingBounds + | ShapeToolFsmState::DraggingLineEndpoints + | ShapeToolFsmState::RotatingBounds + | ShapeToolFsmState::DraggingStarInnerRadius + | ShapeToolFsmState::DraggingStarNumberPointHandle + | ShapeToolFsmState::SkewingBounds { .. } => { + actions!(ShapeToolMessageDiscriminant; + DragStop, + Abort, + PointerMove, + SetShape, + HideShapeTypeWidget, + IncreaseSides, + DecreaseSides, + NudgeSelectedLayers, + ) + } + } + } +} + +impl ToolMetadata for ShapeTool { + fn icon_name(&self) -> String { + "VectorPolygonTool".into() + } + fn tooltip(&self) -> String { + "Shape Tool".into() + } + fn tool_type(&self) -> ToolType { + ToolType::Shape + } +} + +impl ToolTransition for ShapeTool { + fn event_to_message_map(&self) -> EventToMessageMap { + EventToMessageMap { + overlay_provider: Some(|overlay_context| ShapeToolMessage::Overlays(overlay_context).into()), + tool_abort: Some(ShapeToolMessage::Abort.into()), + working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()), + ..Default::default() + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ShapeToolFsmState { + Ready(ShapeType), + Drawing(ShapeType), + + // Line shape-specific + DraggingLineEndpoints, + + // Star shape-specific + DraggingStarInnerRadius, + DraggingStarNumberPointHandle, + + // Transform cage + ResizingBounds, + RotatingBounds, + SkewingBounds { skew: Key }, +} + +impl Default for ShapeToolFsmState { + fn default() -> Self { + ShapeToolFsmState::Ready(ShapeType::default()) + } +} + +#[derive(Clone, Debug, Default)] +pub struct ShapeToolData { + pub data: Resize, + auto_panning: AutoPanning, + + // In viewport space + pub last_mouse_position: DVec2, + + // Hide the dropdown menu when using Line, Rectangle, or Ellipse aliases + pub hide_shape_option_widget: bool, + + // Shape-specific data + pub line_data: LineToolData, + + // Used for by transform cage + pub bounding_box_manager: Option, + layers_dragging: Vec, + snap_candidates: Vec, + skew_edge: EdgeBool, + cursor: MouseCursorIcon, + + // Current shape which is being drawn + current_shape: ShapeType, + + // Gizmo data + pub point_radius_handle: PointRadiusHandle, + pub number_of_points_handle: NumberOfPointsHandle, +} + +impl ShapeToolData { + fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) { + self.snap_candidates.clear(); + for &layer in &self.layers_dragging { + if (self.snap_candidates.len() as f64) < document.snapping_state.tolerance { + snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates); + } + if let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) { + let quad = document.metadata().transform_to_document(layer) * Quad::from_box(bounds); + snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document); + } + } + } + + fn outlines(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + if let Some(layer) = self.number_of_points_handle.layer.or(self.point_radius_handle.layer) { + star_outline(layer, document, overlay_context); + polygon_outline(layer, document, overlay_context); + return; + } + + // Fallback: apply to all selected visible & unlocked star layers + for layer in document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .filter(|layer| { + graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some() + }) { + star_outline(layer, document, overlay_context); + polygon_outline(layer, document, overlay_context); + } + } +} + +impl Fsm for ShapeToolFsmState { + type ToolData = ShapeToolData; + type ToolOptions = ShapeToolOptions; + + fn transition( + self, + event: ToolMessage, + tool_data: &mut Self::ToolData, + ToolActionHandlerData { + document, + global_tool_data, + input, + preferences, + shape_editor, + .. + }: &mut ToolActionHandlerData, + tool_options: &Self::ToolOptions, + responses: &mut VecDeque, + ) -> Self { + let all_selected_layers_line = document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .all(|layer| graph_modification_utils::get_line_id(layer, &document.network_interface).is_some()); + + let ToolMessage::Shape(event) = event else { return self }; + + match (self, event) { + (_, ShapeToolMessage::Overlays(mut overlay_context)) => { + let mouse_position = tool_data + .data + .snap_manager + .indicator_pos() + .map(|pos| document.metadata().document_to_viewport.transform_point2(pos)) + .unwrap_or(input.mouse.position); + let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds); + let dragging_start_gizmos = matches!(self, Self::DraggingStarInnerRadius); + + if matches!(self, ShapeToolFsmState::DraggingStarInnerRadius | Self::DraggingStarNumberPointHandle | Self::Ready(_)) && !input.keyboard.key(Key::Control) { + // Manage state handling of the number of point gizmos + tool_data.number_of_points_handle.handle_actions(document, input, mouse_position, &mut overlay_context, responses); + + // Manage state handling of point radius handle gizmo + tool_data.point_radius_handle.handle_actions(document, mouse_position); + + tool_data.number_of_points_handle.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); + tool_data + .point_radius_handle + .overlays(tool_data.number_of_points_handle.layer.is_some(), document, input, mouse_position, &mut overlay_context); + tool_data.outlines(document, &mut overlay_context); + } + + let hovered = tool_data.number_of_points_handle.is_hovering() || tool_data.number_of_points_handle.is_dragging() || !tool_data.point_radius_handle.is_inactive(); + let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. }); + + if !is_resizing_or_rotating && !dragging_start_gizmos && !hovered && !modifying_transform_cage { + tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + } + + if modifying_transform_cage { + transform_cage_overlays(document, tool_data, &mut overlay_context); + } + + if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) { + anchor_overlays(document, &mut overlay_context); + } else if matches!(self, ShapeToolFsmState::Ready(_)) { + Line::overlays(document, tool_data, &mut overlay_context); + + if all_selected_layers_line { + return self; + } + + transform_cage_overlays(document, tool_data, &mut overlay_context); + + let dragging_bounds = tool_data + .bounding_box_manager + .as_mut() + .and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position)) + .is_some(); + + if let Some(bounds) = tool_data.bounding_box_manager.as_mut() { + let edges = bounds.check_selected_edges(input.mouse.position); + let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. }); + let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge)); + if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating) { + bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge); + } + if !is_skewing && dragging_bounds { + if let Some(edges) = edges { + tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position); + } + } + } + } + + if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { + Line::overlays(document, tool_data, &mut overlay_context); + } + + self + } + (ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1))); + self + } + (ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)))); + self + } + ( + ShapeToolFsmState::Ready(_), + ShapeToolMessage::NudgeSelectedLayers { + delta_x, + delta_y, + resize, + resize_opposite_corner, + }, + ) => { + responses.add(DocumentMessage::NudgeSelectedLayers { + delta_x, + delta_y, + resize, + resize_opposite_corner, + }); + + self + } + (ShapeToolFsmState::Drawing(_), ShapeToolMessage::NudgeSelectedLayers { .. }) => { + let increase = input.keyboard.key(Key::ArrowUp); + let decrease = input.keyboard.key(Key::ArrowDown); + + if increase { + responses.add(ShapeToolMessage::IncreaseSides); + return self; + } + + if decrease { + responses.add(ShapeToolMessage::DecreaseSides); + return self; + } + self + } + (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { + if let Some(layer) = tool_data.data.layer { + let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) + else { + return self; + }; + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs("Regular Polygon") + .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) + else { + return self; + }; + + let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { + return self; + }; + + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::U32(n + 1), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + self + } + (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { + if let Some(layer) = tool_data.data.layer { + let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) + else { + return self; + }; + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs("Regular Polygon") + .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) + else { + return self; + }; + + let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { + return self; + }; + + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3)))); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::U32((n - 1).max(3)), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + self + } + (ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => { + tool_data.line_data.drag_start = input.mouse.position; + + // Snapped position in viewport space + let mouse_pos = tool_data + .data + .snap_manager + .indicator_pos() + .map(|pos| document.metadata().document_to_viewport.transform_point2(pos)) + .unwrap_or(input.mouse.position); + + tool_data.line_data.drag_current = mouse_pos; + + // Check if dragging the inner vertices of a star + if tool_data.point_radius_handle.hovered() { + tool_data.last_mouse_position = mouse_pos; + tool_data.point_radius_handle.update_state(PointRadiusHandleState::Dragging); + + // Always store it in document space + tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos); + + responses.add(DocumentMessage::StartTransaction); + return ShapeToolFsmState::DraggingStarInnerRadius; + } + + // Check if dragging the number of points handle of a star or polygon + if tool_data.number_of_points_handle.is_hovering() { + tool_data.last_mouse_position = mouse_pos; + tool_data.number_of_points_handle.update_state(NumberOfPointsHandleState::Dragging); + + // Always store it in document space + tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos); + + responses.add(DocumentMessage::StartTransaction); + return ShapeToolFsmState::DraggingStarNumberPointHandle; + } + + // If clicked on endpoints of a selected line, drag its endpoints + if let Some((layer, _, _)) = closest_point( + document, + mouse_pos, + SNAP_POINT_TOLERANCE, + document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface), + |_| false, + preferences, + ) { + if clicked_on_line_endpoints(layer, document, input, tool_data) && !input.keyboard.key(Key::Control) { + return ShapeToolFsmState::DraggingLineEndpoints; + } + } + + let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging); + + if !input.keyboard.key(Key::Control) { + match (resize, rotate, skew) { + (true, false, false) => { + tool_data.get_snap_candidates(document, input); + return ShapeToolFsmState::ResizingBounds; + } + (false, true, false) => { + tool_data.data.drag_start = mouse_pos; + return ShapeToolFsmState::RotatingBounds; + } + (false, false, true) => { + tool_data.get_snap_candidates(document, input); + return ShapeToolFsmState::SkewingBounds { skew: Key::Control }; + } + _ => {} + } + }; + + match tool_data.current_shape { + ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input), + ShapeType::Line => { + let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); + let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); + tool_data.data.drag_start = snapped.snapped_point_document; + } + } + + responses.add(DocumentMessage::StartTransaction); + + let node = match tool_data.current_shape { + ShapeType::Polygon => Polygon::create_node(tool_options.vertices), + ShapeType::Star => Star::create_node(tool_options.vertices), + ShapeType::Rectangle => Rectangle::create_node(), + ShapeType::Ellipse => Ellipse::create_node(), + ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), + }; + + let nodes = vec![(NodeId(0), node)]; + let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses); + + responses.add(Message::StartBuffer); + + match tool_data.current_shape { + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => { + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + + tool_options.fill.apply_fill(layer, responses); + } + ShapeType::Line => { + tool_data.line_data.angle = 0.; + tool_data.line_data.weight = tool_options.line_weight; + tool_data.line_data.editing_layer = Some(layer); + } + } + tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + + tool_data.data.layer = Some(layer); + + ShapeToolFsmState::Drawing(tool_data.current_shape) + } + (ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => { + let Some(layer) = tool_data.data.layer else { + return ShapeToolFsmState::Ready(shape); + }; + + match tool_data.current_shape { + ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), + } + + // Auto-panning + let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; + tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); + + self + } + (ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove(modifier)) => { + let Some(layer) = tool_data.line_data.editing_layer else { + return ShapeToolFsmState::Ready(tool_data.current_shape); + }; + + Line::update_shape(document, input, layer, tool_data, modifier, responses); + // Auto-panning + let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; + tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); + + self + } + (ShapeToolFsmState::DraggingStarInnerRadius, ShapeToolMessage::PointerMove(..)) => { + if let Some(layer) = tool_data.point_radius_handle.layer { + tool_data.point_radius_handle.update_inner_radius(document, input, layer, responses, tool_data.data.drag_start); + tool_data.last_mouse_position = input.mouse.position; + } + + responses.add(OverlaysMessage::Draw); + + ShapeToolFsmState::DraggingStarInnerRadius + } + (ShapeToolFsmState::DraggingStarNumberPointHandle, ShapeToolMessage::PointerMove(..)) => { + tool_data.number_of_points_handle.update_number_of_sides(document, input, responses, tool_data.data.drag_start); + + tool_data.last_mouse_position = input.mouse.position; + responses.add(OverlaysMessage::Draw); + + ShapeToolFsmState::DraggingStarNumberPointHandle + } + (ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => { + if let Some(bounds) = &mut tool_data.bounding_box_manager { + let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()]; + resize_bounds( + document, + responses, + bounds, + &mut tool_data.layers_dragging, + &mut tool_data.data.snap_manager, + &mut tool_data.snap_candidates, + input, + input.keyboard.key(modifier[0]), + input.keyboard.key(modifier[1]), + ToolType::Shape, + ); + tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); + } + + responses.add(OverlaysMessage::Draw); + ShapeToolFsmState::ResizingBounds + } + (ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove(modifier)) => { + if let Some(bounds) = &mut tool_data.bounding_box_manager { + rotate_bounds( + document, + responses, + bounds, + &mut tool_data.layers_dragging, + tool_data.data.drag_start, + input.mouse.position, + input.keyboard.key(modifier[1]), + ToolType::Shape, + ); + } + + ShapeToolFsmState::RotatingBounds + } + (ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove(_)) => { + if let Some(bounds) = &mut tool_data.bounding_box_manager { + skew_bounds( + document, + responses, + bounds, + input.keyboard.key(skew), + &mut tool_data.layers_dragging, + input.mouse.position, + ToolType::Shape, + ); + } + + ShapeToolFsmState::SkewingBounds { skew } + } + + (_, ShapeToolMessage::PointerMove(_)) => { + let dragging_bounds = tool_data + .bounding_box_manager + .as_mut() + .and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position)) + .is_some(); + + let cursor = tool_data + .bounding_box_manager + .as_ref() + .map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge))); + + if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && tool_data.point_radius_handle.is_inactive() && !all_selected_layers_line { + tool_data.cursor = cursor; + responses.add(FrontendMessage::UpdateMouseCursor { cursor }); + } + + tool_data.data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); + + responses.add(OverlaysMessage::Draw); + self + } + (ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport(_)) => { + // Auto-panning + if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { + if let Some(bounds) = &mut tool_data.bounding_box_manager { + bounds.center_of_transformation += shift; + bounds.original_bound_transform.translation += shift; + } + } + + self + } + (ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport(..)) => self, + (_, ShapeToolMessage::PointerOutsideViewport { .. }) => { + // Auto-panning + let _ = tool_data.auto_panning.shift_viewport(input, responses); + self + } + ( + ShapeToolFsmState::Drawing(_) + | ShapeToolFsmState::DraggingLineEndpoints + | ShapeToolFsmState::ResizingBounds + | ShapeToolFsmState::RotatingBounds + | ShapeToolFsmState::SkewingBounds { .. } + | ShapeToolFsmState::DraggingStarInnerRadius + | ShapeToolFsmState::DraggingStarNumberPointHandle, + ShapeToolMessage::DragStop, + ) => { + input.mouse.finish_transaction(tool_data.data.drag_start, responses); + tool_data.data.cleanup(responses); + + tool_data.number_of_points_handle.cleanup(); + tool_data.point_radius_handle.cleanup(); + + if let Some(bounds) = &mut tool_data.bounding_box_manager { + bounds.original_transforms.clear(); + } + + tool_data.line_data.dragging_endpoint = None; + + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); + + ShapeToolFsmState::Ready(tool_data.current_shape) + } + ( + ShapeToolFsmState::Drawing(_) + | ShapeToolFsmState::DraggingLineEndpoints + | ShapeToolFsmState::ResizingBounds + | ShapeToolFsmState::RotatingBounds + | ShapeToolFsmState::SkewingBounds { .. } + | ShapeToolFsmState::DraggingStarInnerRadius + | ShapeToolFsmState::DraggingStarNumberPointHandle, + ShapeToolMessage::Abort, + ) => { + responses.add(DocumentMessage::AbortTransaction); + tool_data.data.cleanup(responses); + tool_data.line_data.dragging_endpoint = None; + + // Reset gizmo state + tool_data.number_of_points_handle.cleanup(); + tool_data.point_radius_handle.cleanup(); + + if let Some(bounds) = &mut tool_data.bounding_box_manager { + bounds.original_transforms.clear(); + } + + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); + + ShapeToolFsmState::Ready(tool_data.current_shape) + } + (_, ShapeToolMessage::WorkingColorChanged) => { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } + (_, ShapeToolMessage::SetShape(shape)) => { + responses.add(DocumentMessage::AbortTransaction); + tool_data.data.cleanup(responses); + tool_data.current_shape = shape; + + ShapeToolFsmState::Ready(shape) + } + (_, ShapeToolMessage::HideShapeTypeWidget(hide)) => { + tool_data.hide_shape_option_widget = hide; + responses.add(ToolMessage::RefreshToolOptions); + self + } + _ => self, + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + ShapeToolFsmState::Ready(shape) => { + let hint_groups = match shape { + ShapeType::Polygon | ShapeType::Star => vec![ + HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), + HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ]), + HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), + ], + ShapeType::Ellipse => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), + HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Line => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), + HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), + ])], + ShapeType::Rectangle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), + HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + }; + HintData(hint_groups) + } + ShapeToolFsmState::Drawing(shape) => { + let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; + let tool_hint_group = match shape { + ShapeType::Polygon | ShapeType::Star => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Line => HintGroup(vec![ + HintInfo::keys([Key::Shift], "15° Increments"), + HintInfo::keys([Key::Alt], "From Center"), + HintInfo::keys([Key::Control], "Lock Angle"), + ]), + }; + + common_hint_group.push(tool_hint_group); + + if matches!(shape, ShapeType::Polygon | ShapeType::Star) { + common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); + } + + HintData(common_hint_group) + } + ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![ + HintInfo::keys([Key::Shift], "15° Increments"), + HintInfo::keys([Key::Alt], "From Center"), + HintInfo::keys([Key::Control], "Lock Angle"), + ]), + ]), + ShapeToolFsmState::ResizingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]), + ]), + ShapeToolFsmState::RotatingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), + ]), + ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), + ]), + ShapeToolFsmState::DraggingStarInnerRadius | ShapeToolFsmState::DraggingStarNumberPointHandle => { + HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]) + } + }; + + responses.add(FrontendMessage::UpdateInputHints { hint_data }); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); + } +} diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 5797d3a136..eab68b8866 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -139,6 +139,7 @@ impl MessageHandler> for TransformLayer let using_path_tool = tool_data.active_tool_type == ToolType::Path; let using_select_tool = tool_data.active_tool_type == ToolType::Select; let using_pen_tool = tool_data.active_tool_type == ToolType::Pen; + let using_shape_tool = tool_data.active_tool_type == ToolType::Shape; // TODO: Add support for transforming layer not in the document network let selected_layers = document @@ -390,7 +391,7 @@ impl MessageHandler> for TransformLayer TransformLayerMessage::BeginGRS { transform_type } => { let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); if (using_path_tool && selected_points.is_empty()) - || (!using_path_tool && !using_select_tool && !using_pen_tool) + || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) || selected_layers.is_empty() || transform_type.equivalent_to(self.transform_operation) { @@ -715,7 +716,8 @@ impl MessageHandler> for TransformLayer #[cfg(test)] mod test_transform_layer { - use crate::messages::portfolio::document::graph_operation::{transform_utils, utility_types::ModifyInputsContext}; + use crate::messages::portfolio::document::graph_operation::transform_utils; + use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::prelude::Message; use crate::messages::tool::transform_layer::transform_layer_message_handler::VectorModificationType; diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 42c9bd5427..f2b441331e 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -11,6 +11,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; use crate::messages::preferences::PreferencesMessageHandler; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; use crate::node_graph_executor::NodeGraphExecutor; use graphene_std::raster::color::Color; use graphene_std::text::FontCache; @@ -162,7 +163,7 @@ pub trait ToolTransition { on: event, send: Box::new(mapping.into()), }); - }; + } }; let event_to_tool_map = self.event_to_message_map(); @@ -182,7 +183,7 @@ pub trait ToolTransition { on: event, message: Box::new(mapping.into()), }); - }; + } }; let event_to_tool_map = self.event_to_message_map(); @@ -204,6 +205,7 @@ pub trait ToolMetadata { pub struct ToolData { pub active_tool_type: ToolType, + pub active_shape_type: Option, pub tools: HashMap>, } @@ -225,32 +227,51 @@ impl ToolData { impl LayoutHolder for ToolData { fn layout(&self) -> Layout { + let active_tool = self.active_shape_type.unwrap_or(self.active_tool_type); + let tool_groups_layout = list_tools_in_groups() .iter() - .map(|tool_group| tool_group.iter().map(|tool_availability| { - match tool_availability { - ToolAvailability::Available(tool) => ToolEntry::new(tool.tool_type(), tool.icon_name()) - .tooltip(tool.tooltip()) - .tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))), - ToolAvailability::ComingSoon(tool) => tool.clone(), - } - }) - .collect::>()) + .map(|tool_group| + tool_group + .iter() + .map(|tool_availability| { + match tool_availability { + ToolAvailability::Available(tool) => + ToolEntry::new(tool.tool_type(), tool.icon_name()) + .tooltip(tool.tooltip()) + .tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))), + ToolAvailability::AvailableAsShape(shape) => + ToolEntry::new(shape.tool_type(), shape.icon_name()) + .tooltip(shape.tooltip()) + .tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(shape.tool_type()))), + ToolAvailability::ComingSoon(tool) => tool.clone(), + } + }) + .collect::>() + ) .flat_map(|group| { let separator = std::iter::once(Separator::new(SeparatorType::Section).direction(SeparatorDirection::Vertical).widget_holder()); let buttons = group.into_iter().map(|ToolEntry { tooltip, tooltip_shortcut, tool_type, icon_name }| { IconButton::new(icon_name, 32) .disabled(false) - .active(self.active_tool_type == tool_type) + .active(match tool_type { + ToolType::Line | ToolType::Ellipse | ToolType::Rectangle => { self.active_shape_type.is_some() && active_tool == tool_type } + _ => active_tool == tool_type, + }) .tooltip(tooltip.clone()) .tooltip_shortcut(tooltip_shortcut) .on_update(move |_| { - if !tooltip.contains("Coming Soon") { - ToolMessage::ActivateTool { tool_type }.into() - } else { - DialogMessage::RequestComingSoonDialog { issue: None }.into() + match tool_type { + ToolType::Line => ToolMessage::ActivateToolShapeLine.into(), + ToolType::Rectangle => ToolMessage::ActivateToolShapeRectangle.into(), + ToolType::Ellipse => ToolMessage::ActivateToolShapeEllipse.into(), + ToolType::Shape => ToolMessage::ActivateToolShape.into(), + _ => { + if !tooltip.contains("Coming Soon") { (ToolMessage::ActivateTool { tool_type }).into() } else { (DialogMessage::RequestComingSoonDialog { issue: None }).into() } + } } - }).widget_holder() + }) + .widget_holder() }); separator.chain(buttons) @@ -287,11 +308,13 @@ impl Default for ToolFsmState { Self { tool_data: ToolData { active_tool_type: ToolType::Select, + active_shape_type: None, tools: list_tools_in_groups() .into_iter() .flatten() .filter_map(|tool| match tool { ToolAvailability::Available(tool) => Some((tool.tool_type(), tool)), + ToolAvailability::AvailableAsShape(_) => None, ToolAvailability::ComingSoon(_) => None, }) .collect(), @@ -327,10 +350,10 @@ pub enum ToolType { Pen, Freehand, Spline, - Line, - Rectangle, - Ellipse, - Polygon, + Shape, + Line, // Shape tool alias + Rectangle, // Shape tool alias + Ellipse, // Shape tool alias Text, // Raster tool group @@ -344,8 +367,22 @@ pub enum ToolType { Frame, } +impl ToolType { + pub fn get_shape(&self) -> Option { + match self { + Self::Rectangle | Self::Line | Self::Ellipse => Some(*self), + _ => None, + } + } + + pub fn get_tool(self) -> Self { + if self.get_shape().is_some() { ToolType::Shape } else { self } + } +} + enum ToolAvailability { Available(Box), + AvailableAsShape(ShapeType), ComingSoon(ToolEntry), } @@ -367,10 +404,10 @@ fn list_tools_in_groups() -> Vec> { ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), - ToolAvailability::Available(Box::::default()), - ToolAvailability::Available(Box::::default()), - ToolAvailability::Available(Box::::default()), - ToolAvailability::Available(Box::::default()), + ToolAvailability::AvailableAsShape(ShapeType::Line), + ToolAvailability::AvailableAsShape(ShapeType::Rectangle), + ToolAvailability::AvailableAsShape(ShapeType::Ellipse), + ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ], vec![ @@ -403,10 +440,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { ToolMessage::Pen(_) => ToolType::Pen, ToolMessage::Freehand(_) => ToolType::Freehand, ToolMessage::Spline(_) => ToolType::Spline, - ToolMessage::Line(_) => ToolType::Line, - ToolMessage::Rectangle(_) => ToolType::Rectangle, - ToolMessage::Ellipse(_) => ToolType::Ellipse, - ToolMessage::Polygon(_) => ToolType::Polygon, + ToolMessage::Shape(_) => ToolType::Shape, // Includes the Line, Rectangle, and Ellipse aliases ToolMessage::Text(_) => ToolType::Text, // Raster tool group @@ -436,10 +470,10 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis ToolType::Pen => ToolMessageDiscriminant::ActivateToolPen, ToolType::Freehand => ToolMessageDiscriminant::ActivateToolFreehand, ToolType::Spline => ToolMessageDiscriminant::ActivateToolSpline, - ToolType::Line => ToolMessageDiscriminant::ActivateToolLine, - ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolRectangle, - ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolEllipse, - ToolType::Polygon => ToolMessageDiscriminant::ActivateToolPolygon, + ToolType::Line => ToolMessageDiscriminant::ActivateToolShapeLine, // Shape tool alias + ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolShapeRectangle, // Shape tool alias + ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolShapeEllipse, // Shape tool alias + ToolType::Shape => ToolMessageDiscriminant::ActivateToolShape, ToolType::Text => ToolMessageDiscriminant::ActivateToolText, // Raster tool group diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index aaa7e3416f..683315c767 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -89,7 +89,7 @@ impl EditorTestUtils { } 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; + self.drag_tool(ToolType::Shape, x1, y1, x2, y2, ModifierKeys::default()).await; } pub async fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { @@ -218,7 +218,12 @@ impl EditorTestUtils { } pub async fn select_tool(&mut self, tool_type: ToolType) { - self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await; + match tool_type { + ToolType::Line => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeLine)).await, + ToolType::Rectangle => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeRectangle)).await, + ToolType::Ellipse => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeEllipse)).await, + _ => self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await, + } } pub async fn select_primary_color(&mut self, color: Color) {