Skip to content

Commit 668acd3

Browse files
4adexKeavon
andauthored
Improve the Path tool's segment editing mode and make hovering manipulators react contextually (#2860)
* Improve path editing mode * Code review * Tidy up UI * Update path selection behaviour * Fix linting * Remove frozen flag * Code review * Fix segment split * Fix deleting segments * Add requred methods in vello overlay context --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 523132d commit 668acd3

File tree

7 files changed

+555
-95
lines changed

7 files changed

+555
-95
lines changed

editor/src/consts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
106106
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
107107
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
108108
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
109+
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
109110
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
110111

111112
// PEN TOOL

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,13 @@ pub fn input_mappings() -> Mapping {
212212
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
213213
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
214214
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
215-
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
215+
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, segment_editing_modifier: Control }),
216216
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
217217
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
218218
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
219219
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
220220
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
221-
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt }),
221+
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }),
222222
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
223223
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
224224
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),

editor/src/messages/portfolio/document/overlays/utility_types.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::utility_functions::overlay_canvas_context;
22
use crate::consts::{
33
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
44
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
5-
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
5+
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
66
};
77
use crate::messages::prelude::Message;
88
use bezier_rs::{Bezier, Subpath};
@@ -460,6 +460,42 @@ impl OverlayContext {
460460
self.square(position, None, Some(color_fill), Some(color_stroke));
461461
}
462462

463+
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
464+
self.start_dpi_aware_transform();
465+
466+
let position = position.round() - DVec2::splat(0.5);
467+
468+
self.render_context.begin_path();
469+
self.render_context
470+
.arc(position.x, position.y, (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2., 0., TAU)
471+
.expect("Failed to draw the circle");
472+
473+
self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50);
474+
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
475+
self.render_context.fill();
476+
self.render_context.stroke();
477+
478+
self.render_context.begin_path();
479+
self.render_context
480+
.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU)
481+
.expect("Failed to draw the circle");
482+
483+
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
484+
485+
self.render_context.set_fill_style_str(color_fill);
486+
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
487+
self.render_context.fill();
488+
self.render_context.stroke();
489+
490+
self.end_dpi_aware_transform();
491+
}
492+
493+
pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
494+
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
495+
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
496+
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
497+
}
498+
463499
/// Transforms the canvas context to adjust for DPI scaling
464500
///
465501
/// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`].
@@ -758,7 +794,7 @@ impl OverlayContext {
758794
self.render_context.begin_path();
759795
self.bezier_command(bezier, transform, true);
760796
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
761-
self.render_context.set_line_width(4.);
797+
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
762798
self.render_context.stroke();
763799

764800
self.render_context.set_line_width(1.);
@@ -772,7 +808,7 @@ impl OverlayContext {
772808
self.render_context.begin_path();
773809
self.bezier_command(bezier, transform, true);
774810
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
775-
self.render_context.set_line_width(4.);
811+
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
776812
self.render_context.stroke();
777813

778814
self.render_context.set_line_width(1.);

editor/src/messages/portfolio/document/overlays/utility_types_vello.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,36 @@ impl OverlayContext {
293293
.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
294294
}
295295

296+
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
297+
let transform = self.get_transform();
298+
299+
let position = position.round() - DVec2::splat(0.5);
300+
301+
let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2.);
302+
303+
let fill = COLOR_OVERLAY_BLUE_50;
304+
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
305+
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle);
306+
307+
let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
308+
309+
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
310+
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
311+
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle);
312+
}
313+
296314
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
297315
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
298316
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
299317
self.square(position, None, Some(color_fill), Some(color_stroke));
300318
}
301319

320+
pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
321+
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
322+
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
323+
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
324+
}
325+
302326
fn get_transform(&self) -> kurbo::Affine {
303327
kurbo::Affine::scale(self.device_pixel_ratio)
304328
}

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

Lines changed: 120 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,16 @@ impl ClosestSegment {
302302
(midpoint, segment_ids)
303303
}
304304

305-
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
306-
let (id, _) = self.adjusted_insert(responses);
307-
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
305+
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool, point_mode: bool, is_segment_selected: bool) {
306+
let (id, segments) = self.adjusted_insert(responses);
307+
if point_mode || is_segment_selected {
308+
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection);
309+
}
310+
311+
if is_segment_selected {
312+
let Some(state) = shape_editor.selected_shape_state.get_mut(&self.layer) else { return };
313+
segments.iter().for_each(|segment| state.select_segment(*segment));
314+
}
308315
}
309316

