Skip to content

Commit ddb2d74

Browse files
0SlowPoke0Keavon
andauthored
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>
1 parent 9236bfc commit ddb2d74

File tree

4 files changed

+64
-37
lines changed

4 files changed

+64
-37
lines changed

editor/src/consts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,6 @@ pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
136136
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
137137
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
138138
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
139+
140+
// INPUT
141+
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl SelectedLayerState {
7070
}
7171

7272
pub fn ignore_handles(&mut self, status: bool) {
73-
if self.ignore_handles == !status {
73+
if self.ignore_handles != status {
7474
return;
7575
}
7676

@@ -86,7 +86,7 @@ impl SelectedLayerState {
8686
}
8787

8888
pub fn ignore_anchors(&mut self, status: bool) {
89-
if self.ignore_anchors == !status {
89+
if self.ignore_anchors != status {
9090
return;
9191
}
9292

@@ -774,7 +774,7 @@ impl ShapeState {
774774

775775
// 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)
776776
let mut handle_direction = if segment_count > 1. {
777-
segment_angle = segment_angle / segment_count;
777+
segment_angle /= segment_count;
778778
segment_angle += std::f64::consts::FRAC_PI_2;
779779
DVec2::new(segment_angle.cos(), segment_angle.sin())
780780
} else {
@@ -801,7 +801,7 @@ impl ShapeState {
801801
let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
802802
let Some(direction) = non_zero_handle
803803
.to_manipulator_point()
804-
.get_position(&vector_data)
804+
.get_position(vector_data)
805805
.and_then(|position| (position - anchor_position).try_normalize())
806806
else {
807807
return;
@@ -1538,6 +1538,7 @@ impl ShapeState {
15381538
}
15391539
}
15401540
}
1541+
15411542
/// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)).
15421543
/// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount.
15431544
/// This can can be activated by double clicking on an anchor with the Path tool.
@@ -1568,44 +1569,47 @@ impl ShapeState {
15681569
.count();
15691570

15701571
// Check by comparing the handle positions to the anchor if this manipulator group is a point
1571-
if positions != 0 {
1572-
self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer);
1573-
} else {
1574-
for handle in vector_data.all_connected(id) {
1575-
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
1576-
1577-
match bezier.handles {
1578-
BezierHandles::Linear => {}
1579-
BezierHandles::Quadratic { .. } => {
1580-
let segment = handle.segment;
1581-
// Convert to linear
1582-
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
1583-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1572+
for point in self.selected_points() {
1573+
let Some(point_id) = point.as_anchor() else { continue };
1574+
if positions != 0 {
1575+
self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer);
1576+
} else {
1577+
for handle in vector_data.all_connected(point_id) {
1578+
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
1579+
1580+
match bezier.handles {
1581+
BezierHandles::Linear => {}
1582+
BezierHandles::Quadratic { .. } => {
1583+
let segment = handle.segment;
1584+
// Convert to linear
1585+
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
1586+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
15841587

1585-
// Set the manipulator to have non-colinear handles
1586-
for &handles in &vector_data.colinear_manipulators {
1587-
if handles.contains(&HandleId::primary(segment)) {
1588-
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
1589-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1588+
// Set the manipulator to have non-colinear handles
1589+
for &handles in &vector_data.colinear_manipulators {
1590+
if handles.contains(&HandleId::primary(segment)) {
1591+
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
1592+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1593+
}
15901594
}
15911595
}
1592-
}
1593-
BezierHandles::Cubic { .. } => {
1594-
// Set handle position to anchor position
1595-
let modification_type = handle.set_relative_position(DVec2::ZERO);
1596-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1596+
BezierHandles::Cubic { .. } => {
1597+
// Set handle position to anchor position
1598+
let modification_type = handle.set_relative_position(DVec2::ZERO);
1599+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
15971600

1598-
// Set the manipulator to have non-colinear handles
1599-
for &handles in &vector_data.colinear_manipulators {
1600-
if handles.contains(&handle) {
1601-
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
1602-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1601+
// Set the manipulator to have non-colinear handles
1602+
for &handles in &vector_data.colinear_manipulators {
1603+
if handles.contains(&handle) {
1604+
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
1605+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1606+
}
16031607
}
16041608
}
16051609
}
16061610
}
1607-
}
1608-
};
1611+
};
1612+
}
16091613

16101614
Some(true)
16111615
};

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ struct PathToolData {
365365
select_anchor_toggled: bool,
366366
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
367367
handle_drag_toggle: bool,
368+
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
369+
last_click_time: u64,
368370
dragging_state: DraggingState,
369371
angle: f64,
370372
opposite_handle_position: Option<DVec2>,
@@ -448,6 +450,12 @@ impl PathToolData {
448450

449451
self.drag_start_pos = input.mouse.position;
450452

453+
if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) {
454+
self.saved_points_before_anchor_convert_smooth_sharp.clear();
455+
}
456+
457+
self.last_click_time = input.time;
458+
451459
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
452460

453461
// Check if the point is already selected; if not, select the first point within the threshold (in pixels)
@@ -489,7 +497,7 @@ impl PathToolData {
489497
let modification_type = handle.set_relative_position(DVec2::ZERO);
490498
responses.add(GraphOperationMessage::Vector { layer, modification_type });
491499
for &handles in &vector_data.colinear_manipulators {
492-
if handles.contains(&handle) {
500+
if handles.contains(handle) {
493501
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
494502
responses.add(GraphOperationMessage::Vector { layer, modification_type });
495503
}
@@ -506,7 +514,7 @@ impl PathToolData {
506514

507515
if let Some((Some(point), Some(vector_data))) = shape_editor
508516
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
509-
.and_then(|(layer, point)| Some((point.as_anchor(), document.network_interface.compute_modified_vector(layer))))
517+
.map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer)))
510518
{
511519
let handles = vector_data
512520
.all_connected(point)
@@ -1296,6 +1304,10 @@ impl Fsm for PathToolFsmState {
12961304
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
12971305
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
12981306

1307+
if !tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
1308+
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
1309+
}
1310+
12991311
// If there is a point nearby, then remove the overlay
13001312
if shape_editor
13011313
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
@@ -1490,6 +1502,10 @@ impl Fsm for PathToolFsmState {
14901502
if !drag_occurred && !extend_selection {
14911503
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
14921504
if clicked_selected {
1505+
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
1506+
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
1507+
}
1508+
14931509
shape_editor.deselect_all_points();
14941510
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
14951511
responses.add(OverlaysMessage::Draw);
@@ -1537,7 +1553,11 @@ impl Fsm for PathToolFsmState {
15371553
// Flip the selected point between smooth and sharp
15381554
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
15391555
responses.add(DocumentMessage::StartTransaction);
1556+
1557+
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>());
15401558
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
1559+
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
1560+
15411561
responses.add(DocumentMessage::EndTransaction);
15421562
responses.add(PathToolMessage::SelectedPointUpdated);
15431563
}

frontend/src/io-managers/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
212212
if (textToolInteractiveInputElement) return;
213213

214214
// Allow only double-clicks
215-
if (e.detail !== 2) return;
215+
if (e.detail % 2 == 1) return;
216216

217217
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
218218
let buttons = 1;

0 commit comments

Comments
 (0)