mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 18:28:11 +08:00
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:
co-authored by
Keavon Chambers
parent
1700c3a650
commit
20a5f71bda
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user