Skip to content

Commit 75614eb

Browse files
4adexKeavon
andauthored
Fix vector mesh editing behavior in various edge cases (#2943)
* Fix colinear switch behaviour * Fix Tab to switch handles * Code review --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 3a8c1b6 commit 75614eb

File tree

3 files changed

+140
-24
lines changed

3 files changed

+140
-24
lines changed

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

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
1515
use glam::{DAffine2, DVec2};
1616
use graphene_std::vector::{HandleExt, HandleId, SegmentId};
1717
use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
18+
use std::f64::consts::TAU;
1819

1920
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2021
pub enum SelectionChange {
@@ -892,16 +893,20 @@ impl ShapeState {
892893
ManipulatorPointId::Anchor(point) => self.move_anchor(point, &vector_data, delta, layer, None, responses),
893894
ManipulatorPointId::PrimaryHandle(segment) => {
894895
self.move_primary(segment, delta, layer, responses);
895-
if let Some(handles) = point.get_handle_pair(&vector_data) {
896-
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
897-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
896+
if let Some(handle) = point.as_handle() {
897+
if let Some(handles) = vector_data.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) {
898+
let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false };
899+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
900+
}
898901
}
899902
}
900903
ManipulatorPointId::EndHandle(segment) => {
901904
self.move_end(segment, delta, layer, responses);
902-
if let Some(handles) = point.get_handle_pair(&vector_data) {
903-
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
904-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
905+
if let Some(handle) = point.as_handle() {
906+
if let Some(handles) = vector_data.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) {
907+
let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false };
908+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
909+
}
905910
}
906911
}
907912
}
@@ -1027,6 +1032,9 @@ impl ShapeState {
10271032
/// If only one handle is selected, the other handle will be moved to match the angle of the selected handle.
10281033
/// If both or neither handles are selected, the angle of both handles will be averaged from their current angles, weighted by their lengths.
10291034
/// Assumes all selected manipulators have handles that are already not colinear.
1035+
///
1036+
/// For vector meshes, the non-colinear handle which is nearest in the direction of 180° angle separation becomes colinear with current handle.
1037+
/// If there is no such handle, nothing happens.
10301038
pub fn convert_selected_manipulators_to_colinear_handles(&self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler) {
10311039
let mut skip_set = HashSet::new();
10321040

@@ -1037,7 +1045,55 @@ impl ShapeState {
10371045
let transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
10381046

10391047
for &point in layer_state.selected_points.iter() {
1040-
let Some(handles) = point.get_handle_pair(&vector_data) else { continue };
1048+
// Skip a point which has more than 2 segments connected (vector meshes)
1049+
if let ManipulatorPointId::Anchor(anchor) = point {
1050+
if vector_data.all_connected(anchor).count() > 2 {
1051+
continue;
1052+
}
1053+
}
1054+
1055+
// Here we take handles as the current handle and the most opposite non-colinear-handle
1056+
1057+
let is_handle_colinear = |handle: HandleId| -> bool { vector_data.colinear_manipulators.iter().any(|&handles| handles[0] == handle || handles[1] == handle) };
1058+
1059+
let other_handles = if matches!(point, ManipulatorPointId::Anchor(_)) {
1060+
point.get_handle_pair(&vector_data)
1061+
} else {
1062+
point.get_all_connected_handles(&vector_data).and_then(|handles| {
1063+
let mut non_colinear_handles = handles.iter().filter(|&handle| !is_handle_colinear(*handle)).clone().collect::<Vec<_>>();
1064+
1065+
// Sort these by angle from the current handle
1066+
non_colinear_handles.sort_by(|&handle_a, &handle_b| {
1067+
let anchor = point.get_anchor_position(&vector_data).expect("No anchor position for handle");
1068+
let orig_handle_pos = point.get_position(&vector_data).expect("No handle position");
1069+
1070+
let a_pos = handle_a.to_manipulator_point().get_position(&vector_data).expect("No handle position");
1071+
let b_pos = handle_b.to_manipulator_point().get_position(&vector_data).expect("No handle position");
1072+
1073+
let v_orig = (orig_handle_pos - anchor).normalize_or_zero();
1074+
1075+
let v_a = (a_pos - anchor).normalize_or_zero();
1076+
let v_b = (b_pos - anchor).normalize_or_zero();
1077+
1078+
let angle_a = v_orig.angle_to(v_a).abs();
1079+
let angle_b = v_orig.angle_to(v_b).abs();
1080+
1081+
// Sort by descending angle (180° is furthest)
1082+
angle_b.partial_cmp(&angle_a).unwrap_or(std::cmp::Ordering::Equal)
1083+
});
1084+
1085+
let current = match point {
1086+
ManipulatorPointId::EndHandle(segment) => HandleId::end(segment),
1087+
ManipulatorPointId::PrimaryHandle(segment) => HandleId::primary(segment),
1088+
ManipulatorPointId::Anchor(_) => unreachable!(),
1089+
};
1090+
1091+
non_colinear_handles.first().map(|other| [current, **other])
1092+
})
1093+
};
1094+
1095+
let Some(handles) = other_handles else { continue };
1096+
10411097
if skip_set.contains(&handles) || skip_set.contains(&[handles[1], handles[0]]) {
10421098
continue;
10431099
};
@@ -1317,7 +1373,7 @@ impl ShapeState {
13171373
match point {
13181374
ManipulatorPointId::Anchor(anchor) => {
13191375
if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) {
1320-
if !vector_data.all_connected(anchor).any(|a| selected_segments.contains(&a.segment)) {
1376+
if !vector_data.all_connected(anchor).any(|a| selected_segments.contains(&a.segment)) && vector_data.all_connected(anchor).count() <= 2 {
13211377
missing_anchors.insert(anchor, handles);
13221378
}
13231379
}
@@ -1496,21 +1552,21 @@ impl ShapeState {
14961552
/// Disable colinear handles colinear.
14971553
pub fn disable_colinear_handles_state_on_selected(&self, network_interface: &NodeNetworkInterface, responses: &mut VecDeque<Message>) {
14981554
for (&layer, state) in &self.selected_shape_state {
1499-
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
1500-
continue;
1501-
};
1555+
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
15021556

15031557
for &point in &state.selected_points {
15041558
if let ManipulatorPointId::Anchor(point) = point {
15051559
for connected in vector_data.all_connected(point) {
1506-
if let Some(&handles) = vector_data.colinear_manipulators.iter().find(|target| target.iter().any(|&target| target == connected)) {
1560+
if let Some(&handles) = vector_data.colinear_manipulators.iter().find(|target| target.contains(&connected)) {
15071561
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
15081562
responses.add(GraphOperationMessage::Vector { layer, modification_type });
15091563
}
15101564
}
1511-
} else if let Some(handles) = point.get_handle_pair(&vector_data) {
1512-
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
1513-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1565+
} else if let Some(handle) = point.as_handle() {
1566+
if let Some(handles) = vector_data.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) {
1567+
let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false };
1568+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
1569+
}
15141570
}
15151571
}
15161572
}
@@ -1720,9 +1776,40 @@ impl ShapeState {
17201776
if point.as_anchor().is_some() {
17211777
continue;
17221778
}
1723-
if let Some(handles) = point.get_handle_pair(&vector_data) {
1724-
// handle[0] is selected, handle[1] is opposite / mirror handle
1725-
handles_to_update.push((layer, handles[0].to_manipulator_point(), handles[1].to_manipulator_point()));
1779+
1780+
if let Some(other_handles) = point.get_all_connected_handles(&vector_data) {
1781+
// Find the next closest handle in the clockwise sense
1782+
let mut candidates = other_handles.clone();
1783+
candidates.sort_by(|&handle_a, &handle_b| {
1784+
let anchor = point.get_anchor_position(&vector_data).expect("No anchor position for handle");
1785+
let orig_handle_pos = point.get_position(&vector_data).expect("No handle position");
1786+
1787+
let a_pos = handle_a.to_manipulator_point().get_position(&vector_data).expect("No handle position");
1788+
let b_pos = handle_b.to_manipulator_point().get_position(&vector_data).expect("No handle position");
1789+
1790+
let v_orig = (orig_handle_pos - anchor).normalize_or_zero();
1791+
1792+
let v_a = (a_pos - anchor).normalize_or_zero();
1793+
let v_b = (b_pos - anchor).normalize_or_zero();
1794+
1795+
let signed_angle = |base: DVec2, to: DVec2| -> f64 {
1796+
let angle = base.angle_to(to);
1797+
let cross = base.perp_dot(to);
1798+
1799+
if cross < 0. { TAU - angle } else { angle }
1800+
};
1801+
1802+
let angle_a = signed_angle(v_orig, v_a);
1803+
let angle_b = signed_angle(v_orig, v_b);
1804+
1805+
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
1806+
});
1807+
1808+
if candidates.is_empty() {
1809+
continue;
1810+
}
1811+
1812+
handles_to_update.push((layer, *point, candidates[0].to_manipulator_point()));
17261813
}
17271814
}
17281815
}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,12 @@ impl PathToolData {
583583
SelectionStatus::None => false,
584584
SelectionStatus::One(single_selected_point) => {
585585
let vector_data = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap();
586-
single_selected_point.id.get_handle_pair(&vector_data).is_some()
586+
if single_selected_point.id.get_handle_pair(&vector_data).is_some() {
587+
let anchor = single_selected_point.id.get_anchor(&vector_data).expect("Cannot find connected anchor");
588+
vector_data.all_connected(anchor).count() <= 2
589+
} else {
590+
false
591+
}
587592
}
588593
SelectionStatus::Multiple(_) => true,
589594
};
@@ -600,7 +605,7 @@ impl PathToolData {
600605
}
601606

602607
fn next_drill_through_cycle(&mut self, position: DVec2) -> usize {
603-
if self.last_drill_through_click_position.map_or(true, |last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD) {
608+
if self.last_drill_through_click_position.is_none_or(|last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD) {
604609
// New position, reset cycle
605610
self.drill_through_cycle_index = 0;
606611
} else {
@@ -620,7 +625,7 @@ impl PathToolData {
620625
}
621626

622627
fn has_drill_through_mouse_moved(&self, position: DVec2) -> bool {
623-
self.last_drill_through_click_position.map_or(true, |last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD)
628+
self.last_drill_through_click_position.is_none_or(|last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD)
624629
}
625630

626631
fn set_ghost_outline(&mut self, shape_editor: &ShapeState, document: &DocumentMessageHandler) {
@@ -2406,15 +2411,15 @@ impl Fsm for PathToolFsmState {
24062411

24072412
let compatible_type = first_layer.and_then(|layer| {
24082413
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
2409-
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
2414+
graph_layer.horizontal_layer_flow().nth(1).map(|node_id| {
24102415
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
2411-
Some(format!("type:{}", output_type.nested_type()))
2416+
format!("type:{}", output_type.nested_type())
24122417
})
24132418
});
24142419

24152420
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
24162421

2417-
let is_modifiable = first_layer.map_or(false, |layer| {
2422+
let is_modifiable = first_layer.is_some_and(|layer| {
24182423
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
24192424
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
24202425
});

node-graph/gcore/src/vector/vector_data.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,30 @@ impl ManipulatorPointId {
561561
}
562562
}
563563

564+
/// Finds all the connected handles of a point.
565+
/// For an anchor it is all the connected handles.
566+
/// For a handle it is all the handles connected to its corresponding anchor other than the current handle.
567+
pub fn get_all_connected_handles(self, vector_data: &VectorData) -> Option<Vec<HandleId>> {
568+
match self {
569+
ManipulatorPointId::Anchor(point) => {
570+
let connected = vector_data.all_connected(point).collect::<Vec<_>>();
571+
Some(connected)
572+
}
573+
ManipulatorPointId::PrimaryHandle(segment) => {
574+
let point = vector_data.segment_domain.segment_start_from_id(segment)?;
575+
let current = HandleId::primary(segment);
576+
let connected = vector_data.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
577+
Some(connected)
578+
}
579+
ManipulatorPointId::EndHandle(segment) => {
580+
let point = vector_data.segment_domain.segment_end_from_id(segment)?;
581+
let current = HandleId::end(segment);
582+
let connected = vector_data.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
583+
Some(connected)
584+
}
585+
}
586+
}
587+
564588
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
565589
#[must_use]
566590
pub fn get_anchor(self, vector_data: &VectorData) -> Option<PointId> {

0 commit comments

Comments
 (0)