mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 21:18:12 +08:00
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 <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
523132da17
commit
668acd3c30
@@ -302,9 +302,16 @@ impl ClosestSegment {
|
||||
(midpoint, segment_ids)
|
||||
}
|
||||
|
||||
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
|
||||
let (id, _) = self.adjusted_insert(responses);
|
||||
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
|
||||
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) {
|
||||
let (id, segments) = self.adjusted_insert(responses);
|
||||
if point_mode || is_segment_selected {
|
||||
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection);
|
||||
}
|
||||
|
||||
if is_segment_selected {
|
||||
let Some(state) = shape_editor.selected_shape_state.get_mut(&self.layer) else { return };
|
||||
segments.iter().for_each(|segment| state.select_segment(*segment));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 {
|
||||
@@ -551,7 +558,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;
|
||||
@@ -600,18 +607,18 @@ 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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !point_editing_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
|
||||
// If not point editing mode then only handles are allowed to be dragged
|
||||
if !point_editing_mode && matches!(manipulator_point_id, ManipulatorPointId::Anchor(_)) {
|
||||
return None;
|
||||
}
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
@@ -1483,6 +1490,23 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for point in &state.selected_points {
|
||||
if let ManipulatorPointId::Anchor(anchor) = point {
|
||||
if vector_data.all_connected(*anchor).all(|segment| state.is_segment_selected(segment.segment)) {
|
||||
let modification_type = VectorModificationType::RemovePoint { id: *anchor };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
@@ -1600,7 +1624,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;
|
||||
@@ -1968,20 +1992,91 @@ 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,
|
||||
// Represents if the box/lasso selection touches or encloses the targets (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 self.selected_shape_state.keys() {
|
||||
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);
|
||||
@@ -1997,7 +2092,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)
|
||||
@@ -2037,10 +2132,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2057,21 +2149,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2089,13 +2171,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,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 {
|
||||
@@ -201,7 +201,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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user