310317
pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 {
@@ -551,7 +558,7 @@ impl ShapeState {
551558
select_threshold: f64,
552559
extend_selection: bool,
553560
path_overlay_mode: PathOverlayMode,
554-
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
561+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
555562
) -> Option<Option<SelectedPointsInfo>> {
556563
if self.selected_shape_state.is_empty() {
557564
return None;
@@ -600,18 +607,18 @@ impl ShapeState {
600607
mouse_position: DVec2,
601608
select_threshold: f64,
602609
path_overlay_mode: PathOverlayMode,
603-
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
610+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
604611
point_editing_mode: bool,
605612
) -> Option<(bool, Option<SelectedPointsInfo>)> {
606613
if self.selected_shape_state.is_empty() {
607614
return None;
608615
}
609616

610-
if !point_editing_mode {
611-
return None;
612-
}
613-
614617
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
618+
// If not point editing mode then only handles are allowed to be dragged
619+
if !point_editing_mode && matches!(manipulator_point_id, ManipulatorPointId::Anchor(_)) {
620+
return None;
621+
}
615622
let vector_data = network_interface.compute_modified_vector(layer)?;
616623
let point_position = manipulator_point_id.get_position(&vector_data)?;
617624

@@ -1483,6 +1490,23 @@ impl ShapeState {
14831490
}
14841491
}
14851492

1493+
pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
1494+
for (&layer, state) in &self.selected_shape_state {
1495+
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
1496+
continue;
1497+
};
1498+
1499+
for point in &state.selected_points {
1500+
if let ManipulatorPointId::Anchor(anchor) = point {
1501+
if vector_data.all_connected(*anchor).all(|segment| state.is_segment_selected(segment.segment)) {
1502+
let modification_type = VectorModificationType::RemovePoint { id: *anchor };
1503+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1504+
}
1505+
}
1506+
}
1507+
}
1508+
}
1509+
14861510
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
14871511
for (&layer, state) in &self.selected_shape_state {
14881512
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
@@ -1600,7 +1624,7 @@ impl ShapeState {
16001624
mouse_position: DVec2,
16011625
select_threshold: f64,
16021626
path_overlay_mode: PathOverlayMode,
1603-
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
1627+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
16041628
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
16051629
if self.selected_shape_state.is_empty() {
16061630
return None;
@@ -1968,20 +1992,91 @@ impl ShapeState {
19681992
selection_shape: SelectionShape,
19691993
selection_change: SelectionChange,
19701994
path_overlay_mode: PathOverlayMode,
1971-
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
1995+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
19721996
select_segments: bool,
1997+
select_points: bool,
19731998
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
19741999
selection_mode: SelectionMode,
19752000
) {
2001+
let (points_inside, segments_inside) = self.get_inside_points_and_segments(
2002+
network_interface,
2003+
selection_shape,
2004+
path_overlay_mode,
2005+
frontier_handles_info,
2006+
select_segments,
2007+
select_points,
2008+
selection_mode,
2009+
);
2010+
2011+
if selection_change == SelectionChange::Clear {
2012+
self.deselect_all_points();
2013+
self.deselect_all_segments();
2014+
}
2015+
2016+
for (layer, points) in points_inside {
2017+
let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue };
2018+
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
2019+
2020+
for point in points {
2021+
match (point, selection_change) {
2022+
(_, SelectionChange::Shrink) => state.deselect_point(point),
2023+
(ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_), _) => {
2024+
let handle = point.as_handle().expect("Handle cannot be converted");
2025+
if handle.length(&vector_data) > 0. {
2026+
state.select_point(point);
2027+
}
2028+
}
2029+
(_, _) => state.select_point(point),
2030+
}
2031+
}
2032+
}
2033+
2034+
for (layer, segments) in segments_inside {
2035+
let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue };
2036+
match selection_change {
2037+
SelectionChange::Shrink => segments.iter().for_each(|segment| state.deselect_segment(*segment)),
2038+
_ => segments.iter().for_each(|segment| state.select_segment(*segment)),
2039+
}
2040+
2041+
// Also select/deselect the endpoints of respective segments
2042+
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
2043+
if !select_points && select_segments {
2044+
vector_data
2045+
.segment_bezier_iter()
2046+
.filter(|(segment, _, _, _)| segments.contains(segment))
2047+
.for_each(|(_, _, start, end)| match selection_change {
2048+
SelectionChange::Shrink => {
2049+
state.deselect_point(ManipulatorPointId::Anchor(start));
2050+
state.deselect_point(ManipulatorPointId::Anchor(end));
2051+
}
2052+
_ => {
2053+
state.select_point(ManipulatorPointId::Anchor(start));
2054+
state.select_point(ManipulatorPointId::Anchor(end));
2055+
}
2056+
});
2057+
}
2058+
}
2059+
}
2060+
2061+
#[allow(clippy::too_many_arguments)]
2062+
pub fn get_inside_points_and_segments(
2063+
&mut self,
2064+
network_interface: &NodeNetworkInterface,
2065+
selection_shape: SelectionShape,
2066+
path_overlay_mode: PathOverlayMode,
2067+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
2068+
select_segments: bool,
2069+
select_points: bool,
2070+
// Represents if the box/lasso selection touches or encloses the targets (not to be confused with editing modes).
2071+
selection_mode: SelectionMode,
2072+
) -> (HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>>, HashMap<LayerNodeIdentifier, HashSet<SegmentId>>) {
19762073
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
19772074
let selected_segments = selected_segments(network_interface, self);
19782075

1979-
for (&layer, state) in &mut self.selected_shape_state {
1980-
if selection_change == SelectionChange::Clear {
1981-
state.clear_points();
1982-
state.clear_segments();
1983-
}
2076+
let mut points_inside: HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>> = HashMap::new();
2077+
let mut segments_inside: HashMap<LayerNodeIdentifier, HashSet<SegmentId>> = HashMap::new();
19842078

2079+
for &layer in self.selected_shape_state.keys() {
19852080
let vector_data = network_interface.compute_modified_vector(layer);
19862081
let Some(vector_data) = vector_data else { continue };
19872082
let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
@@ -1997,7 +2092,7 @@ impl ShapeState {
19972092

19982093
let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
19992094
if polygon.len() < 2 {
2000-
return;
2095+
return (points_inside, segments_inside);
20012096
}
20022097
let polygon: Subpath<PointId> = Subpath::from_anchors_linear(polygon.to_vec(), true);
20032098
Some(polygon)
@@ -2037,10 +2132,7 @@ impl ShapeState {
20372132
};
20382133

20392134
if select {
2040-
match selection_change {
2041-
SelectionChange::Shrink => state.deselect_segment(id),
2042-
_ => state.select_segment(id),
2043-
}
2135+
segments_inside.entry(layer).or_default().insert(id);
20442136
}
20452137
}
20462138

@@ -2057,21 +2149,11 @@ impl ShapeState {
20572149
.contains_point(transformed_position),
20582150
};
20592151

2060-
if select {
2061-
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points);
2152+
if select && select_points {
2153+
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments.clone(), &selected_points);
20622154

20632155
if is_visible_handle {
2064-
match selection_change {
2065-
SelectionChange::Shrink => state.deselect_point(id),
2066-
_ => {
2067-
// Select only the handles which are of nonzero length
2068-
if let Some(handle) = id.as_handle() {
2069-
if handle.length(&vector_data) > 0. {
2070-
state.select_point(id)
2071-
}
2072-
}
2073-
}
2074-
}
2156+
points_inside.entry(layer).or_default().insert(id);
20752157
}
20762158
}
20772159
}
@@ -2089,13 +2171,12 @@ impl ShapeState {
20892171
.contains_point(transformed_position),
20902172
};
20912173

2092-
if select {
2093-
match selection_change {
2094-
SelectionChange::Shrink => state.deselect_point(ManipulatorPointId::Anchor(id)),
2095-
_ => state.select_point(ManipulatorPointId::Anchor(id)),
2096-
}
2174+
if select && select_points {
2175+
points_inside.entry(layer).or_default().insert(ManipulatorPointId::Anchor(id));
20972176
}
20982177
}
20992178
}
2179+
2180+
(points_inside, segments_inside)
21002181
}
21012182
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ pub fn is_visible_point(
175175
manipulator_point_id: ManipulatorPointId,
176176
vector_data: &VectorData,
177177
path_overlay_mode: PathOverlayMode,
178-
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
178+
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
179179
selected_segments: Vec<SegmentId>,
180180
selected_points: &HashSet<ManipulatorPointId>,
181181
) -> bool {
@@ -201,7 +201,7 @@ pub fn is_visible_point(
201201
warn!("No anchor for selected handle");
202202
return false;
203203
};
204-
let Some(frontier_handles) = &frontier_handles_info else {
204+
let Some(frontier_handles) = frontier_handles_info else {
205205
warn!("No frontier handles info provided");
206206
return false;
207207
};

0 commit comments

Comments
 (0)