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 <keavon@keavon.com>
This commit is contained in:
mobile-bungalow 2023-08-29 21:41:01 -07:00 committed by GitHub
parent 5944186870
commit 48fdaddc37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 1 deletions

View File

@ -27,6 +27,9 @@ impl SelectedLayerState {
pub fn clear_points(&mut self) { pub fn clear_points(&mut self) {
self.selected_points.clear(); self.selected_points.clear();
} }
pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
} }
pub type SelectedShapeState = HashMap<Vec<LayerId>, SelectedLayerState>; pub type SelectedShapeState = HashMap<Vec<LayerId>, SelectedLayerState>;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -148,6 +151,42 @@ impl ShapeState {
self.selected_shape_state.values().flat_map(|state| &state.selected_points) 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<Message>, 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. /// Move the selected points by dragging the mouse.
pub fn move_selected_points(&self, document: &Document, delta: DVec2, mirror_distance: bool, responses: &mut VecDeque<Message>) { pub fn move_selected_points(&self, document: &Document, delta: DVec2, mirror_distance: bool, responses: &mut VecDeque<Message>) {
for (layer_path, state) in &self.selected_shape_state { for (layer_path, state) in &self.selected_shape_state {

View File

@ -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::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 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::intersection::Quad;
use document_legacy::{LayerId, Operation}; use document_legacy::{LayerId, Operation};
use graphene_core::vector::{ManipulatorPointId, SelectedType}; use graphene_core::vector::{ManipulatorPointId, SelectedType};
@ -56,6 +57,13 @@ pub enum PathToolMessage {
alt_mirror_angle: Key, alt_mirror_angle: Key,
shift_mirror_distance: Key, shift_mirror_distance: Key,
}, },
SelectedPointUpdated,
SelectedPointXChanged {
new_x: f64,
},
SelectedPointYChanged {
new_y: f64,
},
} }
impl ToolMetadata for PathTool { impl ToolMetadata for PathTool {
@ -72,13 +80,51 @@ impl ToolMetadata for PathTool {
impl LayoutHolder for PathTool { impl LayoutHolder for PathTool {
fn layout(&self) -> Layout { 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<ToolMessage, &mut ToolActionHandlerData<'a>> for PathTool { impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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); 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: // Different actions depending on state may be wanted:
@ -137,6 +183,7 @@ struct PathToolData {
alt_debounce: bool, alt_debounce: bool,
opposing_handle_lengths: Option<OpposingHandleLengths>, opposing_handle_lengths: Option<OpposingHandleLengths>,
drag_box_overlay_layer: Option<Vec<LayerId>>, drag_box_overlay_layer: Option<Vec<LayerId>>,
single_selected_point: Option<SingleSelectedPoint>,
} }
impl PathToolData { impl PathToolData {
@ -182,6 +229,8 @@ impl Fsm for PathToolFsmState {
shape_editor.set_selected_layers(layer_paths); shape_editor.set_selected_layers(layer_paths);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); 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) // This can happen in any state (which is why we return self)
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); shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer_path.to_vec(), responses);
} }
responses.add(PathToolMessage::SelectedPointUpdated);
self self
} }
// Mouse down // Mouse down
@ -233,6 +284,7 @@ impl Fsm for PathToolFsmState {
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
responses.add(PathToolMessage::SelectedPointUpdated);
PathToolFsmState::Dragging PathToolFsmState::Dragging
} }
// We didn't find a point nearby, so consider selecting the nearest shape instead // 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); shape_editor.move_selected_points(&document.document_legacy, (delta_x, delta_y).into(), true, responses);
PathToolFsmState::Ready 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, (_, _) => PathToolFsmState::Ready,
} }
} else { } else {
@ -450,3 +519,32 @@ impl Fsm for PathToolFsmState {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
} }
} }
#[derive(Debug, PartialEq)]
struct SingleSelectedPoint {
coordinates: DVec2,
id: ManipulatorPointId,
layer_path: Vec<u64>,
}
// 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<SingleSelectedPoint> {
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::<Vec<_>>()[..] 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,
})
}