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:
parent
5944186870
commit
48fdaddc37
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue