Skip to content

Improve path editing mode #2860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,13 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
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 }),
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 }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
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 }),
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 }),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct CheckboxInput {

pub disabled: bool,

pub frozen: bool,

pub icon: String,

pub tooltip: String,
Expand All @@ -41,6 +43,7 @@ impl Default for CheckboxInput {
Self {
checked: false,
disabled: false,
frozen: false,
icon: "Checkmark".into(),
tooltip: Default::default(),
tooltip_shortcut: Default::default(),
Expand Down
36 changes: 36 additions & 0 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,42 @@ impl OverlayContext {
self.square(position, None, Some(color_fill), Some(color_stroke));
}

pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
self.start_dpi_aware_transform();

let position = position.round() - DVec2::splat(0.5);

self.render_context.begin_path();
self.render_context
.arc(position.x, position.y, (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2., 0., TAU)
.expect("Failed to draw the circle");

self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.fill();
self.render_context.stroke();

self.render_context.begin_path();
self.render_context
.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU)
.expect("Failed to draw the circle");

let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };

self.render_context.set_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.fill();
self.render_context.stroke();

self.end_dpi_aware_transform();
}

pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
}

/// Transforms the canvas context to adjust for DPI scaling
///
/// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`].
Expand Down
135 changes: 101 additions & 34 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@ impl ClosestSegment {
(midpoint, segment_ids)
}

pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool, point_mode: bool) {
let (id, _) = self.adjusted_insert(responses);
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
if point_mode {
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
}
}

pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 {
Expand Down Expand Up @@ -550,7 +552,7 @@ impl ShapeState {
select_threshold: f64,
extend_selection: bool,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
) -> Option<Option<SelectedPointsInfo>> {
if self.selected_shape_state.is_empty() {
return None;
Expand Down Expand Up @@ -599,7 +601,7 @@ impl ShapeState {
mouse_position: DVec2,
select_threshold: f64,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
point_editing_mode: bool,
) -> Option<(bool, Option<SelectedPointsInfo>)> {
if self.selected_shape_state.is_empty() {
Expand Down Expand Up @@ -1544,7 +1546,7 @@ impl ShapeState {
mouse_position: DVec2,
select_threshold: f64,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
if self.selected_shape_state.is_empty() {
return None;
Expand Down Expand Up @@ -1881,20 +1883,99 @@ impl ShapeState {
selection_shape: SelectionShape,
selection_change: SelectionChange,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
select_segments: bool,
select_points: bool,
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
selection_mode: SelectionMode,
) {
let (points_inside, segments_inside) = self.get_inside_points_and_segments(
network_interface,
selection_shape,
path_overlay_mode,
frontier_handles_info,
select_segments,
select_points,
selection_mode,
);

if selection_change == SelectionChange::Clear {
self.deselect_all_points();
self.deselect_all_segments();
}

for (layer, points) in points_inside {
let Some(state) = self.selected_shape_state.get_mut(&layer) else {
continue;
};
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
continue;
};

for point in points {
match (point, selection_change) {
(_, SelectionChange::Shrink) => state.deselect_point(point),
(ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_), _) => {
let handle = point.as_handle().expect("Handle cannot be converted");
if handle.length(&vector_data) > 0. {
state.select_point(point);
}
}
(_, _) => state.select_point(point),
}
}
}

for (layer, segments) in segments_inside {
let Some(state) = self.selected_shape_state.get_mut(&layer) else {
continue;
};
match selection_change {
SelectionChange::Shrink => segments.iter().for_each(|segment| state.deselect_segment(*segment)),
_ => segments.iter().for_each(|segment| state.select_segment(*segment)),
}

// Also select/ deselect the endpoints of respective segments
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
continue;
};
if !select_points && select_segments {
vector_data
.segment_bezier_iter()
.filter(|(segment, _, _, _)| segments.contains(segment))
.for_each(|(_, _, start, end)| match selection_change {
SelectionChange::Shrink => {
state.deselect_point(ManipulatorPointId::Anchor(start));
state.deselect_point(ManipulatorPointId::Anchor(end));
}
_ => {
state.select_point(ManipulatorPointId::Anchor(start));
state.select_point(ManipulatorPointId::Anchor(end));
}
});
}
}
}

#[allow(clippy::too_many_arguments)]
pub fn get_inside_points_and_segments(
&mut self,
network_interface: &NodeNetworkInterface,
selection_shape: SelectionShape,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
select_segments: bool,
select_points: bool,
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
selection_mode: SelectionMode,
) -> (HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>>, HashMap<LayerNodeIdentifier, HashSet<SegmentId>>) {
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
let selected_segments = selected_segments(network_interface, self);

for (&layer, state) in &mut self.selected_shape_state {
if selection_change == SelectionChange::Clear {
state.clear_points();
state.clear_segments();
}
let mut points_inside: HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>> = HashMap::new();
let mut segments_inside: HashMap<LayerNodeIdentifier, HashSet<SegmentId>> = HashMap::new();

for (&layer, _) in &mut self.selected_shape_state {
let vector_data = network_interface.compute_modified_vector(layer);
let Some(vector_data) = vector_data else { continue };
let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
Expand All @@ -1910,7 +1991,7 @@ impl ShapeState {

let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
if polygon.len() < 2 {
return;
return (points_inside, segments_inside);
}
let polygon: Subpath<PointId> = Subpath::from_anchors_linear(polygon.to_vec(), true);
Some(polygon)
Expand Down Expand Up @@ -1950,10 +2031,7 @@ impl ShapeState {
};

if select {
match selection_change {
SelectionChange::Shrink => state.deselect_segment(id),
_ => state.select_segment(id),
}
segments_inside.entry(layer).or_default().insert(id);
}
}

Expand All @@ -1970,21 +2048,11 @@ impl ShapeState {
.contains_point(transformed_position),
};

if select {
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points);
if select && select_points {
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments.clone(), &selected_points);

if is_visible_handle {
match selection_change {
SelectionChange::Shrink => state.deselect_point(id),
_ => {
// Select only the handles which are of nonzero length
if let Some(handle) = id.as_handle() {
if handle.length(&vector_data) > 0. {
state.select_point(id)
}
}
}
}
points_inside.entry(layer).or_default().insert(id);
}
}
}
Expand All @@ -2002,13 +2070,12 @@ impl ShapeState {
.contains_point(transformed_position),
};

if select {
match selection_change {
SelectionChange::Shrink => state.deselect_point(ManipulatorPointId::Anchor(id)),
_ => state.select_point(ManipulatorPointId::Anchor(id)),
}
if select && select_points {
points_inside.entry(layer).or_default().insert(ManipulatorPointId::Anchor(id));
}
}
}

(points_inside, segments_inside)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub fn is_visible_point(
manipulator_point_id: ManipulatorPointId,
vector_data: &VectorData,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
selected_segments: Vec<SegmentId>,
selected_points: &HashSet<ManipulatorPointId>,
) -> bool {
Expand All @@ -193,7 +193,7 @@ pub fn is_visible_point(
warn!("No anchor for selected handle");
return false;
};
let Some(frontier_handles) = &frontier_handles_info else {
let Some(frontier_handles) = frontier_handles_info else {
warn!("No frontier handles info provided");
return false;
};
Expand Down
Loading
Loading