| use super::select_tool::extend_lasso; |
| use super::tool_prelude::*; |
| use crate::consts::{ |
| COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, |
| SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, |
| }; |
| use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments}; |
| use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; |
| use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; |
| use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; |
| use crate::messages::portfolio::document::utility_types::transformation::Axis; |
| use crate::messages::preferences::SelectionMode; |
| use crate::messages::tool::common_functionality::auto_panning::AutoPanning; |
| use crate::messages::tool::common_functionality::shape_editor::{ |
| ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState, |
| }; |
| use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; |
| use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate}; |
| use bezier_rs::{Bezier, TValue}; |
| use graphene_std::renderer::Quad; |
| use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData}; |
| use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType}; |
| use std::vec; |
|
|
| #[derive(Default)] |
| pub struct PathTool { |
| fsm_state: PathToolFsmState, |
| tool_data: PathToolData, |
| options: PathToolOptions, |
| } |
|
|
| #[derive(Default)] |
| pub struct PathToolOptions { |
| path_overlay_mode: PathOverlayMode, |
| path_editing_mode: PathEditingMode, |
| } |
|
|
| #[impl_message(Message, ToolMessage, Path)] |
| #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] |
| pub enum PathToolMessage { |
| |
| Abort, |
| Overlays(OverlayContext), |
| SelectionChanged, |
|
|
| |
| BreakPath, |
| DeselectAllPoints, |
| Delete, |
| DeleteAndBreakPath, |
| DragStop { |
| extend_selection: Key, |
| shrink_selection: Key, |
| }, |
| Enter { |
| extend_selection: Key, |
| shrink_selection: Key, |
| }, |
| Escape, |
| ClosePath, |
| FlipSmoothSharp, |
| GRS { |
| |
| key: Key, |
| }, |
| ManipulatorMakeHandlesFree, |
| ManipulatorMakeHandlesColinear, |
| MouseDown { |
| extend_selection: Key, |
| lasso_select: Key, |
| handle_drag_from_anchor: Key, |
| drag_restore_handle: Key, |
| molding_in_segment_edit: Key, |
| }, |
| NudgeSelectedPoints { |
| delta_x: f64, |
| delta_y: f64, |
| }, |
| PointerMove { |
| equidistant: Key, |
| toggle_colinear: Key, |
| move_anchor_with_handles: Key, |
| snap_angle: Key, |
| lock_angle: Key, |
| delete_segment: Key, |
| break_colinear_molding: Key, |
| }, |
| PointerOutsideViewport { |
| equidistant: Key, |
| toggle_colinear: Key, |
| move_anchor_with_handles: Key, |
| snap_angle: Key, |
| lock_angle: Key, |
| delete_segment: Key, |
| break_colinear_molding: Key, |
| }, |
| RightClick, |
| SelectAllAnchors, |
| SelectedPointUpdated, |
| SelectedPointXChanged { |
| new_x: f64, |
| }, |
| SelectedPointYChanged { |
| new_y: f64, |
| }, |
| SwapSelectedHandles, |
| UpdateOptions(PathOptionsUpdate), |
| UpdateSelectedPointsStatus { |
| overlay_context: OverlayContext, |
| }, |
| } |
|
|
| #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] |
| pub enum PathOverlayMode { |
| AllHandles = 0, |
| #[default] |
| SelectedPointHandles = 1, |
| FrontierHandles = 2, |
| } |
|
|
| #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] |
| pub struct PathEditingMode { |
| point_editing_mode: bool, |
| segment_editing_mode: bool, |
| } |
|
|
| impl Default for PathEditingMode { |
| fn default() -> Self { |
| Self { |
| point_editing_mode: true, |
| segment_editing_mode: false, |
| } |
| } |
| } |
|
|
| #[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] |
| pub enum PathOptionsUpdate { |
| OverlayModeType(PathOverlayMode), |
| PointEditingMode { enabled: bool }, |
| SegmentEditingMode { enabled: bool }, |
| } |
|
|
| impl ToolMetadata for PathTool { |
| fn icon_name(&self) -> String { |
| "VectorPathTool".into() |
| } |
| fn tooltip(&self) -> String { |
| "Path Tool".into() |
| } |
| fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { |
| ToolType::Path |
| } |
| } |
|
|
| impl LayoutHolder for PathTool { |
| fn layout(&self) -> Layout { |
| let coordinates = self.tool_data.selection_status.as_one().as_ref().map(|point| point.coordinates); |
| let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None)); |
|
|
| let selection_status = &self.tool_data.selection_status; |
| let manipulator_angle = selection_status.angle(); |
|
|
| let x_location = NumberInput::new(x) |
| .unit(" px") |
| .label("X") |
| .min_width(120) |
| .disabled(x.is_none()) |
| .min(-((1_u64 << f64::MANTISSA_DIGITS) as f64)) |
| .max((1_u64 << f64::MANTISSA_DIGITS) as f64) |
| .on_update(move |number_input: &NumberInput| { |
| if let Some(new_x) = number_input.value.or(x) { |
| PathToolMessage::SelectedPointXChanged { new_x }.into() |
| } else { |
| Message::NoOp |
| } |
| }) |
| .widget_holder(); |
|
|
| let y_location = NumberInput::new(y) |
| .unit(" px") |
| .label("Y") |
| .min_width(120) |
| .disabled(y.is_none()) |
| .min(-((1_u64 << f64::MANTISSA_DIGITS) as f64)) |
| .max((1_u64 << f64::MANTISSA_DIGITS) as f64) |
| .on_update(move |number_input: &NumberInput| { |
| if let Some(new_y) = number_input.value.or(y) { |
| PathToolMessage::SelectedPointYChanged { new_y }.into() |
| } else { |
| Message::NoOp |
| } |
| }) |
| .widget_holder(); |
|
|
| let related_seperator = Separator::new(SeparatorType::Related).widget_holder(); |
| let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); |
|
|
| let colinear_handles_tooltip = "Keep both handles unbent, each 180° apart, when moving either"; |
| let colinear_handles_state = manipulator_angle.and_then(|angle| match angle { |
| ManipulatorAngle::Colinear => Some(true), |
| ManipulatorAngle::Free => Some(false), |
| ManipulatorAngle::Mixed => None, |
| }) |
| |
| .unwrap_or_default(); |
| let mut checkbox_id = CheckboxId::default(); |
| let colinear_handle_checkbox = CheckboxInput::new(colinear_handles_state) |
| .disabled(!self.tool_data.can_toggle_colinearity) |
| .on_update(|&CheckboxInput { checked, .. }| { |
| if checked { |
| PathToolMessage::ManipulatorMakeHandlesColinear.into() |
| } else { |
| PathToolMessage::ManipulatorMakeHandlesFree.into() |
| } |
| }) |
| .tooltip(colinear_handles_tooltip) |
| .for_label(checkbox_id.clone()) |
| .widget_holder(); |
| let colinear_handles_label = TextLabel::new("Colinear Handles") |
| .disabled(!self.tool_data.can_toggle_colinearity) |
| .tooltip(colinear_handles_tooltip) |
| .for_checkbox(&mut checkbox_id) |
| .widget_holder(); |
|
|
| let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode) |
| |
| .icon("Dot") |
| .tooltip("Point Editing Mode") |
| .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into()) |
| .widget_holder(); |
| let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode) |
| |
| .icon("Remove") |
| .tooltip("Segment Editing Mode") |
| .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into()) |
| .widget_holder(); |
|
|
| let path_overlay_mode_widget = RadioInput::new(vec![ |
| RadioEntryData::new("all") |
| .icon("HandleVisibilityAll") |
| .tooltip("Show all handles regardless of selection") |
| .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()), |
| RadioEntryData::new("selected") |
| .icon("HandleVisibilitySelected") |
| .tooltip("Show only handles of the segments connected to selected points") |
| .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()), |
| RadioEntryData::new("frontier") |
| .icon("HandleVisibilityFrontier") |
| .tooltip("Show only handles at the frontiers of the segments connected to selected points") |
| .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()), |
| ]) |
| .selected_index(Some(self.options.path_overlay_mode as u32)) |
| .widget_holder(); |
|
|
| Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { |
| widgets: vec![ |
| x_location, |
| related_seperator.clone(), |
| y_location, |
| unrelated_seperator.clone(), |
| colinear_handle_checkbox, |
| related_seperator.clone(), |
| colinear_handles_label, |
| unrelated_seperator.clone(), |
| point_editing_mode, |
| related_seperator.clone(), |
| segment_editing_mode, |
| unrelated_seperator, |
| path_overlay_mode_widget, |
| ], |
| }])) |
| } |
| } |
|
|
| impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathTool { |
| fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) { |
| let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); |
|
|
| match message { |
| ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action { |
| PathOptionsUpdate::OverlayModeType(overlay_mode_type) => { |
| self.options.path_overlay_mode = overlay_mode_type; |
| responses.add(OverlaysMessage::Draw); |
| } |
| PathOptionsUpdate::PointEditingMode { enabled } => { |
| self.options.path_editing_mode.point_editing_mode = enabled; |
| responses.add(OverlaysMessage::Draw); |
| } |
| PathOptionsUpdate::SegmentEditingMode { enabled } => { |
| self.options.path_editing_mode.segment_editing_mode = enabled; |
| responses.add(OverlaysMessage::Draw); |
| } |
| }, |
| ToolMessage::Path(PathToolMessage::ClosePath) => { |
| responses.add(DocumentMessage::AddTransaction); |
| tool_data.shape_editor.close_selected_path(tool_data.document, responses); |
| responses.add(DocumentMessage::EndTransaction); |
| responses.add(OverlaysMessage::Draw); |
| } |
| ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => { |
| if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) { |
| tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface); |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); |
| responses.add(OverlaysMessage::Draw); |
| } |
| } |
| _ => { |
| self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); |
| } |
| } |
|
|
| if updating_point { |
| self.send_layout(responses, LayoutTarget::ToolOptions); |
| } |
| } |
|
|
| |
| fn actions(&self) -> ActionList { |
| match self.fsm_state { |
| PathToolFsmState::Ready => actions!(PathToolMessageDiscriminant; |
| FlipSmoothSharp, |
| MouseDown, |
| Delete, |
| NudgeSelectedPoints, |
| Enter, |
| SelectAllAnchors, |
| DeselectAllPoints, |
| BreakPath, |
| DeleteAndBreakPath, |
| ClosePath, |
| PointerMove, |
| ), |
| PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; |
| Escape, |
| RightClick, |
| FlipSmoothSharp, |
| DragStop, |
| PointerMove, |
| Delete, |
| BreakPath, |
| DeleteAndBreakPath, |
| SwapSelectedHandles, |
| ), |
| PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; |
| FlipSmoothSharp, |
| DragStop, |
| PointerMove, |
| Delete, |
| Enter, |
| BreakPath, |
| DeleteAndBreakPath, |
| Escape, |
| RightClick, |
| ), |
| PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; |
| PointerMove, |
| DragStop, |
| Escape, |
| RightClick |
| ), |
| PathToolFsmState::MoldingSegment => actions!(PathToolMessageDiscriminant; |
| PointerMove, |
| DragStop, |
| RightClick, |
| Escape, |
| ), |
| } |
| } |
| } |
|
|
| impl ToolTransition for PathTool { |
| fn event_to_message_map(&self) -> EventToMessageMap { |
| EventToMessageMap { |
| tool_abort: Some(PathToolMessage::Abort.into()), |
| selection_changed: Some(PathToolMessage::SelectionChanged.into()), |
| overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()), |
| ..Default::default() |
| } |
| } |
| } |
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| pub struct DraggingState { |
| point_select_state: PointSelectState, |
| colinear: ManipulatorAngle, |
| } |
|
|
| #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] |
| pub enum PointSelectState { |
| HandleWithPair, |
| #[default] |
| HandleNoPair, |
| Anchor, |
| } |
|
|
| #[derive(Clone, Copy)] |
| pub struct SlidingSegmentData { |
| segment_id: SegmentId, |
| bezier: Bezier, |
| start: PointId, |
| } |
|
|
| #[derive(Clone, Copy)] |
| pub struct SlidingPointInfo { |
| anchor: PointId, |
| layer: LayerNodeIdentifier, |
| connected_segments: [SlidingSegmentData; 2], |
| } |
|
|
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| enum PathToolFsmState { |
| #[default] |
| Ready, |
| Dragging(DraggingState), |
| Drawing { |
| selection_shape: SelectionShapeType, |
| }, |
| SlidingPoint, |
| MoldingSegment, |
| } |
|
|
| #[derive(Default)] |
| struct PathToolData { |
| snap_manager: SnapManager, |
| lasso_polygon: Vec<DVec2>, |
| selection_mode: Option<SelectionMode>, |
| drag_start_pos: DVec2, |
| previous_mouse_position: DVec2, |
| toggle_colinear_debounce: bool, |
| opposing_handle_lengths: Option<OpposingHandleLengths>, |
| |
| |
| |
| selection_status: SelectionStatus, |
| |
| can_toggle_colinearity: bool, |
| segment: Option<ClosestSegment>, |
| snap_cache: SnapCache, |
| double_click_handled: bool, |
| delete_segment_pressed: bool, |
| auto_panning: AutoPanning, |
| saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>, |
| select_anchor_toggled: bool, |
| saved_points_before_handle_drag: Vec<ManipulatorPointId>, |
| handle_drag_toggle: bool, |
| saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>, |
| last_click_time: u64, |
| dragging_state: DraggingState, |
| angle: f64, |
| opposite_handle_position: Option<DVec2>, |
| last_clicked_point_was_selected: bool, |
| last_clicked_segment_was_selected: bool, |
| snapping_axis: Option<Axis>, |
| alt_clicked_on_anchor: bool, |
| alt_dragging_from_anchor: bool, |
| angle_locked: bool, |
| temporary_colinear_handles: bool, |
| molding_info: Option<(DVec2, DVec2)>, |
| molding_segment: bool, |
| temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>, |
| frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>, |
| adjacent_anchor_offset: Option<DVec2>, |
| sliding_point_info: Option<SlidingPointInfo>, |
| started_drawing_from_inside: bool, |
| } |
|
|
| impl PathToolData { |
| fn save_points_before_anchor_toggle(&mut self, points: Vec<ManipulatorPointId>) -> PathToolFsmState { |
| self.saved_points_before_anchor_select_toggle = points; |
| PathToolFsmState::Dragging(self.dragging_state) |
| } |
|
|
| fn remove_saved_points(&mut self) { |
| self.saved_points_before_anchor_select_toggle.clear(); |
| } |
|
|
| pub fn selection_quad(&self, metadata: &DocumentMetadata) -> Quad { |
| let bbox = self.selection_box(metadata); |
| Quad::from_box(bbox) |
| } |
|
|
| pub fn calculate_selection_mode_from_direction(&mut self, metadata: &DocumentMetadata) -> SelectionMode { |
| let bbox = self.selection_box(metadata); |
| let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2); |
|
|
| if self.selection_mode.is_none() && above_threshold { |
| let mode = if bbox[1].x < bbox[0].x { |
| SelectionMode::Touched |
| } else { |
| |
| SelectionMode::Enclosed |
| }; |
| self.selection_mode = Some(mode); |
| } |
|
|
| self.selection_mode.unwrap_or(SelectionMode::Touched) |
| } |
|
|
| pub fn selection_box(&self, metadata: &DocumentMetadata) -> [DVec2; 2] { |
| |
| let document_to_viewport = metadata.document_to_viewport; |
| let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position); |
| if previous_mouse == self.drag_start_pos { |
| let tolerance = DVec2::splat(SELECTION_TOLERANCE); |
| [self.drag_start_pos - tolerance, self.drag_start_pos + tolerance] |
| } else { |
| [self.drag_start_pos, previous_mouse] |
| } |
| } |
|
|
| fn update_selection_status(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) { |
| let selection_status = get_selection_status(&document.network_interface, shape_editor); |
|
|
| self.can_toggle_colinearity = match &selection_status { |
| SelectionStatus::None => false, |
| SelectionStatus::One(single_selected_point) => { |
| let vector_data = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap(); |
| single_selected_point.id.get_handle_pair(&vector_data).is_some() |
| } |
| SelectionStatus::Multiple(_) => true, |
| }; |
| self.selection_status = selection_status; |
| } |
|
|
| |
| #[allow(clippy::too_many_arguments)] |
| fn mouse_down( |
| &mut self, |
| shape_editor: &mut ShapeState, |
| document: &DocumentMessageHandler, |
| input: &InputPreprocessorMessageHandler, |
| responses: &mut VecDeque<Message>, |
| extend_selection: bool, |
| lasso_select: bool, |
| handle_drag_from_anchor: bool, |
| drag_zero_handle: bool, |
| molding_in_segment_edit: bool, |
| path_overlay_mode: PathOverlayMode, |
| segment_editing_mode: bool, |
| point_editing_mode: bool, |
| ) -> PathToolFsmState { |
| self.double_click_handled = false; |
| self.opposing_handle_lengths = None; |
|
|
| self.drag_start_pos = input.mouse.position; |
|
|
| if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) { |
| self.saved_points_before_anchor_convert_smooth_sharp.clear(); |
| } |
|
|
| self.last_click_time = input.time; |
|
|
| let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>(); |
|
|
| |
| |
| if let Some((already_selected, mut selection_info)) = shape_editor.get_point_selection_state( |
| &document.network_interface, |
| input.mouse.position, |
| SELECTION_THRESHOLD, |
| path_overlay_mode, |
| self.frontier_handles_info.clone(), |
| point_editing_mode, |
| ) { |
| responses.add(DocumentMessage::StartTransaction); |
|
|
| self.last_clicked_point_was_selected = already_selected; |
|
|
| |
| |
| if !(already_selected && extend_selection) { |
| if let Some(updated_selection_info) = shape_editor.change_point_selection( |
| &document.network_interface, |
| input.mouse.position, |
| SELECTION_THRESHOLD, |
| extend_selection, |
| path_overlay_mode, |
| self.frontier_handles_info.clone(), |
| ) { |
| selection_info = updated_selection_info; |
| } |
| } |
|
|
| if let Some(selected_points) = selection_info { |
| self.drag_start_pos = input.mouse.position; |
|
|
| |
| let mut dragging_only_handles = true; |
| for point in &selected_points.points { |
| if matches!(point.point_id, ManipulatorPointId::Anchor(_)) { |
| dragging_only_handles = false; |
| break; |
| } |
| } |
| if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() { |
| self.saved_points_before_handle_drag = old_selection; |
| } |
|
|
| if handle_drag_from_anchor { |
| if let Some((layer, point)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { |
| |
| if let (Some(point_id), Some(vector_data)) = (point.as_anchor(), document.network_interface.compute_modified_vector(layer)) { |
| let handles = vector_data.all_connected(point_id).collect::<Vec<_>>(); |
| self.alt_clicked_on_anchor = true; |
| for handle in &handles { |
| let modification_type = handle.set_relative_position(DVec2::ZERO); |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
| for &handles in &vector_data.colinear_manipulators { |
| if handles.contains(handle) { |
| let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
| } |
| } |
| } |
|
|
| let manipulator_point_id = handles[0].to_manipulator_point(); |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&vec![manipulator_point_id]); |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| } |
| } |
| } |
|
|
| if let Some((Some(point), Some(vector_data))) = shape_editor |
| .find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) |
| .map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer))) |
| { |
| let handles = vector_data |
| .all_connected(point) |
| .filter(|handle| handle.length(&vector_data) < 1e-6) |
| .map(|handle| handle.to_manipulator_point()) |
| .collect::<Vec<_>>(); |
| let endpoint = vector_data.extendable_points(false).any(|anchor| point == anchor); |
|
|
| if drag_zero_handle && (handles.len() == 1 && !endpoint) { |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&handles); |
| shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
| } |
| } |
|
|
| self.start_dragging_point(selected_points, input, document, shape_editor); |
| responses.add(OverlaysMessage::Draw); |
| } |
| PathToolFsmState::Dragging(self.dragging_state) |
| } |
| |
| else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { |
| responses.add(DocumentMessage::StartTransaction); |
|
|
| if segment_editing_mode && !molding_in_segment_edit { |
| let layer = segment.layer(); |
| let segment_id = segment.segment(); |
| let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id)); |
| self.last_clicked_segment_was_selected = already_selected; |
|
|
| if !(already_selected && extend_selection) { |
| let retain_existing_selection = extend_selection || already_selected; |
| if !retain_existing_selection { |
| shape_editor.deselect_all_segments(); |
| shape_editor.deselect_all_points(); |
| } |
|
|
| |
| if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { |
| selected_shape_state.select_segment(segment_id); |
| } |
| } |
|
|
| self.drag_start_pos = input.mouse.position; |
|
|
| let viewport_to_document = document.metadata().document_to_viewport.inverse(); |
| self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position); |
|
|
| responses.add(OverlaysMessage::Draw); |
| PathToolFsmState::Dragging(self.dragging_state) |
| } else { |
| let handle1 = ManipulatorPointId::PrimaryHandle(segment.segment()); |
| let handle2 = ManipulatorPointId::EndHandle(segment.segment()); |
| if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
| if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) { |
| self.molding_info = Some((pos1, pos2)) |
| } |
| } |
| PathToolFsmState::MoldingSegment |
| } |
| } |
| |
| else if let Some(layer) = document.click(input) { |
| shape_editor.deselect_all_points(); |
| shape_editor.deselect_all_segments(); |
| if extend_selection { |
| responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); |
| } else { |
| responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); |
| } |
| self.drag_start_pos = input.mouse.position; |
| self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
|
|
| self.started_drawing_from_inside = true; |
|
|
| let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; |
| PathToolFsmState::Drawing { selection_shape } |
| } |
| |
| else { |
| self.drag_start_pos = input.mouse.position; |
| self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
|
|
| let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; |
| PathToolFsmState::Drawing { selection_shape } |
| } |
| } |
|
|
| fn start_dragging_point(&mut self, selected_points: SelectedPointsInfo, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, shape_editor: &mut ShapeState) { |
| let mut manipulators = HashMap::with_hasher(NoHashBuilder); |
| let mut unselected = Vec::new(); |
| for (&layer, state) in &shape_editor.selected_shape_state { |
| let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { |
| continue; |
| }; |
| let transform = document.metadata().transform_to_document(layer); |
|
|
| let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); |
| for point in state.selected_points() { |
| let Some(anchor) = point.get_anchor(&vector_data) else { continue }; |
| layer_manipulators.insert(anchor); |
| let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue }; |
| let Some(handle) = point.as_handle() else { continue }; |
| |
| let opposite = if handle == handle1 { handle2 } else { handle1 }; |
|
|
| self.opposite_handle_position = if self.opposite_handle_position.is_none() { |
| opposite.to_manipulator_point().get_position(&vector_data) |
| } else { |
| self.opposite_handle_position |
| }; |
| } |
| for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { |
| if layer_manipulators.contains(&id) { |
| continue; |
| } |
| unselected.push(SnapCandidatePoint::handle(transform.transform_point2(position))) |
| } |
| if !layer_manipulators.is_empty() { |
| manipulators.insert(layer, layer_manipulators); |
| } |
| } |
| self.snap_cache = SnapCache { manipulators, unselected }; |
|
|
| let viewport_to_document = document.metadata().document_to_viewport.inverse(); |
| self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset); |
| } |
|
|
| fn update_colinear(&mut self, equidistant: bool, toggle_colinear: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool { |
| |
| let is_colinear = self |
| .selection_status |
| .angle() |
| .map(|angle| match angle { |
| ManipulatorAngle::Colinear => true, |
| ManipulatorAngle::Free | ManipulatorAngle::Mixed => false, |
| }) |
| .unwrap_or(false); |
|
|
| |
| if toggle_colinear && !self.toggle_colinear_debounce { |
| self.opposing_handle_lengths = None; |
| if is_colinear { |
| shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
| } else { |
| shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
| } |
| self.toggle_colinear_debounce = true; |
| return true; |
| } |
| self.toggle_colinear_debounce = toggle_colinear; |
|
|
| if equidistant && self.opposing_handle_lengths.is_none() { |
| if !is_colinear { |
| |
| let Some((_, _, selected_handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) else { |
| self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
| return false; |
| }; |
|
|
| let Some((layer, _)) = shape_editor.selected_shape_state.iter().next() else { |
| self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
| return false; |
| }; |
|
|
| let Some(vector_data) = document.network_interface.compute_modified_vector(*layer) else { |
| self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
| return false; |
| }; |
|
|
| |
| if let Some(handle_pair) = selected_handle_id.get_handle_pair(&vector_data) { |
| let opposite_handle_length = handle_pair.iter().filter(|&&h| h.to_manipulator_point() != selected_handle_id).find_map(|&h| { |
| let opp_handle_pos = h.to_manipulator_point().get_position(&vector_data)?; |
| let opp_anchor_id = h.to_manipulator_point().get_anchor(&vector_data)?; |
| let opp_anchor_pos = vector_data.point_domain.position_from_id(opp_anchor_id)?; |
| Some((opp_handle_pos - opp_anchor_pos).length()) |
| }); |
|
|
| |
| if opposite_handle_length == Some(0.) { |
| shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
| return true; |
| } |
| } |
| } |
| self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); |
| } |
| false |
| } |
|
|
| |
| fn try_get_selected_handle_and_anchor(&self, shape_editor: &ShapeState, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2, ManipulatorPointId)> { |
| |
| let (layer, selection) = shape_editor.selected_shape_state.iter().next()?; |
|
|
| |
| if selection.selected_points_count() != 1 { |
| return None; |
| } |
|
|
| |
| let selected_handle = selection.selected_points().next()?.as_handle()?; |
| let handle_id = selected_handle.to_manipulator_point(); |
|
|
| let layer_to_document = document.metadata().transform_to_document(*layer); |
| let vector_data = document.network_interface.compute_modified_vector(*layer)?; |
|
|
| let handle_position_local = selected_handle.to_manipulator_point().get_position(&vector_data)?; |
| let anchor_id = selected_handle.to_manipulator_point().get_anchor(&vector_data)?; |
| let anchor_position_local = vector_data.point_domain.position_from_id(anchor_id)?; |
|
|
| let handle_position_document = layer_to_document.transform_point2(handle_position_local); |
| let anchor_position_document = layer_to_document.transform_point2(anchor_position_local); |
|
|
| Some((handle_position_document, anchor_position_document, handle_id)) |
| } |
|
|
| #[allow(clippy::too_many_arguments)] |
| fn calculate_handle_angle( |
| &mut self, |
| shape_editor: &mut ShapeState, |
| document: &DocumentMessageHandler, |
| responses: &mut VecDeque<Message>, |
| relative_vector: DVec2, |
| handle_vector: DVec2, |
| handle_id: ManipulatorPointId, |
| lock_angle: bool, |
| snap_angle: bool, |
| tangent_to_neighboring_tangents: bool, |
| ) -> f64 { |
| let current_angle = -handle_vector.angle_to(DVec2::X); |
|
|
| if let Some((vector_data, layer)) = shape_editor |
| .selected_shape_state |
| .iter() |
| .next() |
| .and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer).map(|vector_data| (vector_data, layer))) |
| { |
| let adjacent_anchor = check_handle_over_adjacent_anchor(handle_id, &vector_data); |
| let mut required_angle = None; |
|
|
| |
| if adjacent_anchor.is_some() && lock_angle && !self.angle_locked { |
| let anchor = handle_id.get_anchor(&vector_data); |
| let (angle, anchor_position) = calculate_adjacent_anchor_tangent(handle_id, anchor, adjacent_anchor, &vector_data); |
|
|
| let layer_to_document = document.metadata().transform_to_document(*layer); |
|
|
| self.adjacent_anchor_offset = handle_id |
| .get_anchor_position(&vector_data) |
| .and_then(|handle_anchor| anchor_position.map(|adjacent_anchor| layer_to_document.transform_point2(adjacent_anchor) - layer_to_document.transform_point2(handle_anchor))); |
|
|
| required_angle = angle; |
| } |
|
|
| |
| if relative_vector.length() < 25. && lock_angle && !self.angle_locked { |
| required_angle = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents); |
| } |
|
|
| |
| if let Some(angle) = required_angle { |
| self.angle = angle; |
| self.angle_locked = true; |
| return angle; |
| } |
| } |
|
|
| if lock_angle && !self.angle_locked { |
| self.angle_locked = true; |
| self.angle = -relative_vector.angle_to(DVec2::X); |
| return -relative_vector.angle_to(DVec2::X); |
| } |
|
|
| |
| if self.angle_locked { |
| return self.angle; |
| } |
|
|
| |
| let mut handle_angle = current_angle; |
| if snap_angle && !lock_angle { |
| let snap_resolution = HANDLE_ROTATE_SNAP_ANGLE.to_radians(); |
| handle_angle = (handle_angle / snap_resolution).round() * snap_resolution; |
| } |
|
|
| self.angle = handle_angle; |
|
|
| handle_angle |
| } |
|
|
| #[allow(clippy::too_many_arguments)] |
| fn apply_snapping( |
| &mut self, |
| handle_direction: DVec2, |
| new_handle_position: DVec2, |
| anchor_position: DVec2, |
| using_angle_constraints: bool, |
| handle_position: DVec2, |
| document: &DocumentMessageHandler, |
| input: &InputPreprocessorMessageHandler, |
| ) -> DVec2 { |
| let snap_data = SnapData::new(document, input); |
| let snap_point = SnapCandidatePoint::handle_neighbors(new_handle_position, [anchor_position]); |
|
|
| let snap_result = match using_angle_constraints { |
| true => { |
| let snap_constraint = SnapConstraint::Line { |
| origin: anchor_position, |
| direction: handle_direction.normalize_or_zero(), |
| }; |
|
|
| self.snap_manager.constrained_snap(&snap_data, &snap_point, snap_constraint, Default::default()) |
| } |
| false => self.snap_manager.free_snap(&snap_data, &snap_point, Default::default()), |
| }; |
|
|
| self.snap_manager.update_indicator(snap_result.clone()); |
|
|
| document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position) |
| } |
|
|
| fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { |
| |
| let current_mouse = input.mouse.position; |
| let drag_start = self.drag_start_pos; |
| let opposite_delta = drag_start - current_mouse; |
|
|
| shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses); |
|
|
| |
| let delta = current_mouse - drag_start; |
| let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y }; |
| self.snapping_axis = Some(axis); |
| let projected_delta = match axis { |
| Axis::X => DVec2::new(delta.x, 0.), |
| Axis::Y => DVec2::new(0., delta.y), |
| _ => DVec2::new(delta.x, 0.), |
| }; |
|
|
| shape_editor.move_selected_points_and_segments(None, document, projected_delta, false, true, false, None, false, responses); |
| } |
|
|
| fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { |
| |
| let current_mouse = input.mouse.position; |
| let drag_start = self.drag_start_pos; |
|
|
| let opposite_delta = drag_start - current_mouse; |
| let Some(axis) = self.snapping_axis else { return }; |
| let opposite_projected_delta = match axis { |
| Axis::X => DVec2::new(opposite_delta.x, 0.), |
| Axis::Y => DVec2::new(0., opposite_delta.y), |
| _ => DVec2::new(opposite_delta.x, 0.), |
| }; |
|
|
| shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses); |
|
|
| |
| let delta = current_mouse - drag_start; |
|
|
| shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses); |
|
|
| self.snapping_axis = None; |
| } |
|
|
| fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector_data: &VectorData) -> Option<DVec2> { |
| let other_point = vector_data.other_point(segment, point)?; |
| let position = ManipulatorPointId::Anchor(point).get_position(vector_data)?; |
|
|
| let mut handles = vector_data.all_connected(other_point); |
| let other_handle = handles.find(|handle| handle.segment == segment)?; |
|
|
| let target_position = if other_handle.length(vector_data) == 0. { |
| ManipulatorPointId::Anchor(other_point).get_position(vector_data)? |
| } else { |
| other_handle.to_manipulator_point().get_position(vector_data)? |
| }; |
|
|
| let tangent_vector = target_position - position; |
| tangent_vector.try_normalize() |
| } |
|
|
| fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) { |
| |
| if shape_editor |
| .find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone()) |
| .is_some() |
| { |
| self.segment = None; |
| } |
| |
| else if let Some(closest_segment) = &mut self.segment { |
| closest_segment.update_closest_point(document.metadata(), position); |
|
|
| if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) { |
| self.segment = None; |
| } |
| } |
| |
| else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, position, SEGMENT_INSERTION_DISTANCE) { |
| self.segment = Some(closest_segment); |
| } |
| } |
|
|
| fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool { |
| let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
|
|
| if single_anchor_selected { |
| let Some(anchor) = shape_editor.selected_points().next() else { return false }; |
| let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else { |
| return false; |
| }; |
| let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { |
| return false; |
| }; |
|
|
| |
| if !vector_data.colinear(*anchor) { |
| return false; |
| }; |
|
|
| let Some(point_id) = anchor.as_anchor() else { return false }; |
|
|
| let mut connected_segments = [None, None]; |
| for (segment, bezier, start, end) in vector_data.segment_bezier_iter() { |
| if start == point_id || end == point_id { |
| match (connected_segments[0], connected_segments[1]) { |
| (None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), |
| (Some(_), None) => connected_segments[1] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), |
| _ => { |
| warn!("more than two segments connected to the anchor point"); |
| return false; |
| } |
| } |
| } |
| } |
| let connected_segments = if let [Some(seg1), Some(seg2)] = connected_segments { |
| [seg1, seg2] |
| } else { |
| warn!("expected exactly two connected segments"); |
| return false; |
| }; |
|
|
| self.sliding_point_info = Some(SlidingPointInfo { |
| anchor: point_id, |
| layer, |
| connected_segments, |
| }); |
| return true; |
| } |
| false |
| } |
|
|
| fn slide_point(&mut self, target_position: DVec2, responses: &mut VecDeque<Message>, network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) { |
| let Some(sliding_point_info) = self.sliding_point_info else { return }; |
| let anchor = sliding_point_info.anchor; |
| let layer = sliding_point_info.layer; |
|
|
| let Some(vector_data) = network_interface.compute_modified_vector(layer) else { return }; |
| let transform = network_interface.document_metadata().transform_to_viewport(layer); |
| let layer_pos = transform.inverse().transform_point2(target_position); |
|
|
| let segments = sliding_point_info.connected_segments; |
|
|
| let t1 = segments[0].bezier.project(layer_pos); |
| let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1)); |
|
|
| let t2 = segments[1].bezier.project(layer_pos); |
| let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2)); |
|
|
| let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) { |
| (segments[1], segments[0], t2, position2) |
| } else { |
| (segments[0], segments[1], t1, position1) |
| }; |
|
|
| |
| let Some(current_position) = ManipulatorPointId::Anchor(anchor).get_position(&vector_data) else { |
| return; |
| }; |
| let delta = new_position - current_position; |
|
|
| shape_editor.move_anchor(anchor, &vector_data, delta, layer, None, responses); |
|
|
| |
| let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value)); |
| let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start }; |
|
|
| let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) }; |
|
|
| |
| let closer_primary_handle = HandleId::primary(closer_segment.segment_id); |
| let Some(handle_position) = split_segment.handle_start() else { return }; |
| let relative_position1 = handle_position - split_segment.start; |
| let modification_type = closer_primary_handle.set_relative_position(relative_position1); |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
| let closer_secondary_handle = HandleId::end(closer_segment.segment_id); |
| let Some(handle_position) = split_segment.handle_end() else { return }; |
| let relative_position2 = handle_position - split_segment.end; |
| let modification_type = closer_secondary_handle.set_relative_position(relative_position2); |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
| let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 }; |
|
|
| let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start { |
| ( |
| farther_segment.bezier.end, |
| HandleId::end(farther_segment.segment_id), |
| HandleId::primary(farther_segment.segment_id), |
| farther_segment.bezier.handle_end(), |
| ) |
| } else { |
| ( |
| farther_segment.bezier.start, |
| HandleId::primary(farther_segment.segment_id), |
| HandleId::end(farther_segment.segment_id), |
| farther_segment.bezier.handle_start(), |
| ) |
| }; |
| let Some(start_handle_position) = start_handle_pos else { return }; |
| let start_handle_direction = start_handle_position - farther_other_point; |
|
|
| |
| let d1 = start_handle_direction.try_normalize().unwrap_or({ |
| if anchor == farther_segment.start { |
| -farther_segment.bezier.tangent(TValue::Parametric(0.99)) |
| } else { |
| farther_segment.bezier.tangent(TValue::Parametric(0.01)) |
| } |
| }); |
|
|
| let d2 = end_handle_direction.try_normalize().unwrap_or_default(); |
|
|
| let min_len1 = start_handle_direction.length() * 0.4; |
| let min_len2 = end_handle_direction.length() * 0.4; |
|
|
| let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment); |
|
|
| |
| let modification_type = start_handle.set_relative_position(relative_pos1); |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
|
|
| let modification_type = end_handle.set_relative_position(relative_pos2); |
| responses.add(GraphOperationMessage::Vector { layer, modification_type }); |
| } |
|
|
| #[allow(clippy::too_many_arguments)] |
| fn drag( |
| &mut self, |
| equidistant: bool, |
| lock_angle: bool, |
| snap_angle: bool, |
| snap_axis: bool, |
| shape_editor: &mut ShapeState, |
| document: &DocumentMessageHandler, |
| input: &InputPreprocessorMessageHandler, |
| responses: &mut VecDeque<Message>, |
| ) { |
| |
| let selected_points = shape_editor.selected_points(); |
| let single_handle_selected = selected_points.count() == 1 |
| && shape_editor |
| .selected_points() |
| .any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_))); |
|
|
| |
| if snap_axis && self.snapping_axis.is_none() && !single_handle_selected { |
| self.start_snap_along_axis(shape_editor, document, input, responses); |
| } else if !snap_axis && self.snapping_axis.is_some() { |
| self.stop_snap_along_axis(shape_editor, document, input, responses); |
| } |
|
|
| let document_to_viewport = document.metadata().document_to_viewport; |
| let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position); |
| let current_mouse = input.mouse.position; |
| let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse); |
|
|
| let snapped_delta = if let Some((handle_position, anchor_position, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) { |
| let cursor_position = handle_position + raw_delta; |
|
|
| let handle_angle = self.calculate_handle_angle( |
| shape_editor, |
| document, |
| responses, |
| handle_position - anchor_position, |
| cursor_position - anchor_position, |
| handle_id, |
| lock_angle, |
| snap_angle, |
| equidistant, |
| ); |
|
|
| let adjacent_anchor_offset = self.adjacent_anchor_offset.unwrap_or(DVec2::ZERO); |
| let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin()); |
| let projected_length = (cursor_position - anchor_position - adjacent_anchor_offset).dot(constrained_direction); |
| let constrained_target = anchor_position + adjacent_anchor_offset + constrained_direction * projected_length; |
| let constrained_delta = constrained_target - handle_position; |
|
|
| self.apply_snapping( |
| constrained_direction, |
| handle_position + constrained_delta, |
| anchor_position + adjacent_anchor_offset, |
| lock_angle || snap_angle, |
| handle_position, |
| document, |
| input, |
| ) |
| } else { |
| shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse) |
| }; |
|
|
| let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() }; |
| let opposite = if lock_angle { None } else { self.opposite_handle_position }; |
| let unsnapped_delta = current_mouse - previous_mouse; |
| let mut was_alt_dragging = false; |
|
|
| if self.snapping_axis.is_none() { |
| if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
| |
| self.alt_dragging_from_anchor = true; |
| let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else { |
| return; |
| }; |
| let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return }; |
| let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector_data) else { |
| return; |
| }; |
|
|
| if vector_data.connected_count(point_id) == 2 { |
| let connected_segments: Vec<HandleId> = vector_data.all_connected(point_id).collect(); |
| let segment1 = connected_segments[0]; |
| let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector_data) else { |
| return; |
| }; |
| let segment2 = connected_segments[1]; |
| let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector_data) else { |
| return; |
| }; |
|
|
| let delta = input.mouse.position - self.drag_start_pos; |
| let handle = if delta.dot(tangent1) >= delta.dot(tangent2) { |
| segment1.to_manipulator_point() |
| } else { |
| segment2.to_manipulator_point() |
| }; |
|
|
| |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&vec![handle]); |
| responses.add(PathToolMessage::SelectionChanged); |
| } |
| } |
|
|
| if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor { |
| was_alt_dragging = true; |
| self.alt_dragging_from_anchor = false; |
| self.alt_clicked_on_anchor = false; |
| } |
|
|
| let mut skip_opposite = false; |
| if self.temporary_colinear_handles && !lock_angle { |
| shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
| self.temporary_colinear_handles = false; |
| skip_opposite = true; |
| } |
| shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses); |
| self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); |
| } else { |
| let Some(axis) = self.snapping_axis else { return }; |
| let projected_delta = match axis { |
| Axis::X => DVec2::new(unsnapped_delta.x, 0.), |
| Axis::Y => DVec2::new(0., unsnapped_delta.y), |
| _ => DVec2::new(unsnapped_delta.x, 0.), |
| }; |
| shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses); |
| self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); |
| } |
|
|
| |
| if snap_axis && self.snapping_axis.is_some() { |
| let Some(current_axis) = self.snapping_axis else { return }; |
| let total_delta = self.drag_start_pos - input.mouse.position; |
|
|
| if (total_delta.x.abs() > total_delta.y.abs() && current_axis == Axis::Y) || (total_delta.y.abs() > total_delta.x.abs() && current_axis == Axis::X) { |
| self.stop_snap_along_axis(shape_editor, document, input, responses); |
| self.start_snap_along_axis(shape_editor, document, input, responses); |
| } |
| } |
| } |
| } |
|
|
| impl Fsm for PathToolFsmState { |
| type ToolData = PathToolData; |
| type ToolOptions = PathToolOptions; |
|
|
| 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, input, shape_editor, .. } = tool_action_data; |
|
|
| update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options); |
|
|
| let ToolMessage::Path(event) = event else { return self }; |
| match (self, event) { |
| (_, PathToolMessage::SelectionChanged) => { |
| |
| let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect(); |
| shape_editor.set_selected_layers(target_layers); |
|
|
| responses.add(OverlaysMessage::Draw); |
| self |
| } |
| (_, PathToolMessage::UpdateSelectedPointsStatus { overlay_context }) => { |
| let display_anchors = overlay_context.visibility_settings.anchors(); |
| let display_handles = overlay_context.visibility_settings.handles(); |
|
|
| shape_editor.update_selected_anchors_status(display_anchors); |
| shape_editor.update_selected_handles_status(display_handles); |
|
|
| self |
| } |
| (_, PathToolMessage::Overlays(mut overlay_context)) => { |
| |
|
|
| match tool_options.path_overlay_mode { |
| PathOverlayMode::AllHandles => { |
| path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); |
| tool_data.frontier_handles_info = None; |
| } |
| PathOverlayMode::SelectedPointHandles => { |
| let selected_segments = selected_segments(&document.network_interface, shape_editor); |
|
|
| path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); |
| tool_data.frontier_handles_info = None; |
| } |
| PathOverlayMode::FrontierHandles => { |
| let selected_segments = selected_segments(&document.network_interface, shape_editor); |
| let selected_points = shape_editor.selected_points(); |
| let selected_anchors = selected_points |
| .filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None }) |
| .collect::<Vec<_>>(); |
|
|
| |
| if shape_editor.selected_points().count() == 1 { |
| path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); |
| } else { |
| let mut segment_endpoints: HashMap<SegmentId, Vec<PointId>> = HashMap::new(); |
|
|
| 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 mut selected_segments_by_point: HashMap<PointId, Vec<SegmentId>> = HashMap::new(); |
|
|
| for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() { |
| if selected_segments.contains(&segment_id) { |
| selected_segments_by_point.entry(start).or_default().push(segment_id); |
| selected_segments_by_point.entry(end).or_default().push(segment_id); |
| } |
| } |
|
|
| for (point, attached_segments) in selected_segments_by_point { |
| if attached_segments.len() == 1 { |
| segment_endpoints.entry(attached_segments[0]).or_default().push(point); |
| } |
| |
| else if !selected_anchors.contains(&point) { |
| segment_endpoints.entry(attached_segments[0]).or_default().push(point); |
| segment_endpoints.entry(attached_segments[1]).or_default().push(point); |
| } |
| } |
| } |
|
|
| |
| tool_data.frontier_handles_info = Some(segment_endpoints.clone()); |
|
|
| |
| path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context); |
| } |
| } |
| } |
|
|
| match self { |
| Self::Ready => { |
| tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); |
|
|
| if let Some(closest_segment) = &tool_data.segment { |
| if tool_options.path_editing_mode.segment_editing_mode { |
| let transform = document.metadata().transform_to_viewport(closest_segment.layer()); |
|
|
| overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform); |
|
|
| |
| let display_anchors = overlay_context.visibility_settings.anchors(); |
| if display_anchors { |
| let start_pos = transform.transform_point2(closest_segment.bezier().start); |
| let end_pos = transform.transform_point2(closest_segment.bezier().end); |
| let start_id = closest_segment.points()[0]; |
| let end_id = closest_segment.points()[1]; |
| if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) { |
| overlay_context.manipulator_anchor(start_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(start_id)), None); |
| overlay_context.manipulator_anchor(end_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(end_id)), None); |
| } |
| } |
| } else { |
| let perp = closest_segment.calculate_perp(document); |
| let point = closest_segment.closest_point(document.metadata()); |
|
|
| |
| if tool_data.delete_segment_pressed { |
| let angle = 45_f64.to_radians(); |
| let tilted_line = DVec2::from_angle(angle).rotate(perp); |
| let tilted_perp = tilted_line.perp(); |
|
|
| overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
| overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
| } |
| |
| else { |
| overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); |
| } |
| } |
| } |
| } |
| Self::Drawing { selection_shape } => { |
| let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) |
| .unwrap() |
| .with_alpha(0.05) |
| .to_rgba_hex_srgb(); |
| fill_color.insert(0, '#'); |
| let fill_color = Some(fill_color.as_str()); |
|
|
| let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
| SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
| selection_mode => selection_mode, |
| }; |
|
|
| let quad = tool_data.selection_quad(document.metadata()); |
| let polygon = &tool_data.lasso_polygon; |
|
|
| match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) { |
| |
| (SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), |
| (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), |
| (SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color), |
| (SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color), |
| (SelectionShapeType::Box, _, _) => {} |
| } |
| } |
| Self::Dragging(_) => { |
| tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); |
|
|
| |
| if tool_data.snapping_axis.is_some() { |
| let Some(axis) = tool_data.snapping_axis else { return self }; |
| let origin = tool_data.drag_start_pos; |
| let viewport_diagonal = input.viewport_bounds.size().length(); |
|
|
| let faded = |color: &str| { |
| let mut color = graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).to_rgba_hex_srgb(); |
| color.insert(0, '#'); |
| color |
| }; |
| match axis { |
| Axis::Y => { |
| overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(COLOR_OVERLAY_GREEN), None); |
| overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(&faded(COLOR_OVERLAY_RED)), None); |
| } |
| Axis::X | Axis::Both => { |
| overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(COLOR_OVERLAY_RED), None); |
| overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(&faded(COLOR_OVERLAY_GREEN)), None); |
| } |
| } |
| } |
| } |
| Self::SlidingPoint => {} |
| Self::MoldingSegment => {} |
| } |
|
|
| responses.add(PathToolMessage::SelectedPointUpdated); |
| responses.add(PathToolMessage::UpdateSelectedPointsStatus { overlay_context }); |
| self |
| } |
|
|
| |
| ( |
| _, |
| PathToolMessage::MouseDown { |
| extend_selection, |
| lasso_select, |
| handle_drag_from_anchor, |
| drag_restore_handle, |
| molding_in_segment_edit, |
| }, |
| ) => { |
| let extend_selection = input.keyboard.get(extend_selection as usize); |
| let lasso_select = input.keyboard.get(lasso_select as usize); |
| let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize); |
| let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize); |
| let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize); |
|
|
| tool_data.selection_mode = None; |
| tool_data.lasso_polygon.clear(); |
|
|
| tool_data.mouse_down( |
| shape_editor, |
| document, |
| input, |
| responses, |
| extend_selection, |
| lasso_select, |
| handle_drag_from_anchor, |
| drag_zero_handle, |
| molding_in_segment_edit, |
| tool_options.path_overlay_mode, |
| tool_options.path_editing_mode.segment_editing_mode, |
| tool_options.path_editing_mode.point_editing_mode, |
| ) |
| } |
| ( |
| PathToolFsmState::Drawing { selection_shape }, |
| PathToolMessage::PointerMove { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| }, |
| ) => { |
| tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); |
| tool_data.started_drawing_from_inside = false; |
|
|
| if selection_shape == SelectionShapeType::Lasso { |
| extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position); |
| } |
|
|
| responses.add(OverlaysMessage::Draw); |
|
|
| |
| let messages = [ |
| PathToolMessage::PointerOutsideViewport { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| PathToolMessage::PointerMove { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| ]; |
| tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
| PathToolFsmState::Drawing { selection_shape } |
| } |
| ( |
| PathToolFsmState::Dragging(_), |
| PathToolMessage::PointerMove { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| }, |
| ) => { |
| let mut selected_only_handles = true; |
|
|
| let selected_points = shape_editor.selected_points(); |
|
|
| for point in selected_points { |
| if matches!(point, ManipulatorPointId::Anchor(_)) { |
| selected_only_handles = false; |
| break; |
| } |
| } |
|
|
| if !tool_data.saved_points_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) { |
| tool_data.handle_drag_toggle = true; |
| } |
|
|
| let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize); |
| let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled; |
| let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled; |
|
|
| if initial_press { |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| tool_data.select_anchor_toggled = true; |
| tool_data.save_points_before_anchor_toggle(shape_editor.selected_points().cloned().collect()); |
| shape_editor.select_handles_and_anchor_connected_to_current_handle(&document.network_interface); |
| } else if released_from_toggle { |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| tool_data.select_anchor_toggled = false; |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); |
| tool_data.remove_saved_points(); |
| } |
|
|
| let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize); |
| let equidistant_state = input.keyboard.get(equidistant as usize); |
| let lock_angle_state = input.keyboard.get(lock_angle as usize); |
| let snap_angle_state = input.keyboard.get(snap_angle as usize); |
|
|
| if !lock_angle_state { |
| tool_data.angle_locked = false; |
| tool_data.adjacent_anchor_offset = None; |
| } |
|
|
| if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) { |
| if snap_angle_state && lock_angle_state && tool_data.start_sliding_point(tool_action_data.shape_editor, tool_action_data.document) { |
| return PathToolFsmState::SlidingPoint; |
| } |
|
|
| tool_data.drag( |
| equidistant_state, |
| lock_angle_state, |
| snap_angle_state, |
| snap_angle_state, |
| tool_action_data.shape_editor, |
| tool_action_data.document, |
| input, |
| responses, |
| ); |
| } |
|
|
| |
| let messages = [ |
| PathToolMessage::PointerOutsideViewport { |
| toggle_colinear, |
| equidistant, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| PathToolMessage::PointerMove { |
| toggle_colinear, |
| equidistant, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| ]; |
| tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); |
|
|
| PathToolFsmState::Dragging(tool_data.dragging_state) |
| } |
| (PathToolFsmState::SlidingPoint, PathToolMessage::PointerMove { .. }) => { |
| tool_data.slide_point(input.mouse.position, responses, &document.network_interface, shape_editor); |
| PathToolFsmState::SlidingPoint |
| } |
| (PathToolFsmState::MoldingSegment, PathToolMessage::PointerMove { break_colinear_molding, .. }) => { |
| if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
| tool_data.molding_segment = true; |
| } |
|
|
| let break_colinear_molding = input.keyboard.get(break_colinear_molding as usize); |
|
|
| |
| if let Some(segment) = &mut tool_data.segment { |
| if let Some(molding_segment_handles) = tool_data.molding_info { |
| tool_data.temporary_adjacent_handles_while_molding = segment.mold_handle_positions( |
| document, |
| responses, |
| molding_segment_handles, |
| input.mouse.position, |
| break_colinear_molding, |
| tool_data.temporary_adjacent_handles_while_molding, |
| ); |
| } |
| } |
|
|
| PathToolFsmState::MoldingSegment |
| } |
| (PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => { |
| tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize); |
|
|
| if !tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() { |
| tool_data.saved_points_before_anchor_convert_smooth_sharp.clear(); |
| } |
|
|
| if tool_data.adjacent_anchor_offset.is_some() { |
| tool_data.adjacent_anchor_offset = None; |
| } |
|
|
| responses.add(OverlaysMessage::Draw); |
|
|
| self |
| } |
| (PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => { |
| |
| if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { |
| tool_data.drag_start_pos += offset; |
| } |
|
|
| PathToolFsmState::Drawing { selection_shape: selection_type } |
| } |
| (PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { .. }) => { |
| |
| if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { |
| tool_data.drag_start_pos += offset; |
| } |
|
|
| PathToolFsmState::Dragging(dragging_state) |
| } |
| ( |
| state, |
| PathToolMessage::PointerOutsideViewport { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| }, |
| ) => { |
| |
| let messages = [ |
| PathToolMessage::PointerOutsideViewport { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| PathToolMessage::PointerMove { |
| equidistant, |
| toggle_colinear, |
| move_anchor_with_handles, |
| snap_angle, |
| lock_angle, |
| delete_segment, |
| break_colinear_molding, |
| } |
| .into(), |
| ]; |
| tool_data.auto_panning.stop(&messages, responses); |
|
|
| state |
| } |
| (PathToolFsmState::Drawing { selection_shape }, PathToolMessage::Enter { extend_selection, shrink_selection }) => { |
| let extend_selection = input.keyboard.get(extend_selection as usize); |
| let shrink_selection = input.keyboard.get(shrink_selection as usize); |
|
|
| let selection_change = if shrink_selection { |
| SelectionChange::Shrink |
| } else if extend_selection { |
| SelectionChange::Extend |
| } else { |
| SelectionChange::Clear |
| }; |
|
|
| let document_to_viewport = document.metadata().document_to_viewport; |
| let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); |
| if tool_data.drag_start_pos == previous_mouse { |
| responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); |
| } else { |
| let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
| SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
| selection_mode => selection_mode, |
| }; |
|
|
| match selection_shape { |
| SelectionShapeType::Box => { |
| let bbox = [tool_data.drag_start_pos, previous_mouse]; |
| shape_editor.select_all_in_shape( |
| &document.network_interface, |
| SelectionShape::Box(bbox), |
| selection_change, |
| tool_options.path_overlay_mode, |
| tool_data.frontier_handles_info.clone(), |
| tool_options.path_editing_mode.segment_editing_mode, |
| selection_mode, |
| ); |
| } |
| SelectionShapeType::Lasso => shape_editor.select_all_in_shape( |
| &document.network_interface, |
| SelectionShape::Lasso(&tool_data.lasso_polygon), |
| selection_change, |
| tool_options.path_overlay_mode, |
| tool_data.frontier_handles_info.clone(), |
| tool_options.path_editing_mode.segment_editing_mode, |
| selection_mode, |
| ), |
| } |
| } |
|
|
| responses.add(OverlaysMessage::Draw); |
|
|
| PathToolFsmState::Ready |
| } |
| (PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
| if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { |
| shape_editor.deselect_all_points(); |
| shape_editor.deselect_all_segments(); |
| shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); |
|
|
| tool_data.saved_points_before_handle_drag.clear(); |
| tool_data.handle_drag_toggle = false; |
| } |
| tool_data.angle_locked = false; |
| responses.add(DocumentMessage::AbortTransaction); |
| tool_data.snap_manager.cleanup(responses); |
| PathToolFsmState::Ready |
| } |
| (PathToolFsmState::Drawing { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
| tool_data.snap_manager.cleanup(responses); |
| PathToolFsmState::Ready |
| } |
| (PathToolFsmState::SlidingPoint, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
| tool_data.sliding_point_info = None; |
|
|
| responses.add(DocumentMessage::AbortTransaction); |
| tool_data.snap_manager.cleanup(responses); |
|
|
| PathToolFsmState::Ready |
| } |
| (PathToolFsmState::MoldingSegment, PathToolMessage::Escape | PathToolMessage::RightClick) => { |
| |
| tool_data.molding_info = None; |
| tool_data.molding_segment = false; |
| tool_data.temporary_adjacent_handles_while_molding = None; |
|
|
| responses.add(DocumentMessage::AbortTransaction); |
| tool_data.snap_manager.cleanup(responses); |
|
|
| PathToolFsmState::Ready |
| } |
| |
| (PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => { |
| let extend_selection = input.keyboard.get(extend_selection as usize); |
| let shrink_selection = input.keyboard.get(shrink_selection as usize); |
|
|
| let select_kind = if shrink_selection { |
| SelectionChange::Shrink |
| } else if extend_selection { |
| SelectionChange::Extend |
| } else { |
| SelectionChange::Clear |
| }; |
|
|
| let document_to_viewport = document.metadata().document_to_viewport; |
| let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position); |
|
|
| let selection_mode = match tool_action_data.preferences.get_selection_mode() { |
| SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()), |
| selection_mode => selection_mode, |
| }; |
|
|
| if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 { |
| |
| if document.click(input).is_none() { |
| responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); |
| } |
| } else { |
| match selection_shape { |
| SelectionShapeType::Box => { |
| let bbox = [tool_data.drag_start_pos, previous_mouse]; |
| shape_editor.select_all_in_shape( |
| &document.network_interface, |
| SelectionShape::Box(bbox), |
| select_kind, |
| tool_options.path_overlay_mode, |
| tool_data.frontier_handles_info.clone(), |
| tool_options.path_editing_mode.segment_editing_mode, |
| selection_mode, |
| ); |
| } |
| SelectionShapeType::Lasso => shape_editor.select_all_in_shape( |
| &document.network_interface, |
| SelectionShape::Lasso(&tool_data.lasso_polygon), |
| select_kind, |
| tool_options.path_overlay_mode, |
| tool_data.frontier_handles_info.clone(), |
| tool_options.path_editing_mode.segment_editing_mode, |
| selection_mode, |
| ), |
| } |
| } |
| responses.add(OverlaysMessage::Draw); |
| responses.add(PathToolMessage::SelectedPointUpdated); |
|
|
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::DragStop { extend_selection, .. }) => { |
| let extend_selection = input.keyboard.get(extend_selection as usize); |
| let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD; |
|
|
| let nearest_point = shape_editor.find_nearest_visible_point_indices( |
| &document.network_interface, |
| input.mouse.position, |
| SELECTION_THRESHOLD, |
| tool_options.path_overlay_mode, |
| tool_data.frontier_handles_info.clone(), |
| ); |
|
|
| let nearest_segment = tool_data.segment.clone(); |
|
|
| if let Some(segment) = &mut tool_data.segment { |
| let segment_mode = tool_options.path_editing_mode.segment_editing_mode; |
| if !drag_occurred && !tool_data.molding_segment && !segment_mode { |
| if tool_data.delete_segment_pressed { |
| if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
| shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points()); |
| } |
| } else { |
| segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); |
| } |
| } |
|
|
| tool_data.segment = None; |
| tool_data.molding_info = None; |
| tool_data.molding_segment = false; |
| tool_data.temporary_adjacent_handles_while_molding = None; |
| } |
|
|
| let segment_mode = tool_options.path_editing_mode.segment_editing_mode; |
|
|
| if let Some((layer, nearest_point)) = nearest_point { |
| let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); |
| if !drag_occurred && extend_selection { |
| if clicked_selected && tool_data.last_clicked_point_was_selected { |
| shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point); |
| } else { |
| shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point); |
| } |
| responses.add(OverlaysMessage::Draw); |
| } |
| if !drag_occurred && !extend_selection && clicked_selected { |
| if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() { |
| tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>(); |
| } |
|
|
| shape_editor.deselect_all_points(); |
| shape_editor.deselect_all_segments(); |
|
|
| shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point); |
|
|
| responses.add(OverlaysMessage::Draw); |
| } |
| } |
| |
| else if let Some(nearest_segment) = nearest_segment { |
| if segment_mode { |
| let clicked_selected = shape_editor.selected_segments().any(|&segment| segment == nearest_segment.segment()); |
| if !drag_occurred && extend_selection { |
| if clicked_selected && tool_data.last_clicked_segment_was_selected { |
| shape_editor |
| .selected_shape_state |
| .entry(nearest_segment.layer()) |
| .or_default() |
| .deselect_segment(nearest_segment.segment()); |
| } else { |
| shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment()); |
| } |
|
|
| responses.add(OverlaysMessage::Draw); |
| } |
| if !drag_occurred && !extend_selection && clicked_selected { |
| shape_editor.deselect_all_segments(); |
| shape_editor.deselect_all_points(); |
| shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment()); |
|
|
| responses.add(OverlaysMessage::Draw); |
| } |
| } |
| } |
| |
| else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { |
| shape_editor.deselect_all_points(); |
| shape_editor.deselect_all_segments(); |
| } |
|
|
| if tool_data.temporary_colinear_handles { |
| tool_data.temporary_colinear_handles = false; |
| } |
|
|
| if tool_data.handle_drag_toggle && drag_occurred { |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); |
|
|
| tool_data.saved_points_before_handle_drag.clear(); |
| tool_data.handle_drag_toggle = false; |
| } |
|
|
| tool_data.alt_dragging_from_anchor = false; |
| tool_data.alt_clicked_on_anchor = false; |
| tool_data.angle_locked = false; |
|
|
| if tool_data.select_anchor_toggled { |
| shape_editor.deselect_all_points(); |
| shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); |
| tool_data.remove_saved_points(); |
| tool_data.select_anchor_toggled = false; |
| } |
|
|
| tool_data.snapping_axis = None; |
| tool_data.sliding_point_info = None; |
|
|
| responses.add(DocumentMessage::EndTransaction); |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| tool_data.snap_manager.cleanup(responses); |
| tool_data.opposite_handle_position = None; |
|
|
| PathToolFsmState::Ready |
| } |
|
|
| |
| (_, PathToolMessage::Delete) => { |
| |
| responses.add(DocumentMessage::AddTransaction); |
| shape_editor.delete_selected_segments(document, responses); |
| shape_editor.delete_selected_points(document, responses); |
| responses.add(PathToolMessage::SelectionChanged); |
|
|
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::BreakPath) => { |
| shape_editor.break_path_at_selected_point(document, responses); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::DeleteAndBreakPath) => { |
| shape_editor.delete_point_and_break_path(document, responses); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::FlipSmoothSharp) => { |
| |
| let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); |
| if nearest_point.is_some() { |
| |
| if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { |
| responses.add(DocumentMessage::StartTransaction); |
|
|
| shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>()); |
| shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses); |
| tool_data.saved_points_before_anchor_convert_smooth_sharp.clear(); |
|
|
| responses.add(DocumentMessage::EndTransaction); |
| responses.add(PathToolMessage::SelectedPointUpdated); |
| } |
|
|
| return PathToolFsmState::Ready; |
| } |
|
|
| |
| if let Some(layer) = document.click(input) { |
| |
| shape_editor.select_connected_anchors(document, layer, input.mouse.position); |
| responses.add(OverlaysMessage::Draw); |
| } |
|
|
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::Abort) => { |
| responses.add(OverlaysMessage::Draw); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { |
| shape_editor.move_selected_points_and_segments( |
| tool_data.opposing_handle_lengths.take(), |
| document, |
| (delta_x, delta_y).into(), |
| true, |
| false, |
| false, |
| tool_data.opposite_handle_position, |
| false, |
| responses, |
| ); |
|
|
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::SelectAllAnchors) => { |
| shape_editor.select_all_anchors_in_selected_layers(document); |
| responses.add(OverlaysMessage::Draw); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::DeselectAllPoints) => { |
| shape_editor.deselect_all_points(); |
| responses.add(OverlaysMessage::Draw); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::SelectedPointXChanged { new_x }) => { |
| if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { |
| shape_editor.reposition_control_point(&id, &document.network_interface, DVec2::new(new_x, coordinates.y), layer, responses); |
| } |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::SelectedPointYChanged { new_y }) => { |
| if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { |
| shape_editor.reposition_control_point(&id, &document.network_interface, DVec2::new(coordinates.x, new_y), layer, responses); |
| } |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::SelectedPointUpdated) => { |
| let colinear = shape_editor.selected_manipulator_angles(&document.network_interface); |
| tool_data.dragging_state = DraggingState { |
| point_select_state: shape_editor.get_dragging_state(&document.network_interface), |
| colinear, |
| }; |
| tool_data.update_selection_status(shape_editor, document); |
| self |
| } |
| (_, PathToolMessage::ManipulatorMakeHandlesColinear) => { |
| responses.add(DocumentMessage::StartTransaction); |
| shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); |
| responses.add(DocumentMessage::EndTransaction); |
| responses.add(PathToolMessage::SelectionChanged); |
| PathToolFsmState::Ready |
| } |
| (_, PathToolMessage::ManipulatorMakeHandlesFree) => { |
| responses.add(DocumentMessage::StartTransaction); |
| shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses); |
| responses.add(DocumentMessage::EndTransaction); |
| PathToolFsmState::Ready |
| } |
| (_, _) => PathToolFsmState::Ready, |
| } |
| } |
|
|
| fn update_hints(&self, _responses: &mut VecDeque<Message>) { |
| |
| } |
|
|
| fn update_cursor(&self, responses: &mut VecDeque<Message>) { |
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); |
| } |
| } |
|
|
| #[derive(Debug, PartialEq, Default)] |
| enum SelectionStatus { |
| #[default] |
| None, |
| One(SingleSelectedPoint), |
| Multiple(MultipleSelectedPoints), |
| } |
|
|
| impl SelectionStatus { |
| fn as_one(&self) -> Option<&SingleSelectedPoint> { |
| match self { |
| SelectionStatus::One(one) => Some(one), |
| _ => None, |
| } |
| } |
|
|
| fn angle(&self) -> Option<ManipulatorAngle> { |
| match self { |
| Self::None => None, |
| Self::One(one) => Some(one.manipulator_angle), |
| Self::Multiple(one) => Some(one.manipulator_angle), |
| } |
| } |
| } |
|
|
| #[derive(Debug, PartialEq)] |
| struct MultipleSelectedPoints { |
| manipulator_angle: ManipulatorAngle, |
| } |
|
|
| #[derive(Debug, PartialEq)] |
| struct SingleSelectedPoint { |
| coordinates: DVec2, |
| id: ManipulatorPointId, |
| layer: LayerNodeIdentifier, |
| manipulator_angle: ManipulatorAngle, |
| } |
|
|
| |
| |
| fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: &mut ShapeState) -> SelectionStatus { |
| let mut selection_layers = shape_state.selected_shape_state.iter().map(|(k, v)| (*k, v.selected_points_count())); |
| let total_selected_points = selection_layers.clone().map(|(_, v)| v).sum::<usize>(); |
|
|
| |
| if total_selected_points == 1 { |
| let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else { |
| return SelectionStatus::None; |
| }; |
| let Some(vector_data) = network_interface.compute_modified_vector(layer) else { |
| return SelectionStatus::None; |
| }; |
| let Some(&point) = shape_state.selected_points().next() else { |
| return SelectionStatus::None; |
| }; |
| let Some(local_position) = point.get_position(&vector_data) else { |
| return SelectionStatus::None; |
| }; |
|
|
| let coordinates = network_interface.document_metadata().transform_to_document(layer).transform_point2(local_position); |
| let manipulator_angle = if vector_data.colinear(point) { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free }; |
|
|
| return SelectionStatus::One(SingleSelectedPoint { |
| coordinates, |
| layer, |
| id: point, |
| manipulator_angle, |
| }); |
| }; |
|
|
| |
| if total_selected_points > 1 { |
| return SelectionStatus::Multiple(MultipleSelectedPoints { |
| manipulator_angle: shape_state.selected_manipulator_angles(network_interface), |
| }); |
| } |
|
|
| SelectionStatus::None |
| } |
|
|
| fn calculate_lock_angle( |
| tool_data: &mut PathToolData, |
| shape_state: &mut ShapeState, |
| responses: &mut VecDeque<Message>, |
| document: &DocumentMessageHandler, |
| vector_data: &VectorData, |
| handle_id: ManipulatorPointId, |
| tangent_to_neighboring_tangents: bool, |
| ) -> Option<f64> { |
| let anchor = handle_id.get_anchor(vector_data)?; |
| let anchor_position = vector_data.point_domain.position_from_id(anchor); |
| let current_segment = handle_id.get_segment(); |
| let points_connected = vector_data.connected_count(anchor); |
|
|
| let (anchor_position, segment) = anchor_position.zip(current_segment)?; |
| if points_connected == 1 { |
| calculate_segment_angle(anchor, segment, vector_data, false) |
| } else { |
| let opposite_handle = handle_id |
| .get_handle_pair(vector_data) |
| .iter() |
| .flatten() |
| .find(|&h| h.to_manipulator_point() != handle_id) |
| .copied() |
| .map(|h| h.to_manipulator_point()); |
| let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6); |
|
|
| if let Some(opposite_pos) = opposite_handle_position { |
| if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) { |
| shape_state.convert_selected_manipulators_to_colinear_handles(responses, document); |
| tool_data.temporary_colinear_handles = true; |
| } |
| Some(-(opposite_pos - anchor_position).angle_to(DVec2::X)) |
| } else { |
| let angle_1 = vector_data |
| .adjacent_segment(&handle_id) |
| .and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false)); |
|
|
| let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false); |
|
|
| match (angle_1, angle_2) { |
| (Some(angle_1), Some(angle_2)) => { |
| let angle = Some((angle_1 + angle_2) / 2.); |
| if tangent_to_neighboring_tangents { |
| angle.map(|angle| angle + std::f64::consts::FRAC_PI_2) |
| } else { |
| angle |
| } |
| } |
| (Some(angle_1), None) => Some(angle_1), |
| (None, Some(angle_2)) => Some(angle_2), |
| (None, None) => None, |
| } |
| } |
| } |
| } |
|
|
| fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> { |
| let (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?; |
|
|
| let check_if_close = |point_id: &PointId| { |
| let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else { |
| return false; |
| }; |
| (anchor_position - handle_position).length() < 10. |
| }; |
|
|
| vector_data.connected_points(anchor).find(check_if_close) |
| } |
| fn calculate_adjacent_anchor_tangent( |
| currently_dragged_handle: ManipulatorPointId, |
| anchor: Option<PointId>, |
| adjacent_anchor: Option<PointId>, |
| vector_data: &VectorData, |
| ) -> (Option<f64>, Option<DVec2>) { |
| |
|
|
| let Some((dragged_handle_anchor, adjacent_anchor)) = anchor.zip(adjacent_anchor) else { |
| return (None, None); |
| }; |
| let adjacent_anchor_position = vector_data.point_domain.position_from_id(adjacent_anchor); |
|
|
| let handles: Vec<_> = vector_data.all_connected(adjacent_anchor).filter(|handle| handle.length(vector_data) > 1e-6).collect(); |
|
|
| match handles.len() { |
| 0 => { |
| |
| let non_shared_segment: Vec<_> = vector_data |
| .segment_bezier_iter() |
| .filter_map(|(segment_id, _, start, end)| { |
| let touches_adjacent = start == adjacent_anchor || end == adjacent_anchor; |
| let shares_with_dragged = start == dragged_handle_anchor || end == dragged_handle_anchor; |
|
|
| if touches_adjacent && !shares_with_dragged { Some(segment_id) } else { None } |
| }) |
| .collect(); |
|
|
| match non_shared_segment.first() { |
| Some(&segment) => { |
| let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true); |
| (angle, adjacent_anchor_position) |
| } |
| None => (None, None), |
| } |
| } |
|
|
| 1 => { |
| let segment = handles[0].segment; |
| let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true); |
| (angle, adjacent_anchor_position) |
| } |
|
|
| 2 => { |
| |
| let Some(shared_segment_handle) = handles |
| .iter() |
| .find(|handle| handle.opposite().to_manipulator_point() == currently_dragged_handle) |
| .map(|handle| handle.to_manipulator_point()) |
| else { |
| return (None, None); |
| }; |
|
|
| let angle = shared_segment_handle |
| .get_position(vector_data) |
| .zip(adjacent_anchor_position) |
| .map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X)); |
|
|
| (angle, adjacent_anchor_position) |
| } |
|
|
| _ => (None, None), |
| } |
| } |
|
|
| fn update_dynamic_hints( |
| state: PathToolFsmState, |
| responses: &mut VecDeque<Message>, |
| shape_editor: &mut ShapeState, |
| document: &DocumentMessageHandler, |
| tool_data: &PathToolData, |
| tool_options: &PathToolOptions, |
| ) { |
| |
|
|
| let hint_data = match state { |
| PathToolFsmState::Ready => { |
| |
| let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
| let at_least_one_anchor_selected = shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); |
| let at_least_one_point_selected = shape_editor.selected_points().count() >= 1; |
|
|
| let mut single_colinear_anchor_selected = false; |
| if single_anchor_selected { |
| if let (Some(anchor), Some(layer)) = ( |
| shape_editor.selected_points().next(), |
| document.network_interface.selected_nodes().selected_layers(document.metadata()).next(), |
| ) { |
| if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { |
| single_colinear_anchor_selected = vector_data.colinear(*anchor) |
| } |
| } |
| } |
|
|
| let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]; |
| let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")]; |
|
|
| if at_least_one_anchor_selected { |
| delete_selected_hints.push(HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus()); |
| delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus()); |
| } |
|
|
| if single_colinear_anchor_selected { |
| drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); |
| } |
|
|
| let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) { |
| (true, true) => { |
| vec![ |
| HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), |
| HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]), |
| ] |
| } |
| (true, false) => { |
| vec![ |
| HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), |
| HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), |
| HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), |
| ] |
| } |
| (false, _) => { |
| vec![ |
| HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), |
| HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), |
| ] |
| } |
| }; |
|
|
| if at_least_one_anchor_selected { |
| |
| hint_data.push(HintGroup(vec![ |
| HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"), |
| HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"), |
| HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"), |
| ])); |
| } |
|
|
| if at_least_one_point_selected { |
| let mut groups = vec![ |
| HintGroup(drag_selected_hints), |
| HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), |
| HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), |
| HintGroup(delete_selected_hints), |
| ]; |
| hint_data.append(&mut groups); |
| } |
|
|
| HintData(hint_data) |
| } |
| PathToolFsmState::Dragging(dragging_state) => { |
| let colinear = dragging_state.colinear; |
| let mut dragging_hint_data = HintData(Vec::new()); |
| dragging_hint_data |
| .0 |
| .push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); |
|
|
| let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); |
| let toggle_group = match dragging_state.point_select_state { |
| PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => { |
| let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")]; |
| hints.push(HintInfo::keys( |
| [Key::KeyC], |
| if colinear == ManipulatorAngle::Colinear { |
| "Break Colinear Handles" |
| } else { |
| "Make Handles Colinear" |
| }, |
| )); |
| hints |
| } |
| PointSelectState::Anchor => Vec::new(), |
| }; |
| let hold_group = match dragging_state.point_select_state { |
| PointSelectState::HandleNoPair => { |
| let mut hints = vec![]; |
| if colinear != ManipulatorAngle::Free { |
| hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); |
| } |
| hints.push(HintInfo::keys([Key::Shift], "15° Increments")); |
| hints.push(HintInfo::keys([Key::Control], "Lock Angle")); |
| hints.push(drag_anchor); |
| hints |
| } |
| PointSelectState::HandleWithPair => { |
| let mut hints = vec![]; |
| if colinear != ManipulatorAngle::Free { |
| hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); |
| } |
| hints.push(HintInfo::keys([Key::Shift], "15° Increments")); |
| hints.push(HintInfo::keys([Key::Control], "Lock Angle")); |
| hints.push(drag_anchor); |
| hints |
| } |
| PointSelectState::Anchor => Vec::new(), |
| }; |
|
|
| if !toggle_group.is_empty() { |
| dragging_hint_data.0.push(HintGroup(toggle_group)); |
| } |
|
|
| if !hold_group.is_empty() { |
| dragging_hint_data.0.push(HintGroup(hold_group)); |
| } |
|
|
| dragging_hint_data |
| } |
| PathToolFsmState::Drawing { .. } => HintData(vec![ |
| HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), |
| HintGroup(vec![ |
| HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), |
| HintInfo::keys([Key::Shift], "Extend").prepend_plus(), |
| HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), |
| ]), |
| ]), |
| PathToolFsmState::MoldingSegment => { |
| let mut has_colinear_anchors = false; |
|
|
| if let Some(segment) = &tool_data.segment { |
| let handle1 = HandleId::primary(segment.segment()); |
| let handle2 = HandleId::end(segment.segment()); |
|
|
| if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { |
| let other_handle1 = vector_data.other_colinear_handle(handle1); |
| let other_handle2 = vector_data.other_colinear_handle(handle2); |
| if other_handle1.is_some() || other_handle2.is_some() { |
| has_colinear_anchors = true; |
| } |
| }; |
| } |
|
|
| let handles_stored = if let Some(other_handles) = tool_data.temporary_adjacent_handles_while_molding { |
| other_handles[0].is_some() || other_handles[1].is_some() |
| } else { |
| false |
| }; |
|
|
| let molding_disable_possible = has_colinear_anchors || handles_stored; |
|
|
| let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; |
|
|
| if molding_disable_possible { |
| molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")])); |
| } |
|
|
| HintData(molding_hints) |
| } |
| PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), |
| }; |
| responses.add(FrontendMessage::UpdateInputHints { hint_data }); |
| } |
|
|