From 48fdaddc37b8123348d412ee67288bbaf81607bf Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Tue, 29 Aug 2023 21:41:01 -0700 Subject: [PATCH] Add Path tool options for editing X/Y point coordinates (#1404) * implement path point selector in toolbar * Transform point to art board space * fix handle adjustment space * remove unused branches * tidy comments * make function names more descriptive, add guards, fix comments * add auxillary message for layout update * change trace to warn, remove unneccessary messages, fix rustfmt * rustfmt * support handles * style and dimensions corrections * Code review --------- Co-authored-by: Keavon Chambers --- .../tool/common_functionality/shape_editor.rs | 39 +++++++ .../messages/tool/tool_messages/path_tool.rs | 100 +++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 4eb8342b..ed998d31 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -27,6 +27,9 @@ impl SelectedLayerState { pub fn clear_points(&mut self) { self.selected_points.clear(); } + pub fn selected_points_count(&self) -> usize { + self.selected_points.len() + } } pub type SelectedShapeState = HashMap, SelectedLayerState>; #[derive(Debug, Default)] @@ -148,6 +151,42 @@ impl ShapeState { self.selected_shape_state.values().flat_map(|state| &state.selected_points) } + /// Moves a control point to a `new_position` in document space. + /// Returns `Some(())` if successful and `None` otherwise. + pub fn reposition_control_point(&self, point: &ManipulatorPointId, responses: &mut VecDeque, document: &Document, new_position: DVec2, layer_path: &[u64]) -> Option<()> { + let layer = document.layer(layer_path).ok()?; + let vector_data = layer.as_vector_data()?; + let transform = layer.transform.inverse(); + let position = transform.transform_point2(new_position - layer.pivot); + let group = vector_data.manipulator_from_id(point.group)?; + let delta = position - point.manipulator_type.get_position(group)?; + + if point.manipulator_type.is_handle() { + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorHandleMirroring { id: group.id, mirror_angle: false }, + }); + } + + let mut move_point = |point: ManipulatorPointId| { + let Some(position) = point.manipulator_type.get_position(group) else { + return; + }; + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point, position: (position + delta) }, + }); + }; + + move_point(*point); + if !point.manipulator_type.is_handle() { + move_point(ManipulatorPointId::new(point.group, SelectedType::InHandle)); + move_point(ManipulatorPointId::new(point.group, SelectedType::OutHandle)); + } + + Some(()) + } + /// Move the selected points by dragging the mouse. pub fn move_selected_points(&self, document: &Document, delta: DVec2, mirror_distance: bool, responses: &mut VecDeque) { for (layer_path, state) in &self.selected_shape_state { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index d309ab91..269f6538 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -11,6 +11,7 @@ use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::common_functionality::transformation_cage::{add_bounding_box, transform_from_box}; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, HintData, HintGroup, HintInfo, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; +use document_legacy::document::Document; use document_legacy::intersection::Quad; use document_legacy::{LayerId, Operation}; use graphene_core::vector::{ManipulatorPointId, SelectedType}; @@ -56,6 +57,13 @@ pub enum PathToolMessage { alt_mirror_angle: Key, shift_mirror_distance: Key, }, + SelectedPointUpdated, + SelectedPointXChanged { + new_x: f64, + }, + SelectedPointYChanged { + new_y: f64, + }, } impl ToolMetadata for PathTool { @@ -72,13 +80,51 @@ impl ToolMetadata for PathTool { impl LayoutHolder for PathTool { fn layout(&self) -> Layout { - Layout::WidgetLayout(WidgetLayout::default()) + if let Some(SingleSelectedPoint { coordinates: DVec2 { x, y }, .. }) = self.tool_data.single_selected_point { + let x_location = NumberInput::new(Some(x)) + .unit(" px") + .label("X") + .min_width(120) + .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(move |number_input: &NumberInput| { + let new_x = number_input.value.unwrap_or(x); + PathToolMessage::SelectedPointXChanged { new_x }.into() + }) + .widget_holder(); + + let y_location = NumberInput::new(Some(y)) + .unit(" px") + .label("Y") + .min_width(120) + .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(move |number_input: &NumberInput| { + let new_y = number_input.value.unwrap_or(y); + PathToolMessage::SelectedPointYChanged { new_y }.into() + }) + .widget_holder(); + + let seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { + widgets: vec![x_location, seperator, y_location], + }])) + } else { + Layout::WidgetLayout(WidgetLayout::default()) + } } } impl<'a> MessageHandler> for PathTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { + let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); + self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true); + + if updating_point { + self.send_layout(responses, LayoutTarget::ToolOptions); + } } // Different actions depending on state may be wanted: @@ -137,6 +183,7 @@ struct PathToolData { alt_debounce: bool, opposing_handle_lengths: Option, drag_box_overlay_layer: Option>, + single_selected_point: Option, } impl PathToolData { @@ -182,6 +229,8 @@ impl Fsm for PathToolFsmState { shape_editor.set_selected_layers(layer_paths); tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); + + responses.add(PathToolMessage::SelectedPointUpdated); // This can happen in any state (which is why we return self) self } @@ -192,6 +241,8 @@ impl Fsm for PathToolFsmState { shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer_path.to_vec(), responses); } + responses.add(PathToolMessage::SelectedPointUpdated); + self } // Mouse down @@ -233,6 +284,7 @@ impl Fsm for PathToolFsmState { tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); + responses.add(PathToolMessage::SelectedPointUpdated); PathToolFsmState::Dragging } // We didn't find a point nearby, so consider selecting the nearest shape instead @@ -416,6 +468,23 @@ impl Fsm for PathToolFsmState { shape_editor.move_selected_points(&document.document_legacy, (delta_x, delta_y).into(), true, responses); PathToolFsmState::Ready } + (_, PathToolMessage::SelectedPointXChanged { new_x }) => { + if let Some(SingleSelectedPoint { coordinates, id, ref layer_path }) = tool_data.single_selected_point { + shape_editor.reposition_control_point(&id, responses, &document.document_legacy, DVec2::new(new_x, coordinates.y), layer_path); + } + PathToolFsmState::Ready + } + (_, PathToolMessage::SelectedPointYChanged { new_y }) => { + if let Some(SingleSelectedPoint { coordinates, id, ref layer_path }) = tool_data.single_selected_point { + shape_editor.reposition_control_point(&id, responses, &document.document_legacy, DVec2::new(coordinates.x, new_y), layer_path); + } + PathToolFsmState::Ready + } + (_, PathToolMessage::SelectedPointUpdated) => { + let new_point = get_single_selected_point(&document.document_legacy, shape_editor); + tool_data.single_selected_point = new_point; + self + } (_, _) => PathToolFsmState::Ready, } } else { @@ -450,3 +519,32 @@ impl Fsm for PathToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } + +#[derive(Debug, PartialEq)] +struct SingleSelectedPoint { + coordinates: DVec2, + id: ManipulatorPointId, + layer_path: Vec, +} + +// If there is one and only one selected control point this function yields all the information needed to manipulate it. +fn get_single_selected_point(document: &Document, shape_state: &mut ShapeState) -> Option { + let selection_layers: Vec<_> = shape_state.selected_shape_state.iter().take(2).map(|(k, v)| (k, v.selected_points_count())).collect(); + let [(layer, 1)] = selection_layers[..] else { + return None; + }; + let layer_data = document.layer(layer).ok()?; + let vector_data = layer_data.as_vector_data()?; + let [point] = shape_state.selected_points().take(2).collect::>()[..] else { + return None; + }; + + // Get the first selected point and transform it to document space. + let group = vector_data.manipulator_from_id(point.group)?; + let local_position = point.manipulator_type.get_position(group)?; + Some(SingleSelectedPoint { + coordinates: layer_data.transform.transform_point2(local_position) + layer_data.pivot, + layer_path: layer.clone(), + id: *point, + }) +}