Make the Path tool support multi-point conversion between smooth/sharp on double-click (#2498)
* kinda works * solved merge conflicts * implement the multi flip * nit-picks * removed extra functions * Fix inputs not being passed to backend for repeated double-clicks * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
9236bfcec0
commit
ddb2d744d4
|
|
@ -136,3 +136,6 @@ pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||||
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
|
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
|
||||||
|
|
||||||
|
// INPUT
|
||||||
|
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ impl SelectedLayerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ignore_handles(&mut self, status: bool) {
|
pub fn ignore_handles(&mut self, status: bool) {
|
||||||
if self.ignore_handles == !status {
|
if self.ignore_handles != status {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ impl SelectedLayerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ignore_anchors(&mut self, status: bool) {
|
pub fn ignore_anchors(&mut self, status: bool) {
|
||||||
if self.ignore_anchors == !status {
|
if self.ignore_anchors != status {
|
||||||
return;
|
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)
|
// 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. {
|
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;
|
segment_angle += std::f64::consts::FRAC_PI_2;
|
||||||
DVec2::new(segment_angle.cos(), segment_angle.sin())
|
DVec2::new(segment_angle.cos(), segment_angle.sin())
|
||||||
} else {
|
} 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 (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
|
||||||
let Some(direction) = non_zero_handle
|
let Some(direction) = non_zero_handle
|
||||||
.to_manipulator_point()
|
.to_manipulator_point()
|
||||||
.get_position(&vector_data)
|
.get_position(vector_data)
|
||||||
.and_then(|position| (position - anchor_position).try_normalize())
|
.and_then(|position| (position - anchor_position).try_normalize())
|
||||||
else {
|
else {
|
||||||
return;
|
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)).
|
/// 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.
|
/// 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.
|
/// This can can be activated by double clicking on an anchor with the Path tool.
|
||||||
|
|
@ -1568,10 +1569,12 @@ impl ShapeState {
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
// Check by comparing the handle positions to the anchor if this manipulator group is a point
|
// Check by comparing the handle positions to the anchor if this manipulator group is a point
|
||||||
|
for point in self.selected_points() {
|
||||||
|
let Some(point_id) = point.as_anchor() else { continue };
|
||||||
if positions != 0 {
|
if positions != 0 {
|
||||||
self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer);
|
self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer);
|
||||||
} else {
|
} else {
|
||||||
for handle in vector_data.all_connected(id) {
|
for handle in vector_data.all_connected(point_id) {
|
||||||
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
|
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
|
||||||
|
|
||||||
match bezier.handles {
|
match bezier.handles {
|
||||||
|
|
@ -1606,6 +1609,7 @@ impl ShapeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Some(true)
|
Some(true)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,8 @@ struct PathToolData {
|
||||||
select_anchor_toggled: bool,
|
select_anchor_toggled: bool,
|
||||||
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
|
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
|
||||||
handle_drag_toggle: bool,
|
handle_drag_toggle: bool,
|
||||||
|
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
|
||||||
|
last_click_time: u64,
|
||||||
dragging_state: DraggingState,
|
dragging_state: DraggingState,
|
||||||
angle: f64,
|
angle: f64,
|
||||||
opposite_handle_position: Option<DVec2>,
|
opposite_handle_position: Option<DVec2>,
|
||||||
|
|
@ -448,6 +450,12 @@ impl PathToolData {
|
||||||
|
|
||||||
self.drag_start_pos = input.mouse.position;
|
self.drag_start_pos = input.mouse.position;
|
||||||
|
|
||||||
|
if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) {
|
||||||
|
self.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_click_time = input.time;
|
||||||
|
|
||||||
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
|
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
|
||||||
|
|
||||||
// Check if the point is already selected; if not, select the first point within the threshold (in pixels)
|
// 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);
|
let modification_type = handle.set_relative_position(DVec2::ZERO);
|
||||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
for &handles in &vector_data.colinear_manipulators {
|
for &handles in &vector_data.colinear_manipulators {
|
||||||
if handles.contains(&handle) {
|
if handles.contains(handle) {
|
||||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +514,7 @@ impl PathToolData {
|
||||||
|
|
||||||
if let Some((Some(point), Some(vector_data))) = shape_editor
|
if let Some((Some(point), Some(vector_data))) = shape_editor
|
||||||
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
|
.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
|
let handles = vector_data
|
||||||
.all_connected(point)
|
.all_connected(point)
|
||||||
|
|
@ -1296,6 +1304,10 @@ impl Fsm for PathToolFsmState {
|
||||||
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
||||||
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
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 there is a point nearby, then remove the overlay
|
||||||
if shape_editor
|
if shape_editor
|
||||||
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
|
.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 {
|
if !drag_occurred && !extend_selection {
|
||||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||||
if clicked_selected {
|
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::<HashSet<_>>();
|
||||||
|
}
|
||||||
|
|
||||||
shape_editor.deselect_all_points();
|
shape_editor.deselect_all_points();
|
||||||
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
|
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
|
|
@ -1537,7 +1553,11 @@ impl Fsm for PathToolFsmState {
|
||||||
// Flip the selected point between smooth and sharp
|
// 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 {
|
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
|
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>());
|
||||||
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
|
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(DocumentMessage::EndTransaction);
|
||||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (textToolInteractiveInputElement) return;
|
if (textToolInteractiveInputElement) return;
|
||||||
|
|
||||||
// Allow only double-clicks
|
// 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
|
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
|
||||||
let buttons = 1;
|
let buttons = 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue