diff --git a/editor/src/consts.rs b/editor/src/consts.rs index f835d4b963..9b58d589bf 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -136,3 +136,6 @@ pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15; + +// INPUT +pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 39053e93ac..53cb4794aa 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -70,7 +70,7 @@ impl SelectedLayerState { } pub fn ignore_handles(&mut self, status: bool) { - if self.ignore_handles == !status { + if self.ignore_handles != status { return; } @@ -86,7 +86,7 @@ impl SelectedLayerState { } pub fn ignore_anchors(&mut self, status: bool) { - if self.ignore_anchors == !status { + if self.ignore_anchors != status { return; } @@ -774,7 +774,7 @@ impl ShapeState { // For a non-endpoint anchor, handles are perpendicular to the average tangent of adjacent segments.(Refer:https://github.com/GraphiteEditor/Graphite/pull/2620#issuecomment-2881501494) let mut handle_direction = if segment_count > 1. { - segment_angle = segment_angle / segment_count; + segment_angle /= segment_count; segment_angle += std::f64::consts::FRAC_PI_2; DVec2::new(segment_angle.cos(), segment_angle.sin()) } else { @@ -801,7 +801,7 @@ impl ShapeState { let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) }; let Some(direction) = non_zero_handle .to_manipulator_point() - .get_position(&vector_data) + .get_position(vector_data) .and_then(|position| (position - anchor_position).try_normalize()) else { return; @@ -1538,6 +1538,7 @@ impl ShapeState { } } } + /// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)). /// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount. /// This can can be activated by double clicking on an anchor with the Path tool. @@ -1568,44 +1569,47 @@ impl ShapeState { .count(); // Check by comparing the handle positions to the anchor if this manipulator group is a point - if positions != 0 { - self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer); - } else { - for handle in vector_data.all_connected(id) { - let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue }; - - match bezier.handles { - BezierHandles::Linear => {} - BezierHandles::Quadratic { .. } => { - let segment = handle.segment; - // Convert to linear - let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + for point in self.selected_points() { + let Some(point_id) = point.as_anchor() else { continue }; + if positions != 0 { + self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer); + } else { + for handle in vector_data.all_connected(point_id) { + let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue }; + + match bezier.handles { + BezierHandles::Linear => {} + BezierHandles::Quadratic { .. } => { + let segment = handle.segment; + // Convert to linear + let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); - // Set the manipulator to have non-colinear handles - for &handles in &vector_data.colinear_manipulators { - if handles.contains(&HandleId::primary(segment)) { - let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + // Set the manipulator to have non-colinear handles + for &handles in &vector_data.colinear_manipulators { + if handles.contains(&HandleId::primary(segment)) { + let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } } - } - BezierHandles::Cubic { .. } => { - // Set handle position to anchor position - let modification_type = handle.set_relative_position(DVec2::ZERO); - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + BezierHandles::Cubic { .. } => { + // Set handle position to anchor position + let modification_type = handle.set_relative_position(DVec2::ZERO); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); - // Set the manipulator to have non-colinear handles - 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 }); + // Set the manipulator to have non-colinear handles + 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 }); + } } } } } - } - }; + }; + } Some(true) }; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 5d9f622860..d08778db1d 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -365,6 +365,8 @@ struct PathToolData { select_anchor_toggled: bool, saved_points_before_handle_drag: Vec, handle_drag_toggle: bool, + saved_points_before_anchor_convert_smooth_sharp: HashSet, + last_click_time: u64, dragging_state: DraggingState, angle: f64, opposite_handle_position: Option, @@ -448,6 +450,12 @@ impl PathToolData { 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::>(); // Check if the point is already selected; if not, select the first point within the threshold (in pixels) @@ -489,7 +497,7 @@ impl PathToolData { 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) { + if handles.contains(handle) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } @@ -506,7 +514,7 @@ impl PathToolData { if let Some((Some(point), Some(vector_data))) = shape_editor .find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) - .and_then(|(layer, point)| Some((point.as_anchor(), document.network_interface.compute_modified_vector(layer)))) + .map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer))) { let handles = vector_data .all_connected(point) @@ -1296,6 +1304,10 @@ impl Fsm for PathToolFsmState { (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 there is a point nearby, then remove the overlay if shape_editor .find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) @@ -1490,6 +1502,10 @@ impl Fsm for PathToolFsmState { if !drag_occurred && !extend_selection { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); if 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::>(); + } + shape_editor.deselect_all_points(); shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point); responses.add(OverlaysMessage::Draw); @@ -1537,7 +1553,11 @@ impl Fsm for PathToolFsmState { // Flip the selected point between smooth and sharp 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::>()); 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); } diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 4f9ee43a4c..94d3494e6e 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -212,7 +212,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (textToolInteractiveInputElement) return; // Allow only double-clicks - if (e.detail !== 2) return; + if (e.detail % 2 == 1) return; // `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead let buttons = 1;