Allow toggling smooth/sharp angle from the path tool options bar (#1415)
* menu in option * smoothing controls work * fixed type error * fix flipping behavior * silence warning * consolidate selection state * update positions options * blinking logic fixed, smoothing logic implemented * fixed arbitrary looping when flipping from sharp to smooth * remove warning * Tidying up * refactor manipulator smoothing code, remove bitflags, rename * Make the point smooth/sharp support mixed better * Code review tweaks --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
88bdf9580f
commit
9667e5173b
|
|
@ -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<ManipulatorPointId>,
|
||||
}
|
||||
|
||||
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<Vec<LayerId>, SelectedLayerState>;
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ShapeState {
|
||||
|
|
@ -42,6 +51,7 @@ pub struct SelectedPointsInfo<'a> {
|
|||
pub points: Vec<ManipulatorPointInfo<'a>>,
|
||||
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<Item = &'a bezier_rs::ManipulatorGroup<ManipulatorGroupId>> {
|
||||
pub fn manipulator_groups<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a ManipulatorGroup<ManipulatorGroupId>> {
|
||||
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<ManipulatorGroupId>, index: usize, responses: &mut VecDeque<Message>, 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<Message>, 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<Message>) {
|
||||
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<Message>) {
|
||||
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<Item = &'a VectorData> + '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() {
|
||||
|
|
|
|||
|
|
@ -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<OpposingHandleLengths>,
|
||||
drag_box_overlay_layer: Option<Vec<LayerId>>,
|
||||
single_selected_point: Option<SingleSelectedPoint>,
|
||||
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<u64>,
|
||||
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<SingleSelectedPoint> {
|
||||
// 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::<Vec<_>>()[..] 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
{#each entries as entry, index (index)}
|
||||
<button
|
||||
class:active={index === selectedIndex}
|
||||
class:mixed={selectedIndex === undefined}
|
||||
class:disabled
|
||||
class:sharp-right-corners={index === entries.length - 1 && sharpRightCorners}
|
||||
on:click={() => handleEntryClick(entry)}
|
||||
|
|
@ -57,6 +58,10 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.mixed {
|
||||
background: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-6-lowergray);
|
||||
color: var(--color-f-white);
|
||||
|
|
|
|||
Loading…
Reference in New Issue