diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index a454fa09..5a3db571 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -2,7 +2,7 @@ use crate::consts::DRAG_THRESHOLD; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::prelude::*; -use bezier_rs::{Bezier, TValue}; +use bezier_rs::{Bezier, ManipulatorGroup, TValue}; use document_legacy::document::Document; use document_legacy::LayerId; use graphene_core::uuid::ManipulatorGroupId; @@ -10,10 +10,18 @@ use graphene_core::vector::{ManipulatorPointId, SelectedType, VectorData}; use glam::DVec2; +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum ManipulatorAngle { + Smooth, + Sharp, + Mixed, +} + #[derive(Clone, Debug, Default)] pub struct SelectedLayerState { selected_points: HashSet, } + impl SelectedLayerState { pub fn is_selected(&self, point: ManipulatorPointId) -> bool { self.selected_points.contains(&point) @@ -31,6 +39,7 @@ impl SelectedLayerState { self.selected_points.len() } } + pub type SelectedShapeState = HashMap, SelectedLayerState>; #[derive(Debug, Default)] pub struct ShapeState { @@ -42,6 +51,7 @@ pub struct SelectedPointsInfo<'a> { pub points: Vec>, pub offset: DVec2, } + #[derive(Clone, Copy, Eq, PartialEq)] pub struct ManipulatorPointInfo<'a> { pub shape_layer_path: &'a [LayerId], @@ -146,7 +156,7 @@ impl ShapeState { } /// A mutable iterator of all the manipulators, regardless of selection. - pub fn manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator> { + pub fn manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator> { self.iter(document).flat_map(|shape| shape.manipulator_groups()) } @@ -201,6 +211,166 @@ impl ShapeState { Some(()) } + // Iterates over the selected manipulator groups, returning whether they have mixed, sharp, or smooth angles. + // If there are no points selected this function returns mixed. + pub fn selected_manipulator_angles(&self, document: &Document) -> ManipulatorAngle { + // This iterator contains a bool indicating whether or not every selected point has a smooth manipulator angle. + let mut point_smoothness_status = self + .selected_shape_state + .iter() + .filter_map(|(layer_id, selection_state)| { + let layer = document.layer(layer_id).ok()?; + let vector_data = layer.as_vector_data()?; + Some((vector_data, selection_state)) + }) + .flat_map(|(vector_data, selection_state)| selection_state.selected_points.iter().map(|selected_point| vector_data.mirror_angle.contains(&selected_point.group))); + + let Some(first_is_smooth) = point_smoothness_status.next() else { return ManipulatorAngle::Mixed }; + + if point_smoothness_status.any(|point| first_is_smooth != point) { + return ManipulatorAngle::Mixed; + } + match first_is_smooth { + false => ManipulatorAngle::Sharp, + true => ManipulatorAngle::Smooth, + } + } + + pub fn smooth_manipulator_group(&self, subpath: &bezier_rs::Subpath, index: usize, responses: &mut VecDeque, layer_path: &[u64]) { + let manipulator_groups = subpath.manipulator_groups(); + let manipulator = manipulator_groups[index]; + + // Grab the next and previous manipulator groups by simply looking at the next / previous index + let mut previous_position = index.checked_sub(1).and_then(|index| manipulator_groups.get(index)); + let mut next_position = manipulator_groups.get(index + 1); + + // Wrapping around closed path + if subpath.closed() { + previous_position = previous_position.or_else(|| manipulator_groups.last()); + next_position = next_position.or_else(|| manipulator_groups.first()); + } + + let anchor_position = manipulator.anchor; + // To find the length of the new tangent we just take the distance to the anchor and divide by 3 (pretty arbitrary) + let length_previous = previous_position.map(|group| (group.anchor - anchor_position).length() / 3.); + let length_next = next_position.map(|group| (group.anchor - anchor_position).length() / 3.); + + // Use the position relative to the anchor + let previous_angle = previous_position.map(|group| (group.anchor - anchor_position)).map(|pos| pos.y.atan2(pos.x)); + let next_angle = next_position.map(|group| (group.anchor - anchor_position)).map(|pos| pos.y.atan2(pos.x)); + + // The direction of the handles is either the perpendicular vector to the sum of the anchors' positions or just the anchor's position (if only one) + let handle_direction = match (previous_angle, next_angle) { + (Some(previous), Some(next)) => (previous + next) / 2. + core::f64::consts::FRAC_PI_2, + (None, Some(val)) => core::f64::consts::PI + val, + (Some(val), None) => val, + (None, None) => return, + }; + + // Mirror the angle but not the distance + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorHandleMirroring { + id: manipulator.id, + mirror_angle: true, + }, + }); + + let (sin, cos) = handle_direction.sin_cos(); + let mut handle_vector = DVec2::new(cos, sin); + + // Flip the vector if it is not facing towards the same direction as the anchor + if previous_position.filter(|&group| (group.anchor - anchor_position).normalize().dot(handle_vector) < 0.).is_some() + || next_position.filter(|&group| (group.anchor - anchor_position).normalize().dot(handle_vector) > 0.).is_some() + { + handle_vector = -handle_vector; + } + + // Push both in and out handles into the correct position + if let Some(in_handle) = length_previous.map(|length| anchor_position + handle_vector * length) { + let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle); + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point, position: in_handle }, + }); + } + + if let Some(out_handle) = length_next.map(|length| anchor_position - handle_vector * length) { + let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point, position: out_handle }, + }); + } + } + + /// Smooths the set of selected control points, assuming that the selected set is homogeneously sharp. + pub fn smooth_selected_groups(&self, responses: &mut VecDeque, document: &Document) -> Option<()> { + let mut skip_set = HashSet::new(); + + for (layer_id, layer_state) in self.selected_shape_state.iter() { + let layer = document.layer(layer_id).ok()?; + let vector_data = layer.as_vector_data()?; + + for point in layer_state.selected_points.iter() { + if skip_set.contains(&point.group) { + continue; + }; + + skip_set.insert(point.group); + + let anchor_selected = layer_state.selected_points.contains(&ManipulatorPointId { + group: point.group, + manipulator_type: SelectedType::Anchor, + }); + let out_selected = layer_state.selected_points.contains(&ManipulatorPointId { + group: point.group, + manipulator_type: SelectedType::OutHandle, + }); + let in_selected = layer_state.selected_points.contains(&ManipulatorPointId { + group: point.group, + manipulator_type: SelectedType::InHandle, + }); + let group = vector_data.manipulator_from_id(point.group)?; + + match (anchor_selected, out_selected, in_selected) { + (_, true, false) => { + let out_handle = ManipulatorPointId::new(point.group, SelectedType::OutHandle); + if let Some(position) = group.out_handle { + responses.add(GraphOperationMessage::Vector { + layer: layer_id.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point: out_handle, position }, + }); + } + } + (_, false, true) => { + let in_handle = ManipulatorPointId::new(point.group, SelectedType::InHandle); + if let Some(position) = group.in_handle { + responses.add(GraphOperationMessage::Vector { + layer: layer_id.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point: in_handle, position }, + }); + } + } + (_, _, _) => { + let found = vector_data.subpaths.iter().find_map(|subpath| { + let group_slice = subpath.manipulator_groups(); + let index = group_slice.iter().position(|manipulator| manipulator.id == group.id)?; + // TODO: try subpath closed? wrapping + Some((subpath, index)) + }); + + if let Some((subpath, index)) = found { + self.smooth_manipulator_group(subpath, index, responses, layer_id); + } + } + } + } + } + + 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 { @@ -444,6 +614,18 @@ impl ShapeState { } } + /// Toggle if the handles should mirror angle across the anchor position. + pub fn set_handle_mirroring_on_selected(&self, mirror_angle: bool, responses: &mut VecDeque) { + for (layer, state) in &self.selected_shape_state { + for point in &state.selected_points { + responses.add(GraphOperationMessage::Vector { + layer: layer.to_vec(), + modification: VectorDataModification::SetManipulatorHandleMirroring { id: point.group, mirror_angle }, + }); + } + } + } + /// Iterate over the shapes. pub fn iter<'a>(&'a self, document: &'a Document) -> impl Iterator + 'a { self.selected_shape_state @@ -544,7 +726,7 @@ impl ShapeState { responses.add(out_handle); // Insert a new manipulator group between the existing ones - let manipulator_group = bezier_rs::ManipulatorGroup::new(first.end(), first.handle_end(), second.handle_start()); + let manipulator_group = ManipulatorGroup::new(first.end(), first.handle_end(), second.handle_start()); let insert = GraphOperationMessage::Vector { layer: layer_path.clone(), modification: VectorDataModification::AddManipulatorGroup { manipulator_group, after_id: start }, @@ -598,78 +780,28 @@ impl ShapeState { (None, None) => true, }; - let manipulator_groups = subpath.manipulator_groups(); - let (in_handle, out_handle) = if already_sharp { - let is_closed = subpath.closed(); - - // Grab the next and previous manipulator groups by simply looking at the next / previous index - let mut previous_position = index.checked_sub(1).and_then(|index| manipulator_groups.get(index)).map(|group| group.anchor); - let mut next_position = manipulator_groups.get(index + 1).map(|group| group.anchor); - - // Wrapping around closed path - if is_closed { - previous_position = previous_position.or_else(|| manipulator_groups.last().map(|group| group.anchor)); - next_position = next_position.or_else(|| manipulator_groups.first().map(|group| group.anchor)); - } - - // To find the length of the new tangent we just take the distance to the anchor and divide by 3 (pretty arbitrary) - let length_previous = previous_position.map(|point| (point - anchor_position).length() / 3.); - let length_next = next_position.map(|point| (point - anchor_position).length() / 3.); - - // Use the position relative to the anchor - let previous_angle = previous_position.map(|point| (point - anchor_position)).map(|pos| pos.y.atan2(pos.x)); - let next_angle = next_position.map(|point| (point - anchor_position)).map(|pos| pos.y.atan2(pos.x)); - - // The direction of the handles is either the perpendicular vector to the sum of the anchors' positions or just the anchor's position (if only one) - let handle_direction = match (previous_angle, next_angle) { - (Some(previous), Some(next)) => (previous + next) / 2. + core::f64::consts::FRAC_PI_2, - (None, Some(val)) => core::f64::consts::PI + val, - (Some(val), None) => val, - (None, None) => return None, - }; - - // Mirror the angle but not the distance + if already_sharp { + self.smooth_manipulator_group(subpath, index, responses, layer_path); + } else { + let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle); + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position }, + }); + let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); + responses.add(GraphOperationMessage::Vector { + layer: layer_path.to_vec(), + modification: VectorDataModification::SetManipulatorPosition { point, position: anchor_position }, + }); responses.add(GraphOperationMessage::Vector { layer: layer_path.to_vec(), modification: VectorDataModification::SetManipulatorHandleMirroring { id: manipulator.id, - mirror_angle: true, + mirror_angle: false, }, }); - - let (sin, cos) = handle_direction.sin_cos(); - let mut handle_vector = DVec2::new(cos, sin); - - // Flip the vector if it is not facing towards the same direction as the anchor - if previous_position.filter(|&pos| (pos - anchor_position).normalize().dot(handle_vector) < 0.).is_some() - || next_position.filter(|&pos| (pos - anchor_position).normalize().dot(handle_vector) > 0.).is_some() - { - handle_vector = -handle_vector; - } - - ( - length_previous.map(|length| anchor_position + handle_vector * length), - length_next.map(|length| anchor_position - handle_vector * length), - ) - } else { - (Some(anchor_position), Some(anchor_position)) }; - // Push both in and out handles into the correct position - if let Some(in_handle) = in_handle { - let point = ManipulatorPointId::new(manipulator.id, SelectedType::InHandle); - responses.add(GraphOperationMessage::Vector { - layer: layer_path.to_vec(), - modification: VectorDataModification::SetManipulatorPosition { point, position: in_handle }, - }); - } - if let Some(out_handle) = out_handle { - let point = ManipulatorPointId::new(manipulator.id, SelectedType::OutHandle); - responses.add(GraphOperationMessage::Vector { - layer: layer_path.to_vec(), - modification: VectorDataModification::SetManipulatorPosition { point, position: out_handle }, - }); - } Some(true) }; for layer_path in self.selected_shape_state.keys() { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 55e669a0..8ba48bd3 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -6,7 +6,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMot use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer; -use crate::messages::tool::common_functionality::shape_editor::{ManipulatorPointInfo, OpposingHandleLengths, ShapeState}; +use crate::messages::tool::common_functionality::shape_editor::{ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, ShapeState}; 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}; @@ -49,6 +49,8 @@ pub enum PathToolMessage { add_to_selection: Key, }, InsertPoint, + ManipulatorAngleMakeSharp, + ManipulatorAngleMakeSmooth, NudgeSelectedPoints { delta_x: f64, delta_y: f64, @@ -81,9 +83,15 @@ impl ToolMetadata for PathTool { impl LayoutHolder for PathTool { fn layout(&self) -> Layout { - let coordinates = self.tool_data.single_selected_point.as_ref().map(|point| point.coordinates); + let coordinates = self.tool_data.selection_status.as_one().as_ref().map(|point| point.coordinates); let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None)); + let selection_status = &self.tool_data.selection_status; + let manipulator_angle = selection_status + .as_multiple() + .map(|multiple| multiple.manipulator_angle) + .or_else(|| selection_status.as_one().map(|point| point.manipulator_angle)); + let x_location = NumberInput::new(x) .unit(" px") .label("X") @@ -110,10 +118,26 @@ impl LayoutHolder for PathTool { }) .widget_holder(); - let seperator = Separator::new(SeparatorType::Related).widget_holder(); + let related_seperator = Separator::new(SeparatorType::Related).widget_holder(); + let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); + + let manipulator_angle_options = vec![ + RadioEntryData::new("Smooth").on_update(|_| PathToolMessage::ManipulatorAngleMakeSmooth.into()), + RadioEntryData::new("Sharp").on_update(|_| PathToolMessage::ManipulatorAngleMakeSharp.into()), + ]; + let manipulator_angle_index = manipulator_angle.and_then(|angle| match angle { + ManipulatorAngle::Smooth => Some(0), + ManipulatorAngle::Sharp => Some(1), + ManipulatorAngle::Mixed => None, + }); + + let manipulator_angle_radio = RadioInput::new(manipulator_angle_options) + .disabled(self.tool_data.selection_status.is_none()) + .selected_index(manipulator_angle_index) + .widget_holder(); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { - widgets: vec![x_location, seperator, y_location], + widgets: vec![x_location, related_seperator, y_location, unrelated_seperator, manipulator_angle_radio], }])) } } @@ -188,7 +212,7 @@ struct PathToolData { alt_debounce: bool, opposing_handle_lengths: Option, drag_box_overlay_layer: Option>, - single_selected_point: Option, + selection_status: SelectionStatus, } impl PathToolData { @@ -392,6 +416,8 @@ impl Fsm for PathToolFsmState { } .into(), )); + + responses.add(PathToolMessage::SelectedPointUpdated); PathToolFsmState::Ready } @@ -412,6 +438,8 @@ impl Fsm for PathToolFsmState { } .into(), )); + + responses.add(PathToolMessage::SelectedPointUpdated); PathToolFsmState::Ready } (_, PathToolMessage::DragStop { shift_mirror_distance }) => { @@ -432,6 +460,7 @@ impl Fsm for PathToolFsmState { } } + responses.add(PathToolMessage::SelectedPointUpdated); tool_data.snap_manager.cleanup(responses); PathToolFsmState::Ready } @@ -454,6 +483,7 @@ impl Fsm for PathToolFsmState { shape_editor.split(&document.document_legacy, input.mouse.position, SELECTION_TOLERANCE, responses); } + responses.add(PathToolMessage::SelectedPointUpdated); self } (_, PathToolMessage::Abort) => { @@ -480,22 +510,34 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::SelectedPointXChanged { new_x }) => { - if let Some(SingleSelectedPoint { coordinates, id, ref layer_path }) = tool_data.single_selected_point { + if let Some(&SingleSelectedPoint { coordinates, id, ref layer_path, .. }) = tool_data.selection_status.as_one() { 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 { + if let Some(&SingleSelectedPoint { coordinates, id, ref layer_path, .. }) = tool_data.selection_status.as_one() { 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; + tool_data.selection_status = get_selection_status(&document.document_legacy, shape_editor); self } + (_, PathToolMessage::ManipulatorAngleMakeSmooth) => { + responses.add(DocumentMessage::StartTransaction); + shape_editor.set_handle_mirroring_on_selected(true, responses); + shape_editor.smooth_selected_groups(responses, &document.document_legacy); + responses.add(DocumentMessage::CommitTransaction); + PathToolFsmState::Ready + } + (_, PathToolMessage::ManipulatorAngleMakeSharp) => { + responses.add(DocumentMessage::StartTransaction); + shape_editor.set_handle_mirroring_on_selected(false, responses); + responses.add(DocumentMessage::CommitTransaction); + PathToolFsmState::Ready + } (_, _) => PathToolFsmState::Ready, } } else { @@ -531,31 +573,85 @@ impl Fsm for PathToolFsmState { } } +#[derive(Debug, PartialEq, Default)] +enum SelectionStatus { + #[default] + None, + One(SingleSelectedPoint), + Multiple(MultipleSelectedPoints), +} + +impl SelectionStatus { + fn as_one(&self) -> Option<&SingleSelectedPoint> { + match self { + SelectionStatus::One(one) => Some(one), + _ => None, + } + } + + fn as_multiple(&self) -> Option<&MultipleSelectedPoints> { + match self { + SelectionStatus::Multiple(multiple) => Some(multiple), + _ => None, + } + } + + fn is_none(&self) -> bool { + self == &SelectionStatus::None + } +} + +#[derive(Debug, PartialEq)] +struct MultipleSelectedPoints { + manipulator_angle: ManipulatorAngle, +} + #[derive(Debug, PartialEq)] struct SingleSelectedPoint { coordinates: DVec2, id: ManipulatorPointId, layer_path: Vec, + manipulator_angle: ManipulatorAngle, } -// 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 { +// If there is one selected and only one manipulator group this yields the selected control point, +// if only one handle is selected it will yield that handle, otherwise it will yield the group's anchor. +fn get_selection_status(document: &Document, shape_state: &mut ShapeState) -> SelectionStatus { + // Check to see if only one manipulator group is selected 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; + if let [(layer, 1)] = selection_layers[..] { + let Some(layer_data) = document.layer(layer).ok() else { return SelectionStatus::None }; + let Some(vector_data) = layer_data.as_vector_data() else { return SelectionStatus::None }; + let Some(point) = shape_state.selected_points().next() else { + return SelectionStatus::None; + }; + + let Some(group) = vector_data.manipulator_from_id(point.group) else { + return SelectionStatus::None; + }; + let Some(local_position) = point.manipulator_type.get_position(group) else { + return SelectionStatus::None; + }; + + let manipulator_angle = if vector_data.mirror_angle.contains(&point.group) { + ManipulatorAngle::Smooth + } else { + ManipulatorAngle::Sharp + }; + + return SelectionStatus::One(SingleSelectedPoint { + coordinates: layer_data.transform.transform_point2(local_position) + layer_data.pivot, + layer_path: layer.clone(), + id: *point, + manipulator_angle, + }); }; - // 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, - }) + if !selection_layers.is_empty() { + return SelectionStatus::Multiple(MultipleSelectedPoints { + manipulator_angle: shape_state.selected_manipulator_angles(document), + }); + } + + SelectionStatus::None } diff --git a/frontend/src/components/widgets/inputs/RadioInput.svelte b/frontend/src/components/widgets/inputs/RadioInput.svelte index 2bc35e73..c1646ef4 100644 --- a/frontend/src/components/widgets/inputs/RadioInput.svelte +++ b/frontend/src/components/widgets/inputs/RadioInput.svelte @@ -27,6 +27,7 @@ {#each entries as entry, index (index)}