Skip to content

Improve Pen tool behavior when double-clicking anchor points #2498

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
85 changes: 48 additions & 37 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,11 +489,16 @@ impl ShapeState {
!self.selected_shape_state.is_empty()
}

/// Provide the currently selected points by reference.
/// Provide the currently selected points by reference.(TODO: Use unique ids)
pub fn selected_points(&self) -> impl Iterator<Item = &'_ ManipulatorPointId> {
self.selected_shape_state.values().flat_map(|state| &state.selected_points)
}

// Alternative for above
pub fn selected_points_by_layer(&self) -> HashMap<LayerNodeIdentifier, impl Iterator<Item = &ManipulatorPointId> + '_> {
self.selected_shape_state.iter().map(|(&k, v)| (k, v.selected_points.iter())).collect()
}

pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> {
self.selected_shape_state.get(&layer).map(|state| &state.selected_points)
}
Expand Down Expand Up @@ -1318,51 +1323,69 @@ 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.
pub fn flip_smooth_sharp(&self, network_interface: &NodeNetworkInterface, target: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) -> bool {
let mut process_layer = |layer| {
let vector_data = network_interface.compute_modified_vector(layer)?;
let transform_to_screenspace = network_interface.document_metadata().transform_to_viewport(layer);

let mut result = None;
let mut closest_distance_squared = tolerance * tolerance;

// Find the closest anchor point on the current layer
for (&id, &anchor) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
let screenspace = transform_to_screenspace.transform_point2(anchor);
let distance_squared = screenspace.distance_squared(target);

if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
result = Some((id, anchor));
pub fn select_points_by_manipulator_id_by_layer(&mut self, points: &HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>) {
for (layer, point_iter) in points {
if let Some(state) = self.selected_shape_state.get_mut(layer) {
for point in point_iter {
state.select_point(*point);
}
}
}
}

/// 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.
pub fn flip_smooth_sharp(&mut self, network_interface: &NodeNetworkInterface, target: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) {
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, target, tolerance) {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
return;
};
let Some(id) = manipulator_point_id.as_anchor() else {
return;
};
let Some(anchor) = manipulator_point_id.get_position(&vector_data) else {
return;
};

let (id, anchor) = result?;
let handles = vector_data.all_connected(id);
let mut positions = handles
.filter_map(|handle| handle.to_manipulator_point().get_position(&vector_data))
.filter(|&handle| !anchor.abs_diff_eq(handle, 1e-5));

// Check by comparing the handle positions to the anchor if this manipulator group is a point
let already_sharp = positions.next().is_none();
for &layer in self.selected_shape_state.keys() {
self.flip_smooth_sharp_each(network_interface, layer, already_sharp, responses);
}
}
}

fn flip_smooth_sharp_each(&self, network_interface: &NodeNetworkInterface, layer: LayerNodeIdentifier, already_sharp: bool, responses: &mut VecDeque<Message>) {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
return;
};
let Some(selected_state) = self.selected_shape_state.get(&layer) else {
return;
};

let anchors = selected_state.selected_points.iter().filter_map(|point| point.as_anchor());

for id in anchors {
if already_sharp {
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 };
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 };
Expand All @@ -1371,11 +1394,9 @@ impl ShapeState {
}
}
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 };
Expand All @@ -1385,18 +1406,8 @@ impl ShapeState {
}
}
}
};

Some(true)
};

for &layer in self.selected_shape_state.keys() {
if let Some(result) = process_layer(layer) {
return result;
}
}

false
}

pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) {
Expand Down
15 changes: 13 additions & 2 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::messages::portfolio::document::utility_types::transformation::Axis;
use crate::messages::preferences::SelectionMode;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::shape_editor::{
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use graphene_core::renderer::Quad;
Expand Down Expand Up @@ -370,6 +370,8 @@ struct PathToolData {
select_anchor_toggled: bool,
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
handle_drag_toggle: bool,
saved_points_before_flip: HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>,
flip_toggle: bool,
dragging_state: DraggingState,
current_selected_handle_id: Option<ManipulatorPointId>,
angle: f64,
Expand Down Expand Up @@ -496,6 +498,7 @@ impl PathToolData {
self.drag_start_pos = input.mouse.position;

let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
log::info!("old {:?}", old_selection);

// Select the first point within the threshold (in pixels)
if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, extend_selection) {
Expand All @@ -514,6 +517,7 @@ impl PathToolData {
}
if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() {
self.saved_points_before_handle_drag = old_selection;
log::info!("{:?}", self.saved_points_before_handle_drag);
}

self.start_dragging_point(selected_points, input, document, shape_editor);
Expand Down Expand Up @@ -1245,7 +1249,6 @@ impl Fsm for PathToolFsmState {
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);

tool_data.saved_points_before_handle_drag.clear();
tool_data.handle_drag_toggle = false;
}
Expand Down Expand Up @@ -1290,6 +1293,7 @@ impl Fsm for PathToolFsmState {
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
log::info!("after drag {:?}", shape_editor.selected_points().collect::<Vec<_>>());

tool_data.saved_points_before_handle_drag.clear();
tool_data.handle_drag_toggle = false;
Expand All @@ -1310,6 +1314,10 @@ impl Fsm for PathToolFsmState {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected {
if tool_data.flip_toggle {
tool_data.saved_points_before_flip = shape_editor.selected_shape_state.iter().map(|(k, v)| (*k, v.selected().collect::<Vec<_>>())).collect();
tool_data.flip_toggle = false;
}
shape_editor.deselect_all_points();
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
responses.add(OverlaysMessage::Draw);
Expand Down Expand Up @@ -1356,8 +1364,11 @@ impl Fsm for PathToolFsmState {
if nearest_point.is_some() {
// 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 {
tool_data.flip_toggle = true;
responses.add(DocumentMessage::StartTransaction);
shape_editor.select_points_by_manipulator_id_by_layer(&tool_data.saved_points_before_flip);
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
tool_data.saved_points_before_flip.clear();
responses.add(DocumentMessage::EndTransaction);
responses.add(PathToolMessage::SelectedPointUpdated);
}
Expand Down
Loading