mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Add star/polygon gizmos and refactor the separate shape drawing tools into a unified Shape tool (#2683)
* no trait ,not to fix line * add hints * line modification even when other shapes are selected * added transform and anchor overlays * removed old code * fixed transform added hints need to fix modifier keys use * refactored select-tool * add point-handle-gizmo * fix rotate bug * implement angle snapping gizmo , fix overlay and refactor the code * implement snapping for point-handle gizmo and implement no of point gizmo need to refactor * implemented the gizmo for polygon, added tests , brackets to increase sides * formatting-fix * small nit-picks * Make it compile * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
e520c21b66
commit
8e5abf65cb
34 changed files with 3231 additions and 2137 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2264,6 +2264,7 @@ dependencies = [
|
|||
"graphite-proc-macros",
|
||||
"interpreted-executor",
|
||||
"js-sys",
|
||||
"kurbo",
|
||||
"log",
|
||||
"num_enum",
|
||||
"once_cell",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1705,6 +1705,21 @@ impl DocumentMessageHandler {
|
|||
self.click_list(ipp).last()
|
||||
}
|
||||
|
||||
pub fn click_based_on_position(&self, mouse_snapped_positon: DVec2) -> Option<LayerNodeIdentifier> {
|
||||
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
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<LayerNodeIdentifier>,
|
||||
pub snap_manager: SnapManager,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
pub mod number_of_points_handle;
|
||||
pub mod point_radius_handle;
|
|
@ -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<LayerNodeIdentifier>,
|
||||
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<Message>,
|
||||
) {
|
||||
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<Message>, 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);
|
||||
}
|
||||
}
|
|
@ -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<LayerNodeIdentifier>,
|
||||
point: u32,
|
||||
radius_index: usize,
|
||||
snap_radii: Vec<f64>,
|
||||
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<f64> {
|
||||
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<Message>,
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<Message>,
|
||||
) {
|
||||
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<ResolvedEllipse> {
|
||||
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::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
|
||||
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 4.5,
|
||||
radius_y: 5.,
|
||||
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_circle() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 10.,
|
||||
radius_y: 10.,
|
||||
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
println!("{ellipse:?}");
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<LayerNodeIdentifier, [DVec2; 2]>,
|
||||
pub editing_layer: Option<LayerNodeIdentifier>,
|
||||
pub dragging_endpoint: Option<LineEnd>,
|
||||
}
|
||||
|
||||
#[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<Message>,
|
||||
) {
|
||||
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::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
11
editor/src/messages/tool/common_functionality/shapes/mod.rs
Normal file
11
editor/src/messages/tool/common_functionality/shapes/mod.rs
Normal file
|
@ -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;
|
|
@ -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<Message>,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Message>,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Message>) {
|
||||
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<LayerNodeIdentifier>, 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<LayerNodeIdentifier>, 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<ClickTargetType> = 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.));
|
||||
}
|
||||
}
|
|
@ -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<Message>,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -250,6 +250,10 @@ impl SnapManager {
|
|||
self.update_indicator(snapped);
|
||||
}
|
||||
|
||||
pub fn indicator_pos(&self) -> Option<DVec2> {
|
||||
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;
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -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<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
dragging_layers: &mut Vec<LayerNodeIdentifier>,
|
||||
snap_manager: &mut SnapManager,
|
||||
snap_candidates: &mut Vec<SnapCandidatePoint>,
|
||||
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<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
dragging_layers: &mut Vec<LayerNodeIdentifier>,
|
||||
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<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
free_movement: bool,
|
||||
layers: &mut Vec<LayerNodeIdentifier>,
|
||||
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<BoundingBoxManager>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
layers_dragging: &mut Vec<LayerNodeIdentifier>,
|
||||
) -> (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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ToolMessage, ToolMessageData<'_>> 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<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
|||
ActivateToolArtboard,
|
||||
ActivateToolNavigate,
|
||||
ActivateToolEyedropper,
|
||||
ActivateToolText,
|
||||
ActivateToolFill,
|
||||
ActivateToolGradient,
|
||||
|
||||
|
@ -307,10 +333,11 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
|||
ActivateToolPen,
|
||||
ActivateToolFreehand,
|
||||
ActivateToolSpline,
|
||||
ActivateToolLine,
|
||||
ActivateToolRectangle,
|
||||
ActivateToolEllipse,
|
||||
ActivateToolPolygon,
|
||||
ActivateToolShapeLine,
|
||||
ActivateToolShapeRectangle,
|
||||
ActivateToolShapeEllipse,
|
||||
ActivateToolShape,
|
||||
ActivateToolText,
|
||||
|
||||
ActivateToolBrush,
|
||||
// ActivateToolImaginate,
|
||||
|
|
|
@ -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<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
#[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<ToolMessage, &mut ToolActionHandlerData<'a>> for EllipseTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<Message>) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
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<ResolvedEllipse> {
|
||||
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::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
|
||||
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 4.5,
|
||||
radius_y: 5.,
|
||||
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_circle() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 10.,
|
||||
radius_y: 10.,
|
||||
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
println!("{ellipse:?}");
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
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<ToolMessage, &mut ToolActionHandlerData<'a>> for LineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<LayerNodeIdentifier, [DVec2; 2]>,
|
||||
editing_layer: Option<LayerNodeIdentifier>,
|
||||
snap_manager: SnapManager,
|
||||
auto_panning: AutoPanning,
|
||||
dragging_endpoint: Option<LineEnd>,
|
||||
}
|
||||
|
||||
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<Message>) -> 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::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
|
||||
|
||||
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<Message>) {
|
||||
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<Message>) {
|
||||
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}");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
PolygonType(PolygonType),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
Vertices(u32),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
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<ToolMessage, &mut ToolActionHandlerData<'a>> for PolygonTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<Message>) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
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<Message>) {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
#[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<ToolMessage, &mut ToolActionHandlerData<'a>> for RectangleTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<Message>,
|
||||
) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
|
@ -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::<Vec<_>>();
|
||||
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
|
||||
|
|
966
editor/src/messages/tool/tool_messages/shape_tool.rs
Normal file
966
editor/src/messages/tool/tool_messages/shape_tool.rs
Normal file
|
@ -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<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
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<ToolMessage, &mut ToolActionHandlerData<'a>> for ShapeTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<BoundingBoxManager>,
|
||||
layers_dragging: Vec<LayerNodeIdentifier>,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
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<Message>,
|
||||
) -> 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<Message>) {
|
||||
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<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
|
@ -139,6 +139,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> 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<TransformLayerMessage, TransformData<'_>> 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<TransformLayerMessage, TransformData<'_>> 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;
|
||||
|
|
|
@ -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<ToolType>,
|
||||
pub tools: HashMap<ToolType, Box<Tool>>,
|
||||
}
|
||||
|
||||
|
@ -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::<Vec<_>>())
|
||||
.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::<Vec<_>>()
|
||||
)
|
||||
.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<Self> {
|
||||
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<Tool>),
|
||||
AvailableAsShape(ShapeType),
|
||||
ComingSoon(ToolEntry),
|
||||
}
|
||||
|
||||
|
@ -367,10 +404,10 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
|
|||
ToolAvailability::Available(Box::<pen_tool::PenTool>::default()),
|
||||
ToolAvailability::Available(Box::<freehand_tool::FreehandTool>::default()),
|
||||
ToolAvailability::Available(Box::<spline_tool::SplineTool>::default()),
|
||||
ToolAvailability::Available(Box::<line_tool::LineTool>::default()),
|
||||
ToolAvailability::Available(Box::<rectangle_tool::RectangleTool>::default()),
|
||||
ToolAvailability::Available(Box::<ellipse_tool::EllipseTool>::default()),
|
||||
ToolAvailability::Available(Box::<polygon_tool::PolygonTool>::default()),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Line),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Rectangle),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Ellipse),
|
||||
ToolAvailability::Available(Box::<shape_tool::ShapeTool>::default()),
|
||||
ToolAvailability::Available(Box::<text_tool::TextTool>::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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue