use super::tool_prelude::*; use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, SEGMENT_OVERLAY_SIZE}; use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend}; use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData}; use graphene_std::vector::{PointId, VectorModificationType}; #[derive(Default)] pub struct PenTool { fsm_state: PenToolFsmState, tool_data: PenToolData, options: PenOptions, } pub struct PenOptions { line_weight: f64, fill: ToolColorOptions, stroke: ToolColorOptions, pen_overlay_mode: PenOverlayMode, } impl Default for PenOptions { fn default() -> Self { Self { line_weight: DEFAULT_STROKE_WIDTH, fill: ToolColorOptions::new_secondary(), stroke: ToolColorOptions::new_primary(), pen_overlay_mode: PenOverlayMode::FrontierHandles, } } } #[impl_message(Message, ToolMessage, Pen)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum PenToolMessage { // Standard messages Abort, SelectionChanged, WorkingColorChanged, Overlays(OverlayContext), // Tool-specific messages // It is necessary to defer this until the transform of the layer can be accurately computed (quite hacky) AddPointLayerPosition { layer: LayerNodeIdentifier, viewport: DVec2, }, Confirm, DragStart { append_to_selected: Key, }, DragStop, PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key, move_anchor_with_handles: Key, }, PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key, move_anchor_with_handles: Key, }, Redo, Undo, UpdateOptions(PenOptionsUpdate), RecalculateLatestPointsPosition, RemovePreviousHandle, GRS { grab: Key, rotate: Key, scale: Key, }, FinalPosition { final_position: DVec2, }, SwapHandles, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum PenToolFsmState { #[default] Ready, DraggingHandle(HandleMode), PlacingAnchor, GRSHandle, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum PenOverlayMode { AllHandles = 0, FrontierHandles = 1, } #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum PenOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), LineWeight(f64), StrokeColor(Option), StrokeColorType(ToolColorType), WorkingColors(Option, Option), OverlayModeType(PenOverlayMode), } impl ToolMetadata for PenTool { fn icon_name(&self) -> String { "VectorPenTool".into() } fn tooltip(&self) -> String { "Pen Tool".into() } fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { ToolType::Pen } } fn create_weight_widget(line_weight: f64) -> WidgetHolder { NumberInput::new(Some(line_weight)) .unit(" px") .label("Weight") .min(0.) .max((1_u64 << f64::MANTISSA_DIGITS) as f64) .on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) .widget_holder() } impl LayoutHolder for PenTool { fn layout(&self) -> Layout { let mut widgets = self.options.fill.create_widgets( "Fill", true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), ); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push( RadioInput::new(vec![ RadioEntryData::new("all") .icon("HandleVisibilityAll") .tooltip("Show all handles regardless of selection") .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), RadioEntryData::new("frontier") .icon("HandleVisibilityFrontier") .tooltip("Show only handles at the frontiers of the segments connected to selected points") .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), ]) .selected_index(Some(self.options.pen_overlay_mode as u32)) .widget_holder(), ); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } impl<'a> MessageHandler> for PenTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message else { self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); return; }; match action { PenOptionsUpdate::OverlayModeType(overlay_mode_type) => { self.options.pen_overlay_mode = overlay_mode_type; responses.add(OverlaysMessage::Draw); } PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, PenOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; } PenOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, PenOptionsUpdate::StrokeColor(color) => { self.options.stroke.custom_color = color; self.options.stroke.color_type = ToolColorType::Custom; } PenOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, PenOptionsUpdate::WorkingColors(primary, secondary) => { self.options.stroke.primary_working_color = primary; self.options.stroke.secondary_working_color = secondary; self.options.fill.primary_working_color = primary; self.options.fill.secondary_working_color = secondary; } } self.send_layout(responses, LayoutTarget::ToolOptions); } fn actions(&self) -> ActionList { match self.fsm_state { PenToolFsmState::Ready | PenToolFsmState::GRSHandle => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort, PointerMove, FinalPosition ), PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort, RemovePreviousHandle, GRS, SwapHandles ), } } } impl ToolTransition for PenTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(PenToolMessage::Abort.into()), selection_changed: Some(PenToolMessage::SelectionChanged.into()), working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()), overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()), ..Default::default() } } } #[derive(Clone, Debug, Default)] struct ModifierState { snap_angle: bool, lock_angle: bool, break_handle: bool, colinear: bool, move_anchor_with_handles: bool, } #[derive(Clone, Debug)] struct LastPoint { id: PointId, pos: DVec2, in_segment: Option, handle_start: DVec2, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] enum HandleMode { /// Pressing 'C' breaks colinearity Free, /// Pressing 'Alt': Handle length is locked #[default] ColinearLocked, /// Pressing 'Alt': Handles are equidistant ColinearEquidistant, } /// The type of handle which is dragged by the cursor (under the cursor). /// /// ![Terminology](https://files.keavon.com/-/EachNotedLovebird/capture.png) #[derive(Clone, Debug, Default, PartialEq, Copy)] enum TargetHandle { #[default] None, /// This is the handle being dragged and represents the out handle of the next preview segment that will be placed /// after the current preview segment is finalized. Its position is stored in `tool_data.next_handle_start`. /// /// Pressing Tab swaps to the opposite handle type. The swapped handle can be either [`ManipulatorPointId::PreviewInHandle`] /// or, in the case of a bent segment, [`ManipulatorPointId::EndHandle`] or [`ManipulatorPointId::PrimaryHandle`]. /// /// When closing a path, the handle being dragged becomes the end handle of the currently placed anchor. /// /// ![Terminology](https://files.keavon.com/-/EachNotedLovebird/capture.png) FuturePreviewOutHandle, /// The opposite handle that is drawn after placing an anchor and starting to drag the "next handle start", /// continuing until Tab is pressed to swap the handles. /// /// ![Terminology](https://files.keavon.com/-/EachNotedLovebird/capture.png) PreviewInHandle, /// This is the primary handle of the segment from whose endpoint a new handle is being drawn. /// When closing the path, the handle being dragged will be the [`TargetHandle::PreviewInHandle`] (see its documentation); /// otherwise, it will be [`TargetHandle::FuturePreviewOutHandle`]. /// /// If a handle is dragged from a different endpoint within the same layer, the opposite handle will be /// `ManipulatorPoint::Primary` if that point is the starting point of its path. /// /// ![Terminology](https://files.keavon.com/-/EachNotedLovebird/capture.png) PriorOutHandle(SegmentId), /// This is the end handle of the segment from whose endpoint a new handle is being drawn (same cases apply /// as mentioned in [`TargetHandle::PriorOutHandle`]). If a handle is dragged from a different endpoint within the same /// layer, the opposite handle will be `ManipulatorPoint::EndHandle` if that point is the end point of its path. /// /// ![Terminology](https://files.keavon.com/-/EachNotedLovebird/capture.png) PriorInHandle(SegmentId), } #[derive(Clone, Debug, Default)] struct PenToolData { snap_manager: SnapManager, latest_points: Vec, point_index: usize, handle_end: Option, next_point: DVec2, next_handle_start: DVec2, g1_continuous: bool, angle: f64, auto_panning: AutoPanning, modifiers: ModifierState, buffering_merged_vector: bool, previous_handle_start_pos: DVec2, previous_handle_end_pos: Option, toggle_colinear_debounce: bool, colinear: bool, alt_pressed: bool, space_pressed: bool, /// Tracks whether to switch from `HandleMode::ColinearEquidistant` to `HandleMode::Free` /// after releasing Ctrl, specifically when Ctrl was held before the handle was dragged from the anchor. switch_to_free_on_ctrl_release: bool, /// To prevent showing cursor when `KeyC` is pressed when handles are swapped. handle_swapped: bool, /// Prevents conflicts when the handle's angle is already locked and it passes near the anchor, /// avoiding unintended direction changes. Specifically handles the case where a handle is being dragged, /// and Ctrl is pressed near the anchor to make it colinear with its opposite handle. angle_locked: bool, path_closed: bool, handle_mode: HandleMode, prior_segment_layer: Option, current_layer: Option, prior_segment_endpoint: Option, prior_segment: Option, /// For vector meshes, storing all the previous segments the last anchor point was connected to prior_segments: Option>, handle_type: TargetHandle, handle_start_offset: Option, handle_end_offset: Option, snap_cache: SnapCache, } impl PenToolData { fn latest_point(&self) -> Option<&LastPoint> { self.latest_points.get(self.point_index) } fn latest_point_mut(&mut self) -> Option<&mut LastPoint> { self.latest_points.get_mut(self.point_index) } fn add_point(&mut self, point: LastPoint) { self.point_index = (self.point_index + 1).min(self.latest_points.len()); self.latest_points.truncate(self.point_index); self.latest_points.push(point); } fn cleanup(&mut self, responses: &mut VecDeque) { self.handle_end = None; self.latest_points.clear(); self.point_index = 0; self.snap_manager.cleanup(responses); } /// Check whether target handle is primary, end, or `self.handle_end` fn check_end_handle_type(&self, vector_data: &VectorData) -> TargetHandle { match (self.handle_end, self.prior_segment_endpoint, self.prior_segment, self.path_closed) { (Some(_), _, _, false) => TargetHandle::PreviewInHandle, (None, Some(point), Some(segment), false) | (Some(_), Some(point), Some(segment), true) => { if vector_data.segment_start_from_id(segment) == Some(point) { TargetHandle::PriorOutHandle(segment) } else { TargetHandle::PriorInHandle(segment) } } _ => TargetHandle::None, } } fn check_grs_end_handle(&self, vector_data: &VectorData) -> TargetHandle { let Some(point) = self.latest_point().map(|point| point.id) else { return TargetHandle::None }; let Some(segment) = self.prior_segment else { return TargetHandle::None }; if vector_data.segment_start_from_id(segment) == Some(point) { TargetHandle::PriorOutHandle(segment) } else { TargetHandle::PriorInHandle(segment) } } fn get_opposite_handle_type(&self, handle_type: TargetHandle, vector_data: &VectorData) -> TargetHandle { match handle_type { TargetHandle::FuturePreviewOutHandle => self.check_end_handle_type(vector_data), TargetHandle::PreviewInHandle => match (self.path_closed, self.prior_segment_endpoint, self.prior_segment) { (true, Some(point), Some(segment)) => { if vector_data.segment_start_from_id(segment) == Some(point) { TargetHandle::PriorOutHandle(segment) } else { TargetHandle::PriorInHandle(segment) } } (false, _, _) => TargetHandle::FuturePreviewOutHandle, _ => TargetHandle::None, }, _ => { if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle } } } } fn update_handle_type(&mut self, handle_type: TargetHandle) { self.handle_type = handle_type; } fn update_target_handle_pos(&mut self, handle_type: TargetHandle, anchor_pos: DVec2, responses: &mut VecDeque, delta: DVec2, layer: LayerNodeIdentifier) { match handle_type { TargetHandle::FuturePreviewOutHandle => { self.next_handle_start = delta; } TargetHandle::PreviewInHandle => { if let Some(handle) = self.handle_end.as_mut() { *handle = delta; } } TargetHandle::PriorInHandle(segment) => { let relative_position = delta - anchor_pos; let modification_type = VectorModificationType::SetEndHandle { segment, relative_position }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } TargetHandle::PriorOutHandle(segment) => { let relative_position = delta - anchor_pos; let modification_type = VectorModificationType::SetPrimaryHandle { segment, relative_position }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } TargetHandle::None => {} } } fn target_handle_position(&self, handle_type: TargetHandle, vector_data: &VectorData) -> Option { match handle_type { TargetHandle::PriorOutHandle(segment) => ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data), TargetHandle::PriorInHandle(segment) => ManipulatorPointId::EndHandle(segment).get_position(vector_data), TargetHandle::PreviewInHandle => self.handle_end, TargetHandle::FuturePreviewOutHandle => Some(self.next_handle_start), TargetHandle::None => None, } } /// Remove the handles selected when swapping handles fn cleanup_target_selections(&self, shape_editor: &mut ShapeState, layer: Option, document: &DocumentMessageHandler, responses: &mut VecDeque) { let Some(shape_state) = layer.and_then(|layer| shape_editor.selected_shape_state.get_mut(&layer)) else { return; }; let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { return; }; match self.check_end_handle_type(&vector_data) { TargetHandle::PriorInHandle(segment) => shape_state.deselect_point(ManipulatorPointId::EndHandle(segment)), TargetHandle::PriorOutHandle(segment) => shape_state.deselect_point(ManipulatorPointId::PrimaryHandle(segment)), _ => {} } responses.add(OverlaysMessage::Draw); } /// Selects the handle which is currently dragged by the user. fn add_target_selections(&self, shape_editor: &mut ShapeState, layer: Option) { let Some(shape_state) = layer.and_then(|layer| shape_editor.selected_shape_state.get_mut(&layer)) else { return; }; match self.handle_type { TargetHandle::PriorInHandle(segment) => shape_state.select_point(ManipulatorPointId::EndHandle(segment)), TargetHandle::PriorOutHandle(segment) => shape_state.select_point(ManipulatorPointId::PrimaryHandle(segment)), _ => {} } } /// Check whether moving the initially created point. fn moving_start_point(&self) -> bool { self.latest_points.len() == 1 && self.latest_point().is_some_and(|point| point.pos == self.next_point) } // When the vector data transform changes, the positions of the points must be recalculated. fn recalculate_latest_points_position(&mut self, document: &DocumentMessageHandler) { let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); if let (Some(layer), None) = (selected_layers.next(), selected_layers.next()) { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return; }; for point in &mut self.latest_points { let Some(pos) = vector_data.point_domain.position_from_id(point.id) else { continue; }; point.pos = pos; point.handle_start = point.pos; } } } /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged. fn bend_from_previous_point( &mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier, preferences: &PreferencesMessageHandler, shape_editor: &mut ShapeState, responses: &mut VecDeque, ) { self.g1_continuous = true; let document = snap_data.document; self.next_handle_start = self.next_point; let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); self.update_handle_type(TargetHandle::FuturePreviewOutHandle); self.handle_mode = HandleMode::ColinearLocked; // Break the control let Some((last_pos, id)) = self.latest_point().map(|point| (point.pos, point.id)) else { return }; let transform = document.metadata().document_to_viewport * transform; let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2); if on_top { self.handle_end = None; self.handle_mode = HandleMode::Free; self.store_clicked_endpoint(document, &transform, snap_data.input, preferences); if self.modifiers.lock_angle { self.set_lock_angle(&vector_data, id, self.prior_segment); let last_segment = self.prior_segment; let Some(point) = self.latest_point_mut() else { return }; point.in_segment = last_segment; self.switch_to_free_on_ctrl_release = true; return; } if let Some(point) = self.latest_point_mut() { point.in_segment = None; } } // Closing path let closing_path_on_point = self.close_path_on_point(snap_data, &vector_data, document, preferences, id, &transform); if !closing_path_on_point && preferences.vector_meshes { // Attempt to find nearest segment and close path on segment by creating an anchor point on it let tolerance = crate::consts::SNAP_POINT_TOLERANCE; if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, transform.transform_point2(self.next_point), tolerance) { let (point, _) = closest_segment.adjusted_insert(responses); self.update_handle_type(TargetHandle::PreviewInHandle); self.handle_end_offset = None; self.path_closed = true; self.next_handle_start = self.next_point; self.prior_segment_endpoint = Some(point); self.prior_segment_layer = Some(closest_segment.layer()); self.prior_segments = None; self.prior_segment = None; // Should also update the SnapCache here? self.handle_mode = HandleMode::Free; if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) { self.set_lock_angle(&vector_data, prior_endpoint, self.prior_segment); self.switch_to_free_on_ctrl_release = true; } } } } fn close_path_on_point( &mut self, snap_data: SnapData, vector_data: &VectorData, document: &DocumentMessageHandler, preferences: &PreferencesMessageHandler, id: PointId, transform: &DAffine2, ) -> bool { for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != id) { let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); if transformed_distance_between_squared < snap_point_tolerance_squared { self.update_handle_type(TargetHandle::PreviewInHandle); self.handle_end_offset = None; self.path_closed = true; self.next_handle_start = self.next_point; self.store_clicked_endpoint(document, transform, snap_data.input, preferences); self.handle_mode = HandleMode::Free; if let (true, Some(prior_endpoint)) = (self.modifiers.lock_angle, self.prior_segment_endpoint) { self.set_lock_angle(vector_data, prior_endpoint, self.prior_segment); self.switch_to_free_on_ctrl_release = true; } return true; } } false } fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque) -> Option { let document = snap_data.document; let next_handle_start = self.next_handle_start; let handle_start = self.latest_point()?.handle_start; let mouse = snap_data.input.mouse.position; self.handle_swapped = false; self.handle_end_offset = None; self.handle_start_offset = None; let Some(handle_end) = self.handle_end else { responses.add(DocumentMessage::EndTransaction); self.handle_end = Some(next_handle_start); self.place_anchor(snap_data, transform, mouse, preferences, responses); self.latest_point_mut()?.handle_start = next_handle_start; return None; }; let next_point = self.next_point; self.place_anchor(snap_data, transform, mouse, preferences, responses); let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some); // Get close path let mut end = None; let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(self.current_layer)?; let vector_data = document.network_interface.compute_modified_vector(layer)?; let start = self.latest_point()?.id; let transform = document.metadata().document_to_viewport * transform; for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) { let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); if transformed_distance_between_squared < snap_point_tolerance_squared { end = Some(id); } } let close_subpath = end.is_some(); // Generate new point if not closing let end = end.unwrap_or_else(|| { let end = PointId::generate(); let modification_type = VectorModificationType::InsertPoint { id: end, position: next_point }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); end }); // Store the segment let id = SegmentId::generate(); if self.path_closed { if let Some((handles, handle1_pos)) = match self.get_opposite_handle_type(TargetHandle::PreviewInHandle, &vector_data) { TargetHandle::PriorOutHandle(segment) => { let handles = [HandleId::end(id), HandleId::primary(segment)]; let handle1_pos = handles[1].to_manipulator_point().get_position(&vector_data); handle1_pos.map(|pos| (handles, pos)) } TargetHandle::PriorInHandle(segment) => { let handles = [HandleId::end(id), HandleId::end(segment)]; let handle1_pos = handles[1].to_manipulator_point().get_position(&vector_data); handle1_pos.map(|pos| (handles, pos)) } _ => None, } { let angle = (handle_end - next_point).angle_to(handle1_pos - next_point); let pi = std::f64::consts::PI; let colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; responses.add(GraphOperationMessage::Vector { layer, modification_type: VectorModificationType::SetG1Continuous { handles, enabled: colinear }, }); self.cleanup(responses); } } self.prior_segment = Some(id); let points = [start, end]; let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Mirror if let Some((last_segment, last_point)) = self.latest_point().and_then(|point| point.in_segment).zip(self.latest_point()) { let end = vector_data.segment_end_from_id(last_segment) == Some(last_point.id); let handles = if end { [HandleId::end(last_segment), HandleId::primary(id)] } else { [HandleId::primary(last_segment), HandleId::primary(id)] }; if let Some(h1) = handles[0].to_manipulator_point().get_position(&vector_data) { let angle = (h1 - last_point.pos).angle_to(last_point.handle_start - last_point.pos); let pi = std::f64::consts::PI; let colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; responses.add(GraphOperationMessage::Vector { layer, modification_type: VectorModificationType::SetG1Continuous { handles, enabled: colinear }, }); } } if !close_subpath { self.add_point(LastPoint { id: end, pos: next_point, in_segment: self.g1_continuous.then_some(id), handle_start: next_handle_start, }); } self.path_closed = false; self.prior_segment_endpoint = None; responses.add(DocumentMessage::EndTransaction); Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor }) } #[allow(clippy::too_many_arguments)] /// Calculates snap position delta while moving anchor and its handles. fn space_anchor_handle_snap( &mut self, viewport_to_document: &DAffine2, transform: &DAffine2, snap_data: &SnapData<'_>, mouse: &DVec2, vector_data: &VectorData, input: &InputPreprocessorMessageHandler, ) -> Option { let reference_handle = if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; let end_handle = self.get_opposite_handle_type(reference_handle, vector_data); let end_handle_pos = self.target_handle_position(end_handle, vector_data); let ref_pos = self.target_handle_position(reference_handle, vector_data)?; let snap = &mut self.snap_manager; let snap_data = SnapData::new_snap_cache(snap_data.document, input, &self.snap_cache); let handle_start_offset = self.handle_start_offset.unwrap_or(DVec2::ZERO); let document_pos = viewport_to_document.transform_point2(*mouse + handle_start_offset); let anchor_offset = transform.transform_point2(self.next_point - ref_pos); let handle_start = SnapCandidatePoint::handle(document_pos); let anchor = SnapCandidatePoint::handle(document_pos + anchor_offset); let snapped_near_handle_start = snap.free_snap(&snap_data, &handle_start, SnapTypeConfiguration::default()); let snapped_anchor = snap.free_snap(&snap_data, &anchor, SnapTypeConfiguration::default()); let handle_snap_option = end_handle_pos.and_then(|handle| match end_handle { TargetHandle::None => None, TargetHandle::FuturePreviewOutHandle => None, _ => { let handle_offset = transform.transform_point2(handle - ref_pos); let handle_snap = SnapCandidatePoint::handle(document_pos + handle_offset); Some((handle, handle_snap)) } }); let mut delta: DVec2; let best_snapped = if snapped_near_handle_start.other_snap_better(&snapped_anchor) { delta = snapped_anchor.snapped_point_document - transform.transform_point2(self.next_point); snapped_anchor } else { delta = snapped_near_handle_start.snapped_point_document - transform.transform_point2(ref_pos); snapped_near_handle_start }; let Some((handle, handle_snap)) = handle_snap_option else { snap.update_indicator(best_snapped); return Some(transform.inverse().transform_vector2(delta)); }; let snapped_handle = snap.free_snap(&snap_data, &handle_snap, SnapTypeConfiguration::default()); if best_snapped.other_snap_better(&snapped_handle) { delta = snapped_handle.snapped_point_document - transform.transform_point2(handle); snap.update_indicator(snapped_handle); } else { snap.update_indicator(best_snapped); } // Transform delta back to original coordinate space Some(transform.inverse().transform_vector2(delta)) } /// Calculates the offset from the mouse when swapping handles, and swaps the handles. fn swap_handles( &mut self, layer: Option, document: &DocumentMessageHandler, shape_editor: &mut ShapeState, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { // Validate necessary data exists let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { return; }; let Some(viewport) = layer.map(|layer| document.metadata().transform_to_viewport(layer)) else { return; }; // Determine if we need to swap to opposite handle let should_swap_to_opposite = self.path_closed && matches!(self.handle_type, TargetHandle::PreviewInHandle | TargetHandle::PriorOutHandle(..) | TargetHandle::PriorInHandle(..)) || !self.path_closed && matches!(self.handle_type, TargetHandle::FuturePreviewOutHandle); // Determine if we need to swap to start handle let should_swap_to_start = !self.path_closed && !matches!(self.handle_type, TargetHandle::None | TargetHandle::FuturePreviewOutHandle); if should_swap_to_opposite { let opposite_type = self.get_opposite_handle_type(self.handle_type, &vector_data); // Update offset let Some(handle_pos) = self.target_handle_position(opposite_type, &vector_data) else { self.handle_swapped = false; return; }; if (handle_pos - self.next_point).length() < 1e-6 { self.handle_swapped = false; return; } self.handle_end_offset = Some(viewport.transform_point2(handle_pos) - input.mouse.position); // Update selections if in closed path mode if self.path_closed { self.cleanup_target_selections(shape_editor, layer, document, responses); } self.update_handle_type(opposite_type); self.add_target_selections(shape_editor, layer); } else if should_swap_to_start { self.cleanup_target_selections(shape_editor, layer, document, responses); // Calculate offset from mouse position to next handle start if let Some(layer_id) = layer { let transform = document.metadata().transform_to_viewport(layer_id); self.handle_start_offset = Some(transform.transform_point2(self.next_handle_start) - input.mouse.position); } self.update_handle_type(TargetHandle::FuturePreviewOutHandle); } responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); } /// Handles moving the initially created point fn handle_single_point_path_drag(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque) -> Option { self.next_handle_start += delta; self.next_point += delta; let Some(latest) = self.latest_point_mut() else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); }; latest.pos += delta; let modification_type = VectorModificationType::ApplyPointDelta { point: latest.id, delta }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(OverlaysMessage::Draw); Some(PenToolFsmState::DraggingHandle(self.handle_mode)) } fn move_anchor_and_handles(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque, vector_data: &VectorData) { if self.handle_end.is_none() { if let Some(latest_pt) = self.latest_point_mut() { latest_pt.pos += delta; } } let Some(end_point) = self.prior_segment_endpoint else { return }; let modification_type_anchor = VectorModificationType::ApplyPointDelta { point: end_point, delta }; responses.add(GraphOperationMessage::Vector { layer, modification_type: modification_type_anchor, }); let reference_handle = if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; // Move the end handle let end_handle_type = self.get_opposite_handle_type(reference_handle, vector_data); match end_handle_type { TargetHandle::PriorInHandle(..) | TargetHandle::PriorOutHandle(..) => { let Some(handle_pos) = self.target_handle_position(end_handle_type, vector_data) else { return }; self.update_target_handle_pos(end_handle_type, self.next_point, responses, handle_pos + delta, layer); } _ => {} } } fn drag_handle( &mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque, layer: Option, input: &InputPreprocessorMessageHandler, ) -> Option { let colinear = (self.handle_mode == HandleMode::ColinearEquidistant && self.modifiers.break_handle) || (self.handle_mode == HandleMode::ColinearLocked && !self.modifiers.break_handle); let document = snap_data.document; let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) }; let vector_data = document.network_interface.compute_modified_vector(layer)?; let viewport_to_document = document.metadata().document_to_viewport.inverse(); let mut mouse_pos = mouse; // Handles pressing Space to drag anchor and its handles if self.modifiers.move_anchor_with_handles { let Some(delta) = self.space_anchor_handle_snap(&viewport_to_document, &transform, &snap_data, &mouse, &vector_data, input) else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); }; if self.moving_start_point() { return self.handle_single_point_path_drag(delta, layer, responses); } self.next_handle_start += delta; self.next_point += delta; if let Some(handle) = self.handle_end.as_mut() { *handle += delta; if !self.path_closed { responses.add(OverlaysMessage::Draw); return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); }; } self.move_anchor_and_handles(delta, layer, responses, &vector_data); responses.add(OverlaysMessage::Draw); return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); } match self.handle_type { TargetHandle::FuturePreviewOutHandle => { let offset = self.handle_start_offset.unwrap_or(DVec2::ZERO); mouse_pos += offset; self.next_handle_start = self.compute_snapped_angle(snap_data.clone(), transform, colinear, mouse_pos, Some(self.next_point), false); } _ => { mouse_pos += self.handle_end_offset.unwrap_or(DVec2::ZERO); let mouse_pos = self.compute_snapped_angle(snap_data.clone(), transform, colinear, mouse_pos, Some(self.next_point), false); self.update_target_handle_pos(self.handle_type, self.next_point, responses, mouse_pos, layer); } } let mouse_pos = viewport_to_document.transform_point2(mouse_pos); let anchor = transform.transform_point2(self.next_point); let distance = (mouse_pos - anchor).length(); if self.switch_to_free_on_ctrl_release && !self.modifiers.lock_angle { self.switch_to_free_on_ctrl_release = false; self.handle_mode = HandleMode::Free; } if distance > 20. && self.handle_mode == HandleMode::Free && self.modifiers.lock_angle && !self.angle_locked { self.angle_locked = true } match self.handle_mode { HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => { self.g1_continuous = true; self.apply_colinear_constraint(responses, layer, self.next_point, &vector_data); self.adjust_handle_length(responses, layer, &vector_data); } HandleMode::Free => { self.g1_continuous = false; } } if distance < 20. && self.handle_mode == HandleMode::Free && self.modifiers.lock_angle && !self.angle_locked { let Some(endpoint) = self.prior_segment_endpoint else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)); }; self.set_lock_angle(&vector_data, endpoint, self.prior_segment); self.switch_to_free_on_ctrl_release = true; let last_segment = self.prior_segment; if let Some(latest) = self.latest_point_mut() { latest.in_segment = last_segment; } } responses.add(OverlaysMessage::Draw); Some(PenToolFsmState::DraggingHandle(self.handle_mode)) } /// Makes the opposite handle equidistant or locks its length. fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) { let opposite_handle_type = self.get_opposite_handle_type(self.handle_type, vector_data); match self.handle_mode { HandleMode::ColinearEquidistant => { if self.modifiers.break_handle { // Store handle for later restoration only when Alt is first pressed if !self.alt_pressed { self.previous_handle_end_pos = self.target_handle_position(opposite_handle_type, vector_data); self.alt_pressed = true; } // Set handle to opposite position of the other handle let Some(new_position) = self.target_handle_position(self.handle_type, vector_data).map(|handle| self.next_point * 2. - handle) else { return; }; self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, new_position, layer); } else if self.alt_pressed { // Restore the previous handle position when Alt is released if let Some(previous_handle) = self.previous_handle_end_pos { self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, previous_handle, layer); } self.alt_pressed = false; self.previous_handle_end_pos = None; } } HandleMode::ColinearLocked => { if !self.modifiers.break_handle { let Some(new_position) = self.target_handle_position(self.handle_type, vector_data).map(|handle| self.next_point * 2. - handle) else { return; }; self.update_target_handle_pos(opposite_handle_type, self.next_point, responses, new_position, layer); } } HandleMode::Free => {} } } fn apply_colinear_constraint(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, anchor_pos: DVec2, vector_data: &VectorData) { let Some(handle) = self.target_handle_position(self.handle_type, vector_data) else { return; }; if (anchor_pos - handle).length() < 1e-6 && self.modifiers.lock_angle { return; } let Some(direction) = (anchor_pos - handle).try_normalize() else { return; }; let opposite_handle = self.get_opposite_handle_type(self.handle_type, vector_data); let Some(handle_offset) = self.target_handle_position(opposite_handle, vector_data).map(|handle| (handle - anchor_pos).length()) else { return; }; let new_handle_position = anchor_pos + handle_offset * direction; self.update_target_handle_pos(opposite_handle, self.next_point, responses, new_handle_position, layer); } fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque) -> Option { let document = snap_data.document; let relative = if self.path_closed { None } else { self.latest_point().map(|point| point.pos) }; self.next_point = self.compute_snapped_angle(snap_data, transform, false, mouse, relative, true); let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(self.current_layer)?; let vector_data = document.network_interface.compute_modified_vector(layer)?; let transform = document.metadata().document_to_viewport * transform; for point in vector_data.extendable_points(preferences.vector_meshes) { let Some(pos) = vector_data.point_domain.position_from_id(point) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); if transformed_distance_between_squared < snap_point_tolerance_squared { self.next_point = pos; } } if let Some(handle_end) = self.handle_end.as_mut() { *handle_end = self.next_point; self.next_handle_start = self.next_point; } responses.add(OverlaysMessage::Draw); Some(PenToolFsmState::PlacingAnchor) } /// Snap the angle of the line from relative to position if the key is pressed. fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, colinear: bool, mouse: DVec2, relative: Option, neighbor: bool) -> DVec2 { let ModifierState { snap_angle, lock_angle, .. } = self.modifiers; let document = snap_data.document; let mut document_pos = document.metadata().document_to_viewport.inverse().transform_point2(mouse); let snap = &mut self.snap_manager; let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]); let config = SnapTypeConfiguration::default(); if let Some(relative) = relative .map(|layer| transform.transform_point2(layer)) .filter(|&relative| (snap_angle || lock_angle) && (relative - document_pos).length_squared() > f64::EPSILON * 100.) { let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); let angle = if lock_angle { self.angle } else if (relative - document_pos) != DVec2::ZERO && !lock_angle { (-(relative - document_pos).angle_to(DVec2::X) / resolution).round() * resolution } else { self.angle }; document_pos = relative - (relative - document_pos).project_onto(DVec2::new(angle.cos(), angle.sin())); let constraint = SnapConstraint::Line { origin: relative, direction: document_pos - relative, }; let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()); let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors); if colinear { let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config); document_pos = if snapped_far.other_snap_better(&snapped) { snapped.snapped_point_document } else { 2. * relative - snapped_far.snapped_point_document }; snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }); } else { let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config); document_pos = snapped.snapped_point_document; snap.update_indicator(snapped); } } else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| colinear) { let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()), config); let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors), config); document_pos = if snapped_far.other_snap_better(&snapped) { snapped.snapped_point_document } else { 2. * relative - snapped_far.snapped_point_document }; snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }); } else { let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors), config); document_pos = snapped.snapped_point_document; snap.update_indicator(snapped); } if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) { if (relative - document_pos) != DVec2::ZERO && (relative - document_pos).length_squared() > f64::EPSILON * 100. { self.angle = -(relative - document_pos).angle_to(DVec2::X) } } transform.inverse().transform_point2(document_pos) } #[allow(clippy::too_many_arguments)] fn create_initial_point( &mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, tool_options: &PenOptions, append: bool, preferences: &PreferencesMessageHandler, shape_editor: &mut ShapeState, ) { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); self.handle_type = TargetHandle::FuturePreviewOutHandle; let selected_nodes = document.network_interface.selected_nodes(); self.handle_end = None; let tolerance = crate::consts::SNAP_POINT_TOLERANCE; let extension_choice = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences); if let Some((layer, point, position)) = extension_choice { self.current_layer = Some(layer); self.extend_existing_path(document, layer, point, position); return; } else if preferences.vector_meshes { if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { let (point, segments) = closest_segment.adjusted_insert(responses); let layer = closest_segment.layer(); let position = closest_segment.closest_point_document(); // Setting any one of the new segments created as the previous segment self.prior_segment_endpoint = Some(point); self.prior_segment_layer = Some(layer); self.prior_segments = Some(segments.to_vec()); self.extend_existing_path(document, layer, point, position); return; } } if append { if let Some((layer, point, _)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); if self.modifiers.lock_angle { self.set_lock_angle(&vector_data, point, segment); self.switch_to_free_on_ctrl_release = true; } } let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface); let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none()); if let Some(layer) = existing_layer { // Add point to existing layer responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport }); return; } } if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); self.handle_mode = HandleMode::Free; if self.modifiers.lock_angle { self.set_lock_angle(&vector_data, point, segment); self.switch_to_free_on_ctrl_release = true; } } // New path layer let node_type = resolve_document_node_type("Path").expect("Path node does not exist"); let nodes = vec![(NodeId(0), node_type.default_node_template())]; let parent = document.new_layer_bounding_artboard(input); let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); self.current_layer = Some(layer); tool_options.fill.apply_fill(layer, responses); tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); self.prior_segment = None; self.prior_segments = None; responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); // This causes the following message to be run only after the next graph evaluation runs and the transforms are updated responses.add(Message::StartBuffer); // It is necessary to defer this until the transform of the layer can be accurately computed (quite hacky) responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport }); } /// Perform extension of an existing path fn extend_existing_path(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, point: PointId, position: DVec2) { let vector_data = document.network_interface.compute_modified_vector(layer); let (handle_start, in_segment) = if let Some(vector_data) = &vector_data { vector_data .segment_bezier_iter() .find_map(|(segment_id, bezier, start, end)| { let is_end = point == end; let is_start = point == start; if !is_end && !is_start { return None; } let handle = match bezier.handles { BezierHandles::Cubic { handle_start, handle_end, .. } => { if is_start { handle_start } else { handle_end } } BezierHandles::Quadratic { handle } => handle, _ => return None, }; Some((segment_id, is_end, handle)) }) .map(|(segment_id, is_end, handle)| { let mirrored_handle = position * 2. - handle; let in_segment = if is_end { Some(segment_id) } else { None }; (mirrored_handle, in_segment) }) .unwrap_or_else(|| (position, None)) } else { (position, None) }; let in_segment = if self.modifiers.lock_angle { self.prior_segment } else { in_segment }; self.add_point(LastPoint { id: point, pos: position, in_segment, handle_start, }); self.next_point = position; self.next_handle_start = handle_start; let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); self.handle_mode = HandleMode::Free; if self.modifiers.lock_angle { self.set_lock_angle(&vector_data, point, segment); self.switch_to_free_on_ctrl_release = true; } } // Stores the segment and point ID of the clicked endpoint fn store_clicked_endpoint(&mut self, document: &DocumentMessageHandler, transform: &DAffine2, input: &InputPreprocessorMessageHandler, preferences: &PreferencesMessageHandler) { let mut manipulators = HashMap::with_hasher(NoHashBuilder); let mut unselected = Vec::new(); let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let tolerance = crate::consts::SNAP_POINT_TOLERANCE; self.prior_segment = None; self.prior_segment_endpoint = None; self.prior_segment_layer = None; self.prior_segments = None; if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { self.prior_segment_endpoint = Some(point); self.prior_segment_layer = Some(layer); let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); self.prior_segment = segment; layer_manipulators.insert(point); for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { if id == point { continue; } unselected.push(SnapCandidatePoint::handle(transform.transform_point2(position))) } manipulators.insert(layer, layer_manipulators); self.snap_cache = SnapCache { manipulators, unselected } } } fn set_lock_angle(&mut self, vector_data: &VectorData, anchor: PointId, segment: Option) { let anchor_position = vector_data.point_domain.position_from_id(anchor); let Some((anchor_position, segment)) = anchor_position.zip(segment) else { self.handle_mode = HandleMode::Free; return; }; match (self.handle_type, self.path_closed) { (TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => { if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) { self.angle = required_handle; self.handle_mode = HandleMode::ColinearEquidistant; } } (TargetHandle::PriorInHandle(..) | TargetHandle::PriorOutHandle(..), true) => { self.angle = -(self.handle_end.unwrap() - anchor_position).angle_to(DVec2::X); self.handle_mode = HandleMode::ColinearEquidistant; } _ => { self.angle = -(self.next_handle_start - anchor_position).angle_to(DVec2::X); self.handle_mode = HandleMode::ColinearEquidistant; } } } fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque, layer: LayerNodeIdentifier, viewport: DVec2) { // Add the first point let id = PointId::generate(); let pos = document.metadata().transform_to_viewport(layer).inverse().transform_point2(viewport); let modification_type = VectorModificationType::InsertPoint { id, position: pos }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); self.add_point(LastPoint { id, pos, in_segment: None, handle_start: pos, }); self.next_point = pos; self.next_handle_start = pos; self.handle_end = None; } } impl Fsm for PenToolFsmState { type ToolData = PenToolData; type ToolOptions = PenOptions; fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { let ToolActionHandlerData { document, global_tool_data, input, shape_editor, preferences, .. } = tool_action_data; let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); let layer = selected_layers.next().filter(|_| selected_layers.next().is_none()).or(tool_data.current_layer); let mut transform = layer.map(|layer| document.metadata().transform_to_document(layer)).unwrap_or_default(); if !transform.inverse().is_finite() { let parent_transform = layer.and_then(|layer| layer.parent(document.metadata())).map(|layer| document.metadata().transform_to_document(layer)); transform = parent_transform.unwrap_or(DAffine2::IDENTITY); } if !transform.inverse().is_finite() { transform = DAffine2::IDENTITY; } let ToolMessage::Pen(event) = event else { return self }; match (self, event) { (PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => { let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor }; let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor }; if latest.handle_start == latest.pos { return PenToolFsmState::PlacingAnchor; } let latest_pos = latest.pos; let latest_handle_start = latest.handle_start; let viewport = document.metadata().transform_to_viewport(layer); let last_point = viewport.transform_point2(latest.pos); let handle = viewport.transform_point2(latest.handle_start); if input.keyboard.key(grab) { responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle }); } else if input.keyboard.key(rotate) { responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle }); } else if input.keyboard.key(scale) { responses.add(TransformLayerMessage::BeginScalePen { last_point, handle }); } let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); tool_data.previous_handle_start_pos = latest.handle_start; let opposite_handle = tool_data.check_grs_end_handle(&vector_data); tool_data.previous_handle_end_pos = tool_data.target_handle_position(opposite_handle, &vector_data); let handle1 = latest_handle_start - latest_pos; let Some(opposite_handle_pos) = tool_data.target_handle_position(opposite_handle, &vector_data) else { return PenToolFsmState::GRSHandle; }; let handle2 = opposite_handle_pos - latest_pos; let pi = std::f64::consts::PI; let angle = handle1.angle_to(handle2); tool_data.colinear = (angle - pi).abs() < 1e-6 || (angle + pi).abs() < 1e-6; PenToolFsmState::GRSHandle } (PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position }) => { let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; let vector_data = document.network_interface.compute_modified_vector(layer); let Some(vector_data) = vector_data else { return PenToolFsmState::GRSHandle }; if let Some(latest_pt) = tool_data.latest_point_mut() { let layer_space_to_viewport = document.metadata().transform_to_viewport(layer); let final_pos = layer_space_to_viewport.inverse().transform_point2(final_position); latest_pt.handle_start = final_pos; } responses.add(OverlaysMessage::Draw); let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::GRSHandle; }; let opposite_handle = tool_data.check_grs_end_handle(&vector_data); let Some(opposite_handle_pos) = tool_data.target_handle_position(opposite_handle, &vector_data) else { return PenToolFsmState::GRSHandle; }; if tool_data.colinear { let Some(direction) = (latest.pos - latest.handle_start).try_normalize() else { return PenToolFsmState::GRSHandle; }; if (latest.pos - latest.handle_start).length_squared() < f64::EPSILON { return PenToolFsmState::GRSHandle; } let relative_distance = (opposite_handle_pos - latest.pos).length(); let relative_position = relative_distance * direction + latest.pos; tool_data.update_target_handle_pos(opposite_handle, latest.pos, responses, relative_position, layer); } responses.add(OverlaysMessage::Draw); PenToolFsmState::GRSHandle } (PenToolFsmState::GRSHandle, PenToolMessage::Confirm) => { tool_data.next_point = input.mouse.position; tool_data.next_handle_start = input.mouse.position; responses.add(OverlaysMessage::Draw); responses.add(PenToolMessage::PointerMove { snap_angle: Key::Control, break_handle: Key::Alt, lock_angle: Key::Shift, colinear: Key::KeyC, move_anchor_with_handles: Key::Space, }); PenToolFsmState::PlacingAnchor } (PenToolFsmState::GRSHandle, PenToolMessage::Abort) => { tool_data.next_point = input.mouse.position; tool_data.next_handle_start = input.mouse.position; let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let opposite_handle = tool_data.check_grs_end_handle(&vector_data); let previous = tool_data.previous_handle_start_pos; if let Some(latest) = tool_data.latest_point_mut() { latest.handle_start = previous; } else { return PenToolFsmState::PlacingAnchor; } responses.add(OverlaysMessage::Draw); responses.add(PenToolMessage::PointerMove { snap_angle: Key::Control, break_handle: Key::Alt, lock_angle: Key::Shift, colinear: Key::KeyC, move_anchor_with_handles: Key::Space, }); let Some((previous_pos, latest)) = tool_data.previous_handle_end_pos.zip(tool_data.latest_point()) else { return PenToolFsmState::PlacingAnchor; }; tool_data.update_target_handle_pos(opposite_handle, latest.pos, responses, previous_pos, layer); PenToolFsmState::PlacingAnchor } (_, PenToolMessage::SelectionChanged) => { responses.add(OverlaysMessage::Draw); self } (PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => { match tool_options.pen_overlay_mode { PenOverlayMode::AllHandles => { path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); } PenOverlayMode::FrontierHandles => { path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); } } // Check if there is an anchor within threshold // If not check if there is a closest segment within threshold, if yes then draw overlay let tolerance = crate::consts::SNAP_POINT_TOLERANCE; let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some(); if preferences.vector_meshes && !close_to_point { if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { let pos = closest_segment.closest_point_to_viewport(); let perp = closest_segment.calculate_perp(document); overlay_context.manipulator_anchor(pos, true, None); overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); } } tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); self } (_, PenToolMessage::Overlays(mut overlay_context)) => { let display_anchors = overlay_context.visibility_settings.anchors(); let display_handles = overlay_context.visibility_settings.handles(); let valid = |point: DVec2, handle: DVec2| point.distance_squared(handle) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; let transform = document.metadata().document_to_viewport * transform; // The currently-being-placed anchor let next_anchor = transform.transform_point2(tool_data.next_point); // The currently-being-placed anchor's outgoing handle (the one currently being dragged out) let next_handle_start = transform.transform_point2(tool_data.next_handle_start); // The most recently placed anchor let anchor_start = tool_data.latest_point().map(|point| transform.transform_point2(point.pos)); // The most recently placed anchor's incoming handle (opposite the one currently being dragged out) let handle_end = tool_data.handle_end.map(|point| transform.transform_point2(point)); // The most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) { let handles = BezierHandles::Cubic { handle_start, handle_end }; let end = tool_data.next_point; let bezier = Bezier { start, handles, end }; if (end - start).length_squared() > f64::EPSILON { // Draw the curve for the currently-being-placed segment overlay_context.outline_bezier(bezier, transform); } } if display_handles { // Draw the line between the currently-being-placed anchor and its currently-being-dragged-out outgoing handle (opposite the one currently being dragged out) overlay_context.line(next_anchor, next_handle_start, None, None); } match tool_options.pen_overlay_mode { PenOverlayMode::AllHandles => { path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); } PenOverlayMode::FrontierHandles => { if let Some(latest_segment) = tool_data.prior_segment { path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context); } // If a vector mesh then there can be more than one prior segments else if let Some(segments) = tool_data.prior_segments.clone() { if preferences.vector_meshes { path_overlays(document, DrawHandles::SelectedAnchors(segments), shape_editor, &mut overlay_context); } } else { path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); }; } } if let (Some(anchor_start), Some(handle_start), Some(handle_end)) = (anchor_start, handle_start, handle_end) { if display_handles { // Draw the line between the most recently placed anchor and its outgoing handle (which is currently influencing the currently-being-placed segment) overlay_context.line(anchor_start, handle_start, None, None); // Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out) overlay_context.line(next_anchor, handle_end, None, None); } if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle { // Draw the line between the currently-being-placed anchor and last-placed point (lock angle bent overlays) overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5)); } // Draw the line between the currently-being-placed anchor and last-placed point (snap angle bent overlays) if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.snap_angle { overlay_context.dashed_line(anchor_start, next_anchor, None, None, Some(4.), Some(4.), Some(0.5)); } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) && display_handles { // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) let selected = tool_data.handle_type == TargetHandle::PreviewInHandle; if display_handles { overlay_context.manipulator_handle(handle_end, selected, None); overlay_context.manipulator_handle(handle_end, selected, None); } } if valid(anchor_start, handle_start) && display_handles { // Draw the handle circle for the most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) overlay_context.manipulator_handle(handle_start, false, None); } } else { // Draw the whole path and its manipulators when the user is clicking-and-dragging out from the most recently placed anchor to set its outgoing handle, during which it would otherwise not have its overlays drawn match tool_options.pen_overlay_mode { PenOverlayMode::AllHandles => { path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); } PenOverlayMode::FrontierHandles => { path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); } } } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) && display_handles { // Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor) let selected = tool_data.handle_type == TargetHandle::FuturePreviewOutHandle; overlay_context.manipulator_handle(next_handle_start, selected, None); } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && display_anchors { // Draw the anchor square for the most recently placed anchor overlay_context.manipulator_anchor(next_anchor, false, None); } if self == PenToolFsmState::PlacingAnchor && preferences.vector_meshes { let tolerance = crate::consts::SNAP_POINT_TOLERANCE; let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let close_to_point = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences).is_some(); if !close_to_point { if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, viewport, tolerance) { let pos = closest_segment.closest_point_to_viewport(); let perp = closest_segment.calculate_perp(document); overlay_context.manipulator_anchor(pos, true, None); overlay_context.line(pos - perp * SEGMENT_OVERLAY_SIZE, pos + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None); } } } // Display a filled overlay of the shape if the new point closes the path if let Some(latest_point) = tool_data.latest_point() { let handle_start = latest_point.handle_start; let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start); let next_point = tool_data.next_point; let start = latest_point.id; if let Some(layer) = layer { let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { vector_data.point_domain.position_from_id(id).map_or(false, |pos| { let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) }) }); // We have the point. Join the 2 vertices and check if any path is closed. if let Some(end) = closest_point { let segment_id = SegmentId::generate(); vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); let grouped_segments = vector_data.auto_join_paths(); let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); let subpaths: Vec<_> = closed_paths .filter_map(|path| { let segments = path.edges.iter().filter_map(|edge| { vector_data .segment_domain .iter() .find(|(id, _, _, _)| id == &edge.id) .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) }); vector_data.subpath_from_segments_ignore_discontinuities(segments) }) .collect(); 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, '#'); overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); } } } // Draw the overlays that visualize current snapping tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); self } (_, PenToolMessage::WorkingColorChanged) => { responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors( Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color), ))); self } (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { responses.add(DocumentMessage::StartTransaction); tool_data.handle_mode = HandleMode::Free; // Get the closest point and the segment it is on let append = input.keyboard.key(append_to_selected); tool_data.store_clicked_endpoint(document, &transform, input, preferences); tool_data.create_initial_point(document, input, responses, tool_options, append, preferences, shape_editor); // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle PenToolFsmState::DraggingHandle(tool_data.handle_mode) } (_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => { tool_data.add_point_layer_position(document, responses, layer, viewport); self } (state, PenToolMessage::RecalculateLatestPointsPosition) => { tool_data.recalculate_latest_points_position(document); state } (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart { append_to_selected }) => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); // Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector) if tool_data.buffering_merged_vector { if let Some(layer) = layer { tool_data.buffering_merged_vector = false; tool_data.handle_mode = HandleMode::ColinearLocked; tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer, preferences, shape_editor, responses); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses); } tool_data.buffering_merged_vector = false; PenToolFsmState::DraggingHandle(tool_data.handle_mode) } else { if tool_data.handle_end.is_some() { responses.add(DocumentMessage::StartTransaction); } // Merge two layers if the point is connected to the end point of another path // This might not be the correct solution to artboards being included as the other layer, which occurs due to the compute_modified_vector call in should_extend using the click targets for a layer instead of vector data. let layers = LayerNodeIdentifier::ROOT_PARENT .descendants(document.metadata()) .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); if let Some((other_layer, _, _)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, layers, preferences) { let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); if let Some(current_layer) = selected_layers .next() .filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) .or(tool_data.current_layer.filter(|layer| *layer != other_layer)) { merge_layers(document, current_layer, other_layer, responses); } } // Even if no buffer was started, the message still has to be run again in order to call bend_from_previous_point tool_data.buffering_merged_vector = true; responses.add(PenToolMessage::DragStart { append_to_selected }); PenToolFsmState::PlacingAnchor } } (PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => { if let Some(last_point) = tool_data.latest_points.last_mut() { last_point.handle_start = last_point.pos; responses.add(OverlaysMessage::Draw); } else { log::trace!("No latest point available to modify handle_start."); } self } (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { tool_data.cleanup_target_selections(shape_editor, layer, document, responses); tool_data .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) .unwrap_or(PenToolFsmState::PlacingAnchor) } ( PenToolFsmState::DraggingHandle(_), PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, }, ) => { tool_data.modifiers = ModifierState { snap_angle: input.keyboard.key(snap_angle), lock_angle: input.keyboard.key(lock_angle), break_handle: input.keyboard.key(break_handle), colinear: input.keyboard.key(colinear), move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), }; let snap_data = SnapData::new(document, input); if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce { tool_data.handle_mode = match tool_data.handle_mode { HandleMode::Free => { let last_segment = tool_data.prior_segment; if let Some(latest) = tool_data.latest_point_mut() { latest.in_segment = last_segment; } HandleMode::ColinearEquidistant } HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => HandleMode::Free, }; tool_data.toggle_colinear_debounce = true; } let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) else { return self; }; if tool_data.modifiers.move_anchor_with_handles && !tool_data.space_pressed { let reference_handle = if tool_data.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; let handle_start = layer.map(|layer| { document .metadata() .transform_to_viewport(layer) .transform_point2(tool_data.target_handle_position(reference_handle, &vector_data).unwrap()) }); tool_data.handle_start_offset = handle_start.map(|start| start - input.mouse.position); tool_data.space_pressed = true; } if !tool_data.modifiers.move_anchor_with_handles { tool_data.space_pressed = false; } if !tool_data.modifiers.colinear { tool_data.toggle_colinear_debounce = false; } if !tool_data.modifiers.lock_angle { tool_data.angle_locked = false; } let state = tool_data .drag_handle(snap_data, transform, input.mouse.position, responses, layer, input) .unwrap_or(PenToolFsmState::Ready); if tool_data.handle_swapped { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); } // Auto-panning let messages = [ PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); state } ( PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, }, ) => { tool_data.switch_to_free_on_ctrl_release = false; tool_data.alt_pressed = false; tool_data.modifiers = ModifierState { snap_angle: input.keyboard.key(snap_angle), lock_angle: input.keyboard.key(lock_angle), break_handle: input.keyboard.key(break_handle), colinear: input.keyboard.key(colinear), move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), }; let state = tool_data .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) .unwrap_or(PenToolFsmState::Ready); // Auto-panning let messages = [ PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); state } (PenToolFsmState::DraggingHandle(_), PenToolMessage::SwapHandles) => { if !tool_data.handle_swapped { tool_data.handle_swapped = true } tool_data.swap_handles(layer, document, shape_editor, input, responses); responses.add(OverlaysMessage::Draw); self } ( PenToolFsmState::Ready, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, }, ) => { tool_data.modifiers = ModifierState { snap_angle: input.keyboard.key(snap_angle), lock_angle: input.keyboard.key(lock_angle), break_handle: input.keyboard.key(break_handle), colinear: input.keyboard.key(colinear), move_anchor_with_handles: input.keyboard.key(move_anchor_with_handles), }; tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); responses.add(OverlaysMessage::Draw); self } (PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => { // Auto-panning let _ = tool_data.auto_panning.shift_viewport(input, responses); PenToolFsmState::DraggingHandle(mode) } (PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => { if !input.mouse.mouse_keys.contains(MouseKeys::LEFT) { return self; } // Auto-panning let _ = tool_data.auto_panning.shift_viewport(input, responses); PenToolFsmState::PlacingAnchor } ( state, PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, }, ) => { // Auto-panning let messages = [ PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle, colinear, move_anchor_with_handles, } .into(), ]; tool_data.auto_panning.stop(&messages, responses); state } (PenToolFsmState::DraggingHandle(..), PenToolMessage::Confirm) => { // Confirm to end path if let Some((vector_data, layer)) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)).zip(layer) { let single_point_in_layer = vector_data.point_domain.ids().len() == 1; tool_data.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses); let latest_points = tool_data.latest_points.len() == 1; if latest_points && single_point_in_layer { responses.add(NodeGraphMessage::DeleteNodes { node_ids: vec![layer.to_node()], delete_children: true, }); responses.add(NodeGraphMessage::RunDocumentGraph); } else if (latest_points && tool_data.prior_segment_endpoint.is_none()) || (tool_data.prior_segment_endpoint.is_some() && tool_data.prior_segment_layer != Some(layer) && latest_points) { let vector_modification = VectorModificationType::RemovePoint { id: tool_data.latest_point().unwrap().id, }; responses.add(GraphOperationMessage::Vector { layer, modification_type: vector_modification, }); responses.add(PenToolMessage::Abort); } else { responses.add(DocumentMessage::EndTransaction); } } tool_data.cleanup(responses); tool_data.cleanup_target_selections(shape_editor, layer, document, responses); responses.add(OverlaysMessage::Draw); PenToolFsmState::Ready } (PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { responses.add(DocumentMessage::EndTransaction); tool_data.cleanup(responses); tool_data.cleanup_target_selections(shape_editor, layer, document, responses); PenToolFsmState::Ready } (PenToolFsmState::DraggingHandle(..), PenToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); if tool_data.handle_end.is_none() { tool_data.cleanup(responses); tool_data.cleanup_target_selections(shape_editor, layer, document, responses); PenToolFsmState::Ready } else { tool_data .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) .unwrap_or(PenToolFsmState::Ready) } } (PenToolFsmState::PlacingAnchor, PenToolMessage::Abort) => { let should_delete_layer = if let Some(vector_data) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) { vector_data.point_domain.ids().len() == 1 } else { false }; responses.add(DocumentMessage::AbortTransaction); tool_data.cleanup(responses); tool_data.cleanup_target_selections(shape_editor, layer, document, responses); if should_delete_layer { responses.add(NodeGraphMessage::DeleteNodes { node_ids: vec![layer.unwrap().to_node()], delete_children: true, }); responses.add(NodeGraphMessage::RunDocumentGraph); } responses.add(OverlaysMessage::Draw); PenToolFsmState::Ready } (_, PenToolMessage::Abort) => PenToolFsmState::Ready, (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { if tool_data.point_index > 0 { tool_data.point_index -= 1; tool_data .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) .unwrap_or(PenToolFsmState::PlacingAnchor) } else { responses.add(PenToolMessage::Abort); self } } (_, PenToolMessage::Redo) => { tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1)); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses); match tool_data.point_index { 0 => PenToolFsmState::Ready, _ => PenToolFsmState::PlacingAnchor, } } _ => self, } } fn update_hints(&self, responses: &mut VecDeque) { let hint_data = match self { PenToolFsmState::Ready | PenToolFsmState::GRSHandle => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, "Draw Path"), // TODO: Only show this if a single layer is selected and it's of a valid type (e.g. a vector path but not raster or artboard) HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(), ])]), PenToolFsmState::PlacingAnchor => HintData(vec![ HintGroup(vec![ HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "").prepend_slash(), HintInfo::keys([Key::Enter], "End Path").prepend_slash(), ]), HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Control], "Lock Angle")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]), HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash(), HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), ]), ]), PenToolFsmState::DraggingHandle(mode) => { let mut dragging_hint_data = HintData(Vec::new()); dragging_hint_data.0.push(HintGroup(vec![ HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel Segment").prepend_slash(), HintInfo::keys([Key::Enter], "End Path"), ])); let mut toggle_group = match mode { HandleMode::Free => { vec![HintInfo::keys([Key::KeyC], "Make Handles Colinear")] } HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => { vec![HintInfo::keys([Key::KeyC], "Break Colinear Handles")] } }; toggle_group.push(HintInfo::keys([Key::Tab], "Swap Dragged Handle")); let mut common_hints = vec![HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Control], "Lock Angle")]; let mut hold_group = match mode { HandleMode::Free => common_hints, HandleMode::ColinearLocked => { common_hints.push(HintInfo::keys([Key::Alt], "Non-Equidistant Handles")); common_hints } HandleMode::ColinearEquidistant => { common_hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); common_hints } }; hold_group.push(HintInfo::keys([Key::Space], "Drag Anchor")); dragging_hint_data.0.push(HintGroup(toggle_group)); dragging_hint_data.0.push(HintGroup(hold_group)); dragging_hint_data } }; responses.add(FrontendMessage::UpdateInputHints { hint_data }); } fn update_cursor(&self, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } }