Add Pen and Path tool modes to avoid showing all handles (#2264)

* Path tool only show frontier overlays

* Implemented all modes for pen and path tool

* Fixed formatting issue

* Changes in selection behaviour of handles

* Selection toggle only on drag not click

* Changed comment

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta
2025-02-12 01:30:15 -08:00
committed by GitHub
co-authored by Keavon Chambers
parent 1700c3a650
commit 20a5f71bda
5 changed files with 347 additions and 47 deletions
@@ -3,8 +3,8 @@ use super::tool_prelude::*;
use crate::consts::{
COLOR_OVERLAY_BLUE, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
};
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::preferences::SelectionMode;
@@ -15,8 +15,8 @@ use crate::messages::tool::common_functionality::shape_editor::{
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use graphene_core::renderer::Quad;
use graphene_core::vector::ManipulatorPointId;
use graphene_std::vector::NoHashBuilder;
use graphene_core::vector::{ManipulatorPointId, PointId};
use graphene_std::vector::{NoHashBuilder, SegmentId};
use std::vec;
@@ -24,6 +24,12 @@ use std::vec;
pub struct PathTool {
fsm_state: PathToolFsmState,
tool_data: PathToolData,
options: PathToolOptions,
}
#[derive(Default)]
pub struct PathToolOptions {
path_overlay_mode: PathOverlayMode,
}
#[impl_message(Message, ToolMessage, Path)]
@@ -89,6 +95,20 @@ pub enum PathToolMessage {
new_y: f64,
},
SwapSelectedHandles,
UpdateOptions(PathOptionsUpdate),
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PathOverlayMode {
AllHandles = 0,
#[default]
SelectedPointHandles = 1,
FrontierHandles = 2,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PathOptionsUpdate {
OverlayModeType(PathOverlayMode),
}
impl ToolMetadata for PathTool {
@@ -170,15 +190,31 @@ impl LayoutHolder for PathTool {
.tooltip(colinear_handles_tooltip)
.widget_holder();
let path_overlay_mode_widget = RadioInput::new(vec![
RadioEntryData::new("1")
.label("1")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()),
RadioEntryData::new("2")
.label("2")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()),
RadioEntryData::new("3")
.label("3")
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()),
])
.selected_index(Some(self.options.path_overlay_mode as u32))
.widget_holder();
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![
x_location,
related_seperator.clone(),
y_location,
unrelated_seperator,
unrelated_seperator.clone(),
colinear_handle_checkbox,
related_seperator,
colinear_handles_label,
unrelated_seperator,
path_overlay_mode_widget,
],
}]))
}
@@ -189,6 +225,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
match message {
ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action {
PathOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.path_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw);
}
},
ToolMessage::Path(PathToolMessage::ClosePath) => {
responses.add(DocumentMessage::AddTransaction);
tool_data.shape_editor.close_selected_path(tool_data.document, responses);
@@ -204,7 +246,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
}
}
_ => {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
}
}
@@ -321,6 +363,8 @@ struct PathToolData {
auto_panning: AutoPanning,
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
select_anchor_toggled: bool,
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
handle_drag_toggle: bool,
dragging_state: DraggingState,
current_selected_handle_id: Option<ManipulatorPointId>,
angle: f64,
@@ -430,12 +474,27 @@ impl PathToolData {
self.drag_start_pos = input.mouse.position;
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
// 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) {
responses.add(DocumentMessage::StartTransaction);
if let Some(selected_points) = selected_points {
self.drag_start_pos = input.mouse.position;
// If selected points contain only handles and there was some selection before, then it is stored and becomes restored upon release
let mut dragging_only_handles = true;
for point in &selected_points.points {
if matches!(point.point_id, ManipulatorPointId::Anchor(_)) {
dragging_only_handles = false;
break;
}
}
if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() {
self.saved_points_before_handle_drag = old_selection;
}
self.start_dragging_point(selected_points, input, document, shape_editor);
responses.add(OverlaysMessage::Draw);
}
@@ -687,9 +746,9 @@ impl PathToolData {
impl Fsm for PathToolFsmState {
type ToolData = PathToolData;
type ToolOptions = ();
type ToolOptions = PathToolOptions;
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
let ToolMessage::Path(event) = event else { return self };
match (self, event) {
@@ -704,7 +763,51 @@ impl Fsm for PathToolFsmState {
self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context);
// TODO: find the segment ids of which the selected points are a part of
match tool_options.path_overlay_mode {
PathOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
}
PathOverlayMode::SelectedPointHandles => {
let selected_segments = selected_segments(document, shape_editor);
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context);
}
PathOverlayMode::FrontierHandles => {
let selected_segments = selected_segments(document, shape_editor);
// Match the behavior of `PathOverlayMode::SelectedPointHandles` when only one point is selected
if shape_editor.selected_points().count() == 1 {
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context);
} else {
let mut segment_endpoints: HashMap<SegmentId, Vec<PointId>> = HashMap::new();
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
// The points which are part of only one segment will be rendered
let mut selected_segments_by_point: HashMap<PointId, Vec<SegmentId>> = HashMap::new();
for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
selected_segments_by_point.entry(start).or_default().push(segment_id);
selected_segments_by_point.entry(end).or_default().push(segment_id);
}
}
for (point, attached_segments) in selected_segments_by_point {
if attached_segments.len() == 1 {
segment_endpoints.entry(attached_segments[0]).or_default().push(point);
}
}
}
// Now frontier anchors can be sent for rendering overlays
path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context);
}
}
}
match self {
Self::Drawing { selection_shape } => {
@@ -839,6 +942,21 @@ impl Fsm for PathToolFsmState {
lock_angle,
},
) => {
let mut selected_only_handles = true;
let selected_points = shape_editor.selected_points();
for point in selected_points {
if matches!(point, ManipulatorPointId::Anchor(_)) {
selected_only_handles = false;
break;
}
}
if !tool_data.saved_points_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) {
tool_data.handle_drag_toggle = true;
}
if tool_data.selection_status.is_none() {
if let Some(layer) = document.click(input) {
shape_editor.select_all_anchors_in_layer(document, layer);
@@ -1027,6 +1145,14 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready
}
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
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;
}
if tool_data.select_anchor_toggled {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
@@ -2,7 +2,7 @@ use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
@@ -27,6 +27,7 @@ pub struct PenOptions {
line_weight: f64,
fill: ToolColorOptions,
stroke: ToolColorOptions,
pen_overlay_mode: PenOverlayMode,
}
impl Default for PenOptions {
@@ -35,6 +36,7 @@ impl Default for PenOptions {
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
pen_overlay_mode: PenOverlayMode::FrontierHandles,
}
}
}
@@ -75,6 +77,12 @@ enum PenToolFsmState {
GRSHandle,
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PenOverlayMode {
AllHandles = 0,
FrontierHandles = 1,
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PenOptionsUpdate {
FillColor(Option<Color>),
@@ -83,6 +91,7 @@ pub enum PenOptionsUpdate {
StrokeColor(Option<Color>),
StrokeColorType(ToolColorType),
WorkingColors(Option<Color>, Option<Color>),
OverlayModeType(PenOverlayMode),
}
impl ToolMetadata for PenTool {
@@ -126,9 +135,26 @@ impl LayoutHolder for PenTool {
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(
RadioInput::new(vec![
RadioEntryData::new("1")
.label("1")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()),
RadioEntryData::new("2")
.label("2")
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()),
])
.selected_index(Some(self.options.pen_overlay_mode as u32))
.widget_holder(),
);
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
@@ -140,6 +166,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
return;
};
match action {
PenOptionsUpdate::OverlayModeType(overlay_mode_type) => {
self.options.pen_overlay_mode = overlay_mode_type;
responses.add(OverlaysMessage::Draw);
}
PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
PenOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color;
@@ -795,7 +825,7 @@ impl Fsm for PenToolFsmState {
self
}
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context);
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
@@ -839,7 +869,19 @@ impl Fsm for PenToolFsmState {
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
}
path_overlays(document, shape_editor, &mut overlay_context);
match tool_options.pen_overlay_mode {
PenOverlayMode::AllHandles => {
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
}
PenOverlayMode::FrontierHandles => {
// Find the last segment ID to have its handles drawn
if let Some(latest_segment) = tool_data.latest_point().and_then(|point| point.in_segment) {
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context);
} else {
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
};
}
}
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) {
// Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out)
@@ -852,7 +894,7 @@ impl Fsm for PenToolFsmState {
}
} else {
// Draw the whole path and its manipulators when the user is clicking-and-dragging out from the most recently placed anchor to set its outgoing handle, during which it would otherwise not have its overlays drawn
path_overlays(document, shape_editor, &mut overlay_context);
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
}
if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) {